nimbus-eth1/tests/test_accounts_cache.nim

391 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.
import
std/[os, strformat, strutils, tables],
chronicles,
stew/byteutils,
../nimbus/db/ledger,
../nimbus/common/common,
../nimbus/core/chain,
../nimbus/transaction,
../nimbus/constants,
../nimbus/vm_state,
../nimbus/vm_types,
./replay/undump_blocks,
unittest2
type
CaptureSpecs = tuple
network: NetworkId
file: string
numBlocks: int
numTxs: int
const
baseDir = [".", "tests", ".." / "tests", $DirSep] # path containg repo
repoDir = ["replay", "status", "test_clique"] # alternative repo paths
goerliCapture: CaptureSpecs = (
network: GoerliNet,
file: "goerli68161.txt.gz",
numBlocks: 5500, # unconditionally load blocks
numTxs: 10) # txs following (not in block chain)
when false:
goerliCapture1: CaptureSpecs = (
GoerliNet, goerliCapture.file, 5500, 10000)
mainCapture: CaptureSpecs = (
MainNet, "mainnet843841.txt.gz", 50000, 3000)
var
xdb: CoreDbRef
txs: seq[Transaction]
txi: seq[int] # selected index into txs[] (crashable sender addresses)
# ------------------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------------------
proc findFilePath(file: string): string =
result = "?unknown?" / file
for dir in baseDir:
for repo in repoDir:
let path = dir / repo / file
if path.fileExists:
return path
proc pp*(a: EthAddress): string =
a.toHex[32 .. 39].toLowerAscii
proc pp*(tx: Transaction): string =
# "(" & tx.ecRecover.value.pp & "," & $tx.nonce & ")"
"(" & tx.getSender.pp & "," & $tx.nonce & ")"
proc pp*(h: KeccakHash): string =
h.data.toHex[52 .. 63].toLowerAscii
proc pp*(tx: Transaction; vmState: BaseVMState): string =
let address = tx.getSender
"(" & address.pp &
"," & $tx.nonce &
";" & $vmState.readOnlyStateDB.getNonce(address) &
"," & $vmState.readOnlyStateDB.getBalance(address) &
")"
when false:
proc setTraceLevel =
discard
when defined(chronicles_runtime_filtering) and loggingEnabled:
setLogLevel(LogLevel.TRACE)
proc setErrorLevel =
discard
when defined(chronicles_runtime_filtering) and loggingEnabled:
setLogLevel(LogLevel.ERROR)
# ------------------------------------------------------------------------------
# Private functions
# ------------------------------------------------------------------------------
proc blockChainForTesting*(network: NetworkId): CommonRef =
result = CommonRef.new(
newCoreDbRef LegacyDbMemory,
networkId = network,
params = network.networkParams)
initializeEmptyDb(result)
proc importBlocks(com: CommonRef; h: seq[BlockHeader]; b: seq[BlockBody]) =
if com.newChain.persistBlocks(h,b).isErr:
raiseAssert "persistBlocks() failed at block #" & $h[0].blockNumber
proc getVmState(com: CommonRef; number: BlockNumber): BaseVMState =
BaseVMState.new(com.db.getBlockHeader(number), com)
# ------------------------------------------------------------------------------
# Crash test function, finding out about how the transaction framework works ..
# ------------------------------------------------------------------------------
proc modBalance(ac: LedgerRef, address: EthAddress) =
## This function is crucial for profucing the crash. If must
## modify the balance so that the database gets written.
# ac.blindBalanceSetter(address)
ac.addBalance(address, 1.u256)
proc runTrial2ok(vmState: BaseVMState; inx: int) =
## Run two blocks, the first one with *rollback*.
let eAddr = txs[inx].getSender
block:
let accTx = vmState.stateDB.beginSavepoint
vmState.stateDB.modBalance(eAddr)
vmState.stateDB.rollback(accTx)
block:
let accTx = vmState.stateDB.beginSavepoint
vmState.stateDB.modBalance(eAddr)
vmState.stateDB.commit(accTx)
vmState.stateDB.persist()
proc runTrial3(vmState: BaseVMState; inx: int; rollback: bool) =
## Run three blocks, the second one optionally with *rollback*.
let eAddr = txs[inx].getSender
block:
let accTx = vmState.stateDB.beginSavepoint
vmState.stateDB.modBalance(eAddr)
vmState.stateDB.commit(accTx)
vmState.stateDB.persist()
block:
let accTx = vmState.stateDB.beginSavepoint
vmState.stateDB.modBalance(eAddr)
if rollback:
vmState.stateDB.rollback(accTx)
break
vmState.stateDB.commit(accTx)
vmState.stateDB.persist()
block:
let accTx = vmState.stateDB.beginSavepoint
vmState.stateDB.modBalance(eAddr)
vmState.stateDB.commit(accTx)
vmState.stateDB.persist()
proc runTrial3crash(vmState: BaseVMState; inx: int; noisy = false) =
## Run three blocks with extra db frames and *rollback*.
let eAddr = txs[inx].getSender
block:
let dbTx = xdb.beginTransaction()
block:
let accTx = vmState.stateDB.beginSavepoint
vmState.stateDB.modBalance(eAddr)
vmState.stateDB.commit(accTx)
vmState.stateDB.persist()
block:
let accTx = vmState.stateDB.beginSavepoint
vmState.stateDB.modBalance(eAddr)
vmState.stateDB.rollback(accTx)
# The following statement will cause a crash at the next `persist()` call.
dbTx.rollback()
# In order to survive without an exception in the next `persist()` call, the
# following function could be added to `db/ledger`:
#
# proc clobberRootHash*(ac: LedgerRef; root: KeccakHash; prune = true) =
# ac.trie = initAccountsTrie(ac.db, rootHash, prune)
#
# Then, beginning this very function `runTrial3crash()` with
#
# let stateRoot = vmState.stateDB.rootHash
#
# the survival statement would be to re-assign the state-root via
#
# vmState.stateDB.clobberRootHash(stateRoot)
#
# Also mind this comment from Andri:
#
# [..] but as a reminder, only reinit the ac.trie is not enough, you
# should consider the accounts in the cache too. if there is any accounts
# in the cache they must in sync with the new rootHash.
#
block:
let dbTx = xdb.beginTransaction()
block:
let accTx = vmState.stateDB.beginSavepoint
vmState.stateDB.modBalance(eAddr)
vmState.stateDB.commit(accTx)
try:
vmState.stateDB.persist()
except AssertionDefect as e:
if noisy:
let msg = e.msg.rsplit($DirSep,1)[^1]
echo &"*** runVmExec({eAddr.pp}): {e.name}: {msg}"
dbTx.dispose()
raise e
vmState.stateDB.persist()
dbTx.commit()
proc runTrial4(vmState: BaseVMState; inx: int; rollback: bool) =
## Like `runTrial3()` but with four blocks and extra db transaction frames.
let eAddr = txs[inx].getSender
block:
let dbTx = xdb.beginTransaction()
block:
let accTx = vmState.stateDB.beginSavepoint
vmState.stateDB.modBalance(eAddr)
vmState.stateDB.commit(accTx)
vmState.stateDB.persist()
block:
let accTx = vmState.stateDB.beginSavepoint
vmState.stateDB.modBalance(eAddr)
vmState.stateDB.commit(accTx)
vmState.stateDB.persist()
block:
let accTx = vmState.stateDB.beginSavepoint
vmState.stateDB.modBalance(eAddr)
if rollback:
vmState.stateDB.rollback(accTx)
break
vmState.stateDB.commit(accTx)
vmState.stateDB.persist()
# There must be no dbTx.rollback() here unless `vmState.stateDB` is
# discarded and/or re-initialised.
dbTx.commit()
block:
let dbTx = xdb.beginTransaction()
block:
let accTx = vmState.stateDB.beginSavepoint
vmState.stateDB.modBalance(eAddr)
vmState.stateDB.commit(accTx)
vmState.stateDB.persist()
dbTx.commit()
# ------------------------------------------------------------------------------
# Test Runner
# ------------------------------------------------------------------------------
proc runner(noisy = true; capture = goerliCapture) =
let
loadBlocks = capture.numBlocks.u256
loadTxs = capture.numTxs
fileInfo = capture.file.splitFile.name.split(".")[0]
filePath = capture.file.findFilePath
com = capture.network.blockChainForTesting
txs.reset
xdb = com.db
suite &"StateDB nesting scenarios":
var topNumber: BlockNumber
test &"Import from {fileInfo}":
# Import minimum amount of blocks, then collect transactions
for chain in filePath.undumpBlocks:
let leadBlkNum = chain[0].header.blockNumber
topNumber = chain[^1].header.blockNumber
if loadTxs <= txs.len:
break
# Verify Genesis
if leadBlkNum == 0.u256:
doAssert chain[0][0] == xdb.getBlockHeader(0.u256)
continue
# Import block chain blocks
if leadBlkNum < loadBlocks:
com.importBlocks(chain)
continue
# Import transactions
for inx in 0 ..< chain.len:
let blkTxs = chain[inx].transactions
# Continue importing up until first non-trivial block
if txs.len == 0 and blkTxs.len == 0:
com.importBlocks([chain[inx]])
continue
# Load transactions
txs.add blkTxs
test &"Collect unique sender addresses from {txs.len} txs," &
&" head=#{xdb.getCanonicalHead.blockNumber}, top=#{topNumber}":
var seen: Table[EthAddress,bool]
for n,tx in txs:
let a = tx.getSender
if not seen.hasKey(a):
seen[a] = true
txi.add n
test &"Run {txi.len} two-step trials with rollback":
let dbTx = xdb.beginTransaction()
defer: dbTx.dispose()
for n in txi:
let vmState = com.getVmState(xdb.getCanonicalHead.blockNumber)
vmState.runTrial2ok(n)
test &"Run {txi.len} three-step trials with rollback":
let dbTx = xdb.beginTransaction()
defer: dbTx.dispose()
for n in txi:
let vmState = com.getVmState(xdb.getCanonicalHead.blockNumber)
vmState.runTrial3(n, rollback = true)
test &"Run {txi.len} three-step trials with extra db frame rollback" &
" throwing Exceptions":
let dbTx = xdb.beginTransaction()
defer: dbTx.dispose()
for n in txi:
let vmState = com.getVmState(xdb.getCanonicalHead.blockNumber)
expect AssertionDefect:
vmState.runTrial3crash(n, noisy)
test &"Run {txi.len} tree-step trials without rollback":
let dbTx = xdb.beginTransaction()
defer: dbTx.dispose()
for n in txi:
let vmState = com.getVmState(xdb.getCanonicalHead.blockNumber)
vmState.runTrial3(n, rollback = false)
test &"Run {txi.len} four-step trials with rollback and db frames":
let dbTx = xdb.beginTransaction()
defer: dbTx.dispose()
for n in txi:
let vmState = com.getVmState(xdb.getCanonicalHead.blockNumber)
vmState.runTrial4(n, rollback = true)
# ------------------------------------------------------------------------------
# Main function(s)
# ------------------------------------------------------------------------------
proc accountsCacheMain*(noisy = defined(debug)) =
noisy.runner
when isMainModule:
var noisy = defined(debug)
#noisy = true
setErrorLevel()
noisy.runner # mainCapture
# noisy.runner goerliCapture2
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------