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 foundry_config::{Chain, Config};
19use foundry_evm_hardforks::{FoundryHardfork, TempoHardfork};
20use foundry_evm_networks::NetworkConfigs;
21use revm::bytecode::opcode::OpCode;
22use revm_inspectors::tracing::OpcodeFilter;
23use serde::{Deserialize, Serialize};
24use std::{
25    borrow::Cow,
26    collections::{BTreeMap, BTreeSet},
27    fmt,
28    ops::{Deref, DerefMut},
29};
30
31use alloy_primitives::{Address, U256, map::HashMap};
32use tempo_contracts::precompiles::TIP20_CHANNEL_RESERVE_ADDRESS;
33
34/// Network context used to identify and decode execution traces.
35#[derive(Clone, Copy, Debug)]
36pub struct TraceContext {
37    chain: Chain,
38    networks: NetworkConfigs,
39    hardfork: Option<FoundryHardfork>,
40}
41
42impl TraceContext {
43    pub const fn new(
44        chain: Chain,
45        networks: NetworkConfigs,
46        hardfork: Option<FoundryHardfork>,
47    ) -> Self {
48        Self { chain, networks, hardfork }
49    }
50
51    pub const fn chain(self) -> Chain {
52        self.chain
53    }
54
55    pub const fn networks(self) -> NetworkConfigs {
56        self.networks
57    }
58
59    pub const fn hardfork(self) -> Option<FoundryHardfork> {
60        self.hardfork
61    }
62
63    pub const fn with_hardfork(mut self, hardfork: Option<FoundryHardfork>) -> Self {
64        self.hardfork = hardfork;
65        self
66    }
67
68    /// Completes metadata for a remotely executed trace before decoding.
69    /// Locally executed traces must use [`Self::hardfork`] directly, including `None`, so a
70    /// configured hardfork cannot replace an explicit execution-spec override.
71    pub fn decoding_hardfork(self, config: &Config) -> Option<FoundryHardfork> {
72        let execution_network = self.networks.execution_network();
73        let mut hardfork = self
74            .hardfork
75            .or(config.hardfork)
76            .filter(|hardfork| hardfork.namespace() == execution_network.hardfork_namespace());
77        if hardfork.is_none() && execution_network.is_tempo() {
78            hardfork = Some(config.evm_spec_id::<TempoHardfork>().into());
79        }
80        #[cfg(feature = "monad")]
81        if hardfork.is_none() && execution_network.is_monad() {
82            hardfork = Some(config.evm_spec_id::<foundry_evm_hardforks::MonadHardfork>().into());
83        }
84        hardfork
85    }
86}
87
88pub use revm_inspectors::tracing::{
89    CallTraceArena, FourByteInspector, GethTraceBuilder, ParityTraceBuilder, StackSnapshotType,
90    TraceWriter, TracingInspector, TracingInspectorConfig,
91    types::{
92        CallKind, CallLog, CallTrace, CallTraceNode, CallTraceStep, DecodedCallData,
93        DecodedCallLog, DecodedCallTrace, DecodedInternalCall, DecodedTraceStep, RecordedMemory,
94        StorageChange, StorageChangeReason, TraceMemberOrder,
95    },
96};
97
98/// Call trace address identifiers.
99///
100/// Identifiers figure out what ABIs and labels belong to all the addresses of the trace.
101pub mod identifier;
102use identifier::LocalTraceIdentifier;
103
104mod decoder;
105pub use decoder::{CallTraceDecoder, CallTraceDecoderBuilder, DecodedEvent};
106
107pub mod debug;
108pub use debug::DebugTraceIdentifier;
109
110pub mod folded_stack_trace;
111
112pub mod backtrace;
113pub mod speedscope;
114
115pub mod erc8021;
116
117pub type Traces = Vec<(TraceKind, SparsedTraceArena)>;
118
119/// Presentation-only detail for an otherwise empty EVM revert.
120#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
121pub enum RevertDiagnostic {
122    /// A call targeted an address without code.
123    CallToNonContract(Address),
124    /// A delegate call targeted an address without code.
125    DelegateCallToNonContract(Address),
126}
127
128impl fmt::Display for RevertDiagnostic {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        match self {
131            Self::CallToNonContract(addr) => write!(f, "call to non-contract address {addr}"),
132            Self::DelegateCallToNonContract(addr) => write!(
133                f,
134                "delegatecall to non-contract address {addr} (usually an unliked library)"
135            ),
136        }
137    }
138}
139
140/// Trace arena keeping track of ignored trace items.
141#[derive(Debug, Clone, Deserialize)]
142pub struct SparsedTraceArena {
143    /// Full trace arena.
144    #[serde(flatten)]
145    pub arena: CallTraceArena,
146    /// Ranges of trace steps to ignore in format (start_node, start_step) -> (end_node, end_step).
147    /// See `foundry_cheatcodes::utils::IgnoredTraces` for more information.
148    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
149    pub ignored: HashMap<(usize, usize), (usize, usize)>,
150    /// Presentation-only revert diagnostics, keyed by trace node index.
151    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
152    pub diagnostics: HashMap<usize, RevertDiagnostic>,
153}
154
155impl Serialize for SparsedTraceArena {
156    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
157    where
158        S: serde::Serializer,
159    {
160        #[derive(Serialize)]
161        struct ResolvedArena<'a> {
162            #[serde(flatten)]
163            arena: &'a CallTraceArena,
164            #[serde(skip_serializing_if = "HashMap::is_empty")]
165            ignored: &'a HashMap<(usize, usize), (usize, usize)>,
166        }
167
168        ResolvedArena { arena: &self.resolve_diagnostics(), ignored: &self.ignored }
169            .serialize(serializer)
170    }
171}
172
173impl SparsedTraceArena {
174    /// Applies presentation-only diagnostics to the provided arena.
175    fn apply_diagnostics(&self, arena: &mut CallTraceArena) {
176        for (&node_idx, diagnostic) in &self.diagnostics {
177            if let Some(node) = arena.nodes_mut().get_mut(node_idx) {
178                node.trace.decoded.get_or_insert_default().return_data =
179                    Some(diagnostic.to_string());
180            }
181        }
182    }
183
184    /// Applies presentation-only diagnostics without consuming ignored trace ranges.
185    fn resolve_diagnostics(&self) -> Cow<'_, CallTraceArena> {
186        if self.diagnostics.is_empty() {
187            Cow::Borrowed(&self.arena)
188        } else {
189            let mut arena = self.arena.clone();
190            self.apply_diagnostics(&mut arena);
191            Cow::Owned(arena)
192        }
193    }
194
195    /// Goes over entire trace arena and removes ignored trace items.
196    fn resolve_arena(&self) -> Cow<'_, CallTraceArena> {
197        if self.ignored.is_empty() {
198            self.resolve_diagnostics()
199        } else {
200            let mut arena = self.arena.clone();
201
202            fn clear_node(
203                nodes: &mut [CallTraceNode],
204                node_idx: usize,
205                ignored: &HashMap<(usize, usize), (usize, usize)>,
206                cur_ignore_end: &mut Option<(usize, usize)>,
207            ) {
208                // Prepend an additional None item to the ordering to handle the beginning of the
209                // trace.
210                let items = std::iter::once(None)
211                    .chain(nodes[node_idx].ordering.clone().into_iter().map(Some))
212                    .enumerate();
213
214                let mut internal_calls = Vec::new();
215                let mut items_to_remove = BTreeSet::new();
216                for (item_idx, item) in items {
217                    if let Some(end_node) = ignored.get(&(node_idx, item_idx)) {
218                        *cur_ignore_end = Some(*end_node);
219                    }
220
221                    let mut remove = cur_ignore_end.is_some() & item.is_some();
222
223                    match item {
224                        // we only remove calls if they did not start/pause tracing
225                        Some(TraceMemberOrder::Call(child_idx)) => {
226                            clear_node(
227                                nodes,
228                                nodes[node_idx].children[child_idx],
229                                ignored,
230                                cur_ignore_end,
231                            );
232                            remove &= cur_ignore_end.is_some();
233                        }
234                        // we only remove decoded internal calls if they did not start/pause tracing
235                        Some(TraceMemberOrder::Step(step_idx)) => {
236                            // If this is an internal call beginning, track it in `internal_calls`
237                            if let Some(decoded) = &nodes[node_idx].trace.steps[step_idx].decoded
238                                && let DecodedTraceStep::InternalCall(_, end_step_idx) = &**decoded
239                            {
240                                internal_calls.push((item_idx, remove, *end_step_idx));
241                                // we decide if we should remove it later
242                                remove = false;
243                            }
244                            // Handle ends of internal calls
245                            internal_calls.retain(|(start_item_idx, remove_start, end_idx)| {
246                                if *end_idx != step_idx {
247                                    return true;
248                                }
249                                // only remove start if end should be removed as well
250                                if *remove_start && remove {
251                                    items_to_remove.insert(*start_item_idx);
252                                } else {
253                                    remove = false;
254                                }
255
256                                false
257                            });
258                        }
259                        _ => {}
260                    }
261
262                    if remove {
263                        items_to_remove.insert(item_idx);
264                    }
265
266                    if let Some((end_node, end_step_idx)) = cur_ignore_end
267                        && node_idx == *end_node
268                        && item_idx == *end_step_idx
269                    {
270                        *cur_ignore_end = None;
271                    }
272                }
273
274                for (offset, item_idx) in items_to_remove.into_iter().enumerate() {
275                    nodes[node_idx].ordering.remove(item_idx - offset - 1);
276                }
277            }
278
279            clear_node(arena.nodes_mut(), 0, &self.ignored, &mut None);
280
281            self.apply_diagnostics(&mut arena);
282
283            Cow::Owned(arena)
284        }
285    }
286}
287
288impl Deref for SparsedTraceArena {
289    type Target = CallTraceArena;
290
291    fn deref(&self) -> &Self::Target {
292        &self.arena
293    }
294}
295
296impl DerefMut for SparsedTraceArena {
297    fn deref_mut(&mut self) -> &mut Self::Target {
298        &mut self.arena
299    }
300}
301
302/// Decode a collection of call traces.
303///
304/// The traces will be decoded using the given decoder, if possible.
305pub async fn decode_trace_arena(arena: &mut CallTraceArena, decoder: &CallTraceDecoder) {
306    decoder.prefetch_signatures(arena.nodes()).await;
307    decoder.populate_traces(arena.nodes_mut()).await;
308}
309
310/// Render a collection of call traces to a string.
311pub fn render_trace_arena(arena: &SparsedTraceArena) -> String {
312    render_trace_arena_inner(arena, false, false)
313}
314
315/// Prunes trace depth if depth is provided as an argument.
316pub fn prune_trace_depth(arena: &mut CallTraceArena, depth: usize) {
317    for node in arena.nodes_mut() {
318        if node.trace.depth >= depth {
319            node.ordering.clear();
320        }
321    }
322}
323
324/// Returns a serializable trace arena containing only nodes visible at `depth`.
325pub fn trace_arena_at_depth(arena: &SparsedTraceArena, depth: usize) -> SparsedTraceArena {
326    let mut arena = arena.resolve_arena().into_owned();
327    let nodes = arena.nodes_mut();
328    let mut reachable = vec![false; nodes.len()];
329    let mut pending = vec![0];
330    while let Some(node_idx) = pending.pop() {
331        if reachable[node_idx] {
332            continue;
333        }
334        reachable[node_idx] = true;
335        let node = &nodes[node_idx];
336        if node.trace.depth < depth {
337            pending.extend(node.ordering.iter().filter_map(|item| match item {
338                TraceMemberOrder::Call(child_idx) => Some(node.children[*child_idx]),
339                _ => None,
340            }));
341        }
342    }
343
344    let mut remapped = vec![None; nodes.len()];
345    for (next_idx, node) in nodes.iter_mut().filter(|node| reachable[node.idx]).enumerate() {
346        remapped[node.idx] = Some(next_idx);
347        if node.trace.depth >= depth {
348            node.ordering.clear();
349            node.children.clear();
350        } else {
351            let mut child_positions = vec![None; node.children.len()];
352            let mut children = Vec::with_capacity(node.children.len());
353            for (old_position, child) in node.children.iter().copied().enumerate() {
354                if reachable[child] {
355                    child_positions[old_position] = Some(children.len());
356                    children.push(child);
357                }
358            }
359            node.children = children;
360            node.ordering = std::mem::take(&mut node.ordering)
361                .into_iter()
362                .filter_map(|item| match item {
363                    TraceMemberOrder::Call(child_idx) => {
364                        Some(TraceMemberOrder::Call(child_positions[child_idx]?))
365                    }
366                    item => Some(item),
367                })
368                .collect();
369        }
370    }
371
372    nodes.retain(|node| reachable[node.idx]);
373    for node in nodes {
374        node.idx = remapped[node.idx].expect("retained trace node has a remapped index");
375        node.parent = node.parent.and_then(|parent| remapped[parent]);
376        for child in &mut node.children {
377            *child = remapped[*child].expect("retained trace child has a remapped index");
378        }
379    }
380
381    SparsedTraceArena { arena, ignored: Default::default(), diagnostics: Default::default() }
382}
383
384/// Render a collection of call traces to a string optionally including contract creation bytecodes
385/// and in JSON format.
386pub fn render_trace_arena_inner(
387    arena: &SparsedTraceArena,
388    with_bytecodes: bool,
389    with_storage_changes: bool,
390) -> String {
391    if shell::is_json() {
392        return serde_json::to_string(&arena.resolve_arena()).expect("Failed to serialize traces");
393    }
394
395    let mut resolved = arena.resolve_arena();
396
397    let mut tempo_changes = None;
398    if with_storage_changes {
399        tempo_changes = tempo_channel_storage_decodes(&resolved);
400
401        let needs_dedup = resolved.as_ref().nodes().iter().any(|node| {
402            node.trace.steps.iter().any(|step| {
403                step.storage_change.is_some()
404                    && matches!(step.decoded.as_deref(), Some(DecodedTraceStep::Line(_)))
405            })
406        });
407        if needs_dedup {
408            // Remove storage text that is already represented by an opcode line.
409            for node in resolved.to_mut().nodes_mut() {
410                for step in &mut node.trace.steps {
411                    if step.storage_change.is_some()
412                        && matches!(step.decoded.as_deref(), Some(DecodedTraceStep::Line(_)))
413                    {
414                        step.storage_change = None;
415                    }
416                }
417            }
418        }
419    }
420
421    let mut w = TraceWriter::new(Vec::<u8>::new())
422        .color_cheatcodes(true)
423        .use_colors(convert_color_choice(shell::color_choice()))
424        .write_bytecodes(with_bytecodes)
425        .with_storage_changes(with_storage_changes);
426    w.write_arena(resolved.as_ref()).expect("Failed to write traces");
427    let mut rendered =
428        String::from_utf8(w.into_writer()).expect("trace writer wrote invalid UTF-8");
429    if let Some(tempo_changes) = tempo_changes {
430        if !rendered.ends_with('\n') {
431            rendered.push('\n');
432        }
433        rendered.push_str(&tempo_changes);
434    }
435
436    rendered
437}
438
439fn tempo_channel_storage_decodes(arena: &CallTraceArena) -> Option<String> {
440    let decoded_changes = arena
441        .nodes()
442        .iter()
443        .filter(|node| node.trace.address == TIP20_CHANNEL_RESERVE_ADDRESS)
444        .flat_map(compact_channel_storage_changes)
445        .collect::<Vec<_>>();
446
447    if decoded_changes.is_empty() {
448        return None;
449    }
450
451    let mut rendered = String::new();
452    rendered.push_str("Decoded TIP20ChannelReserve storage:\n");
453    for (slot, before, after) in decoded_changes {
454        rendered.push_str(&format!(
455            "  @ {}: {} -> {}\n",
456            format_storage_word(slot),
457            format_channel_state(before),
458            format_channel_state(after),
459        ));
460    }
461    Some(rendered)
462}
463
464fn compact_channel_storage_changes(node: &CallTraceNode) -> Vec<(U256, U256, U256)> {
465    let mut changes_map = BTreeMap::new();
466    for step in &node.trace.steps {
467        if let Some(change) = &step.storage_change
468            && change.had_value.is_some()
469        {
470            let (_first, last) = changes_map.entry(change.key).or_insert((&**change, &**change));
471            *last = &**change;
472        }
473    }
474
475    changes_map
476        .into_iter()
477        .filter_map(|(key, (first, last))| {
478            let before = first.had_value.unwrap_or_default();
479            let after = last.value;
480            (before != after).then_some((key, before, after))
481        })
482        .collect()
483}
484
485fn format_channel_state(value: U256) -> String {
486    let (settled, deposit, close_requested_at) = decode_channel_state(value);
487    format!("{{settled: {settled}, deposit: {deposit}, closeRequestedAt: {close_requested_at}}}")
488}
489
490fn decode_channel_state(value: U256) -> (U256, U256, u32) {
491    let mask96 = (U256::ONE << 96) - U256::ONE;
492    let mask32 = (U256::ONE << 32) - U256::ONE;
493    let settled: U256 = value & mask96;
494    let deposit: U256 = (value >> 96usize) & mask96;
495    let close_requested_at_word: U256 = (value >> 192usize) & mask32;
496    let close_requested_at = close_requested_at_word.to::<u32>();
497    (settled, deposit, close_requested_at)
498}
499
500fn format_storage_word(value: U256) -> String {
501    if value < U256::from(1_000_000u64) { value.to_string() } else { format!("0x{value:x}") }
502}
503
504const fn convert_color_choice(choice: shell::ColorChoice) -> revm_inspectors::ColorChoice {
505    match choice {
506        shell::ColorChoice::Auto => revm_inspectors::ColorChoice::Auto,
507        shell::ColorChoice::Always => revm_inspectors::ColorChoice::Always,
508        shell::ColorChoice::Never => revm_inspectors::ColorChoice::Never,
509    }
510}
511
512/// Specifies the kind of trace.
513#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
514pub enum TraceKind {
515    Deployment,
516    Setup,
517    Execution,
518}
519
520impl TraceKind {
521    /// Returns `true` if the trace kind is [`Deployment`].
522    ///
523    /// [`Deployment`]: TraceKind::Deployment
524    #[must_use]
525    pub const fn is_deployment(self) -> bool {
526        matches!(self, Self::Deployment)
527    }
528
529    /// Returns `true` if the trace kind is [`Setup`].
530    ///
531    /// [`Setup`]: TraceKind::Setup
532    #[must_use]
533    pub const fn is_setup(self) -> bool {
534        matches!(self, Self::Setup)
535    }
536
537    /// Returns `true` if the trace kind is [`Execution`].
538    ///
539    /// [`Execution`]: TraceKind::Execution
540    #[must_use]
541    pub const fn is_execution(self) -> bool {
542        matches!(self, Self::Execution)
543    }
544}
545
546/// Given a list of traces and artifacts, it returns a map connecting address to abi
547pub fn load_contracts<'a>(
548    traces: impl IntoIterator<Item = &'a CallTraceArena>,
549    known_contracts: &ContractsByArtifact,
550) -> ContractsByAddress {
551    let mut local_identifier = LocalTraceIdentifier::new(known_contracts);
552    let decoder = CallTraceDecoder::new();
553    let mut contracts = ContractsByAddress::new();
554    for trace in traces {
555        for address in decoder.identify_addresses(trace, &mut local_identifier) {
556            if let (Some(contract), Some(abi)) = (address.contract, address.abi) {
557                contracts.insert(address.address, (contract, abi.into_owned()));
558            }
559        }
560    }
561    contracts
562}
563
564/// Different kinds of internal functions tracing.
565#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
566pub enum InternalTraceMode {
567    #[default]
568    None,
569    /// Traces internal functions without decoding inputs/outputs from memory.
570    Simple,
571    /// Same as `Simple`, but also tracks memory snapshots.
572    Full,
573}
574
575/// Opcode step recording granularity.
576#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
577pub enum StepRecording {
578    /// No opcode steps.
579    #[default]
580    None,
581    /// Record only JUMP/JUMPDEST steps.
582    Jumps,
583    /// Record all opcode steps.
584    All,
585}
586
587impl StepRecording {
588    const fn merge(self, other: Self) -> Self {
589        match (self, other) {
590            (Self::All, _) | (_, Self::All) => Self::All,
591            (Self::Jumps, _) | (_, Self::Jumps) => Self::Jumps,
592            (Self::None, Self::None) => Self::None,
593        }
594    }
595}
596
597/// Trace data requirements composed across independent feature axes.
598#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
599pub struct TraceRequirements {
600    calls: bool,
601    steps: StepRecording,
602    memory_snapshots: bool,
603    stack_snapshots: bool,
604    returndata_snapshots: bool,
605    immediate_bytes: bool,
606    state_diff: bool,
607    bytecode: bool,
608}
609
610impl TraceRequirements {
611    pub const fn none() -> Self {
612        Self {
613            calls: false,
614            steps: StepRecording::None,
615            memory_snapshots: false,
616            stack_snapshots: false,
617            returndata_snapshots: false,
618            immediate_bytes: false,
619            state_diff: false,
620            bytecode: false,
621        }
622    }
623
624    pub const fn with_calls(mut self, yes: bool) -> Self {
625        self.calls |= yes;
626        self
627    }
628
629    pub const fn merge(mut self, other: Self) -> Self {
630        self.calls |= other.calls;
631        self.steps = self.steps.merge(other.steps);
632        self.memory_snapshots |= other.memory_snapshots;
633        self.stack_snapshots |= other.stack_snapshots;
634        self.returndata_snapshots |= other.returndata_snapshots;
635        self.immediate_bytes |= other.immediate_bytes;
636        self.state_diff |= other.state_diff;
637        self.bytecode |= other.bytecode;
638        self
639    }
640
641    pub const fn with_steps(mut self, steps: StepRecording) -> Self {
642        self.steps = self.steps.merge(steps);
643        self
644    }
645
646    pub const fn with_memory_snapshots(mut self, yes: bool) -> Self {
647        self.memory_snapshots |= yes;
648        self
649    }
650
651    pub const fn with_stack_snapshots(mut self, yes: bool) -> Self {
652        self.stack_snapshots |= yes;
653        self
654    }
655
656    pub const fn with_debug(mut self, yes: bool) -> Self {
657        if yes {
658            self.calls = true;
659            self.steps = StepRecording::All;
660            self.memory_snapshots = true;
661            self.stack_snapshots = true;
662            self.returndata_snapshots = true;
663            self.immediate_bytes = true;
664            self.state_diff = true;
665            self.bytecode = true;
666        }
667        self
668    }
669
670    pub const fn with_decode_internal(self, mode: InternalTraceMode) -> Self {
671        match mode {
672            InternalTraceMode::None => self,
673            InternalTraceMode::Simple => {
674                self.with_calls(true).with_steps(StepRecording::Jumps).with_stack_snapshots(true)
675            }
676            InternalTraceMode::Full => self
677                .with_calls(true)
678                .with_steps(StepRecording::Jumps)
679                .with_memory_snapshots(true)
680                .with_stack_snapshots(true),
681        }
682    }
683
684    pub const fn with_all_steps(self, yes: bool) -> Self {
685        if yes { self.with_calls(true).with_steps(StepRecording::All) } else { self }
686    }
687
688    pub const fn with_state_changes(mut self, yes: bool) -> Self {
689        self.state_diff |= yes;
690        if yes {
691            self.calls = true;
692        }
693        self
694    }
695
696    pub const fn with_verbosity(self, verbosity: u8) -> Self {
697        match verbosity {
698            0..3 => self,
699            3..=4 => self.with_calls(true),
700            _ if matches!(self.steps, StepRecording::All) => self.with_calls(true),
701            _ => self.with_state_changes(true),
702        }
703    }
704
705    pub fn into_config(self) -> Option<TracingInspectorConfig> {
706        if !self.calls && self.steps == StepRecording::None && !self.state_diff {
707            return None;
708        }
709
710        let steps = if self.state_diff { StepRecording::All } else { self.steps };
711        TracingInspectorConfig {
712            record_steps: steps != StepRecording::None,
713            record_inputs: true,
714            record_bytecode: self.bytecode,
715            step_limit: None,
716            record_memory_snapshots: self.memory_snapshots,
717            record_stack_snapshots: if self.stack_snapshots {
718                StackSnapshotType::Full
719            } else {
720                StackSnapshotType::None
721            },
722            record_logs: true,
723            record_state_diff: self.state_diff,
724            record_returndata_snapshots: self.returndata_snapshots,
725            record_opcodes_filter: match steps {
726                StepRecording::None | StepRecording::All => None,
727                StepRecording::Jumps => {
728                    Some(OpcodeFilter::new().enabled(OpCode::JUMP).enabled(OpCode::JUMPDEST))
729                }
730            },
731            exclude_precompile_calls: false,
732            record_immediate_bytes: self.immediate_bytes,
733            record_step_deltas: false,
734        }
735        .into()
736    }
737}
738
739#[cfg(test)]
740mod tests {
741    use super::*;
742    use alloy_primitives::Bytes;
743    use revm::interpreter::InstructionResult;
744
745    #[test]
746    fn trace_context_uses_the_execution_network_hardfork_namespace() {
747        let config = Config {
748            hardfork: Some(FoundryHardfork::Ethereum(
749                foundry_evm_hardforks::EthereumHardfork::Cancun,
750            )),
751            ..Default::default()
752        };
753        let context = TraceContext::new(Chain::tempo_mainnet(), NetworkConfigs::with_tempo(), None);
754
755        assert!(matches!(context.decoding_hardfork(&config), Some(FoundryHardfork::Tempo(_))));
756    }
757
758    #[test]
759    fn trace_depth_projection_removes_and_reindexes_nodes() {
760        let mut arena = CallTraceArena::default();
761        arena.nodes_mut().extend([
762            CallTraceNode {
763                parent: Some(0),
764                children: vec![2],
765                idx: 1,
766                trace: CallTrace { depth: 1, ..Default::default() },
767                ordering: vec![TraceMemberOrder::Call(0)],
768                ..Default::default()
769            },
770            CallTraceNode {
771                parent: Some(1),
772                idx: 2,
773                trace: CallTrace { depth: 2, ..Default::default() },
774                ..Default::default()
775            },
776            CallTraceNode {
777                parent: Some(0),
778                idx: 3,
779                trace: CallTrace { depth: 1, ..Default::default() },
780                ..Default::default()
781            },
782        ]);
783        let root = &mut arena.nodes_mut()[0];
784        root.children = vec![1, 3];
785        root.ordering = vec![TraceMemberOrder::Call(0), TraceMemberOrder::Call(1)];
786        let arena = SparsedTraceArena {
787            arena,
788            ignored: Default::default(),
789            diagnostics: Default::default(),
790        };
791
792        let arena = trace_arena_at_depth(&arena, 1);
793
794        assert_eq!(arena.nodes().len(), 3);
795        assert_eq!(arena.nodes()[0].children, [1, 2]);
796        assert_eq!(arena.nodes()[1].idx, 1);
797        assert!(arena.nodes()[1].children.is_empty());
798        assert!(arena.nodes()[1].ordering.is_empty());
799        assert_eq!(arena.nodes()[2].idx, 2);
800        assert_eq!(arena.nodes()[2].parent, Some(0));
801        assert!(arena.ignored.is_empty());
802    }
803
804    #[test]
805    fn decodes_tip1034_packed_channel_state() {
806        let settled = U256::from(123u64);
807        let deposit = U256::from(456u64);
808        let close_requested_at = U256::from(1_780_495_200u64);
809        let packed = settled | (deposit << 96usize) | (close_requested_at << 192usize);
810
811        assert_eq!(decode_channel_state(packed), (settled, deposit, 1_780_495_200));
812        assert_eq!(
813            format_channel_state(packed),
814            "{settled: 123, deposit: 456, closeRequestedAt: 1780495200}"
815        );
816    }
817
818    #[test]
819    fn tempo_storage_decodes_do_not_insert_extra_blank_line() {
820        let mut arena = CallTraceArena::default();
821        let root = &mut arena.nodes_mut()[0];
822        root.ordering.push(TraceMemberOrder::Step(0));
823        root.trace = CallTrace {
824            address: TIP20_CHANNEL_RESERVE_ADDRESS,
825            success: true,
826            steps: vec![CallTraceStep {
827                pc: 0,
828                op: OpCode::SSTORE,
829                stack: None,
830                push_stack: None,
831                memory: None,
832                returndata: Bytes::new(),
833                gas_remaining: 0,
834                gas_refund_counter: 0,
835                gas_used: 0,
836                gas_cost: 0,
837                state_gas_cost: None,
838                state_gas_reservoir: None,
839                state_gas_spent: 0,
840                storage_change: Some(Box::new(StorageChange {
841                    key: U256::ONE,
842                    value: U256::from(2),
843                    had_value: Some(U256::ONE),
844                    reason: StorageChangeReason::SSTORE,
845                })),
846                status: Some(InstructionResult::Stop),
847                immediate_bytes: None,
848                decoded: None,
849            }],
850            ..Default::default()
851        };
852
853        let rendered = render_trace_arena_inner(
854            &SparsedTraceArena {
855                arena,
856                ignored: Default::default(),
857                diagnostics: Default::default(),
858            },
859            false,
860            true,
861        );
862
863        assert!(rendered.contains("\nDecoded TIP20ChannelReserve storage:\n"));
864        assert!(!rendered.contains("\n\nDecoded TIP20ChannelReserve storage:\n"));
865    }
866
867    #[test]
868    fn revert_diagnostic_only_changes_resolved_trace() {
869        let traces = SparsedTraceArena {
870            arena: CallTraceArena::default(),
871            ignored: Default::default(),
872            diagnostics: HashMap::from_iter([(
873                0,
874                RevertDiagnostic::CallToNonContract(alloy_primitives::Address::ZERO),
875            )]),
876        };
877
878        let resolved = traces.resolve_arena();
879        assert_eq!(
880            resolved.nodes()[0].trace.decoded.as_ref().unwrap().return_data.as_deref(),
881            Some("call to non-contract address 0x0000000000000000000000000000000000000000")
882        );
883        assert!(resolved.nodes()[0].trace.output.is_empty());
884        assert!(traces.arena.nodes()[0].trace.decoded.is_none());
885        assert!(traces.arena.nodes()[0].trace.output.is_empty());
886    }
887
888    #[test]
889    fn serialization_resolves_diagnostics_without_consuming_ignored_ranges() {
890        let mut arena = CallTraceArena::default();
891        let root = &mut arena.nodes_mut()[0];
892        root.logs = vec![CallLog::default(), CallLog::default(), CallLog::default()];
893        root.ordering =
894            vec![TraceMemberOrder::Log(0), TraceMemberOrder::Log(1), TraceMemberOrder::Log(2)];
895
896        let traces = SparsedTraceArena {
897            arena,
898            ignored: HashMap::from_iter([((0, 1), (0, 2))]),
899            diagnostics: HashMap::from_iter([(
900                0,
901                RevertDiagnostic::CallToNonContract(alloy_primitives::Address::ZERO),
902            )]),
903        };
904
905        let serialized = ron::to_string(&traces).unwrap();
906        let deserialized: SparsedTraceArena = ron::from_str(&serialized).unwrap();
907        assert_eq!(
908            deserialized.arena.nodes()[0].ordering,
909            [TraceMemberOrder::Log(0), TraceMemberOrder::Log(1), TraceMemberOrder::Log(2)]
910        );
911        assert_eq!(deserialized.ignored, traces.ignored);
912        assert!(deserialized.diagnostics.is_empty());
913        assert_eq!(
914            deserialized.arena.nodes()[0].trace.decoded.as_ref().unwrap().return_data.as_deref(),
915            Some("call to non-contract address 0x0000000000000000000000000000000000000000")
916        );
917        assert!(deserialized.arena.nodes()[0].trace.output.is_empty());
918
919        let resolved = deserialized.resolve_arena();
920        assert_eq!(resolved.nodes()[0].ordering, [TraceMemberOrder::Log(2)]);
921        assert_eq!(
922            resolved.nodes()[0].trace.decoded.as_ref().unwrap().return_data.as_deref(),
923            Some("call to non-contract address 0x0000000000000000000000000000000000000000")
924        );
925        assert!(render_trace_arena(&deserialized).contains("call to non-contract address"));
926    }
927
928    #[test]
929    fn verbosity_0_through_2_is_noop() {
930        for v in 0..=2 {
931            assert_eq!(
932                TraceRequirements::none().with_verbosity(v),
933                TraceRequirements::none(),
934                "v={v}"
935            );
936            assert_eq!(
937                TraceRequirements::none().with_calls(true).with_verbosity(v),
938                TraceRequirements::none().with_calls(true),
939                "v={v}"
940            );
941            assert_eq!(
942                TraceRequirements::none().with_debug(true).with_verbosity(v),
943                TraceRequirements::none().with_debug(true),
944                "v={v}"
945            );
946        }
947    }
948
949    #[test]
950    fn verbosity_3_and_4_raises_to_call() {
951        for v in 3..=4 {
952            assert_eq!(
953                TraceRequirements::none().with_verbosity(v),
954                TraceRequirements::none().with_calls(true),
955                "v={v}"
956            );
957            assert_eq!(
958                TraceRequirements::none().with_debug(true).with_verbosity(v),
959                TraceRequirements::none().with_debug(true),
960                "v={v}"
961            );
962            assert_eq!(
963                TraceRequirements::none().with_state_changes(true).with_verbosity(v),
964                TraceRequirements::none().with_state_changes(true),
965                "v={v}"
966            );
967        }
968    }
969
970    #[test]
971    fn verbosity_5_raises_to_record_state_diff() {
972        let state_changes = TraceRequirements::none().with_state_changes(true);
973
974        assert_eq!(TraceRequirements::none().with_verbosity(5), state_changes);
975        assert_eq!(TraceRequirements::none().with_calls(true).with_verbosity(5), state_changes);
976        let cfg = TraceRequirements::none()
977            .with_calls(true)
978            .with_steps(StepRecording::Jumps)
979            .with_verbosity(5)
980            .into_config()
981            .unwrap();
982        assert!(cfg.record_state_diff);
983        assert!(cfg.record_opcodes_filter.is_none());
984        assert_eq!(
985            TraceRequirements::none().with_debug(true).with_verbosity(5),
986            TraceRequirements::none().with_debug(true)
987        );
988        assert_eq!(
989            TraceRequirements::none().with_state_changes(true).with_verbosity(5),
990            state_changes
991        );
992    }
993
994    #[test]
995    fn config_at_verbosity_0_is_none() {
996        assert!(TraceRequirements::none().with_verbosity(0).into_config().is_none());
997    }
998
999    #[test]
1000    fn config_at_verbosity_3_records_calls_only() {
1001        let cfg = TraceRequirements::none().with_verbosity(3).into_config().unwrap();
1002        assert!(!cfg.record_steps, "verbosity 3 should not record steps");
1003        assert!(!cfg.record_state_diff, "verbosity 3 should not record state diff");
1004        assert!(cfg.record_logs, "verbosity 3 should record logs");
1005    }
1006
1007    #[test]
1008    fn config_at_verbosity_5_records_steps_and_state_diff() {
1009        let cfg = TraceRequirements::none().with_verbosity(5).into_config().unwrap();
1010        assert!(cfg.record_steps, "verbosity 5 must record steps for backtraces");
1011        assert!(cfg.record_state_diff, "verbosity 5 must record state diff");
1012        assert!(cfg.record_logs, "verbosity 5 must record logs");
1013        // RecordStateDiff should NOT enable expensive debug-level features.
1014        assert!(!cfg.record_memory_snapshots, "verbosity 5 should not record memory snapshots");
1015        assert_eq!(
1016            cfg.record_stack_snapshots,
1017            StackSnapshotType::None,
1018            "verbosity 5 should not record stack snapshots"
1019        );
1020        // State diff requires all opcodes to capture SLOAD/SSTORE, so no filter.
1021        assert!(
1022            cfg.record_opcodes_filter.is_none(),
1023            "verbosity 5 needs unfiltered opcodes for state diff"
1024        );
1025    }
1026
1027    #[test]
1028    fn config_debug_mode_unchanged() {
1029        // Debug mode must still enable full recording for the debugger.
1030        let cfg = TraceRequirements::none().with_debug(true).into_config().unwrap();
1031        assert!(cfg.record_steps);
1032        assert!(cfg.record_memory_snapshots, "Debug must record memory snapshots");
1033        assert_eq!(
1034            cfg.record_stack_snapshots,
1035            StackSnapshotType::Full,
1036            "Debug must record full stack snapshots"
1037        );
1038        assert!(cfg.record_returndata_snapshots, "Debug must record returndata");
1039        assert!(cfg.record_immediate_bytes, "Debug must record immediate bytes");
1040        assert!(cfg.record_opcodes_filter.is_none(), "Debug must record all opcodes (no filter)");
1041        assert!(cfg.record_state_diff, "Debug should record storage accesses for the debugger");
1042    }
1043
1044    #[test]
1045    fn requirements_preserve_internal_decode_with_state_diff() {
1046        let cfg = TraceRequirements::none()
1047            .with_decode_internal(InternalTraceMode::Full)
1048            .with_state_changes(true)
1049            .into_config()
1050            .unwrap();
1051
1052        assert!(cfg.record_steps, "requirements should record opcode steps");
1053        assert!(cfg.record_memory_snapshots, "Full internal decoding needs memory snapshots");
1054        assert_eq!(
1055            cfg.record_stack_snapshots,
1056            StackSnapshotType::Full,
1057            "internal decoding needs stack snapshots"
1058        );
1059        assert!(cfg.record_state_diff, "state changes should be recorded");
1060        assert!(cfg.record_opcodes_filter.is_none(), "state diff needs unfiltered opcodes");
1061    }
1062
1063    #[test]
1064    fn requirements_all_steps_avoid_debug_snapshots() {
1065        let cfg =
1066            TraceRequirements::none().with_all_steps(true).with_verbosity(5).into_config().unwrap();
1067
1068        assert!(cfg.record_steps, "all steps must record opcode steps");
1069        assert!(cfg.record_opcodes_filter.is_none(), "all steps must record every opcode step");
1070        assert!(!cfg.record_memory_snapshots, "all steps should not record memory snapshots");
1071        assert_eq!(
1072            cfg.record_stack_snapshots,
1073            StackSnapshotType::None,
1074            "all steps should not record stack snapshots"
1075        );
1076        assert!(!cfg.record_returndata_snapshots, "all steps should not record returndata");
1077        assert!(!cfg.record_immediate_bytes, "all steps should not record immediate bytes");
1078        assert!(!cfg.record_state_diff, "all steps should not record state diffs");
1079    }
1080}