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