Skip to main content

reth_snap_sync/catch_up/
store.rs

1//! Applies verified block access lists to the downloaded state, in strict block order.
2//!
3//! A list commits with the progress it advances, so recorded progress never runs ahead of the
4//! state it describes, and a block whose list is still missing leaves every later one pending.
5
6use crate::{
7    common::SnapRecord, repair::StoredRepairs, AccountCoverage, BalStateUpdate, DownloadedAccount,
8    SnapAccountStore, SnapAttemptStore, SnapBytecodeStore, SnapStorageStore, SnapSyncError,
9    SnapWrite, StorageProgress,
10};
11use alloy_eip7928::AccountChanges;
12use alloy_eips::BlockNumHash;
13use alloy_primitives::{keccak256, B256, KECCAK256_EMPTY};
14use reth_db_api::{
15    tables,
16    transaction::{DbTx, DbTxMut},
17};
18use reth_primitives_traits::Account;
19use reth_storage_api::{
20    BlockHashReader, DBProvider, MetadataProvider, MetadataWriter, SnapAttemptId, StateWriter,
21};
22use serde::{Deserialize, Serialize};
23
24/// Persistence for the block access lists an attempt applies to its downloaded state.
25///
26/// Blanket-implemented over the node's writers, so the state a list changes, the code it deploys
27/// and the progress it advances join the caller's transaction and commit together or not at all.
28pub trait SnapCatchUpStore {
29    /// Returns the progress recorded for the attempt `write` belongs to, if any.
30    fn catch_up_progress(&self, write: SnapWrite)
31        -> Result<Option<CatchUpProgress>, SnapSyncError>;
32
33    /// Returns what the downloaded state holds for `hashed_address` under `coverage`.
34    fn downloaded_account(
35        &self,
36        coverage: AccountCoverage,
37        hashed_address: B256,
38    ) -> Result<DownloadedAccount, SnapSyncError>
39    where
40        Self: DBProvider;
41
42    /// Returns the state update implied by `bal`, reading downloaded accounts from this provider.
43    ///
44    /// The list must already be verified against its header. Accounts outside `coverage` remain
45    /// unresolved, except for slots of contracts `storage` holds ahead of their range.
46    fn block_access_list_update(
47        &self,
48        coverage: AccountCoverage,
49        storage: StorageProgress,
50        bal: &[AccountChanges],
51    ) -> Result<BalStateUpdate, SnapSyncError>
52    where
53        Self: DBProvider;
54
55    /// Applies `bal` to the downloaded state and records `block` as the last one applied.
56    ///
57    /// The list must be authenticated against `block`'s header commitment, and `block` must be
58    /// canonical and the child of the last applied one.
59    fn commit_block_access_list(
60        &self,
61        write: SnapWrite,
62        block: BlockNumHash,
63        parent: B256,
64        bal: &[AccountChanges],
65    ) -> Result<CatchUpProgress, SnapSyncError>
66    where
67        Self: BlockHashReader + MetadataWriter + StateWriter + DBProvider<Tx: DbTxMut>;
68}
69
70/// How far past the pivot the downloaded state has been carried.
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub struct CatchUpProgress {
73    // Last block whose list is applied, the pivot until one is.
74    applied: BlockNumHash,
75}
76
77impl CatchUpProgress {
78    /// Nothing applied past `pivot`.
79    pub const fn at_pivot(pivot: BlockNumHash) -> Self {
80        Self { applied: pivot }
81    }
82
83    /// Last block whose list is applied.
84    pub const fn applied(&self) -> BlockNumHash {
85        self.applied
86    }
87
88    /// Last applied block still canonical after a reorg back to `ancestor`.
89    pub const fn resume_after(&self, ancestor: BlockNumHash) -> BlockNumHash {
90        if self.applied.number <= ancestor.number {
91            self.applied
92        } else {
93            ancestor
94        }
95    }
96
97    /// Number of the block whose list comes next.
98    pub const fn next(&self) -> u64 {
99        self.applied.number + 1
100    }
101
102    // The progress after applying `block`, which must be the child of the applied one.
103    fn advance(&self, block: BlockNumHash, parent: B256) -> Result<Self, SnapSyncError> {
104        if block.number != self.next() {
105            return Err(SnapSyncError::OutOfOrderBlock { expected: self.next(), got: block.number })
106        }
107        if parent != self.applied.hash {
108            return Err(SnapSyncError::ForkedBlock { expected: self.applied.hash, got: parent })
109        }
110        Ok(Self { applied: block })
111    }
112
113    /// Persists this progress as `attempt`'s record.
114    pub(crate) fn write(
115        &self,
116        provider: &impl MetadataWriter,
117        attempt: SnapAttemptId,
118    ) -> Result<(), SnapSyncError> {
119        StoredCatchUpProgress::new(attempt, *self).write(provider)
120    }
121}
122
123// The progress record as persisted, tied to the attempt that recorded it.
124#[derive(Serialize, Deserialize)]
125struct StoredCatchUpProgress {
126    // Encoding version, checked before the rest is decoded.
127    version: u32,
128    // Attempt the progress belongs to.
129    attempt: SnapAttemptId,
130    // Number of the last block whose list is applied.
131    block: u64,
132    // Hash of that block, which the next one builds on.
133    hash: B256,
134}
135
136impl SnapRecord for StoredCatchUpProgress {
137    const KEY: &'static str = "snap_catch_up_progress";
138    const VERSION: u32 = 1;
139}
140
141impl StoredCatchUpProgress {
142    // `progress` for `attempt` at this build's version.
143    const fn new(attempt: SnapAttemptId, progress: CatchUpProgress) -> Self {
144        Self {
145            version: Self::VERSION,
146            attempt,
147            block: progress.applied.number,
148            hash: progress.applied.hash,
149        }
150    }
151
152    const fn progress(&self) -> CatchUpProgress {
153        CatchUpProgress::at_pivot(BlockNumHash::new(self.block, self.hash))
154    }
155}
156
157impl<T: MetadataProvider> SnapCatchUpStore for T {
158    // A record left by another attempt reads as no progress.
159    fn catch_up_progress(
160        &self,
161        write: SnapWrite,
162    ) -> Result<Option<CatchUpProgress>, SnapSyncError> {
163        self.authorize_snap_write(write)?;
164        let Some(stored) = StoredCatchUpProgress::read(self)? else { return Ok(None) };
165        Ok((stored.attempt == write.attempt()).then(|| stored.progress()))
166    }
167
168    fn downloaded_account(
169        &self,
170        coverage: AccountCoverage,
171        hashed_address: B256,
172    ) -> Result<DownloadedAccount, SnapSyncError>
173    where
174        Self: DBProvider,
175    {
176        // Ranges cover the key space in order, so anything from the cursor on is still pending.
177        if coverage.next().is_some_and(|next| hashed_address >= next) {
178            return Ok(DownloadedAccount::Unknown)
179        }
180        Ok(self
181            .tx_ref()
182            .get::<tables::HashedAccounts>(hashed_address)?
183            .map_or(DownloadedAccount::Absent, DownloadedAccount::Present))
184    }
185
186    fn block_access_list_update(
187        &self,
188        coverage: AccountCoverage,
189        storage: StorageProgress,
190        bal: &[AccountChanges],
191    ) -> Result<BalStateUpdate, SnapSyncError>
192    where
193        Self: DBProvider,
194    {
195        let mut update = BalStateUpdate::default();
196        for account_changes in bal {
197            let account_info = account_changes.account_info();
198            // Read-only entries record accesses, not changes.
199            if !account_info.changes_state_root(account_changes) {
200                continue
201            }
202
203            let hashed_address = keccak256(account_changes.address());
204            let mut account = match self.downloaded_account(coverage, hashed_address)? {
205                // Its range is downloaded against a later root, which includes this change. Slots
206                // persisted ahead of it may predate the change, so they follow the list.
207                DownloadedAccount::Unknown => {
208                    if storage.has_slots(hashed_address) && account_changes.has_storage_changes() {
209                        update.insert_storage(hashed_address, account_changes);
210                    }
211                    update.unresolved.push(hashed_address);
212                    continue
213                }
214                DownloadedAccount::Absent => Account::default(),
215                DownloadedAccount::Present(account) => account,
216            };
217
218            account.apply_bal_info(account_info);
219            // Stored accounts represent empty code with no code hash.
220            account.bytecode_hash = account.bytecode_hash.filter(|hash| *hash != KECCAK256_EMPTY);
221            // Execution removes accounts a block leaves empty, see EIP-161.
222            update.state.accounts.insert(hashed_address, (!account.is_empty()).then_some(account));
223            if account_changes.has_storage_changes() {
224                update.insert_storage(hashed_address, account_changes);
225            }
226            if let Some((code_hash, code)) = account_info
227                .code_hash
228                .zip(account_changes.code_post_state().filter(|code| !code.is_empty()))
229            {
230                update.bytecodes.insert(code_hash, code.clone());
231            }
232        }
233        Ok(update)
234    }
235
236    // Every check runs before the first write, so a refused list changes nothing.
237    fn commit_block_access_list(
238        &self,
239        write: SnapWrite,
240        block: BlockNumHash,
241        parent: B256,
242        bal: &[AccountChanges],
243    ) -> Result<CatchUpProgress, SnapSyncError>
244    where
245        Self: BlockHashReader + MetadataWriter + StateWriter + DBProvider<Tx: DbTxMut>,
246    {
247        // Downloaded ranges are proved against the pivot, so it must still be canonical too.
248        let attempt = self.authorize_canonical_snap_write(write)?;
249        // The chain may have reorged since the list was requested.
250        if self.block_hash(block.number)? != Some(block.hash) {
251            return Err(SnapSyncError::NonCanonicalBlock { block: block.number, hash: block.hash })
252        }
253        let advanced = self
254            .catch_up_progress(write)?
255            .ok_or(SnapSyncError::NoCatchUpProgress)?
256            .advance(block, parent)?;
257        // Pending ranges are proved against the pivot, so they hold no change past it.
258        if block.number > attempt.pivot().number {
259            return Err(SnapSyncError::BlockPastPivot {
260                pivot: attempt.pivot().number,
261                block: block.number,
262            })
263        }
264        let coverage = self.account_coverage(write)?.ok_or(SnapSyncError::NoCoverage)?;
265        let storage = match coverage.next() {
266            Some(origin) => self.storage_progress(write, origin)?,
267            None => StorageProgress::START,
268        };
269        let update = self.block_access_list_update(coverage, storage, bal)?;
270
271        // Entries left unresolved are downloaded whole against the pivot, which already includes
272        // this block, so nothing here has to remember them.
273        let (state, bytecodes, _unresolved) = update.into_parts();
274        for (hashed_address, account) in &state.accounts {
275            // Execution clears the storage of an account it removes, which the list leaves to
276            // whoever applies it.
277            if account.is_none() {
278                self.remove::<tables::HashedStorages>(*hashed_address..=*hashed_address)?;
279            }
280        }
281        // A canonical list overwrites the stale values of the fields it changes, in the accounts it
282        // writes.
283        let mut repairs = self.snap_repairs(write)?;
284        let mut resolved = false;
285        for changes in bal {
286            if repairs.is_empty() {
287                break
288            }
289            let hashed_address = keccak256(changes.address());
290            if state.accounts.contains_key(&hashed_address) {
291                resolved |= repairs.resolve_changes(hashed_address, changes);
292            }
293        }
294        self.commit_bytecodes(write, bytecodes.into_iter().collect())?;
295        self.write_hashed_state(&state.into_sorted())?;
296        if resolved {
297            StoredRepairs::store(self, write.attempt(), repairs)?;
298        }
299        advanced.write(self, write.attempt())?;
300        Ok(advanced)
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307    use crate::{
308        test_utils::{
309            account, hashed_factory, key, state_root, storage_root_of, BalChain, SnapStateSnapshot,
310        },
311        SnapAccountStore, SnapGeneration, StateRepairs, StorageChunk,
312    };
313    use alloy_eip7928::{
314        BalanceChange, BlockAccessIndex, CodeChange, NonceChange, SlotChanges, StorageChange,
315    };
316    use alloy_primitives::{bytes, keccak256, map::B256Map, Address, Bytes, U256};
317    use reth_primitives_traits::Account;
318    use reth_provider::{
319        test_utils::{insert_headers, MockNodeTypesWithDB},
320        DatabaseProviderFactory, ProviderFactory,
321    };
322    use reth_trie_common::{HashedStorage, TrieAccount};
323
324    type Factory = ProviderFactory<MockNodeTypesWithDB>;
325
326    // The account the fixture lists change, holding one storage slot.
327    const CHANGED: Address = Address::repeat_byte(0xaa);
328    const SLOT: U256 = U256::from_limbs([1, 0, 0, 0]);
329
330    fn hashed_slot() -> B256 {
331        keccak256(B256::from(SLOT))
332    }
333
334    fn code() -> Bytes {
335        bytes!("6001")
336    }
337
338    // The downloaded trie: filler accounts a partial range can stop inside, and the account the
339    // lists change, holding one slot and nothing but a balance, so a list can empty it.
340    fn accounts() -> Vec<(B256, TrieAccount)> {
341        let mut changed = account(0);
342        changed.storage_root = storage_root_of(&[(hashed_slot(), U256::from(7))]);
343        let mut accounts: Vec<_> = (1..=3).map(|nonce| (key(nonce), account(nonce))).collect();
344        accounts.push((keccak256(CHANGED), changed));
345        accounts.sort_by_key(|(hashed_address, _)| *hashed_address);
346        accounts
347    }
348
349    // Position of the changed account in the trie.
350    fn changed_index(accounts: &[(B256, TrieAccount)]) -> usize {
351        accounts
352            .iter()
353            .position(|(hashed_address, _)| *hashed_address == keccak256(CHANGED))
354            .unwrap()
355    }
356
357    // An attempt started at block 1, with `served` of the trie's accounts downloaded and its
358    // block access list progress recorded, then moved to pivot 2.
359    fn started(accounts: &[(B256, TrieAccount)], served: usize) -> (Factory, SnapWrite) {
360        let factory = hashed_factory();
361        insert_headers(&factory, &chain().headers);
362        let provider = factory.database_provider_rw().unwrap();
363        let write = provider.start_snap_attempt(generation(1, state_root(accounts))).unwrap();
364        provider.start_account_coverage(write).unwrap();
365        // A partial range needs a proof placing the accounts it leaves out.
366        let targets = (served < accounts.len()).then(|| accounts[served - 1].0);
367        let range =
368            crate::test_utils::verified_range(accounts, 0..served, B256::ZERO, targets.as_slice());
369        let storages = B256Map::from_iter([(
370            keccak256(CHANGED),
371            HashedStorage::from_iter([(hashed_slot(), U256::from(7))]),
372        )]);
373        provider
374            .commit_account_range(
375                write,
376                &range,
377                if served == accounts.len() { storages } else { B256Map::default() },
378                Vec::new(),
379            )
380            .unwrap();
381        let write =
382            provider.advance_snap_pivot(write, generation(2, state_root(accounts))).unwrap();
383        provider.commit().unwrap();
384        (factory, write)
385    }
386
387    // Canonical blocks 0 through 3, pivoted at block 1. The lists the tests apply are their own, as
388    // the store does not authenticate them.
389    fn chain() -> BalChain {
390        BalChain::new(1, [Vec::new(), Vec::new()])
391    }
392
393    // Generation anchored to block `number` of the fixture chain.
394    fn generation(number: u64, state_root: B256) -> SnapGeneration {
395        SnapGeneration::new(chain().block(number as usize - 1), state_root)
396    }
397
398    // Block `number` of the fixture chain, with the hash of its parent.
399    fn block(number: u64) -> (BlockNumHash, B256) {
400        let chain = chain();
401        (chain.block(number as usize - 1), chain.block(number as usize - 2).hash)
402    }
403
404    // The fixture chain with its last block replaced by one changing a balance.
405    fn replacement() -> BalChain {
406        BalChain::new(1, [Vec::new(), credit(1)])
407    }
408
409    // The attempt carried through block 2 and anchored to block 3, which a reorg then replaced,
410    // leaving the applied block canonical. Returns the orphaned pivot.
411    fn orphaned_pivot(accounts: &[(B256, TrieAccount)]) -> (Factory, SnapWrite, BlockNumHash) {
412        let (factory, write) = started(accounts, accounts.len());
413        let provider = factory.database_provider_rw().unwrap();
414        let (applied, parent) = block(2);
415        provider.commit_block_access_list(write, applied, parent, &[]).unwrap();
416        let write =
417            provider.advance_snap_pivot(write, generation(3, state_root(accounts))).unwrap();
418        provider.commit().unwrap();
419        let replacement = replacement();
420        assert_eq!(replacement.block(1), applied);
421        replacement.replace_tip(&factory);
422        (factory, write, block(3).0)
423    }
424
425    fn index(value: u64) -> BlockAccessIndex {
426        BlockAccessIndex::new(value)
427    }
428
429    // A list crediting `CHANGED` with `balance`.
430    fn credit(balance: u64) -> Vec<AccountChanges> {
431        vec![AccountChanges::new(CHANGED)
432            .with_balance_change(BalanceChange::new(index(1), U256::from(balance)))]
433    }
434
435    fn stored(provider: &impl DBProvider, hashed_address: B256) -> Option<Account> {
436        provider.tx_ref().get::<tables::HashedAccounts>(hashed_address).unwrap()
437    }
438
439    #[test]
440    fn progress_starts_at_the_first_pivot_and_resumes_where_it_left_off() {
441        let accounts = accounts();
442        let (factory, write) = started(&accounts, accounts.len());
443        let provider = factory.database_provider_rw().unwrap();
444        let (block, parent) = block(2);
445
446        // The pivot moved to block 2, but ranges committed at block 1 still need its list.
447        assert_eq!(
448            provider.catch_up_progress(write).unwrap().unwrap().applied(),
449            generation(1, B256::ZERO).target()
450        );
451
452        let progress =
453            provider.commit_block_access_list(write, block, parent, &credit(10)).unwrap();
454        assert_eq!(progress.applied(), block);
455        assert_eq!(progress.next(), 3);
456        // Restarting the attempt re-applies nothing.
457        assert_eq!(provider.catch_up_progress(write).unwrap(), Some(progress));
458    }
459
460    #[test]
461    fn a_list_commits_with_the_block_it_carries_the_state_to() {
462        let accounts = accounts();
463        let (factory, write) = started(&accounts, accounts.len());
464        let (block, parent) = block(2);
465        let changes = vec![AccountChanges::new(CHANGED)
466            .with_balance_change(BalanceChange::new(index(1), U256::from(10)))
467            .with_code_change(CodeChange::new(index(1), code()))
468            .with_storage_change(SlotChanges::new(
469                SLOT,
470                vec![StorageChange::new(index(1), U256::from(9))],
471            ))];
472
473        let provider = factory.database_provider_rw().unwrap();
474        provider.commit_block_access_list(write, block, parent, &changes).unwrap();
475        provider.commit().unwrap();
476
477        let provider = factory.database_provider_ro().unwrap();
478        let applied = stored(&provider, keccak256(CHANGED)).unwrap();
479        assert_eq!(applied.balance, U256::from(10));
480        assert_eq!(applied.bytecode_hash, Some(keccak256(code())));
481        // The code the list deploys is stored with the account referencing it.
482        assert!(provider.tx_ref().get::<tables::Bytecodes>(keccak256(code())).unwrap().is_some());
483        assert_eq!(
484            crate::test_utils::stored_slots(&provider, keccak256(CHANGED)),
485            [(hashed_slot(), U256::from(9))]
486        );
487        assert_eq!(provider.catch_up_progress(write).unwrap().unwrap().applied(), block);
488    }
489
490    #[test]
491    fn an_interrupted_application_leaves_nothing_behind() {
492        let accounts = accounts();
493        let (factory, write) = started(&accounts, accounts.len());
494        let (block, parent) = block(2);
495
496        let provider = factory.database_provider_rw().unwrap();
497        provider.commit_block_access_list(write, block, parent, &credit(10)).unwrap();
498        drop(provider);
499
500        let provider = factory.database_provider_ro().unwrap();
501        assert_eq!(stored(&provider, keccak256(CHANGED)).unwrap().balance, U256::from(1));
502        assert_eq!(provider.catch_up_progress(write).unwrap().unwrap().applied().number, 1);
503    }
504
505    #[test]
506    fn a_block_already_applied_is_refused() {
507        let accounts = accounts();
508        let (factory, write) = started(&accounts, accounts.len());
509        let provider = factory.database_provider_rw().unwrap();
510        let (block, parent) = block(2);
511        provider.commit_block_access_list(write, block, parent, &credit(10)).unwrap();
512
513        let duplicate = provider.commit_block_access_list(write, block, parent, &credit(20));
514
515        assert!(matches!(duplicate, Err(SnapSyncError::OutOfOrderBlock { expected: 3, got: 2 })));
516        assert_eq!(stored(&provider, keccak256(CHANGED)).unwrap().balance, U256::from(10));
517    }
518
519    #[test]
520    fn a_block_the_applied_state_has_not_reached_is_refused() {
521        let accounts = accounts();
522        let (factory, write) = started(&accounts, accounts.len());
523        let provider = factory.database_provider_rw().unwrap();
524        let (block, parent) = block(3);
525
526        let gap = provider.commit_block_access_list(write, block, parent, &credit(10));
527
528        assert!(matches!(gap, Err(SnapSyncError::OutOfOrderBlock { expected: 2, got: 3 })));
529        assert_eq!(stored(&provider, keccak256(CHANGED)).unwrap().balance, U256::from(1));
530    }
531
532    #[test]
533    fn a_block_the_canonical_chain_no_longer_holds_is_refused() {
534        let accounts = accounts();
535        let (factory, write) = started(&accounts, accounts.len());
536        let provider = factory.database_provider_rw().unwrap();
537        let (_, parent) = block(2);
538        let orphaned = BlockNumHash::new(2, B256::repeat_byte(0xee));
539
540        let refused = provider.commit_block_access_list(write, orphaned, parent, &credit(10));
541
542        assert!(matches!(refused, Err(SnapSyncError::NonCanonicalBlock { block: 2, .. })));
543        assert_eq!(stored(&provider, keccak256(CHANGED)).unwrap().balance, U256::from(1));
544    }
545
546    #[test]
547    fn a_list_proved_against_an_orphaned_pivot_is_refused() {
548        let accounts = accounts();
549        let (factory, write, orphaned) = orphaned_pivot(&accounts);
550        let provider = factory.database_provider_rw().unwrap();
551        let (applied, _) = block(2);
552        // The replacement block continues the applied one, so only the pivot check refuses it.
553        let (block, parent) = (replacement().block(2), applied.hash);
554        let before = SnapStateSnapshot::read(&provider);
555
556        let refused = provider.commit_block_access_list(write, block, parent, &credit(10));
557
558        assert!(matches!(
559            refused,
560            Err(SnapSyncError::NonCanonicalBlock { block: 3, hash }) if hash == orphaned.hash
561        ));
562        assert_eq!(provider.catch_up_progress(write).unwrap().unwrap().applied(), applied);
563        assert_eq!(SnapStateSnapshot::read(&provider), before);
564        provider.commit().unwrap();
565        assert_eq!(SnapStateSnapshot::read(&factory.database_provider_ro().unwrap()), before);
566    }
567
568    #[test]
569    fn an_orphaned_pivot_cannot_be_replaced() {
570        let accounts = accounts();
571        let (factory, write, orphaned) = orphaned_pivot(&accounts);
572        let provider = factory.database_provider_rw().unwrap();
573        let before = SnapStateSnapshot::read(&provider);
574        let replacement = SnapGeneration::new(replacement().block(2), state_root(&accounts));
575
576        let refused = provider.advance_snap_pivot(write, replacement);
577
578        // Re-anchoring would leave the ranges downloaded against the orphan looking canonical.
579        assert!(matches!(
580            refused,
581            Err(SnapSyncError::NonCanonicalBlock { block: 3, hash }) if hash == orphaned.hash
582        ));
583        assert_eq!(provider.authorize_snap_write(write).unwrap().pivot(), orphaned);
584        assert_eq!(SnapStateSnapshot::read(&provider), before);
585        provider.commit().unwrap();
586        assert_eq!(SnapStateSnapshot::read(&factory.database_provider_ro().unwrap()), before);
587    }
588
589    #[test]
590    fn a_block_building_on_another_chain_is_refused() {
591        let accounts = accounts();
592        let (factory, write) = started(&accounts, accounts.len());
593        let provider = factory.database_provider_rw().unwrap();
594        let (block, _) = block(2);
595
596        let forked =
597            provider.commit_block_access_list(write, block, B256::repeat_byte(0xee), &credit(10));
598
599        assert!(matches!(forked, Err(SnapSyncError::ForkedBlock { .. })));
600        assert_eq!(provider.catch_up_progress(write).unwrap().unwrap().applied().number, 1);
601    }
602
603    #[test]
604    fn a_block_that_changes_nothing_still_carries_the_state_past_it() {
605        let accounts = accounts();
606        let (factory, write) = started(&accounts, accounts.len());
607        let provider = factory.database_provider_rw().unwrap();
608        let (block, parent) = block(2);
609        // A list a peer holds but that touches no state, as against one it does not hold.
610        let read_only = vec![AccountChanges::new(CHANGED).with_storage_read(SLOT)];
611
612        let progress = provider.commit_block_access_list(write, block, parent, &read_only).unwrap();
613
614        assert_eq!(progress.applied(), block);
615        assert_eq!(stored(&provider, keccak256(CHANGED)).unwrap().balance, U256::from(1));
616    }
617
618    #[test]
619    fn an_account_the_list_empties_loses_its_storage() {
620        let accounts = accounts();
621        let (factory, write) = started(&accounts, accounts.len());
622        let provider = factory.database_provider_rw().unwrap();
623        let (block, parent) = block(2);
624
625        provider.commit_block_access_list(write, block, parent, &credit(0)).unwrap();
626
627        assert_eq!(stored(&provider, keccak256(CHANGED)), None);
628        assert!(crate::test_utils::stored_slots(&provider, keccak256(CHANGED)).is_empty());
629    }
630
631    #[test]
632    fn a_block_past_the_pivot_is_refused() {
633        let accounts = accounts();
634        let (factory, write) = started(&accounts, accounts.len());
635        let provider = factory.database_provider_rw().unwrap();
636        let (block, parent) = block(2);
637        provider.commit_block_access_list(write, block, parent, &credit(10)).unwrap();
638        let (block, parent) = self::block(3);
639
640        let refused = provider.commit_block_access_list(write, block, parent, &credit(20));
641
642        assert!(matches!(refused, Err(SnapSyncError::BlockPastPivot { pivot: 2, block: 3 })));
643        assert_eq!(stored(&provider, keccak256(CHANGED)).unwrap().balance, U256::from(10));
644    }
645
646    #[test]
647    fn an_account_outside_the_coverage_is_left_to_its_range() {
648        let accounts = accounts();
649        let index = changed_index(&accounts);
650        let (factory, write) = started(&accounts, index);
651        let provider = factory.database_provider_rw().unwrap();
652        let (block, parent) = block(2);
653
654        let progress =
655            provider.commit_block_access_list(write, block, parent, &credit(10)).unwrap();
656
657        // The range still to download authenticates against a root that already holds the change.
658        assert_eq!(stored(&provider, keccak256(CHANGED)), None);
659        assert_eq!(progress.applied(), block);
660    }
661
662    #[test]
663    fn slots_persisted_ahead_of_their_range_follow_the_lists() {
664        let factory = hashed_factory();
665        insert_headers(&factory, &chain().headers);
666        let provider = factory.database_provider_rw().unwrap();
667        let write = provider.start_snap_attempt(generation(1, state_root(&accounts()))).unwrap();
668        provider.start_account_coverage(write).unwrap();
669        let (changed, stored_slot) = (keccak256(CHANGED), (hashed_slot(), U256::from(7)));
670        let chunk = StorageChunk::new(
671            changed,
672            storage_root_of(&[stored_slot]),
673            B256::ZERO,
674            vec![stored_slot],
675            None,
676        );
677        provider.commit_storage_chunk(write, B256::ZERO, chunk).unwrap();
678        let mut moved = accounts();
679        let position = changed_index(&moved);
680        moved[position].1.storage_root = storage_root_of(&[(hashed_slot(), U256::from(9))]);
681        let write = provider.advance_snap_pivot(write, generation(2, state_root(&moved))).unwrap();
682        let (block, parent) = block(2);
683        let changes = vec![AccountChanges::new(CHANGED).with_storage_change(SlotChanges::new(
684            SLOT,
685            vec![StorageChange::new(index(1), U256::from(9))],
686        ))];
687
688        provider.commit_block_access_list(write, block, parent, &changes).unwrap();
689
690        // Only the slots follow: the account itself comes whole with its range.
691        assert_eq!(stored(&provider, changed), None);
692        assert_eq!(
693            crate::test_utils::stored_slots(&provider, changed),
694            [(hashed_slot(), U256::from(9))]
695        );
696        let range = crate::test_utils::verified_range(&moved, 0..moved.len(), B256::ZERO, &[]);
697        let coverage =
698            provider.commit_account_range(write, &range, B256Map::default(), Vec::new()).unwrap();
699        assert!(coverage.is_complete());
700    }
701
702    #[test]
703    #[allow(clippy::clone_on_copy)]
704    fn skipped_storage_from_an_abandoned_attempt_is_not_reused() {
705        let factory = hashed_factory();
706        insert_headers(&factory, &chain().headers);
707        let provider = factory.database_provider_rw().unwrap();
708        let (changed, later) = (keccak256(CHANGED), B256::repeat_byte(0xff));
709        let stale = (B256::ZERO, U256::from(7));
710        let mut contract = account(1);
711        contract.storage_root = storage_root_of(&[stale]);
712        let mut accounts = vec![(changed, contract.clone()), (later, contract.clone())];
713        let write = provider.start_snap_attempt(generation(1, state_root(&accounts))).unwrap();
714        provider.start_account_coverage(write).unwrap();
715        provider
716            .commit_storage_chunk(
717                write,
718                B256::ZERO,
719                StorageChunk::new(changed, contract.storage_root, B256::ZERO, vec![stale], None),
720            )
721            .unwrap();
722
723        accounts[0].1.storage_root = reth_trie_common::EMPTY_ROOT_HASH;
724        let write = provider.start_snap_attempt(generation(1, state_root(&accounts))).unwrap();
725        provider.start_account_coverage(write).unwrap();
726        provider
727            .commit_storage_chunk(
728                write,
729                B256::ZERO,
730                StorageChunk::new(later, contract.storage_root, B256::ZERO, vec![stale], None),
731            )
732            .unwrap();
733
734        let new_slot = (hashed_slot(), U256::from(9));
735        accounts[0].1.storage_root = storage_root_of(&[new_slot]);
736        let write =
737            provider.advance_snap_pivot(write, generation(2, state_root(&accounts))).unwrap();
738        let (block, parent) = block(2);
739        let changes = vec![AccountChanges::new(CHANGED).with_storage_change(SlotChanges::new(
740            SLOT,
741            vec![StorageChange::new(index(1), new_slot.1)],
742        ))];
743        provider.commit_block_access_list(write, block, parent, &changes).unwrap();
744
745        assert_eq!(crate::test_utils::stored_slots(&provider, changed), [new_slot]);
746        let range =
747            crate::test_utils::verified_range(&accounts, 0..accounts.len(), B256::ZERO, &[]);
748        assert!(provider
749            .commit_account_range(write, &range, B256Map::default(), Vec::new())
750            .unwrap()
751            .is_complete());
752    }
753
754    #[test]
755    fn a_list_from_a_replaced_attempt_changes_nothing() {
756        let accounts = accounts();
757        let (factory, write) = started(&accounts, accounts.len());
758        let provider = factory.database_provider_rw().unwrap();
759        provider.advance_snap_pivot(write, generation(3, B256::repeat_byte(0xcc))).unwrap();
760        let (block, parent) = block(2);
761
762        let refused = provider.commit_block_access_list(write, block, parent, &credit(10));
763
764        assert!(matches!(refused, Err(SnapSyncError::StaleWrite { .. })));
765        assert!(matches!(provider.catch_up_progress(write), Err(SnapSyncError::StaleWrite { .. })));
766    }
767
768    #[test]
769    fn a_record_this_build_cannot_read_is_reported() {
770        let accounts = accounts();
771        let (factory, write) = started(&accounts, accounts.len());
772        let provider = factory.database_provider_rw().unwrap();
773        provider.write_metadata(StoredCatchUpProgress::KEY, br#"{"version":0}"#.to_vec()).unwrap();
774
775        assert!(matches!(
776            provider.catch_up_progress(write),
777            Err(SnapSyncError::UnsupportedRecord {
778                key: StoredCatchUpProgress::KEY,
779                version: Some(0)
780            })
781        ));
782    }
783
784    #[test]
785    fn a_list_resolves_the_repairs_it_overwrites() {
786        let accounts = accounts();
787        let (factory, write) = started(&accounts, accounts.len());
788        let provider = factory.database_provider_rw().unwrap();
789        // The orphaned branch changed the balance and the nonce.
790        let orphaned = AccountChanges::new(CHANGED)
791            .with_balance_change(BalanceChange::new(index(1), U256::from(1)))
792            .with_nonce_change(NonceChange::new(index(1), 1));
793        let mut repairs = StateRepairs::default();
794        repairs.insert_changes(keccak256(CHANGED), &orphaned);
795        provider.schedule_snap_repairs(write, repairs).unwrap();
796        let (block, parent) = block(2);
797
798        // The new branch changes the balance only.
799        provider.commit_block_access_list(write, block, parent, &credit(10)).unwrap();
800
801        let mut expected = StateRepairs::default();
802        expected.insert_changes(
803            keccak256(CHANGED),
804            &AccountChanges::new(CHANGED).with_nonce_change(NonceChange::new(index(1), 1)),
805        );
806        assert_eq!(provider.snap_repairs(write).unwrap(), expected);
807    }
808
809    #[test]
810    fn repairs_of_an_account_outside_the_coverage_stay_scheduled() {
811        let accounts = accounts();
812        let (factory, write) = started(&accounts, changed_index(&accounts));
813        let provider = factory.database_provider_rw().unwrap();
814        let mut repairs = StateRepairs::default();
815        repairs.insert_account(keccak256(CHANGED));
816        provider.schedule_snap_repairs(write, repairs.clone()).unwrap();
817        let (block, parent) = block(2);
818
819        // The list changes the balance, but leaves the account to its range.
820        provider.commit_block_access_list(write, block, parent, &credit(10)).unwrap();
821
822        assert_eq!(provider.snap_repairs(write).unwrap(), repairs);
823    }
824}