mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-01-29 09:08:05 -05:00
feat: implement call tracer (#2349)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
This commit is contained in:
committed by
GitHub
parent
f4c241970e
commit
041b8d3207
@@ -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"] }
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<CallScheme> for CallKind {
|
||||
fn from(scheme: CallScheme) -> Self {
|
||||
match scheme {
|
||||
@@ -120,6 +145,18 @@ pub(crate) struct CallTrace {
|
||||
pub(crate) steps: Vec<CallTraceStep>,
|
||||
}
|
||||
|
||||
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<String> {
|
||||
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
|
||||
|
||||
@@ -21,6 +21,8 @@ pub struct CallFrame {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub revert_reason: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub calls: Option<Vec<CallFrame>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub logs: Option<Vec<CallLogFrame>>,
|
||||
@@ -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<Address>,
|
||||
pub address: Option<Address>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
topics: Option<Vec<H256>>,
|
||||
pub topics: Option<Vec<H256>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
data: Option<Bytes>,
|
||||
pub data: Option<Bytes>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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<StateProviderBox<'_>>,
|
||||
) -> 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!()
|
||||
|
||||
Reference in New Issue
Block a user