Skip to main content

foundry_evm_traces/
lib.rs

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