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