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