diff --git a/README.md b/README.md index cabe8de8d..05b6cb17b 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,12 @@ timestamp before enabling mining; do not move Beacon genesis to hide the gap. Ex are bounded to year 9999. Transaction-body pruning is incompatible with retaining local blobs. Safe/finalized tags remain Anvil's simulated block-depth policy, not Ethereum finality. +State dumps in this mode record the fork block number, hash and timestamp, chain ID, Beacon +genesis, slot duration and `--slots-in-an-epoch`. A Beacon-backed node rejects every state load, +including `--state` resume: dumps with another or no identity are incompatible, and Anvil's +account-merging load cannot replace a Beacon fork's history. Other modes still load legacy dumps +but reject Beacon dumps. + Everything below is inherited from upstream Foundry and works unchanged; only the Base additions above are specific to this fork. --- diff --git a/crates/anvil/src/eth/backend/db.rs b/crates/anvil/src/eth/backend/db.rs index 77d3bd3a5..40e9781c3 100644 --- a/crates/anvil/src/eth/backend/db.rs +++ b/crates/anvil/src/eth/backend/db.rs @@ -12,6 +12,7 @@ use alloy_primitives::{ map::{AddressMap, HashMap}, }; use alloy_rpc_types::BlockId; +use alloy_rpc_types_beacon::genesis::GenesisData; use anvil_core::eth::{ block::Block, transaction::{MaybeImpersonatedTransaction, TransactionInfo}, @@ -486,6 +487,18 @@ where Ok(number) } +/// Identity required to resume a Beacon-backed fork. Connectivity and mining policy are not state. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct SerializableForkBeacon { + pub chain_id: u64, + pub block_number: u64, + pub block_hash: B256, + pub timestamp: u64, + pub genesis: GenesisData, + pub seconds_per_slot: u64, + pub slots_in_an_epoch: u64, +} + #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct SerializableState { /// The block number of the state @@ -506,6 +519,9 @@ pub struct SerializableState { /// Note: This is an Option for backwards compatibility. #[serde(default)] pub historical_states: Option, + /// Absent in legacy dumps and when the historical Beacon upstream is not configured. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub fork_beacon: Option, } impl SerializableState { diff --git a/crates/anvil/src/eth/backend/mem/fork_db.rs b/crates/anvil/src/eth/backend/mem/fork_db.rs index 1ff97edb8..ff531a013 100644 --- a/crates/anvil/src/eth/backend/mem/fork_db.rs +++ b/crates/anvil/src/eth/backend/mem/fork_db.rs @@ -70,6 +70,7 @@ impl Db for ForkedDatabase { blocks, transactions, historical_states, + fork_beacon: None, })) } diff --git a/crates/anvil/src/eth/backend/mem/in_memory_db.rs b/crates/anvil/src/eth/backend/mem/in_memory_db.rs index 766805ee7..8e9a52ea7 100644 --- a/crates/anvil/src/eth/backend/mem/in_memory_db.rs +++ b/crates/anvil/src/eth/backend/mem/in_memory_db.rs @@ -72,6 +72,7 @@ impl Db for MemDb { blocks, transactions, historical_states, + fork_beacon: None, })) } diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index d16e73825..cdfd37190 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -7,7 +7,7 @@ use crate::{ eth::{ backend::{ cheats::{CheatEcrecover, CheatsManager}, - db::{Db, MaybeFullDatabase, SerializableState, StateDb}, + db::{Db, MaybeFullDatabase, SerializableForkBeacon, SerializableState, StateDb}, env::Env, executor::{ExecutedTransactions, TransactionExecutor}, fork::ClientFork, @@ -1106,6 +1106,22 @@ impl Backend { self.active_state_snapshots.lock().clone().into_iter().collect() } + /// Identity of the original fork boundary, not the current local tip. + fn fork_beacon_identity(&self) -> Option { + let fork = self.get_fork()?; + let config = fork.config.read(); + let beacon = config.beacon.as_ref()?; + Some(SerializableForkBeacon { + chain_id: self.env.read().evm_env.cfg_env.chain_id, + block_number: config.block_number, + block_hash: config.block_hash, + timestamp: config.timestamp, + genesis: beacon.genesis.clone(), + seconds_per_slot: beacon.seconds_per_slot, + slots_in_an_epoch: self.slots_in_an_epoch, + }) + } + /// Get the current state. pub async fn serialized_state( &self, @@ -1122,17 +1138,18 @@ impl Backend { None }; - let state = self.db.read().await.dump_state( - at, - best_number, - blocks, - transactions, - historical_states, - )?; - state.ok_or_else(|| { - RpcError::invalid_params("Dumping state not supported with the current configuration") - .into() - }) + let mut state = self + .db + .read() + .await + .dump_state(at, best_number, blocks, transactions, historical_states)? + .ok_or_else(|| { + RpcError::invalid_params( + "Dumping state not supported with the current configuration", + ) + })?; + state.fork_beacon = self.fork_beacon_identity(); + Ok(state) } /// Write all chain data to serialized bytes buffer @@ -1151,6 +1168,19 @@ impl Backend { /// Apply [SerializableState] data to the backend storage. pub async fn load_state(&self, state: SerializableState) -> Result { let _mining_guard = self.mining.lock().await; + let identity = self.fork_beacon_identity(); + if state.fork_beacon != identity { + return Err(RpcError::invalid_params( + "State dump Beacon identity does not match the configured fork, chain or finality settings", + ).into()); + } + if identity.is_some() { + // Loading merges accounts and history; a Beacon fork needs a validated replacement. + return Err(RpcError::invalid_params( + "Loading state is not supported with a Beacon upstream", + ) + .into()); + } // load the blocks and transactions into the storage self.blockchain.storage.write().load_blocks(state.blocks.clone()); self.blockchain.storage.write().load_transactions(state.transactions.clone()); diff --git a/crates/anvil/tests/it/beacon_api.rs b/crates/anvil/tests/it/beacon_api.rs index 3e4eac5a1..908a310b2 100644 --- a/crates/anvil/tests/it/beacon_api.rs +++ b/crates/anvil/tests/it/beacon_api.rs @@ -456,30 +456,41 @@ impl BeaconTargetFixture { timestamp: u64, payloads: &[&[u8]], ) -> Vec<(B256, BlobTransactionSidecar)> { - let provider = http_provider(&self.handle.http_endpoint()); let from = self.handle.dev_accounts().next().unwrap(); - let mut sent = Vec::with_capacity(payloads.len()); - for (nonce, payload) in payloads.iter().enumerate() { - let sidecar = beacon_sidecar(payload); - let tx = TransactionRequest::default() - .with_from(from) - .with_to(from) - .with_nonce(nonce as u64) - .with_gas_limit(21_000) - .with_max_fee_per_gas(10_000_000_000) - .with_max_priority_fee_per_gas(1_000_000_000) - .with_max_fee_per_blob_gas(10_000_000_000) - .with_blob_sidecar_4844(sidecar.clone()); - let mut tx = WithOtherFields::new(tx); - tx.populate_blob_hashes(); - let pending = provider.send_transaction(tx).await.unwrap(); - sent.push((*pending.tx_hash(), sidecar)); - } + let sent = send_blob_txs(&self.handle.http_endpoint(), from, 0, payloads).await; self.api.evm_mine(Some(MineOptions::Timestamp(Some(timestamp)))).await.unwrap(); sent } } +/// Submits one single-blob transaction per payload with consecutive nonces, without mining. +pub(super) async fn send_blob_txs( + endpoint: &str, + from: Address, + first_nonce: u64, + payloads: &[&[u8]], +) -> Vec<(B256, BlobTransactionSidecar)> { + let provider = http_provider(endpoint); + let mut sent = Vec::with_capacity(payloads.len()); + for (nonce, payload) in (first_nonce..).zip(payloads) { + let sidecar = beacon_sidecar(payload); + let tx = TransactionRequest::default() + .with_from(from) + .with_to(from) + .with_nonce(nonce) + .with_gas_limit(21_000) + .with_max_fee_per_gas(10_000_000_000) + .with_max_priority_fee_per_gas(1_000_000_000) + .with_max_fee_per_blob_gas(10_000_000_000) + .with_blob_sidecar_4844(sidecar.clone()); + let mut tx = WithOtherFields::new(tx); + tx.populate_blob_hashes(); + let pending = provider.send_transaction(tx).await.unwrap(); + sent.push((*pending.tx_hash(), sidecar)); + } + sent +} + /// Requests `/eth/v1/beacon/blobs/{block_id}`, which may include a query. async fn beacon_blobs(endpoint: &str, block_id: &str, ssz: bool) -> reqwest::Response { let accept = if ssz { "application/octet-stream" } else { "application/json" }; diff --git a/crates/anvil/tests/it/beacon_state.rs b/crates/anvil/tests/it/beacon_state.rs new file mode 100644 index 000000000..bcfe2ef9a --- /dev/null +++ b/crates/anvil/tests/it/beacon_state.rs @@ -0,0 +1,379 @@ +//! Beacon-backed state persistence: state dump identity and live state loads. + +use crate::{ + beacon_api::{ + BEACON_GENESIS_FORK_VERSION, BEACON_GENESIS_TIME_SECS, BEACON_GENESIS_VALIDATORS_ROOT, + BEACON_LOCAL_BLOB_DATA, BEACON_ORIGIN_BLOCK, BEACON_ORIGIN_TIMESTAMP_SECS, + BEACON_SECONDS_PER_SLOT, BeaconTargetFixture, beacon_slot_timestamp, beacon_target_config, + send_blob_txs, + }, + utils::http_provider, +}; +use alloy_network::TransactionBuilder; +use alloy_primitives::{Address, B256, Bytes, FixedBytes, U256, keccak256}; +use alloy_provider::Provider; +use alloy_rpc_types::{TransactionRequest, anvil::MineOptions}; +use alloy_rpc_types_beacon::genesis::GenesisData; +use anvil::{NodeConfig, eth::EthApi, spawn}; +use flate2::read::GzDecoder; +use foundry_evm::hardfork::EthereumHardfork; +use serde_json::{Value, json}; +use std::{ + collections::BTreeMap, + io::Read, + time::{Duration, Instant}, +}; + +/// Payload of a blob mined only by a node whose history must be discarded or kept intact. +const OTHER_BLOB_DATA: &[u8] = b"other node local blob"; + +async fn rpc_ok(endpoint: &str, method: &str, params: Value) -> Value { + http_provider(endpoint) + .raw_request(method.to_owned().into(), params) + .await + .unwrap_or_else(|error| panic!("{method} failed: {error}")) +} + +/// Reads a JSON number, decimal string, or hex quantity. +fn json_u64(value: &Value) -> u64 { + match value { + Value::Number(number) => number.as_u64().unwrap(), + Value::String(s) => match s.strip_prefix("0x") { + Some(hex) => u64::from_str_radix(hex, 16).unwrap(), + None => s.parse().unwrap(), + }, + _ => panic!("not an integer: {value}"), + } +} + +/// Changes an integer or hex value to a different value of the same shape. +fn perturb(value: &mut Value) { + *value = match value { + Value::String(s) if s.starts_with("0x") => { + let last = if s.ends_with('0') { '1' } else { '0' }; + json!(format!("{}{last}", &s[..s.len() - 1])) + } + Value::String(s) => json!((s.parse::().unwrap() + 1).to_string()), + _ => json!(json_u64(value) + 1), + }; +} + +fn quantity(n: u64) -> Value { + json!(format!("{n:#x}")) +} + +fn ts(slot: u64) -> u64 { + beacon_slot_timestamp(slot) +} + +/// A slow runner may advance several slots while checking the restored state. +fn assert_resumed_timestamp(header: &Value, parent_slot: u64, started: Instant) { + let timestamp = json_u64(&header["timestamp"]); + let earliest = ts(parent_slot + 1); + let latest = earliest.max(ts(parent_slot) + (started.elapsed().as_secs() + 1) / 12 * 12); + assert!( + (earliest..=latest).contains(×tamp) + && (timestamp - BEACON_GENESIS_TIME_SECS).is_multiple_of(BEACON_SECONDS_PER_SLOT), + "timestamp {timestamp} outside restored slot range {earliest}..={latest}" + ); +} + +async fn send_transfer(endpoint: &str, from: Address, to: Address, value: U256) -> B256 { + let tx = TransactionRequest::default() + .with_from(from) + .with_to(to) + .with_nonce(0) + .with_value(value) + .with_gas_limit(21_000) + .with_max_fee_per_gas(10_000_000_000) + .with_max_priority_fee_per_gas(1_000_000_000); + serde_json::from_value(rpc_ok(endpoint, "eth_sendTransaction", json!([tx])).await).unwrap() +} + +async fn mine_at(endpoint: &str, timestamp: u64) { + rpc_ok(endpoint, "evm_mine", json!([MineOptions::Timestamp(Some(timestamp))])).await; +} + +async fn block_at(endpoint: &str, id: Value) -> Value { + rpc_ok(endpoint, "eth_getBlockByNumber", json!([id, false])).await +} + +async fn block_number(endpoint: &str) -> u64 { + http_provider(endpoint).get_block_number().await.unwrap() +} + +async fn balance(endpoint: &str, address: Address) -> U256 { + http_provider(endpoint).get_balance(address).await.unwrap() +} + +async fn nonce(endpoint: &str, address: Address) -> u64 { + http_provider(endpoint).get_transaction_count(address).await.unwrap() +} + +fn blobs_path(query: &str) -> String { + format!("/eth/v1/beacon/blobs/{query}") +} + +/// Fetches a Beacon path as JSON or SSZ, returning the status and exact body bytes. +async fn beacon_get(endpoint: &str, path: &str, ssz: bool) -> (u16, Vec) { + let client = reqwest::Client::builder().timeout(Duration::from_secs(30)).build().unwrap(); + let mut request = client.get(format!("{endpoint}{path}")); + if ssz { + request = request.header(reqwest::header::ACCEPT, "application/octet-stream"); + } + let response = request.send().await.unwrap(); + (response.status().as_u16(), response.bytes().await.unwrap().to_vec()) +} + +/// Execution and Beacon views that must be identical across equivalent states: blocks by number +/// and hash, block receipts by number and hash, transactions and receipts by hash, and hashes of +/// exact Beacon JSON and SSZ bodies. +async fn snapshot( + endpoint: &str, + blocks: &[u64], + txs: &[B256], + beacon_paths: &[String], +) -> BTreeMap { + let mut snapshot = BTreeMap::new(); + for &n in blocks { + let block = rpc_ok(endpoint, "eth_getBlockByNumber", json!([quantity(n), true])).await; + assert!(!block.is_null(), "block {n} is missing"); + let hash = block["hash"].clone(); + // RPC history alone is insufficient: execution must see the same BLOCKHASH. + let contract = Address::repeat_byte(0x73); + let code = format!("0x7f{n:064x}4060005260206000f3"); + let evm_hash = rpc_ok( + endpoint, + "eth_call", + json!([ + {"to": contract}, "pending", {contract.to_string(): {"code": code}} + ]), + ) + .await; + assert_eq!(evm_hash, hash, "EVM BLOCKHASH({n})"); + snapshot.insert(format!("block {n}"), block); + let by_hash = rpc_ok(endpoint, "eth_getBlockByHash", json!([hash, true])).await; + snapshot.insert(format!("block {n} by hash"), by_hash); + let receipts = rpc_ok(endpoint, "eth_getBlockReceipts", json!([quantity(n)])).await; + snapshot.insert(format!("receipts {n}"), receipts); + let receipts = rpc_ok(endpoint, "eth_getBlockReceipts", json!([hash])).await; + snapshot.insert(format!("receipts {n} by hash"), receipts); + } + for tx in txs { + let receipt = rpc_ok(endpoint, "eth_getTransactionReceipt", json!([tx])).await; + snapshot.insert(format!("receipt {tx}"), receipt); + let transaction = rpc_ok(endpoint, "eth_getTransactionByHash", json!([tx])).await; + snapshot.insert(format!("tx {tx}"), transaction); + } + for path in beacon_paths { + for ssz in [false, true] { + let (status, body) = beacon_get(endpoint, path, ssz).await; + let digest = json!({ "status": status, "len": body.len(), "keccak": keccak256(&body) }); + snapshot.insert(format!("{} {path}", if ssz { "ssz" } else { "json" }), digest); + } + } + snapshot +} + +fn brief(value: &Value) -> String { + let s = value.to_string(); + if s.len() <= 240 { s } else { format!("{}... ({} bytes)", &s[..240], s.len()) } +} + +/// Compares snapshots entry by entry, printing only changed entries in truncated form. +fn assert_same( + expected: &BTreeMap, + actual: &BTreeMap, + context: &str, +) { + assert_eq!( + expected.keys().collect::>(), + actual.keys().collect::>(), + "{context}: snapshot keys" + ); + let changed = expected + .iter() + .filter(|(key, value)| actual[*key] != **value) + .map(|(key, value)| format!("{key}: {} -> {}", brief(value), brief(&actual[key]))) + .collect::>(); + assert!( + changed.is_empty(), + "{context}: {} changed entries:\n{}", + changed.len(), + changed.join("\n") + ); +} + +/// Asserts the serialized state carries the exact Beacon fork identity. +fn assert_identity(state: &Value, chain_id: u64, boundary_hash: &Value, slots_in_an_epoch: u64) { + let identity = &state["fork_beacon"]; + assert!(identity.is_object(), "Beacon state must carry a fork_beacon object"); + assert_eq!(json_u64(&identity["chain_id"]), chain_id, "identity chain_id"); + assert_eq!(json_u64(&identity["block_number"]), BEACON_ORIGIN_BLOCK, "identity block_number"); + assert_eq!(identity["block_hash"], *boundary_hash, "identity block_hash"); + assert_eq!( + json_u64(&identity["timestamp"]), + BEACON_ORIGIN_TIMESTAMP_SECS, + "identity timestamp" + ); + let expected_genesis = GenesisData { + genesis_time: BEACON_GENESIS_TIME_SECS, + genesis_validators_root: BEACON_GENESIS_VALIDATORS_ROOT, + genesis_fork_version: FixedBytes::from(BEACON_GENESIS_FORK_VERSION), + }; + let genesis = serde_json::from_value::(identity["genesis"].clone()).unwrap(); + assert_eq!(genesis, expected_genesis, "identity genesis"); + assert_eq!(json_u64(&identity["seconds_per_slot"]), BEACON_SECONDS_PER_SLOT, "identity slot"); + assert_eq!(json_u64(&identity["slots_in_an_epoch"]), slots_in_an_epoch, "identity epoch"); +} + +async fn dump_json(api: &EthApi) -> Value { + let dump = api.anvil_dump_state(None).await.unwrap(); + let mut json = Vec::new(); + GzDecoder::new(dump.as_ref()).read_to_end(&mut json).unwrap(); + serde_json::from_slice(&json).unwrap() +} + +async fn load_json(api: &EthApi, state: &Value) -> Result { + let state = Bytes::from(serde_json::to_vec(state).unwrap()); + api.anvil_load_state(state).await.map_err(|error| error.to_string()) +} + +fn without_identity(state: &Value) -> Value { + let mut state = state.clone(); + state.as_object_mut().unwrap().remove("fork_beacon"); + state +} + +const IDENTITY_MISMATCH: &str = "State dump Beacon identity does not match"; + +#[tokio::test(flavor = "multi_thread")] +async fn beacon_state_rejected_loads_preserve_target() { + let fixture = BeaconTargetFixture::spawn().await; + let source = fixture.handle.http_endpoint(); + let from = fixture.handle.dev_accounts().next().unwrap(); + fixture.mine_blob_block(ts(21), &BEACON_LOCAL_BLOB_DATA).await; + let dump = dump_json(&fixture.api).await; + let source_txs = dump["transactions"] + .as_array() + .unwrap() + .iter() + .map(|tx| serde_json::from_value::(tx["info"]["transaction_hash"].clone()).unwrap()) + .collect::>(); + assert_eq!(source_txs.len(), 2, "source transactions in dump"); + + // The target has its own chain, account changes and blobs at different slots. + let config = beacon_target_config(fixture.origin.http_endpoint(), fixture.beacon.url.clone()); + let (api, target) = spawn(config).await; + let endpoint = target.http_endpoint(); + let sent = send_blob_txs(&endpoint, from, 0, &[OTHER_BLOB_DATA]).await; + let started = Instant::now(); + mine_at(&endpoint, ts(22)).await; + let paths = ["21", "22"].map(blobs_path); + let blocks = [BEACON_ORIGIN_BLOCK, BEACON_ORIGIN_BLOCK + 1]; + let target_txs = [sent[0].0]; + let expected = snapshot(&endpoint, &blocks, &target_txs, &paths).await; + let expected_nonce = nonce(&endpoint, from).await; + let expected_balance = balance(&endpoint, from).await; + + let assert_rejected = async |state: &Value, case: &str, error: &str| { + let result = load_json(&api, state).await; + assert!( + result.as_ref().is_err_and(|message| message.contains(error)), + "load with {case} must fail with {error:?}, got {result:?}" + ); + assert_eq!(block_number(&endpoint).await, blocks[1], "{case} moved the tip"); + let actual = snapshot(&endpoint, &blocks, &target_txs, &paths).await; + assert_same(&expected, &actual, &format!("after rejected load with {case}")); + assert_eq!(nonce(&endpoint, from).await, expected_nonce, "{case} nonce"); + assert_eq!(balance(&endpoint, from).await, expected_balance, "{case} balance"); + for tx in &source_txs { + let receipt = rpc_ok(&endpoint, "eth_getTransactionReceipt", json!([tx])).await; + assert!(receipt.is_null(), "{case} imported source transaction {tx}"); + } + }; + + assert_rejected(&without_identity(&dump), "missing identity", IDENTITY_MISMATCH).await; + + let chain_id = json_u64(&rpc_ok(&source, "eth_chainId", json!([])).await); + let boundary = block_at(&source, quantity(BEACON_ORIGIN_BLOCK)).await; + assert_identity(&dump, chain_id, &boundary["hash"], 32); + for field in [ + "chain_id", + "block_number", + "block_hash", + "timestamp", + "genesis", + "seconds_per_slot", + "slots_in_an_epoch", + ] { + let mut state = dump.clone(); + state["fork_beacon"].as_object_mut().unwrap().remove(field); + assert_rejected(&state, &format!("missing {field}"), "decode state").await; + } + for pointer in [ + "/chain_id", + "/block_number", + "/block_hash", + "/timestamp", + "/genesis/genesis_time", + "/genesis/genesis_validators_root", + "/genesis/genesis_fork_version", + "/seconds_per_slot", + "/slots_in_an_epoch", + ] { + let mut state = dump.clone(); + perturb(state["fork_beacon"].pointer_mut(pointer).unwrap()); + assert_rejected(&state, &format!("mismatched {pointer}"), IDENTITY_MISMATCH).await; + } + let unsupported = "Loading state is not supported with a Beacon upstream"; + assert_rejected(&dump, "matching identity", unsupported).await; + + // Rejections leave the clock on the target's own tip. + rpc_ok(&endpoint, "evm_mine", json!([])).await; + let head = block_at(&endpoint, json!("latest")).await; + assert_eq!(json_u64(&head["number"]), blocks[1] + 1); + assert_resumed_timestamp(&head, 22, started); +} + +#[tokio::test(flavor = "multi_thread")] +async fn beacon_state_legacy_load_works_without_beacon() { + let fixture = BeaconTargetFixture::spawn().await; + let beacon_dump = dump_json(&fixture.api).await; + let execution_only = || { + NodeConfig::test() + .with_eth_rpc_url(Some(fixture.origin.http_endpoint())) + .with_fork_block_number(Some(BEACON_ORIGIN_BLOCK)) + .no_storage_caching() + .with_hardfork(Some(EthereumHardfork::Cancun.into())) + }; + let recipient = Address::repeat_byte(0x24); + let value = U256::from(1_000_000_000u64); + for mode in ["plain", "execution"] { + let config = || match mode { + "plain" => NodeConfig::test().with_hardfork(Some(EthereumHardfork::Cancun.into())), + _ => execution_only(), + }; + let (source_api, source) = spawn(config()).await; + let from = source.dev_accounts().next().unwrap(); + send_transfer(&source.http_endpoint(), from, recipient, value).await; + let tip = block_number(&source.http_endpoint()).await; + let state = dump_json(&source_api).await; + assert!(state.get("fork_beacon").is_none(), "{mode} dump has a Beacon identity"); + + let (api, target) = spawn(config()).await; + load_json(&api, &state).await.unwrap_or_else(|error| panic!("{mode} legacy load: {error}")); + let endpoint = target.http_endpoint(); + assert_eq!(block_number(&endpoint).await, tip, "{mode}"); + assert_eq!(balance(&endpoint, recipient).await, value, "{mode} balance"); + + // Beacon dumps need the Beacon upstream that defines their slots. + let result = load_json(&api, &beacon_dump).await; + assert!( + result.as_ref().is_err_and(|message| message.contains(IDENTITY_MISMATCH)), + "{mode} loaded a Beacon dump: {result:?}" + ); + assert_eq!(block_number(&endpoint).await, tip, "{mode} tip after Beacon dump"); + assert_eq!(balance(&endpoint, recipient).await, value, "{mode} balance after Beacon dump"); + } +} diff --git a/crates/anvil/tests/it/main.rs b/crates/anvil/tests/it/main.rs index 4a52391f7..55942db1e 100644 --- a/crates/anvil/tests/it/main.rs +++ b/crates/anvil/tests/it/main.rs @@ -3,6 +3,7 @@ mod anvil; mod anvil_api; mod api; mod beacon_api; +mod beacon_state; mod eip4844; mod eip7702; mod fork;