1use crate::{
2 db_ext::DbTxPruneExt,
3 segments::{
4 user::history::{finalize_history_prune, HistoryPruneResult},
5 PruneInput, Segment,
6 },
7 PrunerError,
8};
9use alloy_primitives::{map::HashMap, Address, BlockNumber, B256};
10use reth_db_api::{
11 models::{storage_sharded_key::StorageShardedKey, BlockNumberAddress},
12 tables,
13 transaction::DbTxMut,
14};
15use reth_provider::{DBProvider, EitherWriter, RocksDBProviderFactory, StaticFileProviderFactory};
16use reth_prune_types::{
17 PruneMode, PrunePurpose, PruneSegment, SegmentOutput, SegmentOutputCheckpoint,
18};
19use reth_static_file_types::StaticFileSegment;
20use reth_storage_api::{StorageChangeSetReader, StorageSettingsCache};
21use tracing::{instrument, trace};
22
23const STORAGE_HISTORY_TABLES_TO_PRUNE: usize = 2;
28
29#[derive(Debug)]
30pub struct StorageHistory {
31 mode: PruneMode,
32}
33
34impl StorageHistory {
35 pub const fn new(mode: PruneMode) -> Self {
36 Self { mode }
37 }
38}
39
40impl<Provider> Segment<Provider> for StorageHistory
41where
42 Provider: DBProvider<Tx: DbTxMut>
43 + StaticFileProviderFactory
44 + StorageChangeSetReader
45 + StorageSettingsCache
46 + RocksDBProviderFactory,
47{
48 fn segment(&self) -> PruneSegment {
49 PruneSegment::StorageHistory
50 }
51
52 fn mode(&self) -> Option<PruneMode> {
53 Some(self.mode)
54 }
55
56 fn purpose(&self) -> PrunePurpose {
57 PrunePurpose::User
58 }
59
60 #[instrument(
61 name = "StorageHistory::prune",
62 target = "pruner",
63 skip(self, provider),
64 ret(level = "trace")
65 )]
66 fn prune(&self, provider: &Provider, input: PruneInput) -> Result<SegmentOutput, PrunerError> {
67 let range = match input.get_next_block_range() {
68 Some(range) => range,
69 None => {
70 trace!(target: "pruner", "No storage history to prune");
71 return Ok(SegmentOutput::done())
72 }
73 };
74 let range_end = *range.end();
75
76 if provider.cached_storage_settings().storage_v2 {
78 return self.prune_rocksdb(provider, input, range, range_end);
79 }
80
81 if EitherWriter::storage_changesets_destination(provider).is_static_file() {
83 self.prune_static_files(provider, input, range, range_end)
84 } else {
85 self.prune_database(provider, input, range, range_end)
86 }
87 }
88}
89
90impl StorageHistory {
91 fn prune_static_files<Provider>(
93 &self,
94 provider: &Provider,
95 input: PruneInput,
96 range: std::ops::RangeInclusive<BlockNumber>,
97 range_end: BlockNumber,
98 ) -> Result<SegmentOutput, PrunerError>
99 where
100 Provider: DBProvider<Tx: DbTxMut> + StaticFileProviderFactory,
101 {
102 let mut limiter = if let Some(limit) = input.limiter.deleted_entries_limit() {
103 input.limiter.set_deleted_entries_limit(limit / STORAGE_HISTORY_TABLES_TO_PRUNE)
104 } else {
105 input.limiter
106 };
107
108 if limiter.is_limit_reached() {
111 return Ok(SegmentOutput::not_done(
112 limiter.interrupt_reason(),
113 input.previous_checkpoint.map(SegmentOutputCheckpoint::from_prune_checkpoint),
114 ))
115 }
116
117 let mut highest_deleted_storages = HashMap::default();
123 let mut last_changeset_pruned_block = None;
124 let mut pruned_changesets = 0;
125 let mut done = true;
126
127 let walker = provider.static_file_provider().walk_storage_changeset_range(range);
128 for result in walker {
129 let (block_address, entry) = result?;
130 let block_number = block_address.block_number();
131 let address = block_address.address();
132 if limiter.is_limit_reached() &&
137 last_changeset_pruned_block.is_some_and(|last| last != block_number)
138 {
139 done = false;
140 break;
141 }
142 highest_deleted_storages.insert((address, entry.key), block_number);
143 last_changeset_pruned_block = Some(block_number);
144 pruned_changesets += 1;
145 limiter.increment_deleted_entries_count();
146 }
147
148 if done && let Some(last_block) = last_changeset_pruned_block {
150 provider
151 .static_file_provider()
152 .delete_segment_below_block(StaticFileSegment::StorageChangeSets, last_block + 1)?;
153 }
154 trace!(target: "pruner", pruned = %pruned_changesets, %done, "Pruned storage history (changesets from static files)");
155
156 let result = HistoryPruneResult {
157 highest_deleted: highest_deleted_storages,
158 last_pruned_block: last_changeset_pruned_block,
159 pruned_count: pruned_changesets,
160 done,
161 };
162 finalize_history_prune::<_, tables::StoragesHistory, (Address, B256), _>(
163 provider,
164 result,
165 range_end,
166 &limiter,
167 |(address, storage_key), block_number| {
168 StorageShardedKey::new(address, storage_key, block_number)
169 },
170 |a, b| a.address == b.address && a.sharded_key.key == b.sharded_key.key,
171 )
172 .map_err(Into::into)
173 }
174
175 fn prune_database<Provider>(
176 &self,
177 provider: &Provider,
178 input: PruneInput,
179 range: std::ops::RangeInclusive<BlockNumber>,
180 range_end: BlockNumber,
181 ) -> Result<SegmentOutput, PrunerError>
182 where
183 Provider: DBProvider<Tx: DbTxMut>,
184 {
185 let mut limiter = if let Some(limit) = input.limiter.deleted_entries_limit() {
186 input.limiter.set_deleted_entries_limit(limit / STORAGE_HISTORY_TABLES_TO_PRUNE)
187 } else {
188 input.limiter
189 };
190
191 if limiter.is_limit_reached() {
192 return Ok(SegmentOutput::not_done(
193 limiter.interrupt_reason(),
194 input.previous_checkpoint.map(SegmentOutputCheckpoint::from_prune_checkpoint),
195 ))
196 }
197
198 let mut last_changeset_pruned_block = None;
207 let mut highest_deleted_storages = HashMap::default();
208 let (pruned_changesets, done) =
209 provider.tx_ref().prune_table_with_range::<tables::StorageChangeSets>(
210 BlockNumberAddress::range(range),
211 &mut limiter,
212 |_| false,
213 |(BlockNumberAddress((block_number, address)), entry)| {
214 highest_deleted_storages.insert((address, entry.key), block_number);
215 last_changeset_pruned_block = Some(block_number);
216 },
217 )?;
218 trace!(target: "pruner", deleted = %pruned_changesets, %done, "Pruned storage history (changesets)");
219
220 let last_pruned_block = last_changeset_pruned_block.map(|block_number| {
223 if done {
224 block_number
225 } else {
226 block_number.saturating_sub(1)
227 }
228 });
229
230 let result = HistoryPruneResult {
231 highest_deleted: highest_deleted_storages,
232 last_pruned_block,
233 pruned_count: pruned_changesets,
234 done,
235 };
236 finalize_history_prune::<_, tables::StoragesHistory, (Address, B256), _>(
237 provider,
238 result,
239 range_end,
240 &limiter,
241 |(address, storage_key), block_number| {
242 StorageShardedKey::new(address, storage_key, block_number)
243 },
244 |a, b| a.address == b.address && a.sharded_key.key == b.sharded_key.key,
245 )
246 .map_err(Into::into)
247 }
248
249 fn prune_rocksdb<Provider>(
254 &self,
255 provider: &Provider,
256 input: PruneInput,
257 range: std::ops::RangeInclusive<BlockNumber>,
258 range_end: BlockNumber,
259 ) -> Result<SegmentOutput, PrunerError>
260 where
261 Provider: DBProvider + StaticFileProviderFactory + RocksDBProviderFactory,
262 {
263 let mut limiter = input.limiter;
264
265 if limiter.is_limit_reached() {
266 return Ok(SegmentOutput::not_done(
267 limiter.interrupt_reason(),
268 input.previous_checkpoint.map(SegmentOutputCheckpoint::from_prune_checkpoint),
269 ))
270 }
271
272 let mut highest_deleted_storages = HashMap::<_, _>::default();
273 let mut last_changeset_pruned_block = None;
274 let mut changesets_processed = 0usize;
275 let mut done = true;
276
277 let walker = provider.static_file_provider().walk_storage_changeset_range(range);
281 for result in walker {
282 let (block_address, entry) = result?;
283 let block_number = block_address.block_number();
284 let address = block_address.address();
285 if limiter.is_limit_reached() &&
290 last_changeset_pruned_block.is_some_and(|last| last != block_number)
291 {
292 done = false;
293 break;
294 }
295 highest_deleted_storages.insert((address, entry.key), block_number);
296 last_changeset_pruned_block = Some(block_number);
297 changesets_processed += 1;
298 limiter.increment_deleted_entries_count();
299 }
300
301 trace!(target: "pruner", processed = %changesets_processed, %done, "Scanned storage changesets from static files");
302
303 let last_changeset_pruned_block = last_changeset_pruned_block.unwrap_or(range_end);
304
305 let mut deleted_shards = 0usize;
307 let mut updated_shards = 0usize;
308
309 let mut sorted_storages: Vec<_> = highest_deleted_storages.into_iter().collect();
311 sorted_storages.sort_unstable_by_key(|((addr, key), _)| (*addr, *key));
312
313 provider.with_rocksdb_batch(|mut batch| {
314 let targets: Vec<_> = sorted_storages
315 .iter()
316 .map(|((addr, key), highest)| {
317 ((*addr, *key), (*highest).min(last_changeset_pruned_block))
318 })
319 .collect();
320
321 let outcomes = batch.prune_storage_history_batch(&targets)?;
322 deleted_shards = outcomes.deleted;
323 updated_shards = outcomes.updated;
324
325 Ok(((), Some(batch.into_inner())))
326 })?;
327
328 trace!(target: "pruner", deleted = deleted_shards, updated = updated_shards, %done, "Pruned storage history (RocksDB indices)");
329
330 if done {
336 provider.static_file_provider().delete_segment_below_block(
337 StaticFileSegment::StorageChangeSets,
338 last_changeset_pruned_block + 1,
339 )?;
340 }
341
342 let progress = limiter.progress(done);
343
344 Ok(SegmentOutput {
345 progress,
346 pruned: changesets_processed + deleted_shards + updated_shards,
347 checkpoint: Some(SegmentOutputCheckpoint {
348 block_number: Some(last_changeset_pruned_block),
349 tx_number: None,
350 }),
351 })
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::STORAGE_HISTORY_TABLES_TO_PRUNE;
358 use crate::segments::{PruneInput, PruneLimiter, Segment, SegmentOutput, StorageHistory};
359 use alloy_primitives::{Address, BlockNumber, B256, U256};
360 use assert_matches::assert_matches;
361 use reth_db_api::{
362 models::{storage_sharded_key::StorageShardedKey, StorageSettings},
363 tables, BlockNumberList,
364 };
365 use reth_primitives_traits::{Account, StorageEntry};
366 use reth_provider::{
367 DBProvider, DatabaseProviderFactory, PruneCheckpointReader, RocksDBProviderFactory,
368 };
369 use reth_prune_types::{
370 PruneCheckpoint, PruneInterruptReason, PruneMode, PruneProgress, PruneSegment,
371 };
372 use reth_stages::test_utils::{StorageKind, TestStageDB};
373 use reth_storage_api::StorageSettingsCache;
374 use reth_testing_utils::generators::{
375 self, random_block_range, random_changeset_range, random_eoa_accounts, BlockRangeParams,
376 ChangeSet,
377 };
378 use std::{collections::BTreeMap, ops::AddAssign};
379
380 #[test]
381 fn prune_legacy() {
382 let db = TestStageDB::default();
383 let mut rng = generators::rng();
384
385 let blocks = random_block_range(
386 &mut rng,
387 0..=5000,
388 BlockRangeParams { parent: Some(B256::ZERO), tx_count: 0..1, ..Default::default() },
389 );
390 db.insert_blocks(blocks.iter(), StorageKind::Database(None)).expect("insert blocks");
391
392 let accounts = random_eoa_accounts(&mut rng, 2).into_iter().collect::<BTreeMap<_, _>>();
393
394 let (changesets, _) = random_changeset_range(
395 &mut rng,
396 blocks.iter(),
397 accounts.into_iter().map(|(addr, acc)| (addr, (acc, Vec::new()))),
398 1..2,
399 1..2,
400 );
401 db.insert_changesets(changesets.clone(), None).expect("insert changesets");
402 db.insert_history(changesets.clone(), None).expect("insert history");
403
404 let storage_occurrences = db.table::<tables::StoragesHistory>().unwrap().into_iter().fold(
405 BTreeMap::<_, usize>::new(),
406 |mut map, (key, _)| {
407 map.entry((key.address, key.sharded_key.key)).or_default().add_assign(1);
408 map
409 },
410 );
411 assert!(storage_occurrences.into_iter().any(|(_, occurrences)| occurrences > 1));
412
413 assert_eq!(
414 db.table::<tables::StorageChangeSets>().unwrap().len(),
415 changesets.iter().flatten().flat_map(|(_, _, entries)| entries).count()
416 );
417
418 let original_shards = db.table::<tables::StoragesHistory>().unwrap();
419
420 let test_prune = |to_block: BlockNumber,
421 run: usize,
422 expected_result: (PruneProgress, usize)| {
423 let prune_mode = PruneMode::Before(to_block);
424 let deleted_entries_limit = 1000;
425 let mut limiter =
426 PruneLimiter::default().set_deleted_entries_limit(deleted_entries_limit);
427 let input = PruneInput {
428 previous_checkpoint: db
429 .factory
430 .provider()
431 .unwrap()
432 .get_prune_checkpoint(PruneSegment::StorageHistory)
433 .unwrap(),
434 to_block,
435 limiter: limiter.clone(),
436 };
437 let segment = StorageHistory::new(prune_mode);
438
439 let provider = db.factory.database_provider_rw().unwrap();
440 provider.set_storage_settings_cache(StorageSettings::v1());
441 let result = segment.prune(&provider, input).unwrap();
442 limiter.increment_deleted_entries_count_by(result.pruned);
443
444 assert_matches!(
445 result,
446 SegmentOutput {progress, pruned, checkpoint: Some(_)}
447 if (progress, pruned) == expected_result
448 );
449
450 segment
451 .save_checkpoint(
452 &provider,
453 result.checkpoint.unwrap().as_prune_checkpoint(prune_mode),
454 )
455 .unwrap();
456 provider.commit().expect("commit");
457
458 let changesets = changesets
459 .iter()
460 .enumerate()
461 .flat_map(|(block_number, changeset)| {
462 changeset.iter().flat_map(move |(address, _, entries)| {
463 entries.iter().map(move |entry| (block_number, address, entry))
464 })
465 })
466 .collect::<Vec<_>>();
467
468 #[expect(clippy::skip_while_next)]
469 let pruned = changesets
470 .iter()
471 .enumerate()
472 .skip_while(|(i, (block_number, _, _))| {
473 *i < deleted_entries_limit / STORAGE_HISTORY_TABLES_TO_PRUNE * run &&
474 *block_number <= to_block as usize
475 })
476 .next()
477 .map(|(i, _)| i)
478 .unwrap_or_default();
479
480 let mut pruned_changesets = changesets.iter().skip(pruned.saturating_sub(1));
483
484 let last_pruned_block_number = pruned_changesets
485 .next()
486 .map(|(block_number, _, _)| {
487 (if result.progress.is_finished() {
488 *block_number
489 } else {
490 block_number.saturating_sub(1)
491 }) as BlockNumber
492 })
493 .unwrap_or(to_block);
494
495 let pruned_changesets = pruned_changesets.fold(
496 BTreeMap::<_, Vec<_>>::new(),
497 |mut acc, (block_number, address, entry)| {
498 acc.entry((block_number, address)).or_default().push(entry);
499 acc
500 },
501 );
502
503 assert_eq!(
504 db.table::<tables::StorageChangeSets>().unwrap().len(),
505 pruned_changesets.values().flatten().count()
506 );
507
508 let actual_shards = db.table::<tables::StoragesHistory>().unwrap();
509
510 let expected_shards = original_shards
511 .iter()
512 .filter(|(key, _)| key.sharded_key.highest_block_number > last_pruned_block_number)
513 .map(|(key, blocks)| {
514 let new_blocks =
515 blocks.iter().skip_while(|block| *block <= last_pruned_block_number);
516 (key.clone(), BlockNumberList::new_pre_sorted(new_blocks))
517 })
518 .collect::<Vec<_>>();
519
520 assert_eq!(actual_shards, expected_shards);
521
522 assert_eq!(
523 db.factory
524 .provider()
525 .unwrap()
526 .get_prune_checkpoint(PruneSegment::StorageHistory)
527 .unwrap(),
528 Some(PruneCheckpoint {
529 block_number: Some(last_pruned_block_number),
530 tx_number: None,
531 prune_mode
532 })
533 );
534 };
535
536 test_prune(
537 998,
538 1,
539 (PruneProgress::HasMoreData(PruneInterruptReason::DeletedEntriesLimitReached), 500),
540 );
541 test_prune(998, 2, (PruneProgress::Finished, 499));
542 test_prune(1200, 3, (PruneProgress::Finished, 202));
543 }
544
545 #[test]
548 #[allow(clippy::clone_on_copy)]
549 fn prune_partial_progress_mid_block() {
550 let db = TestStageDB::default();
551 let mut rng = generators::rng();
552
553 let blocks = random_block_range(
555 &mut rng,
556 0..=10,
557 BlockRangeParams { parent: Some(B256::ZERO), tx_count: 0..1, ..Default::default() },
558 );
559 db.insert_blocks(blocks.iter(), StorageKind::Database(None)).expect("insert blocks");
560
561 let addr1 = Address::with_last_byte(1);
563 let addr2 = Address::with_last_byte(2);
564
565 let account = Account { nonce: 1, balance: U256::from(100), ..Default::default() };
566
567 let storage_entry = |key: u8| reth_primitives_traits::StorageEntry {
569 key: B256::with_last_byte(key),
570 value: U256::from(100),
571 };
572
573 let changesets: Vec<ChangeSet> = vec![
576 vec![(addr1, account.clone(), vec![storage_entry(1)])], vec![(addr1, account.clone(), vec![storage_entry(1)])], vec![(addr1, account.clone(), vec![storage_entry(1)])], vec![(addr1, account.clone(), vec![storage_entry(1)])], vec![(addr1, account.clone(), vec![storage_entry(1)])], vec![
584 (addr1, account.clone(), vec![storage_entry(1), storage_entry(2)]),
585 (addr2, account.clone(), vec![storage_entry(1), storage_entry(2)]),
586 ],
587 vec![(addr1, account, vec![storage_entry(3)])], ];
589
590 db.insert_changesets(changesets.clone(), None).expect("insert changesets");
591 db.insert_history(changesets.clone(), None).expect("insert history");
592
593 let total_storage_entries: usize =
595 changesets.iter().flat_map(|c| c.iter()).map(|(_, _, entries)| entries.len()).sum();
596 assert_eq!(db.table::<tables::StorageChangeSets>().unwrap().len(), total_storage_entries);
597
598 let prune_mode = PruneMode::Before(10);
599
600 let deleted_entries_limit = 14; let limiter = PruneLimiter::default().set_deleted_entries_limit(deleted_entries_limit);
605
606 let input = PruneInput { previous_checkpoint: None, to_block: 10, limiter };
607 let segment = StorageHistory::new(prune_mode);
608
609 let provider = db.factory.database_provider_rw().unwrap();
610 provider.set_storage_settings_cache(StorageSettings::v1());
611 let result = segment.prune(&provider, input).unwrap();
612
613 assert!(!result.progress.is_finished(), "Expected HasMoreData since we stopped mid-block");
615
616 segment
618 .save_checkpoint(&provider, result.checkpoint.unwrap().as_prune_checkpoint(prune_mode))
619 .unwrap();
620 provider.commit().expect("commit");
621
622 let checkpoint = db
624 .factory
625 .provider()
626 .unwrap()
627 .get_prune_checkpoint(PruneSegment::StorageHistory)
628 .unwrap()
629 .expect("checkpoint should exist");
630
631 assert_eq!(
632 checkpoint.block_number,
633 Some(4),
634 "Checkpoint should be block 4 (block before incomplete block 5)"
635 );
636
637 let remaining_changesets = db.table::<tables::StorageChangeSets>().unwrap();
639 assert!(
640 !remaining_changesets.is_empty(),
641 "Should have remaining changesets for blocks 5-6"
642 );
643
644 let history = db.table::<tables::StoragesHistory>().unwrap();
646 for (key, _blocks) in &history {
647 assert!(
648 key.sharded_key.highest_block_number > 4,
649 "Found stale history shard with highest_block_number {} <= checkpoint 4",
650 key.sharded_key.highest_block_number
651 );
652 }
653
654 let input2 = PruneInput {
656 previous_checkpoint: Some(checkpoint),
657 to_block: 10,
658 limiter: PruneLimiter::default().set_deleted_entries_limit(100), };
660
661 let provider2 = db.factory.database_provider_rw().unwrap();
662 provider2.set_storage_settings_cache(StorageSettings::v1());
663 let result2 = segment.prune(&provider2, input2).unwrap();
664
665 assert!(result2.progress.is_finished(), "Second run should complete");
666
667 segment
668 .save_checkpoint(
669 &provider2,
670 result2.checkpoint.unwrap().as_prune_checkpoint(prune_mode),
671 )
672 .unwrap();
673 provider2.commit().expect("commit");
674
675 let final_checkpoint = db
677 .factory
678 .provider()
679 .unwrap()
680 .get_prune_checkpoint(PruneSegment::StorageHistory)
681 .unwrap()
682 .expect("checkpoint should exist");
683
684 assert_eq!(final_checkpoint.block_number, Some(6), "Final checkpoint should be at block 6");
686
687 let final_changesets = db.table::<tables::StorageChangeSets>().unwrap();
689 assert!(final_changesets.is_empty(), "All changesets up to block 10 should be pruned");
690 }
691
692 #[test]
693 fn prune_rocksdb() {
694 use reth_storage_api::StorageSettings;
695
696 let db = TestStageDB::default();
697 let mut rng = generators::rng();
698
699 let blocks = random_block_range(
700 &mut rng,
701 0..=100,
702 BlockRangeParams { parent: Some(B256::ZERO), tx_count: 0..1, ..Default::default() },
703 );
704 db.insert_blocks(blocks.iter(), StorageKind::Database(None)).expect("insert blocks");
705
706 let accounts = random_eoa_accounts(&mut rng, 2).into_iter().collect::<BTreeMap<_, _>>();
707
708 let (changesets, _) = random_changeset_range(
709 &mut rng,
710 blocks.iter(),
711 accounts.into_iter().map(|(addr, acc)| (addr, (acc, Vec::new()))),
712 1..2,
713 1..2,
714 );
715
716 db.insert_changesets_to_static_files(changesets.clone(), None)
717 .expect("insert changesets to static files");
718
719 let mut storage_indices: BTreeMap<(alloy_primitives::Address, B256), Vec<u64>> =
720 BTreeMap::new();
721 for (block, changeset) in changesets.iter().enumerate() {
722 for (address, _, storage_entries) in changeset {
723 for entry in storage_entries {
724 storage_indices.entry((*address, entry.key)).or_default().push(block as u64);
725 }
726 }
727 }
728
729 {
730 let rocksdb = db.factory.rocksdb_provider();
731 let mut batch = rocksdb.batch();
732 for ((address, storage_key), block_numbers) in &storage_indices {
733 let shard = BlockNumberList::new_pre_sorted(block_numbers.clone());
734 batch
735 .put::<tables::StoragesHistory>(
736 StorageShardedKey::last(*address, *storage_key),
737 &shard,
738 )
739 .expect("insert storage history shard");
740 }
741 batch.commit().expect("commit rocksdb batch");
742 }
743
744 {
745 let rocksdb = db.factory.rocksdb_provider();
746 for (address, storage_key) in storage_indices.keys() {
747 let shards = rocksdb.storage_history_shards(*address, *storage_key).unwrap();
748 assert!(!shards.is_empty(), "RocksDB should contain storage history before prune");
749 }
750 }
751
752 let to_block = 50u64;
753 let prune_mode = PruneMode::Before(to_block);
754 let input =
755 PruneInput { previous_checkpoint: None, to_block, limiter: PruneLimiter::default() };
756 let segment = StorageHistory::new(prune_mode);
757
758 let provider = db.factory.database_provider_rw().unwrap();
759 provider.set_storage_settings_cache(StorageSettings::v2());
760 let result = segment.prune(&provider, input).unwrap();
761 provider.commit().expect("commit");
762
763 assert_matches!(
764 result,
765 SegmentOutput { progress: PruneProgress::Finished, checkpoint: Some(_), .. }
766 );
767
768 {
769 let rocksdb = db.factory.rocksdb_provider();
770 for ((address, storage_key), block_numbers) in &storage_indices {
771 let shards = rocksdb.storage_history_shards(*address, *storage_key).unwrap();
772
773 let remaining_blocks: Vec<u64> =
774 block_numbers.iter().copied().filter(|&b| b > to_block).collect();
775
776 if remaining_blocks.is_empty() {
777 assert!(
778 shards.is_empty(),
779 "Shard for {:?}/{:?} should be deleted when all blocks pruned",
780 address,
781 storage_key
782 );
783 } else {
784 assert!(!shards.is_empty(), "Shard should exist with remaining blocks");
785 let actual_blocks: Vec<u64> =
786 shards.iter().flat_map(|(_, list)| list.iter()).collect();
787 assert_eq!(
788 actual_blocks, remaining_blocks,
789 "RocksDB shard should only contain blocks > {}",
790 to_block
791 );
792 }
793 }
794 }
795 }
796
797 #[test]
801 #[allow(clippy::clone_on_copy)]
802 fn dense_block_advances_rocksdb_checkpoint() {
803 use reth_storage_api::StorageSettings;
804
805 let db = TestStageDB::default();
806 let mut rng = generators::rng();
807
808 let blocks = random_block_range(
809 &mut rng,
810 0..=20,
811 BlockRangeParams { parent: Some(B256::ZERO), tx_count: 0..1, ..Default::default() },
812 );
813 db.insert_blocks(blocks.iter(), StorageKind::Database(None)).expect("insert blocks");
814
815 const ENTRIES_PER_BLOCK: usize = 2;
817 let (address, account) = random_eoa_accounts(&mut rng, 1).into_iter().next().unwrap();
818 let keys = [B256::with_last_byte(1), B256::with_last_byte(2)];
819 let changesets = (0..=20)
820 .map(|_| {
821 vec![(
822 address,
823 account.clone(),
824 keys.iter().map(|key| StorageEntry { key: *key, value: U256::ONE }).collect(),
825 )]
826 })
827 .collect::<Vec<_>>();
828 db.insert_changesets_to_static_files(changesets, None)
829 .expect("insert changesets to static files");
830
831 {
832 let rocksdb = db.factory.rocksdb_provider();
833 let mut batch = rocksdb.batch();
834 for key in keys {
835 batch
836 .put::<tables::StoragesHistory>(
837 StorageShardedKey::last(address, key),
838 &BlockNumberList::new_pre_sorted(0..=20),
839 )
840 .expect("insert storage history shard");
841 }
842 batch.commit().expect("commit rocksdb batch");
843 }
844
845 let to_block = 15u64;
846 let prune_mode = PruneMode::Before(to_block);
847 let segment = StorageHistory::new(prune_mode);
848
849 let mut checkpoint = PruneCheckpoint { block_number: Some(4), tx_number: None, prune_mode };
851
852 let run_prune = |checkpoint: PruneCheckpoint, limit: usize| {
853 let input = PruneInput {
854 previous_checkpoint: Some(checkpoint),
855 to_block,
856 limiter: PruneLimiter::default().set_deleted_entries_limit(limit),
857 };
858
859 let provider = db.factory.database_provider_rw().unwrap();
860 provider.set_storage_settings_cache(StorageSettings::v2());
861 let result = segment.prune(&provider, input).unwrap();
862 segment
863 .save_checkpoint(
864 &provider,
865 result.checkpoint.unwrap().as_prune_checkpoint(prune_mode),
866 )
867 .unwrap();
868 provider.commit().expect("commit");
869
870 let checkpoint = db
871 .factory
872 .provider()
873 .unwrap()
874 .get_prune_checkpoint(PruneSegment::StorageHistory)
875 .unwrap()
876 .unwrap();
877 (result, checkpoint)
878 };
879
880 for _ in 0..3 {
882 let previous = checkpoint.block_number;
883 let (result, next) = run_prune(checkpoint, ENTRIES_PER_BLOCK);
884 checkpoint = next;
885
886 assert!(
887 !result.progress.is_finished(),
888 "the range is longer than one run's budget allows"
889 );
890 assert!(
891 checkpoint.block_number > previous,
892 "checkpoint must advance past the dense block, got {:?} after {previous:?}",
893 checkpoint.block_number
894 );
895 }
896 assert_eq!(checkpoint.block_number, Some(7), "one dense block cleared per run");
897
898 let (result, checkpoint) = run_prune(checkpoint, 1000);
900 assert!(result.progress.is_finished());
901 assert_eq!(checkpoint.block_number, Some(to_block));
902 }
903}