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#[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 pub steps: Vec<CallTraceStep>,
39}
40
41impl DebugNode {
42 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#[cfg(test)]
70fn flatten_call_trace(arena: CallTraceArena, out: &mut Vec<DebugNode>) {
71 flatten_call_trace_with_precompiles(arena, out, &AddressHashMap::default());
72}
73
74pub 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 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
270const 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}