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