anvil_zksync_core/formatter/
mod.rs

1//! Helper methods to display transaction data in more human readable way.
2pub mod transaction;
3
4use crate::bootloader_debug::BootloaderDebug;
5use crate::utils::to_human_size;
6use alloy::hex::ToHexExt;
7use anvil_zksync_common::address_map::ContractType;
8use anvil_zksync_common::address_map::KNOWN_ADDRESSES;
9use anvil_zksync_common::sh_println;
10use anvil_zksync_common::utils::cost::format_gwei;
11use colored::Colorize;
12use serde::Deserialize;
13use std::fmt;
14use std::str;
15use zksync_error::{documentation::Documented, CustomErrorMessage, NamedError};
16use zksync_error_description::ErrorDocumentation;
17use zksync_multivm::interface::VmExecutionResultAndLogs;
18use zksync_types::{
19    fee_model::FeeModelConfigV2, Address, ExecuteTransactionCommon, StorageLogWithPreviousValue,
20    Transaction, H160, U256,
21};
22
23// @dev elected to have GasDetails struct as we can do more with it in the future
24// We can provide more detailed understanding of gas errors and gas usage
25#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
26pub struct GasDetails {
27    total_gas_limit: U256,
28    intrinsic_gas: U256,
29    gas_for_validation: U256,
30    gas_spent_on_compute: U256,
31    gas_used: U256,
32    bytes_published: u64,
33    spent_on_pubdata: u64,
34    gas_spent_on_bytecode_preparation: U256,
35    refund_computed: U256,
36    refund_by_operator: U256,
37    required_overhead: U256,
38    operator_overhead: U256,
39    intrinsic_overhead: U256,
40    overhead_for_length: U256,
41    overhead_for_slot: U256,
42    gas_per_pubdata: U256,
43    total_gas_limit_from_user: U256,
44    gas_spent_on_execution: U256,
45    gas_limit_after_intrinsic: U256,
46    gas_after_validation: U256,
47    reserved_gas: U256,
48}
49
50/// Computes the gas details for the transaction to be displayed.
51pub fn compute_gas_details(
52    bootloader_debug: &BootloaderDebug,
53    spent_on_pubdata: u64,
54) -> GasDetails {
55    let total_gas_limit = bootloader_debug
56        .total_gas_limit_from_user
57        .saturating_sub(bootloader_debug.reserved_gas);
58    let intrinsic_gas = total_gas_limit - bootloader_debug.gas_limit_after_intrinsic;
59    let gas_for_validation =
60        bootloader_debug.gas_limit_after_intrinsic - bootloader_debug.gas_after_validation;
61    let gas_spent_on_compute = bootloader_debug.gas_spent_on_execution
62        - bootloader_debug.gas_spent_on_bytecode_preparation;
63    let gas_used = intrinsic_gas
64        + gas_for_validation
65        + bootloader_debug.gas_spent_on_bytecode_preparation
66        + gas_spent_on_compute;
67
68    let bytes_published = spent_on_pubdata / bootloader_debug.gas_per_pubdata.as_u64();
69
70    GasDetails {
71        total_gas_limit,
72        intrinsic_gas,
73        gas_for_validation,
74        gas_spent_on_compute,
75        gas_used,
76        bytes_published,
77        spent_on_pubdata,
78        gas_spent_on_bytecode_preparation: bootloader_debug.gas_spent_on_bytecode_preparation,
79        refund_computed: bootloader_debug.refund_computed,
80        refund_by_operator: bootloader_debug.refund_by_operator,
81        required_overhead: bootloader_debug.required_overhead,
82        operator_overhead: bootloader_debug.operator_overhead,
83        intrinsic_overhead: bootloader_debug.intrinsic_overhead,
84        overhead_for_length: bootloader_debug.overhead_for_length,
85        overhead_for_slot: bootloader_debug.overhead_for_slot,
86        gas_per_pubdata: bootloader_debug.gas_per_pubdata,
87        total_gas_limit_from_user: bootloader_debug.total_gas_limit_from_user,
88        gas_spent_on_execution: bootloader_debug.gas_spent_on_execution,
89        gas_limit_after_intrinsic: bootloader_debug.gas_limit_after_intrinsic,
90        gas_after_validation: bootloader_debug.gas_after_validation,
91        reserved_gas: bootloader_debug.reserved_gas,
92    }
93}
94
95/// Responsible for formatting the data in a structured log.
96pub struct Formatter {
97    sibling_stack: Vec<bool>,
98}
99
100impl Default for Formatter {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106impl Formatter {
107    /// Creates a new formatter with an empty sibling stack.
108    pub fn new() -> Self {
109        Formatter {
110            sibling_stack: Vec::new(),
111        }
112    }
113    /// Logs a section with a title, applies a scoped function, and manages sibling hierarchy.
114    pub fn section<F>(&mut self, title: &str, is_last_sibling: bool, f: F)
115    where
116        F: FnOnce(&mut Self),
117    {
118        self.format_log(is_last_sibling, title);
119        self.enter_scope(is_last_sibling);
120        f(self);
121        self.exit_scope();
122    }
123    /// Logs a key-value item as part of the formatted output.
124    pub fn item(&mut self, is_last_sibling: bool, key: &str, value: &str) {
125        self.format_log(
126            is_last_sibling,
127            &format!("{}: {}", key.bold(), value.dimmed()),
128        );
129    }
130    /// Enters a new scope for nested logging, tracking sibling relationships.
131    pub fn enter_scope(&mut self, has_more_siblings: bool) {
132        self.sibling_stack.push(has_more_siblings);
133    }
134    /// Exits the current logging scope, removing the last sibling marker.
135    pub fn exit_scope(&mut self) {
136        self.sibling_stack.pop();
137    }
138    /// Logs a formatted message with a hierarchical prefix.
139    pub fn format_log(&self, is_last_sibling: bool, message: &str) {
140        let prefix = build_prefix(&self.sibling_stack, is_last_sibling);
141        sh_println!("{}{}", prefix, message);
142    }
143    /// Logs a formatted error message with a hierarchical prefix.
144    pub fn format_error(&self, is_last_sibling: bool, message: &str) {
145        let prefix = build_prefix(&self.sibling_stack, is_last_sibling);
146        sh_println!("{}", format!("{}{}", prefix, message).red());
147    }
148    /// Prints gas details for the transaction in a structured log.
149    pub fn print_gas_details(
150        &mut self,
151        gas_details: &GasDetails,
152        fee_model_config: &FeeModelConfigV2,
153    ) {
154        let GasDetails {
155            total_gas_limit,
156            intrinsic_gas,
157            gas_for_validation,
158            gas_spent_on_compute,
159            gas_used,
160            bytes_published,
161            spent_on_pubdata,
162            gas_spent_on_bytecode_preparation,
163            refund_computed,
164            refund_by_operator,
165            required_overhead: _required_overhead,
166            operator_overhead,
167            intrinsic_overhead,
168            overhead_for_length,
169            overhead_for_slot,
170            gas_per_pubdata,
171            total_gas_limit_from_user,
172            ..
173        } = *gas_details;
174
175        self.section("[Gas Details]", true, |gas_details_section| {
176            let mut total_items = 0;
177            let mut warnings = Vec::new();
178
179            // Prepare warnings
180            if refund_computed != refund_by_operator {
181                warnings.push(format!(
182                    "WARNING: Refund by VM: {}, but operator refunded: {}",
183                    to_human_size(refund_computed),
184                    to_human_size(refund_by_operator)
185                ));
186            }
187
188            if total_gas_limit_from_user != total_gas_limit {
189                warnings.push(format!(
190                    "WARNING: User provided more gas ({}), but system had a lower max limit.",
191                    to_human_size(total_gas_limit_from_user)
192                ));
193            }
194
195            // Calculate total items under [Gas Details]
196            total_items += 1; // Gas Summary
197            total_items += warnings.len(); // Warnings
198            total_items += 1; // Execution Gas Breakdown
199            total_items += 1; // Transaction Setup Cost Breakdown
200            total_items += 1; // L1 Publishing Costs
201            total_items += 1; // Block Contribution
202
203            let mut item_index = 0;
204
205            // Gas Summary
206            let is_last_sibling = item_index == total_items - 1;
207            gas_details_section.section("Gas Summary", is_last_sibling, |gas_summary_section| {
208                let items = vec![
209                    ("Limit", to_human_size(total_gas_limit)),
210                    ("Used", to_human_size(gas_used)),
211                    ("Refunded", to_human_size(refund_by_operator)),
212                    ("Paid:", to_human_size(total_gas_limit - refund_by_operator)),
213                ];
214
215                let num_items = items.len();
216                for (i, (key, value)) in items.into_iter().enumerate() {
217                    let is_last_item = i == num_items - 1;
218                    gas_summary_section.item(is_last_item, key, &value);
219                }
220            });
221            item_index += 1;
222
223            // warnings
224            for warning in warnings {
225                let is_last_sibling = item_index == total_items - 1;
226                gas_details_section.format_error(is_last_sibling, &warning);
227                item_index += 1;
228            }
229
230            // Execution Gas Breakdown
231            let is_last_sibling = item_index == total_items - 1;
232            gas_details_section.section(
233                "Execution Gas Breakdown",
234                is_last_sibling,
235                |execution_breakdown_section| {
236                    let gas_breakdown_items = vec![
237                        (
238                            "Transaction Setup",
239                            intrinsic_gas,
240                            intrinsic_gas * 100 / gas_used,
241                        ),
242                        (
243                            "Bytecode Preparation",
244                            gas_spent_on_bytecode_preparation,
245                            gas_spent_on_bytecode_preparation * 100 / gas_used,
246                        ),
247                        (
248                            "Account Validation",
249                            gas_for_validation,
250                            gas_for_validation * 100 / gas_used,
251                        ),
252                        (
253                            "Computations (Opcodes)",
254                            gas_spent_on_compute,
255                            gas_spent_on_compute * 100 / gas_used,
256                        ),
257                    ];
258
259                    let num_items = gas_breakdown_items.len();
260                    for (i, (description, amount, percentage)) in
261                        gas_breakdown_items.iter().enumerate()
262                    {
263                        let is_last_item = i == num_items - 1;
264                        execution_breakdown_section.item(
265                            is_last_item,
266                            description,
267                            &format!("{} gas ({:>2}%)", to_human_size(*amount), percentage),
268                        );
269                    }
270                },
271            );
272            item_index += 1;
273
274            // Transaction Setup Cost Breakdown
275            let is_last_sibling = item_index == total_items - 1;
276            gas_details_section.section(
277                "Transaction Setup Cost Breakdown",
278                is_last_sibling,
279                |transaction_setup_section| {
280                    let items = vec![
281                        (
282                            "Total Setup Cost",
283                            format!("{} gas", to_human_size(intrinsic_gas)),
284                        ),
285                        (
286                            "Fixed Cost",
287                            format!(
288                                "{} gas ({:>2}%)",
289                                to_human_size(intrinsic_overhead),
290                                intrinsic_overhead * 100 / intrinsic_gas
291                            ),
292                        ),
293                        (
294                            "Operator Cost",
295                            format!(
296                                "{} gas ({:>2}%)",
297                                to_human_size(operator_overhead),
298                                operator_overhead * 100 / intrinsic_gas
299                            ),
300                        ),
301                    ];
302
303                    let num_items = items.len();
304                    for (i, (key, value)) in items.into_iter().enumerate() {
305                        let is_last_item = i == num_items - 1;
306                        transaction_setup_section.item(is_last_item, key, &value);
307                    }
308                },
309            );
310            item_index += 1;
311
312            // L1 Publishing Costs
313            let is_last_sibling = item_index == total_items - 1;
314            gas_details_section.section(
315                "L1 Publishing Costs",
316                is_last_sibling,
317                |l1_publishing_section| {
318                    let items = vec![
319                        (
320                            "Published",
321                            format!("{} bytes", to_human_size(bytes_published.into())),
322                        ),
323                        (
324                            "Cost per Byte",
325                            format!("{} gas", to_human_size(gas_per_pubdata)),
326                        ),
327                        (
328                            "Total Gas Cost",
329                            format!("{} gas", to_human_size(spent_on_pubdata.into())),
330                        ),
331                    ];
332
333                    let num_items = items.len();
334                    for (i, (key, value)) in items.into_iter().enumerate() {
335                        let is_last_item = i == num_items - 1;
336                        l1_publishing_section.item(is_last_item, key, &value);
337                    }
338                },
339            );
340            item_index += 1;
341
342            // Block Contribution
343            let is_last_sibling = item_index == total_items - 1;
344            gas_details_section.section("Block Contribution", is_last_sibling, |block_section| {
345                let full_block_cost = gas_per_pubdata * fee_model_config.batch_overhead_l1_gas;
346
347                let items = vec![
348                    (
349                        "Length Overhead",
350                        format!("{} gas", to_human_size(overhead_for_length)),
351                    ),
352                    (
353                        "Slot Overhead",
354                        format!("{} gas", to_human_size(overhead_for_slot)),
355                    ),
356                    (
357                        "Full Block Cost",
358                        format!("~{} L2 gas", to_human_size(full_block_cost)),
359                    ),
360                ];
361
362                let num_items = items.len();
363                for (i, (key, value)) in items.into_iter().enumerate() {
364                    let is_last_item = i == num_items - 1;
365                    block_section.item(is_last_item, key, &value);
366                }
367            });
368        });
369    }
370    /// Prints the storage logs of the system in a structured log.
371    pub fn print_storage_logs(
372        &mut self,
373        log_query: &StorageLogWithPreviousValue,
374        pubdata_bytes: Option<PubdataBytesInfo>,
375        log_index: usize,
376        is_last: bool,
377    ) {
378        self.section(&format!("Log #{}", log_index), is_last, |log_section| {
379            let mut items = vec![
380                ("Kind", format!("{:?}", log_query.log.kind)),
381                (
382                    "Address",
383                    address_to_human_readable(*log_query.log.key.address())
384                        .unwrap_or_else(|| format!("{:?}", log_query.log.key.address())),
385                ),
386                ("Key", format!("{:#066x}", log_query.log.key.key())),
387                ("Read Value", format!("{:#066x}", log_query.previous_value)),
388            ];
389
390            if log_query.log.is_write() {
391                items.push(("Written Value", format!("{:#066x}", log_query.log.value)));
392            }
393
394            let pubdata_bytes_str = pubdata_bytes
395                .map(|p| format!("{}", p))
396                .unwrap_or_else(|| "None".to_string());
397            items.push(("Pubdata Bytes", pubdata_bytes_str));
398
399            let num_items = items.len();
400            for (i, (key, value)) in items.iter().enumerate() {
401                let is_last_item = i == num_items - 1;
402                log_section.item(is_last_item, key, value);
403            }
404        });
405    }
406    /// Prints the VM execution results in a structured log.
407    pub fn print_vm_details(&mut self, result: &VmExecutionResultAndLogs) {
408        self.section("[VM Execution Results]", true, |section| {
409            let stats = [
410                (
411                    "Cycles Used",
412                    to_human_size(result.statistics.cycles_used.into()),
413                ),
414                (
415                    "Computation Gas Used",
416                    to_human_size(result.statistics.computational_gas_used.into()),
417                ),
418                (
419                    "Contracts Used",
420                    to_human_size(result.statistics.contracts_used.into()),
421                ),
422            ];
423
424            for (key, value) in stats.iter() {
425                section.item(false, key, value);
426            }
427
428            // Handle execution outcome
429            match &result.result {
430                zksync_multivm::interface::ExecutionResult::Success { .. } => {
431                    section.item(true, "Execution Outcome", "Success");
432                }
433                zksync_multivm::interface::ExecutionResult::Revert { output } => {
434                    section.item(false, "Execution Outcome", "Failure");
435                    section.format_error(
436                        true,
437                        &format!("Revert Reason: {}", output.to_user_friendly_string()),
438                    );
439                }
440                zksync_multivm::interface::ExecutionResult::Halt { reason } => {
441                    section.item(false, "Execution Outcome", "Failure");
442                    section.format_error(true, &format!("Halt Reason: {}", reason));
443                }
444            }
445        });
446    }
447}
448// Builds the branched prefix for the structured logs.
449fn build_prefix(sibling_stack: &[bool], is_last_sibling: bool) -> String {
450    let mut prefix = String::new();
451    if !sibling_stack.is_empty() {
452        for &is_last in sibling_stack {
453            if !is_last {
454                prefix.push_str("│   ");
455            } else {
456                prefix.push_str("    ");
457            }
458        }
459        let branch = if is_last_sibling {
460            "└─ "
461        } else {
462            "├─ "
463        };
464        prefix.push_str(branch);
465    }
466    prefix
467}
468
469fn format_known_address(address: H160) -> Option<String> {
470    KNOWN_ADDRESSES.get(&address).map(|known_address| {
471        let name = match known_address.contract_type {
472            ContractType::System => known_address.name.bold().bright_blue().to_string(),
473            ContractType::Precompile => known_address.name.bold().magenta().to_string(),
474            ContractType::Popular => known_address.name.bold().bright_green().to_string(),
475            ContractType::Unknown => known_address.name.dimmed().to_string(),
476        };
477
478        let formatted_address = format!("{:#x}", address).dimmed();
479        format!("{}{}{}", name, "@".dimmed(), formatted_address)
480    })
481}
482
483fn address_to_human_readable(address: H160) -> Option<String> {
484    format_known_address(address)
485}
486
487/// Amount of pubdata that given write has cost.
488/// Used when displaying Storage Logs.
489pub enum PubdataBytesInfo {
490    // This slot is free
491    FreeSlot,
492    // This slot costs this much.
493    Paid(u32),
494    // This happens when we already paid a little for this slot in the past.
495    // This slots costs additional X, the total cost is Y.
496    AdditionalPayment(u32, u32),
497    // We already paid for this slot in this transaction.
498    PaidAlready,
499}
500
501impl std::fmt::Display for PubdataBytesInfo {
502    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
503        match self {
504            PubdataBytesInfo::FreeSlot => write!(f, "Free Slot (no cost)"),
505            PubdataBytesInfo::Paid(cost) => {
506                write!(f, "Paid: {} bytes", to_human_size((*cost).into()))
507            }
508            PubdataBytesInfo::AdditionalPayment(additional_cost, total_cost) => write!(
509                f,
510                "Additional Payment: {} bytes (Total: {} bytes)",
511                to_human_size((*additional_cost).into()),
512                to_human_size((*total_cost).into())
513            ),
514            PubdataBytesInfo::PaidAlready => write!(f, "Already Paid (no additional cost)"),
515        }
516    }
517}
518
519impl PubdataBytesInfo {
520    // Whether the slot incurs any cost
521    pub fn does_cost(&self) -> bool {
522        match self {
523            PubdataBytesInfo::FreeSlot => false,
524            PubdataBytesInfo::Paid(_) => true,
525            PubdataBytesInfo::AdditionalPayment(_, _) => true,
526            PubdataBytesInfo::PaidAlready => false,
527        }
528    }
529}
530
531/// Encapsulates the execution error report.
532#[derive(Debug)]
533pub struct ExecutionErrorReport<'a, E> {
534    error: &'a E,
535    tx: Option<&'a Transaction>,
536}
537
538impl<'a, E> ExecutionErrorReport<'a, E>
539where
540    E: NamedError + CustomErrorMessage + Documented<Documentation = &'static ErrorDocumentation>,
541{
542    pub fn new(error: &'a E, tx: Option<&'a Transaction>) -> Self {
543        Self { error, tx }
544    }
545
546    /// Returns the error details.
547    fn error_report(&self) -> String {
548        let mut out = String::new();
549        let error_msg = self.error.get_message();
550
551        out += &format!("{}: {}\n", "error".red().bold(), error_msg.red());
552        out += "    |\n";
553        let doc = match self.error.get_documentation() {
554            Ok(opt) => opt,
555            Err(e) => {
556                tracing::info!("Failed to get error documentation: {}", e);
557                None
558            }
559        };
560        let summary = doc
561            .as_ref()
562            .map_or("An unknown error occurred", |d| d.summary.as_str());
563        out += &format!("    = {} {}\n", "error:".bright_red(), summary);
564        out
565    }
566
567    /// Returns the transaction details if available.
568    fn tx_details(&self) -> String {
569        let mut out = String::new();
570        if let Some(tx) = self.tx {
571            out += "    | \n";
572            out += &format!("    | {}\n", "Transaction details:".cyan());
573            out += &format!("    |   Transaction Type: {:?}\n", tx.tx_format());
574            if let Some(nonce) = tx.nonce() {
575                out += &format!("    |   Nonce: {}\n", nonce);
576            }
577            if let Some(contract_address) = tx.recipient_account() {
578                out += &format!("    |   To: {:?}\n", contract_address);
579            }
580            out += &format!("    |   From: {:?}\n", tx.initiator_account());
581            if let ExecuteTransactionCommon::L2(l2_tx) = &tx.common_data {
582                if let Some(input_data) = &l2_tx.input {
583                    let hex_data = input_data.data.encode_hex();
584                    out += &format!("    |   Input Data: 0x{}\n", hex_data);
585                    out += &format!("    |   Hash: {:?}\n", tx.hash());
586                }
587            }
588            out += &format!("    |   Gas Limit: {}\n", tx.gas_limit());
589            out += &format!("    |   Gas Price: {}\n", format_gwei(tx.max_fee_per_gas()));
590            out += &format!(
591                "    |   Gas Per Pubdata Limit: {}\n",
592                tx.gas_per_pubdata_byte_limit()
593            );
594
595            // Log paymaster details if available.
596            if let ExecuteTransactionCommon::L2(l2_tx) = &tx.common_data {
597                let paymaster_address = l2_tx.paymaster_params.paymaster;
598                let paymaster_input = &l2_tx.paymaster_params.paymaster_input;
599                if paymaster_address != Address::zero() || !paymaster_input.is_empty() {
600                    out += &format!("    | {}\n", "Paymaster details:".cyan());
601                    out += &format!("    |   Paymaster Address: {:?}\n", paymaster_address);
602                    let paymaster_input_str = if paymaster_input.is_empty() {
603                        "None".to_string()
604                    } else {
605                        paymaster_input.encode_hex()
606                    };
607                    out += &format!("    |   Paymaster Input: 0x{}\n", paymaster_input_str);
608                }
609            }
610        }
611        out
612    }
613
614    /// Returns documentation details including likely causes, fixes, and references.
615    fn docs(&self) -> String {
616        let mut out = String::new();
617        if let Ok(Some(doc)) = self.error.get_documentation() {
618            if !doc.likely_causes.is_empty() {
619                out += "    | \n";
620                out += &format!("    | {}\n", "Likely causes:".cyan());
621                for cause in &doc.likely_causes {
622                    out += &format!("    |   - {}\n", cause.cause);
623                }
624                // Collect fixes.
625                let all_fixes: Vec<&String> = doc
626                    .likely_causes
627                    .iter()
628                    .flat_map(|cause| &cause.fixes)
629                    .collect();
630                if !all_fixes.is_empty() {
631                    out += "    | \n";
632                    out += &format!("    | {}\n", "Possible fixes:".green().bold());
633                    for fix in &all_fixes {
634                        out += &format!("    |   - {}\n", fix);
635                    }
636                }
637                // Collect references.
638                let all_references: Vec<&String> = doc
639                    .likely_causes
640                    .iter()
641                    .flat_map(|cause| &cause.references)
642                    .collect();
643                if !all_references.is_empty() {
644                    out += &format!(
645                        "\n{} \n",
646                        "For more information about this error, visit:"
647                            .cyan()
648                            .bold()
649                    );
650                    for reference in &all_references {
651                        out += &format!("  - {}\n", reference.underline());
652                    }
653                }
654            }
655            out += "    |\n";
656            out += &format!("{} {}\n", "note:".blue(), doc.description);
657        }
658        out += &format!(
659            "{} transaction execution halted due to the above error\n",
660            "error:".red()
661        );
662        out
663    }
664
665    /// Combines all parts of the error report into one string.
666    pub fn report(&self) -> String {
667        let mut out = String::new();
668        out += &self.error_report();
669        out += &self.tx_details();
670        out += &self.docs();
671        out
672    }
673}
674
675/// Implementing Display allows the error report to be used in formatting macros.
676impl<E> fmt::Display for ExecutionErrorReport<'_, E>
677where
678    E: NamedError + CustomErrorMessage + Documented<Documentation = &'static ErrorDocumentation>,
679{
680    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
681        write!(f, "{}", self.report())
682    }
683}