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