Skip to content

Commit

Permalink
Refactored debugger to extract TUI abstraction. Added option to dump …
Browse files Browse the repository at this point in the history
…debugger context to file as json.
  • Loading branch information
piohei committed Mar 19, 2024
1 parent 0026488 commit 84608e0
Show file tree
Hide file tree
Showing 17 changed files with 382 additions and 98 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/cli/src/utils/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,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?;
}
Expand Down
17 changes: 9 additions & 8 deletions crates/common/src/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,8 @@ impl ProjectCompiler {
pub struct ContractSources {
/// Map over artifacts' contract names -> vector of file IDs
pub ids_by_name: HashMap<String, Vec<u32>>,
/// Map over file_id -> (source code, contract)
pub sources_by_id: FxHashMap<u32, (String, ContractBytecodeSome)>,
/// Map over file_id -> (source code, contract, source path)
pub sources_by_id: FxHashMap<u32, (String, ContractBytecodeSome, Option<PathBuf>)>,
}

impl ContractSources {
Expand All @@ -301,7 +301,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 {
Expand All @@ -310,7 +310,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");
}
Expand All @@ -325,28 +325,29 @@ impl ContractSources {
file_id: u32,
source: String,
bytecode: ContractBytecodeSome,
source_path: Option<PathBuf>
) {
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<PathBuf>)> {
self.sources_by_id.get(&id)
}

/// Returns all sources for a contract by name.
pub fn get_sources(
&self,
name: &str,
) -> Option<impl Iterator<Item = (u32, &(String, ContractBytecodeSome))>> {
) -> Option<impl Iterator<Item = (u32, &(String, ContractBytecodeSome, Option<PathBuf>))>> {
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<Item = (String, &(String, ContractBytecodeSome))> {
pub fn entries(&self) -> impl Iterator<Item = (String, &(String, ContractBytecodeSome, Option<PathBuf>))> {
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)))
})
Expand Down
2 changes: 2 additions & 0 deletions crates/debugger/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ eyre.workspace = true
ratatui = { version = "0.24.0", default-features = false, features = ["crossterm"] }
revm.workspace = true
tracing.workspace = true
serde.workspace = true
arrayvec.workspace = true
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! TUI debugger builder.
//! Debugger builder.

use crate::Debugger;
use alloy_primitives::Address;
Expand Down
16 changes: 16 additions & 0 deletions crates/debugger/src/context.rs
Original file line number Diff line number Diff line change
@@ -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<DebugNodeFlat>,
pub identified_contracts: HashMap<Address, String>,
/// 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<String, (PcIcMap, PcIcMap)>,
pub breakpoints: Breakpoints,
}
83 changes: 83 additions & 0 deletions crates/debugger/src/debugger.rs
Original file line number Diff line number Diff line change
@@ -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<DebugNodeFlat>,
identified_contracts: HashMap<Address, String>,
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<ExitReason> {
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()
}
}
178 changes: 178 additions & 0 deletions crates/debugger/src/file_dumper/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
//! 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<CallDump>,
// Map of contract name to PcIcMapDump
pc_ic_maps: HashMap<String, PcIcMapDump>,
}

#[derive(Serialize)]
struct CallDump {
address: Address,
kind: CallKind,
steps: Vec<StepDump>,
}

#[derive(Serialize)]
struct StepDump {
/// Stack *prior* to running the associated opcode
stack: Vec<U256>,
/// 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: Bytes,
/// The program counter at this step.
pc: usize,
/// Cumulative gas usage
total_gas_used: u64,
}

#[derive(Serialize)]
struct PcIcMapDump {
create_code_map: HashMap<usize, usize>,
runtime_code_map: HashMap<usize, usize>,
}

#[derive(Serialize)]
struct ContractsDump {
// Map of call address to contract name
identified_calls: HashMap<Address, String>,
sources: ContractsSourcesDump,
}

#[derive(Serialize)]
struct ContractsSourcesDump {
ids_by_name: HashMap<String, Vec<u32>>,
sources_by_id: HashMap<u32, ContractSourceDetailsDump>,
}

#[derive(Serialize)]
struct ContractSourceDetailsDump {
source_code: String,
contract_bytecode: ContractBytecodeSome,
source_path: Option<PathBuf>,
}

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: Bytes::from(step.push_bytes.to_vec()),
pc: step.pc,
total_gas_used: step.total_gas_used,
}
}

fn to_pc_ic_map_dump(pc_ic_map: &(PcIcMap, PcIcMap)) -> PcIcMapDump {
let mut create_code_map = HashMap::new();
for (k, v) in pc_ic_map.0.inner.iter() {
create_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 { create_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(),
}
}

0 comments on commit 84608e0

Please sign in to comment.