Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---
Expand Down
16 changes: 16 additions & 0 deletions crates/anvil/src/eth/backend/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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
Expand All @@ -506,6 +519,9 @@ pub struct SerializableState {
/// Note: This is an Option for backwards compatibility.
#[serde(default)]
pub historical_states: Option<SerializableHistoricalStates>,
/// 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<SerializableForkBeacon>,
}

impl SerializableState {
Expand Down
1 change: 1 addition & 0 deletions crates/anvil/src/eth/backend/mem/fork_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ impl Db for ForkedDatabase {
blocks,
transactions,
historical_states,
fork_beacon: None,
}))
}

Expand Down
1 change: 1 addition & 0 deletions crates/anvil/src/eth/backend/mem/in_memory_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ impl Db for MemDb {
blocks,
transactions,
historical_states,
fork_beacon: None,
}))
}

Expand Down
54 changes: 42 additions & 12 deletions crates/anvil/src/eth/backend/mem/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<SerializableForkBeacon> {
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,
Expand All @@ -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
Expand All @@ -1151,6 +1168,19 @@ impl Backend {
/// Apply [SerializableState] data to the backend storage.
pub async fn load_state(&self, state: SerializableState) -> Result<bool, BlockchainError> {
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());
Expand Down
47 changes: 29 additions & 18 deletions crates/anvil/tests/it/beacon_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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" };
Expand Down
Loading
Loading