anvil_zksync/
main.rs

1use crate::bytecode_override::override_bytecodes;
2use crate::cli::{BuiltinNetwork, Cli, Command, ForkUrl, PeriodicStateDumper};
3use crate::utils::update_with_fork_details;
4use alloy::primitives::Bytes;
5use anvil_zksync_api_server::NodeServerBuilder;
6use anvil_zksync_common::shell::get_shell;
7use anvil_zksync_common::utils::predeploys::PREDEPLOYS;
8use anvil_zksync_common::{sh_eprintln, sh_err, sh_println, sh_warn};
9use anvil_zksync_config::constants::{
10    DEFAULT_ESTIMATE_GAS_PRICE_SCALE_FACTOR, DEFAULT_ESTIMATE_GAS_SCALE_FACTOR,
11    DEFAULT_FAIR_PUBDATA_PRICE, DEFAULT_L1_GAS_PRICE, DEFAULT_L2_GAS_PRICE,
12    EVM_EMULATOR_ENABLER_CALLDATA, LEGACY_RICH_WALLETS, PSEUDO_CALLER, RICH_WALLETS,
13    TEST_NODE_NETWORK_ID,
14};
15use anvil_zksync_config::types::SystemContractsOptions;
16use anvil_zksync_config::{ForkPrintInfo, L1Config};
17use anvil_zksync_core::filters::EthFilters;
18use anvil_zksync_core::node::fork::ForkClient;
19use anvil_zksync_core::node::{
20    BlockSealer, BlockSealerMode, ImpersonationManager, InMemoryNode, InMemoryNodeInner,
21    NodeExecutor, StorageKeyLayout, TestNodeFeeInputProvider, TxBatch, TxPool,
22};
23use anvil_zksync_core::observability::Observability;
24use anvil_zksync_core::system_contracts::SystemContractsBuilder;
25use anvil_zksync_l1_sidecar::L1Sidecar;
26use anvil_zksync_types::L2TxBuilder;
27use anyhow::Context;
28use clap::Parser;
29use indicatif::{ProgressBar, ProgressStyle};
30use std::fmt::Write;
31use std::fs::File;
32use std::future::Future;
33use std::path::PathBuf;
34use std::pin::Pin;
35use std::sync::Arc;
36use std::time::Duration;
37use std::{env, net::SocketAddr, str::FromStr};
38use tokio::sync::RwLock;
39use tower_http::cors::AllowOrigin;
40use tracing_subscriber::filter::LevelFilter;
41use zksync_error::anvil_zksync::gen::{generic_error, to_domain};
42use zksync_error::anvil_zksync::AnvilZksyncError;
43use zksync_error::{ICustomError, IError as _};
44use zksync_telemetry::{get_telemetry, init_telemetry, TelemetryProps};
45use zksync_types::fee_model::{FeeModelConfigV2, FeeParams};
46use zksync_types::{
47    L2BlockNumber, Nonce, CONTRACT_DEPLOYER_ADDRESS, EVM_PREDEPLOYS_MANAGER_ADDRESS, H160, U256,
48};
49
50mod bytecode_override;
51mod cli;
52mod utils;
53
54const POSTHOG_API_KEY: &str = "phc_TsD52JxwkT2OXPHA2oKX2Lc3mf30hItCBrE9s9g1MKe";
55const TELEMETRY_CONFIG_NAME: &str = "zksync-tooling";
56
57async fn start_program() -> Result<(), AnvilZksyncError> {
58    // Check for deprecated options
59    Cli::deprecated_config_option();
60
61    let opt = Cli::parse();
62    // We keep a serialized version of the provided arguments to communicate them later if the arguments were incorrect.
63    let debug_opt_string_repr = format!("{opt:#?}");
64
65    let command = opt.command.clone();
66
67    // Track node start
68    let telemetry = get_telemetry().expect("telemetry is not initialized");
69    let cli_telemetry_props = opt.clone().into_telemetry_props();
70    let _ = telemetry
71        .track_event(
72            "node_started",
73            TelemetryProps::new()
74                .insert("params", Some(cli_telemetry_props))
75                .take(),
76        )
77        .await;
78
79    let mut config = opt.into_test_node_config().map_err(to_domain)?;
80    // Set verbosity level for the shell
81    {
82        let mut shell = get_shell();
83        shell.verbosity = config.verbosity;
84    }
85    let log_level_filter = LevelFilter::from(config.log_level);
86    let log_file = File::create(&config.log_file_path).map_err(|inner| {
87        zksync_error::anvil_zksync::env::LogFileAccessFailed {
88            log_file_path: config.log_file_path.to_string(),
89            wrapped_error: inner.to_string(),
90        }
91    })?;
92
93    // Initialize the tracing subscriber
94    let observability = Observability::init(
95        vec!["anvil_zksync".into()],
96        log_level_filter,
97        log_file,
98        config.silent,
99    )
100    .map_err(|error| zksync_error::anvil_zksync::env::GenericError {
101        message: format!(
102            "Internal error: Unable to set up observability. Please report. \n{error:#?}"
103        ),
104    })?;
105
106    // Use `Command::Run` as default.
107    let command = command.as_ref().unwrap_or(&Command::Run);
108    let (fork_client, transactions_to_replay) = match command {
109        Command::Run => {
110            if config.offline {
111                sh_warn!("Running in offline mode: default fee parameters will be used.");
112                config = config
113                    .clone()
114                    .with_l1_gas_price(config.l1_gas_price.or(Some(DEFAULT_L1_GAS_PRICE)))
115                    .with_l2_gas_price(config.l2_gas_price.or(Some(DEFAULT_L2_GAS_PRICE)))
116                    .with_price_scale(
117                        config
118                            .price_scale_factor
119                            .or(Some(DEFAULT_ESTIMATE_GAS_PRICE_SCALE_FACTOR)),
120                    )
121                    .with_gas_limit_scale(
122                        config
123                            .limit_scale_factor
124                            .or(Some(DEFAULT_ESTIMATE_GAS_SCALE_FACTOR)),
125                    )
126                    .with_l1_pubdata_price(
127                        config.l1_pubdata_price.or(Some(DEFAULT_FAIR_PUBDATA_PRICE)),
128                    )
129                    .with_chain_id(config.chain_id.or(Some(TEST_NODE_NETWORK_ID)));
130                (None, Vec::new())
131            } else {
132                // Initialize the client to get the fee params
133                let client = ForkClient::at_block_number(
134                    ForkUrl::Builtin(BuiltinNetwork::Era).to_config(),
135                    None,
136                )
137                .await
138                .map_err(to_domain)?;
139                let fee = client.get_fee_params().await.map_err(to_domain)?;
140
141                match fee {
142                    FeeParams::V2(fee_v2) => {
143                        config = config
144                            .clone()
145                            .with_l1_gas_price(config.l1_gas_price.or(Some(fee_v2.l1_gas_price())))
146                            .with_l2_gas_price(
147                                config
148                                    .l2_gas_price
149                                    .or(Some(fee_v2.config().minimal_l2_gas_price)),
150                            )
151                            .with_price_scale(
152                                config
153                                    .price_scale_factor
154                                    .or(Some(DEFAULT_ESTIMATE_GAS_PRICE_SCALE_FACTOR)),
155                            )
156                            .with_gas_limit_scale(
157                                config
158                                    .limit_scale_factor
159                                    .or(Some(DEFAULT_ESTIMATE_GAS_SCALE_FACTOR)),
160                            )
161                            .with_l1_pubdata_price(
162                                config.l1_pubdata_price.or(Some(fee_v2.l1_pubdata_price())),
163                            )
164                            .with_chain_id(config.chain_id.or(Some(TEST_NODE_NETWORK_ID)));
165                    }
166                    FeeParams::V1(_) => {
167                        return Err(
168                            generic_error!("Unsupported FeeParams::V1 in this context").into()
169                        );
170                    }
171                }
172
173                (None, Vec::new())
174            }
175        }
176        Command::Fork(fork) => {
177            let (fork_client, earlier_txs) = if let Some(tx_hash) = fork.fork_transaction_hash {
178                // If transaction hash is provided, we fork at the parent of block containing tx
179                ForkClient::at_before_tx(fork.fork_url.to_config(), tx_hash)
180                    .await
181                    .map_err(to_domain)?
182            } else {
183                // Otherwise, we fork at the provided block
184                (
185                    ForkClient::at_block_number(
186                        fork.fork_url.to_config(),
187                        fork.fork_block_number.map(|bn| L2BlockNumber(bn as u32)),
188                    )
189                    .await
190                    .map_err(to_domain)?,
191                    Vec::new(),
192                )
193            };
194
195            update_with_fork_details(&mut config, &fork_client.details).await;
196            (Some(fork_client), earlier_txs)
197        }
198        Command::ReplayTx(replay_tx) => {
199            let (fork_client, earlier_txs) =
200                ForkClient::at_before_tx(replay_tx.fork_url.to_config(), replay_tx.tx)
201                    .await
202                    .map_err(to_domain)?;
203
204            update_with_fork_details(&mut config, &fork_client.details).await;
205            (Some(fork_client), earlier_txs)
206        }
207    };
208
209    // Ensure that system_contracts_path is only used with Local.
210    if config.system_contracts_options != SystemContractsOptions::Local
211        && config.system_contracts_path.is_some()
212    {
213        return Err(to_domain(generic_error!(
214            "The --system-contracts-path option can only be specified when --dev-system-contracts is set to 'local'."
215        )));
216    }
217    if let SystemContractsOptions::Local = config.system_contracts_options {
218        // if local system contracts specified, check if the path exists else use env var
219        // ZKSYNC_HOME
220        let path: Option<PathBuf> = config
221            .system_contracts_path
222            .clone()
223            .or_else(|| env::var_os("ZKSYNC_HOME").map(PathBuf::from));
224
225        if let Some(path) = path {
226            if !path.exists() || !path.is_dir() {
227                return Err(to_domain(generic_error!(
228                    "The specified system contracts path '{}' does not exist or is not a directory.",
229                    path.to_string_lossy()
230                )));
231            }
232            tracing::debug!("Reading local contracts from {:?}", path);
233        }
234    }
235
236    let fork_print_info = if let Some(fork_client) = &fork_client {
237        let fee_model_config_v2 = match &fork_client.details.fee_params {
238            FeeParams::V2(fee_params_v2) => {
239                let config = fee_params_v2.config();
240                FeeModelConfigV2 {
241                    minimal_l2_gas_price: config.minimal_l2_gas_price,
242                    compute_overhead_part: config.compute_overhead_part,
243                    pubdata_overhead_part: config.pubdata_overhead_part,
244                    batch_overhead_l1_gas: config.batch_overhead_l1_gas,
245                    max_gas_per_batch: config.max_gas_per_batch,
246                    max_pubdata_per_batch: config.max_pubdata_per_batch,
247                }
248            }
249            _ => {
250                return Err(to_domain(generic_error!(
251                    "fork is using unsupported fee parameters: {:?}",
252                    fork_client.details.fee_params
253                )))
254            }
255        };
256
257        Some(ForkPrintInfo {
258            network_rpc: fork_client.url.to_string(),
259            l1_block: fork_client.details.batch_number.to_string(),
260            l2_block: fork_client.details.block_number.to_string(),
261            block_timestamp: fork_client.details.block_timestamp.to_string(),
262            fork_block_hash: format!("{:#x}", fork_client.details.block_hash),
263            fee_model_config_v2,
264        })
265    } else {
266        None
267    };
268
269    let impersonation = ImpersonationManager::default();
270    if config.enable_auto_impersonate {
271        // Enable auto impersonation if configured
272        impersonation.set_auto_impersonation(true);
273    }
274    let pool = TxPool::new(impersonation.clone(), config.transaction_order);
275
276    let fee_input_provider = TestNodeFeeInputProvider::from_fork(
277        fork_client.as_ref().map(|f| &f.details),
278        &config.base_token_config,
279    );
280    let filters = Arc::new(RwLock::new(EthFilters::default()));
281
282    // Build system contracts
283    let system_contracts = SystemContractsBuilder::new()
284        .system_contracts_options(config.system_contracts_options)
285        .system_contracts_path(config.system_contracts_path.clone())
286        .protocol_version(config.protocol_version())
287        .with_evm_interpreter(config.use_evm_interpreter)
288        .with_boojum(config.boojum.clone())
289        .build();
290
291    let storage_key_layout = if config.boojum.use_boojum {
292        StorageKeyLayout::BoojumOs
293    } else {
294        StorageKeyLayout::ZkEra
295    };
296
297    let is_fork_mode = fork_client.is_some();
298    let (node_inner, storage, blockchain, time, fork, vm_runner) = InMemoryNodeInner::init(
299        fork_client,
300        fee_input_provider.clone(),
301        filters,
302        config.clone(),
303        impersonation.clone(),
304        system_contracts.clone(),
305        storage_key_layout,
306        // Only produce system logs if L1 is enabled
307        config.l1_config.is_some(),
308    );
309
310    let mut node_service_tasks: Vec<Pin<Box<dyn Future<Output = anyhow::Result<()>>>>> = Vec::new();
311    let (node_executor, node_handle) =
312        NodeExecutor::new(node_inner.clone(), vm_runner, storage_key_layout);
313    let l1_sidecar = match config.l1_config.as_ref() {
314        Some(_) if fork_print_info.is_some() => {
315            return Err(zksync_error::anvil_zksync::env::InvalidArguments {
316                details: "Running L1 in forking mode is unsupported".into(),
317                arguments: debug_opt_string_repr,
318            }
319            .into())
320        }
321        Some(L1Config::Spawn { port }) => {
322            let (l1_sidecar, l1_sidecar_runner) = L1Sidecar::process(
323                config.protocol_version(),
324                *port,
325                blockchain.clone(),
326                node_handle.clone(),
327                pool.clone(),
328                config.auto_execute_l1,
329            )
330            .await
331            .map_err(to_domain)?;
332            node_service_tasks.push(Box::pin(l1_sidecar_runner.run()));
333            l1_sidecar
334        }
335        Some(L1Config::External { address }) => {
336            let (l1_sidecar, l1_sidecar_runner) = L1Sidecar::external(
337                config.protocol_version(),
338                address,
339                blockchain.clone(),
340                node_handle.clone(),
341                pool.clone(),
342                config.auto_execute_l1,
343            )
344            .await
345            .map_err(to_domain)?;
346            node_service_tasks.push(Box::pin(l1_sidecar_runner.run()));
347            l1_sidecar
348        }
349        None => L1Sidecar::none(),
350    };
351    let sealing_mode = if config.no_mining {
352        BlockSealerMode::noop()
353    } else if let Some(block_time) = config.block_time {
354        BlockSealerMode::fixed_time(config.max_transactions, block_time)
355    } else {
356        BlockSealerMode::immediate(config.max_transactions, pool.add_tx_listener())
357    };
358    let (block_sealer, block_sealer_state) =
359        BlockSealer::new(sealing_mode, pool.clone(), node_handle.clone());
360    node_service_tasks.push(Box::pin(block_sealer.run()));
361
362    let node: InMemoryNode = InMemoryNode::new(
363        node_inner,
364        blockchain,
365        storage,
366        fork,
367        node_handle.clone(),
368        Some(observability),
369        time,
370        impersonation,
371        pool,
372        block_sealer_state,
373        system_contracts,
374        storage_key_layout,
375    );
376
377    // We start the node executor now so it can receive and handle commands
378    // during replay. Otherwise, replay would send commands and hang.
379    tokio::spawn(async move {
380        if let Err(err) = node_executor.run().await {
381            sh_err!("{err}");
382
383            let _ = telemetry.track_error(Box::new(&err)).await;
384        }
385    });
386
387    if config.use_evm_interpreter {
388        // We need to enable EVM interpreter by setting `allowedBytecodeTypesToDeploy` in `ContractDeployer`
389        // to `1` (i.e. `AllowedBytecodeTypes::EraVmAndEVM`).
390        node.impersonate_account(PSEUDO_CALLER).unwrap();
391        node.set_rich_account(PSEUDO_CALLER, U256::from(1_000_000_000_000u64))
392            .await;
393        let chain_id = node.chain_id().await;
394        let mut txs = Vec::with_capacity(PREDEPLOYS.len() + 1);
395        txs.push(
396            L2TxBuilder::new(
397                PSEUDO_CALLER,
398                Nonce(0),
399                U256::from(300_000),
400                U256::from(u32::MAX),
401                chain_id,
402            )
403            .with_to(CONTRACT_DEPLOYER_ADDRESS)
404            .with_calldata(Bytes::from_static(EVM_EMULATOR_ENABLER_CALLDATA).to_vec())
405            .build_impersonated()
406            .into(),
407        );
408
409        // If evm emulator is enabled, and not in fork mode, deploy pre-deploys for dev convenience
410        if !is_fork_mode {
411            let mut nonce = Nonce(1);
412            for pd in PREDEPLOYS.iter() {
413                let data = pd.encode_manager_call().unwrap();
414                txs.push(
415                    L2TxBuilder::new(
416                        PSEUDO_CALLER,
417                        nonce,
418                        U256::from(10_000_000), // high limit for pre-deploys
419                        U256::from(u32::MAX),
420                        chain_id,
421                    )
422                    .with_to(EVM_PREDEPLOYS_MANAGER_ADDRESS)
423                    .with_calldata(data)
424                    .build_impersonated()
425                    .into(),
426                );
427                nonce += 1;
428            }
429        }
430
431        node_handle
432            .seal_block_sync(TxBatch {
433                impersonating: true,
434                txs,
435            })
436            .await
437            .map_err(to_domain)?;
438        node.set_rich_account(PSEUDO_CALLER, U256::from(0)).await;
439        node.stop_impersonating_account(PSEUDO_CALLER).unwrap();
440    }
441
442    if let Some(ref bytecodes_dir) = config.override_bytecodes_dir {
443        override_bytecodes(&node, bytecodes_dir.to_string())
444            .await
445            .unwrap();
446    }
447
448    if !transactions_to_replay.is_empty() {
449        sh_println!("Executing transactions from the block.");
450        let total_txs = transactions_to_replay.len() as u64;
451        let pb = ProgressBar::new(total_txs);
452        pb.enable_steady_tick(std::time::Duration::from_secs(1));
453        pb.set_style(
454            ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} tx ({eta})")
455                .unwrap()
456                .with_key("eta", |state: &indicatif::ProgressState, w: &mut dyn Write| {
457                    write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()
458                })
459                .progress_chars("#>-")
460            );
461
462        node.node_handle
463            .set_progress_report(Some(pb.clone()))
464            .await
465            .map_err(to_domain)?;
466
467        node.replay_txs(transactions_to_replay)
468            .await
469            .map_err(to_domain)?;
470
471        pb.finish_and_clear();
472        sh_println!("Done replaying transactions.");
473
474        // If we are in replay mode, we don't start the server
475        return Ok(());
476    }
477
478    // TODO: Consider moving to `InMemoryNodeInner::init`
479    let rich_addresses = itertools::chain!(
480        config
481            .genesis_accounts
482            .iter()
483            .map(|acc| H160::from_slice(acc.address().as_ref())),
484        config
485            .signer_accounts
486            .iter()
487            .map(|acc| H160::from_slice(acc.address().as_ref())),
488        LEGACY_RICH_WALLETS
489            .iter()
490            .map(|(address, _)| H160::from_str(address).unwrap()),
491        RICH_WALLETS
492            .iter()
493            .map(|(address, _, _)| H160::from_str(address).unwrap()),
494    )
495    .collect::<Vec<_>>();
496    for address in rich_addresses {
497        node.set_rich_account(address, config.genesis_balance).await;
498    }
499
500    let mut server_builder = NodeServerBuilder::new(
501        node.clone(),
502        l1_sidecar,
503        AllowOrigin::exact(
504            config
505                .allow_origin
506                .parse()
507                .context("allow origin is malformed")
508                .map_err(to_domain)?,
509        ),
510    );
511    if config.health_check_endpoint {
512        server_builder.enable_health_api()
513    }
514    if !config.no_cors {
515        server_builder.enable_cors();
516    }
517    let mut server_handles = Vec::with_capacity(config.host.len());
518    for host in &config.host {
519        let mut addr = SocketAddr::new(*host, config.port);
520
521        match server_builder.clone().build(addr).await {
522            Ok(server) => {
523                config.port = server.local_addr().port();
524                server_handles.push(server.run());
525            }
526            Err(err) => {
527                let port_requested = config.port;
528                sh_eprintln!(
529                    "Failed to bind to address {}:{}: {}. Retrying with a different port...",
530                    host,
531                    config.port,
532                    err
533                );
534
535                // Attempt to bind to a dynamic port
536                addr.set_port(0);
537                match server_builder.clone().build(addr).await {
538                    Ok(server) => {
539                        config.port = server.local_addr().port();
540                        tracing::info!(
541                            "Successfully started server on port {} for host {}",
542                            config.port,
543                            host
544                        );
545                        server_handles.push(server.run());
546                    }
547                    Err(err) => {
548                        return Err(zksync_error::anvil_zksync::env::ServerStartupFailed {
549                            host_requested: host.to_string(),
550                            port_requested: port_requested.into(),
551                            details: err.to_string(),
552                        }
553                        .into());
554                    }
555                }
556            }
557        }
558    }
559    let any_server_stopped =
560        futures::future::select_all(server_handles.into_iter().map(|h| Box::pin(h.stopped())));
561
562    // Load state from `--load-state` if provided
563    if let Some(ref load_state_path) = config.load_state {
564        let bytes = std::fs::read(load_state_path).expect("Failed to read load state file");
565        node.load_state(zksync_types::web3::Bytes(bytes))
566            .await
567            .map_err(to_domain)?;
568    }
569    if let Some(ref state_path) = config.state {
570        let bytes = std::fs::read(state_path).expect("Failed to read load state file");
571        node.load_state(zksync_types::web3::Bytes(bytes))
572            .await
573            .map_err(to_domain)?;
574    }
575
576    let state_path = config.dump_state.clone().or_else(|| config.state.clone());
577    let dump_interval = config
578        .state_interval
579        .map(Duration::from_secs)
580        .unwrap_or(Duration::from_secs(60)); // Default to 60 seconds
581    let preserve_historical_states = config.preserve_historical_states;
582    let node_for_dumper = node.clone();
583    let state_dumper = PeriodicStateDumper::new(
584        node_for_dumper,
585        state_path,
586        dump_interval,
587        preserve_historical_states,
588    );
589    node_service_tasks.push(Box::pin(state_dumper));
590
591    config.print(fork_print_info.as_ref());
592    let node_service_stopped = futures::future::select_all(node_service_tasks);
593
594    tokio::select! {
595        _ = tokio::signal::ctrl_c() => {
596            tracing::trace!("received shutdown signal, shutting down");
597        },
598        _ = any_server_stopped => {
599            tracing::trace!("node server was stopped")
600        },
601        (result, _, _) = node_service_stopped => {
602            // Propagate error that might have happened inside one of the services
603            result.map_err(to_domain)?;
604            tracing::trace!("node service was stopped")
605        }
606    }
607
608    Ok(())
609}
610
611#[tokio::main]
612async fn main() -> Result<(), AnvilZksyncError> {
613    init_telemetry(
614        env!("CARGO_PKG_NAME"),
615        env!("CARGO_PKG_VERSION"),
616        TELEMETRY_CONFIG_NAME,
617        Some(POSTHOG_API_KEY.into()),
618        None,
619        None,
620    )
621    .await
622    .map_err(|inner| zksync_error::anvil_zksync::env::GenericError {
623        message: format!("Failed to initialize telemetry collection subsystem: {inner}."),
624    })?;
625
626    if let Err(err) = start_program().await {
627        let telemetry = get_telemetry().expect("telemetry is not initialized");
628        let _ = telemetry.track_error(Box::new(&err.to_unified())).await;
629        sh_eprintln!("{}", err.to_unified().get_message());
630        return Err(err);
631    }
632    Ok(())
633}