nimbus-eth1/nimbus/vm/state.nim
Jordan Hrycaj 261c0b51a7
Redesign of BaseVMState descriptor (#923)
* Redesign of BaseVMState descriptor

why:
  BaseVMState provides an environment for executing transactions. The
  current descriptor also provides data that cannot generally be known
  within the execution environment, e.g. the total gasUsed which is
  available not before after all transactions have finished.

  Also, the BaseVMState constructor has been replaced by a constructor
  that does not need pre-initialised input of the account database.

also:
  Previous constructor and some fields are provided with a deprecated
  annotation (producing a lot of noise.)

* Replace legacy directives in production sources

* Replace legacy directives in unit test sources

* fix CI (missing premix update)

* Remove legacy directives

* chase CI problem

* rebased

* Re-introduce 'AccountsCache' constructor optimisation for 'BaseVmState' re-initialisation

why:
  Constructing a new 'AccountsCache' descriptor can be avoided sometimes
  when the current state root is properly positioned already. Such a
  feature existed already as the update function 'initStateDB()' for the
  'BaseChanDB' where the accounts cache was linked into this desctiptor.

  The function 'initStateDB()' was removed and re-implemented into the
  'BaseVmState' constructor without optimisation. The old version was of
  restricted use as a wrong accounts cache state would unconditionally
  throw an exception rather than conceptually ask for a remedy.

  The optimised 'BaseVmState' re-initialisation has been implemented for
  the 'persistBlocks()' function.

also:
  moved some test helpers to 'test/replay' folder

* Remove unused & undocumented fields from Chain descriptor

why:
  Reduces attack surface in general & improves reading the code.
2022-01-18 16:19:32 +00:00

394 lines
14 KiB
Nim

# Nimbus
# Copyright (c) 2018 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.
import
std/[json, macros, options, sets, strformat, tables],
../../stateless/[witness_from_tree, witness_types],
../chain_config,
../constants,
../db/[db_chain, accounts_cache],
../errors,
../forks,
../utils/[difficulty, ec_recover],
./interpreter/gas_costs,
./transaction_tracer,
./types,
eth/[common, keys]
{.push raises: [Defect].}
const
nilHash = block:
var rc: Hash256
rc
template safeExecutor(info: string; code: untyped) =
try:
code
except CatchableError as e:
raise (ref CatchableError)(msg: e.msg)
except Defect as e:
raise (ref Defect)(msg: e.msg)
except:
let e = getCurrentException()
raise newException(VmStateError, info & "(): " & $e.name & " -- " & e.msg)
proc getMinerAddress(chainDB: BaseChainDB; header: BlockHeader): EthAddress
{.gcsafe, raises: [Defect,CatchableError].} =
if not chainDB.config.poaEngine:
return header.coinbase
let account = header.ecRecover
if account.isErr:
let msg = "Could not recover account address: " & $account.error
raise newException(ValidationError, msg)
account.value
proc init(
self: BaseVMState;
ac: AccountsCache;
parent: BlockHeader;
timestamp: EthTime;
gasLimit: GasInt;
fee: Option[Uint256];
miner: EthAddress;
chainDB: BaseChainDB;
tracer: TransactionTracer)
{.gcsafe, raises: [Defect,CatchableError].} =
## Initialisation helper
self.prevHeaders = @[]
self.name = "BaseVM"
self.parent = parent
self.timestamp = timestamp
self.gasLimit = gasLimit
self.fee = fee
self.chaindb = chainDB
self.tracer = tracer
self.logEntries = @[]
self.stateDB = ac
self.touchedAccounts = initHashSet[EthAddress]()
self.minerAddress = miner
proc init(
self: BaseVMState;
ac: AccountsCache;
parent: BlockHeader;
timestamp: EthTime;
gasLimit: GasInt;
fee: Option[Uint256];
miner: EthAddress;
chainDB: BaseChainDB;
tracerFlags: set[TracerFlags])
{.gcsafe, raises: [Defect,CatchableError].} =
var tracer: TransactionTracer
tracer.initTracer(tracerFlags)
self.init(
ac = ac,
parent = parent,
timestamp = timestamp,
gasLimit = gasLimit,
fee = fee,
miner = miner,
chainDB = chainDB,
tracer = tracer)
# --------------
proc `$`*(vmState: BaseVMState): string
{.gcsafe, raises: [Defect,ValueError].} =
if vmState.isNil:
result = "nil"
else:
result = &"VMState {vmState.name}:"&
&"\n blockNumber: {vmState.parent.blockNumber + 1}"&
&"\n chaindb: {vmState.chaindb}"
proc new*(
T: type BaseVMState;
parent: BlockHeader; ## parent header, account sync position
timestamp: EthTime; ## tx env: time stamp
gasLimit: GasInt; ## tx env: gas limit
fee: Option[Uint256]; ## tx env: optional base fee
miner: EthAddress; ## tx env: coinbase(PoW) or signer(PoA)
chainDB: BaseChainDB; ## block chain database
tracerFlags: set[TracerFlags] = {};
pruneTrie: bool = true): T
{.gcsafe, raises: [Defect,CatchableError].} =
## 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 = AccountsCache.init(chainDB.db, parent.stateRoot, pruneTrie),
parent = parent,
timestamp = timestamp,
gasLimit = gasLimit,
fee = fee,
miner = miner,
chainDB = chainDB,
tracerFlags = tracerFlags)
proc reinit*(self: BaseVMState; ## Object descriptor
parent: BlockHeader; ## parent header, account sync pos.
timestamp: EthTime; ## tx env: time stamp
gasLimit: GasInt; ## tx env: gas limit
fee: Option[Uint256]; ## tx env: optional base fee
miner: EthAddress; ## tx env: coinbase(PoW) or signer(PoA)
pruneTrie: bool = true): bool
{.gcsafe, raises: [Defect,CatchableError].} =
## Re-initialise state descriptor. The `AccountsCache` 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 `AccountsCache` 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
db = self.chainDB
ac = if self.stateDB.rootHash == parent.stateRoot: self.stateDB
else: AccountsCache.init(db.db, parent.stateRoot, pruneTrie)
self[].reset
self.init(
ac = ac,
parent = parent,
timestamp = timestamp,
gasLimit = gasLimit,
fee = fee,
miner = miner,
chainDB = db,
tracer = tracer)
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
pruneTrie: bool = true): bool
{.gcsafe, raises: [Defect,CatchableError].} =
## 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()`.
self.reinit(
parent = parent,
timestamp = header.timestamp,
gasLimit = header.gasLimit,
fee = header.fee,
miner = self.chainDB.getMinerAddress(header),
pruneTrie = pruneTrie)
proc reinit*(self: BaseVMState; ## Object descriptor
header: BlockHeader; ## header with tx environment data fields
pruneTrie: bool = true): bool
{.gcsafe, raises: [Defect,CatchableError].} =
## 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.
self.reinit(
parent = self.chainDB.getBlockHeader(header.parentHash),
header = header,
pruneTrie = pruneTrie)
proc init*(
self: BaseVMState; ## Object descriptor
parent: BlockHeader; ## parent header, account sync position
header: BlockHeader; ## header with tx environment data fields
chainDB: BaseChainDB; ## block chain database
tracerFlags: set[TracerFlags] = {},
pruneTrie: bool = true)
{.gcsafe, raises: [Defect,CatchableError].} =
## 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(AccountsCache.init(chainDB.db, parent.stateRoot, pruneTrie),
parent,
header.timestamp,
header.gasLimit,
header.fee,
chainDB.getMinerAddress(header),
chainDB,
tracerFlags)
proc new*(
T: type BaseVMState;
parent: BlockHeader; ## parent header, account sync position
header: BlockHeader; ## header with tx environment data fields
chainDB: BaseChainDB; ## block chain database
tracerFlags: set[TracerFlags] = {},
pruneTrie: bool = true): T
{.gcsafe, raises: [Defect,CatchableError].} =
## 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,
chainDB = chainDB,
tracerFlags = tracerFlags,
pruneTrie = pruneTrie)
proc new*(
T: type BaseVMState;
header: BlockHeader; ## header with tx environment data fields
chainDB: BaseChainDB; ## block chain database
tracerFlags: set[TracerFlags] = {};
pruneTrie: bool = true): T
{.gcsafe, raises: [Defect,CatchableError].} =
## 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.
BaseVMState.new(
parent = chainDB.getBlockHeader(header.parentHash),
header = header,
chainDB = chainDB,
tracerFlags = tracerFlags,
pruneTrie = pruneTrie)
proc setupTxContext*(vmState: BaseVMState, origin: EthAddress, gasPrice: GasInt, forkOverride=none(Fork)) =
## this proc will be called each time a new transaction
## is going to be executed
vmState.txOrigin = origin
vmState.txGasPrice = gasPrice
vmState.fork =
if forkOverride.isSome:
forkOverride.get
else:
vmState.chainDB.config.toFork(vmState.parent.blockNumber + 1)
vmState.gasCosts = vmState.fork.forkToSchedule
proc consensusEnginePoA*(vmState: BaseVMState): bool =
# PoA consensus engine have no reward for miner
# TODO: this need to be fixed somehow
# using `real` engine configuration
vmState.chainDB.config.poaEngine
method coinbase*(vmState: BaseVMState): EthAddress {.base, gcsafe.} =
vmState.minerAddress
method blockNumber*(vmState: BaseVMState): BlockNumber {.base, gcsafe.} =
# it should return current block number
# and not head.blockNumber
vmState.parent.blockNumber + 1
method difficulty*(vmState: BaseVMState): UInt256 {.base, gcsafe.} =
vmState.chainDB.config.calcDifficulty(vmState.timestamp, vmState.parent)
method baseFee*(vmState: BaseVMState): UInt256 {.base, gcsafe.} =
if vmState.fee.isSome:
vmState.fee.get
else:
0.u256
when defined(geth):
import db/geth_db
method getAncestorHash*(vmState: BaseVMState, blockNumber: BlockNumber): Hash256 {.base, gcsafe, raises: [Defect,CatchableError].} =
var ancestorDepth = vmState.blockNumber - blockNumber - 1
if ancestorDepth >= constants.MAX_PREV_HEADER_DEPTH:
return
if blockNumber >= vmState.blockNumber:
return
when defined(geth):
result = vmState.chainDB.headerHash(blockNumber.truncate(uint64))
else:
result = vmState.chainDB.getBlockHash(blockNumber)
#TODO: should we use deque here?
# someday we may revive this code when
# we already have working miner
when false:
let idx = ancestorDepth.toInt
if idx >= vmState.prevHeaders.len:
return
var header = vmState.prevHeaders[idx]
result = header.hash
proc readOnlyStateDB*(vmState: BaseVMState): ReadOnlyStateDB {.inline.} =
ReadOnlyStateDB(vmState.stateDB)
template mutateStateDB*(vmState: BaseVMState, body: untyped) =
block:
var db {.inject.} = vmState.stateDB
body
proc getTracingResult*(vmState: BaseVMState): JsonNode {.inline.} =
doAssert(EnableTracing in vmState.tracer.flags)
vmState.tracer.trace
proc getAndClearLogEntries*(vmState: BaseVMState): seq[Log] =
shallowCopy(result, vmState.logEntries)
vmState.logEntries = @[]
proc enableTracing*(vmState: BaseVMState) =
vmState.tracer.flags.incl EnableTracing
proc disableTracing*(vmState: BaseVMState) =
vmState.tracer.flags.excl EnableTracing
iterator tracedAccounts*(vmState: BaseVMState): EthAddress =
for acc in vmState.tracer.accounts:
yield acc
iterator tracedAccountsPairs*(vmState: BaseVMState): (int, EthAddress) =
var idx = 0
for acc in vmState.tracer.accounts:
yield (idx, acc)
inc idx
proc removeTracedAccounts*(vmState: BaseVMState, accounts: varargs[EthAddress]) =
for acc in accounts:
vmState.tracer.accounts.excl acc
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 generateWitness*(vmState: BaseVMState): bool =
GenerateWitness in vmState.flags
proc `generateWitness=`*(vmState: BaseVMState, status: bool) =
if status: vmState.flags.incl GenerateWitness
else: vmState.flags.excl GenerateWitness
proc buildWitness*(vmState: BaseVMState): seq[byte]
{.raises: [Defect, CatchableError].} =
let rootHash = vmState.stateDB.rootHash
let mkeys = vmState.stateDB.makeMultiKeys()
let flags = if vmState.fork >= FKSpurious: {wfEIP170} else: {}
# build witness from tree
var wb = initWitnessBuilder(vmState.chainDB.db, rootHash, flags)
safeExecutor("buildWitness"):
result = wb.buildWitness(mkeys)