@@ -43,7 +43,8 @@ use url::Url;
4343use zstd:: stream:: read:: Decoder as ZstdDecoder ;
4444
4545const BYTE_UNITS : [ & str ; 4 ] = [ "B" , "KB" , "MB" , "GB" ] ;
46- const MERKLE_BASE_URL : & str = "https://downloads.merkle.io" ;
46+ const RETH_SNAPSHOTS_BASE_URL : & str = "https://snapshots-r2.reth.rs" ;
47+ const RETH_SNAPSHOTS_API_URL : & str = "https://snapshots.reth.rs/api/snapshots" ;
4748const EXTENSION_TAR_LZ4 : & str = ".tar.lz4" ;
4849const EXTENSION_TAR_ZSTD : & str = ".tar.zst" ;
4950const DOWNLOAD_CACHE_DIR : & str = ".download-cache" ;
@@ -98,14 +99,14 @@ impl DownloadDefaults {
9899 DOWNLOAD_DEFAULTS . get_or_init ( DownloadDefaults :: default_download_defaults)
99100 }
100101
101- /// Default download configuration with defaults from merkle.io and publicnode
102+ /// Default download configuration with defaults from snapshots.reth.rs and publicnode
102103 pub fn default_download_defaults ( ) -> Self {
103104 Self {
104105 available_snapshots : vec ! [
105- Cow :: Borrowed ( "https://www.merkle.io/snapshots (default, mainnet archive )" ) ,
106+ Cow :: Borrowed ( "https://snapshots.reth.rs (default)" ) ,
106107 Cow :: Borrowed ( "https://publicnode.com/snapshots (full nodes & testnets)" ) ,
107108 ] ,
108- default_base_url : Cow :: Borrowed ( MERKLE_BASE_URL ) ,
109+ default_base_url : Cow :: Borrowed ( RETH_SNAPSHOTS_BASE_URL ) ,
109110 default_chain_aware_base_url : None ,
110111 long_help : None ,
111112 }
@@ -121,7 +122,9 @@ impl DownloadDefaults {
121122 }
122123
123124 let mut help = String :: from (
124- "Specify a snapshot URL or let the command propose a default one.\n \n Available snapshot sources:\n " ,
125+ "Specify a snapshot URL or let the command propose a default one.\n \n \
126+ Browse available snapshots at https://snapshots.reth.rs\n \
127+ or use --list-snapshots to see them from the CLI.\n \n Available snapshot sources:\n ",
125128 ) ;
126129
127130 for source in & self . available_snapshots {
@@ -188,6 +191,7 @@ pub struct DownloadCommand<C: ChainSpecParser> {
188191 /// Custom URL to download a single snapshot archive (legacy mode).
189192 ///
190193 /// When provided, downloads and extracts a single archive without component selection.
194+ /// Browse available snapshots at <https://snapshots.reth.rs> or use --list-snapshots.
191195 #[ arg( long, short, long_help = DownloadDefaults :: get_global( ) . long_help( ) ) ]
192196 url : Option < String > ,
193197
@@ -248,6 +252,13 @@ pub struct DownloadCommand<C: ChainSpecParser> {
248252 /// Maximum number of concurrent modular archive workers.
249253 #[ arg( long, default_value_t = MAX_CONCURRENT_DOWNLOADS ) ]
250254 download_concurrency : usize ,
255+
256+ /// List available snapshots from snapshots.reth.rs and exit.
257+ ///
258+ /// Queries the snapshots API and prints all available snapshots for the selected chain,
259+ /// including block number, size, and manifest URL.
260+ #[ arg( long, alias = "list-snapshots" , conflicts_with_all = [ "url" , "manifest_url" , "manifest_path" ] ) ]
261+ list : bool ,
251262}
252263
253264impl < C : ChainSpecParser < ChainSpec : EthChainSpec + EthereumHardforks > > DownloadCommand < C > {
@@ -260,16 +271,23 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> DownloadCo
260271 let cancel_token = CancellationToken :: new ( ) ;
261272 let _cancel_guard = cancel_token. drop_guard ( ) ;
262273
274+ // --list: print available snapshots and exit
275+ if self . list {
276+ let entries = fetch_snapshot_api_entries ( chain_id) . await ?;
277+ print_snapshot_listing ( & entries, chain_id) ;
278+ return Ok ( ( ) ) ;
279+ }
280+
263281 // Legacy single-URL mode: download one archive and extract it
264- if let Some ( url) = self . url {
282+ if let Some ( ref url) = self . url {
265283 info ! ( target: "reth::cli" ,
266284 dir = ?data_dir. data_dir( ) ,
267285 url = %url,
268286 "Starting snapshot download and extraction"
269287 ) ;
270288
271289 stream_and_extract (
272- & url,
290+ url,
273291 data_dir. data_dir ( ) ,
274292 None ,
275293 self . resumable ,
@@ -282,7 +300,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> DownloadCo
282300 }
283301
284302 // Modular download: fetch manifest and select components
285- let manifest_source = self . resolve_manifest_source ( chain_id) ;
303+ let manifest_source = self . resolve_manifest_source ( chain_id) . await ? ;
286304
287305 info ! ( target: "reth::cli" , source = %manifest_source, "Fetching snapshot manifest" ) ;
288306 let mut manifest = fetch_manifest_from_source ( & manifest_source) . await ?;
@@ -621,17 +639,14 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> DownloadCo
621639 }
622640 }
623641
624- fn resolve_manifest_source ( & self , chain_id : u64 ) -> String {
642+ async fn resolve_manifest_source ( & self , chain_id : u64 ) -> Result < String > {
625643 if let Some ( path) = & self . manifest_path {
626- return path. display ( ) . to_string ( ) ;
644+ return Ok ( path. display ( ) . to_string ( ) ) ;
627645 }
628646
629647 match & self . manifest_url {
630- Some ( url) => url. clone ( ) ,
631- None => {
632- let base_url = get_base_url ( chain_id) ;
633- format ! ( "{base_url}/manifest.json" )
634- }
648+ Some ( url) => Ok ( url. clone ( ) ) ,
649+ None => discover_manifest_url ( chain_id) . await ,
635650 }
636651 }
637652}
@@ -1572,13 +1587,126 @@ fn file_blake3_hex(path: &Path) -> Result<String> {
15721587 Ok ( hasher. finalize ( ) . to_hex ( ) . to_string ( ) )
15731588}
15741589
1575- /// Builds the base URL for the given chain ID using configured defaults.
1576- fn get_base_url ( chain_id : u64 ) -> String {
1577- let defaults = DownloadDefaults :: get_global ( ) ;
1578- match & defaults. default_chain_aware_base_url {
1579- Some ( url) => format ! ( "{url}/{chain_id}" ) ,
1580- None => defaults. default_base_url . to_string ( ) ,
1590+ /// Discovers the latest snapshot manifest URL for the given chain from the snapshots API.
1591+ ///
1592+ /// Queries `snapshots.reth.rs/api/snapshots` and returns the manifest URL for the most
1593+ /// recent modular snapshot matching the requested chain.
1594+ async fn discover_manifest_url ( chain_id : u64 ) -> Result < String > {
1595+ let api_url = RETH_SNAPSHOTS_API_URL ;
1596+
1597+ info ! ( target: "reth::cli" , %api_url, %chain_id, "Discovering latest snapshot manifest" ) ;
1598+
1599+ let entries = fetch_snapshot_api_entries ( chain_id) . await ?;
1600+
1601+ let entry =
1602+ entries. iter ( ) . filter ( |s| s. is_modular ( ) ) . max_by_key ( |s| s. block ) . ok_or_else ( || {
1603+ eyre:: eyre!(
1604+ "No modular snapshot manifest found for chain \
1605+ {chain_id} at {api_url}\n \n \
1606+ You can provide a manifest URL directly with --manifest-url, or\n \
1607+ use a direct snapshot URL with -u from:\n \
1608+ \t - https://snapshots.reth.rs\n \n \
1609+ Use --list to see all available snapshots."
1610+ )
1611+ } ) ?;
1612+
1613+ info ! ( target: "reth::cli" ,
1614+ block = entry. block,
1615+ url = %entry. metadata_url,
1616+ "Found latest snapshot manifest"
1617+ ) ;
1618+
1619+ Ok ( entry. metadata_url . clone ( ) )
1620+ }
1621+
1622+ /// Deserializes a JSON value that may be either a number or a string-encoded number.
1623+ fn deserialize_string_or_u64 < ' de , D > ( deserializer : D ) -> std:: result:: Result < u64 , D :: Error >
1624+ where
1625+ D : serde:: Deserializer < ' de > ,
1626+ {
1627+ use serde:: Deserialize ;
1628+ let value = serde_json:: Value :: deserialize ( deserializer) ?;
1629+ match & value {
1630+ serde_json:: Value :: Number ( n) => {
1631+ n. as_u64 ( ) . ok_or_else ( || serde:: de:: Error :: custom ( "expected u64" ) )
1632+ }
1633+ serde_json:: Value :: String ( s) => {
1634+ s. parse :: < u64 > ( ) . map_err ( |_| serde:: de:: Error :: custom ( "expected numeric string" ) )
1635+ }
1636+ _ => Err ( serde:: de:: Error :: custom ( "expected number or string" ) ) ,
1637+ }
1638+ }
1639+
1640+ /// An entry from the `snapshots.reth.rs/api/snapshots` listing.
1641+ #[ derive( serde:: Deserialize ) ]
1642+ #[ serde( rename_all = "camelCase" ) ]
1643+ struct SnapshotApiEntry {
1644+ #[ serde( deserialize_with = "deserialize_string_or_u64" ) ]
1645+ chain_id : u64 ,
1646+ #[ serde( deserialize_with = "deserialize_string_or_u64" ) ]
1647+ block : u64 ,
1648+ #[ serde( default ) ]
1649+ date : Option < String > ,
1650+ #[ serde( default ) ]
1651+ profile : Option < String > ,
1652+ metadata_url : String ,
1653+ #[ serde( default ) ]
1654+ size : u64 ,
1655+ }
1656+
1657+ impl SnapshotApiEntry {
1658+ fn is_modular ( & self ) -> bool {
1659+ self . metadata_url . ends_with ( "manifest.json" )
1660+ }
1661+ }
1662+
1663+ /// Fetches the full snapshot listing from the snapshots API, filtered by chain ID.
1664+ async fn fetch_snapshot_api_entries ( chain_id : u64 ) -> Result < Vec < SnapshotApiEntry > > {
1665+ let api_url = RETH_SNAPSHOTS_API_URL ;
1666+
1667+ let entries: Vec < SnapshotApiEntry > = Client :: new ( )
1668+ . get ( api_url)
1669+ . send ( )
1670+ . await
1671+ . and_then ( |r| r. error_for_status ( ) )
1672+ . wrap_err_with ( || format ! ( "Failed to fetch snapshot listing from {api_url}" ) ) ?
1673+ . json ( )
1674+ . await ?;
1675+
1676+ Ok ( entries. into_iter ( ) . filter ( |e| e. chain_id == chain_id) . collect ( ) )
1677+ }
1678+
1679+ /// Prints a formatted table of available modular snapshots.
1680+ fn print_snapshot_listing ( entries : & [ SnapshotApiEntry ] , chain_id : u64 ) {
1681+ let modular: Vec < _ > = entries. iter ( ) . filter ( |e| e. is_modular ( ) ) . collect ( ) ;
1682+
1683+ println ! ( "Available snapshots for chain {chain_id} (https://snapshots.reth.rs):\n " ) ;
1684+ println ! ( "{:<12} {:>10} {:<10} {:>10} MANIFEST URL" , "DATE" , "BLOCK" , "PROFILE" , "SIZE" ) ;
1685+ println ! ( "{}" , "-" . repeat( 100 ) ) ;
1686+
1687+ for entry in & modular {
1688+ let date = entry. date . as_deref ( ) . unwrap_or ( "-" ) ;
1689+ let profile = entry. profile . as_deref ( ) . unwrap_or ( "-" ) ;
1690+ let size = if entry. size > 0 {
1691+ DownloadProgress :: format_size ( entry. size )
1692+ } else {
1693+ "-" . to_string ( )
1694+ } ;
1695+
1696+ println ! (
1697+ "{date:<12} {:>10} {profile:<10} {size:>10} {}" ,
1698+ entry. block, entry. metadata_url
1699+ ) ;
1700+ }
1701+
1702+ if modular. is_empty ( ) {
1703+ println ! ( " (no modular snapshots found)" ) ;
15811704 }
1705+
1706+ println ! (
1707+ "\n To download a specific snapshot, copy its manifest URL and run:\n \
1708+ reth download --manifest-url <URL>"
1709+ ) ;
15821710}
15831711
15841712async fn fetch_manifest_from_source ( source : & str ) -> Result < SnapshotManifest > {
@@ -1597,7 +1725,7 @@ async fn fetch_manifest_from_source(source: &str) -> Result<SnapshotManifest> {
15971725 You can use a direct snapshot URL instead:\n \n \
15981726 \t reth download -u <snapshot-url>\n \n \
15991727 Available snapshot sources:\n \
1600- \t - https://www.merkle.io/snapshots \n \
1728+ \t - https://snapshots.reth.rs \n \
16011729 \t - https://publicnode.com/snapshots"
16021730 )
16031731 } ) ?;
@@ -1666,26 +1794,6 @@ fn resolve_manifest_base_url(manifest: &SnapshotManifest, source: &str) -> Resul
16661794 Ok ( base)
16671795}
16681796
1669- /// Builds default URL for latest mainnet archive snapshot using configured defaults.
1670- ///
1671- /// Used by the legacy single-archive download flow when no manifest is available.
1672- #[ allow( dead_code) ]
1673- async fn get_latest_snapshot_url ( chain_id : u64 ) -> Result < String > {
1674- let base_url = get_base_url ( chain_id) ;
1675- let latest_url = format ! ( "{base_url}/latest.txt" ) ;
1676- let filename = Client :: new ( )
1677- . get ( latest_url)
1678- . send ( )
1679- . await ?
1680- . error_for_status ( ) ?
1681- . text ( )
1682- . await ?
1683- . trim ( )
1684- . to_string ( ) ;
1685-
1686- Ok ( format ! ( "{base_url}/{filename}" ) )
1687- }
1688-
16891797#[ cfg( test) ]
16901798mod tests {
16911799 use super :: * ;
@@ -1750,7 +1858,7 @@ mod tests {
17501858 let help = defaults. long_help ( ) ;
17511859
17521860 assert ! ( help. contains( "Available snapshot sources:" ) ) ;
1753- assert ! ( help. contains( "merkle.io " ) ) ;
1861+ assert ! ( help. contains( "snapshots.reth.rs " ) ) ;
17541862 assert ! ( help. contains( "publicnode.com" ) ) ;
17551863 assert ! ( help. contains( "file://" ) ) ;
17561864 }
0 commit comments