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