Skip to main content

reth_cli_commands/download/
config_gen.rs

1use crate::download::{
2    manifest::{ComponentManifest, ComponentSelection, SnapshotComponentType, SnapshotManifest},
3    SelectionPreset,
4};
5use reth_chainspec::{EthereumHardfork, EthereumHardforks};
6use reth_config::config::{BlocksPerFileConfig, Config, PruneConfig, StaticFilesConfig};
7use reth_db::tables;
8use reth_db_api::transaction::{DbTx, DbTxMut};
9use reth_node_core::args::DefaultPruningValues;
10use reth_prune_types::{PruneCheckpoint, PruneMode, PruneSegment};
11use reth_stages_types::StageCheckpoint;
12use std::{collections::BTreeMap, path::Path};
13use tracing::info;
14
15/// Minimum blocks to keep for receipts, matching `--minimal` prune settings.
16const MINIMUM_RECEIPTS_DISTANCE: u64 = 64;
17
18/// Minimum blocks to keep for history/bodies, matching `--minimal` prune settings
19/// (`MINIMUM_UNWIND_SAFE_DISTANCE`).
20const MINIMUM_HISTORY_DISTANCE: u64 = 10064;
21
22/// Writes a [`Config`] as TOML to `<data_dir>/reth.toml`.
23///
24/// If the file already exists, it is not overwritten. Returns `true` if the file was written.
25pub fn write_config(config: &Config, data_dir: &Path) -> eyre::Result<bool> {
26    let config_path = data_dir.join("reth.toml");
27
28    if config_path.exists() {
29        info!(target: "reth::cli",
30            path = ?config_path,
31            "reth.toml already exists, skipping config generation"
32        );
33        return Ok(false);
34    }
35
36    let toml_str = toml::to_string_pretty(config)?;
37    reth_fs_util::write(&config_path, toml_str)?;
38
39    info!(target: "reth::cli",
40        path = ?config_path,
41        "Generated reth.toml based on downloaded components"
42    );
43
44    Ok(true)
45}
46
47/// Writes prune checkpoints to the provided write transaction.
48pub(crate) fn write_prune_checkpoints_tx<Tx>(
49    tx: &Tx,
50    config: &Config,
51    snapshot_block: u64,
52) -> eyre::Result<()>
53where
54    Tx: DbTx + DbTxMut,
55{
56    let segments = &config.prune.segments;
57
58    // Collect (segment, mode) pairs for all configured prune segments
59    let checkpoints: Vec<(PruneSegment, PruneMode)> = [
60        (PruneSegment::SenderRecovery, segments.sender_recovery),
61        (PruneSegment::TransactionLookup, segments.transaction_lookup),
62        (PruneSegment::Receipts, segments.receipts),
63        (PruneSegment::AccountHistory, segments.account_history),
64        (PruneSegment::StorageHistory, segments.storage_history),
65        (PruneSegment::Bodies, segments.bodies_history),
66    ]
67    .into_iter()
68    .filter_map(|(segment, mode)| mode.map(|m| (segment, m)))
69    .collect();
70
71    if checkpoints.is_empty() {
72        return Ok(());
73    }
74
75    // Look up the last tx number for the snapshot block from BlockBodyIndices
76    let tx_number =
77        tx.get::<tables::BlockBodyIndices>(snapshot_block)?.map(|indices| indices.last_tx_num());
78
79    for (segment, prune_mode) in &checkpoints {
80        let checkpoint = PruneCheckpoint {
81            block_number: Some(snapshot_block),
82            tx_number,
83            prune_mode: *prune_mode,
84        };
85
86        tx.put::<tables::PruneCheckpoints>(*segment, checkpoint)?;
87
88        info!(target: "reth::cli",
89            segment = %segment,
90            block = snapshot_block,
91            tx = ?tx_number,
92            mode = ?prune_mode,
93            "Set prune checkpoint"
94        );
95    }
96
97    Ok(())
98}
99
100/// Stage IDs for index stages whose output is stored in RocksDB and is never
101/// distributed in snapshots.
102const INDEX_STAGE_IDS: [&str; 3] =
103    ["TransactionLookup", "IndexAccountHistory", "IndexStorageHistory"];
104
105/// Prune segments that correspond to the index stages.
106const INDEX_PRUNE_SEGMENTS: [PruneSegment; 3] =
107    [PruneSegment::TransactionLookup, PruneSegment::AccountHistory, PruneSegment::StorageHistory];
108
109/// Resets stage and prune checkpoints for stages whose output is not included
110/// in the snapshot inside an existing write transaction.
111///
112/// A snapshot's mdbx comes from a fully synced node, so it has stage checkpoints
113/// at the tip for `TransactionLookup`, `IndexAccountHistory`, and
114/// `IndexStorageHistory`. Since we don't distribute the rocksdb indices those
115/// stages produced, we must reset their checkpoints to block 0. Otherwise the
116/// pipeline would see "already done" and skip rebuilding entirely.
117///
118/// We intentionally do not reset `SenderRecovery`: sender static files are
119/// distributed for archive downloads, and non-archive downloads rely on the
120/// configured prune checkpoints for this segment.
121pub(crate) fn reset_index_stage_checkpoints_tx<Tx>(tx: &Tx) -> eyre::Result<()>
122where
123    Tx: DbTx + DbTxMut,
124{
125    for stage_id in INDEX_STAGE_IDS {
126        tx.put::<tables::StageCheckpoints>(stage_id.to_string(), StageCheckpoint::default())?;
127
128        // Also clear any stage-specific progress data
129        tx.delete::<tables::StageCheckpointProgresses>(stage_id.to_string(), None)?;
130
131        info!(target: "reth::cli", stage = stage_id, "Reset stage checkpoint to block 0");
132    }
133
134    // Clear corresponding prune checkpoints so the pruner doesn't inherit
135    // state from the source node
136    for segment in INDEX_PRUNE_SEGMENTS {
137        tx.delete::<tables::PruneCheckpoints>(segment, None)?;
138    }
139
140    Ok(())
141}
142
143/// Generates a [`Config`] from per-component range selections.
144///
145/// When all data components are selected as `All`, no pruning is configured (archive node).
146/// Otherwise, `--minimal` style pruning is applied for missing/partial components.
147pub(crate) fn config_for_selections(
148    selections: &BTreeMap<SnapshotComponentType, ComponentSelection>,
149    manifest: &SnapshotManifest,
150    preset: Option<SelectionPreset>,
151    chain_spec: Option<&impl EthereumHardforks>,
152) -> Config {
153    let selection_for = |ty| selections.get(&ty).copied().unwrap_or(ComponentSelection::None);
154
155    let tx_sel = selection_for(SnapshotComponentType::Transactions);
156    let senders_sel = selection_for(SnapshotComponentType::TransactionSenders);
157    let receipt_sel = selection_for(SnapshotComponentType::Receipts);
158    let account_cs_sel = selection_for(SnapshotComponentType::AccountChangesets);
159    let storage_cs_sel = selection_for(SnapshotComponentType::StorageChangesets);
160
161    // Archive node — all data components present, no pruning
162    let is_archive = [tx_sel, senders_sel, receipt_sel, account_cs_sel, storage_cs_sel]
163        .iter()
164        .all(|s| *s == ComponentSelection::All);
165
166    // Extract blocks_per_file from manifest for all component types
167    let blocks_per_file = |ty: SnapshotComponentType| -> Option<u64> {
168        match manifest.component(ty)? {
169            ComponentManifest::Chunked(c) => Some(c.blocks_per_file),
170            ComponentManifest::Single(_) => None,
171        }
172    };
173    let static_files = StaticFilesConfig {
174        blocks_per_file: BlocksPerFileConfig {
175            headers: blocks_per_file(SnapshotComponentType::Headers),
176            transactions: blocks_per_file(SnapshotComponentType::Transactions),
177            receipts: blocks_per_file(SnapshotComponentType::Receipts),
178            transaction_senders: blocks_per_file(SnapshotComponentType::TransactionSenders),
179            account_change_sets: blocks_per_file(SnapshotComponentType::AccountChangesets),
180            storage_change_sets: blocks_per_file(SnapshotComponentType::StorageChangesets),
181        },
182    };
183
184    if is_archive || matches!(preset, Some(SelectionPreset::Archive)) {
185        return Config { static_files, ..Default::default() };
186    }
187
188    if matches!(preset, Some(SelectionPreset::Full)) {
189        let defaults = DefaultPruningValues::get_global();
190        let mut segments = defaults.full_prune_modes.clone();
191
192        if defaults.full_bodies_history_use_pre_merge {
193            segments.bodies_history = chain_spec.and_then(|chain_spec| {
194                chain_spec
195                    .ethereum_fork_activation(EthereumHardfork::Paris)
196                    .block_number()
197                    .map(PruneMode::Before)
198            });
199        }
200
201        return Config {
202            prune: PruneConfig { segments, ..Default::default() },
203            static_files,
204            ..Default::default()
205        };
206    }
207
208    let mut config = Config::default();
209    let mut prune = PruneConfig::default();
210
211    if senders_sel != ComponentSelection::All {
212        prune.segments.sender_recovery = Some(PruneMode::Full);
213    }
214    prune.segments.transaction_lookup = Some(PruneMode::Full);
215
216    if let Some(mode) = selection_to_prune_mode(tx_sel, Some(MINIMUM_HISTORY_DISTANCE)) {
217        prune.segments.bodies_history = Some(mode);
218    }
219
220    if let Some(mode) = selection_to_prune_mode(receipt_sel, Some(MINIMUM_RECEIPTS_DISTANCE)) {
221        prune.segments.receipts = Some(mode);
222    }
223
224    if let Some(mode) = selection_to_prune_mode(account_cs_sel, Some(MINIMUM_HISTORY_DISTANCE)) {
225        prune.segments.account_history = Some(mode);
226    }
227
228    if let Some(mode) = selection_to_prune_mode(storage_cs_sel, Some(MINIMUM_HISTORY_DISTANCE)) {
229        prune.segments.storage_history = Some(mode);
230    }
231
232    config.prune = prune;
233    config.static_files = static_files;
234    config
235}
236
237/// Converts a [`ComponentSelection`] to an optional [`PruneMode`].
238///
239/// `min_distance` enforces the minimum blocks required for this segment.
240/// When set, `None` and distances below the minimum are clamped to it
241/// instead of producing `PruneMode::Full` which reth would reject.
242fn selection_to_prune_mode(
243    sel: ComponentSelection,
244    min_distance: Option<u64>,
245) -> Option<PruneMode> {
246    match sel {
247        ComponentSelection::All => None,
248        ComponentSelection::Distance(d) => {
249            Some(PruneMode::Distance(min_distance.map_or(d, |min| d.max(min))))
250        }
251        ComponentSelection::Since(block) => Some(PruneMode::Before(block)),
252        ComponentSelection::None => Some(min_distance.map_or(PruneMode::Full, PruneMode::Distance)),
253    }
254}
255
256/// Human-readable prune config summary.
257pub(crate) fn describe_prune_config(config: &Config) -> Vec<String> {
258    let segments = &config.prune.segments;
259
260    [
261        ("sender_recovery", segments.sender_recovery),
262        ("transaction_lookup", segments.transaction_lookup),
263        ("bodies_history", segments.bodies_history),
264        ("receipts", segments.receipts),
265        ("account_history", segments.account_history),
266        ("storage_history", segments.storage_history),
267    ]
268    .into_iter()
269    .filter_map(|(name, mode)| mode.map(|m| format!("{name}={}", format_mode(&m))))
270    .collect()
271}
272
273/// Formats one prune mode for the generated config summary.
274fn format_mode(mode: &PruneMode) -> String {
275    match mode {
276        PruneMode::Full => "\"full\"".to_string(),
277        PruneMode::Distance(d) => format!("{{ distance = {d} }}"),
278        PruneMode::Before(b) => format!("{{ before = {b} }}"),
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285    use reth_db::Database;
286
287    /// Empty manifest for tests that only care about prune config.
288    fn empty_manifest() -> SnapshotManifest {
289        SnapshotManifest {
290            block: 0,
291            chain_id: 1,
292            storage_version: 2,
293            timestamp: 0,
294            base_url: None,
295            reth_version: None,
296            components: BTreeMap::new(),
297        }
298    }
299
300    #[test]
301    fn write_prune_checkpoints_sets_all_segments() {
302        let dir = tempfile::tempdir().unwrap();
303        let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
304
305        let mut selections = BTreeMap::new();
306        selections.insert(SnapshotComponentType::State, ComponentSelection::All);
307        selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
308        let config = config_for_selections(
309            &selections,
310            &empty_manifest(),
311            None,
312            None::<&reth_chainspec::ChainSpec>,
313        );
314        let snapshot_block = 21_000_000;
315
316        {
317            let tx = db.tx_mut().unwrap();
318            write_prune_checkpoints_tx(&tx, &config, snapshot_block).unwrap();
319            tx.commit().unwrap();
320        }
321
322        // Verify all expected segments have checkpoints
323        let tx = db.tx().unwrap();
324        for segment in [
325            PruneSegment::SenderRecovery,
326            PruneSegment::TransactionLookup,
327            PruneSegment::Receipts,
328            PruneSegment::AccountHistory,
329            PruneSegment::StorageHistory,
330            PruneSegment::Bodies,
331        ] {
332            let checkpoint = tx
333                .get::<tables::PruneCheckpoints>(segment)
334                .unwrap()
335                .unwrap_or_else(|| panic!("expected checkpoint for {segment}"));
336            assert_eq!(checkpoint.block_number, Some(snapshot_block));
337            // No BlockBodyIndices in empty DB, so tx_number should be None
338            assert_eq!(checkpoint.tx_number, None);
339        }
340    }
341
342    #[test]
343    fn write_prune_checkpoints_archive_no_checkpoints() {
344        let dir = tempfile::tempdir().unwrap();
345        let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
346
347        // Archive node — no pruning configured, so no checkpoints written
348        let mut selections = BTreeMap::new();
349        for ty in SnapshotComponentType::ALL {
350            selections.insert(ty, ComponentSelection::All);
351        }
352        let config = config_for_selections(
353            &selections,
354            &empty_manifest(),
355            None,
356            None::<&reth_chainspec::ChainSpec>,
357        );
358
359        {
360            let tx = db.tx_mut().unwrap();
361            write_prune_checkpoints_tx(&tx, &config, 21_000_000).unwrap();
362            tx.commit().unwrap();
363        }
364
365        let tx = db.tx().unwrap();
366        for segment in [PruneSegment::SenderRecovery, PruneSegment::TransactionLookup] {
367            assert!(
368                tx.get::<tables::PruneCheckpoints>(segment).unwrap().is_none(),
369                "expected no checkpoint for {segment} on archive node"
370            );
371        }
372    }
373
374    #[test]
375    fn selections_all_no_pruning() {
376        let mut selections = BTreeMap::new();
377        for ty in SnapshotComponentType::ALL {
378            selections.insert(ty, ComponentSelection::All);
379        }
380        let config = config_for_selections(
381            &selections,
382            &empty_manifest(),
383            None,
384            None::<&reth_chainspec::ChainSpec>,
385        );
386        // Archive node — nothing pruned
387        assert_eq!(config.prune.segments.transaction_lookup, None);
388        assert_eq!(config.prune.segments.sender_recovery, None);
389        assert_eq!(config.prune.segments.bodies_history, None);
390        assert_eq!(config.prune.segments.receipts, None);
391        assert_eq!(config.prune.segments.account_history, None);
392        assert_eq!(config.prune.segments.storage_history, None);
393    }
394
395    #[test]
396    fn selections_none_clamps_to_minimum_distance() {
397        let mut selections = BTreeMap::new();
398        selections.insert(SnapshotComponentType::State, ComponentSelection::All);
399        selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
400        let config = config_for_selections(
401            &selections,
402            &empty_manifest(),
403            None,
404            None::<&reth_chainspec::ChainSpec>,
405        );
406        assert_eq!(config.prune.segments.transaction_lookup, Some(PruneMode::Full));
407        assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
408        // All segments clamped to their minimum distances
409        assert_eq!(
410            config.prune.segments.bodies_history,
411            Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
412        );
413        assert_eq!(
414            config.prune.segments.receipts,
415            Some(PruneMode::Distance(MINIMUM_RECEIPTS_DISTANCE))
416        );
417        assert_eq!(
418            config.prune.segments.account_history,
419            Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
420        );
421        assert_eq!(
422            config.prune.segments.storage_history,
423            Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
424        );
425    }
426
427    #[test]
428    fn selections_distance_maps_bodies_history() {
429        let mut selections = BTreeMap::new();
430        selections.insert(SnapshotComponentType::State, ComponentSelection::All);
431        selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
432        selections
433            .insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(10_064));
434        selections.insert(SnapshotComponentType::Receipts, ComponentSelection::None);
435        selections
436            .insert(SnapshotComponentType::AccountChangesets, ComponentSelection::Distance(10_064));
437        selections
438            .insert(SnapshotComponentType::StorageChangesets, ComponentSelection::Distance(10_064));
439        let config = config_for_selections(
440            &selections,
441            &empty_manifest(),
442            None,
443            None::<&reth_chainspec::ChainSpec>,
444        );
445
446        assert_eq!(config.prune.segments.transaction_lookup, Some(PruneMode::Full));
447        assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
448        // Bodies follows tx selection
449        assert_eq!(config.prune.segments.bodies_history, Some(PruneMode::Distance(10_064)));
450        assert_eq!(
451            config.prune.segments.receipts,
452            Some(PruneMode::Distance(MINIMUM_RECEIPTS_DISTANCE))
453        );
454        assert_eq!(config.prune.segments.account_history, Some(PruneMode::Distance(10_064)));
455        assert_eq!(config.prune.segments.storage_history, Some(PruneMode::Distance(10_064)));
456    }
457
458    #[test]
459    fn selections_since_maps_to_before_prune_mode() {
460        let mut selections = BTreeMap::new();
461        selections.insert(SnapshotComponentType::State, ComponentSelection::All);
462        selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
463        selections
464            .insert(SnapshotComponentType::Transactions, ComponentSelection::Since(15_537_394));
465        selections.insert(SnapshotComponentType::Receipts, ComponentSelection::Since(15_537_394));
466        selections.insert(
467            SnapshotComponentType::AccountChangesets,
468            ComponentSelection::Since(15_537_394),
469        );
470        selections.insert(
471            SnapshotComponentType::StorageChangesets,
472            ComponentSelection::Since(15_537_394),
473        );
474
475        let config = config_for_selections(
476            &selections,
477            &empty_manifest(),
478            None,
479            None::<&reth_chainspec::ChainSpec>,
480        );
481
482        assert_eq!(config.prune.segments.bodies_history, Some(PruneMode::Before(15_537_394)));
483        assert_eq!(config.prune.segments.receipts, Some(PruneMode::Before(15_537_394)));
484        assert_eq!(config.prune.segments.account_history, Some(PruneMode::Before(15_537_394)));
485        assert_eq!(config.prune.segments.storage_history, Some(PruneMode::Before(15_537_394)));
486    }
487
488    #[test]
489    fn full_preset_matches_default_full_prune_config() {
490        let mut selections = BTreeMap::new();
491        selections.insert(SnapshotComponentType::State, ComponentSelection::All);
492        selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
493        selections
494            .insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(500_000));
495        selections.insert(SnapshotComponentType::Receipts, ComponentSelection::Distance(10_064));
496
497        let chain_spec = reth_chainspec::MAINNET.clone();
498        let config = config_for_selections(
499            &selections,
500            &empty_manifest(),
501            Some(SelectionPreset::Full),
502            Some(chain_spec.as_ref()),
503        );
504
505        assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
506        assert_eq!(config.prune.segments.transaction_lookup, None);
507        assert_eq!(
508            config.prune.segments.receipts,
509            Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
510        );
511        assert_eq!(
512            config.prune.segments.account_history,
513            Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
514        );
515        assert_eq!(
516            config.prune.segments.storage_history,
517            Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
518        );
519
520        let paris_block = chain_spec
521            .ethereum_fork_activation(EthereumHardfork::Paris)
522            .block_number()
523            .expect("mainnet Paris block should be known");
524        assert_eq!(config.prune.segments.bodies_history, Some(PruneMode::Before(paris_block)));
525    }
526
527    #[test]
528    fn describe_selections_all_no_pruning() {
529        let mut selections = BTreeMap::new();
530        for ty in SnapshotComponentType::ALL {
531            selections.insert(ty, ComponentSelection::All);
532        }
533        let config = config_for_selections(
534            &selections,
535            &empty_manifest(),
536            None,
537            None::<&reth_chainspec::ChainSpec>,
538        );
539        let desc = describe_prune_config(&config);
540        // Archive node — no prune segments described
541        assert!(desc.is_empty());
542    }
543
544    #[test]
545    fn describe_selections_with_distances() {
546        let mut selections = BTreeMap::new();
547        selections.insert(SnapshotComponentType::State, ComponentSelection::All);
548        selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
549        selections
550            .insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(10_064));
551        selections.insert(SnapshotComponentType::Receipts, ComponentSelection::None);
552        let config = config_for_selections(
553            &selections,
554            &empty_manifest(),
555            None,
556            None::<&reth_chainspec::ChainSpec>,
557        );
558        let desc = describe_prune_config(&config);
559        assert!(desc.contains(&"sender_recovery=\"full\"".to_string()));
560        // Bodies follows tx selection
561        assert!(desc.contains(&"bodies_history={ distance = 10064 }".to_string()));
562        assert!(desc.contains(&"receipts={ distance = 64 }".to_string()));
563    }
564
565    #[test]
566    fn reset_index_stage_checkpoints_clears_only_rocksdb_index_stages() {
567        let dir = tempfile::tempdir().unwrap();
568        let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
569
570        // Simulate a fully synced node: set stage checkpoints at tip
571        let tip_checkpoint = StageCheckpoint::new(24_500_000);
572        {
573            let tx = db.tx_mut().unwrap();
574            for stage_id in INDEX_STAGE_IDS {
575                tx.put::<tables::StageCheckpoints>(stage_id.to_string(), tip_checkpoint).unwrap();
576            }
577            for segment in INDEX_PRUNE_SEGMENTS {
578                tx.put::<tables::PruneCheckpoints>(
579                    segment,
580                    PruneCheckpoint {
581                        block_number: Some(24_500_000),
582                        tx_number: None,
583                        prune_mode: PruneMode::Full,
584                    },
585                )
586                .unwrap();
587            }
588
589            // Sender recovery checkpoints should be preserved by reset.
590            tx.put::<tables::StageCheckpoints>("SenderRecovery".to_string(), tip_checkpoint)
591                .unwrap();
592            tx.put::<tables::PruneCheckpoints>(
593                PruneSegment::SenderRecovery,
594                PruneCheckpoint {
595                    block_number: Some(24_500_000),
596                    tx_number: None,
597                    prune_mode: PruneMode::Full,
598                },
599            )
600            .unwrap();
601            tx.commit().unwrap();
602        }
603
604        // Reset
605        {
606            let tx = db.tx_mut().unwrap();
607            reset_index_stage_checkpoints_tx(&tx).unwrap();
608            tx.commit().unwrap();
609        }
610
611        // Verify stage checkpoints are at block 0
612        let tx = db.tx().unwrap();
613        for stage_id in INDEX_STAGE_IDS {
614            let checkpoint = tx
615                .get::<tables::StageCheckpoints>(stage_id.to_string())
616                .unwrap()
617                .expect("checkpoint should exist");
618            assert_eq!(checkpoint.block_number, 0, "stage {stage_id} should be reset to block 0");
619        }
620
621        // Verify prune checkpoints are deleted
622        for segment in INDEX_PRUNE_SEGMENTS {
623            assert!(
624                tx.get::<tables::PruneCheckpoints>(segment).unwrap().is_none(),
625                "prune checkpoint for {segment} should be deleted"
626            );
627        }
628
629        // Verify sender checkpoints are left untouched.
630        let sender_stage_checkpoint = tx
631            .get::<tables::StageCheckpoints>("SenderRecovery".to_string())
632            .unwrap()
633            .expect("sender checkpoint should exist");
634        assert_eq!(sender_stage_checkpoint.block_number, tip_checkpoint.block_number);
635
636        let sender_prune_checkpoint = tx
637            .get::<tables::PruneCheckpoints>(PruneSegment::SenderRecovery)
638            .unwrap()
639            .expect("sender prune checkpoint should exist");
640        assert_eq!(sender_prune_checkpoint.block_number, Some(tip_checkpoint.block_number));
641    }
642}