alloy_zksync/node_bindings/
anvil_zksync.rs

1// Adapted from Anvil node bindings in the alloy project:
2// https://github.com/alloy-rs/alloy/blob/2d26b057c64cbcc77654f4691141c308d63b286f/crates/node-bindings/src/anvil.rs
3
4//! Utilities for launching an `anvil-zksync` instance.
5
6use 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
20/// How long we will wait for anvil-zksync to indicate that it is ready.
21const ANVIL_ZKSYNC_STARTUP_TIMEOUT_MILLIS: u64 = 10_000;
22
23/// An anvil-zksync CLI instance. Will close the instance when dropped.
24///
25/// Construct this using [`AnvilZKsync`].
26#[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    /// Returns a reference to the child process.
37    pub const fn child(&self) -> &Child {
38        &self.child
39    }
40
41    /// Returns a mutable reference to the child process.
42    pub fn child_mut(&mut self) -> &mut Child {
43        &mut self.child
44    }
45
46    /// Returns the private keys used to instantiate this instance
47    pub fn keys(&self) -> &[K256SecretKey] {
48        &self.private_keys
49    }
50
51    /// Returns the addresses used to instantiate this instance
52    pub fn addresses(&self) -> &[Address] {
53        &self.addresses
54    }
55
56    /// Returns the port of this instance
57    pub const fn port(&self) -> u16 {
58        self.port
59    }
60
61    /// Returns the chain of the anvil-zksync instance
62    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    /// Returns the HTTP endpoint of this instance
68    #[doc(alias = "http_endpoint")]
69    pub fn endpoint(&self) -> String {
70        format!("http://localhost:{}", self.port)
71    }
72
73    /// Returns the HTTP endpoint url of this instance
74    #[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/// Errors that can occur when working with the [`AnvilZKsync`].
87#[derive(Debug, Error)]
88pub enum AnvilZKsyncError {
89    /// Spawning the anvil-zksync process failed.
90    #[error("could not start anvil-zksync: {0}")]
91    SpawnError(std::io::Error),
92
93    /// Timed out waiting for a message from anvil-zksync's stderr.
94    #[error("timed out waiting for anvil-zksync to spawn; anvil-zksync installed?")]
95    Timeout,
96
97    /// A line could not be read from the anvil-zksync stderr.
98    #[error("could not read line from anvil-zksync stderr: {0}")]
99    ReadLineError(std::io::Error),
100
101    /// The child anvil-zksync process's stderr was not captured.
102    #[error("could not get stderr for anvil-zksync child process")]
103    NoStderr,
104
105    /// The private key could not be parsed.
106    #[error("could not parse private key")]
107    ParsePrivateKeyError,
108
109    /// An error occurred while deserializing a private key.
110    #[error("could not deserialize private key from bytes")]
111    DeserializePrivateKeyError,
112
113    /// The port could not be parsed.
114    #[error("could not parse the port")]
115    ParsePortError,
116
117    /// An error occurred while parsing a hex string.
118    #[error(transparent)]
119    FromHexError(#[from] hex::FromHexError),
120
121    /// No private keys were found.
122    #[error("no private keys found")]
123    NoKeysAvailable,
124}
125
126/// Builder for launching `anvil-zksync`.
127///
128/// # Panics
129///
130/// If `spawn` is called without `anvil-zksync` being available in the user's $PATH
131///
132/// # Example
133///
134/// ```no_run
135/// use alloy_zksync::node_bindings::AnvilZKsync;
136///
137/// let port = 8545u16;
138/// let url = format!("http://localhost:{}", port).to_string();
139///
140/// let anvil_zksync = AnvilZKsync::new()
141///     .port(port)
142///     .spawn();
143///
144/// drop(anvil_zksync); // this will kill the instance
145/// ```
146#[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    // If the block_time is an integer, f64::to_string() will output without a decimal point
152    // which allows this to be backwards compatible.
153    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    /// Creates an empty AnvilZKsync builder.
165    /// The default port is 8545. The mnemonic is chosen randomly.
166    ///
167    /// # Example
168    ///
169    /// ```
170    /// # use alloy_zksync::node_bindings::AnvilZKsync;
171    /// fn a() {
172    ///  let anvil_zksync = AnvilZKsync::default().spawn();
173    ///
174    ///  println!("AnvilZKsync running at `{}`", anvil_zksync.endpoint());
175    /// # }
176    /// ```
177    pub fn new() -> Self {
178        let mut self_ = Self::default();
179        // Assign a random port so that we can run multiple instances.
180        let port = rand::thread_rng().gen_range(8000..16000);
181        self_.port = Some(port);
182        self_
183    }
184
185    /// Creates an AnvilZKsync builder which will execute `anvil-zksync` at the given path.
186    ///
187    /// # Example
188    ///
189    /// ```no_run
190    /// # use alloy_zksync::node_bindings::AnvilZKsync;
191    /// fn a() {
192    ///  let anvil_zksync = AnvilZKsync::at("~/some/location/anvil-zksync").spawn();
193    ///
194    ///  println!("AnvilZKsync running at `{}`", anvil_zksync.endpoint());
195    /// # }
196    /// ```
197    pub fn at(path: impl Into<PathBuf>) -> Self {
198        Self::new().path(path)
199    }
200
201    /// Sets the `path` to the `anvil-zksync` cli
202    ///
203    /// By default, it's expected that `anvil-zksync` is in `$PATH`, see also
204    /// [`std::process::Command::new()`]
205    pub fn path<T: Into<PathBuf>>(mut self, path: T) -> Self {
206        self.program = Some(path.into());
207        self
208    }
209
210    /// Sets the port which will be used when the `anvil-zksync` instance is launched.
211    pub fn port<T: Into<u16>>(mut self, port: T) -> Self {
212        self.port = Some(port.into());
213        self
214    }
215
216    /// Sets the chain_id the `anvil-zksync` instance will use.
217    pub const fn chain_id(mut self, chain_id: u64) -> Self {
218        self.chain_id = Some(chain_id);
219        self
220    }
221
222    /// Sets the no-mine status which will be used when the `era_test_node` instance is launched.
223    pub const fn no_mine(mut self) -> Self {
224        self.no_mine = true;
225        self
226    }
227
228    /// Sets the mnemonic which will be used when the `anvil-zksync` instance is launched.
229    pub fn mnemonic<T: Into<String>>(mut self, mnemonic: T) -> Self {
230        self.mnemonic = Some(mnemonic.into());
231        self
232    }
233
234    /// Sets the block-time in seconds which will be used when the `anvil-zksync` instance is launched.
235    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    // TODO
241    // /// Sets the block-time in sub-seconds which will be used when the `anvil-zksync` instance is launched.
242    // /// Older versions of `anvil-zksync` do not support sub-second block times.
243    // pub const fn block_time_f64(mut self, block_time: f64) -> Self {
244    //     self.block_time = Some(block_time);
245    //     self
246    // }
247
248    /// Sets the `fork-block-number` which will be used in addition to [`Self::fork`].
249    ///
250    /// **Note:** if set, then this requires `fork` to be set as well
251    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    /// Sets the `fork` argument to fork from another currently running Ethereum client
257    /// at a given block. Input should be the HTTP location and port of the other client,
258    /// e.g. `http://localhost:8545`. You can optionally specify the block to fork from
259    /// using an @ sign: `http://localhost:8545@1599200`
260    pub fn fork<T: Into<String>>(mut self, fork: T) -> Self {
261        self.fork = Some(fork.into());
262        self
263    }
264
265    /// Adds an argument to pass to the `anvil-zksync`.
266    pub fn arg<T: Into<String>>(mut self, arg: T) -> Self {
267        self.args.push(arg.into());
268        self
269    }
270
271    /// Adds multiple arguments to pass to the `anvil-zksync`.
272    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    /// Sets the timeout which will be used when the `anvil-zksync` instance is launched.
284    pub const fn timeout(mut self, timeout: u64) -> Self {
285        self.timeout = Some(timeout);
286        self
287    }
288
289    /// Consumes the builder and spawns `anvil-zksync`.
290    ///
291    /// # Panics
292    ///
293    /// If spawning the instance fails at any point.
294    #[track_caller]
295    pub fn spawn(self) -> AnvilZKsyncInstance {
296        self.try_spawn().unwrap()
297    }
298
299    /// Consumes the builder and spawns `anvil-zksync`. If spawning fails, returns an error.
300    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        // let mut port = self.port.unwrap_or_default();
308        // cmd.arg("-p").arg(port.to_string());
309        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                // <Node is ready at 127.0.0.1:8011>
367                // parse the actual port
368                port = SocketAddr::from_str(addr.trim())
369                    .map_err(|_| AnvilZKsyncError::ParsePortError)?
370                    .port();
371                break;
372            }
373
374            // Questionable but OK.
375            // Start the internal loop to go over the private keys
376            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                // Chain ID: 260
402                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        // This test is to ensure that older versions of era_test_node are supported
444        // even though the block time is a f64, it should be passed as a whole number
445        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    // #[test]
451    // fn can_launch_anvil_zksync_with_sub_seconds_block_time() {
452    //     let _ = AnvilZKsync::new().block_time_f64(0.5).spawn();
453    // }
454
455    #[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        // Query the latest block number to verify the fork block number.
496        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}