mirror of
https://github.com/status-im/nimbus-eth1.git
synced 2025-01-30 05:55:30 +00:00
f034af422a
Each branch node may have up to 16 sub-items - currently, these are given VertexID based when they are first needed leading to a mostly-random order of vertexid for each subitem. Here, we pre-allocate all 16 vertex ids such that when a branch subitem is filled, it already has a vertexid waiting for it. This brings several important benefits: * subitems are sorted and "close" in their id sequencing - this means that when rocksdb stores them, they are likely to end up in the same data block thus improving read efficiency * because the ids are consequtive, we can store just the starting id and a bitmap representing which subitems are in use - this reduces disk space usage for branches allowing more of them fit into a single disk read, further improving disk read and caching performance - disk usage at block 18M is down from 84 to 78gb! * the in-memory footprint of VertexRef reduced allowing more instances to fit into caches and less memory to be used overall. Because of the increased locality of reference, it turns out that we no longer need to iterate over the entire database to efficiently generate the hash key database because the normal computation is now faster - this significantly benefits "live" chain processing as well where each dirtied key must be accompanied by a read of all branch subitems next to it - most of the performance benefit in this branch comes from this locality-of-reference improvement. On a sample resync, there's already ~20% improvement with later blocks seeing increasing benefit (because the trie is deeper in later blocks leading to more benefit from branch read perf improvements) ``` blocks: 18729664, baseline: 190h43m49s, contender: 153h59m0s Time (total): -36h44m48s, -19.27% ``` Note: clients need to be resynced as the PR changes the on-disk format R.I.P. little bloom filter - your life in the repo was short but valuable
432 lines
15 KiB
Nim
432 lines
15 KiB
Nim
# nimbus-eth1
|
|
# Copyright (c) 2023-2024 Status Research & Development GmbH
|
|
# Licensed under either of
|
|
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
|
|
# http://www.apache.org/licenses/LICENSE-2.0)
|
|
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or
|
|
# http://opensource.org/licenses/MIT)
|
|
# at your option. This file may not be copied, modified, or distributed
|
|
# except according to those terms.
|
|
|
|
## Aristo DB -- Identifier types
|
|
## =============================
|
|
##
|
|
|
|
{.push raises: [].}
|
|
|
|
import
|
|
std/[algorithm, sequtils, sets, strutils, hashes],
|
|
eth/common,
|
|
stew/byteutils,
|
|
chronicles,
|
|
results,
|
|
stint,
|
|
./desc_nibbles
|
|
|
|
export
|
|
desc_nibbles
|
|
|
|
type
|
|
VertexID* = distinct uint64
|
|
## Unique identifier for a vertex of the `Aristo Trie`. The vertex is the
|
|
## prefix tree (aka `Patricia Trie`) component. When augmented by hash
|
|
## keys, the vertex component will be called a node. On the persistent
|
|
## backend of the database, there is no other reference to the node than
|
|
## the very same `VertexID`.
|
|
##
|
|
## Vertex IDs are generated on the fly and thrown away when not needed,
|
|
## anymore. They are not recycled. A quick estimate
|
|
##
|
|
## (2^64) / (100 * 365.25 * 24 * 3600) / 1000 / 1000 / 1000 = 5.86
|
|
##
|
|
## shows that the `uint64` scalar space is not exhausted in a 100 years
|
|
## if the database consumes somewhat less than 6 IDs per nanosecond.
|
|
##
|
|
## A simple recycling mechanism was tested which slowed down the system
|
|
## considerably because large swaths of database vertices were regularly
|
|
## freed so recycling had do deal with extensive lists of non-consecutive
|
|
## IDs.
|
|
|
|
RootedVertexID* = tuple[root, vid: VertexID]
|
|
## Vertex and the root it belongs to in the MPT. Used to group a set of
|
|
## verticies, for example to store them together in the database or perform
|
|
## range operations.
|
|
##
|
|
## `vid` may be a branch, extension or leaf.
|
|
##
|
|
## To reference the root itself, use (root, root).
|
|
|
|
HashKey* = object
|
|
## Ethereum reference MPTs use Keccak hashes as node links if the size of
|
|
## an RLP encoded node is at least 32 bytes. Otherwise, the RLP encoded
|
|
## node value is used as a pseudo node link (rather than a hash.) This is
|
|
## specified in the yellow paper, appendix D. Only for the root hash, the
|
|
## top level node is always referred to by the Keccak hash.
|
|
##
|
|
## On the `Aristo` database node links are called keys which are of this
|
|
## very type `HashKey`. For key-value tables (which assign a key to a
|
|
## vertex), the keys are always stored as such with length probably
|
|
## smaller than 32, including for root vertex keys. Only when used as a
|
|
## root state, the key of the latter is digested to a Keccak hash
|
|
## on-the-fly.
|
|
##
|
|
## This compaction feature nees an abstraction of the hash link object
|
|
## which is either a `Hash32` or a `seq[byte]` of length at most 31 bytes.
|
|
## This leaves two ways of representing an empty/void `HashKey` type.
|
|
## It may be available as an empty `seq[byte]` of zero length, or the
|
|
## `Hash32` type of the Keccak hash of an empty `seq[byte]` (see constant
|
|
## `EMPTY_ROOT_HASH`.)
|
|
##
|
|
## For performance, storing blobs as `seq` is avoided, instead storing
|
|
## their length and sharing the data "space".
|
|
##
|
|
buf: array[32, byte] # Either Hash32 or blob data, depending on `len`
|
|
len: int8 # length in the case of blobs, or 32 when it's a hash
|
|
|
|
PathID* = object
|
|
## Path into the `Patricia Trie`. This is a chain of maximal 64 nibbles
|
|
## (which is 32 bytes.) In most cases, the length is 64. So the path is
|
|
## encoded as a numeric value which is often easier to handle than a
|
|
## chain of nibbles.
|
|
##
|
|
## The path ID should be kept normalised, i.e.
|
|
## * 0 <= `length` <= 64
|
|
## * the unused trailing nibbles in `pfx` are set to `0`
|
|
##
|
|
pfx*: UInt256
|
|
length*: uint8
|
|
|
|
# ----------
|
|
|
|
LeafTie* = object
|
|
## Unique access key for a leaf vertex. It identifies a root vertex
|
|
## followed by a nibble path along the `Patricia Trie` down to a leaf
|
|
## vertex. So this implies an obvious injection from the set of `LeafTie`
|
|
## objects *into* the set of `VertexID` obvious (which is typically *into*
|
|
## only, not a bijection.)
|
|
##
|
|
## Note that `LeafTie` objects have no representation in the `Aristo Trie`.
|
|
## They are used temporarily and in caches or backlog tables.
|
|
root*: VertexID ## Root ID for the sub-trie
|
|
path*: PathID ## Path into the `Patricia Trie`
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Chronicles formatters
|
|
# ------------------------------------------------------------------------------
|
|
|
|
chronicles.formatIt(VertexID): $it
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Public helpers: `VertexID` scalar data model
|
|
# ------------------------------------------------------------------------------
|
|
|
|
func `<`*(a, b: VertexID): bool {.borrow.}
|
|
func `<=`*(a, b: VertexID): bool {.borrow.}
|
|
func `==`*(a, b: VertexID): bool {.borrow.}
|
|
func cmp*(a, b: VertexID): int {.borrow.}
|
|
|
|
func `$`*(vid: VertexID): string =
|
|
"$" & (if vid == VertexID(0): "ø"
|
|
else: vid.uint64.toHex.strip(trailing=false,chars={'0'}).toLowerAscii)
|
|
|
|
func `$`*(rvid: RootedVertexID): string =
|
|
$rvid.root & "/" & $rvid.vid
|
|
|
|
func `==`*(a: VertexID; b: static[uint]): bool = (a == VertexID(b))
|
|
|
|
# Scalar model extension as in `IntervalSetRef[VertexID,uint64]`
|
|
func `+`*(a: VertexID; b: uint64): VertexID = (a.uint64+b).VertexID
|
|
func `-`*(a: VertexID; b: uint64): VertexID = (a.uint64-b).VertexID
|
|
func `-`*(a, b: VertexID): uint64 = (a.uint64 - b.uint64)
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Public helpers: `PathID` ordered scalar data model
|
|
# ------------------------------------------------------------------------------
|
|
|
|
func high*(_: type PathID): PathID =
|
|
## Highest possible `PathID` object for given root vertex.
|
|
PathID(pfx: high(UInt256), length: 64)
|
|
|
|
func low*(_: type PathID): PathID =
|
|
## Lowest possible `PathID` object for given root vertex.
|
|
PathID()
|
|
|
|
func next*(pid: PathID): PathID =
|
|
## Return a `PathID` object with incremented path field. This function might
|
|
## return also a modified `length` field.
|
|
##
|
|
## The function returns the argument `pid` if it is already at its
|
|
## maximum value `high(PathID)`.
|
|
if pid.pfx.isZero and pid.length < 64:
|
|
PathID(length: pid.length + 1)
|
|
elif pid.pfx < high(UInt256):
|
|
PathID(pfx: pid.pfx + 1, length: 64)
|
|
else:
|
|
pid
|
|
|
|
func prev*(pid: PathID): PathID =
|
|
## Return a `PathID` object with decremented path field. This function might
|
|
## return also a modified `length` field.
|
|
##
|
|
## The function returns the argument `pid` if it is already at its
|
|
## minimum value `low(PathID)`.
|
|
if 0 < pid.pfx:
|
|
PathID(pfx: pid.pfx - 1, length: 64)
|
|
elif 0 < pid.length:
|
|
PathID(length: pid.length - 1)
|
|
else:
|
|
pid
|
|
|
|
func `<`*(a, b: PathID): bool =
|
|
## This function assumes that the arguments `a` and `b` are normalised
|
|
## (see `normal()`.)
|
|
a.pfx < b.pfx or (a.pfx == b.pfx and a.length < b.length)
|
|
|
|
func `<=`*(a, b: PathID): bool =
|
|
not (b < a)
|
|
|
|
func `==`*(a, b: PathID): bool =
|
|
## This function assumes that the arguments `a` and `b` are normalised
|
|
## (see `normal()`.)
|
|
a.pfx == b.pfx and a.length == b.length
|
|
|
|
func cmp*(a, b: PathID): int =
|
|
if a < b: -1 elif b < a: 1 else: 0
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Public helpers: `HashKey` ordered scalar data model
|
|
# ------------------------------------------------------------------------------
|
|
|
|
func len*(lid: HashKey): int =
|
|
lid.len.int # if lid.isHash: 32 else: lid.blob.len
|
|
|
|
template data*(lid: HashKey): openArray[byte] =
|
|
lid.buf.toOpenArray(0, lid.len - 1)
|
|
|
|
func to*(lid: HashKey; T: type PathID): T =
|
|
## Helper to bowrrow certain properties from `PathID`
|
|
if lid.len == 32:
|
|
PathID(pfx: UInt256.fromBytesBE lid.data, length: 64)
|
|
elif 0 < lid.len:
|
|
doAssert lid.len < 32
|
|
var a32: array[32,byte]
|
|
(addr a32[0]).copyMem(unsafeAddr lid.data[0], lid.len)
|
|
PathID(pfx: UInt256.fromBytesBE a32, length: 2 * lid.len.uint8)
|
|
else:
|
|
PathID()
|
|
|
|
func fromBytes*(T: type HashKey; data: openArray[byte]): Result[T,void] =
|
|
## Write argument `data` of length 0 or between 2 and 32 bytes as a `HashKey`.
|
|
##
|
|
## A function argument `data` of length 32 is used as-is.
|
|
##
|
|
## For a function argument `data` of length between 2 and 31, the first
|
|
## byte must be the start of an RLP encoded list, i.e. `0xc0 + len` where
|
|
## where `len` is one less as the `data` length.
|
|
##
|
|
if data.len == 32:
|
|
var lid: T
|
|
lid.len = 32
|
|
(addr lid.data[0]).copyMem(unsafeAddr data[0], data.len)
|
|
return ok lid
|
|
if data.len == 0:
|
|
return ok HashKey()
|
|
if 1 < data.len and data.len < 32 and data[0].int == 0xbf + data.len:
|
|
var lid: T
|
|
lid.len = int8 data.len
|
|
(addr lid.data[0]).copyMem(unsafeAddr data[0], data.len)
|
|
return ok lid
|
|
err()
|
|
|
|
func `<`*(a, b: HashKey): bool =
|
|
## Slow, but useful for debug sorting
|
|
a.to(PathID) < b.to(PathID)
|
|
|
|
func `==`*(a, b: HashKey): bool =
|
|
a.data == b.data
|
|
|
|
func cmp*(a, b: HashKey): int =
|
|
## Slow, but useful for debug sorting
|
|
cmp(a.data, b.data)
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Public helpers: `LeafTie` ordered scalar data model
|
|
# ------------------------------------------------------------------------------
|
|
|
|
func high*(_: type LeafTie; root = VertexID(1)): LeafTie =
|
|
## Highest possible `LeafTie` object for given root vertex.
|
|
LeafTie(root: root, path: high(PathID))
|
|
|
|
func low*(_: type LeafTie; root = VertexID(1)): LeafTie =
|
|
## Lowest possible `LeafTie` object for given root vertex.
|
|
LeafTie(root: root, path: low(PathID))
|
|
|
|
func next*(lty: LeafTie): LeafTie =
|
|
## Return a `LeafTie` object with the `next()` path field.
|
|
LeafTie(root: lty.root, path: lty.path.next)
|
|
|
|
func prev*(lty: LeafTie): LeafTie =
|
|
## Return a `LeafTie` object with the `prev()` path field.
|
|
LeafTie(root: lty.root, path: lty.path.prev)
|
|
|
|
func `<`*(a, b: LeafTie): bool =
|
|
## This function assumes that the arguments `a` and `b` are normalised
|
|
## (see `normal()`.)
|
|
a.root < b.root or (a.root == b.root and a.path < b.path)
|
|
|
|
func `==`*(a, b: LeafTie): bool =
|
|
## This function assumes that the arguments `a` and `b` are normalised
|
|
## (see `normal()`.)
|
|
a.root == b.root and a.path == b.path
|
|
|
|
func cmp*(a, b: LeafTie): int =
|
|
## This function assumes that the arguments `a` and `b` are normalised
|
|
## (see `normal()`.)
|
|
if a < b: -1 elif a == b: 0 else: 1
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Public helpers: Reversible conversions between `PathID`, `HashKey`, etc.
|
|
# ------------------------------------------------------------------------------
|
|
|
|
func to*(pid: PathID; T: type NibblesBuf): T =
|
|
## Representation of a `PathID` as `NibbleSeq` (preserving full information)
|
|
let nibbles = NibblesBuf.fromBytes(pid.pfx.toBytesBE)
|
|
if pid.length < 64:
|
|
nibbles.slice(0, pid.length.int)
|
|
else:
|
|
nibbles
|
|
|
|
func `@`*(pid: PathID): seq[byte] =
|
|
## Representation of a `PathID` as a `seq[byte]`. The result is left padded
|
|
## by a zero LSB if the path length was odd.
|
|
result = pid.pfx.toBytesBE.toSeq
|
|
if pid.length < 63:
|
|
result.setLen((pid.length + 1) shl 1)
|
|
|
|
func to*(lid: HashKey; T: type Hash32): T =
|
|
## Returns the `Hash236` key if available, otherwise the Keccak hash of
|
|
## the `seq[byte]` version.
|
|
if lid.len == 32:
|
|
Hash32(lid.buf)
|
|
elif 0 < lid.len:
|
|
lid.data.keccak256
|
|
else:
|
|
EMPTY_ROOT_HASH
|
|
|
|
func to*(key: Hash32; T: type HashKey): T =
|
|
## This is an efficient version of `HashKey.fromBytes(key.data).value`, not
|
|
## to be confused with `digestTo(HashKey)`.
|
|
if key == EMPTY_ROOT_HASH:
|
|
T()
|
|
else:
|
|
T(len: 32, buf: key.data)
|
|
|
|
func to*(n: SomeUnsignedInt; T: type PathID): T =
|
|
## Representation of a scalar as `PathID` (preserving full information)
|
|
T(pfx: n.u256, length: 64)
|
|
|
|
func to*(n: UInt256; T: type PathID): T =
|
|
## Representation of a scalar as `PathID` (preserving full information)
|
|
T(pfx: n, length: 64)
|
|
|
|
func to*(a: PathID; T: type UInt256): T =
|
|
if not a.pfx.isZero:
|
|
assert a.length < 64 # debugging only
|
|
result = a.pfx shr (4 * (64 - a.length))
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Public helpers: Miscellaneous mappings
|
|
# ------------------------------------------------------------------------------
|
|
|
|
func digestTo*(data: openArray[byte]; T: type HashKey): T =
|
|
## For argument `data` with length smaller than 32, import them as-is into
|
|
## the result. Otherwise import the Keccak hash of the argument `data`.
|
|
##
|
|
## The `data` argument is only hashed if the `data` length is at least
|
|
## 32 bytes. Otherwise it is converted as-is to a `HashKey` type result.
|
|
##
|
|
## Note that for calculating a root state (when `data` is a serialised
|
|
## vertex), one would use the expression `data.digestTo(HashKey).to(Hash32)`
|
|
## which would always hash the `data` argument regardless of its length
|
|
## (and might result in an `EMPTY_ROOT_HASH`.) See the comment at the
|
|
## definition of the `HashKey` type for an explanation of its usage.
|
|
##
|
|
if data.len == 0:
|
|
result.len = 0
|
|
elif data.len < 32:
|
|
result.len = int8 data.len
|
|
(addr result.data[0]).copyMem(unsafeAddr data[0], data.len)
|
|
else:
|
|
result.len = 32
|
|
result.buf = data.keccak256.data
|
|
|
|
func normal*(a: PathID): PathID =
|
|
## Normalise path ID representation
|
|
result = a
|
|
if 64 < a.length:
|
|
result.length = 64
|
|
elif a.length < 64:
|
|
result.pfx = a.pfx and not (1.u256 shl (4 * (64 - a.length))) - 1.u256
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Public helpers: `Tables` and `Rlp` support
|
|
# ------------------------------------------------------------------------------
|
|
|
|
func hash*(a: PathID): Hash =
|
|
## Table/KeyedQueue mixin
|
|
var h: Hash = 0
|
|
h = h !& a.pfx.toBytesBE.hash
|
|
h = h !& a.length.hash
|
|
!$h
|
|
|
|
func hash*(a: HashKey): Hash =
|
|
## Table/KeyedQueue mixin
|
|
hash(a.data)
|
|
|
|
func append*(w: var RlpWriter; key: HashKey) =
|
|
if 1 < key.len and key.len < 32:
|
|
w.appendRawBytes key.data
|
|
else:
|
|
w.append key.data
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Miscellaneous helpers
|
|
# ------------------------------------------------------------------------------
|
|
|
|
func `$`*(vids: seq[VertexID]): string =
|
|
"[" & vids.toSeq.mapIt(
|
|
"$" & it.uint64.toHex.strip(trailing=false,chars={'0'})
|
|
).join(",") & "]"
|
|
|
|
func `$`*(vids: HashSet[VertexID]): string =
|
|
"{" & vids.toSeq.sorted.mapIt(
|
|
"$" & it.uint64.toHex.strip(trailing=false,chars={'0'})
|
|
).join(",") & "}"
|
|
|
|
func `$`*(key: HashKey): string =
|
|
toHex(key.data)
|
|
|
|
func `$`*(a: PathID): string =
|
|
if a.pfx.isZero.not:
|
|
var dgts = $a.pfx.toHex
|
|
if a.length < 64:
|
|
dgts = dgts[0 ..< a.length]
|
|
result = dgts.strip(
|
|
leading=true, trailing=false, chars={'0'})
|
|
elif a.length != 0:
|
|
result = "0"
|
|
if a.length < 64:
|
|
result &= "(" & $a.length & ")"
|
|
|
|
func `$`*(a: LeafTie): string =
|
|
if a.root != 0:
|
|
result = ($a.root.uint64.toHex).strip(
|
|
leading=true, trailing=false, chars={'0'})
|
|
else:
|
|
result = "0"
|
|
result &= ":" & $a.path
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# End
|
|
# ------------------------------------------------------------------------------
|