alloy_zksync/node_bindings/
anvil_zksync.rs1use alloy::primitives::{Address, ChainId, hex};
7use k256::{SecretKey as K256SecretKey, ecdsa::SigningKey};
8use rand::Rng;
9use std::{
10 io::{BufRead, BufReader},
11 net::SocketAddr,
12 path::PathBuf,
13 process::{Child, Command},
14 str::FromStr,
15 time::{Duration, Instant},
16};
17use thiserror::Error;
18use url::Url;
19
20const ANVIL_ZKSYNC_STARTUP_TIMEOUT_MILLIS: u64 = 10_000;
22
23#[derive(Debug)]
27pub struct AnvilZKsyncInstance {
28 child: Child,
29 private_keys: Vec<K256SecretKey>,
30 addresses: Vec<Address>,
31 port: u16,
32 chain_id: Option<ChainId>,
33}
34
35impl AnvilZKsyncInstance {
36 pub const fn child(&self) -> &Child {
38 &self.child
39 }
40
41 pub fn child_mut(&mut self) -> &mut Child {
43 &mut self.child
44 }
45
46 pub fn keys(&self) -> &[K256SecretKey] {
48 &self.private_keys
49 }
50
51 pub fn addresses(&self) -> &[Address] {
53 &self.addresses
54 }
55
56 pub const fn port(&self) -> u16 {
58 self.port
59 }
60
61 pub fn chain_id(&self) -> ChainId {
63 const ANVIL_ZKSYNC_CHAIN_ID: ChainId = 260;
64 self.chain_id.unwrap_or(ANVIL_ZKSYNC_CHAIN_ID)
65 }
66
67 #[doc(alias = "http_endpoint")]
69 pub fn endpoint(&self) -> String {
70 format!("http://localhost:{}", self.port)
71 }
72
73 #[doc(alias = "http_endpoint_url")]
75 pub fn endpoint_url(&self) -> Url {
76 Url::parse(&self.endpoint()).unwrap()
77 }
78}
79
80impl Drop for AnvilZKsyncInstance {
81 fn drop(&mut self) {
82 self.child.kill().expect("could not kill anvil-zksync");
83 }
84}
85
86#[derive(Debug, Error)]
88pub enum AnvilZKsyncError {
89 #[error("could not start anvil-zksync: {0}")]
91 SpawnError(std::io::Error),
92
93 #[error("timed out waiting for anvil-zksync to spawn; anvil-zksync installed?")]
95 Timeout,
96
97 #[error("could not read line from anvil-zksync stderr: {0}")]
99 ReadLineError(std::io::Error),
100
101 #[error("could not get stderr for anvil-zksync child process")]
103 NoStderr,
104
105 #[error("could not parse private key")]
107 ParsePrivateKeyError,
108
109 #[error("could not deserialize private key from bytes")]
111 DeserializePrivateKeyError,
112
113 #[error("could not parse the port")]
115 ParsePortError,
116
117 #[error(transparent)]
119 FromHexError(#[from] hex::FromHexError),
120
121 #[error("no private keys found")]
123 NoKeysAvailable,
124}
125
126#[derive(Clone, Debug, Default)]
147#[must_use = "This Builder struct does nothing unless it is `spawn`ed"]
148pub struct AnvilZKsync {
149 program: Option<PathBuf>,
150 port: Option<u16>,
151 block_time: Option<f64>,
154 no_mine: bool,
155 chain_id: Option<ChainId>,
156 mnemonic: Option<String>,
157 fork: Option<String>,
158 fork_block_number: Option<u64>,
159 args: Vec<String>,
160 timeout: Option<u64>,
161}
162
163impl AnvilZKsync {
164 pub fn new() -> Self {
178 let mut self_ = Self::default();
179 let port = rand::thread_rng().gen_range(8000..16000);
181 self_.port = Some(port);
182 self_
183 }
184
185 pub fn at(path: impl Into<PathBuf>) -> Self {
198 Self::new().path(path)
199 }
200
201 pub fn path<T: Into<PathBuf>>(mut self, path: T) -> Self {
206 self.program = Some(path.into());
207 self
208 }
209
210 pub fn port<T: Into<u16>>(mut self, port: T) -> Self {
212 self.port = Some(port.into());
213 self
214 }
215
216 pub const fn chain_id(mut self, chain_id: u64) -> Self {
218 self.chain_id = Some(chain_id);
219 self
220 }
221
222 pub const fn no_mine(mut self) -> Self {
224 self.no_mine = true;
225 self
226 }
227
228 pub fn mnemonic<T: Into<String>>(mut self, mnemonic: T) -> Self {
230 self.mnemonic = Some(mnemonic.into());
231 self
232 }
233
234 pub const fn block_time(mut self, block_time: u64) -> Self {
236 self.block_time = Some(block_time as f64);
237 self
238 }
239
240 pub const fn fork_block_number(mut self, fork_block_number: u64) -> Self {
252 self.fork_block_number = Some(fork_block_number);
253 self
254 }
255
256 pub fn fork<T: Into<String>>(mut self, fork: T) -> Self {
261 self.fork = Some(fork.into());
262 self
263 }
264
265 pub fn arg<T: Into<String>>(mut self, arg: T) -> Self {
267 self.args.push(arg.into());
268 self
269 }
270
271 pub fn args<I, S>(mut self, args: I) -> Self
273 where
274 I: IntoIterator<Item = S>,
275 S: Into<String>,
276 {
277 for arg in args {
278 self = self.arg(arg);
279 }
280 self
281 }
282
283 pub const fn timeout(mut self, timeout: u64) -> Self {
285 self.timeout = Some(timeout);
286 self
287 }
288
289 #[track_caller]
295 pub fn spawn(self) -> AnvilZKsyncInstance {
296 self.try_spawn().unwrap()
297 }
298
299 pub fn try_spawn(self) -> Result<AnvilZKsyncInstance, AnvilZKsyncError> {
301 let mut cmd = self
302 .program
303 .as_ref()
304 .map_or_else(|| Command::new("anvil-zksync"), Command::new);
305 cmd.stdout(std::process::Stdio::piped())
306 .stderr(std::process::Stdio::inherit());
307 if let Some(port) = self.port {
310 cmd.arg("--port").arg(port.to_string());
311 }
312
313 if let Some(mnemonic) = self.mnemonic {
314 cmd.arg("-m").arg(mnemonic);
315 }
316
317 if let Some(chain_id) = self.chain_id {
318 cmd.arg("--chain-id").arg(chain_id.to_string());
319 }
320
321 if let Some(block_time) = self.block_time {
322 cmd.arg("-b").arg(block_time.to_string());
323 }
324
325 if self.no_mine {
326 cmd.arg("--no-mine");
327 }
328
329 cmd.args(self.args);
330
331 if let Some(fork) = self.fork {
332 cmd.arg("fork").arg("--network").arg(fork);
333 if let Some(fork_block_number) = self.fork_block_number {
334 cmd.arg("--fork-block-number")
335 .arg(fork_block_number.to_string());
336 }
337 } else {
338 cmd.arg("run");
339 }
340
341 let mut child = cmd.spawn().map_err(AnvilZKsyncError::SpawnError)?;
342
343 let stdout = child.stdout.as_mut().ok_or(AnvilZKsyncError::NoStderr)?;
344
345 let start = Instant::now();
346 let mut reader = BufReader::new(stdout);
347
348 let mut private_keys = Vec::new();
349 let mut addresses = Vec::new();
350 let mut chain_id = None;
351 let port;
352 loop {
353 if start
354 + Duration::from_millis(self.timeout.unwrap_or(ANVIL_ZKSYNC_STARTUP_TIMEOUT_MILLIS))
355 <= Instant::now()
356 {
357 return Err(AnvilZKsyncError::Timeout);
358 }
359
360 let mut line = String::new();
361 reader
362 .read_line(&mut line)
363 .map_err(AnvilZKsyncError::ReadLineError)?;
364 tracing::trace!(target: "anvil-zksync", line);
365 if let Some(addr) = line.trim().split("Listening on").nth(1) {
366 port = SocketAddr::from_str(addr.trim())
369 .map_err(|_| AnvilZKsyncError::ParsePortError)?
370 .port();
371 break;
372 }
373
374 if line.contains("Private Keys") {
377 loop {
378 let mut pk_line = String::new();
379 reader
380 .read_line(&mut pk_line)
381 .map_err(AnvilZKsyncError::ReadLineError)?;
382 tracing::trace!(target: "anvil-zksync", pk_line);
383 match pk_line.trim() {
384 "" => break,
385 pk_line => {
386 if pk_line.contains("0x") {
387 let key_str = pk_line.split("0x").nth(1).unwrap();
388 let key_hex =
389 hex::decode(key_str).map_err(AnvilZKsyncError::FromHexError)?;
390 let key = K256SecretKey::from_bytes((&key_hex[..]).into())
391 .map_err(|_| AnvilZKsyncError::DeserializePrivateKeyError)?;
392 addresses.push(Address::from_public_key(
393 SigningKey::from(&key).verifying_key(),
394 ));
395 private_keys.push(key);
396 }
397 }
398 }
399 }
400 } else if line.contains("Chain ID:") {
401 if let Ok(chain) = line
403 .split("Chain ID:")
404 .nth(1)
405 .unwrap()
406 .trim()
407 .parse::<u64>()
408 {
409 chain_id = Some(chain);
410 };
411 }
412 }
413
414 Ok(AnvilZKsyncInstance {
415 child,
416 private_keys,
417 addresses,
418 port,
419 chain_id: self.chain_id.or(chain_id),
420 })
421 }
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427 use alloy::providers::{Provider, ProviderBuilder};
428
429 #[test]
430 fn can_launch_anvil_zksync() {
431 let _ = AnvilZKsync::new().spawn();
432 }
433
434 #[test]
435 fn can_launch_anvil_zksync_with_custom_port() {
436 const PORT: u16 = 7555;
437 let anvil_zksync = AnvilZKsync::new().port(PORT).spawn();
438 assert_eq!(anvil_zksync.port(), PORT);
439 }
440
441 #[test]
442 fn assert_block_time_is_natural_number() {
443 let era_test_node = AnvilZKsync::new().block_time(12);
446 assert_eq!(era_test_node.block_time.unwrap().to_string(), "12");
447 let _ = era_test_node.spawn();
448 }
449
450 #[tokio::test(flavor = "multi_thread")]
456 async fn fork_initializes_correct_chain_id() {
457 let chain_id = 92;
458 let anvil_zksync = AnvilZKsync::new().chain_id(chain_id).spawn();
459 let rpc_url = anvil_zksync.endpoint_url();
460 let provider = ProviderBuilder::new().connect_http(rpc_url);
461
462 let returned_chain_id = provider.get_chain_id().await.unwrap();
463
464 assert_eq!(returned_chain_id, chain_id);
465
466 drop(anvil_zksync);
467 }
468
469 #[tokio::test(flavor = "multi_thread")]
470 #[ignore]
471 async fn fork_initializes_correct_chain() {
472 let anvil_zksync = AnvilZKsync::new().fork("mainnet").spawn();
473 let rpc_url = anvil_zksync.endpoint_url();
474 let provider = ProviderBuilder::new().connect_http(rpc_url);
475
476 let chain_id = provider.get_chain_id().await.unwrap();
477
478 assert_eq!(chain_id, 324);
479
480 drop(anvil_zksync);
481 }
482
483 #[tokio::test(flavor = "multi_thread")]
484 async fn fork_initializes_at_specified_block() {
485 let fork_block_number = 62174000;
486
487 let anvil_zksync = AnvilZKsync::new()
488 .fork("mainnet")
489 .fork_block_number(fork_block_number)
490 .spawn();
491
492 let rpc_url = anvil_zksync.endpoint_url();
493 let provider = ProviderBuilder::new().connect_http(rpc_url);
494
495 let block_number = provider.get_block_number().await.unwrap();
497
498 assert_eq!(
499 block_number, fork_block_number,
500 "The node did not fork at the expected block number"
501 );
502
503 drop(anvil_zksync);
504 }
505
506 #[test]
507 fn assert_chain_id_without_rpc() {
508 let anvil_zksync = AnvilZKsync::new().spawn();
509 assert_eq!(anvil_zksync.chain_id(), 260);
510 }
511
512 #[tokio::test(flavor = "multi_thread")]
513 async fn test_mnemonic_usage() {
514 let test_mnemonic =
515 "nasty genius bright property zero practice critic draft turkey cigar option south";
516
517 let anvil_zksync = AnvilZKsync::new().mnemonic(test_mnemonic).spawn();
518
519 let expected_addresses = vec![
520 "0xe99f84afb6fcad9ebe0e1970fc7632ec00b3a5dd",
521 "0x2d0472332f336d00d71a9055a04315684466b7ab",
522 "0x10293d5d0127eaa1838779a54833f2c76a3893db",
523 "0x574e479338bb22b856feb3df7296c65247c99a5a",
524 "0x68256d3e5eae3ee2bc1cf4172c4fdc1f76d51b4d",
525 "0x00c0b6d136ab72156734f08c704704f8130f5062",
526 "0xa6004bae3cd480660e17542a83fe164b8e128362",
527 "0xa845c4de08761a3d93e0aea1006bfc05de02f6ef",
528 "0x76454a9658bec53daee3c5fc1d369ea757ebd5cb",
529 "0xcda2c6614a1014d27f6dfd9b8323d688931f69b9",
530 ];
531
532 let derived_addresses: Vec<_> = anvil_zksync
533 .addresses()
534 .iter()
535 .map(|address| format!("{address:#x}"))
536 .collect();
537
538 assert_eq!(
539 derived_addresses, expected_addresses,
540 "The derived addresses do not match the expected addresses"
541 );
542
543 drop(anvil_zksync);
544 }
545
546 #[test]
547 fn can_launch_era_test_node_with_no_mine() {
548 let anvil_zksync = AnvilZKsync::new().no_mine().spawn();
549
550 drop(anvil_zksync);
551 }
552}