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(), ðrex_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}