Skip to content
Closed
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
30 changes: 30 additions & 0 deletions crates/evm2/src/evm/bal/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,36 @@ impl AccountInfoBal {
changed
}

/// Applies accepted-overlay account writes at or before `bal_index`.
pub(crate) fn populate_account_info_inclusive(
&self,
bal_index: BlockAccessIndex,
account: &mut AccountInfo,
) -> bool {
let mut changed = false;
if let Some(nonce) = self.nonce.get_inclusive(bal_index) {
account.nonce = *nonce;
changed = true;
}
if let Some(balance) = self.balance.get_inclusive(bal_index) {
account.balance = *balance;
changed = true;
}
if let Some((code_hash, code)) = self.code.get_inclusive(bal_index) {
account.code_hash = *code_hash;
account.code = Some(code.clone());
changed = true;
}
changed
}

/// Returns whether accepted-overlay account writes are visible at `bal_index`.
pub(crate) fn has_writes_inclusive(&self, bal_index: BlockAccessIndex) -> bool {
self.nonce.get_inclusive(bal_index).is_some()
|| self.balance.get_inclusive(bal_index).is_some()
|| self.code.get_inclusive(bal_index).is_some()
}

/// Extend account info from another account info.
#[inline]
pub fn update(
Expand Down
59 changes: 58 additions & 1 deletion crates/evm2/src/evm/bal/bal_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ type BalResult<T> = Result<T, BalError>;
/// it at [`Self::bal_index`] (post-state per transaction). A read not covered by the BAL is
/// either an error or falls through to the database, depending on whether fallback is enabled.
/// - **Writes** ([`Self::bal_builder`]): when enabled, `Self::commit_pending` folds each committed
/// transaction's pending post-state into the builder at [`Self::bal_index`].
/// transaction's pending post-state into the builder at [`Self::bal_index`]. Builder writes are
/// also the accepted-state overlay above an attached read BAL.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct BalContext {
/// Optional attached EIP-7928 BAL consulted on reads.
Expand Down Expand Up @@ -132,6 +133,12 @@ impl BalContext {
self.bal_builder.as_ref()
}

/// Returns the BAL builder used as the accepted-state overlay, creating it if needed.
#[inline]
pub(crate) fn bal_builder_mut_or_default(&mut self) -> &mut Bal {
self.bal_builder.get_or_insert_default()
}

/// Returns whether BAL construction is enabled.
#[inline]
pub const fn has_builder(&self) -> bool {
Expand Down Expand Up @@ -251,6 +258,56 @@ impl BalContext {
}
}

/// Applies account writes from the accepted-state BAL overlay.
#[inline]
pub(crate) fn populate_bal_overlay_account(
&self,
address: &Address,
account: &mut Option<AccountInfo>,
) -> bool {
let Some(bal_account) = self.bal_builder.as_ref().and_then(|bal| bal.accounts.get(address))
else {
return false;
};
let was_present = account.is_some();
let mut info = account.take().unwrap_or_default();
let changed =
bal_account.account_info.populate_account_info_inclusive(self.bal_index, &mut info);
if changed || was_present {
*account = Some(info);
}
changed
}

/// Returns whether the accepted-state BAL overlay covers an account.
#[inline]
pub(crate) fn has_bal_overlay_account(&self, address: &Address) -> bool {
self.bal_builder
.as_ref()
.and_then(|bal| bal.accounts.get(address))
.is_some_and(|account| account.account_info.has_writes_inclusive(self.bal_index))
}

/// Returns whether the accepted-state BAL overlay contains an account entry.
#[inline]
pub(crate) fn has_bal_overlay_entry(&self, address: &Address) -> bool {
self.bal_builder.as_ref().is_some_and(|bal| bal.accounts.contains_key(address))
}

/// Returns a storage write from the accepted-state BAL overlay.
#[inline]
pub(crate) fn bal_overlay_storage(&self, address: &Address, key: &Word) -> Option<Word> {
self.bal_builder
.as_ref()?
.accounts
.get(address)?
.storage
.storage
.get(key)?
.get_inclusive(self.bal_index)
.copied()
}

/// Resolves storage slot `key` for `address` from the attached read BAL at the current
/// index.
///
Expand Down
15 changes: 15 additions & 0 deletions crates/evm2/src/evm/bal/changes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,18 @@ impl<T: BalChange> BalChanges<T> {
(i != 0).then(|| self.changes[i - 1].value())
}

/// Returns the latest value written at or before `bal_index`.
///
/// Unlike [`Self::get`], this includes a write at `bal_index`. This is used for accepted
/// overlay state, whose writes are visible immediately after they are applied.
pub(crate) fn get_inclusive(&self, bal_index: BlockAccessIndex) -> Option<&T::Value> {
self.changes
.iter()
.rev()
.find(|change| change.block_access_index() <= bal_index)
.map(BalChange::value)
}

/// Extend the builder with another builder.
pub fn extend(&mut self, other: Self) {
self.changes.extend(other.changes);
Expand Down Expand Up @@ -312,6 +324,9 @@ mod tests {
assert_eq!(bal_changes.get(idx(2)), Some(&2));
assert_eq!(bal_changes.get(idx(3)), Some(&3));
assert_eq!(bal_changes.get(idx(4)), Some(&3));
assert_eq!(bal_changes.get_inclusive(idx(0)), Some(&1));
assert_eq!(bal_changes.get_inclusive(idx(1)), Some(&2));
assert_eq!(bal_changes.get_inclusive(idx(2)), Some(&3));
}

fn get_binary_search(threshold: u64) {
Expand Down
139 changes: 136 additions & 3 deletions crates/evm2/src/evm/db/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,16 @@ pub struct CacheDB<ExtDB = EmptyDB> {
pub _non_exhaustive: (),
}

/// Controls how an account's storage overrides are applied.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum StorageOverrideMode {
/// Override only the supplied slots and fall through for all other slots.
#[default]
Diff,
/// Replace the account's storage, making every unspecified slot zero.
Replace,
}

impl Default for CacheDB<EmptyDB> {
#[inline]
fn default() -> Self {
Expand All @@ -120,6 +130,44 @@ impl<ExtDB> CacheDB<ExtDB> {
let Ok(()) = source.visit(self);
}

/// Applies account and nested per-account storage overrides above BAL reads.
///
/// Overrides reuse the existing BAL builder as the accepted-state overlay; no additional
/// database wrapper or override cache is installed.
pub fn apply_state_overrides(
&mut self,
accounts: impl IntoIterator<Item = (Address, AccountInfo)>,
storage: impl IntoIterator<
Item = (Address, StorageOverrideMode, impl IntoIterator<Item = (Word, Word)>),
>,
) {
let index = self.bal_context.bal_index();
let Self { cache, bal_context, .. } = self;
let overlay = bal_context.bal_builder_mut_or_default();

for (address, mut info) in accounts {
Self::insert_contract_inner(&mut cache.contracts, &mut info);
let code = info.code.take();
let account = &mut overlay.accounts.entry(address).or_default().account_info;
account.nonce.force_update(index, info.nonce);
account.balance.force_update(index, info.balance);
if let Some(code) = code {
account.code.force_update(index, (info.code_hash, code));
}
}

for (address, mode, slots) in storage {
let cached = cache.storage.entry(address).or_default();
if mode == StorageOverrideMode::Replace {
cached.wipe();
}
let overlay = &mut overlay.accounts.entry(address).or_default().storage.storage;
for (key, value) in slots {
overlay.entry(key).or_default().force_update(index, value);
}
}
}

/// Accepts a detached [`PendingState`] -- a committed transaction's post-state -- into this
/// cache.
///
Expand Down Expand Up @@ -286,11 +334,16 @@ impl<ExtDB> StateChangeSink for CacheDB<ExtDB> {
impl<ExtDB: DynDatabase> DynDatabase for CacheDB<ExtDB> {
#[inline]
fn get_account(&mut self, address: &Address) -> DbResult<Option<AccountInfo>> {
let has_overlay = self.bal_context.has_bal_overlay_account(address);
// Resolve the account in the attached read BAL first: with fallback disabled, an
// uncovered account errors before the cache or backing database is consulted.
let bal_account = match self.bal_context.get_bal_account(address) {
Ok(bal_account) => bal_account,
Err(err) => return Err(self.bal_context.store_error(err)),
let bal_account = if has_overlay {
None
} else {
match self.bal_context.get_bal_account(address) {
Ok(bal_account) => bal_account,
Err(err) => return Err(self.bal_context.store_error(err)),
}
};

// Resolve the raw account from the cache or backing database. The cache always stores the
Expand All @@ -314,6 +367,10 @@ impl<ExtDB: DynDatabase> DynDatabase for CacheDB<ExtDB> {
if let Some(bal_account) = bal_account {
self.bal_context.populate_bal_account(bal_account, &mut account);
}
let locally_absent = self.cache.accounts.get(address).is_some_and(Option::is_none);
if self.bal_context.populate_bal_overlay_account(address, &mut account) && locally_absent {
account = None;
}
Ok(account)
}

Expand All @@ -327,6 +384,15 @@ impl<ExtDB: DynDatabase> DynDatabase for CacheDB<ExtDB> {

#[inline]
fn get_storage(&mut self, address: &Address, key: &Word) -> DbResult<Word> {
if let Some(value) = self.bal_context.bal_overlay_storage(address, key) {
return Ok(value);
}
if self.bal_context.has_bal_overlay_entry(address)
&& self.cache.storage.get(address).is_some_and(|storage| storage.wiped)
{
return Ok(Word::ZERO);
}

// Serve the slot from the attached read BAL when it covers a write at or before the current
// index; otherwise fall through to the cache/database.
match self.bal_context.bal_storage(address, key) {
Expand Down Expand Up @@ -581,6 +647,10 @@ mod tests {
Word::from(7),
BalChanges::new(vec![StorageChange::new(BlockAccessIndex::new(1), Word::from(42))]),
);
account.storage.storage.insert(
Word::from(8),
BalChanges::new(vec![StorageChange::new(BlockAccessIndex::new(1), Word::from(43))]),
);
Bal::from_iter([(address, account)])
}

Expand All @@ -607,6 +677,69 @@ mod tests {
assert_eq!(cache.get_storage(&address, &Word::from(7)).unwrap(), Word::from(42));
}

#[test]
fn state_overrides_take_precedence_over_bal() {
let address = Address::with_last_byte(1);
let (overridden_slot, bal_slot, fallback_slot) =
(Word::from(7), Word::from(8), Word::from(9));
let mut cache = cache_with_read_bal(address, true);

let positioned = cache.get_account(&address).unwrap().unwrap();
assert_eq!(positioned.balance, Word::from(500));
let override_code = Bytecode::new_legacy(Bytes::from_static(&[op::STOP]));
let override_code_hash = override_code.hash_slow();

cache.apply_state_overrides(
[(
address,
AccountInfo { balance: Word::from(700), ..positioned.clone() }
.with_code(override_code.clone()),
)],
[(address, StorageOverrideMode::Diff, [(overridden_slot, Word::from(99))])],
);

let overridden = cache.get_account(&address).unwrap().unwrap();
assert_eq!(overridden.balance, Word::from(700));
assert_eq!(overridden.nonce, 3);
assert_eq!(cache.get_code_by_hash(&override_code_hash).unwrap(), override_code);
assert_eq!(cache.get_storage(&address, &overridden_slot).unwrap(), Word::from(99));
assert_eq!(cache.get_storage(&address, &bal_slot).unwrap(), Word::from(43));
assert_eq!(cache.get_storage(&address, &fallback_slot).unwrap(), Word::from(9));

cache.apply_state_overrides(
core::iter::empty(),
[(address, StorageOverrideMode::Replace, [(overridden_slot, Word::from(101))])],
);
assert_eq!(cache.get_storage(&address, &overridden_slot).unwrap(), Word::from(101));
assert_eq!(cache.get_storage(&address, &bal_slot).unwrap(), Word::ZERO);

let committed = AccountInfo { balance: Word::from(800), ..overridden };
let mut pending = PendingState::default();
pending.insert_account(address, Some(positioned), Some(committed));
pending.insert_storage(address, overridden_slot, Word::from(101), Word::from(102));
cache.commit_pending(&pending);
assert_eq!(cache.get_account(&address).unwrap().unwrap().balance, Word::from(800));
assert_eq!(cache.get_storage(&address, &overridden_slot).unwrap(), Word::from(102));
assert!(cache.bal_context.bal_builder().is_some());
}

#[test]
fn state_overrides_bypass_uncovered_bal_entries() {
let covered = Address::with_last_byte(1);
let overridden = Address::with_last_byte(2);
let slot = Word::from(3);
let mut cache = cache_with_read_bal(covered, false);

cache.apply_state_overrides(
[(overridden, AccountInfo::default().with_balance(Word::from(4)))],
[(overridden, StorageOverrideMode::Replace, [(slot, Word::from(5))])],
);

assert_eq!(cache.get_account(&overridden).unwrap().unwrap().balance, Word::from(4));
assert_eq!(cache.get_storage(&overridden, &slot).unwrap(), Word::from(5));
assert_eq!(cache.get_storage(&overridden, &Word::from(6)).unwrap(), Word::ZERO);
}

#[test]
fn uncovered_read_errors_without_fallback() {
let address = Address::with_last_byte(1);
Expand Down
2 changes: 1 addition & 1 deletion crates/evm2/src/evm/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use auto_impl::auto_impl;
use core::error::Error;

mod cache;
pub use cache::{AccountStorageCache, Cache, CacheDB, InMemoryDB};
pub use cache::{AccountStorageCache, Cache, CacheDB, InMemoryDB, StorageOverrideMode};

/// Result of a database operation.
pub type DbResult<T> = Result<T, ErrorCode>;
Expand Down
1 change: 1 addition & 0 deletions crates/evm2/src/evm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ mod tx;
pub use tx::{ExecutedTx, TxResult, TxResultExt, TxResultWithState};

mod state;
pub use db::StorageOverrideMode;
pub use state::{
AccountChangeRef, AccountHandle, AccountInfo, BlockStateAccumulator, JournalEntry,
NoopChangeSink, PendingState, State, StateChangeSink, StateChangeSource, StateCheckpoint,
Expand Down
19 changes: 18 additions & 1 deletion crates/evm2/src/evm/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub use tracked::Tracked;
use super::{
PrewarmSet,
bal::{Bal, BalError, BlockAccessIndex},
db::{CacheDB, DbResult, DynDatabase, boxed_dyn_database},
db::{CacheDB, DbResult, DynDatabase, StorageOverrideMode, boxed_dyn_database},
};
use crate::{
ErrorCode, EvmFeatures, Version,
Expand Down Expand Up @@ -156,6 +156,23 @@ impl<'a> State<'a> {
self.inner.database.commit_source(source);
}

/// Applies account and nested per-account storage overrides above BAL reads.
///
/// Once overrides are installed, subsequently committed execution state is kept above the BAL
/// as well. A [`StorageOverrideMode::Replace`] entry makes unspecified slots read as zero;
/// [`StorageOverrideMode::Diff`] falls through to the BAL and underlying database.
/// Overrides reuse the existing BAL builder; no database wrapper or separate override cache is
/// installed.
pub fn apply_state_overrides(
&mut self,
accounts: impl IntoIterator<Item = (Address, AccountInfo)>,
storage: impl IntoIterator<
Item = (Address, StorageOverrideMode, impl IntoIterator<Item = (Word, Word)>),
>,
) {
self.inner.database.apply_state_overrides(accounts, storage);
}

/// Attaches an EIP-7928 BAL that the accepted-overlay database consults on reads.
///
/// Once attached, account-info and storage reads are served from the BAL at the current block
Expand Down
2 changes: 1 addition & 1 deletion crates/evm2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub mod evm;
pub use evm::config::EvmTypesHost;
pub use evm::{
AccountInfo, BlockStateAccumulator, Evm, ExecutedTx, InterpreterRunner, JournalEntry,
PendingState, TxResult, TxResultExt, TxResultWithState, config,
PendingState, StorageOverrideMode, TxResult, TxResultExt, TxResultWithState, config,
config::{
BaseEvmConfig, BaseEvmConfigSelector, BaseEvmTypes, EvmConfig, EvmConfigSelector, EvmTypes,
ExecutionConfig,
Expand Down
Loading