Skip to main content

reth_provider/providers/database/
mod.rs

1use crate::{
2    providers::{
3        state::latest::LatestStateProvider, NodeTypesForProvider, RocksDBProvider,
4        StaticFileProvider, StaticFileProviderRWRefMut,
5    },
6    to_range,
7    traits::{BlockSource, ReceiptProvider},
8    BalProvider, BalStoreHandle, BlockHashReader, BlockNumReader, BlockReader, ChainSpecProvider,
9    DatabaseProviderFactory, EitherWriterDestination, HeaderProvider, HeaderSyncGapProvider,
10    InMemoryBalStore, MetadataProvider, ProviderError, PruneCheckpointReader,
11    RocksDBProviderFactory, StageCheckpointReader, StateProviderBox, StaticFileProviderFactory,
12    StaticFileWriter, TransactionVariant, TransactionsProvider,
13};
14use alloy_consensus::transaction::TransactionMeta;
15use alloy_eips::BlockHashOrNumber;
16use alloy_primitives::{Address, BlockHash, BlockNumber, TxHash, TxNumber, B256};
17use core::fmt;
18use notify::{RecommendedWatcher, RecursiveMode, Watcher};
19use parking_lot::RwLock;
20use reth_chainspec::ChainInfo;
21use reth_db::{init_db, mdbx::DatabaseArguments, DatabaseEnv};
22use reth_db_api::{database::Database, models::StoredBlockBodyIndices};
23use reth_errors::{RethError, RethResult};
24use reth_execution_types::RecoveredBlockAndExecutionOutput;
25use reth_node_types::{
26    BlockTy, HeaderTy, NodeTypesWithDB, NodeTypesWithDBAdapter, ReceiptTy, TxTy,
27};
28use reth_primitives_traits::{RecoveredBlock, SealedHeader};
29use reth_prune_types::{PruneCheckpoint, PruneModes, PruneSegment, MINIMUM_UNWIND_SAFE_DISTANCE};
30use reth_stages_types::{PipelineTarget, StageCheckpoint, StageId};
31use reth_static_file_types::StaticFileSegment;
32use reth_storage_api::{
33    BlockBodyIndicesProvider, ChainStateBlockReader, ChainStateBlockWriter, DBProvider,
34    NodePrimitivesProvider, StorageSettings, StorageSettingsCache,
35};
36use reth_storage_errors::provider::ProviderResult;
37use reth_storage_overlay::OverlayManager;
38use std::{
39    ops::{RangeBounds, RangeInclusive},
40    path::Path,
41    sync::{
42        atomic::{AtomicU64, Ordering},
43        Arc, Mutex,
44    },
45};
46use tracing::{info, instrument, trace, warn};
47
48mod provider;
49pub use provider::{CommitOrder, DatabaseProvider, DatabaseProviderRO, DatabaseProviderRW};
50
51mod save_blocks;
52pub use save_blocks::SaveBlocksInput;
53
54use super::ProviderNodeTypes;
55mod builder;
56pub use builder::{ProviderFactoryBuilder, ReadOnlyConfig};
57
58mod metrics;
59pub use metrics::DatabaseProviderMetrics;
60
61mod chain;
62pub use chain::*;
63
64/// Sync state for read-only [`ProviderFactory`] instances.
65struct ReadOnlySyncState {
66    /// Last MDBX txn ID we synced `RocksDB` secondary / static file indexes to.
67    last_synced_txnid: AtomicU64,
68    /// Serializes the slow-path catch-up (`RocksDB` + static file re-init).
69    sync_lock: Mutex<()>,
70}
71
72/// A common provider that fetches data from a database or static file.
73///
74/// This provider implements most provider or provider factory traits.
75pub struct ProviderFactory<N: NodeTypesWithDB> {
76    /// Database instance
77    db: N::DB,
78    /// Chain spec
79    chain_spec: Arc<N::ChainSpec>,
80    /// Static File Provider
81    static_file_provider: StaticFileProvider<N::Primitives>,
82    /// Optional pruning configuration
83    prune_modes: PruneModes,
84    /// The node storage handler.
85    storage: Arc<N::Storage>,
86    /// Storage configuration settings for this node
87    storage_settings: Arc<RwLock<StorageSettings>>,
88    /// `RocksDB` provider
89    rocksdb_provider: RocksDBProvider,
90    /// Manager for state trie overlays and cached changesets.
91    overlay_manager: OverlayManager<N::Primitives>,
92    /// Store for block access lists.
93    bal_store: BalStoreHandle,
94    /// Task runtime for spawning parallel I/O work.
95    runtime: reth_tasks::Runtime,
96    /// Minimum distance from tip required before pruning can occur.
97    minimum_pruning_distance: u64,
98    /// Database provider metrics shared by providers created from this factory.
99    database_provider_metrics: Arc<DatabaseProviderMetrics>,
100    /// State for on-demand syncing of `RocksDB` secondary and static file indexes.
101    ///
102    /// Only set for read-only factories. Can be disabled if there is no concurrent read-write
103    /// factory writing to the database (e.g as part of a running reth node).
104    read_only_sync: Option<Arc<ReadOnlySyncState>>,
105}
106
107impl<N: NodeTypesForProvider> ProviderFactory<NodeTypesWithDBAdapter<N, DatabaseEnv>> {
108    /// Instantiates the builder for this type
109    pub fn builder() -> ProviderFactoryBuilder<N> {
110        ProviderFactoryBuilder::default()
111    }
112}
113
114impl<N: ProviderNodeTypes> ProviderFactory<N> {
115    /// Create new database provider factory.
116    ///
117    /// The storage backends used by the produced factory MAY be inconsistent.
118    /// It is recommended to call [`Self::check_consistency`] after
119    /// creation to ensure consistency between the database and static files.
120    /// If the function returns unwind targets, the caller MUST unwind the
121    /// inner database to the minimum of the two targets to ensure consistency.
122    pub fn new(
123        db: N::DB,
124        chain_spec: Arc<N::ChainSpec>,
125        static_file_provider: StaticFileProvider<N::Primitives>,
126        rocksdb_provider: RocksDBProvider,
127        runtime: reth_tasks::Runtime,
128    ) -> ProviderResult<Self> {
129        // Load storage settings from database at init time. Creates a temporary provider
130        // to read persisted settings, falling back to legacy defaults if none exist.
131        //
132        // Both factory and all providers it creates should share these cached settings.
133        let legacy_settings = StorageSettings::v1();
134        let database_provider_metrics = Arc::new(DatabaseProviderMetrics::default());
135        let overlay_manager = OverlayManager::default();
136        let storage_settings = DatabaseProvider::<_, N>::new(
137            db.tx()?,
138            chain_spec.clone(),
139            static_file_provider.clone(),
140            Default::default(),
141            Default::default(),
142            Arc::new(RwLock::new(legacy_settings)),
143            rocksdb_provider.clone(),
144            overlay_manager.clone(),
145            runtime.clone(),
146            db.path(),
147            database_provider_metrics.clone(),
148        )
149        .storage_settings()?
150        .unwrap_or(legacy_settings);
151
152        Ok(Self {
153            db,
154            chain_spec,
155            static_file_provider,
156            prune_modes: PruneModes::default(),
157            storage: Default::default(),
158            storage_settings: Arc::new(RwLock::new(storage_settings)),
159            rocksdb_provider,
160            overlay_manager,
161            bal_store: BalStoreHandle::new(InMemoryBalStore::default()),
162            runtime,
163            minimum_pruning_distance: MINIMUM_UNWIND_SAFE_DISTANCE,
164            database_provider_metrics,
165            read_only_sync: None,
166        })
167    }
168
169    /// Create new database provider factory and perform consistency checks.
170    ///
171    /// This will call [`Self::check_consistency`] internally and return
172    /// [`ProviderError::MustUnwind`] if inconsistencies are found. It may also
173    /// return any [`ProviderError`] that [`Self::new`] may return, or that are
174    /// encountered during consistency checks.
175    pub fn new_checked(
176        db: N::DB,
177        chain_spec: Arc<N::ChainSpec>,
178        static_file_provider: StaticFileProvider<N::Primitives>,
179        rocksdb_provider: RocksDBProvider,
180        runtime: reth_tasks::Runtime,
181    ) -> ProviderResult<Self> {
182        Self::new(db, chain_spec, static_file_provider, rocksdb_provider, runtime)
183            .and_then(Self::assert_consistent)
184    }
185}
186
187impl<N: NodeTypesWithDB> ProviderFactory<N> {
188    /// Sets the pruning configuration for an existing [`ProviderFactory`].
189    pub fn with_prune_modes(mut self, prune_modes: PruneModes) -> Self {
190        self.prune_modes = prune_modes;
191        self
192    }
193
194    /// Sets the BAL store for an existing [`ProviderFactory`].
195    pub fn with_bal_store(mut self, bal_store: BalStoreHandle) -> Self {
196        self.bal_store = bal_store;
197        self
198    }
199
200    /// Sets the overlay manager for an existing [`ProviderFactory`].
201    pub fn with_overlay_manager(mut self, overlay_manager: OverlayManager<N::Primitives>) -> Self {
202        self.overlay_manager = overlay_manager;
203        self
204    }
205
206    /// Returns the shared overlay manager.
207    pub const fn overlay_manager(&self) -> &OverlayManager<N::Primitives> {
208        &self.overlay_manager
209    }
210
211    /// Sets the minimum pruning distance for an existing [`ProviderFactory`].
212    ///
213    /// This controls the minimum distance from tip required before pruning can occur.
214    /// The default is [`MINIMUM_UNWIND_SAFE_DISTANCE`].
215    pub const fn with_minimum_pruning_distance(mut self, distance: u64) -> Self {
216        self.minimum_pruning_distance = distance;
217        self
218    }
219
220    /// Enables on-demand syncing of `RocksDB` secondary and static file indexes for read-only
221    /// factories. Initializes the tracker to the current MDBX txn ID.
222    ///
223    /// Should be used for read-only factories that are running concurrently to a reth node writing
224    /// new data to the database. Would effectively be a no-op if database directory is unchanged.
225    pub fn with_read_only_sync(mut self, watch: bool) -> Self
226    where
227        N::DB: Database,
228    {
229        // Initialize to 0 so the first `sync_providers_if_needed` call always
230        // triggers a RocksDB/static-file catch-up, regardless of what MDBX txnid
231        // the database was at when we opened it.
232        let state = Arc::new(ReadOnlySyncState {
233            last_synced_txnid: AtomicU64::new(0),
234            sync_lock: Mutex::new(()),
235        });
236        self.read_only_sync = Some(state);
237
238        if watch {
239            self.watch_db_directory();
240        }
241        self
242    }
243
244    /// Watches the MDBX data directory for changes and eagerly syncs `RocksDB` secondary and
245    /// static file indexes when modifications are detected.
246    fn watch_db_directory(&self)
247    where
248        N::DB: Database,
249    {
250        let factory = self.clone();
251        let db_path = self.db.path();
252        reth_tasks::spawn_os_thread("ro-sync", move || {
253            let (tx, rx) = std::sync::mpsc::channel();
254            let mut watcher = RecommendedWatcher::new(
255                move |res| {
256                    let _ = tx.send(res);
257                },
258                notify::Config::default(),
259            )
260            .expect("failed to create watcher");
261
262            watcher
263                .watch(&db_path, RecursiveMode::NonRecursive)
264                .expect("failed to watch MDBX path");
265
266            while let Ok(res) = rx.recv() {
267                match res {
268                    Ok(event) => {
269                        if !matches!(
270                            event.kind,
271                            notify::EventKind::Modify(_) | notify::EventKind::Create(_)
272                        ) {
273                            continue;
274                        }
275
276                        if let Err(err) = factory.sync_providers_if_needed() {
277                            warn!(target: "reth::provider", %err, "background ro-sync failed");
278                        }
279                    }
280                    Err(err) => {
281                        warn!(target: "reth::provider", ?err, "MDBX directory watcher error");
282                    }
283                }
284            }
285        });
286    }
287
288    /// For read-only factories, checks whether the MDBX committed txn ID has advanced since the
289    /// last sync and, if so, catches up the `RocksDB` secondary instance and re-initializes the
290    /// static file index.
291    ///
292    /// No-op for read-write factories.
293    pub fn sync_providers_if_needed(&self) -> ProviderResult<()> {
294        let Some(sync_state) = &self.read_only_sync else { return Ok(()) };
295        let current_txnid = self.db.last_txnid().unwrap_or(0);
296
297        // Fast path: no contention when nothing changed.
298        if current_txnid == sync_state.last_synced_txnid.load(Ordering::Relaxed) {
299            return Ok(());
300        }
301
302        // Slow path: serialize the actual catch-up I/O.
303        let _guard = sync_state.sync_lock.lock().unwrap_or_else(|e| e.into_inner());
304
305        // Double-check after acquiring the lock — another thread may have already synced.
306        if current_txnid == sync_state.last_synced_txnid.load(Ordering::Relaxed) {
307            return Ok(());
308        }
309
310        self.rocksdb_provider.try_catch_up_with_primary()?;
311        self.static_file_provider.initialize_index()?;
312        sync_state.last_synced_txnid.store(current_txnid, Ordering::Relaxed);
313        Ok(())
314    }
315
316    /// Returns reference to the underlying database.
317    pub const fn db_ref(&self) -> &N::DB {
318        &self.db
319    }
320
321    #[cfg(any(test, feature = "test-utils"))]
322    /// Consumes Self and returns DB
323    pub fn into_db(self) -> N::DB {
324        self.db
325    }
326}
327
328impl<N: NodeTypesWithDB> StorageSettingsCache for ProviderFactory<N> {
329    fn cached_storage_settings(&self) -> StorageSettings {
330        *self.storage_settings.read()
331    }
332
333    fn set_storage_settings_cache(&self, settings: StorageSettings) {
334        *self.storage_settings.write() = settings;
335    }
336}
337
338impl<N: NodeTypesWithDB> RocksDBProviderFactory for ProviderFactory<N> {
339    fn rocksdb_provider(&self) -> RocksDBProvider {
340        self.rocksdb_provider.clone()
341    }
342
343    fn set_pending_rocksdb_batch(&self, _batch: rocksdb::WriteBatchWithTransaction<true>) {
344        unimplemented!("ProviderFactory is a factory, not a provider - use DatabaseProvider::set_pending_rocksdb_batch instead")
345    }
346
347    fn commit_pending_rocksdb_batches(&self) -> ProviderResult<()> {
348        unimplemented!("ProviderFactory is a factory, not a provider - use DatabaseProvider::commit_pending_rocksdb_batches instead")
349    }
350}
351
352impl<N: ProviderNodeTypes<DB = DatabaseEnv>> ProviderFactory<N> {
353    /// Create new database provider by passing a path. [`ProviderFactory`] will own the database
354    /// instance.
355    pub fn new_with_database_path<P: AsRef<Path>>(
356        path: P,
357        chain_spec: Arc<N::ChainSpec>,
358        args: DatabaseArguments,
359        static_file_provider: StaticFileProvider<N::Primitives>,
360        rocksdb_provider: RocksDBProvider,
361        runtime: reth_tasks::Runtime,
362    ) -> RethResult<Self> {
363        Self::new(
364            init_db(path, args).map_err(RethError::msg)?,
365            chain_spec,
366            static_file_provider,
367            rocksdb_provider,
368            runtime,
369        )
370        .map_err(RethError::Provider)
371    }
372}
373
374impl<N: ProviderNodeTypes> ProviderFactory<N> {
375    /// Returns a provider with a created `DbTx` inside, which allows fetching data from the
376    /// database using different types of providers. Example: [`HeaderProvider`]
377    /// [`BlockHashReader`]. This may fail if the inner read database transaction fails to open.
378    ///
379    /// This sets the [`PruneModes`] to [`None`], because they should only be relevant for writing
380    /// data.
381    #[track_caller]
382    pub fn provider(&self) -> ProviderResult<DatabaseProviderRO<N::DB, N>> {
383        let db_tx = self.db.tx()?;
384
385        // Sync providers after opening the database transaction to make
386        // sure that no data is pruned from rocksdb or static files.
387        //
388        // Reorg logic ensures that no data is pruned from rocksdb or static files while there is an
389        // mdbx transaction open that might rely on this data.
390        self.sync_providers_if_needed()?;
391
392        Ok(DatabaseProvider::new(
393            db_tx,
394            self.chain_spec.clone(),
395            self.static_file_provider.clone(),
396            self.prune_modes.clone(),
397            self.storage.clone(),
398            self.storage_settings.clone(),
399            self.rocksdb_provider.clone(),
400            self.overlay_manager.clone(),
401            self.runtime.clone(),
402            self.db.path(),
403            self.database_provider_metrics.clone(),
404        )
405        .with_minimum_pruning_distance(self.minimum_pruning_distance))
406    }
407
408    /// Returns a provider with a created `DbTxMut` inside, which allows fetching and updating
409    /// data from the database using different types of providers. Example: [`HeaderProvider`]
410    /// [`BlockHashReader`].  This may fail if the inner read/write database transaction fails to
411    /// open.
412    #[track_caller]
413    pub fn provider_rw(&self) -> ProviderResult<DatabaseProviderRW<N::DB, N>> {
414        Ok(DatabaseProviderRW(
415            DatabaseProvider::new_rw(
416                self.db.tx_mut()?,
417                self.chain_spec.clone(),
418                self.static_file_provider.clone(),
419                self.prune_modes.clone(),
420                self.storage.clone(),
421                self.storage_settings.clone(),
422                self.rocksdb_provider.clone(),
423                self.overlay_manager.clone(),
424                self.runtime.clone(),
425                self.db.path(),
426                self.database_provider_metrics.clone(),
427            )
428            .with_reader_txn_tracker(self.db.clone())
429            .with_minimum_pruning_distance(self.minimum_pruning_distance),
430        ))
431    }
432
433    /// Returns a provider with a created `DbTxMut` inside, configured for unwind operations.
434    /// Uses unwind commit order (MDBX first, then `RocksDB`, then static files) to allow
435    /// recovery by truncating static files on restart if interrupted.
436    ///
437    /// Unwind commits may wait for pre-existing readers to drain before finishing later
438    /// cross-store steps. Drop any long-lived read providers before committing this provider.
439    #[track_caller]
440    pub fn unwind_provider_rw(
441        &self,
442    ) -> ProviderResult<DatabaseProvider<<N::DB as Database>::TXMut, N>> {
443        Ok(DatabaseProvider::new_unwind_rw(
444            self.db.tx_mut()?,
445            self.chain_spec.clone(),
446            self.static_file_provider.clone(),
447            self.prune_modes.clone(),
448            self.storage.clone(),
449            self.storage_settings.clone(),
450            self.rocksdb_provider.clone(),
451            self.overlay_manager.clone(),
452            self.runtime.clone(),
453            self.db.path(),
454            self.database_provider_metrics.clone(),
455        )
456        .with_reader_txn_tracker(self.db.clone())
457        .with_minimum_pruning_distance(self.minimum_pruning_distance))
458    }
459
460    /// State provider for latest block
461    #[track_caller]
462    pub fn latest(&self) -> ProviderResult<StateProviderBox> {
463        trace!(target: "providers::db", "Returning latest state provider");
464        Ok(Box::new(LatestStateProvider::new(self.database_provider_ro()?)))
465    }
466
467    /// Asserts that the static files and database are consistent. If not,
468    /// returns [`ProviderError::MustUnwind`] with the appropriate unwind
469    /// target. May also return any [`ProviderError`] that
470    /// [`Self::check_consistency`] may return.
471    pub fn assert_consistent(self) -> ProviderResult<Self> {
472        if let Some(unwind) = self.provider()?.pipeline_unwind()? {
473            return Err(ProviderError::MustUnwind {
474                data_source: "interrupted pipeline unwind",
475                unwind_to: unwind.target,
476            })
477        }
478        let (rocksdb_unwind, static_file_unwind) = self.check_consistency()?;
479
480        let source = match (rocksdb_unwind, static_file_unwind) {
481            (None, None) => return Ok(self),
482            (Some(_), Some(_)) => "RocksDB and Static Files",
483            (Some(_), None) => "RocksDB",
484            (None, Some(_)) => "Static Files",
485        };
486
487        Err(ProviderError::MustUnwind {
488            data_source: source,
489            unwind_to: rocksdb_unwind
490                .into_iter()
491                .chain(static_file_unwind)
492                .min()
493                .expect("at least one unwind target must be Some"),
494        })
495    }
496
497    /// Checks the consistency between the static files and the database. This
498    /// may result in static files being pruned or otherwise healed to ensure
499    /// consistency. I.e. this MAY result in writes to the static files.
500    #[instrument(err, skip(self))]
501    pub fn check_consistency(&self) -> ProviderResult<(Option<u64>, Option<u64>)> {
502        let provider_ro = self
503            .database_provider_ro()?
504            // Healing can run long-lived read transactions (e.g., iterating changesets
505            // over millions of blocks). Disable the default timeout so MDBX doesn't
506            // kill the transaction mid-heal, which causes a crash loop on startup.
507            .disable_long_read_transaction_safety();
508
509        // Step 1: heal file-level inconsistencies (no pruning)
510        self.static_file_provider().check_file_consistency(&provider_ro)?;
511
512        // Step 2: RocksDB consistency check (needs static files tx data)
513        let rocksdb_unwind = self.rocksdb_provider().check_consistency(&provider_ro)?;
514
515        // Step 3: Static file checkpoint consistency (may prune)
516        let static_file_unwind = self.static_file_provider().check_consistency(&provider_ro)?.map(
517            |target| match target {
518                PipelineTarget::Unwind(block) => block,
519                PipelineTarget::Sync(_) => unreachable!("check_consistency returns Unwind"),
520            },
521        );
522
523        // Step 4: Heal finalized/safe block numbers that may be ahead of the
524        // highest header on nodes coming from <=1.10.2.
525        //
526        // Unwinds already set it to the target block.
527        if rocksdb_unwind.is_none() && static_file_unwind.is_none() {
528            self.heal_chain_state_block_numbers(&provider_ro)?;
529        }
530
531        Ok((rocksdb_unwind, static_file_unwind))
532    }
533
534    /// If the stored finalized or safe block number is ahead of the highest
535    /// header, resets it to the highest header.
536    fn heal_chain_state_block_numbers(
537        &self,
538        provider_ro: &DatabaseProvider<<N::DB as Database>::TX, N>,
539    ) -> ProviderResult<()> {
540        let highest_header = self.last_block_number()?;
541
542        let finalized = provider_ro.last_finalized_block_number()?;
543        let safe = provider_ro.last_safe_block_number()?;
544
545        if finalized.is_none_or(|f| f <= highest_header) && safe.is_none_or(|s| s <= highest_header)
546        {
547            return Ok(());
548        }
549
550        let provider_rw = self.provider_rw()?;
551
552        if let Some(finalized) = finalized.filter(|&f| f > highest_header) {
553            info!(
554                target: "providers::db",
555                finalized,
556                highest_header,
557                "Healing finalized block number",
558            );
559            provider_rw.save_finalized_block_number(highest_header)?;
560        }
561
562        if let Some(safe) = safe.filter(|&s| s > highest_header) {
563            info!(
564                target: "providers::db",
565                safe,
566                highest_header,
567                "Healing safe block number",
568            );
569            provider_rw.save_safe_block_number(highest_header)?;
570        }
571
572        provider_rw.commit()?;
573
574        Ok(())
575    }
576
577    /// Returns a static file provider. For read-only instances, this will also invoke
578    /// [`Self::sync_providers_if_needed`] to make sure that the static file provider is up to date.
579    pub fn caught_up_static_file_provider(
580        &self,
581    ) -> ProviderResult<StaticFileProvider<N::Primitives>> {
582        self.sync_providers_if_needed()?;
583        Ok(self.static_file_provider.clone())
584    }
585}
586
587impl<N: NodeTypesWithDB> NodePrimitivesProvider for ProviderFactory<N> {
588    type Primitives = N::Primitives;
589}
590
591impl<N: ProviderNodeTypes> BalProvider for ProviderFactory<N> {
592    fn bal_store(&self) -> &BalStoreHandle {
593        &self.bal_store
594    }
595}
596
597impl<N: ProviderNodeTypes> DatabaseProviderFactory for ProviderFactory<N> {
598    type DB = N::DB;
599    type Provider = DatabaseProvider<<N::DB as Database>::TX, N>;
600    type ProviderRW = DatabaseProvider<<N::DB as Database>::TXMut, N>;
601
602    fn database_provider_ro(&self) -> ProviderResult<Self::Provider> {
603        self.provider()
604    }
605
606    fn database_provider_rw(&self) -> ProviderResult<Self::ProviderRW> {
607        self.provider_rw().map(|provider| provider.0)
608    }
609}
610
611impl<N: NodeTypesWithDB> StaticFileProviderFactory for ProviderFactory<N> {
612    /// Returns static file provider
613    fn static_file_provider(&self) -> StaticFileProvider<Self::Primitives> {
614        self.static_file_provider.clone()
615    }
616
617    fn get_static_file_writer(
618        &self,
619        block: BlockNumber,
620        segment: StaticFileSegment,
621    ) -> ProviderResult<StaticFileProviderRWRefMut<'_, Self::Primitives>> {
622        self.static_file_provider.get_writer(block, segment)
623    }
624}
625
626impl<N: ProviderNodeTypes> HeaderSyncGapProvider for ProviderFactory<N> {
627    type Header = HeaderTy<N>;
628    fn local_tip_header(
629        &self,
630        highest_uninterrupted_block: BlockNumber,
631    ) -> ProviderResult<SealedHeader<Self::Header>> {
632        self.provider()?.local_tip_header(highest_uninterrupted_block)
633    }
634}
635
636impl<N: ProviderNodeTypes> HeaderProvider for ProviderFactory<N> {
637    type Header = HeaderTy<N>;
638
639    fn header(&self, block_hash: BlockHash) -> ProviderResult<Option<Self::Header>> {
640        self.provider()?.header(block_hash)
641    }
642
643    fn header_by_number(&self, num: BlockNumber) -> ProviderResult<Option<Self::Header>> {
644        self.caught_up_static_file_provider()?.header_by_number(num)
645    }
646
647    fn headers_range(
648        &self,
649        range: impl RangeBounds<BlockNumber>,
650    ) -> ProviderResult<Vec<Self::Header>> {
651        self.caught_up_static_file_provider()?.headers_range(range)
652    }
653
654    fn sealed_header(
655        &self,
656        number: BlockNumber,
657    ) -> ProviderResult<Option<SealedHeader<Self::Header>>> {
658        self.caught_up_static_file_provider()?.sealed_header(number)
659    }
660
661    fn sealed_headers_range(
662        &self,
663        range: impl RangeBounds<BlockNumber>,
664    ) -> ProviderResult<Vec<SealedHeader<Self::Header>>> {
665        self.caught_up_static_file_provider()?.sealed_headers_range(range)
666    }
667
668    fn sealed_headers_while(
669        &self,
670        range: impl RangeBounds<BlockNumber>,
671        predicate: impl FnMut(&SealedHeader<Self::Header>) -> bool,
672    ) -> ProviderResult<Vec<SealedHeader<Self::Header>>> {
673        self.caught_up_static_file_provider()?.sealed_headers_while(range, predicate)
674    }
675}
676
677impl<N: ProviderNodeTypes> BlockHashReader for ProviderFactory<N> {
678    fn block_hash(&self, number: u64) -> ProviderResult<Option<B256>> {
679        self.caught_up_static_file_provider()?.block_hash(number)
680    }
681
682    fn canonical_hashes_range(
683        &self,
684        start: BlockNumber,
685        end: BlockNumber,
686    ) -> ProviderResult<Vec<B256>> {
687        self.caught_up_static_file_provider()?.canonical_hashes_range(start, end)
688    }
689}
690
691impl<N: ProviderNodeTypes> BlockNumReader for ProviderFactory<N> {
692    fn chain_info(&self) -> ProviderResult<ChainInfo> {
693        self.provider()?.chain_info()
694    }
695
696    fn best_block_number(&self) -> ProviderResult<BlockNumber> {
697        self.provider()?.best_block_number()
698    }
699
700    fn last_block_number(&self) -> ProviderResult<BlockNumber> {
701        self.caught_up_static_file_provider()?.last_block_number()
702    }
703
704    fn earliest_block_number(&self) -> ProviderResult<BlockNumber> {
705        // earliest history height tracks the lowest block number that has __not__ been expired, in
706        // other words, the first/earliest available block.
707        Ok(self.caught_up_static_file_provider()?.earliest_history_height())
708    }
709
710    fn block_number(&self, hash: B256) -> ProviderResult<Option<BlockNumber>> {
711        self.provider()?.block_number(hash)
712    }
713}
714
715impl<N: ProviderNodeTypes> BlockReader for ProviderFactory<N> {
716    type Block = BlockTy<N>;
717
718    fn find_block_by_hash(
719        &self,
720        hash: B256,
721        source: BlockSource,
722    ) -> ProviderResult<Option<Self::Block>> {
723        self.provider()?.find_block_by_hash(hash, source)
724    }
725
726    fn block(&self, id: BlockHashOrNumber) -> ProviderResult<Option<Self::Block>> {
727        self.provider()?.block(id)
728    }
729
730    fn pending_block(&self) -> ProviderResult<Option<Arc<RecoveredBlock<Self::Block>>>> {
731        self.provider()?.pending_block()
732    }
733
734    fn pending_block_and_receipts(
735        &self,
736    ) -> ProviderResult<Option<RecoveredBlockAndExecutionOutput<Self::Block, Self::Receipt>>> {
737        self.provider()?.pending_block_and_receipts()
738    }
739
740    fn recovered_block(
741        &self,
742        id: BlockHashOrNumber,
743        transaction_kind: TransactionVariant,
744    ) -> ProviderResult<Option<RecoveredBlock<Self::Block>>> {
745        self.provider()?.recovered_block(id, transaction_kind)
746    }
747
748    fn sealed_block_with_senders(
749        &self,
750        id: BlockHashOrNumber,
751        transaction_kind: TransactionVariant,
752    ) -> ProviderResult<Option<RecoveredBlock<Self::Block>>> {
753        self.provider()?.sealed_block_with_senders(id, transaction_kind)
754    }
755
756    fn block_range(&self, range: RangeInclusive<BlockNumber>) -> ProviderResult<Vec<Self::Block>> {
757        self.provider()?.block_range(range)
758    }
759
760    fn block_with_senders_range(
761        &self,
762        range: RangeInclusive<BlockNumber>,
763    ) -> ProviderResult<Vec<RecoveredBlock<Self::Block>>> {
764        self.provider()?.block_with_senders_range(range)
765    }
766
767    fn recovered_block_range(
768        &self,
769        range: RangeInclusive<BlockNumber>,
770    ) -> ProviderResult<Vec<RecoveredBlock<Self::Block>>> {
771        self.provider()?.recovered_block_range(range)
772    }
773
774    fn block_by_transaction_id(&self, id: TxNumber) -> ProviderResult<Option<BlockNumber>> {
775        self.provider()?.block_by_transaction_id(id)
776    }
777}
778
779impl<N: ProviderNodeTypes> TransactionsProvider for ProviderFactory<N> {
780    type Transaction = TxTy<N>;
781
782    fn transaction_id(&self, tx_hash: TxHash) -> ProviderResult<Option<TxNumber>> {
783        self.provider()?.transaction_id(tx_hash)
784    }
785
786    fn transaction_by_id(&self, id: TxNumber) -> ProviderResult<Option<Self::Transaction>> {
787        self.caught_up_static_file_provider()?.transaction_by_id(id)
788    }
789
790    fn transaction_by_id_unhashed(
791        &self,
792        id: TxNumber,
793    ) -> ProviderResult<Option<Self::Transaction>> {
794        self.caught_up_static_file_provider()?.transaction_by_id_unhashed(id)
795    }
796
797    fn transaction_by_hash(&self, hash: TxHash) -> ProviderResult<Option<Self::Transaction>> {
798        self.provider()?.transaction_by_hash(hash)
799    }
800
801    fn transaction_by_hash_with_meta(
802        &self,
803        tx_hash: TxHash,
804    ) -> ProviderResult<Option<(Self::Transaction, TransactionMeta)>> {
805        self.provider()?.transaction_by_hash_with_meta(tx_hash)
806    }
807
808    fn transactions_by_block(
809        &self,
810        id: BlockHashOrNumber,
811    ) -> ProviderResult<Option<Vec<Self::Transaction>>> {
812        self.provider()?.transactions_by_block(id)
813    }
814
815    fn transactions_by_block_range(
816        &self,
817        range: impl RangeBounds<BlockNumber>,
818    ) -> ProviderResult<Vec<Vec<Self::Transaction>>> {
819        self.provider()?.transactions_by_block_range(range)
820    }
821
822    fn transactions_by_tx_range(
823        &self,
824        range: impl RangeBounds<TxNumber>,
825    ) -> ProviderResult<Vec<Self::Transaction>> {
826        self.caught_up_static_file_provider()?.transactions_by_tx_range(range)
827    }
828
829    fn senders_by_tx_range(
830        &self,
831        range: impl RangeBounds<TxNumber>,
832    ) -> ProviderResult<Vec<Address>> {
833        if EitherWriterDestination::senders(self).is_static_file() {
834            self.caught_up_static_file_provider()?.senders_by_tx_range(range)
835        } else {
836            self.provider()?.senders_by_tx_range(range)
837        }
838    }
839
840    fn transaction_sender(&self, id: TxNumber) -> ProviderResult<Option<Address>> {
841        if EitherWriterDestination::senders(self).is_static_file() {
842            self.caught_up_static_file_provider()?.transaction_sender(id)
843        } else {
844            self.provider()?.transaction_sender(id)
845        }
846    }
847}
848
849impl<N: ProviderNodeTypes> ReceiptProvider for ProviderFactory<N> {
850    type Receipt = ReceiptTy<N>;
851
852    fn receipt(&self, id: TxNumber) -> ProviderResult<Option<Self::Receipt>> {
853        self.caught_up_static_file_provider()?.get_with_static_file_or_database(
854            StaticFileSegment::Receipts,
855            id,
856            |static_file| static_file.receipt(id),
857            || self.provider()?.receipt(id),
858        )
859    }
860
861    fn receipt_by_hash(&self, hash: TxHash) -> ProviderResult<Option<Self::Receipt>> {
862        self.provider()?.receipt_by_hash(hash)
863    }
864
865    fn receipts_by_block(
866        &self,
867        block: BlockHashOrNumber,
868    ) -> ProviderResult<Option<Vec<Self::Receipt>>> {
869        self.provider()?.receipts_by_block(block)
870    }
871
872    fn receipts_by_tx_range(
873        &self,
874        range: impl RangeBounds<TxNumber>,
875    ) -> ProviderResult<Vec<Self::Receipt>> {
876        self.caught_up_static_file_provider()?.get_range_with_static_file_or_database(
877            StaticFileSegment::Receipts,
878            to_range(range),
879            |static_file, range, _| static_file.receipts_by_tx_range(range),
880            |range, _| self.provider()?.receipts_by_tx_range(range),
881            |_| true,
882        )
883    }
884
885    fn receipts_by_block_range(
886        &self,
887        block_range: RangeInclusive<BlockNumber>,
888    ) -> ProviderResult<Vec<Vec<Self::Receipt>>> {
889        self.provider()?.receipts_by_block_range(block_range)
890    }
891}
892
893impl<N: ProviderNodeTypes> BlockBodyIndicesProvider for ProviderFactory<N> {
894    fn block_body_indices(
895        &self,
896        number: BlockNumber,
897    ) -> ProviderResult<Option<StoredBlockBodyIndices>> {
898        self.provider()?.block_body_indices(number)
899    }
900
901    fn block_body_indices_range(
902        &self,
903        range: RangeInclusive<BlockNumber>,
904    ) -> ProviderResult<Vec<StoredBlockBodyIndices>> {
905        self.provider()?.block_body_indices_range(range)
906    }
907}
908
909impl<N: ProviderNodeTypes> StageCheckpointReader for ProviderFactory<N> {
910    fn get_stage_checkpoint(&self, id: StageId) -> ProviderResult<Option<StageCheckpoint>> {
911        self.provider()?.get_stage_checkpoint(id)
912    }
913
914    fn get_stage_checkpoint_progress(&self, id: StageId) -> ProviderResult<Option<Vec<u8>>> {
915        self.provider()?.get_stage_checkpoint_progress(id)
916    }
917    fn get_all_checkpoints(&self) -> ProviderResult<Vec<(String, StageCheckpoint)>> {
918        self.provider()?.get_all_checkpoints()
919    }
920}
921
922impl<N: NodeTypesWithDB> ChainSpecProvider for ProviderFactory<N> {
923    type ChainSpec = N::ChainSpec;
924
925    fn chain_spec(&self) -> Arc<N::ChainSpec> {
926        self.chain_spec.clone()
927    }
928}
929
930impl<N: ProviderNodeTypes> PruneCheckpointReader for ProviderFactory<N> {
931    fn get_prune_checkpoint(
932        &self,
933        segment: PruneSegment,
934    ) -> ProviderResult<Option<PruneCheckpoint>> {
935        self.provider()?.get_prune_checkpoint(segment)
936    }
937
938    fn get_prune_checkpoints(&self) -> ProviderResult<Vec<(PruneSegment, PruneCheckpoint)>> {
939        self.provider()?.get_prune_checkpoints()
940    }
941
942    fn get_prune_checkpoints_for_availability(
943        &self,
944    ) -> ProviderResult<Vec<(PruneSegment, PruneCheckpoint)>> {
945        self.provider()?.get_prune_checkpoints_for_availability()
946    }
947}
948
949impl<N: ProviderNodeTypes> MetadataProvider for ProviderFactory<N> {
950    fn get_metadata(&self, key: &str) -> ProviderResult<Option<Vec<u8>>> {
951        self.provider()?.get_metadata(key)
952    }
953}
954
955impl<N> fmt::Debug for ProviderFactory<N>
956where
957    N: NodeTypesWithDB<DB: fmt::Debug, ChainSpec: fmt::Debug, Storage: fmt::Debug>,
958{
959    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
960        let Self {
961            db,
962            chain_spec,
963            static_file_provider,
964            prune_modes,
965            storage,
966            storage_settings,
967            rocksdb_provider,
968            overlay_manager,
969            bal_store,
970            runtime,
971            minimum_pruning_distance,
972            database_provider_metrics: _,
973            read_only_sync,
974        } = self;
975        f.debug_struct("ProviderFactory")
976            .field("db", &db)
977            .field("chain_spec", &chain_spec)
978            .field("static_file_provider", &static_file_provider)
979            .field("prune_modes", &prune_modes)
980            .field("storage", &storage)
981            .field("storage_settings", &*storage_settings.read())
982            .field("rocksdb_provider", &rocksdb_provider)
983            .field("overlay_manager", &overlay_manager)
984            .field("bal_store", &bal_store)
985            .field("runtime", &runtime)
986            .field("minimum_pruning_distance", &minimum_pruning_distance)
987            .field(
988                "read_only_sync",
989                &read_only_sync.as_ref().map(|s| s.last_synced_txnid.load(Ordering::Relaxed)),
990            )
991            .finish()
992    }
993}
994
995impl<N: NodeTypesWithDB> Clone for ProviderFactory<N> {
996    fn clone(&self) -> Self {
997        Self {
998            db: self.db.clone(),
999            chain_spec: self.chain_spec.clone(),
1000            static_file_provider: self.static_file_provider.clone(),
1001            prune_modes: self.prune_modes.clone(),
1002            storage: self.storage.clone(),
1003            storage_settings: self.storage_settings.clone(),
1004            rocksdb_provider: self.rocksdb_provider.clone(),
1005            overlay_manager: self.overlay_manager.clone(),
1006            bal_store: self.bal_store.clone(),
1007            runtime: self.runtime.clone(),
1008            minimum_pruning_distance: self.minimum_pruning_distance,
1009            database_provider_metrics: self.database_provider_metrics.clone(),
1010            read_only_sync: self.read_only_sync.clone(),
1011        }
1012    }
1013}
1014
1015#[cfg(test)]
1016mod tests {
1017    use super::*;
1018    use crate::{
1019        providers::{StaticFileProvider, StaticFileProviderBuilder, StaticFileWriter},
1020        test_utils::{
1021            blocks::TEST_BLOCK, create_test_provider_factory,
1022            create_test_provider_factory_with_genesis_block_number, MockNodeTypesWithDB,
1023        },
1024        BlockHashReader, BlockNumReader, BlockWriter, DBProvider, HeaderSyncGapProvider,
1025        PruneCheckpointWriter, StageCheckpointWriter, TransactionsProvider,
1026    };
1027    use alloy_eips::BlockNumHash;
1028    use alloy_primitives::{TxNumber, B256};
1029    use assert_matches::assert_matches;
1030    use reth_chainspec::ChainSpecBuilder;
1031    use reth_db::{
1032        mdbx::DatabaseArguments,
1033        test_utils::{create_test_rocksdb_dir, create_test_static_files_dir, ERROR_TEMPDIR},
1034    };
1035    use reth_db_api::{models::SnapAttempt, tables, transaction::DbTxMut};
1036    use reth_ethereum_primitives::Receipt;
1037    use reth_primitives_traits::SignerRecoverable;
1038    use reth_prune_types::{PruneMode, PruneModes};
1039    use reth_storage_api::MetadataWriter;
1040    use reth_storage_errors::provider::ProviderError;
1041    use reth_testing_utils::generators::{self, random_block, random_header, BlockParams};
1042    use std::{ops::RangeInclusive, sync::Arc};
1043
1044    #[test]
1045    fn common_history_provider() {
1046        let factory = create_test_provider_factory();
1047        let _ = factory.latest();
1048    }
1049
1050    #[test]
1051    fn default_chain_info() {
1052        let factory = create_test_provider_factory();
1053        let provider = factory.provider().unwrap();
1054
1055        let chain_info = provider.chain_info().expect("should be ok");
1056        assert_eq!(chain_info.best_number, 0);
1057        assert_eq!(chain_info.best_hash, B256::ZERO);
1058    }
1059
1060    #[test]
1061    fn provider_flow() {
1062        let factory = create_test_provider_factory();
1063        let provider = factory.provider().unwrap();
1064        provider.block_hash(0).unwrap();
1065        let provider_rw = factory.provider_rw().unwrap();
1066        provider_rw.block_hash(0).unwrap();
1067        provider.block_hash(0).unwrap();
1068    }
1069
1070    #[test]
1071    fn provider_factory_with_database_path() {
1072        let chain_spec = ChainSpecBuilder::mainnet().build();
1073        let (_static_dir, static_dir_path) = create_test_static_files_dir();
1074        let (_rocksdb_dir, rocksdb_path) = create_test_rocksdb_dir();
1075        let _db_tempdir = tempfile::TempDir::new().expect(ERROR_TEMPDIR);
1076        let factory = ProviderFactory::<MockNodeTypesWithDB<DatabaseEnv>>::new_with_database_path(
1077            _db_tempdir.path(),
1078            Arc::new(chain_spec),
1079            DatabaseArguments::new(Default::default()),
1080            StaticFileProvider::read_write(static_dir_path).unwrap(),
1081            RocksDBProvider::builder(&rocksdb_path).build().unwrap(),
1082            reth_tasks::Runtime::test(),
1083        )
1084        .unwrap();
1085        let provider = factory.provider().unwrap();
1086        provider.block_hash(0).unwrap();
1087        let provider_rw = factory.provider_rw().unwrap();
1088        provider_rw.block_hash(0).unwrap();
1089        provider.block_hash(0).unwrap();
1090    }
1091
1092    #[test]
1093    fn block_range_readers_reject_expired_history() {
1094        use crate::BlockReader;
1095        use reth_static_file_types::{SegmentHeader, SegmentRangeInclusive, StaticFileSegment};
1096
1097        let factory = create_test_provider_factory();
1098        let mut rng = generators::rng();
1099        let provider_rw = factory.provider_rw().unwrap();
1100        let mut parent = None;
1101        for number in 0..6 {
1102            let block = random_block(
1103                &mut rng,
1104                number,
1105                BlockParams { parent, tx_count: Some(1), ..Default::default() },
1106            );
1107            parent = Some(block.hash());
1108            provider_rw.insert_block(&block.try_recover().unwrap()).unwrap();
1109        }
1110        provider_rw.commit().unwrap();
1111
1112        // history below block 3 has been expired
1113        let static_provider = factory.static_file_provider();
1114        {
1115            let mut writer =
1116                static_provider.latest_writer(StaticFileSegment::Transactions).unwrap();
1117            let header = writer.user_header().clone();
1118            *writer.user_header_mut() = SegmentHeader::new(
1119                header.expected_block_range(),
1120                Some(SegmentRangeInclusive::new(3, 5)),
1121                header.tx_range(),
1122                StaticFileSegment::Transactions,
1123            );
1124            writer.inner().set_dirty();
1125            writer.commit().unwrap();
1126        }
1127        static_provider.initialize_index().unwrap();
1128        assert_eq!(static_provider.earliest_history_height(), 3);
1129
1130        let provider = factory.provider().unwrap();
1131        // single block lookups already reject expired blocks
1132        assert_matches!(
1133            provider.block(1.into()),
1134            Err(ProviderError::BlockExpired { requested: 1, earliest_available: 3 })
1135        );
1136        assert_matches!(
1137            provider.recovered_block(1.into(), Default::default()),
1138            Err(ProviderError::BlockExpired { requested: 1, earliest_available: 3 })
1139        );
1140        // and so must the range readers instead of returning blocks with stripped bodies
1141        assert_matches!(
1142            provider.block_range(1..=4),
1143            Err(ProviderError::BlockExpired { requested: 1, earliest_available: 3 })
1144        );
1145        assert_matches!(
1146            provider.block_with_senders_range(1..=4),
1147            Err(ProviderError::BlockExpired { requested: 1, earliest_available: 3 })
1148        );
1149        assert_matches!(
1150            provider.recovered_block_range(1..=4),
1151            Err(ProviderError::BlockExpired { requested: 1, earliest_available: 3 })
1152        );
1153        // ranges within the available history keep working
1154        assert_eq!(provider.block_range(3..=5).unwrap().len(), 3);
1155        assert_eq!(provider.recovered_block_range(3..=5).unwrap().len(), 3);
1156    }
1157
1158    #[test]
1159    fn insert_block_with_prune_modes() {
1160        let block = TEST_BLOCK.clone();
1161
1162        {
1163            let factory = create_test_provider_factory();
1164            let provider = factory.provider_rw().unwrap();
1165            assert_matches!(provider.insert_block(&block.clone().try_recover().unwrap()), Ok(_));
1166            assert_matches!(
1167                provider.transaction_sender(0), Ok(Some(sender))
1168                if sender == block.body().transactions[0].recover_signer().unwrap()
1169            );
1170            assert_matches!(
1171                provider.transaction_id(*block.body().transactions[0].tx_hash()),
1172                Ok(Some(0))
1173            );
1174        }
1175
1176        {
1177            let prune_modes = PruneModes {
1178                sender_recovery: Some(PruneMode::Full),
1179                transaction_lookup: Some(PruneMode::Full),
1180                ..PruneModes::default()
1181            };
1182            // Keep factory alive until provider is dropped to prevent TempDatabase cleanup
1183            let factory = create_test_provider_factory().with_prune_modes(prune_modes);
1184            let provider = factory.provider_rw().unwrap();
1185            assert_matches!(provider.insert_block(&block.clone().try_recover().unwrap()), Ok(_));
1186            assert_matches!(provider.transaction_sender(0), Ok(None));
1187            assert_matches!(
1188                provider.transaction_id(*block.body().transactions[0].tx_hash()),
1189                Ok(None)
1190            );
1191        }
1192    }
1193
1194    #[test]
1195    fn take_block_transaction_range_recover_senders() {
1196        let mut rng = generators::rng();
1197        let block =
1198            random_block(&mut rng, 0, BlockParams { tx_count: Some(3), ..Default::default() });
1199
1200        let tx_ranges: Vec<RangeInclusive<TxNumber>> = vec![0..=0, 1..=1, 2..=2, 0..=1, 1..=2];
1201        for range in tx_ranges {
1202            let factory = create_test_provider_factory();
1203            let provider = factory.provider_rw().unwrap();
1204
1205            assert_matches!(provider.insert_block(&block.clone().try_recover().unwrap()), Ok(_));
1206
1207            let senders = provider.take::<tables::TransactionSenders>(range.clone()).unwrap();
1208            assert_eq!(
1209                senders,
1210                range
1211                    .clone()
1212                    .map(|tx_number| (
1213                        tx_number,
1214                        block.body().transactions[tx_number as usize].recover_signer().unwrap()
1215                    ))
1216                    .collect::<Vec<_>>()
1217            );
1218
1219            let db_senders = provider.senders_by_tx_range(range);
1220            assert!(matches!(db_senders, Ok(ref v) if v.is_empty()));
1221        }
1222    }
1223
1224    #[test]
1225    fn header_sync_gap_lookup() {
1226        let factory = create_test_provider_factory();
1227        let provider = factory.provider_rw().unwrap();
1228
1229        let mut rng = generators::rng();
1230
1231        // Genesis
1232        let checkpoint = 0;
1233        let head = random_header(&mut rng, 0, None);
1234
1235        // Empty database
1236        assert_matches!(
1237            provider.local_tip_header(checkpoint),
1238            Err(ProviderError::HeaderNotFound(block_number))
1239                if block_number.as_number().unwrap() == checkpoint
1240        );
1241
1242        // Checkpoint and no gap
1243        let static_file_provider = provider.static_file_provider();
1244        let mut static_file_writer =
1245            static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1246        static_file_writer.append_header(head.header(), &head.hash()).unwrap();
1247        static_file_writer.commit().unwrap();
1248        drop(static_file_writer);
1249
1250        let local_head = provider.local_tip_header(checkpoint).unwrap();
1251
1252        assert_eq!(local_head, head);
1253    }
1254
1255    #[test]
1256    fn anchored_static_files_resume_after_the_pivot() {
1257        let factory = create_test_provider_factory();
1258        factory.set_storage_settings_cache(StorageSettings::v2());
1259        let provider = factory.database_provider_rw().unwrap();
1260        provider.anchor_pruned_static_files(10).unwrap();
1261        provider.commit().unwrap();
1262
1263        let static_files = factory.static_file_provider();
1264        for segment in StaticFileSegment::iter().filter(|segment| !segment.is_headers()) {
1265            assert_eq!(static_files.get_highest_static_file_block(segment), Some(10), "{segment}");
1266        }
1267    }
1268
1269    #[test]
1270    fn anchored_static_files_expire_history_below_the_pivot() {
1271        let factory = create_test_provider_factory();
1272        factory.set_storage_settings_cache(StorageSettings::v2());
1273        let provider = factory.database_provider_rw().unwrap();
1274        provider.anchor_pruned_static_files(10).unwrap();
1275        provider.commit().unwrap();
1276
1277        let static_files = factory.static_file_provider();
1278        assert_eq!(static_files.earliest_history_height(), 10);
1279        assert_matches!(
1280            factory.provider().unwrap().block(5.into()),
1281            Err(ProviderError::BlockExpired { requested: 5, earliest_available: 10 })
1282        );
1283
1284        static_files.initialize_index().unwrap();
1285        assert_eq!(static_files.earliest_history_height(), 10);
1286    }
1287
1288    #[test]
1289    fn anchoring_static_files_requires_storage_v2() {
1290        let factory = create_test_provider_factory();
1291        factory.set_storage_settings_cache(StorageSettings::v1());
1292        let provider = factory.database_provider_rw().unwrap();
1293        assert!(provider.anchor_pruned_static_files(10).is_err());
1294
1295        let static_files = factory.static_file_provider();
1296        for segment in StaticFileSegment::iter().filter(|segment| !segment.is_headers()) {
1297            assert_eq!(static_files.get_highest_static_file_block(segment), None, "{segment}");
1298        }
1299    }
1300
1301    #[test]
1302    fn rejected_unwind_below_the_anchor_leaves_the_database_untouched() {
1303        for segment in StaticFileSegment::iter().filter(|segment| !segment.is_headers()) {
1304            let factory = create_test_provider_factory();
1305            factory.set_storage_settings_cache(StorageSettings::v2());
1306            let provider = factory.database_provider_rw().unwrap();
1307            provider.anchor_pruned_static_files(10).unwrap();
1308            provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(10)).unwrap();
1309            provider.commit().unwrap();
1310
1311            // An unwind must fail before its updated MDBX checkpoint can be committed.
1312            let provider = factory.unwind_provider_rw().unwrap();
1313            provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(5)).unwrap();
1314            let queued = {
1315                let static_files = factory.static_file_provider();
1316                let mut writer = static_files.latest_writer(segment).unwrap();
1317                match segment {
1318                    StaticFileSegment::Transactions => writer.prune_transactions(0, 5),
1319                    StaticFileSegment::Receipts => writer.prune_receipts(0, 5),
1320                    StaticFileSegment::TransactionSenders => writer.prune_transaction_senders(0, 5),
1321                    StaticFileSegment::AccountChangeSets => writer.prune_account_changesets(5),
1322                    StaticFileSegment::StorageChangeSets => writer.prune_storage_changesets(5),
1323                    StaticFileSegment::Headers => unreachable!(),
1324                }
1325            };
1326            assert!(queued.and_then(|()| provider.commit()).is_err(), "{segment}");
1327
1328            let provider = factory.provider().unwrap();
1329            assert_eq!(
1330                provider.get_stage_checkpoint(StageId::Execution).unwrap(),
1331                Some(StageCheckpoint::new(10)),
1332                "{segment}"
1333            );
1334            assert_eq!(factory.static_file_provider().check_consistency(&provider).unwrap(), None);
1335        }
1336    }
1337
1338    #[test]
1339    fn a_sync_the_database_cannot_continue_is_refused() {
1340        let factory = create_test_provider_factory();
1341        factory.set_storage_settings_cache(StorageSettings::v2());
1342
1343        // Without snap state, either sync may run.
1344        let provider = factory.database_provider_ro().unwrap();
1345        assert!(provider.ensure_sync_mode(false).is_ok());
1346        assert!(provider.ensure_sync_mode(true).is_ok());
1347        drop(provider);
1348
1349        let unfinished = SnapAttempt::start(
1350            None,
1351            BlockNumHash::new(10, B256::repeat_byte(1)),
1352            B256::repeat_byte(2),
1353        );
1354        let mut abandoned = unfinished;
1355        abandoned.abandon();
1356        let mut verified = unfinished;
1357        verified.verify();
1358
1359        // Only snap finishes or replaces what an unverified attempt left in the state tables.
1360        for attempt in [unfinished, abandoned] {
1361            let provider = factory.database_provider_rw().unwrap();
1362            provider.write_snap_attempt(&attempt).unwrap();
1363            provider.commit().unwrap();
1364
1365            let provider = factory.database_provider_ro().unwrap();
1366            assert!(matches!(
1367                provider.ensure_sync_mode(false).err(),
1368                Some(ProviderError::SnapStateRequiresSnapSync { attempt: 0 })
1369            ));
1370            assert!(provider.ensure_sync_mode(true).is_ok());
1371        }
1372
1373        // A verified attempt leaves the pipeline free to continue above the pivot.
1374        let provider = factory.database_provider_rw().unwrap();
1375        provider.write_snap_attempt(&verified).unwrap();
1376        provider.commit().unwrap();
1377        assert!(factory.database_provider_ro().unwrap().ensure_sync_mode(false).is_ok());
1378    }
1379
1380    #[test]
1381    fn the_legacy_layout_refuses_snap_until_execution_passes_genesis() {
1382        // Databases from before stored settings fall back to the legacy layout.
1383        let factory = create_test_provider_factory_with_genesis_block_number(5);
1384        let provider = factory.database_provider_rw().unwrap();
1385        provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(5)).unwrap();
1386        provider.commit().unwrap();
1387
1388        // A fresh database would snap sync into a layout snap can't write.
1389        assert!(matches!(
1390            factory.database_provider_ro().unwrap().ensure_sync_mode(true).err(),
1391            Some(ProviderError::SnapStorageLayoutUnsupported)
1392        ));
1393
1394        // Executed state keeps the staged pipeline, so its layout doesn't matter.
1395        let provider = factory.database_provider_rw().unwrap();
1396        provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(42)).unwrap();
1397        provider.commit().unwrap();
1398        assert!(factory.database_provider_ro().unwrap().ensure_sync_mode(true).is_ok());
1399
1400        // An unverified attempt is finished by snap whatever executed, so the layout matters again.
1401        let provider = factory.database_provider_rw().unwrap();
1402        provider
1403            .write_snap_attempt(&SnapAttempt::start(
1404                None,
1405                BlockNumHash::new(10, B256::repeat_byte(1)),
1406                B256::repeat_byte(2),
1407            ))
1408            .unwrap();
1409        provider.commit().unwrap();
1410        assert!(matches!(
1411            factory.database_provider_ro().unwrap().ensure_sync_mode(true).err(),
1412            Some(ProviderError::SnapStorageLayoutUnsupported)
1413        ));
1414    }
1415
1416    #[test]
1417    fn a_database_without_an_execution_checkpoint_needs_the_snap_layout() {
1418        // Startup checks run after genesis, so a missing checkpoint counts as nothing executed.
1419        let factory = create_test_provider_factory();
1420        assert!(matches!(
1421            factory.database_provider_ro().unwrap().ensure_sync_mode(true).err(),
1422            Some(ProviderError::SnapStorageLayoutUnsupported)
1423        ));
1424
1425        factory.set_storage_settings_cache(StorageSettings::v2());
1426        assert!(factory.database_provider_ro().unwrap().ensure_sync_mode(true).is_ok());
1427    }
1428
1429    #[test]
1430    fn receipt_availability_preserves_prune_checkpoints() {
1431        let moved = PruneCheckpoint {
1432            block_number: Some(7),
1433            tx_number: Some(15),
1434            prune_mode: PruneMode::Before(8),
1435        };
1436        let available = PruneCheckpoint { block_number: None, tx_number: None, ..moved };
1437        let pruned = PruneCheckpoint { block_number: Some(4), tx_number: Some(9), ..moved };
1438        let covered = PruneCheckpoint { block_number: Some(2), tx_number: Some(5), ..moved };
1439        let anchor = PruneCheckpoint { tx_number: None, ..moved };
1440
1441        for (case, blocks_per_file, receipt_blocks, stored, expected) in [
1442            ("continuous", 3, vec![(0, 9)], [moved; 2], [available; 2]),
1443            ("pruned prefix", 10, vec![(5, 9)], [pruned; 2], [pruned; 2]),
1444            ("padded prefix", 10, vec![(5, 9)], [moved; 2], [pruned; 2]),
1445            ("above tip", 3, vec![(0, 4)], [moved; 2], [moved; 2]),
1446            ("gap", 3, vec![(0, 2), (4, 9)], [moved; 2], [moved; 2]),
1447            ("empty file gap", 3, vec![(0, 2), (6, 9)], [moved; 2], [moved; 2]),
1448            ("covered receipts", 3, vec![(0, 2), (4, 9)], [covered, moved], [available, moved]),
1449            ("covered logs", 3, vec![(0, 2), (4, 9)], [moved, covered], [moved, available]),
1450            ("snap anchor", 3, vec![(0, 9)], [anchor; 2], [anchor; 2]),
1451            ("empty first file", 3, vec![(5, 9)], [moved; 2], [moved; 2]),
1452            ("no files", 3, vec![], [moved; 2], [moved; 2]),
1453        ] {
1454            let factory =
1455                receipt_availability_test_factory(blocks_per_file, &receipt_blocks, stored);
1456            let before = factory.get_prune_checkpoints().unwrap();
1457            assert_eq!(
1458                factory.get_prune_checkpoints_for_availability().unwrap(),
1459                [(PruneSegment::Receipts, expected[0]), (PruneSegment::ContractLogs, expected[1])],
1460                "{case}",
1461            );
1462            assert_eq!(factory.get_prune_checkpoints().unwrap(), before);
1463        }
1464
1465        // Storage v1 keeps new receipts in MDBX when receipt pruning is enabled.
1466        let factory = receipt_availability_test_factory(3, &[(0, 9)], [moved; 2]);
1467        factory.set_storage_settings_cache(StorageSettings::v1());
1468        assert_eq!(
1469            factory.get_prune_checkpoints_for_availability().unwrap(),
1470            factory.get_prune_checkpoints().unwrap(),
1471        );
1472    }
1473
1474    #[test]
1475    fn receipt_availability_requires_body_indices() {
1476        let stored = PruneCheckpoint {
1477            block_number: Some(7),
1478            tx_number: Some(15),
1479            prune_mode: PruneMode::Before(8),
1480        };
1481        let factory = receipt_availability_test_factory(10, &[(5, 9)], [stored; 2]);
1482        let unchanged = || {
1483            assert_eq!(
1484                factory.get_prune_checkpoints_for_availability().unwrap(),
1485                factory.get_prune_checkpoints().unwrap(),
1486            );
1487        };
1488
1489        let provider = factory.provider_rw().unwrap();
1490        provider.tx_ref().delete::<tables::BlockBodyIndices>(4, None).unwrap();
1491        provider.commit().unwrap();
1492        unchanged();
1493
1494        // A seek can land on the next block after expired history.
1495        let provider = factory.provider_rw().unwrap();
1496        provider.tx_ref().delete::<tables::TransactionBlocks>(9, None).unwrap();
1497        provider.commit().unwrap();
1498        unchanged();
1499
1500        let provider = factory.provider_rw().unwrap();
1501        provider.tx_ref().clear::<tables::TransactionBlocks>().unwrap();
1502        provider.commit().unwrap();
1503        unchanged();
1504    }
1505
1506    fn receipt_availability_test_factory(
1507        blocks_per_file: u64,
1508        receipt_blocks: &[(u64, u64)],
1509        checkpoints: [PruneCheckpoint; 2],
1510    ) -> ProviderFactory<MockNodeTypesWithDB> {
1511        let mut factory = create_test_provider_factory().with_prune_modes(PruneModes {
1512            receipts: Some(PruneMode::Distance(100)),
1513            ..PruneModes::default()
1514        });
1515        factory.set_storage_settings_cache(StorageSettings::v2());
1516        factory.static_file_provider =
1517            StaticFileProviderBuilder::read_write(factory.db.path().join("receipt_files"))
1518                .with_blocks_per_file(blocks_per_file)
1519                .build()
1520                .unwrap();
1521        assert_eq!(factory.get_prune_checkpoints_for_availability().unwrap(), Vec::new());
1522
1523        let provider = factory.provider_rw().unwrap();
1524        // All blocks have two transactions, including blocks whose receipts were pruned.
1525        for block in 0..=9 {
1526            provider
1527                .tx_ref()
1528                .put::<tables::BlockBodyIndices>(
1529                    block,
1530                    StoredBlockBodyIndices { first_tx_num: 2 * block, tx_count: 2 },
1531                )
1532                .unwrap();
1533            provider.tx_ref().put::<tables::TransactionBlocks>(2 * block + 1, block).unwrap();
1534        }
1535        for (segment, checkpoint) in
1536            [PruneSegment::Receipts, PruneSegment::ContractLogs].into_iter().zip(checkpoints)
1537        {
1538            provider.save_prune_checkpoint(segment, checkpoint).unwrap();
1539        }
1540        provider.commit().unwrap();
1541
1542        for &(start, end) in receipt_blocks {
1543            let static_file_provider = factory.static_file_provider();
1544            let mut writer =
1545                static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1546            if let Some(last_pruned) = start.checked_sub(1) {
1547                writer.ensure_at_block(last_pruned).unwrap();
1548            }
1549            for block in start..=end {
1550                writer.increment_block(block).unwrap();
1551                for tx in 2 * block..2 * block + 2 {
1552                    writer.append_receipt(tx, &Receipt::default()).unwrap();
1553                }
1554            }
1555            writer.commit().unwrap();
1556        }
1557        factory
1558    }
1559}