Skip to content

Commit

Permalink
feature(writer): add support for external decoded data sources (#151)
Browse files Browse the repository at this point in the history
Closes: #14

- Add `nodes_mut` for mutability of nodes, required for populating added
CallTrace fields
- Adds `decoded.label`, `decoded.return_data`, `decoded.call_data`,
wrapped in the `DecodedCallTrace` container type.
- Wraps the raw `LogData` in a `CallLog` to accompany it with additional
decoded data fields: `decoded.name`, `decoded.params`, wrapped in the
`DecodedCallLog` container type.

`decoded.contract_name` has not been added as a field as it is only used
in Foundry's gas report, not in the current trace render.

It is likely that some stylistic updates will be followed up relatively
quickly

Related PR: foundry-rs/foundry#8224

---------

Co-authored-by: DaniPopes <[email protected]>
  • Loading branch information
zerosnacks and DaniPopes authored Jun 27, 2024
1 parent 08c77df commit bc01e05
Show file tree
Hide file tree
Showing 6 changed files with 428 additions and 80 deletions.
9 changes: 7 additions & 2 deletions src/tracing/arena.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,17 @@ impl Default for CallTraceArena {
}

impl CallTraceArena {
/// Returns the nodes in the arena
/// Returns the nodes in the arena.
pub fn nodes(&self) -> &[CallTraceNode] {
&self.arena
}

/// Consumes the arena and returns the nodes
/// Returns a mutable reference to the nodes in the arena.
pub fn nodes_mut(&mut self) -> &mut Vec<CallTraceNode> {
&mut self.arena
}

/// Consumes the arena and returns the nodes.
pub fn into_nodes(self) -> Vec<CallTraceNode> {
self.arena
}
Expand Down
4 changes: 2 additions & 2 deletions src/tracing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ mod opcount;
pub use opcount::OpcodeCountInspector;

pub mod types;
use types::{CallTrace, CallTraceStep};
use types::{CallLog, CallTrace, CallTraceStep};

mod utils;

Expand Down Expand Up @@ -510,7 +510,7 @@ where
if self.config.record_logs {
let trace = self.last_trace();
trace.ordering.push(TraceMemberOrder::Log(trace.logs.len()));
trace.logs.push(log.data.clone());
trace.logs.push(CallLog::from(log.clone()));
}
}

Expand Down
62 changes: 57 additions & 5 deletions src/tracing/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,29 @@ use alloy_rpc_types::trace::{
use revm::interpreter::{opcode, CallScheme, CreateScheme, InstructionResult, OpCode};
use std::collections::VecDeque;

/// A trace of a call.
/// Decoded call data.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DecodedCallData {
/// The function signature.
pub signature: String,
/// The function arguments.
pub args: Vec<String>,
}

/// Additional decoded data enhancing the [CallTrace].
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DecodedCallTrace {
/// Optional decoded label for the call.
pub label: Option<String>,
/// Optional decoded return data.
pub return_data: Option<String>,
/// Optional decoded call data.
pub call_data: Option<DecodedCallData>,
}

/// A trace of a call with optional decoded data.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CallTrace {
Expand Down Expand Up @@ -64,6 +86,8 @@ pub struct CallTrace {
pub status: InstructionResult,
/// Opcode-level execution steps.
pub steps: Vec<CallTraceStep>,
/// Optional complementary decoded call data.
pub decoded: DecodedCallTrace,
}

impl CallTrace {
Expand All @@ -73,7 +97,7 @@ impl CallTrace {
!self.status.is_ok()
}

/// Returns true if the status code is a revert
/// Returns true if the status code is a revert.
#[inline]
pub fn is_revert(&self) -> bool {
self.status == InstructionResult::Revert
Expand Down Expand Up @@ -121,6 +145,34 @@ impl CallTrace {
}
}

/// Additional decoded data enhancing the [CallLog].
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DecodedCallLog {
/// The decoded event name.
pub name: Option<String>,
/// The decoded log parameters, a vector of the parameter name (e.g. foo) and the parameter
/// value (e.g. 0x9d3...45ca).
pub params: Option<Vec<(String, String)>>,
}

/// A log with optional decoded data.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CallLog {
/// The raw log data.
pub raw_log: LogData,
/// Optional complementary decoded log data.
pub decoded: DecodedCallLog,
}

impl From<Log> for CallLog {
/// Converts a [`Log`] into a [`CallLog`].
fn from(log: Log) -> Self {
Self { raw_log: log.data, decoded: DecodedCallLog { name: None, params: None } }
}
}

/// A node in the arena
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
Expand All @@ -134,7 +186,7 @@ pub struct CallTraceNode {
/// The call trace
pub trace: CallTrace,
/// Recorded logs, if enabled
pub logs: Vec<LogData>,
pub logs: Vec<CallLog>,
/// Ordering of child calls and logs
pub ordering: Vec<TraceMemberOrder>,
}
Expand Down Expand Up @@ -344,8 +396,8 @@ impl CallTraceNode {
.iter()
.map(|log| CallLogFrame {
address: Some(self.execution_address()),
topics: Some(log.topics().to_vec()),
data: Some(log.data.clone()),
topics: Some(log.raw_log.topics().to_vec()),
data: Some(log.raw_log.data.clone()),
})
.collect();
}
Expand Down
95 changes: 48 additions & 47 deletions src/tracing/writer.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use super::{
types::{CallKind, CallTrace, CallTraceNode, TraceMemberOrder},
types::{CallKind, CallLog, CallTrace, CallTraceNode, DecodedCallData, TraceMemberOrder},
CallTraceArena,
};
use alloy_primitives::{address, hex, Address, LogData};
use alloy_primitives::{address, hex, Address};
use anstyle::{AnsiColor, Color, Style};
use colorchoice::ColorChoice;
use std::io::{self, Write};
Expand Down Expand Up @@ -86,6 +86,7 @@ impl<W: Write> TraceWriter<W> {
self.writer.flush()
}

/// Writes a single node and its children to the writer.
fn write_node(&mut self, nodes: &[CallTraceNode], idx: usize) -> io::Result<()> {
let node = &nodes[idx];

Expand All @@ -98,7 +99,7 @@ impl<W: Write> TraceWriter<W> {
self.indentation_level += 1;
for child in &node.ordering {
match *child {
TraceMemberOrder::Log(index) => self.write_raw_log(&node.logs[index]),
TraceMemberOrder::Log(index) => self.write_log(&node.logs[index]),
TraceMemberOrder::Call(index) => self.write_node(nodes, node.children[index]),
TraceMemberOrder::Step(_) => Ok(()),
}?;
Expand All @@ -114,28 +115,25 @@ impl<W: Write> TraceWriter<W> {
Ok(())
}

/// Writes the header of a call trace.
fn write_trace_header(&mut self, trace: &CallTrace) -> io::Result<()> {
write!(self.writer, "[{}] ", trace.gas_used)?;

let trace_kind_style = self.trace_kind_style();
let address = trace.address.to_checksum_buffer(None);

if trace.kind.is_any_create() {
#[allow(clippy::write_literal)] // TODO
write!(
self.writer,
"{trace_kind_style}{CALL}new{trace_kind_style:#} {label}@{address}",
// TODO: trace.label.as_deref().unwrap_or("<unknown>")
label = "<unknown>",
label = trace.decoded.label.as_deref().unwrap_or("<unknown>")
)?;
} else {
let (func_name, inputs) = match None::<()> {
// TODO
// Some(DecodedCallData { signature, args }) => {
// let name = signature.split('(').next().unwrap();
// (name.to_string(), args.join(", "))
// }
Some(()) => unreachable!(),
let (func_name, inputs) = match &trace.decoded.call_data {
Some(DecodedCallData { signature, args }) => {
let name = signature.split('(').next().unwrap();
(name.to_string(), args.join(", "))
}
None => {
if trace.data.len() < 4 {
("fallback".to_string(), hex::encode(&trace.data))
Expand All @@ -150,8 +148,7 @@ impl<W: Write> TraceWriter<W> {
self.writer,
"{style}{addr}{style:#}::{style}{func_name}{style:#}",
style = self.trace_style(trace),
// TODO: trace.label
addr = None::<String>.as_deref().unwrap_or(address.as_str())
addr = trace.decoded.label.as_deref().unwrap_or(address.as_str()),
)?;

if !trace.value.is_zero() {
Expand All @@ -176,41 +173,46 @@ impl<W: Write> TraceWriter<W> {
Ok(())
}

fn write_raw_log(&mut self, log: &LogData) -> io::Result<()> {
fn write_log(&mut self, log: &CallLog) -> io::Result<()> {
let log_style = self.log_style();
self.write_branch()?;

for (i, topic) in log.topics().iter().enumerate() {
if i == 0 {
self.writer.write_all(b" emit topic 0")?;
} else {
if let Some(name) = &log.decoded.name {
write!(self.writer, "emit {name}({log_style}")?;
if let Some(params) = &log.decoded.params {
for (i, (param_name, value)) in params.iter().enumerate() {
if i > 0 {
self.writer.write_all(b", ")?;
}
write!(self.writer, "{param_name}: {value}")?;
}
}
writeln!(self.writer, "{log_style:#})")?;
} else {
for (i, topic) in log.raw_log.topics().iter().enumerate() {
if i == 0 {
self.writer.write_all(b" emit topic 0")?;
} else {
self.write_pipes()?;
write!(self.writer, " topic {i}")?;
}
writeln!(self.writer, ": {log_style}{topic}{log_style:#}")?;
}

if !log.raw_log.topics().is_empty() {
self.write_pipes()?;
write!(self.writer, " topic {i}")?;
}
writeln!(self.writer, ": {log_style}{topic}{log_style:#}")?;
writeln!(
self.writer,
" data: {log_style}{data}{log_style:#}",
data = log.raw_log.data
)?;
}

if !log.topics().is_empty() {
self.write_pipes()?;
}
writeln!(self.writer, " data: {log_style}{data}{log_style:#}", data = log.data)
Ok(())
}

// #[cfg(TODO)]
// fn write_decoded_log(&mut self, name: &str, params: &[(String, String)]) -> io::Result<()> {
// let log_style = self.log_style();
// self.write_left_prefix()?;
//
// write!(self.writer, "emit {name}({log_style}")?;
// for (i, (name, value)) in params.iter().enumerate() {
// if i > 0 {
// self.writer.write_all(b", ")?;
// }
// write!(self.writer, "{name}: {value}")?;
// }
// write!(self.writer, "{log_style:#})")
// }

/// Writes the footer of a call trace.
fn write_trace_footer(&mut self, trace: &CallTrace) -> io::Result<()> {
write!(
self.writer,
Expand All @@ -219,10 +221,9 @@ impl<W: Write> TraceWriter<W> {
status = trace.status,
)?;

// TODO:
// if let Some(decoded) = trace.decoded_return_data {
// return self.writer.write_all(decoded.as_bytes());
// }
if let Some(decoded) = &trace.decoded.return_data {
return self.writer.write_all(decoded.as_bytes());
}

if trace.kind.is_any_create() {
write!(self.writer, "{} bytes of code", trace.output.len())?;
Expand All @@ -241,7 +242,7 @@ impl<W: Write> TraceWriter<W> {
Ok(())
}

// FKA left_prefix
#[doc(alias = "left_prefix")]
fn write_branch(&mut self) -> io::Result<()> {
self.write_indentation()?;
if self.indentation_level != 0 {
Expand All @@ -250,7 +251,7 @@ impl<W: Write> TraceWriter<W> {
Ok(())
}

// FKA right_prefix
#[doc(alias = "right_prefix")]
fn write_pipes(&mut self) -> io::Result<()> {
self.write_indentation()?;
self.writer.write_all(PIPE.as_bytes())
Expand Down
23 changes: 18 additions & 5 deletions testdata/Counter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
pragma solidity ^0.8.13;

contract Counter {
event Log(uint256 indexed number, bytes dump);
event Log2(uint256 indexed number, bytes dump);
event Log0(uint256 foo, bytes dump) anonymous;
event Log1(uint256 indexed foo, bytes dump);
event Log2(uint256 indexed foo, uint256 indexed bar, bytes dump);

uint256 public number;

Expand All @@ -16,19 +17,31 @@ contract Counter {
number++;
}

function log0() public {
emit Log0(number, "hi from log0");
}

function log1() public {
emit Log1(number, "hi from log1");
}

function log2() public {
emit Log2(number, 123, "hi from log2");
}

function nest1() public {
emit Log(number, "hi from 1");
emit Log1(number, "hi from 1");
this.nest2();
increment();
}

function nest2() public {
increment();
this.nest3();
emit Log2(number, "hi from 2");
emit Log2(number, 123, "hi from 2");
}

function nest3() public {
emit Log(number, "hi from 3");
emit Log1(number, "hi from 3");
}
}
Loading

0 comments on commit bc01e05

Please sign in to comment.