reth_provider/providers/
consistent_view.rs

1use crate::{BlockNumReader, DatabaseProviderFactory, HeaderProvider};
2use alloy_primitives::B256;
3pub use reth_storage_errors::provider::ConsistentViewError;
4use reth_storage_errors::provider::ProviderResult;
5
6/// A consistent view over state in the database.
7///
8/// View gets initialized with the latest or provided tip.
9/// Upon every attempt to create a database provider, the view will
10/// perform a consistency check of current tip against the initial one.
11///
12/// ## Usage
13///
14/// The view should only be used outside of staged-sync.
15/// Otherwise, any attempt to create a provider will result in [`ConsistentViewError::Syncing`].
16///
17/// When using the view, the consumer should either
18/// 1) have a failover for when the state changes and handle [`ConsistentViewError::Inconsistent`]
19///    appropriately.
20/// 2) be sure that the state does not change.
21#[derive(Clone, Debug)]
22pub struct ConsistentDbView<Factory> {
23    factory: Factory,
24    tip: Option<(B256, u64)>,
25}
26
27impl<Factory> ConsistentDbView<Factory>
28where
29    Factory: DatabaseProviderFactory<Provider: BlockNumReader + HeaderProvider>,
30{
31    /// Creates new consistent database view.
32    pub const fn new(factory: Factory, tip: Option<(B256, u64)>) -> Self {
33        Self { factory, tip }
34    }
35
36    /// Creates new consistent database view with latest tip.
37    pub fn new_with_latest_tip(provider: Factory) -> ProviderResult<Self> {
38        let provider_ro = provider.database_provider_ro()?;
39        let last_num = provider_ro.last_block_number()?;
40        let tip = provider_ro.sealed_header(last_num)?.map(|h| (h.hash(), last_num));
41        Ok(Self::new(provider, tip))
42    }
43
44    /// Creates new read-only provider and performs consistency checks on the current tip.
45    pub fn provider_ro(&self) -> ProviderResult<Factory::Provider> {
46        // Create a new provider.
47        let provider_ro = self.factory.database_provider_ro()?;
48
49        // Check that the currently stored tip is included on-disk.
50        // This means that the database may have moved, but the view was not reorged.
51        //
52        // NOTE: We must use `sealed_header` with the block number here, because if we are using
53        // the consistent view provider while we're persisting blocks, we may enter a race
54        // condition. Recall that we always commit to static files first, then the database, and
55        // that block hash to block number indexes are contained in the database. If we were to
56        // fetch the block by hash while we're persisting, the following situation may occur:
57        //
58        // 1. Persistence appends the latest block to static files.
59        // 2. We initialize the consistent view provider, which fetches based on `last_block_number`
60        //    and `sealed_header`, which both check static files, setting the tip to the newly
61        //    committed block.
62        // 3. We attempt to fetch a header by hash, using for example the `header` method. This
63        //    checks the database first, to fetch the number corresponding to the hash. Because the
64        //    database has not been committed yet, this fails, and we return
65        //    `ConsistentViewError::Reorged`.
66        // 4. Some time later, the database commits.
67        //
68        // To ensure this doesn't happen, we just have to make sure that we fetch from the same
69        // data source that we used during initialization. In this case, that is static files
70        if let Some((hash, number)) = self.tip &&
71            provider_ro.sealed_header(number)?.is_none_or(|header| header.hash() != hash)
72        {
73            return Err(ConsistentViewError::Reorged { block: hash }.into())
74        }
75
76        Ok(provider_ro)
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use reth_errors::ProviderError;
83    use std::str::FromStr;
84
85    use super::*;
86    use crate::{test_utils::create_test_provider_factory, BlockWriter};
87    use alloy_primitives::Bytes;
88    use assert_matches::assert_matches;
89    use reth_chainspec::{ChainSpecProvider, EthChainSpec};
90    use reth_ethereum_primitives::{Block, BlockBody};
91    use reth_primitives_traits::{block::TestBlock, RecoveredBlock, SealedBlock};
92
93    #[test]
94    fn test_consistent_view_extend() {
95        let provider_factory = create_test_provider_factory();
96
97        let genesis_block = SealedBlock::<Block>::seal_parts(
98            provider_factory.chain_spec().genesis_header().clone(),
99            BlockBody::default(),
100        );
101        let genesis_hash: B256 = genesis_block.hash();
102        let genesis_block = RecoveredBlock::new_sealed(genesis_block, vec![]);
103
104        // insert the block
105        let provider_rw = provider_factory.provider_rw().unwrap();
106        provider_rw.insert_block(genesis_block).unwrap();
107        provider_rw.commit().unwrap();
108
109        // create a consistent view provider and check that a ro provider can be made
110        let view = ConsistentDbView::new_with_latest_tip(provider_factory.clone()).unwrap();
111
112        // ensure successful creation of a read-only provider.
113        assert_matches!(view.provider_ro(), Ok(_));
114
115        // generate a block that extends the genesis
116        let mut block = Block::default();
117        block.header_mut().parent_hash = genesis_hash;
118        block.header_mut().number = 1;
119        let sealed_block = SealedBlock::seal_slow(block);
120        let recovered_block = RecoveredBlock::new_sealed(sealed_block, vec![]);
121
122        // insert the block
123        let provider_rw = provider_factory.provider_rw().unwrap();
124        provider_rw.insert_block(recovered_block).unwrap();
125        provider_rw.commit().unwrap();
126
127        // ensure successful creation of a read-only provider, based on this new db state.
128        assert_matches!(view.provider_ro(), Ok(_));
129
130        // generate a block that extends that block
131        let mut block = Block::default();
132        block.header_mut().parent_hash = genesis_hash;
133        block.header_mut().number = 2;
134        let sealed_block = SealedBlock::seal_slow(block);
135        let recovered_block = RecoveredBlock::new_sealed(sealed_block, vec![]);
136
137        // insert the block
138        let provider_rw = provider_factory.provider_rw().unwrap();
139        provider_rw.insert_block(recovered_block).unwrap();
140        provider_rw.commit().unwrap();
141
142        // check that creation of a read-only provider still works
143        assert_matches!(view.provider_ro(), Ok(_));
144    }
145
146    #[test]
147    fn test_consistent_view_remove() {
148        let provider_factory = create_test_provider_factory();
149
150        let genesis_block = SealedBlock::<Block>::seal_parts(
151            provider_factory.chain_spec().genesis_header().clone(),
152            BlockBody::default(),
153        );
154        let genesis_hash: B256 = genesis_block.hash();
155        let genesis_block = RecoveredBlock::new_sealed(genesis_block, vec![]);
156
157        // insert the block
158        let provider_rw = provider_factory.provider_rw().unwrap();
159        provider_rw.insert_block(genesis_block).unwrap();
160        provider_rw.commit().unwrap();
161
162        // create a consistent view provider and check that a ro provider can be made
163        let view = ConsistentDbView::new_with_latest_tip(provider_factory.clone()).unwrap();
164
165        // ensure successful creation of a read-only provider.
166        assert_matches!(view.provider_ro(), Ok(_));
167
168        // generate a block that extends the genesis
169        let mut block = Block::default();
170        block.header_mut().parent_hash = genesis_hash;
171        block.header_mut().number = 1;
172        let sealed_block = SealedBlock::seal_slow(block);
173        let recovered_block = RecoveredBlock::new_sealed(sealed_block.clone(), vec![]);
174
175        // insert the block
176        let provider_rw = provider_factory.provider_rw().unwrap();
177        provider_rw.insert_block(recovered_block).unwrap();
178        provider_rw.commit().unwrap();
179
180        // create a second consistent view provider and check that a ro provider can be made
181        let view = ConsistentDbView::new_with_latest_tip(provider_factory.clone()).unwrap();
182        let initial_tip_hash = sealed_block.hash();
183
184        // ensure successful creation of a read-only provider, based on this new db state.
185        assert_matches!(view.provider_ro(), Ok(_));
186
187        // remove the block above the genesis block
188        let provider_rw = provider_factory.provider_rw().unwrap();
189        provider_rw.remove_blocks_above(0).unwrap();
190        provider_rw.commit().unwrap();
191
192        // ensure unsuccessful creation of a read-only provider, based on this new db state.
193        let Err(ProviderError::ConsistentView(boxed_consistent_view_err)) = view.provider_ro()
194        else {
195            panic!("expected reorged consistent view error, got success");
196        };
197        let unboxed = *boxed_consistent_view_err;
198        assert_eq!(unboxed, ConsistentViewError::Reorged { block: initial_tip_hash });
199
200        // generate a block that extends the genesis with a different hash
201        let mut block = Block::default();
202        block.header_mut().parent_hash = genesis_hash;
203        block.header_mut().number = 1;
204        block.header_mut().extra_data =
205            Bytes::from_str("6a6f75726e657920746f20697468616361").unwrap();
206        let sealed_block = SealedBlock::seal_slow(block);
207        let recovered_block = RecoveredBlock::new_sealed(sealed_block, vec![]);
208
209        // reinsert the block at the same height, but with a different hash
210        let provider_rw = provider_factory.provider_rw().unwrap();
211        provider_rw.insert_block(recovered_block).unwrap();
212        provider_rw.commit().unwrap();
213
214        // ensure unsuccessful creation of a read-only provider, based on this new db state.
215        let Err(ProviderError::ConsistentView(boxed_consistent_view_err)) = view.provider_ro()
216        else {
217            panic!("expected reorged consistent view error, got success");
218        };
219        let unboxed = *boxed_consistent_view_err;
220        assert_eq!(unboxed, ConsistentViewError::Reorged { block: initial_tip_hash });
221    }
222}