feat: enable state root task during persistence (#12392)

Co-authored-by: Roman Krasiuk <rokrassyuk@gmail.com>
This commit is contained in:
Dan Cline
2025-01-29 14:57:32 -05:00
committed by GitHub
parent 974cea1d38
commit 665ca2e89b
7 changed files with 462 additions and 72 deletions

View File

@ -86,7 +86,6 @@ where
+ HeaderProvider
+ HashingWriter
+ StateWriter
+ StateWriter
+ AsRef<PF::ProviderRW>,
PF::ChainSpec: EthChainSpec<Header = <PF::Primitives as NodePrimitives>::BlockHeader>,
{

View File

@ -184,6 +184,12 @@ pub enum ConsistentViewError {
/// The tip diff.
tip: GotExpected<Option<B256>>,
},
/// Error thrown when the database does not contain a block from the previous database view.
#[display("database view no longer contains block: {block:?}")]
Reorged {
/// The previous block
block: B256,
},
}
impl From<ConsistentViewError> for ProviderError {

View File

@ -73,6 +73,7 @@ reth-chain-state = { workspace = true, features = ["test-utils"] }
reth-trie = { workspace = true, features = ["test-utils"] }
reth-testing-utils.workspace = true
reth-ethereum-engine-primitives.workspace = true
reth-ethereum-primitives.workspace = true
parking_lot.workspace = true
tempfile.workspace = true
@ -105,6 +106,7 @@ test-utils = [
"reth-trie/test-utils",
"reth-chain-state/test-utils",
"reth-ethereum-engine-primitives",
"reth-ethereum-primitives/test-utils",
"reth-chainspec/test-utils",
"reth-evm/test-utils",
"reth-network-p2p/test-utils",

View File

@ -1,7 +1,6 @@
use crate::{BlockNumReader, DatabaseProviderFactory, HeaderProvider};
use alloy_primitives::B256;
use reth_errors::ProviderError;
use reth_primitives::GotExpected;
use reth_storage_api::{DBProvider, StateCommitmentProvider};
use reth_storage_errors::provider::ProviderResult;
@ -28,7 +27,7 @@ pub use reth_storage_errors::provider::ConsistentViewError;
#[derive(Clone, Debug)]
pub struct ConsistentDbView<Factory> {
factory: Factory,
tip: Option<B256>,
tip: Option<(B256, u64)>,
}
impl<Factory> ConsistentDbView<Factory>
@ -37,7 +36,7 @@ where
+ StateCommitmentProvider,
{
/// Creates new consistent database view.
pub const fn new(factory: Factory, tip: Option<B256>) -> Self {
pub const fn new(factory: Factory, tip: Option<(B256, u64)>) -> Self {
Self { factory, tip }
}
@ -45,7 +44,7 @@ where
pub fn new_with_latest_tip(provider: Factory) -> ProviderResult<Self> {
let provider_ro = provider.database_provider_ro()?;
let last_num = provider_ro.last_block_number()?;
let tip = provider_ro.sealed_header(last_num)?.map(|h| h.hash());
let tip = provider_ro.sealed_header(last_num)?.map(|h| (h.hash(), last_num));
Ok(Self::new(provider, tip))
}
@ -71,31 +70,185 @@ where
// Create a new provider.
let provider_ro = self.factory.database_provider_ro()?;
// Check that the latest stored header number matches the number
// that consistent view was initialized with.
// The mismatch can happen if a new block was appended while
// the view was being used.
// We compare block hashes instead of block numbers to account for reorgs.
let last_num = provider_ro.last_block_number()?;
let tip = provider_ro.sealed_header(last_num)?.map(|h| h.hash());
if self.tip != tip {
return Err(ConsistentViewError::Inconsistent {
tip: GotExpected { got: tip, expected: self.tip },
// Check that the currently stored tip is included on-disk.
// This means that the database may have moved, but the view was not reorged.
//
// NOTE: We must use `sealed_header` with the block number here, because if we are using
// the consistent view provider while we're persisting blocks, we may enter a race
// condition. Recall that we always commit to static files first, then the database, and
// that block hash to block number indexes are contained in the database. If we were to
// fetch the block by hash while we're persisting, the following situation may occur:
//
// 1. Persistence appends the latest block to static files.
// 2. We initialize the consistent view provider, which fetches based on `last_block_number`
// and `sealed_header`, which both check static files, setting the tip to the newly
// committed block.
// 3. We attempt to fetch a header by hash, using for example the `header` method. This
// checks the database first, to fetch the number corresponding to the hash. Because the
// database has not been committed yet, this fails, and we return
// `ConsistentViewError::Reorged`.
// 4. Some time later, the database commits.
//
// To ensure this doesn't happen, we just have to make sure that we fetch from the same
// data source that we used during initialization. In this case, that is static files
if let Some((hash, number)) = self.tip {
if provider_ro.sealed_header(number)?.is_none_or(|header| header.hash() != hash) {
return Err(ConsistentViewError::Reorged { block: hash }.into())
}
.into())
}
// Check that the best block number is the same as the latest stored header.
// This ensures that the consistent view cannot be used for initializing new providers
// if the node fell back to the staged sync.
let best_block_number = provider_ro.best_block_number()?;
if last_num != best_block_number {
return Err(ConsistentViewError::Syncing {
best_block: GotExpected { got: best_block_number, expected: last_num },
}
.into())
}
Ok(provider_ro)
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::*;
use crate::{
test_utils::create_test_provider_factory_with_chain_spec, BlockWriter,
StaticFileProviderFactory, StaticFileWriter,
};
use alloy_primitives::Bytes;
use assert_matches::assert_matches;
use reth_chainspec::{EthChainSpec, MAINNET};
use reth_ethereum_primitives::{Block, BlockBody};
use reth_primitives::StaticFileSegment;
use reth_primitives_traits::{block::TestBlock, RecoveredBlock, SealedBlock};
use reth_storage_api::StorageLocation;
#[test]
fn test_consistent_view_extend() {
let provider_factory = create_test_provider_factory_with_chain_spec(MAINNET.clone());
let genesis_header = MAINNET.genesis_header();
let genesis_block =
SealedBlock::<Block>::seal_parts(genesis_header.clone(), BlockBody::default());
let genesis_hash: B256 = genesis_block.hash();
let genesis_block = RecoveredBlock::new_sealed(genesis_block, vec![]);
// insert the block
let provider_rw = provider_factory.provider_rw().unwrap();
provider_rw.insert_block(genesis_block, StorageLocation::StaticFiles).unwrap();
provider_rw.commit().unwrap();
// create a consistent view provider and check that a ro provider can be made
let view = ConsistentDbView::new_with_latest_tip(provider_factory.clone()).unwrap();
// ensure successful creation of a read-only provider.
assert_matches!(view.provider_ro(), Ok(_));
// generate a block that extends the genesis
let mut block = Block::default();
block.header_mut().parent_hash = genesis_hash;
block.header_mut().number = 1;
let sealed_block = SealedBlock::seal_slow(block);
let recovered_block = RecoveredBlock::new_sealed(sealed_block, vec![]);
// insert the block
let provider_rw = provider_factory.provider_rw().unwrap();
provider_rw.insert_block(recovered_block, StorageLocation::StaticFiles).unwrap();
provider_rw.commit().unwrap();
// ensure successful creation of a read-only provider, based on this new db state.
assert_matches!(view.provider_ro(), Ok(_));
// generate a block that extends that block
let mut block = Block::default();
block.header_mut().parent_hash = genesis_hash;
block.header_mut().number = 2;
let sealed_block = SealedBlock::seal_slow(block);
let recovered_block = RecoveredBlock::new_sealed(sealed_block, vec![]);
// insert the block
let provider_rw = provider_factory.provider_rw().unwrap();
provider_rw.insert_block(recovered_block, StorageLocation::StaticFiles).unwrap();
provider_rw.commit().unwrap();
// check that creation of a read-only provider still works
assert_matches!(view.provider_ro(), Ok(_));
}
#[test]
fn test_consistent_view_remove() {
let provider_factory = create_test_provider_factory_with_chain_spec(MAINNET.clone());
let genesis_header = MAINNET.genesis_header();
let genesis_block =
SealedBlock::<Block>::seal_parts(genesis_header.clone(), BlockBody::default());
let genesis_hash: B256 = genesis_block.hash();
let genesis_block = RecoveredBlock::new_sealed(genesis_block, vec![]);
// insert the block
let provider_rw = provider_factory.provider_rw().unwrap();
provider_rw.insert_block(genesis_block, StorageLocation::Both).unwrap();
provider_rw.0.static_file_provider().commit().unwrap();
provider_rw.commit().unwrap();
// create a consistent view provider and check that a ro provider can be made
let view = ConsistentDbView::new_with_latest_tip(provider_factory.clone()).unwrap();
// ensure successful creation of a read-only provider.
assert_matches!(view.provider_ro(), Ok(_));
// generate a block that extends the genesis
let mut block = Block::default();
block.header_mut().parent_hash = genesis_hash;
block.header_mut().number = 1;
let sealed_block = SealedBlock::seal_slow(block);
let recovered_block = RecoveredBlock::new_sealed(sealed_block.clone(), vec![]);
// insert the block
let provider_rw = provider_factory.provider_rw().unwrap();
provider_rw.insert_block(recovered_block, StorageLocation::Both).unwrap();
provider_rw.0.static_file_provider().commit().unwrap();
provider_rw.commit().unwrap();
// create a second consistent view provider and check that a ro provider can be made
let view = ConsistentDbView::new_with_latest_tip(provider_factory.clone()).unwrap();
let initial_tip_hash = sealed_block.hash();
// ensure successful creation of a read-only provider, based on this new db state.
assert_matches!(view.provider_ro(), Ok(_));
// remove the block above the genesis block
let provider_rw = provider_factory.provider_rw().unwrap();
provider_rw.remove_blocks_above(0, StorageLocation::Both).unwrap();
let sf_provider = provider_rw.0.static_file_provider();
sf_provider.get_writer(1, StaticFileSegment::Headers).unwrap().prune_headers(1).unwrap();
sf_provider.commit().unwrap();
provider_rw.commit().unwrap();
// ensure unsuccessful creation of a read-only provider, based on this new db state.
let Err(ProviderError::ConsistentView(boxed_consistent_view_err)) = view.provider_ro()
else {
panic!("expected reorged consistent view error, got success");
};
let unboxed = *boxed_consistent_view_err;
assert_eq!(unboxed, ConsistentViewError::Reorged { block: initial_tip_hash });
// generate a block that extends the genesis with a different hash
let mut block = Block::default();
block.header_mut().parent_hash = genesis_hash;
block.header_mut().number = 1;
block.header_mut().extra_data =
Bytes::from_str("6a6f75726e657920746f20697468616361").unwrap();
let sealed_block = SealedBlock::seal_slow(block);
let recovered_block = RecoveredBlock::new_sealed(sealed_block, vec![]);
// reinsert the block at the same height, but with a different hash
let provider_rw = provider_factory.provider_rw().unwrap();
provider_rw.insert_block(recovered_block, StorageLocation::Both).unwrap();
provider_rw.0.static_file_provider().commit().unwrap();
provider_rw.commit().unwrap();
// ensure unsuccessful creation of a read-only provider, based on this new db state.
let Err(ProviderError::ConsistentView(boxed_consistent_view_err)) = view.provider_ro()
else {
panic!("expected reorged consistent view error, got success");
};
let unboxed = *boxed_consistent_view_err;
assert_eq!(unboxed, ConsistentViewError::Reorged { block: initial_tip_hash });
}
}