diff --git a/execution_chain/rpc/debug.nim b/execution_chain/rpc/debug.nim index 36a6bd48be..d2e5c0a46d 100644 --- a/execution_chain/rpc/debug.nim +++ b/execution_chain/rpc/debug.nim @@ -13,6 +13,7 @@ import std/[json, times], json_rpc/rpcserver, web3/[eth_api_types, conversions], + ./handler_utils, ./rpc_utils, ./rpc_types, #../tracer, @@ -184,25 +185,22 @@ proc setupDebugRpc*(com: CommonRef, txPool: TxPoolRef, server: RpcServer) = badBlocks # https://ethereum.github.io/execution-apis/api/methods/debug_getRawBlock - proc debug_getRawBlock(blockTag: BlockTag): seq[byte] {.raises: [ApplicationError].} = + proc debug_getRawBlock(blockTag: BlockTag): seq[byte] {.raises: [ApplicationError, ValueError].} = ## Returns an RLP-encoded block. - let blockFromTag = chain.blockFromTag(blockTag).valueOr: - raise invalidParams(error) + let blockFromTag = getOrRaise(chain.blockFromTag(blockTag), "Block not found") rlp.encode(blockFromTag) # https://ethereum.github.io/execution-apis/api/methods/debug_getRawHeader - proc debug_getRawHeader(blockTag: BlockTag): seq[byte] {.raises: [ApplicationError, RlpError].} = + proc debug_getRawHeader(blockTag: BlockTag): seq[byte] {.raises: [ApplicationError, ValueError, RlpError].} = ## Returns an RLP-encoded header. - let header = chain.headerFromTag(blockTag).valueOr: - raise invalidParams(error) + let header = getOrRaise(chain.headerFromTag(blockTag), "Header not found") rlp.encode(header) # https://ethereum.github.io/execution-apis/api/methods/debug_getRawReceipts - proc debug_getRawReceipts(blockTag: BlockTag): seq[seq[byte]] {.raises: [ApplicationError, RlpError].} = + proc debug_getRawReceipts(blockTag: BlockTag): seq[seq[byte]] {.raises: [ApplicationError, ValueError, RlpError].} = ## Returns an array of EIP-2718 binary-encoded receipts. - let header = chain.headerFromTag(blockTag).valueOr: - raise invalidParams(error) + let header = getOrRaise(chain.headerFromTag(blockTag), "Header not found") let txFrame = chain.txFrame(header) var res: seq[seq[byte]] for receipt in txFrame.getReceipts(header.receiptsRoot): @@ -218,13 +216,13 @@ proc setupDebugRpc*(com: CommonRef, txPool: TxPoolRef, server: RpcServer) = # TODO: remove manual validation when upstream parsing decoding reports strict # hex input failures . if not txHashHex.startsWith("0x"): - raise invalidParams("invalid argument 0: hex string without 0x prefix") + invalidParams("invalid argument 0: hex string without 0x prefix") - let txHash = - try: - Hash32.fromHex(txHashHex) - except ValueError as exc: - raise invalidParams("invalid argument 0: " & exc.msg) + var txHash: Hash32 + try: + txHash = Hash32.fromHex(txHashHex) + except ValueError as exc: + invalidParams("invalid argument 0: " & exc.msg) let res = txPool.getItem(txHash) if res.isOk: @@ -243,10 +241,9 @@ proc setupDebugRpc*(com: CommonRef, txPool: TxPoolRef, server: RpcServer) = # Execution Witness endpoints - not specified in the Execution API - proc debug_executionWitness(quantityTag: BlockTag): ExecutionWitness {.raises: [ValueError].} = + proc debug_executionWitness(quantityTag: BlockTag): ExecutionWitness {.raises: [ApplicationError, ValueError].} = ## Returns an execution witness for the given block number. - let header = chain.headerFromTag(quantityTag).valueOr: - raise newException(ValueError, "Header not found") + let header = getOrRaise(chain.headerFromTag(quantityTag), "Header not found") chain.getExecutionWitness(header.computeBlockHash()).valueOr: raise newException(ValueError, error) diff --git a/execution_chain/rpc/handler_utils.nim b/execution_chain/rpc/handler_utils.nim new file mode 100644 index 0000000000..e38a829117 --- /dev/null +++ b/execution_chain/rpc/handler_utils.nim @@ -0,0 +1,31 @@ +# Nimbus +# Copyright (c) 2026 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +{.push raises: [].} + +import + json_rpc/rpcserver, + results + +proc invalidParams*(msg: string) {.raises: [ref ApplicationError].} = + raise (ref ApplicationError)(code: -32602, msg: msg) + +template getOrInvalidParam*[T](res: Result[Opt[T], string], onMissing: untyped): T = + let resolved = res + if resolved.isErr: + let errMsg = resolved.error + invalidParams(errMsg) + let found = resolved.get() + if found.isNone: + onMissing + found.get() + +template getOrRaise*[T](res: Result[Opt[T], string], msg: string): T = + getOrInvalidParam(res): + raise newException(ValueError, msg) diff --git a/execution_chain/rpc/oracle.nim b/execution_chain/rpc/oracle.nim index 484b9f019f..fcd57e504d 100644 --- a/execution_chain/rpc/oracle.nim +++ b/execution_chain/rpc/oracle.nim @@ -179,11 +179,14 @@ proc resolveBlockRange(oracle: Oracle, blockId: BlockTag, numBlocks: uint64): Re if head < reqEnd: return err("RequestBeyondHead: requested " & $reqEnd & ", head " & $head) else: - let resolved = oracle.chain.headerFromTag(blockId).valueOr: + let resolvedOpt = oracle.chain.headerFromTag(blockId).valueOr: return err(error) + if resolvedOpt.isNone: + return err("Block not found") + # Absolute number resolved. - reqEnd = resolved.number + reqEnd = resolvedOpt.get().number # If there are no blocks to return, short circuit. if blocks == 0: diff --git a/execution_chain/rpc/rpc_utils.nim b/execution_chain/rpc/rpc_utils.nim index c98e48cc4d..08f945d40a 100644 --- a/execution_chain/rpc/rpc_utils.nim +++ b/execution_chain/rpc/rpc_utils.nim @@ -11,10 +11,9 @@ import std/[sequtils, algorithm, strutils], - json_rpc/errors, ./rpc_types, ./params, - ../db/ledger, + ../db/[ledger, core_db, storage_types], ../constants, stint, ../utils/utils, ../transaction, @@ -44,12 +43,6 @@ func median(prices: var openArray[GasInt]): GasInt = prices[middle] -proc invalidParams*(msg: string): ref ApplicationError = - (ref ApplicationError)( - code: -32602, - msg: msg, - ) - proc calculateMedianGasPrice*(chain: ForkedChainRef): GasInt = const minGasPrice = 30_000_000_000.GasInt var prices = chain.latestBlock.transactions.mapIt(it.gasPrice) @@ -422,47 +415,79 @@ proc getTotalDifficulty*(chain: ForkedChainRef, blockHash: Hash32, header: Heade # Note: It's ok to use baseTxFrame for TD as this is for historical blocks chain.baseTxFrame().getScore(blockHash) -proc headerFromTag*(chain: ForkedChainRef, blockTag: BlockTag): Result[Header, string] = +proc baseHasBlockNumber(chain: ForkedChainRef, number: base.BlockNumber): bool = + chain.baseTxFrame().hasKey(blockNumberToHashKey(number).toOpenArray) + +proc baseHasBlockHash(chain: ForkedChainRef, blockHash: Hash32): bool = + chain.baseTxFrame().hasKey(genericHashKey(blockHash).toOpenArray) + +proc earliestBlockNumber(chain: ForkedChainRef): base.BlockNumber = + chain.baseTxFrame().getChainTail() + +proc headerFromTag*(chain: ForkedChainRef, blockTag: BlockTag): Result[Opt[Header], string] = case blockTag.kind of bidAlias: let tag = blockTag.alias.toLowerAscii case tag of "latest": - ok(chain.latestHeader) + ok(Opt.some(chain.latestHeader)) of "finalized": - ok(chain.finalizedHeader) + ok(Opt.some(chain.finalizedHeader)) of "safe": - ok(chain.safeHeader) + ok(Opt.some(chain.safeHeader)) of "earliest": - chain.headerByNumber(base.BlockNumber(0)) + let header = chain.headerByNumber(chain.earliestBlockNumber()).valueOr: + return err(error) + ok(Opt.some(header)) else: err("Unsupported block tag " & tag) of bidNumber: let blockNum = base.BlockNumber blockTag.number - chain.headerByNumber(blockNum) + if blockNum > chain.latestNumber: + return ok(Opt.none(Header)) + if blockNum < chain.baseNumber and not chain.baseHasBlockNumber(blockNum): + return ok(Opt.none(Header)) + let header = chain.headerByNumber(blockNum).valueOr: + return err(error) + ok(Opt.some(header)) of bidHash: - chain.headerByHash(blockTag.hash) + if not chain.isInMemory(blockTag.hash) and not chain.baseHasBlockHash(blockTag.hash): + return ok(Opt.none(Header)) + let header = chain.headerByHash(blockTag.hash).valueOr: + return err(error) + ok(Opt.some(header)) -proc blockFromTag*(chain: ForkedChainRef, blockTag: BlockTag, noHash: bool = false): Result[Block, string] = +proc blockFromTag*(chain: ForkedChainRef, blockTag: BlockTag, noHash: bool = false): Result[Opt[Block], string] = case blockTag.kind of bidAlias: let tag = blockTag.alias.toLowerAscii case tag of "latest": - ok(chain.latestBlock) + ok(Opt.some(chain.latestBlock)) of "finalized": - ok(chain.finalizedBlock) + ok(Opt.some(chain.finalizedBlock)) of "safe": - ok(chain.safeBlock) - # wait till pruner pr is merged for tail semantics to be available, which is the appropriate way to resolve this tag + ok(Opt.some(chain.safeBlock)) of "earliest": - chain.blockByNumber(base.BlockNumber(0)) + let blk = chain.blockByNumber(chain.earliestBlockNumber()).valueOr: + return err(error) + ok(Opt.some(blk)) else: err("Unsupported block tag " & tag) of bidNumber: let blockNum = base.BlockNumber blockTag.number - chain.blockByNumber(blockNum) + if blockNum > chain.latestNumber: + return ok(Opt.none(Block)) + if blockNum < chain.baseNumber and not chain.baseHasBlockNumber(blockNum): + return ok(Opt.none(Block)) + let blk = chain.blockByNumber(blockNum).valueOr: + return err(error) + ok(Opt.some(blk)) of bidHash: if noHash: return err("query by hash not supported for this function") - chain.blockByHash(blockTag.hash) + if not chain.isInMemory(blockTag.hash) and not chain.baseHasBlockHash(blockTag.hash): + return ok(Opt.none(Block)) + let blk = chain.blockByHash(blockTag.hash).valueOr: + return err(error) + ok(Opt.some(blk)) diff --git a/execution_chain/rpc/server_api.nim b/execution_chain/rpc/server_api.nim index 3131b4b215..7edf48ba2f 100644 --- a/execution_chain/rpc/server_api.nim +++ b/execution_chain/rpc/server_api.nim @@ -30,6 +30,7 @@ import ./oracle, ./rpc_types, ./rpc_utils, + ./handler_utils, ./filters logScope: @@ -98,18 +99,22 @@ proc getProof*( storageProof: storage, ) -proc headerFromTag(api: ServerAPIRef, blockTag: BlockTag): Result[Header, string] = +proc headerFromTag(api: ServerAPIRef, blockTag: BlockTag): Result[Opt[Header], string] = api.chain.headerFromTag(blockTag) -proc headerFromTag(api: ServerAPIRef, blockTag: Opt[BlockTag]): Result[Header, string] = +proc headerFromTag(api: ServerAPIRef, blockTag: Opt[BlockTag]): Result[Opt[Header], string] = let blockId = blockTag.get(defaultTag) api.headerFromTag(blockId) proc frameFromTag(api: ServerAPIRef, blockTag: BlockTag): Result[CoreDbTxRef, string] = # TODO avoid loading full header if hash is given - let - header = ?api.headerFromTag(blockTag) + let headerOpt = ?api.headerFromTag(blockTag) + + if headerOpt.isNone: + return err("Block not found") + + let header = headerOpt.get() if header.number < api.chain.baseNumber: return err("Historical data not available") @@ -117,7 +122,7 @@ proc frameFromTag(api: ServerAPIRef, blockTag: BlockTag): Result[CoreDbTxRef, st # TODO maybe use a new frame derived from txFrame, to protect against abuse? ok api.chain.txFrame(header) -proc blockFromTag(api: ServerAPIRef, blockTag: BlockTag, noHash: bool = false): Result[Block, string] = +proc blockFromTag(api: ServerAPIRef, blockTag: BlockTag, noHash: bool = false): Result[Opt[Block], string] = api.chain.blockFromTag(blockTag, noHash) proc getLogsForBlock*( @@ -258,7 +263,7 @@ proc setupServerAPI*(api: ServerAPIRef, server: RpcServer, am: ref AccountsManag proc eth_getBlockByHash( data: Hash32, fullTransactions: bool - ): BlockObject = + ): BlockObject {.raises: [ApplicationError].} = ## Returns information about a block by hash. ## ## data: Hash of a block. @@ -275,13 +280,13 @@ proc setupServerAPI*(api: ServerAPIRef, server: RpcServer, am: ref AccountsManag proc eth_getBlockByNumber( blockTag: BlockTag, fullTransactions: bool - ): BlockObject = + ): BlockObject {.raises: [ApplicationError].} = ## Returns information about a block by block number. ## ## blockTag: integer of a block number, or the string "earliest", "latest" or "pending", as in the default block parameter. ## fullTransactions: If true it returns the full transaction objects, if false only the hashes of the transactions. ## Returns BlockObject or nil when no block was found. - let blk = api.blockFromTag(blockTag, noHash = true).valueOr: + let blk = getOrInvalidParam(api.blockFromTag(blockTag, noHash = true)): return nil let blockHash = blk.header.computeBlockHash @@ -302,7 +307,7 @@ proc setupServerAPI*(api: ServerAPIRef, server: RpcServer, am: ref AccountsManag ) return SyncingStatus(syncing: true, syncObject: sync) - proc eth_getLogs(filterOptions: FilterOptions): seq[FilterLog] {.raises: [ValueError].} = + proc eth_getLogs(filterOptions: FilterOptions): seq[FilterLog] {.raises: [ApplicationError, ValueError].} = ## filterOptions: settings for this filter. ## Returns a list of all logs matching a given filter object. ## TODO: Current implementation is pretty naive and not efficient @@ -326,10 +331,8 @@ proc setupServerAPI*(api: ServerAPIRef, server: RpcServer, am: ref AccountsManag # would operate on this enum instead of raw strings. This change would need # to be done on every endpoint to be consistent. let - blockFrom = api.headerFromTag(filterOptions.fromBlock).valueOr: - raise newException(ValueError, "Block not found") - blockTo = api.headerFromTag(filterOptions.toBlock).valueOr: - raise newException(ValueError, "Block not found") + blockFrom = getOrRaise(api.headerFromTag(filterOptions.fromBlock), "Block not found") + blockTo = getOrRaise(api.headerFromTag(filterOptions.toBlock), "Block not found") # Note: if fromHeader.number > toHeader.number, no logs will be # returned. This is consistent with, what other ethereum clients return @@ -350,15 +353,14 @@ proc setupServerAPI*(api: ServerAPIRef, server: RpcServer, am: ref AccountsManag txHash - proc eth_call(args: TransactionArgs, blockTag: BlockTag): seq[byte] {.raises: [ValueError].} = + proc eth_call(args: TransactionArgs, blockTag: BlockTag): seq[byte] {.raises: [ApplicationError, ValueError].} = ## Executes a new message call immediately without creating a transaction on the block chain. ## ## call: the transaction call object. ## quantityTag: integer block number, or the string "latest", "earliest" or "pending", see the default block parameter. ## Returns the return value of executed contract. let - header = api.headerFromTag(blockTag).valueOr: - raise newException(ValueError, "Block not found") + header = getOrRaise(api.headerFromTag(blockTag), "Block not found") headerHash = header.computeBlockHash txFrame = api.chain.txFrame(headerHash) res = rpcCallEvm(args, header, headerHash, api.com, txFrame).valueOr: @@ -400,8 +402,7 @@ proc setupServerAPI*(api: ServerAPIRef, server: RpcServer, am: ref AccountsManag ## quantityTag: integer block number, or the string "latest", "earliest" or "pending", see the default block parameter. ## Returns the amount of gas used. let - header = api.headerFromTag(blockId("latest")).valueOr: - raise newException(ValueError, "Block not found") + header = getOrRaise(api.headerFromTag(blockId("latest")), "Block not found") headerHash = header.computeBlockHash txFrame = api.chain.txFrame(headerHash) # TODO: change 0 to configureable gas cap @@ -436,13 +437,12 @@ proc setupServerAPI*(api: ServerAPIRef, server: RpcServer, am: ref AccountsManag proc eth_getBlockTransactionCountByNumber( blockTag: BlockTag - ): Quantity {.raises: [ValueError].} = + ): Quantity {.raises: [ApplicationError, ValueError].} = ## Returns the number of transactions in a block from a block matching the given block number. ## ## blockTag: integer of a block number, or the string "latest", "earliest" or "pending", see the default block parameter. ## Returns integer of the number of transactions in this block. - let blk = api.blockFromTag(blockTag, noHash = true).valueOr: - raise newException(ValueError, "Block not found: " & error) + let blk = getOrRaise(api.blockFromTag(blockTag, noHash = true), "Block not found") Quantity(blk.transactions.len) @@ -456,13 +456,12 @@ proc setupServerAPI*(api: ServerAPIRef, server: RpcServer, am: ref AccountsManag Quantity(blk.uncles.len) - proc eth_getUncleCountByBlockNumber(blockTag: BlockTag): Quantity {.raises: [ValueError].} = + proc eth_getUncleCountByBlockNumber(blockTag: BlockTag): Quantity {.raises: [ApplicationError, ValueError].} = ## Returns the number of uncles in a block from a block matching the given block number. ## ## blockTag: integer of a block number, or the string "latest", see the default block parameter. ## Returns integer of the number of uncles in this block. - let blk = api.blockFromTag(blockTag, noHash = true).valueOr: - raise newException(ValueError, "Block not found: " & error) + let blk = getOrRaise(api.blockFromTag(blockTag, noHash = true), "Block not found") Quantity(blk.uncles.len) @@ -581,7 +580,7 @@ proc setupServerAPI*(api: ServerAPIRef, server: RpcServer, am: ref AccountsManag proc eth_getTransactionByBlockHashAndIndex( data: Hash32, quantity: Quantity - ): TransactionObject = + ): TransactionObject {.raises: [ApplicationError].} = ## Returns information about a transaction by block hash and transaction index position. ## ## data: hash of a block. @@ -604,14 +603,14 @@ proc setupServerAPI*(api: ServerAPIRef, server: RpcServer, am: ref AccountsManag proc eth_getTransactionByBlockNumberAndIndex( quantityTag: BlockTag, quantity: Quantity - ): TransactionObject = + ): TransactionObject {.raises: [ApplicationError].} = ## Returns information about a transaction by block number and transaction index position. ## ## quantityTag: a block number, or the string "earliest", "latest" or "pending", as in the default block parameter. ## quantity: the transaction index position. ## NOTE : "pending" blockTag is not supported. let index = uint64(quantity) - let blk = api.blockFromTag(quantityTag, noHash = true).valueOr: + let blk = getOrInvalidParam(api.blockFromTag(quantityTag, noHash = true)): return nil if index >= uint64(blk.transactions.len): @@ -644,14 +643,14 @@ proc setupServerAPI*(api: ServerAPIRef, server: RpcServer, am: ref AccountsManag proc eth_getBlockReceipts( quantityTag: BlockTag - ): Opt[seq[ReceiptObject]] {.raises: [ValueError].} = + ): Opt[seq[ReceiptObject]] {.raises: [ApplicationError, ValueError].} = ## Returns the receipts of a block. if quantityTag.kind == bidHash and quantityTag.requireCanonical: raise newException(ValueError, "requireCanonical is a pre-merge concept and is not supported") let - blk = api.blockFromTag(quantityTag).valueOr: + blk = getOrInvalidParam(api.blockFromTag(quantityTag)): return Opt.none(seq[ReceiptObject]) blkHash = blk.header.computeBlockHash receipts = api.chain.receiptsByBlockHash(blkHash).valueOr: @@ -677,19 +676,17 @@ proc setupServerAPI*(api: ServerAPIRef, server: RpcServer, am: ref AccountsManag proc eth_createAccessList( args: TransactionArgs, quantityTag: BlockTag - ): AccessListResult {.raises: [ValueError].} = + ): AccessListResult {.raises: [ApplicationError, ValueError].} = ## Generates an access list for a transaction. try: - let header = api.headerFromTag(quantityTag).valueOr: - raise newException(ValueError, "Block not found") + let header = getOrRaise(api.headerFromTag(quantityTag), "Block not found") return createAccessList(header, api.com, api.chain, args) except CatchableError as exc: return AccessListResult(error: Opt.some("createAccessList error: " & exc.msg)) - proc eth_blobBaseFee(): Quantity {.raises: [ValueError].} = + proc eth_blobBaseFee(): Quantity {.raises: [ApplicationError, ValueError].} = ## Returns the base fee per blob gas in wei. - let header = api.headerFromTag(blockId("latest")).valueOr: - raise newException(ValueError, "Block not found") + let header = getOrRaise(api.headerFromTag(blockId("latest")), "Block not found") if header.excessBlobGas.isNone: raise newException(ValueError, "excessBlobGas missing from latest header") let blobBaseFee = @@ -724,14 +721,14 @@ proc setupServerAPI*(api: ServerAPIRef, server: RpcServer, am: ref AccountsManag proc eth_getUncleByBlockNumberAndIndex( quantityTag: BlockTag, quantity: Quantity - ): BlockObject = + ): BlockObject {.raises: [ApplicationError].} = # Returns information about a uncle of a block by number and uncle index position. ## ## quantityTag: a block number, or the string "earliest", "latest" or "pending", as in the default block parameter. ## quantity: the uncle's index position. ## Returns BlockObject or nil when no block was found. let index = uint64(quantity) - let blk = api.blockFromTag(quantityTag).valueOr: + let blk = getOrInvalidParam(api.blockFromTag(quantityTag)): return nil if index < 0 or index >= blk.uncles.len.uint64: @@ -761,12 +758,12 @@ proc setupServerAPI*(api: ServerAPIRef, server: RpcServer, am: ref AccountsManag return api.com.getEthConfigObject(api.chain, currentFork, nextFork, lastFork) - proc eth_getBlockAccessList(quantityTag: BlockTag): Opt[BlockAccessList] {.raises: [ValueError].} = + proc eth_getBlockAccessList(quantityTag: BlockTag): Opt[BlockAccessList] {.raises: [ApplicationError, ValueError].} = ## Returns the block access list by block number, tag or block hash. ## - let header = api.chain.headerFromTag(quantityTag).valueOr: - raise newException(ValueError, error) + let header = getOrInvalidParam(api.headerFromTag(quantityTag)): + return Opt.none(BlockAccessList) if not api.com.isAmsterdamOrLater(header.timestamp): raise newException(ValueError, "Block access list not available for pre-Amsterdam blocks")