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    selections: &BTreeMap<SnapshotComponentType, ComponentSelection>,
47    snapshot_block: u64,
48) -> eyre::Result<()>
49where
50    Tx: DbTx + DbTxMut,
51{
52    let segments = &config.prune.segments;
53
54    // Collect (segment, mode) pairs for all configured prune segments
55    let checkpoints: Vec<(PruneSegment, PruneMode)> = [
56        (PruneSegment::SenderRecovery, segments.sender_recovery),
57        (PruneSegment::TransactionLookup, segments.transaction_lookup),
58        (PruneSegment::Receipts, segments.receipts),
59        (PruneSegment::AccountHistory, segments.account_history),
60        (PruneSegment::StorageHistory, segments.storage_history),
61        (PruneSegment::Bodies, segments.bodies_history),
62    ]
63    .into_iter()
64    .filter_map(|(segment, mode)| mode.map(|m| (segment, m)))
65    .collect();
66
67    if checkpoints.is_empty() {
68        return Ok(());
69    }
70
71    // Look up the last tx number for the snapshot block from BlockBodyIndices
72    let tx_number =
73        tx.get::<tables::BlockBodyIndices>(snapshot_block)?.map(|indices| indices.last_tx_num());
74
75    for (segment, prune_mode) in &checkpoints {
76        let (block_number, tx_number) = match segment {
77            // Record the history actually selected, not the future pruning policy, which may
78            // retain a minimum distance even when no changesets were downloaded.
79            PruneSegment::AccountHistory | PruneSegment::StorageHistory => {
80                let component = if *segment == PruneSegment::AccountHistory {
81                    SnapshotComponentType::AccountChangesets
82                } else {
83                    SnapshotComponentType::StorageChangesets
84                };
85                let target = match selections.get(&component) {
86                    Some(ComponentSelection::All) => None,
87                    Some(ComponentSelection::Distance(distance)) => {
88                        snapshot_block.checked_sub(*distance)
89                    }
90                    Some(ComponentSelection::Since(block)) => {
91                        block.checked_sub(1).map(|target| target.min(snapshot_block))
92                    }
93                    Some(ComponentSelection::None) | None => Some(snapshot_block),
94                };
95                let Some(target) = target else { continue };
96                // Downloading a wider range cannot restore history already pruned by the source.
97                let existing = tx
98                    .get::<tables::PruneCheckpoints>(*segment)?
99                    .and_then(|checkpoint| checkpoint.block_number);
100                (existing.map_or(target, |existing| existing.max(target)), None)
101            }
102            _ => (snapshot_block, tx_number),
103        };
104        let checkpoint = PruneCheckpoint {
105            block_number: Some(block_number),
106            tx_number,
107            prune_mode: *prune_mode,
108        };
109
110        tx.put::<tables::PruneCheckpoints>(*segment, checkpoint)?;
111
112        info!(target: "reth::cli",
113            segment = %segment,
114            block = block_number,
115            tx = ?tx_number,
116            mode = ?prune_mode,
117            "Set prune checkpoint"
118        );
119    }
120
121    Ok(())
122}
123
124/// Stage IDs for index stages whose output is stored in RocksDB and is never
125/// distributed in snapshots.
126const INDEX_STAGE_IDS: [&str; 3] =
127    ["TransactionLookup", "IndexAccountHistory", "IndexStorageHistory"];
128
129/// Prune segments that correspond to the index stages.
130const INDEX_PRUNE_SEGMENTS: [PruneSegment; 3] =
131    [PruneSegment::TransactionLookup, PruneSegment::AccountHistory, PruneSegment::StorageHistory];
132
133/// Resets stage and prune checkpoints for stages whose output is not included
134/// in the snapshot inside an existing write transaction.
135///
136/// A snapshot's mdbx comes from a fully synced node, so it has stage checkpoints
137/// at the tip for `TransactionLookup`, `IndexAccountHistory`, and
138/// `IndexStorageHistory`. Since we don't distribute the rocksdb indices those
139/// stages produced, we must reset their checkpoints to block 0. Otherwise the
140/// pipeline would see "already done" and skip rebuilding entirely.
141///
142/// We intentionally do not reset `SenderRecovery`: sender static files are
143/// distributed for archive downloads, and non-archive downloads rely on the
144/// configured prune checkpoints for this segment.
145pub(crate) fn reset_index_stage_checkpoints_tx<Tx>(tx: &Tx) -> eyre::Result<()>
146where
147    Tx: DbTx + DbTxMut,
148{
149    for stage_id in INDEX_STAGE_IDS {
150        tx.put::<tables::StageCheckpoints>(stage_id.to_string(), StageCheckpoint::default())?;
151
152        // Also clear any stage-specific progress data
153        tx.delete::<tables::StageCheckpointProgresses>(stage_id.to_string(), None)?;
154
155        info!(target: "reth::cli", stage = stage_id, "Reset stage checkpoint to block 0");
156    }
157
158    // Clear corresponding prune checkpoints so the pruner doesn't inherit
159    // state from the source node
160    for segment in INDEX_PRUNE_SEGMENTS {
161        tx.delete::<tables::PruneCheckpoints>(segment, None)?;
162    }
163
164    Ok(())
165}
166
167/// Generates a [`Config`] from per-component range selections.
168///
169/// When all data components are selected as `All`, no pruning is configured (archive node).
170/// Otherwise, `--minimal` style pruning is applied for missing/partial components.
171pub(crate) fn config_for_selections(
172    selections: &BTreeMap<SnapshotComponentType, ComponentSelection>,
173    manifest: &SnapshotManifest,
174    preset: Option<SelectionPreset>,
175    chain_spec: Option<&impl EthereumHardforks>,
176) -> Config {
177    let selection_for = |ty| selections.get(&ty).copied().unwrap_or(ComponentSelection::None);
178
179    let tx_sel = selection_for(SnapshotComponentType::Transactions);
180    let senders_sel = selection_for(SnapshotComponentType::TransactionSenders);
181    let receipt_sel = selection_for(SnapshotComponentType::Receipts);
182    let account_cs_sel = selection_for(SnapshotComponentType::AccountChangesets);
183    let storage_cs_sel = selection_for(SnapshotComponentType::StorageChangesets);
184
185    // Archive node — all data components present, no pruning
186    let is_archive = [tx_sel, senders_sel, receipt_sel, account_cs_sel, storage_cs_sel]
187        .iter()
188        .all(|s| *s == ComponentSelection::All);
189
190    // Extract blocks_per_file from manifest for all component types
191    let blocks_per_file = |ty: SnapshotComponentType| -> Option<u64> {
192        match manifest.component(ty)? {
193            ComponentManifest::Chunked(c) => Some(c.blocks_per_file),
194            ComponentManifest::Single(_) => None,
195        }
196    };
197    let static_files = StaticFilesConfig {
198        blocks_per_file: BlocksPerFileConfig {
199            headers: blocks_per_file(SnapshotComponentType::Headers),
200            transactions: blocks_per_file(SnapshotComponentType::Transactions),
201            receipts: blocks_per_file(SnapshotComponentType::Receipts),
202            transaction_senders: blocks_per_file(SnapshotComponentType::TransactionSenders),
203            account_change_sets: blocks_per_file(SnapshotComponentType::AccountChangesets),
204            storage_change_sets: blocks_per_file(SnapshotComponentType::StorageChangesets),
205        },
206    };
207
208    if matches!(preset, Some(SelectionPreset::Archive)) {
209        return Config { static_files, ..Default::default() };
210    }
211
212    if matches!(preset, Some(SelectionPreset::Minimal)) {
213        return Config {
214            prune: PruneConfig {
215                segments: DefaultPruningValues::get_global().minimal_prune_modes.clone(),
216                ..Default::default()
217            },
218            static_files,
219            ..Default::default()
220        };
221    }
222
223    if matches!(preset, Some(SelectionPreset::Full)) {
224        let defaults = DefaultPruningValues::get_global();
225        let mut segments = defaults.full_prune_modes.clone();
226
227        if defaults.full_bodies_history_use_pre_merge {
228            segments.bodies_history = chain_spec.and_then(|chain_spec| {
229                chain_spec
230                    .ethereum_fork_activation(EthereumHardfork::Paris)
231                    .block_number()
232                    .map(PruneMode::Before)
233            });
234        }
235
236        return Config {
237            prune: PruneConfig { segments, ..Default::default() },
238            static_files,
239            ..Default::default()
240        };
241    }
242
243    if is_archive {
244        return Config { static_files, ..Default::default() };
245    }
246
247    let mut config = Config::default();
248    let mut prune = PruneConfig::default();
249
250    if senders_sel != ComponentSelection::All {
251        prune.segments.sender_recovery = Some(PruneMode::Full);
252    }
253    prune.segments.transaction_lookup = Some(PruneMode::Full);
254
255    if let Some(mode) = selection_to_prune_mode(tx_sel, Some(MINIMUM_UNWIND_SAFE_DISTANCE)) {
256        prune.segments.bodies_history = Some(mode);
257    }
258
259    if let Some(mode) = selection_to_prune_mode(receipt_sel, Some(MINIMUM_DISTANCE)) {
260        prune.segments.receipts = Some(mode);
261    }
262
263    if let Some(mode) = selection_to_prune_mode(account_cs_sel, Some(MINIMUM_UNWIND_SAFE_DISTANCE))
264    {
265        prune.segments.account_history = Some(mode);
266    }
267
268    if let Some(mode) = selection_to_prune_mode(storage_cs_sel, Some(MINIMUM_UNWIND_SAFE_DISTANCE))
269    {
270        prune.segments.storage_history = Some(mode);
271    }
272
273    config.prune = prune;
274    config.static_files = static_files;
275    config
276}
277
278/// Converts a [`ComponentSelection`] to an optional [`PruneMode`].
279///
280/// `min_distance` enforces the minimum blocks required for this segment.
281/// When set, `None` and distances below the minimum are clamped to it
282/// instead of producing `PruneMode::Full` which reth would reject.
283fn selection_to_prune_mode(
284    sel: ComponentSelection,
285    min_distance: Option<u64>,
286) -> Option<PruneMode> {
287    match sel {
288        ComponentSelection::All => None,
289        ComponentSelection::Distance(d) => {
290            Some(PruneMode::Distance(min_distance.map_or(d, |min| d.max(min))))
291        }
292        ComponentSelection::Since(block) => Some(PruneMode::Before(block)),
293        ComponentSelection::None => Some(min_distance.map_or(PruneMode::Full, PruneMode::Distance)),
294    }
295}
296
297/// Human-readable prune config summary.
298pub(crate) fn describe_prune_config(config: &Config) -> Vec<String> {
299    let segments = &config.prune.segments;
300
301    [
302        ("sender_recovery", segments.sender_recovery),
303        ("transaction_lookup", segments.transaction_lookup),
304        ("bodies_history", segments.bodies_history),
305        ("receipts", segments.receipts),
306        ("account_history", segments.account_history),
307        ("storage_history", segments.storage_history),
308    ]
309    .into_iter()
310    .filter_map(|(name, mode)| mode.map(|m| format!("{name}={}", format_mode(&m))))
311    .collect()
312}
313
314/// Formats one prune mode for the generated config summary.
315fn format_mode(mode: &PruneMode) -> String {
316    match mode {
317        PruneMode::Full => "\"full\"".to_string(),
318        PruneMode::Distance(d) => format!("{{ distance = {d} }}"),
319        PruneMode::Before(b) => format!("{{ before = {b} }}"),
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326    use reth_db::Database;
327
328    /// Empty manifest for tests that only care about prune config.
329    fn empty_manifest() -> SnapshotManifest {
330        SnapshotManifest {
331            block: 0,
332            chain_id: 1,
333            storage_version: 2,
334            timestamp: 0,
335            base_url: None,
336            reth_version: None,
337            components: BTreeMap::new(),
338            extensions: Default::default(),
339        }
340    }
341
342    #[test]
343    fn write_prune_checkpoints_sets_all_segments() {
344        let dir = tempfile::tempdir().unwrap();
345        let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
346
347        let mut selections = BTreeMap::new();
348        selections.insert(SnapshotComponentType::State, ComponentSelection::All);
349        selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
350        let config = config_for_selections(
351            &selections,
352            &empty_manifest(),
353            None,
354            None::<&reth_chainspec::ChainSpec>,
355        );
356        let snapshot_block = 21_000_000;
357
358        {
359            let tx = db.tx_mut().unwrap();
360            write_prune_checkpoints_tx(&tx, &config, &selections, snapshot_block).unwrap();
361            tx.commit().unwrap();
362        }
363
364        // Verify all expected segments have checkpoints
365        let tx = db.tx().unwrap();
366        for segment in [
367            PruneSegment::SenderRecovery,
368            PruneSegment::TransactionLookup,
369            PruneSegment::Receipts,
370            PruneSegment::AccountHistory,
371            PruneSegment::StorageHistory,
372            PruneSegment::Bodies,
373        ] {
374            let checkpoint = tx
375                .get::<tables::PruneCheckpoints>(segment)
376                .unwrap()
377                .unwrap_or_else(|| panic!("expected checkpoint for {segment}"));
378            assert_eq!(checkpoint.block_number, Some(snapshot_block));
379            // No BlockBodyIndices in empty DB, so tx_number should be None
380            assert_eq!(checkpoint.tx_number, None);
381        }
382    }
383
384    /// Writes prune checkpoints for state history retained over `distance` blocks.
385    fn history_checkpoint(snapshot_block: u64, distance: u64) -> Option<PruneCheckpoint> {
386        let dir = tempfile::tempdir().unwrap();
387        let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
388        let selections = BTreeMap::from([(
389            SnapshotComponentType::AccountChangesets,
390            ComponentSelection::Distance(distance),
391        )]);
392        let config = config_for_selections(
393            &selections,
394            &empty_manifest(),
395            None,
396            None::<&reth_chainspec::ChainSpec>,
397        );
398
399        let tx = db.tx_mut().unwrap();
400        write_prune_checkpoints_tx(&tx, &config, &selections, snapshot_block).unwrap();
401        tx.commit().unwrap();
402
403        db.tx().unwrap().get::<tables::PruneCheckpoints>(PruneSegment::AccountHistory).unwrap()
404    }
405
406    #[test]
407    fn write_prune_checkpoints_sets_history_to_retained_window_start() {
408        let checkpoint = history_checkpoint(21_000_000, 500_000).unwrap();
409        assert_eq!(checkpoint.block_number, Some(20_500_000));
410        assert_eq!(checkpoint.prune_mode, PruneMode::Distance(500_000));
411    }
412
413    #[test]
414    fn write_prune_checkpoints_skips_history_shorter_than_distance() {
415        assert_eq!(history_checkpoint(100_000, 500_000), None);
416    }
417
418    #[test]
419    fn write_prune_checkpoints_archive_no_checkpoints() {
420        let dir = tempfile::tempdir().unwrap();
421        let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
422
423        // Archive node — no pruning configured, so no checkpoints written
424        let mut selections = BTreeMap::new();
425        for ty in SnapshotComponentType::ALL {
426            selections.insert(ty, ComponentSelection::All);
427        }
428        let config = config_for_selections(
429            &selections,
430            &empty_manifest(),
431            None,
432            None::<&reth_chainspec::ChainSpec>,
433        );
434
435        {
436            let tx = db.tx_mut().unwrap();
437            write_prune_checkpoints_tx(&tx, &config, &selections, 21_000_000).unwrap();
438            tx.commit().unwrap();
439        }
440
441        let tx = db.tx().unwrap();
442        for segment in [PruneSegment::SenderRecovery, PruneSegment::TransactionLookup] {
443            assert!(
444                tx.get::<tables::PruneCheckpoints>(segment).unwrap().is_none(),
445                "expected no checkpoint for {segment} on archive node"
446            );
447        }
448    }
449
450    #[test]
451    fn selections_all_no_pruning() {
452        let mut selections = BTreeMap::new();
453        for ty in SnapshotComponentType::ALL {
454            selections.insert(ty, ComponentSelection::All);
455        }
456        let config = config_for_selections(
457            &selections,
458            &empty_manifest(),
459            None,
460            None::<&reth_chainspec::ChainSpec>,
461        );
462        // Archive node — nothing pruned
463        assert_eq!(config.prune.segments.transaction_lookup, None);
464        assert_eq!(config.prune.segments.sender_recovery, None);
465        assert_eq!(config.prune.segments.bodies_history, None);
466        assert_eq!(config.prune.segments.receipts, None);
467        assert_eq!(config.prune.segments.account_history, None);
468        assert_eq!(config.prune.segments.storage_history, None);
469    }
470
471    #[test]
472    fn selections_none_clamps_to_minimum_distance() {
473        let mut selections = BTreeMap::new();
474        selections.insert(SnapshotComponentType::State, ComponentSelection::All);
475        selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
476        let config = config_for_selections(
477            &selections,
478            &empty_manifest(),
479            None,
480            None::<&reth_chainspec::ChainSpec>,
481        );
482        assert_eq!(config.prune.segments.transaction_lookup, Some(PruneMode::Full));
483        assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
484        // All segments clamped to their minimum distances
485        assert_eq!(
486            config.prune.segments.bodies_history,
487            Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE))
488        );
489        assert_eq!(config.prune.segments.receipts, Some(PruneMode::Distance(MINIMUM_DISTANCE)));
490        assert_eq!(
491            config.prune.segments.account_history,
492            Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE))
493        );
494        assert_eq!(
495            config.prune.segments.storage_history,
496            Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE))
497        );
498    }
499
500    #[test]
501    fn selections_distance_maps_bodies_history() {
502        let mut selections = BTreeMap::new();
503        selections.insert(SnapshotComponentType::State, ComponentSelection::All);
504        selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
505        selections
506            .insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(10_064));
507        selections.insert(SnapshotComponentType::Receipts, ComponentSelection::None);
508        selections
509            .insert(SnapshotComponentType::AccountChangesets, ComponentSelection::Distance(10_064));
510        selections
511            .insert(SnapshotComponentType::StorageChangesets, ComponentSelection::Distance(10_064));
512        let config = config_for_selections(
513            &selections,
514            &empty_manifest(),
515            None,
516            None::<&reth_chainspec::ChainSpec>,
517        );
518
519        assert_eq!(config.prune.segments.transaction_lookup, Some(PruneMode::Full));
520        assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
521        // Bodies follows tx selection
522        assert_eq!(config.prune.segments.bodies_history, Some(PruneMode::Distance(10_064)));
523        assert_eq!(config.prune.segments.receipts, Some(PruneMode::Distance(MINIMUM_DISTANCE)));
524        assert_eq!(config.prune.segments.account_history, Some(PruneMode::Distance(10_064)));
525        assert_eq!(config.prune.segments.storage_history, Some(PruneMode::Distance(10_064)));
526    }
527
528    #[test]
529    fn selections_since_maps_to_before_prune_mode() {
530        let mut selections = BTreeMap::new();
531        selections.insert(SnapshotComponentType::State, ComponentSelection::All);
532        selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
533        selections
534            .insert(SnapshotComponentType::Transactions, ComponentSelection::Since(15_537_394));
535        selections.insert(SnapshotComponentType::Receipts, ComponentSelection::Since(15_537_394));
536        selections.insert(
537            SnapshotComponentType::AccountChangesets,
538            ComponentSelection::Since(15_537_394),
539        );
540        selections.insert(
541            SnapshotComponentType::StorageChangesets,
542            ComponentSelection::Since(15_537_394),
543        );
544
545        let config = config_for_selections(
546            &selections,
547            &empty_manifest(),
548            None,
549            None::<&reth_chainspec::ChainSpec>,
550        );
551
552        assert_eq!(config.prune.segments.bodies_history, Some(PruneMode::Before(15_537_394)));
553        assert_eq!(config.prune.segments.receipts, Some(PruneMode::Before(15_537_394)));
554        assert_eq!(config.prune.segments.account_history, Some(PruneMode::Before(15_537_394)));
555        assert_eq!(config.prune.segments.storage_history, Some(PruneMode::Before(15_537_394)));
556    }
557
558    #[test]
559    fn full_preset_matches_default_full_prune_config() {
560        let mut selections = BTreeMap::new();
561        selections.insert(SnapshotComponentType::State, ComponentSelection::All);
562        selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
563        selections
564            .insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(500_000));
565        selections.insert(SnapshotComponentType::Receipts, ComponentSelection::Distance(10_064));
566
567        let chain_spec = reth_chainspec::MAINNET.clone();
568        let config = config_for_selections(
569            &selections,
570            &empty_manifest(),
571            Some(SelectionPreset::Full),
572            Some(chain_spec.as_ref()),
573        );
574
575        assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
576        assert_eq!(config.prune.segments.transaction_lookup, None);
577        assert_eq!(
578            config.prune.segments.receipts,
579            Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE))
580        );
581        assert_eq!(
582            config.prune.segments.account_history,
583            Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE))
584        );
585        assert_eq!(
586            config.prune.segments.storage_history,
587            Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE))
588        );
589
590        let paris_block = chain_spec
591            .ethereum_fork_activation(EthereumHardfork::Paris)
592            .block_number()
593            .expect("mainnet Paris block should be known");
594        assert_eq!(config.prune.segments.bodies_history, Some(PruneMode::Before(paris_block)));
595    }
596
597    #[test]
598    fn minimal_preset_matches_default_minimal_prune_config() {
599        let config = config_for_selections(
600            &BTreeMap::new(),
601            &empty_manifest(),
602            Some(SelectionPreset::Minimal),
603            None::<&reth_chainspec::ChainSpec>,
604        );
605
606        assert_eq!(&config.prune.segments, &DefaultPruningValues::get_global().minimal_prune_modes);
607    }
608
609    #[test]
610    fn describe_selections_all_no_pruning() {
611        let mut selections = BTreeMap::new();
612        for ty in SnapshotComponentType::ALL {
613            selections.insert(ty, ComponentSelection::All);
614        }
615        let config = config_for_selections(
616            &selections,
617            &empty_manifest(),
618            None,
619            None::<&reth_chainspec::ChainSpec>,
620        );
621        let desc = describe_prune_config(&config);
622        // Archive node — no prune segments described
623        assert!(desc.is_empty());
624    }
625
626    #[test]
627    fn describe_selections_with_distances() {
628        let mut selections = BTreeMap::new();
629        selections.insert(SnapshotComponentType::State, ComponentSelection::All);
630        selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
631        selections
632            .insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(10_064));
633        selections.insert(SnapshotComponentType::Receipts, ComponentSelection::None);
634        let config = config_for_selections(
635            &selections,
636            &empty_manifest(),
637            None,
638            None::<&reth_chainspec::ChainSpec>,
639        );
640        let desc = describe_prune_config(&config);
641        assert!(desc.contains(&"sender_recovery=\"full\"".to_string()));
642        // Bodies follows tx selection
643        assert!(desc.contains(&"bodies_history={ distance = 10064 }".to_string()));
644        assert!(desc.contains(&"receipts={ distance = 64 }".to_string()));
645    }
646
647    #[test]
648    fn reset_index_stage_checkpoints_clears_only_rocksdb_index_stages() {
649        let dir = tempfile::tempdir().unwrap();
650        let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
651
652        // Simulate a fully synced node: set stage checkpoints at tip
653        let tip_checkpoint = StageCheckpoint::new(24_500_000);
654        {
655            let tx = db.tx_mut().unwrap();
656            for stage_id in INDEX_STAGE_IDS {
657                tx.put::<tables::StageCheckpoints>(stage_id.to_string(), tip_checkpoint).unwrap();
658            }
659            for segment in INDEX_PRUNE_SEGMENTS {
660                tx.put::<tables::PruneCheckpoints>(
661                    segment,
662                    PruneCheckpoint {
663                        block_number: Some(24_500_000),
664                        tx_number: None,
665                        prune_mode: PruneMode::Full,
666                    },
667                )
668                .unwrap();
669            }
670
671            // Sender recovery checkpoints should be preserved by reset.
672            tx.put::<tables::StageCheckpoints>("SenderRecovery".to_string(), tip_checkpoint)
673                .unwrap();
674            tx.put::<tables::PruneCheckpoints>(
675                PruneSegment::SenderRecovery,
676                PruneCheckpoint {
677                    block_number: Some(24_500_000),
678                    tx_number: None,
679                    prune_mode: PruneMode::Full,
680                },
681            )
682            .unwrap();
683            tx.commit().unwrap();
684        }
685
686        // Reset
687        {
688            let tx = db.tx_mut().unwrap();
689            reset_index_stage_checkpoints_tx(&tx).unwrap();
690            tx.commit().unwrap();
691        }
692
693        // Verify stage checkpoints are at block 0
694        let tx = db.tx().unwrap();
695        for stage_id in INDEX_STAGE_IDS {
696            let checkpoint = tx
697                .get::<tables::StageCheckpoints>(stage_id.to_string())
698                .unwrap()
699                .expect("checkpoint should exist");
700            assert_eq!(checkpoint.block_number, 0, "stage {stage_id} should be reset to block 0");
701        }
702
703        // Verify prune checkpoints are deleted
704        for segment in INDEX_PRUNE_SEGMENTS {
705            assert!(
706                tx.get::<tables::PruneCheckpoints>(segment).unwrap().is_none(),
707                "prune checkpoint for {segment} should be deleted"
708            );
709        }
710
711        // Verify sender checkpoints are left untouched.
712        let sender_stage_checkpoint = tx
713            .get::<tables::StageCheckpoints>("SenderRecovery".to_string())
714            .unwrap()
715            .expect("sender checkpoint should exist");
716        assert_eq!(sender_stage_checkpoint.block_number, tip_checkpoint.block_number);
717
718        let sender_prune_checkpoint = tx
719            .get::<tables::PruneCheckpoints>(PruneSegment::SenderRecovery)
720            .unwrap()
721            .expect("sender prune checkpoint should exist");
722        assert_eq!(sender_prune_checkpoint.block_number, Some(tip_checkpoint.block_number));
723    }
724
725    #[test]
726    fn history_checkpoints_do_not_advertise_undownloaded_changesets() {
727        let snapshot_block = 21_000_000;
728        for (selection, expected) in [
729            (None, Some(snapshot_block)),
730            (Some(ComponentSelection::None), Some(snapshot_block)),
731            (Some(ComponentSelection::Distance(1)), Some(snapshot_block - 1)),
732            (Some(ComponentSelection::Since(snapshot_block)), Some(snapshot_block - 1)),
733            (Some(ComponentSelection::Since(0)), None),
734            (Some(ComponentSelection::All), None),
735        ] {
736            let dir = tempfile::tempdir().unwrap();
737            let db =
738                reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
739            let mut selections = BTreeMap::from([
740                (SnapshotComponentType::State, ComponentSelection::All),
741                (SnapshotComponentType::Headers, ComponentSelection::All),
742                (SnapshotComponentType::RocksdbIndices, ComponentSelection::All),
743            ]);
744            if let Some(selection) = selection {
745                selections.insert(SnapshotComponentType::AccountChangesets, selection);
746                selections.insert(SnapshotComponentType::StorageChangesets, selection);
747            }
748            let config = config_for_selections(
749                &selections,
750                &empty_manifest(),
751                None,
752                None::<&reth_chainspec::ChainSpec>,
753            );
754            assert!(!super::super::should_reset_index_stage_checkpoints(&selections));
755            let tx = db.tx_mut().unwrap();
756            write_prune_checkpoints_tx(&tx, &config, &selections, snapshot_block).unwrap();
757            let mut checkpoints = Vec::new();
758            for segment in [PruneSegment::AccountHistory, PruneSegment::StorageHistory] {
759                let checkpoint = tx.get::<tables::PruneCheckpoints>(segment).unwrap();
760                assert_eq!(checkpoint.and_then(|checkpoint| checkpoint.block_number), expected);
761                if let Some(checkpoint) = checkpoint {
762                    checkpoints.push((segment, checkpoint));
763                }
764            }
765            if let Some(boundary) = expected {
766                assert!(config
767                    .prune
768                    .segments
769                    .ensure_unwind_target_unpruned(snapshot_block, boundary - 1, &checkpoints,)
770                    .is_err());
771                assert!(config
772                    .prune
773                    .segments
774                    .ensure_unwind_target_unpruned(snapshot_block, boundary, &checkpoints,)
775                    .is_ok());
776            }
777        }
778    }
779
780    #[test]
781    fn history_checkpoints_preserve_source_pruning_boundary() {
782        let snapshot_block = 21_000_000;
783        let selected_boundary = 20_500_000;
784        for selection in [
785            ComponentSelection::Distance(snapshot_block - selected_boundary),
786            ComponentSelection::Since(selected_boundary + 1),
787        ] {
788            for existing_boundary in [None, Some(20_000_000), Some(20_750_000)] {
789                let dir = tempfile::tempdir().unwrap();
790                let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default())
791                    .unwrap();
792                let selections = BTreeMap::from([
793                    (SnapshotComponentType::AccountChangesets, selection),
794                    (SnapshotComponentType::StorageChangesets, selection),
795                    (SnapshotComponentType::RocksdbIndices, ComponentSelection::All),
796                ]);
797                let config = config_for_selections(
798                    &selections,
799                    &empty_manifest(),
800                    None,
801                    None::<&reth_chainspec::ChainSpec>,
802                );
803                let tx = db.tx_mut().unwrap();
804                for segment in [PruneSegment::AccountHistory, PruneSegment::StorageHistory] {
805                    tx.put::<tables::PruneCheckpoints>(
806                        segment,
807                        PruneCheckpoint {
808                            block_number: existing_boundary,
809                            tx_number: Some(100),
810                            prune_mode: PruneMode::Full,
811                        },
812                    )
813                    .unwrap();
814                }
815
816                write_prune_checkpoints_tx(&tx, &config, &selections, snapshot_block).unwrap();
817
818                let expected = existing_boundary.unwrap_or_default().max(selected_boundary);
819                for (segment, mode) in [
820                    (PruneSegment::AccountHistory, config.prune.segments.account_history),
821                    (PruneSegment::StorageHistory, config.prune.segments.storage_history),
822                ] {
823                    assert_eq!(
824                        tx.get::<tables::PruneCheckpoints>(segment).unwrap(),
825                        Some(PruneCheckpoint {
826                            block_number: Some(expected),
827                            tx_number: None,
828                            prune_mode: mode.unwrap(),
829                        }),
830                    );
831                }
832            }
833        }
834    }
835}