1use alloy_eip7928::AccountChanges;
8use alloy_primitives::{keccak256, map::B256Map, Bytes, B256};
9use reth_primitives_traits::Account;
10use reth_trie_common::{HashedPostState, HashedStorage};
11
12#[derive(Clone, Debug, Default, PartialEq, Eq)]
14pub struct BalStateUpdate {
15 pub(super) state: HashedPostState,
17 pub(super) bytecodes: B256Map<Bytes>,
19 pub(super) unresolved: Vec<B256>,
21}
22
23impl BalStateUpdate {
24 pub const fn state(&self) -> &HashedPostState {
26 &self.state
27 }
28
29 pub const fn bytecodes(&self) -> &B256Map<Bytes> {
31 &self.bytecodes
32 }
33
34 pub fn unresolved(&self) -> &[B256] {
36 &self.unresolved
37 }
38
39 pub fn into_parts(self) -> (HashedPostState, B256Map<Bytes>, Vec<B256>) {
42 (self.state, self.bytecodes, self.unresolved)
43 }
44
45 pub(super) fn insert_storage(&mut self, hashed_address: B256, changes: &AccountChanges) {
47 let storage = HashedStorage::from_iter(
48 changes.storage_post_states().map(|(slot, value)| (keccak256(B256::from(slot)), value)),
49 );
50 self.state.storages.insert(hashed_address, storage);
51 }
52}
53
54#[derive(Clone, Debug, PartialEq, Eq)]
56pub enum DownloadedAccount {
57 Unknown,
59 Absent,
61 Present(Account),
63}
64
65#[cfg(test)]
66mod tests {
67
68 use super::*;
69 use crate::{test_utils::hashed_factory, AccountCoverage, SnapCatchUpStore, StorageProgress};
70 use alloy_consensus::{Header, TxLegacy};
71 use alloy_eip7928::{
72 AccountChanges, BalanceChange, BlockAccessIndex, CodeChange, NonceChange, SlotChanges,
73 StorageChange,
74 };
75 use alloy_eips::{
76 eip2935::{HISTORY_STORAGE_ADDRESS, HISTORY_STORAGE_CODE},
77 eip4788::{BEACON_ROOTS_ADDRESS, BEACON_ROOTS_CODE},
78 eip4895::Withdrawal,
79 eip7002::{WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, WITHDRAWAL_REQUEST_PREDEPLOY_CODE},
80 eip7251::{CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, CONSOLIDATION_REQUEST_PREDEPLOY_CODE},
81 };
82 use alloy_primitives::{bytes, keccak256, Address, Signature, TxKind, U256};
83 use evm2::{
84 bytecode::Bytecode,
85 evm::{
86 AccountInfo, InMemoryDB, BUILDER_DEPOSIT_REQUEST_ADDRESS, BUILDER_EXIT_REQUEST_ADDRESS,
87 },
88 };
89 use reth_chainspec::ChainSpecBuilder;
90 use reth_db_api::{tables, transaction::DbTxMut};
91 use reth_ethereum_primitives::{Block, BlockBody, Transaction, TransactionSigned};
92 use reth_evm::{execute::BlockExecutor, ConfigureEvm};
93 use reth_evm_ethereum::EthEvmConfig;
94 use reth_primitives_traits::{Block as _, Recovered};
95 use reth_storage_api::DatabaseProviderFactory;
96 use reth_trie_common::{HashedStorage, KeccakKeyHasher};
97 use std::{collections::BTreeMap, sync::Arc};
98
99 const ACCOUNT: Address = Address::repeat_byte(0xaa);
100 const SENDER: Address = Address::repeat_byte(0x11);
101
102 fn index(value: u64) -> BlockAccessIndex {
103 BlockAccessIndex::new(value)
104 }
105
106 fn state_update(
107 bal: &[AccountChanges],
108 coverage: AccountCoverage,
109 accounts: impl IntoIterator<Item = (B256, Account)>,
110 ) -> BalStateUpdate {
111 let factory = hashed_factory();
112 let provider = factory.database_provider_rw().unwrap();
113 for (address, account) in accounts {
114 provider.tx_ref().put::<tables::HashedAccounts>(address, account).unwrap();
115 }
116 provider.block_access_list_update(coverage, StorageProgress::START, bal).unwrap()
117 }
118
119 fn apply(changes: &AccountChanges, base: DownloadedAccount) -> BalStateUpdate {
120 let (coverage, account) = match base {
121 DownloadedAccount::Unknown => (AccountCoverage::START, None),
122 DownloadedAccount::Absent => (AccountCoverage::COMPLETE, None),
123 DownloadedAccount::Present(account) => (AccountCoverage::COMPLETE, Some(account)),
124 };
125 state_update(
126 std::slice::from_ref(changes),
127 coverage,
128 account.map(|account| (keccak256(changes.address()), account)),
129 )
130 }
131
132 #[test]
133 fn read_only_entries_write_nothing() {
134 let changes = AccountChanges::new(ACCOUNT).with_storage_read(U256::ONE);
135
136 let update = apply(&changes, DownloadedAccount::Unknown);
137
138 assert_eq!(update, BalStateUpdate::default());
139 }
140
141 #[test]
142 fn empty_slot_entries_write_nothing() {
143 let changes =
144 AccountChanges::new(ACCOUNT).with_storage_change(SlotChanges::new(U256::ONE, vec![]));
145
146 let update = apply(&changes, DownloadedAccount::Unknown);
147
148 assert_eq!(update, BalStateUpdate::default());
149 }
150
151 #[test]
152 fn accounts_not_downloaded_yet_are_left_unresolved() {
153 let changes = AccountChanges::new(ACCOUNT)
155 .with_balance_change(BalanceChange::new(index(1), U256::from(10)))
156 .with_nonce_change(NonceChange::new(index(1), 1))
157 .with_code_change(CodeChange::new(index(1), bytes!("6001")));
158
159 let update = apply(&changes, DownloadedAccount::Unknown);
160
161 assert_eq!(update.unresolved, vec![keccak256(ACCOUNT)]);
162 assert!(update.state.is_empty());
163 assert!(update.bytecodes.is_empty());
164 }
165
166 #[test]
167 fn zeroed_slots_and_cleared_code_are_written() {
168 let existing = Account::new(1, U256::ONE, Some(B256::repeat_byte(1)));
169 let changes = AccountChanges::new(ACCOUNT)
170 .with_code_change(CodeChange::new(index(1), bytes!("6001")))
171 .with_code_change(CodeChange::new(index(2), Bytes::new()))
172 .with_storage_change(SlotChanges::new(
173 U256::ONE,
174 vec![
175 StorageChange::new(index(1), U256::from(5)),
176 StorageChange::new(index(2), U256::ZERO),
177 ],
178 ));
179
180 let update = apply(&changes, DownloadedAccount::Present(existing));
181
182 let hashed_address = keccak256(ACCOUNT);
183 assert_eq!(update.state.accounts[&hashed_address].as_ref().unwrap().bytecode_hash, None);
184 assert_eq!(
185 update.state.storages[&hashed_address],
186 HashedStorage::from_iter([(keccak256(B256::with_last_byte(1)), U256::ZERO)])
187 );
188 assert!(update.bytecodes.is_empty());
189 }
190
191 type FlatState = (BTreeMap<B256, Account>, BTreeMap<(B256, B256), U256>);
193
194 fn flatten(db: &InMemoryDB) -> FlatState {
195 let mut state = FlatState::default();
196 for (address, info) in &db.cache.accounts {
197 let Some(info) = info else { continue };
198 let hashed_address = keccak256(address);
199 state.0.insert(hashed_address, Account::from(info));
200 if let Some(storage) = db.cache.storage.get(address) {
201 for (slot, value) in &storage.slots {
202 if !value.is_zero() {
203 state.1.insert((hashed_address, keccak256(B256::from(*slot))), *value);
204 }
205 }
206 }
207 }
208 state
209 }
210
211 #[allow(clippy::clone_on_copy)]
212 fn fold(mut state: FlatState, update: &HashedPostState) -> FlatState {
213 for (hashed_address, storage) in &update.storages {
214 for (slot, value) in &storage.storage {
215 if value.is_zero() {
216 state.1.remove(&(*hashed_address, *slot));
217 } else {
218 state.1.insert((*hashed_address, *slot), *value);
219 }
220 }
221 }
222 for (hashed_address, account) in &update.accounts {
223 match account {
224 Some(account) => {
225 state.0.insert(*hashed_address, account.clone());
226 }
227 None => {
228 state.0.remove(hashed_address);
229 state.1.retain(|(address, _), _| address != hashed_address);
230 }
231 }
232 }
233 state
234 }
235
236 fn insert(db: &mut InMemoryDB, address: Address, nonce: u64, code: Bytes) {
237 let code = Bytecode::new_raw(code);
238 let info = AccountInfo {
239 nonce,
240 code_hash: code.hash_slow(),
241 code: Some(code),
242 ..Default::default()
243 };
244 db.insert_account_info(&address, info);
245 }
246
247 fn tx(nonce: u64, to: TxKind, value: u64, input: Bytes) -> Recovered<TransactionSigned> {
248 let tx = Transaction::Legacy(TxLegacy {
249 nonce,
250 gas_price: 1,
251 gas_limit: 1_000_000,
252 to,
253 value: U256::from(value),
254 input,
255 ..Default::default()
256 });
257 Recovered::new_unchecked(
258 TransactionSigned::new_unhashed(tx, Signature::test_signature()),
259 SENDER,
260 )
261 }
262
263 #[test]
264 #[allow(clippy::clone_on_copy)]
265 fn applying_the_list_matches_execution() {
266 let contract = Address::repeat_byte(0xc0);
267 let beneficiary = Address::repeat_byte(0xbe);
268
269 let mut db = InMemoryDB::default();
270 insert(&mut db, BEACON_ROOTS_ADDRESS, 1, BEACON_ROOTS_CODE.clone());
271 insert(&mut db, HISTORY_STORAGE_ADDRESS, 1, HISTORY_STORAGE_CODE.clone());
272 insert(
273 &mut db,
274 WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS,
275 1,
276 WITHDRAWAL_REQUEST_PREDEPLOY_CODE.clone(),
277 );
278 insert(
279 &mut db,
280 CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS,
281 1,
282 CONSOLIDATION_REQUEST_PREDEPLOY_CODE.clone(),
283 );
284 for address in [BUILDER_DEPOSIT_REQUEST_ADDRESS, BUILDER_EXIT_REQUEST_ADDRESS] {
285 insert(&mut db, address, 1, bytes!("00"));
286 }
287 db.insert_account_info(
288 &SENDER,
289 AccountInfo { balance: U256::from(u64::MAX), ..Default::default() },
290 );
291 insert(&mut db, contract, 1, bytes!("6000600155600354504360025534600455"));
293 db.insert_account_storage(&contract, &U256::from(1), &U256::from(5));
294 db.insert_account_storage(&contract, &U256::from(3), &U256::from(7));
295 let pre = flatten(&db);
296
297 let txs = [
298 tx(0, TxKind::Call(contract), 1, Bytes::new()),
300 tx(1, TxKind::Call(contract), 2, Bytes::new()),
301 tx(2, TxKind::Create, 0, bytes!("60fe60005360016000f3")),
303 tx(3, TxKind::Call(SENDER.create(4)), 100, Bytes::new()),
305 tx(
306 4,
307 TxKind::Create,
308 0,
309 [&[0x73][..], beneficiary.as_slice(), &[0xff]].concat().into(),
310 ),
311 ];
312 let header = Header {
313 number: 1,
314 timestamp: 1,
315 gas_limit: 30_000_000,
316 parent_beacon_block_root: Some(B256::repeat_byte(1)),
317 excess_blob_gas: Some(0),
318 blob_gas_used: Some(0),
319 block_access_list_hash: Some(B256::ZERO),
320 ..Default::default()
321 };
322 let withdrawal = Withdrawal { address: beneficiary, amount: 1, ..Default::default() };
323 let block = Block {
324 header,
325 body: BlockBody { withdrawals: Some(vec![withdrawal].into()), ..Default::default() },
326 }
327 .seal_slow();
328
329 let evm_config =
330 EthEvmConfig::new(Arc::new(ChainSpecBuilder::mainnet().amsterdam_activated().build()));
331 let mut executor = evm_config.executor_for_block(&mut db, &block).unwrap();
332 executor.enable_block_access_list_builder();
333 executor.apply_pre_execution_changes().unwrap();
334 for tx in txs {
335 executor.execute_transaction(tx).unwrap();
336 }
337 let (output, bal) = executor.finish_with_block_access_list().unwrap();
338 let bal = bal.unwrap();
339
340 let update = state_update(
341 &bal,
342 AccountCoverage::COMPLETE,
343 pre.0.iter().map(|(address, account)| (*address, account.clone())),
344 );
345 let executed = HashedPostState::from_bundle_state::<KeccakKeyHasher>(&output.state.state);
346
347 let post = fold(pre.clone(), &update.state);
348 assert!(update.unresolved.is_empty());
349 assert_eq!(post, fold(pre, &executed));
350
351 let slot = |address: Address, slot: u64| {
353 post.1.get(&(keccak256(address), keccak256(B256::from(U256::from(slot)))))
354 };
355 assert_eq!((slot(contract, 1), slot(contract, 4)), (None, Some(&U256::from(2))));
356 assert!(!post.0.contains_key(&keccak256(SENDER.create(4))));
357 assert_eq!(post.0[&keccak256(beneficiary)].balance, U256::from(100 + 1_000_000_000u64));
358 assert!(post.1.keys().any(|(address, _)| *address == keccak256(BEACON_ROOTS_ADDRESS)));
359 assert_eq!(
360 update.bytecodes,
361 B256Map::from_iter([(keccak256([0xfe]), Bytes::from_static(&[0xfe]))])
362 );
363 }
364}