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