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