1mod archive;
87pub mod config_gen;
88mod extract;
89mod fetch;
90pub mod manifest;
91pub mod manifest_cmd;
92mod planning;
93mod progress;
94mod prune;
95mod session;
96mod source;
97mod tui;
98mod verify;
99
100pub use planning::{DownloadPlan, DownloadPlanArchive};
101
102use crate::common::EnvironmentArgs;
103use archive::run_modular_downloads;
104use clap::{builder::RangedU64ValueParser, Parser};
105use config_gen::{config_for_selections, write_config};
106use extract::stream_and_extract;
107use eyre::Result;
108use manifest::{ComponentSelection, SnapshotComponentType, SnapshotManifest};
109use planning::{collect_planned_archives, PlannedDownloads};
110use progress::{DownloadProgress, DownloadRequestLimiter};
111use prune::prune_unlisted_outputs;
112use reth_chainspec::{EthChainSpec, EthereumHardfork, EthereumHardforks, MAINNET};
113use reth_cli::chainspec::ChainSpecParser;
114use reth_cli_util::cancellation::CancellationToken;
115use reth_db::{init_db, Database};
116use reth_db_api::transaction::DbTx;
117use reth_fs_util as fs;
118use reth_node_core::args::DefaultPruningValues;
119use reth_prune_types::{PruneMode, PruneModes};
120use source::{
121 discover_manifest_url, fetch_manifest_from_source, fetch_snapshot_api_entries,
122 print_snapshot_listing, resolve_manifest_base_url,
123};
124use std::{
125 borrow::Cow,
126 collections::BTreeMap,
127 path::{Path, PathBuf},
128 sync::{Arc, OnceLock},
129 time::Duration,
130};
131use tracing::info;
132use tui::{run_selector, SelectorOutput};
133
134const RETH_SNAPSHOTS_BASE_URL: &str = "https://snapshots-r2.reth.rs";
135const RETH_SNAPSHOTS_API_URL: &str = "https://snapshots.reth.rs/api/snapshots";
136const RETH_SNAPSHOTS_SOURCE: &str = "https://snapshots.reth.rs (default)";
137const SNAPSHOT_API_PATH: &str = "/api/snapshots";
138const MANAGED_DATADIR_PATHS: &[&str] = &["db", "rocksdb", "static_files", "reth.toml"];
139
140const MAX_CONCURRENT_DOWNLOADS: usize = 8;
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub(crate) enum SelectionPreset {
146 Minimal,
148 Full,
150 Archive,
152}
153
154struct ResolvedComponents {
155 selections: BTreeMap<SnapshotComponentType, ComponentSelection>,
156 preset: Option<SelectionPreset>,
157}
158
159static DOWNLOAD_DEFAULTS: OnceLock<DownloadDefaults> = OnceLock::new();
161
162#[derive(Debug, Clone)]
166pub struct DownloadDefaults {
167 pub available_snapshots: Vec<Cow<'static, str>>,
169 pub default_base_url: Cow<'static, str>,
171 pub default_chain_aware_base_url: Option<Cow<'static, str>>,
179 pub snapshot_api_url: Cow<'static, str>,
183 pub long_help: Option<String>,
185 pub full_preset_includes_rocksdb_indices: bool,
188}
189
190impl DownloadDefaults {
191 #[expect(clippy::result_large_err, reason = "returns the rejected defaults to the caller")]
193 pub fn try_init(self) -> Result<(), Self> {
194 DOWNLOAD_DEFAULTS.set(self)
195 }
196
197 pub fn get_global() -> &'static DownloadDefaults {
199 DOWNLOAD_DEFAULTS.get_or_init(DownloadDefaults::default_download_defaults)
200 }
201
202 pub fn default_download_defaults() -> Self {
204 Self {
205 available_snapshots: vec![
206 Cow::Borrowed(RETH_SNAPSHOTS_SOURCE),
207 Cow::Borrowed("https://publicnode.com/snapshots (full nodes & testnets)"),
208 ],
209 default_base_url: Cow::Borrowed(RETH_SNAPSHOTS_BASE_URL),
210 default_chain_aware_base_url: None,
211 snapshot_api_url: Cow::Borrowed(RETH_SNAPSHOTS_API_URL),
212 long_help: None,
213 full_preset_includes_rocksdb_indices: false,
214 }
215 }
216
217 pub fn long_help(&self) -> String {
222 if let Some(ref custom_help) = self.long_help {
223 return custom_help.clone();
224 }
225
226 let implicit_download_help = if self.mainnet_only_discovery() {
227 "\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."
228 } else {
229 "\nIf no URL is provided, the latest archive snapshot for the selected chain\nwill be proposed for download from "
230 };
231
232 let mut help = format!(
233 "Specify a snapshot URL or let the command propose a default one.\n\n\
234 Browse available snapshots at {}\n\
235 or use --list-snapshots to see them from the CLI.\n\nAvailable snapshot sources:\n",
236 self.snapshot_source_url(),
237 );
238
239 for source in &self.available_snapshots {
240 help.push_str("- ");
241 help.push_str(source);
242 help.push('\n');
243 }
244
245 help.push_str(implicit_download_help);
246 if !self.mainnet_only_discovery() {
247 help.push_str(
248 self.default_chain_aware_base_url.as_deref().unwrap_or(&self.default_base_url),
249 );
250 help.push('.');
251 }
252 help.push_str(
253 "\n\nLocal file:// URLs are also supported for extracting snapshots from disk.",
254 );
255 help
256 }
257
258 fn mainnet_only_discovery(&self) -> bool {
259 self.snapshot_api_url.trim_end_matches('/') == RETH_SNAPSHOTS_API_URL
260 }
261
262 fn snapshot_source_url(&self) -> &str {
263 snapshot_source_url_from_api(&self.snapshot_api_url)
264 }
265
266 pub fn with_snapshot(mut self, source: impl Into<Cow<'static, str>>) -> Self {
268 self.available_snapshots.push(source.into());
269 self
270 }
271
272 pub fn with_snapshots(mut self, sources: Vec<Cow<'static, str>>) -> Self {
274 self.available_snapshots = sources;
275 self
276 }
277
278 pub fn with_base_url(mut self, url: impl Into<Cow<'static, str>>) -> Self {
280 self.default_base_url = url.into();
281 self
282 }
283
284 pub fn with_chain_aware_base_url(mut self, url: impl Into<Cow<'static, str>>) -> Self {
286 self.default_chain_aware_base_url = Some(url.into());
287 self
288 }
289
290 pub fn with_snapshot_api_url(mut self, url: impl Into<Cow<'static, str>>) -> Self {
295 self.snapshot_api_url = url.into();
296
297 let source_url = self.snapshot_source_url().to_string();
298 if self.default_chain_aware_base_url.is_none() {
299 self.default_chain_aware_base_url = Some(Cow::Owned(source_url.clone()));
300 }
301 for source in &mut self.available_snapshots {
302 if source.as_ref() == RETH_SNAPSHOTS_SOURCE {
303 *source = Cow::Owned(format!("{source_url} (default)"));
304 }
305 }
306
307 self
308 }
309
310 pub fn with_snapshot_source_url(mut self, url: impl Into<Cow<'static, str>>) -> Self {
315 let source_url = normalize_snapshot_source_url(url.into());
316 self.available_snapshots = vec![Cow::Owned(format!("{} (default)", source_url.as_ref()))];
317 self.default_base_url = source_url.clone();
318 self.default_chain_aware_base_url = Some(source_url.clone());
319 self.snapshot_api_url = Cow::Owned(format!("{}{SNAPSHOT_API_PATH}", source_url.as_ref()));
320 self
321 }
322
323 pub const fn with_full_preset_rocksdb_indices(mut self) -> Self {
325 self.full_preset_includes_rocksdb_indices = true;
326 self
327 }
328
329 pub fn with_long_help(mut self, help: impl Into<String>) -> Self {
331 self.long_help = Some(help.into());
332 self
333 }
334}
335
336fn snapshot_source_url_from_api(api_url: &str) -> &str {
337 api_url.trim_end_matches('/').trim_end_matches(SNAPSHOT_API_PATH)
338}
339
340fn normalize_snapshot_source_url(url: Cow<'static, str>) -> Cow<'static, str> {
341 match url {
342 Cow::Borrowed(url) => Cow::Borrowed(snapshot_source_url_from_api(url)),
343 Cow::Owned(url) => Cow::Owned(snapshot_source_url_from_api(&url).to_string()),
344 }
345}
346
347impl Default for DownloadDefaults {
348 fn default() -> Self {
350 Self::default_download_defaults()
351 }
352}
353
354#[derive(Debug, Parser)]
356pub struct DownloadCommand<C: ChainSpecParser> {
357 #[command(flatten)]
358 env: EnvironmentArgs<C>,
359
360 #[arg(long, short, long_help = DownloadDefaults::get_global().long_help())]
365 url: Option<String>,
366
367 #[arg(long, value_name = "URL", conflicts_with = "url")]
372 manifest_url: Option<String>,
373
374 #[arg(long, value_name = "PATH", conflicts_with_all = ["url", "manifest_url"])]
376 manifest_path: Option<PathBuf>,
377
378 #[arg(long, conflicts_with_all = ["with_txs_since", "with_txs_distance", "minimal", "full", "archive"])]
380 with_txs: bool,
381
382 #[arg(long, value_name = "BLOCK_NUMBER", conflicts_with_all = ["with_txs", "with_txs_distance", "minimal", "full", "archive"])]
384 with_txs_since: Option<u64>,
385
386 #[arg(long, value_name = "BLOCKS", value_parser = RangedU64ValueParser::<u64>::new().range(1..), conflicts_with_all = ["with_txs", "with_txs_since", "minimal", "full", "archive"])]
388 with_txs_distance: Option<u64>,
389
390 #[arg(long, conflicts_with_all = ["with_receipts_since", "with_receipts_distance", "minimal", "full", "archive"])]
392 with_receipts: bool,
393
394 #[arg(long, value_name = "BLOCK_NUMBER", conflicts_with_all = ["with_receipts", "with_receipts_distance", "minimal", "full", "archive"])]
396 with_receipts_since: Option<u64>,
397
398 #[arg(long, value_name = "BLOCKS", value_parser = RangedU64ValueParser::<u64>::new().range(1..), conflicts_with_all = ["with_receipts", "with_receipts_since", "minimal", "full", "archive"])]
400 with_receipts_distance: Option<u64>,
401
402 #[arg(long, alias = "with-changesets", conflicts_with_all = ["with_state_history_since", "with_state_history_distance", "minimal", "full", "archive"])]
404 with_state_history: bool,
405
406 #[arg(long, value_name = "BLOCK_NUMBER", conflicts_with_all = ["with_state_history", "with_state_history_distance", "minimal", "full", "archive"])]
408 with_state_history_since: Option<u64>,
409
410 #[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"])]
412 with_state_history_distance: Option<u64>,
413
414 #[arg(long, requires = "with_txs", conflicts_with_all = ["minimal", "full", "archive"])]
416 with_senders: bool,
417
418 #[arg(long, conflicts_with_all = ["minimal", "full", "archive", "without_rocksdb"])]
420 with_rocksdb: bool,
421
422 #[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"])]
424 archive: bool,
425
426 #[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"])]
428 minimal: bool,
429
430 #[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"])]
432 full: bool,
433
434 #[arg(long, conflicts_with_all = ["url", "with_rocksdb"])]
439 without_rocksdb: bool,
440
441 #[arg(long, short = 'y')]
444 non_interactive: bool,
445
446 #[arg(long, conflicts_with = "list")]
448 force: bool,
449
450 #[arg(long, conflicts_with_all = ["force", "list", "url"])]
464 prune_unlisted: bool,
465
466 #[arg(long, default_value_t = true, num_args = 0..=1, default_missing_value = "true")]
473 resumable: bool,
474
475 #[arg(long, default_value_t = MAX_CONCURRENT_DOWNLOADS)]
480 download_concurrency: usize,
481
482 #[arg(long, value_name = "DURATION", value_parser = reth_cli_util::parse_duration_from_secs_or_ms)]
488 retry_backoff: Option<Duration>,
489
490 #[arg(long, alias = "list-snapshots", conflicts_with_all = ["url", "manifest_url", "manifest_path"])]
495 list: bool,
496
497 #[arg(long, conflicts_with_all = ["url", "list"])]
499 print_plan_json: bool,
500}
501
502impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> DownloadCommand<C> {
503 pub async fn execute<N>(self) -> Result<Option<PreparedSnapshotDownload>> {
505 let chain = self.env.chain.chain();
506
507 if self.list {
509 let entries = fetch_snapshot_api_entries(chain.id()).await?;
510 print_snapshot_listing(&entries, chain.id());
511 return Ok(None);
512 }
513
514 let data_dir = self.env.datadir.clone().resolve_datadir(chain);
515 let static_files_dir = data_dir.static_files();
516 let static_files_dir = (static_files_dir != data_dir.data_dir().join("static_files"))
517 .then_some(static_files_dir);
518
519 if let Some(ref url) = self.url {
521 let cancel_token = CancellationToken::new();
522 let _cancel_guard = cancel_token.drop_guard();
523 let data_dir = self.env.datadir.clone().resolve_datadir(chain);
524 let target_dir = data_dir.data_dir();
525 if self.force {
526 clear_existing_datadir(target_dir, static_files_dir.as_deref())?;
527 }
528 fs::create_dir_all(target_dir)?;
529
530 let request_limiter = DownloadRequestLimiter::new(self.download_concurrency.max(1));
531 info!(target: "reth::cli",
532 dir = ?data_dir.data_dir(),
533 url = %url,
534 "Starting snapshot download and extraction"
535 );
536
537 stream_and_extract(
538 url,
539 data_dir.data_dir(),
540 static_files_dir.as_deref(),
541 self.resumable,
542 Some(request_limiter),
543 cancel_token.clone(),
544 self.retry_backoff,
545 )
546 .await?;
547 info!(target: "reth::cli", "Snapshot downloaded and extracted successfully");
548
549 return Ok(None);
550 }
551
552 let ResolvedDownload { manifest, selections, preset, planned } =
553 self.resolve_download(chain.id()).await?;
554 let data_dir = self.env.datadir.clone().resolve_datadir(chain).data_dir().to_path_buf();
555 let prepared = PreparedSnapshotDownload { manifest, data_dir };
556 if self.print_plan_json {
557 DownloadPlan::from_planned(&prepared.manifest, &planned)
558 .write_json(std::io::stdout().lock())?;
559 return Ok(Some(prepared))
560 }
561
562 let target_dir = prepared.data_dir.as_path();
563 let cancel_token = CancellationToken::new();
564 let _cancel_guard = cancel_token.drop_guard();
565 if self.force {
566 clear_existing_datadir(target_dir, static_files_dir.as_deref())?;
567 } else if self.prune_unlisted {
568 prune_unlisted_outputs(&planned.archives, target_dir, static_files_dir.as_deref())?;
569 }
570 fs::create_dir_all(target_dir)?;
571 let downloads = {
572 let (target_dir, static_files_dir) =
573 (target_dir.to_path_buf(), static_files_dir.clone());
574 tokio::task::spawn_blocking(move || {
575 planned.partition_reusable(&target_dir, static_files_dir.as_deref())
576 })
577 .await??
578 };
579 info!(target: "reth::cli",
580 reusable = downloads.reused.len(),
581 needs_download = downloads.pending.len(),
582 "Startup integrity summary (plain output files)"
583 );
584
585 info!(target: "reth::cli",
586 archives = downloads.total_archives(),
587 download_total = %DownloadProgress::format_size(downloads.total_download_size),
588 output_total = %DownloadProgress::format_size(downloads.total_output_size),
589 "Downloading all archives"
590 );
591
592 run_modular_downloads(
593 downloads,
594 target_dir,
595 static_files_dir.as_deref(),
596 self.download_concurrency.max(1),
597 cancel_token.clone(),
598 self.retry_backoff,
599 )
600 .await?;
601
602 self.finalize_modular_download(
603 &selections,
604 &prepared.manifest,
605 preset,
606 target_dir,
607 &target_dir.join("db"),
608 )?;
609
610 Ok(Some(prepared))
611 }
612
613 pub async fn plan(&self) -> Result<(DownloadPlan, PreparedSnapshotDownload)> {
616 let chain = self.env.chain.chain();
617 let resolved = self.resolve_download(chain.id()).await?;
618 let plan = DownloadPlan::from_planned(&resolved.manifest, &resolved.planned);
619 let prepared = PreparedSnapshotDownload {
620 manifest: resolved.manifest,
621 data_dir: self.env.datadir.clone().resolve_datadir(chain).data_dir().to_path_buf(),
622 };
623 Ok((plan, prepared))
624 }
625
626 async fn resolve_download(&self, chain_id: u64) -> Result<ResolvedDownload> {
627 let manifest = self.load_manifest(chain_id).await?;
628 let ResolvedComponents { mut selections, preset } = self.resolve_components(&manifest)?;
629
630 match preset {
631 Some(SelectionPreset::Archive) => {
632 inject_archive_only_components(&mut selections, &manifest, !self.without_rocksdb)
633 }
634 Some(SelectionPreset::Full)
635 if DownloadDefaults::get_global().full_preset_includes_rocksdb_indices &&
636 !self.without_rocksdb =>
637 {
638 inject_rocksdb_indices(&mut selections, &manifest)
639 }
640 _ => {}
641 }
642
643 let planned = collect_planned_archives(&manifest, &selections)?;
644 Ok(ResolvedDownload { manifest, selections, preset, planned })
645 }
646
647 async fn load_manifest(&self, chain_id: u64) -> Result<SnapshotManifest> {
649 let manifest_source = self.resolve_manifest_source(chain_id).await?;
650
651 info!(target: "reth::cli", source = %manifest_source, "Fetching snapshot manifest");
652 let mut manifest = fetch_manifest_from_source(&manifest_source).await?;
653 if self.env.datadir.datadir.as_ref().is_none() ||
654 (self.manifest_url.is_none() && self.manifest_path.is_none())
655 {
656 self.validate_manifest_chain(&manifest)?;
657 }
658 manifest.base_url = Some(resolve_manifest_base_url(&manifest, &manifest_source)?);
659
660 info!(target: "reth::cli",
661 block = manifest.block,
662 chain_id = manifest.chain_id,
663 storage_version = %manifest.storage_version,
664 components = manifest.components.len(),
665 "Loaded snapshot manifest"
666 );
667
668 Ok(manifest)
669 }
670
671 fn finalize_modular_download(
673 &self,
674 selections: &BTreeMap<SnapshotComponentType, ComponentSelection>,
675 manifest: &SnapshotManifest,
676 preset: Option<SelectionPreset>,
677 target_dir: &Path,
678 db_path: &Path,
679 ) -> Result<()> {
680 let config =
681 config_for_selections(selections, manifest, preset, Some(self.env.chain.as_ref()));
682 if write_config(&config, target_dir)? {
683 let desc = config_gen::describe_prune_config(&config);
684 info!(target: "reth::cli", "{}", desc.join(", "));
685 }
686
687 let db = init_db(db_path, self.env.db.database_args())?;
688 let should_write_prune = config.prune.segments != Default::default();
689 let should_reset_indices = should_reset_index_stage_checkpoints(selections);
690 if should_write_prune || should_reset_indices {
691 let tx = db.tx_mut()?;
692
693 if should_write_prune {
694 config_gen::write_prune_checkpoints_tx(&tx, &config, selections, manifest.block)?;
695 }
696
697 if should_reset_indices {
698 config_gen::reset_index_stage_checkpoints_tx(&tx)?;
699 }
700
701 tx.commit()?;
702 }
703
704 if self.env.chain.chain().id() == manifest.chain_id {
705 let start_command = startup_node_command::<C>(self.env.chain.as_ref());
706 info!(target: "reth::cli", "Snapshot download complete. Run `{}` to start syncing.", start_command);
707 } else {
708 info!(target: "reth::cli", "Snapshot download complete.");
709 }
710
711 Ok(())
712 }
713
714 fn resolve_components(&self, manifest: &SnapshotManifest) -> Result<ResolvedComponents> {
716 let available = |ty: SnapshotComponentType| manifest.component(ty).is_some();
717
718 if self.archive {
720 return Ok(ResolvedComponents {
721 selections: SnapshotComponentType::ALL
722 .iter()
723 .copied()
724 .filter(|ty| available(*ty))
725 .filter(|ty| {
726 !self.without_rocksdb || *ty != SnapshotComponentType::RocksdbIndices
727 })
728 .map(|ty| (ty, ComponentSelection::All))
729 .collect(),
730 preset: Some(SelectionPreset::Archive),
731 });
732 }
733
734 if self.full {
735 return Ok(ResolvedComponents {
736 selections: self.full_preset_selections(manifest)?,
737 preset: Some(SelectionPreset::Full),
738 });
739 }
740
741 if self.minimal {
742 return Ok(ResolvedComponents {
743 selections: self.minimal_preset_selections(manifest),
744 preset: Some(SelectionPreset::Minimal),
745 });
746 }
747
748 let has_explicit_flags = self.with_txs ||
749 self.with_txs_since.is_some() ||
750 self.with_txs_distance.is_some() ||
751 self.with_receipts ||
752 self.with_receipts_since.is_some() ||
753 self.with_receipts_distance.is_some() ||
754 self.with_state_history ||
755 self.with_state_history_since.is_some() ||
756 self.with_state_history_distance.is_some() ||
757 self.with_senders ||
758 self.with_rocksdb;
759
760 if has_explicit_flags {
761 let mut selections = BTreeMap::new();
762 let tx_selection = explicit_component_selection(
763 self.with_txs,
764 self.with_txs_since,
765 self.with_txs_distance,
766 manifest.block,
767 );
768 let receipt_selection = explicit_component_selection(
769 self.with_receipts,
770 self.with_receipts_since,
771 self.with_receipts_distance,
772 manifest.block,
773 );
774 let state_history_selection = explicit_component_selection(
775 self.with_state_history,
776 self.with_state_history_since,
777 self.with_state_history_distance,
778 manifest.block,
779 );
780
781 if available(SnapshotComponentType::State) {
783 selections.insert(SnapshotComponentType::State, ComponentSelection::All);
784 }
785 if available(SnapshotComponentType::Headers) {
786 selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
787 }
788 if let Some(selection) = tx_selection &&
789 available(SnapshotComponentType::Transactions)
790 {
791 selections.insert(SnapshotComponentType::Transactions, selection);
792 }
793 if let Some(selection) = receipt_selection &&
794 available(SnapshotComponentType::Receipts)
795 {
796 selections.insert(SnapshotComponentType::Receipts, selection);
797 }
798 if let Some(selection) = state_history_selection {
799 if available(SnapshotComponentType::AccountChangesets) {
800 selections.insert(SnapshotComponentType::AccountChangesets, selection);
801 }
802 if available(SnapshotComponentType::StorageChangesets) {
803 selections.insert(SnapshotComponentType::StorageChangesets, selection);
804 }
805 }
806 if self.with_senders && available(SnapshotComponentType::TransactionSenders) {
807 selections
808 .insert(SnapshotComponentType::TransactionSenders, ComponentSelection::All);
809 }
810 if self.with_rocksdb && available(SnapshotComponentType::RocksdbIndices) {
811 selections.insert(SnapshotComponentType::RocksdbIndices, ComponentSelection::All);
812 }
813 return Ok(ResolvedComponents { selections, preset: None });
814 }
815
816 if self.non_interactive {
817 eyre::ensure!(
820 !self.prune_unlisted,
821 "--prune-unlisted with --non-interactive requires an explicit component selection: \
822 --minimal, --full, --archive, or --with-* flags"
823 );
824 return Ok(ResolvedComponents {
825 selections: self.minimal_preset_selections(manifest),
826 preset: Some(SelectionPreset::Minimal),
827 });
828 }
829
830 let minimal_preset = self.minimal_preset_selections(manifest);
832 let full_preset = self.full_preset_selections(manifest)?;
833 let SelectorOutput { selections, preset } =
834 run_selector(manifest.clone(), &minimal_preset, &full_preset)?;
835 let selected =
836 selections.into_iter().filter(|(_, sel)| *sel != ComponentSelection::None).collect();
837
838 Ok(ResolvedComponents { selections: selected, preset })
839 }
840
841 fn minimal_preset_selections(
843 &self,
844 manifest: &SnapshotManifest,
845 ) -> BTreeMap<SnapshotComponentType, ComponentSelection> {
846 self.pruning_preset_selections(manifest, SelectionPreset::Minimal)
847 }
848
849 fn full_preset_selections(
851 &self,
852 manifest: &SnapshotManifest,
853 ) -> Result<BTreeMap<SnapshotComponentType, ComponentSelection>> {
854 if DefaultPruningValues::get_global().full_bodies_history_use_pre_merge {
855 self.validate_manifest_chain(manifest)?;
856 }
857 Ok(self.pruning_preset_selections(manifest, SelectionPreset::Full))
858 }
859
860 fn pruning_preset_selections(
862 &self,
863 manifest: &SnapshotManifest,
864 preset: SelectionPreset,
865 ) -> BTreeMap<SnapshotComponentType, ComponentSelection> {
866 let defaults = DefaultPruningValues::get_global();
867 let (prune_modes, bodies_history_use_pre_merge) = match preset {
868 SelectionPreset::Minimal => (&defaults.minimal_prune_modes, false),
869 SelectionPreset::Full => {
870 (&defaults.full_prune_modes, defaults.full_bodies_history_use_pre_merge)
871 }
872 SelectionPreset::Archive => unreachable!("archive selects every component"),
873 };
874 let mut selections = BTreeMap::new();
875
876 for &ty in &SnapshotComponentType::ALL {
877 if manifest.component(ty).is_none() {
878 continue;
879 }
880
881 let selection = self.pruning_selection_for_component(
882 ty,
883 manifest.block,
884 prune_modes,
885 bodies_history_use_pre_merge,
886 );
887 if selection != ComponentSelection::None {
888 selections.insert(ty, selection);
889 }
890 }
891
892 selections
893 }
894
895 fn pruning_selection_for_component(
897 &self,
898 ty: SnapshotComponentType,
899 snapshot_block: u64,
900 prune_modes: &PruneModes,
901 bodies_history_use_pre_merge: bool,
902 ) -> ComponentSelection {
903 if ty == SnapshotComponentType::Transactions && bodies_history_use_pre_merge {
904 return match self
905 .env
906 .chain
907 .ethereum_fork_activation(EthereumHardfork::Paris)
908 .block_number()
909 {
910 Some(paris) if snapshot_block >= paris => ComponentSelection::Since(paris),
911 Some(_) => ComponentSelection::None,
912 None => ComponentSelection::All,
913 }
914 }
915
916 let mode = match ty {
917 SnapshotComponentType::State | SnapshotComponentType::Headers => {
918 return ComponentSelection::All
919 }
920 SnapshotComponentType::Transactions => prune_modes.bodies_history,
921 SnapshotComponentType::TransactionSenders => prune_modes.sender_recovery,
922 SnapshotComponentType::Receipts => prune_modes.receipts,
923 SnapshotComponentType::AccountChangesets => prune_modes.account_history,
924 SnapshotComponentType::StorageChangesets => prune_modes.storage_history,
925 SnapshotComponentType::RocksdbIndices => return ComponentSelection::None,
926 };
927
928 selection_from_prune_mode(mode, snapshot_block)
929 }
930
931 async fn resolve_manifest_source(&self, chain_id: u64) -> Result<String> {
933 if let Some(path) = &self.manifest_path {
934 return Ok(path.display().to_string());
935 }
936
937 match &self.manifest_url {
938 Some(url) => Ok(url.clone()),
939 None => {
940 let defaults = DownloadDefaults::get_global();
941 if defaults.mainnet_only_discovery() && chain_id != MAINNET.chain.id() {
942 eyre::bail!(
943 "Snapshots are only auto-discovered for Ethereum mainnet.\n\n\
944 Chain {chain_id} requires an explicit source:\n\
945 \t--manifest-url <URL>\n\
946 \t--manifest-path <PATH>\n\
947 \t-u <SNAPSHOT-URL>\n\n\
948 Use --list to inspect snapshots exposed by {}.",
949 defaults.snapshot_source_url(),
950 );
951 }
952
953 discover_manifest_url(chain_id).await
954 }
955 }
956 }
957
958 fn validate_manifest_chain(&self, manifest: &SnapshotManifest) -> Result<()> {
959 let chain_id = self.env.chain.chain().id();
960 eyre::ensure!(
961 manifest.chain_id == chain_id,
962 "Snapshot chain ID {} does not match selected chain ID {chain_id}",
963 manifest.chain_id
964 );
965 Ok(())
966 }
967}
968
969fn explicit_component_selection(
972 all: bool,
973 since: Option<u64>,
974 distance: Option<u64>,
975 snapshot_block: u64,
976) -> Option<ComponentSelection> {
977 if all {
978 Some(ComponentSelection::All)
979 } else if let Some(block) = since {
980 (block <= snapshot_block).then_some(ComponentSelection::Since(block))
981 } else {
982 distance.map(ComponentSelection::Distance)
983 }
984}
985
986fn selection_from_prune_mode(mode: Option<PruneMode>, snapshot_block: u64) -> ComponentSelection {
988 match mode {
989 None => ComponentSelection::All,
990 Some(PruneMode::Full) => ComponentSelection::None,
991 Some(PruneMode::Distance(d)) => ComponentSelection::Distance(d),
992 Some(PruneMode::Before(block)) => {
993 if snapshot_block >= block {
994 ComponentSelection::Since(block)
995 } else {
996 ComponentSelection::None
997 }
998 }
999 }
1000}
1001
1002fn clear_existing_datadir(target_dir: &Path, static_files_dir: Option<&Path>) -> Result<()> {
1004 info!(target: "reth::cli", dir = ?target_dir, "Clearing existing snapshot data");
1005 for entry in MANAGED_DATADIR_PATHS {
1006 let path = managed_datadir_path(entry, target_dir, static_files_dir);
1007 if !path.try_exists()? {
1008 continue;
1009 }
1010
1011 let metadata = fs::metadata(&path)?;
1012 if metadata.is_dir() {
1013 fs::remove_dir_all(&path)?;
1014 } else if metadata.is_file() {
1015 fs::remove_file(&path)?;
1016 }
1017 }
1018
1019 Ok(())
1020}
1021
1022fn managed_datadir_path(
1024 entry: &str,
1025 target_dir: &Path,
1026 static_files_dir: Option<&Path>,
1027) -> PathBuf {
1028 if entry == "static_files" {
1029 static_files_dir.map_or_else(|| target_dir.join(entry), Path::to_path_buf)
1030 } else {
1031 target_dir.join(entry)
1032 }
1033}
1034
1035fn inject_archive_only_components(
1038 selections: &mut BTreeMap<SnapshotComponentType, ComponentSelection>,
1039 manifest: &SnapshotManifest,
1040 include_rocksdb: bool,
1041) {
1042 let is_all =
1043 |ty: SnapshotComponentType| selections.get(&ty).copied() == Some(ComponentSelection::All);
1044
1045 let is_archive = is_all(SnapshotComponentType::Transactions) &&
1046 is_all(SnapshotComponentType::Receipts) &&
1047 is_all(SnapshotComponentType::AccountChangesets) &&
1048 is_all(SnapshotComponentType::StorageChangesets);
1049
1050 if !is_archive {
1051 return;
1052 }
1053
1054 for component in
1055 [SnapshotComponentType::TransactionSenders, SnapshotComponentType::RocksdbIndices]
1056 {
1057 if component == SnapshotComponentType::RocksdbIndices && !include_rocksdb {
1058 continue;
1059 }
1060
1061 if manifest.component(component).is_some() {
1062 selections.insert(component, ComponentSelection::All);
1063 }
1064 }
1065}
1066
1067fn inject_rocksdb_indices(
1069 selections: &mut BTreeMap<SnapshotComponentType, ComponentSelection>,
1070 manifest: &SnapshotManifest,
1071) {
1072 if manifest.component(SnapshotComponentType::RocksdbIndices).is_some() {
1073 selections.insert(SnapshotComponentType::RocksdbIndices, ComponentSelection::All);
1074 }
1075}
1076
1077fn should_reset_index_stage_checkpoints(
1079 selections: &BTreeMap<SnapshotComponentType, ComponentSelection>,
1080) -> bool {
1081 !matches!(selections.get(&SnapshotComponentType::RocksdbIndices), Some(ComponentSelection::All))
1082}
1083
1084fn startup_node_command<C>(chain_spec: &C::ChainSpec) -> String
1085where
1086 C: ChainSpecParser,
1087 C::ChainSpec: EthChainSpec,
1088{
1089 startup_node_command_for_binary::<C>(¤t_binary_name(), chain_spec)
1090}
1091
1092fn startup_node_command_for_binary<C>(binary_name: &str, chain_spec: &C::ChainSpec) -> String
1093where
1094 C: ChainSpecParser,
1095 C::ChainSpec: EthChainSpec,
1096{
1097 let mut command = format!("{binary_name} node");
1098
1099 if let Some(chain_arg) = startup_chain_arg::<C>(chain_spec) {
1100 command.push_str(" --chain ");
1101 command.push_str(&chain_arg);
1102 }
1103
1104 command
1105}
1106
1107fn current_binary_name() -> String {
1108 std::env::args_os()
1109 .next()
1110 .map(PathBuf::from)
1111 .and_then(|path| path.file_stem().map(|name| name.to_owned()))
1112 .and_then(|name| name.into_string().ok())
1113 .filter(|name| !name.is_empty())
1114 .unwrap_or_else(|| "reth".to_string())
1115}
1116
1117fn download_command() -> String {
1118 download_command_for_binary(¤t_binary_name())
1119}
1120
1121fn download_command_for_binary(binary_name: &str) -> String {
1122 format!("{binary_name} download")
1123}
1124
1125fn startup_chain_arg<C>(chain_spec: &C::ChainSpec) -> Option<String>
1126where
1127 C: ChainSpecParser,
1128 C::ChainSpec: EthChainSpec,
1129{
1130 let current_chain = chain_spec.chain();
1131 let current_genesis_hash = chain_spec.genesis_hash();
1132 let default_chain = C::default_value().and_then(|chain_name| C::parse(chain_name).ok());
1133
1134 if default_chain.as_ref().is_some_and(|default_chain| {
1135 default_chain.chain() == current_chain &&
1136 default_chain.genesis_hash() == current_genesis_hash
1137 }) {
1138 return None;
1139 }
1140
1141 C::SUPPORTED_CHAINS
1142 .iter()
1143 .find_map(|chain_name| {
1144 let parsed_chain = C::parse(chain_name).ok()?;
1145 (parsed_chain.chain() == current_chain &&
1146 parsed_chain.genesis_hash() == current_genesis_hash)
1147 .then(|| (*chain_name).to_string())
1148 })
1149 .or_else(|| Some("<chain-or-chainspec>".to_string()))
1150}
1151
1152impl<C: ChainSpecParser> DownloadCommand<C> {
1153 pub const fn env(&self) -> &EnvironmentArgs<C> {
1155 &self.env
1156 }
1157
1158 pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
1160 Some(&self.env.chain)
1161 }
1162
1163 pub const fn prints_plan_json(&self) -> bool {
1165 self.print_plan_json
1166 }
1167}
1168
1169#[derive(Debug)]
1171pub struct PreparedSnapshotDownload {
1172 pub manifest: SnapshotManifest,
1174 pub data_dir: PathBuf,
1176}
1177
1178struct ResolvedDownload {
1179 manifest: SnapshotManifest,
1180 selections: BTreeMap<SnapshotComponentType, ComponentSelection>,
1181 preset: Option<SelectionPreset>,
1182 planned: PlannedDownloads,
1183}
1184
1185const MAX_DOWNLOAD_RETRIES: u32 = 10;
1186const RETRY_BACKOFF_SECS: u64 = 5;
1187
1188#[cfg(test)]
1189mod tests {
1190 use super::*;
1191 use clap::{Args, Parser};
1192 use extract::CompressionFormat;
1193 use manifest::{ComponentManifest, OutputFileChecksum, SingleArchive};
1194 use reth_chainspec::{HOLESKY, MAINNET};
1195 use reth_ethereum_cli::chainspec::EthereumChainSpecParser;
1196
1197 #[derive(Parser)]
1198 struct CommandParser<T: Args> {
1199 #[command(flatten)]
1200 args: T,
1201 }
1202
1203 fn manifest_with_archive_only_components() -> SnapshotManifest {
1204 let mut components = BTreeMap::new();
1205 components.insert(
1206 SnapshotComponentType::TransactionSenders.key().to_string(),
1207 ComponentManifest::Single(SingleArchive {
1208 file: "transaction_senders.tar.zst".to_string(),
1209 size: 1,
1210 decompressed_size: 0,
1211 blake3: None,
1212 output_files: vec![],
1213 }),
1214 );
1215 components.insert(
1216 SnapshotComponentType::RocksdbIndices.key().to_string(),
1217 ComponentManifest::Single(SingleArchive {
1218 file: "rocksdb_indices.tar.zst".to_string(),
1219 size: 1,
1220 decompressed_size: 0,
1221 blake3: None,
1222 output_files: vec![],
1223 }),
1224 );
1225 SnapshotManifest {
1226 block: 0,
1227 chain_id: 1,
1228 storage_version: 2,
1229 timestamp: 0,
1230 base_url: Some("https://example.com".to_string()),
1231 reth_version: None,
1232 components,
1233 extensions: Default::default(),
1234 }
1235 }
1236
1237 #[test]
1238 fn test_download_defaults_builder() {
1239 let defaults = DownloadDefaults::default()
1240 .with_snapshot("https://example.com/snapshots (example)")
1241 .with_base_url("https://example.com");
1242
1243 assert_eq!(defaults.default_base_url, "https://example.com");
1244 assert_eq!(defaults.available_snapshots.len(), 3); }
1246
1247 #[test]
1248 fn test_download_defaults_replace_snapshots() {
1249 let defaults = DownloadDefaults::default().with_snapshots(vec![
1250 Cow::Borrowed("https://custom1.com"),
1251 Cow::Borrowed("https://custom2.com"),
1252 ]);
1253
1254 assert_eq!(defaults.available_snapshots.len(), 2);
1255 assert_eq!(defaults.available_snapshots[0], "https://custom1.com");
1256 }
1257
1258 #[test]
1259 fn test_long_help_generation() {
1260 let defaults = DownloadDefaults::default();
1261 let help = defaults.long_help();
1262
1263 assert!(help.contains("Available snapshot sources:"));
1264 assert!(help.contains("Ethereum mainnet"));
1265 assert!(help.contains("snapshots.reth.rs"));
1266 assert!(help.contains("publicnode.com"));
1267 assert!(help.contains("file://"));
1268 }
1269
1270 #[test]
1271 fn test_custom_snapshot_api_keeps_selected_chain_help() {
1272 let defaults = DownloadDefaults::default()
1273 .with_snapshot_api_url("https://snapshots.tempoxyz.dev/api/snapshots");
1274 let help = defaults.long_help();
1275
1276 assert_eq!(
1277 defaults.default_chain_aware_base_url.as_deref(),
1278 Some("https://snapshots.tempoxyz.dev")
1279 );
1280 assert!(help.contains("Browse available snapshots at https://snapshots.tempoxyz.dev"));
1281 assert!(help.contains("- https://snapshots.tempoxyz.dev (default)"));
1282 assert!(help.contains("selected chain"));
1283 assert!(!help.contains("Ethereum mainnet"));
1284 assert!(!help.contains("snapshots.reth.rs"));
1285 }
1286
1287 #[test]
1288 fn test_snapshot_source_url_sets_generated_references() {
1289 let defaults =
1290 DownloadDefaults::default().with_snapshot_source_url("https://snapshots.tempoxyz.dev/");
1291 let help = defaults.long_help();
1292
1293 assert_eq!(defaults.snapshot_api_url, "https://snapshots.tempoxyz.dev/api/snapshots");
1294 assert_eq!(defaults.default_base_url, "https://snapshots.tempoxyz.dev");
1295 assert_eq!(
1296 defaults.default_chain_aware_base_url.as_deref(),
1297 Some("https://snapshots.tempoxyz.dev")
1298 );
1299 assert_eq!(
1300 defaults.available_snapshots.iter().map(|source| source.as_ref()).collect::<Vec<_>>(),
1301 vec!["https://snapshots.tempoxyz.dev (default)"]
1302 );
1303 assert!(!defaults.mainnet_only_discovery());
1304 assert!(help.contains("Browse available snapshots at https://snapshots.tempoxyz.dev"));
1305 assert!(help.contains("from https://snapshots.tempoxyz.dev."));
1306 }
1307
1308 #[test]
1309 fn test_snapshot_api_url_trailing_slash_sets_source_url() {
1310 let defaults = DownloadDefaults::default()
1311 .with_snapshot_api_url("https://snapshots.tempoxyz.dev/api/snapshots/");
1312 let help = defaults.long_help();
1313
1314 assert_eq!(
1315 defaults.default_chain_aware_base_url.as_deref(),
1316 Some("https://snapshots.tempoxyz.dev")
1317 );
1318 assert!(help.contains("Browse available snapshots at https://snapshots.tempoxyz.dev"));
1319 assert!(help.contains("- https://snapshots.tempoxyz.dev (default)"));
1320 }
1321
1322 #[test]
1323 fn test_long_help_override() {
1324 let custom_help = "This is custom help text for downloading snapshots.";
1325 let defaults = DownloadDefaults::default().with_long_help(custom_help);
1326
1327 let help = defaults.long_help();
1328 assert_eq!(help, custom_help);
1329 assert!(!help.contains("Available snapshot sources:"));
1330 }
1331
1332 #[test]
1333 fn test_builder_chaining() {
1334 let defaults = DownloadDefaults::default()
1335 .with_base_url("https://custom.example.com")
1336 .with_snapshot("https://snapshot1.com")
1337 .with_snapshot("https://snapshot2.com")
1338 .with_long_help("Custom help for snapshots");
1339
1340 assert_eq!(defaults.default_base_url, "https://custom.example.com");
1341 assert_eq!(defaults.available_snapshots.len(), 4); assert_eq!(defaults.long_help, Some("Custom help for snapshots".to_string()));
1343 }
1344
1345 #[test]
1346 fn test_download_prune_unlisted_conflicts_with_force_list_and_url() {
1347 let parse = |args: &[&str]| {
1348 CommandParser::<DownloadCommand<EthereumChainSpecParser>>::try_parse_from(args)
1349 };
1350
1351 assert!(parse(&["reth", "--prune-unlisted"]).unwrap().args.prune_unlisted);
1352 assert!(parse(&["reth", "--prune-unlisted", "--force"]).is_err());
1353 assert!(parse(&["reth", "--prune-unlisted", "--list"]).is_err());
1354 assert!(
1355 parse(&["reth", "--prune-unlisted", "--url", "https://example.com/a.tar.zst"]).is_err()
1356 );
1357 }
1358
1359 #[test]
1360 fn test_download_resumable_defaults_to_true() {
1361 let args =
1362 CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from(["reth"]).args;
1363
1364 assert!(args.resumable);
1365 }
1366
1367 #[test]
1368 fn test_download_resumable_implicit_true() {
1369 let args = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from([
1370 "reth",
1371 "--resumable",
1372 ])
1373 .args;
1374
1375 assert!(args.resumable);
1376 }
1377
1378 #[test]
1379 fn test_download_resumable_explicit_false() {
1380 let args = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from([
1381 "reth",
1382 "--resumable=false",
1383 ])
1384 .args;
1385
1386 assert!(!args.resumable);
1387 }
1388
1389 #[test]
1390 fn test_download_print_plan_json_parses() {
1391 let args = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from([
1392 "reth",
1393 "--manifest-path",
1394 "manifest.json",
1395 "--minimal",
1396 "--print-plan-json",
1397 ])
1398 .args;
1399
1400 assert!(args.prints_plan_json());
1401 }
1402
1403 #[test]
1404 fn test_download_print_plan_json_rejects_single_archive() {
1405 let result = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::try_parse_from([
1406 "reth",
1407 "--url",
1408 "https://example.com/snapshot.tar.zst",
1409 "--print-plan-json",
1410 ]);
1411
1412 assert!(result.is_err());
1413 }
1414
1415 #[test]
1416 fn minimal_component_selection_uses_configured_prune_modes() {
1417 let args =
1418 CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from(["reth"]).args;
1419 let history_distance = 64_864;
1420 let modes = PruneModes {
1421 sender_recovery: Some(PruneMode::Full),
1422 receipts: Some(PruneMode::Distance(128)),
1423 account_history: Some(PruneMode::Distance(history_distance)),
1424 storage_history: Some(PruneMode::Distance(history_distance)),
1425 bodies_history: Some(PruneMode::Distance(history_distance)),
1426 ..Default::default()
1427 };
1428
1429 assert_eq!(
1430 args.pruning_selection_for_component(
1431 SnapshotComponentType::Transactions,
1432 1_000_000,
1433 &modes,
1434 false,
1435 ),
1436 ComponentSelection::Distance(history_distance)
1437 );
1438 assert_eq!(
1439 args.pruning_selection_for_component(
1440 SnapshotComponentType::Receipts,
1441 1_000_000,
1442 &modes,
1443 false,
1444 ),
1445 ComponentSelection::Distance(128)
1446 );
1447 assert_eq!(
1448 args.pruning_selection_for_component(
1449 SnapshotComponentType::AccountChangesets,
1450 1_000_000,
1451 &modes,
1452 false,
1453 ),
1454 ComponentSelection::Distance(history_distance)
1455 );
1456 assert_eq!(
1457 args.pruning_selection_for_component(
1458 SnapshotComponentType::StorageChangesets,
1459 1_000_000,
1460 &modes,
1461 false,
1462 ),
1463 ComponentSelection::Distance(history_distance)
1464 );
1465 assert_eq!(
1466 args.pruning_selection_for_component(
1467 SnapshotComponentType::TransactionSenders,
1468 1_000_000,
1469 &modes,
1470 false,
1471 ),
1472 ComponentSelection::None
1473 );
1474 }
1475
1476 #[test]
1477 fn resolve_manifest_source_requires_explicit_source_for_non_mainnet_defaults() {
1478 let args = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from([
1479 "reth", "--chain", "holesky",
1480 ])
1481 .args;
1482
1483 let err = tokio::runtime::Runtime::new()
1484 .unwrap()
1485 .block_on(args.resolve_manifest_source(HOLESKY.chain.id()))
1486 .unwrap_err();
1487
1488 let message = err.to_string();
1489 assert!(message.contains("only auto-discovered for Ethereum mainnet"));
1490 assert!(message.contains("--manifest-url <URL>"));
1491 assert!(message.contains("-u <SNAPSHOT-URL>"));
1492 }
1493
1494 #[test]
1495 fn resolve_manifest_source_allows_manifest_path_for_non_mainnet_defaults() {
1496 let args = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from([
1497 "reth",
1498 "--chain",
1499 "holesky",
1500 "--manifest-path",
1501 "./manifest.json",
1502 ])
1503 .args;
1504
1505 let source = tokio::runtime::Runtime::new()
1506 .unwrap()
1507 .block_on(args.resolve_manifest_source(HOLESKY.chain.id()))
1508 .unwrap();
1509
1510 assert_eq!(source, "./manifest.json");
1511 }
1512
1513 #[test]
1514 fn test_compression_format_detection() {
1515 assert!(matches!(
1516 CompressionFormat::from_url("https://example.com/snapshot.tar.lz4"),
1517 Ok(CompressionFormat::Lz4)
1518 ));
1519 assert!(matches!(
1520 CompressionFormat::from_url("https://example.com/snapshot.tar.zst"),
1521 Ok(CompressionFormat::Zstd)
1522 ));
1523 assert!(matches!(
1524 CompressionFormat::from_url("file:///path/to/snapshot.tar.lz4"),
1525 Ok(CompressionFormat::Lz4)
1526 ));
1527 assert!(matches!(
1528 CompressionFormat::from_url("file:///path/to/snapshot.tar.zst"),
1529 Ok(CompressionFormat::Zstd)
1530 ));
1531 assert!(CompressionFormat::from_url("https://example.com/snapshot.tar.gz").is_err());
1532 }
1533
1534 #[test]
1535 fn inject_archive_only_components_for_archive_selection() {
1536 let manifest = manifest_with_archive_only_components();
1537 let mut selections = BTreeMap::new();
1538 selections.insert(SnapshotComponentType::Transactions, ComponentSelection::All);
1539 selections.insert(SnapshotComponentType::Receipts, ComponentSelection::All);
1540 selections.insert(SnapshotComponentType::AccountChangesets, ComponentSelection::All);
1541 selections.insert(SnapshotComponentType::StorageChangesets, ComponentSelection::All);
1542
1543 inject_archive_only_components(&mut selections, &manifest, true);
1544
1545 assert_eq!(
1546 selections.get(&SnapshotComponentType::TransactionSenders),
1547 Some(&ComponentSelection::All)
1548 );
1549 assert_eq!(
1550 selections.get(&SnapshotComponentType::RocksdbIndices),
1551 Some(&ComponentSelection::All)
1552 );
1553 }
1554
1555 #[test]
1556 fn inject_archive_only_components_without_rocksdb() {
1557 let manifest = manifest_with_archive_only_components();
1558 let mut selections = BTreeMap::new();
1559 selections.insert(SnapshotComponentType::Transactions, ComponentSelection::All);
1560 selections.insert(SnapshotComponentType::Receipts, ComponentSelection::All);
1561 selections.insert(SnapshotComponentType::AccountChangesets, ComponentSelection::All);
1562 selections.insert(SnapshotComponentType::StorageChangesets, ComponentSelection::All);
1563
1564 inject_archive_only_components(&mut selections, &manifest, false);
1565
1566 assert_eq!(
1567 selections.get(&SnapshotComponentType::TransactionSenders),
1568 Some(&ComponentSelection::All)
1569 );
1570 assert_eq!(selections.get(&SnapshotComponentType::RocksdbIndices), None);
1571 }
1572
1573 #[test]
1574 fn inject_rocksdb_indices_keeps_index_stages() {
1575 let manifest = manifest_with_archive_only_components();
1576 let mut selections = BTreeMap::new();
1577 selections
1578 .insert(SnapshotComponentType::AccountChangesets, ComponentSelection::Distance(10));
1579
1580 inject_rocksdb_indices(&mut selections, &manifest);
1581
1582 assert_eq!(
1583 selections.get(&SnapshotComponentType::RocksdbIndices),
1584 Some(&ComponentSelection::All)
1585 );
1586 assert_eq!(selections.get(&SnapshotComponentType::TransactionSenders), None);
1587 assert!(!should_reset_index_stage_checkpoints(&selections));
1588 }
1589
1590 #[test]
1591 fn inject_rocksdb_indices_skips_manifest_without_indices() {
1592 let mut manifest = manifest_with_archive_only_components();
1593 manifest.components.clear();
1594 let mut selections = BTreeMap::new();
1595
1596 inject_rocksdb_indices(&mut selections, &manifest);
1597
1598 assert!(selections.is_empty());
1599 assert!(should_reset_index_stage_checkpoints(&selections));
1600 }
1601
1602 #[test]
1603 fn should_reset_index_stage_checkpoints_without_rocksdb_indices() {
1604 let mut selections = BTreeMap::new();
1605 selections.insert(SnapshotComponentType::Transactions, ComponentSelection::All);
1606 assert!(should_reset_index_stage_checkpoints(&selections));
1607
1608 selections.insert(SnapshotComponentType::RocksdbIndices, ComponentSelection::All);
1609 assert!(!should_reset_index_stage_checkpoints(&selections));
1610 }
1611
1612 #[test]
1613 fn startup_node_command_omits_default_chain_arg() {
1614 let command =
1615 startup_node_command_for_binary::<EthereumChainSpecParser>("reth", MAINNET.as_ref());
1616
1617 assert_eq!(command, "reth node");
1618 }
1619
1620 #[test]
1621 fn startup_node_command_includes_non_default_chain_arg() {
1622 let command =
1623 startup_node_command_for_binary::<EthereumChainSpecParser>("reth", HOLESKY.as_ref());
1624
1625 assert_eq!(command, "reth node --chain holesky");
1626 }
1627
1628 #[test]
1629 fn startup_node_command_uses_running_binary_name() {
1630 let command =
1631 startup_node_command_for_binary::<EthereumChainSpecParser>("tempo", HOLESKY.as_ref());
1632
1633 assert_eq!(command, "tempo node --chain holesky");
1634 }
1635
1636 #[test]
1637 fn download_command_uses_binary_name() {
1638 assert_eq!(download_command_for_binary("tempo"), "tempo download");
1639 }
1640
1641 #[tokio::test]
1642 async fn test_download_explicit_source_and_datadir_allow_other_chains() {
1643 let dir = tempfile::tempdir().unwrap();
1644 let manifest_path = dir.path().join("manifest.json");
1645 let mut manifest = manifest_with_archive_only_components();
1646 manifest.chain_id = 42431;
1647 for component in manifest.components.values_mut() {
1648 if let ComponentManifest::Single(archive) = component {
1649 archive.output_files.push(OutputFileChecksum {
1650 path: archive.file.clone(),
1651 size: 1,
1652 blake3: blake3::hash(b"x").to_hex().to_string(),
1653 });
1654 }
1655 }
1656 fs::write(&manifest_path, serde_json::to_vec(&manifest).unwrap()).unwrap();
1657 let manifest_url = format!("file://{}", manifest_path.display());
1658 let target = dir.path().join("download");
1659
1660 for source in ["--manifest-url", "--manifest-path"] {
1661 let location = if source == "--manifest-url" {
1662 manifest_url.as_str()
1663 } else {
1664 manifest_path.to_str().unwrap()
1665 };
1666 for selection in ["--archive", "--minimal", "--non-interactive", "--with-txs"] {
1667 let args = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from([
1668 "reth",
1669 source,
1670 location,
1671 "--datadir",
1672 target.to_str().unwrap(),
1673 selection,
1674 ])
1675 .args;
1676 let (plan, prepared) = args.plan().await.unwrap();
1677 assert_eq!(plan.chain_id, 42431);
1678 assert_eq!(prepared.manifest.chain_id, 42431);
1679 assert_eq!(prepared.data_dir, target);
1680 assert!(!target.exists());
1681 }
1682 }
1683 }
1684
1685 #[tokio::test]
1686 async fn test_download_chain_derived_datadir_rejects_other_chains() {
1687 let dir = tempfile::tempdir().unwrap();
1688 let manifest_path = dir.path().join("manifest.json");
1689 let mut manifest = manifest_with_archive_only_components();
1690 manifest.chain_id = 42431;
1691 fs::write(&manifest_path, serde_json::to_vec(&manifest).unwrap()).unwrap();
1692
1693 for datadir in [vec![], vec!["--datadir", "default"]] {
1694 let mut cli_args =
1695 vec!["reth", "--manifest-path", manifest_path.to_str().unwrap(), "--archive"];
1696 cli_args.extend(datadir);
1697 let args =
1698 CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from(cli_args)
1699 .args;
1700 assert_eq!(
1701 args.plan().await.unwrap_err().to_string(),
1702 "Snapshot chain ID 42431 does not match selected chain ID 1"
1703 );
1704 }
1705 }
1706
1707 #[test]
1708 fn test_download_full_chain_derived_pruning_rejects_other_chains() {
1709 let args = CommandParser::<DownloadCommand<EthereumChainSpecParser>>::parse_from([
1710 "reth",
1711 "--datadir",
1712 "download",
1713 "--manifest-url",
1714 "https://example.com/manifest.json",
1715 "--full",
1716 ])
1717 .args;
1718 let mut manifest = manifest_with_archive_only_components();
1719 manifest.chain_id = 42431;
1720 assert_eq!(
1721 args.full_preset_selections(&manifest).unwrap_err().to_string(),
1722 "Snapshot chain ID 42431 does not match selected chain ID 1"
1723 );
1724 manifest.chain_id = 1;
1725 assert!(args.full_preset_selections(&manifest).is_ok());
1726 }
1727}