From 940393caf13cbf1dcafd44ed45a4be4b4177cc87 Mon Sep 17 00:00:00 2001 From: Piotr Heilman Date: Tue, 12 Mar 2024 12:08:57 +0100 Subject: [PATCH] Refactored debugger to extract TUI abstraction. Added option to dump debugger context to file as json. --- Cargo.lock | 1 + crates/cli/src/utils/cmd.rs | 2 +- crates/common/src/compile.rs | 17 +- crates/debugger/Cargo.toml | 1 + crates/debugger/src/{tui => }/builder.rs | 2 +- crates/debugger/src/context.rs | 16 ++ crates/debugger/src/debugger.rs | 83 ++++++++ crates/debugger/src/file_dumper/mod.rs | 179 ++++++++++++++++++ crates/debugger/src/lib.rs | 12 +- crates/debugger/src/tui/context.rs | 24 +-- crates/debugger/src/tui/draw.rs | 16 +- crates/debugger/src/tui/mod.rs | 66 +------ crates/evm/traces/src/identifier/etherscan.rs | 2 +- crates/forge/bin/cmd/debug.rs | 10 + crates/forge/bin/cmd/test/mod.rs | 2 +- crates/script/src/execute.rs | 17 +- crates/script/src/lib.rs | 14 +- 17 files changed, 367 insertions(+), 97 deletions(-) rename crates/debugger/src/{tui => }/builder.rs (99%) create mode 100644 crates/debugger/src/context.rs create mode 100644 crates/debugger/src/debugger.rs create mode 100644 crates/debugger/src/file_dumper/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 8d5f0d37be901..f3e4ac4d9f081 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3371,6 +3371,7 @@ dependencies = [ "ratatui", "revm", "revm-inspectors", + "serde", "tracing", ] diff --git a/crates/cli/src/utils/cmd.rs b/crates/cli/src/utils/cmd.rs index 1ea54d3f6413e..ead85cf086223 100644 --- a/crates/cli/src/utils/cmd.rs +++ b/crates/cli/src/utils/cmd.rs @@ -414,7 +414,7 @@ pub async fn handle_traces( .decoder(&decoder) .sources(sources) .build(); - debugger.try_run()?; + debugger.try_run_tui()?; } else { print_traces(&mut result, &decoder).await?; } diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index 96574f460f257..7d5e1c8dce5b6 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -277,8 +277,8 @@ impl ProjectCompiler { pub struct ContractSources { /// Map over artifacts' contract names -> vector of file IDs pub ids_by_name: HashMap>, - /// Map over file_id -> (source code, contract) - pub sources_by_id: HashMap, + /// Map over file_id -> (source code, contract, source path) + pub sources_by_id: HashMap)>, } impl ContractSources { @@ -291,7 +291,7 @@ impl ContractSources { for (id, artifact) in output.artifact_ids() { if let Some(file_id) = artifact.id { let abs_path = root.join(&id.source); - let source_code = std::fs::read_to_string(abs_path).wrap_err_with(|| { + let source_code = std::fs::read_to_string(abs_path.clone()).wrap_err_with(|| { format!("failed to read artifact source file for `{}`", id.identifier()) })?; let compact = CompactContractBytecode { @@ -300,7 +300,7 @@ impl ContractSources { deployed_bytecode: artifact.deployed_bytecode.clone(), }; let contract = compact_to_contract(compact)?; - sources.insert(&id, file_id, source_code, contract); + sources.insert(&id, file_id, source_code, contract, Some(abs_path)); } else { warn!(id = id.identifier(), "source not found"); } @@ -315,13 +315,14 @@ impl ContractSources { file_id: u32, source: String, bytecode: ContractBytecodeSome, + source_path: Option ) { self.ids_by_name.entry(artifact_id.name.clone()).or_default().push(file_id); - self.sources_by_id.insert(file_id, (source, bytecode)); + self.sources_by_id.insert(file_id, (source, bytecode, source_path)); } /// Returns the source for a contract by file ID. - pub fn get(&self, id: u32) -> Option<&(String, ContractBytecodeSome)> { + pub fn get(&self, id: u32) -> Option<&(String, ContractBytecodeSome, Option)> { self.sources_by_id.get(&id) } @@ -329,14 +330,14 @@ impl ContractSources { pub fn get_sources( &self, name: &str, - ) -> Option> { + ) -> Option))>> { self.ids_by_name .get(name) .map(|ids| ids.iter().filter_map(|id| Some((*id, self.sources_by_id.get(id)?)))) } /// Returns all (name, source) pairs. - pub fn entries(&self) -> impl Iterator { + pub fn entries(&self) -> impl Iterator))> { self.ids_by_name.iter().flat_map(|(name, ids)| { ids.iter().filter_map(|id| self.sources_by_id.get(id).map(|s| (name.clone(), s))) }) diff --git a/crates/debugger/Cargo.toml b/crates/debugger/Cargo.toml index 3c1fe8480116e..9a7c0b83a90ce 100644 --- a/crates/debugger/Cargo.toml +++ b/crates/debugger/Cargo.toml @@ -23,3 +23,4 @@ eyre.workspace = true ratatui = { version = "0.24.0", default-features = false, features = ["crossterm"] } revm.workspace = true tracing.workspace = true +serde.workspace = true diff --git a/crates/debugger/src/tui/builder.rs b/crates/debugger/src/builder.rs similarity index 99% rename from crates/debugger/src/tui/builder.rs rename to crates/debugger/src/builder.rs index 6289b0b8814fa..215f8418f8900 100644 --- a/crates/debugger/src/tui/builder.rs +++ b/crates/debugger/src/builder.rs @@ -1,4 +1,4 @@ -//! TUI debugger builder. +//! Debugger builder. use crate::Debugger; use alloy_primitives::Address; diff --git a/crates/debugger/src/context.rs b/crates/debugger/src/context.rs new file mode 100644 index 0000000000000..4a86e2d873020 --- /dev/null +++ b/crates/debugger/src/context.rs @@ -0,0 +1,16 @@ +use alloy_primitives::Address; +use foundry_common::compile::ContractSources; +use foundry_common::evm::Breakpoints; +use foundry_evm_core::debug::DebugNodeFlat; +use foundry_evm_core::utils::PcIcMap; +use std::collections::{BTreeMap, HashMap}; + +pub struct DebuggerContext { + pub debug_arena: Vec, + pub identified_contracts: HashMap, + /// Source map of contract sources + pub contracts_sources: ContractSources, + /// A mapping of source -> (PC -> IC map for deploy code, PC -> IC map for runtime code) + pub pc_ic_maps: BTreeMap, + pub breakpoints: Breakpoints, +} diff --git a/crates/debugger/src/debugger.rs b/crates/debugger/src/debugger.rs new file mode 100644 index 0000000000000..50f6f192358e0 --- /dev/null +++ b/crates/debugger/src/debugger.rs @@ -0,0 +1,83 @@ +//! Debugger implementation. + +use alloy_primitives::Address; +use eyre::Result; +use foundry_common::{compile::ContractSources, evm::Breakpoints}; +use foundry_evm_core::{debug::DebugNodeFlat, utils::PcIcMap}; +use revm::primitives::SpecId; +use std::collections::HashMap; +use std::path::PathBuf; + +use crate::context::DebuggerContext; +use crate::tui::TUI; +use crate::{DebuggerBuilder, ExitReason, FileDumper}; + +pub struct Debugger { + context: DebuggerContext, +} + +impl Debugger { + /// Creates a new debugger builder. + #[inline] + pub fn builder() -> DebuggerBuilder { + DebuggerBuilder::new() + } + + /// Creates a new debugger. + pub fn new( + debug_arena: Vec, + identified_contracts: HashMap, + contracts_sources: ContractSources, + breakpoints: Breakpoints, + ) -> Self { + let pc_ic_maps = contracts_sources + .entries() + .filter_map(|(contract_name, (_, contract, _))| { + Some(( + contract_name.clone(), + ( + PcIcMap::new(SpecId::LATEST, contract.bytecode.bytes()?), + PcIcMap::new(SpecId::LATEST, contract.deployed_bytecode.bytes()?), + ), + )) + }) + .collect(); + Self { + context: DebuggerContext { + debug_arena, + identified_contracts, + contracts_sources, + pc_ic_maps, + breakpoints, + }, + } + } + + /// Starts the debugger TUI. Terminates the current process on failure or user exit. + pub fn run_tui_exit(mut self) -> ! { + let code = match self.try_run_tui() { + Ok(ExitReason::CharExit) => 0, + Err(e) => { + println!("{e}"); + 1 + } + }; + std::process::exit(code) + } + + /// Starts the debugger TUI. + pub fn try_run_tui(&mut self) -> Result { + eyre::ensure!(!self.context.debug_arena.is_empty(), "debug arena is empty"); + + let mut tui = TUI::new(&mut self.context); + tui.try_run() + } + + /// Dumps debugger data to file. + pub fn dump_to_file(&mut self, path: &PathBuf) -> Result<()> { + eyre::ensure!(!self.context.debug_arena.is_empty(), "debug arena is empty"); + + let mut file_dumper = FileDumper::new(path, &mut self.context); + file_dumper.run() + } +} diff --git a/crates/debugger/src/file_dumper/mod.rs b/crates/debugger/src/file_dumper/mod.rs new file mode 100644 index 0000000000000..5134c8083d90a --- /dev/null +++ b/crates/debugger/src/file_dumper/mod.rs @@ -0,0 +1,179 @@ +//! The file dumper implementation + +use alloy_primitives::{Address, Bytes, U256}; +use serde::Serialize; +use std::collections::HashMap; +use std::path::PathBuf; +use std::option::Option; + +use crate::context::DebuggerContext; +use eyre::Result; +use foundry_common::compile::ContractSources; +use foundry_common::fs::write_json_file; +use foundry_compilers::artifacts::ContractBytecodeSome; +use foundry_evm_core::debug::{DebugNodeFlat, DebugStep, Instruction}; +use foundry_evm_core::utils::PcIcMap; +use revm_inspectors::tracing::types::CallKind; + +/// The file dumper +pub struct FileDumper<'a> { + path: &'a PathBuf, + debugger_context: &'a mut DebuggerContext, +} + +impl<'a> FileDumper<'a> { + pub fn new(path: &'a PathBuf, debugger_context: &'a mut DebuggerContext) -> Self { + Self { path, debugger_context } + } + + pub fn run(&mut self) -> Result<()> { + let data = DebuggerDump::from(self.debugger_context); + write_json_file(self.path, &data).unwrap(); + Ok(()) + } +} + +impl DebuggerDump { + fn from(debugger_context: &DebuggerContext) -> DebuggerDump { + Self { + contracts: to_contracts_dump(debugger_context), + executions: to_executions_dump(debugger_context), + } + } +} + +#[derive(Serialize)] +struct DebuggerDump { + contracts: ContractsDump, + executions: ExecutionsDump, +} + +#[derive(Serialize)] +struct ExecutionsDump { + calls: Vec, + // Map of contract name to PcIcMapDump + pc_ic_maps: HashMap, +} + +#[derive(Serialize)] +struct CallDump { + address: Address, + kind: CallKind, + steps: Vec, +} + +#[derive(Serialize)] +struct StepDump { + /// Stack *prior* to running the associated opcode + stack: Vec, + /// Memory *prior* to running the associated opcode + memory: Bytes, + /// Calldata *prior* to running the associated opcode + calldata: Bytes, + /// Returndata *prior* to running the associated opcode + returndata: Bytes, + /// Opcode to be executed + instruction: Instruction, + /// Optional bytes that are being pushed onto the stack + push_bytes: Option, + /// The program counter at this step. + pc: usize, + /// Cumulative gas usage + total_gas_used: u64, +} + +#[derive(Serialize)] +struct PcIcMapDump { + deploy_code_map: HashMap, + runtime_code_map: HashMap, +} + +#[derive(Serialize)] +struct ContractsDump { + // Map of call address to contract name + identified_calls: HashMap, + sources: ContractsSourcesDump, +} + +#[derive(Serialize)] +struct ContractsSourcesDump { + ids_by_name: HashMap>, + sources_by_id: HashMap, +} + +#[derive(Serialize)] +struct ContractSourceDetailsDump { + source_code: String, + contract_bytecode: ContractBytecodeSome, + source_path: Option, +} + +fn to_executions_dump(debugger_context: &DebuggerContext) -> ExecutionsDump { + ExecutionsDump { + calls: debugger_context.debug_arena.iter().map(|call| to_call_dump(call)).collect(), + pc_ic_maps: debugger_context.pc_ic_maps.iter().map(|(k, v)| (k.clone(), to_pc_ic_map_dump(&v))).collect(), + } +} + +fn to_call_dump(call: &DebugNodeFlat) -> CallDump { + CallDump { + address: call.address, + kind: call.kind, + steps: call.steps.iter().map(|step| to_step_dump(step.clone())).collect(), + } +} + +fn to_step_dump(step: DebugStep) -> StepDump { + StepDump { + stack: step.stack, + memory: step.memory, + calldata: step.calldata, + returndata: step.returndata, + instruction: step.instruction, + push_bytes: step.push_bytes.map(|v| Bytes::from(v)), + pc: step.pc, + total_gas_used: step.total_gas_used, + } +} + +fn to_pc_ic_map_dump(pc_ic_map: &(PcIcMap, PcIcMap)) -> PcIcMapDump { + let mut deploy_code_map = HashMap::new(); + + for (k, v) in pc_ic_map.0.inner.iter() { + deploy_code_map.insert(*k, *v); + } + + let mut runtime_code_map = HashMap::new(); + for (k, v) in pc_ic_map.1.inner.iter() { + runtime_code_map.insert(*k, *v); + } + + PcIcMapDump { deploy_code_map, runtime_code_map } +} + +fn to_contracts_dump(debugger_context: &DebuggerContext) -> ContractsDump { + ContractsDump { + identified_calls: debugger_context.identified_contracts.clone(), + sources: to_contracts_sources_dump(&debugger_context.contracts_sources), + } +} + +fn to_contracts_sources_dump(contracts_sources: &ContractSources) -> ContractsSourcesDump { + ContractsSourcesDump { + ids_by_name: contracts_sources.ids_by_name.clone(), + sources_by_id: contracts_sources + .sources_by_id + .iter() + .map(|(id, (source_code, contract_bytecode, source_path))| { + ( + *id, + ContractSourceDetailsDump { + source_code: source_code.clone(), + contract_bytecode: contract_bytecode.clone(), + source_path: source_path.clone(), + }, + ) + }) + .collect(), + } +} diff --git a/crates/debugger/src/lib.rs b/crates/debugger/src/lib.rs index 5683cb8a5b7c4..d69a8da339c21 100644 --- a/crates/debugger/src/lib.rs +++ b/crates/debugger/src/lib.rs @@ -1,6 +1,6 @@ //! # foundry-debugger //! -//! Interactive Solidity TUI debugger. +//! Interactive Solidity TUI debugger and debugger data file dumper #![warn(unused_crate_dependencies, unreachable_pub)] @@ -9,5 +9,13 @@ extern crate tracing; mod op; +mod builder; +mod context; +mod debugger; +mod file_dumper; mod tui; -pub use tui::{Debugger, DebuggerBuilder, ExitReason}; + +pub use builder::DebuggerBuilder; +pub use debugger::Debugger; +pub use file_dumper::FileDumper; +pub use tui::{ExitReason, TUI}; diff --git a/crates/debugger/src/tui/context.rs b/crates/debugger/src/tui/context.rs index 6f8c30939769e..3b2538c8c3cc9 100644 --- a/crates/debugger/src/tui/context.rs +++ b/crates/debugger/src/tui/context.rs @@ -1,6 +1,6 @@ //! Debugger context and event handler implementation. -use crate::{Debugger, ExitReason}; +use crate::{context::DebuggerContext, ExitReason}; use alloy_primitives::Address; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind}; use foundry_evm_core::debug::{DebugNodeFlat, DebugStep}; @@ -45,8 +45,8 @@ impl BufferKind { } } -pub(crate) struct DebuggerContext<'a> { - pub(crate) debugger: &'a mut Debugger, +pub(crate) struct TUIContext<'a> { + pub(crate) debugger_context: &'a mut DebuggerContext, /// Buffer for keys prior to execution, i.e. '10' + 'k' => move up 10 operations. pub(crate) key_buffer: String, @@ -64,10 +64,10 @@ pub(crate) struct DebuggerContext<'a> { pub(crate) active_buffer: BufferKind, } -impl<'a> DebuggerContext<'a> { - pub(crate) fn new(debugger: &'a mut Debugger) -> Self { - DebuggerContext { - debugger, +impl<'a> TUIContext<'a> { + pub(crate) fn new(debugger_context: &'a mut DebuggerContext) -> Self { + TUIContext { + debugger_context, key_buffer: String::with_capacity(64), current_step: 0, @@ -87,7 +87,7 @@ impl<'a> DebuggerContext<'a> { } pub(crate) fn debug_arena(&self) -> &[DebugNodeFlat] { - &self.debugger.debug_arena + &self.debugger_context.debug_arena } pub(crate) fn debug_call(&self) -> &DebugNodeFlat { @@ -131,7 +131,7 @@ impl<'a> DebuggerContext<'a> { } } -impl DebuggerContext<'_> { +impl TUIContext<'_> { pub(crate) fn handle_event(&mut self, event: Event) -> ControlFlow { if self.last_index != self.draw_memory.inner_call_index { self.gen_opcode_list(); @@ -274,8 +274,8 @@ impl DebuggerContext<'_> { .find_map(|(i, op)| { if i > 0 { match ( - prev_ops[i - 1].contains("JUMP") && - prev_ops[i - 1] != "JUMPDEST", + prev_ops[i - 1].contains("JUMP") + && prev_ops[i - 1] != "JUMPDEST", &**op, ) { (true, "JUMPDEST") => Some(i - 1), @@ -307,7 +307,7 @@ impl DebuggerContext<'_> { fn handle_breakpoint(&mut self, c: char) { // Find the location of the called breakpoint in the whole debug arena (at this address with // this pc) - if let Some((caller, pc)) = self.debugger.breakpoints.get(&c) { + if let Some((caller, pc)) = self.debugger_context.breakpoints.get(&c) { for (i, node) in self.debug_arena().iter().enumerate() { if node.address == *caller { if let Some(step) = node.steps.iter().position(|step| step.pc == *pc) { diff --git a/crates/debugger/src/tui/draw.rs b/crates/debugger/src/tui/draw.rs index 41eca5e76af5f..7f1e6e4502997 100644 --- a/crates/debugger/src/tui/draw.rs +++ b/crates/debugger/src/tui/draw.rs @@ -1,6 +1,6 @@ //! TUI draw implementation. -use super::context::{BufferKind, DebuggerContext}; +use super::context::{BufferKind, TUIContext}; use crate::op::OpcodeParam; use alloy_primitives::U256; use foundry_compilers::sourcemap::SourceElement; @@ -16,7 +16,7 @@ use revm::interpreter::opcode; use revm_inspectors::tracing::types::CallKind; use std::{cmp, collections::VecDeque, fmt::Write, io}; -impl DebuggerContext<'_> { +impl TUIContext<'_> { /// Draws the TUI layout and subcomponents to the given terminal. pub(crate) fn draw(&self, terminal: &mut super::DebuggerTerminal) -> io::Result<()> { terminal.draw(|f| self.draw_layout(f)).map(drop) @@ -330,24 +330,24 @@ impl DebuggerContext<'_> { fn src_map(&self) -> Result<(SourceElement, &str), String> { let address = self.address(); - let Some(contract_name) = self.debugger.identified_contracts.get(address) else { + let Some(contract_name) = self.debugger_context.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) + self.debugger_context.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 { + let Some((create_map, rt_map)) = self.debugger_context.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)) = - files_source_code.find_map(|(file_id, (source_code, contract_source))| { + files_source_code.find_map(|(file_id, (source_code, contract_source, _))| { let bytecode = if is_create { &contract_source.bytecode } else { @@ -366,11 +366,11 @@ impl DebuggerContext<'_> { (index == file_id).then(|| (source_element.clone(), source_code))) .or_else(|| { // otherwise find the source code for the element's index - self.debugger + self.debugger_context .contracts_sources .sources_by_id .get(&(source_element.index?)) - .map(|(source_code, _)| (source_element.clone(), source_code)) + .map(|(source_code, _, _)| (source_element.clone(), source_code)) }) }) else { diff --git a/crates/debugger/src/tui/mod.rs b/crates/debugger/src/tui/mod.rs index d9543fc58a8d8..f4207f9b5148f 100644 --- a/crates/debugger/src/tui/mod.rs +++ b/crates/debugger/src/tui/mod.rs @@ -1,21 +1,16 @@ //! The TUI implementation. -use alloy_primitives::Address; 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 ratatui::{ backend::{Backend, CrosstermBackend}, Terminal, }; -use revm::primitives::SpecId; use std::{ - collections::{BTreeMap, HashMap}, io, ops::ControlFlow, sync::{mpsc, Arc}, @@ -23,11 +18,9 @@ use std::{ time::{Duration, Instant}, }; -mod builder; -pub use builder::DebuggerBuilder; - mod context; -use context::DebuggerContext; +use crate::context::DebuggerContext; +use context::TUIContext; mod draw; @@ -41,61 +34,18 @@ 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, - breakpoints: Breakpoints, +pub struct TUI<'a> { + debugger_context: &'a mut DebuggerContext, } -impl Debugger { - /// Creates a new debugger builder. - #[inline] - pub fn builder() -> DebuggerBuilder { - DebuggerBuilder::new() - } - +impl<'a> TUI<'a> { /// Creates a new debugger. - pub fn new( - debug_arena: Vec, - identified_contracts: HashMap, - contracts_sources: ContractSources, - breakpoints: Breakpoints, - ) -> Self { - let pc_ic_maps = contracts_sources - .entries() - .filter_map(|(contract_name, (_, contract))| { - Some(( - contract_name.clone(), - ( - PcIcMap::new(SpecId::LATEST, contract.bytecode.bytes()?), - PcIcMap::new(SpecId::LATEST, contract.deployed_bytecode.bytes()?), - ), - )) - }) - .collect(); - Self { debug_arena, identified_contracts, contracts_sources, pc_ic_maps, breakpoints } - } - - /// Starts the debugger TUI. Terminates the current process on failure or user exit. - pub fn run_exit(mut self) -> ! { - let code = match self.try_run() { - Ok(ExitReason::CharExit) => 0, - Err(e) => { - println!("{e}"); - 1 - } - }; - std::process::exit(code) + pub fn new(debugger_context: &'a mut DebuggerContext) -> Self { + Self { debugger_context } } /// Starts the debugger TUI. pub fn try_run(&mut self) -> Result { - eyre::ensure!(!self.debug_arena.is_empty(), "debug arena is empty"); - let backend = CrosstermBackend::new(io::stdout()); let terminal = Terminal::new(backend)?; TerminalGuard::with(terminal, |terminal| self.try_run_real(terminal)) @@ -104,7 +54,7 @@ impl Debugger { #[instrument(target = "debugger", name = "run", skip_all, ret)] fn try_run_real(&mut self, terminal: &mut DebuggerTerminal) -> Result { // Create the context. - let mut cx = DebuggerContext::new(self); + let mut cx = TUIContext::new(self.debugger_context); cx.init(); diff --git a/crates/evm/traces/src/identifier/etherscan.rs b/crates/evm/traces/src/identifier/etherscan.rs index 50c273d07b0fe..122dc70504607 100644 --- a/crates/evm/traces/src/identifier/etherscan.rs +++ b/crates/evm/traces/src/identifier/etherscan.rs @@ -85,7 +85,7 @@ impl EtherscanIdentifier { for (results, (_, metadata)) in artifacts.into_iter().zip(contracts_iter) { // get the inner type let (artifact_id, file_id, bytecode) = results?; - sources.insert(&artifact_id, file_id, metadata.source_code(), bytecode); + sources.insert(&artifact_id, file_id, metadata.source_code(), bytecode, None); } Ok(sources) diff --git a/crates/forge/bin/cmd/debug.rs b/crates/forge/bin/cmd/debug.rs index 8fe1d2e32a258..421478bd5762c 100644 --- a/crates/forge/bin/cmd/debug.rs +++ b/crates/forge/bin/cmd/debug.rs @@ -33,6 +33,15 @@ pub struct DebugArgs { #[arg(long)] pub debug: bool, + /// File path to dump execution details as JSON. + #[arg( + long, + requires = "debug", + value_hint = ValueHint::FilePath, + value_name = "PATH" + )] + pub dump: Option, + #[command(flatten)] pub opts: CoreBuildArgs, @@ -51,6 +60,7 @@ impl DebugArgs { opts: self.opts, evm_opts: self.evm_opts, debug: true, + dump: self.dump, retry: RETRY_VERIFY_ON_CREATE, ..Default::default() }; diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index dfb1d8cf5941d..d72dfaae7d662 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -243,7 +243,7 @@ impl TestArgs { builder = builder.decoder(decoder); } let mut debugger = builder.build(); - debugger.try_run()?; + debugger.try_run_tui()?; } Ok(outcome) diff --git a/crates/script/src/execute.rs b/crates/script/src/execute.rs index cdf4353d38608..467afbc0277e2 100644 --- a/crates/script/src/execute.rs +++ b/crates/script/src/execute.rs @@ -33,6 +33,7 @@ use foundry_evm::{ use futures::future::join_all; use itertools::Itertools; use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; use yansi::Paint; /// State after linking, contains the linked build data along with library addresses and optional @@ -510,13 +511,21 @@ impl PreSimulationState { } pub fn run_debugger(&self) -> Result<()> { - let mut debugger = Debugger::builder() + self.create_debugger().try_run_tui()?; + Ok(()) + } + + pub fn run_debug_file_dumper(&self, path: &PathBuf) -> Result<()> { + self.create_debugger().dump_to_file(path)?; + Ok(()) + } + + fn create_debugger(&self) -> Debugger { + Debugger::builder() .debug_arenas(self.execution_result.debug.as_deref().unwrap_or_default()) .decoder(&self.execution_artifacts.decoder) .sources(self.build_data.build_data.sources.clone()) .breakpoints(self.execution_result.breakpoints.clone()) - .build(); - debugger.try_run()?; - Ok(()) + .build() } } diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index 010b5bbe970d1..ce42dee8fa65a 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -49,6 +49,7 @@ use foundry_evm::{ use foundry_wallets::MultiWalletOpts; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap}; +use std::path::PathBuf; use yansi::Paint; mod artifacts; @@ -142,6 +143,14 @@ pub struct ScriptArgs { #[arg(long)] pub debug: bool, + /// Dumps all debugger steps to file. + #[arg( + long, + requires = "debug", + value_hint = ValueHint::FilePath, + )] + pub dump: Option, + /// Makes sure a transaction is sent, /// only after its previous one has been confirmed and succeeded. #[arg(long)] @@ -237,7 +246,10 @@ impl ScriptArgs { .await?; if pre_simulation.args.debug { - pre_simulation.run_debugger()?; + match pre_simulation.args.dump { + Some(ref path) => pre_simulation.run_debug_file_dumper(path)?, + None => pre_simulation.run_debugger()?, + } } if pre_simulation.args.json {