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    let name = known_precompile_name(trace.address)?;
251    let mut notice = format!("precompile: {name} @ {}", trace.address);
252    append_raw_precompile_io(&mut notice, trace);
253    Some(notice)
254}
255
256fn append_raw_precompile_io(notice: &mut String, trace: &CallTrace) {
257    if !trace.data.is_empty() {
258        notice.push_str(" input=");
259        notice.push_str(&hex::encode_prefixed(&trace.data));
260    }
261    if !trace.output.is_empty() {
262        notice.push_str(" output=");
263        notice.push_str(&hex::encode_prefixed(&trace.output));
264    }
265}
266
267// Standard EVM fallback for traces that have no decoded precompile metadata.
268const fn known_precompile_name(address: Address) -> Option<&'static str> {
269    match address {
270        precompiles::EC_RECOVER => Some("ecrecover"),
271        precompiles::SHA_256 => Some("sha256"),
272        precompiles::RIPEMD_160 => Some("ripemd"),
273        precompiles::IDENTITY => Some("identity"),
274        precompiles::MOD_EXP => Some("modexp"),
275        precompiles::EC_ADD => Some("ecadd"),
276        precompiles::EC_MUL => Some("ecmul"),
277        precompiles::EC_PAIRING => Some("ecpairing"),
278        precompiles::BLAKE_2F => Some("blake2f"),
279        precompiles::POINT_EVALUATION => Some("pointEvaluation"),
280        precompiles::BLS12_G1ADD => Some("bls12G1Add"),
281        precompiles::BLS12_G1MSM => Some("bls12G1Msm"),
282        precompiles::BLS12_G2ADD => Some("bls12G2Add"),
283        precompiles::BLS12_G2MSM => Some("bls12G2Msm"),
284        precompiles::BLS12_PAIRING_CHECK => Some("bls12PairingCheck"),
285        precompiles::BLS12_MAP_FP_TO_G1 => Some("bls12MapFpToG1"),
286        precompiles::BLS12_MAP_FP2_TO_G2 => Some("bls12MapFp2ToG2"),
287        precompiles::P256_VERIFY => Some("p256Verify"),
288        _ => None,
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295    use foundry_evm_traces::CallTraceNode;
296    use revm::interpreter::InstructionResult;
297    use revm_inspectors::tracing::types::{DecodedCallData, DecodedInternalCall};
298
299    fn step(pc: usize) -> CallTraceStep {
300        CallTraceStep {
301            pc,
302            op: OpCode::STOP,
303            stack: None,
304            push_stack: None,
305            memory: None,
306            returndata: Bytes::new(),
307            gas_remaining: 0,
308            gas_refund_counter: 0,
309            gas_used: 0,
310            gas_cost: 0,
311            storage_change: None,
312            status: Some(InstructionResult::Stop),
313            immediate_bytes: None,
314            decoded: None,
315        }
316    }
317
318    fn step_with_op(pc: usize, op: OpCode) -> CallTraceStep {
319        CallTraceStep { op, ..step(pc) }
320    }
321
322    fn known_sha256_precompile_node() -> CallTraceNode {
323        CallTraceNode {
324            parent: Some(0),
325            idx: 1,
326            trace: CallTrace {
327                address: precompiles::SHA_256,
328                data: Bytes::from_static(b"hello"),
329                output: alloy_primitives::hex!(
330                    "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
331                )
332                .into(),
333                ..Default::default()
334            },
335            ordering: Vec::new(),
336            ..Default::default()
337        }
338    }
339
340    fn arena_with_child_after_staticcall(child_trace: CallTrace) -> CallTraceArena {
341        let mut arena = CallTraceArena::default();
342
343        {
344            let root = &mut arena.nodes_mut()[0];
345            root.trace.steps =
346                vec![step_with_op(0, OpCode::STATICCALL), step_with_op(1, OpCode::STOP)];
347            root.ordering = vec![
348                TraceMemberOrder::Step(0),
349                TraceMemberOrder::Call(0),
350                TraceMemberOrder::Step(1),
351            ];
352            root.children.push(1);
353        }
354
355        arena.nodes_mut().push(CallTraceNode {
356            parent: Some(0),
357            idx: 1,
358            trace: child_trace,
359            ordering: Vec::new(),
360            ..Default::default()
361        });
362
363        arena
364    }
365
366    fn decoded_fee_manager_trace() -> CallTrace {
367        CallTrace {
368            address: Address::from([0x42; 20]),
369            decoded: Some(Box::new(DecodedCallTrace {
370                label: Some("FeeManager".to_string()),
371                call_data: Some(DecodedCallData {
372                    signature: "userTokens(address)".to_string(),
373                    args: vec!["0x0000000000000000000000000000000000000000".to_string()],
374                }),
375                return_data: Some("0x0000000000000000000000000000000000000000".to_string()),
376            })),
377            ..Default::default()
378        }
379    }
380
381    fn assert_no_precompile_notice(step: &CallTraceStep) {
382        assert!(
383            !matches!(
384                step.decoded.as_deref(),
385                Some(DecodedTraceStep::Line(line)) if line.starts_with("precompile:")
386            ),
387            "unexpected precompile notice: {:?}",
388            step.decoded
389        );
390    }
391
392    fn assert_precompile_notice(step: &CallTraceStep) -> &str {
393        let Some(DecodedTraceStep::Line(notice)) = step.decoded.as_deref() else {
394            panic!("missing precompile notice");
395        };
396        notice
397    }
398
399    #[test]
400    fn flatten_records_original_step_offsets_for_split_segments() {
401        let mut arena = CallTraceArena::default();
402
403        {
404            let root = &mut arena.nodes_mut()[0];
405            root.trace.steps = vec![step(0), step(1), step(2)];
406            root.ordering = vec![
407                TraceMemberOrder::Step(0),
408                TraceMemberOrder::Call(0),
409                TraceMemberOrder::Step(1),
410                TraceMemberOrder::Step(2),
411            ];
412            root.children.push(1);
413        }
414
415        arena.nodes_mut().push(CallTraceNode {
416            parent: Some(0),
417            idx: 1,
418            trace: CallTrace { kind: CallKind::Call, steps: vec![step(10)], ..Default::default() },
419            ordering: vec![TraceMemberOrder::Step(0)],
420            ..Default::default()
421        });
422
423        let mut flattened = Vec::new();
424        flatten_call_trace(arena, &mut flattened);
425
426        assert_eq!(flattened.len(), 3);
427        assert_eq!((flattened[0].trace_node_idx, flattened[0].step_offset), (0, 0));
428        assert_eq!(flattened[0].steps.iter().map(|step| step.pc).collect::<Vec<_>>(), [0]);
429        assert_eq!((flattened[1].trace_node_idx, flattened[1].step_offset), (1, 0));
430        assert_eq!(flattened[1].steps.iter().map(|step| step.pc).collect::<Vec<_>>(), [10]);
431        assert_eq!((flattened[2].trace_node_idx, flattened[2].step_offset), (0, 1));
432        assert_eq!(flattened[2].steps.iter().map(|step| step.pc).collect::<Vec<_>>(), [1, 2]);
433    }
434
435    #[test]
436    fn flatten_keeps_trace_node_ids_unique_when_appending_multiple_arenas() {
437        let mut flattened = vec![DebugNode { trace_node_idx: 1, ..Default::default() }];
438
439        let mut arena = CallTraceArena::default();
440        {
441            let root = &mut arena.nodes_mut()[0];
442            root.trace.steps = vec![step(0)];
443            root.ordering = vec![TraceMemberOrder::Step(0)];
444        }
445
446        flatten_call_trace(arena, &mut flattened);
447
448        assert_eq!(flattened[1].trace_node_idx, 2);
449    }
450
451    #[test]
452    fn flatten_annotates_parent_step_for_precompile_child_calls() {
453        let mut arena = CallTraceArena::default();
454
455        {
456            let root = &mut arena.nodes_mut()[0];
457            root.trace.steps =
458                vec![step_with_op(0, OpCode::STATICCALL), step_with_op(1, OpCode::STOP)];
459            root.ordering = vec![
460                TraceMemberOrder::Step(0),
461                TraceMemberOrder::Call(0),
462                TraceMemberOrder::Step(1),
463            ];
464            root.children.push(1);
465        }
466
467        arena.nodes_mut().push(CallTraceNode {
468            parent: Some(0),
469            idx: 1,
470            trace: CallTrace {
471                decoded: Some(Box::new(DecodedCallTrace {
472                    label: Some("PRECOMPILES".to_string()),
473                    call_data: Some(DecodedCallData {
474                        signature: "sha256(bytes)".to_string(),
475                        args: vec!["0x68656c6c6f".to_string()],
476                    }),
477                    return_data: Some(
478                        "0x2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
479                            .to_string(),
480                    ),
481                })),
482                ..Default::default()
483            },
484            ordering: Vec::new(),
485            ..Default::default()
486        });
487
488        let mut flattened = Vec::new();
489        flatten_call_trace(arena, &mut flattened);
490
491        let Some(DecodedTraceStep::Line(notice)) = flattened[0].steps[0].decoded.as_deref() else {
492            panic!("missing precompile notice");
493        };
494        assert_eq!(
495            notice,
496            "precompile: PRECOMPILES::sha256(0x68656c6c6f) -> 0x2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
497        );
498    }
499
500    #[test]
501    fn flatten_marks_known_precompile_child_calls_without_decoded_trace() {
502        let mut arena = CallTraceArena::default();
503
504        {
505            let root = &mut arena.nodes_mut()[0];
506            root.trace.steps =
507                vec![step_with_op(0, OpCode::STATICCALL), step_with_op(1, OpCode::STOP)];
508            root.ordering = vec![
509                TraceMemberOrder::Step(0),
510                TraceMemberOrder::Call(0),
511                TraceMemberOrder::Step(1),
512            ];
513            root.children.push(1);
514        }
515
516        arena.nodes_mut().push(known_sha256_precompile_node());
517
518        let mut flattened = Vec::new();
519        flatten_call_trace(arena, &mut flattened);
520
521        let Some(DecodedTraceStep::Line(notice)) = flattened[0].steps[0].decoded.as_deref() else {
522            panic!("missing precompile notice");
523        };
524        assert_eq!(
525            notice,
526            "precompile: sha256 @ 0x0000000000000000000000000000000000000002 input=0x68656c6c6f output=0x2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
527        );
528    }
529
530    #[test]
531    fn flatten_annotates_chain_labeled_precompile_child_calls_with_decoded_label() {
532        let address = Address::from([0x42; 20]);
533        let arena = arena_with_child_after_staticcall(decoded_fee_manager_trace());
534        let precompile_labels = AddressHashMap::from_iter([(address, "FeeManager".to_string())]);
535
536        let mut flattened = Vec::new();
537        flatten_call_trace_with_precompiles(arena, &mut flattened, &precompile_labels);
538
539        assert_eq!(
540            assert_precompile_notice(&flattened[0].steps[0]),
541            "precompile: FeeManager::userTokens(0x0000000000000000000000000000000000000000) -> 0x0000000000000000000000000000000000000000"
542        );
543    }
544
545    #[test]
546    fn flatten_skips_decoded_label_without_active_precompile_label() {
547        let arena = arena_with_child_after_staticcall(decoded_fee_manager_trace());
548
549        let mut flattened = Vec::new();
550        flatten_call_trace(arena, &mut flattened);
551
552        assert_no_precompile_notice(&flattened[0].steps[0]);
553    }
554
555    #[test]
556    fn flatten_preserves_existing_decoded_step_metadata_on_precompile_child_calls() {
557        let mut arena = CallTraceArena::default();
558
559        {
560            let root = &mut arena.nodes_mut()[0];
561            let mut call_step = step_with_op(0, OpCode::STATICCALL);
562            call_step.decoded = Some(Box::new(DecodedTraceStep::InternalCall(
563                DecodedInternalCall {
564                    func_name: "DebugMe::foo".to_string(),
565                    args: Some(vec!["1".to_string()]),
566                    return_data: Some(vec!["2".to_string()]),
567                },
568                1,
569            )));
570            root.trace.steps = vec![call_step, step_with_op(1, OpCode::STOP)];
571            root.ordering = vec![
572                TraceMemberOrder::Step(0),
573                TraceMemberOrder::Call(0),
574                TraceMemberOrder::Step(1),
575            ];
576            root.children.push(1);
577        }
578        arena.nodes_mut().push(known_sha256_precompile_node());
579
580        let mut flattened = Vec::new();
581        flatten_call_trace(arena, &mut flattened);
582
583        let Some(DecodedTraceStep::InternalCall(decoded, end_step)) =
584            flattened[0].steps[0].decoded.as_deref()
585        else {
586            panic!("expected existing internal call metadata to be preserved");
587        };
588        assert_eq!(decoded.func_name, "DebugMe::foo");
589        assert_eq!(*end_step, 1);
590    }
591
592    #[test]
593    fn flatten_skips_precompile_notice_without_preceding_step() {
594        let mut arena = CallTraceArena::default();
595
596        {
597            let root = &mut arena.nodes_mut()[0];
598            root.trace.steps = vec![step_with_op(0, OpCode::STOP)];
599            root.ordering = vec![TraceMemberOrder::Call(0), TraceMemberOrder::Step(0)];
600            root.children.push(1);
601        }
602        arena.nodes_mut().push(known_sha256_precompile_node());
603
604        let mut flattened = Vec::new();
605        flatten_call_trace(arena, &mut flattened);
606
607        assert_no_precompile_notice(&flattened[0].steps[0]);
608    }
609
610    #[test]
611    fn flatten_skips_precompile_notice_after_non_call_step() {
612        let mut arena = CallTraceArena::default();
613
614        {
615            let root = &mut arena.nodes_mut()[0];
616            root.trace.steps = vec![step_with_op(0, OpCode::STOP), step_with_op(1, OpCode::STOP)];
617            root.ordering = vec![
618                TraceMemberOrder::Step(0),
619                TraceMemberOrder::Call(0),
620                TraceMemberOrder::Step(1),
621            ];
622            root.children.push(1);
623        }
624        arena.nodes_mut().push(known_sha256_precompile_node());
625
626        let mut flattened = Vec::new();
627        flatten_call_trace(arena, &mut flattened);
628
629        assert_no_precompile_notice(&flattened[0].steps[0]);
630    }
631}