Skip to main content

ethrex_levm/db/
gen_db.rs

1use std::sync::Arc;
2
3use ethrex_common::Address;
4use ethrex_common::H256;
5use ethrex_common::U256;
6use ethrex_common::types::Account;
7use ethrex_common::types::Code;
8use ethrex_common::types::CodeMetadata;
9#[cfg(all(feature = "rayon", not(feature = "eip-8025")))]
10use ethrex_common::types::block_access_list::SlotChange;
11use ethrex_common::types::block_access_list::{
12    BalAddressIndex, BlockAccessList, BlockAccessListRecorder,
13};
14use ethrex_common::utils::ZERO_U256;
15
16use super::Database;
17use crate::account::AccountStatus;
18use crate::account::LevmAccount;
19use crate::call_frame::CallFrameBackup;
20use crate::errors::InternalError;
21use crate::errors::VMError;
22use crate::utils::account_to_levm_account;
23use crate::utils::restore_cache_state;
24use crate::vm::VM;
25pub use ethrex_common::types::AccountUpdate;
26use rustc_hash::{FxHashMap, FxHashSet};
27use std::collections::hash_map::Entry;
28
29pub type CacheDB = FxHashMap<Address, LevmAccount>;
30
31/// Per-tx BAL cursor for lazy on-read prefix materialization.
32/// `bal_index = tx_idx + 1`; cursor's effective max_idx is `bal_index - 1`,
33/// matching `seed_db_from_bal`'s `max_idx = tx_idx` semantics.
34#[derive(Clone)]
35pub struct LazyBalCursor {
36    pub bal: Arc<BlockAccessList>,
37    pub bal_index: u32,
38    pub index: Arc<BalAddressIndex>,
39}
40
41/// Apply balance, nonce, and code fields from BAL for a single account into `db`.
42///
43/// Returns `true` if any info field was applied; `false` if all field positions
44/// were 0 (no info changes for this account at indices <= max_idx).
45/// Does NOT touch `account.storage`.
46#[cfg(all(feature = "rayon", not(feature = "eip-8025")))]
47pub fn seed_one_address_info_from_bal(
48    db: &mut GeneralizedDatabase,
49    bal: &BlockAccessList,
50    acct_idx: usize,
51    max_idx: u32,
52) -> Result<bool, InternalError> {
53    use ethrex_common::types::AccountInfo;
54
55    let acct_changes = bal
56        .accounts()
57        .get(acct_idx)
58        .ok_or(InternalError::AccountNotFound)?;
59    let addr = acct_changes.address;
60
61    let balance_pos = acct_changes
62        .balance_changes
63        .partition_point(|c| c.block_access_index <= max_idx);
64    let nonce_pos = acct_changes
65        .nonce_changes
66        .partition_point(|c| c.block_access_index <= max_idx);
67    let code_pos = acct_changes
68        .code_changes
69        .partition_point(|c| c.block_access_index <= max_idx);
70
71    if balance_pos == 0 && nonce_pos == 0 && code_pos == 0 {
72        return Ok(false);
73    }
74
75    // Compute code update before borrowing acc (borrow checker: can't access
76    // db.codes while acc holds a mutable borrow of db).
77    let code_update = if code_pos > 0 {
78        let entry = acct_changes
79            .code_changes
80            .get(code_pos.saturating_sub(1))
81            .ok_or(InternalError::AccountNotFound)?;
82        Some(code_from_bal(&entry.new_code))
83    } else {
84        None
85    };
86
87    // When BAL covers all account info fields (balance + nonce + code), insert
88    // a default LevmAccount directly to skip the store/shared_base lookup.
89    // For partial coverage, load from store to fill missing fields.
90    //
91    // Invariant: `account.storage` is left empty here. Storage is materialized
92    // lazily through `get_storage_value` (which also consults the cursor).
93    // Callers must NOT assume `account.storage` is fully populated after this
94    // path — iterate-all-keys / bulk-read patterns will see an empty map.
95    let has_all_info = balance_pos > 0 && nonce_pos > 0 && code_pos > 0;
96    if has_all_info {
97        use ethrex_common::constants::EMPTY_KECCAK_HASH;
98        let balance = acct_changes
99            .balance_changes
100            .get(balance_pos.saturating_sub(1))
101            .ok_or(InternalError::AccountNotFound)?
102            .post_balance;
103        let nonce = acct_changes
104            .nonce_changes
105            .get(nonce_pos.saturating_sub(1))
106            .ok_or(InternalError::AccountNotFound)?
107            .post_nonce;
108        let code_hash = code_update
109            .as_ref()
110            .map(|(h, _)| *h)
111            .unwrap_or(*EMPTY_KECCAK_HASH);
112        let acc = db
113            .current_accounts_state
114            .entry(addr)
115            .or_insert_with(|| LevmAccount {
116                info: AccountInfo::default(),
117                storage: FxHashMap::default(),
118                has_storage: false,
119                status: AccountStatus::Modified,
120                exists: true,
121            });
122        acc.info.balance = balance;
123        acc.info.nonce = nonce;
124        acc.info.code_hash = code_hash;
125        acc.mark_modified();
126    } else {
127        db.get_account(addr)
128            .map_err(|e| InternalError::Custom(format!("seed_db_from_bal load: {e}")))?;
129        let acc = db
130            .get_account_mut(addr)
131            .map_err(|e| InternalError::Custom(format!("seed bal: {e}")))?;
132
133        if balance_pos > 0
134            && let Some(entry) = acct_changes
135                .balance_changes
136                .get(balance_pos.saturating_sub(1))
137        {
138            acc.info.balance = entry.post_balance;
139        }
140        if nonce_pos > 0
141            && let Some(entry) = acct_changes.nonce_changes.get(nonce_pos.saturating_sub(1))
142        {
143            acc.info.nonce = entry.post_nonce;
144        }
145        if let Some((hash, _)) = &code_update {
146            acc.info.code_hash = *hash;
147        }
148    }
149
150    // Insert code object after acc borrow is released.
151    if let Some((hash, Some(code_obj))) = code_update {
152        db.codes.entry(hash).or_insert(code_obj);
153    }
154
155    Ok(true)
156}
157
158/// Select the post-value of a single `SlotChange` up to `max_idx`.
159///
160/// Pure read; returns `Some(value)` if any `slot_changes` entry has
161/// `block_access_index <= max_idx`, `None` otherwise.
162#[cfg(all(feature = "rayon", not(feature = "eip-8025")))]
163pub fn post_value_at_or_before(sc: &SlotChange, max_idx: u32) -> Option<U256> {
164    let pos = sc
165        .slot_changes
166        .partition_point(|c| c.block_access_index <= max_idx);
167    sc.slot_changes
168        .get(pos.saturating_sub(1))
169        .filter(|_| pos > 0)
170        .map(|c| c.post_value)
171}
172
173/// Read the post-value of a single storage slot from the BAL up to `max_idx`.
174///
175/// O(1) slot resolution via the precomputed `slot_idx_by_account` map in
176/// `BalAddressIndex`. Pure read; does not touch `db`.
177#[cfg(all(feature = "rayon", not(feature = "eip-8025")))]
178pub fn seed_one_storage_slot_from_bal(
179    bal: &BlockAccessList,
180    index: &BalAddressIndex,
181    acct_idx: usize,
182    key: H256,
183    max_idx: u32,
184) -> Option<U256> {
185    let acct_changes = bal.accounts().get(acct_idx)?;
186    let slot_map = index.slot_idx_by_account.get(acct_idx)?;
187    let sc_idx = *slot_map.get(&key)?;
188    let sc = acct_changes.storage_changes.get(sc_idx)?;
189    post_value_at_or_before(sc, max_idx)
190}
191
192/// Compute code hash and optional `Code` object from raw bytecode in a BAL entry.
193#[cfg(all(feature = "rayon", not(feature = "eip-8025")))]
194pub fn code_from_bal(new_code: &bytes::Bytes) -> (H256, Option<Code>) {
195    use ethrex_common::constants::EMPTY_KECCAK_HASH;
196    if new_code.is_empty() {
197        (*EMPTY_KECCAK_HASH, None)
198    } else {
199        let code_obj = Code::from_bytecode(new_code.clone(), &ethrex_crypto::NativeCrypto);
200        let hash = code_obj.hash;
201        (hash, Some(code_obj))
202    }
203}
204
205#[derive(Clone)]
206pub struct GeneralizedDatabase {
207    pub store: Arc<dyn Database>,
208    pub current_accounts_state: CacheDB,
209    pub initial_accounts_state: CacheDB,
210    /// Shared read-only base state (pre-block snapshot of system-touched addresses for
211    /// parallel groups, captured from `initial_accounts_state` after `prepare_block`).
212    /// Checked on `load_account` AFTER the `lazy_bal` hook so the BAL overlay (which
213    /// includes system-call effects at idx 0) takes precedence for any address the BAL
214    /// covers. Accounts are cloned into `initial_accounts_state` on first access.
215    pub shared_base: Option<Arc<CacheDB>>,
216    pub codes: FxHashMap<H256, Code>,
217    pub code_metadata: FxHashMap<H256, CodeMetadata>,
218    pub tx_backup: Option<CallFrameBackup>,
219    /// Optional BAL recorder for EIP-7928 Block Access List recording.
220    pub bal_recorder: Option<BlockAccessListRecorder>,
221    /// When true, skip cloning accounts into `initial_accounts_state` on load.
222    /// Used for parallel per-tx DBs where `get_state_transitions_tx` is never called
223    /// (state transitions come from BAL instead).
224    skip_initial_tracking: bool,
225    /// Optional tracker for BAL validation: records addresses accessed via load_account.
226    /// Enabled only during parallel execution to detect extraneous BAL pure-access entries.
227    pub accessed_accounts: Option<FxHashSet<Address>>,
228    /// Optional BAL cursor for lazy per-read prefix materialization.
229    /// When set, account loads and storage reads consult the BAL before hitting the store.
230    pub lazy_bal: Option<LazyBalCursor>,
231}
232
233impl GeneralizedDatabase {
234    pub fn new(store: Arc<dyn Database>) -> Self {
235        Self {
236            store,
237            current_accounts_state: Default::default(),
238            initial_accounts_state: Default::default(),
239            shared_base: None,
240            tx_backup: None,
241            codes: Default::default(),
242            code_metadata: Default::default(),
243            bal_recorder: None,
244            skip_initial_tracking: false,
245            accessed_accounts: None,
246            lazy_bal: None,
247        }
248    }
249
250    /// Creates a new GeneralizedDatabase with a shared read-only base state.
251    /// Used for parallel execution groups that share post-system-call state.
252    /// Skips initial_accounts_state tracking since parallel per-tx DBs never
253    /// call get_state_transitions_tx (state comes from BAL instead).
254    pub fn new_with_shared_base(store: Arc<dyn Database>, shared_base: Arc<CacheDB>) -> Self {
255        Self::new_with_shared_base_and_capacity(store, shared_base, 0)
256    }
257
258    /// Like `new_with_shared_base` but pre-allocates account/code maps to
259    /// `capacity` entries, avoiding rehashing during BAL seeding.
260    pub fn new_with_shared_base_and_capacity(
261        store: Arc<dyn Database>,
262        shared_base: Arc<CacheDB>,
263        capacity: usize,
264    ) -> Self {
265        Self {
266            store,
267            current_accounts_state: FxHashMap::with_capacity_and_hasher(
268                capacity,
269                Default::default(),
270            ),
271            initial_accounts_state: Default::default(),
272            shared_base: Some(shared_base),
273            tx_backup: None,
274            codes: FxHashMap::with_capacity_and_hasher(capacity / 4, Default::default()),
275            code_metadata: Default::default(),
276            bal_recorder: None,
277            skip_initial_tracking: true,
278            accessed_accounts: None,
279            lazy_bal: None,
280        }
281    }
282
283    /// Enables BAL recording for EIP-7928.
284    /// After enabling, state changes will be recorded during execution.
285    pub fn enable_bal_recording(&mut self) {
286        self.bal_recorder = Some(BlockAccessListRecorder::new());
287    }
288
289    /// Disables BAL recording.
290    pub fn disable_bal_recording(&mut self) {
291        self.bal_recorder = None;
292    }
293
294    /// Sets the current block access index for BAL recording per EIP-7928 spec (uint32).
295    /// Call this before each transaction or phase.
296    pub fn set_bal_index(&mut self, index: u32) {
297        if let Some(recorder) = &mut self.bal_recorder {
298            recorder.set_block_access_index(index);
299        }
300    }
301
302    /// Takes the BAL recorder and builds the final BlockAccessList.
303    /// Returns None if recording was not enabled.
304    pub fn take_bal(&mut self) -> Option<BlockAccessList> {
305        self.bal_recorder.take().map(|recorder| recorder.build())
306    }
307
308    /// Returns a mutable reference to the BAL recorder if enabled.
309    pub fn bal_recorder_mut(&mut self) -> Option<&mut BlockAccessListRecorder> {
310        self.bal_recorder.as_mut()
311    }
312
313    /// Only used within Levm Runner, where the accounts already have all the storage pre-loaded, not used in real case scenarios.
314    pub fn new_with_account_state(
315        store: Arc<dyn Database>,
316        current_accounts_state: FxHashMap<Address, Account>,
317    ) -> Self {
318        let mut codes: FxHashMap<H256, Code> = Default::default();
319        let levm_accounts: FxHashMap<Address, LevmAccount> = current_accounts_state
320            .into_iter()
321            .map(|(address, account)| {
322                let (levm_account, code) = account_to_levm_account(account);
323                codes.insert(levm_account.info.code_hash, code);
324                (address, levm_account)
325            })
326            .collect();
327        Self {
328            store,
329            current_accounts_state: levm_accounts.clone(),
330            initial_accounts_state: levm_accounts,
331            shared_base: None,
332            tx_backup: None,
333            codes,
334            code_metadata: Default::default(),
335            bal_recorder: None,
336            skip_initial_tracking: false,
337            accessed_accounts: None,
338            lazy_bal: None,
339        }
340    }
341
342    // ================== Account related functions =====================
343    /// Loads account
344    /// If it's the first time it's loaded store it in `initial_accounts_state` and also cache it in `current_accounts_state` for making changes to it
345    fn load_account(&mut self, address: Address) -> Result<&mut LevmAccount, InternalError> {
346        if let Some(tracker) = &mut self.accessed_accounts {
347            tracker.insert(address);
348        }
349
350        if self.current_accounts_state.contains_key(&address) {
351            return self
352                .current_accounts_state
353                .get_mut(&address)
354                .ok_or(InternalError::AccountNotFound);
355        }
356
357        // Initial-state fast path.
358        //
359        // Clone info/flags only, NOT the storage map. The streaming executor drains
360        // `current_accounts_state` into `initial_accounts_state` every few txs, so a hot account
361        // (token contracts, etc.) is re-faulted here repeatedly with an ever-growing storage map
362        // it barely reads. The touched slots are faulted back in lazily by `get_storage_value`,
363        // which resolves a `current` miss against `initial` (the committed baseline) before the
364        // store — so this stays correct and the diff invariant holds. See `clone_without_storage`.
365        //
366        // Exception: destroyed-and-recreated accounts must be full-cloned. `get_storage_value`
367        // early-returns 0 for `DestroyedModified` *before* the `initial` fallback (an unwritten
368        // slot of a destroyed account must read 0, never the stale value in `initial`). With an
369        // info-only clone, a committed slot written after recreation — folded into `initial`
370        // wholesale by the per-flush drain-back — would also read 0, since the lazy fallback is
371        // never reached. Carrying the storage on the clone keeps those committed slots in
372        // `current`, where the `account.storage` hit precedes the early-return.
373        if let Some(account) = self.initial_accounts_state.get(&address) {
374            let clone = match account.status {
375                AccountStatus::Destroyed | AccountStatus::DestroyedModified => account.clone(),
376                _ => account.clone_without_storage(),
377            };
378            return Ok(self.current_accounts_state.entry(address).or_insert(clone));
379        }
380
381        // Lazy-BAL hook: if the cursor finds this address, materialize info from the BAL
382        // before consulting `shared_base` or the store.
383        //
384        // Ordering matters: `shared_base` holds the pre-block snapshot of system-touched
385        // addresses, but the canonical pre-state for tx N is the BAL prefix up to its
386        // `bal_index` (= system-call effects at idx 0 plus all prior txs). If `shared_base`
387        // were consulted first for an address it covers, the BAL overlay would be skipped
388        // and tx N would observe stale balance/nonce/code (consensus bug for system-touched
389        // predeploys mutated by a prior tx in the same block).
390        //
391        // We `.take()` the cursor out of `self.lazy_bal` before calling
392        // `seed_one_address_info_from_bal`. For partial-coverage accounts (e.g. balance-only
393        // change with no nonce/code) the helper calls `db.get_account(addr)` internally to
394        // load the base state before overlaying. If `self.lazy_bal` were still `Some(...)`
395        // at that point, `get_account` → `load_account` would re-enter this same block and
396        // recurse infinitely. Taking the cursor out breaks the cycle: the inner call sees
397        // `lazy_bal = None` and falls through to `shared_base`/store. We restore the cursor
398        // unconditionally afterward (even on error) so the outer caller still sees it.
399        #[cfg(all(feature = "rayon", not(feature = "eip-8025")))]
400        {
401            let cursor_opt = self.lazy_bal.take();
402            let helper_result = if let Some(cursor) = cursor_opt.as_ref() {
403                debug_assert!(
404                    cursor.bal_index >= 1,
405                    "LazyBalCursor bal_index must be >= 1"
406                );
407                let max_idx = cursor.bal_index.saturating_sub(1);
408                if let Some(&acct_idx) = cursor.index.addr_to_idx.get(&address) {
409                    Some(
410                        seed_one_address_info_from_bal(self, &cursor.bal, acct_idx, max_idx)
411                            .map(|_| true),
412                    )
413                } else {
414                    None
415                }
416            } else {
417                None
418            };
419            // Restore the cursor before propagating any error or returning.
420            self.lazy_bal = cursor_opt;
421            if let Some(result) = helper_result {
422                result.map_err(|e| InternalError::Custom(format!("lazy_bal seed: {e}")))?;
423                if self.current_accounts_state.contains_key(&address) {
424                    return self
425                        .current_accounts_state
426                        .get_mut(&address)
427                        .ok_or(InternalError::AccountNotFound);
428                }
429            }
430        }
431
432        // Check shared_base (read-only pre-block snapshot) before hitting store.
433        if let Some(ref base) = self.shared_base
434            && let Some(account) = base.get(&address)
435        {
436            let account = account.clone();
437            if !self.skip_initial_tracking {
438                self.initial_accounts_state.insert(address, account.clone());
439            }
440            return Ok(self
441                .current_accounts_state
442                .entry(address)
443                .or_insert(account));
444        }
445
446        // Store fallback.
447        let state = self.store.get_account_state(address)?;
448        let account = LevmAccount::from(state);
449        if !self.skip_initial_tracking {
450            self.initial_accounts_state.insert(address, account.clone());
451        }
452        Ok(self
453            .current_accounts_state
454            .entry(address)
455            .or_insert(account))
456    }
457
458    /// Gets reference of an account
459    pub fn get_account(&mut self, address: Address) -> Result<&LevmAccount, InternalError> {
460        Ok(self.load_account(address)?)
461    }
462
463    /// Gets mutable reference of an account
464    /// Warning: Use directly only if outside of the EVM, otherwise use `vm.get_account_mut` because it contemplates call frame backups.
465    pub fn get_account_mut(&mut self, address: Address) -> Result<&mut LevmAccount, InternalError> {
466        let acc = self.load_account(address)?;
467        acc.mark_modified();
468        Ok(acc)
469    }
470
471    /// Gets code immutably given the code hash.
472    /// Use this only inside of the VM, when we don't surely know if the code is in the cache or not
473    /// But e.g. in `get_state_transitions` just do `db.codes.get(code_hash)` because we know for sure code is there.
474    pub fn get_code(&mut self, code_hash: H256) -> Result<&Code, InternalError> {
475        match self.codes.entry(code_hash) {
476            Entry::Occupied(entry) => Ok(entry.into_mut()),
477            Entry::Vacant(entry) => {
478                let code = self.store.get_account_code(code_hash)?;
479                Ok(entry.insert(code))
480            }
481        }
482    }
483
484    /// Shortcut for getting the code when we only have the address of an account and we don't need anything else.
485    pub fn get_account_code(&mut self, address: Address) -> Result<&Code, InternalError> {
486        let code_hash = self.get_account(address)?.info.code_hash;
487        self.get_code(code_hash)
488    }
489
490    /// Gets code metadata immutably given the code hash.
491    pub fn get_code_metadata(&mut self, code_hash: H256) -> Result<&CodeMetadata, InternalError> {
492        match self.code_metadata.entry(code_hash) {
493            Entry::Occupied(entry) => Ok(entry.into_mut()),
494            Entry::Vacant(entry) => {
495                // First ensure code is loaded into cache by calling get_code
496                // This handles witness fallbacks and other code loading logic correctly
497                #[expect(clippy::as_conversions, reason = "same sized types (on 64bit)")]
498                let code_length = {
499                    // Note: `self.get_code(code_hash)` has been inlined due to mutability borrow issues.
500                    //   To avoid this inlinement, self.get_code has to be moved into `self.codes` so that it's called
501                    //   like this: `self.codes.get(code_hash)`.
502                    let code = match self.codes.entry(code_hash) {
503                        Entry::Occupied(entry) => entry.into_mut(),
504                        Entry::Vacant(entry) => {
505                            entry.insert(self.store.get_account_code(code_hash)?)
506                        }
507                    };
508
509                    code.len() as u64
510                };
511
512                let metadata = CodeMetadata {
513                    length: code_length,
514                };
515
516                // Insert into cache and return reference
517                Ok(entry.insert(metadata))
518            }
519        }
520    }
521
522    /// Convenience method to get code length by address (optimized for EXTCODESIZE).
523    pub fn get_code_length(&mut self, address: Address) -> Result<usize, InternalError> {
524        use ethrex_common::constants::EMPTY_KECCAK_HASH;
525
526        let code_hash = self.get_account(address)?.info.code_hash;
527        if code_hash == *EMPTY_KECCAK_HASH {
528            return Ok(0);
529        }
530        let metadata = self.get_code_metadata(code_hash)?;
531        #[expect(clippy::as_conversions, reason = "same sized types (on 64bit)")]
532        Ok(metadata.length as usize)
533    }
534
535    /// Gets storage slot from Database, storing in initial_accounts_state for efficiency when getting AccountUpdates.
536    fn get_value_from_database(
537        &mut self,
538        address: Address,
539        key: H256,
540    ) -> Result<U256, InternalError> {
541        let value = self.store.get_storage_value(address, key)?;
542        if self.skip_initial_tracking {
543            return Ok(value);
544        }
545        // Account must already be in initial_accounts_state
546        match self.initial_accounts_state.get_mut(&address) {
547            Some(account) => {
548                account.storage.insert(key, value);
549            }
550            None => {
551                // If we are fetching the storage of an account it means that we previously fetched the account from database before.
552                return Err(InternalError::msg(
553                    "Account not found in InMemoryDB when fetching storage",
554                ));
555            }
556        }
557        Ok(value)
558    }
559
560    /// Gets the transaction backup, if it exists.
561    /// It only works if the `BackupHook` was enabled during the transaction execution.
562    pub fn get_tx_backup(&self) -> Result<CallFrameBackup, InternalError> {
563        self.tx_backup.clone().ok_or_else(|| {
564            InternalError::Custom(
565                "Transaction backup not found. Was BackupHook enabled?".to_string(),
566            )
567        })
568    }
569
570    /// Undoes the last transaction by restoring the cache state to the state before the transaction.
571    pub fn undo_last_transaction(&mut self) -> Result<(), VMError> {
572        let tx_backup = self.get_tx_backup()?;
573        restore_cache_state(self, tx_backup)?;
574        Ok(())
575    }
576
577    pub fn get_state_transitions(&mut self) -> Result<Vec<AccountUpdate>, VMError> {
578        // Upper bound: `current_accounts_state` holds every *read* account, while the loop
579        // emits only *modified* ones, so on read-heavy blocks this over-reserves. Still a
580        // single non-reallocating alloc (never empty — the sender is always modified), which
581        // beats the repeated growth of starting from `vec![]`.
582        let mut account_updates: Vec<AccountUpdate> =
583            Vec::with_capacity(self.current_accounts_state.len());
584        for (address, new_state_account) in self.current_accounts_state.iter() {
585            if new_state_account.is_unmodified() {
586                // Skip processing account that we know wasn't mutably accessed during execution
587                continue;
588            }
589            // In case the account is not in immutable_cache (rare) we search for it in the actual database.
590            let initial_state_account =
591                self.initial_accounts_state.get(address).ok_or_else(|| {
592                    VMError::Internal(InternalError::Custom(format!(
593                        "Failed to get account {address} from immutable cache",
594                    )))
595                })?;
596
597            let mut acc_info_updated = false;
598            let mut storage_updated = false;
599
600            // 1. Account Info has been updated if balance, nonce or bytecode changed.
601            if initial_state_account.info.balance != new_state_account.info.balance {
602                acc_info_updated = true;
603            }
604
605            if initial_state_account.info.nonce != new_state_account.info.nonce {
606                acc_info_updated = true;
607            }
608
609            let code = if initial_state_account.info.code_hash != new_state_account.info.code_hash {
610                acc_info_updated = true;
611                // code should be in `codes`
612                Some(
613                    self.codes
614                        .get(&new_state_account.info.code_hash)
615                        .ok_or_else(|| {
616                            VMError::Internal(InternalError::Custom(format!(
617                                "Failed to get code for account {address}"
618                            )))
619                        })?,
620                )
621            } else {
622                None
623            };
624
625            // Account will have only its storage removed if it was Destroyed and then modified
626            // Edge cases that can make this true:
627            //   1. Account was destroyed and created again afterwards.
628            //   2. Account was destroyed but then was sent ETH, so it's not going to be completely removed from the trie.
629            let was_destroyed = new_state_account.status == AccountStatus::DestroyedModified;
630            // Only emit removed_storage if the account actually had storage in the trie.
631            // If it didn't (e.g. account was created within the batch), there's nothing to
632            // remove, and emitting removed_storage=true would cause a spurious empty
633            // account to be inserted into the state trie.
634            let removed_storage = was_destroyed && initial_state_account.has_storage;
635
636            // 2. Storage has been updated if the current value is different from the one before execution.
637            let mut added_storage: FxHashMap<_, _> = Default::default();
638
639            for (key, new_value) in &new_state_account.storage {
640                let old_value = if !was_destroyed {
641                    initial_state_account.storage.get(key).ok_or_else(|| { VMError::Internal(InternalError::Custom(format!("Failed to get old value from account's initial storage for address: {address:?}. For key: {key:?}")))})?
642                } else {
643                    // There's not an "old value" if the contract was destroyed and re-created.
644                    &ZERO_U256
645                };
646
647                if new_value != old_value {
648                    added_storage.insert(*key, *new_value);
649                    storage_updated = true;
650                }
651            }
652
653            let info = if acc_info_updated {
654                Some(new_state_account.info.clone())
655            } else {
656                None
657            };
658
659            // "At the end of the transaction, any account touched by the execution of that transaction which is now empty SHALL instead become non-existent (i.e. deleted)."
660            // ethrex is a post-Merge client, empty accounts have already been pruned from the trie on Mainnet by the Merge (see EIP-161), so we won't have any empty accounts in the trie.
661            let was_empty = initial_state_account.is_empty();
662            let removed = new_state_account.is_empty() && !was_empty;
663
664            if !removed && !acc_info_updated && !storage_updated && !removed_storage {
665                // Account hasn't been updated
666                continue;
667            }
668
669            let account_update = AccountUpdate {
670                address: *address,
671                removed,
672                info,
673                code: code.cloned(),
674                added_storage,
675                removed_storage,
676            };
677
678            account_updates.push(account_update);
679        }
680        self.initial_accounts_state.clear();
681        self.current_accounts_state.clear();
682        self.codes.clear();
683        self.code_metadata.clear();
684        Ok(account_updates)
685    }
686
687    pub fn get_state_transitions_tx(&mut self) -> Result<Vec<AccountUpdate>, VMError> {
688        // Exact upper bound: one update per modified account. Capture the length before draining.
689        let mut account_updates: Vec<AccountUpdate> =
690            Vec::with_capacity(self.current_accounts_state.len());
691        for (address, new_state_account) in self.current_accounts_state.drain() {
692            if new_state_account.is_unmodified() {
693                // Skip processing account that we know wasn't mutably accessed during execution
694                continue;
695            }
696            // [LIE] In case the account is not in immutable_cache (rare) we search for it in the actual database.
697            let initial_state_account =
698                self.initial_accounts_state.get(&address).ok_or_else(|| {
699                    VMError::Internal(InternalError::Custom(format!(
700                        "Failed to get account {address} from immutable cache",
701                    )))
702                })?;
703
704            let mut acc_info_updated = false;
705            let mut storage_updated = false;
706
707            // 1. Account Info has been updated if balance, nonce or bytecode changed.
708            if initial_state_account.info.balance != new_state_account.info.balance {
709                acc_info_updated = true;
710            }
711
712            if initial_state_account.info.nonce != new_state_account.info.nonce {
713                acc_info_updated = true;
714            }
715
716            let code = if initial_state_account.info.code_hash != new_state_account.info.code_hash {
717                acc_info_updated = true;
718                // code should be in `codes`
719                Some(
720                    self.codes
721                        .get(&new_state_account.info.code_hash)
722                        .cloned()
723                        .ok_or_else(|| {
724                            VMError::Internal(InternalError::Custom(format!(
725                                "Failed to get code for account {address}"
726                            )))
727                        })?,
728                )
729            } else {
730                None
731            };
732
733            // Account will have only its storage removed if it was Destroyed and then modified
734            // Edge cases that can make this true:
735            //   1. Account was destroyed and created again afterwards.
736            //   2. Account was destroyed but then was sent ETH, so it's not going to be completely removed from the trie.
737            let was_destroyed = new_state_account.status == AccountStatus::DestroyedModified;
738            // Only emit removed_storage if the account actually had storage in the trie.
739            // If it didn't (e.g. account was created within the batch), there's nothing to
740            // remove, and emitting removed_storage=true would cause a spurious empty
741            // account to be inserted into the state trie.
742            let removed_storage = was_destroyed && initial_state_account.has_storage;
743
744            // 2. Storage has been updated if the current value is different from the one before execution.
745            let mut added_storage: FxHashMap<_, _> = Default::default();
746
747            for (key, new_value) in &new_state_account.storage {
748                let old_value = if !was_destroyed {
749                    initial_state_account.storage.get(key).ok_or_else(|| { VMError::Internal(InternalError::Custom(format!("Failed to get old value from account's initial storage for address: {address}")))})?
750                } else {
751                    // There's not an "old value" if the contract was destroyed and re-created.
752                    &ZERO_U256
753                };
754
755                if new_value != old_value {
756                    added_storage.insert(*key, *new_value);
757                    storage_updated = true;
758                }
759            }
760
761            let info = acc_info_updated.then(|| new_state_account.info.clone());
762
763            // "At the end of the transaction, any account touched by the execution of that transaction which is now empty SHALL instead become non-existent (i.e. deleted)."
764            // ethrex is a post-Merge client, empty accounts have already been pruned from the trie on Mainnet by the Merge (see EIP-161), so we won't have any empty accounts in the trie.
765            let was_empty = initial_state_account.is_empty();
766            let removed = new_state_account.is_empty() && !was_empty;
767
768            if !removed && !acc_info_updated && !storage_updated && !removed_storage {
769                // Account hasn't been updated
770                continue;
771            }
772
773            // Fold this flush's committed state into the diff baseline. With the info-only clone
774            // in `load_account`, `new_state_account.storage` holds only the slots touched this
775            // batch, so we MERGE them into the existing baseline instead of replacing it: a plain
776            // replace would drop the committed values of slots written in an earlier batch but not
777            // re-touched here (the old full-storage clone preserved them implicitly, which is why
778            // a replace was correct before). On destroy the prior storage is invalid, so the
779            // drained account is authoritative and replaces it wholesale (old behavior).
780            match self.initial_accounts_state.get_mut(&address) {
781                Some(initial_account)
782                    if !matches!(
783                        new_state_account.status,
784                        AccountStatus::Destroyed | AccountStatus::DestroyedModified
785                    ) =>
786                {
787                    // Move each field out of the about-to-be-dropped drained account.
788                    initial_account.info = new_state_account.info;
789                    initial_account.status = new_state_account.status;
790                    initial_account.has_storage = new_state_account.has_storage;
791                    initial_account.exists = new_state_account.exists;
792                    // `extend` overwrites touched slots with their committed value and keeps
793                    // untouched baseline slots from earlier batches.
794                    initial_account.storage.extend(new_state_account.storage);
795                }
796                _ => {
797                    self.initial_accounts_state
798                        .insert(address, new_state_account);
799                }
800            }
801
802            let account_update = AccountUpdate {
803                address,
804                removed,
805                info,
806                code,
807                added_storage,
808                removed_storage,
809            };
810
811            account_updates.push(account_update);
812        }
813        Ok(account_updates)
814    }
815}
816
817impl<'a> VM<'a> {
818    // ================== Account related functions =====================
819
820    /*
821        Each callframe has a CallFrameBackup, which contains:
822
823        - A list with account infos of every account that was modified so far (balance, nonce, bytecode/code hash)
824        - A list with a tuple (address, storage) that contains, for every account whose storage was accessed, a hashmap
825        of the storage slots that were modified, with their original value.
826
827        On every call frame, at the end one of two things can happen:
828
829        - The transaction succeeds. In this case:
830            - The CallFrameBackup of the current callframe has to be merged with the backup of its parent, in the following way:
831            For every account that's present in the parent backup, do nothing (i.e. keep the one that's already there).
832            For every account that's NOT present in the parent backup but is on the child backup, add the child backup to it.
833            Do the same for every individual storage slot.
834        - The transaction reverts. In this case:
835            - Insert into the cache the value of every account on the CallFrameBackup.
836            - Insert into the cache the value of every storage slot in every account on the CallFrameBackup.
837
838    */
839    pub fn get_account_mut(&mut self, address: Address) -> Result<&mut LevmAccount, InternalError> {
840        // Backup must be taken before mark_modified flips `exists` to true.
841        let account = self.db.get_account(address)?;
842        self.current_call_frame
843            .call_frame_backup
844            .backup_account_info(address, account)?;
845
846        let account = self.db.get_account_mut(address)?;
847        Ok(account)
848    }
849
850    pub fn increase_account_balance(
851        &mut self,
852        address: Address,
853        increase: U256,
854    ) -> Result<(), InternalError> {
855        if increase.is_zero() {
856            return Ok(());
857        }
858        let account = self.get_account_mut(address)?;
859
860        // Get initial balance BEFORE modification (avoids duplicate lookup)
861        let initial_balance = account.info.balance;
862
863        // Modify balance
864        account.info.balance = account
865            .info
866            .balance
867            .checked_add(increase)
868            .ok_or(InternalError::Overflow)?;
869        let new_balance = account.info.balance;
870
871        // Record initial and changed balance for BAL
872        if let Some(recorder) = self.db.bal_recorder.as_mut() {
873            recorder.set_initial_balance(address, initial_balance);
874            recorder.record_balance_change(address, new_balance);
875        }
876
877        Ok(())
878    }
879
880    pub fn decrease_account_balance(
881        &mut self,
882        address: Address,
883        decrease: U256,
884    ) -> Result<(), InternalError> {
885        if decrease.is_zero() {
886            return Ok(());
887        }
888        let account = self.get_account_mut(address)?;
889
890        // Get initial balance BEFORE modification (avoids duplicate lookup)
891        let initial_balance = account.info.balance;
892
893        // Modify balance
894        account.info.balance = account
895            .info
896            .balance
897            .checked_sub(decrease)
898            .ok_or(InternalError::Underflow)?;
899        let new_balance = account.info.balance;
900
901        // Record initial and changed balance for BAL
902        if let Some(recorder) = self.db.bal_recorder.as_mut() {
903            recorder.set_initial_balance(address, initial_balance);
904            recorder.record_balance_change(address, new_balance);
905        }
906
907        Ok(())
908    }
909
910    pub fn transfer(
911        &mut self,
912        from: Address,
913        to: Address,
914        value: U256,
915    ) -> Result<(), InternalError> {
916        if value != U256::zero() {
917            self.decrease_account_balance(from, value)?;
918            self.increase_account_balance(to, value)?;
919        }
920
921        Ok(())
922    }
923
924    /// Updates bytecode of given account.
925    pub fn update_account_bytecode(
926        &mut self,
927        address: Address,
928        new_bytecode: Code,
929    ) -> Result<(), InternalError> {
930        // Record code change for BAL
931        if let Some(recorder) = self.db.bal_recorder.as_mut() {
932            // Capture initial code BEFORE recording the change.
933            // This is needed for:
934            // 1. Distinguishing CREATE empty code vs delegation clear
935            // 2. Net-zero code change detection (e.g., delegate then reset in same tx)
936            let current_code_bytes = self
937                .db
938                .current_accounts_state
939                .get(&address)
940                .and_then(|account| self.db.codes.get(&account.info.code_hash))
941                .map(|c| c.code_bytes())
942                .unwrap_or_default();
943            let has_code = !current_code_bytes.is_empty();
944            recorder.capture_initial_code_presence(address, has_code);
945            recorder.set_initial_code(address, current_code_bytes);
946            recorder.record_code_change(address, new_bytecode.code_bytes());
947        }
948
949        let acc = self.get_account_mut(address)?;
950        let code_hash = new_bytecode.hash;
951        acc.info.code_hash = new_bytecode.hash;
952        if let Entry::Vacant(entry) = self.db.codes.entry(code_hash) {
953            entry.insert(new_bytecode);
954            // Track the insertion so a frame revert evicts it: a stale entry
955            // would serve a later read of the same hash from the cache,
956            // hiding the store read from execution-witness recording.
957            self.current_call_frame
958                .call_frame_backup
959                .inserted_code_hashes
960                .push(code_hash);
961        }
962        Ok(())
963    }
964
965    // =================== Nonce related functions ======================
966    /// Increments the nonce of the given account.
967    /// Per EIP-7928, nonce changes are recorded for:
968    /// - EOA senders
969    /// - Contracts performing CREATE/CREATE2
970    /// - Deployed contracts
971    /// - EIP-7702 authorities
972    pub fn increment_account_nonce(&mut self, address: Address) -> Result<u64, InternalError> {
973        let account = self.get_account_mut(address)?;
974        account.info.nonce = account
975            .info
976            .nonce
977            .checked_add(1)
978            .ok_or(InternalError::Overflow)?;
979        let new_nonce = account.info.nonce;
980
981        // Record nonce change for BAL
982        if let Some(recorder) = self.db.bal_recorder.as_mut() {
983            recorder.record_nonce_change(address, new_nonce);
984        }
985
986        Ok(new_nonce)
987    }
988
989    /// SSTORE-specialized storage access path that returns current and original values together.
990    /// This keeps the SSTORE hot path tighter by avoiding extra method-level plumbing.
991    #[inline(always)]
992    pub fn access_storage_slot_for_sstore(
993        &mut self,
994        address: Address,
995        key: H256,
996    ) -> Result<(U256, U256, bool), InternalError> {
997        let storage_slot_was_cold = self.substate.add_accessed_slot(address, key);
998        // SSTORE pre-image flows transitively through get_storage_value, which consults lazy_bal.
999        let current_value = self.get_storage_value(address, key)?;
1000        let original_value = match self
1001            .storage_original_values
1002            .entry(address)
1003            .or_default()
1004            .entry(key)
1005        {
1006            Entry::Occupied(entry) => *entry.get(),
1007            Entry::Vacant(entry) => *entry.insert(current_value),
1008        };
1009        Ok((current_value, original_value, storage_slot_was_cold))
1010    }
1011
1012    /// Records a storage slot read to BAL after gas checks have passed.
1013    /// Per EIP-7928: "If pre-state validation fails, the target is never accessed and must not appear in BAL."
1014    /// This function should be called AFTER the gas check succeeds.
1015    pub fn record_storage_slot_to_bal(&mut self, address: Address, key: U256) {
1016        if let Some(recorder) = self.db.bal_recorder.as_mut() {
1017            recorder.record_storage_read(address, key);
1018        }
1019    }
1020
1021    /// Gets storage value of an account, caching it if not already cached.
1022    #[inline(always)]
1023    pub fn get_storage_value(
1024        &mut self,
1025        address: Address,
1026        key: H256,
1027    ) -> Result<U256, InternalError> {
1028        if let Some(account) = self.db.current_accounts_state.get(&address) {
1029            if let Some(value) = account.storage.get(&key) {
1030                return Ok(*value);
1031            }
1032            // If the account was destroyed and then created then we cannot rely on the DB to obtain storage values
1033            if account.status == AccountStatus::DestroyedModified {
1034                return Ok(U256::zero());
1035            }
1036        } else {
1037            // When requesting storage of an account we should've previously requested and cached the account
1038            return Err(InternalError::AccountNotFound);
1039        }
1040
1041        // Lazy-BAL hook: copy result out BEFORE taking &mut on current_accounts_state
1042        // so the immutable borrow of lazy_bal is released before the mutable reborrow.
1043        #[cfg(all(feature = "rayon", not(feature = "eip-8025")))]
1044        let bal_hit: Option<U256> = self.db.lazy_bal.as_ref().and_then(|cursor| {
1045            debug_assert!(
1046                cursor.bal_index >= 1,
1047                "LazyBalCursor bal_index must be >= 1"
1048            );
1049            let max_idx = cursor.bal_index.saturating_sub(1);
1050            let &acct_idx = cursor.index.addr_to_idx.get(&address)?;
1051            seed_one_storage_slot_from_bal(&cursor.bal, &cursor.index, acct_idx, key, max_idx)
1052        });
1053        #[cfg(all(feature = "rayon", not(feature = "eip-8025")))]
1054        if let Some(value) = bal_hit {
1055            let account = self
1056                .db
1057                .current_accounts_state
1058                .get_mut(&address)
1059                .ok_or(InternalError::AccountNotFound)?;
1060            account.storage.insert(key, value);
1061            return Ok(value);
1062        }
1063
1064        // Resolve against `initial_accounts_state` before the store. With the info-only clone in
1065        // `load_account`, `current.storage` starts empty on a re-fault, but `initial` holds this
1066        // slot's committed in-block value (from the per-flush drain-back) — whereas `self.store`
1067        // only has the stale pre-block value (in-block writes go to the merkleizer, never back to
1068        // the store). Reading `initial` first returns the committed value and keeps the slot in
1069        // `initial` (the diff baseline), so the invariant "every key in `current.storage` is also
1070        // in `initial.storage`" is preserved. `.copied()` releases the immutable borrow before
1071        // the mutable one below.
1072        if let Some(value) = self
1073            .db
1074            .initial_accounts_state
1075            .get(&address)
1076            .and_then(|account| account.storage.get(&key))
1077            .copied()
1078        {
1079            let account = self
1080                .db
1081                .current_accounts_state
1082                .get_mut(&address)
1083                .ok_or(InternalError::AccountNotFound)?;
1084            account.storage.insert(key, value);
1085            return Ok(value);
1086        }
1087
1088        let value = self.db.get_value_from_database(address, key)?;
1089
1090        // Cache-fill only: this is a read-path miss, not a state mutation.
1091        let account = self
1092            .db
1093            .current_accounts_state
1094            .get_mut(&address)
1095            .ok_or(InternalError::AccountNotFound)?;
1096        account.storage.insert(key, value);
1097
1098        Ok(value)
1099    }
1100
1101    /// Updates storage of an account, caching it if not already cached.
1102    pub fn update_account_storage(
1103        &mut self,
1104        address: Address,
1105        key: H256,
1106        slot_key: U256,
1107        new_value: U256,
1108        current_value: U256,
1109    ) -> Result<(), InternalError> {
1110        self.backup_storage_slot(address, key, current_value)?;
1111
1112        // Record storage change for BAL (EIP-7928).
1113        // SSTORE that changes the value (new != current) → storage write.
1114        // SSTORE with same value (new == current) → storage read (no actual mutation).
1115        if let Some(recorder) = self.db.bal_recorder.as_mut() {
1116            if new_value != current_value {
1117                // Record original value before first write. If final value equals original
1118                // after all tx operations, the slot becomes a read per EIP-7928 net-zero filtering.
1119                // This captures the value BEFORE the first write in this transaction
1120                recorder.capture_pre_storage(address, slot_key, current_value);
1121                // Actual write
1122                recorder.record_storage_write(address, slot_key, new_value);
1123            } else {
1124                // No-op write (post == pre) - record as read per EIP-7928
1125                recorder.record_storage_read(address, slot_key);
1126            }
1127        }
1128
1129        let account = self.get_account_mut(address)?;
1130        account.storage.insert(key, new_value);
1131        Ok(())
1132    }
1133
1134    pub fn backup_storage_slot(
1135        &mut self,
1136        address: Address,
1137        key: H256,
1138        current_value: U256,
1139    ) -> Result<(), InternalError> {
1140        self.current_call_frame
1141            .call_frame_backup
1142            .original_account_storage_slots
1143            .entry(address)
1144            .or_default()
1145            .entry(key)
1146            .or_insert(current_value);
1147
1148        Ok(())
1149    }
1150}