From f66d66c0e822d56df0b701c9c250c7f9c084be0a Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 18:18:23 -0400 Subject: [PATCH 01/12] docs: add Parity trace namespace implementation plan 12 tasks covering TraceError, param types, config, Parity tracer functions, block replay helpers, 9 trace_ endpoints, and router wiring. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-03-25-parity-trace-namespace.md | 1552 +++++++++++++++++ 1 file changed, 1552 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-25-parity-trace-namespace.md diff --git a/docs/superpowers/plans/2026-03-25-parity-trace-namespace.md b/docs/superpowers/plans/2026-03-25-parity-trace-namespace.md new file mode 100644 index 00000000..503b6073 --- /dev/null +++ b/docs/superpowers/plans/2026-03-25-parity-trace-namespace.md @@ -0,0 +1,1552 @@ +# Parity `trace_` Namespace Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a Parity/OpenEthereum `trace_` JSON-RPC namespace (9 methods) to signet-rpc for Blockscout and general tooling compatibility. + +**Architecture:** New `trace` module mirroring the `debug` module structure. Two new Parity tracer functions in `debug/tracer.rs` (shared inspector setup, different output builder). Two shared block replay helpers in `trace/endpoints.rs`. All handlers semaphore-gated. No block reward traces (Signet is post-merge L2). + +**Tech Stack:** Rust, ajj 0.7.0, alloy (parity trace types, filter types), revm-inspectors (ParityTraceBuilder, TracingInspector), trevm, signet-evm + +**Spec:** `docs/superpowers/specs/2026-03-25-parity-trace-namespace-design.md` + +**Prerequisite:** Branch off PR #120 (namespace completeness) which depends on PR #119 (structured error codes). Verify `IntoErrorPayload` exists and `response_tri!` is gone before starting. + +--- + +### Task 1: Create `TraceError` + +**Files:** +- Create: `crates/rpc/src/trace/error.rs` + +Model directly after `crates/rpc/src/debug/error.rs`. + +- [ ] **Step 1: Create the error enum with tests** + +```rust +//! 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")); + } +} +``` + +- [ ] **Step 2: Run tests** + +Run: `cargo t -p signet-rpc -- trace::error::tests` +Note: the module won't be wired yet, so you may need to add a temporary +`mod trace;` in `lib.rs` with just `mod error; pub use error::TraceError;` +to make the tests compile. Or run tests after Task 11 wires everything. + +- [ ] **Step 3: Lint and commit** + +Run: `cargo clippy -p signet-rpc --all-features --all-targets` +Run: `cargo +nightly fmt` + +```bash +git add crates/rpc/src/trace/error.rs +git commit -m "feat(rpc): add TraceError for Parity trace namespace" +``` + +--- + +### Task 2: Create param types + +**Files:** +- Create: `crates/rpc/src/trace/types.rs` + +Follow the tuple struct pattern from `debug/types.rs`. + +- [ ] **Step 1: Create the param types** + +```rust +//! Parameter types for the `trace` namespace. + +use alloy::{ + eips::BlockId, + primitives::{Bytes, B256}, + rpc::types::{ + state::StateOverride, BlockNumberOrTag, BlockOverrides, + TransactionRequest, + trace::{filter::TraceFilter, parity::TraceType}, + }, +}; +use std::collections::HashSet; + +/// 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); +``` + +Note: check whether `HashSet` deserializes correctly from +JSON arrays. The alloy `TraceType` implements `Deserialize` and `Hash`. +If `std::collections::HashSet` doesn't work, use +`alloy::primitives::map::HashSet` instead. + +- [ ] **Step 2: Lint and commit** + +```bash +git add crates/rpc/src/trace/types.rs +git commit -m "feat(rpc): add param types for Parity trace namespace" +``` + +--- + +### Task 3: Add `max_trace_filter_blocks` config + +**Files:** +- Modify: `crates/rpc/src/config/rpc_config.rs` + +- [ ] **Step 1: Add field to `StorageRpcConfig`** + +Add after the existing `max_tracing_requests` field: + +```rust +/// Maximum block range for `trace_filter` queries. +/// +/// Default: `100`. +pub max_trace_filter_blocks: u64, +``` + +- [ ] **Step 2: Add to `Default` impl** + +```rust +max_trace_filter_blocks: 100, +``` + +- [ ] **Step 3: Add to builder** + +```rust +/// 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 +} +``` + +- [ ] **Step 4: Add to `StorageRpcConfigEnv`** + +Add field with env var annotation (follow existing pattern): + +```rust +#[from_env( + var = "SIGNET_RPC_MAX_TRACE_FILTER_BLOCKS", + desc = "Maximum block range for trace_filter queries", + optional +)] +max_trace_filter_blocks: Option, +``` + +- [ ] **Step 5: Add to `From` impl** + +```rust +max_trace_filter_blocks: env + .max_trace_filter_blocks + .unwrap_or(defaults.max_trace_filter_blocks), +``` + +- [ ] **Step 6: Lint and commit** + +Run: `cargo clippy -p signet-rpc --all-features --all-targets` +Run: `cargo +nightly fmt` + +```bash +git add crates/rpc/src/config/rpc_config.rs +git commit -m "feat(rpc): add max_trace_filter_blocks config" +``` + +--- + +### Task 4: Add Parity tracer functions + +**Files:** +- Modify: `crates/rpc/src/debug/tracer.rs` + +Add two `pub(crate)` functions alongside the existing Geth tracers. + +- [ ] **Step 1: Add `trace_parity_localized`** + +Add after the existing tracer functions. This follows the exact pattern +of `trace_flat_call` (which already uses `into_parity_builder()`): + +```rust +/// Trace a transaction and return Parity-format localized traces. +/// +/// Used by `trace_block`, `trace_transaction`, `trace_get`, +/// `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())) +} +``` + +Note: check whether `TracingInspector` has +`with_transaction_gas_limit()`. If not, use +`into_parity_builder().with_transaction_gas_used(trevm.gas_used())` +instead. The existing `trace_flat_call` (line 161) shows the exact +pattern — follow it. + +- [ ] **Step 2: Add `trace_parity_replay`** + +This is the more complex function — handles `TraceType` selection and +`StateDiff` enrichment: + +```rust +/// Trace a transaction and return Parity-format `TraceResults`. +/// +/// When `StateDiff` is in `trace_types`, the state diff is enriched +/// with pre-transaction balance/nonce from the database. Requires +/// `Db: DatabaseRef` for this enrichment. +/// +/// Used by `trace_replayBlockTransactions`, `trace_call`, +/// `trace_callMany`, `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(), + })?; + + // Follow the take_result_and_state pattern from trace_pre_state + // (debug/tracer.rs line ~124). This gives us the ExecutionResult + // and state map while keeping trevm alive for DB access. + let (result, mut trevm) = trevm.take_result_and_state(); + + let mut trace_res = inspector + .into_parity_builder() + .into_trace_results(&result.result, trace_types); + + // If StateDiff was requested, enrich with pre-tx balance/nonce. + if let Some(ref mut state_diff) = trace_res.state_diff { + // populate_state_diff reads pre-tx state from db and overlays + // the committed changes. Check revm-inspectors for the exact + // import path and function signature. + revm_inspectors::tracing::builder::parity::populate_state_diff( + state_diff, + trevm.inner_mut_unchecked().db_mut(), + result.state.iter(), + ) + .map_err(|e| DebugError::EvmHalt { + reason: format!("state diff: {e:?}"), + })?; + } + + // Commit the state changes. + trevm.inner_mut_unchecked().db_mut().commit(result.state); + Ok((trace_res, trevm)) +} +``` + +**IMPORTANT:** The code uses `take_result_and_state()` which follows +the pattern from `trace_pre_state` in `debug/tracer.rs` (~line 124). +Verify the exact API during implementation: +- `take_result_and_state()` returns `(ResultAndState, EvmNeedsTx)` or similar +- `inner_mut_unchecked().db_mut()` for `&mut Db` (DatabaseRef access) +- Check `populate_state_diff` import path — may be at + `revm_inspectors::tracing::parity::populate_state_diff` or + `revm_inspectors::tracing::builder::parity::populate_state_diff` + +Build docs: `cargo doc -p revm-inspectors --no-deps` and +`cargo doc -p trevm --no-deps` to find exact paths. + +- [ ] **Step 3: Add required imports** + +At the top of `tracer.rs`, add: + +```rust +use alloy::rpc::types::trace::parity::{ + LocalizedTransactionTrace, TraceResults, TraceType, +}; +use std::collections::HashSet; +``` + +- [ ] **Step 4: Lint and commit** + +Run: `cargo clippy -p signet-rpc --all-features --all-targets` +Run: `cargo +nightly fmt` + +```bash +git add crates/rpc/src/debug/tracer.rs +git commit -m "feat(rpc): add Parity tracer functions (localized + replay)" +``` + +--- + +### Task 5: Create block replay helpers + +**Files:** +- Create: `crates/rpc/src/trace/endpoints.rs` (initial skeleton) + +These parallel `debug::trace_block_inner` but produce Parity output. + +- [ ] **Step 1: Create endpoints.rs with imports and localized helper** + +```rust +//! Parity `trace` namespace RPC endpoint implementations. + +use crate::{ + config::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, Bytes}, + rpc::types::trace::parity::{ + LocalizedTransactionTrace, TraceResults, + TraceResultsWithTransactionHash, TraceType, + }, +}; +use signet_hot::{HotKv, model::HotKvRead}; +use signet_types::{MagicSig, constants::SignetSystemConstants}; +use std::collections::HashSet; +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); + // Convert DebugError from tracer into TraceError. + (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) +} +``` + +- [ ] **Step 2: Add the replay helper** + +```rust +/// 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); + (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) +} +``` + +- [ ] **Step 3: Lint and commit** + +```bash +git add crates/rpc/src/trace/endpoints.rs +git commit -m "feat(rpc): add Parity block replay helpers" +``` + +--- + +### Task 6: Implement `trace_block` and `trace_transaction` + +**Files:** +- Modify: `crates/rpc/src/trace/endpoints.rs` + +- [ ] **Step 1: Add `trace_block` handler** + +```rust +/// `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() + } + ) +} +``` + +- [ ] **Step 2: Add `trace_transaction` handler** + +Follow the pattern of `debug::trace_transaction` — replay preceding txs +without tracing, trace only the target tx: + +```rust +/// `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() + } + ) +} +``` + +- [ ] **Step 3: Lint and commit** + +```bash +git add crates/rpc/src/trace/endpoints.rs +git commit -m "feat(rpc): add trace_block and trace_transaction" +``` + +--- + +### Task 7: Implement `trace_replayBlockTransactions` and `trace_replayTransaction` + +**Files:** +- Modify: `crates/rpc/src/trace/endpoints.rs` + +- [ ] **Step 1: Add `replay_block_transactions`** + +```rust +/// `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(|e| 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 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() + } + ) +} +``` + +- [ ] **Step 2: Add `replay_transaction`** + +This one uses `into_trace_results_with_state` (different from +`replay_block_transactions`), matching reth's divergent pattern: + +```rust +/// `trace_replayTransaction` — replay a single tx with trace type selection. +/// +/// Uses `into_trace_results_with_state` (different from +/// `replayBlockTransactions` which uses `into_trace_results` + +/// `populate_state_diff`). Matches reth's divergent pattern. +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 { + // Same tx lookup + block replay as trace_transaction, but use + // trace_parity_replay for the target tx instead of + // trace_parity_localized. + // + // HOWEVER: this handler needs into_trace_results_with_state, + // not into_trace_results + populate_state_diff. The spec notes + // this divergence. For the initial implementation, use + // trace_parity_replay which uses into_trace_results + + // populate_state_diff. If reth compatibility requires the + // exact into_trace_results_with_state path, refactor later. + // + // The practical difference is minimal — both produce correct + // state diffs, just through different internal paths. + + 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(|e| 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. + 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() + } + ) +} +``` + +- [ ] **Step 3: Lint and commit** + +```bash +git add crates/rpc/src/trace/endpoints.rs +git commit -m "feat(rpc): add trace_replayBlockTransactions and trace_replayTransaction" +``` + +--- + +### Task 8: Implement `trace_call` and `trace_callMany` + +**Files:** +- Modify: `crates/rpc/src/trace/endpoints.rs` + +- [ ] **Step 1: Add `trace_call`** + +Follows `debug_trace_call` pattern but with state/block overrides +(matching reth) and Parity output: + +```rust +/// `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 { + use crate::config::EvmBlockContext; + + 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() + } + ) +} +``` + +- [ ] **Step 2: Add `trace_call_many`** + +Sequential calls with state committed between each: + +```rust +/// `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 { + use crate::config::EvmBlockContext; + + 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()); + let mut calls = calls.into_iter().peekable(); + + while let Some((request, trace_types)) = calls.next() { + 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); + + // accept_state commits the tx's state changes so + // subsequent calls see them. + trevm = next.accept_state(); + } + + Ok(results) + } + .instrument(span); + + await_handler!( + hctx.spawn(fut), + TraceError::EvmHalt { + reason: "task panicked or cancelled".into() + } + ) +} +``` + +Note: the `accept_state()` call may or may not commit to the +underlying DB. Check trevm docs. The key requirement is that call N+1 +sees state from call N. In the debug namespace, `run_tx().accept_state()` +is used for this purpose. + +- [ ] **Step 3: Lint and commit** + +```bash +git add crates/rpc/src/trace/endpoints.rs +git commit -m "feat(rpc): add trace_call and trace_callMany" +``` + +--- + +### Task 9: Implement `trace_rawTransaction` and `trace_get` + +**Files:** +- Modify: `crates/rpc/src/trace/endpoints.rs` + +- [ ] **Step 1: Add `trace_raw_transaction`** + +```rust +/// `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; + use crate::config::EvmBlockContext; + + // 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() + } + ) +} +``` + +- [ ] **Step 2: Add `trace_get`** + +```rust +/// `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]))) +} +``` + +- [ ] **Step 3: Lint and commit** + +```bash +git add crates/rpc/src/trace/endpoints.rs +git commit -m "feat(rpc): add trace_rawTransaction and trace_get" +``` + +--- + +### Task 10: Implement `trace_filter` + +**Files:** +- Modify: `crates/rpc/src/trace/endpoints.rs` + +- [ ] **Step 1: Add `trace_filter`** + +```rust +/// `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(|e| TraceError::Resolve(e))?; + + 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() + } + ) +} +``` + +- [ ] **Step 2: Lint and commit** + +```bash +git add crates/rpc/src/trace/endpoints.rs +git commit -m "feat(rpc): add trace_filter with configurable block range limit" +``` + +--- + +### Task 11: Wire the router + +**Files:** +- Create: `crates/rpc/src/trace/mod.rs` +- Modify: `crates/rpc/src/lib.rs` + +- [ ] **Step 1: Create `trace/mod.rs`** + +```rust +//! 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::) +} +``` + +- [ ] **Step 2: Wire into `lib.rs`** + +Add `mod trace;` and `pub use trace::TraceError;` alongside the +existing module declarations. + +Add `.nest("trace", trace::trace())` to the router function. + +Update the docstring to mention the `trace` namespace. + +- [ ] **Step 3: Lint and verify** + +Run: `cargo clippy -p signet-rpc --all-features --all-targets` +Run: `cargo +nightly fmt` + +- [ ] **Step 4: Commit** + +```bash +git add crates/rpc/src/trace/mod.rs crates/rpc/src/lib.rs +git commit -m "feat(rpc): wire Parity trace namespace into router" +``` + +--- + +### Task 12: Final verification + +- [ ] **Step 1: Run all tests** + +Run: `cargo t -p signet-rpc` +Expected: All pass (existing + new TraceError tests). + +- [ ] **Step 2: Full lint** + +Run: `cargo clippy -p signet-rpc --all-features --all-targets` +Expected: Clean. + +- [ ] **Step 3: Format** + +Run: `cargo +nightly fmt` + +- [ ] **Step 4: Verify route count** + +Count `.route(` calls across all namespace modules. Expected: +eth 41 + debug 9 + trace 9 + signet 2 + web3 2 + net 2 = 65 total. + +- [ ] **Step 5: Workspace-wide lint** + +Run: `cargo clippy --all-features --all-targets` +Verify no other crates broke. + +- [ ] **Step 6: Commit any remaining fixes** + +```bash +git add -A +git commit -m "chore(rpc): final cleanup for Parity trace namespace" +``` From c0df97ea03b676d8dd96a44d87ecd8083363ef0d Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 18:24:05 -0400 Subject: [PATCH 02/12] feat(rpc): add TraceError for Parity trace namespace Co-Authored-By: Claude Sonnet 4.6 --- crates/rpc/src/trace/error.rs | 132 ++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 crates/rpc/src/trace/error.rs diff --git a/crates/rpc/src/trace/error.rs b/crates/rpc/src/trace/error.rs new file mode 100644 index 00000000..3b4983ab --- /dev/null +++ b/crates/rpc/src/trace/error.rs @@ -0,0 +1,132 @@ +//! 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")); + } +} From 53ce6421979214954ec2b11120871e5be0e19d2e Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 18:24:13 -0400 Subject: [PATCH 03/12] feat(rpc): add param types for Parity trace namespace Co-Authored-By: Claude Sonnet 4.6 --- crates/rpc/src/trace/types.rs | 75 +++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 crates/rpc/src/trace/types.rs diff --git a/crates/rpc/src/trace/types.rs b/crates/rpc/src/trace/types.rs new file mode 100644 index 00000000..8a8bc68e --- /dev/null +++ b/crates/rpc/src/trace/types.rs @@ -0,0 +1,75 @@ +//! Parameter types for the `trace` namespace. + +use alloy::{ + eips::BlockId, + primitives::{Bytes, B256}, + rpc::types::{ + state::StateOverride, BlockNumberOrTag, BlockOverrides, + TransactionRequest, + trace::{filter::TraceFilter, parity::TraceType}, + }, +}; +use std::collections::HashSet; + +/// 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); From 1af517f586ef3e695192c4591d9feb1f8f5bb4f6 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 18:24:47 -0400 Subject: [PATCH 04/12] feat(rpc): add max_trace_filter_blocks config Co-Authored-By: Claude Sonnet 4.6 --- crates/rpc/src/config/rpc_config.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/rpc/src/config/rpc_config.rs b/crates/rpc/src/config/rpc_config.rs index c9daac03..74cd2626 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), From a2ca747b469d6ec5a17e2eac613224c0e76faa05 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 18:29:10 -0400 Subject: [PATCH 05/12] feat(rpc): add Parity tracer functions (localized + replay) Co-Authored-By: Claude Sonnet 4.6 --- crates/rpc/src/debug/tracer.rs | 77 +++++++++++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 5 deletions(-) diff --git a/crates/rpc/src/debug/tracer.rs b/crates/rpc/src/debug/tracer.rs index 4165bdd8..722fbaef 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: From c245697b7e14b5b64ccb05b5c144e4976324b73a Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 18:31:28 -0400 Subject: [PATCH 06/12] feat(rpc): add Parity block replay helpers Co-Authored-By: Claude Sonnet 4.6 --- crates/rpc/src/trace/endpoints.rs | 132 ++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 crates/rpc/src/trace/endpoints.rs diff --git a/crates/rpc/src/trace/endpoints.rs b/crates/rpc/src/trace/endpoints.rs new file mode 100644 index 00000000..a05328a4 --- /dev/null +++ b/crates/rpc/src/trace/endpoints.rs @@ -0,0 +1,132 @@ +//! Parity `trace` namespace RPC endpoint implementations. + +use crate::{ + eth::helpers::CfgFiller, + trace::TraceError, +}; +use alloy::{ + consensus::BlockHeader, + primitives::{map::HashSet, B256}, + rpc::types::trace::parity::{ + LocalizedTransactionTrace, TraceResults, + TraceResultsWithTransactionHash, TraceType, + }, +}; +use signet_types::{MagicSig, constants::SignetSystemConstants}; +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) +} From 4f64e12e2b215436252bdf5daa022c2d00815750 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 18:33:32 -0400 Subject: [PATCH 07/12] feat(rpc): add trace_block and trace_transaction Implements the trace_block and trace_transaction async RPC handlers, calling the existing trace_block_localized helper for block-level and per-transaction Parity trace output. Co-Authored-By: Claude Sonnet 4.6 --- crates/rpc/src/trace/endpoints.rs | 315 +++++++++++++++++++++++++++--- 1 file changed, 286 insertions(+), 29 deletions(-) diff --git a/crates/rpc/src/trace/endpoints.rs b/crates/rpc/src/trace/endpoints.rs index a05328a4..0ed20a44 100644 --- a/crates/rpc/src/trace/endpoints.rs +++ b/crates/rpc/src/trace/endpoints.rs @@ -1,18 +1,27 @@ //! Parity `trace` namespace RPC endpoint implementations. use crate::{ - eth::helpers::CfgFiller, - trace::TraceError, + config::StorageRpcCtx, + eth::helpers::{CfgFiller, await_handler}, + trace::{ + TraceError, + types::{ + ReplayBlockParams, ReplayTransactionParams, TraceBlockParams, TraceTransactionParams, + }, + }, }; +use ajj::HandlerCtx; use alloy::{ consensus::BlockHeader, - primitives::{map::HashSet, B256}, + eips::BlockId, + primitives::{B256, map::HashSet}, rpc::types::trace::parity::{ - LocalizedTransactionTrace, TraceResults, - TraceResultsWithTransactionHash, TraceType, + 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}, @@ -43,17 +52,13 @@ where 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 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() - }) + .peeking_take_while(|(_, t)| MagicSig::try_from_signature(t.signature()).is_none()) { let tx_info = alloy::rpc::types::TransactionInfo { hash: Some(*tx.tx_hash()), @@ -64,12 +69,8 @@ where }; 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(), - })?; + 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); } @@ -101,25 +102,17 @@ where 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 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() - }) + .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(), - })?; + 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 { @@ -130,3 +123,267 @@ where 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(|e| 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 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(|e| 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. + 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() } + ) +} From 360d2b168b04884cc17bae7fab9e609dc6ef1e85 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 18:37:56 -0400 Subject: [PATCH 08/12] feat(rpc): add trace_call, trace_callMany, trace_rawTransaction, trace_get, and trace_filter Co-Authored-By: Claude Sonnet 4.6 --- crates/rpc/src/trace/endpoints.rs | 276 +++++++++++++++++++++++++++++- 1 file changed, 273 insertions(+), 3 deletions(-) diff --git a/crates/rpc/src/trace/endpoints.rs b/crates/rpc/src/trace/endpoints.rs index 0ed20a44..962c0289 100644 --- a/crates/rpc/src/trace/endpoints.rs +++ b/crates/rpc/src/trace/endpoints.rs @@ -1,12 +1,14 @@ //! Parity `trace` namespace RPC endpoint implementations. use crate::{ - config::StorageRpcCtx, + config::{EvmBlockContext, StorageRpcCtx}, eth::helpers::{CfgFiller, await_handler}, trace::{ TraceError, types::{ - ReplayBlockParams, ReplayTransactionParams, TraceBlockParams, TraceTransactionParams, + ReplayBlockParams, ReplayTransactionParams, TraceBlockParams, TraceCallManyParams, + TraceCallParams, TraceFilterParams, TraceGetParams, TraceRawTransactionParams, + TraceTransactionParams, }, }, }; @@ -14,7 +16,7 @@ use ajj::HandlerCtx; use alloy::{ consensus::BlockHeader, eips::BlockId, - primitives::{B256, map::HashSet}, + primitives::{B256, Bytes, map::HashSet}, rpc::types::trace::parity::{ LocalizedTransactionTrace, TraceResults, TraceResultsWithTransactionHash, TraceType, }, @@ -387,3 +389,271 @@ where 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.accept_state(); + } + + 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() } + ) +} From 22f0715420e788555f69a3b62c305679263f5793 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 19:12:39 -0400 Subject: [PATCH 09/12] feat(rpc): wire Parity trace namespace into router Creates trace/mod.rs with the router constructor and wires it into the combined router. Fixes HashSet type mismatch (std to alloy) and removes erroneous accept_state() call on already-needs-tx state. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/rpc/src/lib.rs | 6 ++++- crates/rpc/src/trace/endpoints.rs | 34 ++++++++++------------------ crates/rpc/src/trace/mod.rs | 32 ++++++++++++++++++++++++++ crates/rpc/src/trace/types.rs | 37 +++++++++---------------------- 4 files changed, 60 insertions(+), 49 deletions(-) create mode 100644 crates/rpc/src/trace/mod.rs diff --git a/crates/rpc/src/lib.rs b/crates/rpc/src/lib.rs index e687c916..4242cf83 100644 --- a/crates/rpc/src/lib.rs +++ b/crates/rpc/src/lib.rs @@ -25,13 +25,16 @@ 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; pub mod serve; pub use serve::{RpcServerGuard, ServeConfig, ServeConfigEnv, ServeError}; -/// Instantiate a combined router with `eth`, `debug`, and `signet` +/// Instantiate a combined router with `eth`, `debug`, `trace`, and `signet` /// namespaces. pub fn router() -> ajj::Router> where @@ -41,5 +44,6 @@ where ajj::Router::new() .nest("eth", eth::eth()) .nest("debug", debug::debug()) + .nest("trace", trace::trace()) .nest("signet", signet::signet()) } diff --git a/crates/rpc/src/trace/endpoints.rs b/crates/rpc/src/trace/endpoints.rs index 962c0289..f59faa26 100644 --- a/crates/rpc/src/trace/endpoints.rs +++ b/crates/rpc/src/trace/endpoints.rs @@ -16,7 +16,7 @@ use ajj::HandlerCtx; use alloy::{ consensus::BlockHeader, eips::BlockId, - primitives::{B256, Bytes, map::HashSet}, + primitives::{B256, map::HashSet}, rpc::types::trace::parity::{ LocalizedTransactionTrace, TraceResults, TraceResultsWithTransactionHash, TraceType, }, @@ -286,9 +286,8 @@ where TraceError::Resolve(e) })?; - let sealed = ctx - .resolve_header(BlockId::Number(block_num.into())) - .map_err(|e| 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); @@ -349,7 +348,7 @@ where let block_num = meta.block_number(); let block_id = BlockId::Number(block_num.into()); - let sealed = ctx.resolve_header(block_id).map_err(|e| TraceError::Resolve(e))?; + 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)?; @@ -467,12 +466,11 @@ where 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() })?; + 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.accept_state(); + trevm = next; } Ok(results) @@ -506,8 +504,7 @@ where 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 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 { @@ -517,10 +514,8 @@ where 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 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() })?; @@ -600,9 +595,7 @@ where let cold = ctx.cold(); let block_id = BlockId::Number(block_num.into()); - let sealed = ctx - .resolve_header(block_id) - .map_err(TraceError::Resolve)?; + let sealed = ctx.resolve_header(block_id).map_err(TraceError::Resolve)?; let Some(sealed) = sealed else { continue; @@ -611,10 +604,7 @@ where 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 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)) diff --git a/crates/rpc/src/trace/mod.rs b/crates/rpc/src/trace/mod.rs new file mode 100644 index 00000000..bd3e175a --- /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 index 8a8bc68e..43723e97 100644 --- a/crates/rpc/src/trace/types.rs +++ b/crates/rpc/src/trace/types.rs @@ -2,14 +2,13 @@ use alloy::{ eips::BlockId, - primitives::{Bytes, B256}, + primitives::{B256, Bytes, map::HashSet}, rpc::types::{ - state::StateOverride, BlockNumberOrTag, BlockOverrides, - TransactionRequest, + BlockNumberOrTag, BlockOverrides, TransactionRequest, + state::StateOverride, trace::{filter::TraceFilter, parity::TraceType}, }, }; -use std::collections::HashSet; /// Params for `trace_block`. #[derive(Debug, serde::Deserialize)] @@ -21,37 +20,27 @@ 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, -); +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, -); +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>, + #[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, + #[serde(default)] pub(crate) Option, ); /// Params for `trace_rawTransaction`. @@ -59,16 +48,12 @@ pub(crate) struct TraceCallManyParams( pub(crate) struct TraceRawTransactionParams( pub(crate) Bytes, pub(crate) HashSet, - #[serde(default)] - pub(crate) Option, + #[serde(default)] pub(crate) Option, ); /// Params for `trace_get`. #[derive(Debug, serde::Deserialize)] -pub(crate) struct TraceGetParams( - pub(crate) B256, - pub(crate) Vec, -); +pub(crate) struct TraceGetParams(pub(crate) B256, pub(crate) Vec); /// Params for `trace_filter`. #[derive(Debug, serde::Deserialize)] From 54931a9cfe2bc971bd2daf991f66a503b8fb547b Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 19:13:56 -0400 Subject: [PATCH 10/12] style(rpc): apply nightly fmt to TraceError --- crates/rpc/src/trace/error.rs | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/crates/rpc/src/trace/error.rs b/crates/rpc/src/trace/error.rs index 3b4983ab..2fa652f8 100644 --- a/crates/rpc/src/trace/error.rs +++ b/crates/rpc/src/trace/error.rs @@ -48,10 +48,7 @@ impl ajj::IntoErrorPayload for TraceError { fn error_code(&self) -> i64 { match self { - Self::Cold(_) - | Self::Hot(_) - | Self::EvmHalt { .. } - | Self::SenderRecovery => -32000, + 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, @@ -62,24 +59,13 @@ impl ajj::IntoErrorPayload for TraceError { 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::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() + format!("block range too large: {requested} blocks (max {max})").into() } } } @@ -122,10 +108,7 @@ mod tests { #[test] fn block_range_exceeded_code() { - let err = TraceError::BlockRangeExceeded { - requested: 200, - max: 100, - }; + let err = TraceError::BlockRangeExceeded { requested: 200, max: 100 }; assert_eq!(err.error_code(), -32602); assert!(err.error_message().contains("200")); } From e596c5c527370811a840977b4d0c7d014c92ed34 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 30 Mar 2026 13:43:04 -0400 Subject: [PATCH 11/12] chore: remove accidentally committed planning docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The docs/ directory is already in .gitignore — these were force-added. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-03-25-parity-trace-namespace.md | 1552 ----------------- 1 file changed, 1552 deletions(-) delete mode 100644 docs/superpowers/plans/2026-03-25-parity-trace-namespace.md diff --git a/docs/superpowers/plans/2026-03-25-parity-trace-namespace.md b/docs/superpowers/plans/2026-03-25-parity-trace-namespace.md deleted file mode 100644 index 503b6073..00000000 --- a/docs/superpowers/plans/2026-03-25-parity-trace-namespace.md +++ /dev/null @@ -1,1552 +0,0 @@ -# Parity `trace_` Namespace Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add a Parity/OpenEthereum `trace_` JSON-RPC namespace (9 methods) to signet-rpc for Blockscout and general tooling compatibility. - -**Architecture:** New `trace` module mirroring the `debug` module structure. Two new Parity tracer functions in `debug/tracer.rs` (shared inspector setup, different output builder). Two shared block replay helpers in `trace/endpoints.rs`. All handlers semaphore-gated. No block reward traces (Signet is post-merge L2). - -**Tech Stack:** Rust, ajj 0.7.0, alloy (parity trace types, filter types), revm-inspectors (ParityTraceBuilder, TracingInspector), trevm, signet-evm - -**Spec:** `docs/superpowers/specs/2026-03-25-parity-trace-namespace-design.md` - -**Prerequisite:** Branch off PR #120 (namespace completeness) which depends on PR #119 (structured error codes). Verify `IntoErrorPayload` exists and `response_tri!` is gone before starting. - ---- - -### Task 1: Create `TraceError` - -**Files:** -- Create: `crates/rpc/src/trace/error.rs` - -Model directly after `crates/rpc/src/debug/error.rs`. - -- [ ] **Step 1: Create the error enum with tests** - -```rust -//! 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")); - } -} -``` - -- [ ] **Step 2: Run tests** - -Run: `cargo t -p signet-rpc -- trace::error::tests` -Note: the module won't be wired yet, so you may need to add a temporary -`mod trace;` in `lib.rs` with just `mod error; pub use error::TraceError;` -to make the tests compile. Or run tests after Task 11 wires everything. - -- [ ] **Step 3: Lint and commit** - -Run: `cargo clippy -p signet-rpc --all-features --all-targets` -Run: `cargo +nightly fmt` - -```bash -git add crates/rpc/src/trace/error.rs -git commit -m "feat(rpc): add TraceError for Parity trace namespace" -``` - ---- - -### Task 2: Create param types - -**Files:** -- Create: `crates/rpc/src/trace/types.rs` - -Follow the tuple struct pattern from `debug/types.rs`. - -- [ ] **Step 1: Create the param types** - -```rust -//! Parameter types for the `trace` namespace. - -use alloy::{ - eips::BlockId, - primitives::{Bytes, B256}, - rpc::types::{ - state::StateOverride, BlockNumberOrTag, BlockOverrides, - TransactionRequest, - trace::{filter::TraceFilter, parity::TraceType}, - }, -}; -use std::collections::HashSet; - -/// 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); -``` - -Note: check whether `HashSet` deserializes correctly from -JSON arrays. The alloy `TraceType` implements `Deserialize` and `Hash`. -If `std::collections::HashSet` doesn't work, use -`alloy::primitives::map::HashSet` instead. - -- [ ] **Step 2: Lint and commit** - -```bash -git add crates/rpc/src/trace/types.rs -git commit -m "feat(rpc): add param types for Parity trace namespace" -``` - ---- - -### Task 3: Add `max_trace_filter_blocks` config - -**Files:** -- Modify: `crates/rpc/src/config/rpc_config.rs` - -- [ ] **Step 1: Add field to `StorageRpcConfig`** - -Add after the existing `max_tracing_requests` field: - -```rust -/// Maximum block range for `trace_filter` queries. -/// -/// Default: `100`. -pub max_trace_filter_blocks: u64, -``` - -- [ ] **Step 2: Add to `Default` impl** - -```rust -max_trace_filter_blocks: 100, -``` - -- [ ] **Step 3: Add to builder** - -```rust -/// 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 -} -``` - -- [ ] **Step 4: Add to `StorageRpcConfigEnv`** - -Add field with env var annotation (follow existing pattern): - -```rust -#[from_env( - var = "SIGNET_RPC_MAX_TRACE_FILTER_BLOCKS", - desc = "Maximum block range for trace_filter queries", - optional -)] -max_trace_filter_blocks: Option, -``` - -- [ ] **Step 5: Add to `From` impl** - -```rust -max_trace_filter_blocks: env - .max_trace_filter_blocks - .unwrap_or(defaults.max_trace_filter_blocks), -``` - -- [ ] **Step 6: Lint and commit** - -Run: `cargo clippy -p signet-rpc --all-features --all-targets` -Run: `cargo +nightly fmt` - -```bash -git add crates/rpc/src/config/rpc_config.rs -git commit -m "feat(rpc): add max_trace_filter_blocks config" -``` - ---- - -### Task 4: Add Parity tracer functions - -**Files:** -- Modify: `crates/rpc/src/debug/tracer.rs` - -Add two `pub(crate)` functions alongside the existing Geth tracers. - -- [ ] **Step 1: Add `trace_parity_localized`** - -Add after the existing tracer functions. This follows the exact pattern -of `trace_flat_call` (which already uses `into_parity_builder()`): - -```rust -/// Trace a transaction and return Parity-format localized traces. -/// -/// Used by `trace_block`, `trace_transaction`, `trace_get`, -/// `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())) -} -``` - -Note: check whether `TracingInspector` has -`with_transaction_gas_limit()`. If not, use -`into_parity_builder().with_transaction_gas_used(trevm.gas_used())` -instead. The existing `trace_flat_call` (line 161) shows the exact -pattern — follow it. - -- [ ] **Step 2: Add `trace_parity_replay`** - -This is the more complex function — handles `TraceType` selection and -`StateDiff` enrichment: - -```rust -/// Trace a transaction and return Parity-format `TraceResults`. -/// -/// When `StateDiff` is in `trace_types`, the state diff is enriched -/// with pre-transaction balance/nonce from the database. Requires -/// `Db: DatabaseRef` for this enrichment. -/// -/// Used by `trace_replayBlockTransactions`, `trace_call`, -/// `trace_callMany`, `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(), - })?; - - // Follow the take_result_and_state pattern from trace_pre_state - // (debug/tracer.rs line ~124). This gives us the ExecutionResult - // and state map while keeping trevm alive for DB access. - let (result, mut trevm) = trevm.take_result_and_state(); - - let mut trace_res = inspector - .into_parity_builder() - .into_trace_results(&result.result, trace_types); - - // If StateDiff was requested, enrich with pre-tx balance/nonce. - if let Some(ref mut state_diff) = trace_res.state_diff { - // populate_state_diff reads pre-tx state from db and overlays - // the committed changes. Check revm-inspectors for the exact - // import path and function signature. - revm_inspectors::tracing::builder::parity::populate_state_diff( - state_diff, - trevm.inner_mut_unchecked().db_mut(), - result.state.iter(), - ) - .map_err(|e| DebugError::EvmHalt { - reason: format!("state diff: {e:?}"), - })?; - } - - // Commit the state changes. - trevm.inner_mut_unchecked().db_mut().commit(result.state); - Ok((trace_res, trevm)) -} -``` - -**IMPORTANT:** The code uses `take_result_and_state()` which follows -the pattern from `trace_pre_state` in `debug/tracer.rs` (~line 124). -Verify the exact API during implementation: -- `take_result_and_state()` returns `(ResultAndState, EvmNeedsTx)` or similar -- `inner_mut_unchecked().db_mut()` for `&mut Db` (DatabaseRef access) -- Check `populate_state_diff` import path — may be at - `revm_inspectors::tracing::parity::populate_state_diff` or - `revm_inspectors::tracing::builder::parity::populate_state_diff` - -Build docs: `cargo doc -p revm-inspectors --no-deps` and -`cargo doc -p trevm --no-deps` to find exact paths. - -- [ ] **Step 3: Add required imports** - -At the top of `tracer.rs`, add: - -```rust -use alloy::rpc::types::trace::parity::{ - LocalizedTransactionTrace, TraceResults, TraceType, -}; -use std::collections::HashSet; -``` - -- [ ] **Step 4: Lint and commit** - -Run: `cargo clippy -p signet-rpc --all-features --all-targets` -Run: `cargo +nightly fmt` - -```bash -git add crates/rpc/src/debug/tracer.rs -git commit -m "feat(rpc): add Parity tracer functions (localized + replay)" -``` - ---- - -### Task 5: Create block replay helpers - -**Files:** -- Create: `crates/rpc/src/trace/endpoints.rs` (initial skeleton) - -These parallel `debug::trace_block_inner` but produce Parity output. - -- [ ] **Step 1: Create endpoints.rs with imports and localized helper** - -```rust -//! Parity `trace` namespace RPC endpoint implementations. - -use crate::{ - config::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, Bytes}, - rpc::types::trace::parity::{ - LocalizedTransactionTrace, TraceResults, - TraceResultsWithTransactionHash, TraceType, - }, -}; -use signet_hot::{HotKv, model::HotKvRead}; -use signet_types::{MagicSig, constants::SignetSystemConstants}; -use std::collections::HashSet; -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); - // Convert DebugError from tracer into TraceError. - (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) -} -``` - -- [ ] **Step 2: Add the replay helper** - -```rust -/// 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); - (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) -} -``` - -- [ ] **Step 3: Lint and commit** - -```bash -git add crates/rpc/src/trace/endpoints.rs -git commit -m "feat(rpc): add Parity block replay helpers" -``` - ---- - -### Task 6: Implement `trace_block` and `trace_transaction` - -**Files:** -- Modify: `crates/rpc/src/trace/endpoints.rs` - -- [ ] **Step 1: Add `trace_block` handler** - -```rust -/// `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() - } - ) -} -``` - -- [ ] **Step 2: Add `trace_transaction` handler** - -Follow the pattern of `debug::trace_transaction` — replay preceding txs -without tracing, trace only the target tx: - -```rust -/// `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() - } - ) -} -``` - -- [ ] **Step 3: Lint and commit** - -```bash -git add crates/rpc/src/trace/endpoints.rs -git commit -m "feat(rpc): add trace_block and trace_transaction" -``` - ---- - -### Task 7: Implement `trace_replayBlockTransactions` and `trace_replayTransaction` - -**Files:** -- Modify: `crates/rpc/src/trace/endpoints.rs` - -- [ ] **Step 1: Add `replay_block_transactions`** - -```rust -/// `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(|e| 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 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() - } - ) -} -``` - -- [ ] **Step 2: Add `replay_transaction`** - -This one uses `into_trace_results_with_state` (different from -`replay_block_transactions`), matching reth's divergent pattern: - -```rust -/// `trace_replayTransaction` — replay a single tx with trace type selection. -/// -/// Uses `into_trace_results_with_state` (different from -/// `replayBlockTransactions` which uses `into_trace_results` + -/// `populate_state_diff`). Matches reth's divergent pattern. -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 { - // Same tx lookup + block replay as trace_transaction, but use - // trace_parity_replay for the target tx instead of - // trace_parity_localized. - // - // HOWEVER: this handler needs into_trace_results_with_state, - // not into_trace_results + populate_state_diff. The spec notes - // this divergence. For the initial implementation, use - // trace_parity_replay which uses into_trace_results + - // populate_state_diff. If reth compatibility requires the - // exact into_trace_results_with_state path, refactor later. - // - // The practical difference is minimal — both produce correct - // state diffs, just through different internal paths. - - 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(|e| 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. - 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() - } - ) -} -``` - -- [ ] **Step 3: Lint and commit** - -```bash -git add crates/rpc/src/trace/endpoints.rs -git commit -m "feat(rpc): add trace_replayBlockTransactions and trace_replayTransaction" -``` - ---- - -### Task 8: Implement `trace_call` and `trace_callMany` - -**Files:** -- Modify: `crates/rpc/src/trace/endpoints.rs` - -- [ ] **Step 1: Add `trace_call`** - -Follows `debug_trace_call` pattern but with state/block overrides -(matching reth) and Parity output: - -```rust -/// `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 { - use crate::config::EvmBlockContext; - - 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() - } - ) -} -``` - -- [ ] **Step 2: Add `trace_call_many`** - -Sequential calls with state committed between each: - -```rust -/// `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 { - use crate::config::EvmBlockContext; - - 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()); - let mut calls = calls.into_iter().peekable(); - - while let Some((request, trace_types)) = calls.next() { - 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); - - // accept_state commits the tx's state changes so - // subsequent calls see them. - trevm = next.accept_state(); - } - - Ok(results) - } - .instrument(span); - - await_handler!( - hctx.spawn(fut), - TraceError::EvmHalt { - reason: "task panicked or cancelled".into() - } - ) -} -``` - -Note: the `accept_state()` call may or may not commit to the -underlying DB. Check trevm docs. The key requirement is that call N+1 -sees state from call N. In the debug namespace, `run_tx().accept_state()` -is used for this purpose. - -- [ ] **Step 3: Lint and commit** - -```bash -git add crates/rpc/src/trace/endpoints.rs -git commit -m "feat(rpc): add trace_call and trace_callMany" -``` - ---- - -### Task 9: Implement `trace_rawTransaction` and `trace_get` - -**Files:** -- Modify: `crates/rpc/src/trace/endpoints.rs` - -- [ ] **Step 1: Add `trace_raw_transaction`** - -```rust -/// `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; - use crate::config::EvmBlockContext; - - // 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() - } - ) -} -``` - -- [ ] **Step 2: Add `trace_get`** - -```rust -/// `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]))) -} -``` - -- [ ] **Step 3: Lint and commit** - -```bash -git add crates/rpc/src/trace/endpoints.rs -git commit -m "feat(rpc): add trace_rawTransaction and trace_get" -``` - ---- - -### Task 10: Implement `trace_filter` - -**Files:** -- Modify: `crates/rpc/src/trace/endpoints.rs` - -- [ ] **Step 1: Add `trace_filter`** - -```rust -/// `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(|e| TraceError::Resolve(e))?; - - 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() - } - ) -} -``` - -- [ ] **Step 2: Lint and commit** - -```bash -git add crates/rpc/src/trace/endpoints.rs -git commit -m "feat(rpc): add trace_filter with configurable block range limit" -``` - ---- - -### Task 11: Wire the router - -**Files:** -- Create: `crates/rpc/src/trace/mod.rs` -- Modify: `crates/rpc/src/lib.rs` - -- [ ] **Step 1: Create `trace/mod.rs`** - -```rust -//! 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::) -} -``` - -- [ ] **Step 2: Wire into `lib.rs`** - -Add `mod trace;` and `pub use trace::TraceError;` alongside the -existing module declarations. - -Add `.nest("trace", trace::trace())` to the router function. - -Update the docstring to mention the `trace` namespace. - -- [ ] **Step 3: Lint and verify** - -Run: `cargo clippy -p signet-rpc --all-features --all-targets` -Run: `cargo +nightly fmt` - -- [ ] **Step 4: Commit** - -```bash -git add crates/rpc/src/trace/mod.rs crates/rpc/src/lib.rs -git commit -m "feat(rpc): wire Parity trace namespace into router" -``` - ---- - -### Task 12: Final verification - -- [ ] **Step 1: Run all tests** - -Run: `cargo t -p signet-rpc` -Expected: All pass (existing + new TraceError tests). - -- [ ] **Step 2: Full lint** - -Run: `cargo clippy -p signet-rpc --all-features --all-targets` -Expected: Clean. - -- [ ] **Step 3: Format** - -Run: `cargo +nightly fmt` - -- [ ] **Step 4: Verify route count** - -Count `.route(` calls across all namespace modules. Expected: -eth 41 + debug 9 + trace 9 + signet 2 + web3 2 + net 2 = 65 total. - -- [ ] **Step 5: Workspace-wide lint** - -Run: `cargo clippy --all-features --all-targets` -Verify no other crates broke. - -- [ ] **Step 6: Commit any remaining fixes** - -```bash -git add -A -git commit -m "chore(rpc): final cleanup for Parity trace namespace" -``` From 96f0093036b09f9e1242055f440281d9d0f372f5 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 31 Mar 2026 09:05:32 -0400 Subject: [PATCH 12/12] fix(rpc): address trace namespace PR review feedback - Add [default: 100] to trace_filter config description - Move itertools import to top-level, remove inline imports - Remove unused _block_hash param from trace_block_replay - Remove unnecessary #[allow(clippy::too_many_arguments)] - Fix off-by-one in trace_filter block range distance - Add InvalidBlockRange error variant for fromBlock > toBlock - Return specific block numbers in BlockNotFound errors Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/rpc/src/config/rpc_config.rs | 2 +- crates/rpc/src/trace/endpoints.rs | 23 ++++++++--------------- crates/rpc/src/trace/error.rs | 18 +++++++++++++++++- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/crates/rpc/src/config/rpc_config.rs b/crates/rpc/src/config/rpc_config.rs index 74cd2626..13c24a9c 100644 --- a/crates/rpc/src/config/rpc_config.rs +++ b/crates/rpc/src/config/rpc_config.rs @@ -313,7 +313,7 @@ pub struct StorageRpcConfigEnv { /// Maximum block range for trace_filter queries. #[from_env( var = "SIGNET_RPC_MAX_TRACE_FILTER_BLOCKS", - desc = "Maximum block range for trace_filter queries", + desc = "Maximum block range for trace_filter queries [default: 100]", optional )] max_trace_filter_blocks: Option, diff --git a/crates/rpc/src/trace/endpoints.rs b/crates/rpc/src/trace/endpoints.rs index f59faa26..204a3c48 100644 --- a/crates/rpc/src/trace/endpoints.rs +++ b/crates/rpc/src/trace/endpoints.rs @@ -21,6 +21,7 @@ use alloy::{ LocalizedTransactionTrace, TraceResults, TraceResultsWithTransactionHash, TraceType, }, }; +use itertools::Itertools; use signet_hot::{HotKv, model::HotKvRead}; use signet_types::{MagicSig, constants::SignetSystemConstants}; use tracing::Instrument; @@ -35,7 +36,6 @@ use trevm::revm::{ /// /// 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, @@ -50,8 +50,6 @@ where ::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); @@ -84,13 +82,11 @@ where /// /// 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, @@ -100,8 +96,6 @@ where ::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); @@ -227,7 +221,6 @@ where 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() { @@ -293,7 +286,6 @@ where 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)?; @@ -307,7 +299,6 @@ where ctx.constants().clone(), spec_id, &header, - block_hash, &txs, db, &trace_types, @@ -361,7 +352,6 @@ where 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() { @@ -573,17 +563,20 @@ where 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 > latest { + return Err(TraceError::BlockNotFound(BlockId::Number(start.into()))); + } + if end > latest { + return Err(TraceError::BlockNotFound(BlockId::Number(end.into()))); } if start > end { - return Err(TraceError::EvmHalt { + return Err(TraceError::InvalidBlockRange { reason: "fromBlock cannot be greater than toBlock".into(), }); } let max = ctx.config().max_trace_filter_blocks; - let distance = end.saturating_sub(start); + let distance = end.saturating_sub(start) + 1; if distance > max { return Err(TraceError::BlockRangeExceeded { requested: distance, max }); } diff --git a/crates/rpc/src/trace/error.rs b/crates/rpc/src/trace/error.rs index 2fa652f8..8c961057 100644 --- a/crates/rpc/src/trace/error.rs +++ b/crates/rpc/src/trace/error.rs @@ -33,6 +33,12 @@ pub enum TraceError { /// Transaction sender recovery failed. #[error("sender recovery failed")] SenderRecovery, + /// Invalid block range (e.g. fromBlock > toBlock). + #[error("invalid block range: {reason}")] + InvalidBlockRange { + /// Description of the invalid range. + reason: String, + }, /// Block range too large for trace_filter. #[error("block range too large: {requested} blocks (max {max})")] BlockRangeExceeded { @@ -51,7 +57,9 @@ impl ajj::IntoErrorPayload for TraceError { 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, + Self::RlpDecode(_) + | Self::InvalidBlockRange { .. } + | Self::BlockRangeExceeded { .. } => -32602, } } @@ -63,6 +71,7 @@ impl ajj::IntoErrorPayload for TraceError { 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::InvalidBlockRange { reason } => format!("invalid block range: {reason}").into(), Self::SenderRecovery => "sender recovery failed".into(), Self::BlockRangeExceeded { requested, max } => { format!("block range too large: {requested} blocks (max {max})").into() @@ -106,6 +115,13 @@ mod tests { assert_eq!(err.error_code(), -32602); } + #[test] + fn invalid_block_range_code() { + let err = TraceError::InvalidBlockRange { reason: "bad range".into() }; + assert_eq!(err.error_code(), -32602); + assert!(err.error_message().contains("bad range")); + } + #[test] fn block_range_exceeded_code() { let err = TraceError::BlockRangeExceeded { requested: 200, max: 100 };