1mod 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
93pub use planning::{DownloadPlan, DownloadPlanArchive};
94
95use crate::common::EnvironmentArgs;
96use archive::run_modular_downloads;
97use clap::{builder::RangedU64ValueParser, Parser};
98use config_gen::{config_for_selections, write_config};
99use extract::stream_and_extract;
100use eyre::Result;
101use manifest::{ComponentSelection, SnapshotComponentType, SnapshotManifest};
102use planning::{collect_planned_archives, summarize_download_startup, PlannedDownloads};
103use progress::{DownloadProgress, DownloadRequestLimiter};
104use reth_chainspec::{EthChainSpec, EthereumHardfork, EthereumHardforks, MAINNET};
105use reth_cli::chainspec::ChainSpecParser;
106use reth_cli_util::cancellation::CancellationToken;
107use reth_db::{init_db, Database};
108use reth_db_api::transaction::DbTx;
109use reth_fs_util as fs;
110use reth_node_core::args::DefaultPruningValues;
111use reth_prune_types::{PruneMode, PruneModes};
112use source::{
113 discover_manifest_url, fetch_manifest_from_source, fetch_snapshot_api_entries,
114 print_snapshot_listing, resolve_manifest_base_url,
115};
116use std::{
117 borrow::Cow,
118 collections::BTreeMap,
119 path::{Path, PathBuf},
120 sync::{Arc, OnceLock},
121 time::Duration,
122};
123use tracing::info;
124use tui::{run_selector, SelectorOutput};
125
126const RETH_SNAPSHOTS_BASE_URL: &str = "https://snapshots-r2.reth.rs";
127const RETH_SNAPSHOTS_API_URL: &str = "https://snapshots.reth.rs/api/snapshots";
128const RETH_SNAPSHOTS_SOURCE: &str = "https://snapshots.reth.rs (default)";
129const SNAPSHOT_API_PATH: &str = "/api/snapshots";
130const FORCE_REMOVED_DATADIR_PATHS: &[&str] = &["db", "rocksdb", "static_files", "reth.toml"];
131
132const MAX_CONCURRENT_DOWNLOADS: usize = 8;
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub(crate) enum SelectionPreset {
138 Minimal,
140 Full,
142 Archive,
144}
145
146struct ResolvedComponents {
147 selections: BTreeMap<SnapshotComponentType, ComponentSelection>,
148 preset: Option<SelectionPreset>,
149}
150
151static DOWNLOAD_DEFAULTS: OnceLock<DownloadDefaults> = OnceLock::new();
153
154#[derive(Debug, Clone)]
158pub struct DownloadDefaults {
159 pub available_snapshots: Vec<Cow<'static, str>>,
161 pub default_base_url: Cow<'static, str>,
163 pub default_chain_aware_base_url: Option<Cow<'static, str>>,
171 pub snapshot_api_url: Cow<'static, str>,
175 pub long_help: Option<String>,
177}
178
179impl DownloadDefaults {
180 pub fn try_init(self) -> Result<(), Self> {
182 DOWNLOAD_DEFAULTS.set(self)
183 }
184
185 pub fn get_global() -> &'static DownloadDefaults {
187 DOWNLOAD_DEFAULTS.get_or_init(DownloadDefaults::default_download_defaults)
188 }
189
190 pub fn default_download_defaults() -> Self {
192 Self {
193 available_snapshots: vec![
194 Cow::Borrowed(RETH_SNAPSHOTS_SOURCE),
195 Cow::Borrowed("https://publicnode.com/snapshots (full nodes & testnets)"),
196 ],
197 default_base_url: Cow::Borrowed(RETH_SNAPSHOTS_BASE_URL),
198 default_chain_aware_base_url: None,
199 snapshot_api_url: Cow::Borrowed(RETH_SNAPSHOTS_API_URL),
200 long_help: None,
201 }
202 }
203
204 pub fn long_help(&self) -> String {
209 if let Some(ref custom_help) = self.long_help {
210 return custom_help.clone();
211 }
212
213 let implicit_download_help = if self.mainnet_only_discovery() {
214 "\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."
215 } else {
216 "\nIf no URL is provided, the latest archive snapshot for the selected chain\nwill be proposed for download from "
217 };
218
219 let mut help = format!(
220 "Specify a snapshot URL or let the command propose a default one.\n\n\
221 Browse available snapshots at {}\n\
222 or use --list-snapshots to see them from the CLI.\n\nAvailable snapshot sources:\n",
223 self.snapshot_source_url(),
224 );
225
226 for source in &self.available_snapshots {
227 help.push_str("- ");
228 help.push_str(source);
229 help.push('\n');
230 }
231
232 help.push_str(implicit_download_help);
233 if !self.mainnet_only_discovery() {
234 help.push_str(
235 self.default_chain_aware_base_url.as_deref().unwrap_or(&self.default_base_url),
236 );
237 help.push('.');
238 }
239 help.push_str(
240 "\n\nLocal file:// URLs are also supported for extracting snapshots from disk.",
241 );
242 help
243 }
244
245 fn mainnet_only_discovery(&self) -> bool {
246 self.snapshot_api_url.trim_end_matches('/') == RETH_SNAPSHOTS_API_URL
247 }
248
249 fn snapshot_source_url(&self) -> &str {
250 snapshot_source_url_from_api(&self.snapshot_api_url)
251 }
252
253 pub fn with_snapshot(mut self, source: impl Into<Cow<'static, str>>) -> Self {
255 self.available_snapshots.push(source.into());
256 self
257 }
258
259 pub fn with_snapshots(mut self, sources: Vec<Cow<'static, str>>) -> Self {
261 self.available_snapshots = sources;
262 self
263 }
264
265 pub fn with_base_url(mut self, url: impl Into<Cow<'static, str>>) -> Self {
267 self.default_base_url = url.into();
268 self
269 }
270
271 pub fn with_chain_aware_base_url(mut self, url: impl Into<Cow<'static, str>>) -> Self {
273 self.default_chain_aware_base_url = Some(url.into());
274 self
275 }
276
277 pub fn with_snapshot_api_url(mut self, url: impl Into<Cow<'static, str>>) -> Self {
282 self.snapshot_api_url = url.into();
283
284 let source_url = self.snapshot_source_url().to_string();
285 if self.default_chain_aware_base_url.is_none() {
286 self.default_chain_aware_base_url = Some(Cow::Owned(source_url.clone()));
287 }
288 for source in &mut self.available_snapshots {
289 if source.as_ref() == RETH_SNAPSHOTS_SOURCE {
290 *source = Cow::Owned(format!("{source_url} (default)"));
291 }
292 }
293
294 self
295 }
296
297 pub fn with_snapshot_source_url(mut self, url: impl Into<Cow<'static, str>>) -> Self {
302 let source_url = normalize_snapshot_source_url(url.into());
303 self.available_snapshots = vec![Cow::Owned(format!("{} (default)", source_url.as_ref()))];
304 self.default_base_url = source_url.clone();
305 self.default_chain_aware_base_url = Some(source_url.clone());
306 self.snapshot_api_url = Cow::Owned(format!("{}{SNAPSHOT_API_PATH}", source_url.as_ref()));
307 self
308 }
309
310 pub fn with_long_help(mut self, help: impl Into<String>) -> Self {
312 self.long_help = Some(help.into());
313 self
314 }
315}
316
317fn snapshot_source_url_from_api(api_url: &str) -> &str {
318 api_url.trim_end_matches('/').trim_end_matches(SNAPSHOT_API_PATH)
319}
320
321fn normalize_snapshot_source_url(url: Cow<'static, str>) -> Cow<'static, str> {
322 match url {
323 Cow::Borrowed(url) => Cow::Borrowed(snapshot_source_url_from_api(url)),
324 Cow::Owned(url) => Cow::Owned(snapshot_source_url_from_api(&url).to_string()),
325 }
326}
327
328impl Default for DownloadDefaults {
329 fn default() -> Self {
331 Self::default_download_defaults()
332 }
333}
334
335#[derive(Debug, Parser)]
337pub struct DownloadCommand<C: ChainSpecParser> {
338 #[command(flatten)]
339 env: EnvironmentArgs<C>,
340
341 #[arg(long, short, long_help = DownloadDefaults::get_global().long_help())]
346 url: Option<String>,
347
348 #[arg(long, value_name = "URL", conflicts_with = "url")]
353 manifest_url: Option<String>,
354
355 #[arg(long, value_name = "PATH", conflicts_with_all = ["url", "manifest_url"])]
357 manifest_path: Option<PathBuf>,
358
359 #[arg(long, conflicts_with_all = ["with_txs_since", "with_txs_distance", "minimal", "full", "archive"])]
361 with_txs: bool,
362
363 #[arg(long, value_name = "BLOCK_NUMBER", conflicts_with_all = ["with_txs", "with_txs_distance", "minimal", "full", "archive"])]
365 with_txs_since: Option<u64>,
366
367 #[arg(long, value_name = "BLOCKS", value_parser = RangedU64ValueParser::<u64>::new().range(1..), conflicts_with_all = ["with_txs", "with_txs_since", "minimal", "full", "archive"])]
369 with_txs_distance: Option<u64>,
370
371 #[arg(long, conflicts_with_all = ["with_receipts_since", "with_receipts_distance", "minimal", "full", "archive"])]
373 with_receipts: bool,
374
375 #[arg(long, value_name = "BLOCK_NUMBER", conflicts_with_all = ["with_receipts", "with_receipts_distance", "minimal", "full", "archive"])]
377 with_receipts_since: Option<u64>,
378
379 #[arg(long, value_name = "BLOCKS", value_parser = RangedU64ValueParser::<u64>::new().range(1..), conflicts_with_all = ["with_receipts", "with_receipts_since", "minimal", "full", "archive"])]
381 with_receipts_distance: Option<u64>,
382
383 #[arg(long, alias = "with-changesets", conflicts_with_all = ["with_state_history_since", "with_state_history_distance", "minimal", "full", "archive"])]
385 with_state_history: bool,
386
387 #[arg(long, value_name = "BLOCK_NUMBER", conflicts_with_all = ["with_state_history", "with_state_history_distance", "minimal", "full", "archive"])]
389 with_state_history_since: Option<u64>,
390
391 #[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"])]
393 with_state_history_distance: Option<u64>,
394
395 #[arg(long, requires = "with_txs", conflicts_with_all = ["minimal", "full", "archive"])]
397 with_senders: bool,
398
399 #[arg(long, conflicts_with_all = ["minimal", "full", "archive", "without_rocksdb"])]
401 with_rocksdb: bool,
402
403 #[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"])]
405 archive: bool,
406
407 #[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"])]
409 minimal: bool,
410
411 #[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"])]
413 full: bool,
414
415 #[arg(long, conflicts_with_all = ["url", "with_rocksdb"])]
419 without_rocksdb: bool,
420
421 #[arg(long, short = 'y')]
424 non_interactive: bool,
425
426 #[arg(long, conflicts_with = "list")]
428 force: bool,
429
430 #[arg(long, default_value_t = true, num_args = 0..=1, default_missing_value = "true")]
437 resumable: bool,
438
439 #[arg(long, default_value_t = MAX_CONCURRENT_DOWNLOADS)]
444 download_concurrency: usize,
445
446 #[arg(long, value_name = "DURATION", value_parser = reth_cli_util::parse_duration_from_secs_or_ms)]
452 retry_backoff: Option<Duration>,
453
454 #[arg(long, alias = "list-snapshots", conflicts_with_all = ["url", "manifest_url", "manifest_path"])]
459 list: bool,
460
461 #[arg(long, conflicts_with_all = ["url", "list"])]
463 print_plan_json: bool,
464}
465
466impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> DownloadCommand<C> {
467 pub async fn execute<N>(self) -> Result<Option<PreparedSnapshotDownload>> {
469 let chain = self.env.chain.chain();
470
471 if self.list {
473 let entries = fetch_snapshot_api_entries(chain.id()).await?;
474 print_snapshot_listing(&entries, chain.id());
475 return Ok(None);
476 }
477
478 let data_dir = self.env.datadir.clone().resolve_datadir(chain);
479 let static_files_dir = data_dir.static_files();
480 let static_files_dir = (static_files_dir != data_dir.data_dir().join("static_files"))
481 .then_some(static_files_dir);
482
483 if let Some(ref url) = self.url {
485 let cancel_token = CancellationToken::new();
486 let _cancel_guard = cancel_token.drop_guard();
487 let data_dir = self.env.datadir.clone().resolve_datadir(chain);
488 let target_dir = data_dir.data_dir();
489 if self.force {
490 clear_existing_datadir(target_dir, static_files_dir.as_deref())?;
491 }
492 fs::create_dir_all(target_dir)?;
493
494 let request_limiter = DownloadRequestLimiter::new(self.download_concurrency.max(1));
495 info!(target: "reth::cli",
496 dir = ?data_dir.data_dir(),
497 url = %url,
498 "Starting snapshot download and extraction"
499 );
500
501 stream_and_extract(
502 url,
503 data_dir.data_dir(),
504 static_files_dir.as_deref(),
505 self.resumable,
506 Some(request_limiter),
507 cancel_token.clone(),
508 self.retry_backoff,
509 )
510 .await?;
511 info!(target: "reth::cli", "Snapshot downloaded and extracted successfully");
512
513 return Ok(None);
514 }
515
516 let ResolvedDownload { manifest, selections, preset, planned } =
517 self.resolve_download(chain.id()).await?;
518 let data_dir = self.env.datadir.clone().resolve_datadir(chain).data_dir().to_path_buf();
519 let prepared = PreparedSnapshotDownload { manifest, data_dir };
520 if self.print_plan_json {
521 DownloadPlan::from_planned(&prepared.manifest, &planned)
522 .write_json(std::io::stdout().lock())?;
523 return Ok(Some(prepared))
524 }
525
526 let target_dir = prepared.data_dir.as_path();
527 let cancel_token = CancellationToken::new();
528 let _cancel_guard = cancel_token.drop_guard();
529 if self.force {
530 clear_existing_datadir(target_dir, static_files_dir.as_deref())?;
531 }
532 fs::create_dir_all(target_dir)?;
533 let startup_summary =
534 summarize_download_startup(&planned.archives, target_dir, static_files_dir.as_deref())?;
535 info!(target: "reth::cli",
536 reusable = startup_summary.reusable,
537 needs_download = startup_summary.needs_download,
538 "Startup integrity summary (plain output files)"
539 );
540
541 info!(target: "reth::cli",
542 archives = planned.total_archives(),
543 download_total = %DownloadProgress::format_size(planned.total_download_size),
544 output_total = %DownloadProgress::format_size(planned.total_output_size),
545 "Downloading all archives"
546 );
547
548 run_modular_downloads(
549 planned,
550 target_dir,
551 static_files_dir.as_deref(),
552 self.download_concurrency.max(1),
553 cancel_token.clone(),
554 self.retry_backoff,
555 )
556 .await?;
557
558 self.finalize_modular_download(
559 &selections,
560 &prepared.manifest,
561 preset,
562 target_dir,
563 &target_dir.join("db"),
564 )?;
565
566 Ok(Some(prepared))
567 }
568
569 pub async fn plan(&self) -> Result<(DownloadPlan, PreparedSnapshotDownload)> {
572 let chain = self.env.chain.chain();
573 let resolved = self.resolve_download(chain.id()).await?;
574 let plan = DownloadPlan::from_planned(&resolved.manifest, &resolved.planned);
575 let prepared = PreparedSnapshotDownload {
576 manifest: resolved.manifest,
577 data_dir: self.env.datadir.clone().resolve_datadir(chain).data_dir().to_path_buf(),
578 };
579 Ok((plan, prepared))
580 }
581
582 async fn resolve_download(&self, chain_id: u64) -> Result<ResolvedDownload> {
583 let manifest = self.load_manifest(chain_id).await?;
584 let ResolvedComponents { mut selections, preset } = self.resolve_components(&manifest)?;
585
586 if matches!(preset, Some(SelectionPreset::Archive)) {
587 inject_archive_only_components(&mut selections, &manifest, !self.without_rocksdb);
588 }
589
590 let planned = collect_planned_archives(&manifest, &selections)?;
591 Ok(ResolvedDownload { manifest, selections, preset, planned })
592 }
593
594 async fn load_manifest(&self, chain_id: u64) -> Result<SnapshotManifest> {
596 let manifest_source = self.resolve_manifest_source(chain_id).await?;
597
598 info!(target: "reth::cli", source = %manifest_source, "Fetching snapshot manifest");
599 let mut manifest = fetch_manifest_from_source(&manifest_source).await?;
600 eyre::ensure!(
601 manifest.chain_id == chain_id,
602 "Snapshot chain ID {} does not match selected chain ID {chain_id}",
603 manifest.chain_id
604 );
605 manifest.base_url = Some(resolve_manifest_base_url(&manifest, &manifest_source)?);
606
607 info!(target: "reth::cli",
608 block = manifest.block,
609 chain_id = manifest.chain_id,
610 storage_version = %manifest.storage_version,
611 components = manifest.components.len(),
612 "Loaded snapshot manifest"
613 );
614
615 Ok(manifest)
616 }
617
618 fn finalize_modular_download(
620 &self,
621 selections: &BTreeMap<SnapshotComponentType, ComponentSelection>,
622 manifest: &SnapshotManifest,
623 preset: Option<SelectionPreset>,
624 target_dir: &Path,
625 db_path: &Path,
626 ) -> Result<()> {
627 let config =
628 config_for_selections(selections, manifest, preset, Some(self.env.chain.as_ref()));
629 if write_config(&config, target_dir)? {
630 let desc = config_gen::describe_prune_config(&config);
631 info!(target: "reth::cli", "{}", desc.join(", "));
632 }
633
634 let db = init_db(db_path, self.env.db.database_args())?;
635 let should_write_prune = config.prune.segments != Default::default();
636 let should_reset_indices = should_reset_index_stage_checkpoints(selections);
637 if should_write_prune || should_reset_indices {
638 let tx = db.tx_mut()?;
639
640 if should_write_prune {
641 config_gen::write_prune_checkpoints_tx(&tx, &config, manifest.block)?;
642 }
643
644 if should_reset_indices {
645 config_gen::reset_index_stage_checkpoints_tx(&tx)?;
646 }
647
648 tx.commit()?;
649 }
650
651 let start_command = startup_node_command::<C>(self.env.chain.as_ref());
652 info!(target: "reth::cli", "Snapshot download complete. Run `{}` to start syncing.", start_command);
653
654 Ok(())
655 }
656
657 fn resolve_components(&self, manifest: &SnapshotManifest) -> Result<ResolvedComponents> {
659 let available = |ty: SnapshotComponentType| manifest.component(ty).is_some();
660
661 if self.archive {
663 return Ok(ResolvedComponents {
664 selections: SnapshotComponentType::ALL
665 .iter()
666 .copied()
667 .filter(|ty| available(*ty))
668 .filter(|ty| {
669 !self.without_rocksdb || *ty != SnapshotComponentType::RocksdbIndices
670 })
671 .map(|ty| (ty, ComponentSelection::All))
672 .collect(),
673 preset: Some(SelectionPreset::Archive),
674 });
675 }
676
677 if self.full {
678 return Ok(ResolvedComponents {
679 selections: self.full_preset_selections(manifest),
680 preset: Some(SelectionPreset::Full),
681 });
682 }
683
684 if self.minimal {
685 return Ok(ResolvedComponents {
686 selections: self.minimal_preset_selections(manifest),
687 preset: Some(SelectionPreset::Minimal),
688 });
689 }
690
691 let has_explicit_flags = self.with_txs ||
692 self.with_txs_since.is_some() ||
693 self.with_txs_distance.is_some() ||
694 self.with_receipts ||
695 self.with_receipts_since.is_some() ||
696 self.with_receipts_distance.is_some() ||
697 self.with_state_history ||
698 self.with_state_history_since.is_some() ||
699 self.with_state_history_distance.is_some() ||
700 self.with_senders ||
701 self.with_rocksdb;
702
703 if has_explicit_flags {
704 let mut selections = BTreeMap::new();
705 let tx_selection = explicit_component_selection(
706 self.with_txs,
707 self.with_txs_since,
708 self.with_txs_distance,
709 manifest.block,
710 );
711 let receipt_selection = explicit_component_selection(
712 self.with_receipts,
713 self.with_receipts_since,
714 self.with_receipts_distance,
715 manifest.block,
716 );
717 let state_history_selection = explicit_component_selection(
718 self.with_state_history,
719 self.with_state_history_since,
720 self.with_state_history_distance,
721 manifest.block,
722 );
723
724 if available(SnapshotComponentType::State) {
726 selections.insert(SnapshotComponentType::State, ComponentSelection::All);
727 }
728 if available(SnapshotComponentType::Headers) {
729 selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
730 }
731 if let Some(selection) = tx_selection &&
732 available(SnapshotComponentType::Transactions)
733 {
734 selections.insert(SnapshotComponentType::Transactions, selection);
735 }
736 if let Some(selection) = receipt_selection &&
737 available(SnapshotComponentType::Receipts)
738 {
739 selections.insert(SnapshotComponentType::Receipts, selection);
740 }
741 if let Some(selection) = state_history_selection {
742 if available(SnapshotComponentType::AccountChangesets) {
743 selections.insert(SnapshotComponentType::AccountChangesets, selection);
744 }
745 if available(SnapshotComponentType::StorageChangesets) {
746 selections.insert(SnapshotComponentType::StorageChangesets, selection);
747 }
748 }
749 if self.with_senders && available(SnapshotComponentType::TransactionSenders) {
750 selections
751 .insert(SnapshotComponentType::TransactionSenders, ComponentSelection::All);
752 }
753 if self.with_rocksdb && available(SnapshotComponentType::RocksdbIndices) {
754 selections.insert(SnapshotComponentType::RocksdbIndices, ComponentSelection::All);
755 }
756 return Ok(ResolvedComponents { selections, preset: None });
757 }
758
759 if self.non_interactive {
760 return Ok(ResolvedComponents {
761 selections: self.minimal_preset_selections(manifest),
762 preset: Some(SelectionPreset::Minimal),
763 });
764 }
765
766 let minimal_preset = self.minimal_preset_selections(manifest);
768 let full_preset = self.full_preset_selections(manifest);
769 let SelectorOutput { selections, preset } =
770 run_selector(manifest.clone(), &minimal_preset, &full_preset)?;
771 let selected =
772 selections.into_iter().filter(|(_, sel)| *sel != ComponentSelection::None).collect();
773
774 Ok(ResolvedComponents { selections: selected, preset })
775 }
776
777 fn minimal_preset_selections(
779 &self,
780 manifest: &SnapshotManifest,
781 ) -> BTreeMap<SnapshotComponentType, ComponentSelection> {
782 self.pruning_preset_selections(manifest, SelectionPreset::Minimal)
783 }
784
785 fn full_preset_selections(
787 &self,
788 manifest: &SnapshotManifest,
789 ) -> BTreeMap<SnapshotComponentType, ComponentSelection> {
790 self.pruning_preset_selections(manifest, SelectionPreset::Full)
791 }
792
793 fn pruning_preset_selections(
795 &self,
796 manifest: &SnapshotManifest,
797 preset: SelectionPreset,
798 ) -> BTreeMap<SnapshotComponentType, ComponentSelection> {
799 let defaults = DefaultPruningValues::get_global();
800 let (prune_modes, bodies_history_use_pre_merge) = match preset {
801 SelectionPreset::Minimal => (&defaults.minimal_prune_modes, false),
802 SelectionPreset::Full => {
803 (&defaults.full_prune_modes, defaults.full_bodies_history_use_pre_merge)
804 }
805 SelectionPreset::Archive => unreachable!("archive selects every component"),
806 };
807 let mut selections = BTreeMap::new();
808
809 for &ty in &SnapshotComponentType::ALL {
810 if manifest.component(ty).is_none() {
811 continue;
812 }
813
814 let selection = self.pruning_selection_for_component(
815 ty,
816 manifest.block,
817 prune_modes,
818 bodies_history_use_pre_merge,
819 );
820 if selection != ComponentSelection::None {
821 selections.insert(ty, selection);
822 }
823 }
824
825 selections
826 }
827
828 fn pruning_selection_for_component(
830 &self,
831 ty: SnapshotComponentType,
832 snapshot_block: u64,
833 prune_modes: &PruneModes,
834 bodies_history_use_pre_merge: bool,
835 ) -> ComponentSelection {
836 if ty == SnapshotComponentType::Transactions && bodies_history_use_pre_merge {
837 return match self
838 .env
839 .chain
840 .ethereum_fork_activation(EthereumHardfork::Paris)
841 .block_number()
842 {
843 Some(paris) if snapshot_block >= paris => ComponentSelection::Since(paris),
844 Some(_) => ComponentSelection::None,
845 None => ComponentSelection::All,
846 }
847 }
848
849 let mode = match ty {
850 SnapshotComponentType::State | SnapshotComponentType::Headers => {
851 return ComponentSelection::All
852 }
853 SnapshotComponentType::Transactions => prune_modes.bodies_history,
854 SnapshotComponentType::TransactionSenders => prune_modes.sender_recovery,
855 SnapshotComponentType::Receipts => prune_modes.receipts,
856 SnapshotComponentType::AccountChangesets => prune_modes.account_history,
857 SnapshotComponentType::StorageChangesets => prune_modes.storage_history,
858 SnapshotComponentType::RocksdbIndices => return ComponentSelection::None,
859 };
860
861 selection_from_prune_mode(mode, snapshot_block)
862 }
863
864 async fn resolve_manifest_source(&self, chain_id: u64) -> Result<String> {
866 if let Some(path) = &self.manifest_path {
867 return Ok(path.display().to_string());
868 }
869
870 match &self.manifest_url {
871 Some(url) => Ok(url.clone()),
872 None => {
873 let defaults = DownloadDefaults::get_global();
874 if defaults.mainnet_only_discovery() && chain_id != MAINNET.chain.id() {
875 eyre::bail!(
876 "Snapshots are only auto-discovered for Ethereum mainnet.\n\n\
877 Chain {chain_id} requires an explicit source:\n\
878 \t--manifest-url <URL>\n\
879 \t--manifest-path <PATH>\n\
880 \t-u <SNAPSHOT-URL>\n\n\
881 Use --list to inspect snapshots exposed by {}.",
882 defaults.snapshot_source_url(),
883 );
884 }
885
886 discover_manifest_url(chain_id).await
887 }
888 }
889 }
890}
891
892fn explicit_component_selection(
895 all: bool,
896 since: Option<u64>,
897 distance: Option<u64>,
898 snapshot_block: u64,
899) -> Option<ComponentSelection> {
900 if all {
901 Some(ComponentSelection::All)
902 } else if let Some(block) = since {
903 (block <= snapshot_block).then_some(ComponentSelection::Since(block))
904 } else {
905 distance.map(ComponentSelection::Distance)
906 }
907}
908
909fn selection_from_prune_mode(mode: Option<PruneMode>, snapshot_block: u64) -> ComponentSelection {
911 match mode {
912 None => ComponentSelection::All,
913 Some(PruneMode::Full) => ComponentSelection::None,
914 Some(PruneMode::Distance(d)) => ComponentSelection::Distance(d),
915 Some(PruneMode::Before(block)) => {
916 if snapshot_block >= block {
917 ComponentSelection::Since(block)
918 } else {
919 ComponentSelection::None
920 }
921 }
922 }
923}
924
925fn clear_existing_datadir(target_dir: &Path, static_files_dir: Option<&Path>) -> Result<()> {
927 info!(target: "reth::cli", dir = ?target_dir, "Clearing existing snapshot data");
928 for entry in FORCE_REMOVED_DATADIR_PATHS {
929 let path = if *entry == "static_files" {
930 static_files_dir.map_or_else(|| target_dir.join(entry), Path::to_path_buf)
931 } else {
932 target_dir.join(entry)
933 };
934 if !path.try_exists()? {
935 continue;
936 }
937
938 let metadata = fs::metadata(&path)?;
939 if metadata.is_dir() {
940 fs::remove_dir_all(&path)?;
941 } else if metadata.is_file() {
942 fs::remove_file(&path)?;
943 }
944 }
945
946 Ok(())
947}
948
949fn inject_archive_only_components(
952 selections: &mut BTreeMap<SnapshotComponentType, ComponentSelection>,
953 manifest: &SnapshotManifest,
954 include_rocksdb: bool,
955) {
956 let is_all =
957 |ty: SnapshotComponentType| selections.get(&ty).copied() == Some(ComponentSelection::All);
958
959 let is_archive = is_all(SnapshotComponentType::Transactions) &&
960 is_all(SnapshotComponentType::Receipts) &&
961 is_all(SnapshotComponentType::AccountChangesets) &&
962 is_all(SnapshotComponentType::StorageChangesets);
963
964 if !is_archive {
965 return;
966 }
967
968 for component in
969 [SnapshotComponentType::TransactionSenders, SnapshotComponentType::RocksdbIndices]
970 {
971 if component == SnapshotComponentType::RocksdbIndices && !include_rocksdb {
972 continue;
973 }
974
975 if manifest.component(component).is_some() {
976 selections.insert(component, ComponentSelection::All);
977 }
978 }
979}
980
981fn should_reset_index_stage_checkpoints(
983 selections: &BTreeMap<SnapshotComponentType, ComponentSelection>,
984) -> bool {
985 !matches!(selections.get(&SnapshotComponentType::RocksdbIndices), Some(ComponentSelection::All))
986}
987
988fn startup_node_command<C>(chain_spec: &C::ChainSpec) -> String
989where
990 C: ChainSpecParser,
991 C::ChainSpec: EthChainSpec,
992{
993 startup_node_command_for_binary::<C>(¤t_binary_name(), chain_spec)
994}
995
996fn startup_node_command_for_binary<C>(binary_name: &str, chain_spec: &C::ChainSpec) -> String
997where
998 C: ChainSpecParser,
999 C::ChainSpec: EthChainSpec,
1000{
1001 let mut command = format!("{binary_name} node");
1002
1003 if let Some(chain_arg) = startup_chain_arg::<C>(chain_spec) {
1004 command.push_str(" --chain ");
1005 command.push_str(&chain_arg);
1006 }
1007
1008 command
1009}
1010
1011fn current_binary_name() -> String {
1012 std::env::args_os()
1013 .next()
1014 .map(PathBuf::from)
1015 .and_then(|path| path.file_stem().map(|name| name.to_owned()))
1016 .and_then(|name| name.into_string().ok())
1017 .filter(|name| !name.is_empty())
1018 .unwrap_or_else(|| "reth".to_string())
1019}
1020
1021fn download_command() -> String {
1022 download_command_for_binary(¤t_binary_name())
1023}
1024
1025fn download_command_for_binary(binary_name: &str) -> String {
1026 format!("{binary_name} download")
1027}
1028
1029fn startup_chain_arg<C>(chain_spec: &C::ChainSpec) -> Option<String>
1030where
1031 C: ChainSpecParser,
1032 C::ChainSpec: EthChainSpec,
1033{
1034 let current_chain = chain_spec.chain();
1035 let current_genesis_hash = chain_spec.genesis_hash();
1036 let default_chain = C::default_value().and_then(|chain_name| C::parse(chain_name).ok());
1037
1038 if default_chain.as_ref().is_some_and(|default_chain| {
1039 default_chain.chain() == current_chain &&
1040 default_chain.genesis_hash() == current_genesis_hash
1041 }) {
1042 return None;
1043 }
1044
1045 C::SUPPORTED_CHAINS
1046 .iter()
1047 .find_map(|chain_name| {
1048 let parsed_chain = C::parse(chain_name).ok()?;
1049 (parsed_chain.chain() == current_chain &&
1050 parsed_chain.genesis_hash() == current_genesis_hash)
1051 .then(|| (*chain_name).to_string())
1052 })
1053 .or_else(|| Some("<chain-or-chainspec>".to_string()))
1054}
1055
1056impl<C: ChainSpecParser> DownloadCommand<C> {
1057 pub const fn env(&self) -> &EnvironmentArgs<C> {
1059 &self.env
1060 }
1061
1062 pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
1064 Some(&self.env.chain)
1065 }
1066
1067 pub const fn prints_plan_json(&self) -> bool {
1069 self.print_plan_json
1070 }
1071}
1072
1073#[derive(Debug)]
1075pub struct PreparedSnapshotDownload {
1076 pub manifest: SnapshotManifest,
1078 pub data_dir: PathBuf,
1080}
1081
1082struct ResolvedDownload {
1083 manifest: SnapshotManifest,
1084 selections: BTreeMap<SnapshotComponentType, ComponentSelection>,
1085 preset: Option<SelectionPreset>,
1086 planned: PlannedDownloads,
1087}
1088
1089const MAX_DOWNLOAD_RETRIES: u32 = 10;
1090const RETRY_BACKOFF_SECS: u64 = 5;
1091
1092#[cfg(test)]
1093mod tests {
1094 use super::*;
1095 use clap::{Args, Parser};
1096 use extract::CompressionFormat;
1097 use manifest::{ComponentManifest, SingleArchive};
1098 use reth_chainspec::{HOLESKY, MAINNET};
1099 use reth_ethereum_cli::chainspec::EthereumChainSpecParser;
1100
1101 #[derive(Parser)]
1102 struct CommandParser<T: Args> {
1103 #[command(flatten)]
1104 args: T,
1105 }
1106
1107 fn manifest_with_archive_only_components() -> SnapshotManifest {
1108 let mut components = BTreeMap::new();
1109 components.insert(
1110 SnapshotComponentType::TransactionSenders.key().to_string(),
1111 ComponentManifest::Single(SingleArchive {
1112 file: "transaction_senders.tar.zst".to_string(),
1113 size: 1,
1114 decompressed_size: 0,
1115 blake3: None,
1116 output_files: vec![],
1117 }),
1118 );
1119 components.insert(
1120 SnapshotComponentType::RocksdbIndices.key().to_string(),
1121 ComponentManifest::Single(SingleArchive {
1122 file: "rocksdb_indices.tar.zst".to_string(),
1123 size: 1,
1124 decompressed_size: 0,
1125 blake3: None,
1126 output_files: vec![],
1127 }),
1128 );
1129 SnapshotManifest {
1130 block: 0,
1131 chain_id: 1,
1132 storage_version: 2,
1133 timestamp: 0,
1134 base_url: Some("https://example.com".to_string()),
1135 reth_version: None,
1136 components,
1137 extensions: Default::default(),
1138 }
1139 }
1140
1141 #[test]
1142 fn test_download_defaults_builder() {
1143 let defaults = DownloadDefaults::default()
1144 .with_snapshot("https://example.com/snapshots (example)")
1145 .with_base_url("https://example.com");
1146
1147 assert_eq!(defaults.default_base_url, "https://example.com");
1148 assert_eq!(defaults.available_snapshots.len(), 3); }
1150
1151 #[test]
1152 fn test_download_defaults_replace_snapshots() {
1153 let defaults = DownloadDefaults::default().with_snapshots(vec![
1154 Cow::Borrowed("https://custom1.com"),
1155 Cow::Borrowed("https://custom2.com"),
1156 ]);
1157
1158 assert_eq!(defaults.available_snapshots.len(), 2);
1159 assert_eq!(defaults.available_snapshots[0], "https://custom1.com");
1160 }
1161
1162 #[test]
1163 fn test_long_help_generation() {
1164 let defaults = DownloadDefaults::default();
1165 let help = defaults.long_help();
1166
1167 assert!(help.contains("Available snapshot sources:"));
1168 assert!(help.contains("Ethereum mainnet"));
1169 assert!(help.contains("snapshots.reth.rs"));
1170 assert!(help.contains("publicnode.com"));
1171 assert!(help.contains("file://"));
1172 }
1173
1174 #[test]
1175 fn test_custom_snapshot_api_keeps_selected_chain_help() {
1176 let defaults = DownloadDefaults::default()
1177 .with_snapshot_api_url("https://snapshots.tempoxyz.dev/api/snapshots");
1178 let help = defaults.long_help();
1179
1180 assert_eq!(
1181 defaults.default_chain_aware_base_url.as_deref(),
1182 Some("https://snapshots.tempoxyz.dev")
1183 );
1184 assert!(help.contains("Browse available snapshots at https://snapshots.tempoxyz.dev"));
1185 assert!(help.contains("- https://snapshots.tempoxyz.dev (default)"));
1186 assert!(help.contains("selected chain"));
1187 assert!(!help.contains("Ethereum mainnet"));
1188 assert!(!help.contains("snapshots.reth.rs"));
1189 }
1190
1191 #[test]
1192 fn test_snapshot_source_url_sets_generated_references() {
1193 let defaults =
1194 DownloadDefaults::default().with_snapshot_source_url("https://snapshots.tempoxyz.dev/");
1195 let help = defaults.long_help();
1196
1197 assert_eq!(defaults.snapshot_api_url, "https://snapshots.tempoxyz.dev/api/snapshots");
1198 assert_eq!(defaults.default_base_url, "https://snapshots.tempoxyz.dev");
1199 assert_eq!(
1200 defaults.default_chain_aware_base_url.as_deref(),
1201 Some("https://snapshots.tempoxyz.dev")
1202 );
1203 assert_eq!(
1204 defaults.available_snapshots.iter().map(|source| source.as_ref()).collect::<Vec<_>>(),
1205 vec!["https://snapshots.tempoxyz.dev (default)"]
1206 );
1207 assert!(!defaults.mainnet_only_discovery());
1208 assert!(help.contains("Browse available snapshots at https://snapshots.tempoxyz.dev"));
1209 assert!(help.contains("from https://snapshots.tempoxyz.dev."));
1210 }
1211
1212 #[test]
1213 fn test_snapshot_api_url_trailing_slash_sets_source_url() {
1214 let defaults = DownloadDefaults::default()
1215 .with_snapshot_api_url("https://snapshots.tempoxyz.dev/api/snapshots/");
1216 let help = defaults.long_help();
1217
1218 assert_eq!(
1219 defaults.default_chain_aware_base_url.as_deref(),
1220 Some("https://snapshots.tempoxyz.dev")
1221 );
1222 assert!(help.contains("Browse available snapshots at https://snapshots.tempoxyz.dev"));
1223 assert!(help.contains("- https://snapshots.tempoxyz.dev (default)"));
1224 }
1225
1226 #[test]
1227 fn test_long_help_override() {
1228 let custom_help = "This is custom help text for downloading snapshots.";
1229 let defaults = DownloadDefaults::default().with_long_help(custom_help);
1230
1231 let help = defaults.long_help();
1232 assert_eq!(help, custom_help);
1233 assert!(!help.contains("Available snapshot sources:"));
1234 }
1235
1236 #[test]
1237 fn test_builder_chaining() {
1238 let defaults = DownloadDefaults::default()
1239 .with_base_url("https://custom.example.com")
1240 .with_snapshot("https://snapshot1.com")
1241 .with_snapshot("https://snapshot2.com")
1242 .with_long_help("Custom help for snapshots");
1243
1244 assert_eq!(defaults.default_base_url, "https://custom.example.com");
1245 assert_eq!(defaults.available_snapshots.len(), 4); assert_eq!(defaults.long_help, Some("Custom help for snapshots".to_string()));
1247 }
1248
1249 #[test]
1250 fn test_download_retry_backoff() {
1251 let parse = |args: Vec<&str>| {
1252 CommandParser::<DownloadCommand<EthereumChainSpecParser>>::try_parse_from(args)
1253 };
1254 assert_eq!(parse(vec!["reth"]).unwrap().args.retry_backoff, None);
1255 for (value, expected) in [
1256 ("0ms", Duration::ZERO),
1257 ("250ms", Duration::from_millis(250)),
1258 ("2s", Duration::from_secs(2)),
1259 ] {
1260 assert_eq!(
1261 parse(vec!["reth", "--retry-backoff", value]).unwrap().args.retry_backoff,
1262 Some(expected)
1263 );
1264 }
1265 assert!(parse(vec!["reth", "--retry-backoff=-1s"]).is_err());
1266 assert!(parse(vec!["reth", "--retry-backoff", "invalid"]).is_err());
1267 }
1268
1269 #[test]
1270 fn test_download_resumable_defaults_to_true() {
1271 let args =
1272 CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from(["reth"]).args;
1273
1274 assert!(args.resumable);
1275 }
1276
1277 #[test]
1278 fn test_download_resumable_implicit_true() {
1279 let args = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from([
1280 "reth",
1281 "--resumable",
1282 ])
1283 .args;
1284
1285 assert!(args.resumable);
1286 }
1287
1288 #[test]
1289 fn test_download_resumable_explicit_false() {
1290 let args = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from([
1291 "reth",
1292 "--resumable=false",
1293 ])
1294 .args;
1295
1296 assert!(!args.resumable);
1297 }
1298
1299 #[test]
1300 fn test_download_print_plan_json_parses() {
1301 let args = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from([
1302 "reth",
1303 "--manifest-path",
1304 "manifest.json",
1305 "--minimal",
1306 "--print-plan-json",
1307 ])
1308 .args;
1309
1310 assert!(args.prints_plan_json());
1311 }
1312
1313 #[test]
1314 fn test_download_print_plan_json_rejects_single_archive() {
1315 let result = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::try_parse_from([
1316 "reth",
1317 "--url",
1318 "https://example.com/snapshot.tar.zst",
1319 "--print-plan-json",
1320 ]);
1321
1322 assert!(result.is_err());
1323 }
1324
1325 #[test]
1326 fn minimal_component_selection_uses_configured_prune_modes() {
1327 let args =
1328 CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from(["reth"]).args;
1329 let history_distance = 64_864;
1330 let modes = PruneModes {
1331 sender_recovery: Some(PruneMode::Full),
1332 receipts: Some(PruneMode::Distance(128)),
1333 account_history: Some(PruneMode::Distance(history_distance)),
1334 storage_history: Some(PruneMode::Distance(history_distance)),
1335 bodies_history: Some(PruneMode::Distance(history_distance)),
1336 ..Default::default()
1337 };
1338
1339 assert_eq!(
1340 args.pruning_selection_for_component(
1341 SnapshotComponentType::Transactions,
1342 1_000_000,
1343 &modes,
1344 false,
1345 ),
1346 ComponentSelection::Distance(history_distance)
1347 );
1348 assert_eq!(
1349 args.pruning_selection_for_component(
1350 SnapshotComponentType::Receipts,
1351 1_000_000,
1352 &modes,
1353 false,
1354 ),
1355 ComponentSelection::Distance(128)
1356 );
1357 assert_eq!(
1358 args.pruning_selection_for_component(
1359 SnapshotComponentType::AccountChangesets,
1360 1_000_000,
1361 &modes,
1362 false,
1363 ),
1364 ComponentSelection::Distance(history_distance)
1365 );
1366 assert_eq!(
1367 args.pruning_selection_for_component(
1368 SnapshotComponentType::StorageChangesets,
1369 1_000_000,
1370 &modes,
1371 false,
1372 ),
1373 ComponentSelection::Distance(history_distance)
1374 );
1375 assert_eq!(
1376 args.pruning_selection_for_component(
1377 SnapshotComponentType::TransactionSenders,
1378 1_000_000,
1379 &modes,
1380 false,
1381 ),
1382 ComponentSelection::None
1383 );
1384 }
1385
1386 #[test]
1387 fn resolve_manifest_source_requires_explicit_source_for_non_mainnet_defaults() {
1388 let args = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from([
1389 "reth", "--chain", "holesky",
1390 ])
1391 .args;
1392
1393 let err = tokio::runtime::Runtime::new()
1394 .unwrap()
1395 .block_on(args.resolve_manifest_source(HOLESKY.chain.id()))
1396 .unwrap_err();
1397
1398 let message = err.to_string();
1399 assert!(message.contains("only auto-discovered for Ethereum mainnet"));
1400 assert!(message.contains("--manifest-url <URL>"));
1401 assert!(message.contains("-u <SNAPSHOT-URL>"));
1402 }
1403
1404 #[test]
1405 fn resolve_manifest_source_allows_manifest_path_for_non_mainnet_defaults() {
1406 let args = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from([
1407 "reth",
1408 "--chain",
1409 "holesky",
1410 "--manifest-path",
1411 "./manifest.json",
1412 ])
1413 .args;
1414
1415 let source = tokio::runtime::Runtime::new()
1416 .unwrap()
1417 .block_on(args.resolve_manifest_source(HOLESKY.chain.id()))
1418 .unwrap();
1419
1420 assert_eq!(source, "./manifest.json");
1421 }
1422
1423 #[test]
1424 fn test_compression_format_detection() {
1425 assert!(matches!(
1426 CompressionFormat::from_url("https://example.com/snapshot.tar.lz4"),
1427 Ok(CompressionFormat::Lz4)
1428 ));
1429 assert!(matches!(
1430 CompressionFormat::from_url("https://example.com/snapshot.tar.zst"),
1431 Ok(CompressionFormat::Zstd)
1432 ));
1433 assert!(matches!(
1434 CompressionFormat::from_url("file:///path/to/snapshot.tar.lz4"),
1435 Ok(CompressionFormat::Lz4)
1436 ));
1437 assert!(matches!(
1438 CompressionFormat::from_url("file:///path/to/snapshot.tar.zst"),
1439 Ok(CompressionFormat::Zstd)
1440 ));
1441 assert!(CompressionFormat::from_url("https://example.com/snapshot.tar.gz").is_err());
1442 }
1443
1444 #[test]
1445 fn inject_archive_only_components_for_archive_selection() {
1446 let manifest = manifest_with_archive_only_components();
1447 let mut selections = BTreeMap::new();
1448 selections.insert(SnapshotComponentType::Transactions, ComponentSelection::All);
1449 selections.insert(SnapshotComponentType::Receipts, ComponentSelection::All);
1450 selections.insert(SnapshotComponentType::AccountChangesets, ComponentSelection::All);
1451 selections.insert(SnapshotComponentType::StorageChangesets, ComponentSelection::All);
1452
1453 inject_archive_only_components(&mut selections, &manifest, true);
1454
1455 assert_eq!(
1456 selections.get(&SnapshotComponentType::TransactionSenders),
1457 Some(&ComponentSelection::All)
1458 );
1459 assert_eq!(
1460 selections.get(&SnapshotComponentType::RocksdbIndices),
1461 Some(&ComponentSelection::All)
1462 );
1463 }
1464
1465 #[test]
1466 fn inject_archive_only_components_without_rocksdb() {
1467 let manifest = manifest_with_archive_only_components();
1468 let mut selections = BTreeMap::new();
1469 selections.insert(SnapshotComponentType::Transactions, ComponentSelection::All);
1470 selections.insert(SnapshotComponentType::Receipts, ComponentSelection::All);
1471 selections.insert(SnapshotComponentType::AccountChangesets, ComponentSelection::All);
1472 selections.insert(SnapshotComponentType::StorageChangesets, ComponentSelection::All);
1473
1474 inject_archive_only_components(&mut selections, &manifest, false);
1475
1476 assert_eq!(
1477 selections.get(&SnapshotComponentType::TransactionSenders),
1478 Some(&ComponentSelection::All)
1479 );
1480 assert_eq!(selections.get(&SnapshotComponentType::RocksdbIndices), None);
1481 }
1482
1483 #[test]
1484 fn should_reset_index_stage_checkpoints_without_rocksdb_indices() {
1485 let mut selections = BTreeMap::new();
1486 selections.insert(SnapshotComponentType::Transactions, ComponentSelection::All);
1487 assert!(should_reset_index_stage_checkpoints(&selections));
1488
1489 selections.insert(SnapshotComponentType::RocksdbIndices, ComponentSelection::All);
1490 assert!(!should_reset_index_stage_checkpoints(&selections));
1491 }
1492
1493 #[test]
1494 fn startup_node_command_omits_default_chain_arg() {
1495 let command =
1496 startup_node_command_for_binary::<EthereumChainSpecParser>("reth", MAINNET.as_ref());
1497
1498 assert_eq!(command, "reth node");
1499 }
1500
1501 #[test]
1502 fn startup_node_command_includes_non_default_chain_arg() {
1503 let command =
1504 startup_node_command_for_binary::<EthereumChainSpecParser>("reth", HOLESKY.as_ref());
1505
1506 assert_eq!(command, "reth node --chain holesky");
1507 }
1508
1509 #[test]
1510 fn startup_node_command_uses_running_binary_name() {
1511 let command =
1512 startup_node_command_for_binary::<EthereumChainSpecParser>("tempo", HOLESKY.as_ref());
1513
1514 assert_eq!(command, "tempo node --chain holesky");
1515 }
1516
1517 #[test]
1518 fn download_command_uses_binary_name() {
1519 assert_eq!(download_command_for_binary("tempo"), "tempo download");
1520 }
1521}