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