anvil_zksync_traces/
lib.rs

1use anvil_zksync_common::address_map::{self, KNOWN_ADDRESSES};
2use anvil_zksync_types::traces::{
3    CallLog, CallTrace, CallTraceArena, CallTraceNode, DecodedCallEvent, DecodedCallTrace,
4    ExecutionResult, L2L1Log, L2L1Logs, TraceMemberOrder,
5};
6use decode::CallTraceDecoder;
7use writer::TraceWriter;
8use zksync_multivm::interface::{Call, Halt, VmExecutionResultAndLogs};
9use zksync_types::H160;
10
11pub mod abi_utils;
12pub mod decode;
13pub mod format;
14pub mod identifier;
15pub mod writer;
16
17/// Converts a single call into a CallTrace.
18#[inline]
19fn convert_call_to_call_trace(call: &Call) -> CallTrace {
20    let label = KNOWN_ADDRESSES
21        .get(&call.to)
22        .map(|known| known.name.clone());
23
24    // Determine the execution result based on individual call
25    let execution_result = if let Some(ref revert_reason) = call.revert_reason {
26        ExecutionResult::Revert {
27            output: revert_reason.clone(),
28        }
29    } else if let Some(ref err) = call.error {
30        ExecutionResult::Halt {
31            reason: Halt::TracerCustom(err.to_string()),
32        }
33    } else {
34        ExecutionResult::Success {
35            output: call.output.clone(),
36        }
37    };
38
39    CallTrace {
40        success: !execution_result.is_failed(),
41        caller: call.from,
42        address: call.to,
43        execution_result,
44        decoded: DecodedCallTrace {
45            label,
46            ..Default::default()
47        },
48        call: call.clone(),
49    }
50}
51
52/// Builds a call trace arena from the calls and transaction result.
53pub fn build_call_trace_arena(
54    calls: &[Call],
55    tx_result: &VmExecutionResultAndLogs,
56) -> CallTraceArena {
57    let mut arena = CallTraceArena::default();
58
59    // Update the root node's execution result.
60    if let Some(root_node) = arena.arena.get_mut(0) {
61        root_node.trace.execution_result = tx_result.result.clone().into();
62    }
63
64    for call in calls {
65        process_call_and_subcalls(call, 0, &mut arena, tx_result);
66    }
67    arena
68}
69
70/// Recursively process a call and its subcalls, adding them to the arena.
71fn process_call_and_subcalls(
72    call: &Call,
73    parent_idx: usize,
74    arena: &mut CallTraceArena,
75    tx_result: &VmExecutionResultAndLogs,
76) {
77    // Collect logs for the current call.
78    let logs_for_call: Vec<CallLog> = tx_result
79        .logs
80        .events
81        .iter()
82        .enumerate()
83        .filter_map(|(i, vm_event)| {
84            if vm_event.address == call.to {
85                Some(CallLog {
86                    raw_log: vm_event.clone(),
87                    decoded: DecodedCallEvent::default(),
88                    position: i as u64,
89                })
90            } else {
91                None
92            }
93        })
94        .collect();
95
96    // Collect user and system L2-L1 logs associated with this call.
97    let l2_l1_logs_for_call: Vec<L2L1Logs> = tx_result
98        .logs
99        .user_l2_to_l1_logs
100        .iter()
101        .filter(|log| log.0.sender == call.to)
102        .map(|log| L2L1Logs {
103            raw_log: L2L1Log::User(log.clone()),
104            position: log.0.tx_number_in_block as u64,
105        })
106        .chain(
107            tx_result
108                .logs
109                .system_l2_to_l1_logs
110                .iter()
111                .filter(|log| log.0.sender == call.to)
112                .map(|log| L2L1Logs {
113                    raw_log: L2L1Log::System(log.clone()),
114                    position: log.0.tx_number_in_block as u64,
115                }),
116        )
117        .collect();
118
119    let call_trace = convert_call_to_call_trace(call);
120
121    let node = CallTraceNode {
122        parent: None,
123        children: Vec::new(),
124        idx: 0,
125        trace: call_trace,
126        logs: logs_for_call,
127        l2_l1_logs: l2_l1_logs_for_call,
128        ordering: Vec::new(),
129    };
130
131    let new_parent_idx = arena.add_node(Some(parent_idx), node);
132
133    // Process subcalls under the new parent.
134    for subcall in &call.calls {
135        process_call_and_subcalls(subcall, new_parent_idx, arena, tx_result);
136    }
137}
138
139/// Render a collection of call traces to a string
140pub fn render_trace_arena_inner(arena: &CallTraceArena, with_bytecodes: bool) -> String {
141    let mut w = TraceWriter::new(Vec::<u8>::new()).write_bytecodes(with_bytecodes);
142    w.write_arena(arena).expect("Failed to write traces");
143    String::from_utf8(w.into_writer()).expect("trace writer wrote invalid UTF-8")
144}
145
146/// Decode a collection of call traces.
147///
148/// The traces will be decoded if possible using openchain.
149pub async fn decode_trace_arena(arena: &mut CallTraceArena, decoder: &CallTraceDecoder) {
150    decoder.prefetch_signatures(&arena.arena).await;
151    decoder.populate_traces(&mut arena.arena).await;
152}
153
154/// Filter a call trace arena based on verbosity level.
155pub fn filter_call_trace_arena(arena: &CallTraceArena, verbosity: u8) -> CallTraceArena {
156    let mut filtered = CallTraceArena::default();
157
158    if arena.arena.is_empty() {
159        return filtered;
160    }
161
162    let root_idx = 0;
163    let mut root_copy = arena.arena[root_idx].clone();
164    root_copy.parent = None;
165    root_copy.idx = 0;
166    root_copy.children.clear();
167    root_copy.ordering.clear();
168    filtered.arena.push(root_copy);
169
170    filter_node_recursively(
171        &arena.arena[root_idx],
172        arena,
173        &mut filtered,
174        Some(0),
175        verbosity,
176    );
177
178    // Rebuild ordering
179    for node in &mut filtered.arena {
180        rebuild_ordering(node);
181    }
182
183    filtered
184}
185
186fn filter_node_recursively(
187    orig_node: &CallTraceNode,
188    orig_arena: &CallTraceArena,
189    filtered_arena: &mut CallTraceArena,
190    parent_idx: Option<usize>,
191    verbosity: u8,
192) {
193    for &child_idx in &orig_node.children {
194        let child = &orig_arena.arena[child_idx];
195        if should_include_call(&child.trace.address, verbosity) {
196            let new_idx = filtered_arena.arena.len();
197            let mut child_copy = child.clone();
198            child_copy.idx = new_idx;
199            child_copy.parent = parent_idx;
200            child_copy.children.clear();
201            child_copy.ordering.clear();
202
203            // Filter the L2-L1 logs within the node.
204            child_copy.l2_l1_logs.retain(|log| match &log.raw_log {
205                L2L1Log::User(_) => verbosity >= 2, // include user logs if verbosity is >= 2
206                L2L1Log::System(_) => verbosity >= 3, // include system logs if verbosity is >= 3
207            });
208
209            filtered_arena.arena.push(child_copy);
210
211            if let Some(p_idx) = parent_idx {
212                filtered_arena.arena[p_idx].children.push(new_idx);
213            }
214
215            filter_node_recursively(child, orig_arena, filtered_arena, Some(new_idx), verbosity);
216        } else {
217            filter_node_recursively(child, orig_arena, filtered_arena, parent_idx, verbosity);
218        }
219    }
220}
221
222/// Returns whether we should include the call in the trace based on
223/// its address type and the current verbosity level.
224///
225/// Verbosity levels (for quick reference):
226/// - 2: user calls and logs only
227/// - 3: user + system calls and logs
228/// - 4: user + system + precompile  logs
229/// - 5+: everything (future-proof)
230#[inline]
231fn should_include_call(address: &H160, verbosity: u8) -> bool {
232    let is_system = address_map::is_system(address);
233    let is_precompile = address_map::is_precompile(address);
234
235    match verbosity {
236        // -v or less => 0 or 1 => show nothing
237        0 | 1 => false,
238        // -vv => 2 => user calls only (incl. L2–L1 user logs)
239        2 => !(is_system || is_precompile),
240        // -vvv => 3 => user + system (incl. L2–L1 system logs)
241        3 => !is_precompile,
242        // -vvvv => 4 => user + system + precompile
243        4 => true,
244        // -vvvvv => 5 => everything + future logs (e.g. maybe storage logs)
245        _ => true,
246    }
247}
248
249fn rebuild_ordering(node: &mut CallTraceNode) {
250    node.ordering.clear();
251    for i in 0..node.logs.len() {
252        node.ordering.push(TraceMemberOrder::Log(i));
253    }
254    for i in 0..node.l2_l1_logs.len() {
255        node.ordering.push(TraceMemberOrder::L1L2Log(i));
256    }
257    for i in 0..node.children.len() {
258        node.ordering.push(TraceMemberOrder::Call(i));
259    }
260}