1use crate::utils::{
2 TELEMETRY_SENSITIVE_VALUE, get_cli_command_telemetry_props, parse_genesis_file,
3};
4use alloy::signers::local::coins_bip39::{English, Mnemonic};
5use anvil_zksync_common::{
6 cache::{CacheConfig, CacheType, DEFAULT_DISK_CACHE_DIR},
7 sh_err, sh_warn,
8 utils::io::write_json_file,
9};
10use anvil_zksync_config::types::{AccountGenerator, Genesis, SystemContractsOptions};
11use anvil_zksync_config::{BaseTokenConfig, L1Config, TestNodeConfig};
12use anvil_zksync_config::{
13 DebugTraceConfig,
14 constants::{DEFAULT_MNEMONIC, TEST_NODE_NETWORK_ID},
15 types::ZKsyncOsConfig,
16};
17use anvil_zksync_core::node::fork::ForkConfig;
18use anvil_zksync_core::node::{InMemoryNode, VersionedState};
19use anvil_zksync_types::{
20 LogLevel, ShowGasDetails, ShowStorageLogs, ShowVMDetails, TransactionOrder,
21};
22use clap::{ArgAction, Parser, Subcommand, ValueEnum};
23use flate2::read::GzDecoder;
24use futures::FutureExt;
25use num::rational::Ratio;
26use rand::{SeedableRng, rngs::StdRng};
27use std::collections::HashMap;
28use std::env;
29use std::io::Read;
30use std::net::IpAddr;
31use std::num::NonZeroU64;
32use std::path::PathBuf;
33use std::str::FromStr;
34use std::time::Duration;
35use std::{
36 future::Future,
37 pin::Pin,
38 task::{Context, Poll},
39};
40use tokio::time::{Instant, Interval};
41use url::Url;
42use zksync_telemetry::TelemetryProps;
43use zksync_types::fee_model::{BaseTokenConversionRatio, ConversionRatio};
44use zksync_types::{H256, ProtocolVersionId, U256};
45
46const DEFAULT_PORT: &str = "8011";
47const DEFAULT_HOST: &str = "0.0.0.0";
48const DEFAULT_ACCOUNTS: &str = "10";
49const DEFAULT_BALANCE: &str = "10000";
50const DEFAULT_ALLOW_ORIGIN: &str = "*";
51const DEFAULT_TX_ORDER: &str = "fifo";
52
53#[derive(Debug, Parser, Clone)]
54#[command(
55 author = "Matter Labs",
56 version,
57 about = "A fast and extensible local ZKsync test node.",
58 long_about = "anvil-zksync\n\nA developer-friendly ZKsync local node for testing."
59)]
60pub struct Cli {
61 #[command(subcommand)]
62 pub command: Option<Command>,
63
64 #[arg(long, help_heading = "General Options")]
66 pub offline: bool,
68
69 #[arg(long, help_heading = "General Options")]
70 pub health_check_endpoint: bool,
74
75 #[arg(long, value_name = "OUT_FILE", help_heading = "General Options")]
77 pub config_out: Option<String>,
78
79 #[arg(long, default_value = DEFAULT_PORT, help_heading = "Network Options")]
80 pub port: Option<u16>,
82
83 #[arg(
85 long,
86 value_name = "IP_ADDR",
87 env = "ANVIL_ZKSYNC_IP_ADDR",
88 default_value = DEFAULT_HOST,
89 value_delimiter = ',',
90 help_heading = "Network Options"
91 )]
92 pub host: Vec<IpAddr>,
93
94 #[arg(long, help_heading = "Network Options")]
95 pub chain_id: Option<u32>,
97
98 #[arg(long, default_value = "true", default_missing_value = "true", num_args(0..=1), help_heading = "Debugging Options")]
99 pub show_node_config: Option<bool>,
101
102 #[arg(long, help_heading = "Debugging Options")]
104 pub show_storage_logs: Option<ShowStorageLogs>,
106
107 #[arg(long, help_heading = "Debugging Options")]
108 pub show_vm_details: Option<ShowVMDetails>,
110
111 #[arg(long, help_heading = "Debugging Options")]
112 pub show_gas_details: Option<ShowGasDetails>,
114
115 #[arg(short = 'v', long = "verbosity", action = ArgAction::Count, help_heading = "Debugging Options")]
123 pub verbosity: u8,
124
125 #[arg(long, help_heading = "Gas Configuration")]
127 pub l1_gas_price: Option<u64>,
129
130 #[arg(long, alias = "gas-price", help_heading = "Gas Configuration")]
131 pub l2_gas_price: Option<u64>,
133
134 #[arg(long, help_heading = "Gas Configuration")]
135 pub l1_pubdata_price: Option<u64>,
137
138 #[arg(long, help_heading = "Gas Configuration")]
139 pub price_scale_factor: Option<f64>,
141
142 #[arg(long, help_heading = "Gas Configuration")]
143 pub limit_scale_factor: Option<f32>,
145
146 #[arg(long, help_heading = "System Configuration")]
147 pub override_bytecodes_dir: Option<String>,
149
150 #[arg(long, help_heading = "System Configuration")]
151 pub enforce_bytecode_compression: Option<bool>,
153
154 #[arg(long, help_heading = "System Configuration")]
156 pub dev_system_contracts: Option<SystemContractsOptions>,
158
159 #[arg(
161 long,
162 help_heading = "System Configuration",
163 value_parser = clap::value_parser!(PathBuf),
164 )]
165 pub system_contracts_path: Option<PathBuf>,
166
167 #[arg(
170 long,
171 help_heading = "System Configuration",
172 value_parser = clap::value_parser!(PathBuf),
173 )]
174 pub evm_emulator_path: Option<PathBuf>,
175
176 #[arg(long, value_parser = protocol_version_from_str, help_heading = "System Configuration")]
177 pub protocol_version: Option<ProtocolVersionId>,
180
181 #[arg(long, help_heading = "System Configuration")]
182 pub evm_interpreter: bool,
184
185 #[clap(flatten)]
186 pub zksync_os_group: ZKsyncOsGroup,
188
189 #[arg(long, help_heading = "Logging Configuration")]
191 pub log: Option<LogLevel>,
193
194 #[arg(long, help_heading = "Logging Configuration")]
195 pub log_file_path: Option<String>,
197
198 #[arg(long, alias = "quiet", default_missing_value = "true", num_args(0..=1), help_heading = "Logging Configuration")]
199 pub silent: Option<bool>,
201
202 #[arg(long, help_heading = "Cache Options")]
204 pub cache: Option<CacheType>,
206
207 #[arg(long, help_heading = "Cache Options")]
208 pub reset_cache: Option<bool>,
210
211 #[arg(long, help_heading = "Cache Options")]
212 pub cache_dir: Option<String>,
214
215 #[arg(
217 long,
218 short,
219 default_value = DEFAULT_ACCOUNTS,
220 value_name = "NUM",
221 help_heading = "Account Configuration"
222 )]
223 pub accounts: u64,
224
225 #[arg(
227 long,
228 default_value = DEFAULT_BALANCE,
229 value_name = "NUM",
230 help_heading = "Account Configuration"
231 )]
232 pub balance: u64,
233
234 #[arg(long, value_name = "NUM")]
236 pub timestamp: Option<u64>,
237
238 #[arg(long, value_name = "PATH", value_parser= parse_genesis_file)]
240 pub init: Option<Genesis>,
241
242 #[arg(
247 long,
248 value_name = "PATH",
249 conflicts_with_all = &[
250 "init",
251 "dump_state",
252 "load_state"
253 ]
254 )]
255 pub state: Option<PathBuf>,
256
257 #[arg(short, long, value_name = "SECONDS")]
261 pub state_interval: Option<u64>,
262
263 #[arg(long, value_name = "PATH", conflicts_with = "init")]
267 pub dump_state: Option<PathBuf>,
268
269 #[arg(long, conflicts_with = "init", default_value = "false")]
276 pub preserve_historical_states: bool,
277
278 #[arg(long, value_name = "PATH", conflicts_with = "init")]
280 pub load_state: Option<PathBuf>,
281
282 #[arg(long, short, conflicts_with_all = &["mnemonic_seed", "mnemonic_random"], help_heading = "Account Configuration")]
285 pub mnemonic: Option<String>,
286
287 #[arg(long, conflicts_with_all = &["mnemonic", "mnemonic_seed"], default_missing_value = "12", num_args(0..=1), help_heading = "Account Configuration")]
292 pub mnemonic_random: Option<usize>,
293
294 #[arg(long = "mnemonic-seed-unsafe", conflicts_with_all = &["mnemonic", "mnemonic_random"], help_heading = "Account Configuration")]
299 pub mnemonic_seed: Option<u64>,
300
301 #[arg(long, help_heading = "Account Configuration")]
304 pub derivation_path: Option<String>,
305
306 #[arg(
309 long,
310 visible_alias = "auto-unlock",
311 help_heading = "Account Configuration"
312 )]
313 pub auto_impersonate: bool,
314
315 #[arg(short, long, value_name = "SECONDS", value_parser = duration_from_secs_f64, help_heading = "Block Sealing")]
318 pub block_time: Option<Duration>,
319
320 #[arg(long, visible_alias = "no-mine", conflicts_with = "block_time")]
322 pub no_mining: bool,
323
324 #[arg(long, default_value = DEFAULT_ALLOW_ORIGIN, help_heading = "Server options")]
326 pub allow_origin: String,
327
328 #[arg(long, conflicts_with = "allow_origin", help_heading = "Server options")]
330 pub no_cors: bool,
331
332 #[arg(long, default_value = DEFAULT_TX_ORDER)]
334 pub order: TransactionOrder,
335
336 #[clap(flatten)]
337 pub l1_group: Option<L1Group>,
338
339 #[arg(long, requires = "l1_group", default_missing_value = "true", num_args(0..=1), help_heading = "UNSTABLE - L1")]
341 pub auto_execute_l1: Option<bool>,
342
343 #[arg(long, help_heading = "Custom Base Token")]
345 pub base_token_symbol: Option<String>,
346
347 #[arg(long, help_heading = "Custom Base Token")]
349 pub base_token_ratio: Option<Ratio<u64>>,
350}
351
352#[derive(Clone, Debug, clap::Args)]
353pub struct ZKsyncOsGroup {
354 #[arg(long, help_heading = "UNSTABLE - ZKsync OS")]
356 pub zksync_os: bool,
357
358 #[arg(long, requires = "zksync_os", help_heading = "UNSTABLE - ZKsync OS")]
360 pub zksync_os_bin_path: Option<String>,
361}
362
363impl From<ZKsyncOsGroup> for ZKsyncOsConfig {
364 fn from(group: ZKsyncOsGroup) -> Self {
365 ZKsyncOsConfig {
366 zksync_os: group.zksync_os,
367 zksync_os_bin_path: group.zksync_os_bin_path,
368 }
369 }
370}
371
372#[derive(Debug, Clone, clap::Args)]
373#[group(id = "l1_group", multiple = false)]
374pub struct L1Group {
375 #[arg(long, conflicts_with = "external_l1", default_missing_value = "8012", num_args(0..=1), help_heading = "UNSTABLE - L1")]
377 pub spawn_l1: Option<u16>,
378
379 #[arg(long, conflicts_with = "spawn_l1", help_heading = "UNSTABLE - L1")]
381 pub external_l1: Option<String>,
382}
383
384#[derive(Debug, Subcommand, Clone)]
385pub enum Command {
386 #[command(name = "run")]
388 Run,
389 #[command(name = "fork")]
391 Fork(ForkArgs),
392 #[command(name = "replay_tx")]
394 ReplayTx(ReplayArgs),
395 #[command(name = "debug-trace")]
397 DebugTrace(DebugTxArgs),
398}
399
400#[derive(Debug, Parser, Clone)]
401pub struct ForkArgs {
402 #[arg(
416 long,
417 alias = "network",
418 value_enum,
419 help = "Which network to fork (builtins) or an HTTP(S) URL"
420 )]
421 pub fork_url: ForkUrl,
422 #[arg(
425 long,
426 value_name = "BLOCK",
427 long_help = "Fetch state from a specific block number over a remote endpoint.",
428 alias = "fork-at"
429 )]
430 pub fork_block_number: Option<u64>,
431
432 #[arg(
436 long,
437 requires = "fork_url",
438 value_name = "TRANSACTION",
439 conflicts_with = "fork_block_number"
440 )]
441 pub fork_transaction_hash: Option<H256>,
442}
443
444#[derive(Debug, Parser, Clone)]
445pub struct ReplayArgs {
446 #[arg(
461 long,
462 alias = "network",
463 value_enum,
464 help = "Which network to fork (builtins) or an HTTP(S) URL"
465 )]
466 pub fork_url: ForkUrl,
467 #[arg(help = "Transaction hash to replay.")]
469 pub tx: H256,
470}
471
472#[derive(Debug, Parser, Clone)]
473pub struct DebugTxArgs {
474 #[arg(
475 long,
476 alias = "network",
477 value_enum,
478 help = "Which network to fork (builtins) or an HTTP(S) URL"
479 )]
480 pub fork_url: ForkUrl,
481
482 pub tx: H256,
484
485 #[arg(long)]
487 pub only_top: bool,
488}
489
490#[derive(Debug, Clone, ValueEnum)]
492#[value(rename_all = "kebab-case")]
493pub enum BuiltinNetwork {
494 #[value(alias = "mainnet")]
495 Era,
496 #[value(alias = "sepolia-testnet")]
497 EraTestnet,
498 Abstract,
499 AbstractTestnet,
500 #[value(alias = "sophon-mainnet")]
501 Sophon,
502 SophonTestnet,
503 Cronos,
504 CronosTestnet,
505 Lens,
506 LensTestnet,
507 Openzk,
508 OpenzkTestnet,
509 Zkcandy,
510}
511
512#[derive(Debug, Clone)]
514pub enum ForkUrl {
515 Builtin(BuiltinNetwork),
516 Custom(Url),
517}
518
519impl BuiltinNetwork {
520 pub fn to_fork_config(&self) -> ForkConfig {
522 match self {
523 BuiltinNetwork::Era => ForkConfig {
524 url: "https://mainnet.era.zksync.io".parse().unwrap(),
525 estimate_gas_price_scale_factor: 1.5,
526 estimate_gas_scale_factor: 1.3,
527 },
528 BuiltinNetwork::EraTestnet => ForkConfig {
529 url: "https://sepolia.era.zksync.dev".parse().unwrap(),
530 estimate_gas_price_scale_factor: 2.0,
531 estimate_gas_scale_factor: 1.3,
532 },
533 BuiltinNetwork::Abstract => ForkConfig {
534 url: "https://api.mainnet.abs.xyz".parse().unwrap(),
535 estimate_gas_price_scale_factor: 1.5,
536 estimate_gas_scale_factor: 1.3,
537 },
538 BuiltinNetwork::AbstractTestnet => ForkConfig {
539 url: "https://api.testnet.abs.xyz".parse().unwrap(),
540 estimate_gas_price_scale_factor: 1.5,
541 estimate_gas_scale_factor: 1.3,
542 },
543 BuiltinNetwork::Sophon => ForkConfig {
544 url: "https://rpc.sophon.xyz".parse().unwrap(),
545 estimate_gas_price_scale_factor: 1.5,
546 estimate_gas_scale_factor: 1.3,
547 },
548 BuiltinNetwork::SophonTestnet => ForkConfig {
549 url: "https://rpc.testnet.sophon.xyz".parse().unwrap(),
550 estimate_gas_price_scale_factor: 1.5,
551 estimate_gas_scale_factor: 1.3,
552 },
553 BuiltinNetwork::Cronos => ForkConfig {
554 url: "https://mainnet.zkevm.cronos.org".parse().unwrap(),
555 estimate_gas_price_scale_factor: 1.5,
556 estimate_gas_scale_factor: 1.3,
557 },
558 BuiltinNetwork::CronosTestnet => ForkConfig {
559 url: "https://testnet.zkevm.cronos.org".parse().unwrap(),
560 estimate_gas_price_scale_factor: 1.5,
561 estimate_gas_scale_factor: 1.3,
562 },
563 BuiltinNetwork::Lens => ForkConfig {
564 url: "https://rpc.lens.xyz".parse().unwrap(),
565 estimate_gas_price_scale_factor: 1.5,
566 estimate_gas_scale_factor: 1.3,
567 },
568 BuiltinNetwork::LensTestnet => ForkConfig {
569 url: "https://rpc.testnet.lens.xyz".parse().unwrap(),
570 estimate_gas_price_scale_factor: 1.5,
571 estimate_gas_scale_factor: 1.3,
572 },
573 BuiltinNetwork::Openzk => ForkConfig {
574 url: "https://rpc.openzk.net".parse().unwrap(),
575 estimate_gas_price_scale_factor: 1.5,
576 estimate_gas_scale_factor: 1.3,
577 },
578 BuiltinNetwork::OpenzkTestnet => ForkConfig {
579 url: "https://openzk-testnet.rpc.caldera.xyz/http"
580 .parse()
581 .unwrap(),
582 estimate_gas_price_scale_factor: 1.5,
583 estimate_gas_scale_factor: 1.3,
584 },
585 BuiltinNetwork::Zkcandy => ForkConfig {
586 url: "https://rpc.zkcandy.io".parse().unwrap(),
587 estimate_gas_price_scale_factor: 1.5,
588 estimate_gas_scale_factor: 1.3,
589 },
590 }
591 }
592}
593
594impl ForkUrl {
595 pub fn to_config(&self) -> ForkConfig {
597 match self {
598 ForkUrl::Builtin(net) => net.to_fork_config(),
599 ForkUrl::Custom(url) => ForkConfig::unknown(url.clone()),
600 }
601 }
602}
603
604impl FromStr for ForkUrl {
605 type Err = String;
606
607 fn from_str(s: &str) -> Result<Self, Self::Err> {
608 if let Ok(net) = BuiltinNetwork::from_str(s, true) {
609 return Ok(ForkUrl::Builtin(net));
610 }
611 Url::parse(s)
612 .map(ForkUrl::Custom)
613 .map_err(|e| format!("`{s}` is neither a known network nor a valid URL: {e}"))
614 }
615}
616
617impl Cli {
618 pub fn deprecated_config_option() {
620 let args: Vec<String> = env::args().collect();
621
622 let deprecated_flags: HashMap<&str, &str> = [
623 ("--config",
624 "⚠ The '--config' option has been removed. Please migrate to using other configuration options or defaults."),
625
626 ("--show-calls",
627 "⚠ The '--show-calls' option is deprecated. Use verbosity levels instead:\n\
628 -vv → Show user calls\n\
629 -vvv → Show system calls"),
630
631 ("--show-event-logs",
632 "⚠ The '--show-event-logs' option is deprecated. Event logs are now included in traces by default.\n\
633 Use verbosity levels instead:\n\
634 -vv → Show user calls\n\
635 -vvv → Show system calls"),
636
637 ("--resolve-hashes",
638 "⚠ The '--resolve-hashes' option is deprecated. Automatic decoding of function and event selectors\n\
639 using OpenChain is now enabled by default, unless running in offline mode.\n\
640 If needed, disable it explicitly with `--offline`."),
641
642 ("--show-outputs",
643 "⚠ The '--show-outputs' option has been deprecated. Output logs are now included in traces by default."),
644
645 ("--debug",
646 "⚠ The '--debug' (or '-d') option is deprecated. Use verbosity levels instead:\n\
647 -vv → Show user calls\n\
648 -vvv → Show system calls"),
649
650 ("-d",
651 "⚠ The '-d' option is deprecated. Use verbosity levels instead:\n\
652 -vv → Show user calls\n\
653 -vvv → Show system calls"),
654 ]
655 .iter()
656 .copied()
657 .collect();
658
659 let prefix_flags = [
661 "--config=",
662 "--show-calls=",
663 "--resolve-hashes=",
664 "--show-outputs=",
665 "--show-event-logs=",
666 ];
667
668 let mut detected = false;
669
670 for arg in &args {
671 if let Some(warning) = deprecated_flags.get(arg.as_str()) {
672 if !detected {
673 sh_warn!("⚠ Deprecated CLI Options Detected (as of v0.4.0):\n");
674 sh_warn!("[Options below will be removed in v0.4.1]\n");
675 detected = true;
676 }
677 sh_warn!("{}", warning);
678 } else if let Some(base_flag) =
679 prefix_flags.iter().find(|&&prefix| arg.starts_with(prefix))
680 {
681 let warning = deprecated_flags
682 .get(base_flag.trim_end_matches("="))
683 .unwrap_or(&"⚠ Unknown deprecated option.");
684 if !detected {
685 sh_warn!("⚠ Deprecated CLI Options Detected (as of v0.4.0):\n");
686 sh_warn!("[Options below will be removed in v0.4.1]\n");
687 detected = true;
688 }
689 sh_warn!("{}", warning);
690 }
691 }
692 }
693
694 pub fn into_test_node_config(
696 self,
697 ) -> Result<TestNodeConfig, zksync_error::anvil_zksync::env::AnvilEnvironmentError> {
698 let debug_self_repr = format!("{self:#?}");
700
701 let genesis_balance = U256::from(self.balance as u128 * 10u128.pow(18));
702 let mut config = TestNodeConfig::default()
703 .with_port(self.port)
704 .with_offline(if self.offline { Some(true) } else { None })
705 .with_l1_gas_price(self.l1_gas_price)
706 .with_l2_gas_price(self.l2_gas_price)
707 .with_l1_pubdata_price(self.l1_pubdata_price)
708 .with_vm_log_detail(self.show_vm_details)
709 .with_show_storage_logs(self.show_storage_logs)
710 .with_show_gas_details(self.show_gas_details)
711 .with_gas_limit_scale(self.limit_scale_factor)
712 .with_price_scale(self.price_scale_factor)
713 .with_verbosity_level(self.verbosity)
714 .with_show_node_config(self.show_node_config)
715 .with_silent(self.silent)
716 .with_system_contracts(self.dev_system_contracts)
717 .with_system_contracts_path(self.system_contracts_path.clone())
718 .with_evm_emulator_path(self.evm_emulator_path.clone())
719 .with_protocol_version(self.protocol_version)
720 .with_override_bytecodes_dir(self.override_bytecodes_dir.clone())
721 .with_enforce_bytecode_compression(self.enforce_bytecode_compression)
722 .with_log_level(self.log)
723 .with_log_file_path(self.log_file_path.clone())
724 .with_account_generator(self.account_generator())
725 .with_auto_impersonate(self.auto_impersonate)
726 .with_genesis_balance(genesis_balance)
727 .with_cache_dir(self.cache_dir.clone())
728 .with_cache_config(self.cache.map(|cache_type| {
729 match cache_type {
730 CacheType::None => CacheConfig::None,
731 CacheType::Memory => CacheConfig::Memory,
732 CacheType::Disk => CacheConfig::Disk {
733 dir: self
734 .cache_dir
735 .unwrap_or_else(|| DEFAULT_DISK_CACHE_DIR.to_string()),
736 reset: self.reset_cache.unwrap_or(false),
737 },
738 }
739 }))
740 .with_genesis_timestamp(self.timestamp)
741 .with_genesis(self.init)
742 .with_chain_id(self.chain_id)
743 .set_config_out(self.config_out)
744 .with_host(self.host)
745 .with_evm_interpreter(if self.evm_interpreter {
746 Some(true)
747 } else {
748 None
749 })
750 .with_zksync_os(self.zksync_os_group.into())
751 .with_health_check_endpoint(if self.health_check_endpoint {
752 Some(true)
753 } else {
754 None
755 })
756 .with_block_time(self.block_time)
757 .with_no_mining(self.no_mining)
758 .with_allow_origin(self.allow_origin)
759 .with_no_cors(self.no_cors)
760 .with_transaction_order(self.order)
761 .with_state(self.state)
762 .with_state_interval(self.state_interval)
763 .with_dump_state(self.dump_state)
764 .with_preserve_historical_states(self.preserve_historical_states)
765 .with_load_state(self.load_state)
766 .with_l1_config(self.l1_group.and_then(|group| {
767 group.spawn_l1.map(|port| L1Config::Spawn { port }).or(group
768 .external_l1
769 .map(|address| L1Config::External { address }))
770 }))
771 .with_auto_execute_l1(self.auto_execute_l1)
772 .with_base_token_config({
773 let ratio = self.base_token_ratio.unwrap_or(Ratio::ONE);
774 BaseTokenConfig {
775 symbol: self.base_token_symbol.unwrap_or("ETH".to_string()),
776 ratio: BaseTokenConversionRatio::new_simple(ConversionRatio {
777 numerator: NonZeroU64::new(*ratio.numer())
778 .expect("base token conversion ratio cannot have 0 as numerator"),
779 denominator: NonZeroU64::new(*ratio.denom())
780 .expect("base token conversion ratio cannot have 0 as denominator"),
781 }),
782 }
783 });
784
785 if self.evm_interpreter && config.protocol_version() < ProtocolVersionId::Version27 {
786 return Err(zksync_error::anvil_zksync::env::InvalidArguments {
787 details: "EVM interpreter requires protocol version 27 or higher".into(),
788 arguments: debug_self_repr,
789 });
790 }
791
792 if let Some(Command::DebugTrace(args)) = &self.command {
793 let dt = DebugTraceConfig {
794 fork_url: args.fork_url.to_config().url.to_string(),
795 tx: args.tx,
796 only_top: args.only_top,
797 };
798 config = config.with_debug_trace(Some(dt));
799 }
800
801 Ok(config)
802 }
803
804 pub fn into_telemetry_props(self) -> TelemetryProps {
806 TelemetryProps::new()
807 .insert("command", get_cli_command_telemetry_props(self.command))
808 .insert_with("offline", self.offline, |v| v.then_some(v))
809 .insert_with("health_check_endpoint", self.health_check_endpoint, |v| {
810 v.then_some(v)
811 })
812 .insert_with("config_out", self.config_out, |v| {
813 v.map(|_| TELEMETRY_SENSITIVE_VALUE)
814 })
815 .insert_with("port", self.port, |v| {
816 v.filter(|&p| p.to_string() != DEFAULT_PORT)
817 .map(|_| TELEMETRY_SENSITIVE_VALUE)
818 })
819 .insert_with("host", &self.host, |v| {
820 v.first()
821 .filter(|&h| h.to_string() != DEFAULT_HOST || self.host.len() != 1)
822 .map(|_| TELEMETRY_SENSITIVE_VALUE)
823 })
824 .insert_with("chain_id", self.chain_id, |v| {
825 v.map(|_| TELEMETRY_SENSITIVE_VALUE)
826 })
827 .insert_with("show_node_config", self.show_node_config, |v| {
828 (!v.unwrap_or(false)).then_some(false)
829 })
830 .insert(
831 "show_storage_logs",
832 self.show_storage_logs.map(|v| v.to_string()),
833 )
834 .insert(
835 "show_vm_details",
836 self.show_vm_details.map(|v| v.to_string()),
837 )
838 .insert(
839 "show_gas_details",
840 self.show_gas_details.map(|v| v.to_string()),
841 )
842 .insert(
843 "l1_gas_price",
844 self.l1_gas_price.map(serde_json::Number::from),
845 )
846 .insert(
847 "l2_gas_price",
848 self.l2_gas_price.map(serde_json::Number::from),
849 )
850 .insert(
851 "l1_pubdata_price",
852 self.l1_pubdata_price.map(serde_json::Number::from),
853 )
854 .insert(
855 "price_scale_factor",
856 self.price_scale_factor.map(|v| {
857 serde_json::Number::from_f64(v).unwrap_or(serde_json::Number::from(0))
858 }),
859 )
860 .insert(
861 "limit_scale_factor",
862 self.limit_scale_factor.map(|v| {
863 serde_json::Number::from_f64(v as f64).unwrap_or(serde_json::Number::from(0))
864 }),
865 )
866 .insert_with("override_bytecodes_dir", self.override_bytecodes_dir, |v| {
867 v.map(|_| TELEMETRY_SENSITIVE_VALUE)
868 })
869 .insert(
870 "dev_system_contracts",
871 self.dev_system_contracts.map(|v| format!("{v:?}")),
872 )
873 .insert(
874 "protocol_version",
875 self.protocol_version.map(|v| v.to_string()),
876 )
877 .insert_with("evm_interpreter", self.evm_interpreter, |v| v.then_some(v))
878 .insert("log", self.log.map(|v| v.to_string()))
879 .insert_with("log_file_path", self.log_file_path, |v| {
880 v.map(|_| TELEMETRY_SENSITIVE_VALUE)
881 })
882 .insert("silent", self.silent)
883 .insert("cache", self.cache.map(|v| format!("{v:?}")))
884 .insert("reset_cache", self.reset_cache)
885 .insert_with("cache_dir", self.cache_dir, |v| {
886 v.map(|_| TELEMETRY_SENSITIVE_VALUE)
887 })
888 .insert_with("accounts", self.accounts, |v| {
889 (v.to_string() != DEFAULT_ACCOUNTS).then_some(serde_json::Number::from(v))
890 })
891 .insert_with("balance", self.balance, |v| {
892 (v.to_string() != DEFAULT_BALANCE).then_some(serde_json::Number::from(v))
893 })
894 .insert("timestamp", self.timestamp.map(serde_json::Number::from))
895 .insert_with("init", self.init, |v| v.map(|_| TELEMETRY_SENSITIVE_VALUE))
896 .insert_with("state", self.state, |v| {
897 v.map(|_| TELEMETRY_SENSITIVE_VALUE)
898 })
899 .insert(
900 "state_interval",
901 self.state_interval.map(serde_json::Number::from),
902 )
903 .insert_with("dump_state", self.dump_state, |v| {
904 v.map(|_| TELEMETRY_SENSITIVE_VALUE)
905 })
906 .insert_with(
907 "preserve_historical_states",
908 self.preserve_historical_states,
909 |v| v.then_some(v),
910 )
911 .insert_with("load_state", self.load_state, |v| {
912 v.map(|_| TELEMETRY_SENSITIVE_VALUE)
913 })
914 .insert_with("mnemonic", self.mnemonic, |v| {
915 v.map(|_| TELEMETRY_SENSITIVE_VALUE)
916 })
917 .insert_with("mnemonic_random", self.mnemonic_random, |v| {
918 v.map(|_| TELEMETRY_SENSITIVE_VALUE)
919 })
920 .insert_with("mnemonic_seed", self.mnemonic_seed, |v| {
921 v.map(|_| TELEMETRY_SENSITIVE_VALUE)
922 })
923 .insert_with("derivation_path", self.derivation_path, |v| {
924 v.map(|_| TELEMETRY_SENSITIVE_VALUE)
925 })
926 .insert_with("auto_impersonate", self.auto_impersonate, |v| {
927 v.then_some(v)
928 })
929 .insert("block_time", self.block_time.map(|v| format!("{v:?}")))
930 .insert_with("no_mining", self.no_mining, |v| v.then_some(v))
931 .insert_with("allow_origin", self.allow_origin, |v| {
932 (v != DEFAULT_ALLOW_ORIGIN).then_some(TELEMETRY_SENSITIVE_VALUE)
933 })
934 .insert_with("no_cors", self.no_cors, |v| v.then_some(v))
935 .insert_with("order", self.order, |v| {
936 (v.to_string() != DEFAULT_TX_ORDER).then_some(v.to_string())
937 })
938 .take()
939 }
940
941 fn account_generator(&self) -> AccountGenerator {
942 let mut generator = AccountGenerator::new(self.accounts as usize)
943 .phrase(DEFAULT_MNEMONIC)
944 .chain_id(self.chain_id.unwrap_or(TEST_NODE_NETWORK_ID));
945 if let Some(ref mnemonic) = self.mnemonic {
946 generator = generator.phrase(mnemonic);
947 } else if let Some(count) = self.mnemonic_random {
948 let mut rng = rand::thread_rng();
949 let mnemonic = match Mnemonic::<English>::new_with_count(&mut rng, count) {
950 Ok(mnemonic) => mnemonic.to_phrase(),
951 Err(_) => DEFAULT_MNEMONIC.to_string(),
952 };
953 generator = generator.phrase(mnemonic);
954 } else if let Some(seed) = self.mnemonic_seed {
955 let mut seed = StdRng::seed_from_u64(seed);
956 let mnemonic = Mnemonic::<English>::new(&mut seed).to_phrase();
957 generator = generator.phrase(mnemonic);
958 }
959 if let Some(ref derivation) = self.derivation_path {
960 generator = generator.derivation_path(derivation);
961 }
962 generator
963 }
964}
965
966fn duration_from_secs_f64(s: &str) -> Result<Duration, String> {
967 let s = s.parse::<f64>().map_err(|e| e.to_string())?;
968 if s == 0.0 {
969 return Err("Duration must be greater than 0".to_string());
970 }
971 Duration::try_from_secs_f64(s).map_err(|e| e.to_string())
972}
973
974fn protocol_version_from_str(s: &str) -> anyhow::Result<ProtocolVersionId> {
975 let version = s.parse::<u16>()?;
976 Ok(ProtocolVersionId::try_from(version)?)
977}
978
979pub struct PeriodicStateDumper {
982 in_progress_dump: Option<Pin<Box<dyn Future<Output = ()> + Send + Sync + 'static>>>,
983 node: InMemoryNode,
984 dump_state: Option<PathBuf>,
985 preserve_historical_states: bool,
986 interval: Interval,
987}
988
989impl PeriodicStateDumper {
990 pub fn new(
991 node: InMemoryNode,
992 dump_state: Option<PathBuf>,
993 interval: Duration,
994 preserve_historical_states: bool,
995 ) -> Self {
996 let dump_state = dump_state.map(|mut dump_state| {
997 if dump_state.is_dir() {
998 dump_state = dump_state.join("state.json");
999 }
1000 dump_state
1001 });
1002
1003 let interval = tokio::time::interval_at(Instant::now() + interval, interval);
1004 Self {
1005 in_progress_dump: None,
1006 node,
1007 dump_state,
1008 preserve_historical_states,
1009 interval,
1010 }
1011 }
1012
1013 #[allow(dead_code)] pub async fn dump(&self) {
1015 if let Some(state) = self.dump_state.clone() {
1016 Self::dump_state(self.node.clone(), state, self.preserve_historical_states).await
1017 }
1018 }
1019
1020 async fn dump_state(node: InMemoryNode, dump_path: PathBuf, preserve_historical_states: bool) {
1022 tracing::trace!(path=?dump_path, "Dumping state");
1023
1024 let state_bytes = match node.dump_state(preserve_historical_states).await {
1026 Ok(bytes) => bytes,
1027 Err(err) => {
1028 sh_err!("Failed to dump state: {:?}", err);
1029 return;
1030 }
1031 };
1032
1033 let mut decoder = GzDecoder::new(&state_bytes.0[..]);
1034 let mut json_str = String::new();
1035 if let Err(err) = decoder.read_to_string(&mut json_str) {
1036 sh_err!("Failed to decompress state bytes: {}", err);
1037 return;
1038 }
1039
1040 let state = match serde_json::from_str::<VersionedState>(&json_str) {
1041 Ok(state) => state,
1042 Err(err) => {
1043 sh_err!("Failed to parse state JSON: {}", err);
1044 return;
1045 }
1046 };
1047
1048 if let Err(err) = write_json_file(&dump_path, &state) {
1049 sh_err!("Failed to write state to file: {}", err);
1050 } else {
1051 tracing::trace!(path = ?dump_path, "Dumped state successfully");
1052 }
1053 }
1054}
1055
1056impl Future for PeriodicStateDumper {
1059 type Output = anyhow::Result<()>;
1060
1061 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1062 let this = self.get_mut();
1063 if this.dump_state.is_none() {
1064 return Poll::Pending;
1065 }
1066
1067 loop {
1068 if let Some(mut flush) = this.in_progress_dump.take() {
1069 match flush.poll_unpin(cx) {
1070 Poll::Ready(_) => {
1071 this.interval.reset();
1072 }
1073 Poll::Pending => {
1074 this.in_progress_dump = Some(flush);
1075 return Poll::Pending;
1076 }
1077 }
1078 }
1079
1080 if this.interval.poll_tick(cx).is_ready() {
1081 let api = this.node.clone();
1082 let path = this.dump_state.clone().expect("exists; see above");
1083 this.in_progress_dump = Some(Box::pin(Self::dump_state(
1084 api,
1085 path,
1086 this.preserve_historical_states,
1087 )));
1088 } else {
1089 break;
1090 }
1091 }
1092
1093 Poll::Pending
1094 }
1095}
1096
1097#[cfg(test)]
1098mod tests {
1099 use crate::cli::PeriodicStateDumper;
1100
1101 use super::Cli;
1102 use anvil_zksync_core::node::InMemoryNode;
1103 use clap::Parser;
1104 use serde_json::{Value, json};
1105 use std::{
1106 env,
1107 net::{IpAddr, Ipv4Addr},
1108 };
1109 use zksync_types::{H160, U256};
1110
1111 #[test]
1112 fn can_parse_host() {
1113 let args = Cli::parse_from(["anvil-zksync"]);
1115 assert_eq!(args.host, vec![IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))]);
1116
1117 let args = Cli::parse_from([
1118 "anvil-zksync",
1119 "--host",
1120 "::1",
1121 "--host",
1122 "1.1.1.1",
1123 "--host",
1124 "2.2.2.2",
1125 ]);
1126 assert_eq!(
1127 args.host,
1128 ["::1", "1.1.1.1", "2.2.2.2"]
1129 .map(|ip| ip.parse::<IpAddr>().unwrap())
1130 .to_vec()
1131 );
1132
1133 let args = Cli::parse_from(["anvil-zksync", "--host", "::1,1.1.1.1,2.2.2.2"]);
1134 assert_eq!(
1135 args.host,
1136 ["::1", "1.1.1.1", "2.2.2.2"]
1137 .map(|ip| ip.parse::<IpAddr>().unwrap())
1138 .to_vec()
1139 );
1140
1141 unsafe {
1142 env::set_var("ANVIL_ZKSYNC_IP_ADDR", "1.1.1.1");
1143 }
1144 let args = Cli::parse_from(["anvil-zksync"]);
1145 assert_eq!(args.host, vec!["1.1.1.1".parse::<IpAddr>().unwrap()]);
1146
1147 unsafe {
1148 env::set_var("ANVIL_ZKSYNC_IP_ADDR", "::1,1.1.1.1,2.2.2.2");
1149 }
1150 let args = Cli::parse_from(["anvil-zksync"]);
1151 assert_eq!(
1152 args.host,
1153 ["::1", "1.1.1.1", "2.2.2.2"]
1154 .map(|ip| ip.parse::<IpAddr>().unwrap())
1155 .to_vec()
1156 );
1157 }
1158
1159 #[tokio::test]
1160 async fn test_dump_state() -> anyhow::Result<()> {
1161 let temp_dir = tempfile::Builder::new()
1162 .prefix("state-test")
1163 .tempdir()
1164 .expect("failed creating temporary dir");
1165 let dump_path = temp_dir.path().join("state.json");
1166
1167 let config = anvil_zksync_config::TestNodeConfig {
1168 dump_state: Some(dump_path.clone()),
1169 state_interval: Some(1),
1170 preserve_historical_states: true,
1171 ..Default::default()
1172 };
1173
1174 let node = InMemoryNode::test_config(None, config.clone());
1175
1176 let mut state_dumper = PeriodicStateDumper::new(
1177 node.clone(),
1178 config.dump_state.clone(),
1179 std::time::Duration::from_secs(1),
1180 config.preserve_historical_states,
1181 );
1182
1183 let dumper_handle = tokio::spawn(async move {
1185 tokio::select! {
1186 _ = &mut state_dumper => {}
1187 }
1188 state_dumper.dump().await;
1189 });
1190 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
1191
1192 dumper_handle.abort();
1193 let _ = dumper_handle.await;
1194
1195 let dumped_data =
1196 std::fs::read_to_string(&dump_path).expect("Expected state file to be created");
1197 let _: Value =
1198 serde_json::from_str(&dumped_data).expect("Failed to parse dumped state as JSON");
1199
1200 Ok(())
1201 }
1202
1203 #[tokio::test]
1204 async fn test_load_state() -> anyhow::Result<()> {
1205 let temp_dir = tempfile::Builder::new()
1206 .prefix("state-load-test")
1207 .tempdir()
1208 .expect("failed creating temporary dir");
1209 let state_path = temp_dir.path().join("state.json");
1210
1211 let config = anvil_zksync_config::TestNodeConfig {
1212 dump_state: Some(state_path.clone()),
1213 state_interval: Some(1),
1214 preserve_historical_states: true,
1215 ..Default::default()
1216 };
1217
1218 let node = InMemoryNode::test_config(None, config.clone());
1219 let test_address = H160::from_low_u64_be(12345);
1220 node.set_rich_account(test_address, U256::from(1000000u64))
1221 .await;
1222
1223 let mut state_dumper = PeriodicStateDumper::new(
1224 node.clone(),
1225 config.dump_state.clone(),
1226 std::time::Duration::from_secs(1),
1227 config.preserve_historical_states,
1228 );
1229
1230 let dumper_handle = tokio::spawn(async move {
1231 tokio::select! {
1232 _ = &mut state_dumper => {}
1233 }
1234 state_dumper.dump().await;
1235 });
1236
1237 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
1238
1239 dumper_handle.abort();
1240 let _ = dumper_handle.await;
1241
1242 std::fs::read_to_string(&state_path).expect("Expected state file to be created");
1244
1245 let new_config = anvil_zksync_config::TestNodeConfig::default();
1246 let new_node = InMemoryNode::test_config(None, new_config.clone());
1247
1248 new_node
1249 .load_state(zksync_types::web3::Bytes(std::fs::read(&state_path)?))
1250 .await?;
1251
1252 let balance = new_node.get_balance_impl(test_address, None).await?;
1254 assert_eq!(balance, U256::from(1000000u64));
1255
1256 Ok(())
1257 }
1258
1259 #[tokio::test]
1260 async fn test_cli_telemetry_data_skips_missing_args() -> anyhow::Result<()> {
1261 let args = Cli::parse_from(["anvil-zksync"]).into_telemetry_props();
1262 let json = args.to_inner();
1263 let expected_json: serde_json::Value = json!({});
1264 assert_eq!(json, expected_json);
1265 Ok(())
1266 }
1267
1268 #[tokio::test]
1269 async fn test_cli_telemetry_data_hides_sensitive_data() -> anyhow::Result<()> {
1270 let args = Cli::parse_from([
1271 "anvil-zksync",
1272 "--offline",
1273 "--host",
1274 "100.100.100.100",
1275 "--port",
1276 "3333",
1277 "--chain-id",
1278 "123",
1279 "--config-out",
1280 "/some/path",
1281 "fork",
1282 "--fork-url",
1283 "era",
1284 ])
1285 .into_telemetry_props();
1286 let json = args.to_inner();
1287 let expected_json: serde_json::Value = json!({
1288 "command": {
1289 "args": {
1290 "fork_url": "Builtin(Era)"
1291 },
1292 "name": "fork"
1293 },
1294 "offline": true,
1295 "config_out": "***",
1296 "port": "***",
1297 "host": "***",
1298 "chain_id": "***"
1299 });
1300 assert_eq!(json, expected_json);
1301 Ok(())
1302 }
1303}