From 4db23809cc05a359725194e250cf61c61307db60 Mon Sep 17 00:00:00 2001 From: Derek Cofausper <256792747+decofe@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:29:45 -0800 Subject: [PATCH] fix(storage): return early in RocksDB healing when checkpoint is 0 (#22576) Co-authored-by: Amp Co-authored-by: joshieDo <93316087+joshieDo@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 --- .../src/providers/rocksdb/invariants.rs | 49 +++++++++++++++---- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/crates/storage/provider/src/providers/rocksdb/invariants.rs b/crates/storage/provider/src/providers/rocksdb/invariants.rs index d3568e18a5..16850a19dd 100644 --- a/crates/storage/provider/src/providers/rocksdb/invariants.rs +++ b/crates/storage/provider/src/providers/rocksdb/invariants.rs @@ -87,7 +87,7 @@ impl RocksDBProvider { /// Heals the `TransactionHashNumbers` table. /// - /// - Fast path: if checkpoint == 0 AND `RocksDB` has data, clear everything + /// - Fast path: if checkpoint == 0, clear any stale data and return /// - If `sf_tip` < checkpoint, return unwind target (static files behind) /// - If `sf_tip` == checkpoint, nothing to do /// - If `sf_tip` > checkpoint, heal via transaction ranges in batches @@ -112,11 +112,11 @@ impl RocksDBProvider { .get_highest_static_file_block(StaticFileSegment::Transactions) .unwrap_or(0); - // Fast path: if checkpoint is 0 and RocksDB has data, clear everything. - if checkpoint == 0 && self.first::()?.is_some() { + // Fast path: clear any stale data and return. + if checkpoint == 0 { tracing::info!( target: "reth::providers::rocksdb", - "TransactionHashNumbers has data but checkpoint is 0, clearing all" + "TransactionHashNumbers: checkpoint is 0, clearing stale data" ); self.clear::()?; return Ok(None); @@ -264,11 +264,11 @@ impl RocksDBProvider { .map(|cp| cp.block_number) .unwrap_or(0); - // Fast path: if checkpoint is 0 and RocksDB has data, clear everything. - if checkpoint == 0 && self.first::()?.is_some() { + // Fast path: clear any stale data and return. + if checkpoint == 0 { tracing::info!( target: "reth::providers::rocksdb", - "StoragesHistory has data but checkpoint is 0, clearing all" + "StoragesHistory: checkpoint is 0, clearing stale data" ); self.clear::()?; return Ok(None); @@ -358,11 +358,11 @@ impl RocksDBProvider { .map(|cp| cp.block_number) .unwrap_or(0); - // Fast path: if checkpoint is 0 and RocksDB has data, clear everything. - if checkpoint == 0 && self.first::()?.is_some() { + // Fast path: clear any stale data and return. + if checkpoint == 0 { tracing::info!( target: "reth::providers::rocksdb", - "AccountsHistory has data but checkpoint is 0, clearing all" + "AccountsHistory: checkpoint is 0, clearing stale data" ); self.clear::()?; return Ok(None); @@ -506,6 +506,35 @@ mod tests { assert_eq!(result, None); } + /// Tests that `checkpoint=0` with empty `RocksDB` returns early without attempting + /// an expensive healing loop. Previously, when `sf_tip` > `checkpoint=0`, the healer + /// would iterate billions of transactions from static files for no effect, causing + /// the node to hang on startup with MDBX read transaction timeouts. + #[test] + fn test_check_consistency_checkpoint_zero_empty_rocksdb_returns_early() { + let temp_dir = TempDir::new().unwrap(); + let rocksdb = RocksDBBuilder::new(temp_dir.path()).with_default_tables().build().unwrap(); + + let factory = create_test_provider_factory(); + factory.set_storage_settings_cache(StorageSettings::v2()); + + // No checkpoints set — all default to 0 via unwrap_or(0). + // RocksDB tables are empty. + let provider = factory.database_provider_ro().unwrap(); + + let result = rocksdb.heal_transaction_hash_numbers(&provider).unwrap(); + assert_eq!(result, None, "TransactionHashNumbers should return early at checkpoint 0"); + assert!(rocksdb.first::().unwrap().is_none()); + + let result = rocksdb.heal_storages_history(&provider).unwrap(); + assert_eq!(result, None, "StoragesHistory should return early at checkpoint 0"); + assert!(rocksdb.first::().unwrap().is_none()); + + let result = rocksdb.heal_accounts_history(&provider).unwrap(); + assert_eq!(result, None, "AccountsHistory should return early at checkpoint 0"); + assert!(rocksdb.first::().unwrap().is_none()); + } + #[test] fn test_check_consistency_empty_rocksdb_with_checkpoint_is_first_run() { let temp_dir = TempDir::new().unwrap();