reth_provider/providers/
consistent_view.rs

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