From 401537ad38b63907a87cf4806cde8ad99ad26160 Mon Sep 17 00:00:00 2001 From: andri lim Date: Sun, 30 Jun 2024 14:40:14 +0700 Subject: [PATCH] Add ForkedChainRef tests (#2430) ForkedChainRef have become quite complex. test_blockchain_json is not sufficient cover for edge cases or synthetic cases. --- nimbus/core/chain/forked_chain.nim | 192 +++++++++++-- tests/test_forked_chain.nim | 428 +++++++++++++++++++++++++++++ 2 files changed, 594 insertions(+), 26 deletions(-) create mode 100644 tests/test_forked_chain.nim diff --git a/nimbus/core/chain/forked_chain.nim b/nimbus/core/chain/forked_chain.nim index b9c279dc6..5ab74b4e6 100644 --- a/nimbus/core/chain/forked_chain.nim +++ b/nimbus/core/chain/forked_chain.nim @@ -19,6 +19,10 @@ import ../validate, ../executor/process_block +export + common, + core_db + type CursorDesc = object forkJunction: BlockNumber @@ -46,6 +50,8 @@ type cursorHash: Hash256 cursorHeader: BlockHeader cursorHeads: seq[CursorDesc] + extraValidation: bool + baseDistance: uint64 const BaseDistance = 128 @@ -69,7 +75,8 @@ proc processBlock(c: ForkedChainRef, vmState.init(parent, header, c.com) c.com.hardForkTransition(header) - ?c.com.validateHeaderAndKinship(blk, vmState.parent, checkSealOK = false) + if c.extraValidation: + ?c.com.validateHeaderAndKinship(blk, vmState.parent, checkSealOK = false) ?vmState.processBlock( blk, @@ -83,10 +90,15 @@ proc processBlock(c: ForkedChainRef, let blockHash = header.blockHash() if not c.db.persistHeader( blockHash, - header, c.com.consensus == ConsensusType.POS, + header, c.com.startOfHistory): return err("Could not persist header") + # update currentBlock *after* we persist it + # so the rpc return consistent result + # between eth_blockNumber and eth_syncing + c.com.syncCurrent = header.number + ok(move(vmState.receipts)) func updateCursorHeads(c: ForkedChainRef, @@ -166,6 +178,29 @@ proc replaySegment(c: ForkedChainRef, target: Hash256) = c.validateBlock(c.cursorHeader, chain[i], updateCursor = false).expect("have been validated before") c.cursorHeader = chain[i].header + c.cursorHash = target + +proc replaySegment(c: ForkedChainRef, + target: Hash256, + parent: BlockHeader, + parentHash: Hash256) = + # Replay from parent+1 to target block + # with assumption last state is at parent + var + prevHash = target + chain = newSeq[EthBlock]() + + shouldNotKeyError: + while prevHash != parentHash: + chain.add c.blocks[prevHash].blk + prevHash = chain[^1].header.parentHash + + c.cursorHeader = parent + for i in countdown(chain.high, chain.low): + c.validateBlock(c.cursorHeader, chain[i], + updateCursor = false).expect("have been validated before") + c.cursorHeader = chain[i].header + c.cursorHash = target proc writeBaggage(c: ForkedChainRef, target: Hash256) = # Write baggage from base+1 to target block @@ -244,12 +279,12 @@ func findCanonicalHead(c: ForkedChainRef, shouldNotKeyError: # Find hash belong to which chain for cursor in c.cursorHeads: - let header = c.blocks[cursor.hash].blk.header var prevHash = cursor.hash while prevHash != c.baseHash: + let header = c.blocks[prevHash].blk.header if prevHash == hash: return ok(CanonicalDesc(cursorHash: cursor.hash, header: header)) - prevHash = c.blocks[prevHash].blk.header.parentHash + prevHash = header.parentHash err("Block hash is not part of any active chain") @@ -273,14 +308,14 @@ func calculateNewBase(c: ForkedChainRef, finalizedHeader: BlockHeader, headHash: Hash256, headHeader: BlockHeader): BaseDesc = - # It's important to have base at least `BaseDistance` behind head + # It's important to have base at least `baseDistance` behind head # so we can answer state queries about history that deep. let targetNumber = min(finalizedHeader.number, - max(headHeader.number, BaseDistance) - BaseDistance) + max(headHeader.number, c.baseDistance) - c.baseDistance) - # The distance is less than `BaseDistance`, don't move the base - if targetNumber - c.baseHeader.number <= BaseDistance: + # The distance is less than `baseDistance`, don't move the base + if targetNumber - c.baseHeader.number <= c.baseDistance: return BaseDesc(hash: c.baseHash, header: c.baseHeader) shouldNotKeyError: @@ -293,7 +328,9 @@ func calculateNewBase(c: ForkedChainRef, doAssert(false, "Unreachable code") -func trimCanonicalChain(c: ForkedChainRef, head: CanonicalDesc) = +func trimCanonicalChain(c: ForkedChainRef, + head: CanonicalDesc, + headHash: Hash256) = # Maybe the current active chain is longer than canonical chain shouldNotKeyError: var prevHash = head.cursorHash @@ -305,18 +342,56 @@ func trimCanonicalChain(c: ForkedChainRef, head: CanonicalDesc) = break prevHash = header.parentHash + if c.cursorHeads.len == 0: + return + + # Update cursorHeads if indeed we trim + for i in 0.. baseHeader.number if newBase.hash == c.cursorHash: - # Paranoid check, guaranteed by findCanonicalHead - doAssert(c.cursorHash == head.cursorHash) + # Paranoid check, guaranteed by `newBase.hash == c.cursorHash` + doAssert(not c.stagingTx.isNil) + + # CL decide to move backward and then forward? + if c.cursorHeader.number < head.header.number: + c.replaySegment(headHash, c.cursorHeader, c.cursorHash) # Current segment is canonical chain c.writeBaggage(newBase.hash) + c.setHead(headHash, head.header.number) - # Paranoid check, guaranteed by `newBase.hash == c.cursorHash` - doAssert(not c.stagingTx.isNil) c.stagingTx.commit() c.stagingTx = nil @@ -393,7 +477,7 @@ proc forkChoice*(c: ForkedChainRef, 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: + c.db.persistent(newBase.header.number).isOkOr: return err("Failed to save state: " & $$error) return ok() @@ -406,6 +490,7 @@ proc forkChoice*(c: ForkedChainRef, # 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) @@ -416,16 +501,71 @@ proc forkChoice*(c: ForkedChainRef, c.db.persistent(newBase.header.number).isOkOr: return err("Failed to save state: " & $$error) + if c.stagingTx.isNil: + # replaySegment or setHead below don't + # go straight to db + c.stagingTx = c.db.newTransaction() + # 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) + c.setHead(headHash, head.header.number) + # Move cursor to current head - c.trimCanonicalChain(head) + c.trimCanonicalChain(head, headHash) if c.cursorHash != headHash: c.cursorHeader = head.header c.cursorHash = headHash ok() + +func haveBlockAndState*(c: ForkedChainRef, hash: Hash256): bool = + if c.blocks.hasKey(hash): + return true + if c.baseHash == hash: + return true + false + +func stateReady*(c: ForkedChainRef, header: BlockHeader): bool = + let blockHash = header.blockHash + blockHash == c.cursorHash + +func com*(c: ForkedChainRef): CommonRef = + c.com + +func db*(c: ForkedChainRef): CoreDbRef = + c.db + +func latestHeader*(c: ForkedChainRef): BlockHeader = + c.cursorHeader + +func latestHash*(c: ForkedChainRef): Hash256 = + c.cursorHash + +proc headerByNumber*(c: ForkedChainRef, number: BlockNumber): Result[BlockHeader, string] = + if number > c.cursorHeader.number: + return err("Requested block number not exists: " & $number) + + if number == c.cursorHeader.number: + return ok(c.cursorHeader) + + if number == c.baseHeader.number: + return ok(c.baseHeader) + + if number < c.baseHeader.number: + var header: BlockHeader + if c.db.getBlockHeader(number, header): + return ok(header) + else: + return err("Failed to get block with number: " & $number) + + shouldNotKeyError: + var prevHash = c.cursorHeader.parentHash + while prevHash != c.baseHash: + let header = c.blocks[prevHash].blk.header + if header.number == number: + return ok(header) + prevHash = header.parentHash + + doAssert(false, "headerByNumber: Unreachable code") diff --git a/tests/test_forked_chain.nim b/tests/test_forked_chain.nim new file mode 100644 index 000000000..65bad5d77 --- /dev/null +++ b/tests/test_forked_chain.nim @@ -0,0 +1,428 @@ +# 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 + stew/byteutils, + ../nimbus/common, + ../nimbus/config, + ../nimbus/utils/utils, + ../nimbus/core/chain/forked_chain, + ../nimbus/db/ledger, + unittest2 + +const + genesisFile = "tests/customgenesis/cancun123.json" + senderAddr = hexToByteArray[20]("73cf19657412508833f618a15e8251306b3e6ee5") + +type + TestEnv = object + conf: NimbusConf + +proc setupEnv(): TestEnv = + let + conf = makeConfig(@[ + "--custom-network:" & genesisFile + ]) + + TestEnv(conf: conf) + +proc newCom(env: TestEnv): CommonRef = + let + com = CommonRef.new( + newCoreDbRef DefaultDbMemory, + env.conf.networkId, + env.conf.networkParams + ) + + com.initializeEmptyDb() + com + +proc makeBlk(com: CommonRef, number: BlockNumber, parentBlk: EthBlock): EthBlock = + template parent(): BlockHeader = + parentBlk.header + + var wds = newSeqOfCap[Withdrawal](number.int) + for i in 0.. head -> error + check chain.forkChoice(blk1.blockHash, blk3.blockHash).isErr + + # blk4 is not part of chain + check chain.forkChoice(blk4.blockHash, blk2.blockHash).isErr + + # finalized > head -> error + check chain.forkChoice(blk1.blockHash, blk2.blockHash).isErr + + # blk4 is not part of chain + check chain.forkChoice(blk2.blockHash, blk4.blockHash).isErr + + # finalized < head -> ok + check chain.forkChoice(blk2.blockHash, blk1.blockHash).isOk + check com.headHash == blk2.blockHash + check chain.latestHash == blk2.blockHash + + # finalized == head -> ok + check chain.forkChoice(blk2.blockHash, blk2.blockHash).isOk + check com.headHash == blk2.blockHash + check chain.latestHash == blk2.blockHash + + # no baggage written + check com.wdWritten(blk1) == 0 + check com.wdWritten(blk2) == 0 + + test "newBase == cursor": + let com = env.newCom() + + var chain = newForkedChain(com, com.genesisHeader, baseDistance = 3) + check chain.importBlock(blk1).isOk + check chain.importBlock(blk2).isOk + check chain.importBlock(blk3).isOk + check chain.importBlock(blk4).isOk + check chain.importBlock(blk5).isOk + check chain.importBlock(blk6).isOk + check chain.importBlock(blk7).isOk + + check chain.importBlock(blk4).isOk + + # newbase == cursor + check chain.forkChoice(blk7.blockHash, blk6.blockHash).isOk + + check com.headHash == blk7.blockHash + check chain.latestHash == blk7.blockHash + + check com.wdWritten(blk7) == 0 + + # head - baseDistance must been finalized + check com.wdWritten(blk4) == 4 + # make sure aristo not wiped out baggage + check com.wdWritten(blk3) == 3 + + test "newBase between oldBase and cursor": + let com = env.newCom() + + var chain = newForkedChain(com, com.genesisHeader, baseDistance = 3) + check chain.importBlock(blk1).isOk + check chain.importBlock(blk2).isOk + check chain.importBlock(blk3).isOk + check chain.importBlock(blk4).isOk + check chain.importBlock(blk5).isOk + check chain.importBlock(blk6).isOk + check chain.importBlock(blk7).isOk + + check chain.forkChoice(blk7.blockHash, blk6.blockHash).isOk + + check com.headHash == blk7.blockHash + check chain.latestHash == blk7.blockHash + + check com.wdWritten(blk6) == 0 + check com.wdWritten(blk7) == 0 + + # head - baseDistance must been finalized + check com.wdWritten(blk4) == 4 + # make sure aristo not wiped out baggage + check com.wdWritten(blk3) == 3 + + test "newBase == oldBase, fork and keep on that fork": + let com = env.newCom() + + var chain = newForkedChain(com, com.genesisHeader) + check chain.importBlock(blk1).isOk + check chain.importBlock(blk2).isOk + check chain.importBlock(blk3).isOk + check chain.importBlock(blk4).isOk + check chain.importBlock(blk5).isOk + check chain.importBlock(blk6).isOk + check chain.importBlock(blk7).isOk + + check chain.importBlock(B4).isOk + check chain.importBlock(B5).isOk + check chain.importBlock(B6).isOk + check chain.importBlock(B7).isOk + + check chain.forkChoice(B7.blockHash, B5.blockHash).isOk + + check com.headHash == B7.blockHash + check chain.latestHash == B7.blockHash + + test "newBase == cursor, fork and keep on that fork": + let com = env.newCom() + + var chain = newForkedChain(com, com.genesisHeader, baseDistance = 3) + check chain.importBlock(blk1).isOk + check chain.importBlock(blk2).isOk + check chain.importBlock(blk3).isOk + check chain.importBlock(blk4).isOk + check chain.importBlock(blk5).isOk + check chain.importBlock(blk6).isOk + check chain.importBlock(blk7).isOk + + check chain.importBlock(B4).isOk + check chain.importBlock(B5).isOk + check chain.importBlock(B6).isOk + check chain.importBlock(B7).isOk + + check chain.importBlock(B4).isOk + + check chain.forkChoice(B7.blockHash, B6.blockHash).isOk + + check com.headHash == B7.blockHash + check chain.latestHash == B7.blockHash + + test "newBase between oldBase and cursor, fork and keep on that fork": + let com = env.newCom() + + var chain = newForkedChain(com, com.genesisHeader, baseDistance = 3) + check chain.importBlock(blk1).isOk + check chain.importBlock(blk2).isOk + check chain.importBlock(blk3).isOk + check chain.importBlock(blk4).isOk + check chain.importBlock(blk5).isOk + check chain.importBlock(blk6).isOk + check chain.importBlock(blk7).isOk + + check chain.importBlock(B4).isOk + check chain.importBlock(B5).isOk + check chain.importBlock(B6).isOk + check chain.importBlock(B7).isOk + + check chain.forkChoice(B7.blockHash, B5.blockHash).isOk + + check com.headHash == B7.blockHash + check chain.latestHash == B7.blockHash + + test "newBase == oldBase, fork and return to old chain": + let com = env.newCom() + + var chain = newForkedChain(com, com.genesisHeader) + check chain.importBlock(blk1).isOk + check chain.importBlock(blk2).isOk + check chain.importBlock(blk3).isOk + check chain.importBlock(blk4).isOk + check chain.importBlock(blk5).isOk + check chain.importBlock(blk6).isOk + check chain.importBlock(blk7).isOk + + check chain.importBlock(B4).isOk + check chain.importBlock(B5).isOk + check chain.importBlock(B6).isOk + check chain.importBlock(B7).isOk + + check chain.forkChoice(blk7.blockHash, blk5.blockHash).isOk + + check com.headHash == blk7.blockHash + check chain.latestHash == blk7.blockHash + + test "newBase == cursor, fork and return to old chain": + let com = env.newCom() + + var chain = newForkedChain(com, com.genesisHeader, baseDistance = 3) + check chain.importBlock(blk1).isOk + check chain.importBlock(blk2).isOk + check chain.importBlock(blk3).isOk + check chain.importBlock(blk4).isOk + check chain.importBlock(blk5).isOk + check chain.importBlock(blk6).isOk + check chain.importBlock(blk7).isOk + + check chain.importBlock(B4).isOk + check chain.importBlock(B5).isOk + check chain.importBlock(B6).isOk + check chain.importBlock(B7).isOk + + check chain.importBlock(blk4).isOk + + check chain.forkChoice(blk7.blockHash, blk5.blockHash).isOk + + check com.headHash == blk7.blockHash + check chain.latestHash == blk7.blockHash + + test "newBase between oldBase and cursor, fork and return to old chain, switch to new chain": + let com = env.newCom() + + var chain = newForkedChain(com, com.genesisHeader, baseDistance = 3) + check chain.importBlock(blk1).isOk + check chain.importBlock(blk2).isOk + check chain.importBlock(blk3).isOk + check chain.importBlock(blk4).isOk + check chain.importBlock(blk5).isOk + check chain.importBlock(blk6).isOk + check chain.importBlock(blk7).isOk + + check chain.importBlock(B4).isOk + check chain.importBlock(B5).isOk + check chain.importBlock(B6).isOk + check chain.importBlock(B7).isOk + + check chain.importBlock(blk4).isOk + + check chain.forkChoice(B7.blockHash, B5.blockHash).isOk + + check com.headHash == B7.blockHash + check chain.latestHash == B7.blockHash + + test "newBase between oldBase and cursor, fork and return to old chain": + let com = env.newCom() + + var chain = newForkedChain(com, com.genesisHeader, baseDistance = 3) + check chain.importBlock(blk1).isOk + check chain.importBlock(blk2).isOk + check chain.importBlock(blk3).isOk + check chain.importBlock(blk4).isOk + check chain.importBlock(blk5).isOk + check chain.importBlock(blk6).isOk + check chain.importBlock(blk7).isOk + + check chain.importBlock(B4).isOk + check chain.importBlock(B5).isOk + check chain.importBlock(B6).isOk + check chain.importBlock(B7).isOk + + check chain.forkChoice(blk7.blockHash, blk5.blockHash).isOk + + check com.headHash == blk7.blockHash + check chain.latestHash == blk7.blockHash + + test "headerByNumber": + let com = env.newCom() + + var chain = newForkedChain(com, com.genesisHeader, baseDistance = 3) + check chain.importBlock(blk1).isOk + check chain.importBlock(blk2).isOk + check chain.importBlock(blk3).isOk + check chain.importBlock(blk4).isOk + check chain.importBlock(blk5).isOk + check chain.importBlock(blk6).isOk + check chain.importBlock(blk7).isOk + + check chain.importBlock(B4).isOk + check chain.importBlock(B5).isOk + check chain.importBlock(B6).isOk + check chain.importBlock(B7).isOk + + check chain.forkChoice(blk7.blockHash, blk5.blockHash).isOk + + # cursor + check chain.headerByNumber(8).isErr + check chain.headerByNumber(7).expect("OK").number == 7 + check chain.headerByNumber(7).expect("OK").blockHash == blk7.blockHash + + # from db + check chain.headerByNumber(3).expect("OK").number == 3 + check chain.headerByNumber(3).expect("OK").blockHash == blk3.blockHash + + # base + check chain.headerByNumber(4).expect("OK").number == 4 + check chain.headerByNumber(4).expect("OK").blockHash == blk4.blockHash + + # from cache + check chain.headerByNumber(5).expect("OK").number == 5 + check chain.headerByNumber(5).expect("OK").blockHash == blk5.blockHash + +when isMainModule: + forkedChainMain()