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 if let Some(ref url) = self.url {
480 let cancel_token = CancellationToken::new();
481 let _cancel_guard = cancel_token.drop_guard();
482 let data_dir = self.env.datadir.clone().resolve_datadir(chain);
483 let target_dir = data_dir.data_dir();
484 if self.force {
485 clear_existing_datadir(target_dir)?;
486 }
487 fs::create_dir_all(target_dir)?;
488
489 let request_limiter = DownloadRequestLimiter::new(self.download_concurrency.max(1));
490 info!(target: "reth::cli",
491 dir = ?data_dir.data_dir(),
492 url = %url,
493 "Starting snapshot download and extraction"
494 );
495
496 stream_and_extract(
497 url,
498 data_dir.data_dir(),
499 None,
500 self.resumable,
501 Some(request_limiter),
502 cancel_token.clone(),
503 self.retry_backoff,
504 )
505 .await?;
506 info!(target: "reth::cli", "Snapshot downloaded and extracted successfully");
507
508 return Ok(None);
509 }
510
511 let ResolvedDownload { manifest, selections, preset, planned } =
512 self.resolve_download(chain.id()).await?;
513 let data_dir = self.env.datadir.clone().resolve_datadir(chain).data_dir().to_path_buf();
514 let prepared = PreparedSnapshotDownload { manifest, data_dir };
515 if self.print_plan_json {
516 DownloadPlan::from_planned(&prepared.manifest, &planned)
517 .write_json(std::io::stdout().lock())?;
518 return Ok(Some(prepared))
519 }
520
521 let target_dir = prepared.data_dir.as_path();
522 let cancel_token = CancellationToken::new();
523 let _cancel_guard = cancel_token.drop_guard();
524 if self.force {
525 clear_existing_datadir(target_dir)?;
526 }
527 fs::create_dir_all(target_dir)?;
528 let startup_summary = summarize_download_startup(&planned.archives, target_dir)?;
529 info!(target: "reth::cli",
530 reusable = startup_summary.reusable,
531 needs_download = startup_summary.needs_download,
532 "Startup integrity summary (plain output files)"
533 );
534
535 info!(target: "reth::cli",
536 archives = planned.total_archives(),
537 download_total = %DownloadProgress::format_size(planned.total_download_size),
538 output_total = %DownloadProgress::format_size(planned.total_output_size),
539 "Downloading all archives"
540 );
541
542 run_modular_downloads(
543 planned,
544 target_dir,
545 self.download_concurrency.max(1),
546 cancel_token.clone(),
547 self.retry_backoff,
548 )
549 .await?;
550
551 self.finalize_modular_download(
552 &selections,
553 &prepared.manifest,
554 preset,
555 target_dir,
556 &target_dir.join("db"),
557 )?;
558
559 Ok(Some(prepared))
560 }
561
562 pub async fn plan(&self) -> Result<(DownloadPlan, PreparedSnapshotDownload)> {
565 let chain = self.env.chain.chain();
566 let resolved = self.resolve_download(chain.id()).await?;
567 let plan = DownloadPlan::from_planned(&resolved.manifest, &resolved.planned);
568 let prepared = PreparedSnapshotDownload {
569 manifest: resolved.manifest,
570 data_dir: self.env.datadir.clone().resolve_datadir(chain).data_dir().to_path_buf(),
571 };
572 Ok((plan, prepared))
573 }
574
575 async fn resolve_download(&self, chain_id: u64) -> Result<ResolvedDownload> {
576 let manifest = self.load_manifest(chain_id).await?;
577 let ResolvedComponents { mut selections, preset } = self.resolve_components(&manifest)?;
578
579 if matches!(preset, Some(SelectionPreset::Archive)) {
580 inject_archive_only_components(&mut selections, &manifest, !self.without_rocksdb);
581 }
582
583 let planned = collect_planned_archives(&manifest, &selections)?;
584 Ok(ResolvedDownload { manifest, selections, preset, planned })
585 }
586
587 async fn load_manifest(&self, chain_id: u64) -> Result<SnapshotManifest> {
589 let manifest_source = self.resolve_manifest_source(chain_id).await?;
590
591 info!(target: "reth::cli", source = %manifest_source, "Fetching snapshot manifest");
592 let mut manifest = fetch_manifest_from_source(&manifest_source).await?;
593 eyre::ensure!(
594 manifest.chain_id == chain_id,
595 "Snapshot chain ID {} does not match selected chain ID {chain_id}",
596 manifest.chain_id
597 );
598 manifest.base_url = Some(resolve_manifest_base_url(&manifest, &manifest_source)?);
599
600 info!(target: "reth::cli",
601 block = manifest.block,
602 chain_id = manifest.chain_id,
603 storage_version = %manifest.storage_version,
604 components = manifest.components.len(),
605 "Loaded snapshot manifest"
606 );
607
608 Ok(manifest)
609 }
610
611 fn finalize_modular_download(
613 &self,
614 selections: &BTreeMap<SnapshotComponentType, ComponentSelection>,
615 manifest: &SnapshotManifest,
616 preset: Option<SelectionPreset>,
617 target_dir: &Path,
618 db_path: &Path,
619 ) -> Result<()> {
620 let config =
621 config_for_selections(selections, manifest, preset, Some(self.env.chain.as_ref()));
622 if write_config(&config, target_dir)? {
623 let desc = config_gen::describe_prune_config(&config);
624 info!(target: "reth::cli", "{}", desc.join(", "));
625 }
626
627 let db = init_db(db_path, self.env.db.database_args())?;
628 let should_write_prune = config.prune.segments != Default::default();
629 let should_reset_indices = should_reset_index_stage_checkpoints(selections);
630 if should_write_prune || should_reset_indices {
631 let tx = db.tx_mut()?;
632
633 if should_write_prune {
634 config_gen::write_prune_checkpoints_tx(&tx, &config, manifest.block)?;
635 }
636
637 if should_reset_indices {
638 config_gen::reset_index_stage_checkpoints_tx(&tx)?;
639 }
640
641 tx.commit()?;
642 }
643
644 let start_command = startup_node_command::<C>(self.env.chain.as_ref());
645 info!(target: "reth::cli", "Snapshot download complete. Run `{}` to start syncing.", start_command);
646
647 Ok(())
648 }
649
650 fn resolve_components(&self, manifest: &SnapshotManifest) -> Result<ResolvedComponents> {
652 let available = |ty: SnapshotComponentType| manifest.component(ty).is_some();
653
654 if self.archive {
656 return Ok(ResolvedComponents {
657 selections: SnapshotComponentType::ALL
658 .iter()
659 .copied()
660 .filter(|ty| available(*ty))
661 .filter(|ty| {
662 !self.without_rocksdb || *ty != SnapshotComponentType::RocksdbIndices
663 })
664 .map(|ty| (ty, ComponentSelection::All))
665 .collect(),
666 preset: Some(SelectionPreset::Archive),
667 });
668 }
669
670 if self.full {
671 return Ok(ResolvedComponents {
672 selections: self.full_preset_selections(manifest),
673 preset: Some(SelectionPreset::Full),
674 });
675 }
676
677 if self.minimal {
678 return Ok(ResolvedComponents {
679 selections: self.minimal_preset_selections(manifest),
680 preset: Some(SelectionPreset::Minimal),
681 });
682 }
683
684 let has_explicit_flags = self.with_txs ||
685 self.with_txs_since.is_some() ||
686 self.with_txs_distance.is_some() ||
687 self.with_receipts ||
688 self.with_receipts_since.is_some() ||
689 self.with_receipts_distance.is_some() ||
690 self.with_state_history ||
691 self.with_state_history_since.is_some() ||
692 self.with_state_history_distance.is_some() ||
693 self.with_senders ||
694 self.with_rocksdb;
695
696 if has_explicit_flags {
697 let mut selections = BTreeMap::new();
698 let tx_selection = explicit_component_selection(
699 self.with_txs,
700 self.with_txs_since,
701 self.with_txs_distance,
702 manifest.block,
703 );
704 let receipt_selection = explicit_component_selection(
705 self.with_receipts,
706 self.with_receipts_since,
707 self.with_receipts_distance,
708 manifest.block,
709 );
710 let state_history_selection = explicit_component_selection(
711 self.with_state_history,
712 self.with_state_history_since,
713 self.with_state_history_distance,
714 manifest.block,
715 );
716
717 if available(SnapshotComponentType::State) {
719 selections.insert(SnapshotComponentType::State, ComponentSelection::All);
720 }
721 if available(SnapshotComponentType::Headers) {
722 selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
723 }
724 if let Some(selection) = tx_selection &&
725 available(SnapshotComponentType::Transactions)
726 {
727 selections.insert(SnapshotComponentType::Transactions, selection);
728 }
729 if let Some(selection) = receipt_selection &&
730 available(SnapshotComponentType::Receipts)
731 {
732 selections.insert(SnapshotComponentType::Receipts, selection);
733 }
734 if let Some(selection) = state_history_selection {
735 if available(SnapshotComponentType::AccountChangesets) {
736 selections.insert(SnapshotComponentType::AccountChangesets, selection);
737 }
738 if available(SnapshotComponentType::StorageChangesets) {
739 selections.insert(SnapshotComponentType::StorageChangesets, selection);
740 }
741 }
742 if self.with_senders && available(SnapshotComponentType::TransactionSenders) {
743 selections
744 .insert(SnapshotComponentType::TransactionSenders, ComponentSelection::All);
745 }
746 if self.with_rocksdb && available(SnapshotComponentType::RocksdbIndices) {
747 selections.insert(SnapshotComponentType::RocksdbIndices, ComponentSelection::All);
748 }
749 return Ok(ResolvedComponents { selections, preset: None });
750 }
751
752 if self.non_interactive {
753 return Ok(ResolvedComponents {
754 selections: self.minimal_preset_selections(manifest),
755 preset: Some(SelectionPreset::Minimal),
756 });
757 }
758
759 let minimal_preset = self.minimal_preset_selections(manifest);
761 let full_preset = self.full_preset_selections(manifest);
762 let SelectorOutput { selections, preset } =
763 run_selector(manifest.clone(), &minimal_preset, &full_preset)?;
764 let selected =
765 selections.into_iter().filter(|(_, sel)| *sel != ComponentSelection::None).collect();
766
767 Ok(ResolvedComponents { selections: selected, preset })
768 }
769
770 fn minimal_preset_selections(
772 &self,
773 manifest: &SnapshotManifest,
774 ) -> BTreeMap<SnapshotComponentType, ComponentSelection> {
775 self.pruning_preset_selections(manifest, SelectionPreset::Minimal)
776 }
777
778 fn full_preset_selections(
780 &self,
781 manifest: &SnapshotManifest,
782 ) -> BTreeMap<SnapshotComponentType, ComponentSelection> {
783 self.pruning_preset_selections(manifest, SelectionPreset::Full)
784 }
785
786 fn pruning_preset_selections(
788 &self,
789 manifest: &SnapshotManifest,
790 preset: SelectionPreset,
791 ) -> BTreeMap<SnapshotComponentType, ComponentSelection> {
792 let defaults = DefaultPruningValues::get_global();
793 let (prune_modes, bodies_history_use_pre_merge) = match preset {
794 SelectionPreset::Minimal => (&defaults.minimal_prune_modes, false),
795 SelectionPreset::Full => {
796 (&defaults.full_prune_modes, defaults.full_bodies_history_use_pre_merge)
797 }
798 SelectionPreset::Archive => unreachable!("archive selects every component"),
799 };
800 let mut selections = BTreeMap::new();
801
802 for &ty in &SnapshotComponentType::ALL {
803 if manifest.component(ty).is_none() {
804 continue;
805 }
806
807 let selection = self.pruning_selection_for_component(
808 ty,
809 manifest.block,
810 prune_modes,
811 bodies_history_use_pre_merge,
812 );
813 if selection != ComponentSelection::None {
814 selections.insert(ty, selection);
815 }
816 }
817
818 selections
819 }
820
821 fn pruning_selection_for_component(
823 &self,
824 ty: SnapshotComponentType,
825 snapshot_block: u64,
826 prune_modes: &PruneModes,
827 bodies_history_use_pre_merge: bool,
828 ) -> ComponentSelection {
829 if ty == SnapshotComponentType::Transactions && bodies_history_use_pre_merge {
830 return match self
831 .env
832 .chain
833 .ethereum_fork_activation(EthereumHardfork::Paris)
834 .block_number()
835 {
836 Some(paris) if snapshot_block >= paris => ComponentSelection::Since(paris),
837 Some(_) => ComponentSelection::None,
838 None => ComponentSelection::All,
839 }
840 }
841
842 let mode = match ty {
843 SnapshotComponentType::State | SnapshotComponentType::Headers => {
844 return ComponentSelection::All
845 }
846 SnapshotComponentType::Transactions => prune_modes.bodies_history,
847 SnapshotComponentType::TransactionSenders => prune_modes.sender_recovery,
848 SnapshotComponentType::Receipts => prune_modes.receipts,
849 SnapshotComponentType::AccountChangesets => prune_modes.account_history,
850 SnapshotComponentType::StorageChangesets => prune_modes.storage_history,
851 SnapshotComponentType::RocksdbIndices => return ComponentSelection::None,
852 };
853
854 selection_from_prune_mode(mode, snapshot_block)
855 }
856
857 async fn resolve_manifest_source(&self, chain_id: u64) -> Result<String> {
859 if let Some(path) = &self.manifest_path {
860 return Ok(path.display().to_string());
861 }
862
863 match &self.manifest_url {
864 Some(url) => Ok(url.clone()),
865 None => {
866 let defaults = DownloadDefaults::get_global();
867 if defaults.mainnet_only_discovery() && chain_id != MAINNET.chain.id() {
868 eyre::bail!(
869 "Snapshots are only auto-discovered for Ethereum mainnet.\n\n\
870 Chain {chain_id} requires an explicit source:\n\
871 \t--manifest-url <URL>\n\
872 \t--manifest-path <PATH>\n\
873 \t-u <SNAPSHOT-URL>\n\n\
874 Use --list to inspect snapshots exposed by {}.",
875 defaults.snapshot_source_url(),
876 );
877 }
878
879 discover_manifest_url(chain_id).await
880 }
881 }
882 }
883}
884
885fn explicit_component_selection(
888 all: bool,
889 since: Option<u64>,
890 distance: Option<u64>,
891 snapshot_block: u64,
892) -> Option<ComponentSelection> {
893 if all {
894 Some(ComponentSelection::All)
895 } else if let Some(block) = since {
896 (block <= snapshot_block).then_some(ComponentSelection::Since(block))
897 } else {
898 distance.map(ComponentSelection::Distance)
899 }
900}
901
902fn selection_from_prune_mode(mode: Option<PruneMode>, snapshot_block: u64) -> ComponentSelection {
904 match mode {
905 None => ComponentSelection::All,
906 Some(PruneMode::Full) => ComponentSelection::None,
907 Some(PruneMode::Distance(d)) => ComponentSelection::Distance(d),
908 Some(PruneMode::Before(block)) => {
909 if snapshot_block >= block {
910 ComponentSelection::Since(block)
911 } else {
912 ComponentSelection::None
913 }
914 }
915 }
916}
917
918fn clear_existing_datadir(target_dir: &Path) -> Result<()> {
920 if !target_dir.try_exists()? {
921 return Ok(());
922 }
923
924 info!(target: "reth::cli", dir = ?target_dir, "Clearing existing snapshot data");
925 for entry in FORCE_REMOVED_DATADIR_PATHS {
926 let path = target_dir.join(entry);
927 if !path.try_exists()? {
928 continue;
929 }
930
931 let metadata = fs::metadata(&path)?;
932 if metadata.is_dir() {
933 fs::remove_dir_all(&path)?;
934 } else if metadata.is_file() {
935 fs::remove_file(&path)?;
936 }
937 }
938
939 Ok(())
940}
941
942fn inject_archive_only_components(
945 selections: &mut BTreeMap<SnapshotComponentType, ComponentSelection>,
946 manifest: &SnapshotManifest,
947 include_rocksdb: bool,
948) {
949 let is_all =
950 |ty: SnapshotComponentType| selections.get(&ty).copied() == Some(ComponentSelection::All);
951
952 let is_archive = is_all(SnapshotComponentType::Transactions) &&
953 is_all(SnapshotComponentType::Receipts) &&
954 is_all(SnapshotComponentType::AccountChangesets) &&
955 is_all(SnapshotComponentType::StorageChangesets);
956
957 if !is_archive {
958 return;
959 }
960
961 for component in
962 [SnapshotComponentType::TransactionSenders, SnapshotComponentType::RocksdbIndices]
963 {
964 if component == SnapshotComponentType::RocksdbIndices && !include_rocksdb {
965 continue;
966 }
967
968 if manifest.component(component).is_some() {
969 selections.insert(component, ComponentSelection::All);
970 }
971 }
972}
973
974fn should_reset_index_stage_checkpoints(
976 selections: &BTreeMap<SnapshotComponentType, ComponentSelection>,
977) -> bool {
978 !matches!(selections.get(&SnapshotComponentType::RocksdbIndices), Some(ComponentSelection::All))
979}
980
981fn startup_node_command<C>(chain_spec: &C::ChainSpec) -> String
982where
983 C: ChainSpecParser,
984 C::ChainSpec: EthChainSpec,
985{
986 startup_node_command_for_binary::<C>(¤t_binary_name(), chain_spec)
987}
988
989fn startup_node_command_for_binary<C>(binary_name: &str, chain_spec: &C::ChainSpec) -> String
990where
991 C: ChainSpecParser,
992 C::ChainSpec: EthChainSpec,
993{
994 let mut command = format!("{binary_name} node");
995
996 if let Some(chain_arg) = startup_chain_arg::<C>(chain_spec) {
997 command.push_str(" --chain ");
998 command.push_str(&chain_arg);
999 }
1000
1001 command
1002}
1003
1004fn current_binary_name() -> String {
1005 std::env::args_os()
1006 .next()
1007 .map(PathBuf::from)
1008 .and_then(|path| path.file_stem().map(|name| name.to_owned()))
1009 .and_then(|name| name.into_string().ok())
1010 .filter(|name| !name.is_empty())
1011 .unwrap_or_else(|| "reth".to_string())
1012}
1013
1014fn download_command() -> String {
1015 download_command_for_binary(¤t_binary_name())
1016}
1017
1018fn download_command_for_binary(binary_name: &str) -> String {
1019 format!("{binary_name} download")
1020}
1021
1022fn startup_chain_arg<C>(chain_spec: &C::ChainSpec) -> Option<String>
1023where
1024 C: ChainSpecParser,
1025 C::ChainSpec: EthChainSpec,
1026{
1027 let current_chain = chain_spec.chain();
1028 let current_genesis_hash = chain_spec.genesis_hash();
1029 let default_chain = C::default_value().and_then(|chain_name| C::parse(chain_name).ok());
1030
1031 if default_chain.as_ref().is_some_and(|default_chain| {
1032 default_chain.chain() == current_chain &&
1033 default_chain.genesis_hash() == current_genesis_hash
1034 }) {
1035 return None;
1036 }
1037
1038 C::SUPPORTED_CHAINS
1039 .iter()
1040 .find_map(|chain_name| {
1041 let parsed_chain = C::parse(chain_name).ok()?;
1042 (parsed_chain.chain() == current_chain &&
1043 parsed_chain.genesis_hash() == current_genesis_hash)
1044 .then(|| (*chain_name).to_string())
1045 })
1046 .or_else(|| Some("<chain-or-chainspec>".to_string()))
1047}
1048
1049impl<C: ChainSpecParser> DownloadCommand<C> {
1050 pub const fn env(&self) -> &EnvironmentArgs<C> {
1052 &self.env
1053 }
1054
1055 pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
1057 Some(&self.env.chain)
1058 }
1059
1060 pub const fn prints_plan_json(&self) -> bool {
1062 self.print_plan_json
1063 }
1064}
1065
1066#[derive(Debug)]
1068pub struct PreparedSnapshotDownload {
1069 pub manifest: SnapshotManifest,
1071 pub data_dir: PathBuf,
1073}
1074
1075struct ResolvedDownload {
1076 manifest: SnapshotManifest,
1077 selections: BTreeMap<SnapshotComponentType, ComponentSelection>,
1078 preset: Option<SelectionPreset>,
1079 planned: PlannedDownloads,
1080}
1081
1082const MAX_DOWNLOAD_RETRIES: u32 = 10;
1083const RETRY_BACKOFF_SECS: u64 = 5;
1084
1085#[cfg(test)]
1086mod tests {
1087 use super::*;
1088 use clap::{Args, Parser};
1089 use extract::CompressionFormat;
1090 use manifest::{ComponentManifest, SingleArchive};
1091 use reth_chainspec::{HOLESKY, MAINNET};
1092 use reth_ethereum_cli::chainspec::EthereumChainSpecParser;
1093
1094 #[derive(Parser)]
1095 struct CommandParser<T: Args> {
1096 #[command(flatten)]
1097 args: T,
1098 }
1099
1100 fn manifest_with_archive_only_components() -> SnapshotManifest {
1101 let mut components = BTreeMap::new();
1102 components.insert(
1103 SnapshotComponentType::TransactionSenders.key().to_string(),
1104 ComponentManifest::Single(SingleArchive {
1105 file: "transaction_senders.tar.zst".to_string(),
1106 size: 1,
1107 decompressed_size: 0,
1108 blake3: None,
1109 output_files: vec![],
1110 }),
1111 );
1112 components.insert(
1113 SnapshotComponentType::RocksdbIndices.key().to_string(),
1114 ComponentManifest::Single(SingleArchive {
1115 file: "rocksdb_indices.tar.zst".to_string(),
1116 size: 1,
1117 decompressed_size: 0,
1118 blake3: None,
1119 output_files: vec![],
1120 }),
1121 );
1122 SnapshotManifest {
1123 block: 0,
1124 chain_id: 1,
1125 storage_version: 2,
1126 timestamp: 0,
1127 base_url: Some("https://example.com".to_string()),
1128 reth_version: None,
1129 components,
1130 extensions: Default::default(),
1131 }
1132 }
1133
1134 #[test]
1135 fn test_download_defaults_builder() {
1136 let defaults = DownloadDefaults::default()
1137 .with_snapshot("https://example.com/snapshots (example)")
1138 .with_base_url("https://example.com");
1139
1140 assert_eq!(defaults.default_base_url, "https://example.com");
1141 assert_eq!(defaults.available_snapshots.len(), 3); }
1143
1144 #[test]
1145 fn test_download_defaults_replace_snapshots() {
1146 let defaults = DownloadDefaults::default().with_snapshots(vec![
1147 Cow::Borrowed("https://custom1.com"),
1148 Cow::Borrowed("https://custom2.com"),
1149 ]);
1150
1151 assert_eq!(defaults.available_snapshots.len(), 2);
1152 assert_eq!(defaults.available_snapshots[0], "https://custom1.com");
1153 }
1154
1155 #[test]
1156 fn test_long_help_generation() {
1157 let defaults = DownloadDefaults::default();
1158 let help = defaults.long_help();
1159
1160 assert!(help.contains("Available snapshot sources:"));
1161 assert!(help.contains("Ethereum mainnet"));
1162 assert!(help.contains("snapshots.reth.rs"));
1163 assert!(help.contains("publicnode.com"));
1164 assert!(help.contains("file://"));
1165 }
1166
1167 #[test]
1168 fn test_custom_snapshot_api_keeps_selected_chain_help() {
1169 let defaults = DownloadDefaults::default()
1170 .with_snapshot_api_url("https://snapshots.tempoxyz.dev/api/snapshots");
1171 let help = defaults.long_help();
1172
1173 assert_eq!(
1174 defaults.default_chain_aware_base_url.as_deref(),
1175 Some("https://snapshots.tempoxyz.dev")
1176 );
1177 assert!(help.contains("Browse available snapshots at https://snapshots.tempoxyz.dev"));
1178 assert!(help.contains("- https://snapshots.tempoxyz.dev (default)"));
1179 assert!(help.contains("selected chain"));
1180 assert!(!help.contains("Ethereum mainnet"));
1181 assert!(!help.contains("snapshots.reth.rs"));
1182 }
1183
1184 #[test]
1185 fn test_snapshot_source_url_sets_generated_references() {
1186 let defaults =
1187 DownloadDefaults::default().with_snapshot_source_url("https://snapshots.tempoxyz.dev/");
1188 let help = defaults.long_help();
1189
1190 assert_eq!(defaults.snapshot_api_url, "https://snapshots.tempoxyz.dev/api/snapshots");
1191 assert_eq!(defaults.default_base_url, "https://snapshots.tempoxyz.dev");
1192 assert_eq!(
1193 defaults.default_chain_aware_base_url.as_deref(),
1194 Some("https://snapshots.tempoxyz.dev")
1195 );
1196 assert_eq!(
1197 defaults.available_snapshots.iter().map(|source| source.as_ref()).collect::<Vec<_>>(),
1198 vec!["https://snapshots.tempoxyz.dev (default)"]
1199 );
1200 assert!(!defaults.mainnet_only_discovery());
1201 assert!(help.contains("Browse available snapshots at https://snapshots.tempoxyz.dev"));
1202 assert!(help.contains("from https://snapshots.tempoxyz.dev."));
1203 }
1204
1205 #[test]
1206 fn test_snapshot_api_url_trailing_slash_sets_source_url() {
1207 let defaults = DownloadDefaults::default()
1208 .with_snapshot_api_url("https://snapshots.tempoxyz.dev/api/snapshots/");
1209 let help = defaults.long_help();
1210
1211 assert_eq!(
1212 defaults.default_chain_aware_base_url.as_deref(),
1213 Some("https://snapshots.tempoxyz.dev")
1214 );
1215 assert!(help.contains("Browse available snapshots at https://snapshots.tempoxyz.dev"));
1216 assert!(help.contains("- https://snapshots.tempoxyz.dev (default)"));
1217 }
1218
1219 #[test]
1220 fn test_long_help_override() {
1221 let custom_help = "This is custom help text for downloading snapshots.";
1222 let defaults = DownloadDefaults::default().with_long_help(custom_help);
1223
1224 let help = defaults.long_help();
1225 assert_eq!(help, custom_help);
1226 assert!(!help.contains("Available snapshot sources:"));
1227 }
1228
1229 #[test]
1230 fn test_builder_chaining() {
1231 let defaults = DownloadDefaults::default()
1232 .with_base_url("https://custom.example.com")
1233 .with_snapshot("https://snapshot1.com")
1234 .with_snapshot("https://snapshot2.com")
1235 .with_long_help("Custom help for snapshots");
1236
1237 assert_eq!(defaults.default_base_url, "https://custom.example.com");
1238 assert_eq!(defaults.available_snapshots.len(), 4); assert_eq!(defaults.long_help, Some("Custom help for snapshots".to_string()));
1240 }
1241
1242 #[test]
1243 fn test_download_retry_backoff() {
1244 let parse = |args: Vec<&str>| {
1245 CommandParser::<DownloadCommand<EthereumChainSpecParser>>::try_parse_from(args)
1246 };
1247 assert_eq!(parse(vec!["reth"]).unwrap().args.retry_backoff, None);
1248 for (value, expected) in [
1249 ("0ms", Duration::ZERO),
1250 ("250ms", Duration::from_millis(250)),
1251 ("2s", Duration::from_secs(2)),
1252 ] {
1253 assert_eq!(
1254 parse(vec!["reth", "--retry-backoff", value]).unwrap().args.retry_backoff,
1255 Some(expected)
1256 );
1257 }
1258 assert!(parse(vec!["reth", "--retry-backoff=-1s"]).is_err());
1259 assert!(parse(vec!["reth", "--retry-backoff", "invalid"]).is_err());
1260 }
1261
1262 #[test]
1263 fn test_download_resumable_defaults_to_true() {
1264 let args =
1265 CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from(["reth"]).args;
1266
1267 assert!(args.resumable);
1268 }
1269
1270 #[test]
1271 fn test_download_resumable_implicit_true() {
1272 let args = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from([
1273 "reth",
1274 "--resumable",
1275 ])
1276 .args;
1277
1278 assert!(args.resumable);
1279 }
1280
1281 #[test]
1282 fn test_download_resumable_explicit_false() {
1283 let args = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from([
1284 "reth",
1285 "--resumable=false",
1286 ])
1287 .args;
1288
1289 assert!(!args.resumable);
1290 }
1291
1292 #[test]
1293 fn test_download_print_plan_json_parses() {
1294 let args = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from([
1295 "reth",
1296 "--manifest-path",
1297 "manifest.json",
1298 "--minimal",
1299 "--print-plan-json",
1300 ])
1301 .args;
1302
1303 assert!(args.prints_plan_json());
1304 }
1305
1306 #[test]
1307 fn test_download_print_plan_json_rejects_single_archive() {
1308 let result = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::try_parse_from([
1309 "reth",
1310 "--url",
1311 "https://example.com/snapshot.tar.zst",
1312 "--print-plan-json",
1313 ]);
1314
1315 assert!(result.is_err());
1316 }
1317
1318 #[test]
1319 fn minimal_component_selection_uses_configured_prune_modes() {
1320 let args =
1321 CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from(["reth"]).args;
1322 let history_distance = 64_864;
1323 let modes = PruneModes {
1324 sender_recovery: Some(PruneMode::Full),
1325 receipts: Some(PruneMode::Distance(128)),
1326 account_history: Some(PruneMode::Distance(history_distance)),
1327 storage_history: Some(PruneMode::Distance(history_distance)),
1328 bodies_history: Some(PruneMode::Distance(history_distance)),
1329 ..Default::default()
1330 };
1331
1332 assert_eq!(
1333 args.pruning_selection_for_component(
1334 SnapshotComponentType::Transactions,
1335 1_000_000,
1336 &modes,
1337 false,
1338 ),
1339 ComponentSelection::Distance(history_distance)
1340 );
1341 assert_eq!(
1342 args.pruning_selection_for_component(
1343 SnapshotComponentType::Receipts,
1344 1_000_000,
1345 &modes,
1346 false,
1347 ),
1348 ComponentSelection::Distance(128)
1349 );
1350 assert_eq!(
1351 args.pruning_selection_for_component(
1352 SnapshotComponentType::AccountChangesets,
1353 1_000_000,
1354 &modes,
1355 false,
1356 ),
1357 ComponentSelection::Distance(history_distance)
1358 );
1359 assert_eq!(
1360 args.pruning_selection_for_component(
1361 SnapshotComponentType::StorageChangesets,
1362 1_000_000,
1363 &modes,
1364 false,
1365 ),
1366 ComponentSelection::Distance(history_distance)
1367 );
1368 assert_eq!(
1369 args.pruning_selection_for_component(
1370 SnapshotComponentType::TransactionSenders,
1371 1_000_000,
1372 &modes,
1373 false,
1374 ),
1375 ComponentSelection::None
1376 );
1377 }
1378
1379 #[test]
1380 fn resolve_manifest_source_requires_explicit_source_for_non_mainnet_defaults() {
1381 let args = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from([
1382 "reth", "--chain", "holesky",
1383 ])
1384 .args;
1385
1386 let err = tokio::runtime::Runtime::new()
1387 .unwrap()
1388 .block_on(args.resolve_manifest_source(HOLESKY.chain.id()))
1389 .unwrap_err();
1390
1391 let message = err.to_string();
1392 assert!(message.contains("only auto-discovered for Ethereum mainnet"));
1393 assert!(message.contains("--manifest-url <URL>"));
1394 assert!(message.contains("-u <SNAPSHOT-URL>"));
1395 }
1396
1397 #[test]
1398 fn resolve_manifest_source_allows_manifest_path_for_non_mainnet_defaults() {
1399 let args = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from([
1400 "reth",
1401 "--chain",
1402 "holesky",
1403 "--manifest-path",
1404 "./manifest.json",
1405 ])
1406 .args;
1407
1408 let source = tokio::runtime::Runtime::new()
1409 .unwrap()
1410 .block_on(args.resolve_manifest_source(HOLESKY.chain.id()))
1411 .unwrap();
1412
1413 assert_eq!(source, "./manifest.json");
1414 }
1415
1416 #[test]
1417 fn test_compression_format_detection() {
1418 assert!(matches!(
1419 CompressionFormat::from_url("https://example.com/snapshot.tar.lz4"),
1420 Ok(CompressionFormat::Lz4)
1421 ));
1422 assert!(matches!(
1423 CompressionFormat::from_url("https://example.com/snapshot.tar.zst"),
1424 Ok(CompressionFormat::Zstd)
1425 ));
1426 assert!(matches!(
1427 CompressionFormat::from_url("file:///path/to/snapshot.tar.lz4"),
1428 Ok(CompressionFormat::Lz4)
1429 ));
1430 assert!(matches!(
1431 CompressionFormat::from_url("file:///path/to/snapshot.tar.zst"),
1432 Ok(CompressionFormat::Zstd)
1433 ));
1434 assert!(CompressionFormat::from_url("https://example.com/snapshot.tar.gz").is_err());
1435 }
1436
1437 #[test]
1438 fn inject_archive_only_components_for_archive_selection() {
1439 let manifest = manifest_with_archive_only_components();
1440 let mut selections = BTreeMap::new();
1441 selections.insert(SnapshotComponentType::Transactions, ComponentSelection::All);
1442 selections.insert(SnapshotComponentType::Receipts, ComponentSelection::All);
1443 selections.insert(SnapshotComponentType::AccountChangesets, ComponentSelection::All);
1444 selections.insert(SnapshotComponentType::StorageChangesets, ComponentSelection::All);
1445
1446 inject_archive_only_components(&mut selections, &manifest, true);
1447
1448 assert_eq!(
1449 selections.get(&SnapshotComponentType::TransactionSenders),
1450 Some(&ComponentSelection::All)
1451 );
1452 assert_eq!(
1453 selections.get(&SnapshotComponentType::RocksdbIndices),
1454 Some(&ComponentSelection::All)
1455 );
1456 }
1457
1458 #[test]
1459 fn inject_archive_only_components_without_rocksdb() {
1460 let manifest = manifest_with_archive_only_components();
1461 let mut selections = BTreeMap::new();
1462 selections.insert(SnapshotComponentType::Transactions, ComponentSelection::All);
1463 selections.insert(SnapshotComponentType::Receipts, ComponentSelection::All);
1464 selections.insert(SnapshotComponentType::AccountChangesets, ComponentSelection::All);
1465 selections.insert(SnapshotComponentType::StorageChangesets, ComponentSelection::All);
1466
1467 inject_archive_only_components(&mut selections, &manifest, false);
1468
1469 assert_eq!(
1470 selections.get(&SnapshotComponentType::TransactionSenders),
1471 Some(&ComponentSelection::All)
1472 );
1473 assert_eq!(selections.get(&SnapshotComponentType::RocksdbIndices), None);
1474 }
1475
1476 #[test]
1477 fn should_reset_index_stage_checkpoints_without_rocksdb_indices() {
1478 let mut selections = BTreeMap::new();
1479 selections.insert(SnapshotComponentType::Transactions, ComponentSelection::All);
1480 assert!(should_reset_index_stage_checkpoints(&selections));
1481
1482 selections.insert(SnapshotComponentType::RocksdbIndices, ComponentSelection::All);
1483 assert!(!should_reset_index_stage_checkpoints(&selections));
1484 }
1485
1486 #[test]
1487 fn startup_node_command_omits_default_chain_arg() {
1488 let command =
1489 startup_node_command_for_binary::<EthereumChainSpecParser>("reth", MAINNET.as_ref());
1490
1491 assert_eq!(command, "reth node");
1492 }
1493
1494 #[test]
1495 fn startup_node_command_includes_non_default_chain_arg() {
1496 let command =
1497 startup_node_command_for_binary::<EthereumChainSpecParser>("reth", HOLESKY.as_ref());
1498
1499 assert_eq!(command, "reth node --chain holesky");
1500 }
1501
1502 #[test]
1503 fn startup_node_command_uses_running_binary_name() {
1504 let command =
1505 startup_node_command_for_binary::<EthereumChainSpecParser>("tempo", HOLESKY.as_ref());
1506
1507 assert_eq!(command, "tempo node --chain holesky");
1508 }
1509
1510 #[test]
1511 fn download_command_uses_binary_name() {
1512 assert_eq!(download_command_for_binary("tempo"), "tempo download");
1513 }
1514}