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 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 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 {
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 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 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 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 ForkClient::at_before_tx(fork.fork_url.to_config(), tx_hash)
150 .await
151 .map_err(to_domain)?
152 } else {
153 (
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 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 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 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 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 let system_contracts = SystemContractsBuilder::new()
316 .system_contracts_options(config.system_contracts_options)
317 .system_contracts_path(config.system_contracts_path.clone())
318 .protocol_version(config.protocol_version())
319 .with_evm_interpreter(config.use_evm_interpreter)
320 .with_zksync_os(config.zksync_os.clone())
321 .build();
322
323 let storage_key_layout = if config.zksync_os.zksync_os {
324 StorageKeyLayout::ZKsyncOs
325 } else {
326 StorageKeyLayout::Era
327 };
328
329 let is_fork_mode = fork_client.is_some();
330 let (node_inner, storage, blockchain, time, fork, vm_runner) = InMemoryNodeInner::init(
331 fork_client,
332 fee_input_provider.clone(),
333 filters,
334 config.clone(),
335 impersonation.clone(),
336 system_contracts.clone(),
337 storage_key_layout,
338 config.l1_config.is_some(),
340 );
341
342 let mut node_service_tasks: Vec<Pin<Box<dyn Future<Output = anyhow::Result<()>>>>> = Vec::new();
343 let (node_executor, node_handle) =
344 NodeExecutor::new(node_inner.clone(), vm_runner, storage_key_layout);
345 let l1_sidecar = match config.l1_config.as_ref() {
346 Some(_) if fork_print_info.is_some() => {
347 return Err(zksync_error::anvil_zksync::env::InvalidArguments {
348 details: "Running L1 in forking mode is unsupported".into(),
349 arguments: debug_opt_string_repr,
350 }
351 .into());
352 }
353 Some(L1Config::Spawn { port }) => {
354 let (l1_sidecar, l1_sidecar_runner) = L1Sidecar::process(
355 config.protocol_version(),
356 *port,
357 blockchain.clone(),
358 node_handle.clone(),
359 pool.clone(),
360 config.auto_execute_l1,
361 )
362 .await
363 .map_err(to_domain)?;
364 node_service_tasks.push(Box::pin(l1_sidecar_runner.run()));
365 l1_sidecar
366 }
367 Some(L1Config::External { address }) => {
368 let (l1_sidecar, l1_sidecar_runner) = L1Sidecar::external(
369 config.protocol_version(),
370 address,
371 blockchain.clone(),
372 node_handle.clone(),
373 pool.clone(),
374 config.auto_execute_l1,
375 )
376 .await
377 .map_err(to_domain)?;
378 node_service_tasks.push(Box::pin(l1_sidecar_runner.run()));
379 l1_sidecar
380 }
381 None => L1Sidecar::none(),
382 };
383 let sealing_mode = if config.no_mining {
384 BlockSealerMode::noop()
385 } else if let Some(block_time) = config.block_time {
386 BlockSealerMode::fixed_time(config.max_transactions, block_time)
387 } else {
388 BlockSealerMode::immediate(config.max_transactions, pool.add_tx_listener())
389 };
390 let (block_sealer, block_sealer_state) =
391 BlockSealer::new(sealing_mode, pool.clone(), node_handle.clone());
392 node_service_tasks.push(Box::pin(block_sealer.run()));
393
394 let node: InMemoryNode = InMemoryNode::new(
395 node_inner,
396 blockchain,
397 storage,
398 fork,
399 node_handle.clone(),
400 Some(observability),
401 time,
402 impersonation,
403 pool,
404 block_sealer_state,
405 system_contracts,
406 storage_key_layout,
407 );
408
409 tokio::spawn(async move {
412 if let Err(err) = node_executor.run().await {
413 sh_err!("{err}");
414
415 if let Some(tel) = get_telemetry() {
416 let _ = tel.track_error(Box::new(&err)).await;
417 }
418 }
419 });
420
421 if let Some(tel) = get_telemetry() {
423 let cli_telemetry_props = opt.clone().into_telemetry_props();
424 let _ = tel
425 .track_event(
426 "node_started",
427 TelemetryProps::new()
428 .insert("params", Some(cli_telemetry_props))
429 .take(),
430 )
431 .await;
432 }
433
434 if config.use_evm_interpreter {
435 node.impersonate_account(PSEUDO_CALLER).unwrap();
438 node.set_rich_account(PSEUDO_CALLER, U256::from(1_000_000_000_000u64))
439 .await;
440 let chain_id = node.chain_id().await;
441 let mut txs = Vec::with_capacity(PREDEPLOYS.len() + 1);
442 txs.push(
443 L2TxBuilder::new(
444 PSEUDO_CALLER,
445 Nonce(0),
446 U256::from(300_000),
447 U256::from(u32::MAX),
448 chain_id,
449 )
450 .with_to(CONTRACT_DEPLOYER_ADDRESS)
451 .with_calldata(Bytes::from_static(EVM_EMULATOR_ENABLER_CALLDATA).to_vec())
452 .build_impersonated()
453 .into(),
454 );
455
456 if !is_fork_mode {
458 let mut nonce = Nonce(1);
459 for pd in PREDEPLOYS.iter() {
460 let data = pd.encode_manager_call().unwrap();
461 txs.push(
462 L2TxBuilder::new(
463 PSEUDO_CALLER,
464 nonce,
465 U256::from(10_000_000), U256::from(u32::MAX),
467 chain_id,
468 )
469 .with_to(EVM_PREDEPLOYS_MANAGER_ADDRESS)
470 .with_calldata(data)
471 .build_impersonated()
472 .into(),
473 );
474 nonce += 1;
475 }
476 }
477
478 node_handle
479 .seal_block_sync(TxBatch {
480 impersonating: true,
481 txs,
482 })
483 .await
484 .map_err(to_domain)?;
485 node.set_rich_account(PSEUDO_CALLER, U256::from(0)).await;
486 node.stop_impersonating_account(PSEUDO_CALLER).unwrap();
487 }
488
489 if let Some(bytecodes_dir) = &config.override_bytecodes_dir {
490 override_bytecodes(&node, bytecodes_dir.to_string())
491 .await
492 .unwrap();
493 }
494
495 if !transactions_to_replay.is_empty() {
496 sh_println!("Executing transactions from the block.");
497 let total_txs = transactions_to_replay.len() as u64;
498 let pb = ProgressBar::new(total_txs);
499 pb.enable_steady_tick(std::time::Duration::from_secs(1));
500 pb.set_style(
501 ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} tx ({eta})")
502 .unwrap()
503 .with_key("eta", |state: &indicatif::ProgressState, w: &mut dyn Write| {
504 write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()
505 })
506 .progress_chars("#>-")
507 );
508
509 node.node_handle
510 .set_progress_report(Some(pb.clone()))
511 .await
512 .map_err(to_domain)?;
513
514 node.replay_txs(transactions_to_replay)
515 .await
516 .map_err(to_domain)?;
517
518 pb.finish_and_clear();
519 sh_println!("Done replaying transactions.");
520
521 return Ok(());
523 }
524
525 let rich_addresses = itertools::chain!(
527 config
528 .genesis_accounts
529 .iter()
530 .map(|acc| H160::from_slice(acc.address().as_ref())),
531 config
532 .signer_accounts
533 .iter()
534 .map(|acc| H160::from_slice(acc.address().as_ref())),
535 LEGACY_RICH_WALLETS
536 .iter()
537 .map(|(address, _)| H160::from_str(address).unwrap()),
538 RICH_WALLETS
539 .iter()
540 .map(|(address, _, _)| H160::from_str(address).unwrap()),
541 )
542 .collect::<Vec<_>>();
543 for address in rich_addresses {
544 node.set_rich_account(address, config.genesis_balance).await;
545 }
546
547 let mut server_builder = NodeServerBuilder::new(
548 node.clone(),
549 l1_sidecar,
550 AllowOrigin::exact(
551 config
552 .allow_origin
553 .parse()
554 .context("allow origin is malformed")
555 .map_err(to_domain)?,
556 ),
557 );
558 if config.health_check_endpoint {
559 server_builder.enable_health_api()
560 }
561 if !config.no_cors {
562 server_builder.enable_cors();
563 }
564 let mut server_handles = Vec::with_capacity(config.host.len());
565 for host in &config.host {
566 let mut addr = SocketAddr::new(*host, config.port);
567
568 match server_builder.clone().build(addr).await {
569 Ok(server) => {
570 config.port = server.local_addr().port();
571 server_handles.push(server.run());
572 }
573 Err(err) => {
574 let port_requested = config.port;
575 sh_eprintln!(
576 "Failed to bind to address {}:{}: {}. Retrying with a different port...",
577 host,
578 config.port,
579 err
580 );
581
582 addr.set_port(0);
584 match server_builder.clone().build(addr).await {
585 Ok(server) => {
586 config.port = server.local_addr().port();
587 tracing::info!(
588 "Successfully started server on port {} for host {}",
589 config.port,
590 host
591 );
592 server_handles.push(server.run());
593 }
594 Err(err) => {
595 return Err(zksync_error::anvil_zksync::env::ServerStartupFailed {
596 host_requested: host.to_string(),
597 port_requested: port_requested.into(),
598 details: err.to_string(),
599 }
600 .into());
601 }
602 }
603 }
604 }
605 }
606 let any_server_stopped =
607 futures::future::select_all(server_handles.into_iter().map(|h| Box::pin(h.stopped())));
608
609 let state_path = config.load_state.as_ref().or(config.state.as_ref());
610 if let Some(state_path) = state_path {
611 let bytes = std::fs::read(state_path).map_err(|error| {
612 zksync_error::anvil_zksync::state::StateFileAccess {
613 path: state_path.to_string_lossy().to_string(),
614 reason: error.to_string(),
615 }
616 })?;
617 node.load_state(zksync_types::web3::Bytes(bytes))
618 .await
619 .map_err(to_domain)?;
620 }
621
622 let dump_state_path = config.dump_state.clone().or_else(|| config.state.clone());
623 let dump_interval = config
624 .state_interval
625 .map(Duration::from_secs)
626 .unwrap_or(Duration::from_secs(60)); let preserve_historical_states = config.preserve_historical_states;
628 let node_for_dumper = node.clone();
629 let state_dumper = PeriodicStateDumper::new(
630 node_for_dumper,
631 dump_state_path,
632 dump_interval,
633 preserve_historical_states,
634 );
635 node_service_tasks.push(Box::pin(state_dumper));
636
637 config.print(fork_print_info.as_ref());
638 let node_service_stopped = futures::future::select_all(node_service_tasks);
639
640 tokio::select! {
641 _ = tokio::signal::ctrl_c() => {
642 tracing::trace!("received shutdown signal, shutting down");
643 },
644 _ = any_server_stopped => {
645 tracing::trace!("node server was stopped")
646 },
647 (result, _, _) = node_service_stopped => {
648 result.map_err(to_domain)?;
650 tracing::trace!("node service was stopped")
651 }
652 }
653
654 SignaturesIdentifier::global().save().await;
655
656 Ok(())
657}
658
659#[tokio::main]
660async fn main() -> Result<(), AnvilZksyncError> {
661 let cli = Cli::parse();
662 let offline = cli.offline;
663
664 if !offline {
665 init_telemetry(
666 env!("CARGO_PKG_NAME"),
667 env!("CARGO_PKG_VERSION"),
668 TELEMETRY_CONFIG_NAME,
669 Some(POSTHOG_API_KEY.into()),
670 None,
671 None,
672 )
673 .await
674 .map_err(|inner| zksync_error::anvil_zksync::env::GenericError {
675 message: format!("Failed to initialize telemetry collection subsystem: {inner}."),
676 })?;
677 }
678
679 if let Err(err) = start_program(cli).await {
680 if let Some(tel) = get_telemetry() {
682 let _ = tel.track_error(Box::new(&err.to_unified())).await;
683 }
684 sh_eprintln!("{}", err.to_unified().get_message());
685 return Err(err);
686 }
687 Ok(())
688}