From 3fef96a38c99ad7c577881691b0e55997e0d71d2 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Wed, 11 Oct 2023 15:53:17 +0200 Subject: [PATCH] feat(test): add debug trace ext helpers --- crates/rpc/rpc-testing-util/src/debug.rs | 104 ++++++++++++++++++++++- crates/rpc/rpc-testing-util/src/trace.rs | 2 +- crates/rpc/rpc-types/src/eth/block.rs | 73 ++++++++++++++++ 3 files changed, 175 insertions(+), 4 deletions(-) diff --git a/crates/rpc/rpc-testing-util/src/debug.rs b/crates/rpc/rpc-testing-util/src/debug.rs index 9e2711cf52c8..cc9111821657 100644 --- a/crates/rpc/rpc-testing-util/src/debug.rs +++ b/crates/rpc/rpc-testing-util/src/debug.rs @@ -1,12 +1,21 @@ //! Helpers for testing debug trace calls. -use reth_primitives::B256; -use reth_rpc_api::clients::DebugApiClient; +use futures::{Stream, StreamExt}; +use jsonrpsee::core::Error as RpcError; +use reth_primitives::{BlockId, TxHash, B256}; +use reth_rpc_api::{clients::DebugApiClient, EthApiClient}; use reth_rpc_types::trace::geth::{GethDebugTracerType, GethDebugTracingOptions}; +use std::{ + pin::Pin, + task::{Context, Poll}, +}; const NOOP_TRACER: &str = include_str!("../assets/noop-tracer.js"); const JS_TRACER_TEMPLATE: &str = include_str!("../assets/tracer-template.js"); +/// A result type for the `debug_trace_transaction` method that also captures the requested hash. +pub type TraceTransactionResult = Result<(serde_json::Value, TxHash), (RpcError, TxHash)>; + /// An extension trait for the Trace API. #[async_trait::async_trait] pub trait DebugApiExt { @@ -19,10 +28,22 @@ pub trait DebugApiExt { hash: B256, opts: GethDebugTracingOptions, ) -> Result; + + /// Trace all transactions in a block individually with the given tracing opts. + async fn debug_trace_transactions_in_block( + &self, + block: B, + opts: GethDebugTracingOptions, + ) -> Result, jsonrpsee::core::Error> + where + B: Into + Send; } #[async_trait::async_trait] -impl DebugApiExt for T { +impl DebugApiExt for T +where + T: EthApiClient, +{ type Provider = T; async fn debug_trace_transaction_json( @@ -35,6 +56,31 @@ impl DebugApiExt for T { params.insert(opts).unwrap(); self.request("debug_traceTransaction", params).await } + + async fn debug_trace_transactions_in_block( + &self, + block: B, + opts: GethDebugTracingOptions, + ) -> Result, jsonrpsee::core::Error> + where + B: Into + Send, + { + let block = match block.into() { + BlockId::Hash(hash) => self.block_by_hash(hash.block_hash, false).await, + BlockId::Number(tag) => self.block_by_number(tag, false).await, + }? + .ok_or_else(|| RpcError::Custom("block not found".to_string()))?; + let hashes = block.transactions.iter().map(|tx| (tx, opts.clone())).collect::>(); + let stream = futures::stream::iter(hashes.into_iter().map(move |(tx, opts)| async move { + match self.debug_trace_transaction_json(tx, opts).await { + Ok(result) => Ok((result, tx)), + Err(err) => Err((err, tx)), + } + })) + .buffered(10); + + Ok(DebugTraceTransactionsStream { stream: Box::pin(stream) }) + } } /// A helper type that can be used to build a javascript tracer. @@ -146,6 +192,38 @@ impl From for Option { } } +/// A stream that yields the traces for the requested blocks. +#[must_use = "streams do nothing unless polled"] +pub struct DebugTraceTransactionsStream<'a> { + stream: Pin + 'a>>, +} + +impl<'a> DebugTraceTransactionsStream<'a> { + /// Returns the next error result of the stream. + pub async fn next_err(&mut self) -> Option<(RpcError, TxHash)> { + loop { + match self.next().await? { + Ok(_) => continue, + Err(err) => return Some(err), + } + } + } +} + +impl<'a> Stream for DebugTraceTransactionsStream<'a> { + type Item = TraceTransactionResult; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.stream.as_mut().poll_next(cx) + } +} + +impl<'a> std::fmt::Debug for DebugTraceTransactionsStream<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DebugTraceTransactionsStream").finish_non_exhaustive() + } +} + /// A javascript tracer that does nothing #[derive(Debug, Clone, Copy, Default)] #[non_exhaustive] @@ -172,7 +250,9 @@ mod tests { debug::{DebugApiExt, JsTracerBuilder, NoopJsTracer}, utils::parse_env_url, }; + use futures::StreamExt; use jsonrpsee::http_client::HttpClientBuilder; + use reth_rpc_types::trace::geth::{CallConfig, GethDebugTracingOptions}; // random tx const TX_1: &str = "0x5525c63a805df2b83c113ebcc8c7672a3b290673c4e81335b410cd9ebc64e085"; @@ -200,4 +280,22 @@ mod tests { .unwrap(); assert_eq!(res, serde_json::Value::Object(Default::default())); } + + #[tokio::test] + #[ignore] + async fn can_debug_trace_block_transactions() { + let block = 11_117_104u64; + let url = parse_env_url("RETH_RPC_TEST_NODE_URL").unwrap(); + let client = HttpClientBuilder::default().build(url).unwrap(); + + let opts = + GethDebugTracingOptions::default().call_config(CallConfig::default().only_top_call()); + + let mut stream = client.debug_trace_transactions_in_block(block, opts).await.unwrap(); + while let Some(res) = stream.next().await { + if let Err((err, tx)) = res { + println!("failed to trace {:?} {}", tx, err); + } + } + } } diff --git a/crates/rpc/rpc-testing-util/src/trace.rs b/crates/rpc/rpc-testing-util/src/trace.rs index d13a55971cc5..47cd424fd5b5 100644 --- a/crates/rpc/rpc-testing-util/src/trace.rs +++ b/crates/rpc/rpc-testing-util/src/trace.rs @@ -11,7 +11,7 @@ use std::{ use jsonrpsee::core::Error as RpcError; use reth_rpc_types::trace::parity::LocalizedTransactionTrace; -/// A result type for the `trace_block` method that also +/// A result type for the `trace_block` method that also captures the requested block. pub type TraceBlockResult = Result<(Vec, BlockId), (RpcError, BlockId)>; /// An extension trait for the Trace API. diff --git a/crates/rpc/rpc-types/src/eth/block.rs b/crates/rpc/rpc-types/src/eth/block.rs index e238e2774eb4..c2b01a00eb91 100644 --- a/crates/rpc/rpc-types/src/eth/block.rs +++ b/crates/rpc/rpc-types/src/eth/block.rs @@ -25,7 +25,46 @@ impl BlockTransactions { pub fn is_uncle(&self) -> bool { matches!(self, Self::Uncle) } + + /// Returns an iterator over the transaction hashes. + pub fn iter(&self) -> BlockTransactionsHashIterator<'_> { + BlockTransactionsHashIterator::new(self) + } +} + +/// An Iterator over the transaction hashes of a block. +#[derive(Debug, Clone)] +pub struct BlockTransactionsHashIterator<'a> { + txs: &'a BlockTransactions, + idx: usize, +} + +impl<'a> BlockTransactionsHashIterator<'a> { + fn new(txs: &'a BlockTransactions) -> Self { + Self { txs, idx: 0 } + } } + +impl<'a> Iterator for BlockTransactionsHashIterator<'a> { + type Item = B256; + + fn next(&mut self) -> Option { + match self.txs { + BlockTransactions::Full(txs) => { + let tx = txs.get(self.idx); + self.idx += 1; + tx.map(|tx| tx.hash) + } + BlockTransactions::Hashes(txs) => { + let tx = txs.get(self.idx).copied(); + self.idx += 1; + tx + } + BlockTransactions::Uncle => None, + } + } +} + /// Determines how the `transactions` field of [Block] should be filled. /// /// This essentially represents the `full:bool` argument in RPC calls that determine whether the @@ -218,6 +257,7 @@ impl From
for RichHeader { #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct Rich { /// Standard value. + #[serde(flatten)] pub inner: T, /// Additional fields that should be serialized into the `Block` object #[serde(flatten)] @@ -401,4 +441,37 @@ mod tests { let s = r#"{"blockNumber": "0xe39dd0"}"#; let _overrides = serde_json::from_str::(s).unwrap(); } + + #[test] + fn serde_rich_block() { + let s = r#"{ + "hash": "0xb25d0e54ca0104e3ebfb5a1dcdf9528140854d609886a300946fd6750dcb19f4", + "parentHash": "0x9400ec9ef59689c157ac89eeed906f15ddd768f94e1575e0e27d37c241439a5d", + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "miner": "0x829bd824b016326a401d083b33d092293333a830", + "stateRoot": "0x546e330050c66d02923e7f1f3e925efaf64e4384eeecf2288f40088714a77a84", + "transactionsRoot": "0xd5eb3ad6d7c7a4798cc5fb14a6820073f44a941107c5d79dac60bd16325631fe", + "receiptsRoot": "0xb21c41cbb3439c5af25304e1405524c885e733b16203221900cb7f4b387b62f0", + "logsBloom": "0x1f304e641097eafae088627298685d20202004a4a59e4d8900914724e2402b028c9d596660581f361240816e82d00fa14250c9ca89840887a381efa600288283d170010ab0b2a0694c81842c2482457e0eb77c2c02554614007f42aaf3b4dc15d006a83522c86a240c06d241013258d90540c3008888d576a02c10120808520a2221110f4805200302624d22092b2c0e94e849b1e1aa80bc4cc3206f00b249d0a603ee4310216850e47c8997a20aa81fe95040a49ca5a420464600e008351d161dc00d620970b6a801535c218d0b4116099292000c08001943a225d6485528828110645b8244625a182c1a88a41087e6d039b000a180d04300d0680700a15794", + "difficulty": "0xc40faff9c737d", + "number": "0xa9a230", + "gasLimit": "0xbe5a66", + "gasUsed": "0xbe0fcc", + "timestamp": "0x5f93b749", + "extraData": "0x7070796520e4b883e5bda9e7a59ee4bb99e9b1bc0103", + "mixHash": "0xd5e2b7b71fbe4ddfe552fb2377bf7cddb16bbb7e185806036cee86994c6e97fc", + "nonce": "0x4722f2acd35abe0f", + "totalDifficulty": "0x3dc957fd8167fb2684a", + "uncles": [], + "transactions": [ + "0xf435a26acc2a9ef73ac0b73632e32e29bd0e28d5c4f46a7e18ed545c93315916" + ], + "size": "0xaeb6" +}"#; + + let block = serde_json::from_str::(s).unwrap(); + let serialized = serde_json::to_string(&block).unwrap(); + let block2 = serde_json::from_str::(&serialized).unwrap(); + assert_eq!(block, block2); + } }