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