nimbus-eth1/tests/test_blockchain_json.nim

530 lines
17 KiB
Nim
Raw Normal View History

2019-09-03 15:06:43 +00:00
# Nimbus
# Copyright (c) 2018-2024 Status Research & Development GmbH
2019-09-03 15:06:43 +00:00
# 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.
2019-09-03 15:06:43 +00:00
import
std/[json, os, tables, strutils, options, streams],
2022-12-02 04:39:12 +00:00
unittest2,
eth/rlp, eth/trie/trie_defs, eth/common/eth_types_rlp,
stew/byteutils,
2019-12-07 15:37:31 +00:00
./test_helpers, ./test_allowed_to_fail,
../premix/parser, test_config,
../nimbus/[evm/state, evm/types, errors, constants],
../nimbus/db/ledger,
../nimbus/utils/[utils, debug],
../nimbus/evm/tracer/legacy_tracer,
../nimbus/evm/tracer/json_tracer,
../nimbus/core/[validate, chain, pow/header],
../tools/common/helpers as chp,
../tools/evmstate/helpers,
../nimbus/common/common,
../nimbus/core/eip4844,
../nimbus/rpc/experimental
2019-09-03 15:06:43 +00:00
type
SealEngine = enum
NoProof
Ethash
TestBlock = object
goodBlock: bool
blockRLP : Blob
2022-12-09 08:17:33 +00:00
header : BlockHeader
body : BlockBody
hasException: bool
withdrawals: Option[seq[Withdrawal]]
2019-09-03 15:06:43 +00:00
TestCtx = object
2019-09-03 15:06:43 +00:00
lastBlockHash: Hash256
genesisHeader: BlockHeader
blocks : seq[TestBlock]
sealEngine : Option[SealEngine]
debugMode : bool
trace : bool
vmState : BaseVMState
debugData : JsonNode
network : string
postStateHash: Hash256
json : bool
2019-09-10 12:52:36 +00:00
proc testFixture(node: JsonNode, testStatusIMPL: var TestStatus, debugMode = false, trace = false)
2019-09-03 15:06:43 +00:00
func normalizeNumber(n: JsonNode): JsonNode =
let str = n.getStr
if str == "0x":
result = newJString("0x0")
elif str == "0x0":
result = n
elif str == "0x00":
2019-09-03 15:06:43 +00:00
result = newJString("0x0")
elif str[2] == '0':
var i = 2
2019-09-03 15:06:43 +00:00
while str[i] == '0':
inc i
result = newJString("0x" & str.substr(i))
else:
result = n
func normalizeData(n: JsonNode): JsonNode =
if n.getStr() == "":
result = newJString("0x")
else:
result = n
func normalizeBlockHeader(node: JsonNode): JsonNode =
for k, v in node:
case k
of "bloom": node["logsBloom"] = v
of "coinbase": node["miner"] = v
of "uncleHash": node["sha3Uncles"] = v
of "receiptTrie": node["receiptsRoot"] = v
of "transactionsTrie": node["transactionsRoot"] = v
of "number", "difficulty", "gasUsed",
"gasLimit", "timestamp", "baseFeePerGas":
2019-09-03 15:06:43 +00:00
node[k] = normalizeNumber(v)
of "extraData":
node[k] = normalizeData(v)
else: discard
result = node
func normalizeWithdrawal(node: JsonNode): JsonNode =
for k, v in node:
case k
2023-07-05 09:30:01 +00:00
of "amount", "index", "validatorIndex":
node[k] = normalizeNumber(v)
else: discard
result = node
2019-09-03 15:06:43 +00:00
proc parseHeader(blockHeader: JsonNode, testStatusIMPL: var TestStatus): BlockHeader =
result = normalizeBlockHeader(blockHeader).parseBlockHeader
var blockHash: Hash256
blockHeader.fromJson "hash", blockHash
check blockHash == rlpHash(result)
2019-09-03 15:06:43 +00:00
proc parseWithdrawals(withdrawals: JsonNode): Option[seq[Withdrawal]] =
case withdrawals.kind
of JArray:
var ws: seq[Withdrawal]
for v in withdrawals:
ws.add(parseWithdrawal(normalizeWithdrawal(v)))
some(ws)
else:
none[seq[Withdrawal]]()
proc parseBlocks(blocks: JsonNode): seq[TestBlock] =
2019-09-03 15:06:43 +00:00
for fixture in blocks:
var t: TestBlock
t.withdrawals = none[seq[Withdrawal]]()
2019-09-03 15:06:43 +00:00
for key, value in fixture:
case key
of "blockHeader":
# header is absent in bad block
t.goodBlock = true
2019-09-03 15:06:43 +00:00
of "rlp":
fixture.fromJson "rlp", t.blockRLP
2023-07-05 09:30:01 +00:00
of "transactions", "uncleHeaders", "hasBigInt",
"blocknumber", "chainname", "chainnetwork":
discard
of "transactionSequence":
var noError = true
for tx in value:
let valid = tx["valid"].getStr == "true"
noError = noError and valid
doAssert(noError == false, "NOT A VALID TEST CASE")
of "withdrawals":
t.withdrawals = parseWithdrawals(value)
of "rlp_decoded":
# this field is intended for client who
# doesn't support rlp encoding(e.g. evmone)
discard
else:
doAssert("expectException" in key, key)
t.hasException = true
2019-09-03 15:06:43 +00:00
result.add t
proc parseTestCtx(fixture: JsonNode, testStatusIMPL: var TestStatus): TestCtx =
result.blocks = parseBlocks(fixture["blocks"])
2019-09-03 16:41:01 +00:00
fixture.fromJson "lastblockhash", result.lastBlockHash
if "genesisRLP" in fixture:
var genesisRLP: Blob
fixture.fromJson "genesisRLP", genesisRLP
2022-12-09 08:17:33 +00:00
result.genesisHeader = rlp.decode(genesisRLP, EthBlock).header
else:
2022-12-09 08:17:33 +00:00
result.genesisHeader = parseHeader(fixture["genesisBlockHeader"], testStatusIMPL)
var goodBlock = true
for h in result.blocks:
goodBlock = goodBlock and h.goodBlock
check goodBlock == false
2019-09-03 16:41:01 +00:00
if "sealEngine" in fixture:
result.sealEngine = some(parseEnum[SealEngine](fixture["sealEngine"].getStr))
if "postStateHash" in fixture:
result.postStateHash.data = hexToByteArray[32](fixture["postStateHash"].getStr)
2020-04-12 12:02:03 +00:00
result.network = fixture["network"].getStr
2019-09-03 16:41:01 +00:00
proc testGetMultiKeys(chain: ChainRef, parentHeader, currentHeader: BlockHeader) =
# check that current state matches current header
let currentStateRoot = chain.vmState.stateDB.rootHash
if currentStateRoot != currentHeader.stateRoot:
raise newException(ValidationError, "Expected currentStateRoot == currentHeader.stateRoot")
let mkeys = getMultiKeys(chain.com, currentHeader, false)
# check that the vmstate hasn't changed after call to getMultiKeys
if chain.vmState.stateDB.rootHash != currentHeader.stateRoot:
raise newException(ValidationError, "Expected chain.vmstate.stateDB.rootHash == currentHeader.stateRoot")
# use the MultiKeysRef to build the block proofs
let
ac = LedgerRef.init(chain.com.db, currentHeader.stateRoot)
blockProofs = getBlockProofs(ac, mkeys)
if blockProofs.len() != 0:
raise newException(ValidationError, "Expected blockProofs.len() == 0")
proc setupTracer(ctx: TestCtx): TracerRef =
if ctx.trace:
if ctx.json:
var tracerFlags = {
TracerFlags.DisableMemory,
TracerFlags.DisableStorage,
TracerFlags.DisableState,
TracerFlags.DisableStateDiff,
TracerFlags.DisableReturnData
}
let stream = newFileStream(stdout)
newJsonTracer(stream, tracerFlags, false)
else:
newLegacyTracer({})
else:
TracerRef()
proc importBlock(ctx: var TestCtx, com: CommonRef,
tb: TestBlock, checkSeal: bool) =
if ctx.vmState.isNil or ctx.vmState.stateDB.isTopLevelClean.not:
let
parentHeader = com.db.getBlockHeader(tb.header.parentHash)
tracerInst = ctx.setupTracer()
ctx.vmState = BaseVMState.new(
parentHeader,
tb.header,
com,
tracerInst,
)
ctx.vmState.collectWitnessData = true # Enable saving witness data
2019-09-09 06:05:16 +00:00
let
chain = newChain(com, extraValidation = true, ctx.vmState)
res = chain.persistBlocks([EthBlock.init(tb.header, tb.body)])
2024-06-07 02:01:45 +00:00
if res.isErr():
raise newException(ValidationError, res.error())
# testGetMultiKeys fails with:
# Unhandled defect: AccountLedger.init(): RootNotFound(Aristo, ctx=ctx/newColFn(), error=GenericError) [AssertionDefect]
#else:
# testGetMultiKeys(chain, chain.vmState.parent, tb.header)
proc applyFixtureBlockToChain(ctx: var TestCtx, tb: var TestBlock,
com: CommonRef, checkSeal: bool) =
2023-06-12 04:29:03 +00:00
decompose(tb.blockRLP, tb.header, tb.body)
ctx.importBlock(com, tb, checkSeal)
2019-09-04 07:32:17 +00:00
func shouldCheckSeal(ctx: TestCtx): bool =
if ctx.sealEngine.isSome:
result = ctx.sealEngine.get() != NoProof
2019-09-09 05:27:17 +00:00
proc collectDebugData(ctx: var TestCtx) =
if ctx.vmState.isNil:
return
let vmState = ctx.vmState
let tracerInst = LegacyTracer(vmState.tracer)
let tracingResult = if ctx.trace: tracerInst.getTracingResult() else: %[]
ctx.debugData.add %{
2019-09-24 13:18:48 +00:00
"blockNumber": %($vmState.blockNumber),
"structLogs": tracingResult,
}
proc runTestCtx(ctx: var TestCtx, com: CommonRef, testStatusIMPL: var TestStatus) =
doAssert com.db.persistHeader(ctx.genesisHeader,
com.consensus == ConsensusType.POS)
check com.db.getCanonicalHead().blockHash == ctx.genesisHeader.blockHash
let checkSeal = ctx.shouldCheckSeal
2019-09-03 16:41:01 +00:00
if ctx.debugMode:
ctx.debugData = newJArray()
2019-09-24 13:18:48 +00:00
for idx, tb in ctx.blocks:
2022-12-09 08:17:33 +00:00
if tb.goodBlock:
try:
ctx.applyFixtureBlockToChain(
ctx.blocks[idx], com, checkSeal)
2023-06-12 04:29:03 +00:00
except CatchableError as ex:
debugEcho "FATAL ERROR(WE HAVE BUG): ", ex.msg
2019-09-25 11:43:16 +00:00
2019-09-07 10:50:34 +00:00
else:
var noError = true
try:
ctx.applyFixtureBlockToChain(ctx.blocks[idx],
com, checkSeal)
except ValueError, ValidationError, BlockNotFound, RlpError:
2019-09-07 10:50:34 +00:00
# failure is expected on this bad block
2022-12-09 08:17:33 +00:00
check (tb.hasException or (not tb.goodBlock))
2019-09-07 10:50:34 +00:00
noError = false
if ctx.debugMode:
ctx.debugData.add %{
"exception": %($getCurrentException().name),
"msg": %getCurrentExceptionMsg()
}
2019-09-07 10:50:34 +00:00
# Block should have caused a validation error
check noError == false
2019-09-03 15:06:43 +00:00
if ctx.debugMode and not ctx.json:
ctx.collectDebugData()
2019-09-24 13:18:48 +00:00
proc debugDataFromAccountList(ctx: TestCtx): JsonNode =
let vmState = ctx.vmState
result = %{"debugData": ctx.debugData}
if not vmState.isNil:
result["accounts"] = vmState.dumpAccounts()
proc debugDataFromPostStateHash(ctx: TestCtx): JsonNode =
let vmState = ctx.vmState
%{
"debugData": ctx.debugData,
"postStateHash": %($vmState.readOnlyStateDB.rootHash),
"expectedStateHash": %($ctx.postStateHash),
"accounts": vmState.dumpAccounts()
}
proc dumpDebugData(ctx: TestCtx, fixtureName: string, fixtureIndex: int, success: bool) =
let debugData = if ctx.postStateHash != Hash256():
debugDataFromPostStateHash(ctx)
else:
debugDataFromAccountList(ctx)
2019-09-10 12:52:36 +00:00
let status = if success: "_success" else: "_failed"
let name = fixtureName.replace('/', '-').replace(':', '-')
2023-07-05 09:30:01 +00:00
writeFile("debug_" & name & "_" & $fixtureIndex & status & ".json", debugData.pretty())
2019-09-10 12:52:36 +00:00
proc testFixture(node: JsonNode, testStatusIMPL: var TestStatus, debugMode = false, trace = false) =
2019-09-05 09:07:08 +00:00
# 1 - mine the genesis block
# 2 - loop over blocks:
# - apply transactions
# - mine block
# 3 - diff resulting state with expected state
# 4 - check that all previous blocks were valid
let specifyIndex = test_config.getConfiguration().index.get(0)
2019-09-10 12:52:36 +00:00
var fixtureIndex = 0
var fixtureTested = false
2019-09-05 09:07:08 +00:00
2019-09-03 15:06:43 +00:00
for fixtureName, fixture in node:
2019-09-10 12:52:36 +00:00
inc fixtureIndex
if specifyIndex > 0 and fixtureIndex != specifyIndex:
continue
var ctx = parseTestCtx(fixture, testStatusIMPL)
2019-09-03 15:06:43 +00:00
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
let
memDB = newCoreDbRef DefaultDbMemory
stateDB = LedgerRef.init(memDB, emptyRlpHash)
config = getChainConfig(ctx.network)
com = CommonRef.new(memDB, config)
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
setupStateDB(fixture["pre"], stateDB)
stateDB.persist()
check stateDB.rootHash == ctx.genesisHeader.stateRoot
2019-09-03 15:06:43 +00:00
ctx.debugMode = debugMode
ctx.trace = trace
ctx.json = test_config.getConfiguration().json
2019-09-10 12:52:36 +00:00
var success = true
try:
ctx.runTestCtx(com, testStatusIMPL)
let header = com.db.getCanonicalHead()
let lastBlockHash = header.blockHash
check lastBlockHash == ctx.lastBlockHash
success = lastBlockHash == ctx.lastBlockHash
if ctx.postStateHash != Hash256():
let rootHash = ctx.vmState.stateDB.rootHash
if ctx.postStateHash != rootHash:
raise newException(ValidationError, "incorrect postStateHash, expect=" &
$rootHash & ", get=" &
$ctx.postStateHash
)
elif lastBlockHash == ctx.lastBlockHash:
# multiple chain, we are using the last valid canonical
# state root to test against 'postState'
let stateDB = LedgerRef.init(memDB, header.stateRoot)
verifyStateDB(fixture["postState"], ledger.ReadOnlyStateDB(stateDB))
success = lastBlockHash == ctx.lastBlockHash
2019-09-10 12:52:36 +00:00
except ValidationError as E:
2020-07-21 06:15:06 +00:00
echo fixtureName, " ERROR: ", E.msg
2019-09-10 12:52:36 +00:00
success = false
2019-09-25 11:43:16 +00:00
if ctx.debugMode:
ctx.dumpDebugData(fixtureName, fixtureIndex, success)
2019-09-25 11:43:16 +00:00
2019-09-10 12:52:36 +00:00
fixtureTested = true
check success == true
if not fixtureTested:
2020-04-12 12:02:03 +00:00
echo test_config.getConfiguration().testSubject, " not tested at all, wrong index?"
if specifyIndex <= 0 or specifyIndex > node.len:
echo "Maximum subtest available: ", node.len
2019-09-03 15:06:43 +00:00
2019-12-06 05:26:56 +00:00
proc blockchainJsonMain*(debugMode = false) =
const
legacyFolder = "eth_tests/LegacyTests/Constantinople/BlockchainTests"
newFolder = "eth_tests/BlockchainTests"
#newFolder = "eth_tests/EIPTests/BlockchainTests"
#newFolder = "eth_tests/EIPTests/Pyspecs/cancun"
let res = loadKzgTrustedSetup()
if res.isErr:
echo "FATAL: ", res.error
quit(QuitFailure)
2021-01-14 14:33:18 +00:00
let config = test_config.getConfiguration()
if config.testSubject == "" or not debugMode:
2019-09-07 09:47:06 +00:00
# run all test fixtures
2021-01-14 14:33:18 +00:00
if config.legacy:
suite "block chain json tests":
jsonTest(legacyFolder, "BlockchainTests", testFixture, skipBCTests)
else:
suite "new block chain json tests":
jsonTest(newFolder, "newBlockchainTests", testFixture, skipNewBCTests)
2019-09-07 09:47:06 +00:00
else:
# execute single test in debug mode
if config.testSubject.len == 0:
echo "missing test subject"
quit(QuitFailure)
let folder = if config.legacy: legacyFolder else: newFolder
let path = "tests/fixtures/" & folder
2019-09-07 09:47:06 +00:00
let n = json.parseFile(path / config.testSubject)
var testStatusIMPL: TestStatus
testFixture(n, testStatusIMPL, debugMode = true, config.trace)
2019-09-07 09:47:06 +00:00
when isMainModule:
import std/times
2019-09-07 09:47:06 +00:00
var message: string
let start = getTime()
2019-09-07 09:47:06 +00:00
## Processing command line arguments
2020-04-12 12:02:03 +00:00
if test_config.processArguments(message) != test_config.Success:
2019-09-07 09:47:06 +00:00
echo message
quit(QuitFailure)
else:
if len(message) > 0:
echo message
quit(QuitSuccess)
2019-12-06 05:26:56 +00:00
blockchainJsonMain(true)
let elpd = getTime() - start
echo "TIME: ", elpd
2019-09-07 09:47:06 +00:00
2019-09-03 15:06:43 +00:00
# lastBlockHash -> every fixture has it, hash of a block header
# genesisRLP -> NOT every fixture has it, rlp bytes of genesis block header
# _info -> every fixture has it, can be omitted
# pre, postState -> every fixture has it, prestate and post state
# genesisHeader -> every fixture has it
2019-09-03 15:06:43 +00:00
# network -> every fixture has it
# # EIP150 247
# # ConstantinopleFix 286
# # Homestead 256
# # Frontier 396
# # Byzantium 263
# # EIP158ToByzantiumAt5 1
# # EIP158 233
# # HomesteadToDaoAt5 4
# # Constantinople 285
# # HomesteadToEIP150At5 1
# # FrontierToHomesteadAt5 7
# # ByzantiumToConstantinopleFixAt5 1
# sealEngine -> NOT every fixture has it
# # NoProof 1709
# # Ethash 112
# blocks -> every fixture has it, an array of blocks ranging from 1 block to 303 blocks
# # transactions 6230 can be empty
# # # to 6089 -> "" if contractCreation
# # # value 6089
# # # gasLimit 6089 -> "gas"
# # # s 6089
# # # r 6089
# # # gasPrice 6089
# # # v 6089
# # # data 6089 -> "input"
# # # nonce 6089
# # blockHeader 6230 can be not present, e.g. bad rlp
# # uncleHeaders 6230 can be empty
# # rlp 6810 has rlp but no blockheader, usually has exception
# # blocknumber 2733
# # chainname 1821 -> 'A' to 'H', and 'AA' to 'DD'
# # chainnetwork 21 -> all values are "Frontier"
# # expectExceptionALL 420
# # # UncleInChain 55
# # # InvalidTimestamp 42
# # # InvalidGasLimit 42
# # # InvalidNumber 42
# # # InvalidDifficulty 35
# # # InvalidBlockNonce 28
# # # InvalidUncleParentHash 26
# # # ExtraDataTooBig 21
# # # InvalidStateRoot 21
# # # ExtraDataIncorrect 19
# # # UnknownParent 16
# # # TooMuchGasUsed 14
# # # InvalidReceiptsStateRoot 9
# # # InvalidUnclesHash 7
# # # UncleIsBrother 7
# # # UncleTooOld 7
# # # InvalidTransactionsRoot 7
# # # InvalidGasUsed 7
# # # InvalidLogBloom 7
# # # TooManyUncles 7
# # # OutOfGasIntrinsic 1
# # expectExceptionEIP150 17
# # # TooMuchGasUsed 7
# # # InvalidReceiptsStateRoot 7
# # # InvalidStateRoot 3
# # expectExceptionByzantium 17
# # # InvalidStateRoot 10
# # # TooMuchGasUsed 7
# # expectExceptionHomestead 17
# # # InvalidReceiptsStateRoot 7
# # # BlockGasLimitReached 7
# # # InvalidStateRoot 3
# # expectExceptionConstantinople 14
# # # InvalidStateRoot 7
# # # TooMuchGasUsed 7
# # expectExceptionEIP158 14
# # # TooMuchGasUsed 7
# # # InvalidReceiptsStateRoot 7
# # expectExceptionFrontier 14
# # # InvalidReceiptsStateRoot 7
# # # BlockGasLimitReached 7
# # expectExceptionConstantinopleFix 14
# # # InvalidStateRoot 7
# # # TooMuchGasUsed 7