evm_fork_cache/tracing.rs
1//! Call-frame tracing and a composing inspector seam.
2//!
3//! This module provides [`CallTracer`], a [`revm::Inspector`] that reconstructs
4//! the **call-frame tree** of a simulation — the top-level call plus every nested
5//! `CALL`/`STATICCALL`/`DELEGATECALL`/`CALLCODE` and `CREATE`/`CREATE2` frame —
6//! without opcode/step-level tracing. Each [`CallTrace`] records the caller, the
7//! callee (or created address), the call value, calldata, gas used, return data,
8//! a [`CallStatus`], the call depth, and its child frames.
9//!
10//! It also provides [`InspectorStack`], a tiny composing inspector that fans out
11//! every [`Inspector`] hook to two inner inspectors so, e.g., a `CallTracer` and a
12//! [`TransferInspector`](crate::inspector::TransferInspector) can run in a single
13//! pass and each produce its own independent result.
14//!
15//! Attach either via the inspector-generic
16//! [`EvmOverlay::call_raw_with_inspector`](crate::cache::EvmOverlay::call_raw_with_inspector).
17//!
18//! # Calldata resolution caveat
19//!
20//! revm represents a frame's calldata as a [`CallInput`], which is either owned
21//! [`CallInput::Bytes`] or a [`CallInput::SharedBuffer`] range into the EVM's
22//! shared-memory scratch buffer. Resolving a `SharedBuffer` range back to bytes
23//! requires the concrete EVM context (`ContextTr`), which this inspector — written
24//! against the same fully-generic `CTX` as the existing
25//! [`TransferInspector`](crate::inspector::TransferInspector) — deliberately does
26//! not bind. The **top-level** call's calldata is always `CallInput::Bytes` (revm
27//! builds it directly from the transaction), so a root frame's
28//! [`input`](CallTrace::input) is always the real calldata. Nested calls whose
29//! calldata is a `SharedBuffer` are recorded with an **empty** `input`; their
30//! callee address, value, gas, status, and subcalls are captured faithfully. This
31//! is a documented limitation, not a correctness bug: the tracer never fabricates
32//! calldata it cannot resolve.
33
34use alloy_primitives::{Address, Bytes, Log, U256};
35use revm::Inspector;
36use revm::interpreter::{
37 CallInput, CallInputs, CallOutcome, CallScheme, CreateInputs, CreateOutcome, CreateScheme,
38 Interpreter, InterpreterTypes,
39};
40
41/// The kind of EVM frame a [`CallTrace`] represents.
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub enum CallKind {
44 /// A `CALL`.
45 Call,
46 /// A `STATICCALL`.
47 StaticCall,
48 /// A `DELEGATECALL`.
49 DelegateCall,
50 /// A `CALLCODE`.
51 CallCode,
52 /// A `CREATE`.
53 Create,
54 /// A `CREATE2`.
55 Create2,
56}
57
58impl CallKind {
59 /// Map a revm [`CallScheme`] (the opcode behind a message call) to a [`CallKind`].
60 fn from_call_scheme(scheme: CallScheme) -> Self {
61 match scheme {
62 CallScheme::Call => CallKind::Call,
63 CallScheme::CallCode => CallKind::CallCode,
64 CallScheme::DelegateCall => CallKind::DelegateCall,
65 CallScheme::StaticCall => CallKind::StaticCall,
66 }
67 }
68
69 /// Map a revm [`CreateScheme`] to a [`CallKind`].
70 ///
71 /// `CreateScheme::Custom` (an internally-addressed create) is reported as
72 /// [`CallKind::Create`].
73 fn from_create_scheme(scheme: CreateScheme) -> Self {
74 match scheme {
75 CreateScheme::Create => CallKind::Create,
76 CreateScheme::Create2 { .. } => CallKind::Create2,
77 CreateScheme::Custom { .. } => CallKind::Create,
78 }
79 }
80}
81
82/// The terminal status of an EVM frame.
83#[derive(Clone, Debug, PartialEq, Eq)]
84pub enum CallStatus {
85 /// The frame returned normally (`STOP`/`RETURN`/`SELFDESTRUCT`).
86 Success,
87 /// The frame reverted (`REVERT`, or a revert-class condition).
88 Revert,
89 /// The frame halted on an exceptional error (e.g. out of gas, invalid opcode).
90 Halt,
91}
92
93/// A single node in the call-frame tree captured by a [`CallTracer`].
94///
95/// One `CallTrace` is produced per EVM message call or contract creation. Child
96/// frames (the calls/creates made *by* this frame) are nested under
97/// [`subcalls`](Self::subcalls) in execution order.
98#[derive(Clone, Debug)]
99pub struct CallTrace {
100 /// Whether this frame is a call (and which kind) or a contract creation.
101 pub kind: CallKind,
102 /// The caller (the account initiating this frame).
103 pub from: Address,
104 /// The callee, or — for a `CREATE`/`CREATE2` — the created contract address.
105 pub to: Address,
106 /// The call value (wei). Always zero for `STATICCALL`.
107 pub value: U256,
108 /// The calldata (for a create, the init code). Empty when the calldata was a
109 /// shared-memory range this tracer could not resolve — see the
110 /// [module docs](crate::tracing#calldata-resolution-caveat).
111 pub input: Bytes,
112 /// Gas spent executing this frame.
113 pub gas_used: u64,
114 /// The frame's return data (for a successful create, the deployed code).
115 pub output: Bytes,
116 /// The terminal status of the frame.
117 pub status: CallStatus,
118 /// The frame's call depth (the top-level call is depth `0`).
119 pub depth: usize,
120 /// Child frames (calls/creates made by this frame), in execution order.
121 pub subcalls: Vec<CallTrace>,
122}
123
124/// A frame whose `*_end` hook has not yet fired (its output/gas/status are unset).
125///
126/// While a frame is open its already-completed children accumulate in `subcalls`;
127/// when the matching `call_end`/`create_end` fires it is finalized into a
128/// [`CallTrace`] and attached to its parent (or installed as the root).
129#[derive(Clone, Debug)]
130struct PendingFrame {
131 kind: CallKind,
132 from: Address,
133 to: Address,
134 value: U256,
135 input: Bytes,
136 depth: usize,
137 subcalls: Vec<CallTrace>,
138}
139
140/// A [`revm::Inspector`] that builds a [`CallTrace`] tree from the call/create
141/// frame hooks.
142///
143/// Drive it through
144/// [`EvmOverlay::call_raw_with_inspector`](crate::cache::EvmOverlay::call_raw_with_inspector),
145/// then read the captured tree with [`root`](Self::root) or [`into_trace`](Self::into_trace).
146///
147/// Only the call-frame hooks (`call`/`call_end`/`create`/`create_end`) are used;
148/// no opcode/step, `SLOAD`/`SSTORE`, or per-opcode gas tracing is performed.
149///
150/// ```
151/// use evm_fork_cache::CallTracer;
152///
153/// let tracer = CallTracer::new();
154/// assert!(tracer.root().is_none()); // nothing executed yet
155/// ```
156#[derive(Clone, Debug, Default)]
157pub struct CallTracer {
158 /// Open frames, innermost last. A `call`/`create` pushes; the matching
159 /// `*_end` pops.
160 stack: Vec<PendingFrame>,
161 /// The finalized top-level frame, set when the outermost frame's `*_end` fires.
162 root: Option<CallTrace>,
163}
164
165impl CallTracer {
166 /// Create an empty tracer with no captured frames.
167 pub fn new() -> Self {
168 Self::default()
169 }
170
171 /// The root (top-level) frame after a transact, or `None` if nothing executed.
172 pub fn root(&self) -> Option<&CallTrace> {
173 self.root.as_ref()
174 }
175
176 /// Consume the tracer and return the root frame, or `None` if nothing executed.
177 pub fn into_trace(self) -> Option<CallTrace> {
178 self.root
179 }
180
181 /// Push a new open frame onto the stack at the current depth.
182 fn push_frame(
183 &mut self,
184 kind: CallKind,
185 from: Address,
186 to: Address,
187 value: U256,
188 input: Bytes,
189 ) {
190 let depth = self.stack.len();
191 self.stack.push(PendingFrame {
192 kind,
193 from,
194 to,
195 value,
196 input,
197 depth,
198 subcalls: Vec::new(),
199 });
200 }
201
202 /// Finalize the innermost open frame into a [`CallTrace`] and attach it to its
203 /// parent (or install it as the root if it was the top-level frame).
204 ///
205 /// `to_override` lets the create hooks supply the created address, which is
206 /// not known until `create_end`.
207 fn pop_frame(
208 &mut self,
209 gas_used: u64,
210 output: Bytes,
211 status: CallStatus,
212 to_override: Option<Address>,
213 ) {
214 let Some(pending) = self.stack.pop() else {
215 // Defensive: an unbalanced `*_end` with no matching open frame. revm
216 // pairs the hooks, so this should not happen; ignore rather than panic.
217 return;
218 };
219
220 let trace = CallTrace {
221 kind: pending.kind,
222 from: pending.from,
223 to: to_override.unwrap_or(pending.to),
224 value: pending.value,
225 input: pending.input,
226 gas_used,
227 output,
228 status,
229 depth: pending.depth,
230 subcalls: pending.subcalls,
231 };
232
233 if let Some(parent) = self.stack.last_mut() {
234 parent.subcalls.push(trace);
235 } else {
236 self.root = Some(trace);
237 }
238 }
239}
240
241/// Resolve a frame's [`CallInput`] to owned bytes.
242///
243/// Owned [`CallInput::Bytes`] are cloned directly. A [`CallInput::SharedBuffer`]
244/// range cannot be resolved without the concrete EVM context, so it yields empty
245/// bytes — see the [module docs](crate::tracing#calldata-resolution-caveat).
246fn resolve_call_input(input: &CallInput) -> Bytes {
247 match input {
248 CallInput::Bytes(bytes) => bytes.clone(),
249 CallInput::SharedBuffer(_) => Bytes::new(),
250 }
251}
252
253/// Maps a frame's terminal [`InstructionResult`](revm::interpreter::InstructionResult)
254/// to a [`CallStatus`].
255fn status_from_result(result: revm::interpreter::InstructionResult) -> CallStatus {
256 if result.is_ok() {
257 CallStatus::Success
258 } else if result.is_revert() {
259 CallStatus::Revert
260 } else {
261 // Everything else is an exceptional halt (out of gas, invalid opcode, …).
262 CallStatus::Halt
263 }
264}
265
266impl<CTX, INTR> Inspector<CTX, INTR> for CallTracer
267where
268 INTR: InterpreterTypes,
269{
270 fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option<CallOutcome> {
271 self.push_frame(
272 CallKind::from_call_scheme(inputs.scheme),
273 inputs.caller,
274 inputs.target_address,
275 inputs.call_value(),
276 resolve_call_input(&inputs.input),
277 );
278 None
279 }
280
281 fn call_end(&mut self, _context: &mut CTX, _inputs: &CallInputs, outcome: &mut CallOutcome) {
282 let status = status_from_result(*outcome.instruction_result());
283 let gas_used = outcome.gas().spent();
284 let output = outcome.output().clone();
285 self.pop_frame(gas_used, output, status, None);
286 }
287
288 fn create(&mut self, _context: &mut CTX, inputs: &mut CreateInputs) -> Option<CreateOutcome> {
289 self.push_frame(
290 CallKind::from_create_scheme(inputs.scheme()),
291 inputs.caller(),
292 // The created address is not known until `create_end`; fill a
293 // placeholder now and override it on finalize.
294 Address::ZERO,
295 inputs.value(),
296 inputs.init_code().clone(),
297 );
298 None
299 }
300
301 fn create_end(
302 &mut self,
303 _context: &mut CTX,
304 _inputs: &CreateInputs,
305 outcome: &mut CreateOutcome,
306 ) {
307 let status = status_from_result(*outcome.instruction_result());
308 let gas_used = outcome.gas().spent();
309 let output = outcome.output().clone();
310 self.pop_frame(gas_used, output, status, outcome.address);
311 }
312}
313
314/// Runs two [`Inspector`]s over the same execution.
315///
316/// Every [`Inspector`] hook is fanned out to both inner inspectors (`.0` first,
317/// then `.1`) so each captures its own result independently in a single pass. The
318/// canonical use is pairing a [`CallTracer`] with a
319/// [`TransferInspector`](crate::inspector::TransferInspector) to obtain both a
320/// call-frame trace and the ERC-20 transfers from one simulation:
321///
322/// ```no_run
323/// # use std::sync::Arc;
324/// # use alloy_primitives::{Address, Bytes};
325/// # use evm_fork_cache::cache::{EvmOverlay, EvmSnapshot, TxConfig};
326/// # use evm_fork_cache::inspector::TransferInspector;
327/// # use evm_fork_cache::{CallTracer, InspectorStack};
328/// # fn run(snapshot: Arc<EvmSnapshot>, from: Address, to: Address) -> Result<(), Box<dyn std::error::Error>> {
329/// let mut overlay = EvmOverlay::new(snapshot, None);
330/// let (_result, stack) = overlay.call_raw_with_inspector(
331/// from,
332/// to,
333/// Bytes::new(),
334/// &TxConfig::default(),
335/// InspectorStack(CallTracer::new(), TransferInspector::new()),
336/// false,
337/// )?;
338/// let InspectorStack(tracer, transfer) = stack;
339/// let _trace = tracer.into_trace();
340/// let _transfers = transfer.transfers;
341/// # Ok(())
342/// # }
343/// ```
344///
345/// For the `call`/`create` hooks — which return `Option<…Outcome>` to optionally
346/// *override* the result — the first inspector that returns `Some` wins and the
347/// second's hook is not called, mirroring revm's own composition of inspector
348/// tuples. The observe-only inspectors here ([`CallTracer`],
349/// [`TransferInspector`](crate::inspector::TransferInspector)) always return
350/// `None`, so both always observe.
351#[derive(Clone, Debug, Default)]
352pub struct InspectorStack<A, B>(pub A, pub B);
353
354impl<A, B, CTX, INTR> Inspector<CTX, INTR> for InspectorStack<A, B>
355where
356 INTR: InterpreterTypes,
357 A: Inspector<CTX, INTR>,
358 B: Inspector<CTX, INTR>,
359{
360 fn initialize_interp(&mut self, interp: &mut Interpreter<INTR>, context: &mut CTX) {
361 self.0.initialize_interp(interp, context);
362 self.1.initialize_interp(interp, context);
363 }
364
365 fn step(&mut self, interp: &mut Interpreter<INTR>, context: &mut CTX) {
366 self.0.step(interp, context);
367 self.1.step(interp, context);
368 }
369
370 fn step_end(&mut self, interp: &mut Interpreter<INTR>, context: &mut CTX) {
371 self.0.step_end(interp, context);
372 self.1.step_end(interp, context);
373 }
374
375 fn log(&mut self, context: &mut CTX, log: Log) {
376 self.0.log(context, log.clone());
377 self.1.log(context, log);
378 }
379
380 fn log_full(&mut self, interp: &mut Interpreter<INTR>, context: &mut CTX, log: Log) {
381 self.0.log_full(interp, context, log.clone());
382 self.1.log_full(interp, context, log);
383 }
384
385 fn call(&mut self, context: &mut CTX, inputs: &mut CallInputs) -> Option<CallOutcome> {
386 self.0
387 .call(context, inputs)
388 .or_else(|| self.1.call(context, inputs))
389 }
390
391 fn call_end(&mut self, context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) {
392 self.0.call_end(context, inputs, outcome);
393 self.1.call_end(context, inputs, outcome);
394 }
395
396 fn create(&mut self, context: &mut CTX, inputs: &mut CreateInputs) -> Option<CreateOutcome> {
397 self.0
398 .create(context, inputs)
399 .or_else(|| self.1.create(context, inputs))
400 }
401
402 fn create_end(
403 &mut self,
404 context: &mut CTX,
405 inputs: &CreateInputs,
406 outcome: &mut CreateOutcome,
407 ) {
408 self.0.create_end(context, inputs, outcome);
409 self.1.create_end(context, inputs, outcome);
410 }
411
412 fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) {
413 self.0.selfdestruct(contract, target, value);
414 self.1.selfdestruct(contract, target, value);
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 #[test]
423 fn new_tracer_has_no_root() {
424 let tracer = CallTracer::new();
425 assert!(tracer.root().is_none());
426 assert!(tracer.into_trace().is_none());
427 }
428
429 #[test]
430 fn call_kind_maps_call_schemes() {
431 assert_eq!(CallKind::from_call_scheme(CallScheme::Call), CallKind::Call);
432 assert_eq!(
433 CallKind::from_call_scheme(CallScheme::StaticCall),
434 CallKind::StaticCall
435 );
436 assert_eq!(
437 CallKind::from_call_scheme(CallScheme::DelegateCall),
438 CallKind::DelegateCall
439 );
440 assert_eq!(
441 CallKind::from_call_scheme(CallScheme::CallCode),
442 CallKind::CallCode
443 );
444 }
445
446 #[test]
447 fn call_kind_maps_create_schemes() {
448 assert_eq!(
449 CallKind::from_create_scheme(CreateScheme::Create),
450 CallKind::Create
451 );
452 assert_eq!(
453 CallKind::from_create_scheme(CreateScheme::Create2 { salt: U256::ZERO }),
454 CallKind::Create2
455 );
456 }
457
458 #[test]
459 fn resolve_input_reads_owned_bytes_and_empties_shared_buffer() {
460 let owned = CallInput::Bytes(Bytes::from(vec![1, 2, 3]));
461 assert_eq!(resolve_call_input(&owned), Bytes::from(vec![1, 2, 3]));
462
463 let shared = CallInput::SharedBuffer(0..8);
464 assert!(resolve_call_input(&shared).is_empty());
465 }
466
467 #[test]
468 fn status_mapping_classifies_results() {
469 use revm::interpreter::InstructionResult;
470 assert_eq!(
471 status_from_result(InstructionResult::Stop),
472 CallStatus::Success
473 );
474 assert_eq!(
475 status_from_result(InstructionResult::Return),
476 CallStatus::Success
477 );
478 assert_eq!(
479 status_from_result(InstructionResult::Revert),
480 CallStatus::Revert
481 );
482 assert_eq!(
483 status_from_result(InstructionResult::OutOfGas),
484 CallStatus::Halt
485 );
486 }
487
488 /// A finalized child frame attaches to its open parent, and the parent
489 /// becomes the root when its own `*_end` fires.
490 #[test]
491 fn frames_nest_into_a_tree() {
492 let mut tracer = CallTracer::new();
493 let root_addr = Address::repeat_byte(0x11);
494 let child_addr = Address::repeat_byte(0x22);
495
496 tracer.push_frame(
497 CallKind::Call,
498 Address::ZERO,
499 root_addr,
500 U256::ZERO,
501 Bytes::from(vec![0xaa]),
502 );
503 tracer.push_frame(
504 CallKind::StaticCall,
505 root_addr,
506 child_addr,
507 U256::ZERO,
508 Bytes::new(),
509 );
510 // Inner frame ends first.
511 tracer.pop_frame(10, Bytes::new(), CallStatus::Success, None);
512 // Outer frame ends, becoming the root.
513 tracer.pop_frame(100, Bytes::from(vec![0xbb]), CallStatus::Success, None);
514
515 let root = tracer.into_trace().expect("root frame");
516 assert_eq!(root.to, root_addr);
517 assert_eq!(root.depth, 0);
518 assert_eq!(root.input, Bytes::from(vec![0xaa]));
519 assert_eq!(root.subcalls.len(), 1);
520 assert_eq!(root.subcalls[0].to, child_addr);
521 assert_eq!(root.subcalls[0].depth, 1);
522 assert_eq!(root.subcalls[0].kind, CallKind::StaticCall);
523 }
524}