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
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
130const MAX_CONCURRENT_DOWNLOADS: usize = 8;
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub(crate) enum SelectionPreset {
136 Minimal,
138 Full,
140 Archive,
142}
143
144struct ResolvedComponents {
145 selections: BTreeMap<SnapshotComponentType, ComponentSelection>,
146 preset: Option<SelectionPreset>,
147}
148
149static DOWNLOAD_DEFAULTS: OnceLock<DownloadDefaults> = OnceLock::new();
151
152#[derive(Debug, Clone)]
156pub struct DownloadDefaults {
157 pub available_snapshots: Vec<Cow<'static, str>>,
159 pub default_base_url: Cow<'static, str>,
161 pub default_chain_aware_base_url: Option<Cow<'static, str>>,
169 pub snapshot_api_url: Cow<'static, str>,
173 pub long_help: Option<String>,
175}
176
177impl DownloadDefaults {
178 pub fn try_init(self) -> Result<(), Self> {
180 DOWNLOAD_DEFAULTS.set(self)
181 }
182
183 pub fn get_global() -> &'static DownloadDefaults {
185 DOWNLOAD_DEFAULTS.get_or_init(DownloadDefaults::default_download_defaults)
186 }
187
188 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 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 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 pub fn with_snapshots(mut self, sources: Vec<Cow<'static, str>>) -> Self {
259 self.available_snapshots = sources;
260 self
261 }
262
263 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 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 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 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 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 fn default() -> Self {
329 Self::default_download_defaults()
330 }
331}
332
333#[derive(Debug, Parser)]
335pub struct DownloadCommand<C: ChainSpecParser> {
336 #[command(flatten)]
337 env: EnvironmentArgs<C>,
338
339 #[arg(long, short, long_help = DownloadDefaults::get_global().long_help())]
344 url: Option<String>,
345
346 #[arg(long, value_name = "URL", conflicts_with = "url")]
351 manifest_url: Option<String>,
352
353 #[arg(long, value_name = "PATH", conflicts_with_all = ["url", "manifest_url"])]
355 manifest_path: Option<PathBuf>,
356
357 #[arg(long, conflicts_with_all = ["with_txs_since", "with_txs_distance", "minimal", "full", "archive"])]
359 with_txs: bool,
360
361 #[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 #[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 #[arg(long, conflicts_with_all = ["with_receipts_since", "with_receipts_distance", "minimal", "full", "archive"])]
371 with_receipts: bool,
372
373 #[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 #[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 #[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 #[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 #[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 #[arg(long, requires = "with_txs", conflicts_with_all = ["minimal", "full", "archive"])]
395 with_senders: bool,
396
397 #[arg(long, conflicts_with_all = ["minimal", "full", "archive", "without_rocksdb"])]
399 with_rocksdb: bool,
400
401 #[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 #[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 #[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 #[arg(long, conflicts_with_all = ["url", "with_rocksdb"])]
417 without_rocksdb: bool,
418
419 #[arg(long, short = 'y')]
422 non_interactive: bool,
423
424 #[arg(long, conflicts_with = "list")]
426 force: bool,
427
428 #[arg(long, default_value_t = true, num_args = 0..=1, default_missing_value = "true")]
435 resumable: bool,
436
437 #[arg(long, default_value_t = MAX_CONCURRENT_DOWNLOADS)]
442 download_concurrency: usize,
443
444 #[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 pub async fn execute<N>(self) -> Result<()> {
455 let chain = self.env.chain.chain();
456 let chain_id = chain.id();
457
458 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 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 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 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 fn resolve_components(&self, manifest: &SnapshotManifest) -> Result<ResolvedComponents> {
599 let available = |ty: SnapshotComponentType| manifest.component(ty).is_some();
600
601 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 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 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 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 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 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 SnapshotComponentType::RocksdbIndices => ComponentSelection::None,
802 }
803 }
804
805 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
833fn 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
850fn 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
866fn 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
893fn 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
925fn 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>(¤t_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(¤t_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 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); }
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); 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}