anvil_zksync_core/formatter/
log.rs

1use crate::{bootloader_debug::BootloaderDebug, utils::to_human_size};
2use anvil_zksync_common::sh_println;
3use colored::Colorize;
4use serde::Deserialize;
5use zksync_multivm::interface::VmExecutionResultAndLogs;
6use zksync_types::{fee_model::FeeModelConfigV2, StorageLogWithPreviousValue, U256};
7
8use super::{address::address_to_human_readable, pubdata_bytes::PubdataBytesInfo};
9
10// @dev elected to have GasDetails struct as we can do more with it in the future
11// We can provide more detailed understanding of gas errors and gas usage
12#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
13pub struct GasDetails {
14    total_gas_limit: U256,
15    intrinsic_gas: U256,
16    gas_for_validation: U256,
17    gas_spent_on_compute: U256,
18    gas_used: U256,
19    bytes_published: u64,
20    spent_on_pubdata: u64,
21    gas_spent_on_bytecode_preparation: U256,
22    refund_computed: U256,
23    refund_by_operator: U256,
24    required_overhead: U256,
25    operator_overhead: U256,
26    intrinsic_overhead: U256,
27    overhead_for_length: U256,
28    overhead_for_slot: U256,
29    gas_per_pubdata: U256,
30    total_gas_limit_from_user: U256,
31    gas_spent_on_execution: U256,
32    gas_limit_after_intrinsic: U256,
33    gas_after_validation: U256,
34    reserved_gas: U256,
35}
36
37/// Computes the gas details for the transaction to be displayed.
38pub fn compute_gas_details(
39    bootloader_debug: &BootloaderDebug,
40    spent_on_pubdata: u64,
41) -> GasDetails {
42    let total_gas_limit = bootloader_debug
43        .total_gas_limit_from_user
44        .saturating_sub(bootloader_debug.reserved_gas);
45    let intrinsic_gas = total_gas_limit - bootloader_debug.gas_limit_after_intrinsic;
46    let gas_for_validation =
47        bootloader_debug.gas_limit_after_intrinsic - bootloader_debug.gas_after_validation;
48    let gas_spent_on_compute = bootloader_debug.gas_spent_on_execution
49        - bootloader_debug.gas_spent_on_bytecode_preparation;
50    let gas_used = intrinsic_gas
51        + gas_for_validation
52        + bootloader_debug.gas_spent_on_bytecode_preparation
53        + gas_spent_on_compute;
54
55    let bytes_published = spent_on_pubdata / bootloader_debug.gas_per_pubdata.as_u64();
56
57    GasDetails {
58        total_gas_limit,
59        intrinsic_gas,
60        gas_for_validation,
61        gas_spent_on_compute,
62        gas_used,
63        bytes_published,
64        spent_on_pubdata,
65        gas_spent_on_bytecode_preparation: bootloader_debug.gas_spent_on_bytecode_preparation,
66        refund_computed: bootloader_debug.refund_computed,
67        refund_by_operator: bootloader_debug.refund_by_operator,
68        required_overhead: bootloader_debug.required_overhead,
69        operator_overhead: bootloader_debug.operator_overhead,
70        intrinsic_overhead: bootloader_debug.intrinsic_overhead,
71        overhead_for_length: bootloader_debug.overhead_for_length,
72        overhead_for_slot: bootloader_debug.overhead_for_slot,
73        gas_per_pubdata: bootloader_debug.gas_per_pubdata,
74        total_gas_limit_from_user: bootloader_debug.total_gas_limit_from_user,
75        gas_spent_on_execution: bootloader_debug.gas_spent_on_execution,
76        gas_limit_after_intrinsic: bootloader_debug.gas_limit_after_intrinsic,
77        gas_after_validation: bootloader_debug.gas_after_validation,
78        reserved_gas: bootloader_debug.reserved_gas,
79    }
80}
81/// Responsible for formatting the data in a structured log.
82pub struct Formatter {
83    sibling_stack: Vec<bool>,
84}
85
86impl Default for Formatter {
87    fn default() -> Self {
88        Self::new()
89    }
90}
91
92impl Formatter {
93    /// Creates a new formatter with an empty sibling stack.
94    pub fn new() -> Self {
95        Formatter {
96            sibling_stack: Vec::new(),
97        }
98    }
99    /// Logs a section with a title, applies a scoped function, and manages sibling hierarchy.
100    pub fn section<F>(&mut self, title: &str, is_last_sibling: bool, f: F)
101    where
102        F: FnOnce(&mut Self),
103    {
104        self.format_log(is_last_sibling, title);
105        self.enter_scope(is_last_sibling);
106        f(self);
107        self.exit_scope();
108    }
109    /// Logs a key-value item as part of the formatted output.
110    pub fn item(&mut self, is_last_sibling: bool, key: &str, value: &str) {
111        self.format_log(
112            is_last_sibling,
113            &format!("{}: {}", key.bold(), value.dimmed()),
114        );
115    }
116    /// Enters a new scope for nested logging, tracking sibling relationships.
117    pub fn enter_scope(&mut self, has_more_siblings: bool) {
118        self.sibling_stack.push(has_more_siblings);
119    }
120    /// Exits the current logging scope, removing the last sibling marker.
121    pub fn exit_scope(&mut self) {
122        self.sibling_stack.pop();
123    }
124    /// Logs a formatted message with a hierarchical prefix.
125    pub fn format_log(&self, is_last_sibling: bool, message: &str) {
126        let prefix = build_prefix(&self.sibling_stack, is_last_sibling);
127        sh_println!("{}{}", prefix, message);
128    }
129    /// Logs a formatted error message with a hierarchical prefix.
130    pub fn format_error(&self, is_last_sibling: bool, message: &str) {
131        let prefix = build_prefix(&self.sibling_stack, is_last_sibling);
132        sh_println!("{}", format!("{}{}", prefix, message).red());
133    }
134    /// Prints gas details for the transaction in a structured log.
135    pub fn print_gas_details(
136        &mut self,
137        gas_details: &GasDetails,
138        fee_model_config: &FeeModelConfigV2,
139    ) {
140        let GasDetails {
141            total_gas_limit,
142            intrinsic_gas,
143            gas_for_validation,
144            gas_spent_on_compute,
145            gas_used,
146            bytes_published,
147            spent_on_pubdata,
148            gas_spent_on_bytecode_preparation,
149            refund_computed,
150            refund_by_operator,
151            required_overhead: _required_overhead,
152            operator_overhead,
153            intrinsic_overhead,
154            overhead_for_length,
155            overhead_for_slot,
156            gas_per_pubdata,
157            total_gas_limit_from_user,
158            ..
159        } = *gas_details;
160
161        self.section("[Gas Details]", true, |gas_details_section| {
162            let mut total_items = 0;
163            let mut warnings = Vec::new();
164
165            // Prepare warnings
166            if refund_computed != refund_by_operator {
167                warnings.push(format!(
168                    "WARNING: Refund by VM: {}, but operator refunded: {}",
169                    to_human_size(refund_computed),
170                    to_human_size(refund_by_operator)
171                ));
172            }
173
174            if total_gas_limit_from_user != total_gas_limit {
175                warnings.push(format!(
176                    "WARNING: User provided more gas ({}), but system had a lower max limit.",
177                    to_human_size(total_gas_limit_from_user)
178                ));
179            }
180
181            // Calculate total items under [Gas Details]
182            total_items += 1; // Gas Summary
183            total_items += warnings.len(); // Warnings
184            total_items += 1; // Execution Gas Breakdown
185            total_items += 1; // Transaction Setup Cost Breakdown
186            total_items += 1; // L1 Publishing Costs
187            total_items += 1; // Block Contribution
188
189            let mut item_index = 0;
190
191            // Gas Summary
192            let is_last_sibling = item_index == total_items - 1;
193            gas_details_section.section("Gas Summary", is_last_sibling, |gas_summary_section| {
194                let items = vec![
195                    ("Limit", to_human_size(total_gas_limit)),
196                    ("Used", to_human_size(gas_used)),
197                    ("Refunded", to_human_size(refund_by_operator)),
198                    ("Paid:", to_human_size(total_gas_limit - refund_by_operator)),
199                ];
200
201                let num_items = items.len();
202                for (i, (key, value)) in items.into_iter().enumerate() {
203                    let is_last_item = i == num_items - 1;
204                    gas_summary_section.item(is_last_item, key, &value);
205                }
206            });
207            item_index += 1;
208
209            // warnings
210            for warning in warnings {
211                let is_last_sibling = item_index == total_items - 1;
212                gas_details_section.format_error(is_last_sibling, &warning);
213                item_index += 1;
214            }
215
216            // Execution Gas Breakdown
217            let is_last_sibling = item_index == total_items - 1;
218            gas_details_section.section(
219                "Execution Gas Breakdown",
220                is_last_sibling,
221                |execution_breakdown_section| {
222                    let gas_breakdown_items = vec![
223                        (
224                            "Transaction Setup",
225                            intrinsic_gas,
226                            intrinsic_gas * 100 / gas_used,
227                        ),
228                        (
229                            "Bytecode Preparation",
230                            gas_spent_on_bytecode_preparation,
231                            gas_spent_on_bytecode_preparation * 100 / gas_used,
232                        ),
233                        (
234                            "Account Validation",
235                            gas_for_validation,
236                            gas_for_validation * 100 / gas_used,
237                        ),
238                        (
239                            "Computations (Opcodes)",
240                            gas_spent_on_compute,
241                            gas_spent_on_compute * 100 / gas_used,
242                        ),
243                    ];
244
245                    let num_items = gas_breakdown_items.len();
246                    for (i, (description, amount, percentage)) in
247                        gas_breakdown_items.iter().enumerate()
248                    {
249                        let is_last_item = i == num_items - 1;
250                        execution_breakdown_section.item(
251                            is_last_item,
252                            description,
253                            &format!("{} gas ({:>2}%)", to_human_size(*amount), percentage),
254                        );
255                    }
256                },
257            );
258            item_index += 1;
259
260            // Transaction Setup Cost Breakdown
261            let is_last_sibling = item_index == total_items - 1;
262            gas_details_section.section(
263                "Transaction Setup Cost Breakdown",
264                is_last_sibling,
265                |transaction_setup_section| {
266                    let items = vec![
267                        (
268                            "Total Setup Cost",
269                            format!("{} gas", to_human_size(intrinsic_gas)),
270                        ),
271                        (
272                            "Fixed Cost",
273                            format!(
274                                "{} gas ({:>2}%)",
275                                to_human_size(intrinsic_overhead),
276                                intrinsic_overhead * 100 / intrinsic_gas
277                            ),
278                        ),
279                        (
280                            "Operator Cost",
281                            format!(
282                                "{} gas ({:>2}%)",
283                                to_human_size(operator_overhead),
284                                operator_overhead * 100 / intrinsic_gas
285                            ),
286                        ),
287                    ];
288
289                    let num_items = items.len();
290                    for (i, (key, value)) in items.into_iter().enumerate() {
291                        let is_last_item = i == num_items - 1;
292                        transaction_setup_section.item(is_last_item, key, &value);
293                    }
294                },
295            );
296            item_index += 1;
297
298            // L1 Publishing Costs
299            let is_last_sibling = item_index == total_items - 1;
300            gas_details_section.section(
301                "L1 Publishing Costs",
302                is_last_sibling,
303                |l1_publishing_section| {
304                    let items = vec![
305                        (
306                            "Published",
307                            format!("{} bytes", to_human_size(bytes_published.into())),
308                        ),
309                        (
310                            "Cost per Byte",
311                            format!("{} gas", to_human_size(gas_per_pubdata)),
312                        ),
313                        (
314                            "Total Gas Cost",
315                            format!("{} gas", to_human_size(spent_on_pubdata.into())),
316                        ),
317                    ];
318
319                    let num_items = items.len();
320                    for (i, (key, value)) in items.into_iter().enumerate() {
321                        let is_last_item = i == num_items - 1;
322                        l1_publishing_section.item(is_last_item, key, &value);
323                    }
324                },
325            );
326            item_index += 1;
327
328            // Block Contribution
329            let is_last_sibling = item_index == total_items - 1;
330            gas_details_section.section("Block Contribution", is_last_sibling, |block_section| {
331                let full_block_cost = gas_per_pubdata * fee_model_config.batch_overhead_l1_gas;
332
333                let items = vec![
334                    (
335                        "Length Overhead",
336                        format!("{} gas", to_human_size(overhead_for_length)),
337                    ),
338                    (
339                        "Slot Overhead",
340                        format!("{} gas", to_human_size(overhead_for_slot)),
341                    ),
342                    (
343                        "Full Block Cost",
344                        format!("~{} L2 gas", to_human_size(full_block_cost)),
345                    ),
346                ];
347
348                let num_items = items.len();
349                for (i, (key, value)) in items.into_iter().enumerate() {
350                    let is_last_item = i == num_items - 1;
351                    block_section.item(is_last_item, key, &value);
352                }
353            });
354        });
355    }
356    /// Prints the storage logs of the system in a structured log.
357    pub fn print_storage_logs(
358        &mut self,
359        log_query: &StorageLogWithPreviousValue,
360        pubdata_bytes: Option<PubdataBytesInfo>,
361        log_index: usize,
362        is_last: bool,
363    ) {
364        self.section(&format!("Log #{}", log_index), is_last, |log_section| {
365            let mut items = vec![
366                ("Kind", format!("{:?}", log_query.log.kind)),
367                (
368                    "Address",
369                    address_to_human_readable(*log_query.log.key.address())
370                        .unwrap_or_else(|| format!("{:?}", log_query.log.key.address())),
371                ),
372                ("Key", format!("{:#066x}", log_query.log.key.key())),
373                ("Read Value", format!("{:#066x}", log_query.previous_value)),
374            ];
375
376            if log_query.log.is_write() {
377                items.push(("Written Value", format!("{:#066x}", log_query.log.value)));
378            }
379
380            let pubdata_bytes_str = pubdata_bytes
381                .map(|p| format!("{}", p))
382                .unwrap_or_else(|| "None".to_string());
383            items.push(("Pubdata Bytes", pubdata_bytes_str));
384
385            let num_items = items.len();
386            for (i, (key, value)) in items.iter().enumerate() {
387                let is_last_item = i == num_items - 1;
388                log_section.item(is_last_item, key, value);
389            }
390        });
391    }
392    /// Prints the VM execution results in a structured log.
393    pub fn print_vm_details(&mut self, result: &VmExecutionResultAndLogs) {
394        self.section("[VM Execution Results]", true, |section| {
395            let stats = [
396                (
397                    "Cycles Used",
398                    to_human_size(result.statistics.cycles_used.into()),
399                ),
400                (
401                    "Computation Gas Used",
402                    to_human_size(result.statistics.computational_gas_used.into()),
403                ),
404                (
405                    "Contracts Used",
406                    to_human_size(result.statistics.contracts_used.into()),
407                ),
408            ];
409
410            for (key, value) in stats.iter() {
411                section.item(false, key, value);
412            }
413
414            // Handle execution outcome
415            match &result.result {
416                zksync_multivm::interface::ExecutionResult::Success { .. } => {
417                    section.item(true, "Execution Outcome", "Success");
418                }
419                zksync_multivm::interface::ExecutionResult::Revert { output } => {
420                    section.item(false, "Execution Outcome", "Failure");
421                    section.format_error(
422                        true,
423                        &format!("Revert Reason: {}", output.to_user_friendly_string()),
424                    );
425                }
426                zksync_multivm::interface::ExecutionResult::Halt { reason } => {
427                    section.item(false, "Execution Outcome", "Failure");
428                    section.format_error(true, &format!("Halt Reason: {}", reason));
429                }
430            }
431        });
432    }
433}
434// Builds the branched prefix for the structured logs.
435fn build_prefix(sibling_stack: &[bool], is_last_sibling: bool) -> String {
436    let mut prefix = String::new();
437    if !sibling_stack.is_empty() {
438        for &is_last in sibling_stack {
439            if !is_last {
440                prefix.push_str("│   ");
441            } else {
442                prefix.push_str("    ");
443            }
444        }
445        let branch = if is_last_sibling {
446            "└─ "
447        } else {
448            "├─ "
449        };
450        prefix.push_str(branch);
451    }
452    prefix
453}