Skip to main content

reth_snap_sync/account/
store.rs

1//! Persists account ranges, their storage and code, and the coverage cursor in one transaction.
2//!
3//! Each range replaces its key interval, and ranges commit in key order. Accounts scheduled for
4//! repair are fetched again on their own and replace what the state holds for them.
5
6use crate::{
7    common::SnapRecord, repair::StoredRepairs, storage::persisted_storage_root, SnapAttemptStore,
8    SnapCatchUpStore, SnapStorageStore, SnapSyncError, SnapWrite, StateRepairs, StorageProgress,
9};
10use alloy_primitives::{
11    map::{B256Map, B256Set},
12    B256, KECCAK256_EMPTY, U256,
13};
14use reth_db_api::{
15    tables,
16    transaction::{DbTx, DbTxMut},
17    RawKey, RawTable,
18};
19use reth_downloaders::snap::VerifiedAccountRange;
20use reth_primitives_traits::Account;
21use reth_storage_api::{
22    BlockHashReader, DBProvider, MetadataProvider, MetadataWriter, SnapAttemptId, StateWriter,
23};
24use reth_trie_common::{
25    root::storage_root, HashedPostState, HashedPostStateSorted, HashedStorage, TrieAccount,
26    EMPTY_ROOT_HASH,
27};
28use revm::{bytecode::Bytecode, database::states::StateChangeset};
29use serde::{Deserialize, Serialize};
30use std::ops::Bound;
31
32/// Persistence for the account ranges an attempt downloads.
33///
34/// Blanket-implemented over the node's writers, so accounts, their dependencies and the coverage
35/// join the caller's transaction and commit together or not at all.
36pub trait SnapAccountStore {
37    /// Returns the coverage recorded for the attempt `write` belongs to, recording that no
38    /// account has been downloaded yet when there is none.
39    fn start_account_coverage(&self, write: SnapWrite) -> Result<AccountCoverage, SnapSyncError>
40    where
41        Self: MetadataWriter;
42
43    /// Returns the coverage recorded for the attempt `write` belongs to, if any.
44    fn account_coverage(&self, write: SnapWrite) -> Result<Option<AccountCoverage>, SnapSyncError>;
45
46    /// Persists `range` with its storage and code, replacing its key interval.
47    ///
48    /// Storage must match each account's root, supplied or persisted ahead of the range by
49    /// [`SnapStorageStore`], and code must be supplied or already stored. Persisted storage only
50    /// counts once catch-up has carried it to the pivot.
51    fn commit_account_range(
52        &self,
53        write: SnapWrite,
54        range: &VerifiedAccountRange,
55        storages: B256Map<HashedStorage>,
56        bytecodes: Vec<(B256, Bytecode)>,
57    ) -> Result<AccountCoverage, SnapSyncError>
58    where
59        Self: MetadataWriter + StateWriter + DBProvider<Tx: DbTxMut>;
60
61    /// Removes storage in `interval` except the contracts in `kept`, which are sorted and inside
62    /// it.
63    fn remove_storages_except(
64        &self,
65        interval: (Bound<B256>, Bound<B256>),
66        kept: &[B256],
67    ) -> Result<(), SnapSyncError>
68    where
69        Self: DBProvider<Tx: DbTxMut>;
70
71    /// Returns the repairs scheduled for the attempt `write` belongs to.
72    fn snap_repairs(&self, write: SnapWrite) -> Result<StateRepairs, SnapSyncError>;
73
74    /// Adds `repairs` to those scheduled for the attempt `write` belongs to.
75    fn schedule_snap_repairs(
76        &self,
77        write: SnapWrite,
78        repairs: StateRepairs,
79    ) -> Result<(), SnapSyncError>
80    where
81        Self: MetadataWriter;
82
83    /// Writes the pivot's account at `range`'s origin and its `slots`, returning how many accounts
84    /// remain scheduled. Refused until catch-up reaches the pivot.
85    fn commit_account_repair(
86        &self,
87        write: SnapWrite,
88        range: &VerifiedAccountRange,
89        slots: Vec<(B256, U256)>,
90    ) -> Result<usize, SnapSyncError>
91    where
92        Self: BlockHashReader + MetadataWriter + StateWriter + DBProvider<Tx: DbTxMut>;
93}
94
95/// How far the account key space has been downloaded.
96#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
97pub struct AccountCoverage {
98    // Next key to request, or none once the trie is exhausted.
99    next: Option<B256>,
100}
101
102impl AccountCoverage {
103    /// Nothing downloaded yet.
104    pub const START: Self = Self { next: Some(B256::ZERO) };
105
106    /// Every account downloaded.
107    pub const COMPLETE: Self = Self { next: None };
108
109    /// Key the next range is requested from, or `None` once every account is downloaded.
110    pub const fn next(&self) -> Option<B256> {
111        self.next
112    }
113
114    /// Returns whether every account is downloaded.
115    pub const fn is_complete(&self) -> bool {
116        self.next.is_none()
117    }
118
119    // The coverage after `range`, which must start at this cursor and end past it.
120    fn advance(&self, range: &VerifiedAccountRange) -> Result<Self, SnapSyncError> {
121        let origin = range.origin();
122        if self.next != Some(origin) {
123            return Err(SnapSyncError::OutOfOrderRange { expected: self.next, got: origin })
124        }
125        if range.next().is_some_and(|next| next <= origin) {
126            return Err(SnapSyncError::NoProgress { origin })
127        }
128        Ok(Self { next: range.next() })
129    }
130
131    // Keys from this cursor up to `to`, which a range proved hold only its own accounts.
132    fn interval_to(&self, to: &Self) -> (Bound<B256>, Bound<B256>) {
133        (
134            self.next.map_or(Bound::Unbounded, Bound::Included),
135            to.next.map_or(Bound::Unbounded, Bound::Excluded),
136        )
137    }
138}
139
140// The coverage record as persisted, tied to the attempt that recorded it.
141#[derive(Serialize, Deserialize)]
142pub(crate) struct StoredCoverage {
143    // Encoding version, checked before the rest is decoded.
144    version: u32,
145    // Attempt the coverage belongs to.
146    attempt: SnapAttemptId,
147    // How far that attempt has downloaded.
148    coverage: AccountCoverage,
149}
150
151impl SnapRecord for StoredCoverage {
152    const KEY: &'static str = "snap_account_coverage";
153    const VERSION: u32 = 1;
154}
155
156impl StoredCoverage {
157    // `coverage` for `attempt` at this build's version.
158    const fn new(attempt: SnapAttemptId, coverage: AccountCoverage) -> Self {
159        Self { version: Self::VERSION, attempt, coverage }
160    }
161}
162
163impl<T: MetadataProvider> SnapAccountStore for T {
164    // Resumes the recorded coverage, so resuming an attempt keeps its progress.
165    fn start_account_coverage(&self, write: SnapWrite) -> Result<AccountCoverage, SnapSyncError>
166    where
167        Self: MetadataWriter,
168    {
169        if let Some(coverage) = self.account_coverage(write)? {
170            return Ok(coverage)
171        }
172        let start = AccountCoverage::START;
173        StoredCoverage::new(write.attempt(), start).write(self)?;
174        Ok(start)
175    }
176
177    // A record left by another attempt reads as no coverage.
178    fn account_coverage(&self, write: SnapWrite) -> Result<Option<AccountCoverage>, SnapSyncError> {
179        self.authorize_snap_write(write)?;
180        let Some(stored) = StoredCoverage::read(self)? else { return Ok(None) };
181        Ok((stored.attempt == write.attempt()).then_some(stored.coverage))
182    }
183
184    // Every check runs before the first write, so a refused range changes nothing.
185    fn commit_account_range(
186        &self,
187        write: SnapWrite,
188        range: &VerifiedAccountRange,
189        storages: B256Map<HashedStorage>,
190        bytecodes: Vec<(B256, Bytecode)>,
191    ) -> Result<AccountCoverage, SnapSyncError>
192    where
193        Self: MetadataWriter + StateWriter + DBProvider<Tx: DbTxMut>,
194    {
195        let attempt = self.authorize_snap_write(write)?;
196        if range.state_root() != attempt.state_root() {
197            return Err(SnapSyncError::RootMismatch {
198                expected: attempt.state_root(),
199                got: range.state_root(),
200            })
201        }
202        let coverage = self.account_coverage(write)?.ok_or(SnapSyncError::NoCoverage)?;
203        let advanced = coverage.advance(range)?;
204        let progress = self.storage_progress(write, range.origin())?;
205        // Slots persisted before the pivot moved hold its old values until the lists reach it.
206        if progress != StorageProgress::START {
207            let applied = self
208                .catch_up_progress(write)?
209                .ok_or(SnapSyncError::NoCatchUpProgress)?
210                .applied()
211                .number;
212            if applied < attempt.pivot().number {
213                return Err(SnapSyncError::CatchUpBehindPivot {
214                    applied,
215                    pivot: attempt.pivot().number,
216                })
217            }
218        }
219        let dependencies = RangeDependencies::new(range.accounts(), storages, bytecodes);
220        let persisted = dependencies.verify(range.accounts(), &progress, self.tx_ref())?;
221
222        let interval = coverage.interval_to(&advanced);
223        self.remove::<tables::HashedAccounts>(interval)?;
224        self.remove_storages_except(interval, &persisted)?;
225        dependencies.write(self)?;
226        StoredCoverage::new(write.attempt(), advanced).write(self)?;
227        // A page ending before contracts with persisted storage leaves them to the next range.
228        if let Some(next) = advanced.next() {
229            progress.carry_to(self, write, next)?;
230        }
231        Ok(advanced)
232    }
233
234    fn remove_storages_except(
235        &self,
236        (mut start, end): (Bound<B256>, Bound<B256>),
237        kept: &[B256],
238    ) -> Result<(), SnapSyncError>
239    where
240        Self: DBProvider<Tx: DbTxMut>,
241    {
242        for account in kept {
243            self.remove::<tables::HashedStorages>((start, Bound::Excluded(*account)))?;
244            // Walks cannot start at an excluded key, so the next piece starts one key past it.
245            let Some(after) = U256::from_be_bytes(account.0).checked_add(U256::ONE) else {
246                return Ok(())
247            };
248            start = Bound::Included(after.into());
249        }
250        self.remove::<tables::HashedStorages>((start, end))?;
251        Ok(())
252    }
253
254    // A record left by another attempt reads as nothing scheduled.
255    fn snap_repairs(&self, write: SnapWrite) -> Result<StateRepairs, SnapSyncError> {
256        self.authorize_snap_write(write)?;
257        let Some(stored) = StoredRepairs::read(self)? else { return Ok(StateRepairs::default()) };
258        Ok(if stored.attempt == write.attempt() { stored.repairs } else { StateRepairs::default() })
259    }
260
261    fn schedule_snap_repairs(
262        &self,
263        write: SnapWrite,
264        repairs: StateRepairs,
265    ) -> Result<(), SnapSyncError>
266    where
267        Self: MetadataWriter,
268    {
269        let mut scheduled = self.snap_repairs(write)?;
270        scheduled.extend(repairs);
271        StoredRepairs::store(self, write.attempt(), scheduled)
272    }
273
274    // Every check runs before the first write, so a refused repair changes nothing.
275    #[allow(clippy::clone_on_copy)]
276    fn commit_account_repair(
277        &self,
278        write: SnapWrite,
279        range: &VerifiedAccountRange,
280        slots: Vec<(B256, U256)>,
281    ) -> Result<usize, SnapSyncError>
282    where
283        Self: BlockHashReader + MetadataWriter + StateWriter + DBProvider<Tx: DbTxMut>,
284    {
285        // The repaired values belong to the pivot, so it must still be canonical.
286        let attempt = self.authorize_canonical_snap_write(write)?;
287        if range.state_root() != attempt.state_root() {
288            return Err(SnapSyncError::RootMismatch {
289                expected: attempt.state_root(),
290                got: range.state_root(),
291            })
292        }
293        let applied =
294            self.catch_up_progress(write)?.ok_or(SnapSyncError::NoCatchUpProgress)?.applied();
295        if applied != attempt.pivot() {
296            return Err(SnapSyncError::CatchUpBehindPivot {
297                applied: applied.number,
298                pivot: attempt.pivot().number,
299            })
300        }
301        let hashed_address = range.origin();
302        // The proof starts at the origin, so an account keyed past it means none is there.
303        let account = range
304            .accounts()
305            .first()
306            .filter(|(hash, _)| *hash == hashed_address)
307            .map(|(_, account)| account);
308        if let Some(hash) = account.map(|account| account.code_hash) &&
309            hash != KECCAK256_EMPTY &&
310            self.tx_ref().get::<RawTable<tables::Bytecodes>>(RawKey::new(hash))?.is_none()
311        {
312            return Err(SnapSyncError::MissingCode { hash })
313        }
314
315        let has_storage = account.is_some_and(|account| account.storage_root != EMPTY_ROOT_HASH);
316        let mut state = HashedPostState::default().with_accounts([(
317            hashed_address,
318            account.map(|account| Account::from(account.clone())),
319        )]);
320        if has_storage {
321            // Zero values remove their slots.
322            state = state
323                .with_storages([(hashed_address, HashedStorage::from_iter(slots.iter().copied()))]);
324        } else {
325            self.remove::<tables::HashedStorages>(hashed_address..=hashed_address)?;
326        }
327        self.write_hashed_state(&state.into_sorted())?;
328
329        let mut remaining = self.snap_repairs(write)?;
330        remaining.resolve(hashed_address, has_storage.then_some(slots.as_slice()));
331        let accounts = remaining.len();
332        StoredRepairs::store(self, write.attempt(), remaining)?;
333        Ok(accounts)
334    }
335}
336
337// Accounts of a range with the storage and code supplied for them.
338struct RangeDependencies {
339    // Accounts and storage, sorted once for both the root checks and the write.
340    state: HashedPostStateSorted,
341    // Code keyed by the hash it was supplied under.
342    bytecodes: Vec<(B256, Bytecode)>,
343}
344
345impl RangeDependencies {
346    #[allow(clippy::clone_on_copy)]
347    fn new(
348        accounts: &[(B256, TrieAccount)],
349        storages: B256Map<HashedStorage>,
350        bytecodes: Vec<(B256, Bytecode)>,
351    ) -> Self {
352        let state = HashedPostState::default()
353            .with_accounts(
354                accounts
355                    .iter()
356                    .map(|(hash, account)| (*hash, Some(Account::from(account.clone())))),
357            )
358            .with_storages(storages)
359            .into_sorted();
360        Self { state, bytecodes }
361    }
362
363    // Checks storage against each account's root, and code against its hash or the table.
364    // Returns the contracts whose storage was persisted ahead of the range, in key order.
365    fn verify(
366        &self,
367        accounts: &[(B256, TrieAccount)],
368        progress: &StorageProgress,
369        tx: &impl DbTx,
370    ) -> Result<Vec<B256>, SnapSyncError> {
371        let mut available = self.supplied_code()?;
372        let mut contracts = B256Set::default();
373        let mut persisted = Vec::new();
374        for (hash, account) in accounts {
375            if account.storage_root != EMPTY_ROOT_HASH {
376                contracts.insert(*hash);
377                if self.verify_storage(*hash, account, progress, tx)? {
378                    persisted.push(*hash);
379                }
380            }
381            // Only presence matters, so stored code is not decoded.
382            if account.code_hash != KECCAK256_EMPTY &&
383                available.insert(account.code_hash) &&
384                tx.get::<RawTable<tables::Bytecodes>>(RawKey::new(account.code_hash))?.is_none()
385            {
386                return Err(SnapSyncError::MissingCode { hash: account.code_hash })
387            }
388        }
389        if let Some(account) =
390            self.state.account_storages().keys().find(|hash| !contracts.contains(*hash))
391        {
392            return Err(SnapSyncError::UnexpectedStorage { account: *account })
393        }
394        Ok(persisted)
395    }
396
397    // Hashes of the supplied code, refusing code filed under a hash it does not hash to.
398    fn supplied_code(&self) -> Result<B256Set, SnapSyncError> {
399        let mut hashes = B256Set::default();
400        for (hash, code) in &self.bytecodes {
401            let got = code.hash_slow();
402            if got != *hash {
403                return Err(SnapSyncError::CodeMismatch { expected: *hash, got })
404            }
405            hashes.insert(*hash);
406        }
407        Ok(hashes)
408    }
409
410    // Checks the contract's storage, supplied or persisted, against its root. Returns whether it
411    // was persisted.
412    fn verify_storage(
413        &self,
414        hash: B256,
415        account: &TrieAccount,
416        progress: &StorageProgress,
417        tx: &impl DbTx,
418    ) -> Result<bool, SnapSyncError> {
419        let (got, persisted) = match self.state.account_storages().get(&hash) {
420            // Zero slots are deletions, which the trie does not hold.
421            Some(storage) => (
422                storage_root(
423                    storage
424                        .storage_slots_ref()
425                        .iter()
426                        .filter(|(_, value)| !value.is_zero())
427                        .copied(),
428                ),
429                false,
430            ),
431            None if progress.is_complete(hash) => (persisted_storage_root(tx, hash)?, true),
432            None => return Err(SnapSyncError::MissingStorage { account: hash }),
433        };
434        if got != account.storage_root {
435            return Err(SnapSyncError::StorageRootMismatch {
436                account: hash,
437                expected: account.storage_root,
438                got,
439            })
440        }
441        Ok(persisted)
442    }
443
444    // Writes the accounts with their storage and code.
445    fn write(self, writer: &impl StateWriter) -> Result<(), SnapSyncError> {
446        writer.write_hashed_state(&self.state)?;
447        writer.write_state_changes(StateChangeset {
448            contracts: self.bytecodes,
449            ..Default::default()
450        })?;
451        Ok(())
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458    use crate::{
459        test_utils::{
460            account, generation, hashed_factory_with_roots, key, state_root, stored_slots,
461            verified_range, verified_repair,
462        },
463        SnapGeneration, StorageChunk,
464    };
465    use alloy_primitives::{Bytes, U256};
466    use reth_db_api::cursor::DbCursorRO;
467    use reth_provider::{
468        test_utils::MockNodeTypesWithDB, DatabaseProviderFactory, ProviderFactory,
469    };
470    use reth_trie_common::root::storage_root_unsorted;
471
472    const FAR: B256 = B256::repeat_byte(0xaa);
473    const SLOT: B256 = B256::repeat_byte(0x55);
474    // A slot the stale state holds and the pivot does not.
475    const OTHER: B256 = B256::repeat_byte(0x66);
476    // Sorts between the contract and the far account, holding nothing at the pivot.
477    const ABSENT: B256 = B256::repeat_byte(0x33);
478
479    fn code() -> Bytecode {
480        Bytecode::new_raw(Bytes::from_static(&[0x60, 0x00]))
481    }
482
483    // Storage holding one slot, with the root the contract fixture commits to.
484    fn storage() -> (B256, HashedStorage) {
485        let storage = HashedStorage::from_iter([(SLOT, U256::from(7))]);
486        (
487            storage_root_unsorted(storage.storage.iter().map(|(slot, value)| (*slot, *value))),
488            storage,
489        )
490    }
491
492    // A plain account, a contract with storage and code, and a far account.
493    fn accounts() -> Vec<(B256, TrieAccount)> {
494        let mut contract = account(2);
495        contract.storage_root = storage().0;
496        contract.code_hash = code().hash_slow();
497        vec![(key(1), account(1)), (key(2), contract), (FAR, account(3))]
498    }
499
500    // Storage and code that satisfy the contract in `accounts`.
501    fn dependencies() -> (B256Map<HashedStorage>, Vec<(B256, Bytecode)>) {
502        (B256Map::from_iter([(key(2), storage().1)]), vec![(code().hash_slow(), code())])
503    }
504
505    // An attempt anchored to the trie's root, with its coverage recorded.
506    fn started(
507        accounts: &[(B256, TrieAccount)],
508    ) -> (ProviderFactory<MockNodeTypesWithDB>, SnapWrite, SnapGeneration) {
509        started_moving_to(accounts, accounts)
510    }
511
512    // `started`, with the later blocks committing to the state `moved`.
513    fn started_moving_to(
514        accounts: &[(B256, TrieAccount)],
515        moved: &[(B256, TrieAccount)],
516    ) -> (ProviderFactory<MockNodeTypesWithDB>, SnapWrite, SnapGeneration) {
517        let factory = hashed_factory_with_roots(&[
518            state_root(accounts),
519            state_root(moved),
520            state_root(moved),
521        ]);
522        let generation = generation(1, state_root(accounts));
523        let provider = factory.database_provider_rw().unwrap();
524        let write = provider.start_snap_attempt(generation).unwrap();
525        provider.start_account_coverage(write).unwrap();
526        provider.commit().unwrap();
527        (factory, write, generation)
528    }
529
530    fn stored_accounts(provider: &impl DBProvider) -> Vec<B256> {
531        let mut cursor = provider.tx_ref().cursor_read::<tables::HashedAccounts>().unwrap();
532        cursor.walk(None).unwrap().map(|entry| entry.unwrap().0).collect()
533    }
534
535    fn stored(provider: &impl DBProvider) -> (Vec<B256>, bool, bool) {
536        let tx = provider.tx_ref();
537        (
538            stored_accounts(provider),
539            tx.get::<tables::HashedStorages>(key(2)).unwrap().is_some(),
540            tx.get::<tables::Bytecodes>(code().hash_slow()).unwrap().is_some(),
541        )
542    }
543
544    #[test]
545    fn accounts_dependencies_and_coverage_commit_together() {
546        let accounts = accounts();
547        let (factory, write, _) = started(&accounts);
548        let range = verified_range(&accounts, 0..3, B256::ZERO, &[]);
549        let (storages, bytecodes) = dependencies();
550
551        let provider = factory.database_provider_rw().unwrap();
552        let coverage = provider.commit_account_range(write, &range, storages, bytecodes).unwrap();
553        provider.commit().unwrap();
554
555        assert!(coverage.is_complete());
556        let provider = factory.database_provider_rw().unwrap();
557        assert_eq!(stored(&provider), (vec![key(1), key(2), FAR], true, true));
558        assert_eq!(provider.account_coverage(write).unwrap(), Some(coverage));
559    }
560
561    #[test]
562    fn a_partial_range_moves_the_coverage_to_its_next_key() {
563        let accounts = accounts();
564        let (factory, write, _) = started(&accounts);
565        // Only the first account, with a proof placing the next one at key 2.
566        let range = verified_range(&accounts, 0..1, B256::ZERO, &[key(1)]);
567
568        let provider = factory.database_provider_rw().unwrap();
569        let coverage =
570            provider.commit_account_range(write, &range, Default::default(), Vec::new()).unwrap();
571
572        assert_eq!(coverage.next(), Some(key(2)));
573        assert_eq!(stored_accounts(&provider), [key(1)]);
574    }
575
576    #[test]
577    fn a_proven_empty_tail_completes_the_coverage() {
578        let accounts = vec![(key(1), account(1)), (key(2), account(2))];
579        let (factory, write, _) = started(&accounts);
580        let tail = verified_range(&accounts, 0..0, key(3), &[key(3)]);
581        assert!(tail.accounts().is_empty());
582        let provider = factory.database_provider_rw().unwrap();
583        let coverage = AccountCoverage { next: Some(key(3)) };
584        StoredCoverage::new(write.attempt(), coverage).write(&provider).unwrap();
585
586        let coverage =
587            provider.commit_account_range(write, &tail, Default::default(), Vec::new()).unwrap();
588
589        assert!(coverage.is_complete());
590        assert!(stored_accounts(&provider).is_empty());
591    }
592
593    #[test]
594    fn a_range_from_another_origin_changes_nothing() {
595        let accounts = accounts();
596        let (factory, write, _) = started(&accounts);
597        let provider = factory.database_provider_rw().unwrap();
598        let first = verified_range(&accounts, 0..1, B256::ZERO, &[key(1)]);
599        provider.commit_account_range(write, &first, Default::default(), Vec::new()).unwrap();
600
601        // The same range again, and one skipping ahead of the cursor.
602        let duplicate =
603            provider.commit_account_range(write, &first, Default::default(), Vec::new());
604        let skipped = verified_range(&accounts, 2..3, FAR, &[FAR]);
605        let ahead = provider.commit_account_range(write, &skipped, Default::default(), Vec::new());
606
607        assert!(matches!(duplicate, Err(SnapSyncError::OutOfOrderRange { .. })));
608        assert!(matches!(ahead, Err(SnapSyncError::OutOfOrderRange { .. })));
609        assert_eq!(provider.account_coverage(write).unwrap().unwrap().next(), Some(key(2)));
610        assert_eq!(stored_accounts(&provider), [key(1)]);
611    }
612
613    #[test]
614    fn a_contract_without_its_storage_or_code_is_refused() {
615        let accounts = accounts();
616        let (factory, write, _) = started(&accounts);
617        let range = verified_range(&accounts, 0..3, B256::ZERO, &[]);
618        let (storages, bytecodes) = dependencies();
619        let provider = factory.database_provider_rw().unwrap();
620
621        let no_storage =
622            provider.commit_account_range(write, &range, Default::default(), bytecodes.clone());
623        assert!(
624            matches!(no_storage, Err(SnapSyncError::MissingStorage { account }) if account == key(2))
625        );
626
627        let no_code = provider.commit_account_range(write, &range, storages, Vec::new());
628        assert!(matches!(no_code, Err(SnapSyncError::MissingCode { .. })));
629
630        let stray = B256Map::from_iter([(key(1), storage().1), (key(2), storage().1)]);
631        let unexpected = provider.commit_account_range(write, &range, stray, bytecodes);
632        assert!(
633            matches!(unexpected, Err(SnapSyncError::UnexpectedStorage { account }) if account == key(1))
634        );
635
636        assert_eq!(stored(&provider), (Vec::new(), false, false));
637        assert_eq!(provider.account_coverage(write).unwrap(), Some(AccountCoverage::START));
638    }
639
640    #[test]
641    fn missing_code_is_named_after_contracts_whose_code_is_stored() {
642        let stored_code = code();
643        let missing_code = Bytecode::new_raw(Bytes::from_static(&[0x60, 0x01]));
644        let mut first = account(1);
645        first.code_hash = stored_code.hash_slow();
646        let mut second = account(2);
647        second.code_hash = missing_code.hash_slow();
648        let accounts = vec![(key(1), first), (key(2), second), (key(3), account(3))];
649        let (factory, write, _) = started(&accounts);
650        let range = verified_range(&accounts, 0..3, B256::ZERO, &[]);
651        let provider = factory.database_provider_rw().unwrap();
652        provider
653            .write_state_changes(StateChangeset {
654                contracts: vec![(stored_code.hash_slow(), stored_code)],
655                ..Default::default()
656            })
657            .unwrap();
658
659        let refused = provider.commit_account_range(write, &range, Default::default(), Vec::new());
660
661        assert!(matches!(
662            refused,
663            Err(SnapSyncError::MissingCode { hash }) if hash == missing_code.hash_slow()
664        ));
665        assert_eq!(stored_accounts(&provider), Vec::<B256>::new());
666        assert_eq!(provider.account_coverage(write).unwrap(), Some(AccountCoverage::START));
667    }
668
669    #[test]
670    fn storage_and_code_are_checked_against_what_the_account_commits_to() {
671        let accounts = accounts();
672        let (factory, write, _) = started(&accounts);
673        let range = verified_range(&accounts, 0..3, B256::ZERO, &[]);
674        let (storages, bytecodes) = dependencies();
675        let provider = factory.database_provider_rw().unwrap();
676
677        // A slot short of the storage root, as a partial download would be.
678        let partial = B256Map::from_iter([(key(2), HashedStorage::default())]);
679        let short = provider.commit_account_range(write, &range, partial, bytecodes);
680        assert!(matches!(
681            short,
682            Err(SnapSyncError::StorageRootMismatch { account, expected, .. })
683                if account == key(2) && expected == storage().0
684        ));
685
686        // Code filed under a hash it does not hash to.
687        let other = Bytecode::new_raw(Bytes::from_static(&[0x60, 0x01]));
688        let relabelled = vec![(code().hash_slow(), other)];
689        let wrong = provider.commit_account_range(write, &range, storages, relabelled);
690        assert!(matches!(
691            wrong,
692            Err(SnapSyncError::CodeMismatch { expected, .. }) if expected == code().hash_slow()
693        ));
694
695        assert_eq!(stored(&provider), (Vec::new(), false, false));
696        assert_eq!(provider.account_coverage(write).unwrap(), Some(AccountCoverage::START));
697    }
698
699    #[test]
700    fn code_already_stored_need_not_be_supplied() {
701        let accounts = accounts();
702        let (factory, write, _) = started(&accounts);
703        let range = verified_range(&accounts, 0..3, B256::ZERO, &[]);
704        let (storages, _) = dependencies();
705        let provider = factory.database_provider_rw().unwrap();
706        provider
707            .write_state_changes(StateChangeset {
708                contracts: vec![(code().hash_slow(), code())],
709                ..Default::default()
710            })
711            .unwrap();
712
713        let coverage = provider.commit_account_range(write, &range, storages, Vec::new()).unwrap();
714
715        assert!(coverage.is_complete());
716    }
717
718    #[test]
719    fn starting_coverage_again_resumes_where_the_attempt_left_off() {
720        let accounts = accounts();
721        let (factory, write, _) = started(&accounts);
722        let range = verified_range(&accounts, 0..1, B256::ZERO, &[key(1)]);
723        let provider = factory.database_provider_rw().unwrap();
724        let committed =
725            provider.commit_account_range(write, &range, Default::default(), Vec::new()).unwrap();
726
727        assert_eq!(provider.start_account_coverage(write).unwrap(), committed);
728        assert_eq!(provider.account_coverage(write).unwrap(), Some(committed));
729    }
730
731    #[test]
732    fn a_write_from_a_replaced_attempt_changes_nothing() {
733        let accounts = accounts();
734        let (factory, replaced, generation) = started(&accounts);
735        let range = verified_range(&accounts, 0..3, B256::ZERO, &[]);
736        let (storages, bytecodes) = dependencies();
737        let provider = factory.database_provider_rw().unwrap();
738        let current = provider.start_snap_attempt(generation).unwrap();
739
740        let refused = provider.commit_account_range(replaced, &range, storages, bytecodes);
741
742        assert!(matches!(refused, Err(SnapSyncError::StaleWrite { .. })));
743        assert_eq!(stored(&provider), (Vec::new(), false, false));
744        // The replaced attempt's coverage is not the new attempt's either.
745        assert_eq!(provider.account_coverage(current).unwrap(), None);
746        assert!(matches!(
747            provider.account_coverage(replaced),
748            Err(SnapSyncError::StaleWrite { .. })
749        ));
750    }
751
752    #[test]
753    fn a_new_attempt_replaces_what_an_earlier_one_left_in_the_interval() {
754        let earlier = accounts();
755        let current = vec![(key(1), account(9)), (FAR, account(3))];
756        let (factory, write, _) = started_moving_to(&earlier, &current);
757        let (storages, bytecodes) = dependencies();
758        let provider = factory.database_provider_rw().unwrap();
759        let range = verified_range(&earlier, 0..3, B256::ZERO, &[]);
760        provider.commit_account_range(write, &range, storages, bytecodes).unwrap();
761        provider.commit().unwrap();
762
763        // At the new root the contract is gone and the first account changed.
764        let provider = factory.database_provider_rw().unwrap();
765        let write = provider.start_snap_attempt(generation(2, state_root(&current))).unwrap();
766        provider.start_account_coverage(write).unwrap();
767        let range = verified_range(&current, 0..2, B256::ZERO, &[]);
768        let coverage =
769            provider.commit_account_range(write, &range, Default::default(), Vec::new()).unwrap();
770
771        assert!(coverage.is_complete());
772        assert_eq!(stored(&provider), (vec![key(1), FAR], false, true));
773        let first = provider.tx_ref().get::<tables::HashedAccounts>(key(1)).unwrap().unwrap();
774        assert_eq!(first.nonce, 9);
775    }
776
777    #[test]
778    fn a_range_proved_against_another_root_is_refused() {
779        let accounts = accounts();
780        let (factory, write, _) = started(&accounts);
781        let provider = factory.database_provider_rw().unwrap();
782        let other = vec![(key(7), account(7))];
783        let range = verified_range(&other, 0..1, B256::ZERO, &[]);
784
785        let refused = provider.commit_account_range(write, &range, Default::default(), Vec::new());
786
787        assert!(matches!(refused, Err(SnapSyncError::RootMismatch { .. })));
788        assert!(stored_accounts(&provider).is_empty());
789    }
790
791    #[test]
792    fn a_range_without_recorded_coverage_is_refused() {
793        let accounts = accounts();
794        let factory = hashed_factory_with_roots(&[state_root(&accounts)]);
795        let provider = factory.database_provider_rw().unwrap();
796        let write = provider.start_snap_attempt(generation(1, state_root(&accounts))).unwrap();
797        let range = verified_range(&accounts, 0..3, B256::ZERO, &[]);
798
799        assert!(matches!(
800            provider.commit_account_range(write, &range, Default::default(), Vec::new()),
801            Err(SnapSyncError::NoCoverage)
802        ));
803    }
804
805    #[test]
806    fn storage_persisted_ahead_of_the_range_commits_with_it() {
807        let accounts = accounts();
808        let (factory, write, _) = started(&accounts);
809        let range = verified_range(&accounts, 0..3, B256::ZERO, &[]);
810        let (_, bytecodes) = dependencies();
811        let provider = factory.database_provider_rw().unwrap();
812        // Left behind for an account that has no storage at this root.
813        let leftover = HashedPostState::default().with_storages([(key(1), storage().1)]);
814        provider.write_hashed_state(&leftover.into_sorted()).unwrap();
815        let slots = vec![(SLOT, U256::from(7))];
816        let chunk = StorageChunk::new(key(2), storage().0, B256::ZERO, slots, None);
817        provider.commit_storage_chunk(write, B256::ZERO, chunk).unwrap();
818
819        let coverage =
820            provider.commit_account_range(write, &range, Default::default(), bytecodes).unwrap();
821
822        assert!(coverage.is_complete());
823        assert_eq!(stored(&provider), (vec![key(1), key(2), FAR], true, true));
824        assert!(provider.tx_ref().get::<tables::HashedStorages>(key(1)).unwrap().is_none());
825    }
826
827    #[test]
828    fn persisted_storage_must_be_complete_and_match_the_account_root() {
829        let accounts = accounts();
830        let (factory, write, _) = started(&accounts);
831        let range = verified_range(&accounts, 0..3, B256::ZERO, &[]);
832        let (_, bytecodes) = dependencies();
833
834        // Part way through the contract.
835        let provider = factory.database_provider_rw().unwrap();
836        let slots = vec![(SLOT, U256::from(7))];
837        let next = Some(B256::repeat_byte(0x66));
838        let partial = StorageChunk::new(key(2), storage().0, B256::ZERO, slots, next);
839        provider.commit_storage_chunk(write, B256::ZERO, partial).unwrap();
840        let incomplete =
841            provider.commit_account_range(write, &range, Default::default(), bytecodes.clone());
842        assert!(
843            matches!(incomplete, Err(SnapSyncError::MissingStorage { account }) if account == key(2))
844        );
845        drop(provider);
846
847        // Complete, but not the storage the account commits to.
848        let provider = factory.database_provider_rw().unwrap();
849        let slots = vec![(SLOT, U256::from(8))];
850        let wrong = StorageChunk::new(key(2), storage().0, B256::ZERO, slots, None);
851        provider.commit_storage_chunk(write, B256::ZERO, wrong).unwrap();
852        let mismatched =
853            provider.commit_account_range(write, &range, Default::default(), bytecodes);
854        assert!(matches!(
855            mismatched,
856            Err(SnapSyncError::StorageRootMismatch { account, .. }) if account == key(2)
857        ));
858        assert_eq!(provider.account_coverage(write).unwrap(), Some(AccountCoverage::START));
859    }
860
861    #[test]
862    fn a_record_this_build_cannot_read_is_reported() {
863        let accounts = accounts();
864        let (factory, write, _) = started(&accounts);
865
866        for record in [br#"{"version":999}"#.to_vec(), b"{}".to_vec()] {
867            let provider = factory.database_provider_rw().unwrap();
868            provider.write_metadata(StoredCoverage::KEY, record).unwrap();
869
870            assert!(matches!(
871                provider.account_coverage(write),
872                Err(SnapSyncError::UnsupportedRecord { .. })
873            ));
874        }
875    }
876
877    // `started`, with headers so the pivot is canonical, and stale values for the contract and
878    // for `ABSENT`, both scheduled for repair.
879    fn repairing(
880        accounts: &[(B256, TrieAccount)],
881    ) -> (ProviderFactory<MockNodeTypesWithDB>, SnapWrite) {
882        let (factory, write, _) = started(accounts);
883        let provider = factory.database_provider_rw().unwrap();
884        let stale = HashedPostState::default()
885            .with_accounts([
886                (key(2), Some(Account::from(account(9)))),
887                (ABSENT, Some(Account::from(account(9)))),
888            ])
889            .with_storages([
890                (key(2), HashedStorage::from_iter([(SLOT, U256::ONE), (OTHER, U256::from(2))])),
891                (ABSENT, HashedStorage::from_iter([(SLOT, U256::ONE)])),
892            ]);
893        provider.write_hashed_state(&stale.into_sorted()).unwrap();
894        let mut repairs = StateRepairs::default();
895        repairs.insert_slot(key(2), SLOT);
896        repairs.insert_slot(key(2), OTHER);
897        repairs.insert_account(ABSENT);
898        provider.schedule_snap_repairs(write, repairs).unwrap();
899        provider.commit().unwrap();
900        (factory, write)
901    }
902
903    fn repair(
904        factory: &ProviderFactory<MockNodeTypesWithDB>,
905        write: SnapWrite,
906        range: &VerifiedAccountRange,
907        slots: Vec<(B256, U256)>,
908    ) -> Result<usize, SnapSyncError> {
909        let provider = factory.database_provider_rw().unwrap();
910        let remaining = provider.commit_account_repair(write, range, slots)?;
911        provider.commit().unwrap();
912        Ok(remaining)
913    }
914
915    #[test]
916    #[allow(clippy::clone_on_copy)]
917    fn a_repair_takes_the_pivot_account_and_scheduled_slots() {
918        let accounts = accounts();
919        let (factory, write) = repairing(&accounts);
920        let range = verified_repair(&accounts, 1..2, key(2), &[key(2)]);
921        // The pivot holds 7 at `SLOT` and nothing at `OTHER`.
922        let slots = vec![(SLOT, U256::from(7)), (OTHER, U256::ZERO)];
923
924        let refused = repair(&factory, write, &range, slots.clone());
925        assert!(matches!(refused, Err(SnapSyncError::MissingCode { .. })));
926        let provider = factory.database_provider_rw().unwrap();
927        provider
928            .write_state_changes(StateChangeset {
929                contracts: vec![(code().hash_slow(), code())],
930                ..Default::default()
931            })
932            .unwrap();
933        provider.commit().unwrap();
934        let remaining = repair(&factory, write, &range, slots).unwrap();
935
936        assert_eq!(remaining, 1);
937        let provider = factory.database_provider_ro().unwrap();
938        let mut expected = StateRepairs::default();
939        expected.insert_account(ABSENT);
940        assert_eq!(provider.snap_repairs(write).unwrap(), expected);
941        let stored = provider.tx_ref().get::<tables::HashedAccounts>(key(2)).unwrap();
942        assert_eq!(stored, Some(Account::from(accounts[1].1.clone())));
943        assert_eq!(stored_slots(&provider, key(2)), [(SLOT, U256::from(7))]);
944    }
945
946    #[test]
947    fn an_account_the_pivot_lacks_is_removed_with_its_storage() {
948        let accounts = accounts();
949        let (factory, write) = repairing(&accounts);
950        // The far account, the first past `ABSENT`, proves it absent.
951        let range = verified_repair(&accounts, 2..3, ABSENT, &[ABSENT, FAR]);
952
953        repair(&factory, write, &range, Vec::new()).unwrap();
954
955        let provider = factory.database_provider_ro().unwrap();
956        assert_eq!(provider.tx_ref().get::<tables::HashedAccounts>(ABSENT).unwrap(), None);
957        assert!(stored_slots(&provider, ABSENT).is_empty());
958        assert_eq!(provider.snap_repairs(write).unwrap().len(), 1);
959    }
960
961    #[test]
962    fn a_repair_waits_for_catch_up_to_reach_the_pivot() {
963        let accounts = accounts();
964        let (factory, write) = repairing(&accounts);
965        let provider = factory.database_provider_rw().unwrap();
966        let write =
967            provider.advance_snap_pivot(write, generation(2, state_root(&accounts))).unwrap();
968        provider.commit().unwrap();
969        let range = verified_repair(&accounts, 2..3, ABSENT, &[ABSENT, FAR]);
970
971        let refused = repair(&factory, write, &range, Vec::new());
972
973        assert!(matches!(refused, Err(SnapSyncError::CatchUpBehindPivot { applied: 1, pivot: 2 })));
974    }
975
976    #[test]
977    fn a_new_attempt_starts_without_repairs() {
978        let accounts = accounts();
979        let (factory, _) = repairing(&accounts);
980        let provider = factory.database_provider_rw().unwrap();
981
982        let restarted = provider.start_snap_attempt(generation(1, state_root(&accounts))).unwrap();
983
984        assert!(provider.snap_repairs(restarted).unwrap().is_empty());
985        assert!(StoredRepairs::read(&provider).unwrap().is_none());
986    }
987}