anvil_zksync_l1_sidecar/
anvil.rs

1use crate::zkstack_config::ZkstackConfig;
2use alloy::network::EthereumWallet;
3use alloy::primitives::{Address, Bytes, U256};
4use alloy::providers::ext::AnvilApi;
5use alloy::providers::{Provider, ProviderBuilder};
6use alloy::rpc::types::TransactionRequest;
7use alloy::transports::RpcError;
8use anvil_zksync_common::sh_println;
9use anyhow::Context;
10use once_cell::sync::Lazy;
11use semver::Version;
12use std::collections::HashMap;
13use std::fs::File;
14use std::process::Stdio;
15use std::sync::Arc;
16use std::time::Duration;
17use tempfile::TempDir;
18use tokio::io::AsyncWriteExt;
19use tokio::process::{Child as AsyncChild, Command as AsyncCommand};
20use zksync_types::ProtocolVersionId;
21
22static L1_STATES: Lazy<HashMap<ProtocolVersionId, &[u8]>> = Lazy::new(|| {
23    HashMap::from_iter([
24        (
25            ProtocolVersionId::Version26,
26            include_bytes!("../../../l1-setup/state/v26-l1-state.json").as_slice(),
27        ),
28        (
29            ProtocolVersionId::Version27,
30            include_bytes!("../../../l1-setup/state/v27-l1-state.json").as_slice(),
31        ),
32        (
33            ProtocolVersionId::Version28,
34            include_bytes!("../../../l1-setup/state/v28-l1-state.json").as_slice(),
35        ),
36    ])
37});
38
39static L1_PAYLOADS: Lazy<HashMap<ProtocolVersionId, &str>> = Lazy::new(|| {
40    HashMap::from_iter([
41        (
42            ProtocolVersionId::Version26,
43            include_str!("../../../l1-setup/state/v26-l1-state-payload.txt"),
44        ),
45        (
46            ProtocolVersionId::Version27,
47            include_str!("../../../l1-setup/state/v27-l1-state-payload.txt"),
48        ),
49        (
50            ProtocolVersionId::Version28,
51            include_str!("../../../l1-setup/state/v28-l1-state-payload.txt"),
52        ),
53    ])
54});
55
56/// Representation of an anvil process spawned onto an event loop.
57///
58/// Process will be killed once `AnvilHandle` handle has been dropped.
59pub struct AnvilHandle {
60    /// Underlying L1's environment that ensures anvil can continue running normally until this
61    /// handle is dropped.
62    env: L1AnvilEnv,
63}
64
65impl AnvilHandle {
66    /// Runs anvil and its services. Returns on anvil exiting.
67    pub async fn run(self) -> anyhow::Result<()> {
68        match self.env {
69            L1AnvilEnv::Process(ProcessAnvil { mut node_child, .. }) => {
70                node_child.wait().await?;
71            }
72            L1AnvilEnv::External => tokio::signal::ctrl_c().await?,
73        }
74        Ok(())
75    }
76}
77
78async fn ensure_anvil_1_x_x() -> anyhow::Result<()> {
79    let child = AsyncCommand::new("anvil")
80        .arg("--version")
81        .stdout(Stdio::piped())
82        .spawn()
83        .context("could not detect `anvil` version; make sure it is installed on your machine")?;
84    let output = child.wait_with_output().await?;
85    let output = std::str::from_utf8(&output.stdout)?;
86    let version_line = output
87        .lines()
88        .next()
89        .with_context(|| format!("`anvil --version` output did not contain any lines: {output}"))?;
90    let version = version_line
91        .strip_prefix("anvil Version: ")
92        .with_context(|| {
93            format!("`anvil --version` output started with unexpected prefix: {version_line}")
94        })?;
95    let version = Version::parse(version)?;
96    tracing::debug!(%version, "detected installed anvil version");
97    // Allow any non-0.x version (including `1.0.0-stable`, `1.0.0-nightly` and other pre-releases)
98    if version.major >= 1 {
99        Ok(())
100    } else {
101        Err(anyhow::anyhow!(
102            "unsupported `anvil` version ({}), please upgrade to >=1.0.0",
103            version
104        ))
105    }
106}
107
108/// Spawns an anvil instance using the system-provided `anvil` command and built-in precomputed state.
109pub async fn spawn_process(
110    port: u16,
111    zkstack_config: &ZkstackConfig,
112) -> anyhow::Result<(AnvilHandle, Arc<dyn Provider + 'static>)> {
113    ensure_anvil_1_x_x().await?;
114
115    let tmpdir = tempfile::Builder::new()
116        .prefix("anvil_zksync_l1")
117        .tempdir()?;
118    let anvil_state_path = tmpdir.path().join("l1-state.json");
119    let mut anvil_state_file = tokio::fs::File::create(&anvil_state_path).await?;
120    anvil_state_file
121        .write_all(
122            L1_STATES
123                .get(&zkstack_config.genesis.genesis_protocol_version)
124                .expect("zkstack config refers to an unsupported protocol version"),
125        )
126        .await?;
127    anvil_state_file.flush().await?;
128    drop(anvil_state_file);
129
130    tracing::debug!(
131        ?anvil_state_path,
132        "unpacked anvil state into a temporary directory"
133    );
134
135    // TODO: Make log location configurable
136    let log_file = File::create("./anvil-zksync-l1.log")?;
137    let node_child = AsyncCommand::new("anvil")
138        .arg("--port")
139        .arg(port.to_string())
140        .arg("--load-state")
141        .arg(anvil_state_path)
142        .stdout(log_file)
143        .spawn()?;
144
145    let env = L1AnvilEnv::Process(ProcessAnvil {
146        node_child,
147        _tmpdir: tmpdir,
148    });
149    let provider = setup_provider(&format!("http://localhost:{port}"), zkstack_config).await?;
150
151    Ok((AnvilHandle { env }, Arc::new(provider)))
152}
153
154pub async fn external(
155    address: &str,
156    zkstack_config: &ZkstackConfig,
157) -> anyhow::Result<(AnvilHandle, Arc<dyn Provider + 'static>)> {
158    let env = L1AnvilEnv::External;
159    let provider = setup_provider(address, zkstack_config).await?;
160    inject_l1_state(zkstack_config.genesis.genesis_protocol_version, &provider).await?;
161
162    // Submit a transaction with very high gas to refresh anvil's fee estimator. Seems like some
163    // >=1.0.0 versions are still affected by this bug.
164    let fees = provider.estimate_eip1559_fees().await?;
165    provider
166        .send_transaction(
167            TransactionRequest::default()
168                .to(Address::default())
169                .value(U256::from(1))
170                .max_fee_per_gas(fees.max_fee_per_gas * 1000000)
171                .max_priority_fee_per_gas(fees.max_priority_fee_per_gas * 1000000),
172        )
173        .await?
174        .get_receipt()
175        .await?;
176
177    Ok((AnvilHandle { env }, Arc::new(provider)))
178}
179
180/// An environment that holds live resources that were used to spawn an anvil node.
181///
182/// This is not supposed to be dropped until anvil has finished running.
183enum L1AnvilEnv {
184    Process(ProcessAnvil),
185    External,
186}
187
188/// An [anvil](https://github.com/foundry-rs/foundry/tree/master/crates/anvil) instance running in
189/// a separate process spawned from anvil-zksync.
190struct ProcessAnvil {
191    /// A handle to the spawned anvil node and its tasks.
192    node_child: AsyncChild,
193    /// Temporary directory containing state file. Holding it to ensure it does not get deleted prematurely.
194    _tmpdir: TempDir,
195}
196
197async fn setup_provider(address: &str, config: &ZkstackConfig) -> anyhow::Result<impl Provider> {
198    let blob_operator_wallet =
199        EthereumWallet::from(config.wallets.blob_operator.private_key.clone());
200    let provider = ProviderBuilder::new()
201        .wallet(blob_operator_wallet)
202        .connect(address)
203        .await?;
204
205    // Wait for anvil to be up
206    tokio::time::timeout(Duration::from_secs(60), async {
207        loop {
208            match provider.get_accounts().await {
209                Ok(_) => {
210                    return anyhow::Ok(());
211                }
212                Err(err) if err.is_transport_error() => {
213                    tracing::debug!(?err, "L1 Anvil is not up yet; sleeping");
214                    sh_println!("Waiting for L1 to become available at {address}...");
215                    tokio::time::sleep(Duration::from_millis(500)).await;
216                }
217                Err(err) => return Err(err.into()),
218            }
219        }
220    })
221    .await
222    .context("L1 anvil failed to start")?
223    .context("unexpected response from L1 anvil")?;
224
225    Ok(provider)
226}
227
228/// Injects pre-computed L1 state into provider.
229async fn inject_l1_state(
230    protocol_version: ProtocolVersionId,
231    provider: &impl Provider,
232) -> anyhow::Result<()> {
233    // Trim trailing EOL and drop the `0x` prefix
234    let state_payload = &L1_PAYLOADS
235        .get(&protocol_version)
236        .expect("zkstack config refers to an unsupported protocol version")
237        .trim()[2..];
238    let state_payload = Bytes::from(hex::decode(state_payload)?);
239    match provider.anvil_load_state(state_payload).await {
240        Ok(true) => Ok(()),
241        Ok(false) => Err(anyhow::anyhow!(
242            "`anvil` refused to inject L1 state; see its logs for more details"
243        )),
244        Err(RpcError::ErrorResp(e))
245            if e.code == -32600 && e.message.contains("Invalid request") =>
246        {
247            Err(anyhow::anyhow!(
248                "`anvil` rejected `anvil_loadState` request; likely because of the request size limit - try running it with `--no-request-size-limit`: {e}"
249            ))
250        }
251        Err(e) => Err(e.into()),
252    }
253}