From cd21c4fbec012de06143b3c4b478dfca4745cda7 Mon Sep 17 00:00:00 2001 From: andri lim Date: Wed, 26 Jun 2024 07:27:48 +0700 Subject: [PATCH] ForkedChain implementation (#2405) * ForkedChain implementation - revamp test_blockchain_json using ForkedChain - re-enable previously failing test cases. * Remove excess error handling * Avoid reloading parent header * Do not force base update * Write baggage to database * Add findActiveChain to finalizedSegment * Create new stagingTx in addBlock * Check last stateRoot existence in test_blockchain_json * Resolve rebase conflict * More precise nomenclature for block import cursor * Ensure bad block nor imported and good block not rejected * finalizeSegment become forkChoice and align with engine API forkChoice spec * Display reason when good block rejected * Fix comments * Put BaseDistance into CalculateNewBase equation * Separate finalizedHash from baseHash * Add more doAssert constraint * Add push raises: [] --- newBlockchainTests.md | 72 +-- nimbus/core/chain/forked_chain.nim | 425 ++++++++++++++++++ nimbus/core/executor/process_block.nim | 31 +- tests/test_allowed_to_fail.nim | 41 +- tests/test_blockchain_json.nim | 599 +++++-------------------- 5 files changed, 591 insertions(+), 577 deletions(-) create mode 100644 nimbus/core/chain/forked_chain.nim diff --git a/newBlockchainTests.md b/newBlockchainTests.md index 0634d5d2e..088530044 100644 --- a/newBlockchainTests.md +++ b/newBlockchainTests.md @@ -22,17 +22,17 @@ OK: 15/15 Fail: 0/15 Skip: 0/15 ## bcArrowGlacierToParis ```diff + difficultyFormula.json OK - powToPosBlockRejection.json Skip ++ powToPosBlockRejection.json OK + powToPosTest.json OK ``` -OK: 2/3 Fail: 0/3 Skip: 1/3 +OK: 3/3 Fail: 0/3 Skip: 0/3 ## bcBerlinToLondon ```diff + BerlinToLondonTransition.json OK - initialVal.json Skip ++ initialVal.json OK + londonUncles.json OK ``` -OK: 2/3 Fail: 0/3 Skip: 1/3 +OK: 3/3 Fail: 0/3 Skip: 0/3 ## bcBlockGasLimitTest ```diff + BlockGasLimit2p63m1.json OK @@ -115,35 +115,35 @@ OK: 3/4 Fail: 0/4 Skip: 1/4 ## bcForkStressTest ```diff + AmIOnEIP150.json OK - ForkStressTest.json Skip ++ ForkStressTest.json OK ``` -OK: 1/2 Fail: 0/2 Skip: 1/2 +OK: 2/2 Fail: 0/2 Skip: 0/2 ## bcFrontierToHomestead ```diff + CallContractThatCreateContractBeforeAndAfterSwitchover.json OK + ContractCreationFailsOnHomestead.json OK - HomesteadOverrideFrontier.json Skip ++ HomesteadOverrideFrontier.json OK + UncleFromFrontierInHomestead.json OK + UnclePopulation.json OK - blockChainFrontierWithLargerTDvsHomesteadBlockchain.json Skip - blockChainFrontierWithLargerTDvsHomesteadBlockchain2.json Skip ++ blockChainFrontierWithLargerTDvsHomesteadBlockchain.json OK ++ blockChainFrontierWithLargerTDvsHomesteadBlockchain2.json OK ``` -OK: 4/7 Fail: 0/7 Skip: 3/7 +OK: 7/7 Fail: 0/7 Skip: 0/7 ## bcGasPricerTest ```diff - RPC_API_Test.json Skip ++ RPC_API_Test.json OK + highGasUsage.json OK + notxs.json OK ``` -OK: 2/3 Fail: 0/3 Skip: 1/3 +OK: 3/3 Fail: 0/3 Skip: 0/3 ## bcHomesteadToDao ```diff - DaoTransactions.json Skip ++ DaoTransactions.json OK + DaoTransactions_EmptyTransactionAndForkBlocksAhead.json OK + DaoTransactions_UncleExtradata.json OK + DaoTransactions_XBlockm1.json OK ``` -OK: 3/4 Fail: 0/4 Skip: 1/4 +OK: 4/4 Fail: 0/4 Skip: 0/4 ## bcHomesteadToEIP150 ```diff + EIP150Transition.json OK @@ -182,17 +182,17 @@ OK: 22/22 Fail: 0/22 Skip: 0/22 OK: 1/1 Fail: 0/1 Skip: 0/1 ## bcMultiChainTest ```diff - CallContractFromNotBestBlock.json Skip - ChainAtoChainB.json Skip - ChainAtoChainBCallContractFormA.json Skip - ChainAtoChainB_BlockHash.json Skip - ChainAtoChainB_difficultyB.json Skip - ChainAtoChainBtoChainA.json Skip - ChainAtoChainBtoChainAtoChainB.json Skip - UncleFromSideChain.json Skip - lotsOfLeafs.json Skip ++ CallContractFromNotBestBlock.json OK ++ ChainAtoChainB.json OK ++ ChainAtoChainBCallContractFormA.json OK ++ ChainAtoChainB_BlockHash.json OK ++ ChainAtoChainB_difficultyB.json OK ++ ChainAtoChainBtoChainA.json OK ++ ChainAtoChainBtoChainAtoChainB.json OK ++ UncleFromSideChain.json OK ++ lotsOfLeafs.json OK ``` -OK: 0/9 Fail: 0/9 Skip: 9/9 +OK: 9/9 Fail: 0/9 Skip: 0/9 ## bcRandomBlockhashTest ```diff + 201503110226PYTHON_DUP6BC.json OK @@ -408,18 +408,18 @@ OK: 105/105 Fail: 0/105 Skip: 0/105 OK: 99/100 Fail: 0/100 Skip: 1/100 ## bcTotalDifficultyTest ```diff - lotsOfBranchesOverrideAtTheEnd.json Skip - lotsOfBranchesOverrideAtTheMiddle.json Skip - newChainFrom4Block.json Skip - newChainFrom5Block.json Skip - newChainFrom6Block.json Skip - sideChainWithMoreTransactions.json Skip - sideChainWithMoreTransactions2.json Skip - sideChainWithNewMaxDifficultyStartingFromBlock3AfterBlock4.json Skip - uncleBlockAtBlock3AfterBlock3.json Skip - uncleBlockAtBlock3afterBlock4.json Skip ++ lotsOfBranchesOverrideAtTheEnd.json OK ++ lotsOfBranchesOverrideAtTheMiddle.json OK ++ newChainFrom4Block.json OK ++ newChainFrom5Block.json OK ++ newChainFrom6Block.json OK ++ sideChainWithMoreTransactions.json OK ++ sideChainWithMoreTransactions2.json OK ++ sideChainWithNewMaxDifficultyStartingFromBlock3AfterBlock4.json OK ++ uncleBlockAtBlock3AfterBlock3.json OK ++ uncleBlockAtBlock3afterBlock4.json OK ``` -OK: 0/10 Fail: 0/10 Skip: 10/10 +OK: 10/10 Fail: 0/10 Skip: 0/10 ## bcUncleHeaderValidity ```diff + correct.json OK @@ -3726,4 +3726,4 @@ OK: 11/11 Fail: 0/11 Skip: 0/11 OK: 1/1 Fail: 0/1 Skip: 0/1 ---TOTAL--- -OK: 3140/3272 Fail: 0/3272 Skip: 132/3272 +OK: 3167/3272 Fail: 0/3272 Skip: 105/3272 diff --git a/nimbus/core/chain/forked_chain.nim b/nimbus/core/chain/forked_chain.nim new file mode 100644 index 000000000..d6798e621 --- /dev/null +++ b/nimbus/core/chain/forked_chain.nim @@ -0,0 +1,425 @@ +# Nimbus +# Copyright (c) 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. + +{.push raises: [].} + +import + std/tables, + ../../common, + ../../db/core_db, + ../../evm/types, + ../../evm/state, + ../validate, + ../executor/process_block + +type + CursorDesc = object + forkJunction: BlockNumber + hash: Hash256 + + BlockDesc = object + blk: EthBlock + receipts: seq[Receipt] + + BaseDesc = object + hash: Hash256 + header: BlockHeader + + CanonicalDesc = object + cursorHash: Hash256 + header: BlockHeader + + ForkedChain* = object + stagingTx: CoreDbTxRef + db: CoreDbRef + com: CommonRef + blocks: Table[Hash256, BlockDesc] + baseHash: Hash256 + baseHeader: BlockHeader + cursorHash: Hash256 + cursorHeader: BlockHeader + cursorHeads: seq[CursorDesc] + +const + BaseDistance = 128 + +# ------------------------------------------------------------------------------ +# Private +# ------------------------------------------------------------------------------ +template shouldNotKeyError(body: untyped) = + try: + body + except KeyError as exc: + raiseAssert exc.msg + +proc processBlock(c: ForkedChain, + parent: BlockHeader, + blk: EthBlock): Result[seq[Receipt], string] = + template header(): BlockHeader = + blk.header + + let vmState = BaseVMState() + vmState.init(parent, header, c.com) + c.com.hardForkTransition(header) + + ?c.com.validateHeaderAndKinship(blk, vmState.parent, checkSealOK = false) + + ?vmState.processBlock( + blk, + skipValidation = false, + skipReceipts = false, + skipUncles = true, + ) + + # We still need to write header to database + # because validateUncles still need it + let blockHash = header.blockHash() + if not c.db.persistHeader( + blockHash, + header, c.com.consensus == ConsensusType.POS, + c.com.startOfHistory): + return err("Could not persist header") + + ok(move(vmState.receipts)) + +func updateCursorHeads(c: var ForkedChain, + cursorHash: Hash256, + header: BlockHeader) = + # Example of cursorHeads and cursor + # + # -- A1 - A2 - A3 -- D5 - D6 + # / / + # base - B1 - B2 - B3 - B4 + # \ + # --- C3 - C4 + # + # A3, B4, C4, and D6, are in cursorHeads + # Any one of them with blockHash == cursorHash + # is the active chain with cursor pointing to the + # latest block of that chain. + + for i in 0.. head.header.number: + c.blocks.del(prevHash) + else: + break + prevHash = header.parentHash + +# ------------------------------------------------------------------------------ +# Public functions +# ------------------------------------------------------------------------------ + +proc initForkedChain*(com: CommonRef, baseHeader: BlockHeader): ForkedChain = + result.com = com + result.db = com.db + result.baseHeader = baseHeader + result.cursorHash = baseHeader.blockHash + result.baseHash = result.cursorHash + result.cursorHeader = result.baseHeader + +proc importBlock*(c: var ForkedChain, blk: EthBlock): Result[void, string] = + # Try to import block to canonical or side chain. + # return error if the block is invalid + if c.stagingTx.isNil: + c.stagingTx = c.db.newTransaction() + + template header(): BlockHeader = + blk.header + + if header.parentHash == c.cursorHash: + return c.validateBlock(c.cursorHeader, blk) + + if header.parentHash == c.baseHash: + c.stagingTx.rollback() + c.stagingTx = c.db.newTransaction() + return c.validateBlock(c.baseHeader, blk) + + if header.parentHash notin c.blocks: + # If it's parent is an invalid block + # there is no hope the descendant is valid + return err("Block is not part of valid chain") + + # TODO: If engine API keep importing blocks + # but not finalized it, e.g. current chain length > StagedBlocksThreshold + # We need to persist some of the in-memory stuff + # to a "staging area" or disk-backed memory but it must not afect `base`. + # `base` is the point of no return, we only update it on finality. + + c.replaySegment(header.parentHash) + c.validateBlock(c.cursorHeader, blk) + +proc forkChoice*(c: var ForkedChain, + headHash: Hash256, + finalizedHash: Hash256): Result[void, string] = + + # If there are multiple heads, find which chain headHash belongs to + let head = ?c.findCanonicalHead(headHash) + + # Finalized block must be part of canonical chain + let finalizedHeader = ?c.canonicalChain(finalizedHash, headHash) + + let newBase = c.calculateNewBase( + finalizedHeader, headHash, head.header) + + if newBase.hash == c.baseHash: + # The base is not updated but the cursor maybe need update + if c.cursorHash != head.cursorHash: + if not c.stagingTx.isNil: + c.stagingTx.rollback() + c.stagingTx = c.db.newTransaction() + c.replaySegment(headHash) + + c.trimCanonicalChain(head) + if c.cursorHash != headHash: + c.cursorHeader = head.header + c.cursorHash = headHash + return ok() + + # At this point cursorHeader.number > baseHeader.number + if newBase.hash == c.cursorHash: + # Paranoid check, guaranteed by findCanonicalHead + doAssert(c.cursorHash == head.cursorHash) + + # Current segment is canonical chain + c.writeBaggage(newBase.hash) + + # Paranoid check, guaranteed by `newBase.hash == c.cursorHash` + doAssert(not c.stagingTx.isNil) + c.stagingTx.commit() + c.stagingTx = nil + + # Move base to newBase + c.updateBase(newBase.hash, c.cursorHeader, head.cursorHash) + + # Save and record the block number before the last saved block state. + c.db.persistent(c.cursorHeader.number).isOkOr: + return err("Failed to save state: " & $$error) + + return ok() + + # At this point finalizedHeader.number is <= headHeader.number + # and possibly switched to other chain beside the one with cursor + doAssert(finalizedHeader.number <= head.header.number) + doAssert(newBase.header.number <= finalizedHeader.number) + + # Write segment from base+1 to newBase into database + c.stagingTx.rollback() + c.stagingTx = c.db.newTransaction() + if newBase.header.number > c.baseHeader.number: + c.replaySegment(newBase.hash) + c.writeBaggage(newBase.hash) + c.stagingTx.commit() + c.stagingTx = nil + # Update base forward to newBase + c.updateBase(newBase.hash, newBase.header, head.cursorHash) + c.db.persistent(newBase.header.number).isOkOr: + return err("Failed to save state: " & $$error) + + # Move chain state forward to current head + if newBase.header.number < head.header.number: + if c.stagingTx.isNil: + c.stagingTx = c.db.newTransaction() + c.replaySegment(headHash) + + # Move cursor to current head + c.trimCanonicalChain(head) + if c.cursorHash != headHash: + c.cursorHeader = head.header + c.cursorHash = headHash + + ok() diff --git a/nimbus/core/executor/process_block.nim b/nimbus/core/executor/process_block.nim index 3e4941f9a..2002b2afd 100644 --- a/nimbus/core/executor/process_block.nim +++ b/nimbus/core/executor/process_block.nim @@ -117,7 +117,7 @@ proc procBlkPreamble( ok() proc procBlkEpilogue( - vmState: BaseVMState, header: BlockHeader, skipValidation: bool + vmState: BaseVMState, header: BlockHeader, skipValidation: bool, skipReceipts: bool ): Result[void, string] = # Reward beneficiary vmState.mutateStateDB: @@ -141,19 +141,20 @@ proc procBlkEpilogue( arrivedFrom = vmState.com.db.getCanonicalHead().stateRoot return err("stateRoot mismatch") - let bloom = createBloom(vmState.receipts) - - if header.logsBloom != bloom: - return err("bloom mismatch") - - let receiptsRoot = calcReceiptsRoot(vmState.receipts) - if header.receiptsRoot != receiptsRoot: - # TODO replace logging with better error - debug "wrong receiptRoot in block", - blockNumber = header.number, - actual = receiptsRoot, - expected = header.receiptsRoot - return err("receiptRoot mismatch") + if not skipReceipts: + let bloom = createBloom(vmState.receipts) + + if header.logsBloom != bloom: + return err("bloom mismatch") + + let receiptsRoot = calcReceiptsRoot(vmState.receipts) + if header.receiptsRoot != receiptsRoot: + # TODO replace logging with better error + debug "wrong receiptRoot in block", + blockNumber = header.number, + actual = receiptsRoot, + expected = header.receiptsRoot + return err("receiptRoot mismatch") ok() @@ -175,7 +176,7 @@ proc processBlock*( if vmState.com.consensus == ConsensusType.POW: vmState.calculateReward(blk.header, blk.uncles) - ?vmState.procBlkEpilogue(blk.header, skipValidation) + ?vmState.procBlkEpilogue(blk.header, skipValidation, skipReceipts) ok() diff --git a/tests/test_allowed_to_fail.nim b/tests/test_allowed_to_fail.nim index 683d80115..e8b3a7f2b 100644 --- a/tests/test_allowed_to_fail.nim +++ b/tests/test_allowed_to_fail.nim @@ -115,47 +115,10 @@ func skipBCTests*(folder: string, name: string): bool = "DelegateCallSpam.json", ] -# skip failing cases -# TODO: see issue #2260 -const - problematicCases = [ - "powToPosBlockRejection.json", - "initialVal.json", - "ForkStressTest.json", - "HomesteadOverrideFrontier.json", - "blockChainFrontierWithLargerTDvsHomesteadBlockchain.json", - "blockChainFrontierWithLargerTDvsHomesteadBlockchain2.json", - "RPC_API_Test.json", - "DaoTransactions.json", - "CallContractFromNotBestBlock.json", - "ChainAtoChainB.json", - "ChainAtoChainBCallContractFormA.json", - "ChainAtoChainB_BlockHash.json", - "ChainAtoChainB_difficultyB.json", - "ChainAtoChainBtoChainA.json", - "ChainAtoChainBtoChainAtoChainB.json", - "UncleFromSideChain.json", - "lotsOfLeafs.json", - "lotsOfBranchesOverrideAtTheEnd.json", - "lotsOfBranchesOverrideAtTheMiddle.json", - "newChainFrom4Block.json", - "newChainFrom5Block.json", - "newChainFrom6Block.json", - "sideChainWithMoreTransactions.json", - "sideChainWithMoreTransactions2.json", - "sideChainWithNewMaxDifficultyStartingFromBlock3AfterBlock4.json", - "uncleBlockAtBlock3AfterBlock3.json", - "uncleBlockAtBlock3afterBlock4.json", - ] - func skipNewBCTests*(folder: string, name: string): bool = if folder in ["vmPerformance"]: return true - - # TODO: fix this - if name in problematicCases: - return true - + # the new BC tests also contains these slow tests # for Istanbul fork if slowGSTTests(folder, name): @@ -166,7 +129,7 @@ func skipNewBCTests*(folder: string, name: string): bool = "randomStatetest94.json", "DelegateCallSpam.json", ] - + func skipPrecompilesTests*(folder: string, name: string): bool = # EIP2565: modExp gas cost # reason: included in berlin diff --git a/tests/test_blockchain_json.nim b/tests/test_blockchain_json.nim index aa5b3aa78..eae708887 100644 --- a/tests/test_blockchain_json.nim +++ b/tests/test_blockchain_json.nim @@ -9,521 +9,146 @@ # according to those terms. import - std/[json, os, tables, strutils, options, streams], + std/json, 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/[evm/state, evm/types, errors, constants], + ./test_helpers, + ./test_allowed_to_fail, ../nimbus/db/ledger, - ../nimbus/utils/[utils, debug], - ../nimbus/evm/tracer/legacy_tracer, - ../nimbus/evm/tracer/json_tracer, - ../nimbus/core/[validate, chain, pow/header], + ../nimbus/core/chain/forked_chain, ../tools/common/helpers as chp, ../tools/evmstate/helpers, ../nimbus/common/common, - ../nimbus/core/eip4844, - ../nimbus/rpc/experimental + ../nimbus/core/eip4844 + +const + debugMode = false type - SealEngine = enum - NoProof - Ethash + BlockDesc = object + blk: EthBlock + badBlock: bool - TestBlock = object - goodBlock: bool - blockRLP : Blob - header : BlockHeader - body : BlockBody - hasException: bool - withdrawals: Option[seq[Withdrawal]] - - TestCtx = object - lastBlockHash: Hash256 + TestEnv = object + blocks: seq[BlockDesc] genesisHeader: BlockHeader - blocks : seq[TestBlock] - sealEngine : Option[SealEngine] - debugMode : bool - trace : bool - vmState : BaseVMState - debugData : JsonNode - network : string - postStateHash: Hash256 - json : bool + lastBlockHash: Hash256 + network: string + pre: JsonNode -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 "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 == rlpHash(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", "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 - - result.add t - -proc parseTestCtx(fixture: JsonNode, testStatusIMPL: var TestStatus): TestCtx = - 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 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 - - let - chain = newChain(com, extraValidation = true, ctx.vmState) - res = chain.persistBlocks([EthBlock.init(tb.header, tb.body)]) - - 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) = - decompose(tb.blockRLP, tb.header, tb.body) - ctx.importBlock(com, tb, checkSeal) - -func shouldCheckSeal(ctx: TestCtx): bool = - if ctx.sealEngine.isSome: - result = ctx.sealEngine.get() != NoProof - -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 %{ - "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 - - if ctx.debugMode: - ctx.debugData = newJArray() - - for idx, tb in ctx.blocks: - if tb.goodBlock: - try: - - ctx.applyFixtureBlockToChain( - ctx.blocks[idx], com, checkSeal) - - except CatchableError as ex: - debugEcho "FATAL ERROR(WE HAVE BUG): ", ex.msg - - else: - var noError = true - try: - ctx.applyFixtureBlockToChain(ctx.blocks[idx], - com, checkSeal) - except ValueError, ValidationError, BlockNotFound, RlpError: - # failure is expected on this bad block - check (tb.hasException or (not tb.goodBlock)) - noError = false - if ctx.debugMode: - ctx.debugData.add %{ - "exception": %($getCurrentException().name), - "msg": %getCurrentExceptionMsg() - } - - # Block should have caused a validation error - check noError == false - - if ctx.debugMode and not ctx.json: - ctx.collectDebugData() - -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) - - let status = if success: "_success" else: "_failed" - let name = fixtureName.replace('/', '-').replace(':', '-') - writeFile("debug_" & name & "_" & $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 ctx = parseTestCtx(fixture, testStatusIMPL) - - let - memDB = newCoreDbRef DefaultDbMemory - stateDB = LedgerRef.init(memDB, emptyRlpHash) - config = getChainConfig(ctx.network) - com = CommonRef.new(memDB, config) - - setupStateDB(fixture["pre"], stateDB) - stateDB.persist() - - check stateDB.rootHash == ctx.genesisHeader.stateRoot - - ctx.debugMode = debugMode - ctx.trace = trace - ctx.json = test_config.getConfiguration().json - - var success = true +proc parseBlocks(node: JsonNode): seq[BlockDesc] = + for x in node: 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)) + let blockRLP = hexToSeqByte(x["rlp"].getStr) + let blk = rlp.decode(blockRLP, EthBlock) + result.add BlockDesc( + blk: blk, + badBlock: "expectException" in x, + ) + except RlpError: + # invalid rlp will not participate in block validation + # e.g. invalid rlp received from network + discard - success = lastBlockHash == ctx.lastBlockHash - except ValidationError as E: - echo fixtureName, " ERROR: ", E.msg - success = false +proc parseEnv(node: JsonNode): TestEnv = + result.blocks = parseBlocks(node["blocks"]) + let genesisRLP = hexToSeqByte(node["genesisRLP"].getStr) + result.genesisHeader = rlp.decode(genesisRLP, EthBlock).header + result.lastBlockHash = Hash256(data: hexToByteArray[32](node["lastblockhash"].getStr)) + result.network = node["network"].getStr + result.pre = node["pre"] - if ctx.debugMode: - ctx.dumpDebugData(fixtureName, fixtureIndex, success) +proc rootExists(db: CoreDbRef; root: Hash256): bool = + let + ctx = db.ctx + col = ctx.newColumn(CtAccounts, root).valueOr: + return false + ctx.getAcc(col).isOkOr: + return false + true - fixtureTested = true - check success == true +proc executeCase(node: JsonNode): bool = + let + env = parseEnv(node) + memDB = newCoreDbRef DefaultDbMemory + stateDB = LedgerRef.init(memDB, EMPTY_ROOT_HASH) + config = getChainConfig(env.network) + com = CommonRef.new(memDB, config) - 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 + setupStateDB(env.pre, stateDB) + stateDB.persist() -proc blockchainJsonMain*(debugMode = false) = + if not com.db.persistHeader(env.genesisHeader, + com.consensus == ConsensusType.POS): + debugEcho "Failed to put genesis header into database" + return false + + if com.db.getCanonicalHead().blockHash != env.genesisHeader.blockHash: + debugEcho "Genesis block hash is database different with expected genesis block hash" + return false + + var c = initForkedChain(com, env.genesisHeader) + var lastStateRoot = env.genesisHeader.stateRoot + for blk in env.blocks: + let res = c.importBlock(blk.blk) + if res.isOk: + if env.lastBlockHash == blk.blk.header.blockHash: + lastStateRoot = blk.blk.header.stateRoot + if blk.badBlock: + debugEcho "A bug? bad block imported" + return false + else: + if not blk.badBlock: + debugEcho "A bug? good block rejected: ", res.error + return false + + c.forkChoice(env.lastBlockHash, env.lastBlockHash).isOkOr: + debugEcho error + return false + + let head = com.db.getCanonicalHead() + let headHash = head.blockHash + if headHash != env.lastBlockHash: + debugEcho "lastestBlockHash mismatch, get: ", headHash, + " expect: ", env.lastBlockHash + return false + + if not memDB.rootExists(lastStateRoot): + debugEcho "Last stateRoot not exists" + return false + + true + +proc executeFile(node: JsonNode, testStatusIMPL: var TestStatus) = + for name, bctCase in node: + when debugMode: + debugEcho "TEST NAME: ", name + check executeCase(bctCase) + +proc blockchainJsonMain*() = 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) - 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) + if false: + suite "block chain json tests": + jsonTest(legacyFolder, "BlockchainTests", executeFile, skipBCTests) 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) + suite "new block chain json tests": + jsonTest(newFolder, "newBlockchainTests", executeFile, skipNewBCTests) when isMainModule: - import std/times - var message: string + when debugMode: + proc executeFile(name: string) = + var testStatusIMPL: TestStatus + let node = json.parseFile(name) + executeFile(node, testStatusIMPL) - let start = getTime() - - ## Processing command line arguments - if test_config.processArguments(message) != test_config.Success: - echo message - quit(QuitFailure) + executeFile("tests/fixtures/eth_tests/BlockchainTests/ValidBlocks/bcTotalDifficultyTest/sideChainWithMoreTransactions.json") 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 + blockchainJsonMain()