Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: precompile trace decoding #6263

Merged
merged 9 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/anvil/src/eth/backend/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use foundry_evm::{
interpreter::InstructionResult,
primitives::{BlockEnv, CfgEnv, EVMError, Env, ExecutionResult, Output, SpecId},
},
traces::{node::CallTraceNode, CallTraceArena},
traces::{CallTraceArena, CallTraceNode},
utils::{eval_to_instruction_result, halt_to_instruction_result},
};
use foundry_utils::types::{ToAlloy, ToEthers};
Expand Down
334 changes: 240 additions & 94 deletions crates/evm/traces/src/decoder/mod.rs

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions crates/evm/traces/src/decoder/precompiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,13 @@ pub(super) fn decode(trace: &mut CallTrace, _chain_id: u64) -> bool {
0x08 => (ecpairingCall::SIGNATURE, tri!(decode_ecpairing(data))),
0x09 => (blake2fCall::SIGNATURE, tri!(decode_blake2f(data))),
0x0a => (pointEvaluationCall::SIGNATURE, tri!(decode_kzg(data))),
_ => unreachable!(),
0x00 | 0x0b.. => unreachable!(),
};

// TODO: Other chain precompiles

trace.data = TraceCallData::Decoded { signature: signature.to_string(), args };

trace.contract = Some("PRECOMPILES".into());
trace.label = Some("PRECOMPILES".into());

true
}
Expand Down
2 changes: 1 addition & 1 deletion crates/evm/traces/src/identifier/signatures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ impl SignaturesIdentifier {
&mut self,
selector_type: SelectorType,
identifier: &[u8],
get_type: fn(&str) -> eyre::Result<T>,
get_type: impl Fn(&str) -> eyre::Result<T>,
) -> Option<T> {
// Exit early if we have unsuccessfully queried it before.
if self.unavailable.contains(identifier) {
Expand Down
8 changes: 3 additions & 5 deletions crates/evm/traces/src/inspector.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use crate::{
CallTrace, CallTraceArena, CallTraceStep, LogCallOrder, RawOrDecodedLog, TraceCallData,
TraceRetData,
CallTrace, CallTraceArena, CallTraceStep, LogCallOrder, TraceCallData, TraceLog, TraceRetData,
};
use alloy_primitives::{Address, Bytes, Log as RawLog, B256, U256};
use foundry_evm_core::{
Expand Down Expand Up @@ -162,9 +161,8 @@ impl<DB: Database> Inspector<DB> for Tracer {
let node = &mut self.traces.arena[*self.trace_stack.last().expect("no ongoing trace")];
node.ordering.push(LogCallOrder::Log(node.logs.len()));
let data = data.clone();
node.logs.push(RawOrDecodedLog::Raw(
RawLog::new(topics.to_vec(), data).expect("Received invalid log"),
));
node.logs
.push(TraceLog::Raw(RawLog::new(topics.to_vec(), data).expect("Received invalid log")));
}

#[inline]
Expand Down
65 changes: 27 additions & 38 deletions crates/evm/traces/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@ use foundry_evm_core::{constants::CHEATCODE_ADDRESS, debug::Instruction, utils::
use foundry_utils::types::ToEthers;
use hashbrown::HashMap;
use itertools::Itertools;
use node::CallTraceNode;
use revm::interpreter::{opcode, CallContext, InstructionResult, Memory, Stack};
use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeMap, HashSet},
fmt::{self, Write},
fmt,
};
use yansi::{Color, Paint};

Expand All @@ -35,7 +34,9 @@ pub use decoder::{CallTraceDecoder, CallTraceDecoderBuilder};
mod inspector;
pub use inspector::Tracer;

pub mod node;
mod node;
pub use node::CallTraceNode;

pub mod utils;

pub type Traces = Vec<(TraceKind, CallTraceArena)>;
Expand Down Expand Up @@ -198,79 +199,66 @@ impl fmt::Display for CallTraceArena {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn inner(
arena: &CallTraceArena,
writer: &mut (impl Write + ?Sized),
f: &mut fmt::Formatter<'_>,
idx: usize,
left: &str,
child: &str,
verbose: bool,
) -> fmt::Result {
let node = &arena.arena[idx];

// Display trace header
if !verbose {
writeln!(writer, "{left}{}", node.trace)?;
} else {
writeln!(writer, "{left}{:#}", node.trace)?;
}
f.write_str(left)?;
node.trace.fmt(f)?;

// Display logs and subcalls
let left_prefix = format!("{child}{BRANCH}");
let right_prefix = format!("{child}{PIPE}");
for child in &node.ordering {
match child {
LogCallOrder::Log(index) => {
let mut log = String::new();
write!(log, "{}", node.logs[*index])?;

let log = node.logs[*index].to_string();
// Prepend our tree structure symbols to each line of the displayed log
log.lines().enumerate().try_for_each(|(i, line)| {
writeln!(
writer,
f,
"{}{}",
if i == 0 { &left_prefix } else { &right_prefix },
line
)
})?;
}
LogCallOrder::Call(index) => {
inner(
arena,
writer,
node.children[*index],
&left_prefix,
&right_prefix,
verbose,
)?;
inner(arena, f, node.children[*index], &left_prefix, &right_prefix)?;
}
}
}

// Display trace return data
let color = trace_color(&node.trace);
write!(writer, "{child}{EDGE}{}", color.paint(RETURN))?;
write!(f, "{child}{EDGE}{}", color.paint(RETURN))?;
if node.trace.created() {
match &node.trace.output {
TraceRetData::Raw(bytes) => {
writeln!(writer, "{} bytes of code", bytes.len())?;
writeln!(f, "{} bytes of code", bytes.len())?;
}
TraceRetData::Decoded(val) => {
writeln!(writer, "{val}")?;
writeln!(f, "{val}")?;
}
}
} else {
writeln!(writer, "{}", node.trace.output)?;
writeln!(f, "{}", node.trace.output)?;
}

Ok(())
}

inner(self, f, 0, " ", " ", f.alternate())
inner(self, f, 0, " ", " ")
}
}

/// A raw or decoded log.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RawOrDecodedLog {
pub enum TraceLog {
/// A raw log
Raw(RawLog),
/// A decoded log.
Expand All @@ -280,10 +268,10 @@ pub enum RawOrDecodedLog {
Decoded(String, Vec<(String, String)>),
}

impl fmt::Display for RawOrDecodedLog {
impl fmt::Display for TraceLog {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RawOrDecodedLog::Raw(log) => {
TraceLog::Raw(log) => {
for (i, topic) in log.topics().iter().enumerate() {
writeln!(
f,
Expand All @@ -295,7 +283,7 @@ impl fmt::Display for RawOrDecodedLog {

write!(f, " data: {}", Paint::cyan(hex::encode_prefixed(&log.data)))
}
RawOrDecodedLog::Decoded(name, params) => {
TraceLog::Decoded(name, params) => {
let params = params
.iter()
.map(|(name, value)| format!("{name}: {value}"))
Expand Down Expand Up @@ -484,8 +472,6 @@ pub struct CallTrace {
pub steps: Vec<CallTraceStep>,
}

// === impl CallTrace ===

impl CallTrace {
/// Whether this is a contract creation or not
pub fn created(&self) -> bool {
Expand Down Expand Up @@ -516,8 +502,8 @@ impl Default for CallTrace {

impl fmt::Display for CallTrace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let address = self.address.to_checksum(None);
write!(f, "[{}] ", self.gas_cost)?;
let address = self.address.to_checksum(None);
if self.created() {
write!(
f,
Expand All @@ -530,10 +516,13 @@ impl fmt::Display for CallTrace {
} else {
let (func_name, inputs) = match &self.data {
TraceCallData::Raw(bytes) => {
// We assume that the fallback function (`data.len() < 4`) counts as decoded
// calldata
let (selector, data) = bytes.split_at(4);
(hex::encode(selector), hex::encode(data))
debug!(target: "evm::traces", trace=?self, "unhandled raw calldata");
if bytes.len() < 4 {
("fallback".into(), hex::encode(bytes))
} else {
let (selector, data) = bytes.split_at(4);
(hex::encode(selector), hex::encode(data))
}
}
TraceCallData::Decoded { signature, args } => {
let name = signature.split('(').next().unwrap();
Expand Down
126 changes: 3 additions & 123 deletions crates/evm/traces/src/node.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
use crate::{
utils, utils::decode_cheatcode_outputs, CallTrace, LogCallOrder, RawOrDecodedLog,
TraceCallData, TraceRetData,
};
use alloy_dyn_abi::{FunctionExt, JsonAbiExt};
use alloy_json_abi::{Function, JsonAbi as Abi};
use alloy_primitives::Address;
use crate::{CallTrace, LogCallOrder, TraceLog};
use ethers::types::{Action, Call, CallResult, Create, CreateResult, Res, Suicide};
use foundry_common::SELECTOR_LEN;
use foundry_evm_core::{constants::CHEATCODE_ADDRESS, decode, utils::CallKind};
use foundry_evm_core::utils::CallKind;
use foundry_utils::types::ToEthers;
use revm::interpreter::InstructionResult;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// A node in the arena
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
Expand All @@ -26,7 +18,7 @@ pub struct CallTraceNode {
pub trace: CallTrace,
/// Logs
#[serde(skip)]
pub logs: Vec<RawOrDecodedLog>,
pub logs: Vec<TraceLog>,
/// Ordering of child calls and logs
pub ordering: Vec<LogCallOrder>,
}
Expand Down Expand Up @@ -88,116 +80,4 @@ impl CallTraceNode {
}),
}
}

/// Decode a regular function
pub fn decode_function(
&mut self,
funcs: &[Function],
labels: &HashMap<Address, String>,
errors: &Abi,
verbosity: u8,
) {
debug_assert!(!funcs.is_empty(), "requires at least 1 func");
// This is safe because (1) we would not have an entry for the given
// selector if no functions with that selector were added and (2) the
// same selector implies the function has
// the same name and inputs.
let func = &funcs[0];

if let TraceCallData::Raw(ref bytes) = self.trace.data {
let args = if bytes.len() >= SELECTOR_LEN {
if self.trace.address == CHEATCODE_ADDRESS {
// Try to decode cheatcode inputs in a more custom way
utils::decode_cheatcode_inputs(func, bytes, errors, verbosity).unwrap_or_else(
|| {
func.abi_decode_input(&bytes[SELECTOR_LEN..], false)
.expect("bad function input decode")
.iter()
.map(|token| utils::label(token, labels))
.collect()
},
)
} else {
match func.abi_decode_input(&bytes[SELECTOR_LEN..], false) {
Ok(v) => v.iter().map(|token| utils::label(token, labels)).collect(),
Err(_) => Vec::new(),
}
}
} else {
Vec::new()
};

// add signature to decoded calls for better calls filtering
self.trace.data = TraceCallData::Decoded { signature: func.signature(), args };

if let TraceRetData::Raw(bytes) = &self.trace.output {
if self.trace.success {
if self.trace.address == CHEATCODE_ADDRESS {
if let Some(decoded) = funcs
.iter()
.find_map(|func| decode_cheatcode_outputs(func, bytes, verbosity))
{
self.trace.output = TraceRetData::Decoded(decoded);
return
}
}

if let Some(tokens) =
funcs.iter().find_map(|func| func.abi_decode_output(bytes, false).ok())
{
// Functions coming from an external database do not have any outputs
// specified, and will lead to returning an empty list of tokens.
if !tokens.is_empty() {
self.trace.output = TraceRetData::Decoded(
tokens
.iter()
.map(|token| utils::label(token, labels))
.collect::<Vec<_>>()
.join(", "),
);
}
}
} else {
self.trace.output = TraceRetData::Decoded(decode::decode_revert(
bytes,
Some(errors),
Some(self.trace.status),
));
}
}
}
}

/// Decode the node's tracing data for the given precompile function
pub fn decode_precompile(
&mut self,
precompile_fn: &Function,
labels: &HashMap<Address, String>,
) {
if let TraceCallData::Raw(ref bytes) = self.trace.data {
self.trace.label = Some("PRECOMPILE".to_string());
self.trace.data = TraceCallData::Decoded {
signature: precompile_fn.signature(),
args: precompile_fn.abi_decode_input(bytes, false).map_or_else(
|_| vec![hex::encode(bytes)],
|tokens| tokens.iter().map(|token| utils::label(token, labels)).collect(),
),
};

if let TraceRetData::Raw(ref bytes) = self.trace.output {
self.trace.output = TraceRetData::Decoded(
precompile_fn.abi_decode_output(bytes, false).map_or_else(
|_| hex::encode(bytes),
|tokens| {
tokens
.iter()
.map(|token| utils::label(token, labels))
.collect::<Vec<_>>()
.join(", ")
},
),
);
}
}
}
}
Loading