Skip to main content

foundry_evm/inspectors/
debug_recorder.rs

1//! Records the debugger's opcode steps straight into a SolDB step table.
2
3use alloy_primitives::U256;
4use foundry_evm_traces::debug::{DebugRecording, DebugStepSink, RecordedFrame, StepConverter};
5use revm::{
6    Inspector, JournalEntry,
7    bytecode::opcode::OpCode,
8    context::{ContextTr, JournalTr},
9    inspector::JournalExt,
10    interpreter::{
11        CallInputs, CallOutcome, CreateInputs, CreateOutcome, InstructionResult, Interpreter,
12        interpreter_types::{Jumps, LoopControl, ReturnData},
13    },
14};
15use soldb_core::{EvmWord, StepRecord, StepTable};
16use std::{borrow::Borrow, fmt, sync::Arc};
17
18/// The most stack words one instruction reads: `SWAP16` reads 17.
19const MAX_INPUTS: usize = 17;
20
21/// Records every opcode step of the debugged calls into a [`StepTable`], with the steps that
22/// each call and creation spans.
23///
24/// The steps are the ones `TracingInspector` records in debug mode, converted as the SolDB
25/// frontend converts them, but without the per-step `CallTraceStep` and its full stack copy. The
26/// inspector stack calls this recorder next to the tracer, so its frames are the tracer's nodes
27/// in the same order.
28pub struct DebugRecorder {
29    sink: DebugStepSink,
30    /// The sink's table while this recorder appends to it.
31    table: Option<StepTable>,
32    /// The first step this recorder appended.
33    first_step: usize,
34    /// The steps of each call trace node, by node index.
35    frames: Vec<RecordedFrame>,
36    /// The frames that run now, innermost last.
37    open: Vec<OpenFrame>,
38    /// The step that started and did not end yet.
39    pending: Option<PendingStep>,
40    /// Converts the return data and storage of the steps.
41    converter: StepConverter,
42}
43
44/// A frame that runs now.
45struct OpenFrame {
46    /// The index of the frame in `frames`.
47    frame: usize,
48    /// The range the previous opcode may have written, applied when this frame resumes.
49    write: Option<std::ops::Range<usize>>,
50    /// The memory size already recorded for this frame.
51    memory_size: usize,
52}
53
54/// What a step records when it starts.
55struct PendingStep {
56    pc: u64,
57    op: OpCode,
58    gas: u64,
59    depth: u64,
60    #[cfg(test)]
61    memory: Vec<u8>,
62    return_data: Arc<str>,
63    /// The stack height before the step.
64    height: usize,
65    /// The top words the step can pop or change, bottom first.
66    inputs: [U256; MAX_INPUTS],
67    inputs_len: usize,
68    /// The journal length before the step.
69    journal_len: usize,
70}
71
72impl DebugRecorder {
73    /// A recorder that appends to `sink`.
74    pub fn new(sink: DebugStepSink) -> Self {
75        Self {
76            sink,
77            table: None,
78            first_step: 0,
79            // The arena of a new tracer starts with an empty root node.
80            frames: vec![RecordedFrame::default()],
81            open: Vec::new(),
82            pending: None,
83            converter: StepConverter::default(),
84        }
85    }
86
87    /// The steps this recorder appended, by call trace node.
88    pub fn finish(&mut self) -> DebugRecording {
89        let end = self.table.as_ref().map_or(self.first_step, StepTable::len);
90        DebugRecording {
91            sink: self.sink.clone(),
92            steps: self.first_step..end,
93            frames: std::mem::take(&mut self.frames),
94        }
95    }
96
97    /// The table to append to, taken from the sink on first use.
98    fn table(&mut self) -> &mut StepTable {
99        if self.table.is_none() {
100            // A recorder of another stack that has not finished holds the shared table, so this
101            // one records into a table of its own.
102            let table = self.sink.take().unwrap_or_else(|| {
103                self.sink = DebugStepSink::new();
104                self.sink.take().unwrap_or_default()
105            });
106            self.first_step = table.len();
107            self.table = Some(table);
108        }
109        self.table.as_mut().unwrap()
110    }
111
112    /// Opens a frame at journal depth `depth`, as the tracer opens a node.
113    fn enter(&mut self, depth: usize) {
114        let entry_step = self.table().len() - self.first_step;
115        let frame = RecordedFrame { depth, entry_step, exit_step: entry_step };
116        // The tracer replaces the root node with the call at depth zero.
117        let index = if depth == 0 {
118            self.frames[0] = frame;
119            0
120        } else {
121            self.frames.push(frame);
122            self.frames.len() - 1
123        };
124        self.table().start_memory_frame();
125        self.open.push(OpenFrame { frame: index, write: None, memory_size: 0 });
126    }
127
128    /// Closes the innermost frame.
129    fn exit(&mut self) {
130        debug_assert!(self.pending.is_none(), "a step did not end before its frame");
131        self.pending = None;
132        let exit_step = self.table().len() - self.first_step;
133        if let Some(open) = self.open.pop() {
134            self.frames[open.frame].exit_step = exit_step;
135            self.table().end_memory_frame();
136        }
137    }
138
139    /// Starts a step.
140    fn start_step<CTX: ContextTr<Journal: JournalExt>>(
141        &mut self,
142        interp: &Interpreter,
143        context: &CTX,
144    ) {
145        let Some(open) = self.open.last_mut() else { return };
146        let op = OpCode::new_or_unknown(interp.bytecode.opcode());
147
148        // CALL output is copied after the child's call_end hook. Record it when the caller
149        // resumes, along with ordinary writes, before taking this step's memory reference.
150        let current = interp.memory.borrow();
151        let current = current.context_memory();
152        let table = self.table.as_mut().expect("a frame is open");
153        if let Some(range) = open.write.take() {
154            let start = range.start.min(current.len());
155            let end = range.end.min(current.len());
156            table
157                .write_memory(start as u64, &current[start..end], current.len() as u64)
158                .expect("the write is bounded by the interpreter's memory");
159        } else if open.memory_size != current.len() {
160            table
161                .write_memory(0, &[], current.len() as u64)
162                .expect("EVM memory only grows within a frame");
163        }
164        open.memory_size = current.len();
165        open.write = memory_write(op, interp.stack.data());
166        #[cfg(test)]
167        let memory = current.to_vec();
168        drop(current);
169
170        let return_data = self.converter.return_data(interp.return_data.buffer());
171
172        // An instruction changes only the words it reads, so the stack before it is what stays
173        // below them after it, and these.
174        let stack = interp.stack.data();
175        let read = if op.is_valid() { op.inputs() as usize } else { 0 };
176        let inputs_len = read.min(stack.len()).min(MAX_INPUTS);
177        let mut inputs = [U256::ZERO; MAX_INPUTS];
178        inputs[..inputs_len].copy_from_slice(&stack[stack.len() - inputs_len..]);
179
180        let depth = self.frames[open.frame].depth as u64 + 1;
181        self.pending = Some(PendingStep {
182            pc: interp.bytecode.pc() as u64,
183            op,
184            gas: interp.gas.remaining(),
185            depth,
186            #[cfg(test)]
187            memory,
188            return_data,
189            height: stack.len(),
190            inputs,
191            inputs_len,
192            journal_len: context.journal_ref().journal().len(),
193        });
194    }
195
196    /// Ends the pending step after its instruction ran.
197    fn end_step<CTX: ContextTr<Journal: JournalExt>>(
198        &mut self,
199        interp: &mut Interpreter,
200        context: &CTX,
201    ) {
202        let Some(step) = self.pending.take() else { return };
203        let status =
204            interp.bytecode.action().as_ref().and_then(|action| action.instruction_result());
205        let after = interp.stack.data();
206        // The frame runs another step unless this one ended it; that step starts with the value
207        // a load pushed on top.
208        let loaded = status.is_none().then(|| after.last().copied()).flatten();
209        let had_value = (step.op == OpCode::SSTORE
210            && context.journal_ref().journal().len() != step.journal_len)
211            .then(|| match context.journal_ref().journal().last() {
212                Some(JournalEntry::StorageChanged { had_value, .. }) => Some(*had_value),
213                _ => None,
214            })
215            .flatten();
216        let gas_cost = step.gas.saturating_sub(interp.gas.remaining());
217        let kept = &after[..(step.height - step.inputs_len).min(after.len())];
218        self.append(step, kept, status, gas_cost, loaded, had_value);
219    }
220
221    /// Ends the pending step if an inspector halted the frame before its instruction ran.
222    pub fn end_step_not_run(&mut self, interp: &Interpreter) {
223        let Some(step) = self.pending.take() else { return };
224        let stack = interp.stack.data();
225        let kept = &stack[..(step.height - step.inputs_len).min(stack.len())];
226        self.append(step, kept, None, 0, None, None);
227    }
228
229    /// Appends `step`, whose stack is `kept` and its inputs.
230    ///
231    /// `loaded` is the value an `SLOAD` or `TLOAD` pushed, if the frame went on, and `had_value`
232    /// the value an `SSTORE` overwrote, if the journal recorded it.
233    fn append(
234        &mut self,
235        step: PendingStep,
236        kept: &[U256],
237        status: Option<InstructionResult>,
238        gas_cost: u64,
239        loaded: Option<U256>,
240        had_value: Option<U256>,
241    ) {
242        #[cfg(test)]
243        let expected_memory = step.memory;
244        let PendingStep { pc, op, gas, depth, return_data, inputs, inputs_len, .. } = step;
245        let inputs = &inputs[..inputs_len];
246        let storage = self.converter.storage(op.get(), inputs, loaded, had_value);
247        let error = status.filter(|status| !status.is_ok()).map(|status| format!("{status:?}"));
248        let table = self.table.as_mut().expect("a frame is open");
249        table.append(
250            StepRecord {
251                pc,
252                op: op.get(),
253                gas,
254                gas_cost,
255                depth,
256                error,
257                memory: None,
258                storage: Some(storage.storage),
259                storage_diff: storage.storage_diff,
260                transient_storage: storage.transient_storage,
261                return_data: Some(return_data),
262            },
263            kept.iter().chain(inputs).map(|word| EvmWord(word.to_be_bytes())),
264        );
265        #[cfg(test)]
266        assert_eq!(
267            table.step(table.len() - 1).memory().as_deref(),
268            Some(expected_memory.as_slice()),
269            "recorded memory differs at pc {pc}, opcode {op}"
270        );
271    }
272}
273
274impl Default for DebugRecorder {
275    fn default() -> Self {
276        Self::new(DebugStepSink::new())
277    }
278}
279
280/// A clone records the next call into the same sink, from an empty state.
281impl Clone for DebugRecorder {
282    fn clone(&self) -> Self {
283        Self::new(self.sink.clone())
284    }
285}
286
287/// Gives the table back to the sink.
288impl Drop for DebugRecorder {
289    fn drop(&mut self) {
290        if let Some(table) = self.table.take() {
291            self.sink.put(table);
292        }
293    }
294}
295
296impl fmt::Debug for DebugRecorder {
297    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
298        f.debug_struct("DebugRecorder")
299            .field("frames", &self.frames.len())
300            .field("steps", &self.table.as_ref().map(StepTable::len))
301            .finish_non_exhaustive()
302    }
303}
304
305impl<CTX: ContextTr<Journal: JournalExt>> Inspector<CTX> for DebugRecorder {
306    #[inline]
307    fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) {
308        self.start_step(interp, context);
309    }
310
311    #[inline]
312    fn step_end(&mut self, interp: &mut Interpreter, context: &mut CTX) {
313        self.end_step(interp, context);
314    }
315
316    fn call(&mut self, context: &mut CTX, _inputs: &mut CallInputs) -> Option<CallOutcome> {
317        self.enter(context.journal().depth());
318        None
319    }
320
321    fn call_end(&mut self, _context: &mut CTX, _inputs: &CallInputs, _outcome: &mut CallOutcome) {
322        self.exit();
323    }
324
325    fn create(&mut self, context: &mut CTX, _inputs: &mut CreateInputs) -> Option<CreateOutcome> {
326        self.enter(context.journal().depth());
327        None
328    }
329
330    fn create_end(
331        &mut self,
332        _context: &mut CTX,
333        _inputs: &CreateInputs,
334        _outcome: &mut CreateOutcome,
335    ) {
336        self.exit();
337    }
338}
339
340/// The destination a memory-writing opcode takes from its pre-execution stack.
341/// Memory-reading opcodes can expand memory but leave all new bytes zero.
342fn memory_write(op: OpCode, stack: &[U256]) -> Option<std::ops::Range<usize>> {
343    let word = |depth: usize| -> Option<usize> {
344        stack.get(stack.len().checked_sub(depth + 1)?)?.try_into().ok()
345    };
346    let (offset, length) = match op {
347        OpCode::MSTORE => (word(0)?, 32),
348        OpCode::MSTORE8 => (word(0)?, 1),
349        OpCode::CALLDATACOPY | OpCode::CODECOPY | OpCode::RETURNDATACOPY | OpCode::MCOPY => {
350            (word(0)?, word(2)?)
351        }
352        OpCode::EXTCODECOPY => (word(1)?, word(3)?),
353        OpCode::CALL | OpCode::CALLCODE => (word(5)?, word(6)?),
354        OpCode::DELEGATECALL | OpCode::STATICCALL => (word(4)?, word(5)?),
355        _ => return None,
356    };
357    Some(offset..offset.saturating_add(length))
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363    use alloy_primitives::{Address, TxKind, hex};
364    use revm::{
365        Context, InspectEvm, MainBuilder, MainContext,
366        context::TxEnv,
367        database::InMemoryDB,
368        state::{AccountInfo, Bytecode},
369    };
370
371    #[derive(Default)]
372    struct Snapshots(Vec<(Vec<u8>, Vec<EvmWord>)>);
373
374    impl<CTX> Inspector<CTX> for Snapshots {
375        fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) {
376            self.0.push((
377                interp.memory.borrow().context_memory().to_vec(),
378                interp.stack.data().iter().map(|word| EvmWord(word.to_be_bytes())).collect(),
379            ));
380        }
381    }
382
383    #[test]
384    fn memory_writes_reconstruct_interpreter_state_in_both_directions() {
385        let target = Address::with_last_byte(0x40);
386        let child = Address::with_last_byte(0x20);
387        let mut db = InMemoryDB::default();
388        // Stores, overlapping MCOPY, the three copy instructions, and expansion by MLOAD.
389        // The child returns 32 bytes into a 64-byte output range; the rest must stay unchanged.
390        let parent = hex::decode(concat!(
391            "61abcd5f52",                 // MSTORE(0, 0xabcd).
392            "6089600753",                 // MSTORE8(7, 0x89).
393            "601f5f60085e",               // MCOPY(8, 0, 31).
394            "601f60085f5e",               // MCOPY(0, 8, 31).
395            "60295f608037",               // CALLDATACOPY(128, 0, 41).
396            "60295f61010039",             // CODECOPY(256, 0, 41).
397            "60295f61018060203c",         // EXTCODECOPY(child, 384, 0, 41).
398            "6102005150",                 // MLOAD(512).
399            "604261030052",               // MSTORE(768, 66).
400            "60406102e05f5f5f60205af150", // CALL with output at 736.
401            "3d5f6104003e",               // RETURNDATACOPY(1024, 0, RETURNDATASIZE).
402            "60206104405f5f60205afa50",   // STATICCALL.
403            "60206104605f5f60205af450",   // DELEGATECALL.
404            "60206104805f5f5f60205af250", // CALLCODE.
405            "00"
406        ))
407        .unwrap();
408        db.insert_account_info(
409            target,
410            AccountInfo::from_bytecode(Bytecode::new_raw(parent.into())),
411        );
412        db.insert_account_info(
413            child,
414            AccountInfo::from_bytecode(Bytecode::new_raw(
415                hex::decode("61cafe5f5260205ff3").unwrap().into(),
416            )),
417        );
418        let mut recorder = DebugRecorder::default();
419        let mut snapshots = Snapshots::default();
420        let result = Context::mainnet()
421            .with_db(db)
422            .build_mainnet_with_inspector((&mut recorder, &mut snapshots))
423            .inspect_one_tx(
424                TxEnv::builder()
425                    .caller(Address::with_last_byte(0x30))
426                    .kind(TxKind::Call(target))
427                    .gas_limit(1_000_000)
428                    .build()
429                    .unwrap(),
430            )
431            .unwrap();
432        assert!(result.is_success());
433        let table = recorder.table.as_ref().unwrap();
434        assert_eq!(table.len(), snapshots.0.len());
435        for index in (0..table.len()).rev().chain(0..table.len()) {
436            let step = table.step(index);
437            assert_eq!(
438                step.memory().as_deref(),
439                Some(snapshots.0[index].0.as_slice()),
440                "memory at {index}"
441            );
442            assert_eq!(step.stack().to_vec(), snapshots.0[index].1, "stack at {index}");
443        }
444    }
445}