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::{hex, Address, ChainId};
7use k256::{ecdsa::SigningKey, SecretKey as K256SecretKey};
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::rng().random_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                println!("fork_block_number ln 312: {}", fork_block_number);
335                cmd.arg("--fork-block-number")
336                    .arg(fork_block_number.to_string());
337            }
338        } else {
339            cmd.arg("run");
340        }
341
342        let mut child = cmd.spawn().map_err(AnvilZKsyncError::SpawnError)?;
343
344        let stdout = child.stdout.as_mut().ok_or(AnvilZKsyncError::NoStderr)?;
345
346        let start = Instant::now();
347        let mut reader = BufReader::new(stdout);
348
349        let mut private_keys = Vec::new();
350        let mut addresses = Vec::new();
351        let mut chain_id = None;
352        let port;
353        loop {
354            if start
355                + Duration::from_millis(self.timeout.unwrap_or(ANVIL_ZKSYNC_STARTUP_TIMEOUT_MILLIS))
356                <= Instant::now()
357            {
358                return Err(AnvilZKsyncError::Timeout);
359            }
360
361            let mut line = String::new();
362            reader
363                .read_line(&mut line)
364                .map_err(AnvilZKsyncError::ReadLineError)?;
365            tracing::trace!(target: "anvil-zksync", line);
366            if let Some(addr) = line.trim().split("Listening on").nth(1) {
367                // <Node is ready at 127.0.0.1:8011>
368                // parse the actual port
369                port = SocketAddr::from_str(addr.trim())
370                    .map_err(|_| AnvilZKsyncError::ParsePortError)?
371                    .port();
372                break;
373            }
374
375            // Questionable but OK.
376            // Start the internal loop to go over the private keys
377            if line.contains("Private Keys") {
378                loop {
379                    let mut pk_line = String::new();
380                    reader
381                        .read_line(&mut pk_line)
382                        .map_err(AnvilZKsyncError::ReadLineError)?;
383                    tracing::trace!(target: "anvil-zksync", pk_line);
384                    match pk_line.trim() {
385                        "" => break,
386                        pk_line => {
387                            if pk_line.contains("0x") {
388                                let key_str = pk_line.split("0x").nth(1).unwrap();
389                                let key_hex =
390                                    hex::decode(key_str).map_err(AnvilZKsyncError::FromHexError)?;
391                                let key = K256SecretKey::from_bytes((&key_hex[..]).into())
392                                    .map_err(|_| AnvilZKsyncError::DeserializePrivateKeyError)?;
393                                addresses.push(Address::from_public_key(
394                                    SigningKey::from(&key).verifying_key(),
395                                ));
396                                private_keys.push(key);
397                            }
398                        }
399                    }
400                }
401            } else if line.contains("Chain ID:") {
402                // Chain ID: 260
403                if let Ok(chain) = line
404                    .split("Chain ID:")
405                    .nth(1)
406                    .unwrap()
407                    .trim()
408                    .parse::<u64>()
409                {
410                    chain_id = Some(chain);
411                };
412            }
413        }
414
415        Ok(AnvilZKsyncInstance {
416            child,
417            private_keys,
418            addresses,
419            port,
420            chain_id: self.chain_id.or(chain_id),
421        })
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428    use alloy::providers::{Provider, ProviderBuilder};
429
430    #[test]
431    fn can_launch_anvil_zksync() {
432        let _ = AnvilZKsync::new().spawn();
433    }
434
435    #[test]
436    fn can_launch_anvil_zksync_with_custom_port() {
437        const PORT: u16 = 7555;
438        let anvil_zksync = AnvilZKsync::new().port(PORT).spawn();
439        assert_eq!(anvil_zksync.port(), PORT);
440    }
441
442    #[test]
443    fn assert_block_time_is_natural_number() {
444        // This test is to ensure that older versions of era_test_node are supported
445        // even though the block time is a f64, it should be passed as a whole number
446        let era_test_node = AnvilZKsync::new().block_time(12);
447        assert_eq!(era_test_node.block_time.unwrap().to_string(), "12");
448        let _ = era_test_node.spawn();
449    }
450
451    // #[test]
452    // fn can_launch_anvil_zksync_with_sub_seconds_block_time() {
453    //     let _ = AnvilZKsync::new().block_time_f64(0.5).spawn();
454    // }
455
456    #[tokio::test(flavor = "multi_thread")]
457    async fn fork_initializes_correct_chain_id() {
458        let chain_id = 92;
459        let anvil_zksync = AnvilZKsync::new().chain_id(chain_id).spawn();
460        let rpc_url = anvil_zksync.endpoint_url();
461        let provider = ProviderBuilder::new().on_http(rpc_url);
462
463        let returned_chain_id = provider.get_chain_id().await.unwrap();
464
465        assert_eq!(returned_chain_id, chain_id);
466
467        drop(anvil_zksync);
468    }
469
470    #[tokio::test(flavor = "multi_thread")]
471    #[ignore]
472    async fn fork_initializes_correct_chain() {
473        let anvil_zksync = AnvilZKsync::new().fork("mainnet").spawn();
474        let rpc_url = anvil_zksync.endpoint_url();
475        let provider = ProviderBuilder::new().on_http(rpc_url);
476
477        let chain_id = provider.get_chain_id().await.unwrap();
478
479        assert_eq!(chain_id, 324);
480
481        drop(anvil_zksync);
482    }
483
484    #[tokio::test(flavor = "multi_thread")]
485    async fn fork_initializes_at_specified_block() {
486        let fork_block_number = 47854817;
487
488        let anvil_zksync = AnvilZKsync::new()
489            .fork("mainnet")
490            .fork_block_number(fork_block_number)
491            .spawn();
492
493        let rpc_url = anvil_zksync.endpoint_url();
494        let provider = ProviderBuilder::new().on_http(rpc_url);
495
496        // Query the latest block number to verify the fork block number.
497        let block_number = provider.get_block_number().await.unwrap();
498
499        assert_eq!(
500            block_number, fork_block_number,
501            "The node did not fork at the expected block number"
502        );
503
504        drop(anvil_zksync);
505    }
506
507    #[test]
508    fn assert_chain_id_without_rpc() {
509        let anvil_zksync = AnvilZKsync::new().spawn();
510        assert_eq!(anvil_zksync.chain_id(), 260);
511    }
512
513    #[tokio::test(flavor = "multi_thread")]
514    async fn test_mnemonic_usage() {
515        let test_mnemonic =
516            "nasty genius bright property zero practice critic draft turkey cigar option south";
517
518        let anvil_zksync = AnvilZKsync::new().mnemonic(test_mnemonic).spawn();
519
520        let expected_addresses = vec![
521            "0xe99f84afb6fcad9ebe0e1970fc7632ec00b3a5dd",
522            "0x2d0472332f336d00d71a9055a04315684466b7ab",
523            "0x10293d5d0127eaa1838779a54833f2c76a3893db",
524            "0x574e479338bb22b856feb3df7296c65247c99a5a",
525            "0x68256d3e5eae3ee2bc1cf4172c4fdc1f76d51b4d",
526            "0x00c0b6d136ab72156734f08c704704f8130f5062",
527            "0xa6004bae3cd480660e17542a83fe164b8e128362",
528            "0xa845c4de08761a3d93e0aea1006bfc05de02f6ef",
529            "0x76454a9658bec53daee3c5fc1d369ea757ebd5cb",
530            "0xcda2c6614a1014d27f6dfd9b8323d688931f69b9",
531        ];
532
533        let derived_addresses: Vec<_> = anvil_zksync
534            .addresses()
535            .iter()
536            .map(|address| format!("{:#x}", address))
537            .collect();
538
539        assert_eq!(
540            derived_addresses, expected_addresses,
541            "The derived addresses do not match the expected addresses"
542        );
543
544        drop(anvil_zksync);
545    }
546
547    #[test]
548    fn can_launch_era_test_node_with_no_mine() {
549        let anvil_zksync = AnvilZKsync::new().no_mine().spawn();
550
551        drop(anvil_zksync);
552    }
553}