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;
67/// 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}
2728impl<Factory> ConsistentDbView<Factory>
29where
30Factory: DatabaseProviderFactory<Provider: BlockNumReader + HeaderProvider>
31 + StateCommitmentProvider,
32{
33/// Creates new consistent database view.
34pub const fn new(factory: Factory, tip: Option<(B256, u64)>) -> Self {
35Self { factory, tip }
36 }
3738/// Creates new consistent database view with latest tip.
39pub fn new_with_latest_tip(provider: Factory) -> ProviderResult<Self> {
40let provider_ro = provider.database_provider_ro()?;
41let last_num = provider_ro.last_block_number()?;
42let tip = provider_ro.sealed_header(last_num)?.map(|h| (h.hash(), last_num));
43Ok(Self::new(provider, tip))
44 }
4546/// Creates new read-only provider and performs consistency checks on the current tip.
47pub fn provider_ro(&self) -> ProviderResult<Factory::Provider> {
48// Create a new provider.
49let provider_ro = self.factory.database_provider_ro()?;
5051// 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
72if let Some((hash, number)) = self.tip {
73if provider_ro.sealed_header(number)?.is_none_or(|header| header.hash() != hash) {
74return Err(ConsistentViewError::Reorged { block: hash }.into())
75 }
76 }
7778Ok(provider_ro)
79 }
80}
8182#[cfg(test)]
83mod tests {
84use reth_errors::ProviderError;
85use std::str::FromStr;
8687use super::*;
88use crate::{
89 test_utils::create_test_provider_factory_with_chain_spec, BlockWriter,
90 StaticFileProviderFactory, StaticFileWriter,
91 };
92use alloy_primitives::Bytes;
93use assert_matches::assert_matches;
94use reth_chainspec::{EthChainSpec, MAINNET};
95use reth_ethereum_primitives::{Block, BlockBody};
96use reth_primitives_traits::{block::TestBlock, RecoveredBlock, SealedBlock};
97use reth_static_file_types::StaticFileSegment;
98use reth_storage_api::StorageLocation;
99100#[test]
101fn test_consistent_view_extend() {
102let provider_factory = create_test_provider_factory_with_chain_spec(MAINNET.clone());
103104let genesis_header = MAINNET.genesis_header();
105let genesis_block =
106 SealedBlock::<Block>::seal_parts(genesis_header.clone(), BlockBody::default());
107let genesis_hash: B256 = genesis_block.hash();
108let genesis_block = RecoveredBlock::new_sealed(genesis_block, vec![]);
109110// insert the block
111let provider_rw = provider_factory.provider_rw().unwrap();
112 provider_rw.insert_block(genesis_block, StorageLocation::StaticFiles).unwrap();
113 provider_rw.commit().unwrap();
114115// create a consistent view provider and check that a ro provider can be made
116let view = ConsistentDbView::new_with_latest_tip(provider_factory.clone()).unwrap();
117118// ensure successful creation of a read-only provider.
119assert_matches!(view.provider_ro(), Ok(_));
120121// generate a block that extends the genesis
122let mut block = Block::default();
123 block.header_mut().parent_hash = genesis_hash;
124 block.header_mut().number = 1;
125let sealed_block = SealedBlock::seal_slow(block);
126let recovered_block = RecoveredBlock::new_sealed(sealed_block, vec![]);
127128// insert the block
129let provider_rw = provider_factory.provider_rw().unwrap();
130 provider_rw.insert_block(recovered_block, StorageLocation::StaticFiles).unwrap();
131 provider_rw.commit().unwrap();
132133// ensure successful creation of a read-only provider, based on this new db state.
134assert_matches!(view.provider_ro(), Ok(_));
135136// generate a block that extends that block
137let mut block = Block::default();
138 block.header_mut().parent_hash = genesis_hash;
139 block.header_mut().number = 2;
140let sealed_block = SealedBlock::seal_slow(block);
141let recovered_block = RecoveredBlock::new_sealed(sealed_block, vec![]);
142143// insert the block
144let provider_rw = provider_factory.provider_rw().unwrap();
145 provider_rw.insert_block(recovered_block, StorageLocation::StaticFiles).unwrap();
146 provider_rw.commit().unwrap();
147148// check that creation of a read-only provider still works
149assert_matches!(view.provider_ro(), Ok(_));
150 }
151152#[test]
153fn test_consistent_view_remove() {
154let provider_factory = create_test_provider_factory_with_chain_spec(MAINNET.clone());
155156let genesis_header = MAINNET.genesis_header();
157let genesis_block =
158 SealedBlock::<Block>::seal_parts(genesis_header.clone(), BlockBody::default());
159let genesis_hash: B256 = genesis_block.hash();
160let genesis_block = RecoveredBlock::new_sealed(genesis_block, vec![]);
161162// insert the block
163let 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();
167168// create a consistent view provider and check that a ro provider can be made
169let view = ConsistentDbView::new_with_latest_tip(provider_factory.clone()).unwrap();
170171// ensure successful creation of a read-only provider.
172assert_matches!(view.provider_ro(), Ok(_));
173174// generate a block that extends the genesis
175let mut block = Block::default();
176 block.header_mut().parent_hash = genesis_hash;
177 block.header_mut().number = 1;
178let sealed_block = SealedBlock::seal_slow(block);
179let recovered_block = RecoveredBlock::new_sealed(sealed_block.clone(), vec![]);
180181// insert the block
182let 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();
186187// create a second consistent view provider and check that a ro provider can be made
188let view = ConsistentDbView::new_with_latest_tip(provider_factory.clone()).unwrap();
189let initial_tip_hash = sealed_block.hash();
190191// ensure successful creation of a read-only provider, based on this new db state.
192assert_matches!(view.provider_ro(), Ok(_));
193194// remove the block above the genesis block
195let provider_rw = provider_factory.provider_rw().unwrap();
196 provider_rw.remove_blocks_above(0, StorageLocation::Both).unwrap();
197let 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();
201202// ensure unsuccessful creation of a read-only provider, based on this new db state.
203let Err(ProviderError::ConsistentView(boxed_consistent_view_err)) = view.provider_ro()
204else {
205panic!("expected reorged consistent view error, got success");
206 };
207let unboxed = *boxed_consistent_view_err;
208assert_eq!(unboxed, ConsistentViewError::Reorged { block: initial_tip_hash });
209210// generate a block that extends the genesis with a different hash
211let 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();
216let sealed_block = SealedBlock::seal_slow(block);
217let recovered_block = RecoveredBlock::new_sealed(sealed_block, vec![]);
218219// reinsert the block at the same height, but with a different hash
220let 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();
224225// ensure unsuccessful creation of a read-only provider, based on this new db state.
226let Err(ProviderError::ConsistentView(boxed_consistent_view_err)) = view.provider_ro()
227else {
228panic!("expected reorged consistent view error, got success");
229 };
230let unboxed = *boxed_consistent_view_err;
231assert_eq!(unboxed, ConsistentViewError::Reorged { block: initial_tip_hash });
232 }
233}