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 use super::*;
68 use crate::{test_utils::hashed_factory, AccountCoverage, SnapCatchUpStore, StorageProgress};
69 use alloy_consensus::{Header, TxLegacy};
70 use alloy_eip7928::{
71 AccountChanges, BalanceChange, BlockAccessIndex, CodeChange, NonceChange, SlotChanges,
72 StorageChange,
73 };
74 use alloy_eips::{
75 eip2935::{HISTORY_STORAGE_ADDRESS, HISTORY_STORAGE_CODE},
76 eip4788::{BEACON_ROOTS_ADDRESS, BEACON_ROOTS_CODE},
77 eip4895::Withdrawal,
78 eip7002::{WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, WITHDRAWAL_REQUEST_PREDEPLOY_CODE},
79 };
80 use alloy_primitives::{bytes, keccak256, Address, Signature, TxKind, U256};
81 use reth_chainspec::ChainSpecBuilder;
82 use reth_db_api::{tables, transaction::DbTxMut};
83 use reth_ethereum_primitives::{Block, BlockBody, Transaction, TransactionSigned};
84 use reth_evm::{execute::BlockExecutor, ConfigureEvm, Evm};
85 use reth_evm_ethereum::EthEvmConfig;
86 use reth_primitives_traits::{Block as _, Recovered};
87 use reth_storage_api::DatabaseProviderFactory;
88 use reth_trie_common::{HashedStorage, KeccakKeyHasher};
89 use revm::{
90 database::{states::bundle_state::BundleRetention, CacheDB, EmptyDB, State},
91 state::{AccountInfo, Bytecode},
92 };
93 use std::{collections::BTreeMap, sync::Arc};
94
95 const ACCOUNT: Address = Address::repeat_byte(0xaa);
96 const SENDER: Address = Address::repeat_byte(0x11);
97
98 fn index(value: u64) -> BlockAccessIndex {
99 BlockAccessIndex::new(value)
100 }
101
102 fn state_update(
103 bal: &[AccountChanges],
104 coverage: AccountCoverage,
105 accounts: impl IntoIterator<Item = (B256, Account)>,
106 ) -> BalStateUpdate {
107 let factory = hashed_factory();
108 let provider = factory.database_provider_rw().unwrap();
109 for (address, account) in accounts {
110 provider.tx_ref().put::<tables::HashedAccounts>(address, account).unwrap();
111 }
112 provider.block_access_list_update(coverage, StorageProgress::START, bal).unwrap()
113 }
114
115 fn apply(changes: &AccountChanges, base: DownloadedAccount) -> BalStateUpdate {
116 let (coverage, account) = match base {
117 DownloadedAccount::Unknown => (AccountCoverage::START, None),
118 DownloadedAccount::Absent => (AccountCoverage::COMPLETE, None),
119 DownloadedAccount::Present(account) => (AccountCoverage::COMPLETE, Some(account)),
120 };
121 state_update(
122 std::slice::from_ref(changes),
123 coverage,
124 account.map(|account| (keccak256(changes.address()), account)),
125 )
126 }
127
128 #[test]
129 fn read_only_entries_write_nothing() {
130 let changes = AccountChanges::new(ACCOUNT).with_storage_read(U256::from(1));
131
132 let update = apply(&changes, DownloadedAccount::Unknown);
133
134 assert_eq!(update, BalStateUpdate::default());
135 }
136
137 #[test]
138 fn empty_slot_entries_write_nothing() {
139 let changes = AccountChanges::new(ACCOUNT)
140 .with_storage_change(SlotChanges::new(U256::from(1), vec![]));
141
142 let update = apply(&changes, DownloadedAccount::Unknown);
143
144 assert_eq!(update, BalStateUpdate::default());
145 }
146
147 #[test]
148 fn account_changes_with_empty_slots_write_no_storage() {
149 let changes = AccountChanges::new(ACCOUNT)
150 .with_balance_change(BalanceChange::new(index(1), U256::from(10)))
151 .with_storage_change(SlotChanges::new(U256::from(1), vec![]));
152
153 let update = apply(&changes, DownloadedAccount::Absent);
154
155 assert_eq!(
156 update.state.accounts[&keccak256(ACCOUNT)].as_ref().unwrap().balance,
157 U256::from(10)
158 );
159 assert!(update.state.storages.is_empty());
160 }
161
162 #[test]
163 fn accounts_not_downloaded_yet_are_left_unresolved() {
164 let changes = AccountChanges::new(ACCOUNT)
166 .with_balance_change(BalanceChange::new(index(1), U256::from(10)))
167 .with_nonce_change(NonceChange::new(index(1), 1))
168 .with_code_change(CodeChange::new(index(1), bytes!("6001")));
169
170 let update = apply(&changes, DownloadedAccount::Unknown);
171
172 assert_eq!(update.unresolved, vec![keccak256(ACCOUNT)]);
173 assert!(update.state.is_empty());
174 assert!(update.bytecodes.is_empty());
175 }
176
177 #[test]
178 #[allow(clippy::clone_on_copy)]
179 fn untouched_fields_keep_their_downloaded_values() {
180 let existing = Account::new(4, U256::from(9), Some(B256::repeat_byte(1)));
181 let changes = AccountChanges::new(ACCOUNT)
182 .with_balance_change(BalanceChange::new(index(1), U256::from(10)))
183 .with_balance_change(BalanceChange::new(index(2), U256::from(20)));
184
185 let update = apply(&changes, DownloadedAccount::Present(existing.clone()));
186 assert_eq!(
187 update.state.accounts[&keccak256(ACCOUNT)],
188 Some(Account { balance: U256::from(20), ..existing })
189 );
190
191 let update = apply(&changes, DownloadedAccount::Absent);
193 assert_eq!(
194 update.state.accounts[&keccak256(ACCOUNT)],
195 Some(Account { balance: U256::from(20), ..Default::default() })
196 );
197 }
198
199 #[test]
200 fn zeroed_slots_and_cleared_code_are_written() {
201 let existing = Account::new(1, U256::from(1), Some(B256::repeat_byte(1)));
202 let changes = AccountChanges::new(ACCOUNT)
203 .with_code_change(CodeChange::new(index(1), bytes!("6001")))
204 .with_code_change(CodeChange::new(index(2), Bytes::new()))
205 .with_storage_change(SlotChanges::new(
206 U256::from(1),
207 vec![
208 StorageChange::new(index(1), U256::from(5)),
209 StorageChange::new(index(2), U256::ZERO),
210 ],
211 ));
212
213 let update = apply(&changes, DownloadedAccount::Present(existing));
214
215 let hashed_address = keccak256(ACCOUNT);
216 assert_eq!(update.state.accounts[&hashed_address].as_ref().unwrap().bytecode_hash, None);
217 assert_eq!(
218 update.state.storages[&hashed_address],
219 HashedStorage::from_iter([(keccak256(B256::from(U256::from(1))), U256::ZERO)])
220 );
221 assert!(update.bytecodes.is_empty());
222 }
223
224 #[test]
225 fn an_account_the_block_empties_is_removed() {
226 let changes = AccountChanges::new(ACCOUNT)
227 .with_balance_change(BalanceChange::new(index(1), U256::from(100)))
228 .with_balance_change(BalanceChange::new(index(2), U256::ZERO));
229
230 let update = apply(&changes, DownloadedAccount::Absent);
231
232 assert_eq!(update.state.accounts[&keccak256(ACCOUNT)], None);
233 }
234
235 type FlatState = (BTreeMap<B256, Account>, BTreeMap<(B256, B256), U256>);
237
238 fn flatten(db: &CacheDB<EmptyDB>) -> FlatState {
239 let mut state = FlatState::default();
240 for (address, account) in &db.cache.accounts {
241 let hashed_address = keccak256(address);
242 state.0.insert(hashed_address, Account::from(&account.info));
243 for (slot, value) in &account.storage {
244 if !value.is_zero() {
245 state.1.insert((hashed_address, keccak256(B256::from(*slot))), *value);
246 }
247 }
248 }
249 state
250 }
251
252 #[allow(clippy::clone_on_copy)]
253 fn fold(mut state: FlatState, update: &HashedPostState) -> FlatState {
254 for (hashed_address, storage) in &update.storages {
255 for (slot, value) in &storage.storage {
256 if value.is_zero() {
257 state.1.remove(&(*hashed_address, *slot));
258 } else {
259 state.1.insert((*hashed_address, *slot), *value);
260 }
261 }
262 }
263 for (hashed_address, account) in &update.accounts {
264 match account {
265 Some(account) => {
266 state.0.insert(*hashed_address, account.clone());
267 }
268 None => {
269 state.0.remove(hashed_address);
270 state.1.retain(|(address, _), _| address != hashed_address);
271 }
272 }
273 }
274 state
275 }
276
277 #[test]
278 fn flat_state_excludes_zero_slots_and_deleted_account_storage() {
279 let mut db = CacheDB::<EmptyDB>::default();
280 for address in [ACCOUNT, SENDER] {
281 db.insert_account_info(address, AccountInfo::from_balance(U256::from(1)));
282 db.insert_account_storage(address, U256::from(1), U256::from(5)).unwrap();
283 db.insert_account_storage(address, U256::from(2), U256::ZERO).unwrap();
284 }
285 let pre = flatten(&db);
286 assert_eq!(pre.1.len(), 2);
287
288 let mut update = HashedPostState::default();
289 update.accounts.insert(keccak256(ACCOUNT), None);
290 let post = fold(pre, &update);
291
292 assert_eq!(
293 post.0,
294 BTreeMap::from([(
295 keccak256(SENDER),
296 Account { balance: U256::from(1), ..Default::default() },
297 )])
298 );
299 assert_eq!(
300 post.1,
301 BTreeMap::from([(
302 (keccak256(SENDER), keccak256(B256::from(U256::from(1)))),
303 U256::from(5)
304 )])
305 );
306 }
307
308 fn insert(db: &mut CacheDB<EmptyDB>, address: Address, nonce: u64, code: Bytes) {
309 let code = Bytecode::new_raw(code);
310 let info = AccountInfo {
311 nonce,
312 code_hash: code.hash_slow(),
313 code: Some(code),
314 ..Default::default()
315 };
316 db.insert_account_info(address, info);
317 }
318
319 fn tx(nonce: u64, to: TxKind, value: u64, input: Bytes) -> Recovered<TransactionSigned> {
320 let tx = Transaction::Legacy(TxLegacy {
321 nonce,
322 gas_price: 1,
323 gas_limit: 1_000_000,
324 to,
325 value: U256::from(value),
326 input,
327 ..Default::default()
328 });
329 Recovered::new_unchecked(
330 TransactionSigned::new_unhashed(tx, Signature::test_signature()),
331 SENDER,
332 )
333 }
334
335 #[test]
336 #[allow(clippy::clone_on_copy)]
337 fn applying_the_list_matches_execution() {
338 let contract = Address::repeat_byte(0xc0);
339 let beneficiary = Address::repeat_byte(0xbe);
340
341 let mut db = CacheDB::<EmptyDB>::new(Default::default());
342 insert(&mut db, BEACON_ROOTS_ADDRESS, 1, BEACON_ROOTS_CODE.clone());
343 insert(&mut db, HISTORY_STORAGE_ADDRESS, 1, HISTORY_STORAGE_CODE.clone());
344 insert(
345 &mut db,
346 WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS,
347 1,
348 WITHDRAWAL_REQUEST_PREDEPLOY_CODE.clone(),
349 );
350 db.insert_account_info(SENDER, AccountInfo::from_balance(U256::from(u64::MAX)));
351 insert(&mut db, contract, 1, bytes!("6000600155600354504360025534600455"));
353 db.insert_account_storage(contract, U256::from(1), U256::from(5)).unwrap();
354 db.insert_account_storage(contract, U256::from(3), U256::from(7)).unwrap();
355 let pre = flatten(&db);
356
357 let txs = [
358 tx(0, TxKind::Call(contract), 1, Bytes::new()),
360 tx(1, TxKind::Call(contract), 2, Bytes::new()),
361 tx(2, TxKind::Create, 0, bytes!("60fe60005360016000f3")),
363 tx(3, TxKind::Call(SENDER.create(4)), 100, Bytes::new()),
365 tx(
366 4,
367 TxKind::Create,
368 0,
369 [&[0x73][..], beneficiary.as_slice(), &[0xff]].concat().into(),
370 ),
371 ];
372 let header = Header {
373 number: 1,
374 timestamp: 1,
375 gas_limit: 30_000_000,
376 parent_beacon_block_root: Some(B256::repeat_byte(1)),
377 excess_blob_gas: Some(0),
378 blob_gas_used: Some(0),
379 block_access_list_hash: Some(B256::ZERO),
380 ..Default::default()
381 };
382 let withdrawal = Withdrawal { address: beneficiary, amount: 1, ..Default::default() };
383 let block = Block {
384 header,
385 body: BlockBody { withdrawals: Some(vec![withdrawal].into()), ..Default::default() },
386 }
387 .seal_slow();
388
389 let evm_config =
390 EthEvmConfig::new(Arc::new(ChainSpecBuilder::mainnet().amsterdam_activated().build()));
391 let mut state =
392 State::builder().with_database(&mut db).with_bundle_update().with_bal_builder().build();
393 {
394 let mut executor = evm_config.executor_for_block(&mut state, &block).unwrap();
395 executor.apply_pre_execution_changes().unwrap();
396 for tx in txs {
397 executor.evm_mut().db_mut().bump_bal_index();
398 executor.execute_transaction(tx).unwrap();
399 }
400 executor.evm_mut().db_mut().bump_bal_index();
401 executor.apply_post_execution_changes().unwrap();
402 }
403 let bal = state.take_built_alloy_bal().unwrap();
404 state.merge_transitions(BundleRetention::PlainState);
405 let bundle = state.take_bundle();
406
407 let update = state_update(
408 &bal,
409 AccountCoverage::COMPLETE,
410 pre.0.iter().map(|(address, account)| (*address, account.clone())),
411 );
412 let executed = HashedPostState::from_bundle_state::<KeccakKeyHasher>(bundle.state());
413
414 let post = fold(pre.clone(), &update.state);
415 assert!(update.unresolved.is_empty());
416 assert_eq!(post, fold(pre, &executed));
417
418 let slot = |address: Address, slot: u64| {
420 post.1.get(&(keccak256(address), keccak256(B256::from(U256::from(slot)))))
421 };
422 assert_eq!((slot(contract, 1), slot(contract, 4)), (None, Some(&U256::from(2))));
423 assert!(!post.0.contains_key(&keccak256(SENDER.create(4))));
424 assert_eq!(post.0[&keccak256(beneficiary)].balance, U256::from(100 + 1_000_000_000u64));
425 assert!(post.1.keys().any(|(address, _)| *address == keccak256(BEACON_ROOTS_ADDRESS)));
426 assert_eq!(
427 update.bytecodes,
428 B256Map::from_iter([(keccak256([0xfe]), Bytes::from_static(&[0xfe]))])
429 );
430 }
431}