Skip to content

Commit

Permalink
feat(cheatcode): startDebugTraceRecording and `stopDebugTraceRecord…
Browse files Browse the repository at this point in the history
…ing` for ERC4337 testing (foundry-rs#8571)

* feat: add record opcode cheat code

feat: capture stack inputs as part of the opcode

feat: record opcode -> record debug trace

fix: memory OOG, need to only use needed stack, mem input

fix: missing op code, instruction results

fix: accessing out-of-bound idx memory

When running on some project, we noticed that it sometimes try to access memory with out of bound
index and panics.

This commit fix it by:
1. Enfore reset to Nonce after stopDebugTraceRecording(), this ensures the `some(..) = ...` part will not be triggered
2. Change how opcode_utils.rs accesses memory. Return empty vector if trying access out-of-bound memory.

* test: add DebugTrace.t.sol for the debug trace cheatcode

* fix: rebase errors

* feat: use tracer for debug trace instead of recording during inspector

This commit also cleans up the previous implementaiton on inspector.
And then change the cheatcode interface to be of three steps:
1. start recording debug trace
2. stop recording
3. get the debug trace by index

The reason is to avoid out-of-memory issue by returning the whole traces at once.

* fix: rebase duplication

* feat: replace instruction result with isOutOfGas

* fix: CI issues

* fix: remove DebugTrace wrapper in inspector

* fix: revert to original tracer config when stops

* chore: reuse existing opcode functions

* chore: refactor, fmt, clippy run

* chore: use ref instead of clone, returning Error when not able to access

* chore: move buffer to evm_core from debugger

* fix: disable dummy tracer by default, return explicit error

Since enabling dummy tracer still come with performance impact, remove the auto dummy tracer
initiation. The cheatcode will return explicit error and require the test to be run in -vvv mode
to have the tracer enabled by default.

* fix: return all traces, turn on necessary tracer config

There was OOM concern but using the get-by-index style, despite improved, does not solve the root cause.
The main issue is that the tracer config did not turn off after the stop recording cheatcode being called.
It seems too much burden for the tracer to record the returned traces inside forge tests as the tests will
also pass around the debug traces, causing memory boost.

This commit also only turns on necessary tracer config instead of using all().

* chore: cleanup comments, typo

* fix: use bytes for memory, remove flattern function, fix get_slice_from_memory

* fix: style fmt

* fix: ensure steps in the order of node when flatten

A node can have steps that calls to another node, so the child node's step might occur before
some steps of its parent node. This introduce the flatten_call_trace function back using
recursive call to ensure the steps are in correct order despite not in the same order of the
node index.

see PR comment: foundry-rs#8571 (comment)

* doc: remove legacy comment in test

* style: reuse empty initialized var on return val

---------

Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com>
  • Loading branch information
2 people authored and rplusq committed Nov 29, 2024
1 parent 3d56299 commit b31dc28
Show file tree
Hide file tree
Showing 15 changed files with 626 additions and 119 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.

1 change: 1 addition & 0 deletions crates/cheatcodes/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ p256 = "0.13.2"
ecdsa = "0.16"
rand = "0.8"
revm.workspace = true
revm-inspectors.workspace = true
semver.workspace = true
serde_json.workspace = true
thiserror.workspace = true
Expand Down
76 changes: 76 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.

1 change: 1 addition & 0 deletions crates/cheatcodes/spec/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ impl Cheatcodes<'static> {
Vm::AccountAccess::STRUCT.clone(),
Vm::StorageAccess::STRUCT.clone(),
Vm::Gas::STRUCT.clone(),
Vm::DebugStep::STRUCT.clone(),
]),
enums: Cow::Owned(vec![
Vm::CallerMode::ENUM.clone(),
Expand Down
33 changes: 33 additions & 0 deletions crates/cheatcodes/spec/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,28 @@ interface Vm {
uint64 depth;
}

/// The result of the `stopDebugTraceRecording` call
struct DebugStep {
/// The stack before executing the step of the run.
/// stack\[0\] represents the top of the stack.
/// and only stack data relevant to the opcode execution is contained.
uint256[] stack;
/// The memory input data before executing the step of the run.
/// only input data relevant to the opcode execution is contained.
///
/// e.g. for MLOAD, it will have memory\[offset:offset+32\] copied here.
/// the offset value can be get by the stack data.
bytes memoryInput;
/// The opcode that was accessed.
uint8 opcode;
/// The call depth of the step.
uint64 depth;
/// Whether the call end up with out of gas error.
bool isOutOfGas;
/// The contract address where the opcode is running
address contractAddr;
}

// ======== EVM ========

/// Gets the address for a given private key.
Expand All @@ -287,6 +309,17 @@ interface Vm {
#[cheatcode(group = Evm, safety = Unsafe)]
function loadAllocs(string calldata pathToAllocsJson) external;

// -------- Record Debug Traces --------

/// Records the debug trace during the run.
#[cheatcode(group = Evm, safety = Safe)]
function startDebugTraceRecording() external;

/// Stop debug trace recording and returns the recorded debug trace.
#[cheatcode(group = Evm, safety = Safe)]
function stopAndReturnDebugTraceRecording() external returns (DebugStep[] memory step);


/// Clones a source account code, state, balance and nonce to a target account and updates in-memory EVM state.
#[cheatcode(group = Evm, safety = Unsafe)]
function cloneAccount(address source, address target) external;
Expand Down
67 changes: 65 additions & 2 deletions crates/cheatcodes/src/evm.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
//! Implementations of [`Evm`](spec::Group::Evm) cheatcodes.
use crate::{
inspector::InnerEcx, BroadcastableTransaction, Cheatcode, Cheatcodes, CheatcodesExecutor,
CheatsCtxt, Result, Vm::*,
inspector::{InnerEcx, RecordDebugStepInfo},
BroadcastableTransaction, Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, Error, Result,
Vm::*,
};
use alloy_consensus::TxEnvelope;
use alloy_genesis::{Genesis, GenesisAccount};
Expand All @@ -14,10 +15,14 @@ use foundry_evm_core::{
backend::{DatabaseExt, RevertStateSnapshotAction},
constants::{CALLER, CHEATCODE_ADDRESS, HARDHAT_CONSOLE_ADDRESS, TEST_CONTRACT_ADDRESS},
};
use foundry_evm_traces::StackSnapshotType;
use rand::Rng;
use revm::primitives::{Account, Bytecode, SpecId, KECCAK_EMPTY};
use std::{collections::BTreeMap, path::Path};

mod record_debug_step;
use record_debug_step::{convert_call_trace_to_debug_step, flatten_call_trace};

mod fork;
pub(crate) mod mapping;
pub(crate) mod mock;
Expand Down Expand Up @@ -715,6 +720,64 @@ impl Cheatcode for setBlockhashCall {
}
}

impl Cheatcode for startDebugTraceRecordingCall {
fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
let Some(tracer) = executor.tracing_inspector().and_then(|t| t.as_mut()) else {
return Err(Error::from("no tracer initiated, consider adding -vvv flag"))
};

let mut info = RecordDebugStepInfo {
// will be updated later
start_node_idx: 0,
// keep the original config to revert back later
original_tracer_config: *tracer.config(),
};

// turn on tracer configuration for recording
tracer.update_config(|config| {
config
.set_steps(true)
.set_memory_snapshots(true)
.set_stack_snapshots(StackSnapshotType::Full)
});

// track where the recording starts
if let Some(last_node) = tracer.traces().nodes().last() {
info.start_node_idx = last_node.idx;
}

ccx.state.record_debug_steps_info = Some(info);
Ok(Default::default())
}
}

impl Cheatcode for stopAndReturnDebugTraceRecordingCall {
fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
let Some(tracer) = executor.tracing_inspector().and_then(|t| t.as_mut()) else {
return Err(Error::from("no tracer initiated, consider adding -vvv flag"))
};

let Some(record_info) = ccx.state.record_debug_steps_info else {
return Err(Error::from("nothing recorded"))
};

// Revert the tracer config to the one before recording
tracer.update_config(|_config| record_info.original_tracer_config);

// Use the trace nodes to flatten the call trace
let root = tracer.traces();
let steps = flatten_call_trace(0, root, record_info.start_node_idx);

let debug_steps: Vec<DebugStep> =
steps.iter().map(|&step| convert_call_trace_to_debug_step(step)).collect();

// Clean up the recording info
ccx.state.record_debug_steps_info = None;

Ok(debug_steps.abi_encode())
}
}

pub(super) fn get_nonce(ccx: &mut CheatsCtxt, address: &Address) -> Result {
let account = ccx.ecx.journaled_state.load_account(*address, &mut ccx.ecx.db)?;
Ok(account.info.nonce.abi_encode())
Expand Down
Loading

0 comments on commit b31dc28

Please sign in to comment.