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(feature = "rayon")]
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(feature = "rayon")]
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(feature = "rayon")]
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(feature = "rayon")]
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(feature = "rayon")]
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(feature = "rayon")]
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                // Answer from the size-only store lookup rather than the bytecode: a
496                // contract's code is up to 24KB and lives out-of-line, so materializing it
497                // to return a length is the dominant cost of EXTCODESIZE. Code already
498                // loaded for another reason answers for free.
499                #[expect(clippy::as_conversions, reason = "same sized types (on 64bit)")]
500                let length = match self.codes.get(&code_hash) {
501                    Some(code) => code.len() as u64,
502                    None => self.store.get_code_metadata(code_hash)?.length,
503                };
504
505                // Insert into cache and return reference
506                Ok(entry.insert(CodeMetadata { length }))
507            }
508        }
509    }
510
511    /// Convenience method to get code length by address (optimized for EXTCODESIZE).
512    pub fn get_code_length(&mut self, address: Address) -> Result<usize, InternalError> {
513        use ethrex_common::constants::EMPTY_KECCAK_HASH;
514
515        let code_hash = self.get_account(address)?.info.code_hash;
516        if code_hash == *EMPTY_KECCAK_HASH {
517            return Ok(0);
518        }
519        let metadata = self.get_code_metadata(code_hash)?;
520        #[expect(clippy::as_conversions, reason = "same sized types (on 64bit)")]
521        Ok(metadata.length as usize)
522    }
523
524    /// Gets storage slot from Database, storing in initial_accounts_state for efficiency when getting AccountUpdates.
525    fn get_value_from_database(
526        &mut self,
527        address: Address,
528        key: H256,
529    ) -> Result<U256, InternalError> {
530        let value = self.store.get_storage_value(address, key)?;
531        if self.skip_initial_tracking {
532            return Ok(value);
533        }
534        // Account must already be in initial_accounts_state
535        match self.initial_accounts_state.get_mut(&address) {
536            Some(account) => {
537                account.storage.insert(key, value);
538            }
539            None => {
540                // If we are fetching the storage of an account it means that we previously fetched the account from database before.
541                return Err(InternalError::msg(
542                    "Account not found in InMemoryDB when fetching storage",
543                ));
544            }
545        }
546        Ok(value)
547    }
548
549    /// Gets the transaction backup, if it exists.
550    /// It only works if the `BackupHook` was enabled during the transaction execution.
551    pub fn get_tx_backup(&self) -> Result<CallFrameBackup, InternalError> {
552        self.tx_backup.clone().ok_or_else(|| {
553            InternalError::Custom(
554                "Transaction backup not found. Was BackupHook enabled?".to_string(),
555            )
556        })
557    }
558
559    /// Undoes the last transaction by restoring the cache state to the state before the transaction.
560    pub fn undo_last_transaction(&mut self) -> Result<(), VMError> {
561        let tx_backup = self.get_tx_backup()?;
562        restore_cache_state(self, tx_backup)?;
563        Ok(())
564    }
565
566    pub fn get_state_transitions(&mut self) -> Result<Vec<AccountUpdate>, VMError> {
567        // Upper bound: `current_accounts_state` holds every *read* account, while the loop
568        // emits only *modified* ones, so on read-heavy blocks this over-reserves. Still a
569        // single non-reallocating alloc (never empty — the sender is always modified), which
570        // beats the repeated growth of starting from `vec![]`.
571        let mut account_updates: Vec<AccountUpdate> =
572            Vec::with_capacity(self.current_accounts_state.len());
573        for (address, new_state_account) in self.current_accounts_state.iter() {
574            if new_state_account.is_unmodified() {
575                // Skip processing account that we know wasn't mutably accessed during execution
576                continue;
577            }
578            // In case the account is not in immutable_cache (rare) we search for it in the actual database.
579            let initial_state_account =
580                self.initial_accounts_state.get(address).ok_or_else(|| {
581                    VMError::Internal(InternalError::Custom(format!(
582                        "Failed to get account {address} from immutable cache",
583                    )))
584                })?;
585
586            let mut acc_info_updated = false;
587            let mut storage_updated = false;
588
589            // 1. Account Info has been updated if balance, nonce or bytecode changed.
590            if initial_state_account.info.balance != new_state_account.info.balance {
591                acc_info_updated = true;
592            }
593
594            if initial_state_account.info.nonce != new_state_account.info.nonce {
595                acc_info_updated = true;
596            }
597
598            let code = if initial_state_account.info.code_hash != new_state_account.info.code_hash {
599                acc_info_updated = true;
600                // code should be in `codes`
601                Some(
602                    self.codes
603                        .get(&new_state_account.info.code_hash)
604                        .ok_or_else(|| {
605                            VMError::Internal(InternalError::Custom(format!(
606                                "Failed to get code for account {address}"
607                            )))
608                        })?,
609                )
610            } else {
611                None
612            };
613
614            // Account will have only its storage removed if it was Destroyed and then modified
615            // Edge cases that can make this true:
616            //   1. Account was destroyed and created again afterwards.
617            //   2. Account was destroyed but then was sent ETH, so it's not going to be completely removed from the trie.
618            let was_destroyed = new_state_account.status == AccountStatus::DestroyedModified;
619            // Only emit removed_storage if the account actually had storage in the trie.
620            // If it didn't (e.g. account was created within the batch), there's nothing to
621            // remove, and emitting removed_storage=true would cause a spurious empty
622            // account to be inserted into the state trie.
623            let removed_storage = was_destroyed && initial_state_account.has_storage;
624
625            // 2. Storage has been updated if the current value is different from the one before execution.
626            let mut added_storage: FxHashMap<_, _> = Default::default();
627
628            for (key, new_value) in &new_state_account.storage {
629                let old_value = if !was_destroyed {
630                    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:?}")))})?
631                } else {
632                    // There's not an "old value" if the contract was destroyed and re-created.
633                    &ZERO_U256
634                };
635
636                if new_value != old_value {
637                    added_storage.insert(*key, *new_value);
638                    storage_updated = true;
639                }
640            }
641
642            let info = if acc_info_updated {
643                Some(new_state_account.info.clone())
644            } else {
645                None
646            };
647
648            // "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)."
649            // 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.
650            let was_empty = initial_state_account.is_empty();
651            let removed = new_state_account.is_empty() && !was_empty;
652
653            if !removed && !acc_info_updated && !storage_updated && !removed_storage {
654                // Account hasn't been updated
655                continue;
656            }
657
658            let account_update = AccountUpdate {
659                address: *address,
660                removed,
661                info,
662                code: code.cloned(),
663                added_storage,
664                removed_storage,
665            };
666
667            account_updates.push(account_update);
668        }
669        self.initial_accounts_state.clear();
670        self.current_accounts_state.clear();
671        self.codes.clear();
672        self.code_metadata.clear();
673        Ok(account_updates)
674    }
675
676    pub fn get_state_transitions_tx(&mut self) -> Result<Vec<AccountUpdate>, VMError> {
677        // Exact upper bound: one update per modified account. Capture the length before draining.
678        let mut account_updates: Vec<AccountUpdate> =
679            Vec::with_capacity(self.current_accounts_state.len());
680        for (address, new_state_account) in self.current_accounts_state.drain() {
681            if new_state_account.is_unmodified() {
682                // Skip processing account that we know wasn't mutably accessed during execution
683                continue;
684            }
685            // [LIE] In case the account is not in immutable_cache (rare) we search for it in the actual database.
686            let initial_state_account =
687                self.initial_accounts_state.get(&address).ok_or_else(|| {
688                    VMError::Internal(InternalError::Custom(format!(
689                        "Failed to get account {address} from immutable cache",
690                    )))
691                })?;
692
693            let mut acc_info_updated = false;
694            let mut storage_updated = false;
695
696            // 1. Account Info has been updated if balance, nonce or bytecode changed.
697            if initial_state_account.info.balance != new_state_account.info.balance {
698                acc_info_updated = true;
699            }
700
701            if initial_state_account.info.nonce != new_state_account.info.nonce {
702                acc_info_updated = true;
703            }
704
705            let code = if initial_state_account.info.code_hash != new_state_account.info.code_hash {
706                acc_info_updated = true;
707                // code should be in `codes`
708                Some(
709                    self.codes
710                        .get(&new_state_account.info.code_hash)
711                        .cloned()
712                        .ok_or_else(|| {
713                            VMError::Internal(InternalError::Custom(format!(
714                                "Failed to get code for account {address}"
715                            )))
716                        })?,
717                )
718            } else {
719                None
720            };
721
722            // Account will have only its storage removed if it was Destroyed and then modified
723            // Edge cases that can make this true:
724            //   1. Account was destroyed and created again afterwards.
725            //   2. Account was destroyed but then was sent ETH, so it's not going to be completely removed from the trie.
726            let was_destroyed = new_state_account.status == AccountStatus::DestroyedModified;
727            // Only emit removed_storage if the account actually had storage in the trie.
728            // If it didn't (e.g. account was created within the batch), there's nothing to
729            // remove, and emitting removed_storage=true would cause a spurious empty
730            // account to be inserted into the state trie.
731            let removed_storage = was_destroyed && initial_state_account.has_storage;
732
733            // 2. Storage has been updated if the current value is different from the one before execution.
734            let mut added_storage: FxHashMap<_, _> = Default::default();
735
736            for (key, new_value) in &new_state_account.storage {
737                let old_value = if !was_destroyed {
738                    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}")))})?
739                } else {
740                    // There's not an "old value" if the contract was destroyed and re-created.
741                    &ZERO_U256
742                };
743
744                if new_value != old_value {
745                    added_storage.insert(*key, *new_value);
746                    storage_updated = true;
747                }
748            }
749
750            let info = acc_info_updated.then(|| new_state_account.info.clone());
751
752            // "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)."
753            // 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.
754            let was_empty = initial_state_account.is_empty();
755            let removed = new_state_account.is_empty() && !was_empty;
756
757            if !removed && !acc_info_updated && !storage_updated && !removed_storage {
758                // Account hasn't been updated
759                continue;
760            }
761
762            // Fold this flush's committed state into the diff baseline. With the info-only clone
763            // in `load_account`, `new_state_account.storage` holds only the slots touched this
764            // batch, so we MERGE them into the existing baseline instead of replacing it: a plain
765            // replace would drop the committed values of slots written in an earlier batch but not
766            // re-touched here (the old full-storage clone preserved them implicitly, which is why
767            // a replace was correct before). On destroy the prior storage is invalid, so the
768            // drained account is authoritative and replaces it wholesale (old behavior).
769            match self.initial_accounts_state.get_mut(&address) {
770                Some(initial_account)
771                    if !matches!(
772                        new_state_account.status,
773                        AccountStatus::Destroyed | AccountStatus::DestroyedModified
774                    ) =>
775                {
776                    // Move each field out of the about-to-be-dropped drained account.
777                    initial_account.info = new_state_account.info;
778                    initial_account.status = new_state_account.status;
779                    initial_account.has_storage = new_state_account.has_storage;
780                    initial_account.exists = new_state_account.exists;
781                    // `extend` overwrites touched slots with their committed value and keeps
782                    // untouched baseline slots from earlier batches.
783                    initial_account.storage.extend(new_state_account.storage);
784                }
785                _ => {
786                    self.initial_accounts_state
787                        .insert(address, new_state_account);
788                }
789            }
790
791            let account_update = AccountUpdate {
792                address,
793                removed,
794                info,
795                code,
796                added_storage,
797                removed_storage,
798            };
799
800            account_updates.push(account_update);
801        }
802        Ok(account_updates)
803    }
804}
805
806impl<'a> VM<'a> {
807    // ================== Account related functions =====================
808
809    /*
810        Each callframe has a CallFrameBackup, which contains:
811
812        - A list with account infos of every account that was modified so far (balance, nonce, bytecode/code hash)
813        - A list with a tuple (address, storage) that contains, for every account whose storage was accessed, a hashmap
814        of the storage slots that were modified, with their original value.
815
816        On every call frame, at the end one of two things can happen:
817
818        - The transaction succeeds. In this case:
819            - The CallFrameBackup of the current callframe has to be merged with the backup of its parent, in the following way:
820            For every account that's present in the parent backup, do nothing (i.e. keep the one that's already there).
821            For every account that's NOT present in the parent backup but is on the child backup, add the child backup to it.
822            Do the same for every individual storage slot.
823        - The transaction reverts. In this case:
824            - Insert into the cache the value of every account on the CallFrameBackup.
825            - Insert into the cache the value of every storage slot in every account on the CallFrameBackup.
826
827    */
828    pub fn get_account_mut(&mut self, address: Address) -> Result<&mut LevmAccount, InternalError> {
829        // Backup must be taken before mark_modified flips `exists` to true.
830        let account = self.db.get_account(address)?;
831        self.current_call_frame
832            .call_frame_backup
833            .backup_account_info(address, account)?;
834
835        let account = self.db.get_account_mut(address)?;
836        Ok(account)
837    }
838
839    pub fn increase_account_balance(
840        &mut self,
841        address: Address,
842        increase: U256,
843    ) -> Result<(), InternalError> {
844        if increase.is_zero() {
845            return Ok(());
846        }
847        let account = self.get_account_mut(address)?;
848
849        // Get initial balance BEFORE modification (avoids duplicate lookup)
850        let initial_balance = account.info.balance;
851
852        // Modify balance
853        account.info.balance = account
854            .info
855            .balance
856            .checked_add(increase)
857            .ok_or(InternalError::Overflow)?;
858        let new_balance = account.info.balance;
859
860        // Record initial and changed balance for BAL
861        if let Some(recorder) = self.db.bal_recorder.as_mut() {
862            recorder.set_initial_balance(address, initial_balance);
863            recorder.record_balance_change(address, new_balance);
864        }
865
866        Ok(())
867    }
868
869    pub fn decrease_account_balance(
870        &mut self,
871        address: Address,
872        decrease: U256,
873    ) -> Result<(), InternalError> {
874        if decrease.is_zero() {
875            return Ok(());
876        }
877        let account = self.get_account_mut(address)?;
878
879        // Get initial balance BEFORE modification (avoids duplicate lookup)
880        let initial_balance = account.info.balance;
881
882        // Modify balance
883        account.info.balance = account
884            .info
885            .balance
886            .checked_sub(decrease)
887            .ok_or(InternalError::Underflow)?;
888        let new_balance = account.info.balance;
889
890        // Record initial and changed balance for BAL
891        if let Some(recorder) = self.db.bal_recorder.as_mut() {
892            recorder.set_initial_balance(address, initial_balance);
893            recorder.record_balance_change(address, new_balance);
894        }
895
896        Ok(())
897    }
898
899    pub fn transfer(
900        &mut self,
901        from: Address,
902        to: Address,
903        value: U256,
904    ) -> Result<(), InternalError> {
905        if value != U256::zero() {
906            self.decrease_account_balance(from, value)?;
907            self.increase_account_balance(to, value)?;
908        }
909
910        Ok(())
911    }
912
913    /// Updates bytecode of given account.
914    pub fn update_account_bytecode(
915        &mut self,
916        address: Address,
917        new_bytecode: Code,
918    ) -> Result<(), InternalError> {
919        // Record code change for BAL
920        if let Some(recorder) = self.db.bal_recorder.as_mut() {
921            // Capture initial code BEFORE recording the change.
922            // This is needed for:
923            // 1. Distinguishing CREATE empty code vs delegation clear
924            // 2. Net-zero code change detection (e.g., delegate then reset in same tx)
925            let current_code_bytes = self
926                .db
927                .current_accounts_state
928                .get(&address)
929                .and_then(|account| self.db.codes.get(&account.info.code_hash))
930                .map(|c| c.code_bytes())
931                .unwrap_or_default();
932            let has_code = !current_code_bytes.is_empty();
933            recorder.capture_initial_code_presence(address, has_code);
934            recorder.set_initial_code(address, current_code_bytes);
935            recorder.record_code_change(address, new_bytecode.code_bytes());
936        }
937
938        let acc = self.get_account_mut(address)?;
939        let code_hash = new_bytecode.hash;
940        acc.info.code_hash = new_bytecode.hash;
941        if let Entry::Vacant(entry) = self.db.codes.entry(code_hash) {
942            entry.insert(new_bytecode);
943            // Track the insertion so a frame revert evicts it: a stale entry
944            // would serve a later read of the same hash from the cache,
945            // hiding the store read from execution-witness recording.
946            self.current_call_frame
947                .call_frame_backup
948                .inserted_code_hashes
949                .push(code_hash);
950        }
951        Ok(())
952    }
953
954    // =================== Nonce related functions ======================
955    /// Increments the nonce of the given account.
956    /// Per EIP-7928, nonce changes are recorded for:
957    /// - EOA senders
958    /// - Contracts performing CREATE/CREATE2
959    /// - Deployed contracts
960    /// - EIP-7702 authorities
961    pub fn increment_account_nonce(&mut self, address: Address) -> Result<u64, InternalError> {
962        let account = self.get_account_mut(address)?;
963        account.info.nonce = account
964            .info
965            .nonce
966            .checked_add(1)
967            .ok_or(InternalError::Overflow)?;
968        let new_nonce = account.info.nonce;
969
970        // Record nonce change for BAL
971        if let Some(recorder) = self.db.bal_recorder.as_mut() {
972            recorder.record_nonce_change(address, new_nonce);
973        }
974
975        Ok(new_nonce)
976    }
977
978    /// SSTORE-specialized storage access path that returns current and original values together.
979    /// This keeps the SSTORE hot path tighter by avoiding extra method-level plumbing.
980    #[inline(always)]
981    pub fn access_storage_slot_for_sstore(
982        &mut self,
983        address: Address,
984        key: H256,
985    ) -> Result<(U256, U256, bool), InternalError> {
986        let storage_slot_was_cold = self.substate.add_accessed_slot(address, key);
987        // SSTORE pre-image flows transitively through get_storage_value, which consults lazy_bal.
988        let current_value = self.get_storage_value(address, key)?;
989        let original_value = match self
990            .storage_original_values
991            .entry(address)
992            .or_default()
993            .entry(key)
994        {
995            Entry::Occupied(entry) => *entry.get(),
996            Entry::Vacant(entry) => *entry.insert(current_value),
997        };
998        Ok((current_value, original_value, storage_slot_was_cold))
999    }
1000
1001    /// Records a storage slot read to BAL after gas checks have passed.
1002    /// Per EIP-7928: "If pre-state validation fails, the target is never accessed and must not appear in BAL."
1003    /// This function should be called AFTER the gas check succeeds.
1004    pub fn record_storage_slot_to_bal(&mut self, address: Address, key: U256) {
1005        if let Some(recorder) = self.db.bal_recorder.as_mut() {
1006            recorder.record_storage_read(address, key);
1007        }
1008    }
1009
1010    /// Gets storage value of an account, caching it if not already cached.
1011    #[inline(always)]
1012    pub fn get_storage_value(
1013        &mut self,
1014        address: Address,
1015        key: H256,
1016    ) -> Result<U256, InternalError> {
1017        if let Some(account) = self.db.current_accounts_state.get(&address) {
1018            if let Some(value) = account.storage.get(&key) {
1019                return Ok(*value);
1020            }
1021            // If the account was destroyed and then created then we cannot rely on the DB to obtain storage values
1022            if account.status == AccountStatus::DestroyedModified {
1023                return Ok(U256::zero());
1024            }
1025        } else {
1026            // When requesting storage of an account we should've previously requested and cached the account
1027            return Err(InternalError::AccountNotFound);
1028        }
1029
1030        // Lazy-BAL hook: copy result out BEFORE taking &mut on current_accounts_state
1031        // so the immutable borrow of lazy_bal is released before the mutable reborrow.
1032        #[cfg(feature = "rayon")]
1033        let bal_hit: Option<U256> = self.db.lazy_bal.as_ref().and_then(|cursor| {
1034            debug_assert!(
1035                cursor.bal_index >= 1,
1036                "LazyBalCursor bal_index must be >= 1"
1037            );
1038            let max_idx = cursor.bal_index.saturating_sub(1);
1039            let &acct_idx = cursor.index.addr_to_idx.get(&address)?;
1040            seed_one_storage_slot_from_bal(&cursor.bal, &cursor.index, acct_idx, key, max_idx)
1041        });
1042        #[cfg(feature = "rayon")]
1043        if let Some(value) = bal_hit {
1044            let account = self
1045                .db
1046                .current_accounts_state
1047                .get_mut(&address)
1048                .ok_or(InternalError::AccountNotFound)?;
1049            account.storage.insert(key, value);
1050            return Ok(value);
1051        }
1052
1053        // Resolve against `initial_accounts_state` before the store. With the info-only clone in
1054        // `load_account`, `current.storage` starts empty on a re-fault, but `initial` holds this
1055        // slot's committed in-block value (from the per-flush drain-back) — whereas `self.store`
1056        // only has the stale pre-block value (in-block writes go to the merkleizer, never back to
1057        // the store). Reading `initial` first returns the committed value and keeps the slot in
1058        // `initial` (the diff baseline), so the invariant "every key in `current.storage` is also
1059        // in `initial.storage`" is preserved. `.copied()` releases the immutable borrow before
1060        // the mutable one below.
1061        if let Some(value) = self
1062            .db
1063            .initial_accounts_state
1064            .get(&address)
1065            .and_then(|account| account.storage.get(&key))
1066            .copied()
1067        {
1068            let account = self
1069                .db
1070                .current_accounts_state
1071                .get_mut(&address)
1072                .ok_or(InternalError::AccountNotFound)?;
1073            account.storage.insert(key, value);
1074            return Ok(value);
1075        }
1076
1077        let value = self.db.get_value_from_database(address, key)?;
1078
1079        // Cache-fill only: this is a read-path miss, not a state mutation.
1080        let account = self
1081            .db
1082            .current_accounts_state
1083            .get_mut(&address)
1084            .ok_or(InternalError::AccountNotFound)?;
1085        account.storage.insert(key, value);
1086
1087        Ok(value)
1088    }
1089
1090    /// Updates storage of an account, caching it if not already cached.
1091    pub fn update_account_storage(
1092        &mut self,
1093        address: Address,
1094        key: H256,
1095        slot_key: U256,
1096        new_value: U256,
1097        current_value: U256,
1098    ) -> Result<(), InternalError> {
1099        self.backup_storage_slot(address, key, current_value)?;
1100
1101        // Record storage change for BAL (EIP-7928).
1102        // SSTORE that changes the value (new != current) → storage write.
1103        // SSTORE with same value (new == current) → storage read (no actual mutation).
1104        if let Some(recorder) = self.db.bal_recorder.as_mut() {
1105            if new_value != current_value {
1106                // Record original value before first write. If final value equals original
1107                // after all tx operations, the slot becomes a read per EIP-7928 net-zero filtering.
1108                // This captures the value BEFORE the first write in this transaction
1109                recorder.capture_pre_storage(address, slot_key, current_value);
1110                // Actual write
1111                recorder.record_storage_write(address, slot_key, new_value);
1112            } else {
1113                // No-op write (post == pre) - record as read per EIP-7928
1114                recorder.record_storage_read(address, slot_key);
1115            }
1116        }
1117
1118        let account = self.get_account_mut(address)?;
1119        account.storage.insert(key, new_value);
1120        Ok(())
1121    }
1122
1123    pub fn backup_storage_slot(
1124        &mut self,
1125        address: Address,
1126        key: H256,
1127        current_value: U256,
1128    ) -> Result<(), InternalError> {
1129        self.current_call_frame
1130            .call_frame_backup
1131            .original_account_storage_slots
1132            .entry(address)
1133            .or_default()
1134            .entry(key)
1135            .or_insert(current_value);
1136
1137        Ok(())
1138    }
1139}