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::None => Some(min_distance.map_or(PruneMode::Full, PruneMode::Distance)),
252    }
253}
254
255/// Human-readable prune config summary.
256pub(crate) fn describe_prune_config(config: &Config) -> Vec<String> {
257    let segments = &config.prune.segments;
258
259    [
260        ("sender_recovery", segments.sender_recovery),
261        ("transaction_lookup", segments.transaction_lookup),
262        ("bodies_history", segments.bodies_history),
263        ("receipts", segments.receipts),
264        ("account_history", segments.account_history),
265        ("storage_history", segments.storage_history),
266    ]
267    .into_iter()
268    .filter_map(|(name, mode)| mode.map(|m| format!("{name}={}", format_mode(&m))))
269    .collect()
270}
271
272fn format_mode(mode: &PruneMode) -> String {
273    match mode {
274        PruneMode::Full => "\"full\"".to_string(),
275        PruneMode::Distance(d) => format!("{{ distance = {d} }}"),
276        PruneMode::Before(b) => format!("{{ before = {b} }}"),
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use reth_db::Database;
284
285    /// Empty manifest for tests that only care about prune config.
286    fn empty_manifest() -> SnapshotManifest {
287        SnapshotManifest {
288            block: 0,
289            chain_id: 1,
290            storage_version: 2,
291            timestamp: 0,
292            base_url: None,
293            reth_version: None,
294            components: BTreeMap::new(),
295        }
296    }
297
298    #[test]
299    fn write_prune_checkpoints_sets_all_segments() {
300        let dir = tempfile::tempdir().unwrap();
301        let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
302
303        let mut selections = BTreeMap::new();
304        selections.insert(SnapshotComponentType::State, ComponentSelection::All);
305        selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
306        let config = config_for_selections(
307            &selections,
308            &empty_manifest(),
309            None,
310            None::<&reth_chainspec::ChainSpec>,
311        );
312        let snapshot_block = 21_000_000;
313
314        {
315            let tx = db.tx_mut().unwrap();
316            write_prune_checkpoints_tx(&tx, &config, snapshot_block).unwrap();
317            tx.commit().unwrap();
318        }
319
320        // Verify all expected segments have checkpoints
321        let tx = db.tx().unwrap();
322        for segment in [
323            PruneSegment::SenderRecovery,
324            PruneSegment::TransactionLookup,
325            PruneSegment::Receipts,
326            PruneSegment::AccountHistory,
327            PruneSegment::StorageHistory,
328            PruneSegment::Bodies,
329        ] {
330            let checkpoint = tx
331                .get::<tables::PruneCheckpoints>(segment)
332                .unwrap()
333                .unwrap_or_else(|| panic!("expected checkpoint for {segment}"));
334            assert_eq!(checkpoint.block_number, Some(snapshot_block));
335            // No BlockBodyIndices in empty DB, so tx_number should be None
336            assert_eq!(checkpoint.tx_number, None);
337        }
338    }
339
340    #[test]
341    fn write_prune_checkpoints_archive_no_checkpoints() {
342        let dir = tempfile::tempdir().unwrap();
343        let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
344
345        // Archive node — no pruning configured, so no checkpoints written
346        let mut selections = BTreeMap::new();
347        for ty in SnapshotComponentType::ALL {
348            selections.insert(ty, ComponentSelection::All);
349        }
350        let config = config_for_selections(
351            &selections,
352            &empty_manifest(),
353            None,
354            None::<&reth_chainspec::ChainSpec>,
355        );
356
357        {
358            let tx = db.tx_mut().unwrap();
359            write_prune_checkpoints_tx(&tx, &config, 21_000_000).unwrap();
360            tx.commit().unwrap();
361        }
362
363        let tx = db.tx().unwrap();
364        for segment in [PruneSegment::SenderRecovery, PruneSegment::TransactionLookup] {
365            assert!(
366                tx.get::<tables::PruneCheckpoints>(segment).unwrap().is_none(),
367                "expected no checkpoint for {segment} on archive node"
368            );
369        }
370    }
371
372    #[test]
373    fn selections_all_no_pruning() {
374        let mut selections = BTreeMap::new();
375        for ty in SnapshotComponentType::ALL {
376            selections.insert(ty, ComponentSelection::All);
377        }
378        let config = config_for_selections(
379            &selections,
380            &empty_manifest(),
381            None,
382            None::<&reth_chainspec::ChainSpec>,
383        );
384        // Archive node — nothing pruned
385        assert_eq!(config.prune.segments.transaction_lookup, None);
386        assert_eq!(config.prune.segments.sender_recovery, None);
387        assert_eq!(config.prune.segments.bodies_history, None);
388        assert_eq!(config.prune.segments.receipts, None);
389        assert_eq!(config.prune.segments.account_history, None);
390        assert_eq!(config.prune.segments.storage_history, None);
391    }
392
393    #[test]
394    fn selections_none_clamps_to_minimum_distance() {
395        let mut selections = BTreeMap::new();
396        selections.insert(SnapshotComponentType::State, ComponentSelection::All);
397        selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
398        let config = config_for_selections(
399            &selections,
400            &empty_manifest(),
401            None,
402            None::<&reth_chainspec::ChainSpec>,
403        );
404        assert_eq!(config.prune.segments.transaction_lookup, Some(PruneMode::Full));
405        assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
406        // All segments clamped to their minimum distances
407        assert_eq!(
408            config.prune.segments.bodies_history,
409            Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
410        );
411        assert_eq!(
412            config.prune.segments.receipts,
413            Some(PruneMode::Distance(MINIMUM_RECEIPTS_DISTANCE))
414        );
415        assert_eq!(
416            config.prune.segments.account_history,
417            Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
418        );
419        assert_eq!(
420            config.prune.segments.storage_history,
421            Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
422        );
423    }
424
425    #[test]
426    fn selections_distance_maps_bodies_history() {
427        let mut selections = BTreeMap::new();
428        selections.insert(SnapshotComponentType::State, ComponentSelection::All);
429        selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
430        selections
431            .insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(10_064));
432        selections.insert(SnapshotComponentType::Receipts, ComponentSelection::None);
433        selections
434            .insert(SnapshotComponentType::AccountChangesets, ComponentSelection::Distance(10_064));
435        selections
436            .insert(SnapshotComponentType::StorageChangesets, ComponentSelection::Distance(10_064));
437        let config = config_for_selections(
438            &selections,
439            &empty_manifest(),
440            None,
441            None::<&reth_chainspec::ChainSpec>,
442        );
443
444        assert_eq!(config.prune.segments.transaction_lookup, Some(PruneMode::Full));
445        assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
446        // Bodies follows tx selection
447        assert_eq!(config.prune.segments.bodies_history, Some(PruneMode::Distance(10_064)));
448        assert_eq!(
449            config.prune.segments.receipts,
450            Some(PruneMode::Distance(MINIMUM_RECEIPTS_DISTANCE))
451        );
452        assert_eq!(config.prune.segments.account_history, Some(PruneMode::Distance(10_064)));
453        assert_eq!(config.prune.segments.storage_history, Some(PruneMode::Distance(10_064)));
454    }
455
456    #[test]
457    fn full_preset_matches_default_full_prune_config() {
458        let mut selections = BTreeMap::new();
459        selections.insert(SnapshotComponentType::State, ComponentSelection::All);
460        selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
461        selections
462            .insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(500_000));
463        selections.insert(SnapshotComponentType::Receipts, ComponentSelection::Distance(10_064));
464
465        let chain_spec = reth_chainspec::MAINNET.clone();
466        let config = config_for_selections(
467            &selections,
468            &empty_manifest(),
469            Some(SelectionPreset::Full),
470            Some(chain_spec.as_ref()),
471        );
472
473        assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
474        assert_eq!(config.prune.segments.transaction_lookup, None);
475        assert_eq!(
476            config.prune.segments.receipts,
477            Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
478        );
479        assert_eq!(
480            config.prune.segments.account_history,
481            Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
482        );
483        assert_eq!(
484            config.prune.segments.storage_history,
485            Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
486        );
487
488        let paris_block = chain_spec
489            .ethereum_fork_activation(EthereumHardfork::Paris)
490            .block_number()
491            .expect("mainnet Paris block should be known");
492        assert_eq!(config.prune.segments.bodies_history, Some(PruneMode::Before(paris_block)));
493    }
494
495    #[test]
496    fn describe_selections_all_no_pruning() {
497        let mut selections = BTreeMap::new();
498        for ty in SnapshotComponentType::ALL {
499            selections.insert(ty, ComponentSelection::All);
500        }
501        let config = config_for_selections(
502            &selections,
503            &empty_manifest(),
504            None,
505            None::<&reth_chainspec::ChainSpec>,
506        );
507        let desc = describe_prune_config(&config);
508        // Archive node — no prune segments described
509        assert!(desc.is_empty());
510    }
511
512    #[test]
513    fn describe_selections_with_distances() {
514        let mut selections = BTreeMap::new();
515        selections.insert(SnapshotComponentType::State, ComponentSelection::All);
516        selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
517        selections
518            .insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(10_064));
519        selections.insert(SnapshotComponentType::Receipts, ComponentSelection::None);
520        let config = config_for_selections(
521            &selections,
522            &empty_manifest(),
523            None,
524            None::<&reth_chainspec::ChainSpec>,
525        );
526        let desc = describe_prune_config(&config);
527        assert!(desc.contains(&"sender_recovery=\"full\"".to_string()));
528        // Bodies follows tx selection
529        assert!(desc.contains(&"bodies_history={ distance = 10064 }".to_string()));
530        assert!(desc.contains(&"receipts={ distance = 64 }".to_string()));
531    }
532
533    #[test]
534    fn reset_index_stage_checkpoints_clears_only_rocksdb_index_stages() {
535        let dir = tempfile::tempdir().unwrap();
536        let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
537
538        // Simulate a fully synced node: set stage checkpoints at tip
539        let tip_checkpoint = StageCheckpoint::new(24_500_000);
540        {
541            let tx = db.tx_mut().unwrap();
542            for stage_id in INDEX_STAGE_IDS {
543                tx.put::<tables::StageCheckpoints>(stage_id.to_string(), tip_checkpoint).unwrap();
544            }
545            for segment in INDEX_PRUNE_SEGMENTS {
546                tx.put::<tables::PruneCheckpoints>(
547                    segment,
548                    PruneCheckpoint {
549                        block_number: Some(24_500_000),
550                        tx_number: None,
551                        prune_mode: PruneMode::Full,
552                    },
553                )
554                .unwrap();
555            }
556
557            // Sender recovery checkpoints should be preserved by reset.
558            tx.put::<tables::StageCheckpoints>("SenderRecovery".to_string(), tip_checkpoint)
559                .unwrap();
560            tx.put::<tables::PruneCheckpoints>(
561                PruneSegment::SenderRecovery,
562                PruneCheckpoint {
563                    block_number: Some(24_500_000),
564                    tx_number: None,
565                    prune_mode: PruneMode::Full,
566                },
567            )
568            .unwrap();
569            tx.commit().unwrap();
570        }
571
572        // Reset
573        {
574            let tx = db.tx_mut().unwrap();
575            reset_index_stage_checkpoints_tx(&tx).unwrap();
576            tx.commit().unwrap();
577        }
578
579        // Verify stage checkpoints are at block 0
580        let tx = db.tx().unwrap();
581        for stage_id in INDEX_STAGE_IDS {
582            let checkpoint = tx
583                .get::<tables::StageCheckpoints>(stage_id.to_string())
584                .unwrap()
585                .expect("checkpoint should exist");
586            assert_eq!(checkpoint.block_number, 0, "stage {stage_id} should be reset to block 0");
587        }
588
589        // Verify prune checkpoints are deleted
590        for segment in INDEX_PRUNE_SEGMENTS {
591            assert!(
592                tx.get::<tables::PruneCheckpoints>(segment).unwrap().is_none(),
593                "prune checkpoint for {segment} should be deleted"
594            );
595        }
596
597        // Verify sender checkpoints are left untouched.
598        let sender_stage_checkpoint = tx
599            .get::<tables::StageCheckpoints>("SenderRecovery".to_string())
600            .unwrap()
601            .expect("sender checkpoint should exist");
602        assert_eq!(sender_stage_checkpoint.block_number, tip_checkpoint.block_number);
603
604        let sender_prune_checkpoint = tx
605            .get::<tables::PruneCheckpoints>(PruneSegment::SenderRecovery)
606            .unwrap()
607            .expect("sender prune checkpoint should exist");
608        assert_eq!(sender_prune_checkpoint.block_number, Some(tip_checkpoint.block_number));
609    }
610}