diff --git a/hive_integration/nodocker/engine/clmock.nim b/hive_integration/nodocker/engine/clmock.nim index 459368f3f..703112eca 100644 --- a/hive_integration/nodocker/engine/clmock.nim +++ b/hive_integration/nodocker/engine/clmock.nim @@ -34,8 +34,8 @@ type client : RpcClient ttd : DifficultyInt - slotsToSafe : int - slotsToFinalized : int + slotsToSafe* : int + slotsToFinalized* : int headHashHistory : seq[BlockHash] BlockProcessCallbacks* = object diff --git a/hive_integration/nodocker/engine/engine_client.nim b/hive_integration/nodocker/engine/engine_client.nim index 3c876d361..1a02e4c30 100644 --- a/hive_integration/nodocker/engine/engine_client.nim +++ b/hive_integration/nodocker/engine/engine_client.nim @@ -167,7 +167,7 @@ proc namedHeader*(client: RpcClient, name: string, output: var common.BlockHeade 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) @@ -196,9 +196,14 @@ proc txReceipt*(client: RpcClient, txHash: Hash256): Result[eth_api.ReceiptObjec except ValueError as e: return err(e.msg) +proc toDataStr(slot: UInt256): HexDataStr = + let hex = slot.toHex + let prefix = if hex.len mod 2 == 0: "0x" else: "0x0" + HexDataStr(prefix & hex) + proc storageAt*(client: RpcClient, address: EthAddress, slot: UInt256): Result[UInt256, string] = try: - let res = waitFor client.eth_getStorageAt(ethAddressStr(address), encodeQuantity(slot), "latest") + let res = waitFor client.eth_getStorageAt(ethAddressStr(address), toDataStr(slot), "latest") return ok(UInt256.fromHex(res.string)) except ValueError as e: return err(e.msg) @@ -206,7 +211,7 @@ proc storageAt*(client: RpcClient, address: EthAddress, slot: UInt256): Result[U proc storageAt*(client: RpcClient, address: EthAddress, slot: UInt256, number: common.BlockNumber): Result[UInt256, string] = try: let tag = encodeQuantity(number) - let res = waitFor client.eth_getStorageAt(ethAddressStr(address), encodeQuantity(slot), tag.string) + let res = waitFor client.eth_getStorageAt(ethAddressStr(address), toDataStr(slot), tag.string) return ok(UInt256.fromHex(res.string)) except ValueError as e: return err(e.msg) diff --git a/hive_integration/nodocker/engine/engine_sim.nim b/hive_integration/nodocker/engine/engine_sim.nim index 68b8c9a92..55e01fd32 100644 --- a/hive_integration/nodocker/engine/engine_sim.nim +++ b/hive_integration/nodocker/engine/engine_sim.nim @@ -11,6 +11,10 @@ proc main() = for x in engineTestList: var t = setupELClient(x.chainFile) t.setRealTTD(x.ttd) + if x.slotsToFinalized != 0: + t.slotsToFinalized(x.slotsToFinalized) + if x.slotsToSafe != 0: + t.slotsToSafe(x.slotsToSafe) let status = x.run(t) t.stopELClient() stat.inc(x.name, status) diff --git a/hive_integration/nodocker/engine/engine_tests.nim b/hive_integration/nodocker/engine/engine_tests.nim index 1a8fc0789..2541f5db9 100644 --- a/hive_integration/nodocker/engine/engine_tests.nim +++ b/hive_integration/nodocker/engine/engine_tests.nim @@ -17,6 +17,8 @@ type run*: proc(t: TestEnv): TestStatus ttd*: int64 chainFile*: string + slotsToFinalized*: int + slotsToSafe*: int const prevRandaoContractAddr = hexToByteArray[20]("0000000000000000000000000000000000000316") @@ -871,6 +873,130 @@ template blockStatusExecPayloadGen(procname: untyped, transitionBlock: bool) = blockStatusExecPayloadGen(blockStatusExecPayload1, false) blockStatusExecPayloadGen(blockStatusExecPayload2, true) +type + MissingAncestorShadow = ref object + cA: ExecutionPayloadV1 + n: int + altChainPayloads: seq[ExecutionPayloadV1] + +# Attempt to re-org to a chain which at some point contains an unknown payload which is also invalid. +# Then reveal the invalid payload and expect that the client rejects it and rejects forkchoice updated calls to this chain. +# The invalid_index parameter determines how many payloads apart is the common ancestor from the block that invalidates the chain, +# with a value of 1 meaning that the immediate payload after the common ancestor will be invalid. +template invalidMissingAncestorReOrgGen(procName: untyped, + invalid_index: int, payloadField: InvalidPayloadField, p2psync: bool, emptyTxs: 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 + testCond clMock.produceBlocks(5, BlockProcessCallbacks()) + + let shadow = MissingAncestorShadow( + # Save the common ancestor + cA: clMock.latestPayloadBuilt, + + # Amount of blocks to deviate starting from the common ancestor + n: 10, + + # Slice to save the alternate B chain + altChainPayloads: @[] + ) + + # Append the common ancestor + shadow.altChainPayloads.add shadow.cA + + # Produce blocks but at the same time create an alternate chain which contains an invalid payload at some point (INV_P) + # CommonAncestor◄─▲── P1 ◄─ P2 ◄─ P3 ◄─ ... ◄─ Pn + # │ + # └── P1' ◄─ P2' ◄─ ... ◄─ INV_P ◄─ ... ◄─ Pn' + var pbRes = clMock.produceBlocks(shadow.n, BlockProcessCallbacks( + onPayloadProducerSelected: proc(): bool = + # Function to send at least one transaction each block produced. + # Empty Txs Payload with invalid stateRoot discovered an issue in geth sync, hence this is customizable. + when not emptyTxs: + # Send the transaction to the prevRandaoContractAddr + t.sendTx(1.u256) + return true + , + onGetPayload: proc(): bool = + # Insert extraData to ensure we deviate from the main payload, which contains empty extradata + var alternatePayload = customizePayload(clMock.latestPayloadBuilt, CustomPayload( + parentHash: some(shadow.altChainPayloads[^1].blockHash.hash256), + extraData: some(@[1.byte]), + )) + + if shadow.altChainPayloads.len == invalid_index: + alternatePayload = generateInvalidPayload(alternatePayload, payloadField) + + shadow.altChainPayloads.add alternatePayload + return true + )) + testCond pbRes + + pbRes = clMock.produceSingleBlock(BlockProcessCallbacks( + # Note: We perform the test in the middle of payload creation by the CL Mock, in order to be able to + # re-org back into this chain and use the new payload without issues. + onGetPayload: proc(): bool = + # Now let's send the alternate chain to the client using newPayload/sync + for i in 1..shadow.n: + # Send the payload + var payloadValidStr = "VALID" + if i == invalid_index: + payloadValidStr = "INVALID" + elif i > invalid_index: + payloadValidStr = "VALID with INVALID ancestor" + + info "Invalid chain payload", + i, + payloadValidStr, + hash = shadow.altChainPayloads[i].blockHash.toHex + + let rr = client.newPayloadV1(shadow.altChainPayloads[i]) + testCond rr.isOk + + let rs = client.forkchoiceUpdatedV1(ForkchoiceStateV1( + headBlockHash: shadow.altChainPayloads[i].blockHash, + safeBlockHash: shadow.altChainPayloads[i].blockHash + )) + + if i == invalid_index: + # If this is the first payload after the common ancestor, and this is the payload we invalidated, + # then we have all the information to determine that this payload is invalid. + testNP(rr, invalid, some(shadow.altChainPayloads[i-1].blockHash.hash256)) + elif i > invalid_index: + # We have already sent the invalid payload, but the client could've discarded it. + # In reality the CL will not get to this point because it will have already received the `INVALID` + # response from the previous payload. + let cond = {PayloadExecutionStatus.accepted, PayloadExecutionStatus.syncing, PayloadExecutionStatus.invalid} + testNPEither(rr, cond, some(Hash256())) + else: + # This is one of the payloads before the invalid one, therefore is valid. + testNP(rr, valid) + testFCU(rs, valid, some(shadow.altChainPayloads[i].blockHash.hash256)) + + + # Resend the latest correct fcU + let rx = client.forkchoiceUpdatedV1(clMock.latestForkchoice) + testCond rx.isOk + + # After this point, the CL Mock will send the next payload of the canonical chain + return true + )) + + testCond pbRes + +invalidMissingAncestorReOrgGen(invalidMissingAncestor1, 1, InvalidStateRoot, false, true) +invalidMissingAncestorReOrgGen(invalidMissingAncestor2, 9, InvalidStateRoot, false, true) +invalidMissingAncestorReOrgGen(invalidMissingAncestor3, 10, InvalidStateRoot, false, true) + template blockStatusHeadBlockGen(procname: untyped, transitionBlock: bool) = proc procName(t: TestEnv): TestStatus = result = TestStatus.OK @@ -1819,6 +1945,23 @@ const engineTestList* = [ run: invalidPayload15, ), + # Invalid Ancestor Re-Org Tests (Reveal via newPayload) + TestSpec( + name: "Invalid Ancestor Chain Re-Org, Invalid StateRoot, Invalid P1', Reveal using newPayload", + slotsToFinalized: 20, + run: invalidMissingAncestor1, + ), + TestSpec( + name: "Invalid Ancestor Chain Re-Org, Invalid StateRoot, Invalid P9', Reveal using newPayload", + slotsToFinalized: 20, + run: invalidMissingAncestor2, + ), + TestSpec( + name: "Invalid Ancestor Chain Re-Org, Invalid StateRoot, Invalid P10', Reveal using newPayload", + slotsToFinalized: 20, + run: invalidMissingAncestor3, + ), + # Eth RPC Status on ForkchoiceUpdated Events TestSpec( # TODO: fix/debug name: "Latest Block after NewPayload", diff --git a/hive_integration/nodocker/engine/helper.nim b/hive_integration/nodocker/engine/helper.nim index dd24b93c8..e78a8ceb3 100644 --- a/hive_integration/nodocker/engine/helper.nim +++ b/hive_integration/nodocker/engine/helper.nim @@ -180,6 +180,9 @@ proc toExecutableData*(payload: ExecutionPayloadV1): ExecutableData = let tx = rlp.decode(distinctBase data, Transaction) result.transactions.add tx +proc customizePayload*(basePayload: ExecutionPayloadV1, customData: CustomPayload): ExecutionPayloadV1 = + customizePayload(basePayload.toExecutableData, customData) + proc debugPrevRandaoTransaction*(client: RpcClient, tx: Transaction, expectedPrevRandao: Hash256): Result[void, string] = try: let hash = tx.rlpHash diff --git a/hive_integration/nodocker/engine/test_env.nim b/hive_integration/nodocker/engine/test_env.nim index 9404b33b1..ca1624082 100644 --- a/hive_integration/nodocker/engine/test_env.nim +++ b/hive_integration/nodocker/engine/test_env.nim @@ -142,6 +142,12 @@ proc setRealTTD*(t: TestEnv, ttdValue: int64) = t.ttd = realTTD t.clmock = newCLMocker(t.rpcClient, realTTD) +proc slotsToSafe*(t: TestEnv, x: int) = + t.clMock.slotsToSafe = x + +proc slotsToFinalized*(t: TestEnv, x: int) = + t.clMock.slotsToFinalized = x + func gwei(n: int): GasInt {.compileTime.} = GasInt(n * (10 ^ 9))