From 7ea6d719d98e6e87ba36b966e07692f6a46fb715 Mon Sep 17 00:00:00 2001 From: andri lim Date: Thu, 21 Mar 2024 18:24:32 +0700 Subject: [PATCH] Implement RPC method eth_getAccessList (#2091) * Implement RPC method eth_getAccessList * Fix comment --- .../nodocker/engine/engine_client.nim | 20 +--- nimbus/beacon/web3_eth_conv.nim | 31 +++++ nimbus/db/access_list.nim | 17 +++ nimbus/evm/precompiles.nim | 6 +- nimbus/evm/stack.nim | 6 +- nimbus/evm/tracer/access_list_tracer.nim | 77 +++++++++++++ nimbus/rpc/p2p.nim | 13 +++ nimbus/rpc/rpc_utils.nim | 106 ++++++++++++------ nimbus/transaction/call_evm.nim | 13 +++ 9 files changed, 236 insertions(+), 53 deletions(-) create mode 100644 nimbus/evm/tracer/access_list_tracer.nim diff --git a/hive_integration/nodocker/engine/engine_client.nim b/hive_integration/nodocker/engine/engine_client.nim index 45df5d185..479926af4 100644 --- a/hive_integration/nodocker/engine/engine_client.nim +++ b/hive_integration/nodocker/engine/engine_client.nim @@ -208,9 +208,6 @@ proc exchangeCapabilities*(client: RpcClient, wrapTrySimpleRes: client.engine_exchangeCapabilities(methods) -proc toBlockNumber(n: Quantity): common.BlockNumber = - n.uint64.toBlockNumber - proc toBlockNonce(n: Option[FixedBytes[8]]): common.BlockNonce = if n.isNone: return default(BlockNonce) @@ -265,21 +262,6 @@ proc toBlockHeader*(bc: BlockObject): common.BlockHeader = parentBeaconBlockRoot: ethHash bc.parentBeaconBlockRoot, ) -func storageKeys(list: seq[FixedBytes[32]]): seq[StorageKey] = - for x in list: - result.add StorageKey(x) - -func accessList(list: openArray[AccessTuple]): AccessList = - for x in list: - result.add AccessPair( - address : ethAddr x.address, - storageKeys: storageKeys x.storageKeys, - ) - -func accessList(x: Option[seq[AccessTuple]]): AccessList = - if x.isNone: return - else: accessList(x.get) - func vHashes(x: Option[seq[Web3Hash]]): seq[common.Hash256] = if x.isNone: return else: ethHashes(x.get) @@ -296,7 +278,7 @@ proc toTransaction(tx: TransactionObject): Transaction = to : ethAddr tx.to, value : tx.value, payload : tx.input, - accessList : accessList(tx.accessList), + accessList : ethAccessList(tx.accessList), maxFeePerBlobGas: tx.maxFeePerBlobGas.get(0.u256), versionedHashes : vHashes(tx.blobVersionedHashes), V : tx.v.int64, diff --git a/nimbus/beacon/web3_eth_conv.nim b/nimbus/beacon/web3_eth_conv.nim index 0c2df2cd0..0d3a00f55 100644 --- a/nimbus/beacon/web3_eth_conv.nim +++ b/nimbus/beacon/web3_eth_conv.nim @@ -161,6 +161,21 @@ func ethTxs*(list: openArray[Web3Tx], removeBlobs = false): for x in list: result.add ethTx(x) +func storageKeys(list: seq[FixedBytes[32]]): seq[StorageKey] = + for x in list: + result.add StorageKey(x) + +func ethAccessList*(list: openArray[AccessTuple]): common.AccessList = + for x in list: + result.add common.AccessPair( + address : ethAddr x.address, + storageKeys: storageKeys x.storageKeys, + ) + +func ethAccessList*(x: Option[seq[AccessTuple]]): common.AccessList = + if x.isSome: + return ethAccessList(x.get) + # ------------------------------------------------------------------------------ # Eth types to Web3 types # ------------------------------------------------------------------------------ @@ -188,6 +203,11 @@ func w3Hash*(x: Option[common.Hash256]): Option[BlockHash] = func w3Hash*(x: common.BlockHeader): BlockHash = BlockHash rlpHash(x).data +func w3Hash*(list: openArray[StorageKey]): seq[Web3Hash] = + result = newSeqOfCap[Web3Hash](list.len) + for x in list: + result.add Web3Hash x + func w3Addr*(x: common.EthAddress): Web3Address = Web3Address x @@ -266,3 +286,14 @@ func w3Txs*(list: openArray[common.Transaction]): seq[Web3Tx] = result = newSeqOfCap[Web3Tx](list.len) for tx in list: result.add w3Tx(tx) + +proc w3AccessTuple*(ac: AccessPair): AccessTuple = + AccessTuple( + address: w3Addr ac.address, + storageKeys: w3Hash(ac.storageKeys) + ) + +proc w3AccessList*(list: openArray[AccessPair]): seq[AccessTuple] = + result = newSeqOfCap[AccessTuple](list.len) + for x in list: + result.add w3AccessTuple(x) diff --git a/nimbus/db/access_list.nim b/nimbus/db/access_list.nim index 7cb5cf30f..12b35a106 100644 --- a/nimbus/db/access_list.nim +++ b/nimbus/db/access_list.nim @@ -75,3 +75,20 @@ func getAccessList*(ac: AccessList): common.AccessList = address : address, storageKeys: slots.toStorageKeys, ) + +func equal*(ac: AccessList, other: var AccessList): bool = + if ac.slots.len != other.slots.len: + return false + + for address, slots in ac.slots: + other.slots.withValue(address, otherSlots): + if slots.len != otherSlots[].len: + return false + + for slot in slots: + if slot notin otherSlots[]: + return false + do: + return false + + true diff --git a/nimbus/evm/precompiles.nim b/nimbus/evm/precompiles.nim index bdc662980..aaa7a0ffa 100644 --- a/nimbus/evm/precompiles.nim +++ b/nimbus/evm/precompiles.nim @@ -1,5 +1,5 @@ # Nimbus -# Copyright (c) 2018 Status Research & Development GmbH +# Copyright (c) 2018-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) @@ -75,6 +75,10 @@ iterator activePrecompiles*(fork: EVMFork): EthAddress = res[^1] = c.byte yield res +func activePrecompilesList*(fork: EVMFork): seq[EthAddress] = + for address in activePrecompiles(fork): + result.add address + proc getSignature(c: Computation): (array[32, byte], Signature) = # input is Hash, V, R, S template data: untyped = c.msg.data diff --git a/nimbus/evm/stack.nim b/nimbus/evm/stack.nim index ff99d3f4d..b8004faab 100644 --- a/nimbus/evm/stack.nim +++ b/nimbus/evm/stack.nim @@ -1,5 +1,5 @@ # Nimbus -# Copyright (c) 2018 Status Research & Development GmbH +# Copyright (c) 2018-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) @@ -132,5 +132,9 @@ proc peekInt*(stack: Stack): UInt256 = ensurePop(stack, 1) fromStackElement(stack.values[^1], result) +proc peekAddress*(stack: Stack): EthAddress = + ensurePop(stack, 1) + fromStackElement(stack.values[^1], result) + proc top*(stack: Stack, value: uint | int | GasInt | UInt256 | EthAddress | Hash256) {.inline.} = toStackElement(value, stack.values[^1]) diff --git a/nimbus/evm/tracer/access_list_tracer.nim b/nimbus/evm/tracer/access_list_tracer.nim new file mode 100644 index 000000000..b515300bc --- /dev/null +++ b/nimbus/evm/tracer/access_list_tracer.nim @@ -0,0 +1,77 @@ +# Nimbus +# Copyright (c) 2023-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. + +import + std/[sets], + eth/common/eth_types as common, + ".."/[types, stack], + ../interpreter/op_codes, + ../../db/access_list, + ../../errors + +type + AccessListTracer* = ref object of TracerRef + list: access_list.AccessList + excl: HashSet[EthAddress] + +proc new*(T: type AccessListTracer, + acl: common.AccessList, + sender: EthAddress, + to: EthAddress, + precompiles: openArray[EthAddress]): T = + let act = T() + act.excl.incl sender + act.excl.incl to + + for address in precompiles: + act.excl.incl address + + for acp in acl: + if acp.address notin act.excl: + act.list.add acp.address + for slot in acp.storageKeys: + act.list.add(acp.address, UInt256.fromBytesBE(slot)) + + act + +# Opcode level +method captureOpStart*(act: AccessListTracer, c: Computation, + fixed: bool, pc: int, op: Op, gas: GasInt, + depth: int): int {.gcsafe.} = + let stackLen = c.stack.len + try: + if (op in [Sload, Sstore]) and (stackLen >= 1): + let slot = c.stack.peekInt() + act.list.add(c.msg.contractAddress, slot) + + if (op in [ExtCodeCopy, ExtCodeHash, ExtCodeSize, Balance, SelfDestruct]) and (stackLen >= 1): + let address = c.stack.peekAddress() + if address notin act.excl: + act.list.add address + + if (op in [DelegateCall, Call, StaticCall, CallCode]) and (stackLen >= 5): + let address = c.stack[^2, EthAddress] + if address notin act.excl: + act.list.add address + except InsufficientStack as exc: + # should not raise, because we already check the stack len + # this try..except block is to prevent unlisted exception error + discard exc + except ValueError as exc: + discard exc + + # AccessListTracer is not using captureOpEnd + # no need to return op index + +func equal*(ac: AccessListTracer, other: AccessListTracer): bool = + ac.list.equal(other.list) + +func accessList*(ac: AccessListTracer): common.AccessList = + ac.list.getAccessList() diff --git a/nimbus/rpc/p2p.nim b/nimbus/rpc/p2p.nim index 872bb328e..1f82646ae 100644 --- a/nimbus/rpc/p2p.nim +++ b/nimbus/rpc/p2p.nim @@ -575,6 +575,19 @@ proc setupEthRpc*( return Opt.some(recs) except CatchableError: return Opt.none(seq[ReceiptObject]) + + server.rpc("eth_createAccessList") do(args: TransactionArgs, quantityTag: BlockTag) -> AccessListResult: + try: + let + header = chainDB.headerFromTag(quantityTag) + args = callData(args) + + return createAccessList(header, com, args) + except CatchableError as exc: + return AccessListResult( + error: some("createAccessList error: " & exc.msg), + ) + #[ server.rpc("eth_newFilter") do(filterOptions: FilterOptions) -> int: ## Creates a filter object, based on filter options, to notify when the state changes (logs). diff --git a/nimbus/rpc/rpc_utils.nim b/nimbus/rpc/rpc_utils.nim index 9cc9be01b..1d48de715 100644 --- a/nimbus/rpc/rpc_utils.nim +++ b/nimbus/rpc/rpc_utils.nim @@ -12,14 +12,21 @@ import std/[strutils, algorithm, options], ./rpc_types, - eth/[common, keys], + eth/keys, + ../common/common, ../db/core_db, + ../db/ledger, ../constants, stint, ../utils/utils, ../transaction, ../transaction/call_evm, ../core/eip4844, - ../beacon/web3_eth_conv + ../beacon/web3_eth_conv, + ../vm_types, + ../vm_state, + ../evm/precompiles, + ../evm/tracer/access_list_tracer + const defaultTag = blockId("latest") @@ -108,19 +115,18 @@ template optionalU256(src, dst: untyped) = if src.isSome: dst = some(src.get) -template optionalBytes(src, dst: untyped) = - if src.isSome: - dst = src.get - -proc callData*(call: TransactionArgs): RpcCallData {.gcsafe, raises: [].} = - optionalAddress(call.source, result.source) - optionalAddress(call.to, result.to) - optionalGas(call.gas, result.gasLimit) - optionalGas(call.gasPrice, result.gasPrice) - optionalGas(call.maxFeePerGas, result.maxFee) - optionalGas(call.maxPriorityFeePerGas, result.maxPriorityFee) - optionalU256(call.value, result.value) - optionalBytes(call.data, result.data) +proc callData*(args: TransactionArgs): RpcCallData {.gcsafe, raises: [].} = + optionalAddress(args.source, result.source) + optionalAddress(args.to, result.to) + optionalGas(args.gas, result.gasLimit) + optionalGas(args.gasPrice, result.gasPrice) + optionalGas(args.maxFeePerGas, result.maxFee) + optionalGas(args.maxPriorityFeePerGas, result.maxPriorityFee) + optionalU256(args.value, result.value) + result.data = args.payload() + if args.blobVersionedHashes.isSome: + result.versionedHashes = ethHashes args.blobVersionedHashes.get + result.accessList = ethAccessList args.accessList proc toWd(wd: Withdrawal): WithdrawalObject = WithdrawalObject( @@ -135,22 +141,6 @@ proc toWdList(list: openArray[Withdrawal]): seq[WithdrawalObject] = for x in list: result.add toWd(x) -proc toHashList(list: openArray[StorageKey]): seq[Web3Hash] = - result = newSeqOfCap[Web3Hash](list.len) - for x in list: - result.add Web3Hash x - -proc toAccessTuple(ac: AccessPair): AccessTuple = - AccessTuple( - address: w3Addr ac.address, - storageKeys: toHashList(ac.storageKeys) - ) - -proc toAccessTupleList(list: openArray[AccessPair]): seq[AccessTuple] = - result = newSeqOfCap[AccessTuple](list.len) - for x in list: - result.add toAccessTuple(x) - proc populateTransactionObject*(tx: Transaction, optionalHeader: Option[BlockHeader] = none(BlockHeader), txIndex: Option[int] = none(int)): TransactionObject @@ -180,7 +170,7 @@ proc populateTransactionObject*(tx: Transaction, if tx.txType >= TxEip2930: result.chainId = some(Web3Quantity(tx.chainId)) - result.accessList = some(toAccessTupleList(tx.accessList)) + result.accessList = some(w3AccessList(tx.accessList)) if tx.txType >= TxEIP4844: result.maxFeePerBlobGas = some(tx.maxFeePerBlobGas) @@ -301,3 +291,55 @@ proc populateReceipt*(receipt: Receipt, gasUsed: GasInt, tx: Transaction, if tx.txType == TxEip4844: result.blobGasUsed = some(w3Qty(tx.versionedHashes.len.uint64 * GAS_PER_BLOB.uint64)) result.blobGasPrice = some(getBlobBaseFee(header.excessBlobGas.get(0'u64))) + +proc createAccessList*(header: BlockHeader, + com: CommonRef, + args: RpcCallData): AccessListResult {.gcsafe, raises:[CatchableError].} = + var args = args + + # If the gas amount is not set, default to RPC gas cap. + if args.gasLimit.isNone: + args.gasLimit = some(DEFAULT_RPC_GAS_CAP) + + let + vmState = BaseVMState.new(header, com) + fork = com.toEVMFork(forkDeterminationInfo(header.blockNumber, header.timestamp)) + sender = args.source.get(ZERO_ADDRESS) + # TODO: nonce should be retrieved from txPool + nonce = vmState.stateDB.getNonce(sender) + to = if args.to.isSome: args.to.get + else: generateAddress(sender, nonce) + precompiles = activePrecompilesList(fork) + + var prevTracer = AccessListTracer.new( + args.accessList, + sender, + to, + precompiles) + + while true: + # Retrieve the current access list to expand + let accessList = prevTracer.accessList() + + # Set the accesslist to the last accessList + # generated by prevTracer + args.accessList = accessList + + # Apply the transaction with the access list tracer + let + tracer = AccessListTracer.new(accessList, sender, to, precompiles) + vmState = BaseVMState.new(header, com, tracer) + res = rpcCallEvm(args, header, com, vmState) + + if res.isError: + return AccessListResult( + error: some("failed to apply transaction: " & res.error), + ) + + if tracer.equal(prevTracer): + return AccessListResult( + accessList: w3AccessList accessList, + gasUsed: w3Qty res.gasUsed, + ) + + prevTracer = tracer diff --git a/nimbus/transaction/call_evm.nim b/nimbus/transaction/call_evm.nim index f94a80915..b6b6b945f 100644 --- a/nimbus/transaction/call_evm.nim +++ b/nimbus/transaction/call_evm.nim @@ -97,6 +97,19 @@ proc rpcCallEvm*(call: RpcCallData, header: BlockHeader, com: CommonRef): CallRe runComputation(params) +proc rpcCallEvm*(call: RpcCallData, + header: BlockHeader, + com: CommonRef, + vmState: BaseVMState): CallResult + {.gcsafe, raises: [CatchableError].} = + const globalGasCap = 0 # TODO: globalGasCap should configurable by user + let params = toCallParams(vmState, call, globalGasCap, header.fee) + + var dbTx = com.db.beginTransaction() + defer: dbTx.dispose() # always dispose state changes + + runComputation(params) + proc rpcEstimateGas*(cd: RpcCallData, header: BlockHeader, com: CommonRef, gasCap: GasInt): GasInt {.gcsafe, raises: [CatchableError].} = # Binary search the gas requirement, as it may be higher than the amount used