From 041b8d320762fa04e3aba25e5fd51f93562bc9e4 Mon Sep 17 00:00:00 2001 From: Bharath Vedartham Date: Thu, 11 May 2023 10:19:27 +0530 Subject: [PATCH] feat: implement call tracer (#2349) Co-authored-by: Matthias Seitz --- crates/revm/revm-inspectors/Cargo.toml | 2 +- .../src/tracing/builder/geth.rs | 45 ++++++++++ .../revm/revm-inspectors/src/tracing/types.rs | 82 ++++++++++++++++++- .../rpc/rpc-types/src/eth/trace/geth/call.rs | 8 +- .../rpc/rpc-types/src/eth/trace/geth/mod.rs | 2 +- crates/rpc/rpc/src/debug.rs | 64 ++++++++++++++- 6 files changed, 193 insertions(+), 10 deletions(-) diff --git a/crates/revm/revm-inspectors/Cargo.toml b/crates/revm/revm-inspectors/Cargo.toml index 696b8672b9..0a4d851337 100644 --- a/crates/revm/revm-inspectors/Cargo.toml +++ b/crates/revm/revm-inspectors/Cargo.toml @@ -15,4 +15,4 @@ revm = { version = "3" } # remove from reth and reexport from revm hashbrown = "0.13" -serde = { version = "1.0", features = ["derive"] } +serde = { version = "1.0", features = ["derive"] } \ No newline at end of file diff --git a/crates/revm/revm-inspectors/src/tracing/builder/geth.rs b/crates/revm/revm-inspectors/src/tracing/builder/geth.rs index 248f301b1a..df8f1fca3e 100644 --- a/crates/revm/revm-inspectors/src/tracing/builder/geth.rs +++ b/crates/revm/revm-inspectors/src/tracing/builder/geth.rs @@ -106,4 +106,49 @@ impl GethTraceBuilder { struct_logs, } } + + /// Generate a geth-style traces for the call tracer. + /// + /// This decodes all call frames from the recorded traces. + pub fn geth_call_traces(&self, opts: CallConfig) -> CallFrame { + if self.nodes.is_empty() { + return Default::default() + } + + let include_logs = opts.with_log.unwrap_or_default(); + // first fill up the root + let main_trace_node = &self.nodes[0]; + let root_call_frame = main_trace_node.geth_empty_call_frame(include_logs); + + if opts.only_top_call.unwrap_or_default() { + return root_call_frame + } + + // fill all the call frames in the root call frame with the recorded traces. + // traces are identified by their index in the arena + // so we can populate the call frame tree by walking up the call tree + let mut call_frames = Vec::with_capacity(self.nodes.len()); + call_frames.push((0, root_call_frame)); + for (idx, trace) in self.nodes.iter().enumerate().skip(1) { + call_frames.push((idx, trace.geth_empty_call_frame(include_logs))); + } + + // pop the _children_ calls frame and move it to the parent + // this will roll up the child frames to their parent; this works because `child idx > + // parent idx` + loop { + let (idx, call) = call_frames.pop().expect("call frames not empty"); + let node = &self.nodes[idx]; + if let Some(parent) = node.parent { + let parent_frame = &mut call_frames[parent]; + // we need to ensure that calls are in order they are called: the last child node is + // the last call, but since we walk up the tree, we need to always + // insert at position 0 + parent_frame.1.calls.get_or_insert_with(Vec::new).insert(0, call); + } else { + debug_assert!(call_frames.is_empty(), "only one root node has no parent"); + return call + } + } + } } diff --git a/crates/revm/revm-inspectors/src/tracing/types.rs b/crates/revm/revm-inspectors/src/tracing/types.rs index 372deb2b30..906eed9072 100644 --- a/crates/revm/revm-inspectors/src/tracing/types.rs +++ b/crates/revm/revm-inspectors/src/tracing/types.rs @@ -1,9 +1,9 @@ //! Types for representing call trace items. use crate::tracing::utils::convert_memory; -use reth_primitives::{bytes::Bytes, Address, H256, U256}; +use reth_primitives::{abi::decode_revert_reason, bytes::Bytes, Address, H256, U256}; use reth_rpc_types::trace::{ - geth::StructLog, + geth::{CallFrame, CallLogFrame, StructLog}, parity::{ Action, ActionType, CallAction, CallOutput, CallType, ChangedType, CreateAction, CreateOutput, Delta, SelfdestructAction, StateDiff, TraceOutput, TraceResult, @@ -37,6 +37,31 @@ impl CallKind { } } +impl std::fmt::Display for CallKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CallKind::Call => { + write!(f, "CALL") + } + CallKind::StaticCall => { + write!(f, "STATICCALL") + } + CallKind::CallCode => { + write!(f, "CALLCODE") + } + CallKind::DelegateCall => { + write!(f, "DELEGATECALL") + } + CallKind::Create => { + write!(f, "CREATE") + } + CallKind::Create2 => { + write!(f, "CREATE2") + } + } + } +} + impl From for CallKind { fn from(scheme: CallScheme) -> Self { match scheme { @@ -120,6 +145,18 @@ pub(crate) struct CallTrace { pub(crate) steps: Vec, } +impl CallTrace { + // Returns true if the status code is an error or revert, See [InstructionResult::Revert] + pub(crate) fn is_error(&self) -> bool { + self.status as u8 >= InstructionResult::Revert as u8 + } + + /// Returns the error message if it is an erroneous result. + pub(crate) fn as_error(&self) -> Option { + self.is_error().then(|| format!("{:?}", self.status)) + } +} + impl Default for CallTrace { fn default() -> Self { Self { @@ -276,6 +313,47 @@ impl CallTraceNode { }), } } + + /// Converts this call trace into an _empty_ geth [CallFrame] + /// + /// Caution: this does not include any of the child calls + pub(crate) fn geth_empty_call_frame(&self, include_logs: bool) -> CallFrame { + let mut call_frame = CallFrame { + typ: self.trace.kind.to_string(), + from: self.trace.caller, + to: Some(self.trace.address), + value: Some(self.trace.value), + gas: U256::from(self.trace.gas_used), + gas_used: U256::from(self.trace.gas_used), + input: self.trace.data.clone().into(), + output: Some(self.trace.output.clone().into()), + error: None, + revert_reason: None, + calls: None, + logs: None, + }; + + // we need to populate error and revert reason + if !self.trace.success { + call_frame.revert_reason = decode_revert_reason(self.trace.output.clone()); + call_frame.error = self.trace.as_error(); + } + + if include_logs { + call_frame.logs = Some( + self.logs + .iter() + .map(|log| CallLogFrame { + address: Some(self.trace.address), + topics: Some(log.topics.clone()), + data: Some(log.data.clone().into()), + }) + .collect(), + ); + } + + call_frame + } } /// Ordering enum for calls and logs diff --git a/crates/rpc/rpc-types/src/eth/trace/geth/call.rs b/crates/rpc/rpc-types/src/eth/trace/geth/call.rs index 3723929a59..b38737b145 100644 --- a/crates/rpc/rpc-types/src/eth/trace/geth/call.rs +++ b/crates/rpc/rpc-types/src/eth/trace/geth/call.rs @@ -21,6 +21,8 @@ pub struct CallFrame { #[serde(default, skip_serializing_if = "Option::is_none")] pub error: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub revert_reason: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub calls: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub logs: Option>, @@ -29,11 +31,11 @@ pub struct CallFrame { #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct CallLogFrame { #[serde(default, skip_serializing_if = "Option::is_none")] - address: Option
, + pub address: Option
, #[serde(default, skip_serializing_if = "Option::is_none")] - topics: Option>, + pub topics: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] - data: Option, + pub data: Option, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] diff --git a/crates/rpc/rpc-types/src/eth/trace/geth/mod.rs b/crates/rpc/rpc-types/src/eth/trace/geth/mod.rs index 0400c34d29..506e9face3 100644 --- a/crates/rpc/rpc-types/src/eth/trace/geth/mod.rs +++ b/crates/rpc/rpc-types/src/eth/trace/geth/mod.rs @@ -8,7 +8,7 @@ use std::collections::BTreeMap; // re-exports pub use self::{ - call::{CallConfig, CallFrame}, + call::{CallConfig, CallFrame, CallLogFrame}, four_byte::FourByteFrame, noop::NoopFrame, pre_state::{PreStateConfig, PreStateFrame}, diff --git a/crates/rpc/rpc/src/debug.rs b/crates/rpc/rpc/src/debug.rs index f8dce423b9..d45bf16d07 100644 --- a/crates/rpc/rpc/src/debug.rs +++ b/crates/rpc/rpc/src/debug.rs @@ -185,8 +185,54 @@ where // TODO(mattsse) apply block overrides let GethDebugTracingCallOptions { tracing_options, state_overrides, block_overrides: _ } = opts; - let GethDebugTracingOptions { config, .. } = tracing_options; - // TODO(mattsse) support non default tracers + let GethDebugTracingOptions { config, tracer, tracer_config, .. } = tracing_options; + + if let Some(tracer) = tracer { + // valid matching config + if let Some(ref config) = tracer_config { + if !config.matches_tracer(&tracer) { + return Err(EthApiError::InvalidTracerConfig) + } + } + + return match tracer { + GethDebugTracerType::BuiltInTracer(tracer) => match tracer { + GethDebugBuiltInTracerType::FourByteTracer => { + let mut inspector = FourByteInspector::default(); + let (_res, _) = self + .eth_api + .inspect_call_at(call, at, state_overrides, &mut inspector) + .await?; + return Ok(FourByteFrame::from(inspector).into()) + } + GethDebugBuiltInTracerType::CallTracer => { + // we validated the config above + let call_config = + tracer_config.and_then(|c| c.into_call_config()).unwrap_or_default(); + + let mut inspector = TracingInspector::new( + TracingInspectorConfig::from_geth_config(&config), + ); + + let _ = self + .eth_api + .inspect_call_at(call, at, state_overrides, &mut inspector) + .await?; + + let frame = inspector.into_geth_builder().geth_call_traces(call_config); + + return Ok(frame.into()) + } + GethDebugBuiltInTracerType::PreStateTracer => { + Err(EthApiError::Unsupported("pre state tracer currently unsupported.")) + } + GethDebugBuiltInTracerType::NoopTracer => Ok(NoopFrame::default().into()), + }, + GethDebugTracerType::JsTracer(_) => { + Err(EthApiError::Unsupported("javascript tracers are unsupported.")) + } + } + } // default structlog tracer let inspector_config = TracingInspectorConfig::from_geth_config(&config); @@ -358,6 +404,7 @@ fn trace_transaction( db: &mut SubState>, ) -> EthResult<(GethTraceFrame, revm_primitives::State)> { let GethDebugTracingOptions { config, tracer, tracer_config, .. } = opts; + if let Some(tracer) = tracer { // valid matching config if let Some(ref config) = tracer_config { @@ -374,7 +421,18 @@ fn trace_transaction( return Ok((FourByteFrame::from(inspector).into(), res.state)) } GethDebugBuiltInTracerType::CallTracer => { - todo!() + // we validated the config above + let call_config = + tracer_config.and_then(|c| c.into_call_config()).unwrap_or_default(); + + let mut inspector = + TracingInspector::new(TracingInspectorConfig::from_geth_config(&config)); + + let (res, _) = inspect(db, env, &mut inspector)?; + + let frame = inspector.into_geth_builder().geth_call_traces(call_config); + + return Ok((frame.into(), res.state)) } GethDebugBuiltInTracerType::PreStateTracer => { todo!()