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 hash, 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        let root = state_root(accounts);
362        insert_headers(&factory, &chain().headers_with_roots(&[(1, root), (2, root), (3, root)]));
363        let provider = factory.database_provider_rw().unwrap();
364        let write = provider.start_snap_attempt(generation(1, root)).unwrap();
365        provider.start_account_coverage(write).unwrap();
366        // A partial range needs a proof placing the accounts it leaves out.
367        let targets = (served < accounts.len()).then(|| accounts[served - 1].0);
368        let range =
369            crate::test_utils::verified_range(accounts, 0..served, B256::ZERO, targets.as_slice());
370        let storages = B256Map::from_iter([(
371            keccak256(CHANGED),
372            HashedStorage::from_iter([(hashed_slot(), U256::from(7))]),
373        )]);
374        provider
375            .commit_account_range(
376                write,
377                &range,
378                if served == accounts.len() { storages } else { B256Map::default() },
379                Vec::new(),
380            )
381            .unwrap();
382        let write =
383            provider.advance_snap_pivot(write, generation(2, state_root(accounts))).unwrap();
384        provider.commit().unwrap();
385        (factory, write)
386    }
387
388    // Canonical blocks 0 through 3, pivoted at block 1. The lists the tests apply are their own, as
389    // the store does not authenticate them.
390    fn chain() -> BalChain {
391        BalChain::new(1, [Vec::new(), Vec::new()])
392    }
393
394    // Generation anchored to block `number` of the fixture chain.
395    fn generation(number: u64, state_root: B256) -> SnapGeneration {
396        SnapGeneration::new(chain().block(number as usize - 1), state_root)
397    }
398
399    // Block `number` of the fixture chain, with the hash of its parent.
400    fn block(number: u64) -> (BlockNumHash, B256) {
401        let chain = chain();
402        (chain.block(number as usize - 1), chain.block(number as usize - 2).hash)
403    }
404
405    // The fixture chain with its last block replaced by one changing a balance.
406    fn replacement() -> BalChain {
407        BalChain::new(1, [Vec::new(), credit(1)])
408    }
409
410    // The attempt carried through block 2 and anchored to block 3, which a reorg then replaced,
411    // leaving the applied block canonical. Returns the orphaned pivot.
412    fn orphaned_pivot(accounts: &[(B256, TrieAccount)]) -> (Factory, SnapWrite, BlockNumHash) {
413        let (factory, write) = started(accounts, accounts.len());
414        let provider = factory.database_provider_rw().unwrap();
415        let (applied, parent) = block(2);
416        provider.commit_block_access_list(write, applied, parent, &[]).unwrap();
417        let write =
418            provider.advance_snap_pivot(write, generation(3, state_root(accounts))).unwrap();
419        provider.commit().unwrap();
420        let replacement = replacement();
421        assert_eq!(replacement.block(1), applied);
422        replacement.replace_tip(&factory);
423        (factory, write, block(3).0)
424    }
425
426    fn index(value: u64) -> BlockAccessIndex {
427        BlockAccessIndex::new(value)
428    }
429
430    // A list crediting `CHANGED` with `balance`.
431    fn credit(balance: u64) -> Vec<AccountChanges> {
432        vec![AccountChanges::new(CHANGED)
433            .with_balance_change(BalanceChange::new(index(1), U256::from(balance)))]
434    }
435
436    fn stored(provider: &impl DBProvider, hashed_address: B256) -> Option<Account> {
437        provider.tx_ref().get::<tables::HashedAccounts>(hashed_address).unwrap()
438    }
439
440    #[test]
441    fn progress_starts_at_the_first_pivot_and_resumes_where_it_left_off() {
442        let accounts = accounts();
443        let (factory, write) = started(&accounts, accounts.len());
444        let provider = factory.database_provider_rw().unwrap();
445        let (block, parent) = block(2);
446
447        // The pivot moved to block 2, but ranges committed at block 1 still need its list.
448        assert_eq!(
449            provider.catch_up_progress(write).unwrap().unwrap().applied(),
450            generation(1, B256::ZERO).target()
451        );
452
453        let progress =
454            provider.commit_block_access_list(write, block, parent, &credit(10)).unwrap();
455        assert_eq!(progress.applied(), block);
456        assert_eq!(progress.next(), 3);
457        // Restarting the attempt re-applies nothing.
458        assert_eq!(provider.catch_up_progress(write).unwrap(), Some(progress));
459    }
460
461    #[test]
462    fn a_list_commits_with_the_block_it_carries_the_state_to() {
463        let accounts = accounts();
464        let (factory, write) = started(&accounts, accounts.len());
465        let (block, parent) = block(2);
466        let changes = vec![AccountChanges::new(CHANGED)
467            .with_balance_change(BalanceChange::new(index(1), U256::from(10)))
468            .with_code_change(CodeChange::new(index(1), code()))
469            .with_storage_change(SlotChanges::new(
470                SLOT,
471                vec![StorageChange::new(index(1), U256::from(9))],
472            ))];
473
474        let provider = factory.database_provider_rw().unwrap();
475        provider.commit_block_access_list(write, block, parent, &changes).unwrap();
476        provider.commit().unwrap();
477
478        let provider = factory.database_provider_ro().unwrap();
479        let applied = stored(&provider, keccak256(CHANGED)).unwrap();
480        assert_eq!(applied.balance, U256::from(10));
481        assert_eq!(applied.bytecode_hash, Some(keccak256(code())));
482        // The code the list deploys is stored with the account referencing it.
483        assert!(provider.tx_ref().get::<tables::Bytecodes>(keccak256(code())).unwrap().is_some());
484        assert_eq!(
485            crate::test_utils::stored_slots(&provider, keccak256(CHANGED)),
486            [(hashed_slot(), U256::from(9))]
487        );
488        assert_eq!(provider.catch_up_progress(write).unwrap().unwrap().applied(), block);
489    }
490
491    #[test]
492    fn a_block_already_applied_is_refused() {
493        let accounts = accounts();
494        let (factory, write) = started(&accounts, accounts.len());
495        let provider = factory.database_provider_rw().unwrap();
496        let (block, parent) = block(2);
497        provider.commit_block_access_list(write, block, parent, &credit(10)).unwrap();
498
499        let duplicate = provider.commit_block_access_list(write, block, parent, &credit(20));
500
501        assert!(matches!(duplicate, Err(SnapSyncError::OutOfOrderBlock { expected: 3, got: 2 })));
502        assert_eq!(stored(&provider, keccak256(CHANGED)).unwrap().balance, U256::from(10));
503    }
504
505    #[test]
506    fn a_block_the_applied_state_has_not_reached_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(3);
511
512        let gap = provider.commit_block_access_list(write, block, parent, &credit(10));
513
514        assert!(matches!(gap, Err(SnapSyncError::OutOfOrderBlock { expected: 2, got: 3 })));
515        assert_eq!(stored(&provider, keccak256(CHANGED)).unwrap().balance, U256::ONE);
516    }
517
518    #[test]
519    fn a_block_the_canonical_chain_no_longer_holds_is_refused() {
520        let accounts = accounts();
521        let (factory, write) = started(&accounts, accounts.len());
522        let provider = factory.database_provider_rw().unwrap();
523        let (_, parent) = block(2);
524        let orphaned = BlockNumHash::new(2, B256::repeat_byte(0xee));
525
526        let refused = provider.commit_block_access_list(write, orphaned, parent, &credit(10));
527
528        assert!(matches!(refused, Err(SnapSyncError::NonCanonicalBlock { block: 2, .. })));
529        assert_eq!(stored(&provider, keccak256(CHANGED)).unwrap().balance, U256::ONE);
530    }
531
532    #[test]
533    fn a_list_proved_against_an_orphaned_pivot_is_refused() {
534        let accounts = accounts();
535        let (factory, write, orphaned) = orphaned_pivot(&accounts);
536        let provider = factory.database_provider_rw().unwrap();
537        let (applied, _) = block(2);
538        // The replacement block continues the applied one, so only the pivot check refuses it.
539        let (block, parent) = (replacement().block(2), applied.hash);
540        let before = SnapStateSnapshot::read(&provider);
541
542        let refused = provider.commit_block_access_list(write, block, parent, &credit(10));
543
544        assert!(matches!(
545            refused,
546            Err(SnapSyncError::NonCanonicalBlock { block: 3, hash }) if hash == orphaned.hash
547        ));
548        assert_eq!(provider.catch_up_progress(write).unwrap().unwrap().applied(), applied);
549        assert_eq!(SnapStateSnapshot::read(&provider), before);
550        provider.commit().unwrap();
551        assert_eq!(SnapStateSnapshot::read(&factory.database_provider_ro().unwrap()), before);
552    }
553
554    #[test]
555    fn an_orphaned_pivot_cannot_be_replaced() {
556        let accounts = accounts();
557        let (factory, write, orphaned) = orphaned_pivot(&accounts);
558        let provider = factory.database_provider_rw().unwrap();
559        let before = SnapStateSnapshot::read(&provider);
560        let replacement = SnapGeneration::new(replacement().block(2), state_root(&accounts));
561
562        let refused = provider.advance_snap_pivot(write, replacement);
563
564        // Re-anchoring would leave the ranges downloaded against the orphan looking canonical.
565        assert!(matches!(
566            refused,
567            Err(SnapSyncError::NonCanonicalBlock { block: 3, hash }) if hash == orphaned.hash
568        ));
569        assert_eq!(provider.authorize_snap_write(write).unwrap().pivot(), orphaned);
570        assert_eq!(SnapStateSnapshot::read(&provider), before);
571        provider.commit().unwrap();
572        assert_eq!(SnapStateSnapshot::read(&factory.database_provider_ro().unwrap()), before);
573    }
574
575    #[test]
576    fn a_block_building_on_another_chain_is_refused() {
577        let accounts = accounts();
578        let (factory, write) = started(&accounts, accounts.len());
579        let provider = factory.database_provider_rw().unwrap();
580        let (block, _) = block(2);
581
582        let forked =
583            provider.commit_block_access_list(write, block, B256::repeat_byte(0xee), &credit(10));
584
585        assert!(matches!(forked, Err(SnapSyncError::ForkedBlock { .. })));
586        assert_eq!(provider.catch_up_progress(write).unwrap().unwrap().applied().number, 1);
587    }
588
589    #[test]
590    fn an_account_the_list_empties_loses_its_storage() {
591        let accounts = accounts();
592        let (factory, write) = started(&accounts, accounts.len());
593        let provider = factory.database_provider_rw().unwrap();
594        let (block, parent) = block(2);
595
596        provider.commit_block_access_list(write, block, parent, &credit(0)).unwrap();
597
598        assert_eq!(stored(&provider, keccak256(CHANGED)), None);
599        assert!(crate::test_utils::stored_slots(&provider, keccak256(CHANGED)).is_empty());
600    }
601
602    #[test]
603    fn a_block_past_the_pivot_is_refused() {
604        let accounts = accounts();
605        let (factory, write) = started(&accounts, accounts.len());
606        let provider = factory.database_provider_rw().unwrap();
607        let (block, parent) = block(2);
608        provider.commit_block_access_list(write, block, parent, &credit(10)).unwrap();
609        let (block, parent) = self::block(3);
610
611        let refused = provider.commit_block_access_list(write, block, parent, &credit(20));
612
613        assert!(matches!(refused, Err(SnapSyncError::BlockPastPivot { pivot: 2, block: 3 })));
614        assert_eq!(stored(&provider, keccak256(CHANGED)).unwrap().balance, U256::from(10));
615    }
616
617    #[test]
618    fn an_account_outside_the_coverage_is_left_to_its_range() {
619        let accounts = accounts();
620        let index = changed_index(&accounts);
621        let (factory, write) = started(&accounts, index);
622        let provider = factory.database_provider_rw().unwrap();
623        let (block, parent) = block(2);
624
625        let progress =
626            provider.commit_block_access_list(write, block, parent, &credit(10)).unwrap();
627
628        // The range still to download authenticates against a root that already holds the change.
629        assert_eq!(stored(&provider, keccak256(CHANGED)), None);
630        assert_eq!(progress.applied(), block);
631    }
632
633    #[test]
634    fn slots_persisted_ahead_of_their_range_follow_the_lists() {
635        let factory = hashed_factory();
636        let mut moved = accounts();
637        let position = changed_index(&moved);
638        moved[position].1.storage_root = storage_root_of(&[(hashed_slot(), U256::from(9))]);
639        insert_headers(
640            &factory,
641            &chain().headers_with_roots(&[(1, state_root(&accounts())), (2, state_root(&moved))]),
642        );
643        let provider = factory.database_provider_rw().unwrap();
644        let write = provider.start_snap_attempt(generation(1, state_root(&accounts()))).unwrap();
645        provider.start_account_coverage(write).unwrap();
646        let (changed, stored_slot) = (keccak256(CHANGED), (hashed_slot(), U256::from(7)));
647        let chunk = StorageChunk::new(
648            changed,
649            storage_root_of(&[stored_slot]),
650            B256::ZERO,
651            vec![stored_slot],
652            None,
653        );
654        provider.commit_storage_chunk(write, B256::ZERO, chunk).unwrap();
655        let write = provider.advance_snap_pivot(write, generation(2, state_root(&moved))).unwrap();
656        let (block, parent) = block(2);
657        let changes = vec![AccountChanges::new(CHANGED).with_storage_change(SlotChanges::new(
658            SLOT,
659            vec![StorageChange::new(index(1), U256::from(9))],
660        ))];
661
662        provider.commit_block_access_list(write, block, parent, &changes).unwrap();
663
664        // Only the slots follow: the account itself comes whole with its range.
665        assert_eq!(stored(&provider, changed), None);
666        assert_eq!(
667            crate::test_utils::stored_slots(&provider, changed),
668            [(hashed_slot(), U256::from(9))]
669        );
670        let range = crate::test_utils::verified_range(&moved, 0..moved.len(), B256::ZERO, &[]);
671        let coverage =
672            provider.commit_account_range(write, &range, B256Map::default(), Vec::new()).unwrap();
673        assert!(coverage.is_complete());
674    }
675
676    #[test]
677    #[allow(clippy::clone_on_copy)]
678    fn skipped_storage_from_an_abandoned_attempt_is_not_reused() {
679        let factory = hashed_factory();
680        let (changed, later) = (keccak256(CHANGED), B256::repeat_byte(0xff));
681        let stale = (B256::ZERO, U256::from(7));
682        let mut contract = account(1);
683        contract.storage_root = storage_root_of(&[stale]);
684        let mut accounts = vec![(changed, contract.clone()), (later, contract.clone())];
685        let first = state_root(&accounts);
686        accounts[0].1.storage_root = reth_trie_common::EMPTY_ROOT_HASH;
687        let second = state_root(&accounts);
688        let new_slot = (hashed_slot(), U256::from(9));
689        accounts[0].1.storage_root = storage_root_of(&[new_slot]);
690        let third = state_root(&accounts);
691        accounts[0].1.storage_root = contract.storage_root;
692        insert_headers(
693            &factory,
694            &chain().headers_with_roots(&[(1, first), (2, second), (3, third)]),
695        );
696        let provider = factory.database_provider_rw().unwrap();
697        let write = provider.start_snap_attempt(generation(1, state_root(&accounts))).unwrap();
698        provider.start_account_coverage(write).unwrap();
699        provider
700            .commit_storage_chunk(
701                write,
702                B256::ZERO,
703                StorageChunk::new(changed, contract.storage_root, B256::ZERO, vec![stale], None),
704            )
705            .unwrap();
706
707        accounts[0].1.storage_root = reth_trie_common::EMPTY_ROOT_HASH;
708        let write = provider.start_snap_attempt(generation(2, state_root(&accounts))).unwrap();
709        provider.start_account_coverage(write).unwrap();
710        provider
711            .commit_storage_chunk(
712                write,
713                B256::ZERO,
714                StorageChunk::new(later, contract.storage_root, B256::ZERO, vec![stale], None),
715            )
716            .unwrap();
717
718        accounts[0].1.storage_root = storage_root_of(&[new_slot]);
719        let write =
720            provider.advance_snap_pivot(write, generation(3, state_root(&accounts))).unwrap();
721        let (block, parent) = block(3);
722        let changes = vec![AccountChanges::new(CHANGED).with_storage_change(SlotChanges::new(
723            SLOT,
724            vec![StorageChange::new(index(1), new_slot.1)],
725        ))];
726        provider.commit_block_access_list(write, block, parent, &changes).unwrap();
727
728        assert_eq!(crate::test_utils::stored_slots(&provider, changed), [new_slot]);
729        let range =
730            crate::test_utils::verified_range(&accounts, 0..accounts.len(), B256::ZERO, &[]);
731        assert!(provider
732            .commit_account_range(write, &range, B256Map::default(), Vec::new())
733            .unwrap()
734            .is_complete());
735    }
736
737    #[test]
738    fn a_list_from_a_replaced_attempt_changes_nothing() {
739        let accounts = accounts();
740        let (factory, write) = started(&accounts, accounts.len());
741        let provider = factory.database_provider_rw().unwrap();
742        provider.advance_snap_pivot(write, generation(3, state_root(&accounts))).unwrap();
743        let (block, parent) = block(2);
744
745        let refused = provider.commit_block_access_list(write, block, parent, &credit(10));
746
747        assert!(matches!(refused, Err(SnapSyncError::StaleWrite { .. })));
748        assert!(matches!(provider.catch_up_progress(write), Err(SnapSyncError::StaleWrite { .. })));
749    }
750
751    #[test]
752    fn a_record_this_build_cannot_read_is_reported() {
753        let accounts = accounts();
754        let (factory, write) = started(&accounts, accounts.len());
755        let provider = factory.database_provider_rw().unwrap();
756        provider.write_metadata(StoredCatchUpProgress::KEY, br#"{"version":0}"#.to_vec()).unwrap();
757
758        assert!(matches!(
759            provider.catch_up_progress(write),
760            Err(SnapSyncError::UnsupportedRecord {
761                key: StoredCatchUpProgress::KEY,
762                version: Some(0)
763            })
764        ));
765    }
766
767    #[test]
768    fn a_list_resolves_the_repairs_it_overwrites() {
769        let accounts = accounts();
770        let (factory, write) = started(&accounts, accounts.len());
771        let provider = factory.database_provider_rw().unwrap();
772        // The orphaned branch changed the balance and the nonce.
773        let orphaned = AccountChanges::new(CHANGED)
774            .with_balance_change(BalanceChange::new(index(1), U256::ONE))
775            .with_nonce_change(NonceChange::new(index(1), 1));
776        let mut repairs = StateRepairs::default();
777        repairs.insert_changes(keccak256(CHANGED), &orphaned);
778        provider.schedule_snap_repairs(write, repairs).unwrap();
779        let (block, parent) = block(2);
780
781        // The new branch changes the balance only.
782        provider.commit_block_access_list(write, block, parent, &credit(10)).unwrap();
783
784        let mut expected = StateRepairs::default();
785        expected.insert_changes(
786            keccak256(CHANGED),
787            &AccountChanges::new(CHANGED).with_nonce_change(NonceChange::new(index(1), 1)),
788        );
789        assert_eq!(provider.snap_repairs(write).unwrap(), expected);
790    }
791
792    #[test]
793    fn repairs_of_an_account_outside_the_coverage_stay_scheduled() {
794        let accounts = accounts();
795        let (factory, write) = started(&accounts, changed_index(&accounts));
796        let provider = factory.database_provider_rw().unwrap();
797        let mut repairs = StateRepairs::default();
798        repairs.insert_account(keccak256(CHANGED));
799        provider.schedule_snap_repairs(write, repairs.clone()).unwrap();
800        let (block, parent) = block(2);
801
802        // The list changes the balance, but leaves the account to its range.
803        provider.commit_block_access_list(write, block, parent, &credit(10)).unwrap();
804
805        assert_eq!(provider.snap_repairs(write).unwrap(), repairs);
806    }
807}