From 0e52bb4359420f7b65cd25c367c004bdaadd69c7 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Thu, 9 Oct 2025 16:01:49 -0300 Subject: [PATCH 01/17] initial account status implementation --- crates/vm/levm/src/db/gen_db.rs | 29 ++++++++++---------- crates/vm/levm/src/execution_handlers.rs | 7 +++++ crates/vm/levm/src/hooks/default_hook.rs | 4 +-- crates/vm/levm/src/opcode_handlers/system.rs | 8 ++++++ 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/crates/vm/levm/src/db/gen_db.rs b/crates/vm/levm/src/db/gen_db.rs index 3afc4671896..a44dcc891e9 100644 --- a/crates/vm/levm/src/db/gen_db.rs +++ b/crates/vm/levm/src/db/gen_db.rs @@ -1,5 +1,4 @@ use std::collections::BTreeMap; -use std::collections::HashSet; use std::sync::Arc; use bytes::Bytes; @@ -10,6 +9,7 @@ use ethrex_common::types::Account; use ethrex_common::utils::keccak; use super::Database; +use crate::account::AccountStatus; use crate::account::LevmAccount; use crate::call_frame::CallFrameBackup; use crate::errors::InternalError; @@ -29,10 +29,6 @@ pub struct GeneralizedDatabase { pub initial_accounts_state: CacheDB, pub codes: BTreeMap, pub tx_backup: Option, - /// For keeping track of all destroyed accounts during block execution. - /// Used in get_state_transitions for edge case in which account is destroyed and re-created afterwards - /// In that scenario we want to remove the previous storage of the account but we still want the account to exist. - pub destroyed_accounts: HashSet
, } impl GeneralizedDatabase { @@ -42,7 +38,6 @@ impl GeneralizedDatabase { current_accounts_state: CacheDB::new(), initial_accounts_state: CacheDB::new(), tx_backup: None, - destroyed_accounts: HashSet::new(), codes: BTreeMap::new(), } } @@ -65,7 +60,6 @@ impl GeneralizedDatabase { current_accounts_state: levm_accounts.clone(), initial_accounts_state: levm_accounts, tx_backup: None, - destroyed_accounts: HashSet::new(), codes, } } @@ -121,11 +115,6 @@ impl GeneralizedDatabase { address: Address, key: H256, ) -> Result { - // If the account was destroyed then we cannot rely on the DB to obtain its previous value - // This is critical when executing blocks in batches, as an account may be destroyed and created within the same batch - if self.destroyed_accounts.contains(&address) { - return Ok(Default::default()); - } let value = self.store.get_storage_value(address, key)?; // Account must already be in initial_accounts_state match self.initial_accounts_state.get_mut(&address) { @@ -168,10 +157,16 @@ impl GeneralizedDatabase { "Failed to get account {address} from immutable cache", ))))?; - // Edge case: Account was destroyed and created again afterwards with CREATE2. - if self.destroyed_accounts.contains(address) && !new_state_account.is_empty() { + // Edge case: + // 1. Account was destroyed and created again afterwards (usually with CREATE2). + // 2. Account was destroyed but then was sent some balance, so it's not going to be removed completely from the trie. + // This is a way of removing storage of an account. + if (new_state_account.status == AccountStatus::DestroyedCreated + || new_state_account.status == AccountStatus::Destroyed) + && !new_state_account.is_empty() + { // Push to account updates the removal of the account and then push the new state of the account. - // This is for clearing the account's storage when it was selfdestructed in the first place. + // This is for clearing the account's storage when it was destroyed but conserve de info. account_updates.push(AccountUpdate::removed(*address)); let new_account_update = AccountUpdate { address: *address, @@ -404,6 +399,10 @@ impl<'a> VM<'a> { if let Some(value) = account.storage.get(&key) { return Ok(*value); } + // If the account was destroyed and then created then we cannot rely on the DB to obtain storage values + if account.status == AccountStatus::DestroyedCreated { + return Ok(U256::zero()); + } } else { // When requesting storage of an account we should've previously requested and cached the account return Err(InternalError::AccountNotFound); diff --git a/crates/vm/levm/src/execution_handlers.rs b/crates/vm/levm/src/execution_handlers.rs index 86864d3679c..763ed23a000 100644 --- a/crates/vm/levm/src/execution_handlers.rs +++ b/crates/vm/levm/src/execution_handlers.rs @@ -1,4 +1,5 @@ use crate::{ + account::AccountStatus, constants::*, errors::{ContextResult, ExceptionalHalt, InternalError, TxResult, VMError}, gas_cost::CODE_DEPOSIT_COST, @@ -118,6 +119,12 @@ impl<'a> VM<'a> { })); } + if new_account.status == AccountStatus::Destroyed { + new_account.status = AccountStatus::DestroyedCreated + } else { + new_account.status = AccountStatus::Created + } + self.increase_account_balance(new_contract_address, self.current_call_frame.msg_value)?; self.increment_account_nonce(new_contract_address)?; diff --git a/crates/vm/levm/src/hooks/default_hook.rs b/crates/vm/levm/src/hooks/default_hook.rs index 122fd4dbcc2..2839c8eeb81 100644 --- a/crates/vm/levm/src/hooks/default_hook.rs +++ b/crates/vm/levm/src/hooks/default_hook.rs @@ -1,5 +1,5 @@ use crate::{ - account::LevmAccount, + account::{AccountStatus, LevmAccount}, constants::*, errors::{ContextResult, InternalError, TxValidationError, VMError}, gas_cost::{self, STANDARD_TOKEN_COST, TOTAL_COST_FLOOR_PER_TOKEN}, @@ -234,7 +234,7 @@ pub fn delete_self_destruct_accounts(vm: &mut VM<'_>) -> Result<(), VMError> { .backup_account_info(*address, account_to_remove)?; *account_to_remove = LevmAccount::default(); - vm.db.destroyed_accounts.insert(*address); + account_to_remove.status = AccountStatus::Destroyed; } Ok(()) diff --git a/crates/vm/levm/src/opcode_handlers/system.rs b/crates/vm/levm/src/opcode_handlers/system.rs index 67a338a967b..999e87eb457 100644 --- a/crates/vm/levm/src/opcode_handlers/system.rs +++ b/crates/vm/levm/src/opcode_handlers/system.rs @@ -1,4 +1,5 @@ use crate::{ + account::AccountStatus, call_frame::CallFrame, constants::{FAIL, INIT_CODE_MAX_SIZE, SUCCESS}, errors::{ContextResult, ExceptionalHalt, InternalError, OpcodeResult, TxResult, VMError}, @@ -700,6 +701,13 @@ impl<'a> VM<'a> { self.increment_account_nonce(new_address)?; // 0 -> 1 self.transfer(deployer, new_address, value)?; + let new_account = self.get_account_mut(new_address)?; + if new_account.status == AccountStatus::Destroyed { + new_account.status = AccountStatus::DestroyedCreated + } else { + new_account.status = AccountStatus::Created + } + self.substate.push_backup(); self.substate.add_created_account(new_address); // Mostly for SELFDESTRUCT during initcode. From 7b3f9c6debe653c91a47799944a43a7ca9936c8a Mon Sep 17 00:00:00 2001 From: JereSalo Date: Thu, 9 Oct 2025 16:29:20 -0300 Subject: [PATCH 02/17] make state transitions easier --- crates/vm/levm/src/account.rs | 12 ++++++++++++ crates/vm/levm/src/execution_handlers.rs | 8 +------- crates/vm/levm/src/hooks/default_hook.rs | 4 ++-- crates/vm/levm/src/opcode_handlers/system.rs | 9 +-------- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/crates/vm/levm/src/account.rs b/crates/vm/levm/src/account.rs index 919a94f8711..e15a41b66c1 100644 --- a/crates/vm/levm/src/account.rs +++ b/crates/vm/levm/src/account.rs @@ -53,6 +53,18 @@ impl From for LevmAccount { } impl LevmAccount { + pub fn created(&mut self) { + if self.status == AccountStatus::Destroyed { + self.status = AccountStatus::DestroyedCreated + } else { + self.status = AccountStatus::Created + } + } + + pub fn destroyed(&mut self) { + self.status = AccountStatus::Destroyed; + } + pub fn has_nonce(&self) -> bool { self.info.nonce != 0 } diff --git a/crates/vm/levm/src/execution_handlers.rs b/crates/vm/levm/src/execution_handlers.rs index 763ed23a000..90ab305613e 100644 --- a/crates/vm/levm/src/execution_handlers.rs +++ b/crates/vm/levm/src/execution_handlers.rs @@ -1,5 +1,4 @@ use crate::{ - account::AccountStatus, constants::*, errors::{ContextResult, ExceptionalHalt, InternalError, TxResult, VMError}, gas_cost::CODE_DEPOSIT_COST, @@ -119,12 +118,7 @@ impl<'a> VM<'a> { })); } - if new_account.status == AccountStatus::Destroyed { - new_account.status = AccountStatus::DestroyedCreated - } else { - new_account.status = AccountStatus::Created - } - + new_account.created(); self.increase_account_balance(new_contract_address, self.current_call_frame.msg_value)?; self.increment_account_nonce(new_contract_address)?; diff --git a/crates/vm/levm/src/hooks/default_hook.rs b/crates/vm/levm/src/hooks/default_hook.rs index 2839c8eeb81..ee3b3124fac 100644 --- a/crates/vm/levm/src/hooks/default_hook.rs +++ b/crates/vm/levm/src/hooks/default_hook.rs @@ -1,5 +1,5 @@ use crate::{ - account::{AccountStatus, LevmAccount}, + account::LevmAccount, constants::*, errors::{ContextResult, InternalError, TxValidationError, VMError}, gas_cost::{self, STANDARD_TOKEN_COST, TOTAL_COST_FLOOR_PER_TOKEN}, @@ -234,7 +234,7 @@ pub fn delete_self_destruct_accounts(vm: &mut VM<'_>) -> Result<(), VMError> { .backup_account_info(*address, account_to_remove)?; *account_to_remove = LevmAccount::default(); - account_to_remove.status = AccountStatus::Destroyed; + account_to_remove.destroyed(); } Ok(()) diff --git a/crates/vm/levm/src/opcode_handlers/system.rs b/crates/vm/levm/src/opcode_handlers/system.rs index 999e87eb457..aa4d300243f 100644 --- a/crates/vm/levm/src/opcode_handlers/system.rs +++ b/crates/vm/levm/src/opcode_handlers/system.rs @@ -1,5 +1,4 @@ use crate::{ - account::AccountStatus, call_frame::CallFrame, constants::{FAIL, INIT_CODE_MAX_SIZE, SUCCESS}, errors::{ContextResult, ExceptionalHalt, InternalError, OpcodeResult, TxResult, VMError}, @@ -700,13 +699,7 @@ impl<'a> VM<'a> { // Changes that revert in case the Create fails. self.increment_account_nonce(new_address)?; // 0 -> 1 self.transfer(deployer, new_address, value)?; - - let new_account = self.get_account_mut(new_address)?; - if new_account.status == AccountStatus::Destroyed { - new_account.status = AccountStatus::DestroyedCreated - } else { - new_account.status = AccountStatus::Created - } + self.get_account_mut(new_address)?.created(); self.substate.push_backup(); self.substate.add_created_account(new_address); // Mostly for SELFDESTRUCT during initcode. From fb835f3510e8f0117f8830bdf6cf025d024f9378 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Thu, 9 Oct 2025 17:14:15 -0300 Subject: [PATCH 03/17] add modified state --- crates/vm/levm/src/account.rs | 13 ++++++++----- crates/vm/levm/src/db/gen_db.rs | 8 +++++++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/crates/vm/levm/src/account.rs b/crates/vm/levm/src/account.rs index e15a41b66c1..12861bbaf64 100644 --- a/crates/vm/levm/src/account.rs +++ b/crates/vm/levm/src/account.rs @@ -65,6 +65,12 @@ impl LevmAccount { self.status = AccountStatus::Destroyed; } + pub fn mutated(&mut self) { + if self.status == AccountStatus::Unmodified { + self.status = AccountStatus::Modified; + } + } + pub fn has_nonce(&self) -> bool { self.info.nonce != 0 } @@ -81,11 +87,6 @@ impl LevmAccount { self.info.is_empty() } - /// Updates the account status. - pub fn update_status(&mut self, status: AccountStatus) { - self.status = status; - } - /// Checks if the account is unmodified. pub fn is_unmodified(&self) -> bool { matches!(self.status, AccountStatus::Unmodified) @@ -95,7 +96,9 @@ impl LevmAccount { #[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum AccountStatus { #[default] + /// Account was only read and not mutated at all. Unmodified, + /// Account accessed mutably, doesn't necessarily mean that its state has changed though but it could Modified, /// Contract executed a SELFDESTRUCT Destroyed, diff --git a/crates/vm/levm/src/db/gen_db.rs b/crates/vm/levm/src/db/gen_db.rs index a44dcc891e9..b8be0ab77b0 100644 --- a/crates/vm/levm/src/db/gen_db.rs +++ b/crates/vm/levm/src/db/gen_db.rs @@ -87,7 +87,9 @@ impl GeneralizedDatabase { /// Gets mutable reference of an account /// Warning: Use directly only if outside of the EVM, otherwise use `vm.get_account_mut` because it contemplates call frame backups. pub fn get_account_mut(&mut self, address: Address) -> Result<&mut LevmAccount, InternalError> { - self.load_account(address) + let acc = self.load_account(address)?; + acc.mutated(); + Ok(acc) } /// Gets code immutably given the code hash. @@ -149,6 +151,10 @@ impl GeneralizedDatabase { pub fn get_state_transitions(&mut self) -> Result, VMError> { let mut account_updates: Vec = vec![]; for (address, new_state_account) in self.current_accounts_state.iter() { + if new_state_account.is_unmodified() { + // Skip processing account that we know wasn't mutably accessed during execution + continue; + } // In case the account is not in immutable_cache (rare) we search for it in the actual database. let initial_state_account = self.initial_accounts_state From 8beff5223192277780682185b3016f82a698e1ce Mon Sep 17 00:00:00 2001 From: JereSalo Date: Thu, 9 Oct 2025 17:45:00 -0300 Subject: [PATCH 04/17] improve AccountStatus --- crates/vm/levm/src/account.rs | 19 ++++++------------- crates/vm/levm/src/db/gen_db.rs | 6 +++--- crates/vm/levm/src/execution_handlers.rs | 1 - crates/vm/levm/src/opcode_handlers/system.rs | 1 - 4 files changed, 9 insertions(+), 18 deletions(-) diff --git a/crates/vm/levm/src/account.rs b/crates/vm/levm/src/account.rs index 12861bbaf64..9e45241aca5 100644 --- a/crates/vm/levm/src/account.rs +++ b/crates/vm/levm/src/account.rs @@ -53,22 +53,17 @@ impl From for LevmAccount { } impl LevmAccount { - pub fn created(&mut self) { - if self.status == AccountStatus::Destroyed { - self.status = AccountStatus::DestroyedCreated - } else { - self.status = AccountStatus::Created - } - } - pub fn destroyed(&mut self) { self.status = AccountStatus::Destroyed; } - pub fn mutated(&mut self) { + pub fn modified(&mut self) { if self.status == AccountStatus::Unmodified { self.status = AccountStatus::Modified; } + if self.status == AccountStatus::Destroyed { + self.status = AccountStatus::DestroyedModified; + } } pub fn has_nonce(&self) -> bool { @@ -102,9 +97,7 @@ pub enum AccountStatus { Modified, /// Contract executed a SELFDESTRUCT Destroyed, - /// Contract created via external transaction or CREATE/CREATE2 - Created, - /// Contract has been destroyed and then re-created, usually with CREATE2 + /// Contract has been destroyed and then modified /// This is a particular state because we'll still have in the Database the storage (trie) values but they are actually invalid. - DestroyedCreated, + DestroyedModified, } diff --git a/crates/vm/levm/src/db/gen_db.rs b/crates/vm/levm/src/db/gen_db.rs index b8be0ab77b0..e61f21091f3 100644 --- a/crates/vm/levm/src/db/gen_db.rs +++ b/crates/vm/levm/src/db/gen_db.rs @@ -88,7 +88,7 @@ impl GeneralizedDatabase { /// Warning: Use directly only if outside of the EVM, otherwise use `vm.get_account_mut` because it contemplates call frame backups. pub fn get_account_mut(&mut self, address: Address) -> Result<&mut LevmAccount, InternalError> { let acc = self.load_account(address)?; - acc.mutated(); + acc.modified(); Ok(acc) } @@ -167,7 +167,7 @@ impl GeneralizedDatabase { // 1. Account was destroyed and created again afterwards (usually with CREATE2). // 2. Account was destroyed but then was sent some balance, so it's not going to be removed completely from the trie. // This is a way of removing storage of an account. - if (new_state_account.status == AccountStatus::DestroyedCreated + if (new_state_account.status == AccountStatus::DestroyedModified || new_state_account.status == AccountStatus::Destroyed) && !new_state_account.is_empty() { @@ -406,7 +406,7 @@ impl<'a> VM<'a> { return Ok(*value); } // If the account was destroyed and then created then we cannot rely on the DB to obtain storage values - if account.status == AccountStatus::DestroyedCreated { + if account.status == AccountStatus::DestroyedModified { return Ok(U256::zero()); } } else { diff --git a/crates/vm/levm/src/execution_handlers.rs b/crates/vm/levm/src/execution_handlers.rs index 90ab305613e..86864d3679c 100644 --- a/crates/vm/levm/src/execution_handlers.rs +++ b/crates/vm/levm/src/execution_handlers.rs @@ -118,7 +118,6 @@ impl<'a> VM<'a> { })); } - new_account.created(); self.increase_account_balance(new_contract_address, self.current_call_frame.msg_value)?; self.increment_account_nonce(new_contract_address)?; diff --git a/crates/vm/levm/src/opcode_handlers/system.rs b/crates/vm/levm/src/opcode_handlers/system.rs index aa4d300243f..67a338a967b 100644 --- a/crates/vm/levm/src/opcode_handlers/system.rs +++ b/crates/vm/levm/src/opcode_handlers/system.rs @@ -699,7 +699,6 @@ impl<'a> VM<'a> { // Changes that revert in case the Create fails. self.increment_account_nonce(new_address)?; // 0 -> 1 self.transfer(deployer, new_address, value)?; - self.get_account_mut(new_address)?.created(); self.substate.push_backup(); self.substate.add_created_account(new_address); // Mostly for SELFDESTRUCT during initcode. From 1be67f9d47ba486589e464adc2d34f3e20bac1a9 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Thu, 9 Oct 2025 17:46:10 -0300 Subject: [PATCH 05/17] improve naming --- crates/vm/levm/src/account.rs | 4 ++-- crates/vm/levm/src/db/gen_db.rs | 2 +- crates/vm/levm/src/hooks/default_hook.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/vm/levm/src/account.rs b/crates/vm/levm/src/account.rs index 9e45241aca5..65d1988ec78 100644 --- a/crates/vm/levm/src/account.rs +++ b/crates/vm/levm/src/account.rs @@ -53,11 +53,11 @@ impl From for LevmAccount { } impl LevmAccount { - pub fn destroyed(&mut self) { + pub fn mark_destroyed(&mut self) { self.status = AccountStatus::Destroyed; } - pub fn modified(&mut self) { + pub fn mark_modified(&mut self) { if self.status == AccountStatus::Unmodified { self.status = AccountStatus::Modified; } diff --git a/crates/vm/levm/src/db/gen_db.rs b/crates/vm/levm/src/db/gen_db.rs index e61f21091f3..0bb0b8cd6cd 100644 --- a/crates/vm/levm/src/db/gen_db.rs +++ b/crates/vm/levm/src/db/gen_db.rs @@ -88,7 +88,7 @@ impl GeneralizedDatabase { /// Warning: Use directly only if outside of the EVM, otherwise use `vm.get_account_mut` because it contemplates call frame backups. pub fn get_account_mut(&mut self, address: Address) -> Result<&mut LevmAccount, InternalError> { let acc = self.load_account(address)?; - acc.modified(); + acc.mark_modified(); Ok(acc) } diff --git a/crates/vm/levm/src/hooks/default_hook.rs b/crates/vm/levm/src/hooks/default_hook.rs index ee3b3124fac..71f5631a262 100644 --- a/crates/vm/levm/src/hooks/default_hook.rs +++ b/crates/vm/levm/src/hooks/default_hook.rs @@ -234,7 +234,7 @@ pub fn delete_self_destruct_accounts(vm: &mut VM<'_>) -> Result<(), VMError> { .backup_account_info(*address, account_to_remove)?; *account_to_remove = LevmAccount::default(); - account_to_remove.destroyed(); + account_to_remove.mark_destroyed(); } Ok(()) From 9ebd58f2694d7117206aeae65cbd03e10792f818 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Thu, 9 Oct 2025 17:50:01 -0300 Subject: [PATCH 06/17] Improve get state transititons --- crates/vm/levm/src/db/gen_db.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/vm/levm/src/db/gen_db.rs b/crates/vm/levm/src/db/gen_db.rs index 0bb0b8cd6cd..9e10c5efab4 100644 --- a/crates/vm/levm/src/db/gen_db.rs +++ b/crates/vm/levm/src/db/gen_db.rs @@ -163,16 +163,15 @@ impl GeneralizedDatabase { "Failed to get account {address} from immutable cache", ))))?; - // Edge case: - // 1. Account was destroyed and created again afterwards (usually with CREATE2). - // 2. Account was destroyed but then was sent some balance, so it's not going to be removed completely from the trie. - // This is a way of removing storage of an account. + // Edge cases: + // 1. Account was destroyed and created again afterwards. + // 2. Account was destroyed but then was sent ETH, so it's not going to be removed completely from the trie. + // This is a way of removing the storage of an account but keeping the info. if (new_state_account.status == AccountStatus::DestroyedModified || new_state_account.status == AccountStatus::Destroyed) && !new_state_account.is_empty() { // Push to account updates the removal of the account and then push the new state of the account. - // This is for clearing the account's storage when it was destroyed but conserve de info. account_updates.push(AccountUpdate::removed(*address)); let new_account_update = AccountUpdate { address: *address, From eda7ebfda5dc82a2ca0e9f10da053fe9b03d992e Mon Sep 17 00:00:00 2001 From: JereSalo Date: Thu, 9 Oct 2025 18:06:10 -0300 Subject: [PATCH 07/17] remove unnecessary check --- crates/vm/levm/src/db/gen_db.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/vm/levm/src/db/gen_db.rs b/crates/vm/levm/src/db/gen_db.rs index 9e10c5efab4..6116e1b8a70 100644 --- a/crates/vm/levm/src/db/gen_db.rs +++ b/crates/vm/levm/src/db/gen_db.rs @@ -167,10 +167,7 @@ impl GeneralizedDatabase { // 1. Account was destroyed and created again afterwards. // 2. Account was destroyed but then was sent ETH, so it's not going to be removed completely from the trie. // This is a way of removing the storage of an account but keeping the info. - if (new_state_account.status == AccountStatus::DestroyedModified - || new_state_account.status == AccountStatus::Destroyed) - && !new_state_account.is_empty() - { + if new_state_account.status == AccountStatus::DestroyedModified { // Push to account updates the removal of the account and then push the new state of the account. account_updates.push(AccountUpdate::removed(*address)); let new_account_update = AccountUpdate { From 3279a73f8b6fb4ca439e54b35b66a376e118ab00 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Mon, 13 Oct 2025 12:50:47 -0300 Subject: [PATCH 08/17] try to remove storage in account update --- crates/common/types/account_update.rs | 2 ++ crates/l2/common/src/state_diff.rs | 1 + crates/storage/store.rs | 3 +++ crates/vm/levm/src/db/gen_db.rs | 33 ++++++--------------------- 4 files changed, 13 insertions(+), 26 deletions(-) diff --git a/crates/common/types/account_update.rs b/crates/common/types/account_update.rs index cf75d05b57b..59c0a0c5fba 100644 --- a/crates/common/types/account_update.rs +++ b/crates/common/types/account_update.rs @@ -11,6 +11,8 @@ pub struct AccountUpdate { pub info: Option, pub code: Option, pub added_storage: BTreeMap, + /// If account was destroyed and then modified we need this for removing its storage but not the entire account. + pub removed_storage: bool, // Matches TODO in code // removed_storage_keys: Vec, } diff --git a/crates/l2/common/src/state_diff.rs b/crates/l2/common/src/state_diff.rs index 7a50fa110a7..b58d560318d 100644 --- a/crates/l2/common/src/state_diff.rs +++ b/crates/l2/common/src/state_diff.rs @@ -299,6 +299,7 @@ impl StateDiff { info: account_info, code: diff.bytecode.clone(), added_storage: diff.storage.clone().into_iter().collect(), + removed_storage: false, }, ); } diff --git a/crates/storage/store.rs b/crates/storage/store.rs index 8fa690c4b04..f87eeec28ea 100644 --- a/crates/storage/store.rs +++ b/crates/storage/store.rs @@ -396,6 +396,9 @@ impl Store { Some(encoded_state) => AccountState::decode(&encoded_state)?, None => AccountState::default(), }; + if update.removed_storage { + account_state.storage_root = *EMPTY_TRIE_HASH; + } if let Some(info) = &update.info { account_state.nonce = info.nonce; account_state.balance = info.balance; diff --git a/crates/vm/levm/src/db/gen_db.rs b/crates/vm/levm/src/db/gen_db.rs index 6116e1b8a70..87b769706ab 100644 --- a/crates/vm/levm/src/db/gen_db.rs +++ b/crates/vm/levm/src/db/gen_db.rs @@ -163,31 +163,6 @@ impl GeneralizedDatabase { "Failed to get account {address} from immutable cache", ))))?; - // Edge cases: - // 1. Account was destroyed and created again afterwards. - // 2. Account was destroyed but then was sent ETH, so it's not going to be removed completely from the trie. - // This is a way of removing the storage of an account but keeping the info. - if new_state_account.status == AccountStatus::DestroyedModified { - // Push to account updates the removal of the account and then push the new state of the account. - account_updates.push(AccountUpdate::removed(*address)); - let new_account_update = AccountUpdate { - address: *address, - removed: false, - info: Some(new_state_account.info.clone()), - code: Some( - self.codes - .get(&new_state_account.info.code_hash) - .ok_or(VMError::Internal(InternalError::Custom(format!( - "Failed to get code for account {address}" - ))))? - .clone(), - ), - added_storage: new_state_account.storage.clone(), - }; - account_updates.push(new_account_update); - continue; - } - let mut acc_info_updated = false; let mut storage_updated = false; @@ -215,9 +190,14 @@ impl GeneralizedDatabase { // 2. Storage has been updated if the current value is different from the one before execution. let mut added_storage = BTreeMap::new(); + let removed_storage = new_state_account.status == AccountStatus::DestroyedModified; for (key, new_value) in &new_state_account.storage { - let old_value = 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}")))})?; + let old_value = if !removed_storage { + 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}")))})? + } else { + &U256::zero() + }; if new_value != old_value { added_storage.insert(*key, *new_value); @@ -247,6 +227,7 @@ impl GeneralizedDatabase { info, code: code.cloned(), added_storage, + removed_storage, }; account_updates.push(account_update); From f2b7546754dd44ea92a6d9e7e5b467ef5c555a19 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Mon, 13 Oct 2025 14:32:32 -0300 Subject: [PATCH 09/17] add missing check --- crates/vm/levm/src/db/gen_db.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/vm/levm/src/db/gen_db.rs b/crates/vm/levm/src/db/gen_db.rs index 87b769706ab..76895124e7c 100644 --- a/crates/vm/levm/src/db/gen_db.rs +++ b/crates/vm/levm/src/db/gen_db.rs @@ -216,7 +216,7 @@ impl GeneralizedDatabase { let was_empty = initial_state_account.is_empty(); let removed = new_state_account.is_empty() && !was_empty; - if !removed && !acc_info_updated && !storage_updated { + if !removed && !acc_info_updated && !storage_updated && !removed_storage { // Account hasn't been updated continue; } From 4722c79e191a9dba111c13296419cd0f25fd3c57 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Mon, 13 Oct 2025 14:51:00 -0300 Subject: [PATCH 10/17] update comment and create constant --- crates/common/utils.rs | 2 ++ crates/vm/levm/src/db/gen_db.rs | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/common/utils.rs b/crates/common/utils.rs index 1f7ba6903a4..d5f4f242637 100644 --- a/crates/common/utils.rs +++ b/crates/common/utils.rs @@ -4,6 +4,8 @@ use hex::FromHexError; use sha3::Digest; use sha3::Keccak256; +pub const ZERO_U256: U256 = U256([0, 0, 0, 0]); + /// Converts a big endian slice to a u256, faster than `u256::from_big_endian`. pub fn u256_from_big_endian(slice: &[u8]) -> U256 { let mut padded = [0u8; 32]; diff --git a/crates/vm/levm/src/db/gen_db.rs b/crates/vm/levm/src/db/gen_db.rs index 76895124e7c..6f62799e707 100644 --- a/crates/vm/levm/src/db/gen_db.rs +++ b/crates/vm/levm/src/db/gen_db.rs @@ -6,6 +6,7 @@ use ethrex_common::Address; use ethrex_common::H256; use ethrex_common::U256; use ethrex_common::types::Account; +use ethrex_common::utils::ZERO_U256; use ethrex_common::utils::keccak; use super::Database; @@ -196,7 +197,8 @@ impl GeneralizedDatabase { let old_value = if !removed_storage { 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}")))})? } else { - &U256::zero() + // There's not an "old value" if the contract was destroyed and re-created. + &ZERO_U256 }; if new_value != old_value { @@ -212,7 +214,7 @@ impl GeneralizedDatabase { }; // "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)." - // If the account was already empty then this is not an update + // ethrex is post-Merge client, empty accounts have already been pruned from the trie by the Merge (see EIP-161), so we won't have any empty accounts in the trie. let was_empty = initial_state_account.is_empty(); let removed = new_state_account.is_empty() && !was_empty; From ad077aac4244122b3dc79bcd43890bd17bb152d3 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Mon, 13 Oct 2025 14:57:17 -0300 Subject: [PATCH 11/17] improve comment in removed storage --- crates/vm/levm/src/db/gen_db.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/vm/levm/src/db/gen_db.rs b/crates/vm/levm/src/db/gen_db.rs index 6f62799e707..a5ae2cfff23 100644 --- a/crates/vm/levm/src/db/gen_db.rs +++ b/crates/vm/levm/src/db/gen_db.rs @@ -189,9 +189,14 @@ impl GeneralizedDatabase { None }; + // Account will have only its storage removed if it was Destroyed and then modified + // Edge cases that can make this true: + // 1. Account was destroyed and created again afterwards. + // 2. Account was destroyed but then was sent ETH, so it's not going to be completely removed from the trie. + let removed_storage = new_state_account.status == AccountStatus::DestroyedModified; + // 2. Storage has been updated if the current value is different from the one before execution. let mut added_storage = BTreeMap::new(); - let removed_storage = new_state_account.status == AccountStatus::DestroyedModified; for (key, new_value) in &new_state_account.storage { let old_value = if !removed_storage { From 9955c390472850a2ec479c0cf700ccce819a8049 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Mon, 13 Oct 2025 14:58:45 -0300 Subject: [PATCH 12/17] update removed storage in witness --- crates/storage/store.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/storage/store.rs b/crates/storage/store.rs index f87eeec28ea..b523bf8f9aa 100644 --- a/crates/storage/store.rs +++ b/crates/storage/store.rs @@ -468,6 +468,9 @@ impl Store { self.add_account_code(info.code_hash, code.clone()).await?; } } + if update.removed_storage { + account_state.storage_root = *EMPTY_TRIE_HASH; + } // Store the added storage in the account's storage trie and compute its new root if !update.added_storage.is_empty() { let (_witness, storage_trie) = match storage_tries.entry(update.address) { From c1fa5e2cb9fe389b94347a14a26252b61c46b50a Mon Sep 17 00:00:00 2001 From: JereSalo Date: Mon, 13 Oct 2025 15:15:05 -0300 Subject: [PATCH 13/17] removed storage in eftests --- tooling/ef_tests/state/runner/revm_db.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tooling/ef_tests/state/runner/revm_db.rs b/tooling/ef_tests/state/runner/revm_db.rs index b1ae385b369..9f7e4751870 100644 --- a/tooling/ef_tests/state/runner/revm_db.rs +++ b/tooling/ef_tests/state/runner/revm_db.rs @@ -2,9 +2,9 @@ use ethrex_common::types::{AccountInfo, AccountUpdate, ChainConfig}; use ethrex_common::{Address as CoreAddress, BigEndianHash, H256, U256}; use ethrex_vm::{DynVmDatabase, EvmError, VmDatabase}; use revm::context::DBErrorMarker; -use revm::database::states::{AccountStatus, bundle_state::BundleRetention}; +use revm::database::states::{bundle_state::BundleRetention, AccountStatus}; use revm::primitives::{ - Address as RevmAddress, B256 as RevmB256, Bytes as RevmBytes, U256 as RevmU256, + Address as RevmAddress, Bytes as RevmBytes, B256 as RevmB256, U256 as RevmU256, }; use revm::state::{AccountInfo as RevmAccountInfo, Bytecode as RevmBytecode}; @@ -198,6 +198,7 @@ impl RevmState { ) }) .collect(), + removed_storage: false, }; account_updates.push(new_acc_update); } From efea34e0d5ff09693320025e159ae034dad0ada0 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Mon, 13 Oct 2025 16:26:28 -0300 Subject: [PATCH 14/17] make changes in witness as well --- crates/common/types/block_execution_witness.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/common/types/block_execution_witness.rs b/crates/common/types/block_execution_witness.rs index 4901be62207..bbeb59bf5f6 100644 --- a/crates/common/types/block_execution_witness.rs +++ b/crates/common/types/block_execution_witness.rs @@ -12,7 +12,7 @@ use crate::{ use bytes::Bytes; use ethereum_types::{Address, H256, U256}; use ethrex_rlp::{decode::RLPDecode, encode::RLPEncode}; -use ethrex_trie::{NodeRLP, Trie}; +use ethrex_trie::{EMPTY_TRIE_HASH, NodeRLP, Trie}; use rkyv::{Archive, Deserialize as RDeserialize, Serialize as RSerialize}; use serde::de::{SeqAccess, Visitor}; use serde::ser::SerializeSeq; @@ -246,6 +246,9 @@ impl GuestProgramState { .expect("failed to decode account state"), None => AccountState::default(), }; + if update.removed_storage { + account_state.storage_root = *EMPTY_TRIE_HASH; + } if let Some(info) = &update.info { account_state.nonce = info.nonce; account_state.balance = info.balance; From 324a4291f20a6a3b7d7d7193c035cdc9a122a84a Mon Sep 17 00:00:00 2001 From: JereSalo Date: Mon, 13 Oct 2025 16:32:26 -0300 Subject: [PATCH 15/17] fmt state tests --- tooling/ef_tests/state/runner/revm_db.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tooling/ef_tests/state/runner/revm_db.rs b/tooling/ef_tests/state/runner/revm_db.rs index 9f7e4751870..0c2c8a05a9a 100644 --- a/tooling/ef_tests/state/runner/revm_db.rs +++ b/tooling/ef_tests/state/runner/revm_db.rs @@ -2,9 +2,9 @@ use ethrex_common::types::{AccountInfo, AccountUpdate, ChainConfig}; use ethrex_common::{Address as CoreAddress, BigEndianHash, H256, U256}; use ethrex_vm::{DynVmDatabase, EvmError, VmDatabase}; use revm::context::DBErrorMarker; -use revm::database::states::{bundle_state::BundleRetention, AccountStatus}; +use revm::database::states::{AccountStatus, bundle_state::BundleRetention}; use revm::primitives::{ - Address as RevmAddress, Bytes as RevmBytes, B256 as RevmB256, U256 as RevmU256, + Address as RevmAddress, B256 as RevmB256, Bytes as RevmBytes, U256 as RevmU256, }; use revm::state::{AccountInfo as RevmAccountInfo, Bytecode as RevmBytecode}; From d0ea1c10bac5c594497da6899f1ba8afea12f244 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Tue, 14 Oct 2025 07:44:53 -0300 Subject: [PATCH 16/17] merge account updates --- crates/common/types/account_update.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/common/types/account_update.rs b/crates/common/types/account_update.rs index 59c0a0c5fba..e6c2ff58190 100644 --- a/crates/common/types/account_update.rs +++ b/crates/common/types/account_update.rs @@ -37,6 +37,7 @@ impl AccountUpdate { pub fn merge(&mut self, other: AccountUpdate) { self.removed = other.removed; + self.removed_storage |= other.removed_storage; if let Some(info) = other.info { self.info = Some(info); } From 418afa6ca79bf1128a8770334d56b5b1057ee2e3 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Tue, 14 Oct 2025 16:49:52 -0300 Subject: [PATCH 17/17] update comment --- crates/vm/levm/src/db/gen_db.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/vm/levm/src/db/gen_db.rs b/crates/vm/levm/src/db/gen_db.rs index a5ae2cfff23..721992d1b94 100644 --- a/crates/vm/levm/src/db/gen_db.rs +++ b/crates/vm/levm/src/db/gen_db.rs @@ -219,7 +219,7 @@ impl GeneralizedDatabase { }; // "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)." - // ethrex is post-Merge client, empty accounts have already been pruned from the trie by the Merge (see EIP-161), so we won't have any empty accounts in the trie. + // 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. let was_empty = initial_state_account.is_empty(); let removed = new_state_account.is_empty() && !was_empty;