1use crate::{
2 db_ext::DbTxPruneExt,
3 segments::{PruneInput, Segment, SegmentOutput},
4 PrunerError,
5};
6use alloy_primitives::TxNumber;
7use reth_db_api::{table::Value, tables, transaction::DbTxMut};
8use reth_primitives_traits::{NodePrimitives, SignedTransaction};
9use reth_provider::{
10 BlockReader, DBProvider, PruneCheckpointReader, RocksDBProviderFactory,
11 StaticFileProviderFactory, TransactionsProviderExt,
12};
13use reth_prune_types::{
14 PruneCheckpoint, PruneMode, PruneProgress, PrunePurpose, PruneSegment, SegmentOutputCheckpoint,
15};
16use reth_static_file_types::StaticFileSegment;
17use reth_storage_api::StorageSettingsCache;
18use tracing::{debug, instrument, trace};
19
20#[derive(Debug)]
21pub struct TransactionLookup {
22 mode: PruneMode,
23}
24
25impl TransactionLookup {
26 pub const fn new(mode: PruneMode) -> Self {
27 Self { mode }
28 }
29}
30
31impl<Provider> Segment<Provider> for TransactionLookup
32where
33 Provider: DBProvider<Tx: DbTxMut>
34 + BlockReader<Transaction: SignedTransaction>
35 + PruneCheckpointReader
36 + StorageSettingsCache
37 + RocksDBProviderFactory
38 + StaticFileProviderFactory<
39 Primitives: NodePrimitives<SignedTx: Value, Receipt: Value, BlockHeader: Value>,
40 >,
41{
42 fn segment(&self) -> PruneSegment {
43 PruneSegment::TransactionLookup
44 }
45
46 fn mode(&self) -> Option<PruneMode> {
47 Some(self.mode)
48 }
49
50 fn purpose(&self) -> PrunePurpose {
51 PrunePurpose::User
52 }
53
54 #[instrument(
55 name = "TransactionLookup::prune",
56 target = "pruner",
57 skip(self, provider),
58 ret(level = "trace")
59 )]
60 fn prune(
61 &self,
62 provider: &Provider,
63 mut input: PruneInput,
64 ) -> Result<SegmentOutput, PrunerError> {
65 if let Some(lowest_range) =
70 provider.static_file_provider().get_lowest_range(StaticFileSegment::Transactions) &&
71 input
72 .previous_checkpoint
73 .is_none_or(|checkpoint| checkpoint.block_number < Some(lowest_range.start())) &&
74 let Some(previous_block) = lowest_range.start().checked_sub(1) &&
75 let Some(body_indices) = provider.block_body_indices(previous_block)? &&
76 let Some(tx_number) = body_indices.next_tx_num().checked_sub(1)
77 {
78 input.previous_checkpoint = Some(PruneCheckpoint {
80 block_number: Some(previous_block),
81 tx_number: Some(tx_number),
82 prune_mode: self.mode,
83 });
84 debug!(
85 target: "pruner",
86 static_file_checkpoint = ?input.previous_checkpoint,
87 "Using static file transaction checkpoint as TransactionLookup starting point"
88 );
89 }
90
91 let (start, end) = match input.get_next_tx_num_range(provider)? {
92 Some(range) => range,
93 None => {
94 trace!(target: "pruner", "No transaction lookup entries to prune");
95 return Ok(SegmentOutput::done())
96 }
97 }
98 .into_inner();
99
100 if provider.cached_storage_settings().storage_v2 {
102 return self.prune_rocksdb(provider, input, start, end);
103 }
104
105 if self.mode.is_full() {
107 let pruned = provider.tx_ref().clear_table::<tables::TransactionHashNumbers>()?;
108 trace!(target: "pruner", %pruned, "Cleared transaction lookup table");
109
110 let last_pruned_block = provider
111 .block_by_transaction_id(end)?
112 .ok_or(PrunerError::InconsistentData("Block for transaction is not found"))?;
113
114 return Ok(SegmentOutput {
115 progress: PruneProgress::Finished,
116 pruned,
117 checkpoint: Some(SegmentOutputCheckpoint {
118 block_number: Some(last_pruned_block),
119 tx_number: Some(end),
120 }),
121 });
122 }
123
124 let tx_range = start..=
125 Some(end)
126 .min(
127 input
128 .limiter
129 .deleted_entries_limit_left()
130 .map(|left| start.saturating_add(left as u64) - 1),
133 )
134 .unwrap();
135 let tx_range_end = *tx_range.end();
136
137 let mut hashes = provider
138 .static_file_provider()
139 .transaction_hashes_by_range(
140 *tx_range.start()..
141 tx_range_end
142 .checked_add(1)
143 .ok_or(PrunerError::InconsistentData("Transaction range end overflow"))?,
144 )?
145 .into_iter()
146 .map(|(hash, _)| hash)
147 .collect::<Vec<_>>();
148
149 hashes.sort_unstable();
153
154 let tx_count = tx_range.count();
156 if hashes.len() != tx_count {
157 return Err(PrunerError::InconsistentData(
158 "Unexpected number of transaction hashes retrieved by transaction number range",
159 ))
160 }
161
162 let mut limiter = input.limiter;
163
164 let mut last_pruned_transaction = None;
165 let (pruned, done) =
166 provider.tx_ref().prune_table_with_iterator::<tables::TransactionHashNumbers>(
167 hashes,
168 &mut limiter,
169 |row| {
170 last_pruned_transaction =
171 Some(last_pruned_transaction.unwrap_or(row.1).max(row.1))
172 },
173 )?;
174
175 let done = done && tx_range_end == end;
176 trace!(target: "pruner", %pruned, %done, "Pruned transaction lookup");
177
178 let last_pruned_transaction = last_pruned_transaction.unwrap_or(tx_range_end);
179
180 let last_pruned_block = provider
181 .block_by_transaction_id(last_pruned_transaction)?
182 .ok_or(PrunerError::InconsistentData("Block for transaction is not found"))?
183 .checked_sub(if done { 0 } else { 1 });
187
188 let progress = limiter.progress(done);
189
190 Ok(SegmentOutput {
191 progress,
192 pruned,
193 checkpoint: Some(SegmentOutputCheckpoint {
194 block_number: last_pruned_block,
195 tx_number: Some(last_pruned_transaction),
196 }),
197 })
198 }
199}
200
201impl TransactionLookup {
202 fn prune_rocksdb<Provider>(
207 &self,
208 provider: &Provider,
209 input: PruneInput,
210 start: TxNumber,
211 end: TxNumber,
212 ) -> Result<SegmentOutput, PrunerError>
213 where
214 Provider: DBProvider
215 + BlockReader<Transaction: SignedTransaction>
216 + RocksDBProviderFactory
217 + StaticFileProviderFactory<
218 Primitives: NodePrimitives<SignedTx: Value, Receipt: Value, BlockHeader: Value>,
219 >,
220 {
221 if self.mode.is_full() {
223 let rocksdb = provider.rocksdb_provider();
224 rocksdb.clear::<tables::TransactionHashNumbers>()?;
225 trace!(target: "pruner", "Cleared transaction lookup table (RocksDB)");
226
227 let last_pruned_block = provider
228 .block_by_transaction_id(end)?
229 .ok_or(PrunerError::InconsistentData("Block for transaction is not found"))?;
230
231 return Ok(SegmentOutput {
232 progress: PruneProgress::Finished,
233 pruned: 0, checkpoint: Some(SegmentOutputCheckpoint {
235 block_number: Some(last_pruned_block),
236 tx_number: Some(end),
237 }),
238 });
239 }
240
241 let tx_range_end = input
242 .limiter
243 .deleted_entries_limit_left()
244 .map(|left| start.saturating_add(left as u64).saturating_sub(1))
245 .map_or(end, |limited| limited.min(end));
246 let tx_range = start..=tx_range_end;
247
248 let hashes = provider.static_file_provider().transaction_hashes_by_range(
249 *tx_range.start()..
250 tx_range_end
251 .checked_add(1)
252 .ok_or(PrunerError::InconsistentData("Transaction range end overflow"))?,
253 )?;
254
255 let tx_count = tx_range.count();
257 if hashes.len() != tx_count {
258 return Err(PrunerError::InconsistentData(
259 "Unexpected number of transaction hashes retrieved by transaction number range",
260 ))
261 }
262
263 let mut limiter = input.limiter;
264
265 let mut deleted = 0usize;
267 provider.with_rocksdb_batch(|mut batch| {
268 for (hash, _) in &hashes {
269 if limiter.is_limit_reached() {
270 break;
271 }
272 batch.delete::<tables::TransactionHashNumbers>(*hash)?;
273 limiter.increment_deleted_entries_count();
274 deleted += 1;
275 }
276 Ok(((), Some(batch.into_inner())))
277 })?;
278
279 let done = deleted == hashes.len() && tx_range_end == end;
280 trace!(target: "pruner", %deleted, %done, "Pruned transaction lookup (RocksDB)");
281
282 let last_pruned_transaction =
283 if deleted > 0 { start + deleted as u64 - 1 } else { tx_range_end };
284
285 let last_pruned_block = provider
286 .block_by_transaction_id(last_pruned_transaction)?
287 .ok_or(PrunerError::InconsistentData("Block for transaction is not found"))?
288 .checked_sub(if done { 0 } else { 1 });
289
290 let progress = limiter.progress(done);
291
292 Ok(SegmentOutput {
293 progress,
294 pruned: deleted,
295 checkpoint: Some(SegmentOutputCheckpoint {
296 block_number: last_pruned_block,
297 tx_number: Some(last_pruned_transaction),
298 }),
299 })
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use crate::segments::{PruneInput, PruneLimiter, Segment, SegmentOutput, TransactionLookup};
306 use alloy_primitives::{BlockNumber, TxNumber, B256};
307 use assert_matches::assert_matches;
308 use itertools::{
309 FoldWhile::{Continue, Done},
310 Itertools,
311 };
312 use reth_db_api::tables;
313 use reth_provider::{DBProvider, DatabaseProviderFactory, PruneCheckpointReader};
314 use reth_prune_types::{
315 PruneCheckpoint, PruneInterruptReason, PruneMode, PruneProgress, PruneSegment,
316 };
317 use reth_stages::test_utils::{StorageKind, TestStageDB};
318 use reth_testing_utils::generators::{self, random_block_range, BlockRangeParams};
319 use std::ops::Sub;
320
321 #[test]
322 fn prune_transaction_zero_after_empty_genesis() {
323 let db = TestStageDB::default();
324 let mut rng = generators::rng();
325 let blocks = [
326 random_block_range(
327 &mut rng,
328 0..=0,
329 BlockRangeParams { tx_count: 0..1, ..Default::default() },
330 ),
331 random_block_range(
332 &mut rng,
333 1..=2,
334 BlockRangeParams { tx_count: 1..2, ..Default::default() },
335 ),
336 ]
337 .concat();
338 db.insert_blocks(blocks.iter(), StorageKind::Static).unwrap();
339 let hashes = blocks
340 .iter()
341 .flat_map(|block| &block.body().transactions)
342 .map(|tx| *tx.tx_hash())
343 .collect::<Vec<_>>();
344 db.insert_tx_hash_numbers(hashes.iter().enumerate().map(|(id, hash)| (*hash, id as u64)))
345 .unwrap();
346
347 let provider = db.factory.database_provider_rw().unwrap();
348 let result = TransactionLookup::new(PruneMode::Before(2))
349 .prune(
350 &provider,
351 PruneInput {
352 previous_checkpoint: None,
353 to_block: 1,
354 limiter: PruneLimiter::default().set_deleted_entries_limit(10),
355 },
356 )
357 .unwrap();
358 assert_eq!(result.pruned, 1);
359 assert_eq!(result.checkpoint.unwrap().tx_number, Some(0));
360 provider.commit().unwrap();
361 assert_eq!(db.table::<tables::TransactionHashNumbers>().unwrap(), vec![(hashes[1], 1)]);
362 }
363
364 #[test]
365 fn prune_transaction_zero_when_earlier_blocks_are_empty() {
366 use reth_provider::{StaticFileProviderFactory, StaticFileWriter};
367 use reth_static_file_types::{StaticFileSegment, DEFAULT_BLOCKS_PER_STATIC_FILE};
368
369 let db = TestStageDB::default();
370 let mut rng = generators::rng();
371 let first_retained_block = DEFAULT_BLOCKS_PER_STATIC_FILE;
372 let blocks = [
373 random_block_range(
374 &mut rng,
375 first_retained_block - 1..=first_retained_block - 1,
376 BlockRangeParams { tx_count: 0..1, ..Default::default() },
377 ),
378 random_block_range(
379 &mut rng,
380 first_retained_block..=first_retained_block + 1,
381 BlockRangeParams { tx_count: 1..2, ..Default::default() },
382 ),
383 ]
384 .concat();
385 db.insert_blocks(blocks.iter(), StorageKind::Database(None)).unwrap();
386
387 let static_files = db.factory.static_file_provider();
389 {
390 let mut writer = static_files
391 .get_writer(first_retained_block, StaticFileSegment::Transactions)
392 .unwrap();
393 for (tx_number, block) in blocks[1..].iter().enumerate() {
394 writer.append_transaction(tx_number as u64, &block.body().transactions[0]).unwrap();
395 writer.increment_block(block.number).unwrap();
396 }
397 writer.commit().unwrap();
398 }
399 assert_eq!(
400 static_files.get_lowest_range(StaticFileSegment::Transactions).unwrap().start(),
401 first_retained_block,
402 );
403
404 let hashes = blocks[1..]
405 .iter()
406 .map(|block| *block.body().transactions[0].tx_hash())
407 .collect::<Vec<_>>();
408 db.insert_tx_hash_numbers(hashes.iter().enumerate().map(|(id, hash)| (*hash, id as u64)))
409 .unwrap();
410
411 let provider = db.factory.database_provider_rw().unwrap();
412 let result = TransactionLookup::new(PruneMode::Before(first_retained_block + 1))
413 .prune(
414 &provider,
415 PruneInput {
416 previous_checkpoint: None,
417 to_block: first_retained_block,
418 limiter: PruneLimiter::default().set_deleted_entries_limit(10),
419 },
420 )
421 .unwrap();
422 assert!(result.progress.is_finished());
423 assert_eq!(result.pruned, 1);
424 let checkpoint = result.checkpoint.unwrap();
425 assert_eq!(checkpoint.block_number, Some(first_retained_block));
426 assert_eq!(checkpoint.tx_number, Some(0));
427 provider.commit().unwrap();
428 assert_eq!(db.table::<tables::TransactionHashNumbers>().unwrap(), vec![(hashes[1], 1)]);
429 }
430
431 #[test]
432 fn prune() {
433 let db = TestStageDB::default();
434 let mut rng = generators::rng();
435
436 let blocks = random_block_range(
437 &mut rng,
438 1..=10,
439 BlockRangeParams { parent: Some(B256::ZERO), tx_count: 2..3, ..Default::default() },
440 );
441 db.insert_blocks(blocks.iter(), StorageKind::Static).expect("insert blocks");
442
443 let mut tx_hash_numbers = Vec::new();
444 for block in &blocks {
445 tx_hash_numbers.reserve_exact(block.transaction_count());
446 for transaction in &block.body().transactions {
447 tx_hash_numbers.push((*transaction.tx_hash(), tx_hash_numbers.len() as u64));
448 }
449 }
450 let tx_hash_numbers_len = tx_hash_numbers.len();
451 db.insert_tx_hash_numbers(tx_hash_numbers).expect("insert tx hash numbers");
452
453 assert_eq!(
454 db.count_entries::<tables::Transactions>().unwrap(),
455 blocks.iter().map(|block| block.transaction_count()).sum::<usize>()
456 );
457 assert_eq!(
458 db.count_entries::<tables::Transactions>().unwrap(),
459 db.table::<tables::TransactionHashNumbers>().unwrap().len()
460 );
461
462 let test_prune = |to_block: BlockNumber, expected_result: (PruneProgress, usize)| {
463 let prune_mode = PruneMode::Before(to_block);
464 let segment = TransactionLookup::new(prune_mode);
465 let mut limiter = PruneLimiter::default().set_deleted_entries_limit(10);
466 let input = PruneInput {
467 previous_checkpoint: db
468 .factory
469 .provider()
470 .unwrap()
471 .get_prune_checkpoint(PruneSegment::TransactionLookup)
472 .unwrap(),
473 to_block,
474 limiter: limiter.clone(),
475 };
476
477 let next_tx_number_to_prune = db
478 .factory
479 .provider()
480 .unwrap()
481 .get_prune_checkpoint(PruneSegment::TransactionLookup)
482 .unwrap()
483 .and_then(|checkpoint| checkpoint.tx_number)
484 .map(|tx_number| tx_number + 1)
485 .unwrap_or_default();
486
487 let last_pruned_tx_number = blocks
488 .iter()
489 .take(to_block as usize)
490 .map(|block| block.transaction_count())
491 .sum::<usize>()
492 .min(
493 next_tx_number_to_prune as usize +
494 input.limiter.deleted_entries_limit().unwrap(),
495 )
496 .sub(1);
497
498 let last_pruned_block_number = blocks
499 .iter()
500 .fold_while((0, 0), |(_, mut tx_count), block| {
501 tx_count += block.transaction_count();
502
503 if tx_count > last_pruned_tx_number {
504 Done((block.number, tx_count))
505 } else {
506 Continue((block.number, tx_count))
507 }
508 })
509 .into_inner()
510 .0;
511
512 let provider = db.factory.database_provider_rw().unwrap();
513 let result = segment.prune(&provider, input).unwrap();
514 limiter.increment_deleted_entries_count_by(result.pruned);
515
516 assert_matches!(
517 result,
518 SegmentOutput {progress, pruned, checkpoint: Some(_)}
519 if (progress, pruned) == expected_result
520 );
521
522 segment
523 .save_checkpoint(
524 &provider,
525 result.checkpoint.unwrap().as_prune_checkpoint(prune_mode),
526 )
527 .unwrap();
528 provider.commit().expect("commit");
529
530 let last_pruned_block_number = last_pruned_block_number
531 .checked_sub(if result.progress.is_finished() { 0 } else { 1 });
532
533 assert_eq!(
534 db.table::<tables::TransactionHashNumbers>().unwrap().len(),
535 tx_hash_numbers_len - (last_pruned_tx_number + 1)
536 );
537 assert_eq!(
538 db.factory
539 .provider()
540 .unwrap()
541 .get_prune_checkpoint(PruneSegment::TransactionLookup)
542 .unwrap(),
543 Some(PruneCheckpoint {
544 block_number: last_pruned_block_number,
545 tx_number: Some(last_pruned_tx_number as TxNumber),
546 prune_mode
547 })
548 );
549 };
550
551 test_prune(
552 6,
553 (PruneProgress::HasMoreData(PruneInterruptReason::DeletedEntriesLimitReached), 10),
554 );
555 test_prune(6, (PruneProgress::Finished, 2));
556 test_prune(10, (PruneProgress::Finished, 8));
557 }
558
559 #[test]
560 fn prune_rocksdb() {
561 use reth_db_api::models::StorageSettings;
562 use reth_provider::RocksDBProviderFactory;
563 use reth_storage_api::StorageSettingsCache;
564
565 let db = TestStageDB::default();
566 let mut rng = generators::rng();
567
568 let blocks = random_block_range(
569 &mut rng,
570 1..=10,
571 BlockRangeParams { parent: Some(B256::ZERO), tx_count: 2..3, ..Default::default() },
572 );
573 db.insert_blocks(blocks.iter(), StorageKind::Static).expect("insert blocks");
574
575 let mut tx_hash_numbers = Vec::new();
577 for block in &blocks {
578 tx_hash_numbers.reserve_exact(block.transaction_count());
579 for transaction in &block.body().transactions {
580 tx_hash_numbers.push((*transaction.tx_hash(), tx_hash_numbers.len() as u64));
581 }
582 }
583 let tx_hash_numbers_len = tx_hash_numbers.len();
584
585 {
587 let rocksdb = db.factory.rocksdb_provider();
588 let mut batch = rocksdb.batch();
589 for (hash, tx_num) in &tx_hash_numbers {
590 batch.put::<tables::TransactionHashNumbers>(*hash, tx_num).unwrap();
591 }
592 batch.commit().expect("commit rocksdb batch");
593 }
594
595 {
597 let rocksdb = db.factory.rocksdb_provider();
598 for (hash, expected_tx_num) in &tx_hash_numbers {
599 let actual = rocksdb.get::<tables::TransactionHashNumbers>(*hash).unwrap();
600 assert_eq!(actual, Some(*expected_tx_num));
601 }
602 }
603
604 let to_block: BlockNumber = 6;
605 let prune_mode = PruneMode::Before(to_block);
606 let input =
607 PruneInput { previous_checkpoint: None, to_block, limiter: PruneLimiter::default() };
608 let segment = TransactionLookup::new(prune_mode);
609
610 db.factory.set_storage_settings_cache(StorageSettings::v2());
612
613 let provider = db.factory.database_provider_rw().unwrap();
614 let result = segment.prune(&provider, input).unwrap();
615 provider.commit().expect("commit");
616
617 assert_matches!(
618 result,
619 SegmentOutput { progress: PruneProgress::Finished, pruned, checkpoint: Some(_) }
620 if pruned > 0
621 );
622
623 let txs_up_to_block_6: usize = blocks.iter().take(6).map(|b| b.transaction_count()).sum();
625
626 {
628 let rocksdb = db.factory.rocksdb_provider();
629 for (i, (hash, _)) in tx_hash_numbers.iter().enumerate() {
630 let entry = rocksdb.get::<tables::TransactionHashNumbers>(*hash).unwrap();
631 if i < txs_up_to_block_6 {
632 assert!(entry.is_none(), "Entry {} (hash {:?}) should be pruned", i, hash);
633 } else {
634 assert!(entry.is_some(), "Entry {} (hash {:?}) should still exist", i, hash);
635 }
636 }
637 }
638
639 {
641 let rocksdb = db.factory.rocksdb_provider();
642 let remaining: Vec<_> =
643 rocksdb.iter::<tables::TransactionHashNumbers>().unwrap().collect();
644 assert_eq!(
645 remaining.len(),
646 tx_hash_numbers_len - txs_up_to_block_6,
647 "Remaining RocksDB entries should match expected"
648 );
649 }
650 }
651
652 #[test]
660 fn prune_rocksdb_zero_deleted_checkpoint() {
661 use reth_db_api::models::StorageSettings;
662 use reth_provider::RocksDBProviderFactory;
663 use reth_storage_api::StorageSettingsCache;
664
665 let db = TestStageDB::default();
666 let mut rng = generators::rng();
667
668 let blocks = random_block_range(
669 &mut rng,
670 1..=10,
671 BlockRangeParams { parent: Some(B256::ZERO), tx_count: 2..3, ..Default::default() },
672 );
673 db.insert_blocks(blocks.iter(), StorageKind::Static).expect("insert blocks");
674
675 let mut tx_hash_numbers = Vec::new();
677 for block in &blocks {
678 tx_hash_numbers.reserve_exact(block.transaction_count());
679 for transaction in &block.body().transactions {
680 tx_hash_numbers.push((*transaction.tx_hash(), tx_hash_numbers.len() as u64));
681 }
682 }
683
684 {
686 let rocksdb = db.factory.rocksdb_provider();
687 let mut batch = rocksdb.batch();
688 for (hash, tx_num) in &tx_hash_numbers {
689 batch.put::<tables::TransactionHashNumbers>(*hash, tx_num).unwrap();
690 }
691 batch.commit().expect("commit rocksdb batch");
692 }
693
694 db.factory.set_storage_settings_cache(StorageSettings::v2());
696
697 let to_block: BlockNumber = 6;
698 let prune_mode = PruneMode::Before(to_block);
699
700 let previous_checkpoint =
702 Some(PruneCheckpoint { block_number: Some(2), tx_number: Some(5), prune_mode });
703
704 let mut limiter = PruneLimiter::default().set_deleted_entries_limit(1);
707 limiter.increment_deleted_entries_count(); let input = PruneInput { previous_checkpoint, to_block, limiter };
710 let segment = TransactionLookup::new(prune_mode);
711
712 let provider = db.factory.database_provider_rw().unwrap();
713 let result = segment.prune(&provider, input).unwrap();
714 provider.commit().expect("commit");
715
716 assert_eq!(result.pruned, 0, "Nothing should be pruned with exhausted limit");
718
719 if let Some(checkpoint) = &result.checkpoint {
723 assert_eq!(
724 checkpoint.tx_number,
725 Some(5),
726 "Checkpoint should stay at 5 (previous), not advance to 6 (start)"
727 );
728 }
729
730 {
732 let rocksdb = db.factory.rocksdb_provider();
733 let remaining: Vec<_> =
734 rocksdb.iter::<tables::TransactionHashNumbers>().unwrap().collect();
735 assert_eq!(
736 remaining.len(),
737 tx_hash_numbers.len(),
738 "All RocksDB entries should still exist"
739 );
740 }
741 }
742}