diff --git a/hive_integration/nodocker/engine/clmock.nim b/hive_integration/nodocker/engine/clmock.nim index b3b42519e..70e15fb17 100644 --- a/hive_integration/nodocker/engine/clmock.nim +++ b/hive_integration/nodocker/engine/clmock.nim @@ -21,8 +21,8 @@ type executedPayloadHistory*: Table[uint64, ExecutionPayloadV1] # Latest broadcasted data using the PoS Engine API - latestFinalizedNumber*: uint64 - latestFinalizedHeader*: common.BlockHeader + latestHeadNumber*: uint64 + latestHeader*: common.BlockHeader latestPayloadBuilt* : ExecutionPayloadV1 latestExecutedPayload*: ExecutionPayloadV1 latestForkchoice* : ForkchoiceStateV1 @@ -35,13 +35,13 @@ type ttd : DifficultyInt BlockProcessCallbacks* = object - onPayloadProducerSelected* : proc(): bool {.gcsafe.} - onGetPayloadID* : proc(): bool {.gcsafe.} - onGetPayload* : proc(): bool {.gcsafe.} - onNewPayloadBroadcast* : proc(): bool {.gcsafe.} - onHeadBlockForkchoiceBroadcast* : proc(): bool {.gcsafe.} - onSafeBlockForkchoiceBroadcast* : proc(): bool {.gcsafe.} - onFinalizedBlockForkchoiceBroadcast* : proc(): bool {.gcsafe.} + onPayloadProducerSelected* : proc(): bool {.gcsafe.} + onGetPayloadID* : proc(): bool {.gcsafe.} + onGetPayload* : proc(): bool {.gcsafe.} + onNewPayloadBroadcast* : proc(): bool {.gcsafe.} + onForkchoiceBroadcast* : proc(): bool {.gcsafe.} + onSafeBlockChange * : proc(): bool {.gcsafe.} + onFinalizedBlockChange* : proc(): bool {.gcsafe.} proc init*(cl: CLMocker, client: RpcClient, ttd: DifficultyInt) = @@ -58,14 +58,14 @@ proc waitForTTD*(cl: CLMocker): Future[bool] {.async.} = error "timeout while waiting for TTD" return false - cl.latestFinalizedHeader = header + cl.latestHeader = header cl.ttdReached = true - let headerHash = BlockHash(common.blockHash(cl.latestFinalizedHeader).data) + let headerHash = BlockHash(common.blockHash(cl.latestHeader).data) cl.latestForkchoice.headBlockHash = headerHash cl.latestForkchoice.safeBlockHash = headerHash cl.latestForkchoice.finalizedBlockHash = headerHash - cl.latestFinalizedNumber = cl.latestFinalizedHeader.blockNumber.truncate(uint64) + cl.latestHeadNumber = cl.latestHeader.blockNumber.truncate(uint64) let res = cl.client.forkchoiceUpdatedV1(cl.latestForkchoice) if res.isErr: @@ -87,7 +87,7 @@ proc pickNextPayloadProducer(cl: CLMocker): bool = return false let lastBlockNumber = nRes.get - if cl.latestFinalizedNumber != lastBlockNumber: + if cl.latestHeadNumber != lastBlockNumber: return false var header: common.BlockHeader @@ -97,7 +97,7 @@ proc pickNextPayloadProducer(cl: CLMocker): bool = return false let lastBlockHash = header.blockHash - if cl.latestFinalizedHeader.blockHash != lastBlockHash: + if cl.latestHeader.blockHash != lastBlockHash: error "CLMocker: Failed to obtain a client on the latest block number" return false @@ -108,7 +108,7 @@ proc getNextPayloadID(cl: CLMocker): bool = var nextPrevRandao: Hash256 doAssert nimcrypto.randomBytes(nextPrevRandao.data) == 32 - let timestamp = Quantity toUnix(cl.latestFinalizedHeader.timestamp + 1.seconds) + let timestamp = Quantity toUnix(cl.latestHeader.timestamp + 1.seconds) let payloadAttributes = PayloadAttributesV1( timestamp: timestamp, prevRandao: FixedBytes[32] nextPrevRandao.data, @@ -116,7 +116,7 @@ proc getNextPayloadID(cl: CLMocker): bool = ) # Save random value - let number = cl.latestFinalizedHeader.blockNumber.truncate(uint64) + 1 + let number = cl.latestHeader.blockNumber.truncate(uint64) + 1 cl.prevRandaoHistory[number] = nextPrevRandao let res = cl.client.forkchoiceUpdatedV1(cl.latestForkchoice, some(payloadAttributes)) @@ -261,8 +261,8 @@ proc produceSingleBlock*(cl: CLMocker, cb: BlockProcessCallbacks): bool {.gcsafe if not cl.broadcastLatestForkchoice(): return false - if cb.onHeadBlockForkchoiceBroadcast != nil: - if not cb.onHeadBlockForkchoiceBroadcast(): + if cb.onForkchoiceBroadcast != nil: + if not cb.onForkchoiceBroadcast(): return false # Broadcast forkchoice updated with new SafeBlock to all clients @@ -270,8 +270,8 @@ proc produceSingleBlock*(cl: CLMocker, cb: BlockProcessCallbacks): bool {.gcsafe if not cl.broadcastLatestForkchoice(): return false - if cb.onSafeBlockForkchoiceBroadcast != nil: - if not cb.onSafeBlockForkchoiceBroadcast(): + if cb.onSafeBlockChange != nil: + if not cb.onSafeBlockChange(): return false # Broadcast forkchoice updated with new FinalizedBlock to all clients @@ -281,15 +281,15 @@ proc produceSingleBlock*(cl: CLMocker, cb: BlockProcessCallbacks): bool {.gcsafe # Save the number of the first PoS block if cl.firstPoSBlockNumber.isNone: - let number = cl.latestFinalizedHeader.blockNumber.truncate(uint64) + 1 + let number = cl.latestHeader.blockNumber.truncate(uint64) + 1 cl.firstPoSBlockNumber = some(number) # Save the header of the latest block in the PoS chain - cl.latestFinalizedNumber = cl.latestFinalizedNumber + 1 + cl.latestHeadNumber = cl.latestHeadNumber + 1 # Check if any of the clients accepted the new payload var newHeader: common.BlockHeader - let res = cl.client.headerByNumber(cl.latestFinalizedNumber, newHeader) + let res = cl.client.headerByNumber(cl.latestHeadNumber, newHeader) if res.isErr: error "CLMock ProduceSingleBlock", msg=res.error return false @@ -300,10 +300,10 @@ proc produceSingleBlock*(cl: CLMocker, cb: BlockProcessCallbacks): bool {.gcsafe hash=newHash.toHex return false - cl.latestFinalizedHeader = newHeader + cl.latestHeader = newHeader - if cb.onFinalizedBlockForkchoiceBroadcast != nil: - if not cb.onFinalizedBlockForkchoiceBroadcast(): + if cb.onFinalizedBlockChange != nil: + if not cb.onFinalizedBlockChange(): return false return true diff --git a/hive_integration/nodocker/engine/engine_tests.nim b/hive_integration/nodocker/engine/engine_tests.nim index d270119a3..29b8331cc 100644 --- a/hive_integration/nodocker/engine/engine_tests.nim +++ b/hive_integration/nodocker/engine/engine_tests.nim @@ -68,6 +68,9 @@ proc invalidGetPayloadUnderPoW(t: TestEnv): TestStatus = let res = t.rpcClient.getPayloadV1(id) testCond res.isErr + # Check that PoW chain progresses + testCond t.verifyPoWProgress(t.gHeader.blockHash) + # Invalid Terminal Block in NewPayload: # Client must reject NewPayload directives if the referenced ParentHash does not meet the TTD requirement. proc invalidTerminalBlockNewPayload(t: TestEnv): TestStatus = @@ -96,6 +99,9 @@ proc invalidTerminalBlockNewPayload(t: TestEnv): TestStatus = testCond s.status == PayloadExecutionStatus.invalid testCond s.latestValidHash.isNone + # Check that PoW chain progresses + testCond t.verifyPoWProgress(t.gHeader.blockHash) + proc unknownHeadBlockHash(t: TestEnv): TestStatus = result = TestStatus.OK @@ -208,6 +214,156 @@ proc unknownFinalizedBlockHash(t: TestEnv): TestStatus = testCond produceSingleBlockRes +# Send an inconsistent ForkchoiceState with a known payload that belongs to a side chain as head, safe or finalized. +type + Inconsistency {.pure.} = enum + Head + Safe + Finalized + + PayloadList = ref object + canonicalPayloads : seq[ExecutableData] + alternativePayloads: seq[ExecutableData] + +template inconsistentForkchoiceStateGen(procName: untyped, inconsistency: Inconsistency) = + proc procName(t: TestEnv): TestStatus = + result = TestStatus.OK + + # Wait until TTD is reached by this client + let ok = waitFor t.clMock.waitForTTD() + testCond ok + + var pList = PayloadList() + let clMock = t.clMock + let client = t.rpcClient + + # Produce blocks before starting the test + let produceBlockRes = clMock.produceBlocks(3, BlockProcessCallbacks( + onGetPayload: proc(): bool = + # Generate and send an alternative side chain + var customData = CustomPayload( + extraData: some(@[0x01.byte]) + ) + + if pList.alternativePayloads.len > 0: + customData.parentHash = some(pList.alternativePayloads[^1].blockHash) + + let executableData = toExecutableData(clMock.latestPayloadBuilt) + let alternativePayload = customizePayload(executableData, customData) + pList.alternativePayloads.add(alternativePayload.toExecutableData) + + let latestCanonicalPayload = toExecutableData(clMock.latestPayloadBuilt) + pList.canonicalPayloads.add(latestCanonicalPayload) + + # Send the alternative payload + let res = client.newPayloadV1(alternativePayload) + if res.isErr: + return false + + let s = res.get() + s.status == PayloadExecutionStatus.valid or s.status == PayloadExecutionStatus.accepted + )) + + testCond produceBlockRes + + # Send the invalid ForkchoiceStates + let len = pList.alternativePayloads.len + var inconsistentFcU = ForkchoiceStateV1( + headBlockHash: Web3BlockHash pList.canonicalPayloads[len-1].blockHash.data, + safeBlockHash: Web3BlockHash pList.canonicalPayloads[len-2].blockHash.data, + finalizedBlockHash: Web3BlockHash pList.canonicalPayloads[len-3].blockHash.data, + ) + + when inconsistency == Inconsistency.Head: + inconsistentFcU.headBlockHash = Web3BlockHash pList.alternativePayloads[len-1].blockHash.data + elif inconsistency == Inconsistency.Safe: + inconsistentFcU.safeBlockHash = Web3BlockHash pList.alternativePayloads[len-2].blockHash.data + else: + inconsistentFcU.finalizedBlockHash = Web3BlockHash pList.alternativePayloads[len-3].blockHash.data + + var r = client.forkchoiceUpdatedV1(inconsistentFcU) + testCond r.isErr + + # Return to the canonical chain + r = client.forkchoiceUpdatedV1(clMock.latestForkchoice) + testCond r.isOk + let s = r.get() + testCond s.payloadStatus.status == PayloadExecutionStatus.valid + +inconsistentForkchoiceStateGen(inconsistentForkchoiceState1, Inconsistency.Head) +inconsistentForkchoiceStateGen(inconsistentForkchoiceState2, Inconsistency.Safe) +inconsistentForkchoiceStateGen(inconsistentForkchoiceState3, Inconsistency.Finalized) + +# Verify behavior on a forkchoiceUpdated with invalid payload attributes +template invalidPayloadAttributesGen(procName: untyped, syncingCond: bool) = + proc procName(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 + var produceBlockRes = clMock.produceBlocks(5, BlockProcessCallbacks()) + testCond produceBlockRes + + # Send a forkchoiceUpdated with invalid PayloadAttributes + produceBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks( + onNewPayloadBroadcast: proc(): bool = + # Try to apply the new payload with invalid attributes + var blockHash: Hash256 + when syncingCond: + # Setting a random hash will put the client into `SYNCING` + doAssert nimcrypto.randomBytes(blockHash.data) == 32 + else: + # Set the block hash to the next payload that was broadcasted + blockHash = hash256(clMock.latestPayloadBuilt.blockHash) + + let fcu = ForkchoiceStateV1( + headBlockHash: Web3BlockHash blockHash.data, + safeBlockHash: Web3BlockHash blockHash.data, + finalizedBlockHash: Web3BlockHash blockHash.data, + ) + + let attr = PayloadAttributesV1() + + # 0) Check headBlock is known and there is no missing data, if not respond with SYNCING + # 1) Check headBlock is VALID, if not respond with INVALID + # 2) Apply forkchoiceState + # 3) Check payloadAttributes, if invalid respond with error: code: Invalid payload attributes + # 4) Start payload build process and respond with VALID + when syncingCond: + # If we are SYNCING, the outcome should be SYNCING regardless of the validity of the payload atttributes + let r = client.forkchoiceUpdatedV1(fcu, some(attr)) + let s = r.get() + if s.payloadStatus.status != PayloadExecutionStatus.syncing: + return false + if s.payloadId.isSome: + return false + else: + let r = client.forkchoiceUpdatedV1(fcu, some(attr)) + if r.isOk: + debugEcho "EEEE" + return false + + # Check that the forkchoice was applied, regardless of the error + var header: EthBlockHeader + let s = client.latestHeader(header) + if s.isErr: + return false + if header.blockHash != blockHash: + return false + return true + )) + + testCond produceBlockRes + +invalidPayloadAttributesGen(invalidPayloadAttributes1, false) +invalidPayloadAttributesGen(invalidPayloadAttributes2, true) + proc preTTDFinalizedBlockHash(t: TestEnv): TestStatus = result = TestStatus.OK @@ -237,62 +393,131 @@ proc preTTDFinalizedBlockHash(t: TestEnv): TestStatus = let s = res.get() testCond s.payloadStatus.status == PayloadExecutionStatus.valid -proc badHashOnExecPayload(t: TestEnv): TestStatus = - result = TestStatus.OK +# Corrupt the hash of a valid payload, client should reject the payload. +# All possible scenarios: +# (fcU) +# ┌────────┐ ┌────────────────────────┐ +# │ HEAD │◄───────┤ Bad Hash (!Sync,!Side) │ +# └────┬───┘ └────────────────────────┘ +# │ +# │ +# ┌────▼───┐ ┌────────────────────────┐ +# │ HEAD-1 │◄───────┤ Bad Hash (!Sync, Side) │ +# └────┬───┘ └────────────────────────┘ +# │ +# +# +# (fcU) +# ******************** ┌───────────────────────┐ +# * (Unknown) HEAD *◄─┤ Bad Hash (Sync,!Side) │ +# ******************** └───────────────────────┘ +# │ +# │ +# ┌────▼───┐ ┌───────────────────────┐ +# │ HEAD-1 │◄───────────┤ Bad Hash (Sync, Side) │ +# └────┬───┘ └───────────────────────┘ +# │ +# - let ok = waitFor t.clMock.waitForTTD() - testCond ok +type + Shadow = ref object + hash: Hash256 - # Produce blocks before starting the test - let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks()) - testCond produce5BlockRes +template badHashOnNewPayloadGen(procName: untyped, syncingCond: bool, sideChain: bool) = + proc procName(t: TestEnv): TestStatus = + result = TestStatus.OK - type - Shadow = ref object - hash: Hash256 + let ok = waitFor t.clMock.waitForTTD() + testCond ok - let clMock = t.clMock - let client = t.rpcClient - let shadow = Shadow() + # Produce blocks before starting the test + let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks()) + testCond produce5BlockRes - var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks( - # Run test after the new payload has been obtained - onGetPayload: proc(): bool = - # Alter hash on the payload and send it to client, should produce an error - var alteredPayload = clMock.latestPayloadBuilt - var invalidPayloadHash = hash256(alteredPayload.blockHash) - let lastByte = int invalidPayloadHash.data[^1] - invalidPayloadHash.data[^1] = byte(not lastByte) - shadow.hash = invalidPayloadHash - alteredPayload.blockHash = BlockHash invalidPayloadHash.data - let res = client.newPayloadV1(alteredPayload) - # Execution specification:: - # - {status: INVALID_BLOCK_HASH, latestValidHash: null, validationError: null} if the blockHash validation has failed - if res.isErr: - return false - let s = res.get() - s.status == PayloadExecutionStatus.invalid_block_hash - )) - testCond produceSingleBlockRes + let clMock = t.clMock + let client = t.rpcClient + let shadow = Shadow() - # Lastly, attempt to build on top of the invalid payload - produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks( - # Run test after the new payload has been obtained - onGetPayload: proc(): bool = - let payload = toExecutableData(clMock.latestPayloadBuilt) - let alteredPayload = customizePayload(payload, CustomPayload( - parentHash: some(shadow.hash), - )) - let res = client.newPayloadV1(alteredPayload) - if res.isErr: - return false - # Response status can be ACCEPTED (since parent payload could have been thrown out by the client) - # or INVALID (client still has the payload and can verify that this payload is incorrectly building on top of it), - # but a VALID response is incorrect. - let s = res.get() - s.status != PayloadExecutionStatus.valid - )) - testCond produceSingleBlockRes + var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks( + # Run test after the new payload has been obtained + onGetPayload: proc(): bool = + # Alter hash on the payload and send it to client, should produce an error + var alteredPayload = clMock.latestPayloadBuilt + var invalidPayloadHash = hash256(alteredPayload.blockHash) + let lastByte = int invalidPayloadHash.data[^1] + invalidPayloadHash.data[^1] = byte(not lastByte) + shadow.hash = invalidPayloadHash + alteredPayload.blockHash = BlockHash invalidPayloadHash.data + + when not syncingCond and sideChain: + # We alter the payload by setting the parent to a known past block in the + # canonical chain, which makes this payload a side chain payload, and also an invalid block hash + # (because we did not update the block hash appropriately) + alteredPayload.parentHash = Web3BlockHash clMock.latestHeader.parentHash.data + elif syncingCond: + # We need to send an fcU to put the client in SYNCING state. + var randomHeadBlock: Hash256 + doAssert nimcrypto.randomBytes(randomHeadBlock.data) == 32 + + let latestHeaderHash = clMock.latestHeader.blockHash + let fcU = ForkchoiceStateV1( + headBlockHash: Web3BlockHash randomHeadBlock.data, + safeBlockHash: Web3BlockHash latestHeaderHash.data, + finalizedBlockHash: Web3BlockHash latestHeaderHash.data + ) + + let r = client.forkchoiceUpdatedV1(fcU) + if r.isErr: + return false + let z = r.get() + if z.payloadStatus.status != PayloadExecutionStatus.syncing: + return false + + when sidechain: + # Syncing and sidechain, the caonincal head is an unknown payload to us, + # but this specific bad hash payload is in theory part of a side chain. + # Therefore the parent we use is the head hash. + alteredPayload.parentHash = Web3BlockHash latestHeaderHash.data + else: + # The invalid bad-hash payload points to the unknown head, but we know it is + # indeed canonical because the head was set using forkchoiceUpdated. + alteredPayload.parentHash = Web3BlockHash randomHeadBlock.data + + let res = client.newPayloadV1(alteredPayload) + # Execution specification:: + # - {status: INVALID_BLOCK_HASH, latestValidHash: null, validationError: null} if the blockHash validation has failed + if res.isErr: + return false + let s = res.get() + if s.status != PayloadExecutionStatus.invalid_block_hash: + return false + s.latestValidHash.isNone + )) + testCond produceSingleBlockRes + + # Lastly, attempt to build on top of the invalid payload + produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks( + # Run test after the new payload has been obtained + onGetPayload: proc(): bool = + let payload = toExecutableData(clMock.latestPayloadBuilt) + let alteredPayload = customizePayload(payload, CustomPayload( + parentHash: some(shadow.hash), + )) + let res = client.newPayloadV1(alteredPayload) + if res.isErr: + return false + # Response status can be ACCEPTED (since parent payload could have been thrown out by the client) + # or INVALID (client still has the payload and can verify that this payload is incorrectly building on top of it), + # but a VALID response is incorrect. + let s = res.get() + s.status != PayloadExecutionStatus.valid + )) + testCond produceSingleBlockRes + +badHashOnNewPayloadGen(badHashOnNewPayload1, false, false) +badHashOnNewPayloadGen(badHashOnNewPayload2, true, false) +badHashOnNewPayloadGen(badHashOnNewPayload3, false, true) +badHashOnNewPayloadGen(badHashOnNewPayload4, true, true) proc parentHashOnExecPayload(t: TestEnv): TestStatus = result = TestStatus.OK @@ -328,149 +553,234 @@ proc invalidPayloadTestCaseGen(payloadField: string): proc (t: TestEnv): TestSta result = TestStatus.SKIPPED # Test to verify Block information available at the Eth RPC after NewPayload -proc blockStatusExecPayload(t: TestEnv): TestStatus = - result = TestStatus.OK +template blockStatusExecPayloadGen(procName: untyped, transitionBlock: bool) = + proc procName(t: TestEnv): TestStatus = + result = TestStatus.OK - # Wait until TTD is reached by this client - let ok = waitFor t.clMock.waitForTTD() - testCond ok + # Wait until TTD is reached by this client + let ok = waitFor t.clMock.waitForTTD() + testCond ok - # Produce blocks before starting the test - let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks()) - testCond produce5BlockRes + # 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 - let clMock = t.clMock - let client = t.rpcClient - var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks( - onNewPayloadBroadcast: proc(): bool = - # TODO: Ideally, we would need to testCond that the newPayload returned VALID - var lastHeader: EthBlockHeader - var hRes = client.latestHeader(lastHeader) - if hRes.isErr: - error "unable to get latest header", msg=hRes.error - return false + let clMock = t.clMock + let client = t.rpcClient + let shadow = Shadow() - let lastHash = BlockHash lastHeader.blockHash.data - # Latest block header available via Eth RPC should not have changed at this point - if lastHash == clMock.latestExecutedPayload.blockHash or - lastHash != clMock.latestForkchoice.headBlockHash or - lastHash != clMock.latestForkchoice.safeBlockHash or - lastHash != clMock.latestForkchoice.finalizedBlockHash: - error "latest block header incorrect after newPayload", hash=lastHash.toHex - return false + var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks( + onPayloadProducerSelected: proc(): bool = + var address: EthAddress + let tx = t.makeNextTransaction(address, 1.u256) + let res = client.sendTransaction(tx) + if res.isErr: + error "Unable to send transaction" + return false - let nRes = client.blockNumber() - if nRes.isErr: - error "Unable to get latest block number", msg=nRes.error - return false + shadow.hash = rlpHash(tx) + return true + , + onNewPayloadBroadcast: proc(): bool = + # TODO: Ideally, we would need to testCond that the newPayload returned VALID + var lastHeader: EthBlockHeader + var hRes = client.latestHeader(lastHeader) + if hRes.isErr: + error "unable to get latest header", msg=hRes.error + return false - # Latest block number available via Eth RPC should not have changed at this point - let latestNumber = nRes.get - if latestNumber != clMock.latestFinalizedNumber: - error "latest block number incorrect after newPayload", - expected=clMock.latestFinalizedNumber, - get=latestNumber - return false + let lastHash = BlockHash lastHeader.blockHash.data + # Latest block header available via Eth RPC should not have changed at this point + if lastHash!= clMock.latestForkchoice.headBlockHash: + error "latest block header incorrect after newPayload", hash=lastHash.toHex + return false - return true - )) - testCond produceSingleBlockRes + let nRes = client.blockNumber() + if nRes.isErr: + error "Unable to get latest block number", msg=nRes.error + return false -proc blockStatusHeadBlock(t: TestEnv): TestStatus = - result = TestStatus.OK + # Latest block number available via Eth RPC should not have changed at this point + let latestNumber = nRes.get + if latestNumber != clMock.latestHeadNumber: + error "latest block number incorrect after newPayload", + expected=clMock.latestHeadNumber, + get=latestNumber + return false - # Wait until TTD is reached by this client - let ok = waitFor t.clMock.waitForTTD() - testCond ok + # Check that the receipt for the transaction we just sent is still not available + let rr = client.txReceipt(shadow.hash) + if rr.isOk: + error "not expecting receipt" + return false - # Produce blocks before starting the test - let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks()) - testCond produce5BlockRes + return true + )) + testCond produceSingleBlockRes - let clMock = t.clMock - let client = t.rpcClient - var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks( - # Run test after a forkchoice with new HeadBlockHash has been broadcasted - onHeadBlockForkchoiceBroadcast: proc(): bool = - var lastHeader: EthBlockHeader - var hRes = client.latestHeader(lastHeader) - if hRes.isErr: - error "unable to get latest header", msg=hRes.error - return false +blockStatusExecPayloadGen(blockStatusExecPayload1, false) +blockStatusExecPayloadGen(blockStatusExecPayload2, true) - let lastHash = BlockHash lastHeader.blockHash.data - if lastHash != clMock.latestForkchoice.headBlockHash or - lastHash == clMock.latestForkchoice.safeBlockHash or - lastHash == clMock.latestForkchoice.finalizedBlockHash: - error "latest block header doesn't match HeadBlock hash", hash=lastHash.toHex - return false - return true - )) - testCond produceSingleBlockRes +template blockStatusHeadBlockGen(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 - # Wait until TTD is reached by this client - let ok = waitFor t.clMock.waitForTTD() - testCond ok + # 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 - # Produce blocks before starting the test - let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks()) - testCond produce5BlockRes + let clMock = t.clMock + let client = t.rpcClient + let shadow = Shadow() - let clMock = t.clMock - let client = t.rpcClient - var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks( - # Run test after a forkchoice with new HeadBlockHash has been broadcasted - onSafeBlockForkchoiceBroadcast: proc(): bool = - var lastHeader: EthBlockHeader - var hRes = client.latestHeader(lastHeader) - if hRes.isErr: - error "unable to get latest header", msg=hRes.error - return false + var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks( + onPayloadProducerSelected: proc(): bool = + var address: EthAddress + let tx = t.makeNextTransaction(address, 1.u256) + let res = client.sendTransaction(tx) + if res.isErr: + error "Unable to send transaction" + return false - let lastHash = BlockHash lastHeader.blockHash.data - if lastHash != clMock.latestForkchoice.headBlockHash or - lastHash != clMock.latestForkchoice.safeBlockHash or - lastHash == clMock.latestForkchoice.finalizedBlockHash: - error "latest block header doesn't match SafeBlock hash", hash=lastHash.toHex - return false - return true - )) - testCond produceSingleBlockRes + shadow.hash = rlpHash(tx) + return true + , + # Run test after a forkchoice with new HeadBlockHash has been broadcasted + onForkchoiceBroadcast: proc(): bool = + var lastHeader: EthBlockHeader + var hRes = client.latestHeader(lastHeader) + if hRes.isErr: + error "unable to get latest header", msg=hRes.error + return false -proc blockStatusFinalizedBlock(t: TestEnv): TestStatus = - result = TestStatus.OK + let lastHash = BlockHash lastHeader.blockHash.data + if lastHash != clMock.latestForkchoice.headBlockHash: + error "latest block header doesn't match HeadBlock hash", hash=lastHash.toHex + return false - # Wait until TTD is reached by this client - let ok = waitFor t.clMock.waitForTTD() - testCond ok + let rr = client.txReceipt(shadow.hash) + if rr.isErr: + error "unable to get transaction receipt" + return false - # Produce blocks before starting the test - let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks()) - testCond produce5BlockRes + return true + )) + testCond produceSingleBlockRes - let clMock = t.clMock - let client = t.rpcClient - var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks( - # Run test after a forkchoice with new HeadBlockHash has been broadcasted - onFinalizedBlockForkchoiceBroadcast: proc(): bool = - var lastHeader: EthBlockHeader - var hRes = client.latestHeader(lastHeader) - if hRes.isErr: - error "unable to get latest header", msg=hRes.error - return false +blockStatusHeadBlockGen(blockStatusHeadBlock1, false) +blockStatusHeadBlockGen(blockStatusHeadBlock2, true) - let lastHash = BlockHash lastHeader.blockHash.data - if lastHash != clMock.latestForkchoice.headBlockHash or - lastHash != clMock.latestForkchoice.safeBlockHash or - lastHash != clMock.latestForkchoice.finalizedBlockHash: - error "latest block header doesn't match FinalizedBlock hash", hash=lastHash.toHex - return false - return true - )) - testCond produceSingleBlockRes +template blockStatusSafeBlockGen(procName: untyped, transitionBlock: bool) = + proc procName(t: TestEnv): TestStatus = + result = TestStatus.OK + + # Wait until TTD is reached by this client + let ok = waitFor t.clMock.waitForTTD() + testCond ok + + # 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 + + let clMock = t.clMock + let client = t.rpcClient + let shadow = Shadow() + + var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks( + onPayloadProducerSelected: proc(): bool = + var address: EthAddress + let tx = t.makeNextTransaction(address, 1.u256) + let res = client.sendTransaction(tx) + if res.isErr: + error "Unable to send transaction" + return false + + shadow.hash = rlpHash(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 + + 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 rr = client.txReceipt(shadow.hash) + if rr.isErr: + error "unable to get transaction receipt" + return false + return true + )) + testCond produceSingleBlockRes + +blockStatusSafeBlockGen(blockStatusSafeBlock1, false) +blockStatusSafeBlockGen(blockStatusSafeBlock2, true) + +template blockStatusFinalizedBlockGen(procName: untyped, transitionBlock: bool) = + proc procName(t: TestEnv): TestStatus = + result = TestStatus.OK + + # Wait until TTD is reached by this client + let ok = waitFor t.clMock.waitForTTD() + testCond ok + + # 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 + + let clMock = t.clMock + let client = t.rpcClient + let shadow = Shadow() + + var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks( + onPayloadProducerSelected: proc(): bool = + var address: EthAddress + let tx = t.makeNextTransaction(address, 1.u256) + let res = client.sendTransaction(tx) + if res.isErr: + error "Unable to send transaction" + return false + + shadow.hash = rlpHash(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 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) proc blockStatusReorg(t: TestEnv): TestStatus = result = TestStatus.OK @@ -487,7 +797,7 @@ proc blockStatusReorg(t: TestEnv): TestStatus = let client = t.rpcClient var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks( # Run test after a forkchoice with new HeadBlockHash has been broadcasted - onHeadBlockForkchoiceBroadcast: proc(): bool = + onForkchoiceBroadcast: proc(): bool = # Verify the client is serving the latest HeadBlock var currHeader: EthBlockHeader var hRes = client.latestHeader(currHeader) @@ -649,7 +959,7 @@ proc multipleNewCanonicalPayloads(t: TestEnv): TestStatus = return false return true )) - # At the end the CLMocker continues to try to execute fcU with the original payload, which should not fail + # At the end the clMocker continues to try to execute fcU with the original payload, which should not fail testCond produceSingleBlockRes proc outOfOrderPayloads(t: TestEnv): TestStatus = @@ -674,7 +984,7 @@ proc outOfOrderPayloads(t: TestEnv): TestStatus = let clMock = t.clMock let client = t.rpcClient var produceBlockRes = clMock.produceBlocks(payloadCount, BlockProcessCallbacks( - # We send the transactions after we got the Payload ID, before the CLMocker gets the prepared Payload + # We send the transactions after we got the Payload ID, before the clMocker gets the prepared Payload onPayloadProducerSelected: proc(): bool = for i in 0.. 0: + altParentHash = pList.sidechainPayloads[^1].blockHash + + let executableData = toExecutableData(clMock.latestPayloadBuilt) + let altPayload = customizePayload(executableData, + CustomPayload( + parentHash: some(altParentHash.hash256), + extraData: some(@[0x01.byte]), + )) + + pList.sidechainPayloads.add(altPayload) + return true + )) + + testCond r1 + + + # Produce blocks before starting the test (So we don't try to reorg back to the genesis block) + let r2= clMock.produceSingleBlock(BlockProcessCallbacks( + onGetPayload: proc(): bool = + let r = client.newPayloadV1(pList.sidechainPayloads[^1]) + if r.isErr: + return false + let s = r.get() + if s.status notin {PayloadExecutionStatus.syncing, PayloadExecutionStatus.accepted}: + return false + + # We are going to send one of the alternative payloads and fcU to it + let len = pList.sidechainPayloads.len + let forkchoiceUpdatedBack = ForkchoiceStateV1( + headBlockHash: pList.sidechainPayloads[len-1].blockHash, + safeBlockHash: pList.sidechainPayloads[len-2].blockHash, + finalizedBlockHash: pList.sidechainPayloads[len-3].blockHash, + ) + + # It is only expected that the client does not produce an error and the CL Mocker is able to progress after the re-org + let res = client.forkchoiceUpdatedV1(forkchoiceUpdatedBack) + if res.isErr: + return false + + let rs = res.get() + if rs.payloadStatus.status != PayloadExecutionStatus.syncing: + return false + + rs.payloadStatus.latestValidHash.isNone + # After this, the clMocker will continue and try to re-org to canonical chain once again + # clMocker will fail the test if this is not possible, so nothing left to do. + )) + + testCond r2 + proc transactionReorg(t: TestEnv): TestStatus = result = TestStatus.OK @@ -846,12 +1271,12 @@ proc sidechainReorg(t: TestEnv): TestStatus = let singleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks( onNewPayloadBroadcast: proc(): bool = - # At this point the CLMocker has a payload that will result in a specific outcome, + # At this point the clMocker has a payload that will result in a specific outcome, # we can produce an alternative payload, send it, fcU to it, and verify the changes var alternativePrevRandao: Hash256 doAssert nimcrypto.randomBytes(alternativePrevRandao.data) == 32 - let timestamp = Quantity toUnix(clMock.latestFinalizedHeader.timestamp + 1.seconds) + let timestamp = Quantity toUnix(clMock.latestHeader.timestamp + 1.seconds) let payloadAttributes = PayloadAttributesV1( timestamp: timestamp, prevRandao: FixedBytes[32] alternativePrevRandao.data, @@ -910,9 +1335,9 @@ proc sidechainReorg(t: TestEnv): TestStatus = )) testCond singleBlockRes - # The reorg actually happens after the CLMocker continues, + # The reorg actually happens after the clMocker continues, # verify here that the reorg was successful - let latestBlockNum = cLMock.latestFinalizedNumber.uint64 + let latestBlockNum = clMock.latestHeadNumber.uint64 testCond testCondPrevRandaoValue(t, clMock.prevRandaoHistory[latestBlockNum], latestBlockNum) proc suggestedFeeRecipient(t: TestEnv): TestStatus = @@ -1054,7 +1479,8 @@ proc postMergeSync(t: TestEnv): TestStatus = # TODO: need multiple client const engineTestList* = [ - #[TestSpec( + # Engine API Negative Test Cases + TestSpec( name: "Invalid Terminal Block in ForkchoiceUpdated", run: invalidTerminalBlockForkchoiceUpdated, ttd: 1000000 @@ -1069,6 +1495,18 @@ const engineTestList* = [ run: invalidTerminalBlockNewPayload, ttd: 1000000, ), + TestSpec( + name: "Inconsistent Head in ForkchoiceState", + run: inconsistentForkchoiceState1, + ), + TestSpec( + name: "Inconsistent Safe in ForkchoiceState", + run: inconsistentForkchoiceState2, + ), + TestSpec( + name: "Inconsistent Finalized in ForkchoiceState", + run: inconsistentForkchoiceState3, + ), TestSpec( name: "Unknown HeadBlockHash", run: unknownHeadBlockHash, @@ -1081,14 +1519,35 @@ const engineTestList* = [ name: "Unknown FinalizedBlockHash", run: unknownFinalizedBlockHash, ), + TestSpec( + name: "ForkchoiceUpdated Invalid Payload Attributes", + run: invalidPayloadAttributes1, + ), + TestSpec( + name: "ForkchoiceUpdated Invalid Payload Attributes (Syncing)", + run: invalidPayloadAttributes2, + ), TestSpec( name: "Pre-TTD ForkchoiceUpdated After PoS Switch", run: preTTDFinalizedBlockHash, ttd: 2, ), + # Invalid Payload Tests TestSpec( name: "Bad Hash on NewPayload", - run: badHashOnExecPayload, + run: badHashOnNewPayload1, + ), + TestSpec( + name: "Bad Hash on NewPayload Syncing", + run: badHashOnNewPayload2, + ), + TestSpec( + name: "Bad Hash on NewPayload Side Chain", + run: badHashOnNewPayload3, + ), + TestSpec( + name: "Bad Hash on NewPayload Side Chain Syncing", + run: badHashOnNewPayload4, ), TestSpec( name: "ParentHash==BlockHash on NewPayload", @@ -1152,21 +1611,42 @@ const engineTestList* = [ ), # Eth RPC Status on ForkchoiceUpdated Events + TestSpec( name: "Latest Block after NewPayload", - run: blockStatusExecPayload, + run: blockStatusExecPayload1, + ), + TestSpec( + name: "Latest Block after NewPayload (Transition Block)", + run: blockStatusExecPayload2, + ttd: 5, ), TestSpec( name: "Latest Block after New HeadBlock", - run: blockStatusHeadBlock, + run: blockStatusHeadBlock1, + ), + TestSpec( + name: "Latest Block after New HeadBlock (Transition Block)", + run: blockStatusHeadBlock2, + ttd: 5, ), TestSpec( name: "Latest Block after New SafeBlock", - run: blockStatusSafeBlock, + run: blockStatusSafeBlock1, + ), + TestSpec( + name: "Latest Block after New SafeBlock (Transition Block)", + run: blockStatusSafeBlock2, + ttd: 5, ), TestSpec( name: "Latest Block after New FinalizedBlock", - run: blockStatusFinalizedBlock, + run: blockStatusFinalizedBlock1, + ), + TestSpec( + name: "Latest Block after New FinalizedBlock (Transition Block)", + run: blockStatusFinalizedBlock2, + ttd: 5, ), TestSpec( name: "Latest Block after Reorg", @@ -1196,12 +1676,20 @@ const engineTestList* = [ name: "Sidechain Reorg", run: sidechainReorg, ), + TestSpec( + name: "Re-Org Back into Canonical Chain", + run: reorgBack, + ), + TestSpec( + name: "Re-Org Back to Canonical Chain From Syncing Chain", + run: reorgBackFromSyncing, + ), # Suggested Fee Recipient in Payload creation TestSpec( name: "Suggested Fee Recipient Test", run: suggestedFeeRecipient, - ),]# + ), # TODO: debug and fix # PrevRandao opcode tests diff --git a/nimbus/rpc/engine_api.nim b/nimbus/rpc/engine_api.nim index fd52fca4b..93dfede0e 100644 --- a/nimbus/rpc/engine_api.nim +++ b/nimbus/rpc/engine_api.nim @@ -300,7 +300,7 @@ proc setupEngineAPI*( if res.isErr: error "Failed to create sealing payload", err = res.error - return simpleFCU(PayloadExecutionStatus.invalid, res.error) + raise (ref InvalidRequest)(code: engineApiInvalidPayloadAttributes, msg: res.error) let id = computePayloadId(blockHash, payloadAttrs) api.put(id, payload)