anvil_zksync_traces/
writer.rs

1//! Helper methods to display transaction data in more human readable way.
2
3////////////////////////////////////////////////////////////////////////////////////////////////////////////
4// Attribution: File adapted from the `revm-inspectors`crate for zksync usage                             //
5//                                                                                                        //
6// Full credit goes to its authors. See the original implementation here:                                 //
7// https://github.com/paradigmxyz/revm-inspectors/blob/main/src/tracing/writer.rs                         //
8//                                                                                                        //
9// Note: These methods are used under the terms of the original project's license.                        //
10////////////////////////////////////////////////////////////////////////////////////////////////////////////
11
12use alloy::primitives::hex::encode;
13use anstyle::{AnsiColor, Color, Style};
14use anvil_zksync_types::traces::{
15    CallLog, CallTrace, CallTraceArena, CallTraceNode, DecodedCallData, ExecutionResultDisplay,
16    L2L1Log, L2L1Logs, TraceMemberOrder,
17};
18use colorchoice::ColorChoice;
19use std::io::{self, Write};
20use std::str;
21use zksync_multivm::interface::CallType;
22use zksync_types::zk_evm_types::FarCallOpcode;
23
24use crate::format::PrettyDecodedValue;
25
26const PIPE: &str = "  │ ";
27const EDGE: &str = "  └─ ";
28const BRANCH: &str = "  ├─ ";
29const CALL: &str = "→ ";
30const RETURN: &str = "← ";
31
32const TRACE_KIND_STYLE: Style = AnsiColor::Yellow.on_default();
33const LOG_STYLE: Style = AnsiColor::Cyan.on_default();
34
35/// Configuration for a [`TraceWriter`].
36#[derive(Clone, Debug)]
37#[allow(missing_copy_implementations)]
38pub struct TraceWriterConfig {
39    use_colors: bool,
40    write_bytecodes: bool,
41}
42
43impl Default for TraceWriterConfig {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49impl TraceWriterConfig {
50    /// Create a new `TraceWriterConfig` with default settings.
51    pub fn new() -> Self {
52        Self {
53            use_colors: use_colors(ColorChoice::Auto),
54            write_bytecodes: false,
55        }
56    }
57
58    /// Use colors in the output. Default: [`Auto`](ColorChoice::Auto).
59    pub fn color_choice(mut self, choice: ColorChoice) -> Self {
60        self.use_colors = use_colors(choice);
61        self
62    }
63
64    /// Get the current color choice. `Auto` is lost, so this returns `true` if colors are enabled.
65    pub fn get_use_colors(&self) -> bool {
66        self.use_colors
67    }
68
69    /// Write contract creation codes and deployed codes when writing "create" traces.
70    /// Default: false.
71    pub fn write_bytecodes(mut self, yes: bool) -> Self {
72        self.write_bytecodes = yes;
73        self
74    }
75
76    /// Returns `true` if contract creation codes and deployed codes are written.
77    pub fn get_write_bytecodes(&self) -> bool {
78        self.write_bytecodes
79    }
80}
81
82/// Formats [call traces](CallTraceArena) to an [`Write`] writer.
83///
84/// Will never write invalid UTF-8.
85#[derive(Clone, Debug)]
86pub struct TraceWriter<W> {
87    writer: W,
88    indentation_level: u16,
89    config: TraceWriterConfig,
90}
91
92impl<W: Write> TraceWriter<W> {
93    /// Create a new `TraceWriter` with the given writer.
94    #[inline]
95    pub fn new(writer: W) -> Self {
96        Self::with_config(writer, TraceWriterConfig::new())
97    }
98
99    /// Create a new `TraceWriter` with the given writer and configuration.
100    pub fn with_config(writer: W, config: TraceWriterConfig) -> Self {
101        Self {
102            writer,
103            indentation_level: 0,
104            config,
105        }
106    }
107
108    /// Sets the color choice.
109    #[inline]
110    pub fn use_colors(mut self, color_choice: ColorChoice) -> Self {
111        self.config.use_colors = use_colors(color_choice);
112        self
113    }
114
115    /// Sets the starting indentation level.
116    #[inline]
117    pub fn with_indentation_level(mut self, level: u16) -> Self {
118        self.indentation_level = level;
119        self
120    }
121
122    /// Sets whether contract creation codes and deployed codes should be written.
123    #[inline]
124    pub fn write_bytecodes(mut self, yes: bool) -> Self {
125        self.config.write_bytecodes = yes;
126        self
127    }
128
129    /// Returns a reference to the inner writer.
130    #[inline]
131    pub const fn writer(&self) -> &W {
132        &self.writer
133    }
134
135    /// Returns a mutable reference to the inner writer.
136    #[inline]
137    pub fn writer_mut(&mut self) -> &mut W {
138        &mut self.writer
139    }
140
141    /// Consumes the `TraceWriter` and returns the inner writer.
142    #[inline]
143    pub fn into_writer(self) -> W {
144        self.writer
145    }
146
147    /// Writes a call trace arena to the writer.
148    pub fn write_arena(&mut self, arena: &CallTraceArena) -> io::Result<()> {
149        let root_node = &arena.arena[0];
150        for &child_idx in &root_node.children {
151            self.write_node(arena.nodes(), child_idx)?;
152        }
153        self.writer.flush()
154    }
155
156    /// Writes a single item of a single node to the writer. Returns the index of the next item to
157    /// be written.
158    ///
159    /// Note: this will return length of [CallTraceNode::ordering] when last item will get
160    /// processed.
161    /// Writes a single item of a single node to the writer. Returns the index of the next item to
162    /// be written.
163    ///
164    /// Note: this will return the length of [CallTraceNode::ordering] when the last item gets
165    /// processed.
166    fn write_item(
167        &mut self,
168        nodes: &[CallTraceNode],
169        node_idx: usize,
170        item_idx: usize,
171    ) -> io::Result<usize> {
172        let node = &nodes[node_idx];
173        match &node.ordering[item_idx] {
174            TraceMemberOrder::Log(index) => {
175                self.write_log(&node.logs[*index])?;
176                Ok(item_idx + 1)
177            }
178            TraceMemberOrder::Call(index) => {
179                assert!(*index < node.children.len(), "Call index out of bounds");
180                self.write_node(nodes, node.children[*index])?;
181                Ok(item_idx + 1)
182            }
183            TraceMemberOrder::L1L2Log(index) => {
184                self.write_l1_l2_log(&node.l2_l1_logs[*index])?;
185                Ok(item_idx + 1)
186            }
187        }
188    }
189
190    /// Writes items of a single node to the writer, starting from the given index, and until the
191    /// given predicate is false.
192    ///
193    /// Returns the index of the next item to be written.
194    fn write_items_until(
195        &mut self,
196        nodes: &[CallTraceNode],
197        node_idx: usize,
198        first_item_idx: usize,
199        f: impl Fn(usize) -> bool,
200    ) -> io::Result<usize> {
201        let mut item_idx = first_item_idx;
202        while !f(item_idx) {
203            item_idx = self.write_item(nodes, node_idx, item_idx)?;
204        }
205        Ok(item_idx)
206    }
207
208    /// Writes all items of a single node to the writer.
209    fn write_items(&mut self, nodes: &[CallTraceNode], node_idx: usize) -> io::Result<()> {
210        let items_cnt = nodes[node_idx].ordering.len();
211        self.write_items_until(nodes, node_idx, 0, |idx| idx == items_cnt)?;
212        Ok(())
213    }
214
215    /// Writes a single node and its children to the writer.
216    fn write_node(&mut self, nodes: &[CallTraceNode], idx: usize) -> io::Result<()> {
217        let node = &nodes[idx];
218
219        // Write header.
220        self.write_branch()?;
221        self.write_trace_header(&node.trace)?;
222        self.writer.write_all(b"\n")?;
223
224        // Write logs and subcalls.
225        self.indentation_level += 1;
226        self.write_items(nodes, idx)?;
227
228        // Write return data.
229        self.write_edge()?;
230        self.write_trace_footer(&node.trace)?;
231        self.writer.write_all(b"\n")?;
232
233        self.indentation_level -= 1;
234
235        Ok(())
236    }
237
238    /// Writes the header of a call trace.
239    fn write_trace_header(&mut self, trace: &CallTrace) -> io::Result<()> {
240        write!(self.writer, "[{}] ", trace.call.gas_used)?;
241
242        let trace_kind_style = self.trace_kind_style();
243        let address = format!("0x{}", encode(trace.call.to));
244
245        match trace.call.r#type {
246            CallType::Create => {
247                write!(
248                    self.writer,
249                    "{trace_kind_style}{CALL}new{trace_kind_style:#} {label}@{address}",
250                    label = trace.decoded.label.as_deref().unwrap_or("<unknown>")
251                )?;
252                if self.config.write_bytecodes {
253                    write!(self.writer, "({})", encode(&trace.call.input))?;
254                }
255            }
256            CallType::Call(_) | CallType::NearCall => {
257                let (func_name, inputs) = match &trace.decoded.call_data {
258                    Some(DecodedCallData { signature, args }) => {
259                        let name = signature.split('(').next().unwrap();
260                        (
261                            name.to_string(),
262                            args.iter()
263                                .map(|v| PrettyDecodedValue(v).to_string())
264                                .collect::<Vec<_>>()
265                                .join(", "),
266                        )
267                    }
268                    None => {
269                        if trace.call.input.len() < 4 {
270                            ("fallback".to_string(), encode(&trace.call.input))
271                        } else {
272                            let (selector, data) = trace.call.input.split_at(4);
273                            (encode(selector), encode(data))
274                        }
275                    }
276                };
277
278                write!(
279                    self.writer,
280                    "{style}{addr}{style:#}::{style}{func_name}{style:#}",
281                    style = self.trace_style(trace),
282                    addr = trace.decoded.label.as_deref().unwrap_or(&address),
283                )?;
284
285                if !trace.call.value.is_zero() {
286                    write!(self.writer, "{{value: {}}}", trace.call.value)?;
287                }
288
289                write!(self.writer, "({inputs})")?;
290
291                let action = match trace.call.r#type {
292                    CallType::Call(opcode) => match opcode {
293                        FarCallOpcode::Normal => None,
294                        FarCallOpcode::Delegate => Some(" [delegatecall]"),
295                        FarCallOpcode::Mimic => Some(" [mimiccall]"),
296                    },
297                    CallType::NearCall => Some("[nearcall]"),
298                    CallType::Create => unreachable!(), // Create calls are handled separately.
299                };
300
301                if let Some(action) = action {
302                    write!(
303                        self.writer,
304                        "{trace_kind_style}{action}{trace_kind_style:#}"
305                    )?;
306                }
307            }
308        }
309
310        Ok(())
311    }
312
313    fn write_l1_l2_log(&mut self, log: &L2L1Logs) -> io::Result<()> {
314        let log_style = self.log_style();
315        self.write_branch()?; // Write indentation/pipes if needed
316
317        let (log_type, l2_log) = match &log.raw_log {
318            L2L1Log::User(user_log) => ("UserL2L1Log", &user_log.0),
319            L2L1Log::System(system_log) => ("SystemL2L1Log", &system_log.0),
320        };
321
322        write!(
323            self.writer,
324            "{log_type}({log_style}key: {:?}, value: {:?}{log_style:#})",
325            l2_log.key, l2_log.value
326        )?;
327
328        writeln!(self.writer)?;
329
330        Ok(())
331    }
332
333    fn write_log(&mut self, log: &CallLog) -> io::Result<()> {
334        let log_style = self.log_style();
335        self.write_branch()?;
336
337        if let Some(name) = &log.decoded.name {
338            write!(self.writer, "emit {name}({log_style}")?;
339            if let Some(params) = &log.decoded.params {
340                for (i, (param_name, value)) in params.iter().enumerate() {
341                    if i > 0 {
342                        self.writer.write_all(b", ")?;
343                    }
344                    write!(self.writer, "{param_name}: {value}")?;
345                }
346            }
347            writeln!(self.writer, "{log_style:#})")?;
348        } else {
349            for (i, topic) in log.raw_log.indexed_topics.iter().enumerate() {
350                if i == 0 {
351                    self.writer.write_all(b" emit topic 0")?;
352                } else {
353                    self.write_pipes()?;
354                    write!(self.writer, "       topic {i}")?;
355                }
356                writeln!(self.writer, ": {log_style}{topic}{log_style:#}")?;
357            }
358            if !log.raw_log.indexed_topics.is_empty() {
359                self.write_pipes()?;
360            }
361            writeln!(
362                self.writer,
363                "          data: {log_style}{data}{log_style:#}",
364                data = encode(&log.raw_log.value)
365            )?;
366        }
367        Ok(())
368    }
369
370    /// Writes the footer of a call trace.
371    fn write_trace_footer(&mut self, trace: &CallTrace) -> io::Result<()> {
372        // Use the custom trait to format the execution result
373        let status_str = trace.execution_result.display();
374
375        // Write the execution result status using the formatted string
376        write!(
377            self.writer,
378            "{style}{RETURN}[{status}]{style:#}",
379            style = self.trace_style(trace),
380            status = status_str,
381        )?;
382
383        // Write decoded return data if available
384        let decoded_return_data = &trace.decoded.return_data.to_string();
385        if !decoded_return_data.is_empty() {
386            write!(self.writer, " ")?;
387            return self.writer.write_all(decoded_return_data.as_bytes());
388        }
389
390        // Handle contract creation or output data
391        if !self.config.write_bytecodes
392            && matches!(trace.call.r#type, CallType::Create)
393            && !trace.execution_result.is_failed()
394        {
395            write!(self.writer, " {} bytes of code", trace.call.output.len())?;
396        } else if !trace.call.output.is_empty() {
397            write!(self.writer, " {}", encode(&trace.call.output))?;
398        }
399
400        Ok(())
401    }
402
403    fn write_indentation(&mut self) -> io::Result<()> {
404        self.writer.write_all(b"  ")?;
405        for _ in 1..self.indentation_level {
406            self.writer.write_all(PIPE.as_bytes())?;
407        }
408        Ok(())
409    }
410
411    #[doc(alias = "left_prefix")]
412    fn write_branch(&mut self) -> io::Result<()> {
413        self.write_indentation()?;
414        if self.indentation_level != 0 {
415            self.writer.write_all(BRANCH.as_bytes())?;
416        }
417        Ok(())
418    }
419
420    #[doc(alias = "right_prefix")]
421    fn write_pipes(&mut self) -> io::Result<()> {
422        self.write_indentation()?;
423        self.writer.write_all(PIPE.as_bytes())
424    }
425
426    fn write_edge(&mut self) -> io::Result<()> {
427        self.write_indentation()?;
428        self.writer.write_all(EDGE.as_bytes())
429    }
430
431    fn trace_style(&self, trace: &CallTrace) -> Style {
432        if !self.config.use_colors {
433            return Style::default();
434        }
435        let color = if trace.success {
436            AnsiColor::Green
437        } else {
438            AnsiColor::Red
439        };
440        Color::Ansi(color).on_default()
441    }
442
443    fn trace_kind_style(&self) -> Style {
444        if !self.config.use_colors {
445            return Style::default();
446        }
447        TRACE_KIND_STYLE
448    }
449
450    fn log_style(&self) -> Style {
451        if !self.config.use_colors {
452            return Style::default();
453        }
454        LOG_STYLE
455    }
456}
457
458fn use_colors(choice: ColorChoice) -> bool {
459    use io::IsTerminal;
460    match choice {
461        ColorChoice::Auto => io::stdout().is_terminal(),
462        ColorChoice::AlwaysAnsi | ColorChoice::Always => true,
463        ColorChoice::Never => false,
464    }
465}