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