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 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
267const 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}