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
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,14 @@ are bounded to year 9999. Transaction-body pruning is incompatible with retainin
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.
genesis, slot duration and `--slots-in-an-epoch`. Resume with `--state <file>` and the same
explicit fork block and settings. Dumps with another or no identity, or whose local headers do not
extend the fork block contiguously on later slots, are rejected before loading. Other modes still
load legacy dumps but reject Beacon dumps. A Beacon load, including a live `anvil_loadState`,
replaces local accounts, blocks and snapshots instead of merging histories; pause mining and drain
pending transactions first. Clock and fees resume from the canonical tip. Dumps contain neither
endpoint URLs nor clock offsets or pending timestamp overrides, so an orchestrator must reapply
connectivity, mining policy and a current aligned timestamp after downtime.

Everything below is inherited from upstream Foundry and works unchanged; only the Base additions above are specific to this fork.

Expand Down
2 changes: 2 additions & 0 deletions crates/anvil/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1436,6 +1436,8 @@ latest block number: {latest_block}"
total_difficulty: block.header.total_difficulty.unwrap_or_default(),
blob_gas_used: block.header.blob_gas_used.map(|g| g as u128),
blob_excess_gas_and_price: env.evm_env.block_env.blob_excess_gas_and_price,
next_base_fee: fees.base_fee(),
next_blob_excess_gas_and_price: fees.excess_blob_gas_and_price(),
force_transactions,
beacon,
};
Expand Down
20 changes: 13 additions & 7 deletions crates/anvil/src/eth/backend/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,14 +152,20 @@ pub trait Db:

/// Deserialize and add all chain data to the backend storage
fn load_state(&mut self, state: SerializableState) -> DatabaseResult<bool> {
// A Beacon load replaces state, so a nonce set below the upstream's must stay exact.
let replace = state.fork_beacon.is_some();
for (addr, account) in state.accounts.into_iter() {
let old_account_nonce = DatabaseRef::basic_ref(self, addr)
.ok()
.and_then(|acc| acc.map(|acc| acc.nonce))
.unwrap_or_default();
// use max nonce in case account is imported multiple times with difference
// nonces to prevent collisions
let nonce = std::cmp::max(old_account_nonce, account.nonce);
let nonce = if replace {
account.nonce
} else {
let old_account_nonce = DatabaseRef::basic_ref(self, addr)
.ok()
.and_then(|acc| acc.map(|acc| acc.nonce))
.unwrap_or_default();
// use max nonce in case account is imported multiple times with difference
// nonces to prevent collisions
std::cmp::max(old_account_nonce, account.nonce)
};

self.insert_account(
addr,
Expand Down
4 changes: 4 additions & 0 deletions crates/anvil/src/eth/backend/fork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,10 @@ pub struct ClientForkConfig {
pub blob_gas_used: Option<u128>,
/// Blob excess gas and price of the forked block
pub blob_excess_gas_and_price: Option<BlobExcessGasAndPrice>,
/// Base fee of the fork block's successor, explicit or derived from the fork block
pub next_base_fee: u64,
/// Blob excess gas and price of the fork block's successor
pub next_blob_excess_gas_and_price: Option<BlobExcessGasAndPrice>,
/// request timeout
pub timeout: Duration,
/// request retries for spurious networks
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 @@ -111,6 +111,7 @@ impl MaybeFullDatabase for ForkedDatabase {
fn clear(&mut self) {
self.flush_cache();
self.clear_into_state_snapshot();
self.database_mut().cache = Default::default();
}

fn init_from_state_snapshot(&mut self, state_snapshot: StateSnapshot) {
Expand Down
82 changes: 73 additions & 9 deletions crates/anvil/src/eth/backend/mem/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::{
storage::MinedTransactionReceipt,
},
notifications::{NewBlockNotification, NewBlockNotifications},
time::{TimeManager, utc_from_secs},
time::{MAX_BEACON_TIMESTAMP, TimeManager, utc_from_secs},
validate::TransactionValidator,
},
error::{BlockchainError, ErrDetail, InvalidTransactionError},
Expand Down Expand Up @@ -1098,6 +1098,9 @@ impl Backend {
basefee: env.evm_env.block_env.basefee,
..Default::default()
}
} else {
// A state load may invalidate backend snapshots while the database still holds them.
return Ok(false);
}
Ok(self.db.write().await.revert_state(id, RevertStateSnapshotAction::RevertRemove))
}
Expand Down Expand Up @@ -1174,12 +1177,66 @@ impl Backend {
"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());
if let Some(identity) = &identity {
// A Beacon resume is a replacement, unlike the legacy account-merge API. Validate
// the entire local chain before discarding anything, including a live node's suffix.
let mut headers = state.blocks.iter().map(|block| &block.header).collect::<Vec<_>>();
headers.sort_unstable_by_key(|header| header.number);
let mut number = identity.block_number;
let mut hash = identity.block_hash;
let mut timestamp = identity.timestamp;
for header in headers {
if number.checked_add(1) != Some(header.number)
|| header.parent_hash != hash
|| header.timestamp <= timestamp
|| header.timestamp > MAX_BEACON_TIMESTAMP
|| (header.timestamp - identity.genesis.genesis_time)
% identity.seconds_per_slot
!= 0
{
return Err(RpcError::invalid_params(
"Invalid local Beacon chain in state dump",
)
.into());
}
number = header.number;
hash = header.hash_slow();
timestamp = header.timestamp;
}
if state.best_block_number != Some(number)
|| state.block.as_ref().is_none_or(|block| {
block.number != U256::from(number) || block.timestamp != U256::from(timestamp)
})
{
return Err(RpcError::invalid_params(
"State dump does not match its canonical Beacon tip",
)
.into());
}
let fork = self.get_fork().expect("Beacon identity requires a fork");
if state.blocks.is_empty() {
// At the fork block, restore the successor fees from fork setup: they may be
// explicit settings rather than values derived from the fork block.
let config = fork.config.read();
self.time.reset(identity.timestamp);
self.fees.set_base_fee(config.next_base_fee);
if let Some(blob_fees) = config.next_blob_excess_gas_and_price {
self.fees.set_blob_excess_gas_and_price(blob_fees);
}
}
*self.blockchain.storage.write() = BlockchainStorage::forked(
identity.block_number,
identity.block_hash,
fork.total_difficulty(),
);
self.states.write().clear();
self.active_state_snapshots.lock().clear();
let mut db = self.db.write().await;
db.clear();
db.insert_block_hash(U256::from(identity.block_number), identity.block_hash);
for block in &state.blocks {
db.insert_block_hash(U256::from(block.header.number), block.header.hash_slow());
}
}
// load the blocks and transactions into the storage
self.blockchain.storage.write().load_blocks(state.blocks.clone());
Expand Down Expand Up @@ -1230,8 +1287,15 @@ impl Backend {
}
}

if let Some(latest) = state.blocks.iter().max_by_key(|b| b.header.number) {
let header = &latest.header;
let head = state
.blocks
.iter()
.max_by_key(|b| b.header.number)
.map(|latest| AnyHeader::from(latest.header.clone()));
if let Some(header) = head {
if identity.is_some() {
self.time.reset(header.timestamp);
}
let next_block_base_fee = self.fees.get_next_block_base_fee_per_gas(
header.gas_used,
header.gas_limit,
Expand Down
Loading
Loading