diff --git a/crates/rpc/src/config/rpc_config.rs b/crates/rpc/src/config/rpc_config.rs index c9daac0..74cd262 100644 --- a/crates/rpc/src/config/rpc_config.rs +++ b/crates/rpc/src/config/rpc_config.rs @@ -61,6 +61,11 @@ pub struct StorageRpcConfig { /// Default: `25`. pub max_tracing_requests: usize, + /// Maximum block range for `trace_filter` queries. + /// + /// Default: `100`. + pub max_trace_filter_blocks: u64, + /// Time-to-live for stale filters and subscriptions. /// /// Default: `5 minutes`. @@ -136,6 +141,7 @@ impl Default for StorageRpcConfig { max_logs_per_response: 20_000, max_log_query_deadline: Duration::from_secs(10), max_tracing_requests: 25, + max_trace_filter_blocks: 100, stale_filter_ttl: Duration::from_secs(5 * 60), gas_oracle_block_count: 20, gas_oracle_percentile: 60.0, @@ -188,6 +194,12 @@ impl StorageRpcConfigBuilder { self } + /// Set the max block range for trace_filter. + pub const fn max_trace_filter_blocks(mut self, max: u64) -> Self { + self.inner.max_trace_filter_blocks = max; + self + } + /// Set the time-to-live for stale filters and subscriptions. pub const fn stale_filter_ttl(mut self, ttl: Duration) -> Self { self.inner.stale_filter_ttl = ttl; @@ -298,6 +310,13 @@ pub struct StorageRpcConfigEnv { optional )] max_tracing_requests: Option, + /// Maximum block range for trace_filter queries. + #[from_env( + var = "SIGNET_RPC_MAX_TRACE_FILTER_BLOCKS", + desc = "Maximum block range for trace_filter queries", + optional + )] + max_trace_filter_blocks: Option, /// Filter TTL in seconds. #[from_env( var = "SIGNET_RPC_STALE_FILTER_TTL_SECS", @@ -385,6 +404,9 @@ impl From for StorageRpcConfig { max_tracing_requests: env .max_tracing_requests .map_or(defaults.max_tracing_requests, |v| v as usize), + max_trace_filter_blocks: env + .max_trace_filter_blocks + .unwrap_or(defaults.max_trace_filter_blocks), stale_filter_ttl: env .stale_filter_ttl_secs .map_or(defaults.stale_filter_ttl, Duration::from_secs), diff --git a/crates/rpc/src/debug/tracer.rs b/crates/rpc/src/debug/tracer.rs index 4165bdd..722fbae 100644 --- a/crates/rpc/src/debug/tracer.rs +++ b/crates/rpc/src/debug/tracer.rs @@ -3,11 +3,17 @@ //! Largely adapted from reth: `crates/rpc/rpc/src/debug.rs`. use crate::debug::DebugError; -use alloy::rpc::types::{ - TransactionInfo, - trace::geth::{ - FourByteFrame, GethDebugBuiltInTracerType, GethDebugTracerConfig, GethDebugTracerType, - GethDebugTracingOptions, GethTrace, NoopFrame, +use alloy::{ + primitives::map::HashSet, + rpc::types::{ + TransactionInfo, + trace::{ + geth::{ + FourByteFrame, GethDebugBuiltInTracerType, GethDebugTracerConfig, + GethDebugTracerType, GethDebugTracingOptions, GethTrace, NoopFrame, + }, + parity::{LocalizedTransactionTrace, TraceResults, TraceType}, + }, }, }; use revm_inspectors::tracing::{ @@ -197,6 +203,67 @@ where Ok((frame.into(), trevm)) } +/// Trace a transaction and return Parity-format localized traces. +/// +/// Used by `trace_block`, `trace_transaction`, `trace_get`, and +/// `trace_filter`. +pub(crate) fn trace_parity_localized( + trevm: EvmReady, + tx_info: TransactionInfo, +) -> Result<(Vec, EvmNeedsTx), DebugError> +where + Db: Database + DatabaseCommit + DatabaseRef, + Insp: Inspector>, +{ + let gas_limit = trevm.gas_limit(); + let mut inspector = TracingInspector::new(TracingInspectorConfig::default_parity()); + let trevm = trevm + .try_with_inspector(&mut inspector, |trevm| trevm.run()) + .map_err(|err| DebugError::EvmHalt { reason: err.into_error().to_string() })?; + + let traces = inspector + .with_transaction_gas_limit(gas_limit) + .into_parity_builder() + .into_localized_transaction_traces(tx_info); + + Ok((traces, trevm.accept_state())) +} + +/// Trace a transaction and return Parity-format [`TraceResults`]. +/// +/// When [`TraceType::StateDiff`] is in `trace_types`, the state diff is +/// enriched with pre-transaction balance/nonce from the database. +/// +/// Used by `trace_replayBlockTransactions`, `trace_call`, +/// `trace_callMany`, and `trace_rawTransaction`. +pub(crate) fn trace_parity_replay( + trevm: EvmReady, + trace_types: &HashSet, +) -> Result<(TraceResults, EvmNeedsTx), DebugError> +where + Db: Database + DatabaseCommit + DatabaseRef, + ::Error: std::fmt::Debug, + Insp: Inspector>, +{ + let mut inspector = + TracingInspector::new(TracingInspectorConfig::from_parity_config(trace_types)); + let trevm = trevm + .try_with_inspector(&mut inspector, |trevm| trevm.run()) + .map_err(|err| DebugError::EvmHalt { reason: err.into_error().to_string() })?; + + let (result, mut trevm) = trevm.take_result_and_state(); + + let trace_res = inspector + .into_parity_builder() + .into_trace_results_with_state(&result, trace_types, trevm.inner_mut_unchecked().db_mut()) + .map_err(|e| DebugError::EvmHalt { reason: format!("state diff: {e:?}") })?; + + // Equivalent to `trevm.accept_state()`. + trevm.inner_mut_unchecked().db_mut().commit(result.state); + + Ok((trace_res, trevm)) +} + // Some code in this file has been copied and modified from reth // // The original license is included below: diff --git a/crates/rpc/src/lib.rs b/crates/rpc/src/lib.rs index 91621ea..40c5f62 100644 --- a/crates/rpc/src/lib.rs +++ b/crates/rpc/src/lib.rs @@ -25,6 +25,9 @@ pub use interest::{ChainEvent, NewBlockNotification, RemovedBlock, ReorgNotifica mod debug; pub use debug::DebugError; +mod trace; +pub use trace::TraceError; + mod signet; pub use signet::error::SignetError; @@ -34,8 +37,8 @@ mod web3; pub mod serve; pub use serve::{RpcServerGuard, ServeConfig, ServeConfigEnv, ServeError}; -/// Instantiate a combined router with `eth`, `debug`, `signet`, `web3`, and -/// `net` namespaces. +/// Instantiate a combined router with `eth`, `debug`, `trace`, `signet`, +/// `web3`, and `net` namespaces. pub fn router() -> ajj::Router> where H: signet_hot::HotKv + Send + Sync + 'static, @@ -44,6 +47,7 @@ where ajj::Router::new() .nest("eth", eth::eth()) .nest("debug", debug::debug()) + .nest("trace", trace::trace()) .nest("signet", signet::signet()) .nest("web3", web3::web3()) .nest("net", net::net()) diff --git a/crates/rpc/src/trace/endpoints.rs b/crates/rpc/src/trace/endpoints.rs new file mode 100644 index 0000000..f59faa2 --- /dev/null +++ b/crates/rpc/src/trace/endpoints.rs @@ -0,0 +1,649 @@ +//! Parity `trace` namespace RPC endpoint implementations. + +use crate::{ + config::{EvmBlockContext, StorageRpcCtx}, + eth::helpers::{CfgFiller, await_handler}, + trace::{ + TraceError, + types::{ + ReplayBlockParams, ReplayTransactionParams, TraceBlockParams, TraceCallManyParams, + TraceCallParams, TraceFilterParams, TraceGetParams, TraceRawTransactionParams, + TraceTransactionParams, + }, + }, +}; +use ajj::HandlerCtx; +use alloy::{ + consensus::BlockHeader, + eips::BlockId, + primitives::{B256, map::HashSet}, + rpc::types::trace::parity::{ + LocalizedTransactionTrace, TraceResults, TraceResultsWithTransactionHash, TraceType, + }, +}; +use signet_hot::{HotKv, model::HotKvRead}; +use signet_types::{MagicSig, constants::SignetSystemConstants}; +use tracing::Instrument; +use trevm::revm::{ + Database, DatabaseRef, + database::{DBErrorMarker, State}, + primitives::hardfork::SpecId, +}; + +/// Shared localized tracing loop for Parity `trace_block` and +/// `trace_filter`. +/// +/// Replays all transactions in a block (stopping at the first +/// magic-signature tx) and returns localized Parity traces. +#[allow(clippy::too_many_arguments)] +fn trace_block_localized( + ctx_chain_id: u64, + constants: SignetSystemConstants, + spec_id: SpecId, + header: &alloy::consensus::Header, + block_hash: B256, + txs: &[signet_storage_types::RecoveredTx], + db: State, +) -> Result, TraceError> +where + Db: Database + DatabaseRef, + ::Error: DBErrorMarker, + ::Error: DBErrorMarker, +{ + use itertools::Itertools; + + let mut evm = signet_evm::signet_evm(db, constants); + evm.set_spec_id(spec_id); + let mut trevm = evm.fill_cfg(&CfgFiller(ctx_chain_id)).fill_block(header); + + let mut all_traces = Vec::new(); + let mut txns = txs.iter().enumerate().peekable(); + for (idx, tx) in txns + .by_ref() + .peeking_take_while(|(_, t)| MagicSig::try_from_signature(t.signature()).is_none()) + { + let tx_info = alloy::rpc::types::TransactionInfo { + hash: Some(*tx.tx_hash()), + index: Some(idx as u64), + block_hash: Some(block_hash), + block_number: Some(header.number), + base_fee: header.base_fee_per_gas(), + }; + + let t = trevm.fill_tx(tx); + let (traces, next) = crate::debug::tracer::trace_parity_localized(t, tx_info) + .map_err(|e| TraceError::EvmHalt { reason: e.to_string() })?; + trevm = next; + all_traces.extend(traces); + } + + Ok(all_traces) +} + +/// Shared replay tracing loop for Parity `trace_replayBlockTransactions`. +/// +/// Replays all transactions and returns per-tx `TraceResults` with +/// the caller's `TraceType` selection. +#[allow(clippy::too_many_arguments)] +fn trace_block_replay( + ctx_chain_id: u64, + constants: SignetSystemConstants, + spec_id: SpecId, + header: &alloy::consensus::Header, + _block_hash: B256, + txs: &[signet_storage_types::RecoveredTx], + db: State, + trace_types: &HashSet, +) -> Result, TraceError> +where + Db: Database + DatabaseRef, + ::Error: DBErrorMarker, + ::Error: std::fmt::Debug + DBErrorMarker, +{ + use itertools::Itertools; + + let mut evm = signet_evm::signet_evm(db, constants); + evm.set_spec_id(spec_id); + let mut trevm = evm.fill_cfg(&CfgFiller(ctx_chain_id)).fill_block(header); + + let mut results = Vec::with_capacity(txs.len()); + let mut txns = txs.iter().enumerate().peekable(); + for (_idx, tx) in txns + .by_ref() + .peeking_take_while(|(_, t)| MagicSig::try_from_signature(t.signature()).is_none()) + { + let t = trevm.fill_tx(tx); + let (trace_res, next) = crate::debug::tracer::trace_parity_replay(t, trace_types) + .map_err(|e| TraceError::EvmHalt { reason: e.to_string() })?; + trevm = next; + + results.push(TraceResultsWithTransactionHash { + full_trace: trace_res, + transaction_hash: *tx.tx_hash(), + }); + } + + Ok(results) +} + +/// `trace_block` — return Parity traces for all transactions in a block. +pub(super) async fn trace_block( + hctx: HandlerCtx, + TraceBlockParams(id): TraceBlockParams, + ctx: StorageRpcCtx, +) -> Result>, TraceError> +where + H: HotKv + Send + Sync + 'static, + ::Error: DBErrorMarker, +{ + let _permit = ctx.acquire_tracing_permit().await; + let id = BlockId::Number(id); + let span = tracing::debug_span!("trace_block", ?id); + + let fut = async move { + let cold = ctx.cold(); + let block_num = ctx.resolve_block_id(id).map_err(|e| { + tracing::warn!(error = %e, ?id, "block resolution failed"); + TraceError::Resolve(e) + })?; + + let sealed = ctx.resolve_header(BlockId::Number(block_num.into())).map_err(|e| { + tracing::warn!(error = %e, block_num, "header resolution failed"); + TraceError::Resolve(e) + })?; + + let Some(sealed) = sealed else { + return Ok(None); + }; + + let block_hash = sealed.hash(); + let header = sealed.into_inner(); + + let txs = cold.get_transactions_in_block(block_num).await.map_err(TraceError::from)?; + + let db = + ctx.revm_state_at_height(header.number.saturating_sub(1)).map_err(TraceError::from)?; + + let spec_id = ctx.spec_id_for_header(&header); + let traces = trace_block_localized( + ctx.chain_id(), + ctx.constants().clone(), + spec_id, + &header, + block_hash, + &txs, + db, + )?; + + Ok(Some(traces)) + } + .instrument(span); + + await_handler!( + hctx.spawn(fut), + TraceError::EvmHalt { reason: "task panicked or cancelled".into() } + ) +} + +/// `trace_transaction` — return Parity traces for a single transaction. +pub(super) async fn trace_transaction( + hctx: HandlerCtx, + TraceTransactionParams(tx_hash): TraceTransactionParams, + ctx: StorageRpcCtx, +) -> Result>, TraceError> +where + H: HotKv + Send + Sync + 'static, + ::Error: DBErrorMarker, +{ + let _permit = ctx.acquire_tracing_permit().await; + let span = tracing::debug_span!("trace_transaction", %tx_hash); + + let fut = async move { + let cold = ctx.cold(); + + let confirmed = cold.get_tx_by_hash(tx_hash).await.map_err(TraceError::from)?; + + let Some(confirmed) = confirmed else { + return Ok(None); + }; + 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 = ctx.resolve_header(block_id).map_err(|e| { + tracing::warn!(error = %e, block_num, "header resolution failed"); + TraceError::Resolve(e) + })?; + let header = sealed.ok_or(TraceError::BlockNotFound(block_id))?.into_inner(); + + let txs = cold.get_transactions_in_block(block_num).await.map_err(TraceError::from)?; + + let db = ctx.revm_state_at_height(block_num.saturating_sub(1)).map_err(TraceError::from)?; + + let spec_id = ctx.spec_id_for_header(&header); + let mut evm = signet_evm::signet_evm(db, ctx.constants().clone()); + evm.set_spec_id(spec_id); + let mut trevm = evm.fill_cfg(&CfgFiller(ctx.chain_id())).fill_block(&header); + + // Replay preceding txs without tracing. + use itertools::Itertools; + 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 Ok(None); + } + trevm = trevm + .run_tx(tx) + .map_err(|e| TraceError::EvmHalt { reason: e.into_error().to_string() })? + .accept_state(); + } + + let Some((index, tx)) = txns.next() else { + return Ok(None); + }; + + let tx_info = alloy::rpc::types::TransactionInfo { + hash: Some(*tx.tx_hash()), + index: Some(index as u64), + block_hash: Some(block_hash), + block_number: Some(header.number), + base_fee: header.base_fee_per_gas(), + }; + + let trevm = trevm.fill_tx(tx); + let (traces, _) = crate::debug::tracer::trace_parity_localized(trevm, tx_info) + .map_err(|e| TraceError::EvmHalt { reason: e.to_string() })?; + + Ok(Some(traces)) + } + .instrument(span); + + await_handler!( + hctx.spawn(fut), + TraceError::EvmHalt { reason: "task panicked or cancelled".into() } + ) +} + +/// `trace_replayBlockTransactions` — replay all block txs with trace type selection. +pub(super) async fn replay_block_transactions( + hctx: HandlerCtx, + ReplayBlockParams(id, trace_types): ReplayBlockParams, + ctx: StorageRpcCtx, +) -> Result>, TraceError> +where + H: HotKv + Send + Sync + 'static, + ::Error: DBErrorMarker, +{ + let _permit = ctx.acquire_tracing_permit().await; + let id = BlockId::Number(id); + let span = tracing::debug_span!("trace_replayBlockTransactions", ?id); + + let fut = async move { + let cold = ctx.cold(); + let block_num = ctx.resolve_block_id(id).map_err(|e| { + tracing::warn!(error = %e, ?id, "block resolution failed"); + TraceError::Resolve(e) + })?; + + let sealed = + ctx.resolve_header(BlockId::Number(block_num.into())).map_err(TraceError::Resolve)?; + + let Some(sealed) = sealed else { + return Ok(None); + }; + + let block_hash = sealed.hash(); + let header = sealed.into_inner(); + + let txs = cold.get_transactions_in_block(block_num).await.map_err(TraceError::from)?; + + let db = + ctx.revm_state_at_height(header.number.saturating_sub(1)).map_err(TraceError::from)?; + + let spec_id = ctx.spec_id_for_header(&header); + let results = trace_block_replay( + ctx.chain_id(), + ctx.constants().clone(), + spec_id, + &header, + block_hash, + &txs, + db, + &trace_types, + )?; + + Ok(Some(results)) + } + .instrument(span); + + await_handler!( + hctx.spawn(fut), + TraceError::EvmHalt { reason: "task panicked or cancelled".into() } + ) +} + +/// `trace_replayTransaction` — replay a single tx with trace type selection. +pub(super) async fn replay_transaction( + hctx: HandlerCtx, + ReplayTransactionParams(tx_hash, trace_types): ReplayTransactionParams, + ctx: StorageRpcCtx, +) -> Result +where + H: HotKv + Send + Sync + 'static, + ::Error: DBErrorMarker, +{ + let _permit = ctx.acquire_tracing_permit().await; + let span = tracing::debug_span!("trace_replayTransaction", %tx_hash); + + let fut = async move { + let cold = ctx.cold(); + let confirmed = cold + .get_tx_by_hash(tx_hash) + .await + .map_err(TraceError::from)? + .ok_or(TraceError::TransactionNotFound(tx_hash))?; + + let (_tx, meta) = confirmed.into_parts(); + let block_num = meta.block_number(); + + let block_id = BlockId::Number(block_num.into()); + let sealed = ctx.resolve_header(block_id).map_err(TraceError::Resolve)?; + let header = sealed.ok_or(TraceError::BlockNotFound(block_id))?.into_inner(); + + let txs = cold.get_transactions_in_block(block_num).await.map_err(TraceError::from)?; + + let db = ctx.revm_state_at_height(block_num.saturating_sub(1)).map_err(TraceError::from)?; + + let spec_id = ctx.spec_id_for_header(&header); + let mut evm = signet_evm::signet_evm(db, ctx.constants().clone()); + evm.set_spec_id(spec_id); + let mut trevm = evm.fill_cfg(&CfgFiller(ctx.chain_id())).fill_block(&header); + + // Replay preceding txs. + use itertools::Itertools; + 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 Err(TraceError::TransactionNotFound(tx_hash)); + } + trevm = trevm + .run_tx(tx) + .map_err(|e| TraceError::EvmHalt { reason: e.into_error().to_string() })? + .accept_state(); + } + + let (_index, tx) = txns.next().ok_or(TraceError::TransactionNotFound(tx_hash))?; + + let trevm = trevm.fill_tx(tx); + let (results, _) = crate::debug::tracer::trace_parity_replay(trevm, &trace_types) + .map_err(|e| TraceError::EvmHalt { reason: e.to_string() })?; + + Ok(results) + } + .instrument(span); + + await_handler!( + hctx.spawn(fut), + TraceError::EvmHalt { reason: "task panicked or cancelled".into() } + ) +} + +/// `trace_call` — trace a call with Parity output and state overrides. +pub(super) async fn trace_call( + hctx: HandlerCtx, + TraceCallParams(request, trace_types, block_id, state_overrides, block_overrides): TraceCallParams, + ctx: StorageRpcCtx, +) -> Result +where + H: HotKv + Send + Sync + 'static, + ::Error: DBErrorMarker, +{ + let _permit = ctx.acquire_tracing_permit().await; + let id = block_id.unwrap_or(BlockId::latest()); + let span = tracing::debug_span!("trace_call", ?id); + + let fut = async move { + let EvmBlockContext { header, db, spec_id } = + ctx.resolve_evm_block(id).map_err(|e| match e { + crate::eth::EthError::BlockNotFound(id) => TraceError::BlockNotFound(id), + other => TraceError::EvmHalt { reason: other.to_string() }, + })?; + + let mut evm = signet_evm::signet_evm(db, ctx.constants().clone()); + evm.set_spec_id(spec_id); + let trevm = evm.fill_cfg(&CfgFiller(ctx.chain_id())).fill_block(&header); + + // Apply state and block overrides (matching reth trace_call). + let trevm = trevm + .maybe_apply_state_overrides(state_overrides.as_ref()) + .map_err(|e| TraceError::EvmHalt { reason: e.to_string() })? + .maybe_apply_block_overrides(block_overrides.as_deref()) + .fill_tx(&request); + + let (results, _) = crate::debug::tracer::trace_parity_replay(trevm, &trace_types) + .map_err(|e| TraceError::EvmHalt { reason: e.to_string() })?; + + Ok(results) + } + .instrument(span); + + await_handler!( + hctx.spawn(fut), + TraceError::EvmHalt { reason: "task panicked or cancelled".into() } + ) +} + +/// `trace_callMany` — trace sequential calls with accumulated state. +/// +/// Each call sees state changes from prior calls. Per-call trace +/// types. Defaults to `BlockId::pending()` (matching reth). +pub(super) async fn trace_call_many( + hctx: HandlerCtx, + TraceCallManyParams(calls, block_id): TraceCallManyParams, + ctx: StorageRpcCtx, +) -> Result, TraceError> +where + H: HotKv + Send + Sync + 'static, + ::Error: DBErrorMarker, +{ + let _permit = ctx.acquire_tracing_permit().await; + let id = block_id.unwrap_or(BlockId::pending()); + let span = tracing::debug_span!("trace_callMany", ?id, count = calls.len()); + + let fut = async move { + let EvmBlockContext { header, db, spec_id } = + ctx.resolve_evm_block(id).map_err(|e| match e { + crate::eth::EthError::BlockNotFound(id) => TraceError::BlockNotFound(id), + other => TraceError::EvmHalt { reason: other.to_string() }, + })?; + + let mut evm = signet_evm::signet_evm(db, ctx.constants().clone()); + evm.set_spec_id(spec_id); + let mut trevm = evm.fill_cfg(&CfgFiller(ctx.chain_id())).fill_block(&header); + + let mut results = Vec::with_capacity(calls.len()); + + for (request, trace_types) in calls { + let filled = trevm.fill_tx(&request); + let (trace_res, next) = crate::debug::tracer::trace_parity_replay(filled, &trace_types) + .map_err(|e| TraceError::EvmHalt { reason: e.to_string() })?; + + results.push(trace_res); + trevm = next; + } + + Ok(results) + } + .instrument(span); + + await_handler!( + hctx.spawn(fut), + TraceError::EvmHalt { reason: "task panicked or cancelled".into() } + ) +} + +/// `trace_rawTransaction` — trace a transaction from raw RLP bytes. +pub(super) async fn trace_raw_transaction( + hctx: HandlerCtx, + TraceRawTransactionParams(rlp_bytes, trace_types, block_id): TraceRawTransactionParams, + ctx: StorageRpcCtx, +) -> Result +where + H: HotKv + Send + Sync + 'static, + ::Error: DBErrorMarker, +{ + let _permit = ctx.acquire_tracing_permit().await; + let id = block_id.unwrap_or(BlockId::latest()); + let span = tracing::debug_span!("trace_rawTransaction", ?id); + + let fut = async move { + use alloy::consensus::transaction::SignerRecoverable; + + // Decode and recover sender. + let tx: signet_storage_types::TransactionSigned = + alloy::rlp::Decodable::decode(&mut rlp_bytes.as_ref()) + .map_err(|e| TraceError::RlpDecode(e.to_string()))?; + let recovered = tx.try_into_recovered().map_err(|_| TraceError::SenderRecovery)?; + + let EvmBlockContext { header, db, spec_id } = + ctx.resolve_evm_block(id).map_err(|e| match e { + crate::eth::EthError::BlockNotFound(id) => TraceError::BlockNotFound(id), + other => TraceError::EvmHalt { reason: other.to_string() }, + })?; + + let mut evm = signet_evm::signet_evm(db, ctx.constants().clone()); + evm.set_spec_id(spec_id); + let trevm = + evm.fill_cfg(&CfgFiller(ctx.chain_id())).fill_block(&header).fill_tx(&recovered); + + let (results, _) = crate::debug::tracer::trace_parity_replay(trevm, &trace_types) + .map_err(|e| TraceError::EvmHalt { reason: e.to_string() })?; + + Ok(results) + } + .instrument(span); + + await_handler!( + hctx.spawn(fut), + TraceError::EvmHalt { reason: "task panicked or cancelled".into() } + ) +} + +/// `trace_get` — get a specific trace by tx hash and index. +/// +/// Returns `None` if `indices.len() != 1` (Erigon compatibility, +/// matching reth). +pub(super) async fn trace_get( + hctx: HandlerCtx, + TraceGetParams(tx_hash, indices): TraceGetParams, + ctx: StorageRpcCtx, +) -> Result, TraceError> +where + H: HotKv + Send + Sync + 'static, + ::Error: DBErrorMarker, +{ + if indices.len() != 1 { + return Ok(None); + } + + let traces = trace_transaction(hctx, TraceTransactionParams(tx_hash), ctx).await?; + + Ok(traces.and_then(|t| t.into_iter().nth(indices[0]))) +} + +/// `trace_filter` — filter traces across a block range. +/// +/// Brute-force replay with configurable block range limit (default +/// 100 blocks). Matches reth's approach. +pub(super) async fn trace_filter( + hctx: HandlerCtx, + TraceFilterParams(filter): TraceFilterParams, + ctx: StorageRpcCtx, +) -> Result, TraceError> +where + H: HotKv + Send + Sync + 'static, + ::Error: DBErrorMarker, +{ + let _permit = ctx.acquire_tracing_permit().await; + let span = tracing::debug_span!("trace_filter"); + + let fut = async move { + let latest = ctx.tags().latest(); + let start = filter.from_block.unwrap_or(0); + let end = filter.to_block.unwrap_or(latest); + + if start > latest || end > latest { + return Err(TraceError::BlockNotFound(BlockId::latest())); + } + if start > end { + return Err(TraceError::EvmHalt { + reason: "fromBlock cannot be greater than toBlock".into(), + }); + } + + let max = ctx.config().max_trace_filter_blocks; + let distance = end.saturating_sub(start); + if distance > max { + return Err(TraceError::BlockRangeExceeded { requested: distance, max }); + } + + let matcher = filter.matcher(); + let mut all_traces = Vec::new(); + + for block_num in start..=end { + let cold = ctx.cold(); + let block_id = BlockId::Number(block_num.into()); + + let sealed = ctx.resolve_header(block_id).map_err(TraceError::Resolve)?; + + let Some(sealed) = sealed else { + continue; + }; + + let block_hash = sealed.hash(); + let header = sealed.into_inner(); + + let txs = cold.get_transactions_in_block(block_num).await.map_err(TraceError::from)?; + + let db = ctx + .revm_state_at_height(header.number.saturating_sub(1)) + .map_err(TraceError::from)?; + + let spec_id = ctx.spec_id_for_header(&header); + let mut traces = trace_block_localized( + ctx.chain_id(), + ctx.constants().clone(), + spec_id, + &header, + block_hash, + &txs, + db, + )?; + + // Apply filter matcher. + traces.retain(|t| matcher.matches(&t.trace)); + all_traces.extend(traces); + } + + // Apply pagination: skip `after`, limit `count`. + if let Some(after) = filter.after { + let after = after as usize; + if after >= all_traces.len() { + return Ok(vec![]); + } + all_traces.drain(..after); + } + if let Some(count) = filter.count { + all_traces.truncate(count as usize); + } + + Ok(all_traces) + } + .instrument(span); + + await_handler!( + hctx.spawn(fut), + TraceError::EvmHalt { reason: "task panicked or cancelled".into() } + ) +} diff --git a/crates/rpc/src/trace/error.rs b/crates/rpc/src/trace/error.rs new file mode 100644 index 0000000..2fa652f --- /dev/null +++ b/crates/rpc/src/trace/error.rs @@ -0,0 +1,115 @@ +//! Error types for the `trace` namespace. + +use alloy::{eips::BlockId, primitives::B256}; +use std::borrow::Cow; + +/// Errors that can occur in the `trace` namespace. +#[derive(Debug, thiserror::Error)] +pub enum TraceError { + /// Cold storage error. + #[error("cold storage error")] + Cold(#[from] signet_cold::ColdStorageError), + /// Hot storage error. + #[error("hot storage error")] + Hot(#[from] signet_storage::StorageError), + /// Block resolution error. + #[error("resolve: {0}")] + Resolve(crate::config::resolve::ResolveError), + /// 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: {0}")] + TransactionNotFound(B256), + /// RLP decoding failed. + #[error("RLP decode: {0}")] + RlpDecode(String), + /// Transaction sender recovery failed. + #[error("sender recovery failed")] + SenderRecovery, + /// Block range too large for trace_filter. + #[error("block range too large: {requested} blocks (max {max})")] + BlockRangeExceeded { + /// Requested range size. + requested: u64, + /// Maximum allowed range. + max: u64, + }, +} + +impl ajj::IntoErrorPayload for TraceError { + type ErrData = (); + + fn error_code(&self) -> i64 { + match self { + Self::Cold(_) | Self::Hot(_) | Self::EvmHalt { .. } | Self::SenderRecovery => -32000, + Self::Resolve(r) => crate::eth::error::resolve_error_code(r), + Self::BlockNotFound(_) | Self::TransactionNotFound(_) => -32001, + Self::RlpDecode(_) | Self::BlockRangeExceeded { .. } => -32602, + } + } + + 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::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(), + Self::RlpDecode(msg) => format!("RLP decode error: {msg}").into(), + Self::SenderRecovery => "sender recovery failed".into(), + Self::BlockRangeExceeded { requested, max } => { + format!("block range too large: {requested} blocks (max {max})").into() + } + } + } + + fn error_data(self) -> Option { + None + } +} + +#[cfg(test)] +mod tests { + use super::TraceError; + use ajj::IntoErrorPayload; + use alloy::{eips::BlockId, primitives::B256}; + + #[test] + fn cold_error_code() { + // Cold/Hot/EvmHalt/SenderRecovery all map to -32000 + let err = TraceError::SenderRecovery; + assert_eq!(err.error_code(), -32000); + } + + #[test] + fn block_not_found_code() { + let err = TraceError::BlockNotFound(BlockId::latest()); + assert_eq!(err.error_code(), -32001); + } + + #[test] + fn transaction_not_found_code() { + let err = TraceError::TransactionNotFound(B256::ZERO); + assert_eq!(err.error_code(), -32001); + } + + #[test] + fn rlp_decode_code() { + let err = TraceError::RlpDecode("bad".into()); + assert_eq!(err.error_code(), -32602); + } + + #[test] + fn block_range_exceeded_code() { + let err = TraceError::BlockRangeExceeded { requested: 200, max: 100 }; + assert_eq!(err.error_code(), -32602); + assert!(err.error_message().contains("200")); + } +} diff --git a/crates/rpc/src/trace/mod.rs b/crates/rpc/src/trace/mod.rs new file mode 100644 index 0000000..bd3e175 --- /dev/null +++ b/crates/rpc/src/trace/mod.rs @@ -0,0 +1,32 @@ +//! Parity `trace` namespace RPC router backed by storage. + +mod endpoints; +use endpoints::{ + replay_block_transactions, replay_transaction, trace_block, trace_call, trace_call_many, + trace_filter, trace_get, trace_raw_transaction, trace_transaction, +}; +mod error; +pub use error::TraceError; +mod types; + +use crate::config::StorageRpcCtx; +use signet_hot::{HotKv, model::HotKvRead}; +use trevm::revm::database::DBErrorMarker; + +/// Instantiate a `trace` API router backed by storage. +pub(crate) fn trace() -> ajj::Router> +where + H: HotKv + Send + Sync + 'static, + ::Error: DBErrorMarker, +{ + ajj::Router::new() + .route("block", trace_block::) + .route("transaction", trace_transaction::) + .route("replayBlockTransactions", replay_block_transactions::) + .route("replayTransaction", replay_transaction::) + .route("call", trace_call::) + .route("callMany", trace_call_many::) + .route("rawTransaction", trace_raw_transaction::) + .route("get", trace_get::) + .route("filter", trace_filter::) +} diff --git a/crates/rpc/src/trace/types.rs b/crates/rpc/src/trace/types.rs new file mode 100644 index 0000000..43723e9 --- /dev/null +++ b/crates/rpc/src/trace/types.rs @@ -0,0 +1,60 @@ +//! Parameter types for the `trace` namespace. + +use alloy::{ + eips::BlockId, + primitives::{B256, Bytes, map::HashSet}, + rpc::types::{ + BlockNumberOrTag, BlockOverrides, TransactionRequest, + state::StateOverride, + trace::{filter::TraceFilter, parity::TraceType}, + }, +}; + +/// Params for `trace_block`. +#[derive(Debug, serde::Deserialize)] +pub(crate) struct TraceBlockParams(pub(crate) BlockNumberOrTag); + +/// Params for `trace_transaction`. +#[derive(Debug, serde::Deserialize)] +pub(crate) struct TraceTransactionParams(pub(crate) B256); + +/// Params for `trace_replayBlockTransactions`. +#[derive(Debug, serde::Deserialize)] +pub(crate) struct ReplayBlockParams(pub(crate) BlockNumberOrTag, pub(crate) HashSet); + +/// Params for `trace_replayTransaction`. +#[derive(Debug, serde::Deserialize)] +pub(crate) struct ReplayTransactionParams(pub(crate) B256, pub(crate) HashSet); + +/// Params for `trace_call`. +#[derive(Debug, serde::Deserialize)] +pub(crate) struct TraceCallParams( + pub(crate) TransactionRequest, + pub(crate) HashSet, + #[serde(default)] pub(crate) Option, + #[serde(default)] pub(crate) Option, + #[serde(default)] pub(crate) Option>, +); + +/// Params for `trace_callMany`. +#[derive(Debug, serde::Deserialize)] +pub(crate) struct TraceCallManyParams( + pub(crate) Vec<(TransactionRequest, HashSet)>, + #[serde(default)] pub(crate) Option, +); + +/// Params for `trace_rawTransaction`. +#[derive(Debug, serde::Deserialize)] +pub(crate) struct TraceRawTransactionParams( + pub(crate) Bytes, + pub(crate) HashSet, + #[serde(default)] pub(crate) Option, +); + +/// Params for `trace_get`. +#[derive(Debug, serde::Deserialize)] +pub(crate) struct TraceGetParams(pub(crate) B256, pub(crate) Vec); + +/// Params for `trace_filter`. +#[derive(Debug, serde::Deserialize)] +pub(crate) struct TraceFilterParams(pub(crate) TraceFilter);