diff --git a/hive_integration/nodocker/engine/clmock.nim b/hive_integration/nodocker/engine/clmock.nim new file mode 100644 index 000000000..3913e195c --- /dev/null +++ b/hive_integration/nodocker/engine/clmock.nim @@ -0,0 +1,329 @@ +import + std/[times, tables], + chronicles, + nimcrypto, + stew/byteutils, + eth/common, chronos, + web3/engine_api_types, + json_rpc/rpcclient, + ../../../nimbus/merge/mergeutils, + ../../../nimbus/debug, + ./engine_client + +# Consensus Layer Client Mock used to sync the Execution Clients once the TTD has been reached +type + CLMocker* = ref object + nextFeeRecipient*: EthAddress + nextPayloadID: PayloadID + + # PoS Chain History Information + prevRandaoHistory*: Table[uint64, Hash256] + executedPayloadHistory*: Table[uint64, ExecutionPayloadV1] + + # Latest broadcasted data using the PoS Engine API + latestFinalizedNumber*: uint64 + latestFinalizedHeader*: common.BlockHeader + latestPayloadBuilt* : ExecutionPayloadV1 + latestExecutedPayload*: ExecutionPayloadV1 + latestForkchoice* : ForkchoiceStateV1 + + # Merge related + firstPoSBlockNumber : Option[uint64] + ttdReached : bool + + client : RpcClient + 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.} + + +proc init*(cl: CLMocker, client: RpcClient, ttd: DifficultyInt) = + cl.client = client + cl.ttd = ttd + +proc newClMocker*(client: RpcClient, ttd: DifficultyInt): CLMocker = + new result + result.init(client, ttd) + +proc waitForTTD*(cl: CLMocker): Future[bool] {.async.} = + let (header, waitRes) = await cl.client.waitForTTD(cl.ttd) + if not waitRes: + error "timeout while waiting for TTD" + return false + + cl.latestFinalizedHeader = header + cl.ttdReached = true + + let headerHash = BlockHash(common.blockHash(cl.latestFinalizedHeader).data) + cl.latestForkchoice.headBlockHash = headerHash + cl.latestForkchoice.safeBlockHash = headerHash + cl.latestForkchoice.finalizedBlockHash = headerHash + cl.latestFinalizedNumber = cl.latestFinalizedHeader.blockNumber.truncate(uint64) + + let res = cl.client.forkchoiceUpdatedV1(cl.latestForkchoice) + if res.isErr: + error "forkchoiceUpdated error", msg=res.error + return false + + let s = res.get() + if s.payloadStatus.status != PayloadExecutionStatus.valid: + error "forkchoiceUpdated response", + status=s.payloadStatus.status + return false + + return true + +proc pickNextPayloadProducer(cl: CLMocker): bool = + let nRes = cl.client.blockNumber() + if nRes.isErr: + error "CLMocker: could not get block number", msg=nRes.error + return false + + let lastBlockNumber = nRes.get + if cl.latestFinalizedNumber != lastBlockNumber: + return false + + var header: common.BlockHeader + let hRes = cl.client.headerByNumber(lastBlockNumber, header) + if hRes.isErr: + error "CLMocker: Could not get block header", msg=hRes.error + return false + + let lastBlockHash = header.blockHash + if cl.latestFinalizedHeader.blockHash != lastBlockHash: + error "CLMocker: Failed to obtain a client on the latest block number" + return false + + return true + +proc getNextPayloadID(cl: CLMocker): bool = + # Generate a random value for the PrevRandao field + var nextPrevRandao: Hash256 + doAssert nimcrypto.randomBytes(nextPrevRandao.data) == 32 + + let timestamp = Quantity toUnix(cl.latestFinalizedHeader.timestamp + 1.seconds) + let payloadAttributes = PayloadAttributesV1( + timestamp: timestamp, + prevRandao: FixedBytes[32] nextPrevRandao.data, + suggestedFeeRecipient: Address cl.nextFeeRecipient, + ) + + # Save random value + let number = cl.latestFinalizedHeader.blockNumber.truncate(uint64) + 1 + cl.prevRandaoHistory[number] = nextPrevRandao + + let res = cl.client.forkchoiceUpdatedV1(cl.latestForkchoice, some(payloadAttributes)) + if res.isErr: + error "CLMocker: Could not send forkchoiceUpdatedV1", msg=res.error + return false + + let s = res.get() + if s.payloadStatus.status != PayloadExecutionStatus.valid: + error "CLMocker: Unexpected forkchoiceUpdated Response from Payload builder", + status=s.payloadStatus.status + + doAssert s.payLoadID.isSome + cl.nextPayloadID = s.payloadID.get() + return true + +proc getNextPayload(cl: CLMocker): bool = + let res = cl.client.getPayloadV1(cl.nextPayloadID) + if res.isErr: + error "CLMocker: Could not getPayload", + payloadID=toHex(cl.nextPayloadID) + return false + + cl.latestPayloadBuilt = res.get() + let header = toBlockHeader(cl.latestPayloadBuilt) + let blockHash = BlockHash header.blockHash.data + if blockHash != cl.latestPayloadBuilt.blockHash: + error "getNextPayload blockHash mismatch", + expected=cl.latestPayloadBuilt.blockHash.toHex, + get=blockHash.toHex + return false + + return true + +proc broadcastNewPayload(cl: CLMocker, payload: ExecutionPayloadV1): Result[PayloadStatusV1, string] = + let res = cl.client.newPayloadV1(payload) + return res + +proc broadcastNextNewPayload(cl: CLMocker): bool = + let res = cl.broadcastNewPayload(cl.latestPayloadBuilt) + if res.isErr: + error "CLMocker: broadcastNewPayload Error", msg=res.error + return false + + let s = res.get() + if s.status == PayloadExecutionStatus.valid: + # The client is synced and the payload was immediately validated + # https://github.com/ethereum/execution-apis/blob/main/src/engine/specification.md: + # - If validation succeeds, the response MUST contain {status: VALID, latestValidHash: payload.blockHash} + let blockHash = cl.latestPayloadBuilt.blockHash + if s.latestValidHash.isNone: + error "CLMocker: NewPayload returned VALID status with nil LatestValidHash", + expected=blockHash.toHex + return false + + let latestValidHash = s.latestValidHash.get() + if latestValidHash != BlockHash(blockHash): + error "CLMocker: NewPayload returned VALID status with incorrect LatestValidHash", + get=latestValidHash.toHex, expected=blockHash.toHex + return false + + elif s.status == PayloadExecutionStatus.accepted: + # The client is not synced but the payload was accepted + # https://github.com/ethereum/execution-apis/blob/main/src/engine/specification.md: + # - {status: ACCEPTED, latestValidHash: null, validationError: null} if the following conditions are met: + # the blockHash of the payload is valid + # the payload doesn't extend the canonical chain + # the payload hasn't been fully validated. + let nullHash = BlockHash Hash256().data + let latestValidHash = s.latestValidHash.get(nullHash) + if s.latestValidHash.isSome and latestValidHash != nullHash: + error "CLMocker: NewPayload returned ACCEPTED status with incorrect LatestValidHash", + hash=latestValidHash.toHex + return false + + else: + error "CLMocker: broadcastNewPayload Response", + status=s.status + return false + + cl.latestExecutedPayload = cl.latestPayloadBuilt + let number = uint64 cl.latestPayloadBuilt.blockNumber + cl.executedPayloadHistory[number] = cl.latestPayloadBuilt + return true + +proc broadcastForkchoiceUpdated*(cl: CLMocker, + update: ForkchoiceStateV1): Result[ForkchoiceUpdatedResponse, string] = + let res = cl.client.forkchoiceUpdatedV1(update) + return res + +proc broadcastLatestForkchoice(cl: CLMocker): bool = + let res = cl.broadcastForkchoiceUpdated(cl.latestForkchoice) + if res.isErr: + error "CLMocker: broadcastForkchoiceUpdated Error", msg=res.error + return false + + let s = res.get() + if s.payloadStatus.status != PayloadExecutionStatus.valid: + error "CLMocker: broadcastForkchoiceUpdated Response", + status=s.payloadStatus.status + return false + + return true + +proc produceSingleBlock*(cl: CLMocker, cb: BlockProcessCallbacks): bool {.gcsafe.} = + doAssert(cl.ttdReached) + + if not cl.pickNextPayloadProducer(): + return false + + if cb.onPayloadProducerSelected != nil: + if not cb.onPayloadProducerSelected(): + return false + + if not cl.getNextPayloadID(): + return false + + if cb.onGetPayloadID != nil: + if not cb.onGetPayloadID(): + return false + + # Give the client a delay between getting the payload ID and actually retrieving the payload + #time.Sleep(PayloadProductionClientDelay) + + if not cl.getNextPayload(): + return false + + if cb.onGetPayload != nil: + if not cb.onGetPayload(): + return false + + if not cl.broadcastNextNewPayload(): + return false + + if cb.onNewPayloadBroadcast != nil: + if not cb.onNewPayloadBroadcast(): + return false + + # Broadcast forkchoice updated with new HeadBlock to all clients + let blockHash = cl.latestPayloadBuilt.blockHash + cl.latestForkchoice.headBlockHash = blockHash + if not cl.broadcastLatestForkchoice(): + return false + + if cb.onHeadBlockForkchoiceBroadcast != nil: + if not cb.onHeadBlockForkchoiceBroadcast(): + return false + + # Broadcast forkchoice updated with new SafeBlock to all clients + cl.latestForkchoice.safeBlockHash = blockHash + if not cl.broadcastLatestForkchoice(): + return false + + if cb.onSafeBlockForkchoiceBroadcast != nil: + if not cb.onSafeBlockForkchoiceBroadcast(): + return false + + # Broadcast forkchoice updated with new FinalizedBlock to all clients + cl.latestForkchoice.finalizedBlockHash = blockHash + if not cl.broadcastLatestForkchoice(): + return false + + # Save the number of the first PoS block + if cl.firstPoSBlockNumber.isNone: + let number = cl.latestFinalizedHeader.blockNumber.truncate(uint64) + 1 + cl.firstPoSBlockNumber = some(number) + + # Save the header of the latest block in the PoS chain + cl.latestFinalizedNumber = cl.latestFinalizedNumber + 1 + + # Check if any of the clients accepted the new payload + var newHeader: common.BlockHeader + let res = cl.client.headerByNumber(cl.latestFinalizedNumber, newHeader) + if res.isErr: + error "CLMock ProduceSingleBlock", msg=res.error + return false + + let newHash = BlockHash newHeader.blockHash.data + if newHash != cl.latestPayloadBuilt.blockHash: + error "CLMocker: None of the clients accepted the newly constructed payload", + hash=newHash.toHex + return false + + cl.latestFinalizedHeader = newHeader + + if cb.onFinalizedBlockForkchoiceBroadcast != nil: + if not cb.onFinalizedBlockForkchoiceBroadcast(): + return false + + return true + +# Loop produce PoS blocks by using the Engine API +proc produceBlocks*(cl: CLMocker, blockCount: int, cb: BlockProcessCallbacks): bool {.gcsafe.} = + # Produce requested amount of blocks + for i in 0.. bn: + return false + + return true diff --git a/hive_integration/nodocker/engine/engine_client.nim b/hive_integration/nodocker/engine/engine_client.nim new file mode 100644 index 000000000..d8ef0a892 --- /dev/null +++ b/hive_integration/nodocker/engine/engine_client.nim @@ -0,0 +1,191 @@ +import + std/[times, json], + stew/byteutils, + eth/[common, rlp], chronos, + web3/engine_api_types, + json_rpc/rpcclient, + ../../../tests/rpcclient/eth_api, + ../../../premix/parser, + ../../../nimbus/rpc/hexstrings, + ../../../premix/parser + +import web3/engine_api as web3_engine_api + +proc forkchoiceUpdatedV1*(client: RpcClient, + update: ForkchoiceStateV1, + payloadAttributes = none(PayloadAttributesV1)): + Result[ForkchoiceUpdatedResponse, string] = + try: + let res = waitFor client.engine_forkchoiceUpdatedV1(update, payloadAttributes) + return ok(res) + except ValueError as e: + return err(e.msg) + +proc getPayloadV1*(client: RpcClient, payloadId: PayloadID): Result[ExecutionPayloadV1, string] = + try: + let res = waitFor client.engine_getPayloadV1(payloadId) + return ok(res) + except ValueError as e: + return err(e.msg) + +proc newPayloadV1*(client: RpcClient, + payload: ExecutionPayloadV1): + Result[PayloadStatusV1, string] = + try: + let res = waitFor client.engine_newPayloadV1(payload) + return ok(res) + except ValueError as e: + return err(e.msg) + +proc toBlockNumber(n: Option[HexQuantityStr]): common.BlockNumber = + if n.isNone: + return 0.toBlockNumber + toBlockNumber(hexToInt(string n.get, uint64)) + +proc toBlockNonce(n: Option[HexDataStr]): common.BlockNonce = + if n.isNone: + return default(BlockNonce) + hexToByteArray(string n.get, result) + +proc toBaseFeePerGas(n: Option[HexQuantityStr]): Option[UInt256] = + if n.isNone: + return none(UInt256) + some(UInt256.fromHex(string n.get)) + +proc toBlockHeader(bc: eth_api.BlockObject): common.BlockHeader = + common.BlockHeader( + blockNumber: toBlockNumber(bc.number), + parentHash : bc.parentHash, + nonce : toBlockNonce(bc.nonce), + ommersHash : bc.sha3Uncles, + bloom : BloomFilter bc.logsBloom, + txRoot : bc.transactionsRoot, + stateRoot : bc.stateRoot, + receiptRoot: bc.receiptsRoot, + coinbase : bc.miner, + difficulty : UInt256.fromHex(string bc.difficulty), + extraData : hexToSeqByte(string bc.extraData), + mixDigest : bc.mixHash, + gasLimit : hexToInt(string bc.gasLimit, GasInt), + gasUsed : hexToInt(string bc.gasUsed, GasInt), + timestamp : initTime(hexToInt(string bc.timestamp, int64), 0), + fee : toBaseFeePerGas(bc.baseFeePerGas) + ) + +proc toTransactions(txs: openArray[JsonNode]): seq[Transaction] = + for x in txs: + result.add parseTransaction(x) + +proc waitForTTD*(client: RpcClient, + ttd: DifficultyInt): Future[(common.BlockHeader, bool)] {.async.} = + let period = chronos.seconds(5) + var loop = 0 + var emptyHeader: common.BlockHeader + while loop < 5: + let res = await client.eth_getBlockByNumber("latest", false) + if res.isNone: + return (emptyHeader, false) + let bc = res.get() + if hexToInt(string bc.totalDifficulty, int64).u256 >= ttd: + return (toBlockHeader(bc), true) + + await sleepAsync(period) + inc loop + + return (emptyHeader, false) + +proc blockNumber*(client: RpcClient): Result[uint64, string] = + try: + let res = waitFor client.eth_blockNumber() + return ok(hexToInt(string res, uint64)) + except ValueError as e: + return err(e.msg) + +proc headerByNumber*(client: RpcClient, number: uint64, output: var common.BlockHeader): Result[void, string] = + try: + let qty = encodeQuantity(number) + let res = waitFor client.eth_getBlockByNumber(string qty, false) + if res.isNone: + return err("failed to get blockHeader: " & $number) + output = toBlockHeader(res.get()) + return ok() + except ValueError as e: + return err(e.msg) + +proc blockByNumber*(client: RpcClient, number: uint64, output: var common.EthBlock): Result[void, string] = + try: + let qty = encodeQuantity(number) + let res = waitFor client.eth_getBlockByNumber(string qty, true) + if res.isNone: + return err("failed to get block: " & $number) + let blk = res.get() + output.header = toBlockHeader(blk) + output.txs = toTransactions(blk.transactions) + return ok() + except ValueError as e: + return err(e.msg) + +proc latestHeader*(client: RpcClient, output: var common.BlockHeader): Result[void, string] = + try: + let res = waitFor client.eth_getBlockByNumber("latest", false) + if res.isNone: + return err("failed to get latest blockHeader") + output = toBlockHeader(res.get()) + return ok() + except ValueError as e: + return err(e.msg) + +proc latestBlock*(client: RpcClient, output: var common.EthBlock): Result[void, string] = + try: + let res = waitFor client.eth_getBlockByNumber("latest", true) + if res.isNone: + return err("failed to get latest blockHeader") + let blk = res.get() + output.header = toBlockHeader(blk) + output.txs = toTransactions(blk.transactions) + 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) + let res = waitFor client.eth_sendRawTransaction(hexDataStr(encodedTx)) + let txHash = rlpHash(tx) + let getHash = Hash256(data: hexToByteArray[32](string res)) + if txHash != getHash: + return err("sendTransaction: tx hash mismatch") + return ok() + except ValueError as e: + return err(e.msg) + +proc balanceAt*(client: RpcClient, address: EthAddress): Result[UInt256, string] = + try: + let res = waitFor client.eth_getBalance(ethAddressStr(address), "latest") + return ok(UInt256.fromHex(res.string)) + except ValueError as e: + return err(e.msg) + +proc txReceipt*(client: RpcClient, txHash: Hash256): Result[eth_api.ReceiptObject, string] = + try: + let res = waitFor client.eth_getTransactionReceipt(txHash) + if res.isNone: + return err("failed to get receipt: " & txHash.data.toHex) + return ok(res.get) + except ValueError as e: + return err(e.msg) + +proc storageAt*(client: RpcClient, address: EthAddress, slot: UInt256): Result[UInt256, string] = + try: + let res = waitFor client.eth_getStorageAt(ethAddressStr(address), encodeQuantity(slot), "latest") + return ok(UInt256.fromHex(res.string)) + except ValueError as e: + return err(e.msg) + +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) + 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 new file mode 100644 index 000000000..a715fc4c7 --- /dev/null +++ b/hive_integration/nodocker/engine/engine_sim.nim @@ -0,0 +1,19 @@ +import + test_env, + engine_tests, + chronos, + unittest2 + +proc runTest(x: TestSpec, testStatusIMPL: var TestStatus) = + var t = setupELClient() + t.setRealTTD(x.ttd) + x.run(t, testStatusIMPL) + t.stopELClient() + +proc main() = + suite "Engine Tests": + for x in engineTestList: + test x.name: + runTest(x, testStatusIMPL) + +main() diff --git a/hive_integration/nodocker/engine/engine_tests.nim b/hive_integration/nodocker/engine/engine_tests.nim new file mode 100644 index 000000000..2774b1292 --- /dev/null +++ b/hive_integration/nodocker/engine/engine_tests.nim @@ -0,0 +1,1291 @@ +import + std/tables, + test_env, + stew/byteutils, + chronicles, + unittest2, + nimcrypto, + chronos, + ./helper, + ../../../nimbus/transaction, + ../../../nimbus/rpc/rpc_types, + ../../../nimbus/merge/mergeutils + +type + TestSpec* = object + name*: string + run*: proc(t: TestEnv, testStatusIMPL: var TestStatus) + ttd*: int64 + +const + prevRandaoContractAddr = hexToByteArray[20]("0000000000000000000000000000000000000316") + +# Invalid Terminal Block in ForkchoiceUpdated: +# Client must reject ForkchoiceUpdated directives if the referenced HeadBlockHash does not meet the TTD requirement. +proc invalidTerminalBlockForkchoiceUpdated(t: TestEnv, testStatusIMPL: var TestStatus) = + let + gHash = Web3BlockHash t.gHeader.blockHash.data + forkchoiceState = ForkchoiceStateV1( + headBlockHash: gHash, + safeBlockHash: gHash, + finalizedBlockHash: gHash, + ) + + let res = t.rpcClient.forkchoiceUpdatedV1(forkchoiceState) + + # Execution specification: + # {payloadStatus: {status: INVALID_TERMINAL_BLOCK, latestValidHash: null, validationError: errorMessage | null}, payloadId: null} + # either obtained from the Payload validation process or as a result of validating a PoW block referenced by forkchoiceState.headBlockHash + check res.isOk + + if res.isErr: + return + + let s = res.get() + check s.payloadStatus.status == PayloadExecutionStatus.invalid_terminal_block + check s.payloadStatus.latestValidHash.isNone + check s.payloadId.isNone + + # ValidationError is not validated since it can be either null or a string message + +# Invalid GetPayload Under PoW: Client must reject GetPayload directives under PoW. +proc invalidGetPayloadUnderPoW(t: TestEnv, testStatusIMPL: var TestStatus) = + # We start in PoW and try to get an invalid Payload, which should produce an error but nothing should be disrupted. + let id = PayloadID [1.byte, 2,3,4,5,6,7,8] + let res = t.rpcClient.getPayloadV1(id) + check res.isErr + +# Invalid Terminal Block in NewPayload: +# Client must reject NewPayload directives if the referenced ParentHash does not meet the TTD requirement. +proc invalidTerminalBlockNewPayload(t: TestEnv, testStatusIMPL: var TestStatus) = + let gBlock = t.gHeader + let payload = ExecutableData( + parentHash: gBlock.blockHash, + stateRoot: gBlock.stateRoot, + receiptsRoot: BLANK_ROOT_HASH, + number: 1, + gasLimit: gBlock.gasLimit, + gasUsed: 0, + timestamp: gBlock.timestamp + 1.seconds, + baseFeePerGas:gBlock.baseFee + ) + let hashedPayload = customizePayload(payload, CustomPayload()) + let res = t.rpcClient.newPayloadV1(hashedPayload) + + # Execution specification: + # {status: INVALID_TERMINAL_BLOCK, latestValidHash: null, validationError: errorMessage | null} + # if terminal block conditions are not satisfied + check res.isOk + if res.isErr: + return + + let s = res.get() + check s.status == PayloadExecutionStatus.invalid_terminal_block + check s.latestValidHash.isNone + +proc unknownHeadBlockHash(t: TestEnv, testStatusIMPL: var TestStatus) = + let ok = waitFor t.clMock.waitForTTD() + check ok + + if not ok: + return + + var randomHash: Hash256 + check nimcrypto.randomBytes(randomHash.data) == 32 + + let clMock = t.clMock + let forkchoiceStateUnknownHeadHash = ForkchoiceStateV1( + headBlockHash: BlockHash randomHash.data, + safeBlockHash: clMock.latestForkchoice.finalizedBlockHash, + finalizedBlockHash: clMock.latestForkchoice.finalizedBlockHash, + ) + + var res = t.rpcClient.forkchoiceUpdatedV1(forkchoiceStateUnknownHeadHash) + check res.isOk + if res.isErr: + return + + let s = res.get() + # Execution specification:: + # - {payloadStatus: {status: SYNCING, latestValidHash: null, validationError: null}, payloadId: null} + # if forkchoiceState.headBlockHash references an unknown payload or a payload that can't be validated + # because requisite data for the validation is missing + check s.payloadStatus.status == PayloadExecutionStatus.syncing + + # Test again using PayloadAttributes, should also return SYNCING and no PayloadID + let timestamp = uint64 clMock.latestExecutedPayload.timestamp + let payloadAttr = PayloadAttributesV1( + timestamp: Quantity(timestamp + 1) + ) + + res = t.rpcClient.forkchoiceUpdatedV1(forkchoiceStateUnknownHeadHash, some(payloadAttr)) + check res.isOk + if res.isErr: + return + check s.payloadStatus.status == PayloadExecutionStatus.syncing + check s.payloadId.isNone + +proc unknownSafeBlockHash(t: TestEnv, testStatusIMPL: var TestStatus) = + let ok = waitFor t.clMock.waitForTTD() + check ok + + if not ok: + return + + # Produce blocks before starting the test + let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks()) + check produce5BlockRes + + let clMock = t.clMock + let client = t.rpcClient + let produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks( + # Run test after a new payload has been broadcast + onNewPayloadBroadcast: proc(): bool = + # Generate a random SafeBlock hash + var randomSafeBlockHash: Hash256 + doAssert nimcrypto.randomBytes(randomSafeBlockHash.data) == 32 + + # Send forkchoiceUpdated with random SafeBlockHash + let forkchoiceStateUnknownSafeHash = ForkchoiceStateV1( + headBlockHash: clMock.latestExecutedPayload.blockHash, + safeBlockHash: BlockHash randomSafeBlockHash.data, + finalizedBlockHash: clMock.latestForkchoice.finalizedBlockHash, + ) + # Execution specification: + # - This value MUST be either equal to or an ancestor of headBlockHash + let res = client.forkchoiceUpdatedV1(forkchoiceStateUnknownSafeHash) + return res.isErr + )) + + check produceSingleBlockRes + +proc unknownFinalizedBlockHash(t: TestEnv, testStatusIMPL: var TestStatus) = + let ok = waitFor t.clMock.waitForTTD() + check ok + + if not ok: + return + + # Produce blocks before starting the test + let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks()) + check produce5BlockRes + + let clMock = t.clMock + let client = t.rpcClient + let produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks( + # Run test after a new payload has been broadcast + onNewPayloadBroadcast: proc(): bool = + # Generate a random SafeBlock hash + var randomFinalBlockHash: Hash256 + doAssert nimcrypto.randomBytes(randomFinalBlockHash.data) == 32 + + # Send forkchoiceUpdated with random SafeBlockHash + let forkchoiceStateUnknownFinalizedHash = ForkchoiceStateV1( + headBlockHash: clMock.latestExecutedPayload.blockHash, + safeBlockHash: clMock.latestForkchoice.safeBlockHash, + finalizedBlockHash: BlockHash randomFinalBlockHash.data, + ) + # Execution specification: + # - This value MUST be either equal to or an ancestor of headBlockHash + var res = client.forkchoiceUpdatedV1(forkchoiceStateUnknownFinalizedHash) + if res.isOk: + return false + + # Test again using PayloadAttributes, should also return INVALID and no PayloadID + let timestamp = uint64 clMock.latestExecutedPayload.timestamp + let payloadAttr = PayloadAttributesV1( + timestamp: Quantity(timestamp + 1) + ) + res = client.forkchoiceUpdatedV1(forkchoiceStateUnknownFinalizedHash, some(payloadAttr)) + return res.isErr + )) + + check produceSingleBlockRes + +proc preTTDFinalizedBlockHash(t: TestEnv, testStatusIMPL: var TestStatus) = + let ok = waitFor t.clMock.waitForTTD() + check ok + if not ok: + return + + # Produce blocks before starting the test + let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks()) + check produce5BlockRes + if not produce5BlockRes: + return + + let + gHash = Web3BlockHash t.gHeader.blockHash.data + forkchoiceState = ForkchoiceStateV1( + headBlockHash: gHash, + safeBlockHash: gHash, + finalizedBlockHash: gHash, + ) + client = t.rpcClient + clMock = t.clMock + + var res = client.forkchoiceUpdatedV1(forkchoiceState) + # TBD: Behavior on this edge-case is undecided, as behavior of the Execution client + # if not defined on re-orgs to a point before the latest finalized block. + + res = client.forkchoiceUpdatedV1(clMock.latestForkchoice) + check res.isOk + if res.isErr: + return + + let s = res.get() + check s.payloadStatus.status == PayloadExecutionStatus.valid + +proc badHashOnExecPayload(t: TestEnv, testStatusIMPL: var TestStatus) = + let ok = waitFor t.clMock.waitForTTD() + check ok + if not ok: + return + + # Produce blocks before starting the test + let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks()) + check produce5BlockRes + if not produce5BlockRes: + return + + type + Shadow = ref object + hash: Hash256 + + let clMock = t.clMock + let client = t.rpcClient + let shadow = Shadow() + + 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 + )) + check produceSingleBlockRes + if not produceSingleBlockRes: + return + + # 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 + )) + check produceSingleBlockRes + +proc parentHashOnExecPayload(t: TestEnv, testStatusIMPL: var TestStatus) = + # Wait until TTD is reached by this client + let ok = waitFor t.clMock.waitForTTD() + check ok + if not ok: + return + + # Produce blocks before starting the test + let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks()) + check produce5BlockRes + if not produce5BlockRes: + return + + let clMock = t.clMock + let client = t.rpcClient + 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 + alteredPayload.blockHash = alteredPayload.parentHash + let res = client.newPayloadV1(alteredPayload) + if res.isErr: + return false + # Execution specification:: + # - {status: INVALID_BLOCK_HASH, latestValidHash: null, validationError: null} if the blockHash validation has failed + let s = res.get() + s.status == PayloadExecutionStatus.invalid_block_hash + )) + check produceSingleBlockRes + +proc invalidPayloadTestCaseGen(payloadField: string): proc (t: TestEnv, testStatusIMPL: var TestStatus) = + return proc (t: TestEnv, testStatusIMPL: var TestStatus) = + discard + +# Test to verify Block information available at the Eth RPC after NewPayload +proc blockStatusExecPayload(t: TestEnv, testStatusIMPL: var TestStatus) = + # Wait until TTD is reached by this client + let ok = waitFor t.clMock.waitForTTD() + check ok + if not ok: + return + + # Produce blocks before starting the test + let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks()) + check produce5BlockRes + if not produce5BlockRes: + return + + let clMock = t.clMock + let client = t.rpcClient + var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks( + onNewPayloadBroadcast: proc(): bool = + # TODO: Ideally, we would need to check 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 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 + + let nRes = client.blockNumber() + if nRes.isErr: + error "Unable to get latest block number", msg=nRes.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 + + return true + )) + check produceSingleBlockRes + +proc blockStatusHeadBlock(t: TestEnv, testStatusIMPL: var TestStatus) = + # Wait until TTD is reached by this client + let ok = waitFor t.clMock.waitForTTD() + check ok + if not ok: + return + + # Produce blocks before starting the test + let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks()) + check produce5BlockRes + if not produce5BlockRes: + return + + 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 + + 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 + )) + check produceSingleBlockRes + +proc blockStatusSafeBlock(t: TestEnv, testStatusIMPL: var TestStatus) = + # Wait until TTD is reached by this client + let ok = waitFor t.clMock.waitForTTD() + check ok + if not ok: + return + + # Produce blocks before starting the test + let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks()) + check produce5BlockRes + if not produce5BlockRes: + return + + 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 + + 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 + )) + check produceSingleBlockRes + +proc blockStatusFinalizedBlock(t: TestEnv, testStatusIMPL: var TestStatus) = + # Wait until TTD is reached by this client + let ok = waitFor t.clMock.waitForTTD() + check ok + if not ok: + return + + # Produce blocks before starting the test + let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks()) + check produce5BlockRes + if not produce5BlockRes: + return + + 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 + + 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 + )) + check produceSingleBlockRes + +proc blockStatusReorg(t: TestEnv, testStatusIMPL: var TestStatus) = + # Wait until TTD is reached by this client + let ok = waitFor t.clMock.waitForTTD() + check ok + if not ok: + return + + # Produce blocks before starting the test + let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks()) + check produce5BlockRes + if not produce5BlockRes: + return + + 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 = + # Verify the client is serving the latest HeadBlock + var currHeader: EthBlockHeader + var hRes = client.latestHeader(currHeader) + if hRes.isErr: + error "unable to get latest header", msg=hRes.error + return false + + var currHash = BlockHash currHeader.blockHash.data + if currHash != clMock.latestForkchoice.headBlockHash or + currHash == clMock.latestForkchoice.safeBlockHash or + currHash == clMock.latestForkchoice.finalizedBlockHash: + error "latest block header doesn't match HeadBlock hash", hash=currHash.toHex + return false + + # Reorg back to the previous block (FinalizedBlock) + let reorgForkchoice = ForkchoiceStateV1( + headBlockHash: clMock.latestForkchoice.finalizedBlockHash, + safeBlockHash: clMock.latestForkchoice.finalizedBlockHash, + finalizedBlockHash: clMock.latestForkchoice.finalizedBlockHash + ) + + var res = client.forkchoiceUpdatedV1(reorgForkchoice) + if res.isErr: + error "Could not send forkchoiceUpdatedV1", msg=res.error + return false + + var s = res.get() + if s.payloadStatus.status != PayloadExecutionStatus.valid: + error "Incorrect status returned after a HeadBlockHash reorg", status=s.payloadStatus.status + return false + + if s.payloadStatus.latestValidHash.isNone: + error "Cannot get latestValidHash from payloadStatus" + return false + + var latestValidHash = s.payloadStatus.latestValidHash.get + if latestValidHash != reorgForkchoice.headBlockHash: + error "Incorrect latestValidHash returned after a HeadBlockHash reorg", + expected=reorgForkchoice.headBlockHash.toHex, + get=latestValidHash.toHex + return false + + # Check that we reorg to the previous block + hRes = client.latestHeader(currHeader) + if hRes.isErr: + error "unable to get latest header", msg=hRes.error + return false + + currHash = BlockHash currHeader.blockHash.data + if currHash != reorgForkchoice.headBlockHash: + error "`latest` block hash doesn't match reorg hash", + expected=reorgForkchoice.headBlockHash.toHex, + get=currHash.toHex + return false + + # Send the HeadBlock again to leave everything back the way it was + res = client.forkchoiceUpdatedV1(clMock.latestForkchoice) + if res.isErr: + error "Could not send forkchoiceUpdatedV1", msg=res.error + return false + + s = res.get() + if s.payloadStatus.status != PayloadExecutionStatus.valid: + error "Incorrect status returned after a HeadBlockHash reorg", + status=s.payloadStatus.status + return false + + if s.payloadStatus.latestValidHash.isNone: + error "Cannot get latestValidHash from payloadStatus" + return false + + latestValidHash = s.payloadStatus.latestValidHash.get + if latestValidHash != clMock.latestForkchoice.headBlockHash: + error "Incorrect latestValidHash returned after a HeadBlockHash reorg", + expected=clMock.latestForkchoice.headBlockHash.toHex, + get=latestValidHash.toHex + return false + return true + )) + check produceSingleBlockRes + +proc reExecPayloads(t: TestEnv, testStatusIMPL: var TestStatus) = + # Wait until this client catches up with latest PoS + let ok = waitFor t.clMock.waitForTTD() + check ok + if not ok: + return + + # How many Payloads we are going to re-execute + var payloadReExecCount = 10 + + # Create those blocks + let produceBlockRes = t.clMock.produceBlocks(payloadReExecCount, BlockProcessCallbacks()) + check produceBlockRes + if not produceBlockRes: + return + + # Re-execute the payloads + let client = t.rpcClient + var hRes = client.blockNumber() + check hRes.isOk + if hRes.isErr: + error "unable to get blockNumber", msg=hRes.error + return + + let lastBlock = int(hRes.get) + info "Started re-executing payloads at block", number=lastBlock + + let + clMock = t.clMock + start = lastBlock - payloadReExecCount + 1 + + for i in start..lastBlock: + if clMock.executedPayloadHistory.hasKey(uint64 i): + let payload = clMock.executedPayloadHistory[uint64 i] + let res = client.newPayloadV1(payload) + check res.isOk + if res.isErr: + error "FAIL (%s): Unable to re-execute valid payload", msg=res.error + return + + let s = res.get() + check s.status == PayloadExecutionStatus.valid + if s.status != PayloadExecutionStatus.valid: + error "Unexpected status after re-execute valid payload", status=s.status + return + else: + check false + error "(test issue) Payload does not exist", index=i + return + +proc multipleNewCanonicalPayloads(t: TestEnv, testStatusIMPL: var TestStatus) = + # Wait until TTD is reached by this client + let ok = waitFor t.clMock.waitForTTD() + check ok + if not ok: + return + + # Produce blocks before starting the test + let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks()) + check produce5BlockRes + if not produce5BlockRes: + return + + let clMock = t.clMock + let client = t.rpcClient + var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks( + # Run test after a new payload has been obtained + onGetPayload: proc(): bool = + let payloadCount = 80 + let basePayload = toExecutableData(clMock.latestPayloadBuilt) + var newPrevRandao: Hash256 + + # Fabricate and send multiple new payloads by changing the PrevRandao field + for i in 0.. 0 + if blk.txs.len == 0: + error "(Test issue) no transactions went in block" + return + + let storageKey = i.u256 + let rr = client.storageAt(prevRandaoContractAddr, storageKey) + check rr.isOk + if rr.isErr: + error "Unable to get storage", msg=rr.error + return + + let opcodeValueAtBlock = rr.get() + if opcodeValueAtBlock != 2.u256: + error "Incorrect difficulty value in block", + expect=2, + get=opcodeValueAtBlock + return + +proc postMergeSync(t: TestEnv, testStatusIMPL: var TestStatus) = + # TODO: need multiple client + discard + +const engineTestList* = [ + TestSpec( + name: "Invalid Terminal Block in ForkchoiceUpdated", + run: invalidTerminalBlockForkchoiceUpdated, + ttd: 1000000 + ), + TestSpec( + name: "Invalid GetPayload Under PoW", + run: invalidGetPayloadUnderPoW, + ttd: 1000000 + ), + TestSpec( + name: "Invalid Terminal Block in NewPayload", + run: invalidTerminalBlockNewPayload, + ttd: 1000000, + ), + TestSpec( + name: "Unknown HeadBlockHash", + run: unknownHeadBlockHash, + ), + TestSpec( + name: "Unknown SafeBlockHash", + run: unknownSafeBlockHash, + ), + TestSpec( + name: "Unknown FinalizedBlockHash", + run: unknownFinalizedBlockHash, + ), + TestSpec( + name: "Pre-TTD ForkchoiceUpdated After PoS Switch", + run: preTTDFinalizedBlockHash, + ttd: 2, + ), + TestSpec( + name: "Bad Hash on NewPayload", + run: badHashOnExecPayload, + ), + TestSpec( + name: "ParentHash==BlockHash on NewPayload", + run: parentHashOnExecPayload, + ), + #[TestSpec( + name: "Invalid ParentHash NewPayload", + run: invalidPayloadTestCaseGen("ParentHash"), + ), + TestSpec( + name: "Invalid StateRoot NewPayload", + run: invalidPayloadTestCaseGen("StateRoot"), + ), + TestSpec( + name: "Invalid ReceiptsRoot NewPayload", + run: invalidPayloadTestCaseGen("ReceiptsRoot"), + ), + TestSpec( + name: "Invalid Number NewPayload", + run: invalidPayloadTestCaseGen("Number"), + ), + TestSpec( + name: "Invalid GasLimit NewPayload", + run: invalidPayloadTestCaseGen("GasLimit"), + ), + TestSpec( + name: "Invalid GasUsed NewPayload", + run: invalidPayloadTestCaseGen("GasUsed"), + ), + TestSpec( + name: "Invalid Timestamp NewPayload", + run: invalidPayloadTestCaseGen("Timestamp"), + ), + TestSpec( + name: "Invalid PrevRandao NewPayload", + run: invalidPayloadTestCaseGen("PrevRandao"), + ), + TestSpec( + name: "Invalid Incomplete Transactions NewPayload", + run: invalidPayloadTestCaseGen("RemoveTransaction"), + ), + TestSpec( + name: "Invalid Transaction Signature NewPayload", + run: invalidPayloadTestCaseGen("Transaction/Signature"), + ), + TestSpec( + name: "Invalid Transaction Nonce NewPayload", + run: invalidPayloadTestCaseGen("Transaction/Nonce"), + ), + TestSpec( + name: "Invalid Transaction GasPrice NewPayload", + run: invalidPayloadTestCaseGen("Transaction/GasPrice"), + ), + TestSpec( + name: "Invalid Transaction Gas NewPayload", + run: invalidPayloadTestCaseGen("Transaction/Gas"), + ), + TestSpec( + name: "Invalid Transaction Value NewPayload", + run: invalidPayloadTestCaseGen("Transaction/Value"), + ),]# + + # Eth RPC Status on ForkchoiceUpdated Events + TestSpec( + name: "Latest Block after NewPayload", + run: blockStatusExecPayload, + ), + TestSpec( + name: "Latest Block after New HeadBlock", + run: blockStatusHeadBlock, + ), + TestSpec( + name: "Latest Block after New SafeBlock", + run: blockStatusSafeBlock, + ), + TestSpec( + name: "Latest Block after New FinalizedBlock", + run: blockStatusFinalizedBlock, + ), + TestSpec( + name: "Latest Block after Reorg", + run: blockStatusReorg, + ), + + # Payload Tests + TestSpec( + name: "Re-Execute Payload", + run: reExecPayloads, + ), + TestSpec( + name: "Multiple New Payloads Extending Canonical Chain", + run: multipleNewCanonicalPayloads, + ), + TestSpec( + name: "Out of Order Payload Execution", + run: outOfOrderPayloads, + ), + + # Transaction Reorg using Engine API + TestSpec( + name: "Transaction Reorg", + run: transactionReorg, + ), + TestSpec( + name: "Sidechain Reorg", + run: sidechainReorg, + ), + + # Suggested Fee Recipient in Payload creation + TestSpec( + name: "Suggested Fee Recipient Test", + run: suggestedFeeRecipient, + ), + + # TODO: debug and fix + # PrevRandao opcode tests + #TestSpec( + # name: "PrevRandao Opcode Transactions", + # run: prevRandaoOpcodeTx, + # ttd: 10, + #), + + # Multi-Client Sync tests + TestSpec( + name: "Sync Client Post Merge", + run: postMergeSync, + ttd: 10, + ) +] \ No newline at end of file diff --git a/hive_integration/nodocker/engine/genesis.json b/hive_integration/nodocker/engine/genesis.json new file mode 100644 index 000000000..07f82c2ef --- /dev/null +++ b/hive_integration/nodocker/engine/genesis.json @@ -0,0 +1,66 @@ +{ + "config": { + "chainId": 7, + "homesteadBlock": 0, + "eip150Block": 0, + "eip150Hash": "0x5de1ee4135274003348e80b788e5afa4b18b18d320a5622218d5c493fedf5689", + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "muirGlacierBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "clique": { + "epoch": 3000, + "period": 1 + } + }, + "genesis": { + "coinbase": "0x0000000000000000000000000000000000000000", + "difficulty": "0x30000", + "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000658bdf435d810c91414ec09147daa6db624063790000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "gasLimit": "0x2fefd8", + "nonce": "0x0000000000000000", + "timestamp": "0x1234", + "alloc": { + "cf49fda3be353c69b41ed96333cd24302da4556f": { + "balance": "0x123450000000000000000" + }, + "0161e041aad467a890839d5b08b138c1e6373072": { + "balance": "0x123450000000000000000" + }, + "87da6a8c6e9eff15d703fc2773e32f6af8dbe301": { + "balance": "0x123450000000000000000" + }, + "b97de4b8c857e4f6bc354f226dc3249aaee49209": { + "balance": "0x123450000000000000000" + }, + "c5065c9eeebe6df2c2284d046bfc906501846c51": { + "balance": "0x123450000000000000000" + }, + "0000000000000000000000000000000000000314": { + "balance": "0x0", + "code": "0x60606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063a223e05d1461006a578063abd1a0cf1461008d578063abfced1d146100d4578063e05c914a14610110578063e6768b451461014c575b610000565b346100005761007761019d565b6040518082815260200191505060405180910390f35b34610000576100be600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919050506101a3565b6040518082815260200191505060405180910390f35b346100005761010e600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919080359060200190919050506101ed565b005b346100005761014a600480803590602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610236565b005b346100005761017960048080359060200190919080359060200190919080359060200190919050506103c4565b60405180848152602001838152602001828152602001935050505060405180910390f35b60005481565b6000600160008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490505b919050565b80600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055505b5050565b7f6031a8d62d7c95988fa262657cd92107d90ed96e08d8f867d32f26edfe85502260405180905060405180910390a17f47e2689743f14e97f7dcfa5eec10ba1dff02f83b3d1d4b9c07b206cbbda66450826040518082815260200191505060405180910390a1817fa48a6b249a5084126c3da369fbc9b16827ead8cb5cdc094b717d3f1dcd995e2960405180905060405180910390a27f7890603b316f3509577afd111710f9ebeefa15e12f72347d9dffd0d65ae3bade81604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390a18073ffffffffffffffffffffffffffffffffffffffff167f7efef9ea3f60ddc038e50cccec621f86a0195894dc0520482abf8b5c6b659e4160405180905060405180910390a28181604051808381526020018273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019250505060405180910390a05b5050565b6000600060008585859250925092505b935093509390505600a165627a7a72305820aaf842d0d0c35c45622c5263cbb54813d2974d3999c8c38551d7c613ea2bc1170029", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x1234", + "0x6661e9d6d8b923d5bbaab1b96e1dd51ff6ea2a93520fdc9eb75d059238b8c5e9": "0x01" + } + }, + "0000000000000000000000000000000000000315": { + "balance": "0x9999999999999999999999999999999", + "code": "0x60606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063ef2769ca1461003e575b610000565b3461000057610078600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061007a565b005b8173ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051809050600060405180830381858888f1935050505015610106578173ffffffffffffffffffffffffffffffffffffffff167f30a3c50752f2552dcc2b93f5b96866280816a986c0c0408cb6778b9fa198288f826040518082815260200191505060405180910390a25b5b50505600a165627a7a72305820637991fabcc8abad4294bf2bb615db78fbec4edff1635a2647d3894e2daf6a610029" + }, + "0000000000000000000000000000000000000316": { + "balance": "0x0", + "code": "0x444355" + }, + "0000000000000000000000000000000000000317": { + "balance": "0x0", + "code": "0x600160003555" + } + } + } +} diff --git a/hive_integration/nodocker/engine/helper.nim b/hive_integration/nodocker/engine/helper.nim new file mode 100644 index 000000000..d51552daf --- /dev/null +++ b/hive_integration/nodocker/engine/helper.nim @@ -0,0 +1,146 @@ +import + std/typetraits, + test_env, + eth/rlp + +type + ExecutableData* = object + parentHash* : Hash256 + feeRecipient* : EthAddress + stateRoot* : Hash256 + receiptsRoot* : Hash256 + logsBloom* : BloomFilter + prevRandao* : Hash256 + number* : uint64 + gasLimit* : GasInt + gasUsed* : GasInt + timestamp* : EthTime + extraData* : Blob + baseFeePerGas*: UInt256 + blockHash* : Hash256 + transactions* : seq[Transaction] + + CustomPayload* = object + parentHash* : Option[Hash256] + feeRecipient* : Option[EthAddress] + stateRoot* : Option[Hash256] + receiptsRoot* : Option[Hash256] + logsBloom* : Option[BloomFilter] + prevRandao* : Option[Hash256] + number* : Option[uint64] + gasLimit* : Option[GasInt] + gasUsed* : Option[GasInt] + timestamp* : Option[EthTime] + extraData* : Option[Blob] + baseFeePerGas*: Option[UInt256] + blockHash* : Option[Hash256] + transactions* : Option[seq[Transaction]] + +proc customizePayload*(basePayload: ExecutableData, customData: CustomPayload): ExecutionPayloadV1 = + let txs = if customData.transactions.isSome: + customData.transactions.get + else: + basePayload.transactions + + let txRoot = calcTxRoot(txs) + + var customHeader = EthBlockHeader( + parentHash: basePayload.parentHash, + ommersHash: EMPTY_UNCLE_HASH, + coinbase: basePayload.feeRecipient, + stateRoot: basePayload.stateRoot, + txRoot: txRoot, + receiptRoot: basePayload.receiptsRoot, + bloom: basePayload.logsBloom, + difficulty: 0.u256, + blockNumber: basePayload.number.toBlockNumber, + gasLimit: basePayload.gasLimit, + gasUsed: basePayload.gasUsed, + timestamp: basePayload.timestamp, + extraData: basePayload.extraData, + mixDigest: basePayload.prevRandao, + nonce: default(BlockNonce), + fee: some(basePayload.baseFeePerGas) + ) + + # Overwrite custom information + if customData.parentHash.isSome: + customHeader.parentHash = customData.parentHash.get + + if customData.feeRecipient.isSome: + customHeader.coinbase = customData.feeRecipient.get + + if customData.stateRoot.isSome: + customHeader.stateRoot = customData.stateRoot.get + + if customData.receiptsRoot.isSome: + customHeader.receiptRoot = customData.receiptsRoot.get + + if customData.logsBloom.isSome: + customHeader.bloom = customData.logsBloom.get + + if customData.prevRandao.isSome: + customHeader.mixDigest = customData.prevRandao.get + + if customData.number.isSome: + customHeader.blockNumber = toBlockNumber(customData.number.get) + + if customData.gasLimit.isSome: + customHeader.gasLimit = customData.gasLimit.get + + if customData.gasUsed.isSome: + customHeader.gasUsed = customData.gasUsed.get + + if customData.timestamp.isSome: + customHeader.timestamp = customData.timestamp.get + + if customData.extraData.isSome: + customHeader.extraData = customData.extraData.get + + if customData.baseFeePerGas.isSome: + customHeader.baseFee = customData.baseFeePerGas.get + + # Return the new payload + result = ExecutionPayloadV1( + parentHash: Web3BlockHash customHeader.parentHash.data, + feeRecipient: Web3Address customHeader.coinbase, + stateRoot: Web3BlockHash customHeader.stateRoot.data, + receiptsRoot: Web3BlockHash customHeader.receiptRoot.data, + logsBloom: Web3Bloom customHeader.bloom, + prevRandao: Web3PrevRandao customHeader.mixDigest.data, + blockNumber: Web3Quantity customHeader.blockNumber.truncate(uint64), + gasLimit: Web3Quantity customHeader.gasLimit, + gasUsed: Web3Quantity customHeader.gasUsed, + timestamp: Web3Quantity toUnix(customHeader.timestamp), + extraData: Web3ExtraData customHeader.extraData, + baseFeePerGas: customHeader.baseFee, + blockHash: Web3BlockHash customHeader.blockHash.data + ) + + for tx in txs: + let txData = rlp.encode(tx) + result.transactions.add TypedTransaction(txData) + +proc hash256*(h: Web3BlockHash): Hash256 = + Hash256(data: distinctBase h) + +proc toExecutableData*(payload: ExecutionPayloadV1): ExecutableData = + result = ExecutableData( + parentHash : hash256(payload.parentHash), + feeRecipient : distinctBase payload.feeRecipient, + stateRoot : hash256(payload.stateRoot), + receiptsRoot : hash256(payload.receiptsRoot), + logsBloom : distinctBase payload.logsBloom, + prevRandao : hash256(payload.prevRandao), + number : uint64 payload.blockNumber, + gasLimit : GasInt payload.gasLimit, + gasUsed : GasInt payload.gasUsed, + timestamp : fromUnix(int64 payload.timestamp), + extraData : distinctBase payload.extraData, + baseFeePerGas : payload.baseFeePerGas, + blockHash : hash256(payload.blockHash) + ) + + for data in payload.transactions: + let tx = rlp.decode(distinctBase data, Transaction) + result.transactions.add tx diff --git a/hive_integration/nodocker/engine/sealer.key b/hive_integration/nodocker/engine/sealer.key new file mode 100644 index 000000000..7f43fa8aa --- /dev/null +++ b/hive_integration/nodocker/engine/sealer.key @@ -0,0 +1 @@ +9c647b8b7c4e7c3490668fb6c11473619db80c93704c70893d3813af4090c39c \ No newline at end of file diff --git a/hive_integration/nodocker/engine/test_env.nim b/hive_integration/nodocker/engine/test_env.nim new file mode 100644 index 000000000..c7018bfb4 --- /dev/null +++ b/hive_integration/nodocker/engine/test_env.nim @@ -0,0 +1,151 @@ +import + std/[os, options, json, times, math], + eth/[common, keys], + eth/trie/db, + eth/p2p as eth_p2p, + stew/[results, byteutils], + stint, + json_rpc/[rpcserver, rpcclient], + ../../../nimbus/[ + config, + genesis, + context, + constants, + transaction, + utils, + sealer, + p2p/chain, + db/db_chain, + rpc/p2p, + rpc/engine_api, + sync/protocol_ethxx, + utils/tx_pool + ], + ../../../tests/test_helpers, + "."/[clmock, engine_client] + +import web3/engine_api_types +from web3/ethtypes as web3types import nil + +export + common, engine_api_types, times, + options, results, constants, utils, + TypedTransaction, clmock, engine_client + +type + EthBlockHeader* = common.BlockHeader + + TestEnv* = ref object + conf: NimbusConf + ctx: EthContext + ethNode: EthereumNode + chainDB: BaseChainDB + chainRef: Chain + rpcServer: RpcSocketServer + sealingEngine: SealingEngineRef + rpcClient*: RpcSocketClient + gHeader*: EthBlockHeader + ttd*: DifficultyInt + clMock*: CLMocker + nonce: uint64 + vaultKey: PrivateKey + + Web3BlockHash* = web3types.BlockHash + Web3Address* = web3types.Address + Web3Bloom* = web3types.FixedBytes[256] + Web3Quantity* = web3types.Quantity + Web3PrevRandao* = web3types.FixedBytes[32] + Web3ExtraData* = web3types.DynamicBytes[0, 32] + +const + baseFolder = "hive_integration" / "nodocker" / "engine" + genesisFile = baseFolder / "genesis.json" + sealerKey = baseFolder / "sealer.key" + + # This is the account that sends vault funding transactions. + vaultAccountAddr* = hexToByteArray[20]("0xcf49fda3be353c69b41ed96333cd24302da4556f") + vaultKeyHex = "63b508a03c3b5937ceb903af8b1b0c191012ef6eb7e9c3fb7afa94e5d214d376" + +proc setupELClient*(t: TestEnv) = + t.ctx = newEthContext() + let res = t.ctx.am.importPrivateKey(sealerKey) + if res.isErr: + echo res.error() + quit(QuitFailure) + + t.ethNode = setupEthNode(t.conf, t.ctx, eth) + t.chainDB = newBaseChainDB( + newMemoryDb(), + t.conf.pruneMode == PruneMode.Full, + t.conf.networkId, + t.conf.networkParams + ) + t.chainRef = newChain(t.chainDB) + + initializeEmptyDb(t.chainDB) + let txPool = TxPoolRef.new(t.chainDB, t.conf.engineSigner) + + t.rpcServer = newRpcSocketServer(["localhost:" & $t.conf.rpcPort]) + t.sealingEngine = SealingEngineRef.new( + t.chainRef, t.ctx, t.conf.engineSigner, + txPool, EngineStopped + ) + + setupEthRpc(t.ethNode, t.ctx, t.chainDB, txPool, t.rpcServer) + setupEngineAPI(t.sealingEngine, t.rpcServer) + + t.sealingEngine.start() + t.rpcServer.start() + + t.rpcClient = newRpcSocketClient() + waitFor t.rpcClient.connect("localhost", t.conf.rpcPort) + t.gHeader = toGenesisHeader(t.conf.networkParams) + + let kRes = PrivateKey.fromHex(vaultKeyHex) + if kRes.isErr: + echo kRes.error + quit(QuitFailure) + + t.vaultKey = kRes.get + +proc setupELClient*(): TestEnv = + result = TestEnv( + conf: makeConfig(@["--engine-signer:658bdf435d810c91414ec09147daa6db62406379", "--custom-network:" & genesisFile]) + ) + setupELClient(result) + +proc stopELClient*(t: TestEnv) = + waitFor t.rpcClient.close() + waitFor t.sealingEngine.stop() + t.rpcServer.stop() + waitFor t.rpcServer.closeWait() + +# TTD is the value specified in the TestSpec + Genesis.Difficulty +proc setRealTTD*(t: TestEnv, ttdValue: int64) = + let realTTD = t.gHeader.difficulty + ttdValue.u256 + t.chainDB.config.terminalTotalDifficulty = some(realTTD) + t.ttd = realTTD + t.clmock = newCLMocker(t.rpcClient, realTTD) + +func gwei(n: int): GasInt {.compileTime.} = + GasInt(n * (10 ^ 9)) + +proc makeNextTransaction*(t: TestEnv, recipient: EthAddress, amount: UInt256, payload: openArray[byte] = []): Transaction = + const + gasLimit = 75000.GasInt + gasPrice = 30.gwei + + let chainId = t.conf.networkParams.config.chainId + let tx = Transaction( + txType : TxLegacy, + chainId : chainId, + nonce : AccountNonce(t.nonce), + gasPrice: gasPrice, + gasLimit: gasLimit, + to : some(recipient), + value : amount, + payload : @payload + ) + + inc t.nonce + signTransaction(tx, t.vaultKey, chainId, eip155 = true) diff --git a/nimbus/transaction.nim b/nimbus/transaction.nim index add01f335..25031a154 100644 --- a/nimbus/transaction.nim +++ b/nimbus/transaction.nim @@ -165,3 +165,15 @@ func eip1559TxNormalization*(tx: Transaction; result.maxFee = tx.gasPrice if FkLondon <= fork: result.gasPrice = baseFee + min(result.maxPriorityFee, result.maxFee - baseFee) + +func effectiveGasTip*(tx: Transaction; baseFee: Option[UInt256]): GasInt = + var + maxPriorityFee = tx.maxPriorityFee + maxFee = tx.maxFee + baseFee = baseFee.get(0.u256).truncate(GasInt) + + if tx.txType < TxEip1559: + maxPriorityFee = tx.gasPrice + maxFee = tx.gasPrice + + min(maxPriorityFee, maxFee - baseFee) diff --git a/premix/parser.nim b/premix/parser.nim index 4447fb8f1..b12f0c25c 100644 --- a/premix/parser.nim +++ b/premix/parser.nim @@ -136,7 +136,7 @@ proc parseTransaction*(n: JsonNode): Transaction = n.fromJson "r", tx.R n.fromJson "s", tx.S - if n["type"].kind != JNull: + if n.hasKey("type") and n["type"].kind != JNull: n.fromJson "type", tx.txType if tx.txType >= TxEip1559: