anvil_zksync/
main.rs

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