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