From 3b2b1febd96fe15eef6eb8eb697a9ca09a156656 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Fri, 21 Jun 2024 03:37:32 +0300 Subject: [PATCH] [wip] feat: identify internal function invocations in traces --- Cargo.lock | 2 +- Cargo.toml | 2 +- crates/cast/bin/cmd/call.rs | 8 +- crates/cast/bin/cmd/run.rs | 8 +- crates/cli/src/utils/cmd.rs | 30 ++- crates/debugger/src/identifier.rs | 249 ++++++++++++++++++ crates/debugger/src/lib.rs | 2 + crates/debugger/src/tui/builder.rs | 60 ++--- crates/debugger/src/tui/draw.rs | 72 +---- crates/debugger/src/tui/mod.rs | 30 +-- .../evm/evm/src/executors/invariant/replay.rs | 2 +- crates/evm/evm/src/executors/mod.rs | 4 +- crates/evm/evm/src/executors/tracing.rs | 3 +- crates/evm/evm/src/inspectors/stack.rs | 38 ++- crates/evm/traces/src/lib.rs | 131 +++++++-- crates/forge/bin/cmd/test/mod.rs | 58 +++- crates/forge/src/runner.rs | 6 +- crates/script/src/execute.rs | 6 +- crates/verify/src/bytecode.rs | 2 +- 19 files changed, 505 insertions(+), 208 deletions(-) create mode 100644 crates/debugger/src/identifier.rs diff --git a/Cargo.lock b/Cargo.lock index 0e37133b9215..4cb2cdaf0a0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6945,7 +6945,7 @@ dependencies = [ [[package]] name = "revm-inspectors" version = "0.1.0" -source = "git+https://github.com/paradigmxyz/revm-inspectors?rev=4fe17f0#4fe17f08797450d9d5df315e724d14c9f3749b3f" +source = "git+https://github.com/klkvr/evm-inspectors?rev=f9f953e#f9f953ee712812a55a8ca874fb18353891242f67" dependencies = [ "alloy-primitives", "alloy-rpc-types", diff --git a/Cargo.toml b/Cargo.toml index 48178e69ce73..d9049140a8f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,7 +160,7 @@ foundry-compilers = { version = "0.8.0", default-features = false } # no default features to avoid c-kzg revm = { version = "9.0.0", default-features = false } revm-primitives = { version = "4.0.0", default-features = false } -revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", rev = "4fe17f0", features = [ +revm-inspectors = { git = "https://github.com/klkvr/evm-inspectors", rev = "f9f953e", features = [ "serde", ] } diff --git a/crates/cast/bin/cmd/call.rs b/crates/cast/bin/cmd/call.rs index 0499f5e7b275..25bb08bb1610 100644 --- a/crates/cast/bin/cmd/call.rs +++ b/crates/cast/bin/cmd/call.rs @@ -43,6 +43,9 @@ pub struct CallArgs { #[arg(long, requires = "trace")] debug: bool, + #[arg(long, requires = "trace")] + decode_internal: bool, + /// Labels to apply to the traces; format: `address:label`. /// Can only be used with `--trace`. #[arg(long, requires = "trace")] @@ -106,6 +109,7 @@ impl CallArgs { trace, evm_version, debug, + decode_internal, labels, data, } = self; @@ -159,7 +163,7 @@ impl CallArgs { } let (env, fork, chain) = TracingExecutor::get_fork_material(&config, evm_opts).await?; - let mut executor = TracingExecutor::new(env, fork, evm_version, debug); + let mut executor = TracingExecutor::new(env, fork, evm_version, debug, decode_internal); let value = tx.value.unwrap_or_default(); let input = tx.inner.input.into_input().unwrap_or_default(); @@ -175,7 +179,7 @@ impl CallArgs { ), }; - handle_traces(trace, &config, chain, labels, debug).await?; + handle_traces(trace, &config, chain, labels, debug, decode_internal).await?; return Ok(()); } diff --git a/crates/cast/bin/cmd/run.rs b/crates/cast/bin/cmd/run.rs index c830f5ab1aa4..70b27313d6e3 100644 --- a/crates/cast/bin/cmd/run.rs +++ b/crates/cast/bin/cmd/run.rs @@ -27,6 +27,10 @@ pub struct RunArgs { #[arg(long, short)] debug: bool, + /// Whether to identify internal functions in traces. + #[arg(long)] + decode_internal: bool, + /// Print out opcode traces. #[arg(long, short)] trace_printer: bool, @@ -142,7 +146,7 @@ impl RunArgs { } } - let mut executor = TracingExecutor::new(env.clone(), fork, evm_version, self.debug); + let mut executor = TracingExecutor::new(env.clone(), fork, evm_version, self.debug, self.decode_internal); let mut env = EnvWithHandlerCfg::new_with_spec_id(Box::new(env.clone()), executor.spec_id()); @@ -220,7 +224,7 @@ impl RunArgs { } }; - handle_traces(result, &config, chain, self.label, self.debug).await?; + handle_traces(result, &config, chain, self.label, self.debug, self.decode_internal).await?; Ok(()) } diff --git a/crates/cli/src/utils/cmd.rs b/crates/cli/src/utils/cmd.rs index 13da8d01df8d..860f1879d0d2 100644 --- a/crates/cli/src/utils/cmd.rs +++ b/crates/cli/src/utils/cmd.rs @@ -9,14 +9,13 @@ use foundry_compilers::{ Artifact, ProjectCompileOutput, }; use foundry_config::{error::ExtractConfigError, figment::Figment, Chain, Config, NamedChain}; -use foundry_debugger::Debugger; +use foundry_debugger::{DebugTraceIdentifier, Debugger}; use foundry_evm::{ debug::DebugArena, executors::{DeployResult, EvmError, RawCallResult}, opts::EvmOpts, traces::{ - identifier::{EtherscanIdentifier, SignaturesIdentifier}, - render_trace_arena, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, Traces, + identifier::{EtherscanIdentifier, SignaturesIdentifier}, render_trace_arena, render_trace_arena_with_internals, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, Traces }, }; use std::{ @@ -357,6 +356,7 @@ pub async fn handle_traces( chain: Option, labels: Vec, debug: bool, + decode_internal: bool, ) -> Result<()> { let labels = labels.iter().filter_map(|label_str| { let mut iter = label_str.split(':'); @@ -392,23 +392,37 @@ pub async fn handle_traces( }; let mut debugger = Debugger::builder() .debug_arena(result.debug.as_ref().expect("missing debug arena")) - .decoder(&decoder) - .sources(sources) + .identifier(|b| b.decoder(&decoder).sources(sources)) .build(); debugger.try_run()?; } else { - print_traces(&mut result, &decoder).await?; + let identifier = if decode_internal { + let sources = if let Some(etherscan_identifier) = etherscan_identifier { + etherscan_identifier.get_compiled_contracts().await? + } else { + Default::default() + }; + Some(DebugTraceIdentifier::builder().sources(sources).decoder(&decoder).build()) + } else { + None + }; + print_traces(&mut result, &decoder, identifier.as_ref()).await?; } Ok(()) } -pub async fn print_traces(result: &mut TraceResult, decoder: &CallTraceDecoder) -> Result<()> { +pub async fn print_traces(result: &mut TraceResult, decoder: &CallTraceDecoder, identifier: Option<&DebugTraceIdentifier>) -> Result<()> { let traces = result.traces.as_ref().expect("No traces found"); println!("Traces:"); for (_, arena) in traces { - println!("{}", render_trace_arena(arena, decoder).await?); + let arena = if let Some(identifier) = identifier { + render_trace_arena_with_internals(arena, decoder, &identifier.identify_arena(arena)).await? + } else { + render_trace_arena(arena, decoder).await? + }; + println!("{}", arena); } println!(); diff --git a/crates/debugger/src/identifier.rs b/crates/debugger/src/identifier.rs new file mode 100644 index 000000000000..43d041961051 --- /dev/null +++ b/crates/debugger/src/identifier.rs @@ -0,0 +1,249 @@ +use alloy_primitives::Address; +use foundry_common::{compile::ContractSources, get_contract_name}; +use foundry_compilers::{ + artifacts::sourcemap::{Jump, SourceElement}, + multi::MultiCompilerLanguage, +}; +use foundry_evm_core::utils::PcIcMap; +use foundry_evm_traces::{CallTraceArena, CallTraceDecoder, CallTraceNode, DecodedTraceStep}; +use revm::interpreter::OpCode; +use std::collections::HashMap; + +pub struct DebugTraceIdentifier { + /// Mapping of contract address to identified contract name. + identified_contracts: HashMap, + /// Source map of contract sources + contracts_sources: ContractSources, + /// A mapping of source -> (PC -> IC map for deploy code, PC -> IC map for runtime code) + pc_ic_maps: HashMap, +} + +impl DebugTraceIdentifier { + pub fn builder() -> DebugTraceIdentifierBuilder { + DebugTraceIdentifierBuilder::default() + } + + pub fn new( + identified_contracts: HashMap, + contracts_sources: ContractSources, + ) -> Self { + let pc_ic_maps = contracts_sources + .entries() + .filter_map(|(name, artifact, _)| { + Some(( + name.to_owned(), + ( + PcIcMap::new(artifact.bytecode.bytecode.bytes()?), + PcIcMap::new(artifact.bytecode.deployed_bytecode.bytes()?), + ), + )) + }) + .collect(); + Self { identified_contracts, contracts_sources, pc_ic_maps } + } + + pub fn identify( + &self, + address: &Address, + pc: usize, + init_code: bool, + ) -> core::result::Result<(SourceElement, &str, &str), String> { + let Some(contract_name) = self.identified_contracts.get(address) else { + return Err(format!("Unknown contract at address {address}")); + }; + + let Some(mut files_source_code) = self.contracts_sources.get_sources(contract_name) else { + return Err(format!("No source map index for contract {contract_name}")); + }; + + let Some((create_map, rt_map)) = self.pc_ic_maps.get(contract_name) else { + return Err(format!("No PC-IC maps for contract {contract_name}")); + }; + + let Some((source_element, source_code, source_file)) = + files_source_code.find_map(|(artifact, source)| { + let bytecode = if init_code { + &artifact.bytecode.bytecode + } else { + artifact.bytecode.deployed_bytecode.bytecode.as_ref()? + }; + let source_map = bytecode.source_map()?.expect("failed to parse"); + + let pc_ic_map = if init_code { create_map } else { rt_map }; + let ic = pc_ic_map.get(pc)?; + + // Solc indexes source maps by instruction counter, but Vyper indexes by program + // counter. + let source_element = if matches!(source.language, MultiCompilerLanguage::Solc(_)) { + source_map.get(ic)? + } else { + source_map.get(pc)? + }; + // if the source element has an index, find the sourcemap for that index + let res = source_element + .index() + // if index matches current file_id, return current source code + .and_then(|index| { + (index == artifact.file_id) + .then(|| (source_element.clone(), source.source.as_str(), &source.name)) + }) + .or_else(|| { + // otherwise find the source code for the element's index + self.contracts_sources + .sources_by_id + .get(&artifact.build_id)? + .get(&source_element.index()?) + .map(|source| { + (source_element.clone(), source.source.as_str(), &source.name) + }) + }); + + res + }) + else { + return Err(format!("No source map for contract {contract_name}")); + }; + + Ok((source_element, source_code, source_file)) + } + + pub fn identify_arena(&self, arena: &CallTraceArena) -> Vec>> { + arena.nodes().iter().map(move |node| self.identify_node_steps(node)).collect() + } + + pub fn identify_node_steps(&self, node: &CallTraceNode) -> Vec> { + let mut stack = Vec::new(); + let mut identified = Vec::new(); + + // Flag marking whether previous instruction was a jump into function. + // If it was, we expect next instruction to be a JUMPDEST with source location pointing to + // the function. + let mut prev_step_jump_in = false; + for (step_idx, step) in node.trace.steps.iter().enumerate() { + // We are only interested in JUMPs. + if step.op != OpCode::JUMP && step.op != OpCode::JUMPI && step.op != OpCode::JUMPDEST { + continue; + } + + // Resolve source map if possible. + let Ok((source_element, source_code, _)) = + self.identify(&node.trace.address, step.pc, node.trace.kind.is_any_create()) + else { + prev_step_jump_in = false; + continue; + }; + + // Get slice of the source code that corresponds to the current step. + let source_part = { + let start = source_element.offset() as usize; + let end = start + source_element.length() as usize; + &source_code[start..end] + }; + + // If previous step was a jump record source location at JUMPDEST. + if prev_step_jump_in { + if step.op == OpCode::JUMPDEST { + if let Some(name) = parse_function_name(source_part) { + stack.push((name, step_idx)); + } + }; + prev_step_jump_in = false; + } + + match source_element.jump() { + // Source location is collected on the next step. + Jump::In => prev_step_jump_in = true, + Jump::Out => { + // Find index matching the beginning of this function + if let Some(name) = parse_function_name(source_part) { + if let Some((i, _)) = + stack.iter().enumerate().rfind(|(_, (n, _))| n == &name) + { + // We've found a match, remove all records between start and end, those + // are considered invalid. + let (_, start_idx) = stack.split_off(i)[0]; + + let gas_used = node.trace.steps[start_idx].gas_remaining as i64 - + node.trace.steps[step_idx].gas_remaining as i64; + + identified.push(DecodedTraceStep { + start_step_idx: start_idx, + end_step_idx: step_idx, + function_name: name, + gas_used, + }); + } + } + } + _ => {} + }; + } + + // Sort by start step index. + identified.sort_by_key(|i| i.start_step_idx); + + identified + } +} + +/// [DebugTraceIdentifier] builder +#[derive(Debug, Default)] +#[must_use = "builders do nothing unless you call `build` on them"] +pub struct DebugTraceIdentifierBuilder { + /// Identified contracts. + identified_contracts: HashMap, + /// Map of source files. + sources: ContractSources, +} + +impl DebugTraceIdentifierBuilder { + /// Extends the identified contracts from multiple decoders. + #[inline] + pub fn decoders(mut self, decoders: &[CallTraceDecoder]) -> Self { + for decoder in decoders { + self = self.decoder(decoder); + } + self + } + + /// Extends the identified contracts from a decoder. + #[inline] + pub fn decoder(self, decoder: &CallTraceDecoder) -> Self { + let c = decoder.contracts.iter().map(|(k, v)| (*k, get_contract_name(v).to_string())); + self.identified_contracts(c) + } + + /// Extends the identified contracts. + #[inline] + pub fn identified_contracts( + mut self, + identified_contracts: impl IntoIterator, + ) -> Self { + self.identified_contracts.extend(identified_contracts); + self + } + + /// Sets the sources for the debugger. + #[inline] + pub fn sources(mut self, sources: ContractSources) -> Self { + self.sources = sources; + self + } + + /// Builds the [DebugTraceIdentifier]. + #[inline] + pub fn build(self) -> DebugTraceIdentifier { + let Self { identified_contracts, sources } = self; + DebugTraceIdentifier::new(identified_contracts, sources) + } +} + +fn parse_function_name(source: &str) -> Option<&str> { + if !source.starts_with("function") { + return None; + } + if !source.contains("internal") && !source.contains("private") { + return None; + } + Some(source.split_once("function")?.1.split('(').next()?.trim()) +} diff --git a/crates/debugger/src/lib.rs b/crates/debugger/src/lib.rs index ed5da934271a..052dbb1aab47 100644 --- a/crates/debugger/src/lib.rs +++ b/crates/debugger/src/lib.rs @@ -10,5 +10,7 @@ extern crate tracing; mod op; +mod identifier; mod tui; +pub use identifier::DebugTraceIdentifier; pub use tui::{Debugger, DebuggerBuilder, ExitReason}; diff --git a/crates/debugger/src/tui/builder.rs b/crates/debugger/src/tui/builder.rs index 6289b0b8814f..ea80335a39b9 100644 --- a/crates/debugger/src/tui/builder.rs +++ b/crates/debugger/src/tui/builder.rs @@ -1,11 +1,8 @@ //! TUI debugger builder. -use crate::Debugger; -use alloy_primitives::Address; -use foundry_common::{compile::ContractSources, evm::Breakpoints, get_contract_name}; +use crate::{identifier::DebugTraceIdentifierBuilder, Debugger}; +use foundry_common::evm::Breakpoints; use foundry_evm_core::debug::{DebugArena, DebugNodeFlat}; -use foundry_evm_traces::CallTraceDecoder; -use std::collections::HashMap; /// Debugger builder. #[derive(Debug, Default)] @@ -13,10 +10,8 @@ use std::collections::HashMap; pub struct DebuggerBuilder { /// Debug traces returned from the EVM execution. debug_arena: Vec, - /// Identified contracts. - identified_contracts: HashMap, - /// Map of source files. - sources: ContractSources, + /// Builder for [DebugTraceIdentifier]. + identifier: DebugTraceIdentifierBuilder, /// Map of the debugger breakpoints. breakpoints: Breakpoints, } @@ -28,6 +23,16 @@ impl DebuggerBuilder { Self::default() } + /// Configures the [DebugTraceIdentifier]. + #[inline] + pub fn identifier( + mut self, + f: impl FnOnce(DebugTraceIdentifierBuilder) -> DebugTraceIdentifierBuilder, + ) -> Self { + self.identifier = f(self.identifier); + self + } + /// Extends the debug arena. #[inline] pub fn debug_arenas(mut self, arena: &[DebugArena]) -> Self { @@ -44,39 +49,6 @@ impl DebuggerBuilder { self } - /// Extends the identified contracts from multiple decoders. - #[inline] - pub fn decoders(mut self, decoders: &[CallTraceDecoder]) -> Self { - for decoder in decoders { - self = self.decoder(decoder); - } - self - } - - /// Extends the identified contracts from a decoder. - #[inline] - pub fn decoder(self, decoder: &CallTraceDecoder) -> Self { - let c = decoder.contracts.iter().map(|(k, v)| (*k, get_contract_name(v).to_string())); - self.identified_contracts(c) - } - - /// Extends the identified contracts. - #[inline] - pub fn identified_contracts( - mut self, - identified_contracts: impl IntoIterator, - ) -> Self { - self.identified_contracts.extend(identified_contracts); - self - } - - /// Sets the sources for the debugger. - #[inline] - pub fn sources(mut self, sources: ContractSources) -> Self { - self.sources = sources; - self - } - /// Sets the breakpoints for the debugger. #[inline] pub fn breakpoints(mut self, breakpoints: Breakpoints) -> Self { @@ -87,7 +59,7 @@ impl DebuggerBuilder { /// Builds the debugger. #[inline] pub fn build(self) -> Debugger { - let Self { debug_arena, identified_contracts, sources, breakpoints } = self; - Debugger::new(debug_arena, identified_contracts, sources, breakpoints) + let Self { debug_arena, identifier, breakpoints } = self; + Debugger::new(debug_arena, identifier.build(), breakpoints) } } diff --git a/crates/debugger/src/tui/draw.rs b/crates/debugger/src/tui/draw.rs index 79533ad4caae..72bb7b9a41ff 100644 --- a/crates/debugger/src/tui/draw.rs +++ b/crates/debugger/src/tui/draw.rs @@ -3,9 +3,7 @@ use super::context::{BufferKind, DebuggerContext}; use crate::op::OpcodeParam; use alloy_primitives::U256; -use foundry_compilers::{ - artifacts::sourcemap::SourceElement, compilers::multi::MultiCompilerLanguage, -}; +use foundry_compilers::artifacts::sourcemap::SourceElement; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, @@ -342,69 +340,11 @@ impl DebuggerContext<'_> { /// Returns source map, source code and source name of the current line. fn src_map(&self) -> Result<(SourceElement, &str, &str), String> { - let address = self.address(); - let Some(contract_name) = self.debugger.identified_contracts.get(address) else { - return Err(format!("Unknown contract at address {address}")); - }; - - let Some(mut files_source_code) = - self.debugger.contracts_sources.get_sources(contract_name) - else { - return Err(format!("No source map index for contract {contract_name}")); - }; - - let Some((create_map, rt_map)) = self.debugger.pc_ic_maps.get(contract_name) else { - return Err(format!("No PC-IC maps for contract {contract_name}")); - }; - - let is_create = matches!(self.call_kind(), CallKind::Create | CallKind::Create2); - let pc = self.current_step().pc; - let Some((source_element, source_code, source_file)) = - files_source_code.find_map(|(artifact, source)| { - let bytecode = if is_create { - &artifact.bytecode.bytecode - } else { - artifact.bytecode.deployed_bytecode.bytecode.as_ref()? - }; - let source_map = bytecode.source_map()?.expect("failed to parse"); - - let pc_ic_map = if is_create { create_map } else { rt_map }; - let ic = pc_ic_map.get(pc)?; - - // Solc indexes source maps by instruction counter, but Vyper indexes by program - // counter. - let source_element = if matches!(source.language, MultiCompilerLanguage::Solc(_)) { - source_map.get(ic)? - } else { - source_map.get(pc)? - }; - // if the source element has an index, find the sourcemap for that index - let res = source_element - .index() - // if index matches current file_id, return current source code - .and_then(|index| { - (index == artifact.file_id) - .then(|| (source_element.clone(), source.source.as_str(), &source.name)) - }) - .or_else(|| { - // otherwise find the source code for the element's index - self.debugger - .contracts_sources - .sources_by_id - .get(&artifact.build_id)? - .get(&source_element.index()?) - .map(|source| { - (source_element.clone(), source.source.as_str(), &source.name) - }) - }); - - res - }) - else { - return Err(format!("No source map for contract {contract_name}")); - }; - - Ok((source_element, source_code, source_file)) + self.debugger.identifier.identify( + self.address(), + self.current_step().pc, + self.call_kind().is_any_create(), + ) } fn draw_op_list(&self, f: &mut Frame<'_>, area: Rect) { diff --git a/crates/debugger/src/tui/mod.rs b/crates/debugger/src/tui/mod.rs index c810440e5b13..e0b01237c666 100644 --- a/crates/debugger/src/tui/mod.rs +++ b/crates/debugger/src/tui/mod.rs @@ -1,20 +1,19 @@ //! The TUI implementation. -use alloy_primitives::Address; +use crate::DebugTraceIdentifier; use crossterm::{ event::{self, DisableMouseCapture, EnableMouseCapture, Event}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use eyre::Result; -use foundry_common::{compile::ContractSources, evm::Breakpoints}; -use foundry_evm_core::{debug::DebugNodeFlat, utils::PcIcMap}; +use foundry_common::evm::Breakpoints; +use foundry_evm_core::debug::DebugNodeFlat; use ratatui::{ backend::{Backend, CrosstermBackend}, Terminal, }; use std::{ - collections::{BTreeMap, HashMap}, io, ops::ControlFlow, sync::{mpsc, Arc}, @@ -42,11 +41,7 @@ pub enum ExitReason { /// The TUI debugger. pub struct Debugger { debug_arena: Vec, - identified_contracts: HashMap, - /// Source map of contract sources - contracts_sources: ContractSources, - /// A mapping of source -> (PC -> IC map for deploy code, PC -> IC map for runtime code) - pc_ic_maps: BTreeMap, + identifier: DebugTraceIdentifier, breakpoints: Breakpoints, } @@ -60,23 +55,10 @@ impl Debugger { /// Creates a new debugger. pub fn new( debug_arena: Vec, - identified_contracts: HashMap, - contracts_sources: ContractSources, + identifier: DebugTraceIdentifier, breakpoints: Breakpoints, ) -> Self { - let pc_ic_maps = contracts_sources - .entries() - .filter_map(|(name, artifact, _)| { - Some(( - name.to_owned(), - ( - PcIcMap::new(artifact.bytecode.bytecode.bytes()?), - PcIcMap::new(artifact.bytecode.deployed_bytecode.bytes()?), - ), - )) - }) - .collect(); - Self { debug_arena, identified_contracts, contracts_sources, pc_ic_maps, breakpoints } + Self { debug_arena, identifier, breakpoints } } /// Starts the debugger TUI. Terminates the current process on failure or user exit. diff --git a/crates/evm/evm/src/executors/invariant/replay.rs b/crates/evm/evm/src/executors/invariant/replay.rs index cf9fa12e81fc..18f0b5222a3d 100644 --- a/crates/evm/evm/src/executors/invariant/replay.rs +++ b/crates/evm/evm/src/executors/invariant/replay.rs @@ -33,7 +33,7 @@ pub fn replay_run( inputs: &[BasicTxDetails], ) -> Result> { // We want traces for a failed case. - executor.set_tracing(true); + executor.set_tracing(true, false); let mut counterexample_sequence = vec![]; diff --git a/crates/evm/evm/src/executors/mod.rs b/crates/evm/evm/src/executors/mod.rs index 58057c93f675..83ccbb64f745 100644 --- a/crates/evm/evm/src/executors/mod.rs +++ b/crates/evm/evm/src/executors/mod.rs @@ -172,8 +172,8 @@ impl Executor { } #[inline] - pub fn set_tracing(&mut self, tracing: bool) -> &mut Self { - self.inspector.tracing(tracing); + pub fn set_tracing(&mut self, tracing: bool, debug: bool) -> &mut Self { + self.inspector.tracing(tracing, debug); self } diff --git a/crates/evm/evm/src/executors/tracing.rs b/crates/evm/evm/src/executors/tracing.rs index 08c5d92ef5b2..29373ad66865 100644 --- a/crates/evm/evm/src/executors/tracing.rs +++ b/crates/evm/evm/src/executors/tracing.rs @@ -16,13 +16,14 @@ impl TracingExecutor { fork: Option, version: Option, debug: bool, + trace_steps: bool, ) -> Self { let db = Backend::spawn(fork); Self { // configures a bare version of the evm executor: no cheatcode inspector is enabled, // tracing will be enabled only for the targeted transaction executor: ExecutorBuilder::new() - .inspectors(|stack| stack.trace(true).debug(debug)) + .inspectors(|stack| stack.trace(true).debug(debug).debug_trace(trace_steps)) .spec(evm_spec_id(&version.unwrap_or_default())) .build(env, db), } diff --git a/crates/evm/evm/src/inspectors/stack.rs b/crates/evm/evm/src/inspectors/stack.rs index a7e08ab636ae..3573c215eb8e 100644 --- a/crates/evm/evm/src/inspectors/stack.rs +++ b/crates/evm/evm/src/inspectors/stack.rs @@ -42,6 +42,8 @@ pub struct InspectorStackBuilder { pub fuzzer: Option, /// Whether to enable tracing. pub trace: Option, + /// Whether to enable steps tracking in the tracer. + pub debug_trace: Option, /// Whether to enable the debugger. pub debug: Option, /// Whether logs should be collected. @@ -135,6 +137,13 @@ impl InspectorStackBuilder { self } + /// Set whether to enable steps tracking in the tracer. + #[inline] + pub fn debug_trace(mut self, yes: bool) -> Self { + self.debug_trace = Some(yes); + self + } + /// Set whether to enable the call isolation. /// For description of call isolation, see [`InspectorStack::enable_isolation`]. #[inline] @@ -157,6 +166,7 @@ impl InspectorStackBuilder { print, chisel_state, enable_isolation, + debug_trace, } = self; let mut stack = InspectorStack::new(); @@ -174,7 +184,7 @@ impl InspectorStackBuilder { stack.collect_logs(logs.unwrap_or(true)); stack.enable_debugger(debug.unwrap_or(false)); stack.print(print.unwrap_or(false)); - stack.tracing(trace.unwrap_or(false)); + stack.tracing(trace.unwrap_or(false), debug_trace.unwrap_or(false)); stack.enable_isolation(enable_isolation); @@ -388,17 +398,21 @@ impl InspectorStack { /// Set whether to enable the tracer. #[inline] - pub fn tracing(&mut self, yes: bool) { - self.tracer = yes.then(|| { - TracingInspector::new(TracingInspectorConfig { - record_steps: false, - record_memory_snapshots: false, - record_stack_snapshots: StackSnapshotType::None, - record_state_diff: false, - exclude_precompile_calls: false, - record_logs: true, - }) - }); + pub fn tracing(&mut self, yes: bool, debug: bool) { + if self.tracer.is_none() && yes || + !self.tracer.as_ref().map_or(false, |t| t.config().record_steps) && debug + { + self.tracer = Some({ + TracingInspector::new(TracingInspectorConfig { + record_steps: debug, + record_memory_snapshots: false, + record_stack_snapshots: StackSnapshotType::None, + record_state_diff: false, + exclude_precompile_calls: false, + record_logs: true, + }) + }); + } } /// Collects all the data gathered during inspection into a single struct. diff --git a/crates/evm/traces/src/lib.rs b/crates/evm/traces/src/lib.rs index d352abfc8f43..7ffa68623b21 100644 --- a/crates/evm/traces/src/lib.rs +++ b/crates/evm/traces/src/lib.rs @@ -59,68 +59,132 @@ pub enum DecodedCallLog<'a> { Decoded(String, Vec<(String, String)>), } +#[derive(Debug, Clone)] +pub struct DecodedTraceStep<'a> { + pub start_step_idx: usize, + pub end_step_idx: usize, + pub function_name: &'a str, + pub gas_used: i64, +} + const PIPE: &str = " │ "; const EDGE: &str = " └─ "; const BRANCH: &str = " ├─ "; const CALL: &str = "→ "; const RETURN: &str = "← "; -/// Render a collection of call traces. -/// -/// The traces will be decoded using the given decoder, if possible. -pub async fn render_trace_arena( +pub async fn render_trace_arena_with_internals<'a>( arena: &CallTraceArena, decoder: &CallTraceDecoder, + identified_internals: &'a [Vec>], ) -> Result { decoder.prefetch_signatures(arena.nodes()).await; - fn inner<'a>( + fn render_items<'a>( arena: &'a [CallTraceNode], decoder: &'a CallTraceDecoder, + identified_internals: &'a [Vec>], s: &'a mut String, - idx: usize, + node_idx: usize, + mut ordering_idx: usize, + internal_end_step_idx: Option, left: &'a str, - child: &'a str, - ) -> BoxFuture<'a, Result<(), std::fmt::Error>> { + right: &'a str, + ) -> BoxFuture<'a, Result> { async move { - let node = &arena[idx]; + let node = &arena[node_idx]; - // Display trace header - let (trace, return_data) = render_trace(&node.trace, decoder).await?; - writeln!(s, "{left}{trace}")?; - - // Display logs and subcalls - let left_prefix = format!("{child}{BRANCH}"); - let right_prefix = format!("{child}{PIPE}"); - for child in &node.ordering { + while ordering_idx < node.ordering.len() { + let child = &node.ordering[ordering_idx]; match child { LogCallOrder::Log(index) => { let log = render_trace_log(&node.logs[*index], decoder).await?; // Prepend our tree structure symbols to each line of the displayed log log.lines().enumerate().try_for_each(|(i, line)| { - writeln!( - s, - "{}{}", - if i == 0 { &left_prefix } else { &right_prefix }, - line - ) + writeln!(s, "{}{}", if i == 0 { left } else { right }, line) })?; } LogCallOrder::Call(index) => { inner( arena, decoder, + identified_internals, s, node.children[*index], - &left_prefix, - &right_prefix, + left, + right, ) .await?; } + LogCallOrder::Step(step_idx) => { + if let Some(internal_step_end_idx) = internal_end_step_idx { + if *step_idx >= internal_step_end_idx { + return Ok(ordering_idx); + } + } + if let Some(decoded) = identified_internals[node_idx] + .iter() + .find(|d| *step_idx == d.start_step_idx) + { + writeln!(s, "{left}[{}] {}", decoded.gas_used, decoded.function_name)?; + let left_prefix = format!("{right}{BRANCH}"); + let right_prefix = format!("{right}{PIPE}"); + ordering_idx = render_items( + arena, + decoder, + identified_internals, + s, + node_idx, + ordering_idx + 1, + Some(decoded.end_step_idx), + &left_prefix, + &right_prefix, + ) + .await?; + + writeln!(s, "{right}{EDGE}{}", RETURN,)?; + } + } } + ordering_idx += 1; } + Ok(ordering_idx) + } + .boxed() + } + + fn inner<'a>( + arena: &'a [CallTraceNode], + decoder: &'a CallTraceDecoder, + identified_internals: &'a [Vec>], + s: &'a mut String, + idx: usize, + left: &'a str, + child: &'a str, + ) -> BoxFuture<'a, Result<(), std::fmt::Error>> { + async move { + let node = &arena[idx]; + + // Display trace header + let (trace, return_data) = render_trace(&node.trace, decoder).await?; + writeln!(s, "{left}{trace}")?; + + // Display logs and subcalls + render_items( + arena, + decoder, + identified_internals, + s, + idx, + 0, + None, + &format!("{child}{BRANCH}"), + &format!("{child}{PIPE}"), + ) + .await?; + // Display trace return data let color = trace_color(&node.trace); write!( @@ -145,10 +209,25 @@ pub async fn render_trace_arena( } let mut s = String::new(); - inner(arena.nodes(), decoder, &mut s, 0, " ", " ").await?; + inner(arena.nodes(), decoder, identified_internals, &mut s, 0, " ", " ").await?; Ok(s) } +/// Render a collection of call traces. +/// +/// The traces will be decoded using the given decoder, if possible. +pub async fn render_trace_arena( + arena: &CallTraceArena, + decoder: &CallTraceDecoder, +) -> Result { + render_trace_arena_with_internals( + arena, + decoder, + &std::iter::repeat(Vec::new()).take(arena.nodes().len()).collect::>(), + ) + .await +} + /// Render a call trace. /// /// The trace will be decoded using the given decoder, if possible. diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index 6f65dcfec5b4..32316b0eb3fa 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -1,13 +1,16 @@ use super::{install, test::filter::ProjectPathsAwareFilter, watch::WatchArgs}; use alloy_primitives::U256; use clap::Parser; -use eyre::Result; +use eyre::{OptionExt, Result}; use forge::{ decode::decode_console_logs, gas_report::GasReport, multi_runner::matches_contract, result::{SuiteResult, TestOutcome, TestStatus}, - traces::{identifier::SignaturesIdentifier, CallTraceDecoderBuilder, TraceKind}, + traces::{ + identifier::SignaturesIdentifier, render_trace_arena_with_internals, + CallTraceDecoderBuilder, TraceKind, + }, MultiContractRunner, MultiContractRunnerBuilder, TestFilter, TestOptions, TestOptionsBuilder, }; use foundry_cli::{ @@ -32,7 +35,7 @@ use foundry_config::{ }, get_available_profiles, Config, }; -use foundry_debugger::Debugger; +use foundry_debugger::{DebugTraceIdentifier, Debugger}; use foundry_evm::traces::identifier::TraceIdentifiers; use regex::Regex; use std::{ @@ -76,6 +79,10 @@ pub struct TestArgs { #[arg(long, value_name = "TEST_FUNCTION")] debug: Option, + /// Whether to identify internal functions in traces. + #[arg(long)] + decode_internal: bool, + /// Print a gas report. #[arg(long, env = "FORGE_GAS_REPORT")] gas_report: bool, @@ -293,7 +300,7 @@ impl TestArgs { let env = evm_opts.evm_env().await?; // Prepare the test builder - let should_debug = self.debug.is_some(); + let should_debug = self.debug.is_some() || self.decode_internal; // Clone the output only if we actually need it later for the debugger. let output_clone = should_debug.then(|| output.clone()); @@ -341,16 +348,43 @@ impl TestArgs { Some(&libraries), )?; - // Run the debugger. - let mut builder = Debugger::builder() - .debug_arenas(test_result.debug.as_slice()) - .sources(sources) - .breakpoints(test_result.breakpoints.clone()); - if let Some(decoder) = &outcome.last_run_decoder { + if self.decode_internal { + let mut builder = DebugTraceIdentifier::builder().sources(sources); + let Some(decoder) = &outcome.last_run_decoder else { + eyre::bail!("Missing decoder for debugging"); + }; builder = builder.decoder(decoder); + + let identifier = builder.build(); + let arena = &test_result + .traces + .iter() + .find(|(t, _)| t.is_execution()) + .ok_or_eyre("didn't find execution trace for debugging")? + .1; + + let identified = identifier.identify_arena(arena); + + println!( + "{}", + render_trace_arena_with_internals(arena, &decoder, &identified).await? + ); + } else if self.debug.is_some() { + // Run the debugger. + let builder = Debugger::builder() + .debug_arenas(test_result.debug.as_slice()) + .identifier(|mut builder| { + if let Some(decoder) = &outcome.last_run_decoder { + builder = builder.decoder(decoder); + } + + builder.sources(sources) + }) + .breakpoints(test_result.breakpoints.clone()); + + let mut debugger = builder.build(); + debugger.try_run()?; } - let mut debugger = builder.build(); - debugger.try_run()?; } Ok(outcome) diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index 201494b76408..b83fd57af8bf 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -336,13 +336,13 @@ impl<'a> ContractRunner<'a> { let has_invariants = self.contract.abi.functions().any(|func| func.is_invariant_test()); let tmp_tracing = self.executor.inspector.tracer.is_none() && has_invariants && call_setup; if tmp_tracing { - self.executor.set_tracing(true); + self.executor.set_tracing(true, false); } let setup_time = Instant::now(); let setup = self.setup(call_setup); debug!("finished setting up in {:?}", setup_time.elapsed()); if tmp_tracing { - self.executor.set_tracing(false); + self.executor.set_tracing(false, false); } if setup.reason.is_some() { @@ -817,7 +817,7 @@ impl<'a> ContractRunner<'a> { let mut debug_executor = self.executor.clone(); // turn the debug traces on debug_executor.inspector.enable_debugger(true); - debug_executor.inspector.tracing(true); + debug_executor.inspector.tracing(true, true); let calldata = if let Some(counterexample) = result.counterexample.as_ref() { match counterexample { CounterExample::Single(ce) => ce.calldata.clone(), diff --git a/crates/script/src/execute.rs b/crates/script/src/execute.rs index 2b57468e276b..162b1bc9ba20 100644 --- a/crates/script/src/execute.rs +++ b/crates/script/src/execute.rs @@ -493,8 +493,10 @@ impl PreSimulationState { pub fn run_debugger(&self) -> Result<()> { let mut debugger = Debugger::builder() .debug_arenas(self.execution_result.debug.as_deref().unwrap_or_default()) - .decoder(&self.execution_artifacts.decoder) - .sources(self.build_data.sources.clone()) + .identifier(|b| { + b.decoder(&self.execution_artifacts.decoder) + .sources(self.build_data.sources.clone()) + }) .breakpoints(self.execution_result.breakpoints.clone()) .build(); debugger.try_run()?; diff --git a/crates/verify/src/bytecode.rs b/crates/verify/src/bytecode.rs index cc7a733995e7..80be4f82493a 100644 --- a/crates/verify/src/bytecode.rs +++ b/crates/verify/src/bytecode.rs @@ -284,7 +284,7 @@ impl VerifyBytecodeArgs { TracingExecutor::get_fork_material(&fork_config, evm_opts).await?; let mut executor = - TracingExecutor::new(env.clone(), fork, Some(fork_config.evm_version), false); + TracingExecutor::new(env.clone(), fork, Some(fork_config.evm_version), false, false); env.block.number = U256::from(simulation_block); let block = provider.get_block(simulation_block.into(), true.into()).await?;