Skip to content

Commit

Permalink
feat: vm.pauseTracing + vm.resumeTracing (#8696)
Browse files Browse the repository at this point in the history
* feat: vm.pauseTracing + vm.resumeTracing

* clippy

* fixes

* fix --decode-internal edge case

* fmt

* clippy + change tracing_inspector return type

* update fixture

* fix fixture
  • Loading branch information
klkvr authored Aug 26, 2024
1 parent 44cceb4 commit bdf48aa
Show file tree
Hide file tree
Showing 22 changed files with 430 additions and 47 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/cheatcodes/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ foundry-common.workspace = true
foundry-compilers.workspace = true
foundry-config.workspace = true
foundry-evm-core.workspace = true
foundry-evm-traces.workspace = true
foundry-wallets.workspace = true

alloy-dyn-abi.workspace = true
Expand Down
40 changes: 40 additions & 0 deletions crates/cheatcodes/assets/cheatcodes.json

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

9 changes: 9 additions & 0 deletions crates/cheatcodes/spec/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2282,6 +2282,15 @@ interface Vm {
/// Returns a random `address`.
#[cheatcode(group = Utilities)]
function randomAddress() external returns (address);

/// Pauses collection of call traces. Useful in cases when you want to skip tracing of
/// complex calls which are not useful for debugging.
#[cheatcode(group = Utilities)]
function pauseTracing() external view;

/// Unpauses collection of call traces.
#[cheatcode(group = Utilities)]
function resumeTracing() external view;
}
}

Expand Down
18 changes: 10 additions & 8 deletions crates/cheatcodes/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ impl Cheatcode for deployCode_0Call {
) -> Result {
let Self { artifactPath: path } = self;
let bytecode = get_artifact_code(ccx.state, path, false)?;
let output = executor
let address = executor
.exec_create(
CreateInputs {
caller: ccx.caller,
Expand All @@ -282,10 +282,11 @@ impl Cheatcode for deployCode_0Call {
gas_limit: ccx.gas_limit,
},
ccx,
)
.unwrap();
)?
.address
.ok_or_else(|| fmt_err!("contract creation failed"))?;

Ok(output.address.unwrap().abi_encode())
Ok(address.abi_encode())
}
}

Expand All @@ -298,7 +299,7 @@ impl Cheatcode for deployCode_1Call {
let Self { artifactPath: path, constructorArgs } = self;
let mut bytecode = get_artifact_code(ccx.state, path, false)?.to_vec();
bytecode.extend_from_slice(constructorArgs);
let output = executor
let address = executor
.exec_create(
CreateInputs {
caller: ccx.caller,
Expand All @@ -308,10 +309,11 @@ impl Cheatcode for deployCode_1Call {
gas_limit: ccx.gas_limit,
},
ccx,
)
.unwrap();
)?
.address
.ok_or_else(|| fmt_err!("contract creation failed"))?;

Ok(output.address.unwrap().abi_encode())
Ok(address.abi_encode())
}
}

Expand Down
34 changes: 28 additions & 6 deletions crates/cheatcodes/src/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::{
self, ExpectedCallData, ExpectedCallTracker, ExpectedCallType, ExpectedEmit,
ExpectedRevert, ExpectedRevertKind,
},
utils::IgnoredTraces,
CheatsConfig, CheatsCtxt, DynCheatcode, Error, Result, Vm,
Vm::AccountAccess,
};
Expand All @@ -28,14 +29,15 @@ use foundry_evm_core::{
utils::new_evm_with_existing_context,
InspectorExt,
};
use foundry_evm_traces::TracingInspector;
use itertools::Itertools;
use rand::{rngs::StdRng, Rng, SeedableRng};
use revm::{
interpreter::{
opcode, CallInputs, CallOutcome, CallScheme, CreateInputs, CreateOutcome, EOFCreateInputs,
Gas, InstructionResult, Interpreter, InterpreterAction, InterpreterResult,
EOFCreateKind, Gas, InstructionResult, Interpreter, InterpreterAction, InterpreterResult,
},
primitives::{BlockEnv, CreateScheme, EVMError},
primitives::{BlockEnv, CreateScheme, EVMError, SpecId, EOF_MAGIC_BYTES},
EvmContext, InnerEvmContext, Inspector,
};
use rustc_hash::FxHashMap;
Expand Down Expand Up @@ -115,8 +117,19 @@ pub trait CheatcodesExecutor {
self.with_evm(ccx, |evm| {
evm.context.evm.inner.journaled_state.depth += 1;

let first_frame_or_result =
evm.handler.execution().create(&mut evm.context, Box::new(inputs))?;
// Handle EOF bytecode
let first_frame_or_result = if evm.handler.cfg.spec_id.is_enabled_in(SpecId::PRAGUE_EOF)
&& inputs.scheme == CreateScheme::Create && inputs.init_code.starts_with(&EOF_MAGIC_BYTES)
{
evm.handler.execution().eofcreate(
&mut evm.context,
Box::new(EOFCreateInputs::new(inputs.caller, inputs.value, inputs.gas_limit, EOFCreateKind::Tx {
initdata: inputs.init_code,
})),
)?
} else {
evm.handler.execution().create(&mut evm.context, Box::new(inputs))?
};

let mut result = match first_frame_or_result {
revm::FrameOrResult::Frame(first_frame) => evm.run_the_loop(first_frame)?,
Expand All @@ -126,8 +139,8 @@ pub trait CheatcodesExecutor {
evm.handler.execution().last_frame_return(&mut evm.context, &mut result)?;

let outcome = match result {
revm::FrameResult::Call(_) | revm::FrameResult::EOFCreate(_) => unreachable!(),
revm::FrameResult::Create(create) => create,
revm::FrameResult::Call(_) => unreachable!(),
revm::FrameResult::Create(create) | revm::FrameResult::EOFCreate(create) => create,
};

evm.context.evm.inner.journaled_state.depth -= 1;
Expand All @@ -139,6 +152,11 @@ pub trait CheatcodesExecutor {
fn console_log<DB: DatabaseExt>(&mut self, ccx: &mut CheatsCtxt<DB>, message: String) {
self.get_inspector::<DB>(ccx.state).console_log(message);
}

/// Returns a mutable reference to the tracing inspector if it is available.
fn tracing_inspector(&mut self) -> Option<&mut Option<TracingInspector>> {
None
}
}

/// Basic implementation of [CheatcodesExecutor] that simply returns the [Cheatcodes] instance as an
Expand Down Expand Up @@ -310,6 +328,9 @@ pub struct Cheatcodes {

/// Optional RNG algorithm.
rng: Option<StdRng>,

/// Ignored traces.
pub ignored_traces: IgnoredTraces,
}

// This is not derived because calling this in `fn new` with `..Default::default()` creates a second
Expand Down Expand Up @@ -352,6 +373,7 @@ impl Cheatcodes {
pc: Default::default(),
breakpoints: Default::default(),
rng: Default::default(),
ignored_traces: Default::default(),
}
}

Expand Down
63 changes: 62 additions & 1 deletion crates/cheatcodes/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,23 @@ use crate::{Cheatcode, Cheatcodes, Result, Vm::*};
use alloy_primitives::{Address, U256};
use alloy_sol_types::SolValue;
use foundry_common::ens::namehash;
use foundry_evm_core::constants::DEFAULT_CREATE2_DEPLOYER;
use foundry_evm_core::{backend::DatabaseExt, constants::DEFAULT_CREATE2_DEPLOYER};
use rand::Rng;
use std::collections::HashMap;

/// Contains locations of traces ignored via cheatcodes.
///
/// The way we identify location in traces is by (node_idx, item_idx) tuple where node_idx is an
/// index of a call trace node, and item_idx is a value between 0 and `node.ordering.len()` where i
/// represents point after ith item, and 0 represents the beginning of the node trace.
#[derive(Debug, Default, Clone)]
pub struct IgnoredTraces {
/// Mapping from (start_node_idx, start_item_idx) to (end_node_idx, end_item_idx) representing
/// ranges of trace nodes to ignore.
pub ignored: HashMap<(usize, usize), (usize, usize)>,
/// Keeps track of (start_node_idx, start_item_idx) of the last `vm.pauseTracing` call.
pub last_pause_call: Option<(usize, usize)>,
}

impl Cheatcode for labelCall {
fn apply(&self, state: &mut Cheatcodes) -> Result {
Expand Down Expand Up @@ -88,3 +103,49 @@ impl Cheatcode for randomAddressCall {
Ok(addr.abi_encode())
}
}

impl Cheatcode for pauseTracingCall {
fn apply_full<DB: DatabaseExt, E: crate::CheatcodesExecutor>(
&self,
ccx: &mut crate::CheatsCtxt<DB>,
executor: &mut E,
) -> Result {
let Some(tracer) = executor.tracing_inspector().and_then(|t| t.as_ref()) else {
// No tracer -> nothing to pause
return Ok(Default::default())
};

// If paused earlier, ignore the call
if ccx.state.ignored_traces.last_pause_call.is_some() {
return Ok(Default::default())
}

let cur_node = &tracer.traces().nodes().last().expect("no trace nodes");
ccx.state.ignored_traces.last_pause_call = Some((cur_node.idx, cur_node.ordering.len()));

Ok(Default::default())
}
}

impl Cheatcode for resumeTracingCall {
fn apply_full<DB: DatabaseExt, E: crate::CheatcodesExecutor>(
&self,
ccx: &mut crate::CheatsCtxt<DB>,
executor: &mut E,
) -> Result {
let Some(tracer) = executor.tracing_inspector().and_then(|t| t.as_ref()) else {
// No tracer -> nothing to unpause
return Ok(Default::default())
};

let Some(start) = ccx.state.ignored_traces.last_pause_call.take() else {
// Nothing to unpause
return Ok(Default::default())
};

let node = &tracer.traces().nodes().last().expect("no trace nodes");
ccx.state.ignored_traces.ignored.insert(start, (node.idx, node.ordering.len()));

Ok(Default::default())
}
}
2 changes: 1 addition & 1 deletion crates/debugger/src/tui/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ impl DebuggerBuilder {
#[inline]
pub fn traces(mut self, traces: Traces) -> Self {
for (_, arena) in traces {
self = self.trace_arena(arena);
self = self.trace_arena(arena.arena);
}
self
}
Expand Down
6 changes: 3 additions & 3 deletions crates/evm/evm/src/executors/fuzz/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use foundry_evm_fuzz::{
strategies::{fuzz_calldata, fuzz_calldata_from_state, EvmFuzzState},
BaseCounterExample, CounterExample, FuzzCase, FuzzError, FuzzFixtures, FuzzTestResult,
};
use foundry_evm_traces::CallTraceArena;
use foundry_evm_traces::SparsedTraceArena;
use indicatif::ProgressBar;
use proptest::test_runner::{TestCaseError, TestError, TestRunner};
use std::cell::RefCell;
Expand All @@ -29,7 +29,7 @@ pub struct FuzzTestData {
// Stores the result and calldata of the last failed call, if any.
pub counterexample: (Bytes, RawCallResult),
// Stores up to `max_traces_to_collect` traces.
pub traces: Vec<CallTraceArena>,
pub traces: Vec<SparsedTraceArena>,
// Stores breakpoints for the last fuzz case.
pub breakpoints: Option<Breakpoints>,
// Stores coverage information for all fuzz cases.
Expand Down Expand Up @@ -163,7 +163,7 @@ impl FuzzedExecutor {
labeled_addresses: call.labels,
traces: last_run_traces,
breakpoints: last_run_breakpoints,
gas_report_traces: traces,
gas_report_traces: traces.into_iter().map(|a| a.arena).collect(),
coverage: fuzz_result.coverage,
};

Expand Down
4 changes: 2 additions & 2 deletions crates/evm/evm/src/executors/fuzz/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use alloy_primitives::{Bytes, Log};
use foundry_common::evm::Breakpoints;
use foundry_evm_coverage::HitMaps;
use foundry_evm_fuzz::FuzzCase;
use foundry_evm_traces::CallTraceArena;
use foundry_evm_traces::SparsedTraceArena;
use revm::interpreter::InstructionResult;

/// Returned by a single fuzz in the case of a successful run
Expand All @@ -12,7 +12,7 @@ pub struct CaseOutcome {
/// Data of a single fuzz test case
pub case: FuzzCase,
/// The traces of the call
pub traces: Option<CallTraceArena>,
pub traces: Option<SparsedTraceArena>,
/// The coverage info collected during the call
pub coverage: Option<HitMaps>,
/// Breakpoints char pc map
Expand Down
8 changes: 5 additions & 3 deletions crates/evm/evm/src/executors/invariant/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use foundry_evm_fuzz::{
strategies::{invariant_strat, override_call_strat, EvmFuzzState},
FuzzCase, FuzzFixtures, FuzzedCases,
};
use foundry_evm_traces::CallTraceArena;
use foundry_evm_traces::{CallTraceArena, SparsedTraceArena};
use indicatif::ProgressBar;
use parking_lot::RwLock;
use proptest::{
Expand Down Expand Up @@ -199,7 +199,9 @@ impl InvariantTest {

let mut invariant_data = self.execution_data.borrow_mut();
if invariant_data.gas_report_traces.len() < gas_samples {
invariant_data.gas_report_traces.push(run.run_traces);
invariant_data
.gas_report_traces
.push(run.run_traces.into_iter().map(|arena| arena.arena).collect());
}
invariant_data.fuzz_cases.push(FuzzedCases::new(run.fuzz_runs));

Expand All @@ -219,7 +221,7 @@ pub struct InvariantTestRun {
// Contracts created during current invariant run.
pub created_contracts: Vec<Address>,
// Traces of each call of the invariant run call sequence.
pub run_traces: Vec<CallTraceArena>,
pub run_traces: Vec<SparsedTraceArena>,
// Current depth of invariant run.
pub depth: u32,
// Current assume rejects of the invariant run.
Expand Down
3 changes: 2 additions & 1 deletion crates/evm/evm/src/executors/invariant/replay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ pub fn replay_run(
}

// Identify newly generated contracts, if they exist.
ided_contracts.extend(load_contracts(call_result.traces.as_slice(), known_contracts));
ided_contracts
.extend(load_contracts(call_result.traces.iter().map(|a| &a.arena), known_contracts));

// Create counter example to be used in failed case.
counterexample_sequence.push(BaseCounterExample::from_invariant_call(
Expand Down
Loading

0 comments on commit bdf48aa

Please sign in to comment.