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            if provider_ro.sealed_header(number)?.is_none_or(|header| header.hash() != hash) {
72                return Err(ConsistentViewError::Reorged { block: hash }.into())
73            }
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::{
87        test_utils::create_test_provider_factory_with_chain_spec, BlockWriter,
88        StaticFileProviderFactory, StaticFileWriter,
89    };
90    use alloy_primitives::Bytes;
91    use assert_matches::assert_matches;
92    use reth_chainspec::{EthChainSpec, MAINNET};
93    use reth_ethereum_primitives::{Block, BlockBody};
94    use reth_primitives_traits::{block::TestBlock, RecoveredBlock, SealedBlock};
95    use reth_static_file_types::StaticFileSegment;
96    use reth_storage_api::StorageLocation;
97
98    #[test]
99    fn test_consistent_view_extend() {
100        let provider_factory = create_test_provider_factory_with_chain_spec(MAINNET.clone());
101
102        let genesis_header = MAINNET.genesis_header();
103        let genesis_block =
104            SealedBlock::<Block>::seal_parts(genesis_header.clone(), BlockBody::default());
105        let genesis_hash: B256 = genesis_block.hash();
106        let genesis_block = RecoveredBlock::new_sealed(genesis_block, vec![]);
107
108        // insert the block
109        let provider_rw = provider_factory.provider_rw().unwrap();
110        provider_rw.insert_block(genesis_block, StorageLocation::StaticFiles).unwrap();
111        provider_rw.commit().unwrap();
112
113        // create a consistent view provider and check that a ro provider can be made
114        let view = ConsistentDbView::new_with_latest_tip(provider_factory.clone()).unwrap();
115
116        // ensure successful creation of a read-only provider.
117        assert_matches!(view.provider_ro(), Ok(_));
118
119        // generate a block that extends the genesis
120        let mut block = Block::default();
121        block.header_mut().parent_hash = genesis_hash;
122        block.header_mut().number = 1;
123        let sealed_block = SealedBlock::seal_slow(block);
124        let recovered_block = RecoveredBlock::new_sealed(sealed_block, vec![]);
125
126        // insert the block
127        let provider_rw = provider_factory.provider_rw().unwrap();
128        provider_rw.insert_block(recovered_block, StorageLocation::StaticFiles).unwrap();
129        provider_rw.commit().unwrap();
130
131        // ensure successful creation of a read-only provider, based on this new db state.
132        assert_matches!(view.provider_ro(), Ok(_));
133
134        // generate a block that extends that block
135        let mut block = Block::default();
136        block.header_mut().parent_hash = genesis_hash;
137        block.header_mut().number = 2;
138        let sealed_block = SealedBlock::seal_slow(block);
139        let recovered_block = RecoveredBlock::new_sealed(sealed_block, vec![]);
140
141        // insert the block
142        let provider_rw = provider_factory.provider_rw().unwrap();
143        provider_rw.insert_block(recovered_block, StorageLocation::StaticFiles).unwrap();
144        provider_rw.commit().unwrap();
145
146        // check that creation of a read-only provider still works
147        assert_matches!(view.provider_ro(), Ok(_));
148    }
149
150    #[test]
151    fn test_consistent_view_remove() {
152        let provider_factory = create_test_provider_factory_with_chain_spec(MAINNET.clone());
153
154        let genesis_header = MAINNET.genesis_header();
155        let genesis_block =
156            SealedBlock::<Block>::seal_parts(genesis_header.clone(), BlockBody::default());
157        let genesis_hash: B256 = genesis_block.hash();
158        let genesis_block = RecoveredBlock::new_sealed(genesis_block, vec![]);
159
160        // insert the block
161        let provider_rw = provider_factory.provider_rw().unwrap();
162        provider_rw.insert_block(genesis_block, StorageLocation::Both).unwrap();
163        provider_rw.0.static_file_provider().commit().unwrap();
164        provider_rw.commit().unwrap();
165
166        // create a consistent view provider and check that a ro provider can be made
167        let view = ConsistentDbView::new_with_latest_tip(provider_factory.clone()).unwrap();
168
169        // ensure successful creation of a read-only provider.
170        assert_matches!(view.provider_ro(), Ok(_));
171
172        // generate a block that extends the genesis
173        let mut block = Block::default();
174        block.header_mut().parent_hash = genesis_hash;
175        block.header_mut().number = 1;
176        let sealed_block = SealedBlock::seal_slow(block);
177        let recovered_block = RecoveredBlock::new_sealed(sealed_block.clone(), vec![]);
178
179        // insert the block
180        let provider_rw = provider_factory.provider_rw().unwrap();
181        provider_rw.insert_block(recovered_block, StorageLocation::Both).unwrap();
182        provider_rw.0.static_file_provider().commit().unwrap();
183        provider_rw.commit().unwrap();
184
185        // create a second consistent view provider and check that a ro provider can be made
186        let view = ConsistentDbView::new_with_latest_tip(provider_factory.clone()).unwrap();
187        let initial_tip_hash = sealed_block.hash();
188
189        // ensure successful creation of a read-only provider, based on this new db state.
190        assert_matches!(view.provider_ro(), Ok(_));
191
192        // remove the block above the genesis block
193        let provider_rw = provider_factory.provider_rw().unwrap();
194        provider_rw.remove_blocks_above(0, StorageLocation::Both).unwrap();
195        let sf_provider = provider_rw.0.static_file_provider();
196        sf_provider.get_writer(1, StaticFileSegment::Headers).unwrap().prune_headers(1).unwrap();
197        sf_provider.commit().unwrap();
198        provider_rw.commit().unwrap();
199
200        // ensure unsuccessful creation of a read-only provider, based on this new db state.
201        let Err(ProviderError::ConsistentView(boxed_consistent_view_err)) = view.provider_ro()
202        else {
203            panic!("expected reorged consistent view error, got success");
204        };
205        let unboxed = *boxed_consistent_view_err;
206        assert_eq!(unboxed, ConsistentViewError::Reorged { block: initial_tip_hash });
207
208        // generate a block that extends the genesis with a different hash
209        let mut block = Block::default();
210        block.header_mut().parent_hash = genesis_hash;
211        block.header_mut().number = 1;
212        block.header_mut().extra_data =
213            Bytes::from_str("6a6f75726e657920746f20697468616361").unwrap();
214        let sealed_block = SealedBlock::seal_slow(block);
215        let recovered_block = RecoveredBlock::new_sealed(sealed_block, vec![]);
216
217        // reinsert the block at the same height, but with a different hash
218        let provider_rw = provider_factory.provider_rw().unwrap();
219        provider_rw.insert_block(recovered_block, StorageLocation::Both).unwrap();
220        provider_rw.0.static_file_provider().commit().unwrap();
221        provider_rw.commit().unwrap();
222
223        // ensure unsuccessful creation of a read-only provider, based on this new db state.
224        let Err(ProviderError::ConsistentView(boxed_consistent_view_err)) = view.provider_ro()
225        else {
226            panic!("expected reorged consistent view error, got success");
227        };
228        let unboxed = *boxed_consistent_view_err;
229        assert_eq!(unboxed, ConsistentViewError::Reorged { block: initial_tip_hash });
230    }
231}