1use super::RocksDBProvider;
8use crate::StaticFileProviderFactory;
9use alloy_primitives::BlockNumber;
10use reth_chainspec::{ChainSpecProvider, EthChainSpec};
11use reth_db::models::{storage_sharded_key::StorageShardedKey, ShardedKey};
12use reth_db_api::{table::Value, tables};
13use reth_primitives_traits::NodePrimitives;
14use reth_stages_types::StageId;
15use reth_static_file_types::StaticFileSegment;
16use reth_storage_api::{
17 BlockBodyIndicesProvider, ChangeSetReader, DBProvider, StageCheckpointReader,
18 StorageChangeSetReader, StorageSettingsCache, TransactionsProviderExt,
19};
20use reth_storage_errors::provider::ProviderResult;
21use std::collections::HashSet;
22
23const HEAL_HISTORY_BATCH_SIZE: u64 = 10_000;
26
27impl RocksDBProvider {
28 pub fn check_consistency<Provider>(
50 &self,
51 provider: &Provider,
52 ) -> ProviderResult<Option<BlockNumber>>
53 where
54 Provider: DBProvider
55 + StageCheckpointReader
56 + StorageSettingsCache
57 + BlockBodyIndicesProvider
58 + StorageChangeSetReader
59 + ChangeSetReader
60 + ChainSpecProvider
61 + StaticFileProviderFactory<
62 Primitives: NodePrimitives<SignedTx: Value, Receipt: Value, BlockHeader: Value>,
63 >,
64 {
65 let mut unwind_target: Option<BlockNumber> = None;
66
67 if provider.cached_storage_settings().storage_v2 &&
69 let Some(target) = self.heal_transaction_hash_numbers(provider)?
70 {
71 unwind_target = Some(unwind_target.map_or(target, |t| t.min(target)));
72 }
73
74 if provider.cached_storage_settings().storage_v2 &&
76 let Some(target) = self.heal_storages_history(provider)?
77 {
78 unwind_target = Some(unwind_target.map_or(target, |t| t.min(target)));
79 }
80
81 if provider.cached_storage_settings().storage_v2 &&
83 let Some(target) = self.heal_accounts_history(provider)?
84 {
85 unwind_target = Some(unwind_target.map_or(target, |t| t.min(target)));
86 }
87
88 Ok(unwind_target)
89 }
90
91 fn heal_transaction_hash_numbers<Provider>(
98 &self,
99 provider: &Provider,
100 ) -> ProviderResult<Option<BlockNumber>>
101 where
102 Provider: DBProvider
103 + StageCheckpointReader
104 + BlockBodyIndicesProvider
105 + StaticFileProviderFactory<
106 Primitives: NodePrimitives<SignedTx: Value, Receipt: Value, BlockHeader: Value>,
107 >,
108 {
109 let checkpoint = provider
110 .get_stage_checkpoint(StageId::TransactionLookup)?
111 .map(|cp| cp.block_number)
112 .unwrap_or(0);
113
114 let sf_tip = provider
115 .static_file_provider()
116 .get_highest_static_file_block(StaticFileSegment::Transactions)
117 .unwrap_or(0);
118
119 if checkpoint == 0 {
121 if self.first::<tables::TransactionHashNumbers>()?.is_some() {
122 tracing::info!(
123 target: "reth::providers::rocksdb",
124 "TransactionHashNumbers: checkpoint is 0, clearing stale data"
125 );
126 self.clear::<tables::TransactionHashNumbers>()?;
127 }
128
129 return Ok(None);
130 }
131
132 if sf_tip < checkpoint {
133 tracing::warn!(
137 target: "reth::providers::rocksdb",
138 sf_tip,
139 checkpoint,
140 "TransactionHashNumbers: static file tip behind checkpoint, unwind needed"
141 );
142 return Ok(Some(sf_tip));
143 }
144
145 if sf_tip == checkpoint {
147 return Ok(None);
148 }
149
150 let sf_tip_end_tx = provider
152 .static_file_provider()
153 .get_highest_static_file_tx(StaticFileSegment::Transactions)
154 .unwrap_or(0);
155
156 let checkpoint_next_tx = provider
158 .block_body_indices(checkpoint)?
159 .map(|indices| indices.next_tx_num())
160 .unwrap_or(0);
161
162 if sf_tip_end_tx < checkpoint_next_tx {
163 tracing::warn!(
167 target: "reth::providers::rocksdb",
168 sf_tip_end_tx,
169 checkpoint_next_tx,
170 checkpoint,
171 sf_tip,
172 "TransactionHashNumbers: static file tx tip behind checkpoint, unwind needed"
173 );
174 return Ok(Some(sf_tip));
175 }
176
177 tracing::info!(
178 target: "reth::providers::rocksdb",
179 checkpoint,
180 sf_tip,
181 checkpoint_next_tx,
182 sf_tip_end_tx,
183 "TransactionHashNumbers: healing via transaction ranges"
184 );
185
186 const BATCH_SIZE: u64 = 10_000;
187 let mut batch_start = checkpoint_next_tx;
188
189 while batch_start <= sf_tip_end_tx {
190 let batch_end = batch_start.saturating_add(BATCH_SIZE - 1).min(sf_tip_end_tx);
191
192 tracing::debug!(
193 target: "reth::providers::rocksdb",
194 batch_start,
195 batch_end,
196 "Pruning TransactionHashNumbers batch"
197 );
198
199 self.prune_transaction_hash_numbers_in_range(provider, batch_start..=batch_end)?;
200
201 batch_start = batch_end.saturating_add(1);
202 }
203
204 Ok(None)
205 }
206
207 fn prune_transaction_hash_numbers_in_range<Provider>(
220 &self,
221 provider: &Provider,
222 tx_range: std::ops::RangeInclusive<u64>,
223 ) -> ProviderResult<()>
224 where
225 Provider: StaticFileProviderFactory<
226 Primitives: NodePrimitives<SignedTx: Value, Receipt: Value, BlockHeader: Value>,
227 >,
228 {
229 if tx_range.is_empty() {
230 return Ok(());
231 }
232
233 let hashes = provider
234 .static_file_provider()
235 .transaction_hashes_by_range(*tx_range.start()..tx_range.end().saturating_add(1))?;
236
237 if !hashes.is_empty() {
238 tracing::info!(
239 target: "reth::providers::rocksdb",
240 deleted_count = hashes.len(),
241 tx_range_start = *tx_range.start(),
242 tx_range_end = *tx_range.end(),
243 "Pruning TransactionHashNumbers entries by tx range"
244 );
245
246 let mut batch = self.batch();
247 for (hash, _) in hashes {
248 batch.delete::<tables::TransactionHashNumbers>(hash)?;
249 }
250 batch.commit()?;
251 }
252
253 Ok(())
254 }
255
256 fn heal_storages_history<Provider>(
261 &self,
262 provider: &Provider,
263 ) -> ProviderResult<Option<BlockNumber>>
264 where
265 Provider: DBProvider
266 + StageCheckpointReader
267 + StaticFileProviderFactory
268 + StorageChangeSetReader
269 + ChainSpecProvider,
270 {
271 let checkpoint = provider
272 .get_stage_checkpoint(StageId::IndexStorageHistory)?
273 .map(|cp| cp.block_number)
274 .unwrap_or(0);
275
276 let sf_tip = provider
277 .static_file_provider()
278 .get_highest_static_file_block(StaticFileSegment::StorageChangeSets)
279 .unwrap_or(0);
280
281 if sf_tip < checkpoint {
282 tracing::warn!(
286 target: "reth::providers::rocksdb",
287 sf_tip,
288 checkpoint,
289 "StoragesHistory: static file tip behind checkpoint, unwind needed"
290 );
291 return Ok(Some(sf_tip));
292 }
293
294 if sf_tip == checkpoint {
295 return Ok(None);
296 }
297
298 if checkpoint == 0 {
300 tracing::info!(
301 target: "reth::providers::rocksdb",
302 "StoragesHistory: checkpoint is 0, clearing stale data"
303 );
304 self.clear::<tables::StoragesHistory>()?;
305
306 let chain_spec = provider.chain_spec();
307 let genesis = chain_spec.genesis();
308 let list = tables::BlockNumberList::new([0]).expect("single block always fits");
309 for (addr, account) in &genesis.alloc {
310 if let Some(storage) = &account.storage {
311 for key in storage.keys() {
312 self.put::<tables::StoragesHistory>(
313 StorageShardedKey::last(*addr, *key),
314 &list,
315 )?;
316 }
317 }
318 }
319
320 return Ok(None);
321 }
322
323 let total_blocks = sf_tip - checkpoint;
324 tracing::info!(
325 target: "reth::providers::rocksdb",
326 checkpoint,
327 sf_tip,
328 total_blocks,
329 "StoragesHistory: healing via changesets"
330 );
331
332 let mut batch_start = checkpoint + 1;
333 let mut batch_num = 0u64;
334 let total_batches = total_blocks.div_ceil(HEAL_HISTORY_BATCH_SIZE);
335
336 while batch_start <= sf_tip {
337 let batch_end = (batch_start + HEAL_HISTORY_BATCH_SIZE - 1).min(sf_tip);
338 batch_num += 1;
339
340 let changesets = provider.storage_changesets_range(batch_start..=batch_end)?;
341
342 let unique_keys: HashSet<_> = changesets
343 .into_iter()
344 .map(|(block_addr, entry)| (block_addr.address(), entry.key, checkpoint + 1))
345 .collect();
346 let indices: Vec<_> = unique_keys.into_iter().collect();
347
348 if !indices.is_empty() {
349 tracing::info!(
350 target: "reth::providers::rocksdb",
351 batch_num,
352 total_batches,
353 batch_start,
354 batch_end,
355 indices_count = indices.len(),
356 "StoragesHistory: unwinding batch"
357 );
358
359 let batch = self.unwind_storage_history_indices(&indices)?;
360 self.commit_batch(batch)?;
361 }
362
363 batch_start = batch_end + 1;
364 }
365
366 Ok(None)
367 }
368
369 fn heal_accounts_history<Provider>(
374 &self,
375 provider: &Provider,
376 ) -> ProviderResult<Option<BlockNumber>>
377 where
378 Provider: DBProvider
379 + StageCheckpointReader
380 + StaticFileProviderFactory
381 + ChangeSetReader
382 + ChainSpecProvider,
383 {
384 let checkpoint = provider
385 .get_stage_checkpoint(StageId::IndexAccountHistory)?
386 .map(|cp| cp.block_number)
387 .unwrap_or(0);
388
389 let sf_tip = provider
390 .static_file_provider()
391 .get_highest_static_file_block(StaticFileSegment::AccountChangeSets)
392 .unwrap_or(0);
393
394 if sf_tip < checkpoint {
395 tracing::warn!(
399 target: "reth::providers::rocksdb",
400 sf_tip,
401 checkpoint,
402 "AccountsHistory: static file tip behind checkpoint, unwind needed"
403 );
404 return Ok(Some(sf_tip));
405 }
406
407 if sf_tip == checkpoint {
408 return Ok(None);
409 }
410
411 if checkpoint == 0 {
413 tracing::info!(
414 target: "reth::providers::rocksdb",
415 "AccountsHistory: checkpoint is 0, clearing stale data"
416 );
417 self.clear::<tables::AccountsHistory>()?;
418
419 let chain_spec = provider.chain_spec();
420 let genesis = chain_spec.genesis();
421 let list = tables::BlockNumberList::new([0]).expect("single block always fits");
422 for addr in genesis.alloc.keys() {
423 self.put::<tables::AccountsHistory>(ShardedKey::last(*addr), &list)?;
424 }
425
426 return Ok(None);
427 }
428
429 let total_blocks = sf_tip - checkpoint;
430 tracing::info!(
431 target: "reth::providers::rocksdb",
432 checkpoint,
433 sf_tip,
434 total_blocks,
435 "AccountsHistory: healing via changesets"
436 );
437
438 let mut batch_start = checkpoint + 1;
439 let mut batch_num = 0u64;
440 let total_batches = total_blocks.div_ceil(HEAL_HISTORY_BATCH_SIZE);
441
442 while batch_start <= sf_tip {
443 let batch_end = (batch_start + HEAL_HISTORY_BATCH_SIZE - 1).min(sf_tip);
444 batch_num += 1;
445
446 let changesets = provider.account_changesets_range(batch_start..=batch_end)?;
447
448 let mut addresses = HashSet::with_capacity(changesets.len());
449 addresses.extend(changesets.iter().map(|(_, cs)| cs.address));
450 let unwind_from = checkpoint + 1;
451 let indices: Vec<_> = addresses.into_iter().map(|addr| (addr, unwind_from)).collect();
452
453 if !indices.is_empty() {
454 tracing::info!(
455 target: "reth::providers::rocksdb",
456 batch_num,
457 total_batches,
458 batch_start,
459 batch_end,
460 indices_count = indices.len(),
461 "AccountsHistory: unwinding batch"
462 );
463
464 let batch = self.unwind_account_history_indices(&indices)?;
465 self.commit_batch(batch)?;
466 }
467
468 batch_start = batch_end + 1;
469 }
470
471 Ok(None)
472 }
473}
474
475#[cfg(test)]
476mod tests {
477 use std::sync::Arc;
478
479 use super::*;
480 use crate::{
481 init::insert_genesis_history,
482 providers::{rocksdb::RocksDBBuilder, static_file::StaticFileWriter},
483 test_utils::{create_test_provider_factory, create_test_provider_factory_with_chain_spec},
484 BlockWriter, DatabaseProviderFactory, RocksDBProviderFactory, StageCheckpointWriter,
485 TransactionsProvider,
486 };
487 use alloy_primitives::{Address, B256, U256};
488 use reth_chainspec::MAINNET;
489 use reth_db::{
490 cursor::{DbCursorRO, DbCursorRW},
491 models::AccountBeforeTx,
492 };
493 use reth_db_api::{
494 models::{
495 storage_sharded_key::StorageShardedKey, ShardedKey, StorageBeforeTx, StorageSettings,
496 },
497 tables::{self, BlockNumberList},
498 transaction::DbTxMut,
499 };
500 use reth_stages_types::StageCheckpoint;
501 use reth_static_file_types::StaticFileSegment;
502 use reth_testing_utils::generators::{self, BlockRangeParams};
503 use tempfile::TempDir;
504
505 #[test]
506 fn test_first_last_empty_rocksdb() {
507 let temp_dir = TempDir::new().unwrap();
508 let provider = RocksDBBuilder::new(temp_dir.path())
509 .with_table::<tables::TransactionHashNumbers>()
510 .with_table::<tables::StoragesHistory>()
511 .build()
512 .unwrap();
513
514 let first = provider.first::<tables::TransactionHashNumbers>().unwrap();
516 let last = provider.last::<tables::TransactionHashNumbers>().unwrap();
517
518 assert!(first.is_none());
519 assert!(last.is_none());
520 }
521
522 #[test]
523 fn test_first_last_with_data() {
524 let temp_dir = TempDir::new().unwrap();
525 let provider = RocksDBBuilder::new(temp_dir.path())
526 .with_table::<tables::TransactionHashNumbers>()
527 .build()
528 .unwrap();
529
530 let tx_hash = B256::repeat_byte(1u8);
532 provider.put::<tables::TransactionHashNumbers>(tx_hash, &100).unwrap();
533
534 let last = provider.last::<tables::TransactionHashNumbers>().unwrap();
536 assert!(last.is_some());
537 assert_eq!(last.unwrap().1, 100);
538 }
539
540 #[test]
541 fn test_check_consistency_empty_rocksdb_no_checkpoint_is_ok() {
542 let temp_dir = TempDir::new().unwrap();
543 let rocksdb = RocksDBBuilder::new(temp_dir.path()).with_default_tables().build().unwrap();
544
545 let factory = create_test_provider_factory();
547 factory.set_storage_settings_cache(StorageSettings::v2());
548
549 let provider = factory.database_provider_ro().unwrap();
550
551 let result = rocksdb.check_consistency(&provider).unwrap();
553 assert_eq!(result, None);
554 }
555
556 #[test]
561 fn test_check_consistency_checkpoint_zero_empty_rocksdb_returns_early() {
562 let temp_dir = TempDir::new().unwrap();
563 let rocksdb = RocksDBBuilder::new(temp_dir.path()).with_default_tables().build().unwrap();
564
565 let factory = create_test_provider_factory();
566 factory.set_storage_settings_cache(StorageSettings::v2());
567
568 let provider = factory.database_provider_ro().unwrap();
571
572 let result = rocksdb.heal_transaction_hash_numbers(&provider).unwrap();
573 assert_eq!(result, None, "TransactionHashNumbers should return early at checkpoint 0");
574 assert!(rocksdb.first::<tables::TransactionHashNumbers>().unwrap().is_none());
575
576 let result = rocksdb.heal_storages_history(&provider).unwrap();
577 assert_eq!(result, None, "StoragesHistory should return early at checkpoint 0");
578
579 let result = rocksdb.heal_accounts_history(&provider).unwrap();
580 assert_eq!(result, None, "AccountsHistory should return early at checkpoint 0");
581 assert_eq!(rocksdb.iter::<tables::AccountsHistory>().unwrap().count(), 0);
582 }
583
584 #[test]
585 fn test_check_consistency_empty_rocksdb_with_checkpoint_is_first_run() {
586 let temp_dir = TempDir::new().unwrap();
587 let rocksdb = RocksDBBuilder::new(temp_dir.path()).with_default_tables().build().unwrap();
588
589 let factory = create_test_provider_factory();
591 factory.set_storage_settings_cache(StorageSettings::v2());
592
593 {
595 let provider = factory.database_provider_rw().unwrap();
596 provider
597 .save_stage_checkpoint(StageId::TransactionLookup, StageCheckpoint::new(100))
598 .unwrap();
599 provider.commit().unwrap();
600 }
601
602 let provider = factory.database_provider_ro().unwrap();
603
604 let result = rocksdb.check_consistency(&provider).unwrap();
607 assert_eq!(result, Some(0), "Static file tip (0) behind checkpoint (100) triggers unwind");
608 }
609
610 #[test]
613 fn test_check_consistency_checkpoint_zero_with_rocksdb_data_prunes_all() {
614 let temp_dir = TempDir::new().unwrap();
615 let rocksdb = RocksDBBuilder::new(temp_dir.path()).with_default_tables().build().unwrap();
616
617 let factory = create_test_provider_factory();
618 factory.set_storage_settings_cache(StorageSettings::v2());
619
620 let mut rng = generators::rng();
622 let blocks = generators::random_block_range(
623 &mut rng,
624 0..=2,
625 BlockRangeParams { parent: Some(B256::ZERO), tx_count: 2..3, ..Default::default() },
626 );
627
628 let mut tx_hashes = Vec::new();
629 {
630 let provider = factory.database_provider_rw().unwrap();
631 let mut tx_count = 0u64;
632 for block in &blocks {
633 provider
634 .insert_block(&block.clone().try_recover().expect("recover block"))
635 .unwrap();
636 for tx in &block.body().transactions {
637 let hash = *tx.tx_hash();
638 tx_hashes.push(hash);
639 rocksdb.put::<tables::TransactionHashNumbers>(hash, &tx_count).unwrap();
640 tx_count += 1;
641 }
642 }
643 provider.commit().unwrap();
644 }
645
646 {
648 let provider = factory.database_provider_rw().unwrap();
649 provider
650 .save_stage_checkpoint(StageId::TransactionLookup, StageCheckpoint::new(0))
651 .unwrap();
652 provider
653 .save_stage_checkpoint(StageId::IndexStorageHistory, StageCheckpoint::new(0))
654 .unwrap();
655 provider
656 .save_stage_checkpoint(StageId::IndexAccountHistory, StageCheckpoint::new(0))
657 .unwrap();
658 provider.commit().unwrap();
659 }
660
661 assert!(rocksdb.last::<tables::TransactionHashNumbers>().unwrap().is_some());
663
664 let provider = factory.database_provider_ro().unwrap();
665
666 let result = rocksdb.check_consistency(&provider).unwrap();
669 assert_eq!(result, None, "Should heal by clearing, no unwind needed");
670
671 for hash in &tx_hashes {
673 assert!(
674 rocksdb.get::<tables::TransactionHashNumbers>(*hash).unwrap().is_none(),
675 "RocksDB should be empty after pruning"
676 );
677 }
678 }
679
680 #[test]
681 fn test_check_consistency_storages_history_empty_with_checkpoint_is_first_run() {
682 let temp_dir = TempDir::new().unwrap();
683 let rocksdb = RocksDBBuilder::new(temp_dir.path()).with_default_tables().build().unwrap();
684
685 let factory = create_test_provider_factory();
687 factory.set_storage_settings_cache(StorageSettings::v2());
688
689 {
691 let provider = factory.database_provider_rw().unwrap();
692 provider
693 .save_stage_checkpoint(StageId::IndexStorageHistory, StageCheckpoint::new(100))
694 .unwrap();
695 provider.commit().unwrap();
696 }
697
698 let provider = factory.database_provider_ro().unwrap();
699
700 let result = rocksdb.check_consistency(&provider).unwrap();
704 assert_eq!(result, Some(0), "sf_tip=0 < checkpoint=100 returns unwind target");
705 }
706
707 #[test]
708 fn test_check_consistency_storages_history_preserves_genesis_entries_at_checkpoint_zero(
709 ) -> eyre::Result<()> {
710 let mut chain_spec = MAINNET.clone();
712 Arc::make_mut(&mut chain_spec).genesis.alloc.first_entry().unwrap().get_mut().storage =
713 Some(From::from([(B256::random(), B256::random())]));
714
715 let factory = create_test_provider_factory_with_chain_spec(chain_spec);
717 let rocksdb = factory.rocksdb_provider();
718 factory.set_storage_settings_cache(StorageSettings::v2());
719
720 let provider_rw = factory.database_provider_rw().unwrap();
722 insert_genesis_history(&provider_rw, factory.chain_spec().genesis.alloc.iter())?;
723 provider_rw.commit()?;
724
725 let provider = factory.database_provider_ro().unwrap();
726
727 let result = rocksdb.heal_storages_history(&provider).unwrap();
729 assert_eq!(result, None, "Should skip healing when only genesis entries present");
730
731 assert!(
733 rocksdb.iter::<tables::StoragesHistory>().unwrap().count() > 0,
734 "Genesis entries should be preserved"
735 );
736
737 Ok(())
738 }
739
740 #[test]
741 fn test_check_consistency_mdbx_behind_checkpoint_needs_unwind() {
742 let temp_dir = TempDir::new().unwrap();
743 let rocksdb = RocksDBBuilder::new(temp_dir.path()).with_default_tables().build().unwrap();
744
745 let factory = create_test_provider_factory();
746 factory.set_storage_settings_cache(StorageSettings::v2());
747
748 let mut rng = generators::rng();
750 let blocks = generators::random_block_range(
751 &mut rng,
752 0..=2,
753 BlockRangeParams { parent: Some(B256::ZERO), tx_count: 2..3, ..Default::default() },
754 );
755
756 {
757 let provider = factory.database_provider_rw().unwrap();
758 let mut tx_count = 0u64;
759 for block in &blocks {
760 provider
761 .insert_block(&block.clone().try_recover().expect("recover block"))
762 .unwrap();
763 for tx in &block.body().transactions {
764 let hash = *tx.tx_hash();
765 rocksdb.put::<tables::TransactionHashNumbers>(hash, &tx_count).unwrap();
766 tx_count += 1;
767 }
768 }
769 provider.commit().unwrap();
770 }
771
772 {
776 let provider = factory.database_provider_rw().unwrap();
777 provider
778 .save_stage_checkpoint(StageId::TransactionLookup, StageCheckpoint::new(10))
779 .unwrap();
780 provider
782 .save_stage_checkpoint(StageId::IndexStorageHistory, StageCheckpoint::new(0))
783 .unwrap();
784 provider
785 .save_stage_checkpoint(StageId::IndexAccountHistory, StageCheckpoint::new(0))
786 .unwrap();
787 provider.commit().unwrap();
788 }
789
790 let provider = factory.database_provider_ro().unwrap();
791
792 let result = rocksdb.check_consistency(&provider).unwrap();
794 assert_eq!(result, Some(2), "sf_tip < checkpoint requires unwind to sf_tip");
795 }
796
797 #[test]
798 fn test_check_consistency_rocksdb_ahead_of_checkpoint_prunes_excess() {
799 let temp_dir = TempDir::new().unwrap();
800 let rocksdb = RocksDBBuilder::new(temp_dir.path()).with_default_tables().build().unwrap();
801
802 let factory = create_test_provider_factory();
804 factory.set_storage_settings_cache(StorageSettings::v2());
805
806 let mut rng = generators::rng();
809 let blocks = generators::random_block_range(
810 &mut rng,
811 0..=5,
812 BlockRangeParams { parent: Some(B256::ZERO), tx_count: 2..3, ..Default::default() },
813 );
814
815 let mut tx_hashes = Vec::new();
817 let mut tx_count = 0u64;
818 {
819 let provider = factory.database_provider_rw().unwrap();
820 for block in &blocks {
822 provider
823 .insert_block(&block.clone().try_recover().expect("recover block"))
824 .unwrap();
825 for tx in &block.body().transactions {
826 let hash = *tx.tx_hash();
827 tx_hashes.push(hash);
828 rocksdb.put::<tables::TransactionHashNumbers>(hash, &tx_count).unwrap();
829 tx_count += 1;
830 }
831 }
832 provider.commit().unwrap();
833 }
834
835 {
839 let provider = factory.database_provider_rw().unwrap();
840 let mut cursor = provider.tx_ref().cursor_write::<tables::TransactionBlocks>().unwrap();
844 let mut to_delete = Vec::new();
846 let mut walker = cursor.walk(Some(0)).unwrap();
847 while let Some((tx_num, block_num)) = walker.next().transpose().unwrap() {
848 if block_num > 2 {
849 to_delete.push(tx_num);
850 }
851 }
852 drop(walker);
853 for tx_num in to_delete {
854 cursor.seek_exact(tx_num).unwrap();
855 cursor.delete_current().unwrap();
856 }
857
858 provider
860 .save_stage_checkpoint(StageId::TransactionLookup, StageCheckpoint::new(2))
861 .unwrap();
862 provider
864 .save_stage_checkpoint(StageId::IndexStorageHistory, StageCheckpoint::new(0))
865 .unwrap();
866 provider
867 .save_stage_checkpoint(StageId::IndexAccountHistory, StageCheckpoint::new(0))
868 .unwrap();
869 provider.commit().unwrap();
870 }
871
872 let provider = factory.database_provider_ro().unwrap();
873
874 let result = rocksdb.check_consistency(&provider).unwrap();
879 assert_eq!(result, None, "Should heal by pruning, no unwind needed");
880
881 for (i, hash) in tx_hashes.iter().take(6).enumerate() {
884 assert!(
885 rocksdb.get::<tables::TransactionHashNumbers>(*hash).unwrap().is_some(),
886 "tx {} should remain",
887 i
888 );
889 }
890 for (i, hash) in tx_hashes.iter().skip(6).enumerate() {
892 assert!(
893 rocksdb.get::<tables::TransactionHashNumbers>(*hash).unwrap().is_none(),
894 "tx {} should be pruned",
895 i + 6
896 );
897 }
898 }
899
900 #[test]
901 fn test_check_consistency_storages_history_sentinel_only_with_checkpoint_is_first_run() {
902 let temp_dir = TempDir::new().unwrap();
903 let rocksdb = RocksDBBuilder::new(temp_dir.path()).with_default_tables().build().unwrap();
904
905 let key_sentinel_1 = StorageShardedKey::new(Address::ZERO, B256::ZERO, u64::MAX);
908 let key_sentinel_2 = StorageShardedKey::new(Address::random(), B256::random(), u64::MAX);
909 let block_list = BlockNumberList::new_pre_sorted([10, 20, 30]);
910 rocksdb.put::<tables::StoragesHistory>(key_sentinel_1, &block_list).unwrap();
911 rocksdb.put::<tables::StoragesHistory>(key_sentinel_2, &block_list).unwrap();
912
913 assert!(rocksdb.first::<tables::StoragesHistory>().unwrap().is_some());
915
916 let factory = create_test_provider_factory();
918 factory.set_storage_settings_cache(StorageSettings::v2());
919
920 {
922 let provider = factory.database_provider_rw().unwrap();
923 provider
924 .save_stage_checkpoint(StageId::IndexStorageHistory, StageCheckpoint::new(100))
925 .unwrap();
926 provider.commit().unwrap();
927 }
928
929 let provider = factory.database_provider_ro().unwrap();
930
931 let result = rocksdb.check_consistency(&provider).unwrap();
935 assert_eq!(result, Some(0), "sf_tip=0 < checkpoint=100 returns unwind target");
936 }
937
938 #[test]
939 fn test_check_consistency_accounts_history_sentinel_only_with_checkpoint_is_first_run() {
940 let temp_dir = TempDir::new().unwrap();
941 let rocksdb = RocksDBBuilder::new(temp_dir.path()).with_default_tables().build().unwrap();
942
943 let key_sentinel_1 = ShardedKey::new(Address::ZERO, u64::MAX);
945 let key_sentinel_2 = ShardedKey::new(Address::random(), u64::MAX);
946 let block_list = BlockNumberList::new_pre_sorted([10, 20, 30]);
947 rocksdb.put::<tables::AccountsHistory>(key_sentinel_1, &block_list).unwrap();
948 rocksdb.put::<tables::AccountsHistory>(key_sentinel_2, &block_list).unwrap();
949
950 assert!(rocksdb.first::<tables::AccountsHistory>().unwrap().is_some());
952
953 let factory = create_test_provider_factory();
955 factory.set_storage_settings_cache(StorageSettings::v2());
956
957 {
959 let provider = factory.database_provider_rw().unwrap();
960 provider
961 .save_stage_checkpoint(StageId::IndexAccountHistory, StageCheckpoint::new(100))
962 .unwrap();
963 provider.commit().unwrap();
964 }
965
966 let provider = factory.database_provider_ro().unwrap();
967
968 let result = rocksdb.check_consistency(&provider).unwrap();
972 assert_eq!(result, Some(0), "sf_tip=0 < checkpoint=100 returns unwind target");
973 }
974
975 #[test]
979 fn test_prune_transaction_hash_numbers_by_range() {
980 let temp_dir = TempDir::new().unwrap();
981 let rocksdb = RocksDBBuilder::new(temp_dir.path())
982 .with_table::<tables::TransactionHashNumbers>()
983 .build()
984 .unwrap();
985
986 let factory = create_test_provider_factory();
988 factory.set_storage_settings_cache(StorageSettings::v2());
989
990 let mut rng = generators::rng();
994 let blocks = generators::random_block_range(
995 &mut rng,
996 0..=5,
997 BlockRangeParams { parent: Some(B256::ZERO), tx_count: 2..3, ..Default::default() },
998 );
999
1000 let mut tx_count = 0u64;
1002 let mut tx_hashes = Vec::new();
1003 {
1004 let provider = factory.database_provider_rw().unwrap();
1005
1006 for block in &blocks {
1007 provider
1008 .insert_block(&block.clone().try_recover().expect("recover block"))
1009 .unwrap();
1010
1011 for tx in &block.body().transactions {
1013 let hash = *tx.tx_hash();
1014 tx_hashes.push(hash);
1015 rocksdb.put::<tables::TransactionHashNumbers>(hash, &tx_count).unwrap();
1016 tx_count += 1;
1017 }
1018 }
1019
1020 provider
1023 .save_stage_checkpoint(StageId::TransactionLookup, StageCheckpoint::new(2))
1024 .unwrap();
1025 provider.commit().unwrap();
1026 }
1027
1028 let rocksdb_count_before: usize =
1035 rocksdb.iter::<tables::TransactionHashNumbers>().unwrap().count();
1036 assert_eq!(
1037 rocksdb_count_before, tx_count as usize,
1038 "RocksDB should have all {} transaction hashes before pruning",
1039 tx_count
1040 );
1041
1042 let provider = factory.database_provider_ro().unwrap();
1043
1044 let all_txs = provider.transactions_by_tx_range(0..tx_count).unwrap();
1046 assert_eq!(all_txs.len(), tx_count as usize, "Should be able to fetch all transactions");
1047
1048 for (i, tx) in all_txs.iter().enumerate() {
1050 let fetched_hash = *tx.tx_hash();
1051 assert_eq!(
1052 fetched_hash, tx_hashes[i],
1053 "Hash mismatch for tx {}: stored {:?} vs fetched {:?}",
1054 i, tx_hashes[i], fetched_hash
1055 );
1056 }
1057
1058 let max_tx_to_keep = 5u64;
1061 let tx_to_prune_start = max_tx_to_keep + 1;
1062
1063 rocksdb
1065 .prune_transaction_hash_numbers_in_range(&provider, tx_to_prune_start..=(tx_count - 1))
1066 .expect("prune should succeed");
1067
1068 let mut remaining_count = 0;
1070 for result in rocksdb.iter::<tables::TransactionHashNumbers>().unwrap() {
1071 let (_hash, tx_num) = result.unwrap();
1072 assert!(
1073 tx_num <= max_tx_to_keep,
1074 "Transaction {} should have been pruned (> {})",
1075 tx_num,
1076 max_tx_to_keep
1077 );
1078 remaining_count += 1;
1079 }
1080 assert_eq!(
1081 remaining_count,
1082 (max_tx_to_keep + 1) as usize,
1083 "Should have {} transactions (0-{})",
1084 max_tx_to_keep + 1,
1085 max_tx_to_keep
1086 );
1087 }
1088
1089 #[test]
1090 fn test_check_consistency_accounts_history_empty_with_checkpoint_is_first_run() {
1091 let temp_dir = TempDir::new().unwrap();
1092 let rocksdb = RocksDBBuilder::new(temp_dir.path()).with_default_tables().build().unwrap();
1093
1094 let factory = create_test_provider_factory();
1096 factory.set_storage_settings_cache(StorageSettings::v2());
1097
1098 {
1100 let provider = factory.database_provider_rw().unwrap();
1101 provider
1102 .save_stage_checkpoint(StageId::IndexAccountHistory, StageCheckpoint::new(100))
1103 .unwrap();
1104 provider.commit().unwrap();
1105 }
1106
1107 let provider = factory.database_provider_ro().unwrap();
1108
1109 let result = rocksdb.check_consistency(&provider).unwrap();
1113 assert_eq!(result, Some(0), "sf_tip=0 < checkpoint=100 returns unwind target");
1114 }
1115
1116 #[test]
1117 fn test_check_consistency_accounts_history_preserves_genesis_entries_at_checkpoint_zero(
1118 ) -> eyre::Result<()> {
1119 let factory = create_test_provider_factory();
1121 factory.set_storage_settings_cache(StorageSettings::v2());
1122 let rocksdb = factory.rocksdb_provider();
1123
1124 let provider_rw = factory.database_provider_rw().unwrap();
1126 insert_genesis_history(&provider_rw, factory.chain_spec().genesis.alloc.iter())?;
1127 provider_rw.commit()?;
1128
1129 let provider = factory.database_provider_ro().unwrap();
1130
1131 let result = rocksdb.check_consistency(&provider).unwrap();
1133 assert_eq!(result, None, "Should heal by pruning, no unwind needed");
1134
1135 assert!(
1137 rocksdb.iter::<tables::AccountsHistory>().unwrap().count() > 0,
1138 "Genesis entries should be preserved"
1139 );
1140
1141 Ok(())
1142 }
1143
1144 #[test]
1145 fn test_check_consistency_accounts_history_sf_tip_equals_checkpoint_no_action() {
1146 let temp_dir = TempDir::new().unwrap();
1147 let rocksdb = RocksDBBuilder::new(temp_dir.path()).with_default_tables().build().unwrap();
1148
1149 let key1 = ShardedKey::new(Address::ZERO, 50);
1151 let key2 = ShardedKey::new(Address::random(), 75);
1152 let key3 = ShardedKey::new(Address::random(), u64::MAX); let block_list1 = BlockNumberList::new_pre_sorted([10, 20, 30, 50]);
1154 let block_list2 = BlockNumberList::new_pre_sorted([40, 60, 75]);
1155 let block_list3 = BlockNumberList::new_pre_sorted([80, 90, 100]);
1156 rocksdb.put::<tables::AccountsHistory>(key1, &block_list1).unwrap();
1157 rocksdb.put::<tables::AccountsHistory>(key2, &block_list2).unwrap();
1158 rocksdb.put::<tables::AccountsHistory>(key3, &block_list3).unwrap();
1159
1160 let entries_before: Vec<_> =
1162 rocksdb.iter::<tables::AccountsHistory>().unwrap().map(|r| r.unwrap()).collect();
1163 assert_eq!(entries_before.len(), 3, "Should have 3 entries before check");
1164
1165 let factory = create_test_provider_factory();
1167 factory.set_storage_settings_cache(StorageSettings::v2());
1168
1169 {
1171 let sf_provider = factory.static_file_provider();
1172 let mut writer =
1173 sf_provider.latest_writer(StaticFileSegment::AccountChangeSets).unwrap();
1174
1175 for block_num in 0..=100 {
1176 let changeset = vec![AccountBeforeTx { address: Address::random(), info: None }];
1177 writer.append_account_changeset(changeset, block_num).unwrap();
1178 }
1179
1180 writer.commit().unwrap();
1181 }
1182
1183 {
1185 let provider = factory.database_provider_rw().unwrap();
1186 provider
1187 .save_stage_checkpoint(StageId::IndexAccountHistory, StageCheckpoint::new(100))
1188 .unwrap();
1189 provider.commit().unwrap();
1190 }
1191
1192 let provider = factory.database_provider_ro().unwrap();
1193
1194 let sf_tip = provider
1196 .static_file_provider()
1197 .get_highest_static_file_block(StaticFileSegment::AccountChangeSets)
1198 .unwrap();
1199 assert_eq!(sf_tip, 100, "Static file tip should be 100");
1200
1201 let result = rocksdb.check_consistency(&provider).unwrap();
1203 assert_eq!(result, None, "sf_tip == checkpoint should not require unwind");
1204
1205 let entries_after: Vec<_> =
1207 rocksdb.iter::<tables::AccountsHistory>().unwrap().map(|r| r.unwrap()).collect();
1208
1209 assert_eq!(
1210 entries_after.len(),
1211 entries_before.len(),
1212 "RocksDB entry count should be unchanged when sf_tip == checkpoint"
1213 );
1214
1215 for (before, after) in entries_before.iter().zip(entries_after.iter()) {
1217 assert_eq!(before.0.key, after.0.key, "Entry key should be unchanged");
1218 assert_eq!(
1219 before.0.highest_block_number, after.0.highest_block_number,
1220 "Entry highest_block_number should be unchanged"
1221 );
1222 assert_eq!(before.1, after.1, "Entry block list should be unchanged");
1223 }
1224 }
1225
1226 #[test]
1238 fn test_check_consistency_storages_history_heals_via_changesets_large_range() {
1239 const TOTAL_BLOCKS: u64 = 15_000;
1240 const CHECKPOINT_BLOCK: u64 = 5_000;
1241
1242 let temp_dir = TempDir::new().unwrap();
1243 let rocksdb = RocksDBBuilder::new(temp_dir.path()).with_default_tables().build().unwrap();
1244
1245 let factory = create_test_provider_factory();
1246 factory.set_storage_settings_cache(StorageSettings::v2());
1247
1248 #[inline]
1250 fn make_address(block_num: u64) -> Address {
1251 let mut addr_bytes = [0u8; 20];
1252 addr_bytes[0..8].copy_from_slice(&block_num.to_le_bytes());
1253 Address::from(addr_bytes)
1254 }
1255
1256 #[inline]
1258 fn make_slot(block_num: u64) -> B256 {
1259 let mut slot_bytes = [0u8; 32];
1260 slot_bytes[0..8].copy_from_slice(&block_num.to_le_bytes());
1261 B256::from(slot_bytes)
1262 }
1263
1264 {
1267 let sf_provider = factory.static_file_provider();
1268 let mut writer =
1269 sf_provider.latest_writer(StaticFileSegment::StorageChangeSets).unwrap();
1270
1271 let mut changeset = Vec::with_capacity(1);
1273
1274 for block_num in 0..TOTAL_BLOCKS {
1275 changeset.clear();
1276 changeset.push(StorageBeforeTx {
1277 address: make_address(block_num),
1278 key: make_slot(block_num),
1279 value: U256::from(block_num),
1280 });
1281
1282 writer.append_storage_changeset(changeset.clone(), block_num).unwrap();
1283 }
1284
1285 writer.commit().unwrap();
1286 }
1287
1288 {
1290 let sf_provider = factory.static_file_provider();
1291 let highest = sf_provider
1292 .get_highest_static_file_block(StaticFileSegment::StorageChangeSets)
1293 .unwrap();
1294 assert_eq!(highest, TOTAL_BLOCKS - 1, "Static files should have blocks 0..14999");
1295 }
1296
1297 {
1299 let provider = factory.database_provider_rw().unwrap();
1300 provider
1301 .save_stage_checkpoint(
1302 StageId::IndexStorageHistory,
1303 StageCheckpoint::new(CHECKPOINT_BLOCK),
1304 )
1305 .unwrap();
1306 provider.commit().unwrap();
1307 }
1308
1309 for block_num in (CHECKPOINT_BLOCK + 1)..TOTAL_BLOCKS {
1312 let key =
1313 StorageShardedKey::new(make_address(block_num), make_slot(block_num), block_num);
1314 let block_list = BlockNumberList::new_pre_sorted([block_num]);
1315 rocksdb.put::<tables::StoragesHistory>(key, &block_list).unwrap();
1316 }
1317
1318 let count_before: usize = rocksdb.iter::<tables::StoragesHistory>().unwrap().count();
1320 assert_eq!(
1321 count_before,
1322 (TOTAL_BLOCKS - CHECKPOINT_BLOCK - 1) as usize,
1323 "Should have {} stale entries before healing",
1324 TOTAL_BLOCKS - CHECKPOINT_BLOCK - 1
1325 );
1326
1327 let provider = factory.database_provider_ro().unwrap();
1329 let result = rocksdb.check_consistency(&provider).unwrap();
1330 assert_eq!(result, None, "Should heal via changesets, no unwind needed");
1331
1332 let mut remaining_stale = 0;
1335 for result in rocksdb.iter::<tables::StoragesHistory>().unwrap() {
1336 let (key, _) = result.unwrap();
1337 if key.sharded_key.highest_block_number > CHECKPOINT_BLOCK {
1338 remaining_stale += 1;
1339 }
1340 }
1341 assert_eq!(
1342 remaining_stale, 0,
1343 "All stale entries (block > {}) should be pruned",
1344 CHECKPOINT_BLOCK
1345 );
1346 }
1347
1348 #[test]
1352 fn test_check_consistency_storages_history_preserves_checkpoint_block() {
1353 const CHECKPOINT_BLOCK: u64 = 100;
1354 const SF_TIP: u64 = 200;
1355
1356 let temp_dir = TempDir::new().unwrap();
1357 let rocksdb = RocksDBBuilder::new(temp_dir.path()).with_default_tables().build().unwrap();
1358
1359 let factory = create_test_provider_factory();
1360 factory.set_storage_settings_cache(StorageSettings::v2());
1361
1362 let checkpoint_addr = Address::repeat_byte(0xAA);
1363 let checkpoint_slot = B256::repeat_byte(0xBB);
1364 let stale_addr = Address::repeat_byte(0xCC);
1365 let stale_slot = B256::repeat_byte(0xDD);
1366
1367 {
1369 let sf_provider = factory.static_file_provider();
1370 let mut writer =
1371 sf_provider.latest_writer(StaticFileSegment::StorageChangeSets).unwrap();
1372
1373 for block_num in 0..=SF_TIP {
1374 let changeset = if block_num == CHECKPOINT_BLOCK {
1375 vec![StorageBeforeTx {
1376 address: checkpoint_addr,
1377 key: checkpoint_slot,
1378 value: U256::from(block_num),
1379 }]
1380 } else if block_num > CHECKPOINT_BLOCK {
1381 vec![StorageBeforeTx {
1382 address: stale_addr,
1383 key: stale_slot,
1384 value: U256::from(block_num),
1385 }]
1386 } else {
1387 vec![StorageBeforeTx {
1388 address: Address::ZERO,
1389 key: B256::ZERO,
1390 value: U256::ZERO,
1391 }]
1392 };
1393 writer.append_storage_changeset(changeset, block_num).unwrap();
1394 }
1395 writer.commit().unwrap();
1396 }
1397
1398 {
1400 let provider = factory.database_provider_rw().unwrap();
1401 provider
1402 .save_stage_checkpoint(
1403 StageId::IndexStorageHistory,
1404 StageCheckpoint::new(CHECKPOINT_BLOCK),
1405 )
1406 .unwrap();
1407 provider.commit().unwrap();
1408 }
1409
1410 let checkpoint_key =
1412 StorageShardedKey::new(checkpoint_addr, checkpoint_slot, CHECKPOINT_BLOCK);
1413 let checkpoint_list = BlockNumberList::new_pre_sorted([CHECKPOINT_BLOCK]);
1414 rocksdb.put::<tables::StoragesHistory>(checkpoint_key.clone(), &checkpoint_list).unwrap();
1415
1416 let stale_key = StorageShardedKey::new(stale_addr, stale_slot, SF_TIP);
1418 let stale_list = BlockNumberList::new_pre_sorted([CHECKPOINT_BLOCK + 1, SF_TIP]);
1419 rocksdb.put::<tables::StoragesHistory>(stale_key.clone(), &stale_list).unwrap();
1420
1421 let provider = factory.database_provider_ro().unwrap();
1423 let result = rocksdb.check_consistency(&provider).unwrap();
1424 assert_eq!(result, None, "Should heal without unwind");
1425
1426 let preserved = rocksdb.get::<tables::StoragesHistory>(checkpoint_key).unwrap();
1428 assert!(preserved.is_some(), "Entry at checkpoint block should be preserved, not deleted");
1429
1430 let stale = rocksdb.get::<tables::StoragesHistory>(stale_key).unwrap();
1432 assert!(stale.is_none(), "Stale entry after checkpoint should be removed");
1433 }
1434
1435 #[test]
1449 fn test_check_consistency_accounts_history_heals_via_changesets_large_range() {
1450 let temp_dir = TempDir::new().unwrap();
1451 let rocksdb = RocksDBBuilder::new(temp_dir.path()).with_default_tables().build().unwrap();
1452
1453 let factory = create_test_provider_factory();
1455 factory.set_storage_settings_cache(StorageSettings::v2());
1456
1457 const TOTAL_BLOCKS: u64 = 15_000;
1458 const CHECKPOINT_BLOCK: u64 = 5_000;
1459
1460 #[inline]
1462 fn make_address(block_num: u64) -> Address {
1463 let mut addr = Address::ZERO;
1464 addr.0[0..8].copy_from_slice(&block_num.to_le_bytes());
1465 addr
1466 }
1467
1468 {
1470 let sf_provider = factory.static_file_provider();
1471 let mut writer =
1472 sf_provider.latest_writer(StaticFileSegment::AccountChangeSets).unwrap();
1473
1474 let mut changeset = Vec::with_capacity(1);
1476
1477 for block_num in 0..TOTAL_BLOCKS {
1478 changeset.clear();
1479 changeset.push(AccountBeforeTx { address: make_address(block_num), info: None });
1480 writer.append_account_changeset(changeset.clone(), block_num).unwrap();
1481 }
1482
1483 writer.commit().unwrap();
1484 }
1485
1486 for block_num in (CHECKPOINT_BLOCK + 1)..TOTAL_BLOCKS {
1490 let key = ShardedKey::new(make_address(block_num), block_num);
1491 let block_list = BlockNumberList::new_pre_sorted([block_num]);
1492 rocksdb.put::<tables::AccountsHistory>(key, &block_list).unwrap();
1493 }
1494
1495 for block_num in [100u64, 500, 1000, 2500, 5000] {
1497 let key = ShardedKey::new(make_address(block_num), block_num);
1498 let block_list = BlockNumberList::new_pre_sorted([block_num]);
1499 rocksdb.put::<tables::AccountsHistory>(key, &block_list).unwrap();
1500 }
1501
1502 let entries_before: usize = rocksdb.iter::<tables::AccountsHistory>().unwrap().count();
1504 let stale_count = (TOTAL_BLOCKS - CHECKPOINT_BLOCK - 1) as usize;
1505 let valid_count = 5usize;
1506 assert_eq!(
1507 entries_before,
1508 stale_count + valid_count,
1509 "Should have {} stale + {} valid entries before healing",
1510 stale_count,
1511 valid_count
1512 );
1513
1514 {
1516 let provider = factory.database_provider_rw().unwrap();
1517 provider
1518 .save_stage_checkpoint(
1519 StageId::IndexAccountHistory,
1520 StageCheckpoint::new(CHECKPOINT_BLOCK),
1521 )
1522 .unwrap();
1523 provider.commit().unwrap();
1524 }
1525
1526 let provider = factory.database_provider_ro().unwrap();
1527
1528 let sf_tip = provider
1530 .static_file_provider()
1531 .get_highest_static_file_block(StaticFileSegment::AccountChangeSets)
1532 .unwrap();
1533 assert_eq!(sf_tip, TOTAL_BLOCKS - 1, "Static file tip should be 14999");
1534 assert!(sf_tip > CHECKPOINT_BLOCK, "sf_tip should be > checkpoint to trigger healing");
1535
1536 let result = rocksdb.check_consistency(&provider).unwrap();
1538 assert_eq!(result, None, "Healing should succeed without requiring unwind");
1539
1540 let mut remaining_stale = 0;
1543 for result in rocksdb.iter::<tables::AccountsHistory>().unwrap() {
1544 let (key, _) = result.unwrap();
1545 if key.highest_block_number > CHECKPOINT_BLOCK && key.highest_block_number != u64::MAX {
1546 remaining_stale += 1;
1547 }
1548 }
1549 assert_eq!(
1550 remaining_stale, 0,
1551 "All stale entries (block > {}) should be pruned",
1552 CHECKPOINT_BLOCK
1553 );
1554 }
1555
1556 #[test]
1558 fn test_check_consistency_accounts_history_preserves_checkpoint_block() {
1559 const CHECKPOINT_BLOCK: u64 = 100;
1560 const SF_TIP: u64 = 200;
1561
1562 let temp_dir = TempDir::new().unwrap();
1563 let rocksdb = RocksDBBuilder::new(temp_dir.path()).with_default_tables().build().unwrap();
1564
1565 let factory = create_test_provider_factory();
1566 factory.set_storage_settings_cache(StorageSettings::v2());
1567
1568 let checkpoint_addr = Address::repeat_byte(0xAA);
1569 let stale_addr = Address::repeat_byte(0xCC);
1570
1571 {
1573 let sf_provider = factory.static_file_provider();
1574 let mut writer =
1575 sf_provider.latest_writer(StaticFileSegment::AccountChangeSets).unwrap();
1576
1577 for block_num in 0..=SF_TIP {
1578 let changeset = if block_num == CHECKPOINT_BLOCK {
1579 vec![AccountBeforeTx { address: checkpoint_addr, info: None }]
1580 } else if block_num > CHECKPOINT_BLOCK {
1581 vec![AccountBeforeTx { address: stale_addr, info: None }]
1582 } else {
1583 vec![AccountBeforeTx { address: Address::ZERO, info: None }]
1584 };
1585 writer.append_account_changeset(changeset, block_num).unwrap();
1586 }
1587 writer.commit().unwrap();
1588 }
1589
1590 {
1592 let provider = factory.database_provider_rw().unwrap();
1593 provider
1594 .save_stage_checkpoint(
1595 StageId::IndexAccountHistory,
1596 StageCheckpoint::new(CHECKPOINT_BLOCK),
1597 )
1598 .unwrap();
1599 provider.commit().unwrap();
1600 }
1601
1602 let checkpoint_key = ShardedKey::new(checkpoint_addr, CHECKPOINT_BLOCK);
1604 let checkpoint_list = BlockNumberList::new_pre_sorted([CHECKPOINT_BLOCK]);
1605 rocksdb.put::<tables::AccountsHistory>(checkpoint_key.clone(), &checkpoint_list).unwrap();
1606
1607 let stale_key = ShardedKey::new(stale_addr, SF_TIP);
1609 let stale_list = BlockNumberList::new_pre_sorted([CHECKPOINT_BLOCK + 1, SF_TIP]);
1610 rocksdb.put::<tables::AccountsHistory>(stale_key.clone(), &stale_list).unwrap();
1611
1612 let provider = factory.database_provider_ro().unwrap();
1614 let result = rocksdb.check_consistency(&provider).unwrap();
1615 assert_eq!(result, None, "Should heal without unwind");
1616
1617 let preserved = rocksdb.get::<tables::AccountsHistory>(checkpoint_key).unwrap();
1619 assert!(preserved.is_some(), "Entry at checkpoint block should be preserved, not deleted");
1620
1621 let stale = rocksdb.get::<tables::AccountsHistory>(stale_key).unwrap();
1623 assert!(stale.is_none(), "Stale entry after checkpoint should be removed");
1624 }
1625
1626 #[test]
1627 fn test_check_consistency_storages_history_sf_tip_equals_checkpoint_no_action() {
1628 let temp_dir = TempDir::new().unwrap();
1629 let rocksdb = RocksDBBuilder::new(temp_dir.path()).with_default_tables().build().unwrap();
1630
1631 let key1 = StorageShardedKey::new(Address::ZERO, B256::ZERO, 50);
1633 let key2 = StorageShardedKey::new(Address::random(), B256::random(), 80);
1634 let block_list1 = BlockNumberList::new_pre_sorted([10, 20, 30, 50]);
1635 let block_list2 = BlockNumberList::new_pre_sorted([40, 60, 80]);
1636 rocksdb.put::<tables::StoragesHistory>(key1, &block_list1).unwrap();
1637 rocksdb.put::<tables::StoragesHistory>(key2, &block_list2).unwrap();
1638
1639 let entries_before: Vec<_> =
1641 rocksdb.iter::<tables::StoragesHistory>().unwrap().map(|r| r.unwrap()).collect();
1642
1643 let factory = create_test_provider_factory();
1645 factory.set_storage_settings_cache(StorageSettings::v2());
1646
1647 {
1649 let sf_provider = factory.static_file_provider();
1650 let mut writer =
1651 sf_provider.latest_writer(StaticFileSegment::StorageChangeSets).unwrap();
1652
1653 for block_num in 0..=100u64 {
1654 let changeset = vec![StorageBeforeTx {
1655 address: Address::ZERO,
1656 key: B256::with_last_byte(block_num as u8),
1657 value: U256::from(block_num),
1658 }];
1659 writer.append_storage_changeset(changeset, block_num).unwrap();
1660 }
1661 writer.commit().unwrap();
1662 }
1663
1664 {
1666 let provider = factory.database_provider_rw().unwrap();
1667 provider
1668 .save_stage_checkpoint(StageId::IndexStorageHistory, StageCheckpoint::new(100))
1669 .unwrap();
1670 provider.commit().unwrap();
1671 }
1672
1673 let provider = factory.database_provider_ro().unwrap();
1674
1675 let sf_tip = provider
1677 .static_file_provider()
1678 .get_highest_static_file_block(StaticFileSegment::StorageChangeSets)
1679 .unwrap();
1680 assert_eq!(sf_tip, 100, "Static file tip should be 100");
1681
1682 let result = rocksdb.check_consistency(&provider).unwrap();
1684 assert_eq!(result, None, "sf_tip == checkpoint should not require unwind");
1685
1686 let entries_after: Vec<_> =
1688 rocksdb.iter::<tables::StoragesHistory>().unwrap().map(|r| r.unwrap()).collect();
1689
1690 assert_eq!(
1691 entries_after.len(),
1692 entries_before.len(),
1693 "RocksDB entry count should be unchanged when sf_tip == checkpoint"
1694 );
1695
1696 for (before, after) in entries_before.iter().zip(entries_after.iter()) {
1698 assert_eq!(before.0, after.0, "Entry key should be unchanged");
1699 assert_eq!(before.1, after.1, "Entry block list should be unchanged");
1700 }
1701 }
1702}