1#![cfg_attr(not(test), warn(unused_crate_dependencies))]
6#![cfg_attr(docsrs, feature(doc_cfg))]
7
8#[macro_use]
9extern crate foundry_common;
10
11#[macro_use]
12extern crate tracing;
13
14use foundry_common::{
15 contracts::{ContractsByAddress, ContractsByArtifact},
16 shell,
17};
18use revm::bytecode::opcode::OpCode;
19use revm_inspectors::tracing::{
20 OpcodeFilter,
21 types::{DecodedTraceStep, TraceMemberOrder},
22};
23use serde::{Deserialize, Serialize};
24use std::{
25 borrow::Cow,
26 collections::{BTreeMap, BTreeSet},
27 ops::{Deref, DerefMut},
28};
29
30use alloy_primitives::{U256, map::HashMap};
31use tempo_contracts::precompiles::TIP20_CHANNEL_RESERVE_ADDRESS;
32
33pub use revm_inspectors::tracing::{
34 CallTraceArena, FourByteInspector, GethTraceBuilder, ParityTraceBuilder, StackSnapshotType,
35 TraceWriter, TracingInspector, TracingInspectorConfig,
36 types::{
37 CallKind, CallLog, CallTrace, CallTraceNode, DecodedCallData, DecodedCallLog,
38 DecodedCallTrace,
39 },
40};
41
42pub mod identifier;
46use identifier::LocalTraceIdentifier;
47
48mod decoder;
49pub use decoder::{CallTraceDecoder, CallTraceDecoderBuilder};
50
51pub mod debug;
52pub use debug::DebugTraceIdentifier;
53
54pub mod folded_stack_trace;
55
56pub mod backtrace;
57
58pub type Traces = Vec<(TraceKind, SparsedTraceArena)>;
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct SparsedTraceArena {
63 #[serde(flatten)]
65 pub arena: CallTraceArena,
66 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
69 pub ignored: HashMap<(usize, usize), (usize, usize)>,
70}
71
72impl SparsedTraceArena {
73 fn resolve_arena(&self) -> Cow<'_, CallTraceArena> {
75 if self.ignored.is_empty() {
76 Cow::Borrowed(&self.arena)
77 } else {
78 let mut arena = self.arena.clone();
79
80 fn clear_node(
81 nodes: &mut [CallTraceNode],
82 node_idx: usize,
83 ignored: &HashMap<(usize, usize), (usize, usize)>,
84 cur_ignore_end: &mut Option<(usize, usize)>,
85 ) {
86 let items = std::iter::once(None)
89 .chain(nodes[node_idx].ordering.clone().into_iter().map(Some))
90 .enumerate();
91
92 let mut internal_calls = Vec::new();
93 let mut items_to_remove = BTreeSet::new();
94 for (item_idx, item) in items {
95 if let Some(end_node) = ignored.get(&(node_idx, item_idx)) {
96 *cur_ignore_end = Some(*end_node);
97 }
98
99 let mut remove = cur_ignore_end.is_some() & item.is_some();
100
101 match item {
102 Some(TraceMemberOrder::Call(child_idx)) => {
104 clear_node(
105 nodes,
106 nodes[node_idx].children[child_idx],
107 ignored,
108 cur_ignore_end,
109 );
110 remove &= cur_ignore_end.is_some();
111 }
112 Some(TraceMemberOrder::Step(step_idx)) => {
114 if let Some(decoded) = &nodes[node_idx].trace.steps[step_idx].decoded
116 && let DecodedTraceStep::InternalCall(_, end_step_idx) = &**decoded
117 {
118 internal_calls.push((item_idx, remove, *end_step_idx));
119 remove = false;
121 }
122 internal_calls.retain(|(start_item_idx, remove_start, end_idx)| {
124 if *end_idx != step_idx {
125 return true;
126 }
127 if *remove_start && remove {
129 items_to_remove.insert(*start_item_idx);
130 } else {
131 remove = false;
132 }
133
134 false
135 });
136 }
137 _ => {}
138 }
139
140 if remove {
141 items_to_remove.insert(item_idx);
142 }
143
144 if let Some((end_node, end_step_idx)) = cur_ignore_end
145 && node_idx == *end_node
146 && item_idx == *end_step_idx
147 {
148 *cur_ignore_end = None;
149 }
150 }
151
152 for (offset, item_idx) in items_to_remove.into_iter().enumerate() {
153 nodes[node_idx].ordering.remove(item_idx - offset - 1);
154 }
155 }
156
157 clear_node(arena.nodes_mut(), 0, &self.ignored, &mut None);
158
159 Cow::Owned(arena)
160 }
161 }
162}
163
164impl Deref for SparsedTraceArena {
165 type Target = CallTraceArena;
166
167 fn deref(&self) -> &Self::Target {
168 &self.arena
169 }
170}
171
172impl DerefMut for SparsedTraceArena {
173 fn deref_mut(&mut self) -> &mut Self::Target {
174 &mut self.arena
175 }
176}
177
178pub async fn decode_trace_arena(arena: &mut CallTraceArena, decoder: &CallTraceDecoder) {
182 decoder.prefetch_signatures(arena.nodes()).await;
183 decoder.populate_traces(arena.nodes_mut()).await;
184}
185
186pub fn render_trace_arena(arena: &SparsedTraceArena) -> String {
188 render_trace_arena_inner(arena, false, false)
189}
190
191pub fn prune_trace_depth(arena: &mut CallTraceArena, depth: usize) {
193 for node in arena.nodes_mut() {
194 if node.trace.depth >= depth {
195 node.ordering.clear();
196 }
197 }
198}
199
200pub fn render_trace_arena_inner(
203 arena: &SparsedTraceArena,
204 with_bytecodes: bool,
205 with_storage_changes: bool,
206) -> String {
207 if shell::is_json() {
208 return serde_json::to_string(&arena.resolve_arena()).expect("Failed to serialize traces");
209 }
210
211 let resolved = arena.resolve_arena();
212 let mut w = TraceWriter::new(Vec::<u8>::new())
213 .color_cheatcodes(true)
214 .use_colors(convert_color_choice(shell::color_choice()))
215 .write_bytecodes(with_bytecodes)
216 .with_storage_changes(with_storage_changes);
217 w.write_arena(&resolved).expect("Failed to write traces");
218 let mut rendered =
219 String::from_utf8(w.into_writer()).expect("trace writer wrote invalid UTF-8");
220 if with_storage_changes {
221 append_tempo_channel_storage_decodes(&mut rendered, &resolved);
222 }
223 rendered
224}
225
226fn append_tempo_channel_storage_decodes(rendered: &mut String, arena: &CallTraceArena) {
227 let decoded_changes = arena
228 .nodes()
229 .iter()
230 .filter(|node| node.trace.address == TIP20_CHANNEL_RESERVE_ADDRESS)
231 .flat_map(compact_channel_storage_changes)
232 .collect::<Vec<_>>();
233
234 if decoded_changes.is_empty() {
235 return;
236 }
237
238 if !rendered.ends_with('\n') {
239 rendered.push('\n');
240 }
241 rendered.push_str("Decoded TIP20ChannelReserve storage:\n");
242 for (slot, before, after) in decoded_changes {
243 rendered.push_str(&format!(
244 " @ {}: {} -> {}\n",
245 format_storage_word(slot),
246 format_channel_state(before),
247 format_channel_state(after),
248 ));
249 }
250}
251
252fn compact_channel_storage_changes(node: &CallTraceNode) -> Vec<(U256, U256, U256)> {
253 let mut changes_map = BTreeMap::new();
254 for step in &node.trace.steps {
255 if let Some(change) = &step.storage_change
256 && change.had_value.is_some()
257 {
258 let (_first, last) = changes_map.entry(change.key).or_insert((&**change, &**change));
259 *last = &**change;
260 }
261 }
262
263 changes_map
264 .into_iter()
265 .filter_map(|(key, (first, last))| {
266 let before = first.had_value.unwrap_or_default();
267 let after = last.value;
268 (before != after).then_some((key, before, after))
269 })
270 .collect()
271}
272
273fn format_channel_state(value: U256) -> String {
274 let (settled, deposit, close_requested_at) = decode_channel_state(value);
275 format!("{{settled: {settled}, deposit: {deposit}, closeRequestedAt: {close_requested_at}}}")
276}
277
278fn decode_channel_state(value: U256) -> (U256, U256, u32) {
279 let mask96 = (U256::from(1) << 96) - U256::from(1);
280 let mask32 = (U256::from(1) << 32) - U256::from(1);
281 let settled: U256 = value & mask96;
282 let deposit: U256 = (value >> 96usize) & mask96;
283 let close_requested_at_word: U256 = (value >> 192usize) & mask32;
284 let close_requested_at = close_requested_at_word.to::<u32>();
285 (settled, deposit, close_requested_at)
286}
287
288fn format_storage_word(value: U256) -> String {
289 if value < U256::from(1_000_000u64) { value.to_string() } else { format!("0x{value:x}") }
290}
291
292const fn convert_color_choice(choice: shell::ColorChoice) -> revm_inspectors::ColorChoice {
293 match choice {
294 shell::ColorChoice::Auto => revm_inspectors::ColorChoice::Auto,
295 shell::ColorChoice::Always => revm_inspectors::ColorChoice::Always,
296 shell::ColorChoice::Never => revm_inspectors::ColorChoice::Never,
297 }
298}
299
300#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
302pub enum TraceKind {
303 Deployment,
304 Setup,
305 Execution,
306}
307
308impl TraceKind {
309 #[must_use]
313 pub const fn is_deployment(self) -> bool {
314 matches!(self, Self::Deployment)
315 }
316
317 #[must_use]
321 pub const fn is_setup(self) -> bool {
322 matches!(self, Self::Setup)
323 }
324
325 #[must_use]
329 pub const fn is_execution(self) -> bool {
330 matches!(self, Self::Execution)
331 }
332}
333
334pub fn load_contracts<'a>(
336 traces: impl IntoIterator<Item = &'a CallTraceArena>,
337 known_contracts: &ContractsByArtifact,
338) -> ContractsByAddress {
339 let mut local_identifier = LocalTraceIdentifier::new(known_contracts);
340 let decoder = CallTraceDecoder::new();
341 let mut contracts = ContractsByAddress::new();
342 for trace in traces {
343 for address in decoder.identify_addresses(trace, &mut local_identifier) {
344 if let (Some(contract), Some(abi)) = (address.contract, address.abi) {
345 contracts.insert(address.address, (contract, abi.into_owned()));
346 }
347 }
348 }
349 contracts
350}
351
352#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
354pub enum InternalTraceMode {
355 #[default]
356 None,
357 Simple,
359 Full,
361}
362
363impl From<InternalTraceMode> for TraceMode {
364 fn from(mode: InternalTraceMode) -> Self {
365 match mode {
366 InternalTraceMode::None => Self::None,
367 InternalTraceMode::Simple => Self::JumpSimple,
368 InternalTraceMode::Full => Self::Jump,
369 }
370 }
371}
372
373#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
375pub enum TraceMode {
376 #[default]
378 None,
379 Call,
381 Steps,
385 JumpSimple,
389 Jump,
393 Debug,
397 RecordStateDiff,
402}
403
404impl TraceMode {
405 pub const fn is_none(self) -> bool {
406 matches!(self, Self::None)
407 }
408
409 pub const fn is_call(self) -> bool {
410 matches!(self, Self::Call)
411 }
412
413 pub const fn is_steps(self) -> bool {
414 matches!(self, Self::Steps)
415 }
416
417 pub const fn is_jump_simple(self) -> bool {
418 matches!(self, Self::JumpSimple)
419 }
420
421 pub const fn is_jump(self) -> bool {
422 matches!(self, Self::Jump)
423 }
424
425 pub const fn record_state_diff(self) -> bool {
426 matches!(self, Self::RecordStateDiff)
427 }
428
429 pub const fn is_debug(self) -> bool {
430 matches!(self, Self::Debug)
431 }
432
433 pub fn with_debug(self, yes: bool) -> Self {
434 if yes { std::cmp::max(self, Self::Debug) } else { self }
435 }
436
437 pub fn with_decode_internal(self, mode: InternalTraceMode) -> Self {
438 std::cmp::max(self, mode.into())
439 }
440
441 pub fn with_state_changes(self, yes: bool) -> Self {
442 if yes { std::cmp::max(self, Self::RecordStateDiff) } else { self }
443 }
444
445 pub fn with_verbosity(self, verbosity: u8) -> Self {
446 match verbosity {
447 0..3 => self,
448 3..=4 => std::cmp::max(self, Self::Call),
449 _ => std::cmp::max(self, Self::RecordStateDiff),
452 }
453 }
454
455 pub fn into_config(self) -> Option<TracingInspectorConfig> {
456 if self.is_none() {
457 None
458 } else {
459 let effective = if self.record_state_diff() { Self::Steps } else { self };
464 TracingInspectorConfig {
465 record_steps: self >= Self::Steps,
466 record_memory_snapshots: effective >= Self::Jump,
467 record_stack_snapshots: if effective > Self::Steps {
468 StackSnapshotType::Full
469 } else {
470 StackSnapshotType::None
471 },
472 record_logs: true,
473 record_state_diff: self.record_state_diff(),
474 record_returndata_snapshots: effective.is_debug(),
475 record_opcodes_filter: if self.record_state_diff() {
477 None
478 } else {
479 (effective.is_steps() || effective.is_jump() || effective.is_jump_simple())
480 .then(|| {
481 OpcodeFilter::new().enabled(OpCode::JUMP).enabled(OpCode::JUMPDEST)
482 })
483 },
484 exclude_precompile_calls: false,
485 record_immediate_bytes: effective.is_debug(),
486 }
487 .into()
488 }
489 }
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495
496 #[test]
497 fn decodes_tip1034_packed_channel_state() {
498 let settled = U256::from(123u64);
499 let deposit = U256::from(456u64);
500 let close_requested_at = U256::from(1_780_495_200u64);
501 let packed = settled | (deposit << 96usize) | (close_requested_at << 192usize);
502
503 assert_eq!(decode_channel_state(packed), (settled, deposit, 1_780_495_200));
504 assert_eq!(
505 format_channel_state(packed),
506 "{settled: 123, deposit: 456, closeRequestedAt: 1780495200}"
507 );
508 }
509
510 #[test]
513 fn verbosity_0_through_2_is_noop() {
514 for v in 0..=2 {
515 assert_eq!(TraceMode::None.with_verbosity(v), TraceMode::None, "v={v}");
516 assert_eq!(TraceMode::Call.with_verbosity(v), TraceMode::Call, "v={v}");
517 assert_eq!(TraceMode::Debug.with_verbosity(v), TraceMode::Debug, "v={v}");
518 }
519 }
520
521 #[test]
522 fn verbosity_3_and_4_raises_to_call() {
523 for v in 3..=4 {
524 assert_eq!(TraceMode::None.with_verbosity(v), TraceMode::Call, "v={v}");
525 assert_eq!(TraceMode::Debug.with_verbosity(v), TraceMode::Debug, "v={v}");
527 assert_eq!(
528 TraceMode::RecordStateDiff.with_verbosity(v),
529 TraceMode::RecordStateDiff,
530 "v={v}"
531 );
532 }
533 }
534
535 #[test]
536 fn verbosity_5_raises_to_record_state_diff() {
537 assert_eq!(TraceMode::None.with_verbosity(5), TraceMode::RecordStateDiff);
538 assert_eq!(TraceMode::Call.with_verbosity(5), TraceMode::RecordStateDiff);
539 assert_eq!(TraceMode::Steps.with_verbosity(5), TraceMode::RecordStateDiff);
540 assert_eq!(TraceMode::Debug.with_verbosity(5), TraceMode::RecordStateDiff);
541 assert_eq!(TraceMode::RecordStateDiff.with_verbosity(5), TraceMode::RecordStateDiff);
543 }
544
545 #[test]
548 fn config_at_verbosity_0_is_none() {
549 let mode = TraceMode::None.with_verbosity(0);
550 assert!(mode.into_config().is_none());
551 }
552
553 #[test]
554 fn config_at_verbosity_3_records_calls_only() {
555 let cfg = TraceMode::None.with_verbosity(3).into_config().unwrap();
556 assert!(!cfg.record_steps, "verbosity 3 should not record steps");
557 assert!(!cfg.record_state_diff, "verbosity 3 should not record state diff");
558 assert!(cfg.record_logs, "verbosity 3 should record logs");
559 }
560
561 #[test]
562 fn config_at_verbosity_5_records_steps_and_state_diff() {
563 let cfg = TraceMode::None.with_verbosity(5).into_config().unwrap();
564 assert!(cfg.record_steps, "verbosity 5 must record steps for backtraces");
565 assert!(cfg.record_state_diff, "verbosity 5 must record state diff");
566 assert!(cfg.record_logs, "verbosity 5 must record logs");
567 assert!(!cfg.record_memory_snapshots, "verbosity 5 should not record memory snapshots");
569 assert_eq!(
570 cfg.record_stack_snapshots,
571 StackSnapshotType::None,
572 "verbosity 5 should not record stack snapshots"
573 );
574 assert!(
576 cfg.record_opcodes_filter.is_none(),
577 "verbosity 5 needs unfiltered opcodes for state diff"
578 );
579 }
580
581 #[test]
582 fn config_debug_mode_unchanged() {
583 let cfg = TraceMode::Debug.into_config().unwrap();
585 assert!(cfg.record_steps);
586 assert!(cfg.record_memory_snapshots, "Debug must record memory snapshots");
587 assert_eq!(
588 cfg.record_stack_snapshots,
589 StackSnapshotType::Full,
590 "Debug must record full stack snapshots"
591 );
592 assert!(cfg.record_returndata_snapshots, "Debug must record returndata");
593 assert!(cfg.record_immediate_bytes, "Debug must record immediate bytes");
594 assert!(cfg.record_opcodes_filter.is_none(), "Debug must record all opcodes (no filter)");
595 assert!(!cfg.record_state_diff, "Debug alone should not record state diff");
596 }
597}