diff --git a/nimbus/constants.nim b/nimbus/constants.nim index 85aec2a9a..5406191ea 100644 --- a/nimbus/constants.nim +++ b/nimbus/constants.nim @@ -108,4 +108,5 @@ const initAddress(3) HISTORY_STORAGE_ADDRESS* = hexToByteArray[20]("0x0aae40965e6800cd9b1f4b05ff21581047e3f91e") + DEPOSIT_CONTRACT_ADDRESS* = hexToByteArray[20]("0x00000000219ab540356cbb839cbe05303d7705fa") # End diff --git a/nimbus/core/eip6110.nim b/nimbus/core/eip6110.nim new file mode 100644 index 000000000..e855d3bdc --- /dev/null +++ b/nimbus/core/eip6110.nim @@ -0,0 +1,76 @@ +# Nimbus +# Copyright (c) 2024 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or +# http://www.apache.org/licenses/LICENSE-2.0) +# * MIT license ([LICENSE-MIT](LICENSE-MIT) or +# http://opensource.org/licenses/MIT) +# at your option. This file may not be copied, modified, or distributed except +# according to those terms. + +{.push raises: [].} + +import + eth/common, + stew/arrayops, + stew/endians2, + results, + ../constants + +# ----------------------------------------------------------------------------- +# Private helpers +# ----------------------------------------------------------------------------- + +# UnpackIntoDeposit unpacks a serialized DepositEvent. +func unpackIntoDeposit(data: openArray[byte]): Result[Request, string] = + if data.len != 576: + return err("deposit wrong length: want 576, have " & $data.len) + + # The ABI encodes the position of dynamic elements first. Since there + # are 5 elements, skip over the positional data. The first 32 bytes of + # dynamic elements also encode their actual length. Skip over that value too. + const + b = 32*5 + 32 + c = b + 48 + 16 + 32 + d = c + 32 + 32 + e = d + 8 + 24 + 32 + f = e + 96 + 32 + + template copyFrom(T: type, input, a, b): auto = + T.initCopyFrom(input.toOpenArray(a, b)) + + let res = Request( + requestType: DepositRequestType, + deposit: DepositRequest( + # PublicKey is the first element. ABI encoding pads values to 32 bytes, + # so despite BLS public keys being length 48, the value length + # here is 64. Then skip over the next length value. + pubkey: array[48, byte].copyFrom(data, b, b+47), + + # WithdrawalCredentials is 32 bytes. Read that value then skip over next + # length. + withdrawalCredentials: array[32, byte].copyFrom(data, c, c+31), + + # Amount is 8 bytes, but it is padded to 32. Skip over it and the next + # length. + amount: uint64.fromBytesLE(data.toOpenArray(d, d+7)), + + # Signature is 96 bytes. Skip over it and the next length. + signature: array[96, byte].copyFrom(data, e, e+95), + + # Amount is 8 bytes. + index: uint64.fromBytesLE(data.toOpenArray(f, f+7)), + ) + ) + ok(res) + +# ----------------------------------------------------------------------------- +# Public functions +# ----------------------------------------------------------------------------- + +func parseDepositLogs*(logs: openArray[Log]): Result[seq[Request], string] = + var res: seq[Request] + for log in logs: + if log.address == DEPOSIT_CONTRACT_ADDRESS: + res.add ?unpackIntoDeposit(log.data) + ok(res) diff --git a/nimbus/core/executor/process_block.nim b/nimbus/core/executor/process_block.nim index f8c881aaf..1a5b65e20 100644 --- a/nimbus/core/executor/process_block.nim +++ b/nimbus/core/executor/process_block.nim @@ -17,6 +17,7 @@ import ../../evm/state, ../../evm/types, ../dao, + ../eip6110, ./calculate_reward, ./executor_helpers, ./process_transaction, @@ -32,9 +33,11 @@ proc processTransactions*( header: BlockHeader, transactions: seq[Transaction], skipReceipts = false, + collectLogs = false ): Result[void, string] = vmState.receipts.setLen(if skipReceipts: 0 else: transactions.len) vmState.cumulativeGasUsed = 0 + vmState.allLogs = @[] for txIndex, tx in transactions: var sender: EthAddress @@ -46,9 +49,14 @@ proc processTransactions*( if skipReceipts: # TODO don't generate logs at all if we're not going to put them in # receipts - discard vmState.getAndClearLogEntries() + if collectLogs: + vmState.allLogs.add vmState.getAndClearLogEntries() + else: + discard vmState.getAndClearLogEntries() else: vmState.receipts[txIndex] = vmState.makeReceipt(tx.txType) + if collectLogs: + vmState.allLogs.add vmState.receipts[txIndex].logs ok() proc procBlkPreamble( @@ -67,7 +75,13 @@ proc procBlkPreamble( return err("Mismatched txRoot") if com.isPragueOrLater(header.timestamp): + if header.requestsRoot.isNone or blk.requests.isNone: + return err("Post-Prague block header must have requestsRoot/requests") + ?vmState.processParentBlockHash(header.parentHash) + else: + if header.requestsRoot.isSome or blk.requests.isSome: + return err("Pre-Prague block header must not have requestsRoot/requests") if com.isCancunOrLater(header.timestamp): if header.parentBeaconBlockRoot.isNone: @@ -82,7 +96,8 @@ proc procBlkPreamble( if blk.transactions.len == 0: return err("Transactions missing from body") - ?processTransactions(vmState, header, blk.transactions, skipReceipts) + let collectLogs = header.requestsRoot.isSome and not skipValidation + ?processTransactions(vmState, header, blk.transactions, skipReceipts, collectLogs) elif blk.transactions.len > 0: return err("Transactions in block with empty txRoot") @@ -121,8 +136,11 @@ proc procBlkPreamble( ok() proc procBlkEpilogue( - vmState: BaseVMState, header: BlockHeader, skipValidation: bool, skipReceipts: bool + vmState: BaseVMState, blk: EthBlock, skipValidation: bool, skipReceipts: bool ): Result[void, string] = + template header(): BlockHeader = + blk.header + # Reward beneficiary vmState.mutateStateDB: if vmState.collectWitnessData: @@ -160,6 +178,21 @@ proc procBlkEpilogue( expected = header.receiptsRoot return err("receiptRoot mismatch") + if header.requestsRoot.isSome: + let requestsRoot = calcRequestsRoot(blk.requests.get) + if header.requestsRoot.get != requestsRoot: + debug "wrong requestsRoot in block", + blockNumber = header.number, + actual = requestsRoot, + expected = header.requestsRoot.get + return err("requestsRoot mismatch") + let depositReqs = ?parseDepositLogs(vmState.allLogs) + var expectedDeposits: seq[Request] + for req in blk.requests.get: + if req.requestType == DepositRequestType: + expectedDeposits.add req + if depositReqs != expectedDeposits: + return err("EIP-6110 deposit requests mismatch") ok() # ------------------------------------------------------------------------------ @@ -180,7 +213,7 @@ proc processBlock*( if vmState.com.consensus == ConsensusType.POW: vmState.calculateReward(blk.header, blk.uncles) - ?vmState.procBlkEpilogue(blk.header, skipValidation, skipReceipts) + ?vmState.procBlkEpilogue(blk, skipValidation, skipReceipts) ok() diff --git a/nimbus/evm/types.nim b/nimbus/evm/types.nim index e13d277aa..f04d8c9fd 100644 --- a/nimbus/evm/types.nim +++ b/nimbus/evm/types.nim @@ -67,6 +67,7 @@ type cumulativeGasUsed*: GasInt gasCosts* : GasCosts blobGasUsed* : uint64 + allLogs* : seq[Log] # EIP-6110 Computation* = ref object # The execution computation diff --git a/tools/t8n/helpers.nim b/tools/t8n/helpers.nim index da1828862..dd9874a80 100644 --- a/tools/t8n/helpers.nim +++ b/tools/t8n/helpers.nim @@ -346,10 +346,7 @@ proc `@@`*(x: Blob): JsonNode = proc `@@`(x: bool): JsonNode = %(if x: "0x1" else: "0x0") -proc `@@`(x: EthAddress): JsonNode = - %("0x" & x.toHex) - -proc `@@`(x: Topic): JsonNode = +proc `@@`(x: openArray[byte]): JsonNode = %("0x" & x.toHex) proc toJson(x: Table[UInt256, UInt256]): JsonNode = @@ -378,7 +375,7 @@ proc `@@`(x: BloomFilter): JsonNode = %("0x" & toHex[256](x)) proc `@@`(x: Log): JsonNode = - result = %{ + %{ "address": @@(x.address), "topics" : @@(x.topics), "data" : @@(x.data) @@ -401,11 +398,20 @@ proc `@@`(x: TxReceipt): JsonNode = result["type"] = %("0x" & toHex(x.txType.int, 1)) proc `@@`(x: RejectedTx): JsonNode = - result = %{ + %{ "index": %(x.index), "error": %(x.error) } +proc `@@`(x: DepositRequest): JsonNode = + %{ + "pubkey": @@(x.pubkey), + "withdrawalCredentials": @@(x.withdrawalCredentials), + "amount": @@(x.amount), + "signature": @@(x.signature), + "index": @@(x.index), + } + proc `@@`[T](x: seq[T]): JsonNode = result = newJArray() for c in x: @@ -438,3 +444,7 @@ proc `@@`*(x: ExecutionResult): JsonNode = result["currentExcessBlobGas"] = @@(x.currentExcessBlobGas) if x.blobGasUsed.isSome: result["blobGasUsed"] = @@(x.blobGasUsed) + if x.requestsRoot.isSome: + result["requestsRoot"] = @@(x.requestsRoot) + if x.depositRequests.isSome: + result["depositRequests"] = @@(x.depositRequests) diff --git a/tools/t8n/transition.nim b/tools/t8n/transition.nim index ee0dda8dd..d8995492e 100644 --- a/tools/t8n/transition.nim +++ b/tools/t8n/transition.nim @@ -22,6 +22,7 @@ import ../../nimbus/core/dao, ../../nimbus/core/executor/[process_transaction, executor_helpers], ../../nimbus/core/eip4844, + ../../nimbus/core/eip6110, ../../nimbus/evm/tracer/json_tracer const @@ -338,6 +339,20 @@ proc exec(ctx: var TransContext, elif ctx.env.parentExcessBlobGas.isSome and ctx.env.parentBlobGasUsed.isSome: result.result.currentExcessBlobGas = Opt.some calcExcessBlobGas(vmState.parent) + if vmState.com.isPragueOrLater(ctx.env.currentTimestamp): + var allLogs: seq[Log] + for rec in result.result.receipts: + allLogs.add rec.logs + let reqs = parseDepositLogs(allLogs).valueOr: + raise newError(ErrorEVM, error) + result.result.requestsRoot = Opt.some(calcRequestsRoot(reqs)) + var deposits: seq[DepositRequest] + for req in reqs: + # all requests produced by parseDepositLogs + # should be DepositRequest + deposits.add req.deposit + result.result.depositRequests = Opt.some(deposits) + template wrapException(body: untyped) = when wrapExceptionEnabled: try: diff --git a/tools/t8n/types.nim b/tools/t8n/types.nim index 02e41fdac..1b1d43e1b 100644 --- a/tools/t8n/types.nim +++ b/tools/t8n/types.nim @@ -99,6 +99,8 @@ type withdrawalsRoot*: Opt[Hash256] blobGasUsed*: Opt[uint64] currentExcessBlobGas*: Opt[uint64] + requestsRoot*: Opt[Hash256] + depositRequests*: Opt[seq[DepositRequest]] const ErrorEVM* = 2.T8NExitCode