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