Skip to main content

reth_snap_sync/catch_up/
apply.rs

1//! State updates implied by verified block access lists.
2//!
3//! [EIP-8189](https://eips.ethereum.org/EIPS/eip-8189#synchronization-algorithm) advances the
4//! pivot by applying later blocks' lists to the state downloaded so far. Each list records the
5//! final value of every field it changes, so untouched fields come from the downloaded account.
6
7use alloy_eip7928::AccountChanges;
8use alloy_primitives::{keccak256, map::B256Map, Bytes, B256};
9use reth_primitives_traits::Account;
10use reth_trie_common::{HashedPostState, HashedStorage};
11
12/// Changes one block access list makes to the downloaded state.
13#[derive(Clone, Debug, Default, PartialEq, Eq)]
14pub struct BalStateUpdate {
15    // Post-state of every downloaded account, and slots persisted ahead of their range.
16    pub(super) state: HashedPostState,
17    // Final code of accounts whose code changed, keyed by code hash.
18    pub(super) bytecodes: B256Map<Bytes>,
19    // Entries left out because their account is not downloaded yet.
20    pub(super) unresolved: Vec<B256>,
21}
22
23impl BalStateUpdate {
24    /// Post-state of every downloaded account, and slots persisted ahead of their range.
25    pub const fn state(&self) -> &HashedPostState {
26        &self.state
27    }
28
29    /// Final code of accounts whose code changed, keyed by code hash.
30    pub const fn bytecodes(&self) -> &B256Map<Bytes> {
31        &self.bytecodes
32    }
33
34    /// Hashed addresses of entries left out because their account is not downloaded yet.
35    pub fn unresolved(&self) -> &[B256] {
36        &self.unresolved
37    }
38
39    /// Consumes this update into the state it writes, the code it stores and its unresolved
40    /// entries.
41    pub fn into_parts(self) -> (HashedPostState, B256Map<Bytes>, Vec<B256>) {
42        (self.state, self.bytecodes, self.unresolved)
43    }
44
45    // Records the final values of the slots `changes` writes for `hashed_address`.
46    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/// What the downloaded state holds for an account a list changes.
55#[derive(Clone, Debug, PartialEq, Eq)]
56pub enum DownloadedAccount {
57    /// The account's range is not downloaded yet.
58    Unknown,
59    /// The account's range is downloaded and does not hold it.
60    Absent,
61    /// The downloaded account.
62    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        // Even a fully determined account is left to the download against the later root.
154        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    // Final state as a flat map: accounts, and non-zero slots by hashed address and slot.
192    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        // Zeroes slot 1, reads slot 3, stores the block number in slot 2 and the call value in 4.
292        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            // Repeated writes, the second of slots 1 and 2 being no-ops.
299            tx(0, TxKind::Call(contract), 1, Bytes::new()),
300            tx(1, TxKind::Call(contract), 2, Bytes::new()),
301            // Deploys `INVALID` as runtime code.
302            tx(2, TxKind::Create, 0, bytes!("60fe60005360016000f3")),
303            // Funds an address, then creates and destroys a contract there in one transaction.
304            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        // The block did what the cases need.
352        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}