From 74808eddcc4b74d8beca236d1b0374ce4ada33c7 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Tue, 3 Oct 2023 00:21:40 +0200 Subject: [PATCH] feat: support trace_filter (#4818) --- Cargo.lock | 4 +- crates/rpc/rpc-api/src/trace.rs | 4 +- crates/rpc/rpc-builder/tests/it/http.rs | 13 ++- crates/rpc/rpc-types/src/eth/trace/filter.rs | 56 +++++++++++- crates/rpc/rpc/src/trace.rs | 90 +++++++++++++++++++- 5 files changed, 152 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6ac339bd8..b40d36315 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7680,9 +7680,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8faa444297615a4e020acb64146b0603c9c395c03a97c17fd9028816d3b4d63e" +checksum = "7ac3f5b6856e931e15e07b478e98c8045239829a65f9156d4fa7e7788197a5ef" dependencies = [ "displaydoc", "serde", diff --git a/crates/rpc/rpc-api/src/trace.rs b/crates/rpc/rpc-api/src/trace.rs index 41b41d1fe..557016a9a 100644 --- a/crates/rpc/rpc-api/src/trace.rs +++ b/crates/rpc/rpc-api/src/trace.rs @@ -66,7 +66,9 @@ pub trait TraceApi { block_id: BlockId, ) -> RpcResult>>; - /// Returns traces matching given filter + /// Returns traces matching given filter. + /// + /// This is similar to `eth_getLogs` but for traces. #[method(name = "filter")] async fn trace_filter(&self, filter: TraceFilter) -> RpcResult>; diff --git a/crates/rpc/rpc-builder/tests/it/http.rs b/crates/rpc/rpc-builder/tests/it/http.rs index 9d588dbc7..cac2c1873 100644 --- a/crates/rpc/rpc-builder/tests/it/http.rs +++ b/crates/rpc/rpc-builder/tests/it/http.rs @@ -161,10 +161,11 @@ where { let block_id = BlockId::Number(BlockNumberOrTag::default()); let trace_filter = TraceFilter { - from_block: None, - to_block: None, - from_address: None, - to_address: None, + from_block: Default::default(), + to_block: Default::default(), + from_address: Default::default(), + to_address: Default::default(), + mode: Default::default(), after: None, count: None, }; @@ -182,9 +183,7 @@ where TraceApiClient::trace_block(client, block_id).await.unwrap(); TraceApiClient::replay_block_transactions(client, block_id, HashSet::default()).await.unwrap(); - assert!(is_unimplemented( - TraceApiClient::trace_filter(client, trace_filter).await.err().unwrap() - )); + TraceApiClient::trace_filter(client, trace_filter).await.unwrap(); } async fn test_basic_web3_calls(client: &C) diff --git a/crates/rpc/rpc-types/src/eth/trace/filter.rs b/crates/rpc/rpc-types/src/eth/trace/filter.rs index 3121335da..0c9eac10f 100644 --- a/crates/rpc/rpc-types/src/eth/trace/filter.rs +++ b/crates/rpc/rpc-types/src/eth/trace/filter.rs @@ -1,6 +1,7 @@ //! `trace_filter` types and support use reth_primitives::{serde_helper::num::u64_hex_or_decimal_opt, Address}; use serde::{Deserialize, Serialize}; +use std::collections::HashSet; /// Trace filter. #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -14,15 +15,66 @@ pub struct TraceFilter { #[serde(with = "u64_hex_or_decimal_opt")] pub to_block: Option, /// From address - pub from_address: Option>, + #[serde(default)] + pub from_address: Vec
, /// To address - pub to_address: Option>, + #[serde(default)] + pub to_address: Vec
, + /// How to apply `from_address` and `to_address` filters. + #[serde(default)] + pub mode: TraceFilterMode, /// Output offset pub after: Option, /// Output amount pub count: Option, } +// === impl TraceFilter === + +impl TraceFilter { + /// Returns a `TraceFilterMatcher` for this filter. + pub fn matcher(&self) -> TraceFilterMatcher { + let from_addresses = self.from_address.iter().cloned().collect(); + let to_addresses = self.to_address.iter().cloned().collect(); + TraceFilterMatcher { mode: self.mode, from_addresses, to_addresses } + } +} + +/// How to apply `from_address` and `to_address` filters. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum TraceFilterMode { + /// Return traces for transactions with matching `from` OR `to` addresses. + #[default] + Union, + /// Only return traces for transactions with matching `from` _and_ `to` addresses. + Intersection, +} + +/// Helper type for matching `from` and `to` addresses. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TraceFilterMatcher { + mode: TraceFilterMode, + from_addresses: HashSet
, + to_addresses: HashSet
, +} + +impl TraceFilterMatcher { + /// Returns `true` if the given `from` and `to` addresses match this filter. + pub fn matches(&self, from: Address, to: Option
) -> bool { + match self.mode { + TraceFilterMode::Union => { + self.from_addresses.contains(&from) || + to.map_or(false, |to| self.to_addresses.contains(&to)) + } + TraceFilterMode::Intersection => { + self.from_addresses.contains(&from) && + to.map_or(false, |to| self.to_addresses.contains(&to)) + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/rpc/rpc/src/trace.rs b/crates/rpc/rpc/src/trace.rs index 53865b264..f38fc0e57 100644 --- a/crates/rpc/rpc/src/trace.rs +++ b/crates/rpc/rpc/src/trace.rs @@ -5,7 +5,6 @@ use crate::{ utils::recover_raw_transaction, EthTransactions, }, - result::internal_rpc_err, TracingCallGuard, }; use async_trait::async_trait; @@ -249,6 +248,86 @@ where } } + /// Returns all transaction traces that match the given filter. + /// + /// This is similar to [Self::trace_block] but only returns traces for transactions that match + /// the filter. + pub async fn trace_filter( + &self, + filter: TraceFilter, + ) -> EthResult> { + let matcher = filter.matcher(); + let TraceFilter { from_block, to_block, after: _after, count: _count, .. } = filter; + let start = from_block.unwrap_or(0); + let end = if let Some(to_block) = to_block { + to_block + } else { + self.provider().best_block_number()? + }; + + // ensure that the range is not too large, since we need to fetch all blocks in the range + let distance = end.saturating_sub(start); + if distance > 100 { + return Err(EthApiError::InvalidParams( + "Block range too large; currently limited to 100 blocks".to_string(), + )) + } + + // fetch all blocks in that range + let blocks = self.provider().block_range(start..=end)?; + + // find relevant blocks to trace + let mut target_blocks = Vec::new(); + for block in blocks { + let mut transaction_indices = HashSet::new(); + for (tx_idx, tx) in block.body.iter().enumerate() { + let from = tx.recover_signer().ok_or(BlockError::InvalidSignature)?; + let to = tx.to(); + if matcher.matches(from, to) { + transaction_indices.insert(tx_idx as u64); + } + } + if !transaction_indices.is_empty() { + target_blocks.push((block.number, transaction_indices)); + } + } + + // TODO: this could be optimized to only trace the block until the highest matching index in + // that block + + // trace all relevant blocks + let mut block_traces = Vec::with_capacity(target_blocks.len()); + for (num, indices) in target_blocks { + let traces = self.trace_block_with( + num.into(), + TracingInspectorConfig::default_parity(), + move |tx_info, inspector, res, _, _| { + if let Some(idx) = tx_info.index { + if !indices.contains(&idx) { + // only record traces for relevant transactions + return Ok(None) + } + } + let traces = inspector + .with_transaction_gas_used(res.gas_used()) + .into_parity_builder() + .into_localized_transaction_traces(tx_info); + Ok(Some(traces)) + }, + ); + block_traces.push(traces); + } + + let block_traces = futures::future::try_join_all(block_traces).await?; + let all_traces = block_traces + .into_iter() + .flatten() + .flat_map(|traces| traces.into_iter().flatten().flat_map(|traces| traces.into_iter())) + .collect(); + + Ok(all_traces) + } + /// Returns all traces for the given transaction hash pub async fn trace_transaction( &self, @@ -532,8 +611,13 @@ where } /// Handler for `trace_filter` - async fn trace_filter(&self, _filter: TraceFilter) -> Result> { - Err(internal_rpc_err("unimplemented")) + /// + /// This is similar to `eth_getLogs` but for traces. + /// + /// # Limitations + /// This currently requires block filter fields, since reth does not have address indices yet. + async fn trace_filter(&self, filter: TraceFilter) -> Result> { + Ok(TraceApi::trace_filter(self, filter).await?) } /// Returns transaction trace at given index.