Skip to main content

foundry_debugger/
node.rs

1use alloy_primitives::{Address, Bytes, hex, map::AddressHashMap};
2use foundry_evm_core::precompiles;
3use foundry_evm_traces::{CallKind, CallTrace, CallTraceArena};
4use revm::bytecode::opcode::OpCode;
5use revm_inspectors::tracing::types::{
6    CallTraceStep, DecodedCallTrace, DecodedTraceStep, TraceMemberOrder,
7};
8use serde::{Deserialize, Serialize};
9
10const PRECOMPILES_TRACE_LABEL: &str = "PRECOMPILES";
11
12/// Represents a part of the execution frame before the next call or end of the execution.
13#[derive(Clone, Debug, Default, Serialize, Deserialize)]
14pub struct DebugNode {
15    /// Execution context.
16    ///
17    /// Note that this is the address of the *code*, not necessarily the address of the storage.
18    pub address: Address,
19    /// The kind of call this is.
20    pub kind: CallKind,
21    /// Calldata of the call.
22    pub calldata: Bytes,
23    /// Return data produced by the call.
24    #[serde(default)]
25    pub returndata: Bytes,
26    /// The gas limit of the call.
27    pub gas_limit: u64,
28    /// Stable id for the original call trace node within the flattened debugger arena.
29    #[serde(default)]
30    pub trace_node_idx: usize,
31    /// Index of the first step in the original call trace node.
32    #[serde(default)]
33    pub step_offset: usize,
34    /// Decoded call data for the current execution context, if available.
35    #[serde(default)]
36    pub decoded: Option<Box<DecodedCallTrace>>,
37    /// The debug steps.
38    pub steps: Vec<CallTraceStep>,
39}
40
41impl DebugNode {
42    /// Creates a new debug node.
43    pub const fn new(
44        address: Address,
45        kind: CallKind,
46        steps: Vec<CallTraceStep>,
47        calldata: Bytes,
48        gas_limit: u64,
49        decoded: Option<Box<DecodedCallTrace>>,
50    ) -> Self {
51        Self {
52            address,
53            kind,
54            steps,
55            calldata,
56            returndata: Bytes::new(),
57            gas_limit,
58            trace_node_idx: 0,
59            step_offset: 0,
60            decoded,
61        }
62    }
63}
64
65/// Flattens given [CallTraceArena] into a list of [DebugNode]s.
66///
67/// This is done by recursively traversing the call tree and collecting the steps in-between the
68/// calls.
69#[cfg(test)]
70fn flatten_call_trace(arena: CallTraceArena, out: &mut Vec<DebugNode>) {
71    flatten_call_trace_with_precompiles(arena, out, &AddressHashMap::default());
72}
73
74/// Flattens given [CallTraceArena] into a list of [DebugNode]s using active precompile labels.
75pub fn flatten_call_trace_with_precompiles(
76    arena: CallTraceArena,
77    out: &mut Vec<DebugNode>,
78    precompile_labels: &AddressHashMap<String>,
79) {
80    #[derive(Debug, Clone, Copy)]
81    struct PendingNode {
82        node_idx: usize,
83        steps_count: usize,
84        step_offset: usize,
85    }
86
87    fn inner(
88        arena: &CallTraceArena,
89        node_idx: usize,
90        out: &mut Vec<PendingNode>,
91        step_notices: &mut Vec<(usize, usize, String)>,
92        precompile_labels: &AddressHashMap<String>,
93    ) {
94        let mut pending = PendingNode { node_idx, steps_count: 0, step_offset: 0 };
95        let mut next_step_offset = 0;
96        let mut last_step_idx: Option<usize> = None;
97        let node = &arena.nodes()[node_idx];
98        for order in &node.ordering {
99            match order {
100                TraceMemberOrder::Call(idx) => {
101                    let child_idx = node.children[*idx];
102                    if let Some(step_idx) = last_step_idx.take()
103                        && let Some(step) = node.trace.steps.get(step_idx)
104                        && is_call_like_op(step.op)
105                        && let Some(notice) = precompile_call_notice(
106                            &arena.nodes()[child_idx].trace,
107                            precompile_labels,
108                        )
109                    {
110                        step_notices.push((node_idx, step_idx, notice));
111                    }
112                    out.push(pending);
113                    pending =
114                        PendingNode { node_idx, steps_count: 0, step_offset: next_step_offset };
115                    inner(arena, child_idx, out, step_notices, precompile_labels);
116                }
117                TraceMemberOrder::Step(step_idx) => {
118                    if pending.steps_count == 0 {
119                        pending.step_offset = *step_idx;
120                    }
121                    pending.steps_count += 1;
122                    next_step_offset = step_idx.saturating_add(1);
123                    last_step_idx = Some(*step_idx);
124                }
125                _ => {
126                    last_step_idx = None;
127                }
128            }
129        }
130        out.push(pending);
131    }
132    let mut nodes = Vec::new();
133    let mut step_notices = Vec::new();
134    inner(&arena, 0, &mut nodes, &mut step_notices, precompile_labels);
135
136    let mut arena_nodes = arena.into_nodes();
137    for (node_idx, step_idx, notice) in step_notices {
138        if let Some(step) =
139            arena_nodes.get_mut(node_idx).and_then(|node| node.trace.steps.get_mut(step_idx))
140        {
141            set_step_notice(step, notice);
142        }
143    }
144
145    let trace_node_idx_offset =
146        out.iter().map(|node| node.trace_node_idx).max().map_or(0, |idx| idx.saturating_add(1));
147
148    for pending in nodes {
149        let steps = {
150            let other_steps =
151                arena_nodes[pending.node_idx].trace.steps.split_off(pending.steps_count);
152            std::mem::replace(&mut arena_nodes[pending.node_idx].trace.steps, other_steps)
153        };
154
155        // Skip nodes with empty steps as there's nothing to display for them.
156        if steps.is_empty() {
157            continue;
158        }
159
160        let call = &arena_nodes[pending.node_idx].trace;
161        let calldata = if call.kind.is_any_create() { Bytes::new() } else { call.data.clone() };
162        let mut node = DebugNode::new(
163            call.address,
164            call.kind,
165            steps,
166            calldata,
167            call.gas_limit,
168            call.decoded.clone(),
169        );
170        node.returndata = call.output.clone();
171        node.trace_node_idx = trace_node_idx_offset.saturating_add(pending.node_idx);
172        node.step_offset = pending.step_offset;
173
174        out.push(node);
175    }
176}
177
178fn set_step_notice(step: &mut CallTraceStep, notice: String) {
179    match step.decoded.as_deref_mut() {
180        None => step.decoded = Some(Box::new(DecodedTraceStep::Line(notice))),
181        Some(DecodedTraceStep::Line(line)) if line.is_empty() => *line = notice,
182        Some(_) => {}
183    }
184}
185
186const fn is_call_like_op(op: OpCode) -> bool {
187    matches!(
188        op,
189        OpCode::CALL
190            | OpCode::STATICCALL
191            | OpCode::DELEGATECALL
192            | OpCode::CALLCODE
193            | OpCode::CREATE
194            | OpCode::CREATE2
195    )
196}
197
198fn precompile_call_notice(
199    trace: &CallTrace,
200    precompile_labels: &AddressHashMap<String>,
201) -> Option<String> {
202    decoded_precompile_call_notice(&trace.decoded)
203        .or_else(|| labeled_precompile_call_notice(trace, precompile_labels))
204        .or_else(|| known_precompile_call_notice(trace))
205}
206
207fn decoded_precompile_call_notice(decoded: &Option<Box<DecodedCallTrace>>) -> Option<String> {
208    let decoded = decoded.as_ref()?;
209    let label = decoded.label.as_deref()?;
210    if label != PRECOMPILES_TRACE_LABEL {
211        return None;
212    }
213
214    Some(decoded_call_notice(label, decoded))
215}
216
217fn decoded_call_notice(label: &str, decoded: &DecodedCallTrace) -> String {
218    let Some(call_data) = &decoded.call_data else {
219        return format!("precompile: {label}");
220    };
221    let args = call_data.args.join(", ");
222    let function_name =
223        call_data.signature.split_once('(').map_or(call_data.signature.as_str(), |(name, _)| name);
224    let mut notice = format!("precompile: {label}::{function_name}({args})");
225    if let Some(return_data) = decoded.return_data.as_deref() {
226        notice.push_str(" -> ");
227        notice.push_str(return_data);
228    }
229    notice
230}
231
232fn labeled_precompile_call_notice(
233    trace: &CallTrace,
234    precompile_labels: &AddressHashMap<String>,
235) -> Option<String> {
236    let label = precompile_labels.get(&trace.address)?;
237    if let Some(decoded) = trace.decoded.as_ref() {
238        return Some(decoded_call_notice(
239            decoded.label.as_deref().unwrap_or(label.as_str()),
240            decoded,
241        ));
242    }
243
244    let mut notice = format!("precompile: {label} @ {}", trace.address);
245    append_raw_precompile_io(&mut notice, trace);
246    Some(notice)
247}
248
249fn known_precompile_call_notice(trace: &CallTrace) -> Option<String> {
250    if trace.address == precompiles::P256_VERIFY && trace.maybe_precompile != Some(true) {
251        return None;
252    }
253    let name = known_precompile_name(trace.address)?;
254    let mut notice = format!("precompile: {name} @ {}", trace.address);
255    append_raw_precompile_io(&mut notice, trace);
256    Some(notice)
257}
258
259fn append_raw_precompile_io(notice: &mut String, trace: &CallTrace) {
260    if !trace.data.is_empty() {
261        notice.push_str(" input=");
262        notice.push_str(&hex::encode_prefixed(&trace.data));
263    }
264    if !trace.output.is_empty() {
265        notice.push_str(" output=");
266        notice.push_str(&hex::encode_prefixed(&trace.output));
267    }
268}
269
270// Standard EVM fallback for traces that have no decoded precompile metadata.
271const fn known_precompile_name(address: Address) -> Option<&'static str> {
272    match address {
273        precompiles::EC_RECOVER => Some("ecrecover"),
274        precompiles::SHA_256 => Some("sha256"),
275        precompiles::RIPEMD_160 => Some("ripemd"),
276        precompiles::IDENTITY => Some("identity"),
277        precompiles::MOD_EXP => Some("modexp"),
278        precompiles::EC_ADD => Some("ecadd"),
279        precompiles::EC_MUL => Some("ecmul"),
280        precompiles::EC_PAIRING => Some("ecpairing"),
281        precompiles::BLAKE_2F => Some("blake2f"),
282        precompiles::POINT_EVALUATION => Some("pointEvaluation"),
283        precompiles::BLS12_G1ADD => Some("bls12G1Add"),
284        precompiles::BLS12_G1MSM => Some("bls12G1Msm"),
285        precompiles::BLS12_G2ADD => Some("bls12G2Add"),
286        precompiles::BLS12_G2MSM => Some("bls12G2Msm"),
287        precompiles::BLS12_PAIRING_CHECK => Some("bls12PairingCheck"),
288        precompiles::BLS12_MAP_FP_TO_G1 => Some("bls12MapFpToG1"),
289        precompiles::BLS12_MAP_FP2_TO_G2 => Some("bls12MapFp2ToG2"),
290        precompiles::P256_VERIFY => Some("p256Verify"),
291        _ => None,
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298    use foundry_evm_traces::CallTraceNode;
299    use revm::interpreter::InstructionResult;
300    use revm_inspectors::tracing::types::{DecodedCallData, DecodedInternalCall};
301
302    fn step(pc: usize) -> CallTraceStep {
303        CallTraceStep {
304            pc,
305            op: OpCode::STOP,
306            stack: None,
307            push_stack: None,
308            memory: None,
309            returndata: Bytes::new(),
310            gas_remaining: 0,
311            gas_refund_counter: 0,
312            gas_used: 0,
313            gas_cost: 0,
314            state_gas_cost: None,
315            state_gas_reservoir: None,
316            state_gas_spent: 0,
317            storage_change: None,
318            status: Some(InstructionResult::Stop),
319            immediate_bytes: None,
320            decoded: None,
321        }
322    }
323
324    fn step_with_op(pc: usize, op: OpCode) -> CallTraceStep {
325        CallTraceStep { op, ..step(pc) }
326    }
327
328    fn known_sha256_precompile_node() -> CallTraceNode {
329        CallTraceNode {
330            parent: Some(0),
331            idx: 1,
332            trace: CallTrace {
333                address: precompiles::SHA_256,
334                data: Bytes::from_static(b"hello"),
335                output: alloy_primitives::hex!(
336                    "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
337                )
338                .into(),
339                ..Default::default()
340            },
341            ordering: Vec::new(),
342            ..Default::default()
343        }
344    }
345
346    fn arena_with_child_after_staticcall(child_trace: CallTrace) -> CallTraceArena {
347        let mut arena = CallTraceArena::default();
348
349        {
350            let root = &mut arena.nodes_mut()[0];
351            root.trace.steps =
352                vec![step_with_op(0, OpCode::STATICCALL), step_with_op(1, OpCode::STOP)];
353            root.ordering = vec![
354                TraceMemberOrder::Step(0),
355                TraceMemberOrder::Call(0),
356                TraceMemberOrder::Step(1),
357            ];
358            root.children.push(1);
359        }
360
361        arena.nodes_mut().push(CallTraceNode {
362            parent: Some(0),
363            idx: 1,
364            trace: child_trace,
365            ordering: Vec::new(),
366            ..Default::default()
367        });
368
369        arena
370    }
371
372    fn decoded_fee_manager_trace() -> CallTrace {
373        CallTrace {
374            address: Address::from([0x42; 20]),
375            decoded: Some(Box::new(DecodedCallTrace {
376                label: Some("FeeManager".to_string()),
377                call_data: Some(DecodedCallData {
378                    signature: "userTokens(address)".to_string(),
379                    args: vec!["0x0000000000000000000000000000000000000000".to_string()],
380                }),
381                return_data: Some("0x0000000000000000000000000000000000000000".to_string()),
382            })),
383            ..Default::default()
384        }
385    }
386
387    fn assert_no_precompile_notice(step: &CallTraceStep) {
388        assert!(
389            !matches!(
390                step.decoded.as_deref(),
391                Some(DecodedTraceStep::Line(line)) if line.starts_with("precompile:")
392            ),
393            "unexpected precompile notice: {:?}",
394            step.decoded
395        );
396    }
397
398    fn assert_precompile_notice(step: &CallTraceStep) -> &str {
399        let Some(DecodedTraceStep::Line(notice)) = step.decoded.as_deref() else {
400            panic!("missing precompile notice");
401        };
402        notice
403    }
404
405    #[test]
406    fn flatten_records_original_step_offsets_for_split_segments() {
407        let mut arena = CallTraceArena::default();
408
409        {
410            let root = &mut arena.nodes_mut()[0];
411            root.trace.steps = vec![step(0), step(1), step(2)];
412            root.ordering = vec![
413                TraceMemberOrder::Step(0),
414                TraceMemberOrder::Call(0),
415                TraceMemberOrder::Step(1),
416                TraceMemberOrder::Step(2),
417            ];
418            root.children.push(1);
419        }
420
421        arena.nodes_mut().push(CallTraceNode {
422            parent: Some(0),
423            idx: 1,
424            trace: CallTrace { kind: CallKind::Call, steps: vec![step(10)], ..Default::default() },
425            ordering: vec![TraceMemberOrder::Step(0)],
426            ..Default::default()
427        });
428
429        let mut flattened = Vec::new();
430        flatten_call_trace(arena, &mut flattened);
431
432        assert_eq!(flattened.len(), 3);
433        assert_eq!((flattened[0].trace_node_idx, flattened[0].step_offset), (0, 0));
434        assert_eq!(flattened[0].steps.iter().map(|step| step.pc).collect::<Vec<_>>(), [0]);
435        assert_eq!((flattened[1].trace_node_idx, flattened[1].step_offset), (1, 0));
436        assert_eq!(flattened[1].steps.iter().map(|step| step.pc).collect::<Vec<_>>(), [10]);
437        assert_eq!((flattened[2].trace_node_idx, flattened[2].step_offset), (0, 1));
438        assert_eq!(flattened[2].steps.iter().map(|step| step.pc).collect::<Vec<_>>(), [1, 2]);
439    }
440
441    #[test]
442    fn flatten_keeps_trace_node_ids_unique_when_appending_multiple_arenas() {
443        let mut flattened = vec![DebugNode { trace_node_idx: 1, ..Default::default() }];
444
445        let mut arena = CallTraceArena::default();
446        {
447            let root = &mut arena.nodes_mut()[0];
448            root.trace.steps = vec![step(0)];
449            root.ordering = vec![TraceMemberOrder::Step(0)];
450        }
451
452        flatten_call_trace(arena, &mut flattened);
453
454        assert_eq!(flattened[1].trace_node_idx, 2);
455    }
456
457    #[test]
458    fn flatten_annotates_parent_step_for_precompile_child_calls() {
459        let mut arena = CallTraceArena::default();
460
461        {
462            let root = &mut arena.nodes_mut()[0];
463            root.trace.steps =
464                vec![step_with_op(0, OpCode::STATICCALL), step_with_op(1, OpCode::STOP)];
465            root.ordering = vec![
466                TraceMemberOrder::Step(0),
467                TraceMemberOrder::Call(0),
468                TraceMemberOrder::Step(1),
469            ];
470            root.children.push(1);
471        }
472
473        arena.nodes_mut().push(CallTraceNode {
474            parent: Some(0),
475            idx: 1,
476            trace: CallTrace {
477                decoded: Some(Box::new(DecodedCallTrace {
478                    label: Some("PRECOMPILES".to_string()),
479                    call_data: Some(DecodedCallData {
480                        signature: "sha256(bytes)".to_string(),
481                        args: vec!["0x68656c6c6f".to_string()],
482                    }),
483                    return_data: Some(
484                        "0x2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
485                            .to_string(),
486                    ),
487                })),
488                ..Default::default()
489            },
490            ordering: Vec::new(),
491            ..Default::default()
492        });
493
494        let mut flattened = Vec::new();
495        flatten_call_trace(arena, &mut flattened);
496
497        let Some(DecodedTraceStep::Line(notice)) = flattened[0].steps[0].decoded.as_deref() else {
498            panic!("missing precompile notice");
499        };
500        assert_eq!(
501            notice,
502            "precompile: PRECOMPILES::sha256(0x68656c6c6f) -> 0x2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
503        );
504    }
505
506    #[test]
507    fn flatten_marks_known_precompile_child_calls_without_decoded_trace() {
508        let mut arena = CallTraceArena::default();
509
510        {
511            let root = &mut arena.nodes_mut()[0];
512            root.trace.steps =
513                vec![step_with_op(0, OpCode::STATICCALL), step_with_op(1, OpCode::STOP)];
514            root.ordering = vec![
515                TraceMemberOrder::Step(0),
516                TraceMemberOrder::Call(0),
517                TraceMemberOrder::Step(1),
518            ];
519            root.children.push(1);
520        }
521
522        arena.nodes_mut().push(known_sha256_precompile_node());
523
524        let mut flattened = Vec::new();
525        flatten_call_trace(arena, &mut flattened);
526
527        let Some(DecodedTraceStep::Line(notice)) = flattened[0].steps[0].decoded.as_deref() else {
528            panic!("missing precompile notice");
529        };
530        assert_eq!(
531            notice,
532            "precompile: sha256 @ 0x0000000000000000000000000000000000000002 input=0x68656c6c6f output=0x2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
533        );
534    }
535
536    #[test]
537    fn flatten_does_not_guess_unconfirmed_p256_precompile_calls() {
538        for maybe_precompile in [None, Some(false)] {
539            let arena = arena_with_child_after_staticcall(CallTrace {
540                address: precompiles::P256_VERIFY,
541                maybe_precompile,
542                ..Default::default()
543            });
544
545            let mut flattened = Vec::new();
546            flatten_call_trace(arena, &mut flattened);
547
548            assert_no_precompile_notice(&flattened[0].steps[0]);
549        }
550
551        let arena = arena_with_child_after_staticcall(CallTrace {
552            address: precompiles::P256_VERIFY,
553            maybe_precompile: Some(true),
554            ..Default::default()
555        });
556        let mut flattened = Vec::new();
557        flatten_call_trace(arena, &mut flattened);
558        assert_eq!(
559            assert_precompile_notice(&flattened[0].steps[0]),
560            "precompile: p256Verify @ 0x0000000000000000000000000000000000000100"
561        );
562    }
563
564    #[test]
565    fn flatten_annotates_chain_labeled_precompile_child_calls_with_decoded_label() {
566        let address = Address::from([0x42; 20]);
567        let arena = arena_with_child_after_staticcall(decoded_fee_manager_trace());
568        let precompile_labels = AddressHashMap::from_iter([(address, "FeeManager".to_string())]);
569
570        let mut flattened = Vec::new();
571        flatten_call_trace_with_precompiles(arena, &mut flattened, &precompile_labels);
572
573        assert_eq!(
574            assert_precompile_notice(&flattened[0].steps[0]),
575            "precompile: FeeManager::userTokens(0x0000000000000000000000000000000000000000) -> 0x0000000000000000000000000000000000000000"
576        );
577    }
578
579    #[test]
580    fn flatten_skips_decoded_label_without_active_precompile_label() {
581        let arena = arena_with_child_after_staticcall(decoded_fee_manager_trace());
582
583        let mut flattened = Vec::new();
584        flatten_call_trace(arena, &mut flattened);
585
586        assert_no_precompile_notice(&flattened[0].steps[0]);
587    }
588
589    #[test]
590    fn flatten_preserves_existing_decoded_step_metadata_on_precompile_child_calls() {
591        let mut arena = CallTraceArena::default();
592
593        {
594            let root = &mut arena.nodes_mut()[0];
595            let mut call_step = step_with_op(0, OpCode::STATICCALL);
596            call_step.decoded = Some(Box::new(DecodedTraceStep::InternalCall(
597                DecodedInternalCall {
598                    func_name: "DebugMe::foo".to_string(),
599                    args: Some(vec!["1".to_string()]),
600                    return_data: Some(vec!["2".to_string()]),
601                },
602                1,
603            )));
604            root.trace.steps = vec![call_step, step_with_op(1, OpCode::STOP)];
605            root.ordering = vec![
606                TraceMemberOrder::Step(0),
607                TraceMemberOrder::Call(0),
608                TraceMemberOrder::Step(1),
609            ];
610            root.children.push(1);
611        }
612        arena.nodes_mut().push(known_sha256_precompile_node());
613
614        let mut flattened = Vec::new();
615        flatten_call_trace(arena, &mut flattened);
616
617        let Some(DecodedTraceStep::InternalCall(decoded, end_step)) =
618            flattened[0].steps[0].decoded.as_deref()
619        else {
620            panic!("expected existing internal call metadata to be preserved");
621        };
622        assert_eq!(decoded.func_name, "DebugMe::foo");
623        assert_eq!(*end_step, 1);
624    }
625
626    #[test]
627    fn flatten_skips_precompile_notice_without_preceding_step() {
628        let mut arena = CallTraceArena::default();
629
630        {
631            let root = &mut arena.nodes_mut()[0];
632            root.trace.steps = vec![step_with_op(0, OpCode::STOP)];
633            root.ordering = vec![TraceMemberOrder::Call(0), TraceMemberOrder::Step(0)];
634            root.children.push(1);
635        }
636        arena.nodes_mut().push(known_sha256_precompile_node());
637
638        let mut flattened = Vec::new();
639        flatten_call_trace(arena, &mut flattened);
640
641        assert_no_precompile_notice(&flattened[0].steps[0]);
642    }
643
644    #[test]
645    fn flatten_skips_precompile_notice_after_non_call_step() {
646        let mut arena = CallTraceArena::default();
647
648        {
649            let root = &mut arena.nodes_mut()[0];
650            root.trace.steps = vec![step_with_op(0, OpCode::STOP), step_with_op(1, OpCode::STOP)];
651            root.ordering = vec![
652                TraceMemberOrder::Step(0),
653                TraceMemberOrder::Call(0),
654                TraceMemberOrder::Step(1),
655            ];
656            root.children.push(1);
657        }
658        arena.nodes_mut().push(known_sha256_precompile_node());
659
660        let mut flattened = Vec::new();
661        flatten_call_trace(arena, &mut flattened);
662
663        assert_no_precompile_notice(&flattened[0].steps[0]);
664    }
665}