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