Skip to content

Commit

Permalink
[wip] feat: identify internal function invocations in traces
Browse files Browse the repository at this point in the history
  • Loading branch information
klkvr committed Jun 21, 2024
1 parent 0460633 commit 9fd779a
Show file tree
Hide file tree
Showing 19 changed files with 571 additions and 273 deletions.
126 changes: 62 additions & 64 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -259,3 +259,6 @@ tower = "0.4"
tower-http = "0.5"
# soldeer
soldeer = "0.2.15"

[patch.crates-io]
revm-inspectors = { git = "https://github.com/klkvr/evm-inspectors", rev = "7b649e5" }
8 changes: 6 additions & 2 deletions crates/cast/bin/cmd/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ pub struct CallArgs {
#[arg(long, requires = "trace")]
debug: bool,

#[arg(long, requires = "trace")]
decode_internal: bool,

/// Labels to apply to the traces; format: `address:label`.
/// Can only be used with `--trace`.
#[arg(long, requires = "trace")]
Expand Down Expand Up @@ -106,6 +109,7 @@ impl CallArgs {
trace,
evm_version,
debug,
decode_internal,
labels,
data,
} = self;
Expand Down Expand Up @@ -159,7 +163,7 @@ impl CallArgs {
}

let (env, fork, chain) = TracingExecutor::get_fork_material(&config, evm_opts).await?;
let mut executor = TracingExecutor::new(env, fork, evm_version, debug);
let mut executor = TracingExecutor::new(env, fork, evm_version, debug, decode_internal);

let value = tx.value.unwrap_or_default();
let input = tx.inner.input.into_input().unwrap_or_default();
Expand All @@ -175,7 +179,7 @@ impl CallArgs {
),
};

handle_traces(trace, &config, chain, labels, debug).await?;
handle_traces(trace, &config, chain, labels, debug, decode_internal).await?;

return Ok(());
}
Expand Down
8 changes: 6 additions & 2 deletions crates/cast/bin/cmd/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ pub struct RunArgs {
#[arg(long, short)]
debug: bool,

/// Whether to identify internal functions in traces.
#[arg(long)]
decode_internal: bool,

/// Print out opcode traces.
#[arg(long, short)]
trace_printer: bool,
Expand Down Expand Up @@ -142,7 +146,7 @@ impl RunArgs {
}
}

let mut executor = TracingExecutor::new(env.clone(), fork, evm_version, self.debug);
let mut executor = TracingExecutor::new(env.clone(), fork, evm_version, self.debug, self.decode_internal);
let mut env =
EnvWithHandlerCfg::new_with_spec_id(Box::new(env.clone()), executor.spec_id());

Expand Down Expand Up @@ -220,7 +224,7 @@ impl RunArgs {
}
};

handle_traces(result, &config, chain, self.label, self.debug).await?;
handle_traces(result, &config, chain, self.label, self.debug, self.decode_internal).await?;

Ok(())
}
Expand Down
30 changes: 22 additions & 8 deletions crates/cli/src/utils/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,13 @@ use foundry_compilers::{
Artifact, ProjectCompileOutput,
};
use foundry_config::{error::ExtractConfigError, figment::Figment, Chain, Config, NamedChain};
use foundry_debugger::Debugger;
use foundry_debugger::{DebugTraceIdentifier, Debugger};
use foundry_evm::{
debug::DebugArena,
executors::{DeployResult, EvmError, RawCallResult},
opts::EvmOpts,
traces::{
identifier::{EtherscanIdentifier, SignaturesIdentifier},
render_trace_arena, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, Traces,
identifier::{EtherscanIdentifier, SignaturesIdentifier}, render_trace_arena, render_trace_arena_with_internals, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, Traces
},
};
use std::{
Expand Down Expand Up @@ -357,6 +356,7 @@ pub async fn handle_traces(
chain: Option<Chain>,
labels: Vec<String>,
debug: bool,
decode_internal: bool,
) -> Result<()> {
let labels = labels.iter().filter_map(|label_str| {
let mut iter = label_str.split(':');
Expand Down Expand Up @@ -392,23 +392,37 @@ pub async fn handle_traces(
};
let mut debugger = Debugger::builder()
.debug_arena(result.debug.as_ref().expect("missing debug arena"))
.decoder(&decoder)
.sources(sources)
.identifier(|b| b.decoder(&decoder).sources(sources))
.build();
debugger.try_run()?;
} else {
print_traces(&mut result, &decoder).await?;
let identifier = if decode_internal {
let sources = if let Some(etherscan_identifier) = etherscan_identifier {
etherscan_identifier.get_compiled_contracts().await?
} else {
Default::default()
};
Some(DebugTraceIdentifier::builder().sources(sources).decoder(&decoder).build())
} else {
None
};
print_traces(&mut result, &decoder, identifier.as_ref()).await?;
}

Ok(())
}

pub async fn print_traces(result: &mut TraceResult, decoder: &CallTraceDecoder) -> Result<()> {
pub async fn print_traces(result: &mut TraceResult, decoder: &CallTraceDecoder, identifier: Option<&DebugTraceIdentifier>) -> Result<()> {
let traces = result.traces.as_ref().expect("No traces found");

println!("Traces:");
for (_, arena) in traces {
println!("{}", render_trace_arena(arena, decoder).await?);
let arena = if let Some(identifier) = identifier {
render_trace_arena_with_internals(arena, decoder, &identifier.identify_arena(arena)).await?
} else {
render_trace_arena(arena, decoder).await?
};
println!("{}", arena);
}
println!();

Expand Down
249 changes: 249 additions & 0 deletions crates/debugger/src/identifier.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
use alloy_primitives::Address;
use foundry_common::{compile::ContractSources, get_contract_name};
use foundry_compilers::{
artifacts::sourcemap::{Jump, SourceElement},
multi::MultiCompilerLanguage,
};
use foundry_evm_core::utils::PcIcMap;
use foundry_evm_traces::{CallTraceArena, CallTraceDecoder, CallTraceNode, DecodedTraceStep};
use revm::interpreter::OpCode;
use std::collections::HashMap;

pub struct DebugTraceIdentifier {
/// Mapping of contract address to identified contract name.
identified_contracts: HashMap<Address, String>,
/// Source map of contract sources
contracts_sources: ContractSources,
/// A mapping of source -> (PC -> IC map for deploy code, PC -> IC map for runtime code)
pc_ic_maps: HashMap<String, (PcIcMap, PcIcMap)>,
}

impl DebugTraceIdentifier {
pub fn builder() -> DebugTraceIdentifierBuilder {
DebugTraceIdentifierBuilder::default()
}

pub fn new(
identified_contracts: HashMap<Address, String>,
contracts_sources: ContractSources,
) -> Self {
let pc_ic_maps = contracts_sources
.entries()
.filter_map(|(name, artifact, _)| {
Some((
name.to_owned(),
(
PcIcMap::new(artifact.bytecode.bytecode.bytes()?),
PcIcMap::new(artifact.bytecode.deployed_bytecode.bytes()?),
),
))
})
.collect();
Self { identified_contracts, contracts_sources, pc_ic_maps }
}

pub fn identify(
&self,
address: &Address,
pc: usize,
init_code: bool,
) -> core::result::Result<(SourceElement, &str, &str), String> {
let Some(contract_name) = self.identified_contracts.get(address) else {
return Err(format!("Unknown contract at address {address}"));
};

let Some(mut files_source_code) = self.contracts_sources.get_sources(contract_name) else {
return Err(format!("No source map index for contract {contract_name}"));
};

let Some((create_map, rt_map)) = self.pc_ic_maps.get(contract_name) else {
return Err(format!("No PC-IC maps for contract {contract_name}"));
};

let Some((source_element, source_code, source_file)) =
files_source_code.find_map(|(artifact, source)| {
let bytecode = if init_code {
&artifact.bytecode.bytecode
} else {
artifact.bytecode.deployed_bytecode.bytecode.as_ref()?
};
let source_map = bytecode.source_map()?.expect("failed to parse");

let pc_ic_map = if init_code { create_map } else { rt_map };
let ic = pc_ic_map.get(pc)?;

// Solc indexes source maps by instruction counter, but Vyper indexes by program
// counter.
let source_element = if matches!(source.language, MultiCompilerLanguage::Solc(_)) {
source_map.get(ic)?
} else {
source_map.get(pc)?
};
// if the source element has an index, find the sourcemap for that index
let res = source_element
.index()
// if index matches current file_id, return current source code
.and_then(|index| {
(index == artifact.file_id)
.then(|| (source_element.clone(), source.source.as_str(), &source.name))
})
.or_else(|| {
// otherwise find the source code for the element's index
self.contracts_sources
.sources_by_id
.get(&artifact.build_id)?
.get(&source_element.index()?)
.map(|source| {
(source_element.clone(), source.source.as_str(), &source.name)
})
});

res
})
else {
return Err(format!("No source map for contract {contract_name}"));
};

Ok((source_element, source_code, source_file))
}

pub fn identify_arena(&self, arena: &CallTraceArena) -> Vec<Vec<DecodedTraceStep<'_>>> {
arena.nodes().iter().map(move |node| self.identify_node_steps(node)).collect()
}

pub fn identify_node_steps(&self, node: &CallTraceNode) -> Vec<DecodedTraceStep<'_>> {
let mut stack = Vec::new();
let mut identified = Vec::new();

// Flag marking whether previous instruction was a jump into function.
// If it was, we expect next instruction to be a JUMPDEST with source location pointing to
// the function.
let mut prev_step_jump_in = false;
for (step_idx, step) in node.trace.steps.iter().enumerate() {
// We are only interested in JUMPs.
if step.op != OpCode::JUMP && step.op != OpCode::JUMPI && step.op != OpCode::JUMPDEST {
continue;
}

// Resolve source map if possible.
let Ok((source_element, source_code, _)) =
self.identify(&node.trace.address, step.pc, node.trace.kind.is_any_create())
else {
prev_step_jump_in = false;
continue;
};

// Get slice of the source code that corresponds to the current step.
let source_part = {
let start = source_element.offset() as usize;
let end = start + source_element.length() as usize;
&source_code[start..end]
};

// If previous step was a jump record source location at JUMPDEST.
if prev_step_jump_in {
if step.op == OpCode::JUMPDEST {
if let Some(name) = parse_function_name(source_part) {
stack.push((name, step_idx));
}
};
prev_step_jump_in = false;
}

match source_element.jump() {
// Source location is collected on the next step.
Jump::In => prev_step_jump_in = true,
Jump::Out => {
// Find index matching the beginning of this function
if let Some(name) = parse_function_name(source_part) {
if let Some((i, _)) =
stack.iter().enumerate().rfind(|(_, (n, _))| n == &name)
{
// We've found a match, remove all records between start and end, those
// are considered invalid.
let (_, start_idx) = stack.split_off(i)[0];

let gas_used = node.trace.steps[start_idx].gas_remaining as i64 -
node.trace.steps[step_idx].gas_remaining as i64;

identified.push(DecodedTraceStep {
start_step_idx: start_idx,
end_step_idx: step_idx,
function_name: name,
gas_used,
});
}
}
}
_ => {}
};
}

// Sort by start step index.
identified.sort_by_key(|i| i.start_step_idx);

identified
}
}

/// [DebugTraceIdentifier] builder
#[derive(Debug, Default)]
#[must_use = "builders do nothing unless you call `build` on them"]
pub struct DebugTraceIdentifierBuilder {
/// Identified contracts.
identified_contracts: HashMap<Address, String>,
/// Map of source files.
sources: ContractSources,
}

impl DebugTraceIdentifierBuilder {
/// Extends the identified contracts from multiple decoders.
#[inline]
pub fn decoders(mut self, decoders: &[CallTraceDecoder]) -> Self {
for decoder in decoders {
self = self.decoder(decoder);
}
self
}

/// Extends the identified contracts from a decoder.
#[inline]
pub fn decoder(self, decoder: &CallTraceDecoder) -> Self {
let c = decoder.contracts.iter().map(|(k, v)| (*k, get_contract_name(v).to_string()));
self.identified_contracts(c)
}

/// Extends the identified contracts.
#[inline]
pub fn identified_contracts(
mut self,
identified_contracts: impl IntoIterator<Item = (Address, String)>,
) -> Self {
self.identified_contracts.extend(identified_contracts);
self
}

/// Sets the sources for the debugger.
#[inline]
pub fn sources(mut self, sources: ContractSources) -> Self {
self.sources = sources;
self
}

/// Builds the [DebugTraceIdentifier].
#[inline]
pub fn build(self) -> DebugTraceIdentifier {
let Self { identified_contracts, sources } = self;
DebugTraceIdentifier::new(identified_contracts, sources)
}
}

fn parse_function_name(source: &str) -> Option<&str> {
if !source.starts_with("function") {
return None;
}
if !source.contains("internal") && !source.contains("private") {
return None;
}
Some(source.split_once("function")?.1.split('(').next()?.trim())
}
2 changes: 2 additions & 0 deletions crates/debugger/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ extern crate tracing;

mod op;

mod identifier;
mod tui;
pub use identifier::DebugTraceIdentifier;
pub use tui::{Debugger, DebuggerBuilder, ExitReason};
Loading

0 comments on commit 9fd779a

Please sign in to comment.