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