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