Skip to content

Commit

Permalink
feat: identify internal function invocations in traces (#8222)
Browse files Browse the repository at this point in the history
* fix: small debugger updates

* [wip] feat: identify internal function invocations in traces

* fmt

* doc

* correctly enable tracing

* correctly enable tracing

* collect contract definition locs

* feat: print traces in format of Contract::function

* wip

* refactor

* clippy

* fix doc

* track input/output values

* clippy

* clean up

* TraceMode

* small fixes

* add doc

* clippy

* safer decofing from stack and memory

* use Into<Option<TraceMode>>

* TraceMode::None

* fmt

* review fixes

* --decode-internal for single fn

* use Vec

* TraceMode builder

* optional --decode-internal and tests

* update doc

* InternalTraceMode
  • Loading branch information
klkvr authored Jul 11, 2024
1 parent 6818c84 commit 6bb5c8e
Show file tree
Hide file tree
Showing 31 changed files with 1,075 additions and 317 deletions.
6 changes: 4 additions & 2 deletions Cargo.lock

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

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
9 changes: 7 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,8 @@ 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 +225,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
4 changes: 2 additions & 2 deletions crates/chisel/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use eyre::{Result, WrapErr};
use foundry_compilers::Artifact;
use foundry_evm::{
backend::Backend, decode::decode_console_logs, executors::ExecutorBuilder,
inspectors::CheatsConfig,
inspectors::CheatsConfig, traces::TraceMode,
};
use solang_parser::pt::{self, CodeLocation};
use std::str::FromStr;
Expand Down Expand Up @@ -302,7 +302,7 @@ impl SessionSource {
// Build a new executor
let executor = ExecutorBuilder::new()
.inspectors(|stack| {
stack.chisel_state(final_pc).trace(true).cheatcodes(
stack.chisel_state(final_pc).trace_mode(TraceMode::Call).cheatcodes(
CheatsConfig::new(
&self.config.foundry_config,
self.config.evm_opts.clone(),
Expand Down
11 changes: 11 additions & 0 deletions crates/cli/src/utils/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use foundry_evm::{
executors::{DeployResult, EvmError, RawCallResult},
opts::EvmOpts,
traces::{
debug::DebugTraceIdentifier,
decode_trace_arena,
identifier::{EtherscanIdentifier, SignaturesIdentifier},
render_trace_arena, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, Traces,
Expand Down Expand Up @@ -351,6 +352,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 @@ -378,6 +380,15 @@ pub async fn handle_traces(
}
}

if decode_internal {
let sources = if let Some(etherscan_identifier) = &etherscan_identifier {
etherscan_identifier.get_compiled_contracts().await?
} else {
Default::default()
};
decoder.debug_identifier = Some(DebugTraceIdentifier::new(sources));
}

if debug {
let sources = if let Some(etherscan_identifier) = etherscan_identifier {
etherscan_identifier.get_compiled_contracts().await?
Expand Down
1 change: 0 additions & 1 deletion crates/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ foundry-block-explorers = { workspace = true, features = ["foundry-compilers"] }
foundry-common-fmt.workspace = true
foundry-compilers.workspace = true
foundry-config.workspace = true
foundry-linking.workspace = true

alloy-contract.workspace = true
alloy-dyn-abi = { workspace = true, features = ["arbitrary", "eip712"] }
Expand Down
138 changes: 5 additions & 133 deletions crates/common/src/compile.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
//! Support for compiling [foundry_compilers::Project]
use crate::{compact_to_contract, term::SpinnerReporter, TestFunctionExt};
use crate::{term::SpinnerReporter, TestFunctionExt};
use comfy_table::{presets::ASCII_MARKDOWN, Attribute, Cell, CellAlignment, Color, Table};
use eyre::{Context, Result};
use eyre::Result;
use foundry_block_explorers::contract::Metadata;
use foundry_compilers::{
artifacts::{remappings::Remapping, BytecodeObject, ContractBytecodeSome, Libraries, Source},
artifacts::{remappings::Remapping, BytecodeObject, Source},
compilers::{
multi::MultiCompilerLanguage,
solc::{Solc, SolcCompiler},
Compiler,
},
report::{BasicStdoutReporter, NoReporter, Report},
Artifact, Project, ProjectBuilder, ProjectCompileOutput, ProjectPathsConfig, SolcConfig,
};
use foundry_linking::Linker;
use num_format::{Locale, ToFormattedString};
use rustc_hash::FxHashMap;
use std::{
collections::{BTreeMap, HashMap},
collections::BTreeMap,
fmt::Display,
io::IsTerminal,
path::{Path, PathBuf},
sync::Arc,
time::Instant,
};

Expand Down Expand Up @@ -261,130 +257,6 @@ impl ProjectCompiler {
}
}

#[derive(Clone, Debug)]
pub struct SourceData {
pub source: Arc<String>,
pub language: MultiCompilerLanguage,
pub name: String,
}

#[derive(Clone, Debug)]
pub struct ArtifactData {
pub bytecode: ContractBytecodeSome,
pub build_id: String,
pub file_id: u32,
}

/// Contract source code and bytecode data used for debugger.
#[derive(Clone, Debug, Default)]
pub struct ContractSources {
/// Map over build_id -> file_id -> (source code, language)
pub sources_by_id: HashMap<String, FxHashMap<u32, SourceData>>,
/// Map over contract name -> Vec<(bytecode, build_id, file_id)>
pub artifacts_by_name: HashMap<String, Vec<ArtifactData>>,
}

impl ContractSources {
/// Collects the contract sources and artifacts from the project compile output.
pub fn from_project_output(
output: &ProjectCompileOutput,
root: impl AsRef<Path>,
libraries: Option<&Libraries>,
) -> Result<Self> {
let mut sources = Self::default();
sources.insert(output, root, libraries)?;
Ok(sources)
}

pub fn insert<C: Compiler>(
&mut self,
output: &ProjectCompileOutput<C>,
root: impl AsRef<Path>,
libraries: Option<&Libraries>,
) -> Result<()>
where
C::Language: Into<MultiCompilerLanguage>,
{
let root = root.as_ref();
let link_data = libraries.map(|libraries| {
let linker = Linker::new(root, output.artifact_ids().collect());
(linker, libraries)
});

for (id, artifact) in output.artifact_ids() {
if let Some(file_id) = artifact.id {
let artifact = if let Some((linker, libraries)) = link_data.as_ref() {
linker.link(&id, libraries)?.into_contract_bytecode()
} else {
artifact.clone().into_contract_bytecode()
};
let bytecode = compact_to_contract(artifact.clone().into_contract_bytecode())?;

self.artifacts_by_name.entry(id.name.clone()).or_default().push(ArtifactData {
bytecode,
build_id: id.build_id.clone(),
file_id,
});
} else {
warn!(id = id.identifier(), "source not found");
}
}

// Not all source files produce artifacts, so we are populating sources by using build
// infos.
let mut files: BTreeMap<PathBuf, Arc<String>> = BTreeMap::new();
for (build_id, build) in output.builds() {
for (source_id, path) in &build.source_id_to_path {
let source_code = if let Some(source) = files.get(path) {
source.clone()
} else {
let source = Source::read(path).wrap_err_with(|| {
format!("failed to read artifact source file for `{}`", path.display())
})?;
files.insert(path.clone(), source.content.clone());
source.content
};

self.sources_by_id.entry(build_id.clone()).or_default().insert(
*source_id,
SourceData {
source: source_code,
language: build.language.into(),
name: path.strip_prefix(root).unwrap_or(path).to_string_lossy().to_string(),
},
);
}
}

Ok(())
}

/// Returns all sources for a contract by name.
pub fn get_sources(
&self,
name: &str,
) -> Option<impl Iterator<Item = (&ArtifactData, &SourceData)>> {
self.artifacts_by_name.get(name).map(|artifacts| {
artifacts.iter().filter_map(|artifact| {
let source =
self.sources_by_id.get(artifact.build_id.as_str())?.get(&artifact.file_id)?;
Some((artifact, source))
})
})
}

/// Returns all (name, bytecode, source) sets.
pub fn entries(&self) -> impl Iterator<Item = (&str, &ArtifactData, &SourceData)> {
self.artifacts_by_name.iter().flat_map(|(name, artifacts)| {
artifacts.iter().filter_map(|artifact| {
let source =
self.sources_by_id.get(artifact.build_id.as_str())?.get(&artifact.file_id)?;
Some((name.as_str(), artifact, source))
})
})
}
}

// https://eips.ethereum.org/EIPS/eip-170
const CONTRACT_SIZE_LIMIT: usize = 24576;

Expand Down Expand Up @@ -501,7 +373,7 @@ pub fn etherscan_project(
let sources_path = target_path.join(&metadata.contract_name);
metadata.source_tree().write_to(&target_path)?;

let mut settings = metadata.source_code.settings()?.unwrap_or_default();
let mut settings = metadata.settings()?;

// make remappings absolute with our root
for remapping in settings.remappings.iter_mut() {
Expand Down
3 changes: 0 additions & 3 deletions crates/common/src/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,6 @@ pub struct ContractsByArtifact(Arc<BTreeMap<ArtifactId, ContractData>>);

impl ContractsByArtifact {
/// Creates a new instance by collecting all artifacts with present bytecode from an iterator.
///
/// It is recommended to use this method with an output of
/// [foundry_linking::Linker::get_linked_artifacts].
pub fn new(artifacts: impl IntoIterator<Item = (ArtifactId, CompactContractBytecode)>) -> Self {
let map = artifacts
.into_iter()
Expand Down
1 change: 0 additions & 1 deletion crates/debugger/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ workspace = true
[dependencies]
foundry-common.workspace = true
foundry-compilers.workspace = true
foundry-evm-core.workspace = true
foundry-evm-traces.workspace = true
revm-inspectors.workspace = true

Expand Down
4 changes: 2 additions & 2 deletions crates/debugger/src/tui/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
use crate::{node::flatten_call_trace, DebugNode, Debugger};
use alloy_primitives::Address;
use foundry_common::{compile::ContractSources, evm::Breakpoints, get_contract_name};
use foundry_evm_traces::{CallTraceArena, CallTraceDecoder, Traces};
use foundry_common::{evm::Breakpoints, get_contract_name};
use foundry_evm_traces::{debug::ContractSources, CallTraceArena, CallTraceDecoder, Traces};
use std::collections::HashMap;

/// Debugger builder.
Expand Down
Loading

0 comments on commit 6bb5c8e

Please sign in to comment.