From 112c417270efef3652c091383348318ed5505bcf Mon Sep 17 00:00:00 2001 From: James Date: Thu, 26 Mar 2026 17:40:54 -0400 Subject: [PATCH] feat(rpc): structured JSON-RPC error codes and ajj 0.7.0 (ENG-1899) Replace string-based RPC errors with structured error types implementing ajj's IntoErrorPayload trait. Each namespace (eth, debug, signet) gets a dedicated error enum with spec-compliant error codes. - Add EthError, DebugError, SignetError with thiserror + IntoErrorPayload - Replace response_tri! macro and string errors with typed error propagation - Add resolve_block_id/resolve_header helpers with ResolveError - Update ajj dependency from git branch to published 0.7.0 - Preserve hot storage reader consistency from #118 Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 2 +- Cargo.toml | 2 +- crates/rpc/src/debug/endpoints.rs | 99 +++++---- crates/rpc/src/debug/error.rs | 57 +++-- crates/rpc/src/debug/tracer.rs | 18 +- crates/rpc/src/eth/endpoints.rs | 346 +++++++++++++---------------- crates/rpc/src/eth/error.rs | 166 +++++++++++--- crates/rpc/src/eth/helpers.rs | 68 ++---- crates/rpc/src/eth/mod.rs | 2 +- crates/rpc/src/signet/endpoints.rs | 32 ++- crates/rpc/src/signet/error.rs | 41 +++- 11 files changed, 469 insertions(+), 364 deletions(-) diff --git a/.gitignore b/.gitignore index faa6dc5c..8b08ddc6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ .DS_Store Cargo.lock .idea/ -docs/superpowers/ +docs/ diff --git a/Cargo.toml b/Cargo.toml index a39de44b..7c2d184f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,7 +64,7 @@ signet-cold-mdbx = "0.6.5" signet-storage-types = "0.6.5" # ajj -ajj = { version = "0.6.0" } +ajj = "0.7.0" # trevm trevm = { version = "0.34.0", features = ["full_env_cfg"] } diff --git a/crates/rpc/src/debug/endpoints.rs b/crates/rpc/src/debug/endpoints.rs index 88a1cc6d..b2c6f200 100644 --- a/crates/rpc/src/debug/endpoints.rs +++ b/crates/rpc/src/debug/endpoints.rs @@ -6,16 +6,15 @@ use crate::{ DebugError, types::{TraceBlockParams, TraceTransactionParams}, }, - eth::helpers::{CfgFiller, await_handler, response_tri}, + eth::helpers::{CfgFiller, await_handler}, }; -use ajj::{HandlerCtx, ResponsePayload}; +use ajj::HandlerCtx; use alloy::{ consensus::BlockHeader, eips::BlockId, rpc::types::trace::geth::{GethTrace, TraceResult}, }; use itertools::Itertools; -use signet_evm::EvmErrored; use signet_hot::{HotKv, model::HotKvRead}; use signet_types::MagicSig; use tracing::Instrument; @@ -26,13 +25,13 @@ pub(super) async fn trace_block( hctx: HandlerCtx, TraceBlockParams(id, opts): TraceBlockParams, ctx: StorageRpcCtx, -) -> ResponsePayload, DebugError> +) -> Result, DebugError> where T: Into, H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, { - let opts = response_tri!(opts.ok_or(DebugError::InvalidTracerConfig)); + let opts = opts.ok_or(DebugError::InvalidTracerConfig)?; // Acquire a tracing semaphore permit to limit concurrent debug // requests. The permit is held for the entire handler lifetime and @@ -44,41 +43,37 @@ where let fut = async move { let cold = ctx.cold(); - let block_num = response_tri!(ctx.resolve_block_id(id).map_err(|e| { + let block_num = ctx.resolve_block_id(id).map_err(|e| { tracing::warn!(error = %e, ?id, "block resolution failed"); - DebugError::BlockNotFound(id) - })); + DebugError::Resolve(e) + })?; - let sealed = - response_tri!(ctx.resolve_header(BlockId::Number(block_num.into())).map_err(|e| { - tracing::warn!(error = %e, block_num, "header resolution failed"); - DebugError::BlockNotFound(id) - })); + let sealed = ctx.resolve_header(BlockId::Number(block_num.into())).map_err(|e| { + tracing::warn!(error = %e, block_num, "header resolution failed"); + DebugError::Resolve(e) + })?; let Some(sealed) = sealed else { - return ResponsePayload::internal_error_message( - format!("block not found: {id}").into(), - ); + return Err(DebugError::BlockNotFound(id)); }; let block_hash = sealed.hash(); let header = sealed.into_inner(); - let txs = response_tri!(cold.get_transactions_in_block(block_num).await.map_err(|e| { + let txs = cold.get_transactions_in_block(block_num).await.map_err(|e| { tracing::warn!(error = %e, block_num, "cold storage read failed"); DebugError::from(e) - })); + })?; tracing::debug!(number = header.number, "Loaded block"); let mut frames = Vec::with_capacity(txs.len()); // State BEFORE this block. - let db = - response_tri!(ctx.revm_state_at_height(header.number.saturating_sub(1)).map_err(|e| { - tracing::warn!(error = %e, block_num, "hot storage read failed"); - DebugError::from(e) - })); + let db = ctx.revm_state_at_height(header.number.saturating_sub(1)).map_err(|e| { + tracing::warn!(error = %e, block_num, "hot storage read failed"); + DebugError::from(e) + })?; let spec_id = ctx.spec_id_for_header(&header); let mut evm = signet_evm::signet_evm(db, ctx.constants().clone()); @@ -100,17 +95,20 @@ where let t = trevm.fill_tx(tx); let frame; - (frame, trevm) = response_tri!(crate::debug::tracer::trace(t, &opts, tx_info)); + (frame, trevm) = crate::debug::tracer::trace(t, &opts, tx_info)?; frames.push(TraceResult::Success { result: frame, tx_hash: Some(*tx.tx_hash()) }); tracing::debug!(tx_index = idx, tx_hash = ?tx.tx_hash(), "Traced transaction"); } - ResponsePayload(Ok(frames)) + Ok(frames) } .instrument(span); - await_handler!(@response_option hctx.spawn(fut)) + await_handler!( + hctx.spawn(fut), + DebugError::EvmHalt { reason: "task panicked or cancelled".into() } + ) } /// `debug_traceTransaction` handler. @@ -118,12 +116,12 @@ pub(super) async fn trace_transaction( hctx: HandlerCtx, TraceTransactionParams(tx_hash, opts): TraceTransactionParams, ctx: StorageRpcCtx, -) -> ResponsePayload +) -> Result where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, { - let opts = response_tri!(opts.ok_or(DebugError::InvalidTracerConfig)); + let opts = opts.ok_or(DebugError::InvalidTracerConfig)?; // Held for the handler duration; dropped when the async block completes. let _permit = ctx.acquire_tracing_permit().await; @@ -134,37 +132,36 @@ where let cold = ctx.cold(); // Look up the transaction and its containing block. - let confirmed = response_tri!(cold.get_tx_by_hash(tx_hash).await.map_err(|e| { + let confirmed = cold.get_tx_by_hash(tx_hash).await.map_err(|e| { tracing::warn!(error = %e, %tx_hash, "cold storage read failed"); DebugError::from(e) - })); + })?; - let confirmed = response_tri!(confirmed.ok_or(DebugError::TransactionNotFound)); + let confirmed = confirmed.ok_or(DebugError::TransactionNotFound(tx_hash))?; let (_tx, meta) = confirmed.into_parts(); let block_num = meta.block_number(); let block_hash = meta.block_hash(); let block_id = BlockId::Number(block_num.into()); - let sealed = response_tri!(ctx.resolve_header(block_id).map_err(|e| { + let sealed = ctx.resolve_header(block_id).map_err(|e| { tracing::warn!(error = %e, block_num, "header resolution failed"); DebugError::BlockNotFound(block_id) - })); - let header = response_tri!(sealed.ok_or(DebugError::BlockNotFound(block_id))).into_inner(); + })?; + let header = sealed.ok_or(DebugError::BlockNotFound(block_id))?.into_inner(); - let txs = response_tri!(cold.get_transactions_in_block(block_num).await.map_err(|e| { + let txs = cold.get_transactions_in_block(block_num).await.map_err(|e| { tracing::warn!(error = %e, block_num, "cold storage read failed"); DebugError::from(e) - })); + })?; tracing::debug!(number = block_num, "Loaded containing block"); // State BEFORE this block. - let db = - response_tri!(ctx.revm_state_at_height(block_num.saturating_sub(1)).map_err(|e| { - tracing::warn!(error = %e, block_num, "hot storage read failed"); - DebugError::from(e) - })); + let db = ctx.revm_state_at_height(block_num.saturating_sub(1)).map_err(|e| { + tracing::warn!(error = %e, block_num, "hot storage read failed"); + DebugError::from(e) + })?; let spec_id = ctx.spec_id_for_header(&header); let mut evm = signet_evm::signet_evm(db, ctx.constants().clone()); @@ -175,15 +172,16 @@ where let mut txns = txs.iter().enumerate().peekable(); for (_idx, tx) in txns.by_ref().peeking_take_while(|(_, t)| t.tx_hash() != &tx_hash) { if MagicSig::try_from_signature(tx.signature()).is_some() { - return ResponsePayload::internal_error_message( - DebugError::TransactionNotFound.to_string().into(), - ); + return Err(DebugError::TransactionNotFound(tx_hash)); } - trevm = response_tri!(trevm.run_tx(tx).map_err(EvmErrored::into_error)).accept_state(); + trevm = trevm + .run_tx(tx) + .map_err(|e| DebugError::EvmHalt { reason: e.into_error().to_string() })? + .accept_state(); } - let (index, tx) = response_tri!(txns.next().ok_or(DebugError::TransactionNotFound)); + let (index, tx) = txns.next().ok_or(DebugError::TransactionNotFound(tx_hash))?; let trevm = trevm.fill_tx(tx); @@ -195,11 +193,14 @@ where base_fee: header.base_fee_per_gas(), }; - let res = response_tri!(crate::debug::tracer::trace(trevm, &opts, tx_info)).0; + let res = crate::debug::tracer::trace(trevm, &opts, tx_info)?.0; - ResponsePayload(Ok(res)) + Ok(res) } .instrument(span); - await_handler!(@response_option hctx.spawn(fut)) + await_handler!( + hctx.spawn(fut), + DebugError::EvmHalt { reason: "task panicked or cancelled".into() } + ) } diff --git a/crates/rpc/src/debug/error.rs b/crates/rpc/src/debug/error.rs index 6b8a8cd6..5de761bc 100644 --- a/crates/rpc/src/debug/error.rs +++ b/crates/rpc/src/debug/error.rs @@ -1,13 +1,9 @@ //! Error types for the debug namespace. -use alloy::eips::BlockId; +use alloy::{eips::BlockId, primitives::B256}; +use std::borrow::Cow; /// Errors that can occur in the `debug` namespace. -/// -/// The [`serde::Serialize`] impl emits sanitized messages suitable for -/// API responses — internal storage details are not exposed to callers. -/// Use [`tracing`] to log the full error chain before constructing the -/// variant. #[derive(Debug, thiserror::Error)] pub enum DebugError { /// Cold storage error. @@ -16,28 +12,55 @@ pub enum DebugError { /// Hot storage error. #[error("hot storage error")] Hot(#[from] signet_storage::StorageError), + /// Block resolution error. + #[error("resolve: {0}")] + Resolve(crate::config::resolve::ResolveError), /// Invalid tracer configuration. #[error("invalid tracer config")] InvalidTracerConfig, /// Unsupported tracer type. #[error("unsupported: {0}")] Unsupported(&'static str), - /// EVM execution error. - #[error("evm execution error")] - Evm(String), + /// EVM execution halted. + #[error("execution halted: {reason}")] + EvmHalt { + /// Debug-formatted halt reason. + reason: String, + }, /// Block not found. #[error("block not found: {0}")] BlockNotFound(BlockId), /// Transaction not found. - #[error("transaction not found")] - TransactionNotFound, + #[error("transaction not found: {0}")] + TransactionNotFound(B256), } -impl serde::Serialize for DebugError { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) +impl ajj::IntoErrorPayload for DebugError { + type ErrData = (); + + fn error_code(&self) -> i64 { + match self { + Self::Cold(_) | Self::Hot(_) | Self::EvmHalt { .. } => -32000, + Self::Resolve(r) => crate::eth::error::resolve_error_code(r), + Self::InvalidTracerConfig => -32602, + Self::Unsupported(_) => -32601, + Self::BlockNotFound(_) | Self::TransactionNotFound(_) => -32001, + } + } + + fn error_message(&self) -> Cow<'static, str> { + match self { + Self::Cold(_) | Self::Hot(_) => "server error".into(), + Self::Resolve(r) => crate::eth::error::resolve_error_message(r), + Self::InvalidTracerConfig => "invalid tracer config".into(), + Self::Unsupported(msg) => format!("unsupported: {msg}").into(), + Self::EvmHalt { reason } => format!("execution halted: {reason}").into(), + Self::BlockNotFound(id) => format!("block not found: {id}").into(), + Self::TransactionNotFound(h) => format!("transaction not found: {h}").into(), + } + } + + fn error_data(self) -> Option { + None } } diff --git a/crates/rpc/src/debug/tracer.rs b/crates/rpc/src/debug/tracer.rs index fa240736..4165bdd8 100644 --- a/crates/rpc/src/debug/tracer.rs +++ b/crates/rpc/src/debug/tracer.rs @@ -53,7 +53,7 @@ where NoopFrame::default().into(), trevm .run() - .map_err(|err| DebugError::Evm(err.into_error().to_string()))? + .map_err(|err| DebugError::EvmHalt { reason: err.into_error().to_string() })? .accept_state(), )), GethDebugBuiltInTracerType::MuxTracer => trace_mux(&config.tracer_config, trevm, tx_info), @@ -70,7 +70,7 @@ where let mut four_byte = FourByteInspector::default(); let trevm = trevm .try_with_inspector(&mut four_byte, |trevm| trevm.run()) - .map_err(|e| DebugError::Evm(e.into_error().to_string()))?; + .map_err(|e| DebugError::EvmHalt { reason: e.into_error().to_string() })?; Ok((FourByteFrame::from(four_byte).into(), trevm.accept_state())) } @@ -90,7 +90,7 @@ where let trevm = trevm .try_with_inspector(&mut inspector, |trevm| trevm.run()) - .map_err(|e| DebugError::Evm(e.into_error().to_string()))?; + .map_err(|e| DebugError::EvmHalt { reason: e.into_error().to_string() })?; let frame = inspector .with_transaction_gas_limit(trevm.gas_limit()) @@ -118,7 +118,7 @@ where let trevm = trevm .try_with_inspector(&mut inspector, |trevm| trevm.run()) - .map_err(|e| DebugError::Evm(e.into_error().to_string()))?; + .map_err(|e| DebugError::EvmHalt { reason: e.into_error().to_string() })?; let gas_limit = trevm.gas_limit(); // NB: state must be UNCOMMITTED for prestate diff computation. @@ -128,7 +128,7 @@ where .with_transaction_gas_limit(gas_limit) .into_geth_builder() .geth_prestate_traces(&result, &prestate_config, trevm.inner_mut_unchecked().db_mut()) - .map_err(|err| DebugError::Evm(err.to_string()))?; + .map_err(|err| DebugError::EvmHalt { reason: err.to_string() })?; // Equivalent to `trevm.accept_state()`. trevm.inner_mut_unchecked().db_mut().commit(result.state); @@ -155,7 +155,7 @@ where let trevm = trevm .try_with_inspector(&mut inspector, |trevm| trevm.run()) - .map_err(|e| DebugError::Evm(e.into_error().to_string()))?; + .map_err(|e| DebugError::EvmHalt { reason: e.into_error().to_string() })?; let frame = inspector .with_transaction_gas_limit(trevm.gas_limit()) @@ -178,18 +178,18 @@ where tracer_config.clone().into_mux_config().map_err(|_| DebugError::InvalidTracerConfig)?; let mut inspector = MuxInspector::try_from_config(mux_config) - .map_err(|err| DebugError::Evm(err.to_string()))?; + .map_err(|err| DebugError::EvmHalt { reason: err.to_string() })?; let trevm = trevm .try_with_inspector(&mut inspector, |trevm| trevm.run()) - .map_err(|e| DebugError::Evm(e.into_error().to_string()))?; + .map_err(|e| DebugError::EvmHalt { reason: e.into_error().to_string() })?; // NB: state must be UNCOMMITTED for prestate diff computation. let (result, mut trevm) = trevm.take_result_and_state(); let frame = inspector .try_into_mux_frame(&result, trevm.inner_mut_unchecked().db_mut(), tx_info) - .map_err(|err| DebugError::Evm(err.to_string()))?; + .map_err(|err| DebugError::EvmHalt { reason: err.to_string() })?; // Equivalent to `trevm.accept_state()`. trevm.inner_mut_unchecked().db_mut().commit(result.state); diff --git a/crates/rpc/src/eth/endpoints.rs b/crates/rpc/src/eth/endpoints.rs index 6e818e21..3c9b4406 100644 --- a/crates/rpc/src/eth/endpoints.rs +++ b/crates/rpc/src/eth/endpoints.rs @@ -3,11 +3,11 @@ use crate::{ config::{EvmBlockContext, StorageRpcCtx, gas_oracle}, eth::{ - error::{CallErrorData, EthError}, + error::EthError, helpers::{ AddrWithBlock, BlockParams, CfgFiller, FeeHistoryArgs, StorageAtArgs, SubscribeArgs, TxParams, await_handler, build_receipt, build_rpc_transaction, hot_reader_at_block, - normalize_gas_stateless, response_tri, + normalize_gas_stateless, }, types::{ BlockTransactions, EmptyArray, LazyReceipts, RpcBlock, RpcHeader, RpcReceipt, @@ -16,7 +16,7 @@ use crate::{ }, interest::{FilterOutput, InterestKind}, }; -use ajj::{HandlerCtx, ResponsePayload}; +use ajj::HandlerCtx; use alloy::{ consensus::Transaction, eips::{ @@ -84,7 +84,7 @@ pub(crate) async fn uncle_block() -> Result, ()> { // --------------------------------------------------------------------------- /// `eth_blockNumber` — returns the latest block number from block tags. -pub(crate) async fn block_number(ctx: StorageRpcCtx) -> Result { +pub(crate) async fn block_number(ctx: StorageRpcCtx) -> Result { Ok(U64::from(ctx.tags().latest())) } @@ -98,7 +98,7 @@ pub(crate) async fn chain_id(ctx: StorageRpcCtx) -> Result // --------------------------------------------------------------------------- /// `eth_gasPrice` — suggests gas price based on recent block tips + base fee. -pub(crate) async fn gas_price(hctx: HandlerCtx, ctx: StorageRpcCtx) -> Result +pub(crate) async fn gas_price(hctx: HandlerCtx, ctx: StorageRpcCtx) -> Result where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, @@ -107,40 +107,35 @@ where let latest = ctx.tags().latest(); let cold = ctx.cold(); - let tip = gas_oracle::suggest_tip_cap(&cold, latest, ctx.config(), ctx.gas_cache()) - .await - .map_err(|e| e.to_string())?; + let tip = gas_oracle::suggest_tip_cap(&cold, latest, ctx.config(), ctx.gas_cache()).await?; let base_fee = cold .get_header_by_number(latest) - .await - .map_err(|e| e.to_string())? + .await? .and_then(|h| h.base_fee_per_gas) .unwrap_or_default(); Ok(tip + U256::from(base_fee)) }; - await_handler!(@option hctx.spawn(task)) + await_handler!(hctx.spawn(task), EthError::task_panic()) } /// `eth_maxPriorityFeePerGas` — suggests priority fee from recent block tips. pub(crate) async fn max_priority_fee_per_gas( hctx: HandlerCtx, ctx: StorageRpcCtx, -) -> Result +) -> Result where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, { let task = async move { let latest = ctx.tags().latest(); - gas_oracle::suggest_tip_cap(&ctx.cold(), latest, ctx.config(), ctx.gas_cache()) - .await - .map_err(|e| e.to_string()) + Ok(gas_oracle::suggest_tip_cap(&ctx.cold(), latest, ctx.config(), ctx.gas_cache()).await?) }; - await_handler!(@option hctx.spawn(task)) + await_handler!(hctx.spawn(task), EthError::task_panic()) } /// `eth_feeHistory` — returns base fee and reward percentile data. @@ -148,7 +143,7 @@ pub(crate) async fn fee_history( hctx: HandlerCtx, FeeHistoryArgs(block_count, newest, reward_percentiles): FeeHistoryArgs, ctx: StorageRpcCtx, -) -> Result +) -> Result where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, @@ -184,14 +179,14 @@ where if let Some(percentiles) = &reward_percentiles && percentiles.windows(2).any(|w| w[0] > w[1] || w[0] > 100.) { - return Err("invalid reward percentiles".to_string()); + return Err(EthError::InvalidParams("invalid reward percentiles".into())); } let start_block = end_block_plus - block_count; let cold = ctx.cold(); let specs: Vec<_> = (start_block..=end_block).map(HeaderSpecifier::Number).collect(); - let headers = cold.get_headers(specs).await.map_err(|e| e.to_string())?; + let headers = cold.get_headers(specs).await?; let mut base_fee_per_gas: Vec = Vec::with_capacity(headers.len() + 1); let mut gas_used_ratio: Vec = Vec::with_capacity(headers.len()); @@ -199,7 +194,10 @@ where for (offset, maybe_header) in headers.iter().enumerate() { let Some(header) = maybe_header else { - return Err(format!("missing header at block {}", start_block + offset as u64)); + return Err(EthError::Internal(format!( + "missing header at block {}", + start_block + offset as u64 + ))); }; base_fee_per_gas.push(header.base_fee_per_gas.unwrap_or_default() as u128); @@ -215,8 +213,7 @@ where let (txs, receipts) = tokio::try_join!( cold.get_transactions_in_block(block_num), cold.get_receipts_in_block(block_num), - ) - .map_err(|e| e.to_string())?; + )?; let block_rewards = calculate_reward_percentiles( percentiles, @@ -251,7 +248,7 @@ where }) }; - await_handler!(@option hctx.spawn(task)) + await_handler!(hctx.spawn(task), EthError::task_panic()) } /// Calculate reward percentiles for a single block. @@ -314,7 +311,7 @@ pub(crate) async fn block( hctx: HandlerCtx, BlockParams(t, full): BlockParams, ctx: StorageRpcCtx, -) -> Result, String> +) -> Result, EthError> where T: Into, H: HotKv + Send + Sync + 'static, @@ -325,13 +322,12 @@ where let task = async move { let cold = ctx.cold(); - let block_num = ctx.resolve_block_id(id).map_err(|e| e.to_string())?; + let block_num = ctx.resolve_block_id(id)?; let (header, txs) = tokio::try_join!( cold.get_header_by_number(block_num), cold.get_transactions_in_block(block_num), - ) - .map_err(|e| e.to_string())?; + )?; let Some(header) = header else { return Ok(None); @@ -358,7 +354,7 @@ where })) }; - await_handler!(@option hctx.spawn(task)) + await_handler!(hctx.spawn(task), EthError::task_panic()) } /// `eth_getBlockTransactionCount*` — transaction count in a block. @@ -366,7 +362,7 @@ pub(crate) async fn block_tx_count( hctx: HandlerCtx, (t,): (T,), ctx: StorageRpcCtx, -) -> Result, String> +) -> Result, EthError> where T: Into, H: HotKv + Send + Sync + 'static, @@ -376,15 +372,12 @@ where let task = async move { let cold = ctx.cold(); - let block_num = ctx.resolve_block_id(id).map_err(|e| e.to_string())?; + let block_num = ctx.resolve_block_id(id)?; - cold.get_transaction_count(block_num) - .await - .map(|c| Some(U64::from(c))) - .map_err(|e| e.to_string()) + Ok(Some(U64::from(cold.get_transaction_count(block_num).await?))) }; - await_handler!(@option hctx.spawn(task)) + await_handler!(hctx.spawn(task), EthError::task_panic()) } /// `eth_getBlockReceipts` — all receipts in a block. @@ -392,21 +385,20 @@ pub(crate) async fn block_receipts( hctx: HandlerCtx, (id,): (BlockId,), ctx: StorageRpcCtx, -) -> Result, String> +) -> Result, EthError> where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, { let task = async move { let cold = ctx.cold(); - let block_num = ctx.resolve_block_id(id).map_err(|e| e.to_string())?; + let block_num = ctx.resolve_block_id(id)?; let (header, txs, receipts) = tokio::try_join!( cold.get_header_by_number(block_num), cold.get_transactions_in_block(block_num), cold.get_receipts_in_block(block_num), - ) - .map_err(|e| e.to_string())?; + )?; let Some(header) = header else { return Ok(None); @@ -417,7 +409,7 @@ where Ok(Some(LazyReceipts { txs, receipts, base_fee })) }; - await_handler!(@option hctx.spawn(task)) + await_handler!(hctx.spawn(task), EthError::task_panic()) } /// `eth_getBlockHeaderByHash` / `eth_getBlockHeaderByNumber`. @@ -425,7 +417,7 @@ pub(crate) async fn header_by( hctx: HandlerCtx, (t,): (T,), ctx: StorageRpcCtx, -) -> Result, String> +) -> Result, EthError> where T: Into, H: HotKv + Send + Sync + 'static, @@ -434,22 +426,18 @@ where let id = t.into(); let task = async move { - ctx.resolve_header(id) - .map(|opt| { - opt.map(|sh| { - let hash = sh.hash(); - alloy::rpc::types::Header { - inner: sh.into_inner(), - hash, - total_difficulty: None, - size: None, - } - }) - }) - .map_err(|e| e.to_string()) + Ok(ctx.resolve_header(id)?.map(|sh| { + let hash = sh.hash(); + alloy::rpc::types::Header { + inner: sh.into_inner(), + hash, + total_difficulty: None, + size: None, + } + })) }; - await_handler!(@option hctx.spawn_blocking(task)) + await_handler!(hctx.spawn_blocking(task), EthError::task_panic()) } // --------------------------------------------------------------------------- @@ -461,28 +449,27 @@ pub(crate) async fn transaction_by_hash( hctx: HandlerCtx, (hash,): (B256,), ctx: StorageRpcCtx, -) -> Result, String> +) -> Result, EthError> where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, { let task = async move { let cold = ctx.cold(); - let Some(confirmed) = cold.get_tx_by_hash(hash).await.map_err(|e| e.to_string())? else { + let Some(confirmed) = cold.get_tx_by_hash(hash).await? else { return Ok(None); }; let (tx, meta) = confirmed.into_parts(); // Fetch header for base_fee - let header = - cold.get_header_by_number(meta.block_number()).await.map_err(|e| e.to_string())?; + let header = cold.get_header_by_number(meta.block_number()).await?; let base_fee = header.and_then(|h| h.base_fee_per_gas); Ok(Some(build_rpc_transaction(&tx, &meta, base_fee))) }; - await_handler!(@option hctx.spawn(task)) + await_handler!(hctx.spawn(task), EthError::task_panic()) } /// `eth_getRawTransactionByHash` — RLP-encoded transaction bytes. @@ -490,20 +477,16 @@ pub(crate) async fn raw_transaction_by_hash( hctx: HandlerCtx, (hash,): (B256,), ctx: StorageRpcCtx, -) -> Result, String> +) -> Result, EthError> where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, { let task = async move { - ctx.cold() - .get_tx_by_hash(hash) - .await - .map(|opt| opt.map(|c| c.into_inner().encoded_2718().into())) - .map_err(|e| e.to_string()) + Ok(ctx.cold().get_tx_by_hash(hash).await?.map(|c| c.into_inner().encoded_2718().into())) }; - await_handler!(@option hctx.spawn(task)) + await_handler!(hctx.spawn(task), EthError::task_panic()) } /// `eth_getTransactionByBlock*AndIndex` — transaction by position in block. @@ -511,7 +494,7 @@ pub(crate) async fn transaction_by_block_and_index( hctx: HandlerCtx, (t, index): (T, U64), ctx: StorageRpcCtx, -) -> Result, String> +) -> Result, EthError> where T: Into, H: HotKv + Send + Sync + 'static, @@ -521,25 +504,21 @@ where let task = async move { let cold = ctx.cold(); - let block_num = ctx.resolve_block_id(id).map_err(|e| e.to_string())?; + let block_num = ctx.resolve_block_id(id)?; - let Some(confirmed) = cold - .get_tx_by_block_and_index(block_num, index.to::()) - .await - .map_err(|e| e.to_string())? + let Some(confirmed) = cold.get_tx_by_block_and_index(block_num, index.to::()).await? else { return Ok(None); }; let (tx, meta) = confirmed.into_parts(); - let header = - cold.get_header_by_number(meta.block_number()).await.map_err(|e| e.to_string())?; + let header = cold.get_header_by_number(meta.block_number()).await?; let base_fee = header.and_then(|h| h.base_fee_per_gas); Ok(Some(build_rpc_transaction(&tx, &meta, base_fee))) }; - await_handler!(@option hctx.spawn(task)) + await_handler!(hctx.spawn(task), EthError::task_panic()) } /// `eth_getRawTransactionByBlock*AndIndex` — raw RLP bytes by position. @@ -547,7 +526,7 @@ pub(crate) async fn raw_transaction_by_block_and_index( hctx: HandlerCtx, (t, index): (T, U64), ctx: StorageRpcCtx, -) -> Result, String> +) -> Result, EthError> where T: Into, H: HotKv + Send + Sync + 'static, @@ -557,15 +536,15 @@ where let task = async move { let cold = ctx.cold(); - let block_num = ctx.resolve_block_id(id).map_err(|e| e.to_string())?; + let block_num = ctx.resolve_block_id(id)?; - cold.get_tx_by_block_and_index(block_num, index.to::()) - .await - .map(|opt| opt.map(|c| c.into_inner().encoded_2718().into())) - .map_err(|e| e.to_string()) + Ok(cold + .get_tx_by_block_and_index(block_num, index.to::()) + .await? + .map(|c| c.into_inner().encoded_2718().into())) }; - await_handler!(@option hctx.spawn(task)) + await_handler!(hctx.spawn(task), EthError::task_panic()) } /// `eth_getTransactionReceipt` — receipt by tx hash. Fetches the receipt, @@ -574,7 +553,7 @@ pub(crate) async fn transaction_receipt( hctx: HandlerCtx, (hash,): (B256,), ctx: StorageRpcCtx, -) -> Result, String> +) -> Result, EthError> where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, @@ -582,25 +561,22 @@ where let task = async move { let cold = ctx.cold(); - let Some(cr) = - cold.get_receipt(ReceiptSpecifier::TxHash(hash)).await.map_err(|e| e.to_string())? - else { + let Some(cr) = cold.get_receipt(ReceiptSpecifier::TxHash(hash)).await? else { return Ok(None); }; let (tx, header) = tokio::try_join!( cold.get_tx_by_hash(cr.tx_hash), cold.get_header_by_number(cr.block_number), - ) - .map_err(|e| e.to_string())?; + )?; - let tx = tx.ok_or(EthError::TransactionMissing).map_err(|e| e.to_string())?.into_inner(); + let tx = tx.ok_or(EthError::TransactionMissing(cr.tx_hash))?.into_inner(); let base_fee = header.and_then(|h| h.base_fee_per_gas); Ok(Some(build_receipt(&cr, &tx, base_fee))) }; - await_handler!(@option hctx.spawn(task)) + await_handler!(hctx.spawn(task), EthError::task_panic()) } // --------------------------------------------------------------------------- @@ -612,7 +588,7 @@ pub(crate) async fn balance( hctx: HandlerCtx, AddrWithBlock(address, block): AddrWithBlock, ctx: StorageRpcCtx, -) -> Result +) -> Result where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, @@ -621,13 +597,14 @@ where let task = async move { let (reader, height) = hot_reader_at_block(&ctx, id)?; - let acct = - reader.get_account_at_height(&address, Some(height)).map_err(|e| e.to_string())?; + let acct = reader + .get_account_at_height(&address, Some(height)) + .map_err(|e| EthError::Internal(e.to_string()))?; Ok(acct.map(|a| a.balance).unwrap_or_default()) }; - await_handler!(@option hctx.spawn_blocking(task)) + await_handler!(hctx.spawn_blocking(task), EthError::task_panic()) } /// `eth_getStorageAt` — contract storage slot at a given block. @@ -635,7 +612,7 @@ pub(crate) async fn storage_at( hctx: HandlerCtx, StorageAtArgs(address, key, block): StorageAtArgs, ctx: StorageRpcCtx, -) -> Result +) -> Result where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, @@ -646,12 +623,12 @@ where let (reader, height) = hot_reader_at_block(&ctx, id)?; let val = reader .get_storage_at_height(&address, &key, Some(height)) - .map_err(|e| e.to_string())?; + .map_err(|e| EthError::Internal(e.to_string()))?; Ok(val.unwrap_or_default().to_be_bytes().into()) }; - await_handler!(@option hctx.spawn_blocking(task)) + await_handler!(hctx.spawn_blocking(task), EthError::task_panic()) } /// `eth_getTransactionCount` — account nonce at a given block. @@ -659,7 +636,7 @@ pub(crate) async fn addr_tx_count( hctx: HandlerCtx, AddrWithBlock(address, block): AddrWithBlock, ctx: StorageRpcCtx, -) -> Result +) -> Result where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, @@ -668,13 +645,14 @@ where let task = async move { let (reader, height) = hot_reader_at_block(&ctx, id)?; - let acct = - reader.get_account_at_height(&address, Some(height)).map_err(|e| e.to_string())?; + let acct = reader + .get_account_at_height(&address, Some(height)) + .map_err(|e| EthError::Internal(e.to_string()))?; Ok(U64::from(acct.map(|a| a.nonce).unwrap_or_default())) }; - await_handler!(@option hctx.spawn_blocking(task)) + await_handler!(hctx.spawn_blocking(task), EthError::task_panic()) } /// `eth_getCode` — contract bytecode at a given block. @@ -682,7 +660,7 @@ pub(crate) async fn code_at( hctx: HandlerCtx, AddrWithBlock(address, block): AddrWithBlock, ctx: StorageRpcCtx, -) -> Result +) -> Result where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, @@ -691,8 +669,9 @@ where let task = async move { let (reader, height) = hot_reader_at_block(&ctx, id)?; - let acct = - reader.get_account_at_height(&address, Some(height)).map_err(|e| e.to_string())?; + let acct = reader + .get_account_at_height(&address, Some(height)) + .map_err(|e| EthError::Internal(e.to_string()))?; let Some(acct) = acct else { return Ok(alloy::primitives::Bytes::new()); @@ -702,12 +681,13 @@ where return Ok(alloy::primitives::Bytes::new()); }; - let code = reader.get_bytecode(&code_hash).map_err(|e| e.to_string())?; + let code = + reader.get_bytecode(&code_hash).map_err(|e| EthError::Internal(e.to_string()))?; Ok(code.map(|c| c.original_bytes()).unwrap_or_default()) }; - await_handler!(@option hctx.spawn_blocking(task)) + await_handler!(hctx.spawn_blocking(task), EthError::task_panic()) } // --------------------------------------------------------------------------- @@ -722,7 +702,7 @@ pub(crate) async fn run_call( hctx: HandlerCtx, TxParams(request, block, state_overrides, block_overrides): TxParams, ctx: StorageRpcCtx, -) -> ResponsePayload +) -> Result where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, @@ -731,28 +711,30 @@ where let span = trace_span!("run_call", block_id = %id); let task = async move { - let EvmBlockContext { header, db, spec_id } = response_tri!(ctx.resolve_evm_block(id)); + let EvmBlockContext { header, db, spec_id } = ctx.resolve_evm_block(id)?; let mut trevm = signet_evm::signet_evm(db, ctx.constants().clone()); trevm.set_spec_id(spec_id); let trevm = trevm.fill_cfg(&CfgFiller(ctx.chain_id())).fill_block(&header); - let trevm = response_tri!(trevm.maybe_apply_state_overrides(state_overrides.as_ref())) + let trevm = trevm + .maybe_apply_state_overrides(state_overrides.as_ref()) + .map_err(|e| EthError::Internal(e.to_string()))? .maybe_apply_block_overrides(block_overrides.as_deref()) .fill_tx(&request); let mut trevm = trevm; - let new_gas = response_tri!(trevm.cap_tx_gas()); + let new_gas = trevm.cap_tx_gas().map_err(|e| EthError::Internal(e.to_string()))?; if Some(new_gas) != request.gas { debug!(req_gas = ?request.gas, new_gas, "capping gas for call"); } - let result = response_tri!(trevm.call().map_err(signet_evm::EvmErrored::into_error)); - ResponsePayload(Ok(result.0)) + let result = trevm.call().map_err(|e| EthError::Internal(e.into_error().to_string()))?; + Ok(result.0) } .instrument(span); - await_handler!(@response_option hctx.spawn_blocking(task)) + await_handler!(hctx.spawn_blocking(task), EthError::task_panic()) } /// `eth_call` — execute a call and return the output bytes. @@ -763,7 +745,7 @@ pub(crate) async fn call( hctx: HandlerCtx, mut params: TxParams, ctx: StorageRpcCtx, -) -> ResponsePayload +) -> Result where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, @@ -771,30 +753,22 @@ where let max_gas = ctx.config().rpc_gas_cap; normalize_gas_stateless(&mut params.0, max_gas); - await_handler!(@response_option hctx.spawn_with_ctx(|hctx| async move { - let res = match run_call(hctx, params, ctx).await.0 { - Ok(res) => res, - Err(err) => return ResponsePayload(Err(err)), - }; + await_handler!( + hctx.spawn_with_ctx(|hctx| async move { + let res = run_call(hctx, params, ctx).await?; - match res { - ExecutionResult::Success { output, .. } => { - ResponsePayload(Ok(output.data().clone())) - } - ExecutionResult::Revert { output, .. } => { - ResponsePayload::internal_error_with_message_and_obj( - "execution reverted".into(), - output.clone().into(), - ) - } - ExecutionResult::Halt { reason, .. } => { - ResponsePayload::internal_error_with_message_and_obj( - "execution halted".into(), - format!("{reason:?}").into(), - ) + match res { + ExecutionResult::Success { output, .. } => Ok(output.data().clone()), + ExecutionResult::Revert { output, .. } => { + Err(EthError::EvmRevert { output: output.clone() }) + } + ExecutionResult::Halt { reason, .. } => { + Err(EthError::EvmHalt { reason: format!("{reason:?}") }) + } } - } - })) + }), + EthError::task_panic() + ) } /// `eth_estimateGas` — estimate gas required for a transaction. @@ -802,7 +776,7 @@ pub(crate) async fn estimate_gas( hctx: HandlerCtx, TxParams(mut request, block, state_overrides, block_overrides): TxParams, ctx: StorageRpcCtx, -) -> ResponsePayload +) -> Result where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, @@ -814,38 +788,34 @@ where let span = trace_span!("eth_estimateGas", block_id = %id); let task = async move { - let EvmBlockContext { header, db, spec_id } = response_tri!(ctx.resolve_evm_block(id)); + let EvmBlockContext { header, db, spec_id } = ctx.resolve_evm_block(id)?; let mut trevm = signet_evm::signet_evm(db, ctx.constants().clone()); trevm.set_spec_id(spec_id); let trevm = trevm.fill_cfg(&CfgFiller(ctx.chain_id())).fill_block(&header); - let trevm = response_tri!(trevm.maybe_apply_state_overrides(state_overrides.as_ref())) + let trevm = trevm + .maybe_apply_state_overrides(state_overrides.as_ref()) + .map_err(|e| EthError::Internal(e.to_string()))? .maybe_apply_block_overrides(block_overrides.as_deref()) .fill_tx(&request); let (estimate, _) = - response_tri!(trevm.estimate_gas().map_err(signet_evm::EvmErrored::into_error)); + trevm.estimate_gas().map_err(|e| EthError::Internal(e.into_error().to_string()))?; match estimate { - EstimationResult::Success { limit, .. } => ResponsePayload(Ok(U64::from(limit))), + EstimationResult::Success { limit, .. } => Ok(U64::from(limit)), EstimationResult::Revert { reason, .. } => { - ResponsePayload::internal_error_with_message_and_obj( - "execution reverted".into(), - reason.clone().into(), - ) + Err(EthError::EvmRevert { output: reason.clone() }) } EstimationResult::Halt { reason, .. } => { - ResponsePayload::internal_error_with_message_and_obj( - "execution halted".into(), - format!("{reason:?}").into(), - ) + Err(EthError::EvmHalt { reason: format!("{reason:?}") }) } } } .instrument(span); - await_handler!(@response_option hctx.spawn_blocking(task)) + await_handler!(hctx.spawn_blocking(task), EthError::task_panic()) } /// `eth_createAccessList` — generate an access list for a transaction. @@ -853,7 +823,7 @@ pub(crate) async fn create_access_list( hctx: HandlerCtx, TxParams(mut request, block, state_overrides, block_overrides): TxParams, ctx: StorageRpcCtx, -) -> ResponsePayload +) -> Result where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, @@ -865,13 +835,15 @@ where let span = trace_span!("eth_createAccessList", block_id = %id); let task = async move { - let EvmBlockContext { header, db, spec_id } = response_tri!(ctx.resolve_evm_block(id)); + let EvmBlockContext { header, db, spec_id } = ctx.resolve_evm_block(id)?; let mut trevm = signet_evm::signet_evm(db, ctx.constants().clone()); trevm.set_spec_id(spec_id); let trevm = trevm.fill_cfg(&CfgFiller(ctx.chain_id())).fill_block(&header); - let trevm = response_tri!(trevm.maybe_apply_state_overrides(state_overrides.as_ref())) + let trevm = trevm + .maybe_apply_state_overrides(state_overrides.as_ref()) + .map_err(|e| EthError::Internal(e.to_string()))? .maybe_apply_block_overrides(block_overrides.as_deref()) .fill_tx(&request); @@ -889,11 +861,11 @@ where let access_list = inspector.into_access_list(); - ResponsePayload(Ok(AccessListResult { access_list, gas_used, error })) + Ok(AccessListResult { access_list, gas_used, error }) } .instrument(span); - await_handler!(@response_option hctx.spawn_blocking(task)) + await_handler!(hctx.spawn_blocking(task), EthError::task_panic()) } // --------------------------------------------------------------------------- @@ -908,18 +880,18 @@ pub(crate) async fn send_raw_transaction( hctx: HandlerCtx, (tx,): (alloy::primitives::Bytes,), ctx: StorageRpcCtx, -) -> Result +) -> Result where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, { let Some(tx_cache) = ctx.tx_cache().cloned() else { - return Err("tx-cache URL not provided".to_string()); + return Err(EthError::Internal("tx-cache URL not provided".into())); }; let task = |hctx: HandlerCtx| async move { let envelope = alloy::consensus::TxEnvelope::decode_2718(&mut tx.as_ref()) - .map_err(|e| e.to_string())?; + .map_err(|e| EthError::InvalidParams(e.to_string()))?; let hash = *envelope.tx_hash(); hctx.spawn(async move { @@ -931,7 +903,7 @@ where Ok(hash) }; - await_handler!(@option hctx.spawn_blocking_with_ctx(task)) + await_handler!(hctx.spawn_blocking_with_ctx(task), EthError::task_panic()) } // --------------------------------------------------------------------------- @@ -960,7 +932,7 @@ pub(crate) async fn get_logs( hctx: HandlerCtx, (filter,): (Filter,), ctx: StorageRpcCtx, -) -> Result, String> +) -> Result, EthError> where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, @@ -979,11 +951,15 @@ where .unwrap_or_else(|| ctx.tags().latest()); if from > to { - return Err("fromBlock must not exceed toBlock".to_string()); + return Err(EthError::InvalidParams( + "fromBlock must not exceed toBlock".into(), + )); } let max_blocks = ctx.config().max_blocks_per_filter; if to - from >= max_blocks { - return Err(format!("query exceeds max block range ({max_blocks})")); + return Err(EthError::InvalidParams(format!( + "query exceeds max block range ({max_blocks})" + ))); } Filter { @@ -999,15 +975,12 @@ where let max_logs = ctx.config().max_logs_per_response; let deadline = ctx.config().max_log_query_deadline; - let stream = cold - .stream_logs(resolved_filter, max_logs, deadline) - .await - .map_err(|e| e.to_string())?; + let stream = cold.stream_logs(resolved_filter, max_logs, deadline).await?; - collect_log_stream(stream).await.map_err(|e| e.to_string()) + Ok(collect_log_stream(stream).await?) }; - await_handler!(@option hctx.spawn(task)) + await_handler!(hctx.spawn(task), EthError::task_panic()) } // --------------------------------------------------------------------------- @@ -1019,7 +992,7 @@ pub(crate) async fn new_filter( hctx: HandlerCtx, (filter,): (Filter,), ctx: StorageRpcCtx, -) -> Result +) -> Result where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, @@ -1029,14 +1002,14 @@ where Ok(ctx.filter_manager().install_log_filter(latest, filter)) }; - await_handler!(@option hctx.spawn_blocking(task)) + await_handler!(hctx.spawn_blocking(task), EthError::task_panic()) } /// `eth_newBlockFilter` — install a block hash filter for polling. pub(crate) async fn new_block_filter( hctx: HandlerCtx, ctx: StorageRpcCtx, -) -> Result +) -> Result where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, @@ -1046,7 +1019,7 @@ where Ok(ctx.filter_manager().install_block_filter(latest)) }; - await_handler!(@option hctx.spawn_blocking(task)) + await_handler!(hctx.spawn_blocking(task), EthError::task_panic()) } /// `eth_uninstallFilter` — remove a filter. @@ -1054,13 +1027,13 @@ pub(crate) async fn uninstall_filter( hctx: HandlerCtx, (id,): (U64,), ctx: StorageRpcCtx, -) -> Result +) -> Result where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, { let task = async move { Ok(ctx.filter_manager().uninstall(id).is_some()) }; - await_handler!(@option hctx.spawn_blocking(task)) + await_handler!(hctx.spawn_blocking(task), EthError::task_panic()) } /// `eth_getFilterChanges` / `eth_getFilterLogs` — poll a filter for new @@ -1069,14 +1042,16 @@ pub(crate) async fn get_filter_changes( hctx: HandlerCtx, (id,): (U64,), ctx: StorageRpcCtx, -) -> Result +) -> Result where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, { let task = async move { let fm = ctx.filter_manager(); - let mut entry = fm.get_mut(id).ok_or_else(|| format!("filter not found: {id}"))?; + let mut entry = fm + .get_mut(id) + .ok_or_else(|| EthError::InvalidParams(format!("filter not found: {id}")))?; // Scan the global reorg ring buffer for notifications received // since this filter's last poll, then lazily compute removed logs @@ -1116,7 +1091,7 @@ where if entry.is_block() { let specs: Vec<_> = (start..=latest).map(HeaderSpecifier::Number).collect(); - let headers = cold.get_headers(specs).await.map_err(|e| e.to_string())?; + let headers = cold.get_headers(specs).await?; let hashes: Vec = headers.into_iter().flatten().map(|h| h.hash()).collect(); entry.mark_polled(latest); Ok(FilterOutput::from(hashes)) @@ -1133,10 +1108,9 @@ where let max_logs = ctx.config().max_logs_per_response; let deadline = ctx.config().max_log_query_deadline; - let stream = - cold.stream_logs(resolved, max_logs, deadline).await.map_err(|e| e.to_string())?; + let stream = cold.stream_logs(resolved, max_logs, deadline).await?; - let mut logs = collect_log_stream(stream).await.map_err(|e| e.to_string())?; + let mut logs = collect_log_stream(stream).await?; // Prepend removed logs so the client sees removals before // the replacement data. @@ -1151,7 +1125,7 @@ where } }; - await_handler!(@option hctx.spawn(task)) + await_handler!(hctx.spawn(task), EthError::task_panic()) } // --------------------------------------------------------------------------- @@ -1163,7 +1137,7 @@ pub(crate) async fn subscribe( hctx: HandlerCtx, sub: SubscribeArgs, ctx: StorageRpcCtx, -) -> Result +) -> Result where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, @@ -1172,7 +1146,7 @@ where ctx.sub_manager() .subscribe(&hctx, interest) - .ok_or_else(|| "notifications not enabled on this transport".to_string()) + .ok_or_else(|| EthError::Internal("notifications not enabled on this transport".into())) } /// `eth_unsubscribe` — cancel a push-based subscription. @@ -1180,11 +1154,11 @@ pub(crate) async fn unsubscribe( hctx: HandlerCtx, (id,): (U64,), ctx: StorageRpcCtx, -) -> Result +) -> Result where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, { let task = async move { Ok(ctx.sub_manager().unsubscribe(id)) }; - await_handler!(@option hctx.spawn_blocking(task)) + await_handler!(hctx.spawn_blocking(task), EthError::task_panic()) } diff --git a/crates/rpc/src/eth/error.rs b/crates/rpc/src/eth/error.rs index 135b6b8f..7316d2d4 100644 --- a/crates/rpc/src/eth/error.rs +++ b/crates/rpc/src/eth/error.rs @@ -1,7 +1,11 @@ //! Error types for the storage-backed ETH RPC. -use alloy::{eips::BlockId, primitives::Bytes}; -use serde::Serialize; +use ajj::IntoErrorPayload; +use alloy::{ + eips::BlockId, + primitives::{B256, Bytes}, +}; +use std::borrow::Cow; /// Errors from the storage-backed ETH RPC. #[derive(Debug, thiserror::Error)] @@ -15,47 +19,153 @@ pub enum EthError { /// Block resolution error. #[error("resolve: {0}")] Resolve(#[from] crate::config::resolve::ResolveError), - /// Invalid transaction signature. - #[error("invalid transaction signature")] - InvalidSignature, /// Block not found. #[error("block not found: {0}")] BlockNotFound(BlockId), /// Receipt found but the corresponding transaction is missing. - #[error("receipt found but transaction missing")] - TransactionMissing, - /// EVM execution error. - #[error("evm: {0}")] - Evm(String), + #[error("transaction not found: {0}")] + TransactionMissing(B256), + /// EVM execution reverted with output data. + #[error("execution reverted")] + EvmRevert { + /// ABI-encoded revert data. + output: Bytes, + }, + /// EVM execution halted with a non-revert reason. + #[error("execution halted: {reason}")] + EvmHalt { + /// Human-readable halt reason. + reason: String, + }, + /// Invalid RPC parameters. + #[error("{0}")] + InvalidParams(String), + /// Internal server error. + #[error("{0}")] + Internal(String), } impl EthError { - /// Convert the error to a string for JSON-RPC responses. - pub fn into_string(self) -> String { - self.to_string() + /// Construct the standard error for a spawned task that panicked or + /// was cancelled before producing a result. + pub(crate) fn task_panic() -> Self { + Self::Internal("task panicked or cancelled".into()) } } -/// Error data for `eth_call` and `eth_estimateGas` responses. +/// Returns the JSON-RPC error code for a [`ResolveError`]. /// -/// Serialized as JSON in the error response `data` field. -#[derive(Debug, Clone, Serialize)] -#[serde(untagged)] -pub(crate) enum CallErrorData { - /// Revert data bytes. - Bytes(Bytes), - /// Error message string. - String(String), +/// [`ResolveError`]: crate::config::resolve::ResolveError +pub(crate) const fn resolve_error_code(e: &crate::config::resolve::ResolveError) -> i64 { + use crate::config::resolve::ResolveError; + match e { + ResolveError::HashNotFound(_) => -32001, + ResolveError::Storage(_) | ResolveError::Db(_) => -32000, + } +} + +/// Returns the JSON-RPC error message for a [`ResolveError`]. +/// +/// [`ResolveError`]: crate::config::resolve::ResolveError +pub(crate) fn resolve_error_message(e: &crate::config::resolve::ResolveError) -> Cow<'static, str> { + use crate::config::resolve::ResolveError; + match e { + ResolveError::HashNotFound(hash) => Cow::Owned(format!("block hash not found: {hash}")), + ResolveError::Storage(_) | ResolveError::Db(_) => Cow::Borrowed("server error"), + } } -impl From for CallErrorData { - fn from(b: Bytes) -> Self { - Self::Bytes(b) +impl IntoErrorPayload for EthError { + type ErrData = Bytes; + + fn error_code(&self) -> i64 { + match self { + Self::Cold(..) | Self::Hot(..) => -32000, + Self::Resolve(r) => resolve_error_code(r), + Self::BlockNotFound(_) | Self::TransactionMissing(_) => -32001, + Self::EvmRevert { .. } => 3, + Self::EvmHalt { .. } => -32000, + Self::InvalidParams(_) => -32602, + Self::Internal(_) => -32000, + } + } + + fn error_message(&self) -> Cow<'static, str> { + match self { + Self::Cold(..) | Self::Hot(..) => Cow::Borrowed("server error"), + Self::Resolve(r) => resolve_error_message(r), + Self::BlockNotFound(id) => Cow::Owned(format!("block not found: {id}")), + Self::TransactionMissing(h) => Cow::Owned(format!("transaction not found: {h}")), + Self::EvmRevert { .. } => Cow::Borrowed("execution reverted"), + Self::EvmHalt { reason } => Cow::Owned(format!("execution halted: {reason}")), + Self::InvalidParams(msg) => Cow::Owned(msg.clone()), + Self::Internal(msg) => Cow::Owned(msg.clone()), + } + } + + fn error_data(self) -> Option { + match self { + Self::EvmRevert { output } => Some(output), + _ => None, + } } } -impl From for CallErrorData { - fn from(s: String) -> Self { - Self::String(s) +#[cfg(test)] +mod tests { + use super::*; + use alloy::primitives::B256; + + #[test] + fn block_not_found_code() { + let err = EthError::BlockNotFound(BlockId::latest()); + assert_eq!(err.error_code(), -32001); + assert!(err.error_message().contains("block not found")); + } + + #[test] + fn transaction_missing_code() { + let err = EthError::TransactionMissing(B256::ZERO); + assert_eq!(err.error_code(), -32001); + assert!(err.error_message().contains("transaction not found")); + } + + #[test] + fn evm_revert_code_and_data() { + let output = Bytes::from(vec![0xde, 0xad]); + let err = EthError::EvmRevert { output: output.clone() }; + assert_eq!(err.error_code(), 3); + assert_eq!(err.error_message(), "execution reverted"); + assert_eq!(err.error_data(), Some(output)); + } + + #[test] + fn evm_halt_code() { + let err = EthError::EvmHalt { reason: "OutOfGas".into() }; + assert_eq!(err.error_code(), -32000); + assert!(err.error_message().contains("OutOfGas")); + assert_eq!(err.error_data(), None); + } + + #[test] + fn invalid_params_code() { + let err = EthError::InvalidParams("bad param".into()); + assert_eq!(err.error_code(), -32602); + assert_eq!(err.error_message(), "bad param"); + } + + #[test] + fn internal_code() { + let err = EthError::Internal("something broke".into()); + assert_eq!(err.error_code(), -32000); + assert_eq!(err.error_message(), "something broke"); + } + + #[test] + fn resolve_hash_not_found_code() { + use crate::config::resolve::ResolveError; + let inner = ResolveError::HashNotFound(B256::ZERO); + assert_eq!(resolve_error_code(&inner), -32001); + assert!(resolve_error_message(&inner).contains("block hash not found")); } } diff --git a/crates/rpc/src/eth/helpers.rs b/crates/rpc/src/eth/helpers.rs index 8a1e534d..b0fa90c0 100644 --- a/crates/rpc/src/eth/helpers.rs +++ b/crates/rpc/src/eth/helpers.rs @@ -1,7 +1,10 @@ //! Parameter types, macros, and utility helpers for ETH RPC endpoints. -use super::types::{RpcReceipt, RpcTransaction}; -use crate::interest::InterestKind; +use super::{ + error::EthError, + types::{RpcReceipt, RpcTransaction}, +}; +use crate::{config::resolve::ResolveError, interest::InterestKind}; use alloy::{ consensus::{ ReceiptEnvelope, ReceiptWithBloom, Transaction, TxReceipt, transaction::Recovered, @@ -56,22 +59,25 @@ pub(crate) struct SubscribeArgs( ); impl TryFrom for InterestKind { - type Error = String; + type Error = EthError; fn try_from(args: SubscribeArgs) -> Result { match args.0 { - SubscriptionKind::Logs => args - .1 - .map(InterestKind::Log) - .ok_or_else(|| "missing filter for Logs subscription".to_string()), + SubscriptionKind::Logs => args.1.map(InterestKind::Log).ok_or_else(|| { + EthError::InvalidParams("missing filter for Logs subscription".into()) + }), SubscriptionKind::NewHeads => { if args.1.is_some() { - Err("filter not supported for NewHeads subscription".to_string()) + Err(EthError::InvalidParams( + "filter not supported for NewHeads subscription".into(), + )) } else { Ok(InterestKind::Block) } } - other => Err(format!("unsupported subscription kind: {other:?}")), + other => { + Err(EthError::InvalidParams(format!("unsupported subscription kind: {other:?}"))) + } } } } @@ -88,46 +94,18 @@ pub(crate) const fn normalize_gas_stateless(request: &mut TransactionRequest, ma } } -/// Await a handler task, returning an error string on panic/cancel. +/// Await a spawned handler task. Returns the error expression on +/// panic, cancellation, or `None` result. macro_rules! await_handler { - ($h:expr) => { - match $h.await { - Ok(res) => res, - Err(_) => return Err("task panicked or cancelled".to_string()), - } - }; - - (@option $h:expr) => { - match $h.await { - Ok(Some(res)) => res, - _ => return Err("task panicked or cancelled".to_string()), - } - }; - - (@response_option $h:expr) => { + ($h:expr, $err:expr) => { match $h.await { Ok(Some(res)) => res, - _ => { - return ajj::ResponsePayload::internal_error_message(std::borrow::Cow::Borrowed( - "task panicked or cancelled", - )) - } + _ => return Err($err), } }; } pub(crate) use await_handler; -/// Try-operator for `ResponsePayload`. -macro_rules! response_tri { - ($h:expr) => { - match $h { - Ok(res) => res, - Err(err) => return ajj::ResponsePayload::internal_error_message(err.to_string().into()), - } - }; -} -pub(crate) use response_tri; - /// Resolve a block ID and open a hot storage reader at that height. /// /// Opens a single MDBX read transaction and resolves the block ID @@ -144,18 +122,18 @@ pub(crate) use response_tri; pub(crate) fn hot_reader_at_block( ctx: &crate::config::StorageRpcCtx, id: BlockId, -) -> Result<(H::RoTx, u64), String> +) -> Result<(H::RoTx, u64), EthError> where H: signet_hot::HotKv, ::Error: std::error::Error + Send + Sync + 'static, { - let reader = ctx.hot_reader().map_err(|e| e.to_string())?; + let reader = ctx.hot_reader()?; let height = match id { BlockId::Number(tag) => ctx.resolve_block_tag(tag), BlockId::Hash(h) => reader .get_header_number(&h.block_hash) - .map_err(|e| e.to_string())? - .ok_or_else(|| format!("block hash not found: {}", h.block_hash))?, + .map_err(|e| ResolveError::Db(Box::new(e)))? + .ok_or(ResolveError::HashNotFound(h.block_hash))?, }; Ok((reader, height)) } diff --git a/crates/rpc/src/eth/mod.rs b/crates/rpc/src/eth/mod.rs index 4e1309a9..fc68c41c 100644 --- a/crates/rpc/src/eth/mod.rs +++ b/crates/rpc/src/eth/mod.rs @@ -10,7 +10,7 @@ use endpoints::{ uncle_block, uncle_count, uninstall_filter, unsubscribe, }; -mod error; +pub(crate) mod error; pub use error::EthError; pub(crate) mod helpers; diff --git a/crates/rpc/src/signet/endpoints.rs b/crates/rpc/src/signet/endpoints.rs index 01193eac..c87d973c 100644 --- a/crates/rpc/src/signet/endpoints.rs +++ b/crates/rpc/src/signet/endpoints.rs @@ -2,10 +2,10 @@ use crate::{ config::{EvmBlockContext, StorageRpcCtx}, - eth::helpers::{CfgFiller, await_handler, response_tri}, + eth::helpers::{CfgFiller, await_handler}, signet::error::SignetError, }; -use ajj::{HandlerCtx, ResponsePayload}; +use ajj::HandlerCtx; use alloy::eips::BlockId; use signet_bundle::{SignetBundleDriver, SignetCallBundle, SignetCallBundleResponse}; use signet_hot::{HotKv, model::HotKvRead}; @@ -23,13 +23,13 @@ pub(super) async fn send_order( hctx: HandlerCtx, order: SignedOrder, ctx: StorageRpcCtx, -) -> ResponsePayload<(), SignetError> +) -> Result<(), SignetError> where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, { let Some(tx_cache) = ctx.tx_cache().cloned() else { - return ResponsePayload(Err(SignetError::TxCacheNotProvided.into())); + return Err(SignetError::TxCacheNotProvided); }; let task = |hctx: HandlerCtx| async move { @@ -38,10 +38,10 @@ where tracing::warn!(error = %e, "failed to forward order"); } }); - ResponsePayload(Ok(())) + Ok(()) }; - await_handler!(@response_option hctx.spawn_with_ctx(task)) + await_handler!(hctx.spawn_with_ctx(task), SignetError::Timeout) } /// `signet_callBundle` handler. @@ -49,7 +49,7 @@ pub(super) async fn call_bundle( hctx: HandlerCtx, bundle: SignetCallBundle, ctx: StorageRpcCtx, -) -> ResponsePayload +) -> Result where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, @@ -61,10 +61,10 @@ where let block_id: BlockId = id.into(); let EvmBlockContext { header, db, spec_id } = - response_tri!(ctx.resolve_evm_block(block_id).map_err(|e| { + ctx.resolve_evm_block(block_id).map_err(|e| { tracing::warn!(error = %e, ?block_id, "block resolution failed for bundle"); SignetError::Resolve(e.to_string()) - })); + })?; let mut driver = SignetBundleDriver::from(&bundle); @@ -72,21 +72,19 @@ where trevm.set_spec_id(spec_id); let trevm = trevm.fill_cfg(&CfgFiller(ctx.chain_id())).fill_block(&header); - response_tri!(trevm.drive_bundle(&mut driver).map_err(|e| { + trevm.drive_bundle(&mut driver).map_err(|e| { let e = e.into_error(); tracing::warn!(error = %e, "evm error during bundle simulation"); - SignetError::Evm(e.to_string()) - })); + SignetError::EvmHalt { reason: e.to_string() } + })?; - ResponsePayload(Ok(driver.into_response())) + Ok(driver.into_response()) }; let task = async move { select! { _ = tokio::time::sleep(Duration::from_millis(timeout)) => { - ResponsePayload::internal_error_message( - SignetError::Timeout.to_string().into(), - ) + Err(SignetError::Timeout) } result = task => { result @@ -94,5 +92,5 @@ where } }; - await_handler!(@response_option hctx.spawn(task)) + await_handler!(hctx.spawn(task), SignetError::Timeout) } diff --git a/crates/rpc/src/signet/error.rs b/crates/rpc/src/signet/error.rs index 83570abc..134a9193 100644 --- a/crates/rpc/src/signet/error.rs +++ b/crates/rpc/src/signet/error.rs @@ -1,7 +1,9 @@ //! Error types for the signet namespace. +use std::borrow::Cow; + /// Errors that can occur in the `signet` namespace. -#[derive(Debug, Clone, thiserror::Error)] +#[derive(Debug, thiserror::Error)] pub enum SignetError { /// The transaction cache was not provided. #[error("transaction cache not provided")] @@ -9,19 +11,38 @@ pub enum SignetError { /// Block resolution failed. #[error("block resolution error")] Resolve(String), - /// EVM execution error. - #[error("evm execution error")] - Evm(String), + /// EVM execution halted for a non-revert reason. + #[error("execution halted: {reason}")] + EvmHalt { + /// The halt reason. + reason: String, + }, /// Bundle simulation timed out. #[error("timeout during bundle simulation")] Timeout, } -impl serde::Serialize for SignetError { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) +impl ajj::IntoErrorPayload for SignetError { + type ErrData = (); + + fn error_code(&self) -> i64 { + match self { + Self::TxCacheNotProvided | Self::Resolve(_) | Self::EvmHalt { .. } | Self::Timeout => { + -32000 + } + } + } + + fn error_message(&self) -> Cow<'static, str> { + match self { + Self::TxCacheNotProvided => "transaction cache not provided".into(), + Self::Resolve(_) => "block resolution error".into(), + Self::EvmHalt { reason } => format!("execution halted: {reason}").into(), + Self::Timeout => "timeout during bundle simulation".into(), + } + } + + fn error_data(self) -> Option { + None } }