Compare commits

...

14 Commits

Author SHA1 Message Date
yongkangc
96673921e6 comment 2025-12-23 08:59:29 +00:00
yongkangc
5910807b94 reverted 2025-12-23 08:53:48 +00:00
yongkangc
ef678bfe37 fix: unwind when rocksdb history lags checkpoint 2025-12-23 08:47:16 +00:00
yongkangc
8eb4ba2f65 feat(provider): add check_file_consistency for proper ordering
Addresses Joshi's review comment about consistency check ordering.

The ordering is now:
1. check_file_consistency() - heals NippyJar files without pruning
2. rocksdb check_consistency() - needs static file tx data
3. static file check_consistency() - compares with MDBX, may prune

This ensures RocksDB can access transaction data before static files
potentially prune it during checkpoint comparison.
2025-12-23 08:19:11 +00:00
YK
9ff7a228b3 del 2025-12-23 16:06:48 +08:00
yongkangc
a2fec130f6 fix: adapt to #20508 API and add const to stub functions
- Remove non-existent check_file_consistency() call
- Simplify ordering: static file check first, then RocksDB check
- Add const to first() and last() stub functions
2025-12-23 04:20:34 +00:00
yongkangc
8a7a0f2f16 fix: make stub iter return empty iterator instead of error
This is consistent with first() and last() returning Ok(None) -
the stub behaves as if the database is empty rather than unavailable.
2025-12-23 04:17:07 +00:00
yongkangc
d3e812a985 fix: update consistency check ordering per Joshi's feedback
Reorder consistency checks as follows:
1. StaticFileProvider::check_file_consistency() - heals interrupted writes
2. RocksDB::check_consistency() - uses static file data for tx hash pruning
3. StaticFileProvider::check_consistency() - compares with MDBX checkpoints

This ensures RocksDB can access transaction data before static files prune it.

Amp-Thread-ID: https://ampcode.com/threads/T-019b3076-f7ed-700d-8916-779712facb8e
2025-12-23 04:17:07 +00:00
yongkangc
5dc4b5248e fix: address Joshi's review comments
- Revert 'allow RocksDB unwind to 0' - keep panic on unwind to 0 as it would
  leave MDBX with huge free list size
- Remove checked_add overflow protection (not needed in practice)
- Created issue #20506 for refactoring StaticFileProvider::check_consistency

Amp-Thread-ID: https://ampcode.com/threads/T-019b3076-f7ed-700d-8916-779712facb8e
2025-12-23 04:17:07 +00:00
yongkangc
d358d62353 fix: allow RocksDB unwind to block 0 for migration scenario
Address review feedback: nodes migrating to RocksDB will commonly trigger
unwind to block 0, which is expected. Only panic if static files also
require unwind to 0 (indicating corruption). Otherwise, warn and proceed
with full resync.
2025-12-23 04:17:07 +00:00
yongkangc
f741dd4792 fix(storage): address review findings for RocksDB consistency check
- Add missing methods to rocksdb_stub.rs (batch, first, last, iter, commit, RocksDBIter)
- Add checkpoint > 0 guard in (None, Some(highest_tx)) case for TransactionHashNumbers
- Use checked_add for mdbx_tx + 1 to prevent u64 overflow
- Update misleading docstring in stub's RocksDBProvider::new

Amp-Thread-ID: https://ampcode.com/threads/T-019b3076-f7ed-700d-8916-779712facb8e
2025-12-23 04:17:07 +00:00
yongkangc
3c776bbbc8 fix: add const to stub check_consistency fn 2025-12-23 04:17:07 +00:00
yongkangc
2a77d5f6de chore: format code 2025-12-23 04:17:07 +00:00
yongkangc
fb8eb0e3e6 feat(storage): add RocksDB consistency check on startup
Amp-Thread-ID: https://ampcode.com/threads/T-019b2fb1-ce5c-7251-b454-0d7472a0754a
2025-12-23 04:17:00 +00:00
4 changed files with 513 additions and 14 deletions

View File

@@ -66,8 +66,9 @@ use reth_node_metrics::{
};
use reth_provider::{
providers::{NodeTypesForProvider, ProviderNodeTypes, RocksDBProvider, StaticFileProvider},
BlockHashReader, BlockNumReader, ProviderError, ProviderFactory, ProviderResult,
StageCheckpointReader, StaticFileProviderBuilder, StaticFileProviderFactory,
BlockHashReader, BlockNumReader, DatabaseProviderFactory, ProviderError, ProviderFactory,
ProviderResult, RocksDBProviderFactory, StageCheckpointReader, StaticFileProviderBuilder,
StaticFileProviderFactory,
};
use reth_prune::{PruneModes, PrunerBuilder};
use reth_rpc_builder::config::RethRpcServerConfig;
@@ -501,20 +502,61 @@ where
)?
.with_prune_modes(self.prune_modes());
// Check for consistency between database and static files. If it fails, it unwinds to
// the first block that's consistent between database and static files.
if let Some(unwind_target) =
factory.static_file_provider().check_consistency(&factory.provider()?)?
{
// Check for consistency between database, static files, and RocksDB. If any
// inconsistencies are found, unwind to the first block that's consistent across all
// storage layers.
//
// The ordering is critical:
// 1. File healing - heals NippyJar inconsistencies without pruning datae
// 2. RocksDB check - needs static file tx data for hash lookups
// 3. Static file checkpoint check - compares with MDBX, may prune data
//
// We compute a combined unwind target from all checks and run a single unwind pass.
// Step 1: Heal file-level inconsistencies (no pruning)
let file_unwind = factory.static_file_provider().check_file_consistency()?;
// Step 2: RocksDB consistency check (needs static files tx data)
let rocksdb_unwind =
factory.rocksdb_provider().check_consistency(&factory.database_provider_ro()?)?;
// Step 3: Static file checkpoint consistency (may prune)
let static_file_unwind = factory
.static_file_provider()
.check_consistency(&factory.provider()?)?
.map(|target| match target {
PipelineTarget::Unwind(block) => block,
PipelineTarget::Sync(_) => unreachable!("check_consistency returns Unwind"),
});
// Combine all unwind targets - take the minimum (most conservative)
let unwind_target =
[file_unwind, rocksdb_unwind, static_file_unwind].into_iter().flatten().min();
if let Some(unwind_block) = unwind_target {
// Highly unlikely to happen, and given its destructive nature, it's better to panic
// instead.
// instead. Unwinding to 0 would leave MDBX with a huge free list size.
let inconsistency_source = match (file_unwind, rocksdb_unwind, static_file_unwind) {
(Some(_), Some(_), Some(_)) => {
"static file healing, RocksDB <> database, and static file <> database"
}
(Some(_), Some(_), None) => "static file healing and RocksDB <> database",
(Some(_), None, Some(_)) => "static file healing and static file <> database",
(None, Some(_), Some(_)) => "RocksDB <> database and static file <> database",
(Some(_), None, None) => "static file healing",
(None, Some(_), None) => "RocksDB <> database",
(None, None, Some(_)) => "static file <> database",
(None, None, None) => unreachable!(),
};
assert_ne!(
unwind_target,
PipelineTarget::Unwind(0),
"A static file <> database inconsistency was found that would trigger an unwind to block 0"
unwind_block,
0,
"A {inconsistency_source} inconsistency was found that would trigger an unwind to block 0"
);
info!(target: "reth::cli", unwind_target = %unwind_target, "Executing an unwind after a failed storage consistency check.");
let unwind_target = PipelineTarget::Unwind(unwind_block);
info!(target: "reth::cli", %unwind_target, %inconsistency_source, "Executing unwind after consistency check.");
let (_tip_tx, tip_rx) = watch::channel(B256::ZERO);
@@ -548,7 +590,7 @@ where
}),
);
rx.await?.inspect_err(|err| {
error!(target: "reth::cli", unwind_target = %unwind_target, %err, "failed to run unwind")
error!(target: "reth::cli", unwind_target=%unwind_target, %err, "failed to run unwind")
})?;
}

View File

@@ -69,6 +69,13 @@ impl RocksDBProvider {
unwind_target = Some(unwind_target.map_or(target, |t| t.min(target)));
}
// Check AccountsHistory if stored in RocksDB
if provider.cached_storage_settings().account_history_in_rocksdb &&
let Some(target) = self.check_accounts_history(provider)?
{
unwind_target = Some(unwind_target.map_or(target, |t| t.min(target)));
}
Ok(unwind_target)
}
@@ -155,6 +162,16 @@ impl RocksDBProvider {
"MDBX empty but static files have data, pruning all TransactionHashNumbers"
);
self.prune_transaction_hash_numbers_in_range(provider, 0..=highest_tx)?;
// If checkpoint claims progress but MDBX is empty, that's an inconsistency
if checkpoint > 0 {
tracing::warn!(
target: "reth::providers::rocksdb",
checkpoint,
"Checkpoint set but MDBX has no transactions, unwind needed"
);
return Ok(Some(0));
}
}
(None, None) => {
// Both MDBX and static files are empty.
@@ -275,6 +292,18 @@ impl RocksDBProvider {
"StoragesHistory ahead of checkpoint, pruning excess data"
);
self.prune_storages_history_above(checkpoint)?;
return Ok(None);
}
// If RocksDB is behind the checkpoint, request an unwind to rebuild.
if max_highest_block < checkpoint {
tracing::warn!(
target: "reth::providers::rocksdb",
rocks_highest = max_highest_block,
checkpoint,
"StoragesHistory behind checkpoint, unwind needed"
);
return Ok(Some(max_highest_block));
}
Ok(None)
@@ -327,6 +356,125 @@ impl RocksDBProvider {
Ok(())
}
/// Checks invariants for the `AccountsHistory` table.
///
/// Returns a block number to unwind to if `RocksDB` is behind the checkpoint.
/// If `RocksDB` is ahead of the checkpoint, excess entries are pruned (healed).
fn check_accounts_history<Provider>(
&self,
provider: &Provider,
) -> ProviderResult<Option<BlockNumber>>
where
Provider: DBProvider + StageCheckpointReader,
{
// Get the IndexAccountHistory stage checkpoint
let checkpoint = provider
.get_stage_checkpoint(StageId::IndexAccountHistory)?
.map(|cp| cp.block_number)
.unwrap_or(0);
// Check if RocksDB has any data
let rocks_first = self.first::<tables::AccountsHistory>()?;
match rocks_first {
Some(_) => {
// If checkpoint is 0 but we have data, clear everything
if checkpoint == 0 {
tracing::info!(
target: "reth::providers::rocksdb",
"AccountsHistory has data but checkpoint is 0, clearing all"
);
self.prune_accounts_history_above(0)?;
return Ok(None);
}
// Find the max highest_block_number (excluding u64::MAX sentinel) across all
// entries
let mut max_highest_block = 0u64;
for result in self.iter::<tables::AccountsHistory>()? {
let (key, _) = result?;
let highest = key.highest_block_number;
if highest != u64::MAX && highest > max_highest_block {
max_highest_block = highest;
}
}
// If any entry has highest_block > checkpoint, prune excess
if max_highest_block > checkpoint {
tracing::info!(
target: "reth::providers::rocksdb",
rocks_highest = max_highest_block,
checkpoint,
"AccountsHistory ahead of checkpoint, pruning excess data"
);
self.prune_accounts_history_above(checkpoint)?;
return Ok(None);
}
// If RocksDB is behind the checkpoint, request an unwind to rebuild.
if max_highest_block < checkpoint {
tracing::warn!(
target: "reth::providers::rocksdb",
rocks_highest = max_highest_block,
checkpoint,
"AccountsHistory behind checkpoint, unwind needed"
);
return Ok(Some(max_highest_block));
}
Ok(None)
}
None => {
// Empty RocksDB table
if checkpoint > 0 {
// Stage says we should have data but we don't
return Ok(Some(0));
}
Ok(None)
}
}
}
/// Prunes `AccountsHistory` entries where `highest_block_number` > `max_block`.
///
/// For `AccountsHistory`, the key is `ShardedKey<Address>` which contains
/// `highest_block_number`, so we can iterate and delete entries where
/// `key.highest_block_number > max_block`.
///
/// TODO(<https://github.com/paradigmxyz/reth/issues/20417>): this iterates the whole table,
/// which is inefficient. Use changeset-based pruning instead.
fn prune_accounts_history_above(&self, max_block: BlockNumber) -> ProviderResult<()> {
use alloy_primitives::Address;
use reth_db_api::models::ShardedKey;
let mut to_delete: Vec<ShardedKey<Address>> = Vec::new();
for result in self.iter::<tables::AccountsHistory>()? {
let (key, _) = result?;
let highest_block = key.highest_block_number;
if max_block == 0 || (highest_block != u64::MAX && highest_block > max_block) {
to_delete.push(key);
}
}
let deleted = to_delete.len();
if deleted > 0 {
tracing::info!(
target: "reth::providers::rocksdb",
deleted_count = deleted,
max_block,
"Pruning AccountsHistory entries"
);
let mut batch = self.batch();
for key in to_delete {
batch.delete::<tables::AccountsHistory>(key)?;
}
batch.commit()?;
}
Ok(())
}
}
#[cfg(test)]
@@ -580,6 +728,44 @@ mod tests {
);
}
#[test]
fn test_check_consistency_storages_history_behind_checkpoint_needs_unwind() {
let temp_dir = TempDir::new().unwrap();
let rocksdb = RocksDBBuilder::new(temp_dir.path())
.with_table::<tables::StoragesHistory>()
.build()
.unwrap();
// Insert data into RocksDB with highest_block_number below checkpoint
let key_block_50 = StorageShardedKey::new(Address::ZERO, B256::ZERO, 50);
let block_list = BlockNumberList::new_pre_sorted([10, 20, 30, 50]);
rocksdb.put::<tables::StoragesHistory>(key_block_50, &block_list).unwrap();
let factory = create_test_provider_factory();
factory.set_storage_settings_cache(
StorageSettings::legacy().with_storages_history_in_rocksdb(true),
);
// Set checkpoint to block 100
{
let provider = factory.database_provider_rw().unwrap();
provider
.save_stage_checkpoint(StageId::IndexStorageHistory, StageCheckpoint::new(100))
.unwrap();
provider.commit().unwrap();
}
let provider = factory.database_provider_ro().unwrap();
// RocksDB only has data up to block 50, but checkpoint says block 100 was processed
let result = rocksdb.check_consistency(&provider).unwrap();
assert_eq!(
result,
Some(50),
"Should require unwind to block 50 to rebuild StoragesHistory"
);
}
#[test]
fn test_check_consistency_mdbx_behind_checkpoint_needs_unwind() {
let temp_dir = TempDir::new().unwrap();
@@ -803,6 +989,177 @@ mod tests {
);
}
#[test]
fn test_check_consistency_accounts_history_empty_with_checkpoint_needs_unwind() {
let temp_dir = TempDir::new().unwrap();
let rocksdb = RocksDBBuilder::new(temp_dir.path())
.with_table::<tables::AccountsHistory>()
.build()
.unwrap();
// Create a test provider factory for MDBX
let factory = create_test_provider_factory();
factory.set_storage_settings_cache(
StorageSettings::legacy().with_account_history_in_rocksdb(true),
);
// Set a checkpoint indicating we should have processed up to block 100
{
let provider = factory.database_provider_rw().unwrap();
provider
.save_stage_checkpoint(StageId::IndexAccountHistory, StageCheckpoint::new(100))
.unwrap();
provider.commit().unwrap();
}
let provider = factory.database_provider_ro().unwrap();
// RocksDB is empty but checkpoint says block 100 was processed
let result = rocksdb.check_consistency(&provider).unwrap();
assert_eq!(result, Some(0), "Should require unwind to block 0 to rebuild AccountsHistory");
}
#[test]
fn test_check_consistency_accounts_history_has_data_no_checkpoint_prunes_data() {
use reth_db_api::models::ShardedKey;
let temp_dir = TempDir::new().unwrap();
let rocksdb = RocksDBBuilder::new(temp_dir.path())
.with_table::<tables::AccountsHistory>()
.build()
.unwrap();
// Insert data into RocksDB
let key = ShardedKey::new(Address::ZERO, 50);
let block_list = BlockNumberList::new_pre_sorted([10, 20, 30, 50]);
rocksdb.put::<tables::AccountsHistory>(key, &block_list).unwrap();
// Verify data exists
assert!(rocksdb.last::<tables::AccountsHistory>().unwrap().is_some());
// Create a test provider factory for MDBX with NO checkpoint
let factory = create_test_provider_factory();
factory.set_storage_settings_cache(
StorageSettings::legacy().with_account_history_in_rocksdb(true),
);
let provider = factory.database_provider_ro().unwrap();
// RocksDB has data but checkpoint is 0
// This means RocksDB has stale data that should be pruned (healed)
let result = rocksdb.check_consistency(&provider).unwrap();
assert_eq!(result, None, "Should heal by pruning, no unwind needed");
// Verify data was pruned
assert!(
rocksdb.last::<tables::AccountsHistory>().unwrap().is_none(),
"RocksDB should be empty after pruning"
);
}
#[test]
fn test_check_consistency_accounts_history_ahead_of_checkpoint_prunes_excess() {
use reth_db_api::models::ShardedKey;
let temp_dir = TempDir::new().unwrap();
let rocksdb = RocksDBBuilder::new(temp_dir.path())
.with_table::<tables::AccountsHistory>()
.build()
.unwrap();
// Insert data into RocksDB with different highest_block_numbers
let key_block_50 = ShardedKey::new(Address::ZERO, 50);
let key_block_100 = ShardedKey::new(Address::random(), 100);
let key_block_150 = ShardedKey::new(Address::random(), 150);
let key_block_max = ShardedKey::new(Address::random(), u64::MAX);
let block_list = BlockNumberList::new_pre_sorted([10, 20, 30]);
rocksdb.put::<tables::AccountsHistory>(key_block_50.clone(), &block_list).unwrap();
rocksdb.put::<tables::AccountsHistory>(key_block_100.clone(), &block_list).unwrap();
rocksdb.put::<tables::AccountsHistory>(key_block_150.clone(), &block_list).unwrap();
rocksdb.put::<tables::AccountsHistory>(key_block_max.clone(), &block_list).unwrap();
// Create a test provider factory for MDBX
let factory = create_test_provider_factory();
factory.set_storage_settings_cache(
StorageSettings::legacy().with_account_history_in_rocksdb(true),
);
// Set checkpoint to block 100
{
let provider = factory.database_provider_rw().unwrap();
provider
.save_stage_checkpoint(StageId::IndexAccountHistory, StageCheckpoint::new(100))
.unwrap();
provider.commit().unwrap();
}
let provider = factory.database_provider_ro().unwrap();
// RocksDB has entries with highest_block = 150 which exceeds checkpoint (100)
// Should prune entries where highest_block > 100 (but not u64::MAX sentinel)
let result = rocksdb.check_consistency(&provider).unwrap();
assert_eq!(result, None, "Should heal by pruning, no unwind needed");
// Verify key_block_150 was pruned, but others remain
assert!(
rocksdb.get::<tables::AccountsHistory>(key_block_50).unwrap().is_some(),
"Entry with highest_block=50 should remain"
);
assert!(
rocksdb.get::<tables::AccountsHistory>(key_block_100).unwrap().is_some(),
"Entry with highest_block=100 should remain"
);
assert!(
rocksdb.get::<tables::AccountsHistory>(key_block_150).unwrap().is_none(),
"Entry with highest_block=150 should be pruned"
);
assert!(
rocksdb.get::<tables::AccountsHistory>(key_block_max).unwrap().is_some(),
"Entry with highest_block=u64::MAX (sentinel) should remain"
);
}
#[test]
fn test_check_consistency_accounts_history_behind_checkpoint_needs_unwind() {
use reth_db_api::models::ShardedKey;
let temp_dir = TempDir::new().unwrap();
let rocksdb = RocksDBBuilder::new(temp_dir.path())
.with_table::<tables::AccountsHistory>()
.build()
.unwrap();
// Insert data into RocksDB with highest_block_number below checkpoint
let key_block_50 = ShardedKey::new(Address::ZERO, 50);
let block_list = BlockNumberList::new_pre_sorted([10, 20, 30, 50]);
rocksdb.put::<tables::AccountsHistory>(key_block_50, &block_list).unwrap();
let factory = create_test_provider_factory();
factory.set_storage_settings_cache(
StorageSettings::legacy().with_account_history_in_rocksdb(true),
);
// Set checkpoint to block 100
{
let provider = factory.database_provider_rw().unwrap();
provider
.save_stage_checkpoint(StageId::IndexAccountHistory, StageCheckpoint::new(100))
.unwrap();
provider.commit().unwrap();
}
let provider = factory.database_provider_ro().unwrap();
// RocksDB only has data up to block 50, but checkpoint says block 100 was processed
let result = rocksdb.check_consistency(&provider).unwrap();
assert_eq!(
result,
Some(50),
"Should require unwind to block 50 to rebuild AccountsHistory"
);
}
/// Test that pruning works by fetching transactions and computing their hashes,
/// rather than iterating all rows. This test uses random blocks with unique
/// transactions so we can verify the correct entries are pruned.

View File

@@ -23,7 +23,9 @@ pub struct RocksDBProvider;
impl RocksDBProvider {
/// Creates a new stub `RocksDB` provider.
///
/// On non-Unix platforms, this returns an error indicating `RocksDB` is not supported.
/// This stub always succeeds but any actual operations will return `UnsupportedProvider`.
/// When using this stub, all `*_in_rocksdb` flags should be set to `false` to ensure
/// operations route to MDBX instead.
pub fn new(_path: impl AsRef<Path>) -> ProviderResult<Self> {
Ok(Self)
}
@@ -77,6 +79,39 @@ impl RocksDBProvider {
pub const fn tx(&self) -> RocksTx {
RocksTx
}
/// Creates a new batch for atomic writes (stub implementation).
pub const fn batch(&self) -> RocksDBBatch {
RocksDBBatch
}
/// Gets the first key-value pair from a table (stub implementation).
pub const fn first<T: Table>(&self) -> ProviderResult<Option<(T::Key, T::Value)>> {
Ok(None)
}
/// Gets the last key-value pair from a table (stub implementation).
pub const fn last<T: Table>(&self) -> ProviderResult<Option<(T::Key, T::Value)>> {
Ok(None)
}
/// Creates an iterator for the specified table (stub implementation).
///
/// Returns an empty iterator. This is consistent with `first()` and `last()` returning
/// `Ok(None)` - the stub behaves as if the database is empty rather than unavailable.
pub const fn iter<T: Table>(&self) -> ProviderResult<RocksDBIter<'_, T>> {
Ok(RocksDBIter { _marker: std::marker::PhantomData })
}
/// Check consistency of `RocksDB` tables (stub implementation).
///
/// Returns `None` since there is no `RocksDB` data to check when the feature is disabled.
pub const fn check_consistency<Provider>(
&self,
_provider: &Provider,
) -> ProviderResult<Option<alloy_primitives::BlockNumber>> {
Ok(None)
}
}
/// A stub batch writer for `RocksDB` on non-Unix platforms.
@@ -102,6 +137,25 @@ impl RocksDBBatch {
pub fn delete<T: Table>(&self, _key: T::Key) -> ProviderResult<()> {
Err(UnsupportedProvider)
}
/// Commits the batch (stub implementation).
pub const fn commit(self) -> ProviderResult<()> {
Err(UnsupportedProvider)
}
}
/// A stub iterator for `RocksDB` (non-transactional).
#[derive(Debug)]
pub struct RocksDBIter<'a, T> {
_marker: std::marker::PhantomData<(&'a (), T)>,
}
impl<T: Table> Iterator for RocksDBIter<'_, T> {
type Item = ProviderResult<(T::Key, T::Value)>;
fn next(&mut self) -> Option<Self::Item> {
None
}
}
/// A stub builder for `RocksDB` on non-Unix platforms.
@@ -213,3 +267,11 @@ impl RocksTx {
pub struct RocksTxIter<'a, T> {
_marker: std::marker::PhantomData<(&'a (), T)>,
}
impl<T: Table> Iterator for RocksTxIter<'_, T> {
type Item = ProviderResult<(T::Key, T::Value)>;
fn next(&mut self) -> Option<Self::Item> {
None
}
}

View File

@@ -1184,6 +1184,44 @@ impl<N: NodePrimitives> StaticFileProvider<N> {
Ok(unwind_target.map(PipelineTarget::Unwind))
}
/// Heals file-level (`NippyJar`) inconsistencies for all static file segments.
///
/// This should be called BEFORE any checks that depend on static file data (e.g., `RocksDB`
/// consistency checks that need transaction data), as it ensures files are in a consistent
/// state without pruning any data.
///
/// Unlike [`Self::check_consistency`], this method:
/// - Does NOT compare with database checkpoints
/// - Does NOT prune data
/// - Applies to ALL segments unconditionally
///
/// Returns an unwind target if file healing detected a decrease in the highest block
/// (indicating a pruning interruption that needs a database unwind).
pub fn check_file_consistency(&self) -> ProviderResult<Option<BlockNumber>> {
info!(target: "reth::cli", "Healing static file inconsistencies.");
let mut unwind_target: Option<BlockNumber> = None;
for segment in StaticFileSegment::iter() {
let (initial_highest_block, highest_block) = self.maybe_heal_segment(segment)?;
if initial_highest_block != highest_block {
// Healing decreased highest block - need unwind
info!(
target: "reth::providers::static_file",
?segment,
?initial_highest_block,
?highest_block,
"File healing changed highest block, unwind needed"
);
let target = highest_block.unwrap_or_default();
unwind_target = Some(unwind_target.map_or(target, |t| t.min(target)));
}
}
Ok(unwind_target)
}
/// Checks consistency of the latest static file segment and throws an error if at fault.
/// Read-only.
pub fn check_segment_consistency(&self, segment: StaticFileSegment) -> ProviderResult<()> {