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