anvil_zksync_l1_sidecar/
anvil.rs1use 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
56pub struct AnvilHandle {
60 env: L1AnvilEnv,
63}
64
65impl AnvilHandle {
66 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 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
108pub 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 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 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
180enum L1AnvilEnv {
184 Process(ProcessAnvil),
185 External,
186}
187
188struct ProcessAnvil {
191 node_child: AsyncChild,
193 _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 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
228async fn inject_l1_state(
230 protocol_version: ProtocolVersionId,
231 provider: &impl Provider,
232) -> anyhow::Result<()> {
233 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}