# 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, eth/common/eth_types_rlp, 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, debug], ../nimbus/core/[executor, validate, pow/header], ../stateless/[tree_from_witness, witness_types], ../tools/common/helpers as chp, ../tools/evmstate/helpers, ../nimbus/common/common type 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 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 == "0x0000000000000000000000000000000000000000": # withdrawalsAddressBounds contains this; it's meant as an address, not a number, # so it shouldn't be shortened to "0x0" result = n 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 t.withdrawals = none[seq[Withdrawal]]() 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) 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) = decompose(tb.blockRLP, tb.header, tb.body) 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) 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 CatchableError as ex: debugEcho "FATAL ERROR(WE HAVE BUG): ", ex.msg 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 debugDataFromAccountList(tester: Tester): JsonNode = let vmState = tester.vmState result = %{"debugData": tester.debugData} if not vmState.isNil: result["accounts"] = vmState.dumpAccounts() proc debugDataFromPostStateHash(tester: Tester): JsonNode = let vmState = tester.vmState %{ "debugData": tester.debugData, "postStateHash": %($vmState.readOnlyStateDB.rootHash), "expectedStateHash": %($tester.postStateHash), "accounts": vmState.dumpAccounts() } proc dumpDebugData(tester: Tester, fixtureName: string, fixtureIndex: int, success: bool) = let debugData = if tester.postStateHash != Hash256(): debugDataFromPostStateHash(tester) else: debugDataFromAccountList(tester) 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(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: import std/times var message: string let start = getTime() ## 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) let elpd = getTime() - start echo "TIME: ", elpd # 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