Skip to main content

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}