diff --git a/Cargo.lock b/Cargo.lock index 81da6b2be3294..6484bda7ef84c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7033,6 +7033,7 @@ dependencies = [ "foundry-evm", "ratatui", "revm", + "tracing", ] [[package]] diff --git a/crates/cli/src/utils/cmd.rs b/crates/cli/src/utils/cmd.rs index 4323ddb2c6463..8e58fe8d9bc85 100644 --- a/crates/cli/src/utils/cmd.rs +++ b/crates/cli/src/utils/cmd.rs @@ -2,11 +2,11 @@ use ethers::{ abi::Abi, core::types::Chain, solc::{ - artifacts::{CompactBytecode, CompactDeployedBytecode, ContractBytecodeSome}, + artifacts::{CompactBytecode, CompactDeployedBytecode}, cache::{CacheEntry, SolFilesCache}, info::ContractInfo, utils::read_json_file, - Artifact, ArtifactId, ProjectCompileOutput, + Artifact, ProjectCompileOutput, }, }; use eyre::{Result, WrapErr}; @@ -20,9 +20,9 @@ use foundry_evm::{ CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, Traces, }, }; -use std::{collections::BTreeMap, fmt::Write, path::PathBuf, str::FromStr}; +use std::{fmt::Write, path::PathBuf, str::FromStr}; use tracing::trace; -use ui::{TUIExitReason, Tui, Ui}; +use ui::DebuggerArgs; use yansi::Paint; /// Given a `Project`'s output, removes the matching ABI, Bytecode and @@ -391,8 +391,14 @@ pub async fn handle_traces( } if debug { - let (sources, bytecode) = etherscan_identifier.get_compiled_contracts().await?; - run_debugger(result, decoder, bytecode, sources)?; + let sources = etherscan_identifier.get_compiled_contracts().await?; + let debugger = DebuggerArgs { + debug: vec![result.debug], + decoder: &decoder, + sources, + breakpoints: Default::default(), + }; + debugger.run()?; } else { print_traces(&mut result, &decoder, verbose).await?; } @@ -429,31 +435,3 @@ pub async fn print_traces( println!("Gas used: {}", result.gas_used); Ok(()) } - -pub fn run_debugger( - result: TraceResult, - decoder: CallTraceDecoder, - known_contracts: BTreeMap, - sources: BTreeMap, -) -> Result<()> { - let calls: Vec = vec![result.debug]; - let flattened = calls.last().expect("we should have collected debug info").flatten(0); - let tui = Tui::new( - flattened, - 0, - decoder.contracts, - known_contracts.into_iter().map(|(id, artifact)| (id.name, artifact)).collect(), - sources - .into_iter() - .map(|(id, source)| { - let mut sources = BTreeMap::new(); - sources.insert(0, source); - (id.name, sources) - }) - .collect(), - Default::default(), - )?; - match tui.start().expect("Failed to start tui") { - TUIExitReason::CharExit => Ok(()), - } -} diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index 37d765ae2b747..bfe9ec9901a30 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -1,5 +1,5 @@ //! Support for compiling [ethers::solc::Project] -use crate::{glob::GlobMatcher, term, TestFunctionExt}; +use crate::{compact_to_contract, glob::GlobMatcher, term, TestFunctionExt}; use comfy_table::{presets::ASCII_MARKDOWN, *}; use ethers_etherscan::contract::Metadata; use ethers_solc::{ @@ -11,7 +11,7 @@ use ethers_solc::{ }; use eyre::Result; use std::{ - collections::BTreeMap, + collections::{BTreeMap, HashMap}, convert::Infallible, fmt::Display, path::{Path, PathBuf}, @@ -171,6 +171,10 @@ impl ProjectCompiler { } } +/// Map over artifacts contract sources name -> file_id -> (source, contract) +#[derive(Default, Debug, Clone)] +pub struct ContractSources(pub HashMap>); + // https://eips.ethereum.org/EIPS/eip-170 const CONTRACT_SIZE_LIMIT: usize = 24576; @@ -398,10 +402,11 @@ pub fn compile_target_with_filter( } } -/// Creates and compiles a project from an Etherscan source. +/// Compiles an Etherscan source from metadata by creating a project. +/// Returns the artifact_id, the file_id, and the bytecode pub async fn compile_from_source( metadata: &Metadata, -) -> Result<(ArtifactId, ContractBytecodeSome)> { +) -> Result<(ArtifactId, u32, ContractBytecodeSome)> { let root = tempfile::tempdir()?; let root_path = root.path(); let project = etherscan_project(metadata, root_path)?; @@ -412,19 +417,18 @@ pub async fn compile_from_source( eyre::bail!(project_output.to_string()) } - let (artifact_id, contract) = project_output - .into_contract_bytecodes() + let (artifact_id, file_id, contract) = project_output + .into_artifacts() .find(|(artifact_id, _)| artifact_id.name == metadata.contract_name) + .map(|(aid, art)| { + (aid, art.source_file().expect("no source file").id, art.into_contract_bytecode()) + }) .expect("there should be a contract with bytecode"); - let bytecode = ContractBytecodeSome { - abi: contract.abi.unwrap(), - bytecode: contract.bytecode.unwrap().into(), - deployed_bytecode: contract.deployed_bytecode.unwrap().into(), - }; + let bytecode = compact_to_contract(contract)?; root.close()?; - Ok((artifact_id, bytecode)) + Ok((artifact_id, file_id, bytecode)) } /// Creates a [Project] from an Etherscan source. diff --git a/crates/common/src/contracts.rs b/crates/common/src/contracts.rs index 0440b000d9821..c819c41aefc3f 100644 --- a/crates/common/src/contracts.rs +++ b/crates/common/src/contracts.rs @@ -5,7 +5,10 @@ use ethers_core::{ types::{Address, H256}, utils::hex, }; -use ethers_solc::{artifacts::ContractBytecodeSome, ArtifactId, ProjectPathsConfig}; +use ethers_solc::{ + artifacts::{CompactContractBytecode, ContractBytecodeSome}, + ArtifactId, ProjectPathsConfig, +}; use once_cell::sync::Lazy; use regex::Regex; use std::{ @@ -265,3 +268,17 @@ mod tests { let _decoded = abi::decode(¶ms, args).unwrap(); } } + +/// Helper function to convert CompactContractBytecode ~> ContractBytecodeSome +pub fn compact_to_contract( + contract: CompactContractBytecode, +) -> eyre::Result { + Ok(ContractBytecodeSome { + abi: contract.abi.ok_or(eyre::eyre!("No contract abi"))?, + bytecode: contract.bytecode.ok_or(eyre::eyre!("No contract bytecode"))?.into(), + deployed_bytecode: contract + .deployed_bytecode + .ok_or(eyre::eyre!("No contract deployed bytecode"))? + .into(), + }) +} diff --git a/crates/evm/src/debug.rs b/crates/evm/src/debug.rs index 885830abb4da1..c229dbca90b3e 100644 --- a/crates/evm/src/debug.rs +++ b/crates/evm/src/debug.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::Display; /// An arena of [DebugNode]s -#[derive(Default, Debug, Clone)] +#[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct DebugArena { /// The arena of nodes pub arena: Vec, @@ -78,7 +78,7 @@ impl DebugArena { } /// A node in the arena -#[derive(Default, Debug, Clone)] +#[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct DebugNode { /// Parent node index in the arena pub parent: Option, @@ -109,7 +109,7 @@ impl DebugNode { /// It holds the current program counter (where in the program you are), /// the stack and memory (prior to the opcodes execution), any bytes to be /// pushed onto the stack, and the instruction counter for use with sourcemap. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct DebugStep { /// Stack *prior* to running the associated opcode pub stack: Vec, diff --git a/crates/evm/src/fuzz/mod.rs b/crates/evm/src/fuzz/mod.rs index e0dc9e466fbba..f704a0696f88c 100644 --- a/crates/evm/src/fuzz/mod.rs +++ b/crates/evm/src/fuzz/mod.rs @@ -81,12 +81,7 @@ impl<'a> FuzzedExecutor<'a> { // Stores coverage information for all fuzz cases let coverage: RefCell> = RefCell::default(); - // Stores fuzz state for use with [fuzz_calldata_from_state] - let state: EvmFuzzState = if let Some(fork_db) = self.executor.backend.active_fork_db() { - build_initial_state(fork_db, &self.config.dictionary) - } else { - build_initial_state(self.executor.backend.mem_db(), &self.config.dictionary) - }; + let state = self.build_fuzz_state(); let mut weights = vec![]; let dictionary_weight = self.config.dictionary.dictionary_weight.min(100); @@ -248,6 +243,15 @@ impl<'a> FuzzedExecutor<'a> { })) } } + + /// Stores fuzz state for use with [fuzz_calldata_from_state] + pub fn build_fuzz_state(&self) -> EvmFuzzState { + if let Some(fork_db) = self.executor.backend.active_fork_db() { + build_initial_state(fork_db, &self.config.dictionary) + } else { + build_initial_state(self.executor.backend.mem_db(), &self.config.dictionary) + } + } } #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/crates/evm/src/trace/identifier/etherscan.rs b/crates/evm/src/trace/identifier/etherscan.rs index b9bdf43feb100..2658c032aac37 100644 --- a/crates/evm/src/trace/identifier/etherscan.rs +++ b/crates/evm/src/trace/identifier/etherscan.rs @@ -4,10 +4,10 @@ use ethers::{ abi::Address, etherscan, etherscan::contract::{ContractMetadata, Metadata}, - prelude::{artifacts::ContractBytecodeSome, errors::EtherscanError, ArtifactId}, + prelude::errors::EtherscanError, types::H160, }; -use foundry_common::compile; +use foundry_common::compile::{self, ContractSources}; use foundry_config::{Chain, Config}; use futures::{ future::{join_all, Future}, @@ -58,13 +58,7 @@ impl EtherscanIdentifier { /// Goes over the list of contracts we have pulled from the traces, clones their source from /// Etherscan and compiles them locally, for usage in the debugger. - pub async fn get_compiled_contracts( - &self, - ) -> eyre::Result<(BTreeMap, BTreeMap)> - { - let mut compiled_contracts = BTreeMap::new(); - let mut sources = BTreeMap::new(); - + pub async fn get_compiled_contracts(&self) -> eyre::Result { // TODO: Add caching so we dont double-fetch contracts. let contracts_iter = self .contracts @@ -87,15 +81,20 @@ impl EtherscanIdentifier { // poll all the futures concurrently let artifacts = join_all(outputs_fut).await; + let mut sources: ContractSources = Default::default(); + // construct the map for (results, (_, metadata)) in artifacts.into_iter().zip(contracts_iter) { // get the inner type - let (artifact_id, bytecode) = results?; - compiled_contracts.insert(artifact_id.clone(), bytecode); - sources.insert(artifact_id, metadata.source_code()); + let (artifact_id, file_id, bytecode) = results?; + sources + .0 + .entry(artifact_id.clone().name) + .or_default() + .insert(file_id, (metadata.source_code(), bytecode)); } - Ok((sources, compiled_contracts)) + Ok(sources) } } diff --git a/crates/forge/bin/cmd/debug.rs b/crates/forge/bin/cmd/debug.rs index abd7362720870..bdcea1c0db60c 100644 --- a/crates/forge/bin/cmd/debug.rs +++ b/crates/forge/bin/cmd/debug.rs @@ -1,8 +1,7 @@ use super::{build::BuildArgs, retry::RETRY_VERIFY_ON_CREATE, script::ScriptArgs}; use clap::{Parser, ValueHint}; -use eyre::Result; use foundry_cli::opts::CoreBuildArgs; -use foundry_common::evm::{Breakpoints, EvmArgs}; +use foundry_common::evm::EvmArgs; use std::path::PathBuf; // Loads project's figment and merges the build cli arguments into it @@ -41,7 +40,7 @@ pub struct DebugArgs { } impl DebugArgs { - pub async fn debug(self, breakpoints: Breakpoints) -> Result<()> { + pub async fn run(self) -> eyre::Result<()> { let script = ScriptArgs { path: self.path.to_str().expect("Invalid path string.").to_string(), args: self.args, @@ -54,6 +53,6 @@ impl DebugArgs { retry: RETRY_VERIFY_ON_CREATE, ..Default::default() }; - script.run_script(breakpoints).await + script.run_script().await } } diff --git a/crates/forge/bin/cmd/script/build.rs b/crates/forge/bin/cmd/script/build.rs index e80a4ec922494..1ed67955aba11 100644 --- a/crates/forge/bin/cmd/script/build.rs +++ b/crates/forge/bin/cmd/script/build.rs @@ -1,8 +1,7 @@ use super::*; use ethers::{ prelude::{ - artifacts::Libraries, cache::SolFilesCache, ArtifactId, Graph, Project, - ProjectCompileOutput, + artifacts::Libraries, cache::SolFilesCache, ArtifactId, Project, ProjectCompileOutput, }, solc::{ artifacts::{CompactContractBytecode, ContractBytecode, ContractBytecodeSome}, @@ -13,7 +12,10 @@ use ethers::{ }; use eyre::{Context, ContextCompat, Result}; use foundry_cli::utils::get_cached_entry_by_name; -use foundry_common::compile; +use foundry_common::{ + compact_to_contract, + compile::{self, ContractSources}, +}; use foundry_utils::{PostLinkInput, ResolvedDependency}; use std::{collections::BTreeMap, fs, str::FromStr}; use tracing::{trace, warn}; @@ -31,7 +33,7 @@ impl ScriptArgs { let (project, output) = self.get_project_and_output(script_config)?; let output = output.with_stripped_file_prefixes(project.root()); - let mut sources: BTreeMap = BTreeMap::new(); + let mut sources: ContractSources = Default::default(); let contracts = output .into_artifacts() @@ -39,13 +41,18 @@ impl ScriptArgs { // Sources are only required for the debugger, but it *might* mean that there's // something wrong with the build and/or artifacts. if let Some(source) = artifact.source_file() { - sources.insert( - source.id, - source - .ast - .ok_or(eyre::eyre!("Source from artifact has no AST."))? - .absolute_path, - ); + let abs_path = source + .ast + .ok_or(eyre::eyre!("Source from artifact has no AST."))? + .absolute_path; + let source_code = fs::read_to_string(abs_path)?; + let contract = artifact.clone().into_contract_bytecode(); + let source_contract = compact_to_contract(contract)?; + sources + .0 + .entry(id.clone().name) + .or_default() + .insert(source.id, (source_code, source_contract)); } else { warn!("source not found for artifact={:?}", id); } @@ -195,7 +202,7 @@ impl ScriptArgs { known_contracts: contracts, highlevel_known_contracts: ArtifactContracts(highlevel_known_contracts), predeploy_libraries, - sources: BTreeMap::new(), + sources: Default::default(), project, libraries: new_libraries, }) @@ -261,74 +268,6 @@ impl ScriptArgs { } } -/// Resolve the import tree of our target path, and get only the artifacts and -/// sources we need. If it's a standalone script, don't filter anything out. -pub fn filter_sources_and_artifacts( - target: &str, - sources: BTreeMap, - highlevel_known_contracts: ArtifactContracts, - project: Project, -) -> Result<(BTreeMap, HashMap)> { - // Find all imports - let graph = Graph::resolve(&project.paths)?; - let target_path = project.root().join(target); - let mut target_tree = BTreeMap::new(); - let mut is_standalone = false; - - if let Some(target_index) = graph.files().get(&target_path) { - target_tree.extend( - graph - .all_imported_nodes(*target_index) - .map(|index| graph.node(index).unpack()) - .collect::>(), - ); - - // Add our target into the tree as well. - let (target_path, target_source) = graph.node(*target_index).unpack(); - target_tree.insert(target_path, target_source); - } else { - is_standalone = true; - } - - let sources = sources - .into_iter() - .filter_map(|(id, path)| { - let mut resolved = project - .paths - .resolve_library_import(project.root(), &PathBuf::from(&path)) - .unwrap_or_else(|| PathBuf::from(&path)); - - if !resolved.is_absolute() { - resolved = project.root().join(&resolved); - } - - if !is_standalone { - target_tree.get(&resolved).map(|source| (id, source.content.as_str().to_string())) - } else { - Some(( - id, - fs::read_to_string(&resolved).unwrap_or_else(|_| { - panic!("Something went wrong reading the source file: {path:?}") - }), - )) - } - }) - .collect(); - - let artifacts = highlevel_known_contracts - .into_iter() - .filter_map(|(id, artifact)| { - if !is_standalone { - target_tree.get(&id.source).map(|_| (id.name, artifact)) - } else { - Some((id.name, artifact)) - } - }) - .collect(); - - Ok((sources, artifacts)) -} - struct ExtraLinkingInfo<'a> { no_target_name: bool, target_fname: String, @@ -346,5 +285,5 @@ pub struct BuildOutput { pub highlevel_known_contracts: ArtifactContracts, pub libraries: Libraries, pub predeploy_libraries: Vec, - pub sources: BTreeMap, + pub sources: ContractSources, } diff --git a/crates/forge/bin/cmd/script/cmd.rs b/crates/forge/bin/cmd/script/cmd.rs index 70c836877cdcb..8185e6e9e7e9d 100644 --- a/crates/forge/bin/cmd/script/cmd.rs +++ b/crates/forge/bin/cmd/script/cmd.rs @@ -8,13 +8,14 @@ use foundry_cli::utils::LoadConfig; use foundry_common::{contracts::flatten_contracts, try_get_http_provider}; use std::sync::Arc; use tracing::trace; +use ui::DebuggerArgs; /// Helper alias type for the collection of data changed due to the new sender. type NewSenderChanges = (CallTraceDecoder, Libraries, ArtifactContracts); impl ScriptArgs { /// Executes the script - pub async fn run_script(mut self, breakpoints: Breakpoints) -> Result<()> { + pub async fn run_script(mut self) -> Result<()> { trace!(target: "script", "executing script command"); let (config, evm_opts) = self.load_config_and_evm_opts_emit_warnings()?; @@ -23,6 +24,7 @@ impl ScriptArgs { sender_nonce: U256::one(), config, evm_opts, + debug: self.debug, ..Default::default() }; @@ -83,14 +85,13 @@ impl ScriptArgs { let mut decoder = self.decode_traces(&script_config, &mut result, &known_contracts)?; if self.debug { - return self.run_debugger( - &decoder, + let debugger = DebuggerArgs { + debug: result.debug.clone().unwrap_or_default(), + decoder: &decoder, sources, - result, - project, - highlevel_known_contracts, - breakpoints, - ) + breakpoints: result.breakpoints.clone(), + }; + debugger.run()?; } if let Some((new_traces, updated_libraries, updated_contracts)) = self diff --git a/crates/forge/bin/cmd/script/executor.rs b/crates/forge/bin/cmd/script/executor.rs index bc31d78bfe916..d084c8caa7053 100644 --- a/crates/forge/bin/cmd/script/executor.rs +++ b/crates/forge/bin/cmd/script/executor.rs @@ -72,6 +72,7 @@ impl ScriptArgs { result.labeled_addresses.extend(script_result.labeled_addresses); result.returned = script_result.returned; result.script_wallets.extend(script_result.script_wallets); + result.breakpoints = script_result.breakpoints; match (&mut result.transactions, script_result.transactions) { (Some(txs), Some(new_txs)) => { diff --git a/crates/forge/bin/cmd/script/mod.rs b/crates/forge/bin/cmd/script/mod.rs index 4e10066332f3c..519b35da2d570 100644 --- a/crates/forge/bin/cmd/script/mod.rs +++ b/crates/forge/bin/cmd/script/mod.rs @@ -1,7 +1,4 @@ -use self::{ - build::{filter_sources_and_artifacts, BuildOutput}, - runner::ScriptRunner, -}; +use self::{build::BuildOutput, runner::ScriptRunner}; use super::{build::BuildArgs, retry::RetryArgs}; use clap::{Parser, ValueHint}; use dialoguer::Confirm; @@ -56,12 +53,7 @@ use foundry_evm::{ }; use futures::future; use serde::{Deserialize, Serialize}; -use std::{ - collections::{BTreeMap, HashMap, HashSet, VecDeque}, - path::PathBuf, -}; -use tracing::log::trace; -use ui::{TUIExitReason, Tui, Ui}; +use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; use yansi::Paint; mod artifacts; @@ -449,49 +441,6 @@ impl ScriptArgs { .collect() } - fn run_debugger( - &self, - decoder: &CallTraceDecoder, - sources: BTreeMap, - result: ScriptResult, - project: Project, - highlevel_known_contracts: ArtifactContracts, - breakpoints: Breakpoints, - ) -> Result<()> { - trace!(target: "script", "debugging script"); - - let (sources, artifacts) = filter_sources_and_artifacts( - &self.path, - sources, - highlevel_known_contracts.clone(), - project, - )?; - let flattened = result - .debug - .and_then(|arena| arena.last().map(|arena| arena.flatten(0))) - .expect("We should have collected debug information"); - let identified_contracts = decoder - .contracts - .iter() - .map(|(addr, identifier)| (*addr, get_contract_name(identifier).to_string())) - .collect(); - - let tui = Tui::new( - flattened, - 0, - identified_contracts, - artifacts, - highlevel_known_contracts - .into_iter() - .map(|(id, _)| (id.name, sources.clone())) - .collect(), - breakpoints, - )?; - match tui.start().expect("Failed to start tui") { - TUIExitReason::CharExit => Ok(()), - } - } - /// Returns the Function and calldata based on the signature /// /// If the `sig` is a valid human-readable function we find the corresponding function in the @@ -656,6 +605,7 @@ impl Provider for ScriptArgs { } } +#[derive(Default)] pub struct ScriptResult { pub success: bool, pub logs: Vec, @@ -667,6 +617,7 @@ pub struct ScriptResult { pub returned: bytes::Bytes, pub address: Option
, pub script_wallets: Vec, + pub breakpoints: Breakpoints, } #[derive(Serialize, Deserialize)] @@ -697,6 +648,8 @@ pub struct ScriptConfig { pub total_rpcs: HashSet, /// If true, one of the transactions did not have a rpc pub missing_rpc: bool, + /// Should return some debug information + pub debug: bool, } impl ScriptConfig { diff --git a/crates/forge/bin/cmd/script/runner.rs b/crates/forge/bin/cmd/script/runner.rs index 8a50bb21eaf32..1781d9f9f5d57 100644 --- a/crates/forge/bin/cmd/script/runner.rs +++ b/crates/forge/bin/cmd/script/runner.rs @@ -167,6 +167,7 @@ impl ScriptRunner { debug, address: None, script_wallets, + ..Default::default() }, )) } @@ -236,10 +237,8 @@ impl ScriptRunner { }) .unwrap_or_default(), debug: vec![debug].into_iter().collect(), - labeled_addresses: Default::default(), - transactions: Default::default(), address: Some(address), - script_wallets: vec![], + ..Default::default() }) } else { eyre::bail!("ENS not supported."); @@ -284,6 +283,7 @@ impl ScriptRunner { script_wallets, .. } = res; + let breakpoints = res.cheatcodes.map(|cheats| cheats.breakpoints).unwrap_or_default(); Ok(ScriptResult { returned: result, @@ -302,6 +302,7 @@ impl ScriptRunner { transactions, address: None, script_wallets, + breakpoints, }) } diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index d2ab5c0de80ff..b04f799e1550d 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -1,4 +1,4 @@ -use super::{debug::DebugArgs, install, test::filter::ProjectPathsAwareFilter, watch::WatchArgs}; +use super::{install, test::filter::ProjectPathsAwareFilter, watch::WatchArgs}; use clap::Parser; use ethers::types::U256; use eyre::Result; @@ -6,7 +6,7 @@ use forge::{ decode::decode_console_logs, executor::inspector::CheatsConfig, gas_report::GasReport, - result::{SuiteResult, TestKind, TestResult, TestStatus}, + result::{SuiteResult, TestResult, TestStatus}, trace::{ identifier::{EtherscanIdentifier, LocalTraceIdentifier, SignaturesIdentifier}, CallTraceDecoderBuilder, TraceKind, @@ -18,7 +18,8 @@ use foundry_cli::{ utils::{self, LoadConfig}, }; use foundry_common::{ - compile::{self, ProjectCompiler}, + compact_to_contract, + compile::{self, ContractSources, ProjectCompiler}, evm::EvmArgs, get_contract_name, get_file_name, shell, }; @@ -32,8 +33,9 @@ use foundry_config::{ }; use foundry_evm::fuzz::CounterExample; use regex::Regex; -use std::{collections::BTreeMap, path::PathBuf, sync::mpsc::channel, time::Duration}; +use std::{collections::BTreeMap, fs, sync::mpsc::channel, time::Duration}; use tracing::trace; +use ui::DebuggerArgs; use watchexec::config::{InitConfig, RuntimeConfig}; use yansi::Paint; @@ -177,64 +179,167 @@ impl TestArgs { let env = evm_opts.evm_env().await?; // Prepare the test builder - let mut runner = MultiContractRunnerBuilder::default() + let should_debug = self.debug.is_some(); + + let mut runner_builder = MultiContractRunnerBuilder::default() + .set_debug(should_debug) .initial_balance(evm_opts.initial_balance) .evm_spec(config.evm_spec_id()) .sender(evm_opts.sender) .with_fork(evm_opts.get_fork(&config, env.clone())) .with_cheats_config(CheatsConfig::new(&config, &evm_opts)) - .with_test_options(test_options.clone()) - .build(project_root, output, env, evm_opts)?; + .with_test_options(test_options.clone()); + + let mut runner = runner_builder.clone().build( + project_root, + output.clone(), + env.clone(), + evm_opts.clone(), + )?; + + if should_debug { + filter.args_mut().test_pattern = self.debug.clone(); + let n = runner.count_filtered_tests(&filter); + if n != 1 { + return Err( + eyre::eyre!("{n} tests matched your criteria, but exactly 1 test must match in order to run the debugger.\n + \n + Use --match-contract and --match-path to further limit the search.")); + } + let test_funcs = runner.get_typed_tests(&filter); + // if we debug a fuzz test, we should not collect data on the first run + if !test_funcs.get(0).unwrap().inputs.is_empty() { + runner_builder = runner_builder.set_debug(false); + runner = runner_builder.clone().build( + project_root, + output.clone(), + env.clone(), + evm_opts.clone(), + )?; + } + } - if self.debug.is_some() { - filter.args_mut().test_pattern = self.debug; - - match runner.count_filtered_tests(&filter) { - 1 => { - // Run the test - let results = runner.test(&filter, None, test_options).await; - - // Get the result of the single test - let (id, sig, test_kind, counterexample, breakpoints) = results.iter().map(|(id, SuiteResult{ test_results, .. })| { - let (sig, result) = test_results.iter().next().unwrap(); - - (id.clone(), sig.clone(), result.kind.clone(), result.counterexample.clone(), result.breakpoints.clone()) - }).next().unwrap(); - - // Build debugger args if this is a fuzz test - let sig = match test_kind { - TestKind::Fuzz { first_case, .. } => { - if let Some(CounterExample::Single(counterexample)) = counterexample { - counterexample.calldata.to_string() - } else { - first_case.calldata.to_string() - } - }, - _ => sig, - }; + let known_contracts = runner.known_contracts.clone(); + let mut local_identifier = LocalTraceIdentifier::new(&known_contracts); + let remote_chain_id = runner.evm_opts.get_remote_chain_id(); - // Run the debugger - let mut opts = self.opts.clone(); - opts.silent = true; - let debugger = DebugArgs { - path: PathBuf::from(runner.source_paths.get(&id).unwrap()), - target_contract: Some(get_contract_name(&id).to_string()), - sig, - args: Vec::new(), - debug: true, - opts, - evm_opts: self.evm_opts, + let outcome = self + .run_tests(runner, config.clone(), verbosity, filter.clone(), test_options.clone()) + .await?; + let tests = outcome.clone().into_tests(); + + let mut decoded_traces = Vec::new(); + let mut decoders = Vec::new(); + for test in tests { + let mut result = test.result; + // Identify addresses in each trace + let mut builder = CallTraceDecoderBuilder::new() + .with_labels(result.labeled_addresses.clone()) + .with_events(local_identifier.events().cloned()) + .with_verbosity(verbosity); + + // Signatures are of no value for gas reports + if !self.gas_report { + let sig_identifier = + SignaturesIdentifier::new(Config::foundry_cache_dir(), config.offline)?; + builder = builder.with_signature_identifier(sig_identifier.clone()); + } + + let mut decoder = builder.build(); + + if !result.traces.is_empty() { + // Set up identifiers + // Do not re-query etherscan for contracts that you've already queried today. + let mut etherscan_identifier = EtherscanIdentifier::new(&config, remote_chain_id)?; + + // Decode the traces + for (kind, trace) in &mut result.traces { + decoder.identify(trace, &mut local_identifier); + decoder.identify(trace, &mut etherscan_identifier); + + let should_include = match kind { + // At verbosity level 3, we only display traces for failed tests + // At verbosity level 4, we also display the setup trace for failed + // tests At verbosity level 5, we display + // all traces for all tests + TraceKind::Setup => { + (verbosity >= 5) || + (verbosity == 4 && result.status == TestStatus::Failure) + } + TraceKind::Execution => { + verbosity > 3 || + (verbosity == 3 && result.status == TestStatus::Failure) + } + _ => false, }; - debugger.debug(breakpoints).await?; - Ok(TestOutcome::new(results, self.allow_failure)) + // We decode the trace if we either need to build a gas report or we need + // to print it + if should_include || self.gas_report { + decoder.decode(trace).await; + } + + if should_include { + decoded_traces.push(trace.to_string()); + } + } + } + + decoders.push(decoder); + } + + if should_debug { + let mut sources: ContractSources = Default::default(); + for (id, artifact) in output.into_artifacts() { + // Sources are only required for the debugger, but it *might* mean that there's + // something wrong with the build and/or artifacts. + if let Some(source) = artifact.source_file() { + let abs_path = source + .ast + .ok_or(eyre::eyre!("Source from artifact has no AST."))? + .absolute_path; + let source_code = fs::read_to_string(abs_path)?; + let contract = artifact.clone().into_contract_bytecode(); + let source_contract = compact_to_contract(contract)?; + sources + .0 + .entry(id.clone().name) + .or_default() + .insert(source.id, (source_code, source_contract)); } - n => - Err( - eyre::eyre!("{n} tests matched your criteria, but exactly 1 test must match in order to run the debugger.\n - \n - Use --match-contract and --match-path to further limit the search.")) } + + let test = outcome.clone().into_tests().next().unwrap(); + let result = test.result; + + // Run the debugger + let debugger = DebuggerArgs { + debug: result.debug.map_or(vec![], |debug| vec![debug]), + decoder: decoders.first().unwrap(), + sources, + breakpoints: result.breakpoints, + }; + debugger.run()?; + } + + Ok(outcome) + } + + /// Run all tests that matches the filter predicate from a test runner + pub async fn run_tests( + &self, + mut runner: MultiContractRunner, + config: Config, + verbosity: u8, + mut filter: ProjectPathsAwareFilter, + test_options: TestOptions, + ) -> eyre::Result { + if self.debug.is_some() { + filter.args_mut().test_pattern = self.debug.clone(); + // Run the test + let results = runner.test(&filter, None, test_options).await; + + Ok(TestOutcome::new(results, self.allow_failure)) } else if self.list { list(runner, filter, self.json) } else { @@ -327,6 +432,7 @@ impl Test { } /// Represents the bundled results of all tests +#[derive(Clone)] pub struct TestOutcome { /// Whether failures are allowed pub allow_failure: bool, diff --git a/crates/forge/bin/main.rs b/crates/forge/bin/main.rs index d311007f8134a..5ef7e7581632c 100644 --- a/crates/forge/bin/main.rs +++ b/crates/forge/bin/main.rs @@ -35,7 +35,7 @@ fn main() -> Result<()> { cmd.opts.args.silent, cmd.json, ))?; - utils::block_on(cmd.run_script(Default::default())) + utils::block_on(cmd.run_script()) } Subcommands::Coverage(cmd) => utils::block_on(cmd.run()), Subcommands::Bind(cmd) => cmd.run(), @@ -46,7 +46,7 @@ fn main() -> Result<()> { cmd.run().map(|_| ()) } } - Subcommands::Debug(cmd) => utils::block_on(cmd.debug(Default::default())), + Subcommands::Debug(cmd) => utils::block_on(cmd.run()), Subcommands::VerifyContract(args) => utils::block_on(args.run()), Subcommands::VerifyCheck(args) => utils::block_on(args.run()), Subcommands::Cache(cmd) => match cmd.sub { diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 55b2c34e13646..ad2c2846f413d 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -1,6 +1,6 @@ use crate::{result::SuiteResult, ContractRunner, TestFilter, TestOptions}; use ethers::{ - abi::Abi, + abi::{Abi, Function}, prelude::{artifacts::CompactContractBytecode, ArtifactId, ArtifactOutput}, solc::{contracts::ArtifactContracts, Artifact, ProjectCompileOutput}, types::{Address, Bytes, U256}, @@ -19,6 +19,7 @@ use rayon::prelude::*; use revm::primitives::SpecId; use std::{ collections::{BTreeMap, HashSet}, + iter::Iterator, path::Path, sync::{mpsc::Sender, Arc}, }; @@ -51,6 +52,8 @@ pub struct MultiContractRunner { pub cheats_config: Arc, /// Whether to collect coverage info pub coverage: bool, + /// Whether to collect debug info + pub debug: bool, /// Settings related to fuzz and/or invariant tests pub test_options: TestOptions, } @@ -70,19 +73,33 @@ impl MultiContractRunner { .count() } - // Get all tests of matching path and contract - pub fn get_tests(&self, filter: &impl TestFilter) -> Vec { + /// Get an iterator over all test functions that matches the filter path and contract name + fn filtered_tests<'a>( + &'a self, + filter: &'a impl TestFilter, + ) -> impl Iterator { self.contracts .iter() .filter(|(id, _)| { filter.matches_path(id.source.to_string_lossy()) && filter.matches_contract(&id.name) }) - .flat_map(|(_, (abi, _, _))| abi.functions().map(|func| func.name.clone())) - .filter(|sig| sig.is_test()) + .flat_map(|(_, (abi, _, _))| abi.functions()) + } + + /// Get all test names matching the filter + pub fn get_tests(&self, filter: &impl TestFilter) -> Vec { + self.filtered_tests(filter) + .map(|func| func.name.clone()) + .filter(|name| name.is_test()) .collect() } + /// Returns all test functions matching the filter + pub fn get_typed_tests<'a>(&'a self, filter: &'a impl TestFilter) -> Vec<&Function> { + self.filtered_tests(filter).filter(|func| func.name.is_test()).collect() + } + /// Returns all matching tests grouped by contract grouped by file (file -> (contract -> tests)) pub fn list( &self, @@ -143,7 +160,8 @@ impl MultiContractRunner { .inspectors(|stack| { stack .cheatcodes(self.cheats_config.clone()) - .trace(self.evm_opts.verbosity >= 3) + .trace(self.evm_opts.verbosity >= 3 || self.debug) + .debug(self.debug) .coverage(self.coverage) }) .spec(self.evm_spec) @@ -193,13 +211,14 @@ impl MultiContractRunner { self.sender, self.errors.as_ref(), libs, + self.debug, ); runner.run_tests(filter, test_options, Some(&self.known_contracts)) } } /// Builder used for instantiating the multi-contract runner -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct MultiContractRunnerBuilder { /// The address which will be used to deploy the initial contracts and send all /// transactions @@ -214,6 +233,8 @@ pub struct MultiContractRunnerBuilder { pub cheats_config: Option, /// Whether or not to collect coverage info pub coverage: bool, + /// Whether or not to collect debug info + pub debug: bool, /// Settings related to fuzz and/or invariant tests pub test_options: Option, } @@ -326,6 +347,7 @@ impl MultiContractRunnerBuilder { fork: self.fork, cheats_config: self.cheats_config.unwrap_or_default().into(), coverage: self.coverage, + debug: self.debug, test_options: self.test_options.unwrap_or_default(), }) } @@ -371,4 +393,10 @@ impl MultiContractRunnerBuilder { self.coverage = enable; self } + + #[must_use] + pub fn set_debug(mut self, enable: bool) -> Self { + self.debug = enable; + self + } } diff --git a/crates/forge/src/result.rs b/crates/forge/src/result.rs index ca717e61b6484..bd80de843c6c0 100644 --- a/crates/forge/src/result.rs +++ b/crates/forge/src/result.rs @@ -5,6 +5,7 @@ use ethers::prelude::Log; use foundry_common::evm::Breakpoints; use foundry_evm::{ coverage::HitMaps, + debug::DebugArena, executor::EvmError, fuzz::{types::FuzzCase, CounterExample}, trace::{TraceKind, Traces}, @@ -122,6 +123,9 @@ pub struct TestResult { /// Labeled addresses pub labeled_addresses: BTreeMap, + /// The debug nodes of the call + pub debug: Option, + /// pc breakpoint char map pub breakpoints: Breakpoints, } diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index b81ce598aca64..1d1bd5e13ded0 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -20,7 +20,8 @@ use foundry_evm::{ replay_run, InvariantContract, InvariantExecutor, InvariantFuzzError, InvariantFuzzTestResult, }, - FuzzedExecutor, + types::{CaseOutcome, CounterExampleOutcome, FuzzOutcome}, + CounterExample, FuzzedExecutor, }, trace::{load_contracts, TraceKind}, CALLER, @@ -46,11 +47,12 @@ pub struct ContractRunner<'a> { pub contract: &'a Abi, /// All known errors, used to decode reverts pub errors: Option<&'a Abi>, - /// The initial balance of the test contract pub initial_balance: U256, /// The address which will be used as the `from` field in all EVM calls pub sender: Address, + /// Should generate debug traces + pub debug: bool, } impl<'a> ContractRunner<'a> { @@ -64,6 +66,7 @@ impl<'a> ContractRunner<'a> { sender: Option
, errors: Option<&'a Abi>, predeploy_libs: &'a [Bytes], + debug: bool, ) -> Self { Self { name, @@ -74,6 +77,7 @@ impl<'a> ContractRunner<'a> { sender: sender.unwrap_or_default(), errors, predeploy_libs, + debug, } } } @@ -232,7 +236,7 @@ impl<'a> ContractRunner<'a> { traces: setup.traces, coverage: None, labeled_addresses: setup.labeled_addresses, - breakpoints: Default::default(), + ..Default::default() }, )] .into(), @@ -307,6 +311,7 @@ impl<'a> ContractRunner<'a> { // Run unit test let mut executor = self.executor.clone(); let start = Instant::now(); + let mut debug_arena = None; let (reverted, reason, gas, stipend, coverage, state_changeset, breakpoints) = match executor.execute_test::<(), _, _>( self.sender, @@ -325,12 +330,14 @@ impl<'a> ContractRunner<'a> { coverage, labels: new_labels, state_changeset, + debug, breakpoints, .. }) => { traces.extend(execution_trace.map(|traces| (TraceKind::Execution, traces))); labeled_addresses.extend(new_labels); logs.extend(execution_logs); + debug_arena = debug; (reverted, None, gas, stipend, coverage, state_changeset, breakpoints) } Err(EvmError::Execution(err)) => { @@ -398,6 +405,7 @@ impl<'a> ContractRunner<'a> { traces, coverage, labeled_addresses, + debug: debug_arena, breakpoints, } } @@ -530,7 +538,7 @@ impl<'a> ContractRunner<'a> { coverage: None, // TODO ? traces, labeled_addresses: labeled_addresses.clone(), - breakpoints: Default::default(), + ..Default::default() // TODO collect debug traces on the last run or error } } @@ -547,8 +555,13 @@ impl<'a> ContractRunner<'a> { // Run fuzz test let start = Instant::now(); - let mut result = FuzzedExecutor::new(&self.executor, runner, self.sender, fuzz_config) - .fuzz(func, address, should_fail, self.errors); + let fuzzed_executor = + FuzzedExecutor::new(&self.executor, runner.clone(), self.sender, fuzz_config); + let state = fuzzed_executor.build_fuzz_state(); + let mut result = fuzzed_executor.fuzz(func, address, should_fail, self.errors); + + let mut debug = Default::default(); + let mut breakpoints = Default::default(); // Check the last test result and skip the test // if it's marked as so. @@ -560,10 +573,50 @@ impl<'a> ContractRunner<'a> { traces, labeled_addresses, kind: TestKind::Standard(0), + debug, + breakpoints, ..Default::default() } } + // if should debug + if self.debug { + let mut debug_executor = self.executor.clone(); + // turn the debug traces on + debug_executor.inspector.enable_debugger(true); + debug_executor.inspector.tracing(true); + let calldata = if let Some(counterexample) = result.counterexample.as_ref() { + match counterexample { + CounterExample::Single(ce) => ce.calldata.clone(), + _ => unimplemented!(), + } + } else { + result.first_case.calldata.clone() + }; + // rerun the last relevant test with traces + let debug_result = FuzzedExecutor::new( + &debug_executor, + runner, + self.sender, + fuzz_config, + ) + .single_fuzz(&state, address, should_fail, calldata); + + (debug, breakpoints) = match debug_result { + Ok(fuzz_outcome) => match fuzz_outcome { + FuzzOutcome::Case(CaseOutcome { debug, breakpoints, .. }) => { + (debug, breakpoints) + } + FuzzOutcome::CounterExample(CounterExampleOutcome { + debug, + breakpoints, + .. + }) => (debug, breakpoints), + }, + Err(_) => (Default::default(), Default::default()), + }; + } + let kind = TestKind::Fuzz { median_gas: result.median_gas(false), mean_gas: result.mean_gas(false), @@ -595,7 +648,8 @@ impl<'a> ContractRunner<'a> { traces, coverage: result.coverage, labeled_addresses, - breakpoints: Default::default(), + debug, + breakpoints, } } } diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 74bce4d2fea8b..0fdc22ded8121 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -17,5 +17,6 @@ ethers.workspace = true crossterm = "0.26" eyre = "0.6" +tracing = "0.1" revm = { workspace = true, features = ["std", "serde"] } -ratatui = { version = "0.22.0", default-features = false, features = ["crossterm"]} \ No newline at end of file +ratatui = { version = "0.22.0", default-features = false, features = ["crossterm"]} diff --git a/crates/ui/src/debugger.rs b/crates/ui/src/debugger.rs new file mode 100644 index 0000000000000..55ddcbd92a29e --- /dev/null +++ b/crates/ui/src/debugger.rs @@ -0,0 +1,47 @@ +use crate::Ui; +use foundry_common::{compile::ContractSources, evm::Breakpoints, get_contract_name}; +use foundry_evm::{debug::DebugArena, trace::CallTraceDecoder}; +use tracing::trace; + +use crate::{TUIExitReason, Tui}; + +/// Standardized way of firing up the debugger +pub struct DebuggerArgs<'a> { + /// debug traces returned from the execution + pub debug: Vec, + /// trace decoder + pub decoder: &'a CallTraceDecoder, + /// map of source files + pub sources: ContractSources, + /// map of the debugger breakpoints + pub breakpoints: Breakpoints, +} + +impl DebuggerArgs<'_> { + pub fn run(&self) -> eyre::Result { + trace!(target: "debugger", "running debugger"); + + let flattened = self + .debug + .last() + .map(|arena| arena.flatten(0)) + .expect("We should have collected debug information"); + + let identified_contracts = self + .decoder + .contracts + .iter() + .map(|(addr, identifier)| (*addr, get_contract_name(identifier).to_string())) + .collect(); + + let tui = Tui::new( + flattened, + 0, + identified_contracts, + self.sources.clone(), + self.breakpoints.clone(), + )?; + + tui.start() + } +} diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index ca13218fa726f..7b25d2aa0d0b2 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -8,9 +8,9 @@ use crossterm::{ execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; -use ethers::{solc::artifacts::ContractBytecodeSome, types::Address}; +use ethers::types::Address; use eyre::Result; -use foundry_common::evm::Breakpoints; +use foundry_common::{compile::ContractSources, evm::Breakpoints}; use foundry_evm::{ debug::{DebugStep, Instruction}, utils::{build_pc_ic_map, PCICMap}, @@ -50,6 +50,9 @@ pub enum TUIExitReason { mod op_effects; use op_effects::stack_indices_affected; +mod debugger; +pub use debugger::*; + pub struct Tui { debug_arena: Vec<(Address, Vec, CallKind)>, terminal: Terminal>, @@ -58,8 +61,8 @@ pub struct Tui { /// Current step in the debug steps current_step: usize, identified_contracts: HashMap, - known_contracts: HashMap, - known_contracts_sources: 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, @@ -72,8 +75,7 @@ impl Tui { debug_arena: Vec<(Address, Vec, CallKind)>, current_step: usize, identified_contracts: HashMap, - known_contracts: HashMap, - known_contracts_sources: HashMap>, + contracts_sources: ContractSources, breakpoints: Breakpoints, ) -> Result { enable_raw_mode()?; @@ -82,28 +84,31 @@ impl Tui { let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; terminal.hide_cursor(); - let pc_ic_maps = known_contracts + let pc_ic_maps = contracts_sources + .0 .iter() - .filter_map(|(contract_name, bytecode)| { - Some(( - contract_name.clone(), - ( - build_pc_ic_map( - SpecId::LATEST, - bytecode.bytecode.object.as_bytes()?.as_ref(), - ), - build_pc_ic_map( - SpecId::LATEST, - bytecode - .deployed_bytecode - .bytecode - .as_ref()? - .object - .as_bytes()? - .as_ref(), + .flat_map(|(contract_name, files_sources)| { + files_sources.iter().filter_map(|(_, (_, contract))| { + Some(( + contract_name.clone(), + ( + build_pc_ic_map( + SpecId::LATEST, + contract.bytecode.object.as_bytes()?.as_ref(), + ), + build_pc_ic_map( + SpecId::LATEST, + contract + .deployed_bytecode + .bytecode + .as_ref()? + .object + .as_bytes()? + .as_ref(), + ), ), - ), - )) + )) + }) }) .collect(); Ok(Tui { @@ -112,8 +117,7 @@ impl Tui { key_buffer: String::new(), current_step, identified_contracts, - known_contracts, - known_contracts_sources, + contracts_sources, pc_ic_maps, breakpoints, }) @@ -138,9 +142,8 @@ impl Tui { f: &mut Frame, address: Address, identified_contracts: &HashMap, - known_contracts: &HashMap, pc_ic_maps: &BTreeMap, - known_contracts_sources: &HashMap>, + contracts_sources: &ContractSources, debug_steps: &[DebugStep], opcode_list: &[String], current_step: usize, @@ -156,9 +159,8 @@ impl Tui { f, address, identified_contracts, - known_contracts, pc_ic_maps, - known_contracts_sources, + contracts_sources, debug_steps, opcode_list, current_step, @@ -173,9 +175,8 @@ impl Tui { f, address, identified_contracts, - known_contracts, pc_ic_maps, - known_contracts_sources, + contracts_sources, debug_steps, opcode_list, current_step, @@ -193,9 +194,8 @@ impl Tui { f: &mut Frame, address: Address, identified_contracts: &HashMap, - known_contracts: &HashMap, pc_ic_maps: &BTreeMap, - known_contracts_sources: &HashMap>, + contracts_sources: &ContractSources, debug_steps: &[DebugStep], opcode_list: &[String], current_step: usize, @@ -235,9 +235,8 @@ impl Tui { f, address, identified_contracts, - known_contracts, pc_ic_maps, - known_contracts_sources, + contracts_sources, debug_steps[current_step].pc, call_kind, src_pane, @@ -273,9 +272,8 @@ impl Tui { f: &mut Frame, address: Address, identified_contracts: &HashMap, - known_contracts: &HashMap, pc_ic_maps: &BTreeMap, - known_contracts_sources: &HashMap>, + contracts_sources: &ContractSources, debug_steps: &[DebugStep], opcode_list: &[String], current_step: usize, @@ -320,9 +318,8 @@ impl Tui { f, address, identified_contracts, - known_contracts, pc_ic_maps, - known_contracts_sources, + contracts_sources, debug_steps[current_step].pc, call_kind, src_pane, @@ -384,9 +381,8 @@ Line::from(Span::styled("[t]: stack labels | [m]: memory decoding | [shift + j/k f: &mut Frame, address: Address, identified_contracts: &HashMap, - known_contracts: &HashMap, pc_ic_maps: &BTreeMap, - known_contracts_sources: &HashMap>, + contracts_sources: &ContractSources, pc: usize, call_kind: CallKind, area: Rect, @@ -404,291 +400,283 @@ Line::from(Span::styled("[t]: stack labels | [m]: memory decoding | [shift + j/k let mut text_output: Text = Text::from(""); if let Some(contract_name) = identified_contracts.get(&address) { - if let (Some(known), Some(source_code)) = - (known_contracts.get(contract_name), known_contracts_sources.get(contract_name)) - { + if let Some(files_source_code) = contracts_sources.0.get(contract_name) { let pc_ic_map = pc_ic_maps.get(contract_name); - // grab either the creation source map or runtime sourcemap - if let Some((sourcemap, ic)) = - if matches!(call_kind, CallKind::Create | CallKind::Create2) { - known.bytecode.source_map().zip(pc_ic_map.and_then(|(c, _)| c.get(&pc))) + // find the contract source with the correct source_element's file_id + if let Some((source_element, source_code)) = files_source_code.iter().find_map( + |(file_id, (source_code, contract_source))| { + // grab either the creation source map or runtime sourcemap + if let Some((Ok(source_map), ic)) = + if matches!(call_kind, CallKind::Create | CallKind::Create2) { + contract_source + .bytecode + .source_map() + .zip(pc_ic_map.and_then(|(c, _)| c.get(&pc))) + } else { + contract_source + .deployed_bytecode + .bytecode + .as_ref() + .expect("no bytecode") + .source_map() + .zip(pc_ic_map.and_then(|(_, r)| r.get(&pc))) + } + { + let source_element = source_map[*ic].clone(); + if let Some(index) = source_element.index { + if *file_id == index { + Some((source_element, source_code)) + } else { + None + } + } else { + None + } + } else { + None + } + }, + ) { + // we are handed a vector of SourceElements that give + // us a span of sourcecode that is currently being executed + // This includes an offset and length. This vector is in + // instruction pointer order, meaning the location of + // the instruction - sum(push_bytes[..pc]) + let offset = source_element.offset; + let len = source_element.length; + let max = source_code.len(); + + // split source into before, relevant, and after chunks + // split by line as well to do some formatting stuff + let mut before = source_code[..std::cmp::min(offset, max)] + .split_inclusive('\n') + .collect::>(); + let actual = source_code + [std::cmp::min(offset, max)..std::cmp::min(offset + len, max)] + .split_inclusive('\n') + .map(|s| s.to_string()) + .collect::>(); + let mut after = source_code[std::cmp::min(offset + len, max)..] + .split_inclusive('\n') + .collect::>(); + + let mut line_number = 0; + + let num_lines = before.len() + actual.len() + after.len(); + let height = area.height as usize; + let needed_highlight = actual.len(); + let mid_len = before.len() + actual.len(); + + // adjust what text we show of the source code + let (start_line, end_line) = if needed_highlight > height { + // highlighted section is more lines than we have avail + (before.len(), before.len() + needed_highlight) + } else if height > num_lines { + // we can fit entire source + (0, num_lines) } else { - known - .deployed_bytecode - .bytecode - .as_ref() - .expect("no bytecode") - .source_map() - .zip(pc_ic_map.and_then(|(_, r)| r.get(&pc))) - } - { - match sourcemap { - Ok(sourcemap) => { - // we are handed a vector of SourceElements that give - // us a span of sourcecode that is currently being executed - // This includes an offset and length. This vector is in - // instruction pointer order, meaning the location of - // the instruction - sum(push_bytes[..pc]) - if let Some(source_idx) = sourcemap[*ic].index { - if let Some(source) = source_code.get(&source_idx) { - let offset = sourcemap[*ic].offset; - let len = sourcemap[*ic].length; - let max = source.len(); - - // split source into before, relevant, and after chunks - // split by line as well to do some formatting stuff - let mut before = source[..std::cmp::min(offset, max)] - .split_inclusive('\n') - .collect::>(); - let actual = source[std::cmp::min(offset, max).. - std::cmp::min(offset + len, max)] - .split_inclusive('\n') - .map(|s| s.to_string()) - .collect::>(); - let mut after = source[std::cmp::min(offset + len, max)..] - .split_inclusive('\n') - .collect::>(); - - let mut line_number = 0; - - let num_lines = before.len() + actual.len() + after.len(); - let height = area.height as usize; - let needed_highlight = actual.len(); - let mid_len = before.len() + actual.len(); - - // adjust what text we show of the source code - let (start_line, end_line) = if needed_highlight > height { - // highlighted section is more lines than we have avail - (before.len(), before.len() + needed_highlight) - } else if height > num_lines { - // we can fit entire source - (0, num_lines) - } else { - let remaining = height - needed_highlight; - let mut above = remaining / 2; - let mut below = remaining / 2; - if below > after.len() { - // unused space below the highlight - above += below - after.len(); - } else if above > before.len() { - // we have unused space above the highlight - below += above - before.len(); - } else { - // no unused space - } + let remaining = height - needed_highlight; + let mut above = remaining / 2; + let mut below = remaining / 2; + if below > after.len() { + // unused space below the highlight + above += below - after.len(); + } else if above > before.len() { + // we have unused space above the highlight + below += above - before.len(); + } else { + // no unused space + } - (before.len().saturating_sub(above), mid_len + below) - }; - - let max_line_num = num_lines.to_string().len(); - // We check if there is other text on the same line before the - // highlight starts - if let Some(last) = before.pop() { - if !last.ends_with('\n') { - before.iter().skip(start_line).for_each(|line| { - text_output.lines.push(Line::from(vec![ - Span::styled( - format!( - "{: >max_line_num$}", - line_number.to_string(), - max_line_num = max_line_num - ), - Style::default() - .fg(Color::Gray) - .bg(Color::DarkGray), - ), - Span::styled( - "\u{2800} ".to_string() + line, - Style::default() - .add_modifier(Modifier::DIM), - ), - ])); - line_number += 1; - }); - - text_output.lines.push(Line::from(vec![ - Span::styled( - format!( - "{: >max_line_num$}", - line_number.to_string(), - max_line_num = max_line_num - ), - Style::default() - .fg(Color::Cyan) - .bg(Color::DarkGray) - .add_modifier(Modifier::BOLD), - ), - Span::raw("\u{2800} "), - Span::raw(last), - Span::styled( - actual[0].to_string(), - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - ])); - line_number += 1; - - actual.iter().skip(1).for_each(|s| { - text_output.lines.push(Line::from(vec![ - Span::styled( - format!( - "{: >max_line_num$}", - line_number.to_string(), - max_line_num = max_line_num - ), - Style::default() - .fg(Color::Cyan) - .bg(Color::DarkGray) - .add_modifier(Modifier::BOLD), - ), - Span::raw("\u{2800} "), - Span::styled( - // this is a hack to add coloring - // because tui does weird trimming - if s.is_empty() || s == "\n" { - "\u{2800} \n".to_string() - } else { - s.to_string() - }, - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - ])); - line_number += 1; - }); + (before.len().saturating_sub(above), mid_len + below) + }; + + let max_line_num = num_lines.to_string().len(); + // We check if there is other text on the same line before the + // highlight starts + if let Some(last) = before.pop() { + if !last.ends_with('\n') { + before.iter().skip(start_line).for_each(|line| { + text_output.lines.push(Line::from(vec![ + Span::styled( + format!( + "{: >max_line_num$}", + line_number.to_string(), + max_line_num = max_line_num + ), + Style::default().fg(Color::Gray).bg(Color::DarkGray), + ), + Span::styled( + "\u{2800} ".to_string() + line, + Style::default().add_modifier(Modifier::DIM), + ), + ])); + line_number += 1; + }); + + text_output.lines.push(Line::from(vec![ + Span::styled( + format!( + "{: >max_line_num$}", + line_number.to_string(), + max_line_num = max_line_num + ), + Style::default() + .fg(Color::Cyan) + .bg(Color::DarkGray) + .add_modifier(Modifier::BOLD), + ), + Span::raw("\u{2800} "), + Span::raw(last), + Span::styled( + actual[0].to_string(), + Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), + ), + ])); + line_number += 1; + + actual.iter().skip(1).for_each(|s| { + text_output.lines.push(Line::from(vec![ + Span::styled( + format!( + "{: >max_line_num$}", + line_number.to_string(), + max_line_num = max_line_num + ), + Style::default() + .fg(Color::Cyan) + .bg(Color::DarkGray) + .add_modifier(Modifier::BOLD), + ), + Span::raw("\u{2800} "), + Span::styled( + // this is a hack to add coloring + // because tui does weird trimming + if s.is_empty() || s == "\n" { + "\u{2800} \n".to_string() } else { - before.push(last); - before.iter().skip(start_line).for_each(|line| { - text_output.lines.push(Line::from(vec![ - Span::styled( - format!( - "{: >max_line_num$}", - line_number.to_string(), - max_line_num = max_line_num - ), - Style::default() - .fg(Color::Gray) - .bg(Color::DarkGray), - ), - Span::styled( - "\u{2800} ".to_string() + line, - Style::default() - .add_modifier(Modifier::DIM), - ), - ])); - - line_number += 1; - }); - actual.iter().for_each(|s| { - text_output.lines.push(Line::from(vec![ - Span::styled( - format!( - "{: >max_line_num$}", - line_number.to_string(), - max_line_num = max_line_num - ), - Style::default() - .fg(Color::Cyan) - .bg(Color::DarkGray) - .add_modifier(Modifier::BOLD), - ), - Span::raw("\u{2800} "), - Span::styled( - if s.is_empty() || s == "\n" { - "\u{2800} \n".to_string() - } else { - s.to_string() - }, - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - ])); - line_number += 1; - }); - } + s.to_string() + }, + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + ])); + line_number += 1; + }); + } else { + before.push(last); + before.iter().skip(start_line).for_each(|line| { + text_output.lines.push(Line::from(vec![ + Span::styled( + format!( + "{: >max_line_num$}", + line_number.to_string(), + max_line_num = max_line_num + ), + Style::default().fg(Color::Gray).bg(Color::DarkGray), + ), + Span::styled( + "\u{2800} ".to_string() + line, + Style::default().add_modifier(Modifier::DIM), + ), + ])); + + line_number += 1; + }); + actual.iter().for_each(|s| { + text_output.lines.push(Line::from(vec![ + Span::styled( + format!( + "{: >max_line_num$}", + line_number.to_string(), + max_line_num = max_line_num + ), + Style::default() + .fg(Color::Cyan) + .bg(Color::DarkGray) + .add_modifier(Modifier::BOLD), + ), + Span::raw("\u{2800} "), + Span::styled( + if s.is_empty() || s == "\n" { + "\u{2800} \n".to_string() + } else { + s.to_string() + }, + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + ])); + line_number += 1; + }); + } + } else { + actual.iter().for_each(|s| { + text_output.lines.push(Line::from(vec![ + Span::styled( + format!( + "{: >max_line_num$}", + line_number.to_string(), + max_line_num = max_line_num + ), + Style::default() + .fg(Color::Cyan) + .bg(Color::DarkGray) + .add_modifier(Modifier::BOLD), + ), + Span::raw("\u{2800} "), + Span::styled( + if s.is_empty() || s == "\n" { + "\u{2800} \n".to_string() } else { - actual.iter().for_each(|s| { - text_output.lines.push(Line::from(vec![ - Span::styled( - format!( - "{: >max_line_num$}", - line_number.to_string(), - max_line_num = max_line_num - ), - Style::default() - .fg(Color::Cyan) - .bg(Color::DarkGray) - .add_modifier(Modifier::BOLD), - ), - Span::raw("\u{2800} "), - Span::styled( - if s.is_empty() || s == "\n" { - "\u{2800} \n".to_string() - } else { - s.to_string() - }, - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - ])); - line_number += 1; - }); - } - - // fill in the rest of the line as unhighlighted - if let Some(last) = actual.last() { - if !last.ends_with('\n') { - if let Some(post) = after.pop_front() { - if let Some(last) = text_output.lines.last_mut() { - last.spans.push(Span::raw(post)); - } - } - } - } + s.to_string() + }, + Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), + ), + ])); + line_number += 1; + }); + } - // add after highlighted text - while mid_len + after.len() > end_line { - after.pop_back(); - } - after.iter().for_each(|line| { - text_output.lines.push(Line::from(vec![ - Span::styled( - format!( - "{: >max_line_num$}", - line_number.to_string(), - max_line_num = max_line_num - ), - Style::default() - .fg(Color::Gray) - .bg(Color::DarkGray), - ), - Span::styled( - "\u{2800} ".to_string() + line, - Style::default().add_modifier(Modifier::DIM), - ), - ])); - line_number += 1; - }); - } else { - text_output.extend(Text::from("No source for srcmap index")); + // fill in the rest of the line as unhighlighted + if let Some(last) = actual.last() { + if !last.ends_with('\n') { + if let Some(post) = after.pop_front() { + if let Some(last) = text_output.lines.last_mut() { + last.spans.push(Span::raw(post)); } - } else { - text_output.extend(Text::from("No srcmap index")); } } - Err(e) => text_output.extend(Text::from(format!( - "Error in source map parsing: '{e}', please open an issue" - ))), } + + // add after highlighted text + while mid_len + after.len() > end_line { + after.pop_back(); + } + after.iter().for_each(|line| { + text_output.lines.push(Line::from(vec![ + Span::styled( + format!( + "{: >max_line_num$}", + line_number.to_string(), + max_line_num = max_line_num + ), + Style::default().fg(Color::Gray).bg(Color::DarkGray), + ), + Span::styled( + "\u{2800} ".to_string() + line, + Style::default().add_modifier(Modifier::DIM), + ), + ])); + line_number += 1; + }); } else { text_output.extend(Text::from("No sourcemap for contract")); } } else { - text_output.extend(Text::from(format!("Unknown contract at address {address:?}"))); + text_output.extend(Text::from("No srcmap index for contract {contract_name}")); } } else { text_output.extend(Text::from(format!("Unknown contract at address {address:?}"))); @@ -1290,9 +1278,8 @@ impl Ui for Tui { f, debug_call[draw_memory.inner_call_index].0, &self.identified_contracts, - &self.known_contracts, &self.pc_ic_maps, - &self.known_contracts_sources, + &self.contracts_sources, &debug_call[draw_memory.inner_call_index].1[..], &opcode_list, current_step,