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#[derive(Clone, Debug, Default, Serialize, Deserialize)]
14pub struct DebugNode {
15 pub address: Address,
19 pub kind: CallKind,
21 pub calldata: Bytes,
23 #[serde(default)]
25 pub returndata: Bytes,
26 pub gas_limit: u64,
28 #[serde(default)]
30 pub trace_node_idx: usize,
31 #[serde(default)]
33 pub step_offset: usize,
34 #[serde(default)]
36 pub decoded: Option<Box<DecodedCallTrace>>,
37 #[serde(default)]
39 pub contract_name: Option<String>,
40 pub steps: Vec<CallTraceStep>,
42}
43
44impl DebugNode {
45 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#[cfg(test)]
74fn flatten_call_trace(arena: CallTraceArena, out: &mut Vec<DebugNode>) {
75 flatten_call_trace_with_precompiles(arena, out, &AddressHashMap::default(), &[]);
76}
77
78pub 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 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
278const 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}