foundry_evm_core/fork/
bal.rs1use super::Fork;
4use crate::opts::ForkContext;
5use alloy_chains::{Chain, NamedChain};
6use alloy_consensus::BlockHeader;
7use alloy_eips::{
8 BlockId,
9 eip7928::{BlockAccessList, compute_block_access_list_hash, validate_block_access_list},
10};
11use alloy_hardforks::EthereumHardfork;
12use alloy_network::{AnyNetwork, AnyRpcBlock};
13use alloy_primitives::{
14 B256, U256,
15 map::{AddressHashMap, U256Map},
16};
17use alloy_provider::Provider;
18use eyre::{Result, WrapErr};
19use foundry_common::provider::is_rpc_method_not_found;
20use foundry_evm_networks::NetworkConfigs;
21use foundry_fork_db::cache::MemDb;
22use revm::state::{AccountInfo, Bytecode};
23use std::time::Duration;
24
25pub(super) async fn prepare<P: Provider<AnyNetwork>>(
27 provider: &P,
28 resolved: &Fork,
29 block: &AnyRpcBlock,
30) -> Option<BlockAccessList> {
31 if !eligible_source(resolved.context())
32 || block.header.hash != resolved.hash()
33 || block.header.number() != resolved.number()
34 || !EthereumHardfork::from_chain_and_timestamp(
35 Chain::from_id(resolved.context().source_chain_id),
36 block.header.timestamp(),
37 )
38 .is_some_and(|hardfork| hardfork >= EthereumHardfork::Cancun)
39 {
40 return None;
41 }
42
43 let prepare = async {
44 if !immutable_source(provider).await {
46 return None;
47 }
48 let bal = provider.get_block_access_list(BlockId::hash(resolved.hash())).await.ok()??;
49 if let Err(err) =
50 validate_bal(&bal, block.transactions.len(), block.header.block_access_list_hash())
51 {
52 debug!(target: "backend::fork", block_hash = %resolved.hash(), %err, "ignoring invalid fork BAL");
53 return None;
54 }
55 immutable_source(provider).await.then_some(bal)
57 };
58
59 let bal = tokio::time::timeout(Duration::from_millis(500), prepare).await.ok().flatten();
62 if bal.is_none() {
63 debug!(target: "backend::fork", block_hash = %resolved.hash(), "fork BAL unavailable or ineligible");
64 }
65 bal
66}
67
68fn eligible_source(context: ForkContext) -> bool {
69 context.network.is_ethereum()
70 && context.network_profile.canonical_execution_profile() == NetworkConfigs::default()
71 && context.hardfork.is_none()
72 && context.instance_id.is_none()
73 && context.source_fork_block_number.is_none()
74 && context.source_fork_block_hash.is_none()
75 && matches!(
76 NamedChain::try_from(context.source_chain_id),
77 Ok(NamedChain::Mainnet | NamedChain::Sepolia | NamedChain::Holesky | NamedChain::Hoodi)
78 )
79}
80
81async fn immutable_source<P: Provider<AnyNetwork>>(provider: &P) -> bool {
82 matches!(
83 provider.raw_request::<_, serde_json::Value>("anvil_nodeInfo".into(), ()).await,
84 Err(error) if is_rpc_method_not_found(&error)
85 )
86}
87
88pub fn validate_bal(
92 bal: &BlockAccessList,
93 transaction_count: usize,
94 expected_hash: Option<B256>,
95) -> Result<()> {
96 validate_block_access_list(bal, transaction_count).wrap_err("invalid BAL structure")?;
97 if let Some(expected_hash) = expected_hash {
98 eyre::ensure!(compute_block_access_list_hash(bal) == expected_hash, "BAL hash mismatch");
99 }
100 for account in bal {
101 for change in &account.code_changes {
103 Bytecode::new_raw_checked(change.new_code.clone()).wrap_err("invalid BAL code")?;
104 }
105 }
106 Ok(())
107}
108
109pub fn cache_bal(db: &MemDb, bal: BlockAccessList) {
115 let mut accounts = db.accounts.write();
116 let inserted_accounts = cache_bal_accounts(&mut accounts, &bal);
117 drop(accounts);
118
119 let mut storage = db.storage.write();
120 let inserted_slots = cache_bal_storage(&mut storage, &bal);
121 drop(storage);
122 debug!(target: "backend::fork", inserted_accounts, inserted_slots, "prefilled fork cache from BAL");
123}
124
125fn cache_bal_accounts(accounts: &mut AddressHashMap<AccountInfo>, bal: &BlockAccessList) -> usize {
127 let accounts_before = accounts.len();
128 for account in bal {
129 if let (Some(balance), Some(nonce), Some(code)) =
130 (account.balance_post_state(), account.nonce_post_state(), account.code_changes.last())
131 {
132 accounts.entry(account.address()).or_insert_with(|| {
133 let code = Bytecode::new_raw(code.new_code.clone());
134 AccountInfo {
135 balance,
136 nonce,
137 code_hash: code.hash_slow(),
138 code: Some(code),
139 account_id: None,
140 }
141 });
142 }
143 }
144 accounts.len() - accounts_before
145}
146
147fn cache_bal_storage(storage: &mut AddressHashMap<U256Map<U256>>, bal: &BlockAccessList) -> usize {
151 let mut inserted_slots = 0;
152 for account in bal {
153 if !account.storage_changes.is_empty() {
154 let cached_slots = storage.entry(account.address()).or_insert_with(|| {
155 U256Map::with_capacity_and_hasher(account.storage_changes.len(), Default::default())
156 });
157 let slots_before = cached_slots.len();
158 for (slot, value) in account.storage_post_states() {
159 cached_slots.entry(slot).or_insert(value);
160 }
161 inserted_slots += cached_slots.len() - slots_before;
162 }
163 }
164 inserted_slots
165}
166
167#[cfg(test)]
168mod tests;