From d94ae12d24e4990b4bb713049e226bbe97256c8a Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 09:22:24 -0400 Subject: [PATCH 01/15] feat(rpc): add eth_protocolVersion Co-Authored-By: Claude Sonnet 4.6 --- crates/rpc/src/eth/endpoints.rs | 8 ++++++++ crates/rpc/src/eth/mod.rs | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/rpc/src/eth/endpoints.rs b/crates/rpc/src/eth/endpoints.rs index 3c9b4406..1ce6604b 100644 --- a/crates/rpc/src/eth/endpoints.rs +++ b/crates/rpc/src/eth/endpoints.rs @@ -79,6 +79,14 @@ pub(crate) async fn uncle_block() -> Result, ()> { Ok(None) } +/// `eth_protocolVersion` — returns the Ethereum wire protocol version. +/// +/// Signet does not implement devp2p. Returns a fixed value corresponding +/// to eth/68. +pub(crate) async fn protocol_version() -> Result { + Ok("0x44".to_owned()) +} + // --------------------------------------------------------------------------- // Simple Queries // --------------------------------------------------------------------------- diff --git a/crates/rpc/src/eth/mod.rs b/crates/rpc/src/eth/mod.rs index fc68c41c..89cf680b 100644 --- a/crates/rpc/src/eth/mod.rs +++ b/crates/rpc/src/eth/mod.rs @@ -4,7 +4,7 @@ mod endpoints; use endpoints::{ addr_tx_count, balance, block, block_number, block_receipts, block_tx_count, call, chain_id, code_at, create_access_list, estimate_gas, fee_history, gas_price, get_filter_changes, - get_logs, header_by, max_priority_fee_per_gas, new_block_filter, new_filter, + get_logs, header_by, max_priority_fee_per_gas, new_block_filter, new_filter, protocol_version, raw_transaction_by_block_and_index, raw_transaction_by_hash, send_raw_transaction, storage_at, subscribe, syncing, transaction_by_block_and_index, transaction_by_hash, transaction_receipt, uncle_block, uncle_count, uninstall_filter, unsubscribe, @@ -79,8 +79,9 @@ where .route("getUncleCountByBlockNumber", uncle_count) .route("getUncleByBlockHashAndIndex", uncle_block) .route("getUncleByBlockNumberAndIndex", uncle_block) + .route("protocolVersion", protocol_version) // Unsupported methods (return method_not_found by default): - // - protocolVersion, coinbase, accounts, blobBaseFee + // - coinbase, accounts, blobBaseFee // - getWork, hashrate, mining, submitHashrate, submitWork // - sendTransaction, sign, signTransaction, signTypedData // - getProof, newPendingTransactionFilter From 7a18d43acef7fd6bedddf81a6af593ee19ca0402 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 09:22:25 -0400 Subject: [PATCH 02/15] feat(rpc): add web3 namespace (clientVersion, sha3) Co-Authored-By: Claude Sonnet 4.6 --- crates/rpc/src/lib.rs | 9 +++++-- crates/rpc/src/web3/mod.rs | 50 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 crates/rpc/src/web3/mod.rs diff --git a/crates/rpc/src/lib.rs b/crates/rpc/src/lib.rs index e687c916..91621eac 100644 --- a/crates/rpc/src/lib.rs +++ b/crates/rpc/src/lib.rs @@ -28,11 +28,14 @@ pub use debug::DebugError; mod signet; pub use signet::error::SignetError; +mod net; +mod web3; + pub mod serve; pub use serve::{RpcServerGuard, ServeConfig, ServeConfigEnv, ServeError}; -/// Instantiate a combined router with `eth`, `debug`, and `signet` -/// namespaces. +/// Instantiate a combined router with `eth`, `debug`, `signet`, `web3`, and +/// `net` namespaces. pub fn router() -> ajj::Router> where H: signet_hot::HotKv + Send + Sync + 'static, @@ -42,4 +45,6 @@ where .nest("eth", eth::eth()) .nest("debug", debug::debug()) .nest("signet", signet::signet()) + .nest("web3", web3::web3()) + .nest("net", net::net()) } diff --git a/crates/rpc/src/web3/mod.rs b/crates/rpc/src/web3/mod.rs new file mode 100644 index 00000000..f5e68cd0 --- /dev/null +++ b/crates/rpc/src/web3/mod.rs @@ -0,0 +1,50 @@ +//! `web3` namespace RPC handlers. + +use crate::config::StorageRpcCtx; +use alloy::primitives::{B256, Bytes, keccak256}; +use signet_hot::{HotKv, model::HotKvRead}; +use trevm::revm::database::DBErrorMarker; + +/// Instantiate the `web3` API router. +pub(crate) fn web3() -> ajj::Router> +where + H: HotKv + Send + Sync + 'static, + ::Error: DBErrorMarker, +{ + ajj::Router::new().route("clientVersion", client_version).route("sha3", sha3) +} + +/// `web3_clientVersion` — returns the signet client version string. +pub(crate) async fn client_version() -> Result { + Ok(format!("signet/v{}/{}", env!("CARGO_PKG_VERSION"), std::env::consts::OS,)) +} + +/// `web3_sha3` — returns the keccak256 hash of the given data. +pub(crate) async fn sha3((data,): (Bytes,)) -> Result { + Ok(keccak256(&data)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn client_version_format() { + let version = client_version().await.unwrap(); + assert!(version.starts_with("signet/v"), "got: {version}"); + assert!(version.contains('/'), "expected platform suffix, got: {version}"); + } + + #[tokio::test] + async fn sha3_empty_input() { + let result = sha3((Bytes::new(),)).await.unwrap(); + assert_eq!(result, keccak256(b"")); + } + + #[tokio::test] + async fn sha3_nonempty_input() { + let input = Bytes::from_static(b"hello"); + let result = sha3((input.clone(),)).await.unwrap(); + assert_eq!(result, keccak256(&input)); + } +} From 2ac55142cb57b58015e8320e4ba224a163d85975 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 09:22:30 -0400 Subject: [PATCH 03/15] feat(rpc): add net namespace (version, listening) Co-Authored-By: Claude Sonnet 4.6 --- crates/rpc/src/net/mod.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 crates/rpc/src/net/mod.rs diff --git a/crates/rpc/src/net/mod.rs b/crates/rpc/src/net/mod.rs new file mode 100644 index 00000000..44d45f02 --- /dev/null +++ b/crates/rpc/src/net/mod.rs @@ -0,0 +1,24 @@ +//! `net` namespace RPC handlers. + +use crate::config::StorageRpcCtx; +use signet_hot::{HotKv, model::HotKvRead}; +use trevm::revm::database::DBErrorMarker; + +/// Instantiate the `net` API router. +pub(crate) fn net() -> ajj::Router> +where + H: HotKv + Send + Sync + 'static, + ::Error: DBErrorMarker, +{ + ajj::Router::new().route("version", version::).route("listening", listening) +} + +/// `net_version` — returns the chain ID as a decimal string. +pub(crate) async fn version(ctx: StorageRpcCtx) -> Result { + Ok(ctx.chain_id().to_string()) +} + +/// `net_listening` — always returns true (the server is listening). +pub(crate) async fn listening() -> Result { + Ok(true) +} From 318de839708da3a7cabd1493bc5b72f1738c18e3 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 09:24:10 -0400 Subject: [PATCH 04/15] feat(rpc): extend DebugError with RlpDecode and SenderRecovery variants Co-Authored-By: Claude Sonnet 4.6 --- crates/rpc/src/debug/error.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/rpc/src/debug/error.rs b/crates/rpc/src/debug/error.rs index 7b2e6d63..78bd3722 100644 --- a/crates/rpc/src/debug/error.rs +++ b/crates/rpc/src/debug/error.rs @@ -36,6 +36,12 @@ pub enum DebugError { /// Internal server error. #[error("{0}")] Internal(String), + /// RLP decoding failed (malformed input). + #[error("RLP decode: {0}")] + RlpDecode(String), + /// Transaction sender recovery failed. + #[error("sender recovery failed")] + SenderRecovery, } impl ajj::IntoErrorPayload for DebugError { @@ -48,6 +54,8 @@ impl ajj::IntoErrorPayload for DebugError { Self::InvalidTracerConfig => -32602, Self::Unsupported(_) => -32601, Self::BlockNotFound(_) | Self::TransactionNotFound(_) => -32001, + Self::RlpDecode(_) => -32602, + Self::SenderRecovery => -32000, } } @@ -61,6 +69,8 @@ impl ajj::IntoErrorPayload for DebugError { 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(), } } @@ -130,4 +140,17 @@ mod tests { assert_eq!(err.error_code(), -32000); assert!(err.error_message().contains("task panicked")); } + + #[test] + fn rlp_decode_error_code() { + let err = DebugError::RlpDecode("invalid block RLP".into()); + assert_eq!(err.error_code(), -32602); + assert!(err.error_message().contains("RLP")); + } + + #[test] + fn sender_recovery_error_code() { + let err = DebugError::SenderRecovery; + assert_eq!(err.error_code(), -32000); + } } From 84201ceccc67b82317a6ecba392771760fe77896 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 09:44:48 -0400 Subject: [PATCH 05/15] feat(rpc): add debug_getRawHeader and debug_getRawTransaction Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/rpc/src/debug/endpoints.rs | 80 ++++++++++++++++++++++++++++++- crates/rpc/src/debug/mod.rs | 4 +- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/crates/rpc/src/debug/endpoints.rs b/crates/rpc/src/debug/endpoints.rs index a049df65..861162d0 100644 --- a/crates/rpc/src/debug/endpoints.rs +++ b/crates/rpc/src/debug/endpoints.rs @@ -11,7 +11,8 @@ use crate::{ use ajj::HandlerCtx; use alloy::{ consensus::BlockHeader, - eips::BlockId, + eips::{BlockId, eip2718::Encodable2718}, + primitives::{B256, Bytes}, rpc::types::trace::geth::{GethTrace, TraceResult}, }; use itertools::Itertools; @@ -198,3 +199,80 @@ where await_handler!(hctx.spawn(fut), DebugError::Internal("task panicked or cancelled".into())) } + +/// `debug_getRawHeader` handler. +/// +/// Resolves the given [`BlockId`] and returns the RLP-encoded block header. +pub(super) async fn get_raw_header( + hctx: HandlerCtx, + (id,): (BlockId,), + ctx: StorageRpcCtx, +) -> Result +where + H: HotKv + Send + Sync + 'static, + ::Error: DBErrorMarker, +{ + let span = tracing::debug_span!("getRawHeader", ?id); + + let fut = async move { + let block_num = ctx.resolve_block_id(id).map_err(|e| { + tracing::warn!(error = %e, ?id, "block resolution failed"); + DebugError::Resolve(e) + })?; + + let sealed = ctx.resolve_header(BlockId::Number(block_num.into())).map_err(|e| { + tracing::warn!(error = %e, block_num, "header resolution failed"); + DebugError::BlockNotFound(id) + })?; + + let Some(sealed) = sealed else { + return Err(DebugError::BlockNotFound(id)); + }; + + let header = sealed.into_inner(); + Ok(Bytes::from(alloy::rlp::encode(&header))) + } + .instrument(span); + + await_handler!( + hctx.spawn(fut), + DebugError::EvmHalt { reason: "task panicked or cancelled".into() } + ) +} + +/// `debug_getRawTransaction` handler. +/// +/// Fetches the transaction by hash from cold storage and returns the +/// EIP-2718 encoded bytes. +pub(super) async fn get_raw_transaction( + hctx: HandlerCtx, + (hash,): (B256,), + ctx: StorageRpcCtx, +) -> Result +where + H: HotKv + Send + Sync + 'static, + ::Error: DBErrorMarker, +{ + let span = tracing::debug_span!("getRawTransaction", %hash); + + let fut = async move { + let confirmed = ctx + .cold() + .get_tx_by_hash(hash) + .await + .map_err(|e| { + tracing::warn!(error = %e, %hash, "cold storage read failed"); + DebugError::from(e) + })? + .ok_or(DebugError::TransactionNotFound(hash))?; + + let tx = confirmed.into_inner().into_inner(); + Ok(Bytes::from(tx.encoded_2718())) + } + .instrument(span); + + await_handler!( + hctx.spawn(fut), + DebugError::EvmHalt { reason: "task panicked or cancelled".into() } + ) +} diff --git a/crates/rpc/src/debug/mod.rs b/crates/rpc/src/debug/mod.rs index 11e373d0..f549ad8b 100644 --- a/crates/rpc/src/debug/mod.rs +++ b/crates/rpc/src/debug/mod.rs @@ -1,7 +1,7 @@ //! Debug namespace RPC router backed by storage. mod endpoints; -use endpoints::{trace_block, trace_transaction}; +use endpoints::{get_raw_header, get_raw_transaction, trace_block, trace_transaction}; mod error; pub use error::DebugError; pub(crate) mod tracer; @@ -22,4 +22,6 @@ where .route("traceBlockByNumber", trace_block::) .route("traceBlockByHash", trace_block::) .route("traceTransaction", trace_transaction::) + .route("getRawHeader", get_raw_header::) + .route("getRawTransaction", get_raw_transaction::) } From eb53770af16823f057a90c64de9547ad446f9eb1 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 09:52:49 -0400 Subject: [PATCH 06/15] feat(rpc): add debug_getRawBlock and debug_getRawReceipts Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/rpc/src/debug/endpoints.rs | 120 +++++++++++++++++++++++++++++- crates/rpc/src/debug/mod.rs | 7 +- 2 files changed, 124 insertions(+), 3 deletions(-) diff --git a/crates/rpc/src/debug/endpoints.rs b/crates/rpc/src/debug/endpoints.rs index 861162d0..cabed71f 100644 --- a/crates/rpc/src/debug/endpoints.rs +++ b/crates/rpc/src/debug/endpoints.rs @@ -10,9 +10,9 @@ use crate::{ }; use ajj::HandlerCtx; use alloy::{ - consensus::BlockHeader, + consensus::{BlockHeader, Receipt, ReceiptEnvelope, ReceiptWithBloom, TxReceipt}, eips::{BlockId, eip2718::Encodable2718}, - primitives::{B256, Bytes}, + primitives::{B256, Bytes, Log}, rpc::types::trace::geth::{GethTrace, TraceResult}, }; use itertools::Itertools; @@ -200,6 +200,122 @@ where await_handler!(hctx.spawn(fut), DebugError::Internal("task panicked or cancelled".into())) } +/// `debug_getRawBlock` handler. +/// +/// Resolves the given [`BlockId`], fetches header and transactions from cold +/// storage, assembles them into an [`alloy::consensus::Block`], and returns +/// the RLP-encoded bytes. +pub(super) async fn get_raw_block( + hctx: HandlerCtx, + (id,): (BlockId,), + ctx: StorageRpcCtx, +) -> Result +where + H: HotKv + Send + Sync + 'static, + ::Error: DBErrorMarker, +{ + let span = tracing::debug_span!("getRawBlock", ?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"); + DebugError::Resolve(e) + })?; + + let sealed = ctx.resolve_header(BlockId::Number(block_num.into())).map_err(|e| { + tracing::warn!(error = %e, block_num, "header resolution failed"); + DebugError::BlockNotFound(id) + })?; + + let Some(sealed) = sealed else { + return Err(DebugError::BlockNotFound(id)); + }; + + let txs = cold.get_transactions_in_block(block_num).await.map_err(|e| { + tracing::warn!(error = %e, block_num, "cold storage read failed"); + DebugError::from(e) + })?; + + let header = sealed.into_inner(); + let tx_bodies: Vec<_> = txs.into_iter().map(|tx| tx.into_inner()).collect(); + let block = alloy::consensus::Block { + header, + body: alloy::consensus::BlockBody { + transactions: tx_bodies, + ommers: vec![], + withdrawals: None, + }, + }; + + Ok(Bytes::from(alloy::rlp::encode(&block))) + } + .instrument(span); + + await_handler!( + hctx.spawn(fut), + DebugError::EvmHalt { reason: "task panicked or cancelled".into() } + ) +} + +/// `debug_getRawReceipts` handler. +/// +/// Fetches all receipts for the given [`BlockId`] and returns a list of +/// EIP-2718 encoded consensus receipt envelopes (one per transaction). +pub(super) async fn get_raw_receipts( + hctx: HandlerCtx, + (id,): (BlockId,), + ctx: StorageRpcCtx, +) -> Result, DebugError> +where + H: HotKv + Send + Sync + 'static, + ::Error: DBErrorMarker, +{ + let span = tracing::debug_span!("getRawReceipts", ?id); + + let fut = async move { + let block_num = ctx.resolve_block_id(id).map_err(|e| { + tracing::warn!(error = %e, ?id, "block resolution failed"); + DebugError::Resolve(e) + })?; + + let receipts = ctx.cold().get_receipts_in_block(block_num).await.map_err(|e| { + tracing::warn!(error = %e, block_num, "cold storage read failed"); + DebugError::from(e) + })?; + + let encoded = receipts + .into_iter() + .map(|cr| { + let logs_bloom = cr.receipt.bloom(); + let logs: Vec = cr.receipt.logs.into_iter().map(|l| l.inner).collect(); + let receipt = Receipt { + status: cr.receipt.status, + cumulative_gas_used: cr.receipt.cumulative_gas_used, + logs, + }; + let rwb = ReceiptWithBloom { receipt, logs_bloom }; + let envelope: ReceiptEnvelope = match cr.tx_type { + alloy::consensus::TxType::Legacy => ReceiptEnvelope::Legacy(rwb), + alloy::consensus::TxType::Eip2930 => ReceiptEnvelope::Eip2930(rwb), + alloy::consensus::TxType::Eip1559 => ReceiptEnvelope::Eip1559(rwb), + alloy::consensus::TxType::Eip4844 => ReceiptEnvelope::Eip4844(rwb), + alloy::consensus::TxType::Eip7702 => ReceiptEnvelope::Eip7702(rwb), + }; + Bytes::from(envelope.encoded_2718()) + }) + .collect(); + + Ok(encoded) + } + .instrument(span); + + await_handler!( + hctx.spawn(fut), + DebugError::EvmHalt { reason: "task panicked or cancelled".into() } + ) +} + /// `debug_getRawHeader` handler. /// /// Resolves the given [`BlockId`] and returns the RLP-encoded block header. diff --git a/crates/rpc/src/debug/mod.rs b/crates/rpc/src/debug/mod.rs index f549ad8b..687206b6 100644 --- a/crates/rpc/src/debug/mod.rs +++ b/crates/rpc/src/debug/mod.rs @@ -1,7 +1,10 @@ //! Debug namespace RPC router backed by storage. mod endpoints; -use endpoints::{get_raw_header, get_raw_transaction, trace_block, trace_transaction}; +use endpoints::{ + get_raw_block, get_raw_header, get_raw_receipts, get_raw_transaction, trace_block, + trace_transaction, +}; mod error; pub use error::DebugError; pub(crate) mod tracer; @@ -22,6 +25,8 @@ where .route("traceBlockByNumber", trace_block::) .route("traceBlockByHash", trace_block::) .route("traceTransaction", trace_transaction::) + .route("getRawBlock", get_raw_block::) .route("getRawHeader", get_raw_header::) + .route("getRawReceipts", get_raw_receipts::) .route("getRawTransaction", get_raw_transaction::) } From 2c34ed827bb7fab99654dbf016c9f192ee4e3a2a Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 12:06:00 -0400 Subject: [PATCH 07/15] refactor(rpc): extract shared tracing loop from trace_block Extract trace_block_inner() so it can be reused by the new debug_traceBlock (raw RLP) handler. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/rpc/src/debug/endpoints.rs | 98 +++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 31 deletions(-) diff --git a/crates/rpc/src/debug/endpoints.rs b/crates/rpc/src/debug/endpoints.rs index cabed71f..c038666e 100644 --- a/crates/rpc/src/debug/endpoints.rs +++ b/crates/rpc/src/debug/endpoints.rs @@ -13,13 +13,67 @@ use alloy::{ consensus::{BlockHeader, Receipt, ReceiptEnvelope, ReceiptWithBloom, TxReceipt}, eips::{BlockId, eip2718::Encodable2718}, primitives::{B256, Bytes, Log}, - rpc::types::trace::geth::{GethTrace, TraceResult}, + rpc::types::trace::geth::{GethDebugTracingOptions, GethTrace, TraceResult}, }; use itertools::Itertools; use signet_hot::{HotKv, model::HotKvRead}; -use signet_types::MagicSig; +use signet_types::{MagicSig, constants::SignetSystemConstants}; use tracing::Instrument; -use trevm::revm::database::DBErrorMarker; +use trevm::revm::{ + Database, DatabaseRef, + database::{DBErrorMarker, State}, + primitives::hardfork::SpecId, +}; + +/// Shared tracing loop used by block-level debug handlers. +/// +/// Sets up the EVM from pre-resolved components, iterates through +/// transactions (stopping at the first magic-signature tx), and traces +/// each one according to the provided [`GethDebugTracingOptions`]. +#[allow(clippy::too_many_arguments)] +fn trace_block_inner( + ctx_chain_id: u64, + constants: SignetSystemConstants, + spec_id: SpecId, + header: &alloy::consensus::Header, + block_hash: B256, + txs: &[signet_storage_types::RecoveredTx], + db: State, + opts: &GethDebugTracingOptions, +) -> Result, DebugError> +where + Db: Database + DatabaseRef, + ::Error: DBErrorMarker, + ::Error: DBErrorMarker, +{ + 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 frames = 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 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 frame; + (frame, trevm) = crate::debug::tracer::trace(t, opts, tx_info)?; + frames.push(TraceResult::Success { result: frame, tx_hash: Some(*tx.tx_hash()) }); + + tracing::debug!(tx_index = idx, tx_hash = ?tx.tx_hash(), "Traced transaction"); + } + + Ok(frames) +} /// `debug_traceBlockByNumber` and `debug_traceBlockByHash` handler. pub(super) async fn trace_block( @@ -68,8 +122,6 @@ where tracing::debug!(number = header.number, "Loaded block"); - let mut frames = Vec::with_capacity(txs.len()); - // State BEFORE this block. let db = ctx.revm_state_at_height(header.number.saturating_sub(1)).map_err(|e| { tracing::warn!(error = %e, block_num, "hot storage read failed"); @@ -77,32 +129,16 @@ where })?; 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); - - 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 frame; - (frame, trevm) = crate::debug::tracer::trace(t, &opts, tx_info)?; - frames.push(TraceResult::Success { result: frame, tx_hash: Some(*tx.tx_hash()) }); - - tracing::debug!(tx_index = idx, tx_hash = ?tx.tx_hash(), "Traced transaction"); - } - - Ok(frames) + trace_block_inner( + ctx.chain_id(), + ctx.constants().clone(), + spec_id, + &header, + block_hash, + &txs, + db, + &opts, + ) } .instrument(span); From 88ee2b909f819451f009e1e4c1a2b70a1e5c55df Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 12:08:56 -0400 Subject: [PATCH 08/15] feat(rpc): add debug_traceBlock (raw RLP input) Decodes an RLP-encoded block, recovers transaction senders, and traces all transactions using the shared trace_block_inner loop. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/rpc/src/debug/endpoints.rs | 60 ++++++++++++++++++++++++++++++- crates/rpc/src/debug/mod.rs | 3 +- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/crates/rpc/src/debug/endpoints.rs b/crates/rpc/src/debug/endpoints.rs index c038666e..01df33a4 100644 --- a/crates/rpc/src/debug/endpoints.rs +++ b/crates/rpc/src/debug/endpoints.rs @@ -10,7 +10,10 @@ use crate::{ }; use ajj::HandlerCtx; use alloy::{ - consensus::{BlockHeader, Receipt, ReceiptEnvelope, ReceiptWithBloom, TxReceipt}, + consensus::{ + BlockHeader, Receipt, ReceiptEnvelope, ReceiptWithBloom, TxReceipt, + transaction::SignerRecoverable, + }, eips::{BlockId, eip2718::Encodable2718}, primitives::{B256, Bytes, Log}, rpc::types::trace::geth::{GethDebugTracingOptions, GethTrace, TraceResult}, @@ -236,6 +239,61 @@ where await_handler!(hctx.spawn(fut), DebugError::Internal("task panicked or cancelled".into())) } +/// `debug_traceBlock` — trace all transactions in a raw RLP-encoded block. +pub(super) async fn trace_block_rlp( + hctx: HandlerCtx, + (rlp_bytes, opts): (Bytes, Option), + ctx: StorageRpcCtx, +) -> Result, DebugError> +where + H: HotKv + Send + Sync + 'static, + ::Error: DBErrorMarker, +{ + let opts = opts.ok_or(DebugError::InvalidTracerConfig)?; + let _permit = ctx.acquire_tracing_permit().await; + + let span = tracing::debug_span!("traceBlock(RLP)", bytes_len = rlp_bytes.len()); + + let fut = async move { + let block: alloy::consensus::Block = + alloy::rlp::Decodable::decode(&mut rlp_bytes.as_ref()) + .map_err(|e| DebugError::RlpDecode(e.to_string()))?; + + let block_hash = block.header.hash_slow(); + + let txs = block + .body + .transactions + .into_iter() + .map(|tx| tx.try_into_recovered().map_err(|_| DebugError::SenderRecovery)) + .collect::, _>>()?; + + let db = ctx.revm_state_at_height(block.header.number.saturating_sub(1)).map_err(|e| { + tracing::warn!(error = %e, number = block.header.number, "hot storage read failed"); + DebugError::from(e) + })?; + + let spec_id = ctx.spec_id_for_header(&block.header); + + trace_block_inner( + ctx.chain_id(), + ctx.constants().clone(), + spec_id, + &block.header, + block_hash, + &txs, + db, + &opts, + ) + } + .instrument(span); + + await_handler!( + hctx.spawn(fut), + DebugError::EvmHalt { reason: "task panicked or cancelled".into() } + ) +} + /// `debug_getRawBlock` handler. /// /// Resolves the given [`BlockId`], fetches header and transactions from cold diff --git a/crates/rpc/src/debug/mod.rs b/crates/rpc/src/debug/mod.rs index 687206b6..9207d26d 100644 --- a/crates/rpc/src/debug/mod.rs +++ b/crates/rpc/src/debug/mod.rs @@ -3,7 +3,7 @@ mod endpoints; use endpoints::{ get_raw_block, get_raw_header, get_raw_receipts, get_raw_transaction, trace_block, - trace_transaction, + trace_block_rlp, trace_transaction, }; mod error; pub use error::DebugError; @@ -25,6 +25,7 @@ where .route("traceBlockByNumber", trace_block::) .route("traceBlockByHash", trace_block::) .route("traceTransaction", trace_transaction::) + .route("traceBlock", trace_block_rlp::) .route("getRawBlock", get_raw_block::) .route("getRawHeader", get_raw_header::) .route("getRawReceipts", get_raw_receipts::) From b8f3022b5179ed1137b7a75558b5a3a8c5de0935 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 12:10:38 -0400 Subject: [PATCH 09/15] feat(rpc): add debug_traceCall Co-Authored-By: Claude Sonnet 4.6 --- crates/rpc/src/debug/endpoints.rs | 60 +++++++++++++++++++++++++++++++ crates/rpc/src/debug/mod.rs | 5 +-- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/crates/rpc/src/debug/endpoints.rs b/crates/rpc/src/debug/endpoints.rs index 01df33a4..f13e948e 100644 --- a/crates/rpc/src/debug/endpoints.rs +++ b/crates/rpc/src/debug/endpoints.rs @@ -450,6 +450,66 @@ where ) } +/// `debug_traceCall` — trace a call without submitting a transaction. +/// +/// Resolves EVM state at the target block, prepares the transaction +/// from a [`alloy::rpc::types::TransactionRequest`], then routes through +/// the tracer. State overrides are not supported in this initial +/// implementation. +pub(super) async fn debug_trace_call( + hctx: HandlerCtx, + (request, block_id, opts): ( + alloy::rpc::types::TransactionRequest, + Option, + Option, + ), + ctx: StorageRpcCtx, +) -> Result +where + H: HotKv + Send + Sync + 'static, + ::Error: DBErrorMarker, +{ + let opts = opts.ok_or(DebugError::InvalidTracerConfig)?; + let _permit = ctx.acquire_tracing_permit().await; + + let id = block_id.unwrap_or(BlockId::latest()); + let span = tracing::debug_span!("traceCall", ?id, tracer = ?opts.tracer.as_ref()); + + 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) => DebugError::BlockNotFound(id), + other => DebugError::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); + + let trevm = trevm.fill_tx(&request); + + let tx_info = alloy::rpc::types::TransactionInfo { + hash: None, + index: None, + block_hash: None, + block_number: Some(header.number), + base_fee: header.base_fee_per_gas(), + }; + + let res = crate::debug::tracer::trace(trevm, &opts, tx_info)?.0; + + Ok(res) + } + .instrument(span); + + await_handler!( + hctx.spawn(fut), + DebugError::EvmHalt { reason: "task panicked or cancelled".into() } + ) +} + /// `debug_getRawTransaction` handler. /// /// Fetches the transaction by hash from cold storage and returns the diff --git a/crates/rpc/src/debug/mod.rs b/crates/rpc/src/debug/mod.rs index 9207d26d..5825bfa9 100644 --- a/crates/rpc/src/debug/mod.rs +++ b/crates/rpc/src/debug/mod.rs @@ -2,8 +2,8 @@ mod endpoints; use endpoints::{ - get_raw_block, get_raw_header, get_raw_receipts, get_raw_transaction, trace_block, - trace_block_rlp, trace_transaction, + debug_trace_call, get_raw_block, get_raw_header, get_raw_receipts, get_raw_transaction, + trace_block, trace_block_rlp, trace_transaction, }; mod error; pub use error::DebugError; @@ -30,4 +30,5 @@ where .route("getRawHeader", get_raw_header::) .route("getRawReceipts", get_raw_receipts::) .route("getRawTransaction", get_raw_transaction::) + .route("traceCall", debug_trace_call::) } From ef0f5c74fce1c98867d25412db8b888d43f4f03d Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 12:12:09 -0400 Subject: [PATCH 10/15] =?UTF-8?q?test(rpc):=20update=20test=5Fnot=5Fsuppor?= =?UTF-8?q?ted=20=E2=86=92=20test=5Fprotocol=5Fversion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit eth_protocolVersion is now implemented, so the test that expected method_not_found is updated to assert the "0x44" response. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/rpc/tests/eth_rpc.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/rpc/tests/eth_rpc.rs b/crates/rpc/tests/eth_rpc.rs index 81e386b1..c4bff04a 100644 --- a/crates/rpc/tests/eth_rpc.rs +++ b/crates/rpc/tests/eth_rpc.rs @@ -598,12 +598,10 @@ async fn test_get_logs_empty() { // --------------------------------------------------------------------------- #[tokio::test] -async fn test_not_supported() { +async fn test_protocol_version() { let h = TestHarness::new(0).await; let resp = rpc_call_raw(&h.app, "eth_protocolVersion", json!([])).await; - assert!(resp.get("error").is_some()); - let msg = resp["error"]["message"].as_str().unwrap(); - assert!(msg.contains("not found"), "unexpected error: {msg}"); + assert_eq!(resp["result"].as_str().unwrap(), "0x44"); } #[tokio::test] From a20c735a2946154402874f9448061afbe524efb6 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 14:34:11 -0400 Subject: [PATCH 11/15] fix(rpc): address review feedback on debug endpoints - Document partial-move pattern in get_raw_receipts - Consolidate duplicate -32602 error code arms in DebugError - Fix resolve_header error mapping: use Resolve(e) instead of BlockNotFound(id) to preserve the distinction between "block not found" (-32001) and "storage layer failed" (-32000). The resolve_error_message helper already sanitizes DB errors to the opaque "server error" string, so no internals are leaked. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/rpc/src/debug/endpoints.rs | 7 +++++-- crates/rpc/src/debug/error.rs | 3 +-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/rpc/src/debug/endpoints.rs b/crates/rpc/src/debug/endpoints.rs index f13e948e..75b998f3 100644 --- a/crates/rpc/src/debug/endpoints.rs +++ b/crates/rpc/src/debug/endpoints.rs @@ -319,7 +319,7 @@ where let sealed = ctx.resolve_header(BlockId::Number(block_num.into())).map_err(|e| { tracing::warn!(error = %e, block_num, "header resolution failed"); - DebugError::BlockNotFound(id) + DebugError::Resolve(e) })?; let Some(sealed) = sealed else { @@ -381,6 +381,9 @@ where let encoded = receipts .into_iter() .map(|cr| { + // Compute bloom before moving logs out. `status` and + // `cumulative_gas_used` are Copy, so they remain + // accessible after the partial move of `logs`. let logs_bloom = cr.receipt.bloom(); let logs: Vec = cr.receipt.logs.into_iter().map(|l| l.inner).collect(); let receipt = Receipt { @@ -432,7 +435,7 @@ where let sealed = ctx.resolve_header(BlockId::Number(block_num.into())).map_err(|e| { tracing::warn!(error = %e, block_num, "header resolution failed"); - DebugError::BlockNotFound(id) + DebugError::Resolve(e) })?; let Some(sealed) = sealed else { diff --git a/crates/rpc/src/debug/error.rs b/crates/rpc/src/debug/error.rs index 78bd3722..6fcd793b 100644 --- a/crates/rpc/src/debug/error.rs +++ b/crates/rpc/src/debug/error.rs @@ -51,10 +51,9 @@ impl ajj::IntoErrorPayload for DebugError { match self { Self::Cold(_) | Self::Hot(_) | Self::EvmHalt { .. } | Self::Internal(_) => -32000, Self::Resolve(r) => crate::eth::error::resolve_error_code(r), - Self::InvalidTracerConfig => -32602, + Self::InvalidTracerConfig | Self::RlpDecode(_) => -32602, Self::Unsupported(_) => -32601, Self::BlockNotFound(_) | Self::TransactionNotFound(_) => -32001, - Self::RlpDecode(_) => -32602, Self::SenderRecovery => -32000, } } From e628f052b54033061c27e06d479503a811fff42f Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 18:07:58 -0400 Subject: [PATCH 12/15] docs: add Parity trace_ namespace design spec 9 methods (trace_block, trace_transaction, trace_replayBlockTransactions, trace_replayTransaction, trace_call, trace_callMany, trace_rawTransaction, trace_get, trace_filter) for Blockscout/tooling compatibility. ENG-1064, ENG-1065. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...026-03-25-parity-trace-namespace-design.md | 242 ++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-25-parity-trace-namespace-design.md diff --git a/docs/superpowers/specs/2026-03-25-parity-trace-namespace-design.md b/docs/superpowers/specs/2026-03-25-parity-trace-namespace-design.md new file mode 100644 index 00000000..4ba88ff7 --- /dev/null +++ b/docs/superpowers/specs/2026-03-25-parity-trace-namespace-design.md @@ -0,0 +1,242 @@ +# Parity `trace_` Namespace + +Add the Parity/OpenEthereum `trace_` JSON-RPC namespace to signet-rpc for +Blockscout and general tooling compatibility. Driven by ENG-1064 and ENG-1065. + +## Scope + +9 methods in a new `trace` namespace. No block/uncle reward traces — Signet +is a post-merge L2. No `debug_storageRangeAt` — Geth's hashed-key pagination +format doesn't match Signet's plain-key storage, and reth hasn't implemented +it either. + +## Methods + +| Method | Input | Output | +|--------|-------|--------| +| `trace_block` | `BlockNumberOrTag` | `Option>` | +| `trace_transaction` | `B256` | `Option>` | +| `trace_replayBlockTransactions` | `BlockNumberOrTag, HashSet` | `Option>` | +| `trace_replayTransaction` | `B256, HashSet` | `TraceResults` | +| `trace_call` | `TransactionRequest, HashSet, Option` | `TraceResults` | +| `trace_callMany` | `Vec<(TransactionRequest, HashSet)>, Option` | `Vec` | +| `trace_rawTransaction` | `Bytes, HashSet, Option` | `TraceResults` | +| `trace_get` | `B256, Vec` | `Option` | +| `trace_filter` | `TraceFilter` | `Vec` | + +All types from `alloy::rpc::types::trace::parity` and +`alloy::rpc::types::trace::filter`. + +## Architecture + +### New files + +- `crates/rpc/src/trace/mod.rs` — router (9 routes) + re-exports +- `crates/rpc/src/trace/endpoints.rs` — 9 handlers + 2 shared replay helpers +- `crates/rpc/src/trace/error.rs` — `TraceError` with `IntoErrorPayload` +- `crates/rpc/src/trace/types.rs` — param tuple structs for ajj positional + param deserialization (following `debug/types.rs` pattern) + +### Modified files + +- `crates/rpc/src/debug/tracer.rs` — add 2 `pub(crate)` Parity tracer + functions +- `crates/rpc/src/config/rpc_config.rs` — add `max_trace_filter_blocks` +- `crates/rpc/src/lib.rs` — add `mod trace`, export `TraceError`, nest router + +### No new dependencies + +`revm-inspectors` 0.34.2 already has `ParityTraceBuilder`. `alloy` 1.7.3 +already has all Parity trace types. The existing `trace_flat_call` in +`debug/tracer.rs` already uses `inspector.into_parity_builder()`. + +## Param Types + +Tuple structs in `trace/types.rs` for ajj positional param deserialization, +following the pattern in `debug/types.rs`: + +``` +TraceBlockParams(BlockNumberOrTag) +TraceTransactionParams(B256) +ReplayBlockParams(BlockNumberOrTag, HashSet) +ReplayTransactionParams(B256, HashSet) +TraceCallParams(TransactionRequest, HashSet, Option, + Option, Option>) +TraceCallManyParams(Vec<(TransactionRequest, HashSet)>, + Option) +TraceRawTransactionParams(Bytes, HashSet, Option) +TraceGetParams(B256, Vec) +TraceFilterParams(TraceFilter) +``` + +`trace_call` includes state and block override fields to support reth's +`TraceCallRequest` semantics via positional params. + +## Parity Tracer Functions + +Two new `pub(crate)` functions in `debug/tracer.rs`: + +**`trace_parity_localized(trevm, tx_info)`** +Returns `(Vec, EvmNeedsTx)`. Creates +`TracingInspector` with `TracingInspectorConfig::default_parity()`, runs the +tx via `try_with_inspector`, extracts `gas_used` from the result (matching +`trace_flat_call` pattern), converts via +`into_parity_builder().with_transaction_gas_used(gas).into_localized_transaction_traces(tx_info)`. + +Used by: `trace_block`, `trace_transaction`, `trace_get`, `trace_filter`. + +**`trace_parity_replay(trevm, trace_types)`** +Returns `(TraceResults, EvmNeedsTx)`. Requires `Db: Database + DatabaseRef` +(the `DatabaseRef` bound is needed for `populate_state_diff`). Creates +`TracingInspector` with +`TracingInspectorConfig::from_parity_config(&trace_types)`. Runs the tx, +then: + +1. Takes result WITHOUT committing state (holds uncommitted state). +2. Converts via `into_parity_builder().into_trace_results(&result, &trace_types)`. +3. If `StateDiff` is requested: calls `populate_state_diff(&mut state_diff, &db, state.iter())` to enrich with pre-tx balance/nonce from the DB. +4. Then commits state. + +This matches reth's `replayBlockTransactions` pattern. + +Used by: `trace_replayBlockTransactions`, `trace_call`, `trace_callMany`, +`trace_rawTransaction`. + +**Exception:** `trace_replayTransaction` uses +`into_trace_results_with_state(&res, &trace_types, &db)` instead. This +method takes `&ResultAndState` (a different type) and handles state diff +population internally. Matches reth's divergent pattern for single-tx replay. +The `DB::Error` from this call must be mapped into `TraceError`. + +## Shared Block Replay Helpers + +Two inner functions in `trace/endpoints.rs`: + +**`trace_block_localized()`** — replays block txs, calls +`trace_parity_localized` for each. Stops at the first magic-sig tx (using +`peeking_take_while`, same as `debug::trace_block_inner`). Returns +`Vec`. No reward traces (Signet is post-merge). + +**`trace_block_replay()`** — same replay loop but calls +`trace_parity_replay` with the caller's `HashSet`. Returns +`Vec`. + +Both follow the same EVM setup pattern as `debug::trace_block_inner`: +`signet_evm::signet_evm()`, `fill_cfg`, `fill_block`, iterate txs with +`peeking_take_while`. All handlers use `tracing::debug_span!` + +`.instrument(span)` for instrumentation (matching existing debug endpoints). + +## Method Details + +### `trace_block` + +Semaphore-gated. Resolves block, delegates to `trace_block_localized`. +Returns `None` if block not found. No reward traces. + +### `trace_transaction` + +Semaphore-gated. Finds tx by hash in cold storage, resolves containing block, +replays preceding txs without tracing (using `run_tx` + `accept_state`), +traces target tx with `trace_parity_localized`. Returns `None` if tx not +found. + +### `trace_replayBlockTransactions` + +Semaphore-gated. Resolves block, delegates to `trace_block_replay` with the +caller's `HashSet`. For each tx, wraps result in +`TraceResultsWithTransactionHash`. Returns `None` if block not found. + +### `trace_replayTransaction` + +Semaphore-gated. Replays preceding txs, traces target tx. Uses +`into_trace_results_with_state(&res, &trace_types, &db)` — different from +`replayBlockTransactions` which uses `into_trace_results()` + +`populate_state_diff()`. This matches reth's divergent pattern. Returns error +(not `None`) if tx not found. + +### `trace_call` + +Semaphore-gated. Resolves EVM state at block via `resolve_evm_block`. +Supports state overrides and block overrides (matching reth). Fills tx from +`TransactionRequest`, traces with `trace_parity_replay`. Defaults to latest +block if unspecified. + +### `trace_callMany` + +Semaphore-gated. Resolves EVM state once, then processes calls sequentially. +Each call can have different `HashSet`. State is committed between +calls via `db.commit(res.state)` — each subsequent call sees prior state +changes. Last call's state is not committed. Defaults to `BlockId::pending()` +if unspecified (matching reth). + +### `trace_rawTransaction` + +Semaphore-gated. Decodes RLP bytes into a transaction, recovers sender. Takes +optional `block_id` (defaults to latest). Traces with `trace_parity_replay`. + +### `trace_get` + +Semaphore-gated. Returns `None` if `indices.len() != 1` (Erigon +compatibility, matching reth). Delegates to `trace_transaction` to get all +traces, then selects the trace at `indices[0]`. Returns `None` if index is +out of bounds or tx not found. + +### `trace_filter` + +Semaphore-gated. Validates block range: +- `from_block` defaults to 0, `to_block` defaults to latest +- Both must be <= latest block +- `from_block` must be <= `to_block` +- Range must be <= `max_trace_filter_blocks` (default 100, configurable) + +Processes blocks sequentially. For each block, calls +`trace_block_localized()`, filters results with +`TraceFilter::matcher().matches(&trace.trace)`. Applies `after` (skip) and +`count` (limit) pagination. No reward traces. + +## Error Type + +`TraceError` in `trace/error.rs`: + +``` +Cold(ColdStorageError) — -32000 +Hot(StorageError) — -32000 +Resolve(ResolveError) — via resolve_error_code() +EvmHalt { reason: String } — -32000 +BlockNotFound(BlockId) — -32001 +TransactionNotFound(B256) — -32001 +RlpDecode(String) — -32602 +SenderRecovery — -32000 +BlockRangeExceeded — -32602 +``` + +Implements `IntoErrorPayload`. Reuses `resolve_error_code` and +`resolve_error_message` from `crate::eth::error`. Error messages are +sanitized — storage/DB errors return `"server error"`, no internals leaked. + +## Configuration + +New field in `StorageRpcConfig`: + +- `max_trace_filter_blocks: u64` — default 100 +- Env var: `SIGNET_RPC_MAX_TRACE_FILTER_BLOCKS` +- Builder setter: `max_trace_filter_blocks(u64)` + +## Router + +``` +router() + |- eth::eth() (41 methods) + |- debug::debug() (9 methods) + |- signet::signet() (2 methods) + |- web3::web3() (2 methods) + |- net::net() (2 methods) + +- trace::trace() (9 methods) +``` + +65 total routes. + +## Ordering + +This work depends on PR #120 (namespace completeness) which depends on +PR #119 (structured error codes). Branch off PR #120's head. From c2a771285c7e727037d14e4a1ecdbc7efe458f78 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 18:18:23 -0400 Subject: [PATCH 13/15] 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 f945ca7d6a8cf3d2768360c7dca4c89fccd0bea4 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 30 Mar 2026 13:34:36 -0400 Subject: [PATCH 14/15] fix(rpc): address code review findings on debug namespace - Replace EvmHalt with Internal for task panic/cancellation errors - Replace EvmHalt with Internal for resolve_evm_block error mapping - Use unwrap_or_default() for optional tracer options per Geth spec - Change pub(super) to pub(crate) on all debug handler functions - Replace glob import with explicit imports in web3 test module - Remove accidentally committed planning/spec documents Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/rpc/src/debug/endpoints.rs | 56 +- crates/rpc/src/web3/mod.rs | 3 +- .../2026-03-25-parity-trace-namespace.md | 1552 ----------------- ...026-03-25-parity-trace-namespace-design.md | 242 --- 4 files changed, 21 insertions(+), 1832 deletions(-) delete mode 100644 docs/superpowers/plans/2026-03-25-parity-trace-namespace.md delete mode 100644 docs/superpowers/specs/2026-03-25-parity-trace-namespace-design.md diff --git a/crates/rpc/src/debug/endpoints.rs b/crates/rpc/src/debug/endpoints.rs index 75b998f3..cb763eb8 100644 --- a/crates/rpc/src/debug/endpoints.rs +++ b/crates/rpc/src/debug/endpoints.rs @@ -79,7 +79,7 @@ where } /// `debug_traceBlockByNumber` and `debug_traceBlockByHash` handler. -pub(super) async fn trace_block( +pub(crate) async fn trace_block( hctx: HandlerCtx, TraceBlockParams(id, opts): TraceBlockParams, ctx: StorageRpcCtx, @@ -89,7 +89,7 @@ where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, { - let opts = opts.ok_or(DebugError::InvalidTracerConfig)?; + let opts = opts.unwrap_or_default(); // Acquire a tracing semaphore permit to limit concurrent debug // requests. The permit is held for the entire handler lifetime and @@ -149,7 +149,7 @@ where } /// `debug_traceTransaction` handler. -pub(super) async fn trace_transaction( +pub(crate) async fn trace_transaction( hctx: HandlerCtx, TraceTransactionParams(tx_hash, opts): TraceTransactionParams, ctx: StorageRpcCtx, @@ -158,7 +158,7 @@ where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, { - let opts = opts.ok_or(DebugError::InvalidTracerConfig)?; + let opts = opts.unwrap_or_default(); // Held for the handler duration; dropped when the async block completes. let _permit = ctx.acquire_tracing_permit().await; @@ -240,7 +240,7 @@ where } /// `debug_traceBlock` — trace all transactions in a raw RLP-encoded block. -pub(super) async fn trace_block_rlp( +pub(crate) async fn trace_block_rlp( hctx: HandlerCtx, (rlp_bytes, opts): (Bytes, Option), ctx: StorageRpcCtx, @@ -249,7 +249,7 @@ where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, { - let opts = opts.ok_or(DebugError::InvalidTracerConfig)?; + let opts = opts.unwrap_or_default(); let _permit = ctx.acquire_tracing_permit().await; let span = tracing::debug_span!("traceBlock(RLP)", bytes_len = rlp_bytes.len()); @@ -288,10 +288,7 @@ where } .instrument(span); - await_handler!( - hctx.spawn(fut), - DebugError::EvmHalt { reason: "task panicked or cancelled".into() } - ) + await_handler!(hctx.spawn(fut), DebugError::Internal("task panicked or cancelled".into())) } /// `debug_getRawBlock` handler. @@ -299,7 +296,7 @@ where /// Resolves the given [`BlockId`], fetches header and transactions from cold /// storage, assembles them into an [`alloy::consensus::Block`], and returns /// the RLP-encoded bytes. -pub(super) async fn get_raw_block( +pub(crate) async fn get_raw_block( hctx: HandlerCtx, (id,): (BlockId,), ctx: StorageRpcCtx, @@ -346,17 +343,14 @@ where } .instrument(span); - await_handler!( - hctx.spawn(fut), - DebugError::EvmHalt { reason: "task panicked or cancelled".into() } - ) + await_handler!(hctx.spawn(fut), DebugError::Internal("task panicked or cancelled".into())) } /// `debug_getRawReceipts` handler. /// /// Fetches all receipts for the given [`BlockId`] and returns a list of /// EIP-2718 encoded consensus receipt envelopes (one per transaction). -pub(super) async fn get_raw_receipts( +pub(crate) async fn get_raw_receipts( hctx: HandlerCtx, (id,): (BlockId,), ctx: StorageRpcCtx, @@ -407,16 +401,13 @@ where } .instrument(span); - await_handler!( - hctx.spawn(fut), - DebugError::EvmHalt { reason: "task panicked or cancelled".into() } - ) + await_handler!(hctx.spawn(fut), DebugError::Internal("task panicked or cancelled".into())) } /// `debug_getRawHeader` handler. /// /// Resolves the given [`BlockId`] and returns the RLP-encoded block header. -pub(super) async fn get_raw_header( +pub(crate) async fn get_raw_header( hctx: HandlerCtx, (id,): (BlockId,), ctx: StorageRpcCtx, @@ -447,10 +438,7 @@ where } .instrument(span); - await_handler!( - hctx.spawn(fut), - DebugError::EvmHalt { reason: "task panicked or cancelled".into() } - ) + await_handler!(hctx.spawn(fut), DebugError::Internal("task panicked or cancelled".into())) } /// `debug_traceCall` — trace a call without submitting a transaction. @@ -459,7 +447,7 @@ where /// from a [`alloy::rpc::types::TransactionRequest`], then routes through /// the tracer. State overrides are not supported in this initial /// implementation. -pub(super) async fn debug_trace_call( +pub(crate) async fn debug_trace_call( hctx: HandlerCtx, (request, block_id, opts): ( alloy::rpc::types::TransactionRequest, @@ -472,7 +460,7 @@ where H: HotKv + Send + Sync + 'static, ::Error: DBErrorMarker, { - let opts = opts.ok_or(DebugError::InvalidTracerConfig)?; + let opts = opts.unwrap_or_default(); let _permit = ctx.acquire_tracing_permit().await; let id = block_id.unwrap_or(BlockId::latest()); @@ -484,7 +472,7 @@ where let EvmBlockContext { header, db, spec_id } = ctx.resolve_evm_block(id).map_err(|e| match e { crate::eth::EthError::BlockNotFound(id) => DebugError::BlockNotFound(id), - other => DebugError::EvmHalt { reason: other.to_string() }, + other => DebugError::Internal(other.to_string()), })?; let mut evm = signet_evm::signet_evm(db, ctx.constants().clone()); @@ -507,17 +495,14 @@ where } .instrument(span); - await_handler!( - hctx.spawn(fut), - DebugError::EvmHalt { reason: "task panicked or cancelled".into() } - ) + await_handler!(hctx.spawn(fut), DebugError::Internal("task panicked or cancelled".into())) } /// `debug_getRawTransaction` handler. /// /// Fetches the transaction by hash from cold storage and returns the /// EIP-2718 encoded bytes. -pub(super) async fn get_raw_transaction( +pub(crate) async fn get_raw_transaction( hctx: HandlerCtx, (hash,): (B256,), ctx: StorageRpcCtx, @@ -544,8 +529,5 @@ where } .instrument(span); - await_handler!( - hctx.spawn(fut), - DebugError::EvmHalt { reason: "task panicked or cancelled".into() } - ) + await_handler!(hctx.spawn(fut), DebugError::Internal("task panicked or cancelled".into())) } diff --git a/crates/rpc/src/web3/mod.rs b/crates/rpc/src/web3/mod.rs index f5e68cd0..4e728dae 100644 --- a/crates/rpc/src/web3/mod.rs +++ b/crates/rpc/src/web3/mod.rs @@ -26,7 +26,8 @@ pub(crate) async fn sha3((data,): (Bytes,)) -> Result { #[cfg(test)] mod tests { - use super::*; + use super::{client_version, sha3}; + use alloy::primitives::{Bytes, keccak256}; #[tokio::test] async fn client_version_format() { 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" -``` diff --git a/docs/superpowers/specs/2026-03-25-parity-trace-namespace-design.md b/docs/superpowers/specs/2026-03-25-parity-trace-namespace-design.md deleted file mode 100644 index 4ba88ff7..00000000 --- a/docs/superpowers/specs/2026-03-25-parity-trace-namespace-design.md +++ /dev/null @@ -1,242 +0,0 @@ -# Parity `trace_` Namespace - -Add the Parity/OpenEthereum `trace_` JSON-RPC namespace to signet-rpc for -Blockscout and general tooling compatibility. Driven by ENG-1064 and ENG-1065. - -## Scope - -9 methods in a new `trace` namespace. No block/uncle reward traces — Signet -is a post-merge L2. No `debug_storageRangeAt` — Geth's hashed-key pagination -format doesn't match Signet's plain-key storage, and reth hasn't implemented -it either. - -## Methods - -| Method | Input | Output | -|--------|-------|--------| -| `trace_block` | `BlockNumberOrTag` | `Option>` | -| `trace_transaction` | `B256` | `Option>` | -| `trace_replayBlockTransactions` | `BlockNumberOrTag, HashSet` | `Option>` | -| `trace_replayTransaction` | `B256, HashSet` | `TraceResults` | -| `trace_call` | `TransactionRequest, HashSet, Option` | `TraceResults` | -| `trace_callMany` | `Vec<(TransactionRequest, HashSet)>, Option` | `Vec` | -| `trace_rawTransaction` | `Bytes, HashSet, Option` | `TraceResults` | -| `trace_get` | `B256, Vec` | `Option` | -| `trace_filter` | `TraceFilter` | `Vec` | - -All types from `alloy::rpc::types::trace::parity` and -`alloy::rpc::types::trace::filter`. - -## Architecture - -### New files - -- `crates/rpc/src/trace/mod.rs` — router (9 routes) + re-exports -- `crates/rpc/src/trace/endpoints.rs` — 9 handlers + 2 shared replay helpers -- `crates/rpc/src/trace/error.rs` — `TraceError` with `IntoErrorPayload` -- `crates/rpc/src/trace/types.rs` — param tuple structs for ajj positional - param deserialization (following `debug/types.rs` pattern) - -### Modified files - -- `crates/rpc/src/debug/tracer.rs` — add 2 `pub(crate)` Parity tracer - functions -- `crates/rpc/src/config/rpc_config.rs` — add `max_trace_filter_blocks` -- `crates/rpc/src/lib.rs` — add `mod trace`, export `TraceError`, nest router - -### No new dependencies - -`revm-inspectors` 0.34.2 already has `ParityTraceBuilder`. `alloy` 1.7.3 -already has all Parity trace types. The existing `trace_flat_call` in -`debug/tracer.rs` already uses `inspector.into_parity_builder()`. - -## Param Types - -Tuple structs in `trace/types.rs` for ajj positional param deserialization, -following the pattern in `debug/types.rs`: - -``` -TraceBlockParams(BlockNumberOrTag) -TraceTransactionParams(B256) -ReplayBlockParams(BlockNumberOrTag, HashSet) -ReplayTransactionParams(B256, HashSet) -TraceCallParams(TransactionRequest, HashSet, Option, - Option, Option>) -TraceCallManyParams(Vec<(TransactionRequest, HashSet)>, - Option) -TraceRawTransactionParams(Bytes, HashSet, Option) -TraceGetParams(B256, Vec) -TraceFilterParams(TraceFilter) -``` - -`trace_call` includes state and block override fields to support reth's -`TraceCallRequest` semantics via positional params. - -## Parity Tracer Functions - -Two new `pub(crate)` functions in `debug/tracer.rs`: - -**`trace_parity_localized(trevm, tx_info)`** -Returns `(Vec, EvmNeedsTx)`. Creates -`TracingInspector` with `TracingInspectorConfig::default_parity()`, runs the -tx via `try_with_inspector`, extracts `gas_used` from the result (matching -`trace_flat_call` pattern), converts via -`into_parity_builder().with_transaction_gas_used(gas).into_localized_transaction_traces(tx_info)`. - -Used by: `trace_block`, `trace_transaction`, `trace_get`, `trace_filter`. - -**`trace_parity_replay(trevm, trace_types)`** -Returns `(TraceResults, EvmNeedsTx)`. Requires `Db: Database + DatabaseRef` -(the `DatabaseRef` bound is needed for `populate_state_diff`). Creates -`TracingInspector` with -`TracingInspectorConfig::from_parity_config(&trace_types)`. Runs the tx, -then: - -1. Takes result WITHOUT committing state (holds uncommitted state). -2. Converts via `into_parity_builder().into_trace_results(&result, &trace_types)`. -3. If `StateDiff` is requested: calls `populate_state_diff(&mut state_diff, &db, state.iter())` to enrich with pre-tx balance/nonce from the DB. -4. Then commits state. - -This matches reth's `replayBlockTransactions` pattern. - -Used by: `trace_replayBlockTransactions`, `trace_call`, `trace_callMany`, -`trace_rawTransaction`. - -**Exception:** `trace_replayTransaction` uses -`into_trace_results_with_state(&res, &trace_types, &db)` instead. This -method takes `&ResultAndState` (a different type) and handles state diff -population internally. Matches reth's divergent pattern for single-tx replay. -The `DB::Error` from this call must be mapped into `TraceError`. - -## Shared Block Replay Helpers - -Two inner functions in `trace/endpoints.rs`: - -**`trace_block_localized()`** — replays block txs, calls -`trace_parity_localized` for each. Stops at the first magic-sig tx (using -`peeking_take_while`, same as `debug::trace_block_inner`). Returns -`Vec`. No reward traces (Signet is post-merge). - -**`trace_block_replay()`** — same replay loop but calls -`trace_parity_replay` with the caller's `HashSet`. Returns -`Vec`. - -Both follow the same EVM setup pattern as `debug::trace_block_inner`: -`signet_evm::signet_evm()`, `fill_cfg`, `fill_block`, iterate txs with -`peeking_take_while`. All handlers use `tracing::debug_span!` + -`.instrument(span)` for instrumentation (matching existing debug endpoints). - -## Method Details - -### `trace_block` - -Semaphore-gated. Resolves block, delegates to `trace_block_localized`. -Returns `None` if block not found. No reward traces. - -### `trace_transaction` - -Semaphore-gated. Finds tx by hash in cold storage, resolves containing block, -replays preceding txs without tracing (using `run_tx` + `accept_state`), -traces target tx with `trace_parity_localized`. Returns `None` if tx not -found. - -### `trace_replayBlockTransactions` - -Semaphore-gated. Resolves block, delegates to `trace_block_replay` with the -caller's `HashSet`. For each tx, wraps result in -`TraceResultsWithTransactionHash`. Returns `None` if block not found. - -### `trace_replayTransaction` - -Semaphore-gated. Replays preceding txs, traces target tx. Uses -`into_trace_results_with_state(&res, &trace_types, &db)` — different from -`replayBlockTransactions` which uses `into_trace_results()` + -`populate_state_diff()`. This matches reth's divergent pattern. Returns error -(not `None`) if tx not found. - -### `trace_call` - -Semaphore-gated. Resolves EVM state at block via `resolve_evm_block`. -Supports state overrides and block overrides (matching reth). Fills tx from -`TransactionRequest`, traces with `trace_parity_replay`. Defaults to latest -block if unspecified. - -### `trace_callMany` - -Semaphore-gated. Resolves EVM state once, then processes calls sequentially. -Each call can have different `HashSet`. State is committed between -calls via `db.commit(res.state)` — each subsequent call sees prior state -changes. Last call's state is not committed. Defaults to `BlockId::pending()` -if unspecified (matching reth). - -### `trace_rawTransaction` - -Semaphore-gated. Decodes RLP bytes into a transaction, recovers sender. Takes -optional `block_id` (defaults to latest). Traces with `trace_parity_replay`. - -### `trace_get` - -Semaphore-gated. Returns `None` if `indices.len() != 1` (Erigon -compatibility, matching reth). Delegates to `trace_transaction` to get all -traces, then selects the trace at `indices[0]`. Returns `None` if index is -out of bounds or tx not found. - -### `trace_filter` - -Semaphore-gated. Validates block range: -- `from_block` defaults to 0, `to_block` defaults to latest -- Both must be <= latest block -- `from_block` must be <= `to_block` -- Range must be <= `max_trace_filter_blocks` (default 100, configurable) - -Processes blocks sequentially. For each block, calls -`trace_block_localized()`, filters results with -`TraceFilter::matcher().matches(&trace.trace)`. Applies `after` (skip) and -`count` (limit) pagination. No reward traces. - -## Error Type - -`TraceError` in `trace/error.rs`: - -``` -Cold(ColdStorageError) — -32000 -Hot(StorageError) — -32000 -Resolve(ResolveError) — via resolve_error_code() -EvmHalt { reason: String } — -32000 -BlockNotFound(BlockId) — -32001 -TransactionNotFound(B256) — -32001 -RlpDecode(String) — -32602 -SenderRecovery — -32000 -BlockRangeExceeded — -32602 -``` - -Implements `IntoErrorPayload`. Reuses `resolve_error_code` and -`resolve_error_message` from `crate::eth::error`. Error messages are -sanitized — storage/DB errors return `"server error"`, no internals leaked. - -## Configuration - -New field in `StorageRpcConfig`: - -- `max_trace_filter_blocks: u64` — default 100 -- Env var: `SIGNET_RPC_MAX_TRACE_FILTER_BLOCKS` -- Builder setter: `max_trace_filter_blocks(u64)` - -## Router - -``` -router() - |- eth::eth() (41 methods) - |- debug::debug() (9 methods) - |- signet::signet() (2 methods) - |- web3::web3() (2 methods) - |- net::net() (2 methods) - +- trace::trace() (9 methods) -``` - -65 total routes. - -## Ordering - -This work depends on PR #120 (namespace completeness) which depends on -PR #119 (structured error codes). Branch off PR #120's head. From 21c38775173564c7acb0a806c84d0300d4370433 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 31 Mar 2026 09:08:04 -0400 Subject: [PATCH 15/15] chore: trigger CI Co-Authored-By: Claude Opus 4.6 (1M context)