ethrex_levm/account.rs
1use ethrex_common::H256;
2use ethrex_common::constants::EMPTY_TRIE_HASH;
3use ethrex_common::seeded_hash::SeededHashMap;
4use ethrex_common::types::{AccountState, GenesisAccount};
5use ethrex_common::utils::keccak;
6use ethrex_common::{U256, constants::EMPTY_KECCAK_HASH, types::AccountInfo};
7use serde::{Deserialize, Serialize};
8
9/// Similar to `Account` struct but suited for LEVM implementation.
10/// Difference is this doesn't have code and it contains an additional `status` field for decision-making.
11/// The code is stored in the `GeneralizedDatabase` and can be accessed with its hash.\
12/// **Some advantages:**
13/// - We'll fetch the code only if we need to, this means less accesses to the database.
14/// - If there's duplicate code between accounts (which is pretty common) we'll store it in memory only once.
15/// - We'll be able to make better decisions without relying on external structures, based on the current status of an Account. e.g. If it was untouched we skip processing it when calculating Account Updates, or if the account has been destroyed and re-created with same address we know that the storage on the Database is not valid and we shouldn't access it, etc.
16#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
17pub struct LevmAccount {
18 pub info: AccountInfo,
19 /// Seeded: contracts choose the slot keys.
20 pub storage: SeededHashMap<H256, U256>,
21 /// If true it means that attempting to create an account with this address it would at least collide because of storage.
22 /// We just care about this kind of collision if the account doesn't have code or nonce. Otherwise its value doesn't matter.
23 /// For more information see EIP-7610: https://eips.ethereum.org/EIPS/eip-7610
24 /// Warning: This attribute should only be used for handling create collisions as it's not necessary appropriate for every scenario. Read the caveat below.
25 ///
26 /// How this works:
27 /// - When getting an account from the DB this is set to true if the account has non-empty storage root.
28 /// - Upon destruction of an account this is set to false because storage is emptied for sure.
29 ///
30 /// **Important Caveat**
31 /// This only works for accounts of these characteristics that have been created in the past, we consider that accounts with storage
32 /// but no nonce or code cannot be created anymore, otherwise the fix would need to be more complex because we should keep track of the
33 /// storage root of an account during execution instead of just keeping track of it when fetching it from the Database or updating it when
34 /// destroying it. The EIP that adds to the spec this check did it because there are 28 accounts with these characteristics already deployed
35 /// in mainnet (back when they were deployed with nonce 0), but they cannot be created intentionally anymore.
36 pub has_storage: bool,
37 /// Current status of the account.
38 pub status: AccountStatus,
39 /// Whether this account exists in the state trie.
40 /// Used for EIP-7702 auth refund: `account_exists` (EELS) differs from `!is_empty()`.
41 /// An account can exist but be empty (e.g., has non-empty storage root only).
42 /// Default is `false` (non-existent); set to `true` when loaded from DB with actual state.
43 pub exists: bool,
44}
45
46// This is used only in state_v2 runner, storage is already fully filled in the genesis account.
47impl From<GenesisAccount> for LevmAccount {
48 fn from(genesis: GenesisAccount) -> Self {
49 let storage: SeededHashMap<H256, U256> = genesis
50 .storage
51 .into_iter()
52 .map(|(key, value)| (H256::from(key.to_big_endian()), value))
53 .collect();
54
55 LevmAccount {
56 info: AccountInfo {
57 code_hash: keccak(genesis.code),
58 balance: genesis.balance,
59 nonce: genesis.nonce,
60 },
61 has_storage: !storage.is_empty(),
62 storage,
63 status: AccountStatus::Unmodified,
64 exists: true,
65 }
66 }
67}
68impl From<AccountState> for LevmAccount {
69 fn from(state: AccountState) -> Self {
70 let is_default = state == AccountState::default();
71 LevmAccount {
72 info: AccountInfo {
73 code_hash: state.code_hash,
74 balance: state.balance,
75 nonce: state.nonce,
76 },
77 storage: Default::default(),
78 status: AccountStatus::Unmodified,
79 has_storage: state.storage_root != *EMPTY_TRIE_HASH,
80 // An account with all default fields was not found in the DB.
81 // Post-EIP-161, truly empty accounts are pruned from the trie,
82 // so default == non-existent. Accounts with non-empty storage root
83 // but empty balance/nonce/code DO exist (state != default).
84 exists: !is_default,
85 }
86 }
87}
88
89impl LevmAccount {
90 pub fn mark_destroyed(&mut self) {
91 self.status = AccountStatus::Destroyed;
92 }
93
94 pub fn mark_modified(&mut self) {
95 if self.status == AccountStatus::Unmodified {
96 self.status = AccountStatus::Modified;
97 }
98 if self.status == AccountStatus::Destroyed {
99 self.status = AccountStatus::DestroyedModified;
100 }
101 // A modified account exists in the current state
102 // (even if it didn't exist in the trie before this tx).
103 self.exists = true;
104 }
105
106 pub fn has_nonce(&self) -> bool {
107 self.info.nonce != 0
108 }
109
110 pub fn has_code(&self) -> bool {
111 self.info.code_hash != *EMPTY_KECCAK_HASH
112 }
113
114 pub fn create_would_collide(&self) -> bool {
115 self.has_code() || self.has_nonce() || self.has_storage
116 }
117
118 pub fn is_empty(&self) -> bool {
119 self.info.is_empty()
120 }
121
122 /// Checks if the account is unmodified.
123 pub fn is_unmodified(&self) -> bool {
124 matches!(self.status, AccountStatus::Unmodified)
125 }
126
127 /// Clones the account's metadata (info + flags) but leaves `storage` empty.
128 ///
129 /// Used on the streaming-executor read-fault path (`load_account`): when the streaming
130 /// merkleizer drains `current_accounts_state`, a hot account re-faulted on the next tx would
131 /// otherwise deep-copy its entire accumulated storage map (hundreds–thousands of slots) just
132 /// so the tx can read the ~3 it touches. Cloning info/flags only and faulting those slots in
133 /// lazily avoids that copy. Correctness relies on `get_storage_value` resolving a `current`
134 /// miss against `initial_accounts_state` (the committed in-block baseline) before the
135 /// pre-block store, which keeps the diff invariant "every key in `current.storage` is also in
136 /// `initial.storage`" intact.
137 ///
138 /// Destructured (not `..`) so adding a field to `LevmAccount` fails to compile here until it
139 /// is explicitly carried — a missing flag would silently corrupt the state-transition diff.
140 #[inline]
141 pub fn clone_without_storage(&self) -> Self {
142 let Self {
143 info,
144 storage: _,
145 has_storage,
146 status,
147 exists,
148 } = self;
149 Self {
150 info: info.clone(),
151 storage: Default::default(),
152 has_storage: *has_storage,
153 status: status.clone(),
154 exists: *exists,
155 }
156 }
157}
158
159#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
160pub enum AccountStatus {
161 #[default]
162 /// Account was only read and not mutated at all.
163 Unmodified,
164 /// Account accessed mutably, doesn't necessarily mean that its state has changed though but it could
165 Modified,
166 /// Contract executed a SELFDESTRUCT
167 Destroyed,
168 /// Contract has been destroyed and then modified
169 /// This is a particular state because we'll still have in the Database the storage (trie) values but they are actually invalid.
170 DestroyedModified,
171}