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    };
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        // Even a fully determined account is left to the download against the later root.
151        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    // Final state as a flat map: accounts, and non-zero slots by hashed address and slot.
189    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        // Zeroes slot 1, reads slot 3, stores the block number in slot 2 and the call value in 4.
274        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            // Repeated writes, the second of slots 1 and 2 being no-ops.
281            tx(0, TxKind::Call(contract), 1, Bytes::new()),
282            tx(1, TxKind::Call(contract), 2, Bytes::new()),
283            // Deploys `INVALID` as runtime code.
284            tx(2, TxKind::Create, 0, bytes!("60fe60005360016000f3")),
285            // Funds an address, then creates and destroys a contract there in one transaction.
286            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        // The block did what the cases need.
341        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}