Skip to content

Commit

Permalink
feat: support trace_filter (#4818)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattsse authored Oct 2, 2023
1 parent 08c5c43 commit 74808ed
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 15 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion crates/rpc/rpc-api/src/trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ pub trait TraceApi {
block_id: BlockId,
) -> RpcResult<Option<Vec<LocalizedTransactionTrace>>>;

/// 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<Vec<LocalizedTransactionTrace>>;

Expand Down
13 changes: 6 additions & 7 deletions crates/rpc/rpc-builder/tests/it/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand All @@ -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<C>(client: &C)
Expand Down
56 changes: 54 additions & 2 deletions crates/rpc/rpc-types/src/eth/trace/filter.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -14,15 +15,66 @@ pub struct TraceFilter {
#[serde(with = "u64_hex_or_decimal_opt")]
pub to_block: Option<u64>,
/// From address
pub from_address: Option<Vec<Address>>,
#[serde(default)]
pub from_address: Vec<Address>,
/// To address
pub to_address: Option<Vec<Address>>,
#[serde(default)]
pub to_address: Vec<Address>,
/// How to apply `from_address` and `to_address` filters.
#[serde(default)]
pub mode: TraceFilterMode,
/// Output offset
pub after: Option<u64>,
/// Output amount
pub count: Option<u64>,
}

// === 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<Address>,
to_addresses: HashSet<Address>,
}

impl TraceFilterMatcher {
/// Returns `true` if the given `from` and `to` addresses match this filter.
pub fn matches(&self, from: Address, to: Option<Address>) -> 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::*;
Expand Down
90 changes: 87 additions & 3 deletions crates/rpc/rpc/src/trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use crate::{
utils::recover_raw_transaction,
EthTransactions,
},
result::internal_rpc_err,
TracingCallGuard,
};
use async_trait::async_trait;
Expand Down Expand Up @@ -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<Vec<LocalizedTransactionTrace>> {
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,
Expand Down Expand Up @@ -532,8 +611,13 @@ where
}

/// Handler for `trace_filter`
async fn trace_filter(&self, _filter: TraceFilter) -> Result<Vec<LocalizedTransactionTrace>> {
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<Vec<LocalizedTransactionTrace>> {
Ok(TraceApi::trace_filter(self, filter).await?)
}

/// Returns transaction trace at given index.
Expand Down

0 comments on commit 74808ed

Please sign in to comment.