Introduce frame snapshots (#3098)

With the introduction of layered frames, each database lookup may result
in hundreds of table lookups as the frame stack is traversed.

This change restores performance by introducing snapshots to limit the
lookup depth at the expense of slightly increased memory usage.

The snapshot contains the cumulative changes of all ancestors and itself
allowing the lookup recursion to stop whenever it is encountered.

The number of snapshots to keep in memory is a tradeoff between lookup
performance and memory usage - this change starts with a simple strategy
of keeping snapshots for head frames (approximately). T

he snapshot is created during checkpointing, ie after block validation,
to make sure that it's cheap to start verifying blocks - parent
snapshots are moved to the descendant as part of checkpointing which
effectively means that head frames hold snapshots in most cases.

The outcome of this tradeoff is that applying a block to a known head is
fast while creating a new branch of history remains expensive.

Another consequence is that when persisting changes to disk, we must
re-traverse the stack of changes to build a cumulative set of changes to
be persisted.

A future strategy might be to keep additional "keyframes" along the way,
ie one per epoch for example - this would bound the "branch creation"
cost to a constant factor, but memory overhead should first be
considered.

Another strategy might be to avoid keeping snapshots for non-canonical
branches, specially when they become older and thus less likely to be
branched from.

* `level` is updated to work like a temporary serial number to maintain
its relative position in the sorting order as frames are persisted
* a `snapshot` is added to some TxFrame instances - the snapshot
collects all ancestor changes up to and including the given frame.
`level` is used as a marker to prune the snapshot of changes that have
been persisted already.
* stack traversals for the purpose of lookup stop when they encounter a
snapshot - this bounds the lookup depth to the first encountered
snapshot

After this PR, sync performance lands at about 2-3 blocks per second
(~10x improvement) - this is quite reasonable when comparing with block
import which skips the expensive state root verification and thus
achieves ~20 blk/s on the same hardware. Additional work to bring live
syncing performance in line with disk-based block import would focus on
reducing state root verification cost.
This commit is contained in:
Jacek Sieka 2025-02-28 14:33:42 +01:00 committed by GitHub
parent 0f89c1f901
commit 4576727817
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 691 additions and 640 deletions

View File

@ -147,8 +147,12 @@ proc initializeDb(com: CommonRef) =
txFrame.persistHeaderAndSetHead(com.genesisHeader, txFrame.persistHeaderAndSetHead(com.genesisHeader,
startOfHistory=com.genesisHeader.parentHash). startOfHistory=com.genesisHeader.parentHash).
expect("can persist genesis header") expect("can persist genesis header")
doAssert(canonicalHeadHashKey().toOpenArray in txFrame) doAssert(canonicalHeadHashKey().toOpenArray in txFrame)
txFrame.checkpoint(com.genesisHeader.number)
com.db.persist(txFrame)
# The database must at least contain the base and head pointers - the base # The database must at least contain the base and head pointers - the base
# is implicitly considered finalized # is implicitly considered finalized
let let

View File

@ -143,8 +143,12 @@ proc validateBlock(c: ForkedChainRef,
c.writeBaggage(blk, blkHash, txFrame, receipts) c.writeBaggage(blk, blkHash, txFrame, receipts)
# Block fully written to txFrame, mark it as such # Block fully written to txFrame, mark it as such
# Checkpoint creates a snapshot of ancestor changes in txFrame - it is an
# expensive operation, specially when creating a new branch (ie when blk
# is being applied to a block that is currently not a head)
txFrame.checkpoint(blk.header.number) txFrame.checkpoint(blk.header.number)
c.updateBranch(parent, blk, blkHash, txFrame, move(receipts)) c.updateBranch(parent, blk, blkHash, txFrame, move(receipts))
for i, tx in blk.transactions: for i, tx in blk.transactions:

View File

@ -102,7 +102,9 @@ proc checkpoint*(p: var Persister): Result[void, string] =
) )
# Move in-memory state to disk # Move in-memory state to disk
p.vmState.ledger.txFrame.checkpoint(p.parent.number, skipSnapshot = true)
p.com.db.persist(p.vmState.ledger.txFrame) p.com.db.persist(p.vmState.ledger.txFrame)
# Get a new frame since the DB assumes ownership # Get a new frame since the DB assumes ownership
p.vmState.ledger.txFrame = p.com.db.baseTxFrame().txFrameBegin() p.vmState.ledger.txFrame = p.com.db.baseTxFrame().txFrameBegin()
@ -172,8 +174,6 @@ proc persistBlock*(p: var Persister, blk: Block): Result[void, string] =
p.stats.txs += blk.transactions.len p.stats.txs += blk.transactions.len
p.stats.gas += blk.header.gasUsed p.stats.gas += blk.header.gasUsed
txFrame.checkpoint(header.number)
assign(p.parent, header) assign(p.parent, header)
ok() ok()

View File

@ -15,7 +15,7 @@ import
chronicles, chronicles,
eth/common/[accounts_rlp, base_rlp, hashes_rlp], eth/common/[accounts_rlp, base_rlp, hashes_rlp],
results, results,
"."/[aristo_desc, aristo_get, aristo_walk/persistent], "."/[aristo_desc, aristo_get, aristo_tx_frame, aristo_walk/persistent],
./aristo_desc/desc_backend ./aristo_desc/desc_backend
type WriteBatch = tuple[writer: PutHdlRef, count: int, depth: int, prefix: uint64] type WriteBatch = tuple[writer: PutHdlRef, count: int, depth: int, prefix: uint64]
@ -74,7 +74,7 @@ proc putKeyAtLevel(
## set (vertex data may have been committed to disk without computing the ## set (vertex data may have been committed to disk without computing the
## corresponding hash!) ## corresponding hash!)
if level == -2: if level < db.db.baseTxFrame().level:
?batch.putVtx(db.db, rvid, vtx, key) ?batch.putVtx(db.db, rvid, vtx, key)
if batch.count mod batchSize == 0: if batch.count mod batchSize == 0:
@ -90,16 +90,6 @@ proc putKeyAtLevel(
ok() ok()
func maxLevel(cur, other: int): int =
# Compare two levels and return the topmost in the stack, taking into account
# the odd reversal of order around the zero point
if cur < 0:
max(cur, other) # >= 0 is always more topmost than <0
elif other < 0:
cur
else:
min(cur, other) # Here the order is reversed and 0 is the top layer
template encodeLeaf(w: var RlpWriter, pfx: NibblesBuf, leafData: untyped): HashKey = template encodeLeaf(w: var RlpWriter, pfx: NibblesBuf, leafData: untyped): HashKey =
w.startList(2) w.startList(2)
w.append(pfx.toHexPrefix(isLeaf = true).data()) w.append(pfx.toHexPrefix(isLeaf = true).data())
@ -123,7 +113,7 @@ proc getKey(
db: AristoTxRef, rvid: RootedVertexID, skipLayers: static bool db: AristoTxRef, rvid: RootedVertexID, skipLayers: static bool
): Result[((HashKey, VertexRef), int), AristoError] = ): Result[((HashKey, VertexRef), int), AristoError] =
ok when skipLayers: ok when skipLayers:
(?db.db.getKeyBe(rvid, {GetVtxFlag.PeekCache}), -2) (?db.db.getKeyBe(rvid, {GetVtxFlag.PeekCache}), dbLevel)
else: else:
?db.getKeyRc(rvid, {}) ?db.getKeyRc(rvid, {})
@ -178,7 +168,7 @@ proc computeKeyImpl(
keyvtxl[1], keyvtxl[1],
skipLayers = skipLayers, skipLayers = skipLayers,
) )
level = maxLevel(level, sl) level = max(level, sl)
skey skey
else: else:
VOID_HASH_KEY VOID_HASH_KEY
@ -252,7 +242,7 @@ proc computeKeyImpl(
template writeBranch(w: var RlpWriter): HashKey = template writeBranch(w: var RlpWriter): HashKey =
w.encodeBranch(vtx): w.encodeBranch(vtx):
if subvid.isValid: if subvid.isValid:
level = maxLevel(level, keyvtxs[n][1]) level = max(level, keyvtxs[n][1])
keyvtxs[n][0][0] keyvtxs[n][0][0]
else: else:
VOID_HASH_KEY VOID_HASH_KEY
@ -280,7 +270,7 @@ proc computeKeyImpl(
): Result[HashKey, AristoError] = ): Result[HashKey, AristoError] =
let (keyvtx, level) = let (keyvtx, level) =
when skipLayers: when skipLayers:
(?db.db.getKeyBe(rvid, {GetVtxFlag.PeekCache}), -2) (?db.db.getKeyBe(rvid, {GetVtxFlag.PeekCache}), dbLevel)
else: else:
?db.getKeyRc(rvid, {}) ?db.getKeyRc(rvid, {})

View File

@ -22,7 +22,7 @@
{.push raises: [].} {.push raises: [].}
import import
std/[hashes, sets, tables], std/[hashes, sequtils, sets, tables],
eth/common/hashes, eth/common/hashes,
results, results,
./aristo_constants, ./aristo_constants,
@ -73,6 +73,21 @@ type
blockNumber*: Opt[uint64] ## Block number set when checkpointing the frame blockNumber*: Opt[uint64] ## Block number set when checkpointing the frame
snapshot*: Table[RootedVertexID, Snapshot]
## Optional snapshot containing the cumulative changes from ancestors and
## the current frame
snapshotLevel*: Opt[int] # base level when the snapshot was taken
level*: int
## Ancestry level of frame, increases with age but otherwise meaningless -
## used to order data by age when working with layers.
## -1 = stored in database, where relevant though typically should be
## compared with the base layer level instead.
Snapshot* = (VertexRef, HashKey, int)
## Unlike sTab/kMap, snapshot contains both vertex and key since at the time
## of writing, it's primarily used in contexts where both are present
AristoDbRef* = ref object AristoDbRef* = ref object
## Backend interface. ## Backend interface.
getVtxFn*: GetVtxFn ## Read vertex record getVtxFn*: GetVtxFn ## Read vertex record
@ -116,6 +131,8 @@ type
legs*: ArrayBuf[NibblesBuf.high + 1, Leg] ## Chain of vertices and IDs legs*: ArrayBuf[NibblesBuf.high + 1, Leg] ## Chain of vertices and IDs
tail*: NibblesBuf ## Portion of non completed path tail*: NibblesBuf ## Portion of non completed path
const dbLevel* = -1
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Public helpers # Public helpers
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -181,52 +198,40 @@ func isValid*(sqv: HashSet[RootedVertexID]): bool =
# Public functions, miscellaneous # Public functions, miscellaneous
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Hash set helper func hash*(db: AristoDbRef): Hash {.error.}
func hash*(db: AristoDbRef): Hash = func hash*(db: AristoTxRef): Hash {.error.}
## Table/KeyedQueue/HashSet mixin
cast[pointer](db).hash
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Public helpers # Public helpers
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
iterator stack*(tx: AristoTxRef): AristoTxRef = iterator rstack*(tx: AristoTxRef, stopAtSnapshot = false): AristoTxRef =
# Stack going from base to tx # Stack in reverse order, ie going from tx to base
var frames: seq[AristoTxRef]
var tx = tx var tx = tx
while tx != nil: while tx != nil:
frames.add tx yield tx
if stopAtSnapshot and tx.snapshotLevel.isSome():
break
tx = tx.parent tx = tx.parent
iterator stack*(tx: AristoTxRef, stopAtSnapshot = false): AristoTxRef =
# Stack going from base to tx
var frames = toSeq(tx.rstack(stopAtSnapshot))
while frames.len > 0: while frames.len > 0:
yield frames.pop() yield frames.pop()
iterator rstack*(tx: AristoTxRef): (AristoTxRef, int) =
# Stack in reverse order, ie going from tx to base
var tx = tx
var i = 0
while tx != nil:
let level = if tx.parent == nil: -1 else: i
yield (tx, level)
tx = tx.parent
i += 1
proc deltaAtLevel*(db: AristoTxRef, level: int): AristoTxRef = proc deltaAtLevel*(db: AristoTxRef, level: int): AristoTxRef =
if level == -2: if level < db.db.txRef.level:
nil nil
elif level == -1:
db.db.txRef
else: else:
var for frame in db.rstack():
frame = db if frame.level == level:
level = level return frame
nil
while level > 0:
frame = frame.parent
level -= 1
frame
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# End # End

View File

@ -69,7 +69,7 @@ proc getVtxRc*(
else: else:
return err(GetVtxNotFound) return err(GetVtxNotFound)
ok (?db.db.getVtxBe(rvid, flags), -2) ok (?db.db.getVtxBe(rvid, flags), dbLevel)
proc getVtx*(db: AristoTxRef; rvid: RootedVertexID, flags: set[GetVtxFlag] = {}): VertexRef = proc getVtx*(db: AristoTxRef; rvid: RootedVertexID, flags: set[GetVtxFlag] = {}): VertexRef =
## Cascaded attempt to fetch a vertex from the cache layers or the backend. ## Cascaded attempt to fetch a vertex from the cache layers or the backend.
@ -103,7 +103,7 @@ proc getKeyRc*(
# The vertex is to be deleted. So is the value key. # The vertex is to be deleted. So is the value key.
return err(GetKeyNotFound) return err(GetKeyNotFound)
ok (?db.db.getKeyBe(rvid, flags), -2) ok (?db.db.getKeyBe(rvid, flags), dbLevel)
proc getKey*(db: AristoTxRef; rvid: RootedVertexID): HashKey = proc getKey*(db: AristoTxRef; rvid: RootedVertexID): HashKey =
## Cascaded attempt to fetch a vertex from the cache layers or the backend. ## Cascaded attempt to fetch a vertex from the cache layers or the backend.

View File

@ -83,7 +83,7 @@ proc finishSession*(hdl: TypedPutHdlRef; db: TypedBackendRef) =
proc initInstance*(db: AristoDbRef): Result[void, AristoError] = proc initInstance*(db: AristoDbRef): Result[void, AristoError] =
let vTop = ?db.getTuvFn() let vTop = ?db.getTuvFn()
db.txRef = AristoTxRef(db: db, vTop: vTop) db.txRef = AristoTxRef(db: db, vTop: vTop, snapshotLevel: Opt.some(0))
db.accLeaves = LruCache[Hash32, VertexRef].init(ACC_LRU_SIZE) db.accLeaves = LruCache[Hash32, VertexRef].init(ACC_LRU_SIZE)
db.stoLeaves = LruCache[Hash32, VertexRef].init(ACC_LRU_SIZE) db.stoLeaves = LruCache[Hash32, VertexRef].init(ACC_LRU_SIZE)
ok() ok()

View File

@ -25,9 +25,14 @@ func layersGetVtx*(db: AristoTxRef; rvid: RootedVertexID): Opt[(VertexRef, int)]
## Find a vertex on the cache layers. An `ok()` result might contain a ## Find a vertex on the cache layers. An `ok()` result might contain a
## `nil` vertex if it is stored on the cache that way. ## `nil` vertex if it is stored on the cache that way.
## ##
for w, level in db.rstack: for w in db.rstack(stopAtSnapshot = true):
if w.snapshotLevel.isSome():
w.snapshot.withValue(rvid, item):
return Opt.some((item[][0], item[][2]))
break
w.sTab.withValue(rvid, item): w.sTab.withValue(rvid, item):
return Opt.some((item[], level)) return Opt.some((item[], w.level))
Opt.none((VertexRef, int)) Opt.none((VertexRef, int))
@ -36,11 +41,17 @@ func layersGetKey*(db: AristoTxRef; rvid: RootedVertexID): Opt[(HashKey, int)] =
## hash key if it is stored on the cache that way. ## hash key if it is stored on the cache that way.
## ##
for w, level in db.rstack: for w in db.rstack(stopAtSnapshot = true):
if w.snapshotLevel.isSome():
w.snapshot.withValue(rvid, item):
return Opt.some((item[][1], item[][2]))
break
w.kMap.withValue(rvid, item): w.kMap.withValue(rvid, item):
return ok((item[], level)) return ok((item[], w.level))
if rvid in w.sTab: if rvid in w.sTab:
return Opt.some((VOID_HASH_KEY, level)) return Opt.some((VOID_HASH_KEY, w.level))
Opt.none((HashKey, int)) Opt.none((HashKey, int))
@ -49,14 +60,14 @@ func layersGetKeyOrVoid*(db: AristoTxRef; rvid: RootedVertexID): HashKey =
(db.layersGetKey(rvid).valueOr (VOID_HASH_KEY, 0))[0] (db.layersGetKey(rvid).valueOr (VOID_HASH_KEY, 0))[0]
func layersGetAccLeaf*(db: AristoTxRef; accPath: Hash32): Opt[VertexRef] = func layersGetAccLeaf*(db: AristoTxRef; accPath: Hash32): Opt[VertexRef] =
for w, _ in db.rstack: for w in db.rstack:
w.accLeaves.withValue(accPath, item): w.accLeaves.withValue(accPath, item):
return Opt.some(item[]) return Opt.some(item[])
Opt.none(VertexRef) Opt.none(VertexRef)
func layersGetStoLeaf*(db: AristoTxRef; mixPath: Hash32): Opt[VertexRef] = func layersGetStoLeaf*(db: AristoTxRef; mixPath: Hash32): Opt[VertexRef] =
for w, _ in db.rstack: for w in db.rstack:
w.stoLeaves.withValue(mixPath, item): w.stoLeaves.withValue(mixPath, item):
return Opt.some(item[]) return Opt.some(item[])
@ -75,6 +86,9 @@ func layersPutVtx*(
db.sTab[rvid] = vtx db.sTab[rvid] = vtx
db.kMap.del(rvid) db.kMap.del(rvid)
if db.snapshotLevel.isSome():
db.snapshot[rvid] = (vtx, VOID_HASH_KEY, db.level)
func layersResVtx*( func layersResVtx*(
db: AristoTxRef; db: AristoTxRef;
rvid: RootedVertexID; rvid: RootedVertexID;
@ -93,6 +107,9 @@ func layersPutKey*(
db.sTab[rvid] = vtx db.sTab[rvid] = vtx
db.kMap[rvid] = key db.kMap[rvid] = key
if db.snapshotLevel.isSome():
db.snapshot[rvid] = (vtx, key, db.level)
func layersResKey*(db: AristoTxRef; rvid: RootedVertexID, vtx: VertexRef) = func layersResKey*(db: AristoTxRef; rvid: RootedVertexID, vtx: VertexRef) =
## Shortcut for `db.layersPutKey(vid, VOID_HASH_KEY)`. It is sort of the ## Shortcut for `db.layersPutKey(vid, VOID_HASH_KEY)`. It is sort of the
## equivalent of a delete function. ## equivalent of a delete function.
@ -116,16 +133,35 @@ func layersPutStoLeaf*(db: AristoTxRef; mixPath: Hash32; leafVtx: VertexRef) =
func isEmpty*(ly: AristoTxRef): bool = func isEmpty*(ly: AristoTxRef): bool =
## Returns `true` if the layer does not contain any changes, i.e. all the ## Returns `true` if the layer does not contain any changes, i.e. all the
## tables are empty. ## tables are empty.
ly.snapshot.len == 0 and
ly.sTab.len == 0 and ly.sTab.len == 0 and
ly.kMap.len == 0 and ly.kMap.len == 0 and
ly.accLeaves.len == 0 and ly.accLeaves.len == 0 and
ly.stoLeaves.len == 0 ly.stoLeaves.len == 0
proc copyFrom*(snapshot: var Table[RootedVertexID, Snapshot], tx: AristoTxRef) =
for rvid, vtx in tx.sTab:
tx.kMap.withValue(rvid, key):
snapshot[rvid] = (vtx, key[], tx.level)
do:
snapshot[rvid] = (vtx, VOID_HASH_KEY, tx.level)
proc mergeAndReset*(trg, src: AristoTxRef) = proc mergeAndReset*(trg, src: AristoTxRef) =
## Merges the argument `src` into the argument `trg` and clears `src`. ## Merges the argument `src` into the argument `trg` and clears `src`.
trg.vTop = src.vTop trg.vTop = src.vTop
trg.blockNumber = src.blockNumber trg.blockNumber = src.blockNumber
trg.level = src.level
trg.parent = move(src.parent)
doAssert not src.snapshotLevel.isSome(),
"If the source is a snapshot, it should have been used as a starting point for merge"
if trg.snapshotLevel.isSome():
# If there already was a snapshot, we might as well add to it
trg.snapshot.copyFrom(src)
src.sTab.reset()
src.kMap.reset()
else:
if trg.kMap.len > 0: if trg.kMap.len > 0:
# Invalidate cached keys in the lower layer # Invalidate cached keys in the lower layer
for vid in src.sTab.keys: for vid in src.sTab.keys:
@ -133,37 +169,10 @@ proc mergeAndReset*(trg, src: AristoTxRef) =
mergeAndReset(trg.sTab, src.sTab) mergeAndReset(trg.sTab, src.sTab)
mergeAndReset(trg.kMap, src.kMap) mergeAndReset(trg.kMap, src.kMap)
mergeAndReset(trg.accLeaves, src.accLeaves) mergeAndReset(trg.accLeaves, src.accLeaves)
mergeAndReset(trg.stoLeaves, src.stoLeaves) mergeAndReset(trg.stoLeaves, src.stoLeaves)
# func layersCc*(db: AristoDbRef; level = high(int)): LayerRef =
# ## Provide a collapsed copy of layers up to a particular transaction level.
# ## If the `level` argument is too large, the maximum transaction level is
# ## returned.
# ##
# let layers = if db.stack.len <= level: db.stack & @[db.top]
# else: db.stack[0 .. level]
# # Set up initial layer (bottom layer)
# result = LayerRef(
# sTab: layers[0].sTab.dup, # explicit dup for ref values
# kMap: layers[0].kMap,
# vTop: layers[^1].vTop,
# accLeaves: layers[0].accLeaves,
# stoLeaves: layers[0].stoLeaves)
# # Consecutively merge other layers on top
# for n in 1 ..< layers.len:
# for (vid,vtx) in layers[n].sTab.pairs:
# result.sTab[vid] = vtx
# result.kMap.del vid
# for (vid,key) in layers[n].kMap.pairs:
# result.kMap[vid] = key
# for (accPath,vtx) in layers[n].accLeaves.pairs:
# result.accLeaves[accPath] = vtx
# for (mixPath,vtx) in layers[n].stoLeaves.pairs:
# result.stoLeaves[mixPath] = vtx
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Public iterators # Public iterators
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -179,11 +188,10 @@ iterator layersWalkVtx*(
## the one with a zero vertex which are othewise skipped by the iterator. ## the one with a zero vertex which are othewise skipped by the iterator.
## The `seen` argument must not be modified while the iterator is active. ## The `seen` argument must not be modified while the iterator is active.
## ##
for w, _ in db.rstack: for w in db.rstack:
for (rvid,vtx) in w.sTab.pairs: for (rvid,vtx) in w.sTab.pairs:
if rvid.vid notin seen: if not seen.containsOrIncl(rvid.vid):
yield (rvid,vtx) yield (rvid,vtx)
seen.incl rvid.vid
iterator layersWalkVtx*( iterator layersWalkVtx*(
db: AristoTxRef; db: AristoTxRef;
@ -200,11 +208,10 @@ iterator layersWalkKey*(
## Walk over all `(VertexID,HashKey)` pairs on the cache layers. Note that ## Walk over all `(VertexID,HashKey)` pairs on the cache layers. Note that
## entries are unsorted. ## entries are unsorted.
var seen: HashSet[VertexID] var seen: HashSet[VertexID]
for w, _ in db.rstack: for w in db.rstack:
for (rvid,key) in w.kMap.pairs: for (rvid,key) in w.kMap.pairs:
if rvid.vid notin seen: if not seen.containsOrIncl(rvid.vid):
yield (rvid,key) yield (rvid,key)
seen.incl rvid.vid
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# End # End

View File

@ -13,24 +13,64 @@
## ##
{.push raises: [].} {.push raises: [].}
import import results, ./[aristo_desc, aristo_layers]
results,
./[aristo_desc, aristo_layers]
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Public functions # Public functions
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
proc txFrameBegin*(db: AristoDbRef, parent: AristoTxRef): AristoTxRef = proc buildSnapshot(txFrame: AristoTxRef, minLevel: int) =
let parent = if parent == nil: # Starting from the previous snapshot, build a snapshot that includes all
db.txRef # ancestor changes as well as the changes in txFrame itself
else: for frame in txFrame.stack(stopAtSnapshot = true):
parent if frame != txFrame:
# Keyframes keep their snapshot insted of it being transferred to the new
# frame - right now, only the base frame is a keyframe but this support
# could be extended for example to epoch boundary frames which are likely
# to become new bases.
let isKeyframe = frame == frame.db.txRef
AristoTxRef( if frame.snapshotLevel.isSome() and not isKeyframe:
db: db, # `frame` has a snapshot only in the first iteration of the for loop
parent: parent, txFrame.snapshot = move(frame.snapshot)
vTop: parent.vTop) txFrame.snapshotLevel = frame.snapshotLevel
assert frame.snapshot.len == 0 # https://github.com/nim-lang/Nim/issues/23759
frame.snapshotLevel.reset() # in case there was a snapshot in txFrame already
if txFrame.snapshotLevel != Opt.some(minLevel):
# When recycling an existing snapshot, some of its content may have
# already been persisted to disk (since it was made base on the
# in-memory frames at the time of its creation).
# Annoyingly, there's no way to remove items while iterating but even
# with the extra seq, move + remove turns out to be faster than
# creating a new table - specially when the ratio between old and
# and current items favors current items.
var toRemove = newSeqOfCap[RootedVertexID](txFrame.snapshot.len div 2)
for rvid, v in txFrame.snapshot:
if v[2] < minLevel:
toRemove.add rvid
for rvid in toRemove:
txFrame.snapshot.del(rvid)
if frame.snapshotLevel.isSome() and isKeyframe:
txFrame.snapshot = initTable[RootedVertexID, Snapshot](
max(1024, max(frame.sTab.len, frame.snapshot.len))
)
for k, v in frame.snapshot:
if v[2] >= minLevel:
txFrame.snapshot[k] = v
# Copy changes into snapshot but keep the diff - the next builder might
# steal the snapshot!
txFrame.snapshot.copyFrom(frame)
txFrame.snapshotLevel = Opt.some(minLevel)
proc txFrameBegin*(db: AristoDbRef, parent: AristoTxRef): AristoTxRef =
let parent = if parent == nil: db.txRef else: parent
AristoTxRef(db: db, parent: parent, vTop: parent.vTop, level: parent.level + 1)
proc baseTxFrame*(db: AristoDbRef): AristoTxRef = proc baseTxFrame*(db: AristoDbRef): AristoTxRef =
db.txRef db.txRef
@ -38,43 +78,83 @@ proc baseTxFrame*(db: AristoDbRef): AristoTxRef=
proc dispose*(tx: AristoTxRef) = proc dispose*(tx: AristoTxRef) =
tx[].reset() tx[].reset()
proc checkpoint*(tx: AristoTxRef; blockNumber: uint64) = proc checkpoint*(tx: AristoTxRef, blockNumber: uint64, skipSnapshot: bool) =
tx.blockNumber = Opt.some(blockNumber) tx.blockNumber = Opt.some(blockNumber)
proc persist*( if not skipSnapshot:
db: AristoDbRef; # Database # Snapshots are expensive, therefore we only do it at checkpoints (which
batch: PutHdlRef; # presumably have gone through enough validation)
txFrame: AristoTxRef; tx.buildSnapshot(tx.db.txRef.level)
) =
if txFrame == db.txRef and txFrame.sTab.len == 0: proc persist*(db: AristoDbRef, batch: PutHdlRef, txFrame: AristoTxRef) =
if txFrame == db.txRef and txFrame.isEmpty():
# No changes in frame - no `checkpoint` requirement - nothing to do here # No changes in frame - no `checkpoint` requirement - nothing to do here
return return
let lSst = SavedState( let lSst = SavedState(
key: emptyRoot, # placeholder for more key: emptyRoot, # placeholder for more
serial: txFrame.blockNumber.expect("`checkpoint` before persisting frame")) serial: txFrame.blockNumber.expect("`checkpoint` before persisting frame"),
)
# Squash all changes up to the base let oldLevel = db.txRef.level
if txFrame != db.txRef: if txFrame != db.txRef:
# Consolidate the changes from the old to the new base going from the # Consolidate the changes from the old to the new base going from the
# bottom of the stack to avoid having to cascade each change through # bottom of the stack to avoid having to cascade each change through
# the full stack # the full stack
assert txFrame.parent != nil assert txFrame.parent != nil
for frame in txFrame.stack():
if frame == db.txRef: var bottom: AristoTxRef
for frame in txFrame.stack(stopAtSnapshot = true):
if bottom == nil:
# db.txRef always is a snapshot, therefore we're guaranteed to end up
# here
bottom = frame
# If there is no snapshot, consolidate changes into sTab/kMap instead
# which caters to the scenario where changes from multiple blocks
# have already been written to sTab and the changes can moved into
# the bottom.
if bottom.snapshot.len == 0:
bottom.snapshotLevel.reset()
else:
# Incoming snapshots already have sTab baked in - make sure we don't
# overwrite merged data from more recent layers with this old version
bottom.sTab.reset()
bottom.kMap.reset()
continue continue
mergeAndReset(db.txRef, frame)
doAssert not bottom.isNil, "should have found db.txRef at least"
mergeAndReset(bottom, frame)
frame.dispose() # This will also dispose `txFrame` itself! frame.dispose() # This will also dispose `txFrame` itself!
# Put the now-merged contents in txFrame and make it the new base # Put the now-merged contents in txFrame and make it the new base
swap(db.txRef[], txFrame[]) swap(bottom[], txFrame[])
db.txRef = txFrame db.txRef = txFrame
if txFrame.parent != nil:
# Can't use rstack here because dispose will break the parent chain
for frame in txFrame.parent.stack():
frame.dispose()
txFrame.parent = nil
else:
if txFrame.snapshotLevel.isSome():
# Clear out redundant copy so we don't write it twice, below
txFrame.sTab.reset()
txFrame.kMap.reset()
# Store structural single trie entries # Store structural single trie entries
assert txFrame.snapshot.len == 0 or txFrame.sTab.len == 0,
"Either snapshot or sTab should have been cleared as part of merging"
for rvid, item in txFrame.snapshot:
if item[2] >= oldLevel:
db.putVtxFn(batch, rvid, item[0], item[1])
for rvid, vtx in txFrame.sTab: for rvid, vtx in txFrame.sTab:
txFrame.kMap.withValue(rvid, key) do: txFrame.kMap.withValue(rvid, key):
db.putVtxFn(batch, rvid, vtx, key[]) db.putVtxFn(batch, rvid, vtx, key[])
do: do:
db.putVtxFn(batch, rvid, vtx, default(HashKey)) db.putVtxFn(batch, rvid, vtx, default(HashKey))
@ -94,6 +174,10 @@ proc persist*(
for mixPath, vtx in txFrame.stoLeaves: for mixPath, vtx in txFrame.stoLeaves:
db.stoLeaves.put(mixPath, vtx) db.stoLeaves.put(mixPath, vtx)
txFrame.snapshot.clear()
# Since txFrame is now the base, it contains all changes and therefore acts
# as a snapshot
txFrame.snapshotLevel = Opt.some(txFrame.level)
txFrame.sTab.clear() txFrame.sTab.clear()
txFrame.kMap.clear() txFrame.kMap.clear()
txFrame.accLeaves.clear() txFrame.accLeaves.clear()

View File

@ -448,8 +448,8 @@ proc txFrameBegin*(parent: CoreDbTxRef): CoreDbTxRef =
CoreDbTxRef(kTx: kTx, aTx: aTx) CoreDbTxRef(kTx: kTx, aTx: aTx)
proc checkpoint*(tx: CoreDbTxRef, blockNumber: BlockNumber) = proc checkpoint*(tx: CoreDbTxRef, blockNumber: BlockNumber, skipSnapshot = false) =
tx.aTx.checkpoint(blockNumber) tx.aTx.checkpoint(blockNumber, skipSnapshot)
proc dispose*(tx: CoreDbTxRef) = proc dispose*(tx: CoreDbTxRef) =
tx.aTx.dispose() tx.aTx.dispose()

View File

@ -1,5 +1,5 @@
# Nimbus # Nimbus
# Copyright (c) 2023-2024 Status Research & Development GmbH # Copyright (c) 2023-2025 Status Research & Development GmbH
# Licensed and distributed under either of # Licensed and distributed under either of
# * MIT license (license terms in the root directory or at # * MIT license (license terms in the root directory or at
# https://opensource.org/licenses/MIT). # https://opensource.org/licenses/MIT).

View File

@ -1,5 +1,5 @@
# Nimbus # Nimbus
# Copyright (c) 2023-2024 Status Research & Development GmbH # Copyright (c) 2023-2025 Status Research & Development GmbH
# Licensed and distributed under either of # Licensed and distributed under either of
# * MIT license (license terms in the root directory or at # * MIT license (license terms in the root directory or at
# https://opensource.org/licenses/MIT). # https://opensource.org/licenses/MIT).

View File

@ -10,7 +10,6 @@
import import
./[ ./[
test_aristo, test_aristo,
test_blockchain_json,
test_configuration, test_configuration,
test_coredb, test_coredb,
test_difficulty, test_difficulty,
@ -19,7 +18,6 @@ import
test_filters, test_filters,
test_forked_chain, test_forked_chain,
test_forkid, test_forkid,
test_generalstate_json,
test_genesis, test_genesis,
test_getproof_json, test_getproof_json,
test_jwt_auth, test_jwt_auth,
@ -36,4 +34,8 @@ import
test_tracer_json, test_tracer_json,
test_transaction_json, test_transaction_json,
test_txpool, test_txpool,
# These two suites are much slower than all the rest, so run them last
test_blockchain_json,
test_generalstate_json,
] ]

View File

@ -122,7 +122,7 @@ suite "Aristo compute":
for (k, v, r) in samples[^1]: for (k, v, r) in samples[^1]:
check: check:
txFrame.mergeAccountRecord(k, v) == Result[bool, AristoError].ok(true) txFrame.mergeAccountRecord(k, v) == Result[bool, AristoError].ok(true)
txFrame.checkpoint(1) txFrame.checkpoint(1, skipSnapshot = true)
let batch = db.putBegFn()[] let batch = db.putBegFn()[]
db.persist(batch, txFrame) db.persist(batch, txFrame)

View File

@ -23,7 +23,7 @@ import
aristo_init/init_common, aristo_init/init_common,
aristo_init/memory_only, aristo_init/memory_only,
aristo_layers, aristo_layers,
aristo_merge aristo_merge,
] ]
proc makeAccount(i: uint64): (Hash32, AristoAccount) = proc makeAccount(i: uint64): (Hash32, AristoAccount) =
@ -38,8 +38,7 @@ const
suite "Aristo TxFrame": suite "Aristo TxFrame":
setup: setup:
let let db = AristoDbRef.init()
db = AristoDbRef.init()
test "Frames should independently keep data": test "Frames should independently keep data":
let let
@ -77,9 +76,24 @@ suite "Aristo TxFrame":
# The vid for acc1 gets created in tx1 because it has to move to a new # The vid for acc1 gets created in tx1 because it has to move to a new
# mpt node from the root - tx2c updates only data, so the level at which # mpt node from the root - tx2c updates only data, so the level at which
# we find the vtx should be one below tx2c! # we find the vtx should be one below tx2c!
tx2c.layersGetVtx((VertexID(1), acc1Hike.legs[^1].wp.vid)).value()[1] == 1 (
tx2c.level -
tx2c.layersGetVtx((VertexID(1), acc1Hike.legs[^1].wp.vid)).value()[1]
) == 1
tx0.checkpoint(1, skipSnapshot = false)
tx1.checkpoint(2, skipSnapshot = false)
tx2.checkpoint(3, skipSnapshot = false)
tx2b.checkpoint(3, skipSnapshot = false)
tx2c.checkpoint(3, skipSnapshot = false)
check:
# Even after checkpointing, we should maintain the same relative levels
(
tx2c.level -
tx2c.layersGetVtx((VertexID(1), acc1Hike.legs[^1].wp.vid)).value()[1]
) == 1
tx2.checkpoint(1)
let batch = db.putBegFn().expect("working batch") let batch = db.putBegFn().expect("working batch")
db.persist(batch, tx2) db.persist(batch, tx2)
check: check:

View File

@ -11,16 +11,19 @@
import import
pkg/chronicles, pkg/chronicles,
pkg/unittest2, pkg/unittest2,
std/[os, strutils],
../execution_chain/common, ../execution_chain/common,
../execution_chain/config, ../execution_chain/config,
../execution_chain/utils/utils, ../execution_chain/utils/utils,
../execution_chain/core/chain/forked_chain, ../execution_chain/core/chain/forked_chain,
../execution_chain/db/ledger, ../execution_chain/db/ledger,
../execution_chain/db/era1_db,
./test_forked_chain/chain_debug ./test_forked_chain/chain_debug
const const
genesisFile = "tests/customgenesis/cancun123.json" genesisFile = "tests/customgenesis/cancun123.json"
senderAddr = address"73cf19657412508833f618a15e8251306b3e6ee5" senderAddr = address"73cf19657412508833f618a15e8251306b3e6ee5"
sourcePath = currentSourcePath.rsplit({DirSep, AltSep}, 1)[0]
type type
TestEnv = object TestEnv = object
@ -154,90 +157,69 @@ proc forkedChainMain*() =
genesisHash = cc.genesisHeader.blockHash genesisHash = cc.genesisHeader.blockHash
genesis = Block.init(cc.genesisHeader, BlockBody()) genesis = Block.init(cc.genesisHeader, BlockBody())
baseTxFrame = cc.db.baseTxFrame() baseTxFrame = cc.db.baseTxFrame()
let let
blk1 = baseTxFrame.makeBlk(1, genesis) blk1 = baseTxFrame.makeBlk(1, genesis)
blk2 = baseTxFrame.makeBlk(2, blk1) blk2 = baseTxFrame.makeBlk(2, blk1)
blk3 = baseTxFrame.makeBlk(3, blk2) blk3 = baseTxFrame.makeBlk(3, blk2)
dbTx = baseTxFrame.txFrameBegin dbTx = baseTxFrame.txFrameBegin
blk4 = dbTx.makeBlk(4, blk3) blk4 = dbTx.makeBlk(4, blk3)
blk5 = dbTx.makeBlk(5, blk4) blk5 = dbTx.makeBlk(5, blk4)
blk6 = dbTx.makeBlk(6, blk5) blk6 = dbTx.makeBlk(6, blk5)
blk7 = dbTx.makeBlk(7, blk6) blk7 = dbTx.makeBlk(7, blk6)
dbTx.dispose() dbTx.dispose()
let let
B4 = baseTxFrame.makeBlk(4, blk3, 1.byte) B4 = baseTxFrame.makeBlk(4, blk3, 1.byte)
dbTx2 = baseTxFrame.txFrameBegin dbTx2 = baseTxFrame.txFrameBegin
B5 = dbTx2.makeBlk(5, B4) B5 = dbTx2.makeBlk(5, B4)
B6 = dbTx2.makeBlk(6, B5) B6 = dbTx2.makeBlk(6, B5)
B7 = dbTx2.makeBlk(7, B6) B7 = dbTx2.makeBlk(7, B6)
dbTx2.dispose() dbTx2.dispose()
let let
C5 = baseTxFrame.makeBlk(5, blk4, 1.byte) C5 = baseTxFrame.makeBlk(5, blk4, 1.byte)
C6 = baseTxFrame.makeBlk(6, C5) C6 = baseTxFrame.makeBlk(6, C5)
C7 = baseTxFrame.makeBlk(7, C6) C7 = baseTxFrame.makeBlk(7, C6)
test "newBase == oldBase": test "newBase == oldBase":
const info = "newBase == oldBase" const info = "newBase == oldBase"
let com = env.newCom() let com = env.newCom()
var chain = ForkedChainRef.init(com) var chain = ForkedChainRef.init(com)
# same header twice # same header twice
checkImportBlock(chain, blk1) checkImportBlock(chain, blk1)
checkImportBlock(chain, blk1) checkImportBlock(chain, blk1)
checkImportBlock(chain, blk2) checkImportBlock(chain, blk2)
checkImportBlock(chain, blk3) checkImportBlock(chain, blk3)
check chain.validate info & " (1)" check chain.validate info & " (1)"
# no parent # no parent
checkImportBlockErr(chain, blk5) checkImportBlockErr(chain, blk5)
check chain.headHash == genesisHash check chain.headHash == genesisHash
check chain.latestHash == blk3.blockHash check chain.latestHash == blk3.blockHash
check chain.validate info & " (2)" check chain.validate info & " (2)"
# finalized > head -> error # finalized > head -> error
checkForkChoiceErr(chain, blk1, blk3) checkForkChoiceErr(chain, blk1, blk3)
check chain.validate info & " (3)" check chain.validate info & " (3)"
# blk4 is not part of chain # blk4 is not part of chain
checkForkChoiceErr(chain, blk4, blk2) checkForkChoiceErr(chain, blk4, blk2)
# finalized > head -> error # finalized > head -> error
checkForkChoiceErr(chain, blk1, blk2) checkForkChoiceErr(chain, blk1, blk2)
# blk4 is not part of chain # blk4 is not part of chain
checkForkChoiceErr(chain, blk2, blk4) checkForkChoiceErr(chain, blk2, blk4)
# finalized < head -> ok # finalized < head -> ok
checkForkChoice(chain, blk2, blk1) checkForkChoice(chain, blk2, blk1)
check chain.headHash == blk2.blockHash check chain.headHash == blk2.blockHash
check chain.latestHash == blk2.blockHash check chain.latestHash == blk2.blockHash
check chain.validate info & " (7)" check chain.validate info & " (7)"
# finalized == head -> ok # finalized == head -> ok
checkForkChoice(chain, blk2, blk2) checkForkChoice(chain, blk2, blk2)
check chain.headHash == blk2.blockHash check chain.headHash == blk2.blockHash
check chain.latestHash == blk2.blockHash check chain.latestHash == blk2.blockHash
check chain.baseNumber == 0'u64 check chain.baseNumber == 0'u64
check chain.validate info & " (8)" check chain.validate info & " (8)"
# baggage written # baggage written
check chain.wdWritten(blk1) == 1 check chain.wdWritten(blk1) == 1
check chain.wdWritten(blk2) == 2 check chain.wdWritten(blk2) == 2
check chain.validate info & " (9)" check chain.validate info & " (9)"
test "newBase on activeBranch": test "newBase on activeBranch":
const info = "newBase on activeBranch" const info = "newBase on activeBranch"
let com = env.newCom() let com = env.newCom()
var chain = ForkedChainRef.init(com, baseDistance = 3) var chain = ForkedChainRef.init(com, baseDistance = 3)
checkImportBlock(chain, blk1) checkImportBlock(chain, blk1)
checkImportBlock(chain, blk2) checkImportBlock(chain, blk2)
@ -246,31 +228,23 @@ proc forkedChainMain*() =
checkImportBlock(chain, blk5) checkImportBlock(chain, blk5)
checkImportBlock(chain, blk6) checkImportBlock(chain, blk6)
checkImportBlock(chain, blk7) checkImportBlock(chain, blk7)
checkImportBlock(chain, blk4) checkImportBlock(chain, blk4)
check chain.validate info & " (1)" check chain.validate info & " (1)"
# newbase == head # newbase == head
checkForkChoice(chain, blk7, blk6) checkForkChoice(chain, blk7, blk6)
check chain.validate info & " (2)" check chain.validate info & " (2)"
check chain.headHash == blk7.blockHash check chain.headHash == blk7.blockHash
check chain.latestHash == blk7.blockHash check chain.latestHash == blk7.blockHash
check chain.baseBranch == chain.activeBranch check chain.baseBranch == chain.activeBranch
check chain.wdWritten(blk7) == 7 check chain.wdWritten(blk7) == 7
# head - baseDistance must been persisted # head - baseDistance must been persisted
checkPersisted(chain, blk3) checkPersisted(chain, blk3)
# make sure aristo not wiped out baggage # make sure aristo not wiped out baggage
check chain.wdWritten(blk3) == 3 check chain.wdWritten(blk3) == 3
check chain.validate info & " (9)" check chain.validate info & " (9)"
test "newBase between oldBase and head": test "newBase between oldBase and head":
const info = "newBase between oldBase and head" const info = "newBase between oldBase and head"
let com = env.newCom() let com = env.newCom()
var chain = ForkedChainRef.init(com, baseDistance = 3) var chain = ForkedChainRef.init(com, baseDistance = 3)
checkImportBlock(chain, blk1) checkImportBlock(chain, blk1)
checkImportBlock(chain, blk2) checkImportBlock(chain, blk2)
@ -280,28 +254,21 @@ proc forkedChainMain*() =
checkImportBlock(chain, blk6) checkImportBlock(chain, blk6)
checkImportBlock(chain, blk7) checkImportBlock(chain, blk7)
check chain.validate info & " (1)" check chain.validate info & " (1)"
checkForkChoice(chain, blk7, blk6) checkForkChoice(chain, blk7, blk6)
check chain.validate info & " (2)" check chain.validate info & " (2)"
check chain.headHash == blk7.blockHash check chain.headHash == blk7.blockHash
check chain.latestHash == blk7.blockHash check chain.latestHash == blk7.blockHash
check chain.baseBranch == chain.activeBranch check chain.baseBranch == chain.activeBranch
check chain.wdWritten(blk6) == 6 check chain.wdWritten(blk6) == 6
check chain.wdWritten(blk7) == 7 check chain.wdWritten(blk7) == 7
# head - baseDistance must been persisted # head - baseDistance must been persisted
checkPersisted(chain, blk3) checkPersisted(chain, blk3)
# make sure aristo not wiped out baggage # make sure aristo not wiped out baggage
check chain.wdWritten(blk3) == 3 check chain.wdWritten(blk3) == 3
check chain.validate info & " (9)" check chain.validate info & " (9)"
test "newBase == oldBase, fork and stay on that fork": test "newBase == oldBase, fork and stay on that fork":
const info = "newBase == oldBase, fork .." const info = "newBase == oldBase, fork .."
let com = env.newCom() let com = env.newCom()
var chain = ForkedChainRef.init(com) var chain = ForkedChainRef.init(com)
checkImportBlock(chain, blk1) checkImportBlock(chain, blk1)
checkImportBlock(chain, blk2) checkImportBlock(chain, blk2)
@ -310,26 +277,20 @@ proc forkedChainMain*() =
checkImportBlock(chain, blk5) checkImportBlock(chain, blk5)
checkImportBlock(chain, blk6) checkImportBlock(chain, blk6)
checkImportBlock(chain, blk7) checkImportBlock(chain, blk7)
checkImportBlock(chain, B4) checkImportBlock(chain, B4)
checkImportBlock(chain, B5) checkImportBlock(chain, B5)
checkImportBlock(chain, B6) checkImportBlock(chain, B6)
checkImportBlock(chain, B7) checkImportBlock(chain, B7)
check chain.validate info & " (1)" check chain.validate info & " (1)"
checkForkChoice(chain, B7, B5) checkForkChoice(chain, B7, B5)
check chain.headHash == B7.blockHash check chain.headHash == B7.blockHash
check chain.latestHash == B7.blockHash check chain.latestHash == B7.blockHash
check chain.baseNumber == 0'u64 check chain.baseNumber == 0'u64
check chain.branches.len == 2 check chain.branches.len == 2
check chain.validate info & " (9)" check chain.validate info & " (9)"
test "newBase move forward, fork and stay on that fork": test "newBase move forward, fork and stay on that fork":
const info = "newBase move forward, fork .." const info = "newBase move forward, fork .."
let com = env.newCom() let com = env.newCom()
var chain = ForkedChainRef.init(com, baseDistance = 3) var chain = ForkedChainRef.init(com, baseDistance = 3)
checkImportBlock(chain, blk1) checkImportBlock(chain, blk1)
checkImportBlock(chain, blk2) checkImportBlock(chain, blk2)
@ -338,28 +299,22 @@ proc forkedChainMain*() =
checkImportBlock(chain, blk5) checkImportBlock(chain, blk5)
checkImportBlock(chain, blk6) checkImportBlock(chain, blk6)
checkImportBlock(chain, blk7) checkImportBlock(chain, blk7)
checkImportBlock(chain, B4) checkImportBlock(chain, B4)
checkImportBlock(chain, B5) checkImportBlock(chain, B5)
checkImportBlock(chain, B6) checkImportBlock(chain, B6)
checkImportBlock(chain, B7) checkImportBlock(chain, B7)
checkImportBlock(chain, B4) checkImportBlock(chain, B4)
check chain.validate info & " (1)" check chain.validate info & " (1)"
checkForkChoice(chain, B6, B4) checkForkChoice(chain, B6, B4)
check chain.validate info & " (2)" check chain.validate info & " (2)"
check chain.headHash == B6.blockHash check chain.headHash == B6.blockHash
check chain.latestHash == B6.blockHash check chain.latestHash == B6.blockHash
check chain.baseNumber == 3'u64 check chain.baseNumber == 3'u64
check chain.branches.len == 2 check chain.branches.len == 2
check chain.validate info & " (9)" check chain.validate info & " (9)"
test "newBase on shorter canonical arc, remove oldBase branches": test "newBase on shorter canonical arc, remove oldBase branches":
const info = "newBase on shorter canonical, remove oldBase branches" const info = "newBase on shorter canonical, remove oldBase branches"
let com = env.newCom() let com = env.newCom()
var chain = ForkedChainRef.init(com, baseDistance = 3) var chain = ForkedChainRef.init(com, baseDistance = 3)
checkImportBlock(chain, blk1) checkImportBlock(chain, blk1)
checkImportBlock(chain, blk2) checkImportBlock(chain, blk2)
@ -368,26 +323,21 @@ proc forkedChainMain*() =
checkImportBlock(chain, blk5) checkImportBlock(chain, blk5)
checkImportBlock(chain, blk6) checkImportBlock(chain, blk6)
checkImportBlock(chain, blk7) checkImportBlock(chain, blk7)
checkImportBlock(chain, B4) checkImportBlock(chain, B4)
checkImportBlock(chain, B5) checkImportBlock(chain, B5)
checkImportBlock(chain, B6) checkImportBlock(chain, B6)
checkImportBlock(chain, B7) checkImportBlock(chain, B7)
check chain.validate info & " (1)" check chain.validate info & " (1)"
checkForkChoice(chain, B7, B6) checkForkChoice(chain, B7, B6)
check chain.validate info & " (2)" check chain.validate info & " (2)"
check chain.headHash == B7.blockHash check chain.headHash == B7.blockHash
check chain.latestHash == B7.blockHash check chain.latestHash == B7.blockHash
check chain.baseNumber == 4'u64 check chain.baseNumber == 4'u64
check chain.branches.len == 1 check chain.branches.len == 1
check chain.validate info & " (9)" check chain.validate info & " (9)"
test "newBase on curbed non-canonical arc": test "newBase on curbed non-canonical arc":
const info = "newBase on curbed non-canonical .." const info = "newBase on curbed non-canonical .."
let com = env.newCom() let com = env.newCom()
var chain = ForkedChainRef.init(com, baseDistance = 5) var chain = ForkedChainRef.init(com, baseDistance = 5)
checkImportBlock(chain, blk1) checkImportBlock(chain, blk1)
checkImportBlock(chain, blk2) checkImportBlock(chain, blk2)
@ -396,27 +346,22 @@ proc forkedChainMain*() =
checkImportBlock(chain, blk5) checkImportBlock(chain, blk5)
checkImportBlock(chain, blk6) checkImportBlock(chain, blk6)
checkImportBlock(chain, blk7) checkImportBlock(chain, blk7)
checkImportBlock(chain, B4) checkImportBlock(chain, B4)
checkImportBlock(chain, B5) checkImportBlock(chain, B5)
checkImportBlock(chain, B6) checkImportBlock(chain, B6)
checkImportBlock(chain, B7) checkImportBlock(chain, B7)
check chain.validate info & " (1)" check chain.validate info & " (1)"
checkForkChoice(chain, B7, B5) checkForkChoice(chain, B7, B5)
check chain.validate info & " (2)" check chain.validate info & " (2)"
check chain.headHash == B7.blockHash check chain.headHash == B7.blockHash
check chain.latestHash == B7.blockHash check chain.latestHash == B7.blockHash
check chain.baseNumber > 0 check chain.baseNumber > 0
check chain.baseNumber < B4.header.number check chain.baseNumber < B4.header.number
check chain.branches.len == 2 check chain.branches.len == 2
check chain.validate info & " (9)" check chain.validate info & " (9)"
test "newBase == oldBase, fork and return to old chain": test "newBase == oldBase, fork and return to old chain":
const info = "newBase == oldBase, fork .." const info = "newBase == oldBase, fork .."
let com = env.newCom() let com = env.newCom()
var chain = ForkedChainRef.init(com) var chain = ForkedChainRef.init(com)
checkImportBlock(chain, blk1) checkImportBlock(chain, blk1)
checkImportBlock(chain, blk2) checkImportBlock(chain, blk2)
@ -425,25 +370,20 @@ proc forkedChainMain*() =
checkImportBlock(chain, blk5) checkImportBlock(chain, blk5)
checkImportBlock(chain, blk6) checkImportBlock(chain, blk6)
checkImportBlock(chain, blk7) checkImportBlock(chain, blk7)
checkImportBlock(chain, B4) checkImportBlock(chain, B4)
checkImportBlock(chain, B5) checkImportBlock(chain, B5)
checkImportBlock(chain, B6) checkImportBlock(chain, B6)
checkImportBlock(chain, B7) checkImportBlock(chain, B7)
check chain.validate info & " (1)" check chain.validate info & " (1)"
checkForkChoice(chain, blk7, blk5) checkForkChoice(chain, blk7, blk5)
check chain.validate info & " (2)" check chain.validate info & " (2)"
check chain.headHash == blk7.blockHash check chain.headHash == blk7.blockHash
check chain.latestHash == blk7.blockHash check chain.latestHash == blk7.blockHash
check chain.baseNumber == 0'u64 check chain.baseNumber == 0'u64
check chain.validate info & " (9)" check chain.validate info & " (9)"
test "newBase on activeBranch, fork and return to old chain": test "newBase on activeBranch, fork and return to old chain":
const info = "newBase on activeBranch, fork .." const info = "newBase on activeBranch, fork .."
let com = env.newCom() let com = env.newCom()
var chain = ForkedChainRef.init(com, baseDistance = 3) var chain = ForkedChainRef.init(com, baseDistance = 3)
checkImportBlock(chain, blk1) checkImportBlock(chain, blk1)
checkImportBlock(chain, blk2) checkImportBlock(chain, blk2)
@ -452,28 +392,22 @@ proc forkedChainMain*() =
checkImportBlock(chain, blk5) checkImportBlock(chain, blk5)
checkImportBlock(chain, blk6) checkImportBlock(chain, blk6)
checkImportBlock(chain, blk7) checkImportBlock(chain, blk7)
checkImportBlock(chain, B4) checkImportBlock(chain, B4)
checkImportBlock(chain, B5) checkImportBlock(chain, B5)
checkImportBlock(chain, B6) checkImportBlock(chain, B6)
checkImportBlock(chain, B7) checkImportBlock(chain, B7)
checkImportBlock(chain, blk4) checkImportBlock(chain, blk4)
check chain.validate info & " (1)" check chain.validate info & " (1)"
checkForkChoice(chain, blk7, blk5) checkForkChoice(chain, blk7, blk5)
check chain.validate info & " (2)" check chain.validate info & " (2)"
check chain.headHash == blk7.blockHash check chain.headHash == blk7.blockHash
check chain.latestHash == blk7.blockHash check chain.latestHash == blk7.blockHash
check chain.baseBranch == chain.activeBranch check chain.baseBranch == chain.activeBranch
check chain.validate info & " (9)" check chain.validate info & " (9)"
test "newBase on shorter canonical arc, discard arc with oldBase" & test "newBase on shorter canonical arc, discard arc with oldBase" &
" (ign dup block)": " (ign dup block)":
const info = "newBase on shorter canonical .." const info = "newBase on shorter canonical .."
let com = env.newCom() let com = env.newCom()
var chain = ForkedChainRef.init(com, baseDistance = 3) var chain = ForkedChainRef.init(com, baseDistance = 3)
checkImportBlock(chain, blk1) checkImportBlock(chain, blk1)
checkImportBlock(chain, blk2) checkImportBlock(chain, blk2)
@ -482,28 +416,22 @@ proc forkedChainMain*() =
checkImportBlock(chain, blk5) checkImportBlock(chain, blk5)
checkImportBlock(chain, blk6) checkImportBlock(chain, blk6)
checkImportBlock(chain, blk7) checkImportBlock(chain, blk7)
checkImportBlock(chain, B4) checkImportBlock(chain, B4)
checkImportBlock(chain, B5) checkImportBlock(chain, B5)
checkImportBlock(chain, B6) checkImportBlock(chain, B6)
checkImportBlock(chain, B7) checkImportBlock(chain, B7)
checkImportBlock(chain, blk4) checkImportBlock(chain, blk4)
check chain.validate info & " (1)" check chain.validate info & " (1)"
checkForkChoice(chain, B7, B5) checkForkChoice(chain, B7, B5)
check chain.validate info & " (2)" check chain.validate info & " (2)"
check chain.headHash == B7.blockHash check chain.headHash == B7.blockHash
check chain.latestHash == B7.blockHash check chain.latestHash == B7.blockHash
check chain.baseNumber == 4'u64 check chain.baseNumber == 4'u64
check chain.branches.len == 1 check chain.branches.len == 1
check chain.validate info & " (9)" check chain.validate info & " (9)"
test "newBase on longer canonical arc, discard new branch": test "newBase on longer canonical arc, discard new branch":
const info = "newBase on longer canonical .." const info = "newBase on longer canonical .."
let com = env.newCom() let com = env.newCom()
var chain = ForkedChainRef.init(com, baseDistance = 3) var chain = ForkedChainRef.init(com, baseDistance = 3)
checkImportBlock(chain, blk1) checkImportBlock(chain, blk1)
checkImportBlock(chain, blk2) checkImportBlock(chain, blk2)
@ -512,27 +440,22 @@ proc forkedChainMain*() =
checkImportBlock(chain, blk5) checkImportBlock(chain, blk5)
checkImportBlock(chain, blk6) checkImportBlock(chain, blk6)
checkImportBlock(chain, blk7) checkImportBlock(chain, blk7)
checkImportBlock(chain, B4) checkImportBlock(chain, B4)
checkImportBlock(chain, B5) checkImportBlock(chain, B5)
checkImportBlock(chain, B6) checkImportBlock(chain, B6)
checkImportBlock(chain, B7) checkImportBlock(chain, B7)
check chain.validate info & " (1)" check chain.validate info & " (1)"
checkForkChoice(chain, blk7, blk5) checkForkChoice(chain, blk7, blk5)
check chain.validate info & " (2)" check chain.validate info & " (2)"
check chain.headHash == blk7.blockHash check chain.headHash == blk7.blockHash
check chain.latestHash == blk7.blockHash check chain.latestHash == blk7.blockHash
check chain.baseNumber > 0 check chain.baseNumber > 0
check chain.baseNumber < blk5.header.number check chain.baseNumber < blk5.header.number
check chain.branches.len == 1 check chain.branches.len == 1
check chain.validate info & " (9)" check chain.validate info & " (9)"
test "headerByNumber": test "headerByNumber":
const info = "headerByNumber" const info = "headerByNumber"
let com = env.newCom() let com = env.newCom()
var chain = ForkedChainRef.init(com, baseDistance = 3) var chain = ForkedChainRef.init(com, baseDistance = 3)
checkImportBlock(chain, blk1) checkImportBlock(chain, blk1)
checkImportBlock(chain, blk2) checkImportBlock(chain, blk2)
@ -541,98 +464,77 @@ proc forkedChainMain*() =
checkImportBlock(chain, blk5) checkImportBlock(chain, blk5)
checkImportBlock(chain, blk6) checkImportBlock(chain, blk6)
checkImportBlock(chain, blk7) checkImportBlock(chain, blk7)
checkImportBlock(chain, B4) checkImportBlock(chain, B4)
checkImportBlock(chain, B5) checkImportBlock(chain, B5)
checkImportBlock(chain, B6) checkImportBlock(chain, B6)
checkImportBlock(chain, B7) checkImportBlock(chain, B7)
check chain.validate info & " (1)" check chain.validate info & " (1)"
checkForkChoice(chain, blk7, blk5) checkForkChoice(chain, blk7, blk5)
check chain.validate info & " (2)" check chain.validate info & " (2)"
# cursor # cursor
check chain.headerByNumber(8).isErr check chain.headerByNumber(8).isErr
check chain.headerByNumber(7).expect("OK").number == 7 check chain.headerByNumber(7).expect("OK").number == 7
check chain.headerByNumber(7).expect("OK").blockHash == blk7.blockHash check chain.headerByNumber(7).expect("OK").blockHash == blk7.blockHash
# from db # from db
check chain.headerByNumber(3).expect("OK").number == 3 check chain.headerByNumber(3).expect("OK").number == 3
check chain.headerByNumber(3).expect("OK").blockHash == blk3.blockHash check chain.headerByNumber(3).expect("OK").blockHash == blk3.blockHash
# base # base
check chain.headerByNumber(4).expect("OK").number == 4 check chain.headerByNumber(4).expect("OK").number == 4
check chain.headerByNumber(4).expect("OK").blockHash == blk4.blockHash check chain.headerByNumber(4).expect("OK").blockHash == blk4.blockHash
# from cache # from cache
check chain.headerByNumber(5).expect("OK").number == 5 check chain.headerByNumber(5).expect("OK").number == 5
check chain.headerByNumber(5).expect("OK").blockHash == blk5.blockHash check chain.headerByNumber(5).expect("OK").blockHash == blk5.blockHash
check chain.validate info & " (9)" check chain.validate info & " (9)"
test "3 branches, alternating imports": test "3 branches, alternating imports":
const info = "3 branches, alternating imports" const info = "3 branches, alternating imports"
let com = env.newCom() let com = env.newCom()
var chain = ForkedChainRef.init(com, baseDistance = 3) var chain = ForkedChainRef.init(com, baseDistance = 3)
checkImportBlock(chain, blk1) checkImportBlock(chain, blk1)
checkImportBlock(chain, blk2) checkImportBlock(chain, blk2)
checkImportBlock(chain, blk3) checkImportBlock(chain, blk3)
checkImportBlock(chain, B4) checkImportBlock(chain, B4)
checkImportBlock(chain, blk4) checkImportBlock(chain, blk4)
checkImportBlock(chain, B5) checkImportBlock(chain, B5)
checkImportBlock(chain, blk5) checkImportBlock(chain, blk5)
checkImportBlock(chain, C5) checkImportBlock(chain, C5)
checkImportBlock(chain, B6) checkImportBlock(chain, B6)
checkImportBlock(chain, blk6) checkImportBlock(chain, blk6)
checkImportBlock(chain, C6) checkImportBlock(chain, C6)
checkImportBlock(chain, B7) checkImportBlock(chain, B7)
checkImportBlock(chain, blk7) checkImportBlock(chain, blk7)
checkImportBlock(chain, C7) checkImportBlock(chain, C7)
check chain.validate info & " (1)" check chain.validate info & " (1)"
check chain.latestHash == C7.blockHash check chain.latestHash == C7.blockHash
check chain.latestNumber == 7'u64 check chain.latestNumber == 7'u64
check chain.branches.len == 3 check chain.branches.len == 3
checkForkChoice(chain, B7, blk3) checkForkChoice(chain, B7, blk3)
check chain.validate info & " (2)" check chain.validate info & " (2)"
check chain.branches.len == 3 check chain.branches.len == 3
checkForkChoice(chain, B7, B6) checkForkChoice(chain, B7, B6)
check chain.validate info & " (2)" check chain.validate info & " (2)"
check chain.branches.len == 1 check chain.branches.len == 1
test "importing blocks with new CommonRef and FC instance, 3 blocks": test "importing blocks with new CommonRef and FC instance, 3 blocks":
const info = "importing blocks with new CommonRef and FC instance, 3 blocks" const info = "importing blocks with new CommonRef and FC instance, 3 blocks"
let com = env.newCom() let com = env.newCom()
let chain = ForkedChainRef.init(com, baseDistance = 0) let chain = ForkedChainRef.init(com, baseDistance = 0)
checkImportBlock(chain, blk1) checkImportBlock(chain, blk1)
checkImportBlock(chain, blk2) checkImportBlock(chain, blk2)
checkImportBlock(chain, blk3) checkImportBlock(chain, blk3)
checkForkChoice(chain, blk3, blk3) checkForkChoice(chain, blk3, blk3)
check chain.validate info & " (1)" check chain.validate info & " (1)"
let cc = env.newCom(com.db) let cc = env.newCom(com.db)
let fc = ForkedChainRef.init(cc, baseDistance = 0) let fc = ForkedChainRef.init(cc, baseDistance = 0)
check fc.headHash == blk3.blockHash check fc.headHash == blk3.blockHash
checkImportBlock(fc, blk4) checkImportBlock(fc, blk4)
checkForkChoice(fc, blk4, blk4) checkForkChoice(fc, blk4, blk4)
check chain.validate info & " (2)" check chain.validate info & " (2)"
test "importing blocks with new CommonRef and FC instance, 1 block": test "importing blocks with new CommonRef and FC instance, 1 block":
const info = "importing blocks with new CommonRef and FC instance, 1 block" const info = "importing blocks with new CommonRef and FC instance, 1 block"
let com = env.newCom() let com = env.newCom()
let chain = ForkedChainRef.init(com, baseDistance = 0) let chain = ForkedChainRef.init(com, baseDistance = 0)
checkImportBlock(chain, blk1) checkImportBlock(chain, blk1)
checkForkChoice(chain, blk1, blk1) checkForkChoice(chain, blk1, blk1)
check chain.validate info & " (1)" check chain.validate info & " (1)"
let cc = env.newCom(com.db) let cc = env.newCom(com.db)
let fc = ForkedChainRef.init(cc, baseDistance = 0) let fc = ForkedChainRef.init(cc, baseDistance = 0)
check fc.headHash == blk1.blockHash check fc.headHash == blk1.blockHash
@ -640,5 +542,48 @@ proc forkedChainMain*() =
checkForkChoice(fc, blk2, blk2) checkForkChoice(fc, blk2, blk2)
check chain.validate info & " (2)" check chain.validate info & " (2)"
suite "ForkedChain mainnet replay":
# A short mainnet replay test to check that the first few hundred blocks can
# be imported using a typical importBlock / fcu sequence - this does not
# test any transactions since these blocks are practically empty, but thanks
# to block rewards the state db keeps changing anyway providing a simple
# smoke test
setup:
let
era0 = Era1DbRef.init(sourcePath / "replay", "mainnet").expect("Era files present")
com = CommonRef.new(AristoDbMemory.newCoreDbRef(), nil)
fc = ForkedChainRef.init(com)
test "Replay mainnet era, single FCU":
var blk: EthBlock
for i in 1..<fc.baseDistance * 2:
era0.getEthBlock(i.BlockNumber, blk).expect("block in test database")
check:
fc.importBlock(blk).isOk()
check:
fc.forkChoice(blk.blockHash, blk.blockHash).isOk()
test "Replay mainnet era, multiple FCU":
# Simulates the typical case where fcu comes after the block
var blk: EthBlock
era0.getEthBlock(0.BlockNumber, blk).expect("block in test database")
var blocks = [blk.blockHash, blk.blockHash]
for i in 1..<fc.baseDistance * 2:
era0.getEthBlock(i.BlockNumber, blk).expect("block in test database")
check:
fc.importBlock(blk).isOk()
let hash = blk.blockHash
check:
fc.forkChoice(hash, blocks[0]).isOk()
if i mod 32 == 0:
# in reality, finalized typically lags a bit more than this, but
# for the purpose of the test, this should be good enough
blocks[0] = blocks[1]
blocks[1] = hash
forkedChainMain() forkedChainMain()

View File

@ -138,7 +138,7 @@ createRpcSigsFromNim(RpcClient):
# Test Runners # Test Runners
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
proc runKeyLoader(noisy = true; proc runKeyLoader(noisy = defined(debug);
keyFile = jwtKeyFile; strippedFile = jwtKeyStripped) = keyFile = jwtKeyFile; strippedFile = jwtKeyStripped) =
let let
filePath = keyFile.findFilePath.value filePath = keyFile.findFilePath.value
@ -229,7 +229,7 @@ proc runKeyLoader(noisy = true;
# Compare key against contents of shared key file # Compare key against contents of shared key file
check hexKey.cmpIgnoreCase(hexLine) == 0 check hexKey.cmpIgnoreCase(hexLine) == 0
proc runJwtAuth(noisy = true; keyFile = jwtKeyFile) = proc runJwtAuth(noisy = defined(debug); keyFile = jwtKeyFile) =
let let
filePath = keyFile.findFilePath.value filePath = keyFile.findFilePath.value
dataDir = filePath.splitPath.head dataDir = filePath.splitPath.head
@ -297,10 +297,6 @@ proc runJwtAuth(noisy = true; keyFile = jwtKeyFile) =
# Main function(s) # Main function(s)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
proc jwtAuthMain*(noisy = defined(debug)) =
noisy.runKeyLoader
noisy.runJwtAuth
when isMainModule: when isMainModule:
setErrorLevel() setErrorLevel()