From 06249abc0159354e8163f45f6fb7737c370d786b Mon Sep 17 00:00:00 2001 From: jangko Date: Fri, 16 Sep 2022 11:23:25 +0700 Subject: [PATCH] add beacon sync skeleton test --- nimbus/p2p/chain/chain_desc.nim | 13 + nimbus/p2p/chain/persist_blocks.nim | 8 +- tests/customgenesis/post-merge.json | 33 ++ tests/test_skeleton.nim | 648 ++++++++++++++++++++++++++++ 4 files changed, 700 insertions(+), 2 deletions(-) create mode 100644 tests/customgenesis/post-merge.json create mode 100644 tests/test_skeleton.nim diff --git a/nimbus/p2p/chain/chain_desc.nim b/nimbus/p2p/chain/chain_desc.nim index ddfe33385..7251af13b 100644 --- a/nimbus/p2p/chain/chain_desc.nim +++ b/nimbus/p2p/chain/chain_desc.nim @@ -52,6 +52,10 @@ type blockZeroStateRoot: KeccakHash + validateBlock: bool ##\ + ## If turn off, `persistBlocks` will always return + ## ValidationResult.OK and disable extraValidation too. + extraValidation: bool ##\ ## Trigger extra validation, currently within `persistBlocks()` ## function only. @@ -167,6 +171,7 @@ proc initChain(c: Chain; db: BaseChainDB; poa: Clique; extraValidation: bool) if not db.config.daoForkSupport: db.config.daoForkBlock = db.config.homesteadBlock + c.validateBlock = true c.extraValidation = extraValidation c.setForkId() @@ -246,6 +251,10 @@ proc db*(c: Chain): BaseChainDB = ## Getter c.db +proc validateBlock*(c: Chain): bool = + ## Getter + c.validateBlock + proc extraValidation*(c: Chain): bool = ## Getter c.extraValidation @@ -268,6 +277,10 @@ proc currentBlock*(c: Chain): BlockHeader # ------------------------------------------------------------------------------ # Public `Chain` setters # ------------------------------------------------------------------------------ +proc `validateBlock=`*(c: Chain; validateBlock: bool) = + ## Setter. If set `true`, the assignment value `validateBlock` enables + ## block execution, else it will always return ValidationResult.OK + c.validateBlock = validateBlock proc `extraValidation=`*(c: Chain; extraValidation: bool) = ## Setter. If set `true`, the assignment value `extraValidation` enables diff --git a/nimbus/p2p/chain/persist_blocks.nim b/nimbus/p2p/chain/persist_blocks.nim index 90583556d..47f628d19 100644 --- a/nimbus/p2p/chain/persist_blocks.nim +++ b/nimbus/p2p/chain/persist_blocks.nim @@ -76,7 +76,10 @@ proc persistBlocksImpl(c: Chain; headers: openArray[BlockHeader]; return ValidationResult.Error let - validationResult = vmState.processBlock(c.clique, header, body) + validationResult = if c.validateBlock: + vmState.processBlock(c.clique, header, body) + else: + ValidationResult.OK when not defined(release): if validationResult == ValidationResult.Error and body.transactions.calcTxRoot == header.txRoot: @@ -86,7 +89,8 @@ proc persistBlocksImpl(c: Chain; headers: openArray[BlockHeader]; if validationResult != ValidationResult.OK: return validationResult - if c.extraValidation and c.verifyFrom <= header.blockNumber: + if c.validateBlock and c.extraValidation and + c.verifyFrom <= header.blockNumber: let isBlockAfterTtd = c.isBlockAfterTtd(header) if c.db.config.poaEngine and not isBlockAfterTtd: var parent = if 0 < i: @[headers[i-1]] else: @[] diff --git a/tests/customgenesis/post-merge.json b/tests/customgenesis/post-merge.json new file mode 100644 index 000000000..dbe6e4dd4 --- /dev/null +++ b/tests/customgenesis/post-merge.json @@ -0,0 +1,33 @@ +{ + "config": { + "chainId": 1, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "muirGlacierBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "terminalTotalDifficulty": 0 + }, + "genesis": { + "nonce": "0x42", + "timestamp": "0x0", + "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "gasLimit": "0x1C9C380", + "difficulty": "0x400000000", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { "balance": "0x6d6172697573766477000000" } + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "baseFeePerGas": "0x7" + } +} diff --git a/tests/test_skeleton.nim b/tests/test_skeleton.nim new file mode 100644 index 000000000..898de8501 --- /dev/null +++ b/tests/test_skeleton.nim @@ -0,0 +1,648 @@ +import + unittest2, + chronicles, + stew/[results, byteutils], + eth/[common, trie/db], + ../nimbus/sync/skeleton, + ../nimbus/db/db_chain, + ../nimbus/p2p/chain, + ../nimbus/[chain_config, config, genesis, constants], + ./test_helpers, + ./test_txpool/helpers + +const + baseDir = [".", "tests"] + repoDir = [".", "customgenesis"] + genesisFile = "post-merge.json" + +type + Subchain = object + head: int + tail: int + + TestEnv = object + conf : NimbusConf + chainDB : BaseChainDB + chain : Chain + + CCModify = proc(cc: NetworkParams) + +# TODO: too bad that blockHash +# cannot be executed at compile time +let + block49 = BlockHeader( + blockNumber: 49.toBlockNumber + ) + block49B = BlockHeader( + blockNumber: 49.toBlockNumber, + extraData: @['B'.byte] + ) + block50 = BlockHeader( + blockNumber: 50.toBlockNumber, + parentHash: block49.blockHash + ) + block51 = BlockHeader( + blockNumber: 51.toBlockNumber, + parentHash: block50.blockHash + ) + +proc initEnv(ccm: CCModify = nil): TestEnv = + let + conf = makeConfig(@[ + "--custom-network:" & genesisFile.findFilePath(baseDir,repoDir).value + ]) + + if ccm.isNil.not: + ccm(conf.networkParams) + + let + chainDB = newBaseChainDB( + newMemoryDb(), + conf.pruneMode == PruneMode.Full, + conf.networkId, + conf.networkParams + ) + chain = newChain(chainDB) + + initializeEmptyDb(chainDB) + + result = TestEnv( + conf: conf, + chainDB: chainDB, + chain: chain + ) + +proc `subchains=`(sk: SkeletonRef, subchains: openArray[Subchain]) = + var sc = newSeqOfCap[SkeletonSubchain](subchains.len) + for i in 0.. 0: + skeleton.subchains = testCase.oldState + + skeleton.initSync(testCase.head) + + check skeleton.len == testCase.newState.len + for i, sc in skeleton: + check sc.head == testCase.newState[i].head.toBlockNumber + check sc.tail == testCase.newState[i].tail.toBlockNumber + +suite "sync extend": + type + TestCase = object + head : BlockHeader # New head header to announce to reorg to + extend : BlockHeader # New head header to announce to extend with + newState: seq[Subchain] # Expected sync state after the reorg + err : string # Whether extension succeeds or not + + let testCases = [ + # Initialize a sync and try to extend it with a subsequent block. + TestCase( + head: block49, + extend: block50, + newState: @[Subchain(head: 50, tail: 49)], + ), + # Initialize a sync and try to extend it with the existing head block. + TestCase( + head: block49, + extend: block49, + newState: @[Subchain(head: 49, tail: 49)], + ), + # Initialize a sync and try to extend it with a sibling block. + TestCase( + head: block49, + extend: block49B, + newState: @[Subchain(head: 49, tail: 49)], + err: "ErrReorgDenied", + ), + # Initialize a sync and try to extend it with a number-wise sequential + # header, but a hash wise non-linking one. + TestCase( + head: block49B, + extend: block50, + newState: @[Subchain(head: 49, tail: 49)], + err: "ErrReorgDenied", + ), + # Initialize a sync and try to extend it with a non-linking future block. + TestCase( + head: block49, + extend: block51, + newState: @[Subchain(head: 49, tail: 49)], + err: "ErrReorgDenied", + ), + # Initialize a sync and try to extend it with a past canonical block. + TestCase( + head: block50, + extend: block49, + newState: @[Subchain(head: 50, tail: 50)], + err: "ErrReorgDenied", + ), + # Initialize a sync and try to extend it with a past sidechain block. + TestCase( + head: block50, + extend: block49B, + newState: @[Subchain(head: 50, tail: 50)], + err: "ErrReorgDenied", + ) + ] + + for z, testCase in testCases: + test "test case #" & $z: + let env = initEnv() + let skeleton = SkeletonRef.new(env.chain) + skeleton.open() + + skeleton.initSync(testCase.head) + + try: + skeleton.setHead(testCase.extend) + check testCase.err.len == 0 + except Exception as e: + check testCase.err.len > 0 + check testCase.err == e.name + + check skeleton.len == testCase.newState.len + for i, sc in skeleton: + check sc.head == testCase.newState[i].head.toBlockNumber + check sc.tail == testCase.newState[i].tail.toBlockNumber + + +template testCond(expr: untyped) = + if not (expr): + return TestStatus.Failed + +template testCond(expr, body: untyped) = + if not (expr): + body + return TestStatus.Failed + +proc linkedToGenesis(env: TestEnv): TestStatus = + result = TestStatus.OK + env.chain.validateBlock = false + let skeleton = SkeletonRef.new(env.chain) + + let + genesis = env.chainDB.getCanonicalHead() + block1 = BlockHeader( + blockNumber: 1.toBlockNumber, parentHash: genesis.blockHash, difficulty: 100.u256 + ) + block2 = BlockHeader( + blockNumber: 2.toBlockNumber, parentHash: block1.blockHash, difficulty: 100.u256 + ) + block3 = BlockHeader( + blockNumber: 3.toBlockNumber, parentHash: block2.blockHash, difficulty: 100.u256 + ) + block4 = BlockHeader( + blockNumber: 4.toBlockNumber, parentHash: block3.blockHash, difficulty: 100.u256 + ) + block5 = BlockHeader( + blockNumber: 5.toBlockNumber, parentHash: block4.blockHash, difficulty: 100.u256 + ) + + skeleton.open() + skeleton.initSync(block4) + + skeleton.ignoreTxs = true + discard skeleton.putBlocks([block3, block2]) + testCond env.chainDB.currentBlock == 0.toBlockNumber: + error "canonical height should be at genesis" + + discard skeleton.putBlocks([block1]) + testCond env.chainDB.currentBlock == 4.toBlockNumber: + error "canonical height should update after being linked" + + skeleton.setHead(block5, false) + testCond env.chainDB.currentBlock == 4.toBlockNumber: + error "canonical height should not change when setHead is set with force=false" + + skeleton.setHead(block5, true) + testCond env.chainDB.currentBlock == 5.toBlockNumber: + error "canonical height should change when setHead is set with force=true" + + var h: BlockHeader + for header in [block1, block2, block3, block4, block5]: + var res = skeleton.getHeader(header.blockNumber, h, true) + testCond res == false: + error "skeleton block should be cleaned up after filling canonical chain", + number=header.blockNumber + + res = skeleton.getHeaderByHash(header.blockHash, h) + testCond res == false: + error "skeleton block should be cleaned up after filling canonical chain", + number=header.blockNumber + +proc linkedPastGenesis(env: TestEnv): TestStatus = + result = TestStatus.OK + env.chain.validateBlock = false + let skeleton = SkeletonRef.new(env.chain) + + skeleton.open() + let + genesis = env.chainDB.getCanonicalHead() + block1 = BlockHeader( + blockNumber: 1.toBlockNumber, parentHash: genesis.blockHash, difficulty: 100.u256 + ) + block2 = BlockHeader( + blockNumber: 2.toBlockNumber, parentHash: block1.blockHash, difficulty: 100.u256 + ) + block3 = BlockHeader( + blockNumber: 3.toBlockNumber, parentHash: block2.blockHash, difficulty: 100.u256 + ) + block4 = BlockHeader( + blockNumber: 4.toBlockNumber, parentHash: block3.blockHash, difficulty: 100.u256 + ) + block5 = BlockHeader( + blockNumber: 5.toBlockNumber, parentHash: block4.blockHash, difficulty: 100.u256 + ) + + var body: BlockBody + let vr = env.chain.persistBlocks([block1, block2], [body, body]) + testCond vr == ValidationResult.OK + + skeleton.initSync(block4) + testCond env.chainDB.currentBlock == 2.toBlockNumber: + error "canonical height should be at block 2" + + skeleton.ignoreTxs = true + discard skeleton.putBlocks([block3]) + testCond env.chainDB.currentBlock == 4.toBlockNumber: + error "canonical height should update after being linked" + + skeleton.setHead(block5, false) + testCond env.chainDB.currentBlock == 4.toBlockNumber: + error "canonical height should not change when setHead with force=false" + + skeleton.setHead(block5, true) + testCond env.chainDB.currentBlock == 5.toBlockNumber: + error "canonical height should change when setHead with force=true" + + var h: BlockHeader + for header in [block3, block4, block5]: + var res = skeleton.getHeader(header.blockNumber, h, true) + testCond res == false: + error "skeleton block should be cleaned up after filling canonical chain", + number=header.blockNumber + + res = skeleton.getHeaderByHash(header.blockHash, h) + testCond res == false: + error "skeleton block should be cleaned up after filling canonical chain", + number=header.blockNumber + +proc ccmAbortTerminalInvalid(cc: NetworkParams) = + cc.config.terminalTotalDifficulty = some(200.u256) + cc.genesis.extraData = hexToSeqByte("0x000000000000000000") + cc.genesis.difficulty = UInt256.fromHex("0x01") + +proc abortTerminalInvalid(env: TestEnv): TestStatus = + result = TestStatus.OK + env.chain.validateBlock = false + let skeleton = SkeletonRef.new(env.chain) + + let + genesisBlock = env.chainDB.getCanonicalHead() + block1 = BlockHeader( + blockNumber: 1.toBlockNumber, parentHash: genesisBlock.blockHash, difficulty: 100.u256 + ) + block2 = BlockHeader( + blockNumber: 2.toBlockNumber, parentHash: block1.blockHash, difficulty: 100.u256 + ) + block3PoW = BlockHeader( + blockNumber: 3.toBlockNumber, parentHash: block2.blockHash, difficulty: 100.u256 + ) + block3PoS = BlockHeader( + blockNumber: 3.toBlockNumber, parentHash: block2.blockHash, difficulty: 0.u256 + #{ common, hardforkByTTD: BigInt(200) } + ) + block4InvalidPoS = BlockHeader( + blockNumber: 4.toBlockNumber, parentHash: block3PoW.blockHash, difficulty: 0.u256 + #{ common, hardforkByTTD: BigInt(200) } + ) + block4PoS = BlockHeader( + blockNumber: 4.toBlockNumber, parentHash: block3PoS.blockHash, difficulty: 0.u256 + #{ common, hardforkByTTD: BigInt(200) } + ) + block5 = BlockHeader( + blockNumber: 5.toBlockNumber, parentHash: block4PoS.blockHash, difficulty: 0.u256 + #{ common, hardforkByTTD: BigInt(200) } + ) + + skeleton.ignoreTxs = true + skeleton.open() + skeleton.initSync(block4InvalidPoS) + + discard skeleton.putBlocks([block3PoW, block2]) + testCond env.chainDB.currentBlock == 0.toBlockNumber: + error "canonical height should be at genesis" + + discard skeleton.putBlocks([block1]) + testCond env.chainDB.currentBlock == 2.toBlockNumber: + error "canonical height should stop at block 2 (valid terminal block), since block 3 is invalid (past ttd)" + + try: + skeleton.setHead(block5, false) + except ErrReorgDenied: + testCond true + except: + testCond false + + testCond env.chainDB.currentBlock == 2.toBlockNumber: + error "canonical height should not change when setHead is set with force=false" + + # Put correct chain + skeleton.initSync(block4PoS) + try: + discard skeleton.putBlocks([block3PoS]) + except ErrSyncMerged: + testCond true + except: + testCond false + + testCond env.chainDB.currentBlock == 4.toBlockNumber: + error "canonical height should now be at head with correct chain" + + var header: BlockHeader + testCond env.chainDB.getBlockHeader(env.chainDB.highestBlock, header): + error "cannot get block header", number = env.chainDB.highestBlock + + testCond header.blockHash == block4PoS.blockHash: + error "canonical height should now be at head with correct chain" + + skeleton.setHead(block5, false) + testCond skeleton.bounds().head == 5.toBlockNumber: + error "should update to new height" + +proc ccmAbortAndBackstep(cc: NetworkParams) = + cc.config.terminalTotalDifficulty = some(200.u256) + cc.genesis.extraData = hexToSeqByte("0x000000000000000000") + cc.genesis.difficulty = UInt256.fromHex("0x01") + +proc abortAndBackstep(env: TestEnv): TestStatus = + result = TestStatus.OK + env.chain.validateBlock = false + let skeleton = SkeletonRef.new(env.chain) + + let + genesisBlock = env.chainDB.getCanonicalHead() + block1 = BlockHeader( + blockNumber: 1.toBlockNumber, parentHash: genesisBlock.blockHash, difficulty: 100.u256 + ) + block2 = BlockHeader( + blockNumber: 2.toBlockNumber, parentHash: block1.blockHash, difficulty: 100.u256 + ) + block3PoW = BlockHeader( + blockNumber: 3.toBlockNumber, parentHash: block2.blockHash, difficulty: 100.u256 + ) + block4InvalidPoS = BlockHeader( + blockNumber: 4.toBlockNumber, parentHash: block3PoW.blockHash, difficulty: 0.u256 + #{ common, hardforkByTTD: 200 } + ) + + skeleton.open() + skeleton.ignoreTxs = true + skeleton.initSync(block4InvalidPoS) + discard skeleton.putBlocks([block3PoW, block2]) + + testCond env.chainDB.currentBlock == 0.toBlockNumber: + error "canonical height should be at genesis" + + discard skeleton.putBlocks([block1]) + testCond env.chainDB.currentBlock == 2.toBlockNumber: + error "canonical height should stop at block 2 (valid terminal block), since block 3 is invalid (past ttd)" + + testCond skeleton.bounds().tail == 4.toBlockNumber: + error "Subchain should have been backstepped to 4" + +proc ccmAbortPOSTooEarly(cc: NetworkParams) = + cc.config.terminalTotalDifficulty = some(200.u256) + #skeletonFillCanonicalBackStep: 0, + cc.genesis.difficulty = UInt256.fromHex("0x01") + +proc abortPOSTooEarly(env: TestEnv): TestStatus = + result = TestStatus.OK + env.chain.validateBlock = false + let skeleton = SkeletonRef.new(env.chain) + + let + genesisBlock = env.chainDB.getCanonicalHead() + block1 = BlockHeader( + blockNumber: 1.toBlockNumber, parentHash: genesisBlock.blockHash, difficulty: 100.u256 + ) + block2 = BlockHeader( + blockNumber: 2.toBlockNumber, parentHash: block1.blockHash, difficulty: 100.u256 + ) + block2PoS = BlockHeader( + blockNumber: 2.toBlockNumber, parentHash: block1.blockHash, difficulty: 0.u256 + ) + block3 = BlockHeader( + blockNumber: 3.toBlockNumber, parentHash: block2.blockHash, difficulty: 0.u256 + ) + + skeleton.ignoreTxs = true + skeleton.open() + skeleton.initSync(block2PoS) + discard skeleton.putBlocks([block1]) + + testCond env.chainDB.currentBlock == 1.toBlockNumber: + error "canonical height should stop at block 1 (valid PoW block), since block 2 is invalid (invalid PoS, not past ttd)" + + # Put correct chain + skeleton.initSync(block3) + try: + discard skeleton.putBlocks([block2]) + except ErrSyncMerged: + testCond true + except: + testCond false + + testCond env.chainDB.currentBlock == 3.toBlockNumber: + error "canonical height should now be at head with correct chain" + + var header: BlockHeader + testCond env.chainDB.getBlockHeader(env.chainDB.highestBlock, header): + error "cannot get block header", number = env.chainDB.highestBlock + + testCond header.blockHash == block3.blockHash: + error "canonical height should now be at head with correct chain" + +suite "fillCanonicalChain tests": + type + TestCase = object + name: string + ccm : CCModify + run : proc(env: TestEnv): TestStatus + + const testCases = [ + TestCase( + name: "should fill the canonical chain after being linked to genesis", + run : linkedToGenesis + ), + TestCase( + name: "should fill the canonical chain after being linked to a canonical block past genesis", + run : linkedPastGenesis + ), + TestCase( + name: "should abort filling the canonical chain if the terminal block is invalid", + ccm : ccmAbortTerminalInvalid, + run : abortTerminalInvalid + ), + TestCase( + name: "should abort filling the canonical chain and backstep if the terminal block is invalid", + ccm : ccmAbortAndBackstep, + run : abortAndBackstep + ), + TestCase( + name: "should abort filling the canonical chain if a PoS block comes too early without hitting ttd", + ccm : ccmAbortPOSTooEarly, + run : abortPOSTooEarly + ) + ] + for testCase in testCases: + test testCase.name: + let env = initEnv(testCase.ccm) + check testCase.run(env) == TestStatus.OK