diff --git a/execution_chain/common/common.nim b/execution_chain/common/common.nim index 19dc1aab3b..159b5f3377 100644 --- a/execution_chain/common/common.nim +++ b/execution_chain/common/common.nim @@ -104,6 +104,10 @@ type statelessWitnessValidation*: bool ## Enable full validation of execution witnesses. + optimisticStatePrefetch*: bool + ## Optimistically pre-execute the transactions of a block on background + ## threads to warm database caches before the main thread executes them. + # ------------------------------------------------------------------------------ # Private helper functions # ------------------------------------------------------------------------------ @@ -176,7 +180,8 @@ proc init(com : CommonRef, genesis : Genesis, initializeDb: bool, statelessProviderEnabled: bool, - statelessWitnessValidation: bool) = + statelessWitnessValidation: bool, + optimisticStatePrefetch: bool) = config.daoCheck() @@ -215,6 +220,7 @@ proc init(com : CommonRef, com.statelessProviderEnabled = statelessProviderEnabled com.statelessWitnessValidation = statelessWitnessValidation + com.optimisticStatePrefetch = optimisticStatePrefetch proc isBlockAfterTtd(com: CommonRef, header: Header, txFrame: CoreDbTxRef): bool = if com.config.terminalTotalDifficulty.isNone: @@ -239,6 +245,7 @@ proc new*( initializeDb = true; statelessProviderEnabled = false; statelessWitnessValidation = false; + optimisticStatePrefetch = false; ): CommonRef = ## If genesis data is present, the forkIds will be initialized @@ -251,7 +258,8 @@ proc new*( params.genesis, initializeDb, statelessProviderEnabled, - statelessWitnessValidation) + statelessWitnessValidation, + optimisticStatePrefetch) proc new*( _: type CommonRef; @@ -260,7 +268,8 @@ proc new*( networkId: NetworkId = MainNet; initializeDb = true; statelessProviderEnabled = false; - statelessWitnessValidation = false + statelessWitnessValidation = false; + optimisticStatePrefetch = false; ): CommonRef = ## There is no genesis data present @@ -273,7 +282,8 @@ proc new*( nil, initializeDb, statelessProviderEnabled, - statelessWitnessValidation) + statelessWitnessValidation, + optimisticStatePrefetch) func clone*(com: CommonRef, db: CoreDbRef): CommonRef = ## clone but replace the db @@ -287,7 +297,8 @@ func clone*(com: CommonRef, db: CoreDbRef): CommonRef = genesisHeader: com.genesisHeader, networkId : com.networkId, statelessProviderEnabled: com.statelessProviderEnabled, - statelessWitnessValidation: com.statelessWitnessValidation + statelessWitnessValidation: com.statelessWitnessValidation, + optimisticStatePrefetch: com.optimisticStatePrefetch ) func clone*(com: CommonRef): CommonRef = diff --git a/execution_chain/concurrency/utils.nim b/execution_chain/concurrency/utils.nim new file mode 100644 index 0000000000..e9916ae1e0 --- /dev/null +++ b/execution_chain/concurrency/utils.nim @@ -0,0 +1,28 @@ +# Nimbus +# Copyright (c) 2026 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +# These borrow functions are a workaround to avoid updating the ref count +# of a ref type when assigning it into another ref type or heap object. +# This is needed because refc doesn't support atomic reference counts +# and when passing in a ref type from the main thread as a parameter +# of a new ref instance in a child thread/task the ref count update can cause +# memory corruption and crashes due to a race with the ref counts being updated +# in both the main thread and the child thread concurrently. +# +# The unborrowRef function is needed to cleanup the borrowed ref after usage +# and before the GC runs on the ref type containing the borrowed ref. + +template borrowRef*[T](dest, src: ref T) = + # Copies the ref type without updating the ref count. + copyMem(addr dest, addr src, sizeof(pointer)) + +template unborrowRef*[T](dest: ref T) = + # Sets the ref type back to nil without updating the ref count. + var p: pointer = nil + copyMem(addr dest, addr p, sizeof(pointer)) \ No newline at end of file diff --git a/execution_chain/conf.nim b/execution_chain/conf.nim index 0fe9a2b3a3..1453403265 100644 --- a/execution_chain/conf.nim +++ b/execution_chain/conf.nim @@ -358,6 +358,13 @@ type desc: "Compute state root in parallel using multiple threads" name: "debug-parallel-state-root".}: bool + optimisticStatePrefetch* {. + hidden + defaultValue: true + desc: "Optimistically pre-execute block transactions on background " & + "threads to warm DB caches" + name: "debug-optimistic-state-prefetch".}: bool + eagerStateRootCheck* {. hidden desc: "Eagerly check state roots when syncing finalized blocks" diff --git a/execution_chain/core/executor/process_block.nim b/execution_chain/core/executor/process_block.nim index 80e8d1675f..af02ed45fe 100644 --- a/execution_chain/core/executor/process_block.nim +++ b/execution_chain/core/executor/process_block.nim @@ -18,7 +18,9 @@ import ../../transaction, ../../evm/state, ../../evm/types, + ../../evm/interpreter/gas_costs, ../../block_access_list/block_access_list_validation, + ../../concurrency/utils, ../dao, ../eip6110, ./calculate_reward, @@ -26,43 +28,130 @@ import ./process_transaction, eth/common/[keys, transaction_utils], chronicles, - results + results, + stew/assign2 when compileOption("threads"): - import taskpools - - template withSenderParallel(txs: openArray[Transaction], body: untyped, taskpool: Taskpool) = - type Entry = (Signature, Hash32, Flowvar[Address]) - - proc recoverTask(e: ptr Entry): Address {.nimcall.} = - let pk = recover(e[][0], SkMessage(e[][1].data)) - if pk.isOk(): - pk[].to(Address) - else: - default(Address) - - var entries = newSeq[Entry](txs.len) - - # Prepare signature recovery tasks for each transaction - for simplicity, - # we use `default(Address)` to signal sig check failure + import std/atomics, taskpools + + type + Entry = object + sig: Signature + hash: Hash32 + sender: Address + senderReady: Atomic[bool] + fut: Flowvar[bool] + + PrefetchCtx = object + cancel: Atomic[bool] + parent: Header + blockCtx: BlockContext + com: CommonRef + txFrame: CoreDbTxRef + + proc recoverAndPrefetchTask( + e: ptr Entry, ctx: ptr PrefetchCtx, tx: ptr Transaction): bool {.nimcall.} = + + # Recover the sender from the signature. `default(Address)` signals sig + # check failure. + let + pk = recover(e[].sig, SkMessage(e[].hash.data)) + sender = + if pk.isOk(): pk[].to(Address) + else: default(Address) + e[].sender = sender + e[].senderReady.store(true, moRelease) + + # When ctx is non-nil, optimistic state prefetch is enabled + if ctx.isNil() or sender == default(Address) or ctx[].cancel.load(moAcquire): + return true + + # Create the ledger without triggering a ref count increment on the txFrame + # which is owned by the main/parent thread. + let ledger = LedgerRef() + ledger.txFrame.borrowRef(ctx[].txFrame) + defer: + ledger.txFrame.unborrowRef() + discard ledger.beginSavePoint() + + # Create the vmState without triggering a ref count increment on the common object + # which is owned by the main/parent thread. + let vmState = BaseVMState() + vmState.com.borrowRef(ctx[].com) + defer: + vmState.com.unborrowRef() + vmState.ledger = ledger + assign(vmState.parent, ctx[].parent) + assign(vmState.blockCtx, ctx[].blockCtx) + const txCtx = default(TxContext) + assign(vmState.txCtx, txCtx) + vmState.hardFork = vmState.determineFork + vmState.fork = ToEVMFork[vmState.hardFork] + vmState.gasCosts = vmState.fork.forkToSchedule + vmState.tracer = nil + vmState.receipts.setLen(0) + vmState.cumulativeGasUsed = 0 + vmState.blockRegularGasUsed = 0 + vmState.blockStateGasUsed = 0 + vmState.blobGasUsed = 0'u64 + vmState.allLogs.setLen(0) + vmState.gasRefunded = 0 + vmState.balTracker = nil + + # Execute the transaction discarding the results in order to fill the in memory caches. + vmState.prefetchTransaction(tx[], sender) + + true + + template withSenderParallel( + vmState: BaseVMState, txs: openArray[Transaction], body: untyped) = + doAssert not vmState.com.taskpool.isNil() + + var + entries = newSeq[Entry](txs.len) + ctx: PrefetchCtx + ctxPtr: ptr PrefetchCtx = nil + + if vmState.com.optimisticStatePrefetch and vmState.com.taskpool.numThreads > 1: + ctx.parent = vmState.parent + ctx.blockCtx = vmState.blockCtx + ctx.com = vmState.com + # Run the prefetch on the parent frame because the current frame will + # be writen to during block execution and this way we avoid having to + # use locking on the frame data structures. + ctx.txFrame = vmState.ledger.txFrame.parent() + ctx.cancel.store(false, moRelease) + ctxPtr = ctx.addr + + # Spawn one task per transaction that recovers the sender and, when ctxPtr + # is non-nil, also performs an optimistic state prefetch. Spawning here + # allows the task to start early, while we still haven't hashed subsequent txs. for i, e in entries.mpairs(): - e[0] = txs[i].signature().valueOr(default(Signature)) - e[1] = txs[i].rlpHashForSigning(txs[i].isEip155) - let a = addr e - # Spawning the task here allows it to start early, while we still haven't - # hashed subsequent txs - e[2] = taskpool.spawn recoverTask(a) - - for txIndex {.inject.}, e in entries.mpairs(): - template tx(): untyped = - txs[txIndex] - - # Sync blocks until the sender is available from the task pool - as soon - # as we have it, we can process this transaction while the senders of the - # other transactions are being computed - let sender {.inject.} = sync(e[2]) - - body + e.sig = txs[i].signature().valueOr(default(Signature)) + e.hash = txs[i].rlpHashForSigning(txs[i].isEip155) + let entryPtr = e.addr + e.fut = vmState.com.taskpool.spawn recoverAndPrefetchTask( + entryPtr, ctxPtr, txs[i].addr) + + try: + for txIndex {.inject.}, e in entries.mpairs(): + template tx(): untyped = + txs[txIndex] + + # Wait until the worker has published the sender. + while not e.senderReady.load(moAcquire): + cpuRelax() + let sender {.inject.} = e.sender + + body + finally: + if not ctxPtr.isNil(): + # Cancel any in-flight prefetch tasks so that they bail out quickly. + ctxPtr[].cancel.store(true, moRelease) + # Wait for all tasks to complete before returning so that no task + # outlives the local data it references. + for e in entries.mitems(): + discard sync(e.fut) template withSenderSerial(txs: openArray[Transaction], body: untyped) = for txIndex {.inject.}, tx {.inject.} in txs: @@ -76,7 +165,7 @@ template withSender(vmState: BaseVMState, txs: openArray[Transaction], body: unt if vmState.com.taskpool == nil: withSenderSerial(txs, body) else: - withSenderParallel(txs, body, vmState.com.taskpool) + withSenderParallel(vmState, txs, body) else: withSenderSerial(txs, body) diff --git a/execution_chain/core/executor/process_transaction.nim b/execution_chain/core/executor/process_transaction.nim index 9392f793b4..4e2de99c8f 100644 --- a/execution_chain/core/executor/process_transaction.nim +++ b/execution_chain/core/executor/process_transaction.nim @@ -88,25 +88,26 @@ proc commitOrRollbackDependingOnGasUsed( emitClosureLogs(vmState, callResult.logEntries) ok() -# ------------------------------------------------------------------------------ -# Public functions -# ------------------------------------------------------------------------------ - -proc processTransaction*( - vmState: BaseVMState; ## Parent accounts environment for transaction - tx: Transaction; ## Transaction to validate - sender: Address; ## tx.recoverSender - rollbackReads: bool = false; - ): Result[LogResult, string] = - ## Modelled after `https://eips.ethereum.org/EIPS/eip-1559#specification`_ - ## which provides a backward compatible framwork for EIP1559. +template validateForInclusion( + vmState: BaseVMState; + tx: Transaction; + sender: Address; + skipNonceCheck: bool; + buildError: static bool; + intrinsicVar, blobGasUsedVar: untyped) = + + template fail(msg: untyped): untyped = + when buildError: + return err(msg) + else: + return let com = vmState.com fork = vmState.hardFork regularGasAvailable = vmState.blockCtx.gasLimit - vmState.blockRegularGasUsed stateGasAvailable = vmState.blockCtx.gasLimit - vmState.blockStateGasUsed - intrinsic = tx.intrinsicGas(fork, vmState.blockCtx.gasLimit) + intrinsicVar = tx.intrinsicGas(fork, vmState.blockCtx.gasLimit) # Per-tx 2D gas inclusion check: for each dimension the worst-case # contribution must fit in the remaining budget. Block-end @@ -114,33 +115,48 @@ proc processTransaction*( if fork < Amsterdam: let want = min(TX_GAS_LIMIT.GasInt, tx.gasLimit) if want > regularGasAvailable: - return err("regular gas used exceeds limit, want: " & $want & ", available: " & $regularGasAvailable) + fail("regular gas used exceeds limit, want: " & $want & ", available: " & $regularGasAvailable) else: # https://github.com/ethereum/execution-specs/pull/2703/changes # Worst-case regular contribution: tx.gasLimit minus the portion that # must go to intrinsic state gas, capped at TX_MAX_GAS_LIMIT. - let want = min(TX_GAS_LIMIT.GasInt, tx.gasLimit - intrinsic.state) + let want = min(TX_GAS_LIMIT.GasInt, tx.gasLimit - intrinsicVar.state) if want > regularGasAvailable: - return err("regular gas used exceeds limit, want: " & $want & ", available: " & $regularGasAvailable) + fail("regular gas used exceeds limit, want: " & $want & ", available: " & $regularGasAvailable) # Worst-case state contribution: tx.gasLimit minus the portion that # must go to intrinsic regular gas. - let stateGas = tx.gasLimit - intrinsic.regular + let stateGas = tx.gasLimit - intrinsicVar.regular if stateGas > stateGasAvailable: - return err("state gas used exceeds limit, want: " & $stateGas & ", available: " & $stateGasAvailable) + fail("state gas used exceeds limit, want: " & $stateGas & ", available: " & $stateGasAvailable) # blobGasUsed will be added to vmState.blobGasUsed if the tx is ok. let - blobGasUsed = tx.getTotalBlobGas + blobGasUsedVar = tx.getTotalBlobGas maxBlobGasPerBlock = getMaxBlobGasPerBlock(com, fork) - if vmState.blobGasUsed + blobGasUsed > maxBlobGasPerBlock: - return err("blobGasUsed " & $blobGasUsed & + if vmState.blobGasUsed + blobGasUsedVar > maxBlobGasPerBlock: + fail("blobGasUsed " & $blobGasUsedVar & " exceeds maximum allowance " & $maxBlobGasPerBlock) - ? validateTxBasic(com, tx, intrinsic, fork) + validateTxBasic(com, tx, intrinsicVar, fork).isOkOr: + fail(error) + vmState.validateTransaction(tx, sender, skipNonceCheck).isOkOr: + fail(error) + +# ------------------------------------------------------------------------------ +# Public functions +# ------------------------------------------------------------------------------ + +proc processTransaction*( + vmState: BaseVMState; ## Parent accounts environment for transaction + tx: Transaction; ## Transaction to validate + sender: Address; ## tx.recoverSender + rollbackReads: bool = false; + ): Result[LogResult, string] = + ## Modelled after `https://eips.ethereum.org/EIPS/eip-1559#specification`_ + ## which provides a backward compatible framwork for EIP1559. - vmState.validateTransaction(tx, sender).isOkOr: - return err(error) + validateForInclusion(vmState, tx, sender, false, true, intrinsic, blobGasUsed) # Execute the transaction. vmState.captureTxStart(tx.gasLimit) @@ -160,10 +176,22 @@ proc processTransaction*( else: ok(move(callResult)) - vmState.ledger.persist(clearEmptyAccount = fork >= Spurious) + vmState.ledger.persist(clearEmptyAccount = vmState.hardFork >= Spurious) res +proc prefetchTransaction*( + vmState: BaseVMState; ## Throwaway accounts environment for prefetching + tx: Transaction; ## Transaction to speculatively execute + sender: Address; ## Pre-recovered sender + ) = + + validateForInclusion(vmState, tx, sender, true, false, intrinsic, blobGasUsed) + + let savePoint = vmState.ledger.beginSavePoint() + tx.txCallEvm(sender, vmState, intrinsic, discardResult = true) + vmState.ledger.rollback(savePoint) + proc processBeaconBlockRoot*(vmState: BaseVMState, beaconRoot: Hash32) = ## processBeaconBlockRoot applies the EIP-4788 system call to the ## beacon block root contract. This method is exported to be used in tests. diff --git a/execution_chain/core/validate.nim b/execution_chain/core/validate.nim index e960f5bf13..053849d392 100644 --- a/execution_chain/core/validate.nim +++ b/execution_chain/core/validate.nim @@ -347,8 +347,9 @@ proc validateTransaction*( vmState: BaseVMState; tx: Transaction; ## tx to validate sender: Address; ## tx.recoverSender + skipNonceCheck = false ): Result[void, string] = - + let ledger = vmState.ledger baseFee = vmState.blockCtx.baseFeePerGas @@ -384,8 +385,9 @@ proc validateTransaction*( if balance - gasCost < tx.value: return err(&"invalid tx: not enough cash to send. avail={balance}, availMinusGas={balance-gasCost}, require={tx.value}") - if tx.nonce != nonce: - return err(&"invalid tx: account nonce mismatch. txNonce={tx.nonce}, accNonce={nonce}") + if not skipNonceCheck: + if tx.nonce != nonce: + return err(&"invalid tx: account nonce mismatch. txNonce={tx.nonce}, accNonce={nonce}") if tx.nonce == high(uint64): return err(&"invalid tx: nonce at maximum") diff --git a/execution_chain/db/aristo/aristo_desc.nim b/execution_chain/db/aristo/aristo_desc.nim index 4bec7a7052..a01463de34 100644 --- a/execution_chain/db/aristo/aristo_desc.nim +++ b/execution_chain/db/aristo/aristo_desc.nim @@ -25,7 +25,7 @@ import std/[hashes, sequtils, sets, tables, heapqueue], eth/common/hashes, eth/trie/nibbles, results, - minilru, + ../../concurrency/lru, ./aristo_constants, ./aristo_desc/[desc_error, desc_identifiers, desc_structural], ./aristo_desc/desc_backend @@ -37,7 +37,7 @@ when compileOption("threads"): # Not auto-exporting backend export tables, aristo_constants, desc_error, desc_identifiers, nibbles, - desc_structural, minilru, hashes, heapqueue, PutHdlRef + desc_structural, lru, hashes, heapqueue, PutHdlRef type AristoTxRef* = ref object @@ -129,7 +129,7 @@ type txRef*: AristoTxRef ## Bottom-most in-memory frame - accLeaves*: LruCache[Hash32, CachedAccLeaf] + accLeaves*: ConcurrentLruCache[Hash32, CachedAccLeaf] ## Account path to payload cache - accounts are frequently accessed by ## account path when contracts interact with them - this cache ensures ## that we don't have to re-traverse the storage trie for every such @@ -137,7 +137,7 @@ type ## TODO a better solution would probably be to cache this in a type ## exposed to the high-level API - stoLeaves*: LruCache[Hash32, CachedStoLeaf] + stoLeaves*: ConcurrentLruCache[Hash32, CachedStoLeaf] ## Mixed account/storage path to payload cache - same as above but caches ## the full lookup of storage slots diff --git a/execution_chain/db/aristo/aristo_init/init_common.nim b/execution_chain/db/aristo/aristo_init/init_common.nim index a235df906d..d5e7b80f2f 100644 --- a/execution_chain/db/aristo/aristo_init/init_common.nim +++ b/execution_chain/db/aristo/aristo_init/init_common.nim @@ -81,7 +81,9 @@ proc finishSession*(hdl: TypedPutHdlRef; db: TypedBackendRef) = proc initInstance*( db: AristoDbRef, maxSnapshots = defaultMaxSnapshots, - parallelStateRootComputation = false + parallelStateRootComputation = true, + accLeavesLruSize = 0, + stoLeavesLruSize = 0 ): Result[void, AristoError] = doAssert maxSnapshots > 0 let vTop = (?db.getLstFn()).vTop @@ -90,8 +92,8 @@ proc initInstance*( when compileOption("threads"): db.txRef.lock.init() - db.accLeaves = LruCache[Hash32, CachedAccLeaf].init(ACC_LRU_SIZE) - db.stoLeaves = LruCache[Hash32, CachedStoLeaf].init(ACC_LRU_SIZE) + db.accLeaves.init(accLeavesLruSize) + db.stoLeaves.init(stoLeavesLruSize) db.maxSnapshots = maxSnapshots db.parallelStateRootComputation = parallelStateRootComputation @@ -108,6 +110,11 @@ proc close*(db: AristoDbRef; wipe = false) = ## ## This distructor may be used on already *destructed* descriptors. ## + db.accLeaves.dispose() + db.stoLeaves.dispose() + db.accLeaves.reset() + db.stoLeaves.reset() + db.closeFn wipe # ------------------------------------------------------------------------------ diff --git a/execution_chain/db/aristo/aristo_init/memory_only.nim b/execution_chain/db/aristo/aristo_init/memory_only.nim index 8d69fe50fb..b553a54f06 100644 --- a/execution_chain/db/aristo/aristo_init/memory_only.nim +++ b/execution_chain/db/aristo/aristo_init/memory_only.nim @@ -1,5 +1,5 @@ # nimbus-eth1 -# Copyright (c) 2023-2025 Status Research & Development GmbH +# Copyright (c) 2023-2026 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) @@ -22,11 +22,14 @@ import # Public database constuctors, destructor # ------------------------------------------------------------------------------ -proc init*(T: type AristoDbRef): T = +proc init*(T: type AristoDbRef, enableCaches: static bool = false): T = ## Memory backend constructor. ## let db = memoryBackend() - db.initInstance()[] + when enableCaches: + db.initInstance(accLeavesLruSize = ACC_LRU_SIZE, stoLeavesLruSize = ACC_LRU_SIZE)[] + else: + db.initInstance(accLeavesLruSize = 0, stoLeavesLruSize = 0)[] db # --+---------------------------------------------------------------------------- diff --git a/execution_chain/db/aristo/aristo_init/persistent.nim b/execution_chain/db/aristo/aristo_init/persistent.nim index 776c9c10e1..d0e96b258e 100644 --- a/execution_chain/db/aristo/aristo_init/persistent.nim +++ b/execution_chain/db/aristo/aristo_init/persistent.nim @@ -44,7 +44,9 @@ proc init*( ): Result[T, AristoError] = let db = rocksDbBackend(opts, baseDb) - db.initInstance(opts.maxSnapshots, opts.parallelStateRootComputation).isOkOr: + db.initInstance(opts.maxSnapshots, opts.parallelStateRootComputation, + accLeavesLruSize = ACC_LRU_SIZE, + stoLeavesLruSize = ACC_LRU_SIZE).isOkOr: db.closeFn(wipe = false) return err(error) diff --git a/execution_chain/db/aristo/aristo_init/rocks_db/rdb_desc.nim b/execution_chain/db/aristo/aristo_init/rocks_db/rdb_desc.nim index a0ba4b543f..918821335e 100644 --- a/execution_chain/db/aristo/aristo_init/rocks_db/rdb_desc.nim +++ b/execution_chain/db/aristo/aristo_init/rocks_db/rdb_desc.nim @@ -16,12 +16,13 @@ import std/concurrency/atomics, stew/endians2, + ../../../../concurrency/lru, ../../../core_db/backend/rocksdb_desc, ../../[aristo_blobify, aristo_desc], - ../init_common, - minilru + ../init_common -export minilru, rocksdb_desc + +export lru, rocksdb_desc const AdmKey* = default(seq[byte]) @@ -54,13 +55,13 @@ type # is less memory and time efficient (the latter one due to internal LRU # handling of the longer key.) # - rdKeyLru*: LruCache[VertexID,HashKey] ## Read cache + rdKeyLru*: ConcurrentLruCache[VertexID,HashKey] ## Read cache rdKeySize*: int - rdVtxLru*: LruCache[VertexID,VertexBuf] ## Read cache + rdVtxLru*: ConcurrentLruCache[VertexID,VertexBuf] ## Read cache rdVtxSize*: int - rdBranchLru*: LruCache[VertexID, (VertexID, uint16)] + rdBranchLru*: ConcurrentLruCache[VertexID, (VertexID, uint16)] rdBranchSize*: int rdbPrintStats*: bool ## Print statistics on closure diff --git a/execution_chain/db/aristo/aristo_init/rocks_db/rdb_init.nim b/execution_chain/db/aristo/aristo_init/rocks_db/rdb_init.nim index 0bf03e1355..ec0968403d 100644 --- a/execution_chain/db/aristo/aristo_init/rocks_db/rdb_init.nim +++ b/execution_chain/db/aristo/aristo_init/rocks_db/rdb_init.nim @@ -97,9 +97,9 @@ proc init*(rdb: var RdbInst, opts: DbOptions, baseDb: RocksDbInstanceRef) = rdb.rdBranchSize = opts.rdbBranchCacheSize div (sizeof(typeof(rdb.rdBranchLru).V) + lruOverhead) - rdb.rdKeyLru = typeof(rdb.rdKeyLru).init(rdb.rdKeySize) - rdb.rdVtxLru = typeof(rdb.rdVtxLru).init(rdb.rdVtxSize) - rdb.rdBranchLru = typeof(rdb.rdBranchLru).init(rdb.rdBranchSize) + rdb.rdKeyLru.init(rdb.rdKeySize) + rdb.rdVtxLru.init(rdb.rdVtxSize) + rdb.rdBranchLru.init(rdb.rdBranchSize) rdb.rdbPrintStats = opts.rdbPrintStats rdb.vtxCol = baseDb.db.getColFamily($VtxCF).valueOr: @@ -107,6 +107,13 @@ proc init*(rdb: var RdbInst, opts: DbOptions, baseDb: RocksDbInstanceRef) = proc close*(rdb: var RdbInst, wipe: bool) = ## Destructor + rdb.rdKeyLru.dispose() + rdb.rdVtxLru.dispose() + rdb.rdBranchLru.dispose() + rdb.rdKeyLru.reset() + rdb.rdVtxLru.reset() + rdb.rdBranchLru.reset() + let ks = rdb.rdKeySize vs = rdb.rdVtxSize diff --git a/execution_chain/db/aristo/aristo_init/rocks_db/rdb_put.nim b/execution_chain/db/aristo/aristo_init/rocks_db/rdb_put.nim index afb014002d..d37d4213e8 100644 --- a/execution_chain/db/aristo/aristo_init/rocks_db/rdb_put.nim +++ b/execution_chain/db/aristo/aristo_init/rocks_db/rdb_put.nim @@ -38,9 +38,15 @@ proc begin*(rdb: var RdbInst): SharedWriteBatchRef = proc rollback*(rdb: var RdbInst, session: SharedWriteBatchRef) = if not session.isClosed(): - rdb.rdKeyLru = typeof(rdb.rdKeyLru).init(rdb.rdKeySize) - rdb.rdVtxLru = typeof(rdb.rdVtxLru).init(rdb.rdVtxSize) - rdb.rdBranchLru = typeof(rdb.rdBranchLru).init(rdb.rdBranchSize) + rdb.rdKeyLru.dispose() + rdb.rdVtxLru.dispose() + rdb.rdBranchLru.dispose() + rdb.rdKeyLru.reset() + rdb.rdVtxLru.reset() + rdb.rdBranchLru.reset() + rdb.rdKeyLru.init(rdb.rdKeySize) + rdb.rdVtxLru.init(rdb.rdVtxSize) + rdb.rdBranchLru.init(rdb.rdBranchSize) session.close() proc commit*(rdb: var RdbInst, session: SharedWriteBatchRef): Result[void,(AristoError,string)] = diff --git a/execution_chain/db/core_db/backend/aristo_memory.nim b/execution_chain/db/core_db/backend/aristo_memory.nim index 3de4e47d64..b03557bb32 100644 --- a/execution_chain/db/core_db/backend/aristo_memory.nim +++ b/execution_chain/db/core_db/backend/aristo_memory.nim @@ -23,8 +23,8 @@ export base_desc # Public constructors # ------------------------------------------------------------------------------ -proc newMemoryCoreDbRef*(): CoreDbRef = - CoreDbRef(mpt: AristoDbRef.init(), kvt: KvtDbRef.init()) +proc newMemoryCoreDbRef*(enableCaches: static bool): CoreDbRef = + CoreDbRef(mpt: AristoDbRef.init(enableCaches), kvt: KvtDbRef.init()) # ------------------------------------------------------------------------------ # End diff --git a/execution_chain/db/core_db/base.nim b/execution_chain/db/core_db/base.nim index 59729d7ae2..e2760f4d1c 100644 --- a/execution_chain/db/core_db/base.nim +++ b/execution_chain/db/core_db/base.nim @@ -498,6 +498,11 @@ proc txFrameBegin*( CoreDbTxRef(kTx: kTx, aTx: aTx) +proc parent*(tx: CoreDbTxRef): CoreDbTxRef = + assert not tx.kTx.parent.isNil() + assert not tx.aTx.parent.isNil() + CoreDbTxRef(kTx: tx.kTx.parent, aTx: tx.aTx.parent) + proc checkpoint*(tx: CoreDbTxRef, blockNumber: BlockNumber, skipSnapshot = false) = tx.aTx.checkpoint(blockNumber, skipSnapshot) diff --git a/execution_chain/db/core_db/memory_only.nim b/execution_chain/db/core_db/memory_only.nim index deae25ef6d..729713f1fd 100644 --- a/execution_chain/db/core_db/memory_only.nim +++ b/execution_chain/db/core_db/memory_only.nim @@ -26,6 +26,7 @@ export proc newCoreDbRef*( dbType: static[CoreDbType]; # Database type symbol + enableCaches: static bool = false ): CoreDbRef = ## Constructor for volatile/memory type DB ## @@ -33,7 +34,7 @@ proc newCoreDbRef*( ## `CoreDbRef.init()` because of compiler coughing. ## when dbType == AristoDbMemory: - newMemoryCoreDbRef() + newMemoryCoreDbRef(enableCaches) else: {.error: "Unsupported constructor " & $dbType & ".newCoreDbRef()".} diff --git a/execution_chain/db/opts.nim b/execution_chain/db/opts.nim index aff268d96f..72b822559f 100644 --- a/execution_chain/db/opts.nim +++ b/execution_chain/db/opts.nim @@ -72,7 +72,7 @@ func init*( rdbBranchCacheSize = defaultRdbBranchCacheSize, rdbPrintStats = false, maxSnapshots = defaultMaxSnapshots, - parallelStateRootComputation = false, + parallelStateRootComputation = true, blockCacheType = defaultBlockCacheType, ): T = T( diff --git a/execution_chain/evm/async_evm.nim b/execution_chain/evm/async_evm.nim index 20e6780bf9..e268a92834 100644 --- a/execution_chain/evm/async_evm.nim +++ b/execution_chain/evm/async_evm.nim @@ -111,6 +111,9 @@ proc init*( AsyncEvm(com: com, backend: backend) +proc dispose*(evm: AsyncEvm) = + evm.com.db.close() + template toCallResult(evmResult: EvmResult[CallResult]): Result[CallResult, string] = let callResult = ?evmResult.mapErr( diff --git a/execution_chain/evm/state.nim b/execution_chain/evm/state.nim index 76c0360e02..c666d764f7 100644 --- a/execution_chain/evm/state.nim +++ b/execution_chain/evm/state.nim @@ -23,7 +23,7 @@ import func forkDeterminationInfoForVMState(vmState: BaseVMState): ForkDeterminationInfo = forkDeterminationInfo(vmState.parent.number + 1, vmState.blockCtx.timestamp) -func determineFork(vmState: BaseVMState): HardFork = +func determineFork*(vmState: BaseVMState): HardFork = vmState.com.toHardFork(vmState.forkDeterminationInfoForVMState) proc init( diff --git a/execution_chain/nimbus_execution_client.nim b/execution_chain/nimbus_execution_client.nim index 74f803dab2..e728db9e29 100644 --- a/execution_chain/nimbus_execution_client.nim +++ b/execution_chain/nimbus_execution_client.nim @@ -287,7 +287,8 @@ proc setupCommonRef*(config: ExecutionClientConf): (CommonRef, bool) = networkId = config.networkId, params = config.networkParams, statelessProviderEnabled = config.statelessProviderEnabled, - statelessWitnessValidation = config.statelessWitnessValidation) + statelessWitnessValidation = config.statelessWitnessValidation, + optimisticStatePrefetch = config.optimisticStatePrefetch) if config.extraData.len > 32: warn "ExtraData exceeds 32 bytes limit, truncate", diff --git a/execution_chain/stateless/stateless_execution.nim b/execution_chain/stateless/stateless_execution.nim index 490b634ebc..8c767676de 100644 --- a/execution_chain/stateless/stateless_execution.nim +++ b/execution_chain/stateless/stateless_execution.nim @@ -46,6 +46,8 @@ proc statelessProcessBlock*( let memoryDb = newCoreDbRef(DefaultDbMemory) memoryTxFrame = memoryDb.baseTxFrame() + defer: + memoryDb.close() # Load the subtrie of trie nodes (both account and storage tries) into the # in memory database. diff --git a/execution_chain/transaction/call_common.nim b/execution_chain/transaction/call_common.nim index 1428ed90b1..7d1ae5f580 100644 --- a/execution_chain/transaction/call_common.nim +++ b/execution_chain/transaction/call_common.nim @@ -313,6 +313,8 @@ proc finishRunningComputation( result.blockStateGasUsed = gasUsed.blockStateGasUsed if c.isSuccess: result.logEntries = move(c.logEntries) + elif T is VoidResult: + discard else: {.error: "Unknown computation output".} diff --git a/execution_chain/transaction/call_evm.nim b/execution_chain/transaction/call_evm.nim index ea7121c50d..7f176a1489 100644 --- a/execution_chain/transaction/call_evm.nim +++ b/execution_chain/transaction/call_evm.nim @@ -48,11 +48,15 @@ proc callParamsForTx(tx: Transaction, sender: Address, proc txCallEvm*(tx: Transaction, sender: Address, vmState: BaseVMState, - intrinsic: IntrinsicGas): LogResult = + intrinsic: IntrinsicGas, + discardResult: static bool = false): auto = let baseFee = vmState.blockCtx.baseFeePerGas call = callParamsForTx(tx, sender, vmState, baseFee, intrinsic) - runComputation(call, LogResult) + when discardResult: + discard runComputation(call, VoidResult) + else: + runComputation(call, LogResult) proc testCallEvm*(tx: Transaction, sender: Address, diff --git a/execution_chain/transaction/call_types.nim b/execution_chain/transaction/call_types.nim index d36c30eeef..aa2d269ae7 100644 --- a/execution_chain/transaction/call_types.nim +++ b/execution_chain/transaction/call_types.nim @@ -59,6 +59,8 @@ type error*: string output*: seq[byte] + VoidResult* = object + IntrinsicGas* = object regular*: GasInt state*: GasInt diff --git a/nimbus_verified_proxy/engine/rpc_frontend.nim b/nimbus_verified_proxy/engine/rpc_frontend.nim index 98bb6f8e9e..0bd329c8ca 100644 --- a/nimbus_verified_proxy/engine/rpc_frontend.nim +++ b/nimbus_verified_proxy/engine/rpc_frontend.nim @@ -488,8 +488,12 @@ proc getExecutionApiFrontend*(engine: RpcVerificationEngine): ExecutionApiFronte .} = engine.beaconSync() + let db = DefaultDbMemory.newCoreDbRef() + defer: + db.close() + let com = CommonRef.new( - DefaultDbMemory.newCoreDbRef(), + db, config = chainConfigForNetwork(engine.chainId), initializeDb = false, statelessProviderEnabled = true, # Enables collection of witness keys diff --git a/tests/eest/eest_blockchain.nim b/tests/eest/eest_blockchain.nim index 177ac8e300..fe3af60f48 100644 --- a/tests/eest/eest_blockchain.nim +++ b/tests/eest/eest_blockchain.nim @@ -229,7 +229,7 @@ proc runTest(env: TestEnv, unit: BlockchainUnitEnv, statelessEnabled = false): F ok() -proc processFile*(filePath: string, statelessEnabled = false, skipFiles: seq[string] = @[]) = +proc processFile*(filePath: string, statelessEnabled = false, parallelEnabled = false, skipFiles: seq[string] = @[]) = let fixture = parseFixture(filePath, BlockchainFixture) let fileName = filePath.splitPath().tail @@ -243,7 +243,7 @@ proc processFile*(filePath: string, statelessEnabled = false, skipFiles: seq[str else: let header = testUnit.genesisBlockHeader.to(Header) check testUnit.genesisBlockHeader.hash == header.computeRlpHash - let env = prepareEnv(testUnit, header, rpcEnabled = false, statelessEnabled) + let env = prepareEnv(testUnit, header, rpcEnabled = false, statelessEnabled, parallelEnabled) let testResult = waitFor env.runTest(testUnit, statelessEnabled) check testResult == Result[void, string].ok() @@ -258,4 +258,4 @@ when isMainModule: echo "Usage: " & testFile & " vector.json" quit(QuitFailure) - processFile(paramStr(1), true) + processFile(paramStr(1), statelessEnabled = true) diff --git a/tests/eest/eest_blockchain_test.nim b/tests/eest/eest_blockchain_test.nim index 7469e804f1..91c47962da 100644 --- a/tests/eest/eest_blockchain_test.nim +++ b/tests/eest/eest_blockchain_test.nim @@ -29,5 +29,6 @@ runEESTSuite( eestReleases, skipFiles, baseFolder, - eestType + eestType, + parallelEnabled = true ) diff --git a/tests/eest/eest_engine.nim b/tests/eest/eest_engine.nim index 6c49ef4a34..5af5a648cb 100644 --- a/tests/eest/eest_engine.nim +++ b/tests/eest/eest_engine.nim @@ -117,7 +117,7 @@ proc runTest(env: TestEnv, unit: EngineUnitEnv): Result[void, string] = ok() -proc processFile*(filePath: string, statelessEnabled = false, skipFiles: seq[string] = @[]) = +proc processFile*(filePath: string, statelessEnabled = false, parallelEnabled = false, skipFiles: seq[string] = @[]) = let fixture = parseFixture(filePath, EngineFixture) let fileName = filePath.splitPath().tail @@ -131,7 +131,7 @@ proc processFile*(filePath: string, statelessEnabled = false, skipFiles: seq[str else: let header = testUnit.genesisBlockHeader.to(Header) check testUnit.genesisBlockHeader.hash == header.computeRlpHash - let env = prepareEnv(testUnit, header, rpcEnabled = true, statelessEnabled) + let env = prepareEnv(testUnit, header, rpcEnabled = true, statelessEnabled, parallelEnabled) let testResult = env.runTest(testUnit) check testResult == Result[void, string].ok() diff --git a/tests/eest/eest_engine_test.nim b/tests/eest/eest_engine_test.nim index 0a22362f81..4c7885d83c 100644 --- a/tests/eest/eest_engine_test.nim +++ b/tests/eest/eest_engine_test.nim @@ -31,5 +31,6 @@ runEESTSuite( eestReleases, skipFiles, baseFolder, - eestType + eestType, + parallelEnabled = true ) diff --git a/tests/eest/eest_helpers.nim b/tests/eest/eest_helpers.nim index d1830ba58c..0d283ff1c6 100644 --- a/tests/eest/eest_helpers.nim +++ b/tests/eest/eest_helpers.nim @@ -10,7 +10,7 @@ {.push raises: [].} import - std/[json, strutils], + std/[json, strutils, cpuinfo], unittest2, eth/common/headers_rlp, web3/eth_api_types, @@ -21,6 +21,7 @@ import json_rpc/rpcclient, json_rpc/rpcserver, kzg4844/kzg, + taskpools, ./chain_config_wrapper, ./path_handler, ../../execution_chain/rpc, @@ -107,6 +108,7 @@ type chain*: ForkedChainRef server*: Opt[RpcHttpServer] client*: Opt[RpcHttpClient] + taskpool*: Taskpool ## Blockchain Test Types BlockchainUnitEnv* = object of UnitEnv @@ -234,11 +236,12 @@ proc prepareEnv*( unit: UnitEnv, genesis: Header, rpcEnabled = false, - statelessEnabled = false): TestEnv = + statelessEnabled = false, + parallelEnabled = false): TestEnv = try: let - memDB = newCoreDbRef DefaultDbMemory + memDB = newCoreDbRef(DefaultDbMemory, enableCaches = true) ledger = LedgerRef.init(memDB.baseTxFrame()) config = getChainConfig(unit.network) @@ -257,8 +260,21 @@ proc prepareEnv*( let com = CommonRef.new(memDB, config, statelessProviderEnabled = statelessEnabled, - statelessWitnessValidation = false) # Running stateless execution separately in test runner - chain = ForkedChainRef.init(com, enableQueue = true, persistBatchSize = 1) + statelessWitnessValidation = false, # Running stateless execution separately in test runner + optimisticStatePrefetch = parallelEnabled) + + if parallelEnabled: + let taskpool = + try: + Taskpool.new(numThreads = min(countProcessors(), 16)) + except CatchableError as exc: + debugEcho "Failed to start taskpool: ", exc.msg + quit(QuitFailure) + com.taskpool = taskpool + com.db.mpt.taskpool = taskpool + testEnv.taskpool = taskpool + + let chain = ForkedChainRef.init(com, enableQueue = true, persistBatchSize = 1) testEnv.chain = chain testEnv.client = Opt.none(RpcHttpClient) @@ -295,6 +311,9 @@ proc close*(env: TestEnv) = if env.server.isSome: waitFor env.server.get().closeWait() waitFor env.chain.stopProcessingQueue() + env.chain.com.db.close() + if env.taskpool != nil: + env.taskpool.shutdown() except CatchableError as exc: debugEcho "Close error: ", exc.msg quit(QuitFailure) @@ -323,9 +342,10 @@ template runEESTSuite*( skipFiles: openArray[string], baseFolder: string, eestType: string, - statelessEnabled = false + statelessEnabled = false, + parallelEnabled = false ) = for eest in eestReleases: suite eest & ": " & eestType: for filePath in walkDirRec(baseFolder / eest / eestType): - processFile(handleLongPath(filePath), statelessEnabled, @skipFiles) + processFile(handleLongPath(filePath), statelessEnabled, parallelEnabled, @skipFiles) diff --git a/tests/eest/eest_stateless_execution_test.nim b/tests/eest/eest_stateless_execution_test.nim index 9d0f9a2cdd..1f1f764d67 100644 --- a/tests/eest/eest_stateless_execution_test.nim +++ b/tests/eest/eest_stateless_execution_test.nim @@ -54,5 +54,6 @@ runEESTSuite( skipFiles, baseFolder, eestType, - statelessEnabled = true + statelessEnabled = true, + parallelEnabled = false ) diff --git a/tests/test_concurrency.nim b/tests/test_concurrency.nim index 09f34f57d8..b764e54d90 100644 --- a/tests/test_concurrency.nim +++ b/tests/test_concurrency.nim @@ -8,4 +8,7 @@ # at your option. This file may not be copied, modified, or # distributed except according to those terms. -import ./test_concurrency/[test_lru, test_queue, test_readwritelock, test_semaphore] \ No newline at end of file +import + ./test_concurrency/[ + test_lru, test_queue, test_readwritelock, test_semaphore, test_utils + ] \ No newline at end of file diff --git a/tests/test_concurrency/test_utils.nim b/tests/test_concurrency/test_utils.nim new file mode 100644 index 0000000000..925f0e276f --- /dev/null +++ b/tests/test_concurrency/test_utils.nim @@ -0,0 +1,191 @@ +# Nimbus +# Copyright (c) 2026 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. + +{.used.} + +import unittest2, ../../execution_chain/concurrency/utils + +proc getRefcount(p: pointer): int {.importc: "getRefcount".} + +template rc(x: ref): int = + getRefcount(cast[pointer](x)) + +type + Payload = object + value: int + text: string + + Container = ref object + inner: ref Payload + +suite "Concurrency Utils Tests": + + test "borrowRef copies the pointer to the destination": + var src: ref Payload + new(src) + src.value = 42 + src.text = "hello" + + var dest: ref Payload + dest.borrowRef(src) + + check: + dest != nil + cast[pointer](dest) == cast[pointer](src) + dest.value == 42 + dest.text == "hello" + + dest.unborrowRef() + + test "unborrowRef clears the destination back to nil": + var src: ref Payload + new(src) + src.value = 7 + + var dest: ref Payload + dest.borrowRef(src) + check dest != nil + + dest.unborrowRef() + check: + dest == nil + src != nil + src.value == 7 + + test "mutations through borrowed ref are visible via source": + var src: ref Payload + new(src) + src.value = 1 + + var dest: ref Payload + dest.borrowRef(src) + dest.value = 99 + dest.text = "mutated" + + check: + src.value == 99 + src.text == "mutated" + + dest.unborrowRef() + + test "borrowRef into a field of a heap-allocated object": + var src: ref Payload + new(src) + src.value = 555 + + let container = Container() + container.inner.borrowRef(src) + + check: + container.inner != nil + cast[pointer](container.inner) == cast[pointer](src) + container.inner.value == 555 + + container.inner.unborrowRef() + check container.inner == nil + + test "repeated borrow and unborrow cycles": + var src: ref Payload + new(src) + src.value = 10 + + var dest: ref Payload + for i in 0 ..< 16: + dest.borrowRef(src) + check: + dest != nil + dest.value == 10 + dest.unborrowRef() + check dest == nil + + check: + src != nil + src.value == 10 + + test "rebinding a borrowed ref to a different source": + var srcA, srcB: ref Payload + new(srcA) + new(srcB) + srcA.value = 1 + srcB.value = 2 + + var dest: ref Payload + dest.borrowRef(srcA) + check cast[pointer](dest) == cast[pointer](srcA) + + dest.unborrowRef() + dest.borrowRef(srcB) + check: + cast[pointer](dest) == cast[pointer](srcB) + dest.value == 2 + + dest.unborrowRef() + + test "borrowRef does not change the source refcount": + var src: ref Payload + new(src) + let before = rc(src) + + var dest: ref Payload + dest.borrowRef(src) + check: + rc(src) == before + rc(dest) == before + + dest.unborrowRef() + + test "unborrowRef does not change the source refcount": + var src: ref Payload + new(src) + let before = rc(src) + + var dest: ref Payload + dest.borrowRef(src) + dest.unborrowRef() + + check rc(src) == before + + test "normal ref assignment to a heap field bumps the refcount (control)": + var src: ref Payload + new(src) + let before = rc(src) + + let container = Container() + container.inner = src + check rc(src) == before + 1 + + container.inner = nil + check rc(src) == before + + test "borrowRef into a heap field does not change the source refcount": + var src: ref Payload + new(src) + let before = rc(src) + + let container = Container() + container.inner.borrowRef(src) + check: + rc(src) == before + rc(container.inner) == before + + container.inner.unborrowRef() + check rc(src) == before + + test "repeated borrow/unborrow cycles leave the refcount unchanged": + var src: ref Payload + new(src) + let before = rc(src) + + var dest: ref Payload + for i in 0 ..< 16: + dest.borrowRef(src) + check rc(src) == before + dest.unborrowRef() + check rc(src) == before diff --git a/tests/test_generalstate_json.nim b/tests/test_generalstate_json.nim index 58f55f902e..4754da08e2 100644 --- a/tests/test_generalstate_json.nim +++ b/tests/test_generalstate_json.nim @@ -6,7 +6,7 @@ # at your option. This file may not be copied, modified, or distributed except according to those terms. import - std/[strutils, tables, json, os, sets], + std/[strutils, tables, json, os, sets, cpuinfo], ./test_helpers, ./test_allowed_to_fail, ../execution_chain/core/executor, test_config, ../execution_chain/transaction, @@ -21,6 +21,7 @@ import ../tools/common/state_clearing, eth/common/transaction_utils, kzg4844/kzg, + taskpools, unittest2, stew/byteutils, results @@ -36,6 +37,13 @@ block: createThread(t, loadKzgSetup) joinThread(t) +let taskpool = + try: + Taskpool.new(numThreads = min(countProcessors(), 16)) + except CatchableError as exc: + echo "Failed to start taskpool: ", exc.msg + quit(QuitFailure) + type TestCtx = object name: string @@ -105,7 +113,12 @@ proc dumpDebugData(ctx: TestCtx, vmState: BaseVMState, gasUsed: GasInt, logs: op proc testFixtureIndexes(ctx: var TestCtx, testStatusIMPL: var TestStatus) = let - com = CommonRef.new(newCoreDbRef DefaultDbMemory, ctx.chainConfig) + com = CommonRef.new(newCoreDbRef DefaultDbMemory, ctx.chainConfig, + optimisticStatePrefetch = true) + com.taskpool = taskpool + com.db.mpt.taskpool = taskpool + + let parent = Header(stateRoot: emptyRoot) tracer = if ctx.trace: newLegacyTracer({})