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