Skip to main content

foundry_evm_core/fork/
bal.rs

1//! Validates and caches BAL post-state, and prepares transaction forks' parent-block BALs.
2
3use 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
25/// Fetches and validates a parent BAL without mutating a database or propagating BAL failures.
26pub(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        // An inconclusive discovery probe is not proof of an immutable source.
45        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        // A local node can change state without changing its block hash.
56        immutable_source(provider).await.then_some(bal)
57    };
58
59    // Reuse the validated block; optional BAL and source probes share one budget, including
60    // retries.
61    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
88/// Validates the entire BAL before any values can enter the cache.
89///
90/// Callers must separately establish that the BAL and cache belong to the same immutable block.
91pub 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        // Reject invalid code even in earlier changes or incomplete accounts.
102        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
109/// Inserts a validated BAL's post-state into its selected remote cache, retaining existing values.
110///
111/// The BAL must pass [`validate_bal`] before this call, and the cache must belong to its immutable
112/// source block. Account and storage locks are acquired separately; insertion is not atomic across
113/// the two maps.
114pub 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
125/// Inserts complete account post-states, retaining existing accounts, and returns the added count.
126fn 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
147/// Inserts final slot writes, retaining cached values and leaving read-only slots unknown.
148///
149/// Returns the number of added slots.
150fn 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;