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 fmt,
25 ops::{Deref, DerefMut},
26};
27
28use alloy_primitives::{Address, U256, map::HashMap};
29use tempo_contracts::precompiles::TIP20_CHANNEL_RESERVE_ADDRESS;
30
31pub use revm_inspectors::tracing::{
32 CallTraceArena, FourByteInspector, GethTraceBuilder, ParityTraceBuilder, StackSnapshotType,
33 TraceWriter, TracingInspector, TracingInspectorConfig,
34 types::{
35 CallKind, CallLog, CallTrace, CallTraceNode, DecodedCallData, DecodedCallLog,
36 DecodedCallTrace, TraceMemberOrder,
37 },
38};
39
40pub mod identifier;
44use identifier::LocalTraceIdentifier;
45
46mod decoder;
47pub use decoder::{CallTraceDecoder, CallTraceDecoderBuilder};
48
49pub mod debug;
50pub use debug::DebugTraceIdentifier;
51
52pub mod folded_stack_trace;
53
54pub mod backtrace;
55pub mod speedscope;
56
57pub type Traces = Vec<(TraceKind, SparsedTraceArena)>;
58
59#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
61pub enum RevertDiagnostic {
62 CallToNonContract(Address),
64 DelegateCallToNonContract(Address),
66}
67
68impl fmt::Display for RevertDiagnostic {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 match self {
71 Self::CallToNonContract(addr) => write!(f, "call to non-contract address {addr}"),
72 Self::DelegateCallToNonContract(addr) => write!(
73 f,
74 "delegatecall to non-contract address {addr} (usually an unliked library)"
75 ),
76 }
77 }
78}
79
80#[derive(Debug, Clone, Deserialize)]
82pub struct SparsedTraceArena {
83 #[serde(flatten)]
85 pub arena: CallTraceArena,
86 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
89 pub ignored: HashMap<(usize, usize), (usize, usize)>,
90 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
92 pub diagnostics: HashMap<usize, RevertDiagnostic>,
93}
94
95impl Serialize for SparsedTraceArena {
96 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
97 where
98 S: serde::Serializer,
99 {
100 #[derive(Serialize)]
101 struct ResolvedArena<'a> {
102 #[serde(flatten)]
103 arena: &'a CallTraceArena,
104 #[serde(skip_serializing_if = "HashMap::is_empty")]
105 ignored: &'a HashMap<(usize, usize), (usize, usize)>,
106 }
107
108 ResolvedArena { arena: &self.resolve_diagnostics(), ignored: &self.ignored }
109 .serialize(serializer)
110 }
111}
112
113impl SparsedTraceArena {
114 fn apply_diagnostics(&self, arena: &mut CallTraceArena) {
116 for (&node_idx, diagnostic) in &self.diagnostics {
117 if let Some(node) = arena.nodes_mut().get_mut(node_idx) {
118 node.trace.decoded.get_or_insert_default().return_data =
119 Some(diagnostic.to_string());
120 }
121 }
122 }
123
124 fn resolve_diagnostics(&self) -> Cow<'_, CallTraceArena> {
126 if self.diagnostics.is_empty() {
127 Cow::Borrowed(&self.arena)
128 } else {
129 let mut arena = self.arena.clone();
130 self.apply_diagnostics(&mut arena);
131 Cow::Owned(arena)
132 }
133 }
134
135 fn resolve_arena(&self) -> Cow<'_, CallTraceArena> {
137 if self.ignored.is_empty() {
138 self.resolve_diagnostics()
139 } else {
140 let mut arena = self.arena.clone();
141
142 fn clear_node(
143 nodes: &mut [CallTraceNode],
144 node_idx: usize,
145 ignored: &HashMap<(usize, usize), (usize, usize)>,
146 cur_ignore_end: &mut Option<(usize, usize)>,
147 ) {
148 let items = std::iter::once(None)
151 .chain(nodes[node_idx].ordering.clone().into_iter().map(Some))
152 .enumerate();
153
154 let mut internal_calls = Vec::new();
155 let mut items_to_remove = BTreeSet::new();
156 for (item_idx, item) in items {
157 if let Some(end_node) = ignored.get(&(node_idx, item_idx)) {
158 *cur_ignore_end = Some(*end_node);
159 }
160
161 let mut remove = cur_ignore_end.is_some() & item.is_some();
162
163 match item {
164 Some(TraceMemberOrder::Call(child_idx)) => {
166 clear_node(
167 nodes,
168 nodes[node_idx].children[child_idx],
169 ignored,
170 cur_ignore_end,
171 );
172 remove &= cur_ignore_end.is_some();
173 }
174 Some(TraceMemberOrder::Step(step_idx)) => {
176 if let Some(decoded) = &nodes[node_idx].trace.steps[step_idx].decoded
178 && let DecodedTraceStep::InternalCall(_, end_step_idx) = &**decoded
179 {
180 internal_calls.push((item_idx, remove, *end_step_idx));
181 remove = false;
183 }
184 internal_calls.retain(|(start_item_idx, remove_start, end_idx)| {
186 if *end_idx != step_idx {
187 return true;
188 }
189 if *remove_start && remove {
191 items_to_remove.insert(*start_item_idx);
192 } else {
193 remove = false;
194 }
195
196 false
197 });
198 }
199 _ => {}
200 }
201
202 if remove {
203 items_to_remove.insert(item_idx);
204 }
205
206 if let Some((end_node, end_step_idx)) = cur_ignore_end
207 && node_idx == *end_node
208 && item_idx == *end_step_idx
209 {
210 *cur_ignore_end = None;
211 }
212 }
213
214 for (offset, item_idx) in items_to_remove.into_iter().enumerate() {
215 nodes[node_idx].ordering.remove(item_idx - offset - 1);
216 }
217 }
218
219 clear_node(arena.nodes_mut(), 0, &self.ignored, &mut None);
220
221 self.apply_diagnostics(&mut arena);
222
223 Cow::Owned(arena)
224 }
225 }
226}
227
228impl Deref for SparsedTraceArena {
229 type Target = CallTraceArena;
230
231 fn deref(&self) -> &Self::Target {
232 &self.arena
233 }
234}
235
236impl DerefMut for SparsedTraceArena {
237 fn deref_mut(&mut self) -> &mut Self::Target {
238 &mut self.arena
239 }
240}
241
242pub async fn decode_trace_arena(arena: &mut CallTraceArena, decoder: &CallTraceDecoder) {
246 decoder.prefetch_signatures(arena.nodes()).await;
247 decoder.populate_traces(arena.nodes_mut()).await;
248}
249
250pub fn render_trace_arena(arena: &SparsedTraceArena) -> String {
252 render_trace_arena_inner(arena, false, false)
253}
254
255pub fn prune_trace_depth(arena: &mut CallTraceArena, depth: usize) {
257 for node in arena.nodes_mut() {
258 if node.trace.depth >= depth {
259 node.ordering.clear();
260 }
261 }
262}
263
264pub fn trace_arena_at_depth(arena: &SparsedTraceArena, depth: usize) -> SparsedTraceArena {
266 let mut arena = arena.resolve_arena().into_owned();
267 let nodes = arena.nodes_mut();
268 let mut reachable = vec![false; nodes.len()];
269 let mut pending = vec![0];
270 while let Some(node_idx) = pending.pop() {
271 if reachable[node_idx] {
272 continue;
273 }
274 reachable[node_idx] = true;
275 let node = &nodes[node_idx];
276 if node.trace.depth < depth {
277 pending.extend(node.ordering.iter().filter_map(|item| match item {
278 TraceMemberOrder::Call(child_idx) => Some(node.children[*child_idx]),
279 _ => None,
280 }));
281 }
282 }
283
284 let mut remapped = vec![None; nodes.len()];
285 for (next_idx, node) in nodes.iter_mut().filter(|node| reachable[node.idx]).enumerate() {
286 remapped[node.idx] = Some(next_idx);
287 if node.trace.depth >= depth {
288 node.ordering.clear();
289 node.children.clear();
290 } else {
291 let mut child_positions = vec![None; node.children.len()];
292 let mut children = Vec::with_capacity(node.children.len());
293 for (old_position, child) in node.children.iter().copied().enumerate() {
294 if reachable[child] {
295 child_positions[old_position] = Some(children.len());
296 children.push(child);
297 }
298 }
299 node.children = children;
300 node.ordering = std::mem::take(&mut node.ordering)
301 .into_iter()
302 .filter_map(|item| match item {
303 TraceMemberOrder::Call(child_idx) => {
304 Some(TraceMemberOrder::Call(child_positions[child_idx]?))
305 }
306 item => Some(item),
307 })
308 .collect();
309 }
310 }
311
312 nodes.retain(|node| reachable[node.idx]);
313 for node in nodes {
314 node.idx = remapped[node.idx].expect("retained trace node has a remapped index");
315 node.parent = node.parent.and_then(|parent| remapped[parent]);
316 for child in &mut node.children {
317 *child = remapped[*child].expect("retained trace child has a remapped index");
318 }
319 }
320
321 SparsedTraceArena { arena, ignored: Default::default(), diagnostics: Default::default() }
322}
323
324pub fn render_trace_arena_inner(
327 arena: &SparsedTraceArena,
328 with_bytecodes: bool,
329 with_storage_changes: bool,
330) -> String {
331 if shell::is_json() {
332 return serde_json::to_string(&arena.resolve_arena()).expect("Failed to serialize traces");
333 }
334
335 let mut resolved = arena.resolve_arena();
336
337 let mut tempo_changes = None;
338 if with_storage_changes {
339 tempo_changes = tempo_channel_storage_decodes(&resolved);
340
341 let needs_dedup = resolved.as_ref().nodes().iter().any(|node| {
342 node.trace.steps.iter().any(|step| {
343 step.storage_change.is_some()
344 && matches!(step.decoded.as_deref(), Some(DecodedTraceStep::Line(_)))
345 })
346 });
347 if needs_dedup {
348 for node in resolved.to_mut().nodes_mut() {
350 for step in &mut node.trace.steps {
351 if step.storage_change.is_some()
352 && matches!(step.decoded.as_deref(), Some(DecodedTraceStep::Line(_)))
353 {
354 step.storage_change = None;
355 }
356 }
357 }
358 }
359 }
360
361 let mut w = TraceWriter::new(Vec::<u8>::new())
362 .color_cheatcodes(true)
363 .use_colors(convert_color_choice(shell::color_choice()))
364 .write_bytecodes(with_bytecodes)
365 .with_storage_changes(with_storage_changes);
366 w.write_arena(resolved.as_ref()).expect("Failed to write traces");
367 let mut rendered =
368 String::from_utf8(w.into_writer()).expect("trace writer wrote invalid UTF-8");
369 if let Some(tempo_changes) = tempo_changes {
370 if !rendered.ends_with('\n') {
371 rendered.push('\n');
372 }
373 rendered.push_str(&tempo_changes);
374 }
375
376 rendered
377}
378
379fn tempo_channel_storage_decodes(arena: &CallTraceArena) -> Option<String> {
380 let decoded_changes = arena
381 .nodes()
382 .iter()
383 .filter(|node| node.trace.address == TIP20_CHANNEL_RESERVE_ADDRESS)
384 .flat_map(compact_channel_storage_changes)
385 .collect::<Vec<_>>();
386
387 if decoded_changes.is_empty() {
388 return None;
389 }
390
391 let mut rendered = String::new();
392 rendered.push_str("Decoded TIP20ChannelReserve storage:\n");
393 for (slot, before, after) in decoded_changes {
394 rendered.push_str(&format!(
395 " @ {}: {} -> {}\n",
396 format_storage_word(slot),
397 format_channel_state(before),
398 format_channel_state(after),
399 ));
400 }
401 Some(rendered)
402}
403
404fn compact_channel_storage_changes(node: &CallTraceNode) -> Vec<(U256, U256, U256)> {
405 let mut changes_map = BTreeMap::new();
406 for step in &node.trace.steps {
407 if let Some(change) = &step.storage_change
408 && change.had_value.is_some()
409 {
410 let (_first, last) = changes_map.entry(change.key).or_insert((&**change, &**change));
411 *last = &**change;
412 }
413 }
414
415 changes_map
416 .into_iter()
417 .filter_map(|(key, (first, last))| {
418 let before = first.had_value.unwrap_or_default();
419 let after = last.value;
420 (before != after).then_some((key, before, after))
421 })
422 .collect()
423}
424
425fn format_channel_state(value: U256) -> String {
426 let (settled, deposit, close_requested_at) = decode_channel_state(value);
427 format!("{{settled: {settled}, deposit: {deposit}, closeRequestedAt: {close_requested_at}}}")
428}
429
430fn decode_channel_state(value: U256) -> (U256, U256, u32) {
431 let mask96 = (U256::from(1) << 96) - U256::from(1);
432 let mask32 = (U256::from(1) << 32) - U256::from(1);
433 let settled: U256 = value & mask96;
434 let deposit: U256 = (value >> 96usize) & mask96;
435 let close_requested_at_word: U256 = (value >> 192usize) & mask32;
436 let close_requested_at = close_requested_at_word.to::<u32>();
437 (settled, deposit, close_requested_at)
438}
439
440fn format_storage_word(value: U256) -> String {
441 if value < U256::from(1_000_000u64) { value.to_string() } else { format!("0x{value:x}") }
442}
443
444const fn convert_color_choice(choice: shell::ColorChoice) -> revm_inspectors::ColorChoice {
445 match choice {
446 shell::ColorChoice::Auto => revm_inspectors::ColorChoice::Auto,
447 shell::ColorChoice::Always => revm_inspectors::ColorChoice::Always,
448 shell::ColorChoice::Never => revm_inspectors::ColorChoice::Never,
449 }
450}
451
452#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
454pub enum TraceKind {
455 Deployment,
456 Setup,
457 Execution,
458}
459
460impl TraceKind {
461 #[must_use]
465 pub const fn is_deployment(self) -> bool {
466 matches!(self, Self::Deployment)
467 }
468
469 #[must_use]
473 pub const fn is_setup(self) -> bool {
474 matches!(self, Self::Setup)
475 }
476
477 #[must_use]
481 pub const fn is_execution(self) -> bool {
482 matches!(self, Self::Execution)
483 }
484}
485
486pub fn load_contracts<'a>(
488 traces: impl IntoIterator<Item = &'a CallTraceArena>,
489 known_contracts: &ContractsByArtifact,
490) -> ContractsByAddress {
491 let mut local_identifier = LocalTraceIdentifier::new(known_contracts);
492 let decoder = CallTraceDecoder::new();
493 let mut contracts = ContractsByAddress::new();
494 for trace in traces {
495 for address in decoder.identify_addresses(trace, &mut local_identifier) {
496 if let (Some(contract), Some(abi)) = (address.contract, address.abi) {
497 contracts.insert(address.address, (contract, abi.into_owned()));
498 }
499 }
500 }
501 contracts
502}
503
504#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
506pub enum InternalTraceMode {
507 #[default]
508 None,
509 Simple,
511 Full,
513}
514
515#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
517pub enum StepRecording {
518 #[default]
520 None,
521 Jumps,
523 All,
525}
526
527impl StepRecording {
528 const fn merge(self, other: Self) -> Self {
529 match (self, other) {
530 (Self::All, _) | (_, Self::All) => Self::All,
531 (Self::Jumps, _) | (_, Self::Jumps) => Self::Jumps,
532 (Self::None, Self::None) => Self::None,
533 }
534 }
535}
536
537#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
539pub struct TraceRequirements {
540 calls: bool,
541 steps: StepRecording,
542 memory_snapshots: bool,
543 stack_snapshots: bool,
544 returndata_snapshots: bool,
545 immediate_bytes: bool,
546 state_diff: bool,
547}
548
549impl TraceRequirements {
550 pub const fn none() -> Self {
551 Self {
552 calls: false,
553 steps: StepRecording::None,
554 memory_snapshots: false,
555 stack_snapshots: false,
556 returndata_snapshots: false,
557 immediate_bytes: false,
558 state_diff: false,
559 }
560 }
561
562 pub const fn with_calls(mut self, yes: bool) -> Self {
563 self.calls |= yes;
564 self
565 }
566
567 pub const fn merge(mut self, other: Self) -> Self {
568 self.calls |= other.calls;
569 self.steps = self.steps.merge(other.steps);
570 self.memory_snapshots |= other.memory_snapshots;
571 self.stack_snapshots |= other.stack_snapshots;
572 self.returndata_snapshots |= other.returndata_snapshots;
573 self.immediate_bytes |= other.immediate_bytes;
574 self.state_diff |= other.state_diff;
575 self
576 }
577
578 pub const fn with_steps(mut self, steps: StepRecording) -> Self {
579 self.steps = self.steps.merge(steps);
580 self
581 }
582
583 pub const fn with_memory_snapshots(mut self, yes: bool) -> Self {
584 self.memory_snapshots |= yes;
585 self
586 }
587
588 pub const fn with_stack_snapshots(mut self, yes: bool) -> Self {
589 self.stack_snapshots |= yes;
590 self
591 }
592
593 pub const fn with_debug(mut self, yes: bool) -> Self {
594 if yes {
595 self.calls = true;
596 self.steps = StepRecording::All;
597 self.memory_snapshots = true;
598 self.stack_snapshots = true;
599 self.returndata_snapshots = true;
600 self.immediate_bytes = true;
601 self.state_diff = true;
602 }
603 self
604 }
605
606 pub const fn with_decode_internal(self, mode: InternalTraceMode) -> Self {
607 match mode {
608 InternalTraceMode::None => self,
609 InternalTraceMode::Simple => {
610 self.with_calls(true).with_steps(StepRecording::Jumps).with_stack_snapshots(true)
611 }
612 InternalTraceMode::Full => self
613 .with_calls(true)
614 .with_steps(StepRecording::Jumps)
615 .with_memory_snapshots(true)
616 .with_stack_snapshots(true),
617 }
618 }
619
620 pub const fn with_all_steps(self, yes: bool) -> Self {
621 if yes { self.with_calls(true).with_steps(StepRecording::All) } else { self }
622 }
623
624 pub const fn with_state_changes(mut self, yes: bool) -> Self {
625 self.state_diff |= yes;
626 if yes {
627 self.calls = true;
628 }
629 self
630 }
631
632 pub const fn with_verbosity(self, verbosity: u8) -> Self {
633 match verbosity {
634 0..3 => self,
635 3..=4 => self.with_calls(true),
636 _ if matches!(self.steps, StepRecording::All) => self.with_calls(true),
637 _ => self.with_state_changes(true),
638 }
639 }
640
641 pub fn into_config(self) -> Option<TracingInspectorConfig> {
642 if !self.calls && self.steps == StepRecording::None && !self.state_diff {
643 return None;
644 }
645
646 let steps = if self.state_diff { StepRecording::All } else { self.steps };
647 TracingInspectorConfig {
648 record_steps: steps != StepRecording::None,
649 record_memory_snapshots: self.memory_snapshots,
650 record_stack_snapshots: if self.stack_snapshots {
651 StackSnapshotType::Full
652 } else {
653 StackSnapshotType::None
654 },
655 record_logs: true,
656 record_state_diff: self.state_diff,
657 record_returndata_snapshots: self.returndata_snapshots,
658 record_opcodes_filter: match steps {
659 StepRecording::None | StepRecording::All => None,
660 StepRecording::Jumps => {
661 Some(OpcodeFilter::new().enabled(OpCode::JUMP).enabled(OpCode::JUMPDEST))
662 }
663 },
664 exclude_precompile_calls: false,
665 record_immediate_bytes: self.immediate_bytes,
666 }
667 .into()
668 }
669}
670
671#[cfg(test)]
672mod tests {
673 use super::*;
674 use alloy_primitives::Bytes;
675 use revm::interpreter::InstructionResult;
676 use revm_inspectors::tracing::types::{CallTraceStep, StorageChange, StorageChangeReason};
677
678 #[test]
679 fn trace_depth_projection_removes_and_reindexes_nodes() {
680 let mut arena = CallTraceArena::default();
681 arena.nodes_mut().extend([
682 CallTraceNode {
683 parent: Some(0),
684 children: vec![2],
685 idx: 1,
686 trace: CallTrace { depth: 1, ..Default::default() },
687 ordering: vec![TraceMemberOrder::Call(0)],
688 ..Default::default()
689 },
690 CallTraceNode {
691 parent: Some(1),
692 idx: 2,
693 trace: CallTrace { depth: 2, ..Default::default() },
694 ..Default::default()
695 },
696 CallTraceNode {
697 parent: Some(0),
698 idx: 3,
699 trace: CallTrace { depth: 1, ..Default::default() },
700 ..Default::default()
701 },
702 ]);
703 let root = &mut arena.nodes_mut()[0];
704 root.children = vec![1, 3];
705 root.ordering = vec![TraceMemberOrder::Call(0), TraceMemberOrder::Call(1)];
706 let arena = SparsedTraceArena {
707 arena,
708 ignored: Default::default(),
709 diagnostics: Default::default(),
710 };
711
712 let arena = trace_arena_at_depth(&arena, 1);
713
714 assert_eq!(arena.nodes().len(), 3);
715 assert_eq!(arena.nodes()[0].children, [1, 2]);
716 assert_eq!(arena.nodes()[1].idx, 1);
717 assert!(arena.nodes()[1].children.is_empty());
718 assert!(arena.nodes()[1].ordering.is_empty());
719 assert_eq!(arena.nodes()[2].idx, 2);
720 assert_eq!(arena.nodes()[2].parent, Some(0));
721 assert!(arena.ignored.is_empty());
722 }
723
724 #[test]
725 fn decodes_tip1034_packed_channel_state() {
726 let settled = U256::from(123u64);
727 let deposit = U256::from(456u64);
728 let close_requested_at = U256::from(1_780_495_200u64);
729 let packed = settled | (deposit << 96usize) | (close_requested_at << 192usize);
730
731 assert_eq!(decode_channel_state(packed), (settled, deposit, 1_780_495_200));
732 assert_eq!(
733 format_channel_state(packed),
734 "{settled: 123, deposit: 456, closeRequestedAt: 1780495200}"
735 );
736 }
737
738 #[test]
739 fn tempo_storage_decodes_do_not_insert_extra_blank_line() {
740 let mut arena = CallTraceArena::default();
741 let root = &mut arena.nodes_mut()[0];
742 root.ordering.push(TraceMemberOrder::Step(0));
743 root.trace = CallTrace {
744 address: TIP20_CHANNEL_RESERVE_ADDRESS,
745 success: true,
746 steps: vec![CallTraceStep {
747 pc: 0,
748 op: OpCode::SSTORE,
749 stack: None,
750 push_stack: None,
751 memory: None,
752 returndata: Bytes::new(),
753 gas_remaining: 0,
754 gas_refund_counter: 0,
755 gas_used: 0,
756 gas_cost: 0,
757 storage_change: Some(Box::new(StorageChange {
758 key: U256::from(1),
759 value: U256::from(2),
760 had_value: Some(U256::from(1)),
761 reason: StorageChangeReason::SSTORE,
762 })),
763 status: Some(InstructionResult::Stop),
764 immediate_bytes: None,
765 decoded: None,
766 }],
767 ..Default::default()
768 };
769
770 let rendered = render_trace_arena_inner(
771 &SparsedTraceArena {
772 arena,
773 ignored: Default::default(),
774 diagnostics: Default::default(),
775 },
776 false,
777 true,
778 );
779
780 assert!(rendered.contains("\nDecoded TIP20ChannelReserve storage:\n"));
781 assert!(!rendered.contains("\n\nDecoded TIP20ChannelReserve storage:\n"));
782 }
783
784 #[test]
785 fn revert_diagnostic_only_changes_resolved_trace() {
786 let traces = SparsedTraceArena {
787 arena: CallTraceArena::default(),
788 ignored: Default::default(),
789 diagnostics: HashMap::from_iter([(
790 0,
791 RevertDiagnostic::CallToNonContract(alloy_primitives::Address::ZERO),
792 )]),
793 };
794
795 let resolved = traces.resolve_arena();
796 assert_eq!(
797 resolved.nodes()[0].trace.decoded.as_ref().unwrap().return_data.as_deref(),
798 Some("call to non-contract address 0x0000000000000000000000000000000000000000")
799 );
800 assert!(resolved.nodes()[0].trace.output.is_empty());
801 assert!(traces.arena.nodes()[0].trace.decoded.is_none());
802 assert!(traces.arena.nodes()[0].trace.output.is_empty());
803 }
804
805 #[test]
806 fn serialization_resolves_diagnostics_without_consuming_ignored_ranges() {
807 let mut arena = CallTraceArena::default();
808 let root = &mut arena.nodes_mut()[0];
809 root.logs = vec![CallLog::default(), CallLog::default(), CallLog::default()];
810 root.ordering =
811 vec![TraceMemberOrder::Log(0), TraceMemberOrder::Log(1), TraceMemberOrder::Log(2)];
812
813 let traces = SparsedTraceArena {
814 arena,
815 ignored: HashMap::from_iter([((0, 1), (0, 2))]),
816 diagnostics: HashMap::from_iter([(
817 0,
818 RevertDiagnostic::CallToNonContract(alloy_primitives::Address::ZERO),
819 )]),
820 };
821
822 let serialized = ron::to_string(&traces).unwrap();
823 let deserialized: SparsedTraceArena = ron::from_str(&serialized).unwrap();
824 assert_eq!(
825 deserialized.arena.nodes()[0].ordering,
826 [TraceMemberOrder::Log(0), TraceMemberOrder::Log(1), TraceMemberOrder::Log(2)]
827 );
828 assert_eq!(deserialized.ignored, traces.ignored);
829 assert!(deserialized.diagnostics.is_empty());
830 assert_eq!(
831 deserialized.arena.nodes()[0].trace.decoded.as_ref().unwrap().return_data.as_deref(),
832 Some("call to non-contract address 0x0000000000000000000000000000000000000000")
833 );
834 assert!(deserialized.arena.nodes()[0].trace.output.is_empty());
835
836 let resolved = deserialized.resolve_arena();
837 assert_eq!(resolved.nodes()[0].ordering, [TraceMemberOrder::Log(2)]);
838 assert_eq!(
839 resolved.nodes()[0].trace.decoded.as_ref().unwrap().return_data.as_deref(),
840 Some("call to non-contract address 0x0000000000000000000000000000000000000000")
841 );
842 assert!(render_trace_arena(&deserialized).contains("call to non-contract address"));
843 }
844
845 #[test]
846 fn verbosity_0_through_2_is_noop() {
847 for v in 0..=2 {
848 assert_eq!(
849 TraceRequirements::none().with_verbosity(v),
850 TraceRequirements::none(),
851 "v={v}"
852 );
853 assert_eq!(
854 TraceRequirements::none().with_calls(true).with_verbosity(v),
855 TraceRequirements::none().with_calls(true),
856 "v={v}"
857 );
858 assert_eq!(
859 TraceRequirements::none().with_debug(true).with_verbosity(v),
860 TraceRequirements::none().with_debug(true),
861 "v={v}"
862 );
863 }
864 }
865
866 #[test]
867 fn verbosity_3_and_4_raises_to_call() {
868 for v in 3..=4 {
869 assert_eq!(
870 TraceRequirements::none().with_verbosity(v),
871 TraceRequirements::none().with_calls(true),
872 "v={v}"
873 );
874 assert_eq!(
875 TraceRequirements::none().with_debug(true).with_verbosity(v),
876 TraceRequirements::none().with_debug(true),
877 "v={v}"
878 );
879 assert_eq!(
880 TraceRequirements::none().with_state_changes(true).with_verbosity(v),
881 TraceRequirements::none().with_state_changes(true),
882 "v={v}"
883 );
884 }
885 }
886
887 #[test]
888 fn verbosity_5_raises_to_record_state_diff() {
889 let state_changes = TraceRequirements::none().with_state_changes(true);
890
891 assert_eq!(TraceRequirements::none().with_verbosity(5), state_changes);
892 assert_eq!(TraceRequirements::none().with_calls(true).with_verbosity(5), state_changes);
893 let cfg = TraceRequirements::none()
894 .with_calls(true)
895 .with_steps(StepRecording::Jumps)
896 .with_verbosity(5)
897 .into_config()
898 .unwrap();
899 assert!(cfg.record_state_diff);
900 assert!(cfg.record_opcodes_filter.is_none());
901 assert_eq!(
902 TraceRequirements::none().with_debug(true).with_verbosity(5),
903 TraceRequirements::none().with_debug(true)
904 );
905 assert_eq!(
906 TraceRequirements::none().with_state_changes(true).with_verbosity(5),
907 state_changes
908 );
909 }
910
911 #[test]
912 fn config_at_verbosity_0_is_none() {
913 assert!(TraceRequirements::none().with_verbosity(0).into_config().is_none());
914 }
915
916 #[test]
917 fn config_at_verbosity_3_records_calls_only() {
918 let cfg = TraceRequirements::none().with_verbosity(3).into_config().unwrap();
919 assert!(!cfg.record_steps, "verbosity 3 should not record steps");
920 assert!(!cfg.record_state_diff, "verbosity 3 should not record state diff");
921 assert!(cfg.record_logs, "verbosity 3 should record logs");
922 }
923
924 #[test]
925 fn config_at_verbosity_5_records_steps_and_state_diff() {
926 let cfg = TraceRequirements::none().with_verbosity(5).into_config().unwrap();
927 assert!(cfg.record_steps, "verbosity 5 must record steps for backtraces");
928 assert!(cfg.record_state_diff, "verbosity 5 must record state diff");
929 assert!(cfg.record_logs, "verbosity 5 must record logs");
930 assert!(!cfg.record_memory_snapshots, "verbosity 5 should not record memory snapshots");
932 assert_eq!(
933 cfg.record_stack_snapshots,
934 StackSnapshotType::None,
935 "verbosity 5 should not record stack snapshots"
936 );
937 assert!(
939 cfg.record_opcodes_filter.is_none(),
940 "verbosity 5 needs unfiltered opcodes for state diff"
941 );
942 }
943
944 #[test]
945 fn config_debug_mode_unchanged() {
946 let cfg = TraceRequirements::none().with_debug(true).into_config().unwrap();
948 assert!(cfg.record_steps);
949 assert!(cfg.record_memory_snapshots, "Debug must record memory snapshots");
950 assert_eq!(
951 cfg.record_stack_snapshots,
952 StackSnapshotType::Full,
953 "Debug must record full stack snapshots"
954 );
955 assert!(cfg.record_returndata_snapshots, "Debug must record returndata");
956 assert!(cfg.record_immediate_bytes, "Debug must record immediate bytes");
957 assert!(cfg.record_opcodes_filter.is_none(), "Debug must record all opcodes (no filter)");
958 assert!(cfg.record_state_diff, "Debug should record storage accesses for the debugger");
959 }
960
961 #[test]
962 fn requirements_preserve_internal_decode_with_state_diff() {
963 let cfg = TraceRequirements::none()
964 .with_decode_internal(InternalTraceMode::Full)
965 .with_state_changes(true)
966 .into_config()
967 .unwrap();
968
969 assert!(cfg.record_steps, "requirements should record opcode steps");
970 assert!(cfg.record_memory_snapshots, "Full internal decoding needs memory snapshots");
971 assert_eq!(
972 cfg.record_stack_snapshots,
973 StackSnapshotType::Full,
974 "internal decoding needs stack snapshots"
975 );
976 assert!(cfg.record_state_diff, "state changes should be recorded");
977 assert!(cfg.record_opcodes_filter.is_none(), "state diff needs unfiltered opcodes");
978 }
979
980 #[test]
981 fn requirements_all_steps_avoid_debug_snapshots() {
982 let cfg =
983 TraceRequirements::none().with_all_steps(true).with_verbosity(5).into_config().unwrap();
984
985 assert!(cfg.record_steps, "all steps must record opcode steps");
986 assert!(cfg.record_opcodes_filter.is_none(), "all steps must record every opcode step");
987 assert!(!cfg.record_memory_snapshots, "all steps should not record memory snapshots");
988 assert_eq!(
989 cfg.record_stack_snapshots,
990 StackSnapshotType::None,
991 "all steps should not record stack snapshots"
992 );
993 assert!(!cfg.record_returndata_snapshots, "all steps should not record returndata");
994 assert!(!cfg.record_immediate_bytes, "all steps should not record immediate bytes");
995 assert!(!cfg.record_state_diff, "all steps should not record state diffs");
996 }
997}