1#![cfg_attr(not(test), warn(unused_crate_dependencies))]
6#![cfg_attr(docsrs, feature(doc_cfg))]
7
8#[macro_use]
9extern crate foundry_common;
10
11#[macro_use]
12extern crate tracing;
13
14use foundry_common::{
15 contracts::{ContractsByAddress, ContractsByArtifact},
16 shell,
17};
18use revm::bytecode::opcode::OpCode;
19use revm_inspectors::tracing::{OpcodeFilter, types::DecodedTraceStep};
20use serde::{Deserialize, Serialize};
21use std::{
22 borrow::Cow,
23 collections::{BTreeMap, BTreeSet},
24 ops::{Deref, DerefMut},
25};
26
27use alloy_primitives::{U256, map::HashMap};
28use tempo_contracts::precompiles::TIP20_CHANNEL_RESERVE_ADDRESS;
29
30pub use revm_inspectors::tracing::{
31 CallTraceArena, FourByteInspector, GethTraceBuilder, ParityTraceBuilder, StackSnapshotType,
32 TraceWriter, TracingInspector, TracingInspectorConfig,
33 types::{
34 CallKind, CallLog, CallTrace, CallTraceNode, DecodedCallData, DecodedCallLog,
35 DecodedCallTrace, TraceMemberOrder,
36 },
37};
38
39pub mod identifier;
43use identifier::LocalTraceIdentifier;
44
45mod decoder;
46pub use decoder::{CallTraceDecoder, CallTraceDecoderBuilder};
47
48pub mod debug;
49pub use debug::DebugTraceIdentifier;
50
51pub mod folded_stack_trace;
52
53pub mod backtrace;
54pub mod speedscope;
55
56pub type Traces = Vec<(TraceKind, SparsedTraceArena)>;
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct SparsedTraceArena {
61 #[serde(flatten)]
63 pub arena: CallTraceArena,
64 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
67 pub ignored: HashMap<(usize, usize), (usize, usize)>,
68}
69
70impl SparsedTraceArena {
71 fn resolve_arena(&self) -> Cow<'_, CallTraceArena> {
73 if self.ignored.is_empty() {
74 Cow::Borrowed(&self.arena)
75 } else {
76 let mut arena = self.arena.clone();
77
78 fn clear_node(
79 nodes: &mut [CallTraceNode],
80 node_idx: usize,
81 ignored: &HashMap<(usize, usize), (usize, usize)>,
82 cur_ignore_end: &mut Option<(usize, usize)>,
83 ) {
84 let items = std::iter::once(None)
87 .chain(nodes[node_idx].ordering.clone().into_iter().map(Some))
88 .enumerate();
89
90 let mut internal_calls = Vec::new();
91 let mut items_to_remove = BTreeSet::new();
92 for (item_idx, item) in items {
93 if let Some(end_node) = ignored.get(&(node_idx, item_idx)) {
94 *cur_ignore_end = Some(*end_node);
95 }
96
97 let mut remove = cur_ignore_end.is_some() & item.is_some();
98
99 match item {
100 Some(TraceMemberOrder::Call(child_idx)) => {
102 clear_node(
103 nodes,
104 nodes[node_idx].children[child_idx],
105 ignored,
106 cur_ignore_end,
107 );
108 remove &= cur_ignore_end.is_some();
109 }
110 Some(TraceMemberOrder::Step(step_idx)) => {
112 if let Some(decoded) = &nodes[node_idx].trace.steps[step_idx].decoded
114 && let DecodedTraceStep::InternalCall(_, end_step_idx) = &**decoded
115 {
116 internal_calls.push((item_idx, remove, *end_step_idx));
117 remove = false;
119 }
120 internal_calls.retain(|(start_item_idx, remove_start, end_idx)| {
122 if *end_idx != step_idx {
123 return true;
124 }
125 if *remove_start && remove {
127 items_to_remove.insert(*start_item_idx);
128 } else {
129 remove = false;
130 }
131
132 false
133 });
134 }
135 _ => {}
136 }
137
138 if remove {
139 items_to_remove.insert(item_idx);
140 }
141
142 if let Some((end_node, end_step_idx)) = cur_ignore_end
143 && node_idx == *end_node
144 && item_idx == *end_step_idx
145 {
146 *cur_ignore_end = None;
147 }
148 }
149
150 for (offset, item_idx) in items_to_remove.into_iter().enumerate() {
151 nodes[node_idx].ordering.remove(item_idx - offset - 1);
152 }
153 }
154
155 clear_node(arena.nodes_mut(), 0, &self.ignored, &mut None);
156
157 Cow::Owned(arena)
158 }
159 }
160}
161
162impl Deref for SparsedTraceArena {
163 type Target = CallTraceArena;
164
165 fn deref(&self) -> &Self::Target {
166 &self.arena
167 }
168}
169
170impl DerefMut for SparsedTraceArena {
171 fn deref_mut(&mut self) -> &mut Self::Target {
172 &mut self.arena
173 }
174}
175
176pub async fn decode_trace_arena(arena: &mut CallTraceArena, decoder: &CallTraceDecoder) {
180 decoder.prefetch_signatures(arena.nodes()).await;
181 decoder.populate_traces(arena.nodes_mut()).await;
182}
183
184pub fn render_trace_arena(arena: &SparsedTraceArena) -> String {
186 render_trace_arena_inner(arena, false, false)
187}
188
189pub fn prune_trace_depth(arena: &mut CallTraceArena, depth: usize) {
191 for node in arena.nodes_mut() {
192 if node.trace.depth >= depth {
193 node.ordering.clear();
194 }
195 }
196}
197
198pub fn render_trace_arena_inner(
201 arena: &SparsedTraceArena,
202 with_bytecodes: bool,
203 with_storage_changes: bool,
204) -> String {
205 if shell::is_json() {
206 return serde_json::to_string(&arena.resolve_arena()).expect("Failed to serialize traces");
207 }
208
209 let mut resolved = arena.resolve_arena();
210
211 let mut tempo_changes = None;
212 if with_storage_changes {
213 tempo_changes = tempo_channel_storage_decodes(&resolved);
214
215 let needs_dedup = resolved.as_ref().nodes().iter().any(|node| {
216 node.trace.steps.iter().any(|step| {
217 step.storage_change.is_some()
218 && matches!(step.decoded.as_deref(), Some(DecodedTraceStep::Line(_)))
219 })
220 });
221 if needs_dedup {
222 for node in resolved.to_mut().nodes_mut() {
224 for step in &mut node.trace.steps {
225 if step.storage_change.is_some()
226 && matches!(step.decoded.as_deref(), Some(DecodedTraceStep::Line(_)))
227 {
228 step.storage_change = None;
229 }
230 }
231 }
232 }
233 }
234
235 let mut w = TraceWriter::new(Vec::<u8>::new())
236 .color_cheatcodes(true)
237 .use_colors(convert_color_choice(shell::color_choice()))
238 .write_bytecodes(with_bytecodes)
239 .with_storage_changes(with_storage_changes);
240 w.write_arena(resolved.as_ref()).expect("Failed to write traces");
241 let mut rendered =
242 String::from_utf8(w.into_writer()).expect("trace writer wrote invalid UTF-8");
243 if let Some(tempo_changes) = tempo_changes {
244 if !rendered.ends_with('\n') {
245 rendered.push('\n');
246 }
247 rendered.push_str(&tempo_changes);
248 }
249
250 rendered
251}
252
253fn tempo_channel_storage_decodes(arena: &CallTraceArena) -> Option<String> {
254 let decoded_changes = arena
255 .nodes()
256 .iter()
257 .filter(|node| node.trace.address == TIP20_CHANNEL_RESERVE_ADDRESS)
258 .flat_map(compact_channel_storage_changes)
259 .collect::<Vec<_>>();
260
261 if decoded_changes.is_empty() {
262 return None;
263 }
264
265 let mut rendered = String::new();
266 rendered.push_str("Decoded TIP20ChannelReserve storage:\n");
267 for (slot, before, after) in decoded_changes {
268 rendered.push_str(&format!(
269 " @ {}: {} -> {}\n",
270 format_storage_word(slot),
271 format_channel_state(before),
272 format_channel_state(after),
273 ));
274 }
275 Some(rendered)
276}
277
278fn compact_channel_storage_changes(node: &CallTraceNode) -> Vec<(U256, U256, U256)> {
279 let mut changes_map = BTreeMap::new();
280 for step in &node.trace.steps {
281 if let Some(change) = &step.storage_change
282 && change.had_value.is_some()
283 {
284 let (_first, last) = changes_map.entry(change.key).or_insert((&**change, &**change));
285 *last = &**change;
286 }
287 }
288
289 changes_map
290 .into_iter()
291 .filter_map(|(key, (first, last))| {
292 let before = first.had_value.unwrap_or_default();
293 let after = last.value;
294 (before != after).then_some((key, before, after))
295 })
296 .collect()
297}
298
299fn format_channel_state(value: U256) -> String {
300 let (settled, deposit, close_requested_at) = decode_channel_state(value);
301 format!("{{settled: {settled}, deposit: {deposit}, closeRequestedAt: {close_requested_at}}}")
302}
303
304fn decode_channel_state(value: U256) -> (U256, U256, u32) {
305 let mask96 = (U256::from(1) << 96) - U256::from(1);
306 let mask32 = (U256::from(1) << 32) - U256::from(1);
307 let settled: U256 = value & mask96;
308 let deposit: U256 = (value >> 96usize) & mask96;
309 let close_requested_at_word: U256 = (value >> 192usize) & mask32;
310 let close_requested_at = close_requested_at_word.to::<u32>();
311 (settled, deposit, close_requested_at)
312}
313
314fn format_storage_word(value: U256) -> String {
315 if value < U256::from(1_000_000u64) { value.to_string() } else { format!("0x{value:x}") }
316}
317
318const fn convert_color_choice(choice: shell::ColorChoice) -> revm_inspectors::ColorChoice {
319 match choice {
320 shell::ColorChoice::Auto => revm_inspectors::ColorChoice::Auto,
321 shell::ColorChoice::Always => revm_inspectors::ColorChoice::Always,
322 shell::ColorChoice::Never => revm_inspectors::ColorChoice::Never,
323 }
324}
325
326#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
328pub enum TraceKind {
329 Deployment,
330 Setup,
331 Execution,
332}
333
334impl TraceKind {
335 #[must_use]
339 pub const fn is_deployment(self) -> bool {
340 matches!(self, Self::Deployment)
341 }
342
343 #[must_use]
347 pub const fn is_setup(self) -> bool {
348 matches!(self, Self::Setup)
349 }
350
351 #[must_use]
355 pub const fn is_execution(self) -> bool {
356 matches!(self, Self::Execution)
357 }
358}
359
360pub fn load_contracts<'a>(
362 traces: impl IntoIterator<Item = &'a CallTraceArena>,
363 known_contracts: &ContractsByArtifact,
364) -> ContractsByAddress {
365 let mut local_identifier = LocalTraceIdentifier::new(known_contracts);
366 let decoder = CallTraceDecoder::new();
367 let mut contracts = ContractsByAddress::new();
368 for trace in traces {
369 for address in decoder.identify_addresses(trace, &mut local_identifier) {
370 if let (Some(contract), Some(abi)) = (address.contract, address.abi) {
371 contracts.insert(address.address, (contract, abi.into_owned()));
372 }
373 }
374 }
375 contracts
376}
377
378#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
380pub enum InternalTraceMode {
381 #[default]
382 None,
383 Simple,
385 Full,
387}
388
389#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
391pub enum StepRecording {
392 #[default]
394 None,
395 Jumps,
397 All,
399}
400
401impl StepRecording {
402 const fn merge(self, other: Self) -> Self {
403 match (self, other) {
404 (Self::All, _) | (_, Self::All) => Self::All,
405 (Self::Jumps, _) | (_, Self::Jumps) => Self::Jumps,
406 (Self::None, Self::None) => Self::None,
407 }
408 }
409}
410
411#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
413pub struct TraceRequirements {
414 calls: bool,
415 steps: StepRecording,
416 memory_snapshots: bool,
417 stack_snapshots: bool,
418 returndata_snapshots: bool,
419 immediate_bytes: bool,
420 state_diff: bool,
421}
422
423impl TraceRequirements {
424 pub const fn none() -> Self {
425 Self {
426 calls: false,
427 steps: StepRecording::None,
428 memory_snapshots: false,
429 stack_snapshots: false,
430 returndata_snapshots: false,
431 immediate_bytes: false,
432 state_diff: false,
433 }
434 }
435
436 pub const fn with_calls(mut self, yes: bool) -> Self {
437 self.calls |= yes;
438 self
439 }
440
441 pub const fn merge(mut self, other: Self) -> Self {
442 self.calls |= other.calls;
443 self.steps = self.steps.merge(other.steps);
444 self.memory_snapshots |= other.memory_snapshots;
445 self.stack_snapshots |= other.stack_snapshots;
446 self.returndata_snapshots |= other.returndata_snapshots;
447 self.immediate_bytes |= other.immediate_bytes;
448 self.state_diff |= other.state_diff;
449 self
450 }
451
452 pub const fn with_steps(mut self, steps: StepRecording) -> Self {
453 self.steps = self.steps.merge(steps);
454 self
455 }
456
457 pub const fn with_memory_snapshots(mut self, yes: bool) -> Self {
458 self.memory_snapshots |= yes;
459 self
460 }
461
462 pub const fn with_stack_snapshots(mut self, yes: bool) -> Self {
463 self.stack_snapshots |= yes;
464 self
465 }
466
467 pub const fn with_debug(mut self, yes: bool) -> Self {
468 if yes {
469 self.calls = true;
470 self.steps = StepRecording::All;
471 self.memory_snapshots = true;
472 self.stack_snapshots = true;
473 self.returndata_snapshots = true;
474 self.immediate_bytes = true;
475 self.state_diff = true;
476 }
477 self
478 }
479
480 pub const fn with_decode_internal(self, mode: InternalTraceMode) -> Self {
481 match mode {
482 InternalTraceMode::None => self,
483 InternalTraceMode::Simple => {
484 self.with_calls(true).with_steps(StepRecording::Jumps).with_stack_snapshots(true)
485 }
486 InternalTraceMode::Full => self
487 .with_calls(true)
488 .with_steps(StepRecording::Jumps)
489 .with_memory_snapshots(true)
490 .with_stack_snapshots(true),
491 }
492 }
493
494 pub const fn with_all_steps(self, yes: bool) -> Self {
495 if yes { self.with_calls(true).with_steps(StepRecording::All) } else { self }
496 }
497
498 pub const fn with_state_changes(mut self, yes: bool) -> Self {
499 self.state_diff |= yes;
500 if yes {
501 self.calls = true;
502 }
503 self
504 }
505
506 pub const fn with_verbosity(self, verbosity: u8) -> Self {
507 match verbosity {
508 0..3 => self,
509 3..=4 => self.with_calls(true),
510 _ if matches!(self.steps, StepRecording::All) => self.with_calls(true),
511 _ => self.with_state_changes(true),
512 }
513 }
514
515 pub fn into_config(self) -> Option<TracingInspectorConfig> {
516 if !self.calls && self.steps == StepRecording::None && !self.state_diff {
517 return None;
518 }
519
520 let steps = if self.state_diff { StepRecording::All } else { self.steps };
521 TracingInspectorConfig {
522 record_steps: steps != StepRecording::None,
523 record_memory_snapshots: self.memory_snapshots,
524 record_stack_snapshots: if self.stack_snapshots {
525 StackSnapshotType::Full
526 } else {
527 StackSnapshotType::None
528 },
529 record_logs: true,
530 record_state_diff: self.state_diff,
531 record_returndata_snapshots: self.returndata_snapshots,
532 record_opcodes_filter: match steps {
533 StepRecording::None | StepRecording::All => None,
534 StepRecording::Jumps => {
535 Some(OpcodeFilter::new().enabled(OpCode::JUMP).enabled(OpCode::JUMPDEST))
536 }
537 },
538 exclude_precompile_calls: false,
539 record_immediate_bytes: self.immediate_bytes,
540 }
541 .into()
542 }
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548 use alloy_primitives::Bytes;
549 use revm::interpreter::InstructionResult;
550 use revm_inspectors::tracing::types::{CallTraceStep, StorageChange, StorageChangeReason};
551
552 #[test]
553 fn decodes_tip1034_packed_channel_state() {
554 let settled = U256::from(123u64);
555 let deposit = U256::from(456u64);
556 let close_requested_at = U256::from(1_780_495_200u64);
557 let packed = settled | (deposit << 96usize) | (close_requested_at << 192usize);
558
559 assert_eq!(decode_channel_state(packed), (settled, deposit, 1_780_495_200));
560 assert_eq!(
561 format_channel_state(packed),
562 "{settled: 123, deposit: 456, closeRequestedAt: 1780495200}"
563 );
564 }
565
566 #[test]
567 fn tempo_storage_decodes_do_not_insert_extra_blank_line() {
568 let mut arena = CallTraceArena::default();
569 let root = &mut arena.nodes_mut()[0];
570 root.ordering.push(TraceMemberOrder::Step(0));
571 root.trace = CallTrace {
572 address: TIP20_CHANNEL_RESERVE_ADDRESS,
573 success: true,
574 steps: vec![CallTraceStep {
575 pc: 0,
576 op: OpCode::SSTORE,
577 stack: None,
578 push_stack: None,
579 memory: None,
580 returndata: Bytes::new(),
581 gas_remaining: 0,
582 gas_refund_counter: 0,
583 gas_used: 0,
584 gas_cost: 0,
585 storage_change: Some(Box::new(StorageChange {
586 key: U256::from(1),
587 value: U256::from(2),
588 had_value: Some(U256::from(1)),
589 reason: StorageChangeReason::SSTORE,
590 })),
591 status: Some(InstructionResult::Stop),
592 immediate_bytes: None,
593 decoded: None,
594 }],
595 ..Default::default()
596 };
597
598 let rendered = render_trace_arena_inner(
599 &SparsedTraceArena { arena, ignored: Default::default() },
600 false,
601 true,
602 );
603
604 assert!(rendered.contains("\nDecoded TIP20ChannelReserve storage:\n"));
605 assert!(!rendered.contains("\n\nDecoded TIP20ChannelReserve storage:\n"));
606 }
607
608 #[test]
609 fn verbosity_0_through_2_is_noop() {
610 for v in 0..=2 {
611 assert_eq!(
612 TraceRequirements::none().with_verbosity(v),
613 TraceRequirements::none(),
614 "v={v}"
615 );
616 assert_eq!(
617 TraceRequirements::none().with_calls(true).with_verbosity(v),
618 TraceRequirements::none().with_calls(true),
619 "v={v}"
620 );
621 assert_eq!(
622 TraceRequirements::none().with_debug(true).with_verbosity(v),
623 TraceRequirements::none().with_debug(true),
624 "v={v}"
625 );
626 }
627 }
628
629 #[test]
630 fn verbosity_3_and_4_raises_to_call() {
631 for v in 3..=4 {
632 assert_eq!(
633 TraceRequirements::none().with_verbosity(v),
634 TraceRequirements::none().with_calls(true),
635 "v={v}"
636 );
637 assert_eq!(
638 TraceRequirements::none().with_debug(true).with_verbosity(v),
639 TraceRequirements::none().with_debug(true),
640 "v={v}"
641 );
642 assert_eq!(
643 TraceRequirements::none().with_state_changes(true).with_verbosity(v),
644 TraceRequirements::none().with_state_changes(true),
645 "v={v}"
646 );
647 }
648 }
649
650 #[test]
651 fn verbosity_5_raises_to_record_state_diff() {
652 let state_changes = TraceRequirements::none().with_state_changes(true);
653
654 assert_eq!(TraceRequirements::none().with_verbosity(5), state_changes);
655 assert_eq!(TraceRequirements::none().with_calls(true).with_verbosity(5), state_changes);
656 let cfg = TraceRequirements::none()
657 .with_calls(true)
658 .with_steps(StepRecording::Jumps)
659 .with_verbosity(5)
660 .into_config()
661 .unwrap();
662 assert!(cfg.record_state_diff);
663 assert!(cfg.record_opcodes_filter.is_none());
664 assert_eq!(
665 TraceRequirements::none().with_debug(true).with_verbosity(5),
666 TraceRequirements::none().with_debug(true)
667 );
668 assert_eq!(
669 TraceRequirements::none().with_state_changes(true).with_verbosity(5),
670 state_changes
671 );
672 }
673
674 #[test]
675 fn config_at_verbosity_0_is_none() {
676 assert!(TraceRequirements::none().with_verbosity(0).into_config().is_none());
677 }
678
679 #[test]
680 fn config_at_verbosity_3_records_calls_only() {
681 let cfg = TraceRequirements::none().with_verbosity(3).into_config().unwrap();
682 assert!(!cfg.record_steps, "verbosity 3 should not record steps");
683 assert!(!cfg.record_state_diff, "verbosity 3 should not record state diff");
684 assert!(cfg.record_logs, "verbosity 3 should record logs");
685 }
686
687 #[test]
688 fn config_at_verbosity_5_records_steps_and_state_diff() {
689 let cfg = TraceRequirements::none().with_verbosity(5).into_config().unwrap();
690 assert!(cfg.record_steps, "verbosity 5 must record steps for backtraces");
691 assert!(cfg.record_state_diff, "verbosity 5 must record state diff");
692 assert!(cfg.record_logs, "verbosity 5 must record logs");
693 assert!(!cfg.record_memory_snapshots, "verbosity 5 should not record memory snapshots");
695 assert_eq!(
696 cfg.record_stack_snapshots,
697 StackSnapshotType::None,
698 "verbosity 5 should not record stack snapshots"
699 );
700 assert!(
702 cfg.record_opcodes_filter.is_none(),
703 "verbosity 5 needs unfiltered opcodes for state diff"
704 );
705 }
706
707 #[test]
708 fn config_debug_mode_unchanged() {
709 let cfg = TraceRequirements::none().with_debug(true).into_config().unwrap();
711 assert!(cfg.record_steps);
712 assert!(cfg.record_memory_snapshots, "Debug must record memory snapshots");
713 assert_eq!(
714 cfg.record_stack_snapshots,
715 StackSnapshotType::Full,
716 "Debug must record full stack snapshots"
717 );
718 assert!(cfg.record_returndata_snapshots, "Debug must record returndata");
719 assert!(cfg.record_immediate_bytes, "Debug must record immediate bytes");
720 assert!(cfg.record_opcodes_filter.is_none(), "Debug must record all opcodes (no filter)");
721 assert!(cfg.record_state_diff, "Debug should record storage accesses for the debugger");
722 }
723
724 #[test]
725 fn requirements_preserve_internal_decode_with_state_diff() {
726 let cfg = TraceRequirements::none()
727 .with_decode_internal(InternalTraceMode::Full)
728 .with_state_changes(true)
729 .into_config()
730 .unwrap();
731
732 assert!(cfg.record_steps, "requirements should record opcode steps");
733 assert!(cfg.record_memory_snapshots, "Full internal decoding needs memory snapshots");
734 assert_eq!(
735 cfg.record_stack_snapshots,
736 StackSnapshotType::Full,
737 "internal decoding needs stack snapshots"
738 );
739 assert!(cfg.record_state_diff, "state changes should be recorded");
740 assert!(cfg.record_opcodes_filter.is_none(), "state diff needs unfiltered opcodes");
741 }
742
743 #[test]
744 fn requirements_all_steps_avoid_debug_snapshots() {
745 let cfg =
746 TraceRequirements::none().with_all_steps(true).with_verbosity(5).into_config().unwrap();
747
748 assert!(cfg.record_steps, "all steps must record opcode steps");
749 assert!(cfg.record_opcodes_filter.is_none(), "all steps must record every opcode step");
750 assert!(!cfg.record_memory_snapshots, "all steps should not record memory snapshots");
751 assert_eq!(
752 cfg.record_stack_snapshots,
753 StackSnapshotType::None,
754 "all steps should not record stack snapshots"
755 );
756 assert!(!cfg.record_returndata_snapshots, "all steps should not record returndata");
757 assert!(!cfg.record_immediate_bytes, "all steps should not record immediate bytes");
758 assert!(!cfg.record_state_diff, "all steps should not record state diffs");
759 }
760}