From b80eca071814238d7ae124e06988f75c3a1b95bb Mon Sep 17 00:00:00 2001 From: jangko Date: Mon, 27 Jun 2022 11:15:23 +0700 Subject: [PATCH] json-rpc: able to query finalized block and safe block header engine-api: - store safe block hash and finalized block hash engine-api test: - fix test case related to safe block hash and finalized block hash --- hive_integration/nodocker/engine/clmock.nim | 78 ++++++++-- .../nodocker/engine/engine_client.nim | 10 ++ .../nodocker/engine/engine_tests.nim | 146 +++++++----------- nimbus/db/db_chain.nim | 18 +++ nimbus/db/storage_types.nim | 10 ++ nimbus/rpc/engine_api.nim | 2 + nimbus/rpc/rpc_utils.nim | 2 + 7 files changed, 155 insertions(+), 111 deletions(-) diff --git a/hive_integration/nodocker/engine/clmock.nim b/hive_integration/nodocker/engine/clmock.nim index 717ee32d0..a0c0b3f08 100644 --- a/hive_integration/nodocker/engine/clmock.nim +++ b/hive_integration/nodocker/engine/clmock.nim @@ -7,7 +7,7 @@ import web3/engine_api_types, json_rpc/rpcclient, ../../../nimbus/merge/mergeutils, - ../../../nimbus/debug, + ../../../nimbus/[debug, constants], ./engine_client # Consensus Layer Client Mock used to sync the Execution Clients once the TTD has been reached @@ -34,6 +34,10 @@ type client : RpcClient ttd : DifficultyInt + slotsToSafe : int + slotsToFinalized : int + headHashHistory : seq[BlockHash] + BlockProcessCallbacks* = object onPayloadProducerSelected* : proc(): bool {.gcsafe.} onGetPayloadID* : proc(): bool {.gcsafe.} @@ -47,6 +51,8 @@ type proc init*(cl: CLMocker, client: RpcClient, ttd: DifficultyInt) = cl.client = client cl.ttd = ttd + cl.slotsToSafe = 1 + cl.slotsToFinalized = 2 proc newClMocker*(client: RpcClient, ttd: DifficultyInt): CLMocker = new result @@ -63,8 +69,13 @@ proc waitForTTD*(cl: CLMocker): Future[bool] {.async.} = let headerHash = BlockHash(common.blockHash(cl.latestHeader).data) cl.latestForkchoice.headBlockHash = headerHash - cl.latestForkchoice.safeBlockHash = headerHash - cl.latestForkchoice.finalizedBlockHash = headerHash + + if cl.slotsToSafe == 0: + cl.latestForkchoice.safeBlockHash = headerHash + + if cl.slotsToFinalized == 0: + cl.latestForkchoice.finalizedBlockHash = headerHash + cl.latestHeadNumber = cl.latestHeader.blockNumber.truncate(uint64) let res = cl.client.forkchoiceUpdatedV1(cl.latestForkchoice) @@ -256,8 +267,19 @@ proc produceSingleBlock*(cl: CLMocker, cb: BlockProcessCallbacks): bool {.gcsafe return false # Broadcast forkchoice updated with new HeadBlock to all clients - let blockHash = cl.latestPayloadBuilt.blockHash - cl.latestForkchoice.headBlockHash = blockHash + let previousForkchoice = cl.latestForkchoice + cl.headHashHistory.add cl.latestPayloadBuilt.blockHash + + cl.latestForkchoice = ForkchoiceStateV1() + cl.latestForkchoice.headBlockHash = cl.latestPayloadBuilt.blockHash + + let hhLen = cl.headHashHistory.len + if hhLen > cl.slotsToSafe: + cl.latestForkchoice.safeBlockHash = cl.headHashHistory[hhLen - cl.slotsToSafe - 1] + + if hhLen > cl.slotsToFinalized: + cl.latestForkchoice.finalizedBlockHash = cl.headHashHistory[hhLen - cl.slotsToFinalized - 1] + if not cl.broadcastLatestForkchoice(): return false @@ -266,19 +288,16 @@ proc produceSingleBlock*(cl: CLMocker, cb: BlockProcessCallbacks): bool {.gcsafe return false # Broadcast forkchoice updated with new SafeBlock to all clients - cl.latestForkchoice.safeBlockHash = blockHash - if not cl.broadcastLatestForkchoice(): - return false - - if cb.onSafeBlockChange != nil: + if cb.onSafeBlockChange != nil and cl.latestForkchoice.safeBlockHash != previousForkchoice.safeBlockHash: if not cb.onSafeBlockChange(): return false # Broadcast forkchoice updated with new FinalizedBlock to all clients - cl.latestForkchoice.finalizedBlockHash = blockHash - if not cl.broadcastLatestForkchoice(): - return false + if cb.onFinalizedBlockChange != nil and cl.latestForkchoice.finalizedBlockHash != previousForkchoice.finalizedBlockHash: + if not cb.onFinalizedBlockChange(): + return false + # Broadcast forkchoice updated with new FinalizedBlock to all clients # Save the number of the first PoS block if cl.firstPoSBlockNumber.isNone: let number = cl.latestHeader.blockNumber.truncate(uint64) + 1 @@ -300,11 +319,36 @@ proc produceSingleBlock*(cl: CLMocker, cb: BlockProcessCallbacks): bool {.gcsafe hash=newHash.toHex return false - cl.latestHeader = newHeader + # Check that the new finalized header has the correct properties + # ommersHash == 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347 + if newHeader.ommersHash != EMPTY_UNCLE_HASH: + error "CLMocker: Client produced a new header with incorrect ommersHash", ommersHash = newHeader.ommersHash + return false - if cb.onFinalizedBlockChange != nil: - if not cb.onFinalizedBlockChange(): - return false + # difficulty == 0 + if newHeader.difficulty != 0.u256: + error "CLMocker: Client produced a new header with incorrect difficulty", difficulty = newHeader.difficulty + return false + + # mixHash == prevRandao + if newHeader.mixDigest != cl.prevRandaoHistory[cl.latestHeadNumber]: + error "CLMocker: Client produced a new header with incorrect mixHash", + get = newHeader.mixDigest.data.toHex, + expect = cl.prevRandaoHistory[cl.latestHeadNumber].data.toHex + return false + + # nonce == 0x0000000000000000 + if newHeader.nonce != default(BlockNonce): + error "CLMocker: Client produced a new header with incorrect nonce", + nonce = newHeader.nonce.toHex + return false + + if newHeader.extraData.len > 32: + error "CLMocker: Client produced a new header with incorrect extraData (len > 32)", + len = newHeader.extraData.len + return false + + cl.latestHeader = newHeader return true diff --git a/hive_integration/nodocker/engine/engine_client.nim b/hive_integration/nodocker/engine/engine_client.nim index ca7e3895d..3c876d361 100644 --- a/hive_integration/nodocker/engine/engine_client.nim +++ b/hive_integration/nodocker/engine/engine_client.nim @@ -158,6 +158,16 @@ proc latestBlock*(client: RpcClient, output: var common.EthBlock): Result[void, except ValueError as e: return err(e.msg) +proc namedHeader*(client: RpcClient, name: string, output: var common.BlockHeader): Result[void, string] = + try: + let res = waitFor client.eth_getBlockByNumber(name, false) + if res.isNone: + return err("failed to get named blockHeader") + output = toBlockHeader(res.get()) + return ok() + except ValueError as e: + return err(e.msg) + proc sendTransaction*(client: RpcClient, tx: common.Transaction): Result[void, string] = try: let encodedTx = rlp.encode(tx) diff --git a/hive_integration/nodocker/engine/engine_tests.nim b/hive_integration/nodocker/engine/engine_tests.nim index ca18b49f7..430db08cd 100644 --- a/hive_integration/nodocker/engine/engine_tests.nim +++ b/hive_integration/nodocker/engine/engine_tests.nim @@ -929,101 +929,67 @@ template blockStatusHeadBlockGen(procname: untyped, transitionBlock: bool) = blockStatusHeadBlockGen(blockStatusHeadBlock1, false) blockStatusHeadBlockGen(blockStatusHeadBlock2, true) -template blockStatusSafeBlockGen(procname: untyped, transitionBlock: bool) = - proc procName(t: TestEnv): TestStatus = - result = TestStatus.OK +proc blockStatusSafeBlock(t: TestEnv): TestStatus = + result = TestStatus.OK - # Wait until TTD is reached by this client - let ok = waitFor t.clMock.waitForTTD() - testCond ok + let clMock = t.clMock + let client = t.rpcClient - # Produce blocks before starting the test, only if we are not testing the transition block - when not transitionBlock: - let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks()) - testCond produce5BlockRes + # On PoW mode, `safe` tag shall return error. + var header: EthBlockHeader + var rr = client.namedHeader("safe", header) + testCond rr.isErr - let clMock = t.clMock - let client = t.rpcClient - let shadow = Shadow() + # Wait until this client catches up with latest PoS Block + let ok = waitFor t.clMock.waitForTTD() + testCond ok - var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks( - onPayloadProducerSelected: proc(): bool = - var address: EthAddress - testCond t.sendTx(address, 1.u256) - shadow.hash = rlpHash(t.tx) - return true - , - # Run test after a forkchoice with new HeadBlockHash has been broadcasted - onSafeBlockChange: proc(): bool = - var lastHeader: EthBlockHeader - var hRes = client.latestHeader(lastHeader) - if hRes.isErr: - error "unable to get latest header", msg=hRes.error - return false + # First ForkchoiceUpdated sent was equal to 0x00..00, `safe` should return error now + rr = client.namedHeader("safe", header) + testCond rr.isErr - let lastHash = BlockHash lastHeader.blockHash.data - if lastHash != clMock.latestForkchoice.headBlockHash: - error "latest block header doesn't match SafeBlock hash", hash=lastHash.toHex - return false + let pbres = clMock.produceBlocks(3, BlockProcessCallbacks( + # Run test after a forkchoice with new SafeBlockHash has been broadcasted + onSafeBlockChange: proc(): bool = + var header: EthBlockHeader + let rr = client.namedHeader("safe", header) + testCond rr.isOk + let safeBlockHash = hash256(clMock.latestForkchoice.safeBlockHash) + header.blockHash == safeBlockHash + )) - let rr = client.txReceipt(shadow.hash) - if rr.isErr: - error "unable to get transaction receipt" - return false - return true - )) - testCond produceSingleBlockRes + testCond pbres -blockStatusSafeBlockGen(blockStatusSafeBlock1, false) -blockStatusSafeBlockGen(blockStatusSafeBlock2, true) +proc blockStatusFinalizedBlock(t: TestEnv): TestStatus = + result = TestStatus.OK -template blockStatusFinalizedBlockGen(procname: untyped, transitionBlock: bool) = - proc procName(t: TestEnv): TestStatus = - result = TestStatus.OK + let clMock = t.clMock + let client = t.rpcClient - # Wait until TTD is reached by this client - let ok = waitFor t.clMock.waitForTTD() - testCond ok + # On PoW mode, `finalized` tag shall return error. + var header: EthBlockHeader + var rr = client.namedHeader("finalized", header) + testCond rr.isErr - # Produce blocks before starting the test, only if we are not testing the transition block - when not transitionBlock: - let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks()) - testCond produce5BlockRes + # Wait until this client catches up with latest PoS Block + let ok = waitFor t.clMock.waitForTTD() + testCond ok - let clMock = t.clMock - let client = t.rpcClient - let shadow = Shadow() + # First ForkchoiceUpdated sent was equal to 0x00..00, `finalized` should return error now + rr = client.namedHeader("finalized", header) + testCond rr.isErr - var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks( - onPayloadProducerSelected: proc(): bool = - var address: EthAddress - testCond t.sendTx(address, 1.u256) - shadow.hash = rlpHash(t.tx) - return true - , - # Run test after a forkchoice with new HeadBlockHash has been broadcasted - onFinalizedBlockChange: proc(): bool = - var lastHeader: EthBlockHeader - var hRes = client.latestHeader(lastHeader) - if hRes.isErr: - error "unable to get latest header", msg=hRes.error - return false + let pbres = clMock.produceBlocks(3, BlockProcessCallbacks( + # Run test after a forkchoice with new FinalizedBlockHash has been broadcasted + onFinalizedBlockChange: proc(): bool = + var header: EthBlockHeader + let rr = client.namedHeader("finalized", header) + testCond rr.isOk + let finalizedBlockHash = hash256(clMock.latestForkchoice.finalizedBlockHash) + header.blockHash == finalizedBlockHash + )) - let lastHash = BlockHash lastHeader.blockHash.data - if lastHash != clMock.latestForkchoice.headBlockHash: - error "latest block header doesn't match FinalizedBlock hash", hash=lastHash.toHex - return false - - let rr = client.txReceipt(shadow.hash) - if rr.isErr: - error "unable to get transaction receipt" - return false - return true - )) - testCond produceSingleBlockRes - -blockStatusFinalizedBlockGen(blockStatusFinalizedBlock1, false) -blockStatusFinalizedBlockGen(blockStatusFinalizedBlock2, true) + testCond pbres proc blockStatusReorg(t: TestEnv): TestStatus = result = TestStatus.OK @@ -1900,21 +1866,13 @@ const engineTestList* = [ ttd: 5, ), TestSpec( - name: "Latest Block after New SafeBlock", - run: blockStatusSafeBlock1, - ), - TestSpec( - name: "Latest Block after New SafeBlock (Transition Block)", - run: blockStatusSafeBlock2, + name: "safe Block after New SafeBlockHash", + run: blockStatusSafeBlock, ttd: 5, ), TestSpec( - name: "Latest Block after New FinalizedBlock", - run: blockStatusFinalizedBlock1, - ), - TestSpec( - name: "Latest Block after New FinalizedBlock (Transition Block)", - run: blockStatusFinalizedBlock2, + name: "finalized Block after New FinalizedBlockHash", + run: blockStatusFinalizedBlock, ttd: 5, ), TestSpec( diff --git a/nimbus/db/db_chain.nim b/nimbus/db/db_chain.nim index 1c3c8d332..d9d012e06 100644 --- a/nimbus/db/db_chain.nim +++ b/nimbus/db/db_chain.nim @@ -435,3 +435,21 @@ proc persistUncles*(self: BaseChainDB, uncles: openArray[BlockHeader]): Hash256 let enc = rlp.encode(uncles) result = keccakHash(enc) self.db.put(genericHashKey(result).toOpenArray, enc) + +proc safeHeaderHash*(self: BaseChainDB): Hash256 = + discard self.getHash(safeHashKey(), result) + +proc safeHeaderHash*(self: BaseChainDB, headerHash: Hash256) = + self.db.put(safeHashKey().toOpenArray, rlp.encode(headerHash)) + +proc finalizedHeaderHash*(self: BaseChainDB): Hash256 = + discard self.getHash(finalizedHashKey(), result) + +proc finalizedHeaderHash*(self: BaseChainDB, headerHash: Hash256) = + self.db.put(finalizedHashKey().toOpenArray, rlp.encode(headerHash)) + +proc safeHeader*(self: BaseChainDB): BlockHeader = + self.getBlockHeader(self.safeHeaderHash) + +proc finalizedHeader*(self: BaseChainDB): BlockHeader = + self.getBlockHeader(self.finalizedHeaderHash) diff --git a/nimbus/db/storage_types.nim b/nimbus/db/storage_types.nim index 262e39e93..ae8b9f3de 100644 --- a/nimbus/db/storage_types.nim +++ b/nimbus/db/storage_types.nim @@ -13,6 +13,8 @@ type cliqueSnapshot transitionStatus terminalHash + safeHash + finalizedHash DbKey* = object # The first byte stores the key type. The rest are key-specific values @@ -69,6 +71,14 @@ proc terminalHashKey*(): DbKey = result.data[0] = byte ord(terminalHash) result.dataEndPos = uint8 1 +proc safeHashKey*(): DbKey {.inline.} = + result.data[0] = byte ord(safeHash) + result.dataEndPos = uint8 1 + +proc finalizedHashKey*(): DbKey {.inline.} = + result.data[0] = byte ord(finalizedHash) + result.dataEndPos = uint8 1 + template toOpenArray*(k: DbKey): openArray[byte] = k.data.toOpenArray(0, int(k.dataEndPos)) diff --git a/nimbus/rpc/engine_api.nim b/nimbus/rpc/engine_api.nim index c718d0b88..91748ec6d 100644 --- a/nimbus/rpc/engine_api.nim +++ b/nimbus/rpc/engine_api.nim @@ -277,6 +277,7 @@ proc setupEngineAPI*( finalHash=finalHash.data.toHex, finalizedBlockHash=finalizedBlockHash.data.toHex raise (ref InvalidRequest)(code: engineApiInvalidParams, msg: "finalilized block not canonical") + db.finalizedHeaderHash(finalizedBlockHash) let safeBlockHash = update.safeBlockHash.asEthHash if safeBlockHash != Hash256(): @@ -295,6 +296,7 @@ proc setupEngineAPI*( safeHash=safeHash.data.toHex, safeBlockHash=safeBlockHash.data.toHex raise (ref InvalidRequest)(code: engineApiInvalidParams, msg: "safe head not canonical") + db.safeHeaderHash(safeBlockHash) # If payload generation was requested, create a new block to be potentially # sealed by the beacon client. The payload will be requested later, and we diff --git a/nimbus/rpc/rpc_utils.nim b/nimbus/rpc/rpc_utils.nim index 94b6ea8f6..e51fb8c43 100644 --- a/nimbus/rpc/rpc_utils.nim +++ b/nimbus/rpc/rpc_utils.nim @@ -35,6 +35,8 @@ proc headerFromTag*(chain: BaseChainDB, blockTag: string): BlockHeader = case tag of "latest": result = chain.getCanonicalHead() of "earliest": result = chain.getBlockHeader(GENESIS_BLOCK_NUMBER) + of "safe": result = chain.safeHeader() + of "finalized": result = chain.finalizedHeader() of "pending": #TODO: Implement get pending block raise newException(ValueError, "Pending tag not yet implemented")