Skip to content
Open
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
18 changes: 18 additions & 0 deletions crates/anvil/src/eth/backend/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ pub trait MaybeFullDatabase: crate::mem::DatabaseRef + Send + Sync {

/// Reverses `clear_into_snapshot` by initializing the db's state with the state snapshot.
fn init_from_state_snapshot(&mut self, state_snapshot: StateSnapshot);

/// Starts fetching `accounts` and `slots` that are not held locally, without waiting, so the
/// reads that follow do not each make a round trip.
///
/// Only a database backed by a remote endpoint fetches anything.
fn prefetch(&self, _accounts: &[Address], _slots: &[(Address, U256)]) {}
}

/// A borrowed, type-erased state that overlays are built on, so that EVMs are monomorphized once.
Expand Down Expand Up @@ -137,6 +143,10 @@ where
fn clear(&mut self) {}

fn init_from_state_snapshot(&mut self, _state_snapshot: StateSnapshot) {}

fn prefetch(&self, accounts: &[Address], slots: &[(Address, U256)]) {
T::prefetch(self, accounts, slots)
}
}

impl<T: MaybeFullDatabase + ?Sized> MaybeFullDatabase for Box<T>
Expand Down Expand Up @@ -178,6 +188,10 @@ where
fn init_from_state_snapshot(&mut self, state_snapshot: StateSnapshot) {
T::init_from_state_snapshot(self, state_snapshot)
}

fn prefetch(&self, accounts: &[Address], slots: &[(Address, U256)]) {
T::prefetch(self, accounts, slots)
}
}

/// Helper trait to reset the DB if it's forked
Expand Down Expand Up @@ -627,6 +641,10 @@ impl<T: MaybeFullDatabase> MaybeFullDatabase for CacheDB<T> {
}
self.cache.block_hashes = block_hashes;
}

fn prefetch(&self, accounts: &[Address], slots: &[(Address, U256)]) {
self.db.prefetch(accounts, slots)
}
}

impl MaybeFullDatabase for EmptyDBWrapper {
Expand Down
114 changes: 114 additions & 0 deletions crates/anvil/src/eth/backend/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use alloy_consensus::{
use alloy_eips::{
Encodable2718, eip2935, eip4788,
eip6110::DEPOSIT_REQUEST_TYPE,
eip7002::WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS,
eip7251::CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS,
eip7685::Requests,
eip7702::{RecoveredAuthority, RecoveredAuthorization},
};
Expand Down Expand Up @@ -149,6 +151,57 @@ fn append_deposit_requests(
Ok(())
}

/// Length of the EIP-4788 beacon roots ring buffer.
///
/// See <https://eips.ethereum.org/EIPS/eip-4788>.
const BEACON_ROOTS_HISTORY_BUFFER_LENGTH: u64 = 8191;

/// Returns the accounts and storage slots the system calls of a block at `number` and `timestamp`
/// read, so that a fork can fetch them at once instead of one round trip per read.
///
/// The EIP-2935 call stores the parent hash in slot `(number - 1) % 8191`, the EIP-4788 call
/// stores the timestamp and the root in slots `timestamp % 8191` and `timestamp % 8191 + 8191`,
/// and the EIP-7002/7251 calls read their excess, count, queue head and queue tail (slots 0 to 3)
/// after the transactions.
pub(crate) fn system_call_prefetch_keys(
transitions: &EthereumBlockTransitions,
number: u64,
timestamp: u64,
) -> (Vec<Address>, Vec<(Address, U256)>) {
let mut accounts = Vec::new();
let mut slots = Vec::new();
if number == 0 {
return (accounts, slots);
}
if transitions.hardfork >= EthereumHardfork::Prague {
let slot = (number - 1) % eip2935::HISTORY_SERVE_WINDOW as u64;
accounts.push(eip2935::HISTORY_STORAGE_ADDRESS);
slots.push((eip2935::HISTORY_STORAGE_ADDRESS, U256::from(slot)));
}
if transitions.hardfork >= EthereumHardfork::Cancun
&& transitions.parent_beacon_block_root.is_some()
{
let slot = timestamp % BEACON_ROOTS_HISTORY_BUFFER_LENGTH;
accounts.push(eip4788::BEACON_ROOTS_ADDRESS);
slots.push((eip4788::BEACON_ROOTS_ADDRESS, U256::from(slot)));
slots.push((
eip4788::BEACON_ROOTS_ADDRESS,
U256::from(slot + BEACON_ROOTS_HISTORY_BUFFER_LENGTH),
));
}
if transitions.hardfork >= EthereumHardfork::Prague
&& transitions.execution_kind == BlockExecutionKind::Complete
{
for address in
[WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS]
{
accounts.push(address);
slots.extend((0..4u64).map(|slot| (address, U256::from(slot))));
}
}
(accounts, slots)
}

/// Receipt builder for transaction types that do not require network-specific metadata.
#[derive(Debug, Default, Clone, Copy)]
#[non_exhaustive]
Expand Down Expand Up @@ -968,4 +1021,65 @@ mod tests {
assert_eq!(request[0], DEPOSIT_REQUEST_TYPE);
assert_eq!(request.len(), 1 + 48 + 32 + 8 + 96 + 8);
}

#[test]
fn system_call_prefetch_keys_follow_the_system_contracts() {
let transitions = |hardfork, execution_kind| EthereumBlockTransitions {
hardfork,
deposit_contract_address: Address::ZERO,
parent_beacon_block_root: Some(B256::ZERO),
execution_kind,
};

let (accounts, slots) = system_call_prefetch_keys(
&transitions(EthereumHardfork::Prague, BlockExecutionKind::Complete),
100,
8191 * 2 + 5,
);
assert_eq!(
accounts,
[
eip2935::HISTORY_STORAGE_ADDRESS,
eip4788::BEACON_ROOTS_ADDRESS,
WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS,
CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS,
]
);
let mut expected = vec![
(eip2935::HISTORY_STORAGE_ADDRESS, U256::from(99)),
(eip4788::BEACON_ROOTS_ADDRESS, U256::from(5)),
(eip4788::BEACON_ROOTS_ADDRESS, U256::from(5 + 8191)),
];
for address in
[WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS]
{
expected.extend((0..4u64).map(|slot| (address, U256::from(slot))));
}
assert_eq!(slots, expected);

// A transaction prefix does not drain the request queues.
let (_, slots) = system_call_prefetch_keys(
&transitions(EthereumHardfork::Prague, BlockExecutionKind::TransactionPrefix),
100,
5,
);
assert_eq!(slots.len(), 3);

// Before Prague there is no block hash history nor request queues.
let (accounts, slots) = system_call_prefetch_keys(
&transitions(EthereumHardfork::Cancun, BlockExecutionKind::Complete),
100,
5,
);
assert_eq!(accounts, [eip4788::BEACON_ROOTS_ADDRESS]);
assert_eq!(slots.len(), 2);

// The genesis block runs no system calls.
let (accounts, slots) = system_call_prefetch_keys(
&transitions(EthereumHardfork::Prague, BlockExecutionKind::Complete),
0,
5,
);
assert!(accounts.is_empty() && slots.is_empty());
}
}
4 changes: 4 additions & 0 deletions crates/anvil/src/eth/backend/mem/fork_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ impl<N: Network> MaybeFullDatabase for ForkedDatabase<N> {
*db.storage.write() = storage;
*db.block_hashes.write() = block_hashes;
}

fn prefetch(&self, accounts: &[Address], slots: &[(Address, U256)]) {
Self::prefetch(self, accounts, slots)
}
}

impl<N: Network> MaybeFullDatabase for ForkDbStateSnapshot<N> {
Expand Down
12 changes: 11 additions & 1 deletion crates/anvil/src/eth/backend/mem/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::{
ExecutedPoolTransactions, FoundryReceiptBuilder, PoolTxGasConfig,
apply_ethereum_post_execution_changes, apply_ethereum_pre_execution_changes,
block_blob_gas_limit, build_tx_env_for_pending, execute_pool_transaction,
execute_pool_transactions,
execute_pool_transactions, system_call_prefetch_keys,
},
fork::{ClientFork, ForkEndpointIdentity},
genesis::GenesisConfig,
Expand Down Expand Up @@ -3180,6 +3180,16 @@ impl<N: Network> Backend<N> {
let inspector = self.build_mining_inspector();
let transitions =
self.ethereum_block_transitions(hardfork, parent_beacon_block_root, execution_kind);
if let Some(transitions) = &transitions {
// A fork reads each slot of the system calls with its own round trip, one after
// another; start them all now so the calls find them in flight or cached.
let (accounts, slots) = system_call_prefetch_keys(
transitions,
evm_env.block_env.number.saturating_to(),
evm_env.block_env.timestamp.saturating_to(),
);
db.0.prefetch(&accounts, &slots);
}

macro_rules! execute {
($executor:expr) => {{
Expand Down
51 changes: 51 additions & 0 deletions crates/anvil/tests/it/eip2935.rs
Original file line number Diff line number Diff line change
Expand Up @@ -531,3 +531,54 @@ async fn geth_block_replay_discards_partial_results_on_execution_error() {
assert_eq!(provider.get_transaction_count(from).await.unwrap(), 3);
assert_eq!(provider.get_block_number().await.unwrap(), block_number);
}

/// Blocks mined on a fork run their system calls against slots prefetched from the origin, and
/// store the same values a local chain does.
#[tokio::test(flavor = "multi_thread")]
async fn fork_system_calls_store_parent_hash_and_beacon_root() {
let (origin_api, origin_handle) =
spawn(NodeConfig::test().with_hardfork(Some(EthereumHardfork::Prague.into()))).await;
for _ in 0..3 {
origin_api.mine_one().await.unwrap();
}
let (fork_api, fork_handle) = spawn(
NodeConfig::test()
.with_no_storage_caching(true)
.with_eth_rpc_url(Some(origin_handle.http_endpoint())),
)
.await;
let provider = http_provider(&fork_handle.http_endpoint());
let history = |number: u64| {
TransactionRequest::default()
.with_to(HISTORY_STORAGE_ADDRESS)
.with_input(U256::from(number).to_be_bytes::<32>())
};

// A hash the origin stored before the fork is read from the origin.
let block2 = provider.get_block_by_number(2.into()).await.unwrap().unwrap();
let stored = provider.call(history(2).into()).await.unwrap();
assert_eq!(B256::from_slice(&stored), block2.header.hash);

for _ in 0..2 {
fork_api.mine_one().await.unwrap();
let block = provider.get_block_by_number(BlockNumberOrTag::Latest).await.unwrap().unwrap();
let parent = block.header.number - 1;

let stored = provider.call(history(parent).into()).await.unwrap();
assert_eq!(B256::from_slice(&stored), block.header.parent_hash);

let slot = block.header.timestamp % 8191;
let timestamp = provider
.get_storage_at(BEACON_ROOTS_ADDRESS, U256::from(slot))
.block_id(BlockId::latest())
.await
.unwrap();
assert_eq!(timestamp, U256::from(block.header.timestamp));
let root = provider
.get_storage_at(BEACON_ROOTS_ADDRESS, U256::from(slot + 8191))
.block_id(BlockId::latest())
.await
.unwrap();
assert_eq!(B256::from(root), block.header.parent_beacon_block_root.unwrap());
}
}
16 changes: 16 additions & 0 deletions crates/evm/core/src/fork/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,22 @@ impl<N: Network, B: ForkBlockEnv> ForkedDatabase<N, B> {
false
}
}

/// Starts fetching `accounts` and `slots` from the remote endpoint at once, without waiting,
/// so the reads that follow find them in flight or cached instead of making one round trip
/// each.
///
/// Keys this database already holds locally are not fetched.
pub fn prefetch(&self, accounts: &[Address], slots: &[(Address, U256)]) {
let local = &self.cache_db.cache.accounts;
super::prefetch::spawn_reads(
&self.backend,
accounts.iter().copied().filter(|address| !local.contains_key(address)),
slots.iter().copied().filter(|(address, slot)| {
!local.get(address).is_some_and(|account| account.storage.contains_key(slot))
}),
);
}
}

impl<N: Network, B: ForkBlockEnv> Database for ForkedDatabase<N, B> {
Expand Down
29 changes: 29 additions & 0 deletions crates/evm/core/src/fork/prefetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,35 @@ impl Prefetch {
}
}

/// Starts reading `accounts` and `slots` through `backend` in the background and returns at once.
///
/// The backend sends each read as its own request, so reads started together are in flight
/// together, and an EVM read of a key still in flight joins that request instead of sending
/// another one. As with the cache prefetch, a key the EVM then does not read stays out of the cache
/// file. Without a tokio runtime this does nothing, and the EVM reads the keys itself.
pub(crate) fn spawn_reads<N: Network, B: ForkBlockEnv>(
backend: &SharedBackend<N, B>,
accounts: impl IntoIterator<Item = Address>,
slots: impl IntoIterator<Item = (Address, U256)>,
) {
let Ok(runtime) = tokio::runtime::Handle::try_current() else { return };
let backend = backend.with_blocking_mode(BlockingMode::Block);
let keys = accounts
.into_iter()
.map(Key::Account)
.chain(slots.into_iter().map(|(address, slot)| Key::Slot(address, slot)));
for key in keys {
let backend = backend.clone();
runtime.spawn_blocking(move || {
// The EVM reads any key a failed prefetch did not fetch.
let _ = match key {
Key::Account(address) => backend.prefetch_basic(address),
Key::Slot(address, slot) => backend.prefetch_storage(address, slot),
};
});
}
}

/// Returns the cache file of the newest block below `number` in a chain cache directory.
fn previous_cache_file(dir: &Path, number: u64) -> Option<PathBuf> {
std::fs::read_dir(dir)
Expand Down