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            extensions: Default::default(),
298        }
299    }
300
301    #[test]
302    fn write_prune_checkpoints_sets_all_segments() {
303        let dir = tempfile::tempdir().unwrap();
304        let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
305
306        let mut selections = BTreeMap::new();
307        selections.insert(SnapshotComponentType::State, ComponentSelection::All);
308        selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
309        let config = config_for_selections(
310            &selections,
311            &empty_manifest(),
312            None,
313            None::<&reth_chainspec::ChainSpec>,
314        );
315        let snapshot_block = 21_000_000;
316
317        {
318            let tx = db.tx_mut().unwrap();
319            write_prune_checkpoints_tx(&tx, &config, snapshot_block).unwrap();
320            tx.commit().unwrap();
321        }
322
323        // Verify all expected segments have checkpoints
324        let tx = db.tx().unwrap();
325        for segment in [
326            PruneSegment::SenderRecovery,
327            PruneSegment::TransactionLookup,
328            PruneSegment::Receipts,
329            PruneSegment::AccountHistory,
330            PruneSegment::StorageHistory,
331            PruneSegment::Bodies,
332        ] {
333            let checkpoint = tx
334                .get::<tables::PruneCheckpoints>(segment)
335                .unwrap()
336                .unwrap_or_else(|| panic!("expected checkpoint for {segment}"));
337            assert_eq!(checkpoint.block_number, Some(snapshot_block));
338            // No BlockBodyIndices in empty DB, so tx_number should be None
339            assert_eq!(checkpoint.tx_number, None);
340        }
341    }
342
343    #[test]
344    fn write_prune_checkpoints_archive_no_checkpoints() {
345        let dir = tempfile::tempdir().unwrap();
346        let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
347
348        // Archive node — no pruning configured, so no checkpoints written
349        let mut selections = BTreeMap::new();
350        for ty in SnapshotComponentType::ALL {
351            selections.insert(ty, ComponentSelection::All);
352        }
353        let config = config_for_selections(
354            &selections,
355            &empty_manifest(),
356            None,
357            None::<&reth_chainspec::ChainSpec>,
358        );
359
360        {
361            let tx = db.tx_mut().unwrap();
362            write_prune_checkpoints_tx(&tx, &config, 21_000_000).unwrap();
363            tx.commit().unwrap();
364        }
365
366        let tx = db.tx().unwrap();
367        for segment in [PruneSegment::SenderRecovery, PruneSegment::TransactionLookup] {
368            assert!(
369                tx.get::<tables::PruneCheckpoints>(segment).unwrap().is_none(),
370                "expected no checkpoint for {segment} on archive node"
371            );
372        }
373    }
374
375    #[test]
376    fn selections_all_no_pruning() {
377        let mut selections = BTreeMap::new();
378        for ty in SnapshotComponentType::ALL {
379            selections.insert(ty, ComponentSelection::All);
380        }
381        let config = config_for_selections(
382            &selections,
383            &empty_manifest(),
384            None,
385            None::<&reth_chainspec::ChainSpec>,
386        );
387        // Archive node — nothing pruned
388        assert_eq!(config.prune.segments.transaction_lookup, None);
389        assert_eq!(config.prune.segments.sender_recovery, None);
390        assert_eq!(config.prune.segments.bodies_history, None);
391        assert_eq!(config.prune.segments.receipts, None);
392        assert_eq!(config.prune.segments.account_history, None);
393        assert_eq!(config.prune.segments.storage_history, None);
394    }
395
396    #[test]
397    fn selections_none_clamps_to_minimum_distance() {
398        let mut selections = BTreeMap::new();
399        selections.insert(SnapshotComponentType::State, ComponentSelection::All);
400        selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
401        let config = config_for_selections(
402            &selections,
403            &empty_manifest(),
404            None,
405            None::<&reth_chainspec::ChainSpec>,
406        );
407        assert_eq!(config.prune.segments.transaction_lookup, Some(PruneMode::Full));
408        assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
409        // All segments clamped to their minimum distances
410        assert_eq!(
411            config.prune.segments.bodies_history,
412            Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
413        );
414        assert_eq!(
415            config.prune.segments.receipts,
416            Some(PruneMode::Distance(MINIMUM_RECEIPTS_DISTANCE))
417        );
418        assert_eq!(
419            config.prune.segments.account_history,
420            Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
421        );
422        assert_eq!(
423            config.prune.segments.storage_history,
424            Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
425        );
426    }
427
428    #[test]
429    fn selections_distance_maps_bodies_history() {
430        let mut selections = BTreeMap::new();
431        selections.insert(SnapshotComponentType::State, ComponentSelection::All);
432        selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
433        selections
434            .insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(10_064));
435        selections.insert(SnapshotComponentType::Receipts, ComponentSelection::None);
436        selections
437            .insert(SnapshotComponentType::AccountChangesets, ComponentSelection::Distance(10_064));
438        selections
439            .insert(SnapshotComponentType::StorageChangesets, ComponentSelection::Distance(10_064));
440        let config = config_for_selections(
441            &selections,
442            &empty_manifest(),
443            None,
444            None::<&reth_chainspec::ChainSpec>,
445        );
446
447        assert_eq!(config.prune.segments.transaction_lookup, Some(PruneMode::Full));
448        assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
449        // Bodies follows tx selection
450        assert_eq!(config.prune.segments.bodies_history, Some(PruneMode::Distance(10_064)));
451        assert_eq!(
452            config.prune.segments.receipts,
453            Some(PruneMode::Distance(MINIMUM_RECEIPTS_DISTANCE))
454        );
455        assert_eq!(config.prune.segments.account_history, Some(PruneMode::Distance(10_064)));
456        assert_eq!(config.prune.segments.storage_history, Some(PruneMode::Distance(10_064)));
457    }
458
459    #[test]
460    fn selections_since_maps_to_before_prune_mode() {
461        let mut selections = BTreeMap::new();
462        selections.insert(SnapshotComponentType::State, ComponentSelection::All);
463        selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
464        selections
465            .insert(SnapshotComponentType::Transactions, ComponentSelection::Since(15_537_394));
466        selections.insert(SnapshotComponentType::Receipts, ComponentSelection::Since(15_537_394));
467        selections.insert(
468            SnapshotComponentType::AccountChangesets,
469            ComponentSelection::Since(15_537_394),
470        );
471        selections.insert(
472            SnapshotComponentType::StorageChangesets,
473            ComponentSelection::Since(15_537_394),
474        );
475
476        let config = config_for_selections(
477            &selections,
478            &empty_manifest(),
479            None,
480            None::<&reth_chainspec::ChainSpec>,
481        );
482
483        assert_eq!(config.prune.segments.bodies_history, Some(PruneMode::Before(15_537_394)));
484        assert_eq!(config.prune.segments.receipts, Some(PruneMode::Before(15_537_394)));
485        assert_eq!(config.prune.segments.account_history, Some(PruneMode::Before(15_537_394)));
486        assert_eq!(config.prune.segments.storage_history, Some(PruneMode::Before(15_537_394)));
487    }
488
489    #[test]
490    fn full_preset_matches_default_full_prune_config() {
491        let mut selections = BTreeMap::new();
492        selections.insert(SnapshotComponentType::State, ComponentSelection::All);
493        selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
494        selections
495            .insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(500_000));
496        selections.insert(SnapshotComponentType::Receipts, ComponentSelection::Distance(10_064));
497
498        let chain_spec = reth_chainspec::MAINNET.clone();
499        let config = config_for_selections(
500            &selections,
501            &empty_manifest(),
502            Some(SelectionPreset::Full),
503            Some(chain_spec.as_ref()),
504        );
505
506        assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
507        assert_eq!(config.prune.segments.transaction_lookup, None);
508        assert_eq!(
509            config.prune.segments.receipts,
510            Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
511        );
512        assert_eq!(
513            config.prune.segments.account_history,
514            Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
515        );
516        assert_eq!(
517            config.prune.segments.storage_history,
518            Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
519        );
520
521        let paris_block = chain_spec
522            .ethereum_fork_activation(EthereumHardfork::Paris)
523            .block_number()
524            .expect("mainnet Paris block should be known");
525        assert_eq!(config.prune.segments.bodies_history, Some(PruneMode::Before(paris_block)));
526    }
527
528    #[test]
529    fn describe_selections_all_no_pruning() {
530        let mut selections = BTreeMap::new();
531        for ty in SnapshotComponentType::ALL {
532            selections.insert(ty, ComponentSelection::All);
533        }
534        let config = config_for_selections(
535            &selections,
536            &empty_manifest(),
537            None,
538            None::<&reth_chainspec::ChainSpec>,
539        );
540        let desc = describe_prune_config(&config);
541        // Archive node — no prune segments described
542        assert!(desc.is_empty());
543    }
544
545    #[test]
546    fn describe_selections_with_distances() {
547        let mut selections = BTreeMap::new();
548        selections.insert(SnapshotComponentType::State, ComponentSelection::All);
549        selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
550        selections
551            .insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(10_064));
552        selections.insert(SnapshotComponentType::Receipts, ComponentSelection::None);
553        let config = config_for_selections(
554            &selections,
555            &empty_manifest(),
556            None,
557            None::<&reth_chainspec::ChainSpec>,
558        );
559        let desc = describe_prune_config(&config);
560        assert!(desc.contains(&"sender_recovery=\"full\"".to_string()));
561        // Bodies follows tx selection
562        assert!(desc.contains(&"bodies_history={ distance = 10064 }".to_string()));
563        assert!(desc.contains(&"receipts={ distance = 64 }".to_string()));
564    }
565
566    #[test]
567    fn reset_index_stage_checkpoints_clears_only_rocksdb_index_stages() {
568        let dir = tempfile::tempdir().unwrap();
569        let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
570
571        // Simulate a fully synced node: set stage checkpoints at tip
572        let tip_checkpoint = StageCheckpoint::new(24_500_000);
573        {
574            let tx = db.tx_mut().unwrap();
575            for stage_id in INDEX_STAGE_IDS {
576                tx.put::<tables::StageCheckpoints>(stage_id.to_string(), tip_checkpoint).unwrap();
577            }
578            for segment in INDEX_PRUNE_SEGMENTS {
579                tx.put::<tables::PruneCheckpoints>(
580                    segment,
581                    PruneCheckpoint {
582                        block_number: Some(24_500_000),
583                        tx_number: None,
584                        prune_mode: PruneMode::Full,
585                    },
586                )
587                .unwrap();
588            }
589
590            // Sender recovery checkpoints should be preserved by reset.
591            tx.put::<tables::StageCheckpoints>("SenderRecovery".to_string(), tip_checkpoint)
592                .unwrap();
593            tx.put::<tables::PruneCheckpoints>(
594                PruneSegment::SenderRecovery,
595                PruneCheckpoint {
596                    block_number: Some(24_500_000),
597                    tx_number: None,
598                    prune_mode: PruneMode::Full,
599                },
600            )
601            .unwrap();
602            tx.commit().unwrap();
603        }
604
605        // Reset
606        {
607            let tx = db.tx_mut().unwrap();
608            reset_index_stage_checkpoints_tx(&tx).unwrap();
609            tx.commit().unwrap();
610        }
611
612        // Verify stage checkpoints are at block 0
613        let tx = db.tx().unwrap();
614        for stage_id in INDEX_STAGE_IDS {
615            let checkpoint = tx
616                .get::<tables::StageCheckpoints>(stage_id.to_string())
617                .unwrap()
618                .expect("checkpoint should exist");
619            assert_eq!(checkpoint.block_number, 0, "stage {stage_id} should be reset to block 0");
620        }
621
622        // Verify prune checkpoints are deleted
623        for segment in INDEX_PRUNE_SEGMENTS {
624            assert!(
625                tx.get::<tables::PruneCheckpoints>(segment).unwrap().is_none(),
626                "prune checkpoint for {segment} should be deleted"
627            );
628        }
629
630        // Verify sender checkpoints are left untouched.
631        let sender_stage_checkpoint = tx
632            .get::<tables::StageCheckpoints>("SenderRecovery".to_string())
633            .unwrap()
634            .expect("sender checkpoint should exist");
635        assert_eq!(sender_stage_checkpoint.block_number, tip_checkpoint.block_number);
636
637        let sender_prune_checkpoint = tx
638            .get::<tables::PruneCheckpoints>(PruneSegment::SenderRecovery)
639            .unwrap()
640            .expect("sender prune checkpoint should exist");
641        assert_eq!(sender_prune_checkpoint.block_number, Some(tip_checkpoint.block_number));
642    }
643}