nimbus-eth1/tests/test_blockchain_json.nim
Adam Spitz d8a1adacaa
More work on withdrawals (#1482)
* Part of EIP-4895: add withdrawals processing to block processing.

* Refactoring: extracted the engine API handler bodies into procs.

Intending to implement the V2 versions next. (I need the bodies to be
in separate procs so that multiple versions can use them.)

* Working on Engine API changes for Shanghai.

* Updated nim-web3, resolved ambiguity in Hash256 type.

* Updated nim-eth3 to point to master, now that I've merged that.

* I'm confused about what's going on with engine_client.

But let's try resolving this Hash256 ambiguity.

* Still trying to fix this conflict with the Hash256 types.

* Does this work now that nimbus-eth2 has been updated?

* Corrected blockValue in getPayload responses back to UInt256.

c834f67a37

* Working on getting the withdrawals-related tests to pass.

* Fixing more of those Hash256 ambiguities.

(I'm not sure why the nim-web3 library introduced a conflicting type
named Hash256, but right now I just want to get this code to compile again.)

* Bumped a couple of libraries to fix some error messages.

* Needed to get "make fluffy-tools" to pass, too.

* Getting "make nimbus_verified_proxy" to build.
2023-03-09 18:40:55 -05:00

558 lines
18 KiB
Nim

# Nimbus
# Copyright (c) 2018-2023 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, os, tables, strutils, options],
unittest2,
eth/rlp, eth/trie/trie_defs,
stew/byteutils,
./test_helpers, ./test_allowed_to_fail,
../premix/parser, test_config,
../nimbus/[vm_state, vm_types, errors, constants],
../nimbus/db/accounts_cache,
../nimbus/utils/utils,
../nimbus/core/[executor, validate, pow/header],
../stateless/[tree_from_witness, witness_types],
../tools/common/helpers as chp,
../tools/evmstate/helpers,
../nimbus/common/common
type
# trick the rlp decoder
# so we can separate the body and header
EthHeader = object
header: BlockHeader
SealEngine = enum
NoProof
Ethash
TestBlock = object
goodBlock: bool
blockRLP : Blob
header : BlockHeader
body : BlockBody
hasException: bool
withdrawals: Option[seq[Withdrawal]]
Tester = object
lastBlockHash: Hash256
genesisHeader: BlockHeader
blocks : seq[TestBlock]
sealEngine : Option[SealEngine]
debugMode : bool
trace : bool
vmState : BaseVMState
debugData : JsonNode
network : string
postStateHash: Hash256
var pow = PowRef.new
proc testFixture(node: JsonNode, testStatusIMPL: var TestStatus, debugMode = false, trace = false)
func normalizeNumber(n: JsonNode): JsonNode =
let str = n.getStr
if str == "0x":
result = newJString("0x0")
elif str == "0x0":
result = n
elif str == "0x00":
result = newJString("0x0")
elif str[2] == '0':
var i = 2
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":
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
of "address", "amount", "index", "validatorIndex":
node[k] = normalizeNumber(v)
else: discard
result = node
proc parseHeader(blockHeader: JsonNode, testStatusIMPL: var TestStatus): BlockHeader =
result = normalizeBlockHeader(blockHeader).parseBlockHeader
var blockHash: Hash256
blockHeader.fromJson "hash", blockHash
check blockHash == hash(result)
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] =
for fixture in blocks:
var t: TestBlock
for key, value in fixture:
case key
of "blockHeader":
# header is absent in bad block
t.goodBlock = true
of "rlp":
fixture.fromJson "rlp", t.blockRLP
of "transactions", "uncleHeaders",
"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)
else:
doAssert("expectException" in key, key)
t.hasException = true
result.add t
proc parseTester(fixture: JsonNode, testStatusIMPL: var TestStatus): Tester =
result.blocks = parseBlocks(fixture["blocks"])
fixture.fromJson "lastblockhash", result.lastBlockHash
if "genesisRLP" in fixture:
var genesisRLP: Blob
fixture.fromJson "genesisRLP", genesisRLP
result.genesisHeader = rlp.decode(genesisRLP, EthBlock).header
else:
result.genesisHeader = parseHeader(fixture["genesisBlockHeader"], testStatusIMPL)
var goodBlock = true
for h in result.blocks:
goodBlock = goodBlock and h.goodBlock
check goodBlock == false
if "sealEngine" in fixture:
result.sealEngine = some(parseEnum[SealEngine](fixture["sealEngine"].getStr))
if "postStateHash" in fixture:
result.postStateHash.data = hexToByteArray[32](fixture["postStateHash"].getStr)
result.network = fixture["network"].getStr
proc blockWitness(vmState: BaseVMState, chainDB: ChainDBRef) =
let rootHash = vmState.stateDB.rootHash
let witness = vmState.buildWitness()
let fork = vmState.fork
let flags = if fork >= FKSpurious: {wfEIP170} else: {}
# build tree from witness
var db = newMemoryDB()
when defined(useInputStream):
var input = memoryInput(witness)
var tb = initTreeBuilder(input, db, flags)
else:
var tb = initTreeBuilder(witness, db, flags)
let root = tb.buildTree()
# compare the result
if root != rootHash:
raise newException(ValidationError, "Invalid trie generated from block witness")
proc importBlock(tester: var Tester, com: CommonRef,
tb: TestBlock, checkSeal, validation: bool) =
let parentHeader = com.db.getBlockHeader(tb.header.parentHash)
let td = some(com.db.getScore(tb.header.parentHash))
com.hardForkTransition(tb.header.blockNumber, td, some(tb.header.timestamp))
tester.vmState = BaseVMState.new(
parentHeader,
tb.header,
com,
(if tester.trace: {TracerFlags.EnableTracing} else: {}),
)
if validation:
let rc = com.validateHeaderAndKinship(
tb.header, tb.body, checkSeal, pow)
if rc.isErr:
raise newException(
ValidationError, "validateHeaderAndKinship: " & rc.error)
let res = tester.vmState.processBlockNotPoA(tb.header, tb.body)
if res == ValidationResult.Error:
if not (tb.hasException or (not tb.goodBlock)):
raise newException(ValidationError, "process block validation")
else:
if tester.vmState.generateWitness():
blockWitness(tester.vmState, com.db)
discard com.db.persistHeaderToDb(tb.header,
com.consensus == ConsensusType.POS)
proc applyFixtureBlockToChain(tester: var Tester, tb: var TestBlock,
com: CommonRef, checkSeal, validation: bool) =
var rlp = rlpFromBytes(tb.blockRLP)
tb.header = rlp.read(EthHeader).header
tb.body = rlp.readRecordType(BlockBody, false)
tb.body.withdrawals = tb.withdrawals
tester.importBlock(com, tb, checkSeal, validation)
func shouldCheckSeal(tester: Tester): bool =
if tester.sealEngine.isSome:
result = tester.sealEngine.get() != NoProof
proc collectDebugData(tester: var Tester) =
if tester.vmState.isNil:
return
let vmState = tester.vmState
let tracingResult = if tester.trace: vmState.getTracingResult() else: %[]
tester.debugData.add %{
"blockNumber": %($vmState.blockNumber),
"structLogs": tracingResult,
}
proc runTester(tester: var Tester, com: CommonRef, testStatusIMPL: var TestStatus) =
discard com.db.persistHeaderToDb(tester.genesisHeader,
com.consensus == ConsensusType.POS)
check com.db.getCanonicalHead().blockHash == tester.genesisHeader.blockHash
let checkSeal = tester.shouldCheckSeal
if tester.debugMode:
tester.debugData = newJArray()
for idx, tb in tester.blocks:
if tb.goodBlock:
try:
tester.applyFixtureBlockToChain(
tester.blocks[idx], com, checkSeal, validation = false)
# manually validating
let res = com.validateHeaderAndKinship(
tb.header, tb.body, checkSeal, pow)
check res.isOk
when defined(noisy):
if res.isErr:
debugEcho "blockNumber : ", tb.header.blockNumber
debugEcho "fork : ", com.toHardFork(tb.header.blockNumber)
debugEcho "error message: ", res.error
debugEcho "consensusType: ", com.consensus
except:
debugEcho "FATAL ERROR(WE HAVE BUG): ", getCurrentExceptionMsg()
else:
var noError = true
try:
tester.applyFixtureBlockToChain(tester.blocks[idx],
com, checkSeal, validation = true)
except ValueError, ValidationError, BlockNotFound, RlpError:
# failure is expected on this bad block
check (tb.hasException or (not tb.goodBlock))
noError = false
if tester.debugMode:
tester.debugData.add %{
"exception": %($getCurrentException().name),
"msg": %getCurrentExceptionMsg()
}
# Block should have caused a validation error
check noError == false
if tester.debugMode:
tester.collectDebugData()
proc dumpAccount(stateDB: ReadOnlyStateDB, address: EthAddress, name: string): JsonNode =
result = %{
"name": %name,
"address": %($address),
"nonce": %toHex(stateDB.getNonce(address)),
"balance": %stateDB.getBalance(address).toHex(),
"codehash": %($stateDB.getCodeHash(address)),
"storageRoot": %($stateDB.getStorageRoot(address))
}
proc dumpDebugData(tester: Tester, vmState: BaseVMState, accountList: JsonNode): JsonNode =
var accounts = newJObject()
var i = 0
for ac, _ in accountList:
let account = ethAddressFromHex(ac)
accounts[$account] = dumpAccount(vmState.readOnlyStateDB, account, "acc" & $i)
inc i
%{
"debugData": tester.debugData,
"accounts": accounts
}
proc accountList(fixture: JsonNode): JsonNode =
if fixture["postState"].kind == JObject:
fixture["postState"]
else:
fixture["pre"]
proc debugDataFromAccountList(tester: Tester, fixture: JsonNode): JsonNode =
let accountList = fixture.accountList
let vmState = tester.vmState
if vmState.isNil:
%{"debugData": tester.debugData}
else:
dumpDebugData(tester, vmState, accountList)
proc debugDataFromPostStateHash(tester: Tester): JsonNode =
var
accounts = newJObject()
accountList = newSeq[EthAddress]()
vmState = tester.vmState
for address in vmState.stateDB.addresses:
accountList.add address
for i, ac in accountList:
accounts[ac.toHex] = dumpAccount(vmState.readOnlyStateDB, ac, "acc" & $i)
%{
"debugData": tester.debugData,
"postStateHash": %($vmState.readOnlyStateDB.rootHash),
"expectedStateHash": %($tester.postStateHash),
"accounts": accounts
}
proc dumpDebugData(tester: Tester, fixture: JsonNode, fixtureName: string, fixtureIndex: int, success: bool) =
let debugData = if tester.postStateHash != Hash256():
debugDataFromPostStateHash(tester)
else:
debugDataFromAccountList(tester, fixture)
let status = if success: "_success" else: "_failed"
writeFile("debug_" & fixtureName & "_" & $fixtureIndex & status & ".json", debugData.pretty())
proc testFixture(node: JsonNode, testStatusIMPL: var TestStatus, debugMode = false, trace = false) =
# 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)
var fixtureIndex = 0
var fixtureTested = false
for fixtureName, fixture in node:
inc fixtureIndex
if specifyIndex > 0 and fixtureIndex != specifyIndex:
continue
var tester = parseTester(fixture, testStatusIMPL)
let
pruneTrie = test_config.getConfiguration().pruning
memDB = newMemoryDb()
stateDB = AccountsCache.init(memDB, emptyRlpHash, pruneTrie)
config = getChainConfig(tester.network)
com = CommonRef.new(memDB, config, pruneTrie)
setupStateDB(fixture["pre"], stateDB)
stateDB.persist()
check stateDB.rootHash == tester.genesisHeader.stateRoot
tester.debugMode = debugMode
tester.trace = trace
var success = true
try:
tester.runTester(com, testStatusIMPL)
let header = com.db.getCanonicalHead()
let lastBlockHash = header.blockHash
check lastBlockHash == tester.lastBlockHash
success = lastBlockHash == tester.lastBlockHash
if tester.postStateHash != Hash256():
let rootHash = tester.vmState.stateDB.rootHash
if tester.postStateHash != rootHash:
raise newException(ValidationError, "incorrect postStateHash, expect=" &
$rootHash & ", get=" &
$tester.postStateHash
)
elif lastBlockHash == tester.lastBlockHash:
# multiple chain, we are using the last valid canonical
# state root to test against 'postState'
let stateDB = AccountsCache.init(memDB, header.stateRoot, pruneTrie)
verifyStateDB(fixture["postState"], ReadOnlyStateDB(stateDB))
success = lastBlockHash == tester.lastBlockHash
except ValidationError as E:
echo fixtureName, " ERROR: ", E.msg
success = false
if tester.debugMode:
tester.dumpDebugData(fixture, fixtureName, fixtureIndex, success)
fixtureTested = true
check success == true
if not fixtureTested:
echo test_config.getConfiguration().testSubject, " not tested at all, wrong index?"
if specifyIndex <= 0 or specifyIndex > node.len:
echo "Maximum subtest available: ", node.len
proc blockchainJsonMain*(debugMode = false) =
const
legacyFolder = "eth_tests" / "LegacyTests" / "Constantinople" / "BlockchainTests"
newFolder = "eth_tests" / "BlockchainTests"
let config = test_config.getConfiguration()
if config.testSubject == "" or not debugMode:
# run all test fixtures
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)
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
let n = json.parseFile(path / config.testSubject)
var testStatusIMPL: TestStatus
testFixture(n, testStatusIMPL, debugMode = true, config.trace)
when isMainModule:
var message: string
## Processing command line arguments
if test_config.processArguments(message) != test_config.Success:
echo message
quit(QuitFailure)
else:
if len(message) > 0:
echo message
quit(QuitSuccess)
blockchainJsonMain(true)
# 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
# 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