Skip to main content

reth_snap_sync/
verify.rs

1//! Decides when downloaded state is complete, and accepts it once its trie matches the pivot.
2//!
3//! The trie itself is rebuilt by the merkle stage, which commits its progress in chunks and checks
4//! the root against the target header, so a rebuild survives restarts on any state size.
5
6use crate::{
7    common::SnapRecord, SnapAccountStore, SnapAttemptStore, SnapCatchUpStore, SnapSyncError,
8    SnapWrite,
9};
10use alloy_eips::BlockNumHash;
11use alloy_primitives::{B256, KECCAK256_EMPTY};
12use reth_db_api::{
13    cursor::DbCursorRO,
14    tables,
15    transaction::{DbTx, DbTxMut},
16    RawKey, RawTable,
17};
18use reth_primitives_traits::{AlloyBlockHeader, GotExpected};
19use reth_prune_types::{PruneCheckpoint, PruneSegment};
20use reth_stages_types::{StageCheckpoint, StageId};
21use reth_storage_api::{
22    BlockHashReader, BlockWriter, DBProvider, HeaderProvider, MetadataProvider, MetadataWriter,
23    PruneCheckpointWriter, SnapAttemptId, StageCheckpointReader, StageCheckpointWriter,
24};
25use reth_storage_errors::provider::{ProviderError, RootMismatch};
26use serde::{Deserialize, Serialize};
27use tokio_util::sync::CancellationToken;
28
29/// Accounts scanned between cancellation checks, bounding how long a cancelled session keeps
30/// running.
31pub const DEFAULT_SCAN_CHUNK: u64 = 100_000;
32
33// Stages publishing moves to the pivot, since the downloaded state replaces their output. Headers
34// and era import run on their own, the merkle stage still rebuilds the trie, and `Finish` waits
35// for verification.
36const PUBLISHED_STAGES: [StageId; 11] = [
37    StageId::Bodies,
38    StageId::SenderRecovery,
39    StageId::Execution,
40    StageId::PruneSenderRecovery,
41    StageId::MerkleUnwind,
42    StageId::AccountHashing,
43    StageId::StorageHashing,
44    StageId::TransactionLookup,
45    StageId::IndexStorageHistory,
46    StageId::IndexAccountHistory,
47    StageId::Prune,
48];
49
50/// Decides whether downloaded state can be trusted as the node's state.
51///
52/// Verification writes join the caller's transaction, so a refused check changes nothing.
53pub trait SnapStateVerifier {
54    /// Hands complete state to the merkle stage, which rebuilds its trie from scratch.
55    ///
56    /// Progress the stage recorded before the hand-off describes other state, so it is discarded.
57    /// The hand-off is recorded for this attempt and pivot, so only a rebuild of this state counts.
58    fn start_trie_rebuild(
59        &self,
60        write: SnapWrite,
61        chunk: u64,
62        cancel: &CancellationToken,
63    ) -> Result<(), SnapSyncError>
64    where
65        Self: MetadataWriter + StageCheckpointWriter + DBProvider;
66
67    /// Refuses to finish while any downloaded state is still pending. Storage commits with its
68    /// accounts, so it needs no separate check.
69    fn verify_completeness(
70        &self,
71        write: SnapWrite,
72        chunk: u64,
73        cancel: &CancellationToken,
74    ) -> Result<(), SnapSyncError>
75    where
76        Self: DBProvider;
77
78    /// Publishes the state downloaded at `pivot`: makes it the node's state at that block, so the
79    /// pipeline resumes at `pivot + 1` instead of executing from genesis.
80    ///
81    /// - Moves the stages the downloaded state covers to `pivot`.
82    /// - Records history below `pivot` as pruned.
83    /// - Clears body indices, database receipts and transaction lookups, so transaction numbers
84    ///   restart at 0 above `pivot`. `RocksDB` lookups are cleared immediately, not on commit.
85    ///
86    /// Publishing is not verification: the merkle stage and `Finish` wait for the trie rebuild.
87    /// It cannot be undone, the node must not unwind below `pivot`, and the caller resets the
88    /// static files to `pivot` in the same commit.
89    fn publish_snap_state(&self, pivot: u64) -> Result<(), SnapSyncError>
90    where
91        Self: BlockWriter + PruneCheckpointWriter + StageCheckpointWriter + DBProvider<Tx: DbTxMut>;
92
93    /// Returns whether `write`'s state was handed to the merkle stage, so a resumed sync does not
94    /// reset its rebuild.
95    fn is_trie_rebuild_started(&self, write: SnapWrite) -> Result<bool, SnapSyncError>;
96
97    /// Accepts the state once the merkle stage has rebuilt its trie up to the pivot.
98    ///
99    /// The stage checks the root against the pivot header, which must commit to the root the
100    /// state was downloaded against.
101    fn verify_state_root(&self, write: SnapWrite) -> Result<VerifiedSnapState, SnapSyncError>
102    where
103        Self: BlockHashReader + HeaderProvider + MetadataWriter + StageCheckpointReader;
104}
105
106/// Downloaded state whose trie root matched the header of the block it is anchored to.
107#[derive(Clone, Copy, Debug, Eq, PartialEq)]
108pub struct VerifiedSnapState {
109    // Attempt that downloaded the state.
110    attempt: SnapAttemptId,
111    // Block whose header commits to the state.
112    target: BlockNumHash,
113    // Root the target header commits to.
114    state_root: B256,
115}
116
117impl VerifiedSnapState {
118    /// Attempt that downloaded the state.
119    pub const fn attempt(&self) -> SnapAttemptId {
120        self.attempt
121    }
122
123    /// Block whose header commits to the state.
124    pub const fn target(&self) -> BlockNumHash {
125        self.target
126    }
127
128    /// Root the target header commits to.
129    pub const fn state_root(&self) -> B256 {
130        self.state_root
131    }
132}
133
134impl<T: MetadataProvider> SnapStateVerifier for T {
135    fn start_trie_rebuild(
136        &self,
137        write: SnapWrite,
138        chunk: u64,
139        cancel: &CancellationToken,
140    ) -> Result<(), SnapSyncError>
141    where
142        Self: MetadataWriter + StageCheckpointWriter + DBProvider,
143    {
144        // The stage treats a checkpoint at genesis as already rebuilt, so it would never check
145        // state anchored there.
146        if self.authorize_snap_write(write)?.pivot().number == 0 {
147            return Err(SnapSyncError::GenesisPivot)
148        }
149        self.verify_completeness(write, chunk, cancel)?;
150        // Starting from block zero makes the stage clear the trie tables and rebuild them, since
151        // downloaded state has no changesets to update an existing trie from.
152        self.save_stage_checkpoint(StageId::MerkleExecute, StageCheckpoint::default())?;
153        self.save_stage_checkpoint_progress(StageId::MerkleExecute, Vec::new())?;
154        StoredRebuild::new(write).write(self)?;
155        Ok(())
156    }
157
158    fn publish_snap_state(&self, pivot: u64) -> Result<(), SnapSyncError>
159    where
160        Self: BlockWriter + PruneCheckpointWriter + StageCheckpointWriter + DBProvider<Tx: DbTxMut>,
161    {
162        if pivot == 0 {
163            return Err(SnapSyncError::GenesisPivot)
164        }
165        // Bodies downloaded before the pivot moved allocated transaction numbers that the emptied
166        // transaction segments no longer hold.
167        self.tx_ref().clear::<tables::BlockBodyIndices>()?;
168        self.tx_ref().clear::<tables::TransactionBlocks>()?;
169        // Their withdrawals and ommers are appended per block, so writing above the pivot again
170        // would fail on the rows left behind. These are the Ethereum body tables, a chain storage
171        // with body tables of its own has to clear them as well.
172        self.tx_ref().clear::<tables::BlockWithdrawals>()?;
173        self.tx_ref().clear::<tables::BlockOmmers>()?;
174        // Receipt log filtering keeps receipts in MDBX even with storage v2. Their transaction
175        // numbers must be reusable after publication.
176        self.tx_ref().clear::<tables::Receipts>()?;
177        self.clear_transaction_lookup()?;
178
179        let checkpoint = StageCheckpoint::new(pivot);
180        for stage in PUBLISHED_STAGES {
181            self.save_stage_checkpoint(stage, checkpoint)?;
182        }
183        // Snap sync wrote no history below the pivot.
184        let pruned = PruneCheckpoint::pruned_through(pivot);
185        for segment in PruneSegment::variants() {
186            self.save_prune_checkpoint(segment, pruned)?;
187        }
188        Ok(())
189    }
190
191    fn verify_completeness(
192        &self,
193        write: SnapWrite,
194        chunk: u64,
195        cancel: &CancellationToken,
196    ) -> Result<(), SnapSyncError>
197    where
198        Self: DBProvider,
199    {
200        let attempt = self.authorize_snap_write(write)?;
201        let coverage = self.account_coverage(write)?.ok_or(SnapSyncError::NoCoverage)?;
202        if let Some(next) = coverage.next() {
203            return Err(SnapSyncError::IncompleteAccounts { next })
204        }
205        let applied =
206            self.catch_up_progress(write)?.ok_or(SnapSyncError::NoCatchUpProgress)?.applied();
207        if applied != attempt.pivot() {
208            return Err(SnapSyncError::CatchUpBehindPivot {
209                applied: applied.number,
210                pivot: attempt.pivot().number,
211            })
212        }
213        let repairs = self.snap_repairs(write)?;
214        if !repairs.is_empty() {
215            return Err(SnapSyncError::PendingRepairs { accounts: repairs.len() })
216        }
217        ensure_code_present(self.tx_ref(), chunk, cancel)
218    }
219
220    fn is_trie_rebuild_started(&self, write: SnapWrite) -> Result<bool, SnapSyncError> {
221        Ok(StoredRebuild::read(self)?.is_some_and(|stored| stored.write == write))
222    }
223
224    fn verify_state_root(&self, write: SnapWrite) -> Result<VerifiedSnapState, SnapSyncError>
225    where
226        Self: BlockHashReader + HeaderProvider + MetadataWriter + StageCheckpointReader,
227    {
228        let attempt = self.authorize_canonical_snap_write(write)?;
229        let target = attempt.pivot();
230        let header = self
231            .sealed_header(target.number)?
232            .filter(|header| header.hash() == target.hash)
233            .ok_or(SnapSyncError::MissingHeader { block: target.number })?;
234        // The stage answers to the header, while downloaded ranges answered to the attempt.
235        if header.state_root() != attempt.state_root() {
236            return Err(ProviderError::StateRootMismatch(Box::new(RootMismatch {
237                root: GotExpected { got: attempt.state_root(), expected: header.state_root() },
238                block_number: target.number,
239                block_hash: target.hash,
240            }))
241            .into())
242        }
243        // Until the stage reaches the pivot after this state's hand-off, the trie holds no state
244        // for it: an earlier attempt's rebuild can end at the same block.
245        let handed_off = self.is_trie_rebuild_started(write)?;
246        let rebuilt = self.get_stage_checkpoint(StageId::MerkleExecute)?;
247        if !handed_off || rebuilt.map(|checkpoint| checkpoint.block_number) != Some(target.number) {
248            return Err(ProviderError::StateForNumberNotFound(target.number).into())
249        }
250        self.verify_snap_attempt(write)?;
251        Ok(VerifiedSnapState { attempt: attempt.id(), target, state_root: header.state_root() })
252    }
253}
254
255// The trie rebuild hand-off as persisted, tied to the write it was made for.
256#[derive(Serialize, Deserialize)]
257pub(crate) struct StoredRebuild {
258    // Encoding version, checked before the rest is decoded.
259    version: u32,
260    // Attempt and pivot the state was handed off at.
261    write: SnapWrite,
262}
263
264impl SnapRecord for StoredRebuild {
265    const KEY: &'static str = "snap_trie_rebuild";
266    const VERSION: u32 = 1;
267}
268
269impl StoredRebuild {
270    // The hand-off of `write`'s state at this build's version.
271    const fn new(write: SnapWrite) -> Self {
272        Self { version: Self::VERSION, write }
273    }
274}
275
276// Refuses the first account whose code is not stored, checking for cancellation every `chunk`
277// accounts.
278fn ensure_code_present(
279    tx: &impl DbTx,
280    chunk: u64,
281    cancel: &CancellationToken,
282) -> Result<(), SnapSyncError> {
283    let chunk = chunk.max(1);
284    let mut cursor = tx.cursor_read::<tables::HashedAccounts>()?;
285    for (scanned, entry) in cursor.walk(None)?.enumerate() {
286        if (scanned as u64).is_multiple_of(chunk) && cancel.is_cancelled() {
287            return Err(SnapSyncError::Cancelled)
288        }
289        let (_, account) = entry?;
290        // Only presence matters, so stored code is not decoded.
291        if let Some(hash) = account.bytecode_hash.filter(|hash| *hash != KECCAK256_EMPTY) &&
292            tx.get::<RawTable<tables::Bytecodes>>(RawKey::new(hash))?.is_none()
293        {
294            return Err(SnapSyncError::MissingCode { hash })
295        }
296    }
297    Ok(())
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303    use crate::{
304        test_utils::{account, hashed_factory, header, key, state_root},
305        SnapGeneration,
306    };
307    use alloy_consensus::TxLegacy;
308    use alloy_eips::eip4895::{Withdrawal, Withdrawals};
309    use alloy_primitives::{map::B256Map, Address, Bytes, Signature, U256};
310    use reth_db_api::transaction::DbTxMut;
311    use reth_ethereum_primitives::{BlockBody, Receipt, Transaction, TransactionSigned};
312    use reth_primitives_traits::SealedHeader;
313    use reth_provider::{
314        test_utils::{insert_headers, MockNodeTypesWithDB},
315        BlockBodyIndicesProvider, DatabaseProviderFactory, EitherWriter, ProviderFactory,
316        PruneCheckpointReader, StaticFileProviderFactory, StaticFileSegment, StaticFileWriter,
317        StorageSettings, StorageSettingsCache, TransactionsProvider,
318    };
319    use reth_prune_types::{PruneMode, PruneModes, ReceiptsLogPruneConfig};
320    use reth_stages::stages::MerkleStage;
321    use reth_stages_api::{ExecInput, Stage, StageError};
322    use reth_trie_common::{root::storage_root_unsorted, HashedStorage, TrieAccount};
323    use revm::bytecode::Bytecode;
324    use std::collections::BTreeMap;
325
326    type Factory = ProviderFactory<MockNodeTypesWithDB>;
327    type Provider = <Factory as DatabaseProviderFactory>::ProviderRW;
328
329    const CONTRACT: B256 = B256::repeat_byte(0xaa);
330    const SLOT: B256 = B256::repeat_byte(0x55);
331
332    fn code() -> Bytecode {
333        Bytecode::new_raw(Bytes::from_static(&[0x60, 0x00]))
334    }
335
336    fn storage() -> HashedStorage {
337        HashedStorage::from_iter([(SLOT, U256::from(7))])
338    }
339
340    // Two plain accounts and a contract with one slot and code.
341    fn accounts() -> Vec<(B256, TrieAccount)> {
342        let mut contract = account(3);
343        contract.storage_root = storage_root_unsorted([(SLOT, U256::from(7))]);
344        contract.code_hash = code().hash_slow();
345        vec![(key(1), account(1)), (key(2), account(2)), (CONTRACT, contract)]
346    }
347
348    // Blocks 0 through 2, each header committing to `header_root`.
349    fn insert_chain(factory: &Factory, header_root: B256) -> Vec<BlockNumHash> {
350        let mut parent = B256::ZERO;
351        let headers: Vec<_> = (0..=2)
352            .map(|number| {
353                let mut header = header(number, parent, None);
354                header.state_root = header_root;
355                let sealed = SealedHeader::seal_slow(header);
356                parent = sealed.hash();
357                sealed
358            })
359            .collect();
360        insert_headers(factory, &headers);
361        headers.iter().map(|header| BlockNumHash::new(header.number, header.hash())).collect()
362    }
363
364    // An attempt pivoted at block 1 of a chain committing to `header_root`, with `served` of the
365    // accounts downloaded.
366    fn downloaded(header_root: B256, served: usize) -> (Factory, SnapWrite, Vec<BlockNumHash>) {
367        let accounts = accounts();
368        let factory = hashed_factory();
369        let blocks = insert_chain(&factory, header_root);
370        let provider = factory.database_provider_rw().unwrap();
371        let write = provider
372            .start_snap_attempt(SnapGeneration::new(blocks[1], state_root(&accounts)))
373            .unwrap();
374        provider.start_account_coverage(write).unwrap();
375        let targets = (served < accounts.len()).then(|| accounts[served - 1].0);
376        let range =
377            crate::test_utils::verified_range(&accounts, 0..served, B256::ZERO, targets.as_slice());
378        let complete = served == accounts.len();
379        let (storages, bytecodes) = if complete {
380            (B256Map::from_iter([(CONTRACT, storage())]), vec![(code().hash_slow(), code())])
381        } else {
382            Default::default()
383        };
384        provider.commit_account_range(write, &range, storages, bytecodes).unwrap();
385        provider.commit().unwrap();
386        (factory, write, blocks)
387    }
388
389    fn start(provider: &Provider, write: SnapWrite) -> Result<(), SnapSyncError> {
390        provider.start_trie_rebuild(write, 1, &CancellationToken::new())
391    }
392
393    // Runs the merkle stage from its checkpoint to `target`, as the pipeline would.
394    fn run_merkle(provider: &Provider, target: u64) -> Result<(), StageError> {
395        let mut stage = MerkleStage::default_execution();
396        loop {
397            let checkpoint = provider.get_stage_checkpoint(StageId::MerkleExecute).unwrap();
398            let output = stage.execute(provider, ExecInput { target: Some(target), checkpoint })?;
399            provider.save_stage_checkpoint(StageId::MerkleExecute, output.checkpoint).unwrap();
400            if output.done {
401                return Ok(())
402            }
403        }
404    }
405
406    fn merkle_checkpoint(provider: &Provider) -> Option<u64> {
407        provider.get_stage_checkpoint(StageId::MerkleExecute).unwrap().map(|c| c.block_number)
408    }
409
410    #[test]
411    fn complete_state_is_verified_once_the_merkle_stage_reaches_the_pivot() {
412        let root = state_root(&accounts());
413        let (factory, write, blocks) = downloaded(root, accounts().len());
414        let provider = factory.database_provider_rw().unwrap();
415
416        start(&provider, write).unwrap();
417        run_merkle(&provider, 1).unwrap();
418        let verified = provider.verify_state_root(write).unwrap();
419        provider.commit().unwrap();
420
421        assert_eq!((verified.target(), verified.state_root()), (blocks[1], root));
422        let provider = factory.database_provider_rw().unwrap();
423        assert!(provider.snap_attempt().unwrap().unwrap().is_verified());
424        // Verified state accepts no further writes, including a second verification.
425        assert!(matches!(provider.verify_state_root(write), Err(SnapSyncError::StaleWrite { .. })));
426    }
427
428    #[test]
429    fn unfinished_accounts_prevent_the_hand_off() {
430        let (factory, write, _) = downloaded(state_root(&accounts()), 1);
431        let provider = factory.database_provider_rw().unwrap();
432
433        assert!(matches!(
434            start(&provider, write),
435            Err(SnapSyncError::IncompleteAccounts { next }) if next == key(2)
436        ));
437        assert_eq!(merkle_checkpoint(&provider), None);
438    }
439
440    #[test]
441    fn missing_code_prevents_the_hand_off() {
442        let (factory, write, _) = downloaded(state_root(&accounts()), accounts().len());
443        let provider = factory.database_provider_rw().unwrap();
444        provider.tx_ref().delete::<tables::Bytecodes>(code().hash_slow(), None).unwrap();
445
446        assert!(matches!(
447            start(&provider, write),
448            Err(SnapSyncError::MissingCode { hash }) if hash == code().hash_slow()
449        ));
450    }
451
452    #[test]
453    fn catch_up_short_of_the_pivot_prevents_the_hand_off() {
454        let root = state_root(&accounts());
455        let (factory, write, blocks) = downloaded(root, accounts().len());
456        let provider = factory.database_provider_rw().unwrap();
457        let write =
458            provider.advance_snap_pivot(write, SnapGeneration::new(blocks[2], root)).unwrap();
459
460        assert!(matches!(
461            start(&provider, write),
462            Err(SnapSyncError::CatchUpBehindPivot { applied: 1, pivot: 2 })
463        ));
464    }
465
466    #[test]
467    fn a_cancelled_session_stops_the_completeness_scan() {
468        let (factory, write, _) = downloaded(state_root(&accounts()), accounts().len());
469        let provider = factory.database_provider_rw().unwrap();
470        let cancel = CancellationToken::new();
471        cancel.cancel();
472
473        assert!(matches!(
474            provider.start_trie_rebuild(write, 1, &cancel),
475            Err(SnapSyncError::Cancelled)
476        ));
477        assert_eq!(merkle_checkpoint(&provider), None);
478    }
479
480    #[test]
481    fn corrupted_state_never_reaches_the_pivot() {
482        let (factory, write, _) = downloaded(state_root(&accounts()), accounts().len());
483        let provider = factory.database_provider_rw().unwrap();
484        start(&provider, write).unwrap();
485        provider.tx_ref().delete::<tables::HashedStorages>(CONTRACT, None).unwrap();
486
487        // The stage refuses a root other than the header's.
488        assert!(matches!(run_merkle(&provider, 1), Err(StageError::Block { .. })));
489        assert!(matches!(
490            provider.verify_state_root(write),
491            Err(SnapSyncError::Provider(ProviderError::StateForNumberNotFound(1)))
492        ));
493        assert!(provider.snap_attempt().unwrap().unwrap().is_unfinished());
494    }
495
496    #[test]
497    fn an_interrupted_rebuild_is_not_verified() {
498        let (factory, write, _) = downloaded(state_root(&accounts()), accounts().len());
499        let provider = factory.database_provider_rw().unwrap();
500        // Progress recorded before the hand-off describes other state.
501        provider.save_stage_checkpoint(StageId::MerkleExecute, StageCheckpoint::new(1)).unwrap();
502
503        start(&provider, write).unwrap();
504
505        assert!(matches!(
506            provider.verify_state_root(write),
507            Err(SnapSyncError::Provider(ProviderError::StateForNumberNotFound(1)))
508        ));
509        run_merkle(&provider, 1).unwrap();
510        provider.verify_state_root(write).unwrap();
511    }
512
513    #[test]
514    fn a_rebuild_for_an_earlier_attempt_is_not_trusted() {
515        let root = state_root(&accounts());
516        let (factory, write, blocks) = downloaded(root, accounts().len());
517        let provider = factory.database_provider_rw().unwrap();
518        start(&provider, write).unwrap();
519        run_merkle(&provider, 1).unwrap();
520
521        // A new attempt at the same pivot, with nothing downloaded yet.
522        let restarted = provider.start_snap_attempt(SnapGeneration::new(blocks[1], root)).unwrap();
523
524        assert_eq!(merkle_checkpoint(&provider), Some(1));
525        assert!(matches!(
526            provider.verify_state_root(restarted),
527            Err(SnapSyncError::Provider(ProviderError::StateForNumberNotFound(1)))
528        ));
529    }
530
531    #[test]
532    fn moving_the_pivot_after_the_hand_off_requires_a_new_one() {
533        let root = state_root(&accounts());
534        let (factory, write, blocks) = downloaded(root, accounts().len());
535        let provider = factory.database_provider_rw().unwrap();
536        start(&provider, write).unwrap();
537        let advanced =
538            provider.advance_snap_pivot(write, SnapGeneration::new(blocks[2], root)).unwrap();
539
540        run_merkle(&provider, 2).unwrap();
541
542        assert!(matches!(
543            provider.verify_state_root(advanced),
544            Err(SnapSyncError::Provider(ProviderError::StateForNumberNotFound(2)))
545        ));
546    }
547
548    #[test]
549    fn a_genesis_pivot_is_not_handed_off() {
550        let root = state_root(&accounts());
551        let (factory, _, blocks) = downloaded(root, accounts().len());
552        let provider = factory.database_provider_rw().unwrap();
553        let write = provider.start_snap_attempt(SnapGeneration::new(blocks[0], root)).unwrap();
554
555        assert!(matches!(start(&provider, write), Err(SnapSyncError::GenesisPivot)));
556        assert_eq!(merkle_checkpoint(&provider), None);
557    }
558
559    #[test]
560    fn publishing_moves_the_covered_stages_and_prunes_the_history_below_the_pivot() {
561        let factory = hashed_factory();
562        let provider = factory.database_provider_rw().unwrap();
563
564        provider.publish_snap_state(7).unwrap();
565
566        // Every stage must be classified, so a new one fails here until it is.
567        let kept = [StageId::Era, StageId::Headers, StageId::MerkleExecute, StageId::Finish];
568        for stage in StageId::ALL {
569            let published = PUBLISHED_STAGES.contains(&stage);
570            assert_ne!(published, kept.contains(&stage), "{stage} must be published or kept");
571            let expected = published.then(|| StageCheckpoint::new(7));
572            assert_eq!(provider.get_stage_checkpoint(stage).unwrap(), expected, "{stage}");
573        }
574
575        // The history below the pivot is recorded as pruned for every segment.
576        for segment in PruneSegment::variants() {
577            let checkpoint = provider.get_prune_checkpoint(segment).unwrap();
578            assert_eq!(checkpoint, Some(PruneCheckpoint::pruned_through(7)), "{segment}");
579        }
580    }
581
582    #[test]
583    fn publishing_refuses_the_genesis_pivot() {
584        let factory = hashed_factory();
585        let provider = factory.database_provider_rw().unwrap();
586
587        assert!(matches!(provider.publish_snap_state(0), Err(SnapSyncError::GenesisPivot)));
588    }
589
590    #[test]
591    fn bodies_downloaded_past_the_pivot_can_be_written_again_after_publishing() {
592        let factory = hashed_factory();
593        insert_chain(&factory, B256::ZERO);
594        let body = BlockBody {
595            withdrawals: Some(Withdrawals::new(vec![Withdrawal::default()])),
596            ..Default::default()
597        };
598        let provider = factory.database_provider_rw().unwrap();
599        provider
600            .append_block_bodies(vec![
601                (0, None),
602                (1, Some(&body)),
603                (2, Some(&body)),
604                (3, Some(&body)),
605            ])
606            .unwrap();
607        provider.commit().unwrap();
608
609        // Publish at block 1 and reset the transaction file to it, as the caller does.
610        let static_files = factory.static_file_provider();
611        let provider = factory.database_provider_rw().unwrap();
612        provider.publish_snap_state(1).unwrap();
613        static_files.delete_segment(StaticFileSegment::Transactions).unwrap();
614        static_files
615            .latest_writer(StaticFileSegment::Transactions)
616            .unwrap()
617            .ensure_at_block(1)
618            .unwrap();
619        provider.commit().unwrap();
620
621        let provider = factory.database_provider_rw().unwrap();
622        provider.append_block_bodies(vec![(2, Some(&body))]).unwrap();
623        provider.commit().unwrap();
624    }
625
626    #[test]
627    fn published_state_can_unwind_bodies_to_the_pivot() {
628        for tx_count in [0, 2] {
629            let factory = hashed_factory();
630            insert_chain(&factory, B256::ZERO);
631            let provider = factory.database_provider_rw().unwrap();
632            provider.publish_snap_state(1).unwrap();
633            factory
634                .static_file_provider()
635                .latest_writer(StaticFileSegment::Transactions)
636                .unwrap()
637                .ensure_at_block(1)
638                .unwrap();
639            provider.commit().unwrap();
640
641            let body = BlockBody {
642                transactions: (0..tx_count)
643                    .map(|nonce| {
644                        TransactionSigned::new_unhashed(
645                            Transaction::Legacy(TxLegacy { nonce, ..Default::default() }),
646                            Signature::test_signature(),
647                        )
648                    })
649                    .collect(),
650                ..Default::default()
651            };
652            let provider = factory.database_provider_rw().unwrap();
653            provider.append_block_bodies(vec![(2, Some(&body))]).unwrap();
654            provider.commit().unwrap();
655
656            let provider = factory.database_provider_rw().unwrap();
657            assert_eq!(provider.block_body_indices(1).unwrap(), None);
658            assert_eq!(provider.next_tx_num_after_block(1).unwrap(), 0);
659            assert_eq!(provider.next_tx_num_after_block(2).unwrap(), tx_count);
660            provider.remove_bodies_above(1).unwrap();
661            provider.commit().unwrap();
662
663            let provider = factory.database_provider_ro().unwrap();
664            assert_eq!(provider.block_body_indices(1).unwrap(), None);
665            assert_eq!(provider.block_body_indices(2).unwrap(), None);
666            let static_files = factory.static_file_provider();
667            assert_eq!(
668                static_files.get_highest_static_file_block(StaticFileSegment::Transactions),
669                Some(1)
670            );
671            assert_eq!(
672                static_files.get_highest_static_file_tx(StaticFileSegment::Transactions),
673                None
674            );
675        }
676    }
677
678    #[test]
679    fn startup_heals_an_interrupted_first_write_after_publishing() {
680        // An empty block is covered too, since transaction counts alone can't see its height.
681        for tx_count in [0, 1] {
682            let factory = hashed_factory();
683            insert_chain(&factory, B256::ZERO);
684            let static_files = factory.static_file_provider();
685            let provider = factory.database_provider_rw().unwrap();
686            provider.publish_snap_state(1).unwrap();
687            static_files
688                .latest_writer(StaticFileSegment::Transactions)
689                .unwrap()
690                .ensure_at_block(1)
691                .unwrap();
692            provider.commit().unwrap();
693
694            let body = BlockBody {
695                transactions: (0..tx_count)
696                    .map(|nonce| {
697                        TransactionSigned::new_unhashed(
698                            Transaction::Legacy(TxLegacy { nonce, ..Default::default() }),
699                            Signature::test_signature(),
700                        )
701                    })
702                    .collect(),
703                ..Default::default()
704            };
705
706            // The static files commit, the database does not.
707            let provider = factory.database_provider_rw().unwrap();
708            provider.append_block_bodies(vec![(2, Some(&body))]).unwrap();
709            static_files.commit().unwrap();
710            drop(provider);
711
712            let provider = factory.provider().unwrap();
713            assert_eq!(static_files.check_consistency(&provider).unwrap(), None, "{tx_count}");
714            assert_eq!(
715                static_files.get_highest_static_file_block(StaticFileSegment::Transactions),
716                Some(1),
717                "{tx_count}"
718            );
719            drop(provider);
720
721            let provider = factory.database_provider_rw().unwrap();
722            provider.append_block_bodies(vec![(2, Some(&body))]).unwrap();
723            provider.commit().unwrap();
724        }
725    }
726
727    #[test]
728    fn publishing_clears_transaction_lookups_in_the_active_backend() {
729        for settings in [StorageSettings::v1(), StorageSettings::v2()] {
730            let factory = hashed_factory();
731            factory.set_storage_settings_cache(settings);
732            let old_hash = B256::repeat_byte(0xaa);
733            let new_hash = B256::repeat_byte(0xbb);
734            let insert_lookup = |hash| {
735                let provider = factory.database_provider_rw().unwrap();
736                provider
737                    .with_rocksdb_batch(|batch| {
738                        let mut writer =
739                            EitherWriter::new_transaction_hash_numbers(&provider, batch)?;
740                        writer.put_transaction_hash_numbers_batch(vec![(hash, 0)], false)?;
741                        Ok(((), writer.into_raw_rocksdb_batch()))
742                    })
743                    .unwrap();
744                provider.commit().unwrap();
745            };
746            insert_lookup(old_hash);
747            assert_eq!(
748                factory.database_provider_ro().unwrap().transaction_id(old_hash).unwrap(),
749                Some(0)
750            );
751
752            let provider = factory.database_provider_rw().unwrap();
753            provider.publish_snap_state(7).unwrap();
754            provider.commit().unwrap();
755            assert_eq!(
756                factory.database_provider_ro().unwrap().transaction_id(old_hash).unwrap(),
757                None
758            );
759
760            insert_lookup(new_hash);
761            let provider = factory.database_provider_ro().unwrap();
762            assert_eq!(provider.transaction_id(old_hash).unwrap(), None);
763            assert_eq!(provider.transaction_id(new_hash).unwrap(), Some(0));
764        }
765    }
766
767    #[test]
768    fn publishing_resets_database_receipts_before_reusing_transaction_numbers() {
769        let factory = hashed_factory().with_prune_modes(PruneModes {
770            receipts_log_filter: ReceiptsLogPruneConfig(BTreeMap::from([(
771                Address::ZERO,
772                PruneMode::Before(0),
773            )])),
774            ..Default::default()
775        });
776        insert_chain(&factory, B256::ZERO);
777        let body = BlockBody {
778            transactions: (0..2)
779                .map(|nonce| {
780                    TransactionSigned::new_unhashed(
781                        Transaction::Legacy(TxLegacy { nonce, ..Default::default() }),
782                        Signature::test_signature(),
783                    )
784                })
785                .collect(),
786            ..Default::default()
787        };
788        let provider = factory.database_provider_rw().unwrap();
789        provider.append_block_bodies(vec![(0, None), (1, Some(&body))]).unwrap();
790        {
791            let mut writer = EitherWriter::new_receipts(&provider, 1).unwrap();
792            assert!(matches!(&writer, EitherWriter::Database(_)));
793            writer.append_receipt(0, &Receipt::default()).unwrap();
794            writer.append_receipt(1, &Receipt::default()).unwrap();
795        }
796        provider.commit().unwrap();
797        let provider = factory.database_provider_rw().unwrap();
798        provider.anchor_pruned_static_files(1).unwrap();
799        provider.publish_snap_state(1).unwrap();
800        provider.commit().unwrap();
801        let provider = factory.database_provider_rw().unwrap();
802        provider.append_block_bodies(vec![(2, Some(&body))]).unwrap();
803        {
804            let mut writer = EitherWriter::new_receipts(&provider, 2).unwrap();
805            writer.append_receipt(0, &Receipt::default()).unwrap();
806            writer.append_receipt(1, &Receipt::default()).unwrap();
807        }
808        provider.commit().unwrap();
809    }
810}