feat: implement call tracer (#2349)

Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
This commit is contained in:
Bharath Vedartham
2023-05-11 10:19:27 +05:30
committed by GitHub
parent f4c241970e
commit 041b8d3207
6 changed files with 193 additions and 10 deletions

View File

@@ -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"] }

View File

@@ -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
}
}
}
}

View File

@@ -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

View File

@@ -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)]

View File

@@ -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},

View File

@@ -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!()