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