Skip to main content

foundry_evm_traces/
lib.rs

1//! # foundry-evm-traces
2//!
3//! EVM trace identifying and decoding.
4
5#![cfg_attr(not(test), warn(unused_crate_dependencies))]
6#![cfg_attr(docsrs, feature(doc_cfg))]
7
8#[macro_use]
9extern crate foundry_common;
10
11#[macro_use]
12extern crate tracing;
13
14use foundry_common::{
15    contracts::{ContractsByAddress, ContractsByArtifact},
16    shell,
17};
18use revm::bytecode::opcode::OpCode;
19use revm_inspectors::tracing::{OpcodeFilter, types::DecodedTraceStep};
20use serde::{Deserialize, Serialize};
21use std::{
22    borrow::Cow,
23    collections::{BTreeMap, BTreeSet},
24    ops::{Deref, DerefMut},
25};
26
27use alloy_primitives::{U256, map::HashMap};
28use tempo_contracts::precompiles::TIP20_CHANNEL_RESERVE_ADDRESS;
29
30pub use revm_inspectors::tracing::{
31    CallTraceArena, FourByteInspector, GethTraceBuilder, ParityTraceBuilder, StackSnapshotType,
32    TraceWriter, TracingInspector, TracingInspectorConfig,
33    types::{
34        CallKind, CallLog, CallTrace, CallTraceNode, DecodedCallData, DecodedCallLog,
35        DecodedCallTrace, TraceMemberOrder,
36    },
37};
38
39/// Call trace address identifiers.
40///
41/// Identifiers figure out what ABIs and labels belong to all the addresses of the trace.
42pub mod identifier;
43use identifier::LocalTraceIdentifier;
44
45mod decoder;
46pub use decoder::{CallTraceDecoder, CallTraceDecoderBuilder};
47
48pub mod debug;
49pub use debug::DebugTraceIdentifier;
50
51pub mod folded_stack_trace;
52
53pub mod backtrace;
54pub mod speedscope;
55
56pub type Traces = Vec<(TraceKind, SparsedTraceArena)>;
57
58/// Trace arena keeping track of ignored trace items.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct SparsedTraceArena {
61    /// Full trace arena.
62    #[serde(flatten)]
63    pub arena: CallTraceArena,
64    /// Ranges of trace steps to ignore in format (start_node, start_step) -> (end_node, end_step).
65    /// See `foundry_cheatcodes::utils::IgnoredTraces` for more information.
66    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
67    pub ignored: HashMap<(usize, usize), (usize, usize)>,
68}
69
70impl SparsedTraceArena {
71    /// Goes over entire trace arena and removes ignored trace items.
72    fn resolve_arena(&self) -> Cow<'_, CallTraceArena> {
73        if self.ignored.is_empty() {
74            Cow::Borrowed(&self.arena)
75        } else {
76            let mut arena = self.arena.clone();
77
78            fn clear_node(
79                nodes: &mut [CallTraceNode],
80                node_idx: usize,
81                ignored: &HashMap<(usize, usize), (usize, usize)>,
82                cur_ignore_end: &mut Option<(usize, usize)>,
83            ) {
84                // Prepend an additional None item to the ordering to handle the beginning of the
85                // trace.
86                let items = std::iter::once(None)
87                    .chain(nodes[node_idx].ordering.clone().into_iter().map(Some))
88                    .enumerate();
89
90                let mut internal_calls = Vec::new();
91                let mut items_to_remove = BTreeSet::new();
92                for (item_idx, item) in items {
93                    if let Some(end_node) = ignored.get(&(node_idx, item_idx)) {
94                        *cur_ignore_end = Some(*end_node);
95                    }
96
97                    let mut remove = cur_ignore_end.is_some() & item.is_some();
98
99                    match item {
100                        // we only remove calls if they did not start/pause tracing
101                        Some(TraceMemberOrder::Call(child_idx)) => {
102                            clear_node(
103                                nodes,
104                                nodes[node_idx].children[child_idx],
105                                ignored,
106                                cur_ignore_end,
107                            );
108                            remove &= cur_ignore_end.is_some();
109                        }
110                        // we only remove decoded internal calls if they did not start/pause tracing
111                        Some(TraceMemberOrder::Step(step_idx)) => {
112                            // If this is an internal call beginning, track it in `internal_calls`
113                            if let Some(decoded) = &nodes[node_idx].trace.steps[step_idx].decoded
114                                && let DecodedTraceStep::InternalCall(_, end_step_idx) = &**decoded
115                            {
116                                internal_calls.push((item_idx, remove, *end_step_idx));
117                                // we decide if we should remove it later
118                                remove = false;
119                            }
120                            // Handle ends of internal calls
121                            internal_calls.retain(|(start_item_idx, remove_start, end_idx)| {
122                                if *end_idx != step_idx {
123                                    return true;
124                                }
125                                // only remove start if end should be removed as well
126                                if *remove_start && remove {
127                                    items_to_remove.insert(*start_item_idx);
128                                } else {
129                                    remove = false;
130                                }
131
132                                false
133                            });
134                        }
135                        _ => {}
136                    }
137
138                    if remove {
139                        items_to_remove.insert(item_idx);
140                    }
141
142                    if let Some((end_node, end_step_idx)) = cur_ignore_end
143                        && node_idx == *end_node
144                        && item_idx == *end_step_idx
145                    {
146                        *cur_ignore_end = None;
147                    }
148                }
149
150                for (offset, item_idx) in items_to_remove.into_iter().enumerate() {
151                    nodes[node_idx].ordering.remove(item_idx - offset - 1);
152                }
153            }
154
155            clear_node(arena.nodes_mut(), 0, &self.ignored, &mut None);
156
157            Cow::Owned(arena)
158        }
159    }
160}
161
162impl Deref for SparsedTraceArena {
163    type Target = CallTraceArena;
164
165    fn deref(&self) -> &Self::Target {
166        &self.arena
167    }
168}
169
170impl DerefMut for SparsedTraceArena {
171    fn deref_mut(&mut self) -> &mut Self::Target {
172        &mut self.arena
173    }
174}
175
176/// Decode a collection of call traces.
177///
178/// The traces will be decoded using the given decoder, if possible.
179pub async fn decode_trace_arena(arena: &mut CallTraceArena, decoder: &CallTraceDecoder) {
180    decoder.prefetch_signatures(arena.nodes()).await;
181    decoder.populate_traces(arena.nodes_mut()).await;
182}
183
184/// Render a collection of call traces to a string.
185pub fn render_trace_arena(arena: &SparsedTraceArena) -> String {
186    render_trace_arena_inner(arena, false, false)
187}
188
189/// Prunes trace depth if depth is provided as an argument
190pub fn prune_trace_depth(arena: &mut CallTraceArena, depth: usize) {
191    for node in arena.nodes_mut() {
192        if node.trace.depth >= depth {
193            node.ordering.clear();
194        }
195    }
196}
197
198/// Render a collection of call traces to a string optionally including contract creation bytecodes
199/// and in JSON format.
200pub fn render_trace_arena_inner(
201    arena: &SparsedTraceArena,
202    with_bytecodes: bool,
203    with_storage_changes: bool,
204) -> String {
205    if shell::is_json() {
206        return serde_json::to_string(&arena.resolve_arena()).expect("Failed to serialize traces");
207    }
208
209    let mut resolved = arena.resolve_arena();
210
211    let mut tempo_changes = None;
212    if with_storage_changes {
213        tempo_changes = tempo_channel_storage_decodes(&resolved);
214
215        let needs_dedup = resolved.as_ref().nodes().iter().any(|node| {
216            node.trace.steps.iter().any(|step| {
217                step.storage_change.is_some()
218                    && matches!(step.decoded.as_deref(), Some(DecodedTraceStep::Line(_)))
219            })
220        });
221        if needs_dedup {
222            // Remove storage text that is already represented by an opcode line.
223            for node in resolved.to_mut().nodes_mut() {
224                for step in &mut node.trace.steps {
225                    if step.storage_change.is_some()
226                        && matches!(step.decoded.as_deref(), Some(DecodedTraceStep::Line(_)))
227                    {
228                        step.storage_change = None;
229                    }
230                }
231            }
232        }
233    }
234
235    let mut w = TraceWriter::new(Vec::<u8>::new())
236        .color_cheatcodes(true)
237        .use_colors(convert_color_choice(shell::color_choice()))
238        .write_bytecodes(with_bytecodes)
239        .with_storage_changes(with_storage_changes);
240    w.write_arena(resolved.as_ref()).expect("Failed to write traces");
241    let mut rendered =
242        String::from_utf8(w.into_writer()).expect("trace writer wrote invalid UTF-8");
243    if let Some(tempo_changes) = tempo_changes {
244        if !rendered.ends_with('\n') {
245            rendered.push('\n');
246        }
247        rendered.push_str(&tempo_changes);
248    }
249
250    rendered
251}
252
253fn tempo_channel_storage_decodes(arena: &CallTraceArena) -> Option<String> {
254    let decoded_changes = arena
255        .nodes()
256        .iter()
257        .filter(|node| node.trace.address == TIP20_CHANNEL_RESERVE_ADDRESS)
258        .flat_map(compact_channel_storage_changes)
259        .collect::<Vec<_>>();
260
261    if decoded_changes.is_empty() {
262        return None;
263    }
264
265    let mut rendered = String::new();
266    rendered.push_str("Decoded TIP20ChannelReserve storage:\n");
267    for (slot, before, after) in decoded_changes {
268        rendered.push_str(&format!(
269            "  @ {}: {} -> {}\n",
270            format_storage_word(slot),
271            format_channel_state(before),
272            format_channel_state(after),
273        ));
274    }
275    Some(rendered)
276}
277
278fn compact_channel_storage_changes(node: &CallTraceNode) -> Vec<(U256, U256, U256)> {
279    let mut changes_map = BTreeMap::new();
280    for step in &node.trace.steps {
281        if let Some(change) = &step.storage_change
282            && change.had_value.is_some()
283        {
284            let (_first, last) = changes_map.entry(change.key).or_insert((&**change, &**change));
285            *last = &**change;
286        }
287    }
288
289    changes_map
290        .into_iter()
291        .filter_map(|(key, (first, last))| {
292            let before = first.had_value.unwrap_or_default();
293            let after = last.value;
294            (before != after).then_some((key, before, after))
295        })
296        .collect()
297}
298
299fn format_channel_state(value: U256) -> String {
300    let (settled, deposit, close_requested_at) = decode_channel_state(value);
301    format!("{{settled: {settled}, deposit: {deposit}, closeRequestedAt: {close_requested_at}}}")
302}
303
304fn decode_channel_state(value: U256) -> (U256, U256, u32) {
305    let mask96 = (U256::from(1) << 96) - U256::from(1);
306    let mask32 = (U256::from(1) << 32) - U256::from(1);
307    let settled: U256 = value & mask96;
308    let deposit: U256 = (value >> 96usize) & mask96;
309    let close_requested_at_word: U256 = (value >> 192usize) & mask32;
310    let close_requested_at = close_requested_at_word.to::<u32>();
311    (settled, deposit, close_requested_at)
312}
313
314fn format_storage_word(value: U256) -> String {
315    if value < U256::from(1_000_000u64) { value.to_string() } else { format!("0x{value:x}") }
316}
317
318const fn convert_color_choice(choice: shell::ColorChoice) -> revm_inspectors::ColorChoice {
319    match choice {
320        shell::ColorChoice::Auto => revm_inspectors::ColorChoice::Auto,
321        shell::ColorChoice::Always => revm_inspectors::ColorChoice::Always,
322        shell::ColorChoice::Never => revm_inspectors::ColorChoice::Never,
323    }
324}
325
326/// Specifies the kind of trace.
327#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
328pub enum TraceKind {
329    Deployment,
330    Setup,
331    Execution,
332}
333
334impl TraceKind {
335    /// Returns `true` if the trace kind is [`Deployment`].
336    ///
337    /// [`Deployment`]: TraceKind::Deployment
338    #[must_use]
339    pub const fn is_deployment(self) -> bool {
340        matches!(self, Self::Deployment)
341    }
342
343    /// Returns `true` if the trace kind is [`Setup`].
344    ///
345    /// [`Setup`]: TraceKind::Setup
346    #[must_use]
347    pub const fn is_setup(self) -> bool {
348        matches!(self, Self::Setup)
349    }
350
351    /// Returns `true` if the trace kind is [`Execution`].
352    ///
353    /// [`Execution`]: TraceKind::Execution
354    #[must_use]
355    pub const fn is_execution(self) -> bool {
356        matches!(self, Self::Execution)
357    }
358}
359
360/// Given a list of traces and artifacts, it returns a map connecting address to abi
361pub fn load_contracts<'a>(
362    traces: impl IntoIterator<Item = &'a CallTraceArena>,
363    known_contracts: &ContractsByArtifact,
364) -> ContractsByAddress {
365    let mut local_identifier = LocalTraceIdentifier::new(known_contracts);
366    let decoder = CallTraceDecoder::new();
367    let mut contracts = ContractsByAddress::new();
368    for trace in traces {
369        for address in decoder.identify_addresses(trace, &mut local_identifier) {
370            if let (Some(contract), Some(abi)) = (address.contract, address.abi) {
371                contracts.insert(address.address, (contract, abi.into_owned()));
372            }
373        }
374    }
375    contracts
376}
377
378/// Different kinds of internal functions tracing.
379#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
380pub enum InternalTraceMode {
381    #[default]
382    None,
383    /// Traces internal functions without decoding inputs/outputs from memory.
384    Simple,
385    /// Same as `Simple`, but also tracks memory snapshots.
386    Full,
387}
388
389/// Opcode step recording granularity.
390#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
391pub enum StepRecording {
392    /// No opcode steps.
393    #[default]
394    None,
395    /// Record only JUMP/JUMPDEST steps.
396    Jumps,
397    /// Record all opcode steps.
398    All,
399}
400
401impl StepRecording {
402    const fn merge(self, other: Self) -> Self {
403        match (self, other) {
404            (Self::All, _) | (_, Self::All) => Self::All,
405            (Self::Jumps, _) | (_, Self::Jumps) => Self::Jumps,
406            (Self::None, Self::None) => Self::None,
407        }
408    }
409}
410
411/// Trace data requirements composed across independent feature axes.
412#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
413pub struct TraceRequirements {
414    calls: bool,
415    steps: StepRecording,
416    memory_snapshots: bool,
417    stack_snapshots: bool,
418    returndata_snapshots: bool,
419    immediate_bytes: bool,
420    state_diff: bool,
421}
422
423impl TraceRequirements {
424    pub const fn none() -> Self {
425        Self {
426            calls: false,
427            steps: StepRecording::None,
428            memory_snapshots: false,
429            stack_snapshots: false,
430            returndata_snapshots: false,
431            immediate_bytes: false,
432            state_diff: false,
433        }
434    }
435
436    pub const fn with_calls(mut self, yes: bool) -> Self {
437        self.calls |= yes;
438        self
439    }
440
441    pub const fn merge(mut self, other: Self) -> Self {
442        self.calls |= other.calls;
443        self.steps = self.steps.merge(other.steps);
444        self.memory_snapshots |= other.memory_snapshots;
445        self.stack_snapshots |= other.stack_snapshots;
446        self.returndata_snapshots |= other.returndata_snapshots;
447        self.immediate_bytes |= other.immediate_bytes;
448        self.state_diff |= other.state_diff;
449        self
450    }
451
452    pub const fn with_steps(mut self, steps: StepRecording) -> Self {
453        self.steps = self.steps.merge(steps);
454        self
455    }
456
457    pub const fn with_memory_snapshots(mut self, yes: bool) -> Self {
458        self.memory_snapshots |= yes;
459        self
460    }
461
462    pub const fn with_stack_snapshots(mut self, yes: bool) -> Self {
463        self.stack_snapshots |= yes;
464        self
465    }
466
467    pub const fn with_debug(mut self, yes: bool) -> Self {
468        if yes {
469            self.calls = true;
470            self.steps = StepRecording::All;
471            self.memory_snapshots = true;
472            self.stack_snapshots = true;
473            self.returndata_snapshots = true;
474            self.immediate_bytes = true;
475            self.state_diff = true;
476        }
477        self
478    }
479
480    pub const fn with_decode_internal(self, mode: InternalTraceMode) -> Self {
481        match mode {
482            InternalTraceMode::None => self,
483            InternalTraceMode::Simple => {
484                self.with_calls(true).with_steps(StepRecording::Jumps).with_stack_snapshots(true)
485            }
486            InternalTraceMode::Full => self
487                .with_calls(true)
488                .with_steps(StepRecording::Jumps)
489                .with_memory_snapshots(true)
490                .with_stack_snapshots(true),
491        }
492    }
493
494    pub const fn with_all_steps(self, yes: bool) -> Self {
495        if yes { self.with_calls(true).with_steps(StepRecording::All) } else { self }
496    }
497
498    pub const fn with_state_changes(mut self, yes: bool) -> Self {
499        self.state_diff |= yes;
500        if yes {
501            self.calls = true;
502        }
503        self
504    }
505
506    pub const fn with_verbosity(self, verbosity: u8) -> Self {
507        match verbosity {
508            0..3 => self,
509            3..=4 => self.with_calls(true),
510            _ if matches!(self.steps, StepRecording::All) => self.with_calls(true),
511            _ => self.with_state_changes(true),
512        }
513    }
514
515    pub fn into_config(self) -> Option<TracingInspectorConfig> {
516        if !self.calls && self.steps == StepRecording::None && !self.state_diff {
517            return None;
518        }
519
520        let steps = if self.state_diff { StepRecording::All } else { self.steps };
521        TracingInspectorConfig {
522            record_steps: steps != StepRecording::None,
523            record_memory_snapshots: self.memory_snapshots,
524            record_stack_snapshots: if self.stack_snapshots {
525                StackSnapshotType::Full
526            } else {
527                StackSnapshotType::None
528            },
529            record_logs: true,
530            record_state_diff: self.state_diff,
531            record_returndata_snapshots: self.returndata_snapshots,
532            record_opcodes_filter: match steps {
533                StepRecording::None | StepRecording::All => None,
534                StepRecording::Jumps => {
535                    Some(OpcodeFilter::new().enabled(OpCode::JUMP).enabled(OpCode::JUMPDEST))
536                }
537            },
538            exclude_precompile_calls: false,
539            record_immediate_bytes: self.immediate_bytes,
540        }
541        .into()
542    }
543}
544
545#[cfg(test)]
546mod tests {
547    use super::*;
548    use alloy_primitives::Bytes;
549    use revm::interpreter::InstructionResult;
550    use revm_inspectors::tracing::types::{CallTraceStep, StorageChange, StorageChangeReason};
551
552    #[test]
553    fn decodes_tip1034_packed_channel_state() {
554        let settled = U256::from(123u64);
555        let deposit = U256::from(456u64);
556        let close_requested_at = U256::from(1_780_495_200u64);
557        let packed = settled | (deposit << 96usize) | (close_requested_at << 192usize);
558
559        assert_eq!(decode_channel_state(packed), (settled, deposit, 1_780_495_200));
560        assert_eq!(
561            format_channel_state(packed),
562            "{settled: 123, deposit: 456, closeRequestedAt: 1780495200}"
563        );
564    }
565
566    #[test]
567    fn tempo_storage_decodes_do_not_insert_extra_blank_line() {
568        let mut arena = CallTraceArena::default();
569        let root = &mut arena.nodes_mut()[0];
570        root.ordering.push(TraceMemberOrder::Step(0));
571        root.trace = CallTrace {
572            address: TIP20_CHANNEL_RESERVE_ADDRESS,
573            success: true,
574            steps: vec![CallTraceStep {
575                pc: 0,
576                op: OpCode::SSTORE,
577                stack: None,
578                push_stack: None,
579                memory: None,
580                returndata: Bytes::new(),
581                gas_remaining: 0,
582                gas_refund_counter: 0,
583                gas_used: 0,
584                gas_cost: 0,
585                storage_change: Some(Box::new(StorageChange {
586                    key: U256::from(1),
587                    value: U256::from(2),
588                    had_value: Some(U256::from(1)),
589                    reason: StorageChangeReason::SSTORE,
590                })),
591                status: Some(InstructionResult::Stop),
592                immediate_bytes: None,
593                decoded: None,
594            }],
595            ..Default::default()
596        };
597
598        let rendered = render_trace_arena_inner(
599            &SparsedTraceArena { arena, ignored: Default::default() },
600            false,
601            true,
602        );
603
604        assert!(rendered.contains("\nDecoded TIP20ChannelReserve storage:\n"));
605        assert!(!rendered.contains("\n\nDecoded TIP20ChannelReserve storage:\n"));
606    }
607
608    #[test]
609    fn verbosity_0_through_2_is_noop() {
610        for v in 0..=2 {
611            assert_eq!(
612                TraceRequirements::none().with_verbosity(v),
613                TraceRequirements::none(),
614                "v={v}"
615            );
616            assert_eq!(
617                TraceRequirements::none().with_calls(true).with_verbosity(v),
618                TraceRequirements::none().with_calls(true),
619                "v={v}"
620            );
621            assert_eq!(
622                TraceRequirements::none().with_debug(true).with_verbosity(v),
623                TraceRequirements::none().with_debug(true),
624                "v={v}"
625            );
626        }
627    }
628
629    #[test]
630    fn verbosity_3_and_4_raises_to_call() {
631        for v in 3..=4 {
632            assert_eq!(
633                TraceRequirements::none().with_verbosity(v),
634                TraceRequirements::none().with_calls(true),
635                "v={v}"
636            );
637            assert_eq!(
638                TraceRequirements::none().with_debug(true).with_verbosity(v),
639                TraceRequirements::none().with_debug(true),
640                "v={v}"
641            );
642            assert_eq!(
643                TraceRequirements::none().with_state_changes(true).with_verbosity(v),
644                TraceRequirements::none().with_state_changes(true),
645                "v={v}"
646            );
647        }
648    }
649
650    #[test]
651    fn verbosity_5_raises_to_record_state_diff() {
652        let state_changes = TraceRequirements::none().with_state_changes(true);
653
654        assert_eq!(TraceRequirements::none().with_verbosity(5), state_changes);
655        assert_eq!(TraceRequirements::none().with_calls(true).with_verbosity(5), state_changes);
656        let cfg = TraceRequirements::none()
657            .with_calls(true)
658            .with_steps(StepRecording::Jumps)
659            .with_verbosity(5)
660            .into_config()
661            .unwrap();
662        assert!(cfg.record_state_diff);
663        assert!(cfg.record_opcodes_filter.is_none());
664        assert_eq!(
665            TraceRequirements::none().with_debug(true).with_verbosity(5),
666            TraceRequirements::none().with_debug(true)
667        );
668        assert_eq!(
669            TraceRequirements::none().with_state_changes(true).with_verbosity(5),
670            state_changes
671        );
672    }
673
674    #[test]
675    fn config_at_verbosity_0_is_none() {
676        assert!(TraceRequirements::none().with_verbosity(0).into_config().is_none());
677    }
678
679    #[test]
680    fn config_at_verbosity_3_records_calls_only() {
681        let cfg = TraceRequirements::none().with_verbosity(3).into_config().unwrap();
682        assert!(!cfg.record_steps, "verbosity 3 should not record steps");
683        assert!(!cfg.record_state_diff, "verbosity 3 should not record state diff");
684        assert!(cfg.record_logs, "verbosity 3 should record logs");
685    }
686
687    #[test]
688    fn config_at_verbosity_5_records_steps_and_state_diff() {
689        let cfg = TraceRequirements::none().with_verbosity(5).into_config().unwrap();
690        assert!(cfg.record_steps, "verbosity 5 must record steps for backtraces");
691        assert!(cfg.record_state_diff, "verbosity 5 must record state diff");
692        assert!(cfg.record_logs, "verbosity 5 must record logs");
693        // RecordStateDiff should NOT enable expensive debug-level features.
694        assert!(!cfg.record_memory_snapshots, "verbosity 5 should not record memory snapshots");
695        assert_eq!(
696            cfg.record_stack_snapshots,
697            StackSnapshotType::None,
698            "verbosity 5 should not record stack snapshots"
699        );
700        // State diff requires all opcodes to capture SLOAD/SSTORE, so no filter.
701        assert!(
702            cfg.record_opcodes_filter.is_none(),
703            "verbosity 5 needs unfiltered opcodes for state diff"
704        );
705    }
706
707    #[test]
708    fn config_debug_mode_unchanged() {
709        // Debug mode must still enable full recording for the debugger.
710        let cfg = TraceRequirements::none().with_debug(true).into_config().unwrap();
711        assert!(cfg.record_steps);
712        assert!(cfg.record_memory_snapshots, "Debug must record memory snapshots");
713        assert_eq!(
714            cfg.record_stack_snapshots,
715            StackSnapshotType::Full,
716            "Debug must record full stack snapshots"
717        );
718        assert!(cfg.record_returndata_snapshots, "Debug must record returndata");
719        assert!(cfg.record_immediate_bytes, "Debug must record immediate bytes");
720        assert!(cfg.record_opcodes_filter.is_none(), "Debug must record all opcodes (no filter)");
721        assert!(cfg.record_state_diff, "Debug should record storage accesses for the debugger");
722    }
723
724    #[test]
725    fn requirements_preserve_internal_decode_with_state_diff() {
726        let cfg = TraceRequirements::none()
727            .with_decode_internal(InternalTraceMode::Full)
728            .with_state_changes(true)
729            .into_config()
730            .unwrap();
731
732        assert!(cfg.record_steps, "requirements should record opcode steps");
733        assert!(cfg.record_memory_snapshots, "Full internal decoding needs memory snapshots");
734        assert_eq!(
735            cfg.record_stack_snapshots,
736            StackSnapshotType::Full,
737            "internal decoding needs stack snapshots"
738        );
739        assert!(cfg.record_state_diff, "state changes should be recorded");
740        assert!(cfg.record_opcodes_filter.is_none(), "state diff needs unfiltered opcodes");
741    }
742
743    #[test]
744    fn requirements_all_steps_avoid_debug_snapshots() {
745        let cfg =
746            TraceRequirements::none().with_all_steps(true).with_verbosity(5).into_config().unwrap();
747
748        assert!(cfg.record_steps, "all steps must record opcode steps");
749        assert!(cfg.record_opcodes_filter.is_none(), "all steps must record every opcode step");
750        assert!(!cfg.record_memory_snapshots, "all steps should not record memory snapshots");
751        assert_eq!(
752            cfg.record_stack_snapshots,
753            StackSnapshotType::None,
754            "all steps should not record stack snapshots"
755        );
756        assert!(!cfg.record_returndata_snapshots, "all steps should not record returndata");
757        assert!(!cfg.record_immediate_bytes, "all steps should not record immediate bytes");
758        assert!(!cfg.record_state_diff, "all steps should not record state diffs");
759    }
760}