nimbus-eth1/nimbus/evm/state.nim
Jacek Sieka 0b32078c4b
Consolidate block type for block processing (#2325)
This PR consolidates the split header-body sequences into a single EthBlock
sequence and cleans up the fallout from that which significantly reduces
block processing overhead during import thanks to less garbage collection
and fewer copies of things all around.

Notably, since the number of headers must always match the number of bodies,
we also get rid of a pointless degree of freedom that in the future could
introduce unnecessary bugs.

* only read header and body from era file
* avoid several unnecessary copies along the block processing way
* simplify signatures, cleaning up unused arguemnts and returns
* use `stew/assign2` in a few strategic places where the generated
  nim assignent is slow and add a few `move` to work around poor
  analysis in nim 1.6 (will need to be revisited for 2.0)

```
stats-20240607_2223-a814aa0b.csv vs stats-20240608_0714-21c1d0a9.csv
                       bps_x     bps_y     tps_x        tps_y    bpsd    tpsd    timed
block_number
(498305, 713245]    1,540.52  1,809.73  2,361.58  2775.340189  17.63%  17.63%  -14.92%
(713245, 928185]      730.36    865.26  1,715.90  2028.973852  18.01%  18.01%  -15.21%
(928185, 1143126]     663.03    789.10  2,529.26  3032.490771  19.79%  19.79%  -16.28%
(1143126, 1358066]    393.46    508.05  2,152.50  2777.578119  29.13%  29.13%  -22.50%
(1358066, 1573007]    370.88    440.72  2,351.31  2791.896052  18.81%  18.81%  -15.80%
(1573007, 1787947]    283.65    335.11  2,068.93  2441.373402  17.60%  17.60%  -14.91%
(1787947, 2002888]    287.29    342.11  2,078.39  2474.179448  18.99%  18.99%  -15.91%
(2002888, 2217828]    293.38    343.16  2,208.83   2584.77457  17.16%  17.16%  -14.61%
(2217828, 2432769]    140.09    167.86  1,081.87  1296.336926  18.82%  18.82%  -15.80%

blocks: 1934464, baseline: 3h13m1s, contender: 2h43m47s
bpsd (mean): 19.55%
tpsd (mean): 19.55%
Time (total): -29m13s, -15.14%
```
2024-06-09 16:32:20 +02:00

347 lines
12 KiB
Nim

# Nimbus
# Copyright (c) 2018-2024 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
# http://www.apache.org/licenses/LICENSE-2.0)
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or
# http://opensource.org/licenses/MIT)
# at your option. This file may not be copied, modified, or distributed except
# according to those terms.
{.push raises: [].}
import
std/[options, sets, strformat],
stew/assign2,
eth/keys,
../db/ledger,
../common/[common, evmforks],
./interpreter/[op_codes, gas_costs],
./types,
./evm_errors
proc init(
self: BaseVMState;
ac: LedgerRef,
parent: BlockHeader;
blockCtx: BlockContext;
com: CommonRef;
tracer: TracerRef,
flags: set[VMFlag] = self.flags) =
## Initialisation helper
assign(self.parent, parent)
self.blockCtx = blockCtx
self.gasPool = blockCtx.gasLimit
self.com = com
self.tracer = tracer
self.stateDB = ac
self.flags = flags
func blockCtx(com: CommonRef, header: BlockHeader):
BlockContext =
BlockContext(
timestamp : header.timestamp,
gasLimit : header.gasLimit,
fee : header.fee,
prevRandao : header.prevRandao,
difficulty : header.difficulty,
coinbase : com.minerAddress(header),
excessBlobGas: header.excessBlobGas.get(0'u64),
)
# --------------
proc `$`*(vmState: BaseVMState): string
{.gcsafe, raises: [ValueError].} =
if vmState.isNil:
result = "nil"
else:
result = &"VMState:"&
&"\n blockNumber: {vmState.parent.blockNumber + 1}"
proc new*(
T: type BaseVMState;
parent: BlockHeader; ## parent header, account sync position
blockCtx: BlockContext;
com: CommonRef; ## block chain config
tracer: TracerRef = nil): T =
## Create a new `BaseVMState` descriptor from a parent block header. This
## function internally constructs a new account state cache rooted at
## `parent.stateRoot`
##
## This `new()` constructor and its variants (see below) provide a save
## `BaseVMState` environment where the account state cache is synchronised
## with the `parent` block header.
new result
result.init(
ac = LedgerRef.init(com.db, parent.stateRoot),
parent = parent,
blockCtx = blockCtx,
com = com,
tracer = tracer)
proc reinit*(self: BaseVMState; ## Object descriptor
parent: BlockHeader; ## parent header, account sync pos.
blockCtx: BlockContext
): bool =
## Re-initialise state descriptor. The `LedgerRef` database is
## re-initilaise only if its `rootHash` doe not point to `parent.stateRoot`,
## already. Accumulated state data are reset.
##
## This function returns `true` unless the `LedgerRef` database could be
## queries about its `rootHash`, i.e. `isTopLevelClean` evaluated `true`. If
## this function returns `false`, the function argument `self` is left
## untouched.
if self.stateDB.isTopLevelClean:
let
tracer = self.tracer
com = self.com
db = com.db
ac = if self.stateDB.rootHash == parent.stateRoot: self.stateDB
else: LedgerRef.init(db, parent.stateRoot)
flags = self.flags
self[].reset
self.init(
ac = ac,
parent = parent,
blockCtx = blockCtx,
com = com,
tracer = tracer,
flags = flags)
return true
# else: false
proc reinit*(self: BaseVMState; ## Object descriptor
parent: BlockHeader; ## parent header, account sync pos.
header: BlockHeader; ## header with tx environment data fields
): bool =
## Variant of `reinit()`. The `parent` argument is used to sync the accounts
## cache and the `header` is used as a container to pass the `timestamp`,
## `gasLimit`, and `fee` values.
##
## It requires the `header` argument properly initalised so that for PoA
## networks, the miner address is retrievable via `ecRecover()`.
result = self.reinit(
parent = parent,
blockCtx = self.com.blockCtx(header),
)
proc reinit*(self: BaseVMState; ## Object descriptor
header: BlockHeader; ## header with tx environment data fields
): bool =
## This is a variant of the `reinit()` function above where the field
## `header.parentHash`, is used to fetch the `parent` BlockHeader to be
## used in the `update()` variant, above.
var parent: BlockHeader
if self.com.db.getBlockHeader(header.parentHash, parent):
return self.reinit(
parent = parent,
header = header)
proc init*(
self: BaseVMState; ## Object descriptor
parent: BlockHeader; ## parent header, account sync position
header: BlockHeader; ## header with tx environment data fields
com: CommonRef; ## block chain config
tracer: TracerRef = nil) =
## Variant of `new()` constructor above for in-place initalisation. The
## `parent` argument is used to sync the accounts cache and the `header`
## is used as a container to pass the `timestamp`, `gasLimit`, and `fee`
## values.
##
## It requires the `header` argument properly initalised so that for PoA
## networks, the miner address is retrievable via `ecRecover()`.
self.init(
ac = LedgerRef.init(com.db, parent.stateRoot),
parent = parent,
blockCtx = com.blockCtx(header),
com = com,
tracer = tracer)
proc new*(
T: type BaseVMState;
parent: BlockHeader; ## parent header, account sync position
header: BlockHeader; ## header with tx environment data fields
com: CommonRef; ## block chain config
tracer: TracerRef = nil): T =
## This is a variant of the `new()` constructor above where the `parent`
## argument is used to sync the accounts cache and the `header` is used
## as a container to pass the `timestamp`, `gasLimit`, and `fee` values.
##
## It requires the `header` argument properly initalised so that for PoA
## networks, the miner address is retrievable via `ecRecover()`.
new result
result.init(
parent = parent,
header = header,
com = com,
tracer = tracer)
proc new*(
T: type BaseVMState;
header: BlockHeader; ## header with tx environment data fields
com: CommonRef; ## block chain config
tracer: TracerRef = nil): EvmResult[T] =
## This is a variant of the `new()` constructor above where the field
## `header.parentHash`, is used to fetch the `parent` BlockHeader to be
## used in the `new()` variant, above.
var parent: BlockHeader
if com.db.getBlockHeader(header.parentHash, parent):
ok(BaseVMState.new(
parent = parent,
header = header,
com = com,
tracer = tracer))
else:
err(evmErr(EvmHeaderNotFound))
proc init*(
vmState: BaseVMState;
header: BlockHeader; ## header with tx environment data fields
com: CommonRef; ## block chain config
tracer: TracerRef = nil): bool =
## Variant of `new()` which does not throw an exception on a dangling
## `BlockHeader` parent hash reference.
var parent: BlockHeader
if com.db.getBlockHeader(header.parentHash, parent):
vmState.init(
parent = parent,
header = header,
com = com,
tracer = tracer)
return true
proc coinbase*(vmState: BaseVMState): EthAddress =
vmState.blockCtx.coinbase
proc blockNumber*(vmState: BaseVMState): BlockNumber =
# it should return current block number
# and not head.blockNumber
vmState.parent.blockNumber + 1
proc difficultyOrPrevRandao*(vmState: BaseVMState): UInt256 =
if vmState.com.consensus == ConsensusType.POS:
# EIP-4399/EIP-3675
UInt256.fromBytesBE(vmState.blockCtx.prevRandao.data)
else:
vmState.blockCtx.difficulty
proc baseFee*(vmState: BaseVMState): UInt256 =
vmState.blockCtx.fee.get(0.u256)
method getAncestorHash*(
vmState: BaseVMState, blockNumber: BlockNumber): Hash256 {.base.} =
let db = vmState.com.db
try:
var blockHash: Hash256
if db.getBlockHash(blockNumber, blockHash):
blockHash
else:
Hash256()
except RlpError:
Hash256()
proc readOnlyStateDB*(vmState: BaseVMState): ReadOnlyStateDB {.inline.} =
ReadOnlyStateDB(vmState.stateDB)
template mutateStateDB*(vmState: BaseVMState, body: untyped) =
block:
var db {.inject.} = vmState.stateDB
body
proc getAndClearLogEntries*(vmState: BaseVMState): seq[Log] =
vmState.stateDB.getAndClearLogEntries()
proc status*(vmState: BaseVMState): bool =
ExecutionOK in vmState.flags
proc `status=`*(vmState: BaseVMState, status: bool) =
if status: vmState.flags.incl ExecutionOK
else: vmState.flags.excl ExecutionOK
proc collectWitnessData*(vmState: BaseVMState): bool =
CollectWitnessData in vmState.flags
proc `collectWitnessData=`*(vmState: BaseVMState, status: bool) =
if status: vmState.flags.incl CollectWitnessData
else: vmState.flags.excl CollectWitnessData
func forkDeterminationInfoForVMState*(vmState: BaseVMState): ForkDeterminationInfo =
# FIXME-Adam: Is this timestamp right? Note that up above in blockNumber we add 1;
# should timestamp be adding 12 or something?
# Also, can I get the TD? Do I need to?
forkDeterminationInfo(vmState.blockNumber, vmState.blockCtx.timestamp)
func determineFork*(vmState: BaseVMState): EVMFork =
vmState.com.toEVMFork(vmState.forkDeterminationInfoForVMState)
func tracingEnabled*(vmState: BaseVMState): bool =
vmState.tracer.isNil.not
proc captureTxStart*(vmState: BaseVMState, gasLimit: GasInt) =
if vmState.tracingEnabled:
vmState.tracer.captureTxStart(gasLimit)
proc captureTxEnd*(vmState: BaseVMState, restGas: GasInt) =
if vmState.tracingEnabled:
vmState.tracer.captureTxEnd(restGas)
proc captureStart*(vmState: BaseVMState, comp: Computation,
sender: EthAddress, to: EthAddress,
create: bool, input: openArray[byte],
gasLimit: GasInt, value: UInt256) =
if vmState.tracingEnabled:
vmState.tracer.captureStart(comp, sender, to, create, input, gasLimit, value)
proc captureEnd*(vmState: BaseVMState, comp: Computation, output: openArray[byte],
gasUsed: GasInt, error: Opt[string]) =
if vmState.tracingEnabled:
vmState.tracer.captureEnd(comp, output, gasUsed, error)
proc captureEnter*(vmState: BaseVMState, comp: Computation, op: Op,
sender: EthAddress, to: EthAddress,
input: openArray[byte], gasLimit: GasInt,
value: UInt256) =
if vmState.tracingEnabled:
vmState.tracer.captureEnter(comp, op, sender, to, input, gasLimit, value)
proc captureExit*(vmState: BaseVMState, comp: Computation, output: openArray[byte],
gasUsed: GasInt, error: Opt[string]) =
if vmState.tracingEnabled:
vmState.tracer.captureExit(comp, output, gasUsed, error)
proc captureOpStart*(vmState: BaseVMState, comp: Computation, pc: int,
op: Op, gas: GasInt,
depth: int): int =
if vmState.tracingEnabled:
let fixed = vmState.gasCosts[op].kind == GckFixed
result = vmState.tracer.captureOpStart(comp, fixed, pc, op, gas, depth)
proc captureGasCost*(vmState: BaseVMState,
comp: Computation,
op: Op, gasCost: GasInt, gasRemaining: GasInt,
depth: int) =
if vmState.tracingEnabled:
let fixed = vmState.gasCosts[op].kind == GckFixed
vmState.tracer.captureGasCost(comp, fixed, op, gasCost, gasRemaining, depth)
proc captureOpEnd*(vmState: BaseVMState, comp: Computation, pc: int,
op: Op, gas: GasInt, refund: GasInt,
rData: openArray[byte],
depth: int, opIndex: int) =
if vmState.tracingEnabled:
let fixed = vmState.gasCosts[op].kind == GckFixed
vmState.tracer.captureOpEnd(comp, fixed, pc, op, gas, refund, rData, depth, opIndex)
proc captureFault*(vmState: BaseVMState, comp: Computation, pc: int,
op: Op, gas: GasInt, refund: GasInt,
rData: openArray[byte],
depth: int, error: Opt[string]) =
if vmState.tracingEnabled:
let fixed = vmState.gasCosts[op].kind == GckFixed
vmState.tracer.captureFault(comp, fixed, pc, op, gas, refund, rData, depth, error)
proc capturePrepare*(vmState: BaseVMState, comp: Computation, depth: int) =
if vmState.tracingEnabled:
vmState.tracer.capturePrepare(comp, depth)