1pub 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#[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
50pub 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
95pub 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 pub fn new() -> Self {
109 Formatter {
110 sibling_stack: Vec::new(),
111 }
112 }
113 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 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 pub fn enter_scope(&mut self, has_more_siblings: bool) {
132 self.sibling_stack.push(has_more_siblings);
133 }
134 pub fn exit_scope(&mut self) {
136 self.sibling_stack.pop();
137 }
138 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 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 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 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 total_items += 1; total_items += warnings.len(); total_items += 1; total_items += 1; total_items += 1; total_items += 1; let mut item_index = 0;
204
205 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 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 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 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 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 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 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 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 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}
448fn 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
487pub enum PubdataBytesInfo {
490 FreeSlot,
492 Paid(u32),
494 AdditionalPayment(u32, u32),
497 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 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#[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 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 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 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 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 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 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 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
675impl<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}