Skip to main content

reth_cli_commands/download/
mod.rs

1//! Snapshot download command.
2//!
3//! `reth download` prepares a data directory from published snapshot archives. [`DownloadCommand`]
4//! covers both a single-archive path and a manifest-driven path, and owns the steps required to
5//! turn downloaded bytes into a bootable node directory.
6//!
7//! ## Entry modes
8//!
9//! [`DownloadCommand`] has two main execution modes:
10//!
11//! - Single-archive mode processes one `.tar.lz4` or `.tar.zst` archive from `--url`.
12//!   Depending on the source and flags, it either extracts a local `file://` archive, streams a
13//!   remote archive straight into extraction, or downloads the archive to disk first and then
14//!   extracts it.
15//! - Manifest mode resolves a [`SnapshotManifest`], turns CLI or TUI choices into
16//!   [`ComponentSelection`]s, plans the required archives, processes them, and then writes the
17//!   resulting config and database checkpoints.
18//!
19//! [`DownloadDefaults`] defines the discovery endpoints and default help text used when the command
20//! needs to discover a manifest instead of consuming an explicit source.
21//!
22//! ## Selection and planning
23//!
24//! Manifest mode first reduces user input into `ResolvedComponents`: a map of
25//! [`SnapshotComponentType`] to [`ComponentSelection`] plus an optional `SelectionPreset`.
26//! This turns CLI input (`minimal`, `full`, `archive`, or explicit `--with-*` flags) into the
27//! component selections used by the download code.
28//!
29//! The selected components are then expanded into `PlannedDownloads`, which is the set of
30//! `PlannedArchive`s that must be verified, downloaded, or reused. Planning also computes the
31//! total byte count used by progress reporting.
32//!
33//! ## Archive processing
34//!
35//! Each planned archive is processed independently, but `DownloadSession` holds the shared
36//! progress, request limit, and cancellation token for the whole command.
37//! `ArchiveProcessContext` adds the paths needed to process one archive.
38//!
39//! Archive processing is modeled around `ModularDownloadJob`, which schedules work, and
40//! `ArchiveProcessor`, which owns the explicit retry state machine for one archive.
41//! `ArchiveMode` decides whether that archive should be fetched through the cache or streamed
42//! directly:
43//!
44//! - reuse verified plain output files when possible,
45//! - otherwise fetch and extract the archive,
46//! - verify the declared output files,
47//! - retry the entire archive attempt if extraction succeeded but verification failed.
48//!
49//! Reuse and completion are based on verified output files, not on whether an old archive file is
50//! present.
51//!
52//! ## Fetch and extraction
53//!
54//! `stream_and_extract` handles the single-archive path. It supports local files, resumable
55//! downloads to disk, and direct streaming extraction.
56//!
57//! When the code needs to fetch an archive to disk, it uses `ArchiveFetcher`. The fetcher probes
58//! the remote source and chooses between a sequential download and a segmented download plan
59//! (`SegmentedDownloadPlan`). `SequentialDownloadFallback` records why a source could not use the
60//! segmented path, while `SegmentedDownload` runs the worker queue and piece retries for the
61//! parallel path.
62//!
63//! Segmented download retries individual byte ranges. Archive processing retries whole-archive
64//! attempts. These are separate layers: range retries deal with transient request failures, while
65//! archive retries deal with extraction or output verification failures.
66//!
67//! `CompressionFormat` determines how the archive stream is unpacked once bytes are available, and
68//! `OutputVerifier` checks the extracted output files before reuse or completion.
69//!
70//! ## Progress and finalization
71//!
72//! `DownloadProgress` reports progress for the single-archive path. `SharedProgress` reports
73//! aggregate progress for modular downloads. It tracks fetched bytes separately from completed
74//! bytes so repeated fetches during retries do not overstate completion.
75//!
76//! After all required archives are complete, [`DownloadCommand`] finalizes the directory by
77//! writing the derived node configuration and updating prune or index-stage checkpoints. A
78//! successful command leaves a data directory that matches the snapshot shape that was selected.
79
80mod archive;
81pub mod config_gen;
82mod extract;
83mod fetch;
84pub mod manifest;
85pub mod manifest_cmd;
86mod planning;
87mod progress;
88mod session;
89mod source;
90mod tui;
91mod verify;
92
93use crate::common::EnvironmentArgs;
94use archive::run_modular_downloads;
95use clap::{builder::RangedU64ValueParser, Parser};
96use config_gen::{config_for_selections, write_config};
97use extract::stream_and_extract;
98use eyre::Result;
99use manifest::{ComponentSelection, SnapshotComponentType, SnapshotManifest};
100use planning::{collect_planned_archives, summarize_download_startup};
101use progress::{DownloadProgress, DownloadRequestLimiter};
102use reth_chainspec::{EthChainSpec, EthereumHardfork, EthereumHardforks, MAINNET};
103use reth_cli::chainspec::ChainSpecParser;
104use reth_cli_util::cancellation::CancellationToken;
105use reth_db::{init_db, Database};
106use reth_db_api::transaction::DbTx;
107use reth_fs_util as fs;
108use reth_node_core::args::DefaultPruningValues;
109use reth_prune_types::PruneMode;
110use source::{
111    discover_manifest_url, fetch_manifest_from_source, fetch_snapshot_api_entries,
112    print_snapshot_listing, resolve_manifest_base_url,
113};
114use std::{
115    borrow::Cow,
116    collections::BTreeMap,
117    path::{Path, PathBuf},
118    sync::{Arc, OnceLock},
119};
120use tracing::info;
121use tui::{run_selector, SelectorOutput};
122
123const RETH_SNAPSHOTS_BASE_URL: &str = "https://snapshots-r2.reth.rs";
124const RETH_SNAPSHOTS_API_URL: &str = "https://snapshots.reth.rs/api/snapshots";
125const RETH_SNAPSHOTS_SOURCE: &str = "https://snapshots.reth.rs (default)";
126const SNAPSHOT_API_PATH: &str = "/api/snapshots";
127
128/// Maximum number of simultaneous HTTP downloads across the entire snapshot job.
129const MAX_CONCURRENT_DOWNLOADS: usize = 8;
130
131/// Built-in component presets for snapshot selection.
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub(crate) enum SelectionPreset {
134    /// Minimal node data needed to start from a snapshot.
135    Minimal,
136    /// Full-node data matching the default full prune settings.
137    Full,
138    /// All available snapshot data.
139    Archive,
140}
141
142struct ResolvedComponents {
143    selections: BTreeMap<SnapshotComponentType, ComponentSelection>,
144    preset: Option<SelectionPreset>,
145}
146
147/// Global static download defaults
148static DOWNLOAD_DEFAULTS: OnceLock<DownloadDefaults> = OnceLock::new();
149
150/// Download configuration defaults
151///
152/// Global defaults can be set via [`DownloadDefaults::try_init`].
153#[derive(Debug, Clone)]
154pub struct DownloadDefaults {
155    /// List of available snapshot sources
156    pub available_snapshots: Vec<Cow<'static, str>>,
157    /// Default base URL for snapshots
158    pub default_base_url: Cow<'static, str>,
159    /// Default base URL for chain-aware snapshots.
160    ///
161    /// When set, the chain ID is appended to form the full URL: `{base_url}/{chain_id}`.
162    /// For example, given a base URL of `https://snapshots.example.com` and chain ID `1`,
163    /// the resulting URL would be `https://snapshots.example.com/1`.
164    ///
165    /// Falls back to [`default_base_url`](Self::default_base_url) when `None`.
166    pub default_chain_aware_base_url: Option<Cow<'static, str>>,
167    /// URL for the snapshot discovery API that lists available snapshots.
168    ///
169    /// Defaults to `https://snapshots.reth.rs/api/snapshots`.
170    pub snapshot_api_url: Cow<'static, str>,
171    /// Optional custom long help text that overrides the generated help
172    pub long_help: Option<String>,
173}
174
175impl DownloadDefaults {
176    /// Initialize the global download defaults with this configuration
177    pub fn try_init(self) -> Result<(), Self> {
178        DOWNLOAD_DEFAULTS.set(self)
179    }
180
181    /// Get a reference to the global download defaults
182    pub fn get_global() -> &'static DownloadDefaults {
183        DOWNLOAD_DEFAULTS.get_or_init(DownloadDefaults::default_download_defaults)
184    }
185
186    /// Default download configuration with defaults from snapshots.reth.rs and publicnode
187    pub fn default_download_defaults() -> Self {
188        Self {
189            available_snapshots: vec![
190                Cow::Borrowed(RETH_SNAPSHOTS_SOURCE),
191                Cow::Borrowed("https://publicnode.com/snapshots (full nodes & testnets)"),
192            ],
193            default_base_url: Cow::Borrowed(RETH_SNAPSHOTS_BASE_URL),
194            default_chain_aware_base_url: None,
195            snapshot_api_url: Cow::Borrowed(RETH_SNAPSHOTS_API_URL),
196            long_help: None,
197        }
198    }
199
200    /// Generates the long help text for the download URL argument using these defaults.
201    ///
202    /// If a custom long_help is set, it will be returned. Otherwise, help text is generated
203    /// from the available_snapshots list.
204    pub fn long_help(&self) -> String {
205        if let Some(ref custom_help) = self.long_help {
206            return custom_help.clone();
207        }
208
209        let implicit_download_help = if self.mainnet_only_discovery() {
210            "\nIf no URL is provided, the latest archive snapshot will only be proposed\nfor Ethereum mainnet. For other chains, provide --manifest-url, --manifest-path,\nor -u explicitly."
211        } else {
212            "\nIf no URL is provided, the latest archive snapshot for the selected chain\nwill be proposed for download from "
213        };
214
215        let mut help = format!(
216            "Specify a snapshot URL or let the command propose a default one.\n\n\
217             Browse available snapshots at {}\n\
218             or use --list-snapshots to see them from the CLI.\n\nAvailable snapshot sources:\n",
219            self.snapshot_source_url(),
220        );
221
222        for source in &self.available_snapshots {
223            help.push_str("- ");
224            help.push_str(source);
225            help.push('\n');
226        }
227
228        help.push_str(implicit_download_help);
229        if !self.mainnet_only_discovery() {
230            help.push_str(
231                self.default_chain_aware_base_url.as_deref().unwrap_or(&self.default_base_url),
232            );
233            help.push('.');
234        }
235        help.push_str(
236            "\n\nLocal file:// URLs are also supported for extracting snapshots from disk.",
237        );
238        help
239    }
240
241    fn mainnet_only_discovery(&self) -> bool {
242        self.snapshot_api_url.trim_end_matches('/') == RETH_SNAPSHOTS_API_URL
243    }
244
245    fn snapshot_source_url(&self) -> &str {
246        snapshot_source_url_from_api(&self.snapshot_api_url)
247    }
248
249    /// Add a snapshot source to the list
250    pub fn with_snapshot(mut self, source: impl Into<Cow<'static, str>>) -> Self {
251        self.available_snapshots.push(source.into());
252        self
253    }
254
255    /// Replace all snapshot sources
256    pub fn with_snapshots(mut self, sources: Vec<Cow<'static, str>>) -> Self {
257        self.available_snapshots = sources;
258        self
259    }
260
261    /// Set the default base URL, e.g. `https://downloads.merkle.io`.
262    pub fn with_base_url(mut self, url: impl Into<Cow<'static, str>>) -> Self {
263        self.default_base_url = url.into();
264        self
265    }
266
267    /// Set the default chain-aware base URL.
268    pub fn with_chain_aware_base_url(mut self, url: impl Into<Cow<'static, str>>) -> Self {
269        self.default_chain_aware_base_url = Some(url.into());
270        self
271    }
272
273    /// Set the snapshot discovery API URL.
274    ///
275    /// Generated help uses the API root as the default snapshot source unless a custom
276    /// chain-aware base URL or source list was already provided.
277    pub fn with_snapshot_api_url(mut self, url: impl Into<Cow<'static, str>>) -> Self {
278        self.snapshot_api_url = url.into();
279
280        let source_url = self.snapshot_source_url().to_string();
281        if self.default_chain_aware_base_url.is_none() {
282            self.default_chain_aware_base_url = Some(Cow::Owned(source_url.clone()));
283        }
284        for source in &mut self.available_snapshots {
285            if source.as_ref() == RETH_SNAPSHOTS_SOURCE {
286                *source = Cow::Owned(format!("{source_url} (default)"));
287            }
288        }
289
290        self
291    }
292
293    /// Set one default snapshot source URL for discovery and generated CLI references.
294    ///
295    /// The provided URL is the public snapshot root, such as `https://snapshots.example.com`.
296    /// The discovery API is derived as `{url}/api/snapshots`.
297    pub fn with_snapshot_source_url(mut self, url: impl Into<Cow<'static, str>>) -> Self {
298        let source_url = normalize_snapshot_source_url(url.into());
299        self.available_snapshots = vec![Cow::Owned(format!("{} (default)", source_url.as_ref()))];
300        self.default_base_url = source_url.clone();
301        self.default_chain_aware_base_url = Some(source_url.clone());
302        self.snapshot_api_url = Cow::Owned(format!("{}{SNAPSHOT_API_PATH}", source_url.as_ref()));
303        self
304    }
305
306    /// Builder: Set custom long help text, overriding the generated help
307    pub fn with_long_help(mut self, help: impl Into<String>) -> Self {
308        self.long_help = Some(help.into());
309        self
310    }
311}
312
313fn snapshot_source_url_from_api(api_url: &str) -> &str {
314    api_url.trim_end_matches('/').trim_end_matches(SNAPSHOT_API_PATH)
315}
316
317fn normalize_snapshot_source_url(url: Cow<'static, str>) -> Cow<'static, str> {
318    match url {
319        Cow::Borrowed(url) => Cow::Borrowed(snapshot_source_url_from_api(url)),
320        Cow::Owned(url) => Cow::Owned(snapshot_source_url_from_api(&url).to_string()),
321    }
322}
323
324impl Default for DownloadDefaults {
325    /// Returns the built-in download defaults.
326    fn default() -> Self {
327        Self::default_download_defaults()
328    }
329}
330
331/// CLI command that downloads snapshot archives and configures a reth node from them.
332#[derive(Debug, Parser)]
333pub struct DownloadCommand<C: ChainSpecParser> {
334    #[command(flatten)]
335    env: EnvironmentArgs<C>,
336
337    /// Custom URL to download a single snapshot archive (legacy mode).
338    ///
339    /// When provided, downloads and extracts a single archive without component selection.
340    /// Browse available snapshots with --list-snapshots.
341    #[arg(long, short, long_help = DownloadDefaults::get_global().long_help())]
342    url: Option<String>,
343
344    /// URL to a snapshot manifest.json for modular component downloads.
345    ///
346    /// When provided, fetches this manifest instead of discovering it from the default
347    /// base URL. Useful for testing with custom or local manifests.
348    #[arg(long, value_name = "URL", conflicts_with = "url")]
349    manifest_url: Option<String>,
350
351    /// Local path to a snapshot manifest.json for modular component downloads.
352    #[arg(long, value_name = "PATH", conflicts_with_all = ["url", "manifest_url"])]
353    manifest_path: Option<PathBuf>,
354
355    /// Include all transaction static files.
356    #[arg(long, conflicts_with_all = ["with_txs_since", "with_txs_distance", "minimal", "full", "archive"])]
357    with_txs: bool,
358
359    /// Include transaction static files starting at the specified block.
360    #[arg(long, value_name = "BLOCK_NUMBER", conflicts_with_all = ["with_txs", "with_txs_distance", "minimal", "full", "archive"])]
361    with_txs_since: Option<u64>,
362
363    /// Include transaction static files covering the last N blocks.
364    #[arg(long, value_name = "BLOCKS", value_parser = RangedU64ValueParser::<u64>::new().range(1..), conflicts_with_all = ["with_txs", "with_txs_since", "minimal", "full", "archive"])]
365    with_txs_distance: Option<u64>,
366
367    /// Include all receipt static files.
368    #[arg(long, conflicts_with_all = ["with_receipts_since", "with_receipts_distance", "minimal", "full", "archive"])]
369    with_receipts: bool,
370
371    /// Include receipt static files starting at the specified block.
372    #[arg(long, value_name = "BLOCK_NUMBER", conflicts_with_all = ["with_receipts", "with_receipts_distance", "minimal", "full", "archive"])]
373    with_receipts_since: Option<u64>,
374
375    /// Include receipt static files covering the last N blocks.
376    #[arg(long, value_name = "BLOCKS", value_parser = RangedU64ValueParser::<u64>::new().range(1..), conflicts_with_all = ["with_receipts", "with_receipts_since", "minimal", "full", "archive"])]
377    with_receipts_distance: Option<u64>,
378
379    /// Include all account and storage history static files.
380    #[arg(long, alias = "with-changesets", conflicts_with_all = ["with_state_history_since", "with_state_history_distance", "minimal", "full", "archive"])]
381    with_state_history: bool,
382
383    /// Include account and storage history static files starting at the specified block.
384    #[arg(long, value_name = "BLOCK_NUMBER", conflicts_with_all = ["with_state_history", "with_state_history_distance", "minimal", "full", "archive"])]
385    with_state_history_since: Option<u64>,
386
387    /// Include account and storage history static files covering the last N blocks.
388    #[arg(long, value_name = "BLOCKS", value_parser = RangedU64ValueParser::<u64>::new().range(1..), conflicts_with_all = ["with_state_history", "with_state_history_since", "minimal", "full", "archive"])]
389    with_state_history_distance: Option<u64>,
390
391    /// Include transaction sender static files. Requires `--with-txs`.
392    #[arg(long, requires = "with_txs", conflicts_with_all = ["minimal", "full", "archive"])]
393    with_senders: bool,
394
395    /// Include RocksDB index files.
396    #[arg(long, conflicts_with_all = ["minimal", "full", "archive", "without_rocksdb"])]
397    with_rocksdb: bool,
398
399    /// Download all available components (archive node, no pruning).
400    #[arg(long, alias = "all", conflicts_with_all = ["with_txs", "with_txs_since", "with_txs_distance", "with_receipts", "with_receipts_since", "with_receipts_distance", "with_state_history", "with_state_history_since", "with_state_history_distance", "with_senders", "with_rocksdb", "minimal", "full"])]
401    archive: bool,
402
403    /// Download the minimal component set (same default as --non-interactive).
404    #[arg(long, conflicts_with_all = ["with_txs", "with_txs_since", "with_txs_distance", "with_receipts", "with_receipts_since", "with_receipts_distance", "with_state_history", "with_state_history_since", "with_state_history_distance", "with_senders", "with_rocksdb", "archive", "full"])]
405    minimal: bool,
406
407    /// Download the full node component set (matches default full prune settings).
408    #[arg(long, conflicts_with_all = ["with_txs", "with_txs_since", "with_txs_distance", "with_receipts", "with_receipts_since", "with_receipts_distance", "with_state_history", "with_state_history_since", "with_state_history_distance", "with_senders", "with_rocksdb", "archive", "minimal"])]
409    full: bool,
410
411    /// Skip optional RocksDB indices even when archive components are selected.
412    ///
413    /// This affects `--archive`/`--all` and TUI archive preset (`a`).
414    #[arg(long, conflicts_with_all = ["url", "with_rocksdb"])]
415    without_rocksdb: bool,
416
417    /// Skip interactive component selection. Downloads the minimal set
418    /// (state + headers + transactions + changesets) unless explicit --with-* flags narrow it.
419    #[arg(long, short = 'y')]
420    non_interactive: bool,
421
422    /// Enable resumable two-phase downloads (download to disk first, then extract).
423    ///
424    /// Archives are downloaded to a `.part` file with HTTP Range resume support
425    /// before extraction. This is enabled by default because it tolerates
426    /// network interruptions without restarting. Pass `--resumable=false` to
427    /// stream archives directly into the extractor instead.
428    #[arg(long, default_value_t = true, num_args = 0..=1, default_missing_value = "true")]
429    resumable: bool,
430
431    /// Maximum number of simultaneous HTTP downloads.
432    ///
433    /// Applies across the entire snapshot download. Small files use one slot,
434    /// while large files may use multiple slots by splitting into fixed-size pieces.
435    #[arg(long, default_value_t = MAX_CONCURRENT_DOWNLOADS)]
436    download_concurrency: usize,
437
438    /// List available snapshots and exit.
439    ///
440    /// Queries the snapshots API and prints all available snapshots for the selected chain,
441    /// including block number, size, and manifest URL.
442    #[arg(long, alias = "list-snapshots", conflicts_with_all = ["url", "manifest_url", "manifest_path"])]
443    list: bool,
444}
445
446impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> DownloadCommand<C> {
447    /// Runs the download command in single-archive or manifest mode.
448    pub async fn execute<N>(self) -> Result<()> {
449        let chain = self.env.chain.chain();
450        let chain_id = chain.id();
451        let data_dir = self.env.datadir.clone().resolve_datadir(chain);
452        fs::create_dir_all(&data_dir)?;
453
454        let cancel_token = CancellationToken::new();
455        let _cancel_guard = cancel_token.drop_guard();
456
457        // --list: print available snapshots and exit
458        if self.list {
459            let entries = fetch_snapshot_api_entries(chain_id).await?;
460            print_snapshot_listing(&entries, chain_id);
461            return Ok(());
462        }
463
464        // Legacy single-URL mode: download one archive and extract it
465        if let Some(ref url) = self.url {
466            let request_limiter = DownloadRequestLimiter::new(self.download_concurrency.max(1));
467            info!(target: "reth::cli",
468                dir = ?data_dir.data_dir(),
469                url = %url,
470                "Starting snapshot download and extraction"
471            );
472
473            stream_and_extract(
474                url,
475                data_dir.data_dir(),
476                None,
477                self.resumable,
478                Some(request_limiter),
479                cancel_token.clone(),
480            )
481            .await?;
482            info!(target: "reth::cli", "Snapshot downloaded and extracted successfully");
483
484            return Ok(());
485        }
486
487        let manifest = self.load_manifest(chain_id).await?;
488        let ResolvedComponents { mut selections, preset } = self.resolve_components(&manifest)?;
489
490        if matches!(preset, Some(SelectionPreset::Archive)) {
491            inject_archive_only_components(&mut selections, &manifest, !self.without_rocksdb);
492        }
493
494        let target_dir = data_dir.data_dir();
495        let planned_downloads = collect_planned_archives(&manifest, &selections)?;
496        let startup_summary = summarize_download_startup(&planned_downloads.archives, target_dir)?;
497        info!(target: "reth::cli",
498            reusable = startup_summary.reusable,
499            needs_download = startup_summary.needs_download,
500            "Startup integrity summary (plain output files)"
501        );
502
503        info!(target: "reth::cli",
504            archives = planned_downloads.total_archives(),
505            download_total = %DownloadProgress::format_size(planned_downloads.total_download_size),
506            output_total = %DownloadProgress::format_size(planned_downloads.total_output_size),
507            "Downloading all archives"
508        );
509
510        run_modular_downloads(
511            planned_downloads,
512            target_dir,
513            self.download_concurrency.max(1),
514            cancel_token.clone(),
515        )
516        .await?;
517
518        self.finalize_modular_download(&selections, &manifest, preset, target_dir, &data_dir.db())?;
519
520        Ok(())
521    }
522
523    /// Loads the manifest and resolves its effective base URL.
524    async fn load_manifest(&self, chain_id: u64) -> Result<SnapshotManifest> {
525        let manifest_source = self.resolve_manifest_source(chain_id).await?;
526
527        info!(target: "reth::cli", source = %manifest_source, "Fetching snapshot manifest");
528        let mut manifest = fetch_manifest_from_source(&manifest_source).await?;
529        manifest.base_url = Some(resolve_manifest_base_url(&manifest, &manifest_source)?);
530
531        info!(target: "reth::cli",
532            block = manifest.block,
533            chain_id = manifest.chain_id,
534            storage_version = %manifest.storage_version,
535            components = manifest.components.len(),
536            "Loaded snapshot manifest"
537        );
538
539        Ok(manifest)
540    }
541
542    /// Writes config and checkpoint state after all modular archives complete.
543    fn finalize_modular_download(
544        &self,
545        selections: &BTreeMap<SnapshotComponentType, ComponentSelection>,
546        manifest: &SnapshotManifest,
547        preset: Option<SelectionPreset>,
548        target_dir: &Path,
549        db_path: &Path,
550    ) -> Result<()> {
551        let config =
552            config_for_selections(selections, manifest, preset, Some(self.env.chain.as_ref()));
553        if write_config(&config, target_dir)? {
554            let desc = config_gen::describe_prune_config(&config);
555            info!(target: "reth::cli", "{}", desc.join(", "));
556        }
557
558        let db = init_db(db_path, self.env.db.database_args())?;
559        let should_write_prune = config.prune.segments != Default::default();
560        let should_reset_indices = should_reset_index_stage_checkpoints(selections);
561        if should_write_prune || should_reset_indices {
562            let tx = db.tx_mut()?;
563
564            if should_write_prune {
565                config_gen::write_prune_checkpoints_tx(&tx, &config, manifest.block)?;
566            }
567
568            if should_reset_indices {
569                config_gen::reset_index_stage_checkpoints_tx(&tx)?;
570            }
571
572            tx.commit()?;
573        }
574
575        let start_command = startup_node_command::<C>(self.env.chain.as_ref());
576        info!(target: "reth::cli", "Snapshot download complete. Run `{}` to start syncing.", start_command);
577
578        Ok(())
579    }
580
581    /// Determines which components to download based on CLI flags or interactive selection.
582    fn resolve_components(&self, manifest: &SnapshotManifest) -> Result<ResolvedComponents> {
583        let available = |ty: SnapshotComponentType| manifest.component(ty).is_some();
584
585        // --archive/--all: everything available as All
586        if self.archive {
587            return Ok(ResolvedComponents {
588                selections: SnapshotComponentType::ALL
589                    .iter()
590                    .copied()
591                    .filter(|ty| available(*ty))
592                    .filter(|ty| {
593                        !self.without_rocksdb || *ty != SnapshotComponentType::RocksdbIndices
594                    })
595                    .map(|ty| (ty, ComponentSelection::All))
596                    .collect(),
597                preset: Some(SelectionPreset::Archive),
598            });
599        }
600
601        if self.full {
602            return Ok(ResolvedComponents {
603                selections: self.full_preset_selections(manifest),
604                preset: Some(SelectionPreset::Full),
605            });
606        }
607
608        if self.minimal {
609            return Ok(ResolvedComponents {
610                selections: self.minimal_preset_selections(manifest),
611                preset: Some(SelectionPreset::Minimal),
612            });
613        }
614
615        let has_explicit_flags = self.with_txs ||
616            self.with_txs_since.is_some() ||
617            self.with_txs_distance.is_some() ||
618            self.with_receipts ||
619            self.with_receipts_since.is_some() ||
620            self.with_receipts_distance.is_some() ||
621            self.with_state_history ||
622            self.with_state_history_since.is_some() ||
623            self.with_state_history_distance.is_some() ||
624            self.with_senders ||
625            self.with_rocksdb;
626
627        if has_explicit_flags {
628            let mut selections = BTreeMap::new();
629            let tx_selection = explicit_component_selection(
630                self.with_txs,
631                self.with_txs_since,
632                self.with_txs_distance,
633                manifest.block,
634            );
635            let receipt_selection = explicit_component_selection(
636                self.with_receipts,
637                self.with_receipts_since,
638                self.with_receipts_distance,
639                manifest.block,
640            );
641            let state_history_selection = explicit_component_selection(
642                self.with_state_history,
643                self.with_state_history_since,
644                self.with_state_history_distance,
645                manifest.block,
646            );
647
648            // Required components always All
649            if available(SnapshotComponentType::State) {
650                selections.insert(SnapshotComponentType::State, ComponentSelection::All);
651            }
652            if available(SnapshotComponentType::Headers) {
653                selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
654            }
655            if let Some(selection) = tx_selection &&
656                available(SnapshotComponentType::Transactions)
657            {
658                selections.insert(SnapshotComponentType::Transactions, selection);
659            }
660            if let Some(selection) = receipt_selection &&
661                available(SnapshotComponentType::Receipts)
662            {
663                selections.insert(SnapshotComponentType::Receipts, selection);
664            }
665            if let Some(selection) = state_history_selection {
666                if available(SnapshotComponentType::AccountChangesets) {
667                    selections.insert(SnapshotComponentType::AccountChangesets, selection);
668                }
669                if available(SnapshotComponentType::StorageChangesets) {
670                    selections.insert(SnapshotComponentType::StorageChangesets, selection);
671                }
672            }
673            if self.with_senders && available(SnapshotComponentType::TransactionSenders) {
674                selections
675                    .insert(SnapshotComponentType::TransactionSenders, ComponentSelection::All);
676            }
677            if self.with_rocksdb && available(SnapshotComponentType::RocksdbIndices) {
678                selections.insert(SnapshotComponentType::RocksdbIndices, ComponentSelection::All);
679            }
680            return Ok(ResolvedComponents { selections, preset: None });
681        }
682
683        if self.non_interactive {
684            return Ok(ResolvedComponents {
685                selections: self.minimal_preset_selections(manifest),
686                preset: Some(SelectionPreset::Minimal),
687            });
688        }
689
690        // Interactive TUI
691        let full_preset = self.full_preset_selections(manifest);
692        let SelectorOutput { selections, preset } = run_selector(manifest.clone(), &full_preset)?;
693        let selected =
694            selections.into_iter().filter(|(_, sel)| *sel != ComponentSelection::None).collect();
695
696        Ok(ResolvedComponents { selections: selected, preset })
697    }
698
699    /// Builds the default minimal component selection for the manifest.
700    fn minimal_preset_selections(
701        &self,
702        manifest: &SnapshotManifest,
703    ) -> BTreeMap<SnapshotComponentType, ComponentSelection> {
704        SnapshotComponentType::ALL
705            .iter()
706            .copied()
707            .filter(|ty| manifest.component(*ty).is_some())
708            .map(|ty| (ty, ty.minimal_selection()))
709            .collect()
710    }
711
712    /// Builds the default full-node component selection for the manifest.
713    fn full_preset_selections(
714        &self,
715        manifest: &SnapshotManifest,
716    ) -> BTreeMap<SnapshotComponentType, ComponentSelection> {
717        let mut selections = BTreeMap::new();
718
719        for ty in [
720            SnapshotComponentType::State,
721            SnapshotComponentType::Headers,
722            SnapshotComponentType::Transactions,
723            SnapshotComponentType::Receipts,
724            SnapshotComponentType::AccountChangesets,
725            SnapshotComponentType::StorageChangesets,
726            SnapshotComponentType::TransactionSenders,
727            SnapshotComponentType::RocksdbIndices,
728        ] {
729            if manifest.component(ty).is_none() {
730                continue;
731            }
732
733            let selection = self.full_selection_for_component(ty, manifest.block);
734            if selection != ComponentSelection::None {
735                selections.insert(ty, selection);
736            }
737        }
738
739        selections
740    }
741
742    /// Returns the full preset selection for one component type.
743    fn full_selection_for_component(
744        &self,
745        ty: SnapshotComponentType,
746        snapshot_block: u64,
747    ) -> ComponentSelection {
748        let defaults = DefaultPruningValues::get_global();
749        match ty {
750            SnapshotComponentType::State | SnapshotComponentType::Headers => {
751                ComponentSelection::All
752            }
753            SnapshotComponentType::Transactions => {
754                if defaults.full_bodies_history_use_pre_merge {
755                    match self
756                        .env
757                        .chain
758                        .ethereum_fork_activation(EthereumHardfork::Paris)
759                        .block_number()
760                    {
761                        Some(paris) if snapshot_block >= paris => ComponentSelection::Since(paris),
762                        Some(_) => ComponentSelection::None,
763                        None => ComponentSelection::All,
764                    }
765                } else {
766                    selection_from_prune_mode(
767                        defaults.full_prune_modes.bodies_history,
768                        snapshot_block,
769                    )
770                }
771            }
772            SnapshotComponentType::Receipts => {
773                selection_from_prune_mode(defaults.full_prune_modes.receipts, snapshot_block)
774            }
775            SnapshotComponentType::AccountChangesets => {
776                selection_from_prune_mode(defaults.full_prune_modes.account_history, snapshot_block)
777            }
778            SnapshotComponentType::StorageChangesets => {
779                selection_from_prune_mode(defaults.full_prune_modes.storage_history, snapshot_block)
780            }
781            SnapshotComponentType::TransactionSenders => {
782                selection_from_prune_mode(defaults.full_prune_modes.sender_recovery, snapshot_block)
783            }
784            // Keep hidden by default in full mode; if users want indices they can use archive.
785            SnapshotComponentType::RocksdbIndices => ComponentSelection::None,
786        }
787    }
788
789    /// Resolves the manifest source from CLI input or snapshot discovery.
790    async fn resolve_manifest_source(&self, chain_id: u64) -> Result<String> {
791        if let Some(path) = &self.manifest_path {
792            return Ok(path.display().to_string());
793        }
794
795        match &self.manifest_url {
796            Some(url) => Ok(url.clone()),
797            None => {
798                let defaults = DownloadDefaults::get_global();
799                if defaults.mainnet_only_discovery() && chain_id != MAINNET.chain.id() {
800                    eyre::bail!(
801                        "Snapshots are only auto-discovered for Ethereum mainnet.\n\n\
802                         Chain {chain_id} requires an explicit source:\n\
803                         \t--manifest-url <URL>\n\
804                         \t--manifest-path <PATH>\n\
805                         \t-u <SNAPSHOT-URL>\n\n\
806                         Use --list to inspect snapshots exposed by {}.",
807                        defaults.snapshot_source_url(),
808                    );
809                }
810
811                discover_manifest_url(chain_id).await
812            }
813        }
814    }
815}
816
817/// Resolves explicit `--with-*` / `--with-*-since` / `--with-*-distance` flags
818/// into a component selection.
819fn explicit_component_selection(
820    all: bool,
821    since: Option<u64>,
822    distance: Option<u64>,
823    snapshot_block: u64,
824) -> Option<ComponentSelection> {
825    if all {
826        Some(ComponentSelection::All)
827    } else if let Some(block) = since {
828        (block <= snapshot_block).then_some(ComponentSelection::Since(block))
829    } else {
830        distance.map(ComponentSelection::Distance)
831    }
832}
833
834/// Converts a prune mode into the matching component selection.
835fn selection_from_prune_mode(mode: Option<PruneMode>, snapshot_block: u64) -> ComponentSelection {
836    match mode {
837        None => ComponentSelection::All,
838        Some(PruneMode::Full) => ComponentSelection::None,
839        Some(PruneMode::Distance(d)) => ComponentSelection::Distance(d),
840        Some(PruneMode::Before(block)) => {
841            if snapshot_block >= block {
842                ComponentSelection::Since(block)
843            } else {
844                ComponentSelection::None
845            }
846        }
847    }
848}
849
850/// If all data components (txs, receipts, changesets) are `All`, automatically
851/// include hidden archive-only components when available in the manifest.
852fn inject_archive_only_components(
853    selections: &mut BTreeMap<SnapshotComponentType, ComponentSelection>,
854    manifest: &SnapshotManifest,
855    include_rocksdb: bool,
856) {
857    let is_all =
858        |ty: SnapshotComponentType| selections.get(&ty).copied() == Some(ComponentSelection::All);
859
860    let is_archive = is_all(SnapshotComponentType::Transactions) &&
861        is_all(SnapshotComponentType::Receipts) &&
862        is_all(SnapshotComponentType::AccountChangesets) &&
863        is_all(SnapshotComponentType::StorageChangesets);
864
865    if !is_archive {
866        return;
867    }
868
869    for component in
870        [SnapshotComponentType::TransactionSenders, SnapshotComponentType::RocksdbIndices]
871    {
872        if component == SnapshotComponentType::RocksdbIndices && !include_rocksdb {
873            continue;
874        }
875
876        if manifest.component(component).is_some() {
877            selections.insert(component, ComponentSelection::All);
878        }
879    }
880}
881
882/// Returns `true` when RocksDB-backed index stages should be reset after download.
883fn should_reset_index_stage_checkpoints(
884    selections: &BTreeMap<SnapshotComponentType, ComponentSelection>,
885) -> bool {
886    !matches!(selections.get(&SnapshotComponentType::RocksdbIndices), Some(ComponentSelection::All))
887}
888
889fn startup_node_command<C>(chain_spec: &C::ChainSpec) -> String
890where
891    C: ChainSpecParser,
892    C::ChainSpec: EthChainSpec,
893{
894    startup_node_command_for_binary::<C>(&current_binary_name(), chain_spec)
895}
896
897fn startup_node_command_for_binary<C>(binary_name: &str, chain_spec: &C::ChainSpec) -> String
898where
899    C: ChainSpecParser,
900    C::ChainSpec: EthChainSpec,
901{
902    let mut command = format!("{binary_name} node");
903
904    if let Some(chain_arg) = startup_chain_arg::<C>(chain_spec) {
905        command.push_str(" --chain ");
906        command.push_str(&chain_arg);
907    }
908
909    command
910}
911
912fn current_binary_name() -> String {
913    std::env::args_os()
914        .next()
915        .map(PathBuf::from)
916        .and_then(|path| path.file_stem().map(|name| name.to_owned()))
917        .and_then(|name| name.into_string().ok())
918        .filter(|name| !name.is_empty())
919        .unwrap_or_else(|| "reth".to_string())
920}
921
922fn download_command() -> String {
923    download_command_for_binary(&current_binary_name())
924}
925
926fn download_command_for_binary(binary_name: &str) -> String {
927    format!("{binary_name} download")
928}
929
930fn startup_chain_arg<C>(chain_spec: &C::ChainSpec) -> Option<String>
931where
932    C: ChainSpecParser,
933    C::ChainSpec: EthChainSpec,
934{
935    let current_chain = chain_spec.chain();
936    let current_genesis_hash = chain_spec.genesis_hash();
937    let default_chain = C::default_value().and_then(|chain_name| C::parse(chain_name).ok());
938
939    if default_chain.as_ref().is_some_and(|default_chain| {
940        default_chain.chain() == current_chain &&
941            default_chain.genesis_hash() == current_genesis_hash
942    }) {
943        return None;
944    }
945
946    C::SUPPORTED_CHAINS
947        .iter()
948        .find_map(|chain_name| {
949            let parsed_chain = C::parse(chain_name).ok()?;
950            (parsed_chain.chain() == current_chain &&
951                parsed_chain.genesis_hash() == current_genesis_hash)
952                .then(|| (*chain_name).to_string())
953        })
954        .or_else(|| Some("<chain-or-chainspec>".to_string()))
955}
956
957impl<C: ChainSpecParser> DownloadCommand<C> {
958    /// Returns the underlying chain being used to run this command
959    pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
960        Some(&self.env.chain)
961    }
962}
963
964const MAX_DOWNLOAD_RETRIES: u32 = 10;
965const RETRY_BACKOFF_SECS: u64 = 5;
966
967#[cfg(test)]
968mod tests {
969    use super::*;
970    use clap::{Args, Parser};
971    use extract::CompressionFormat;
972    use manifest::{ComponentManifest, SingleArchive};
973    use reth_chainspec::{HOLESKY, MAINNET};
974    use reth_ethereum_cli::chainspec::EthereumChainSpecParser;
975
976    #[derive(Parser)]
977    struct CommandParser<T: Args> {
978        #[command(flatten)]
979        args: T,
980    }
981
982    fn manifest_with_archive_only_components() -> SnapshotManifest {
983        let mut components = BTreeMap::new();
984        components.insert(
985            SnapshotComponentType::TransactionSenders.key().to_string(),
986            ComponentManifest::Single(SingleArchive {
987                file: "transaction_senders.tar.zst".to_string(),
988                size: 1,
989                decompressed_size: 0,
990                blake3: None,
991                output_files: vec![],
992            }),
993        );
994        components.insert(
995            SnapshotComponentType::RocksdbIndices.key().to_string(),
996            ComponentManifest::Single(SingleArchive {
997                file: "rocksdb_indices.tar.zst".to_string(),
998                size: 1,
999                decompressed_size: 0,
1000                blake3: None,
1001                output_files: vec![],
1002            }),
1003        );
1004        SnapshotManifest {
1005            block: 0,
1006            chain_id: 1,
1007            storage_version: 2,
1008            timestamp: 0,
1009            base_url: Some("https://example.com".to_string()),
1010            reth_version: None,
1011            components,
1012        }
1013    }
1014
1015    #[test]
1016    fn test_download_defaults_builder() {
1017        let defaults = DownloadDefaults::default()
1018            .with_snapshot("https://example.com/snapshots (example)")
1019            .with_base_url("https://example.com");
1020
1021        assert_eq!(defaults.default_base_url, "https://example.com");
1022        assert_eq!(defaults.available_snapshots.len(), 3); // 2 defaults + 1 added
1023    }
1024
1025    #[test]
1026    fn test_download_defaults_replace_snapshots() {
1027        let defaults = DownloadDefaults::default().with_snapshots(vec![
1028            Cow::Borrowed("https://custom1.com"),
1029            Cow::Borrowed("https://custom2.com"),
1030        ]);
1031
1032        assert_eq!(defaults.available_snapshots.len(), 2);
1033        assert_eq!(defaults.available_snapshots[0], "https://custom1.com");
1034    }
1035
1036    #[test]
1037    fn test_long_help_generation() {
1038        let defaults = DownloadDefaults::default();
1039        let help = defaults.long_help();
1040
1041        assert!(help.contains("Available snapshot sources:"));
1042        assert!(help.contains("Ethereum mainnet"));
1043        assert!(help.contains("snapshots.reth.rs"));
1044        assert!(help.contains("publicnode.com"));
1045        assert!(help.contains("file://"));
1046    }
1047
1048    #[test]
1049    fn test_custom_snapshot_api_keeps_selected_chain_help() {
1050        let defaults = DownloadDefaults::default()
1051            .with_snapshot_api_url("https://snapshots.tempoxyz.dev/api/snapshots");
1052        let help = defaults.long_help();
1053
1054        assert_eq!(
1055            defaults.default_chain_aware_base_url.as_deref(),
1056            Some("https://snapshots.tempoxyz.dev")
1057        );
1058        assert!(help.contains("Browse available snapshots at https://snapshots.tempoxyz.dev"));
1059        assert!(help.contains("- https://snapshots.tempoxyz.dev (default)"));
1060        assert!(help.contains("selected chain"));
1061        assert!(!help.contains("Ethereum mainnet"));
1062        assert!(!help.contains("snapshots.reth.rs"));
1063    }
1064
1065    #[test]
1066    fn test_snapshot_source_url_sets_generated_references() {
1067        let defaults =
1068            DownloadDefaults::default().with_snapshot_source_url("https://snapshots.tempoxyz.dev/");
1069        let help = defaults.long_help();
1070
1071        assert_eq!(defaults.snapshot_api_url, "https://snapshots.tempoxyz.dev/api/snapshots");
1072        assert_eq!(defaults.default_base_url, "https://snapshots.tempoxyz.dev");
1073        assert_eq!(
1074            defaults.default_chain_aware_base_url.as_deref(),
1075            Some("https://snapshots.tempoxyz.dev")
1076        );
1077        assert_eq!(
1078            defaults.available_snapshots.iter().map(|source| source.as_ref()).collect::<Vec<_>>(),
1079            vec!["https://snapshots.tempoxyz.dev (default)"]
1080        );
1081        assert!(!defaults.mainnet_only_discovery());
1082        assert!(help.contains("Browse available snapshots at https://snapshots.tempoxyz.dev"));
1083        assert!(help.contains("from https://snapshots.tempoxyz.dev."));
1084    }
1085
1086    #[test]
1087    fn test_snapshot_api_url_trailing_slash_sets_source_url() {
1088        let defaults = DownloadDefaults::default()
1089            .with_snapshot_api_url("https://snapshots.tempoxyz.dev/api/snapshots/");
1090        let help = defaults.long_help();
1091
1092        assert_eq!(
1093            defaults.default_chain_aware_base_url.as_deref(),
1094            Some("https://snapshots.tempoxyz.dev")
1095        );
1096        assert!(help.contains("Browse available snapshots at https://snapshots.tempoxyz.dev"));
1097        assert!(help.contains("- https://snapshots.tempoxyz.dev (default)"));
1098    }
1099
1100    #[test]
1101    fn test_long_help_override() {
1102        let custom_help = "This is custom help text for downloading snapshots.";
1103        let defaults = DownloadDefaults::default().with_long_help(custom_help);
1104
1105        let help = defaults.long_help();
1106        assert_eq!(help, custom_help);
1107        assert!(!help.contains("Available snapshot sources:"));
1108    }
1109
1110    #[test]
1111    fn test_builder_chaining() {
1112        let defaults = DownloadDefaults::default()
1113            .with_base_url("https://custom.example.com")
1114            .with_snapshot("https://snapshot1.com")
1115            .with_snapshot("https://snapshot2.com")
1116            .with_long_help("Custom help for snapshots");
1117
1118        assert_eq!(defaults.default_base_url, "https://custom.example.com");
1119        assert_eq!(defaults.available_snapshots.len(), 4); // 2 defaults + 2 added
1120        assert_eq!(defaults.long_help, Some("Custom help for snapshots".to_string()));
1121    }
1122
1123    #[test]
1124    fn test_download_resumable_defaults_to_true() {
1125        let args =
1126            CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from(["reth"]).args;
1127
1128        assert!(args.resumable);
1129    }
1130
1131    #[test]
1132    fn test_download_resumable_implicit_true() {
1133        let args = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from([
1134            "reth",
1135            "--resumable",
1136        ])
1137        .args;
1138
1139        assert!(args.resumable);
1140    }
1141
1142    #[test]
1143    fn test_download_resumable_explicit_false() {
1144        let args = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from([
1145            "reth",
1146            "--resumable=false",
1147        ])
1148        .args;
1149
1150        assert!(!args.resumable);
1151    }
1152
1153    #[test]
1154    fn resolve_manifest_source_requires_explicit_source_for_non_mainnet_defaults() {
1155        let args = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from([
1156            "reth", "--chain", "holesky",
1157        ])
1158        .args;
1159
1160        let err = tokio::runtime::Runtime::new()
1161            .unwrap()
1162            .block_on(args.resolve_manifest_source(HOLESKY.chain.id()))
1163            .unwrap_err();
1164
1165        let message = err.to_string();
1166        assert!(message.contains("only auto-discovered for Ethereum mainnet"));
1167        assert!(message.contains("--manifest-url <URL>"));
1168        assert!(message.contains("-u <SNAPSHOT-URL>"));
1169    }
1170
1171    #[test]
1172    fn resolve_manifest_source_allows_manifest_path_for_non_mainnet_defaults() {
1173        let args = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from([
1174            "reth",
1175            "--chain",
1176            "holesky",
1177            "--manifest-path",
1178            "./manifest.json",
1179        ])
1180        .args;
1181
1182        let source = tokio::runtime::Runtime::new()
1183            .unwrap()
1184            .block_on(args.resolve_manifest_source(HOLESKY.chain.id()))
1185            .unwrap();
1186
1187        assert_eq!(source, "./manifest.json");
1188    }
1189
1190    #[test]
1191    fn test_compression_format_detection() {
1192        assert!(matches!(
1193            CompressionFormat::from_url("https://example.com/snapshot.tar.lz4"),
1194            Ok(CompressionFormat::Lz4)
1195        ));
1196        assert!(matches!(
1197            CompressionFormat::from_url("https://example.com/snapshot.tar.zst"),
1198            Ok(CompressionFormat::Zstd)
1199        ));
1200        assert!(matches!(
1201            CompressionFormat::from_url("file:///path/to/snapshot.tar.lz4"),
1202            Ok(CompressionFormat::Lz4)
1203        ));
1204        assert!(matches!(
1205            CompressionFormat::from_url("file:///path/to/snapshot.tar.zst"),
1206            Ok(CompressionFormat::Zstd)
1207        ));
1208        assert!(CompressionFormat::from_url("https://example.com/snapshot.tar.gz").is_err());
1209    }
1210
1211    #[test]
1212    fn inject_archive_only_components_for_archive_selection() {
1213        let manifest = manifest_with_archive_only_components();
1214        let mut selections = BTreeMap::new();
1215        selections.insert(SnapshotComponentType::Transactions, ComponentSelection::All);
1216        selections.insert(SnapshotComponentType::Receipts, ComponentSelection::All);
1217        selections.insert(SnapshotComponentType::AccountChangesets, ComponentSelection::All);
1218        selections.insert(SnapshotComponentType::StorageChangesets, ComponentSelection::All);
1219
1220        inject_archive_only_components(&mut selections, &manifest, true);
1221
1222        assert_eq!(
1223            selections.get(&SnapshotComponentType::TransactionSenders),
1224            Some(&ComponentSelection::All)
1225        );
1226        assert_eq!(
1227            selections.get(&SnapshotComponentType::RocksdbIndices),
1228            Some(&ComponentSelection::All)
1229        );
1230    }
1231
1232    #[test]
1233    fn inject_archive_only_components_without_rocksdb() {
1234        let manifest = manifest_with_archive_only_components();
1235        let mut selections = BTreeMap::new();
1236        selections.insert(SnapshotComponentType::Transactions, ComponentSelection::All);
1237        selections.insert(SnapshotComponentType::Receipts, ComponentSelection::All);
1238        selections.insert(SnapshotComponentType::AccountChangesets, ComponentSelection::All);
1239        selections.insert(SnapshotComponentType::StorageChangesets, ComponentSelection::All);
1240
1241        inject_archive_only_components(&mut selections, &manifest, false);
1242
1243        assert_eq!(
1244            selections.get(&SnapshotComponentType::TransactionSenders),
1245            Some(&ComponentSelection::All)
1246        );
1247        assert_eq!(selections.get(&SnapshotComponentType::RocksdbIndices), None);
1248    }
1249
1250    #[test]
1251    fn should_reset_index_stage_checkpoints_without_rocksdb_indices() {
1252        let mut selections = BTreeMap::new();
1253        selections.insert(SnapshotComponentType::Transactions, ComponentSelection::All);
1254        assert!(should_reset_index_stage_checkpoints(&selections));
1255
1256        selections.insert(SnapshotComponentType::RocksdbIndices, ComponentSelection::All);
1257        assert!(!should_reset_index_stage_checkpoints(&selections));
1258    }
1259
1260    #[test]
1261    fn startup_node_command_omits_default_chain_arg() {
1262        let command =
1263            startup_node_command_for_binary::<EthereumChainSpecParser>("reth", MAINNET.as_ref());
1264
1265        assert_eq!(command, "reth node");
1266    }
1267
1268    #[test]
1269    fn startup_node_command_includes_non_default_chain_arg() {
1270        let command =
1271            startup_node_command_for_binary::<EthereumChainSpecParser>("reth", HOLESKY.as_ref());
1272
1273        assert_eq!(command, "reth node --chain holesky");
1274    }
1275
1276    #[test]
1277    fn startup_node_command_uses_running_binary_name() {
1278        let command =
1279            startup_node_command_for_binary::<EthereumChainSpecParser>("tempo", HOLESKY.as_ref());
1280
1281        assert_eq!(command, "tempo node --chain holesky");
1282    }
1283
1284    #[test]
1285    fn download_command_uses_binary_name() {
1286        assert_eq!(download_command_for_binary("tempo"), "tempo download");
1287    }
1288}