Skip to content

Commit 0c659f0

Browse files
feat(cheatcode): startDebugTraceRecording and stopDebugTraceRecording for ERC4337 testing (#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/foundry#8571 (comment) * doc: remove legacy comment in test * style: reuse empty initialized var on return val --------- Co-authored-by: zerosnacks <[email protected]>
1 parent 5101a32 commit 0c659f0

File tree

15 files changed

+626
-119
lines changed

15 files changed

+626
-119
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cheatcodes/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ p256 = "0.13.2"
5858
ecdsa = "0.16"
5959
rand = "0.8"
6060
revm.workspace = true
61+
revm-inspectors.workspace = true
6162
semver.workspace = true
6263
serde_json.workspace = true
6364
thiserror.workspace = true

crates/cheatcodes/assets/cheatcodes.json

Lines changed: 76 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cheatcodes/spec/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ impl Cheatcodes<'static> {
8585
Vm::AccountAccess::STRUCT.clone(),
8686
Vm::StorageAccess::STRUCT.clone(),
8787
Vm::Gas::STRUCT.clone(),
88+
Vm::DebugStep::STRUCT.clone(),
8889
]),
8990
enums: Cow::Owned(vec![
9091
Vm::CallerMode::ENUM.clone(),

crates/cheatcodes/spec/src/vm.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,28 @@ interface Vm {
261261
uint64 depth;
262262
}
263263

264+
/// The result of the `stopDebugTraceRecording` call
265+
struct DebugStep {
266+
/// The stack before executing the step of the run.
267+
/// stack\[0\] represents the top of the stack.
268+
/// and only stack data relevant to the opcode execution is contained.
269+
uint256[] stack;
270+
/// The memory input data before executing the step of the run.
271+
/// only input data relevant to the opcode execution is contained.
272+
///
273+
/// e.g. for MLOAD, it will have memory\[offset:offset+32\] copied here.
274+
/// the offset value can be get by the stack data.
275+
bytes memoryInput;
276+
/// The opcode that was accessed.
277+
uint8 opcode;
278+
/// The call depth of the step.
279+
uint64 depth;
280+
/// Whether the call end up with out of gas error.
281+
bool isOutOfGas;
282+
/// The contract address where the opcode is running
283+
address contractAddr;
284+
}
285+
264286
// ======== EVM ========
265287

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

312+
// -------- Record Debug Traces --------
313+
314+
/// Records the debug trace during the run.
315+
#[cheatcode(group = Evm, safety = Safe)]
316+
function startDebugTraceRecording() external;
317+
318+
/// Stop debug trace recording and returns the recorded debug trace.
319+
#[cheatcode(group = Evm, safety = Safe)]
320+
function stopAndReturnDebugTraceRecording() external returns (DebugStep[] memory step);
321+
322+
290323
/// Clones a source account code, state, balance and nonce to a target account and updates in-memory EVM state.
291324
#[cheatcode(group = Evm, safety = Unsafe)]
292325
function cloneAccount(address source, address target) external;

crates/cheatcodes/src/evm.rs

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
//! Implementations of [`Evm`](spec::Group::Evm) cheatcodes.
22
33
use crate::{
4-
inspector::InnerEcx, BroadcastableTransaction, Cheatcode, Cheatcodes, CheatcodesExecutor,
5-
CheatsCtxt, Result, Vm::*,
4+
inspector::{InnerEcx, RecordDebugStepInfo},
5+
BroadcastableTransaction, Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, Error, Result,
6+
Vm::*,
67
};
78
use alloy_consensus::TxEnvelope;
89
use alloy_genesis::{Genesis, GenesisAccount};
@@ -14,10 +15,14 @@ use foundry_evm_core::{
1415
backend::{DatabaseExt, RevertStateSnapshotAction},
1516
constants::{CALLER, CHEATCODE_ADDRESS, HARDHAT_CONSOLE_ADDRESS, TEST_CONTRACT_ADDRESS},
1617
};
18+
use foundry_evm_traces::StackSnapshotType;
1719
use rand::Rng;
1820
use revm::primitives::{Account, Bytecode, SpecId, KECCAK_EMPTY};
1921
use std::{collections::BTreeMap, path::Path};
2022

23+
mod record_debug_step;
24+
use record_debug_step::{convert_call_trace_to_debug_step, flatten_call_trace};
25+
2126
mod fork;
2227
pub(crate) mod mapping;
2328
pub(crate) mod mock;
@@ -715,6 +720,64 @@ impl Cheatcode for setBlockhashCall {
715720
}
716721
}
717722

723+
impl Cheatcode for startDebugTraceRecordingCall {
724+
fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
725+
let Some(tracer) = executor.tracing_inspector().and_then(|t| t.as_mut()) else {
726+
return Err(Error::from("no tracer initiated, consider adding -vvv flag"))
727+
};
728+
729+
let mut info = RecordDebugStepInfo {
730+
// will be updated later
731+
start_node_idx: 0,
732+
// keep the original config to revert back later
733+
original_tracer_config: *tracer.config(),
734+
};
735+
736+
// turn on tracer configuration for recording
737+
tracer.update_config(|config| {
738+
config
739+
.set_steps(true)
740+
.set_memory_snapshots(true)
741+
.set_stack_snapshots(StackSnapshotType::Full)
742+
});
743+
744+
// track where the recording starts
745+
if let Some(last_node) = tracer.traces().nodes().last() {
746+
info.start_node_idx = last_node.idx;
747+
}
748+
749+
ccx.state.record_debug_steps_info = Some(info);
750+
Ok(Default::default())
751+
}
752+
}
753+
754+
impl Cheatcode for stopAndReturnDebugTraceRecordingCall {
755+
fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
756+
let Some(tracer) = executor.tracing_inspector().and_then(|t| t.as_mut()) else {
757+
return Err(Error::from("no tracer initiated, consider adding -vvv flag"))
758+
};
759+
760+
let Some(record_info) = ccx.state.record_debug_steps_info else {
761+
return Err(Error::from("nothing recorded"))
762+
};
763+
764+
// Revert the tracer config to the one before recording
765+
tracer.update_config(|_config| record_info.original_tracer_config);
766+
767+
// Use the trace nodes to flatten the call trace
768+
let root = tracer.traces();
769+
let steps = flatten_call_trace(0, root, record_info.start_node_idx);
770+
771+
let debug_steps: Vec<DebugStep> =
772+
steps.iter().map(|&step| convert_call_trace_to_debug_step(step)).collect();
773+
774+
// Clean up the recording info
775+
ccx.state.record_debug_steps_info = None;
776+
777+
Ok(debug_steps.abi_encode())
778+
}
779+
}
780+
718781
pub(super) fn get_nonce(ccx: &mut CheatsCtxt, address: &Address) -> Result {
719782
let account = ccx.ecx.journaled_state.load_account(*address, &mut ccx.ecx.db)?;
720783
Ok(account.info.nonce.abi_encode())

0 commit comments

Comments
 (0)