reth_cli_commands/download/
planning.rs1use super::{manifest::*, verify::OutputVerifier};
2use eyre::Result;
3use std::{collections::BTreeMap, path::Path};
4use tracing::info;
5
6#[derive(Debug, Clone)]
8pub(crate) struct PlannedArchive {
9 pub(crate) ty: SnapshotComponentType,
11 pub(crate) component: String,
13 pub(crate) archive: SnapshotArchive,
15}
16
17#[derive(Debug)]
19pub(crate) struct PlannedDownloads {
20 pub(crate) archives: Vec<PlannedArchive>,
22 pub(crate) total_download_size: u64,
24 pub(crate) total_output_size: u64,
26}
27
28impl PlannedDownloads {
29 pub(crate) const fn total_archives(&self) -> usize {
31 self.archives.len()
32 }
33}
34
35pub(crate) const fn archive_priority_rank(ty: SnapshotComponentType) -> u8 {
37 match ty {
38 SnapshotComponentType::State => 0,
39 SnapshotComponentType::RocksdbIndices => 1,
40 _ => 2,
41 }
42}
43
44#[derive(Debug, Default, Clone, Copy)]
46pub(crate) struct DownloadStartupSummary {
47 pub(crate) reusable: usize,
49 pub(crate) needs_download: usize,
51}
52
53pub(crate) fn summarize_download_startup(
55 all_downloads: &[PlannedArchive],
56 target_dir: &Path,
57) -> Result<DownloadStartupSummary> {
58 let mut summary = DownloadStartupSummary::default();
59 let verifier = OutputVerifier::new(target_dir);
60
61 for planned in all_downloads {
62 if verifier.verify(&planned.archive.output_files)? {
63 summary.reusable += 1;
64 } else {
65 summary.needs_download += 1;
66 }
67 }
68
69 Ok(summary)
70}
71
72fn selection_archive_distance(
74 selection: &ComponentSelection,
75 snapshot_block: u64,
76) -> Option<Option<u64>> {
77 match selection {
78 ComponentSelection::All => Some(None),
79 ComponentSelection::Distance(distance) => Some(Some(*distance)),
80 ComponentSelection::Since(block) => Some(Some(snapshot_block.saturating_sub(*block) + 1)),
81 ComponentSelection::None => None,
82 }
83}
84
85fn sort_planned_archives(all_downloads: &mut [PlannedArchive]) {
87 all_downloads.sort_by(|a, b| {
88 archive_priority_rank(a.ty)
89 .cmp(&archive_priority_rank(b.ty))
90 .then_with(|| a.component.cmp(&b.component))
91 .then_with(|| a.archive.file_name.cmp(&b.archive.file_name))
92 });
93}
94
95pub(crate) fn collect_planned_archives(
97 manifest: &SnapshotManifest,
98 selections: &BTreeMap<SnapshotComponentType, ComponentSelection>,
99) -> Result<PlannedDownloads> {
100 let mut archives = Vec::new();
101 let mut total_download_size = 0;
102 let mut total_output_size = 0;
103
104 for (ty, selection) in selections {
105 let Some(distance) = selection_archive_distance(selection, manifest.block) else {
106 continue;
107 };
108 total_download_size += manifest.size_for_distance(*ty, distance);
109 total_output_size += manifest.output_size_for_distance(*ty, distance);
110
111 let snapshot_archives = manifest.snapshot_archives_for_distance(*ty, distance);
112 let component = ty.display_name().to_string();
113 if !snapshot_archives.is_empty() {
114 info!(target: "reth::cli",
115 component = %component,
116 archives = snapshot_archives.len(),
117 selection = %selection,
118 "Queued component for download"
119 );
120 }
121
122 for archive in snapshot_archives {
123 if archive.output_files.is_empty() {
124 eyre::bail!(
125 "Invalid modular manifest: {} is missing plain output checksum metadata",
126 archive.file_name
127 );
128 }
129
130 archives.push(PlannedArchive { ty: *ty, component: component.clone(), archive });
131 }
132 }
133
134 sort_planned_archives(&mut archives);
135 Ok(PlannedDownloads { archives, total_download_size, total_output_size })
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use tempfile::tempdir;
142
143 #[test]
144 fn summarize_download_startup_counts_reusable_and_needs_download() {
145 let dir = tempdir().unwrap();
146 let target_dir = dir.path();
147 let ok_file = target_dir.join("ok.bin");
148 std::fs::write(&ok_file, vec![1_u8; 4]).unwrap();
149 let ok_hash = blake3::hash(&[1_u8; 4]).to_hex().to_string();
150
151 let planned = vec![
152 PlannedArchive {
153 ty: SnapshotComponentType::State,
154 component: "State".to_string(),
155 archive: SnapshotArchive {
156 url: "https://example.com/ok.tar.zst".to_string(),
157 file_name: "ok.tar.zst".to_string(),
158 size: 10,
159 blake3: None,
160 output_files: vec![OutputFileChecksum {
161 path: "ok.bin".to_string(),
162 size: 4,
163 blake3: ok_hash,
164 }],
165 },
166 },
167 PlannedArchive {
168 ty: SnapshotComponentType::Headers,
169 component: "Headers".to_string(),
170 archive: SnapshotArchive {
171 url: "https://example.com/missing.tar.zst".to_string(),
172 file_name: "missing.tar.zst".to_string(),
173 size: 10,
174 blake3: None,
175 output_files: vec![OutputFileChecksum {
176 path: "missing.bin".to_string(),
177 size: 1,
178 blake3: "deadbeef".to_string(),
179 }],
180 },
181 },
182 PlannedArchive {
183 ty: SnapshotComponentType::Transactions,
184 component: "Transactions".to_string(),
185 archive: SnapshotArchive {
186 url: "https://example.com/bad-size.tar.zst".to_string(),
187 file_name: "bad-size.tar.zst".to_string(),
188 size: 10,
189 blake3: None,
190 output_files: vec![],
191 },
192 },
193 ];
194
195 let summary = summarize_download_startup(&planned, target_dir).unwrap();
196 assert_eq!(summary.reusable, 1);
197 assert_eq!(summary.needs_download, 2);
198 }
199
200 #[test]
201 fn archive_priority_prefers_state_then_rocksdb() {
202 let mut planned = [
203 PlannedArchive {
204 ty: SnapshotComponentType::Transactions,
205 component: "Transactions".to_string(),
206 archive: SnapshotArchive {
207 url: "u3".to_string(),
208 file_name: "t.tar.zst".to_string(),
209 size: 1,
210 blake3: None,
211 output_files: vec![OutputFileChecksum {
212 path: "a".to_string(),
213 size: 1,
214 blake3: "x".to_string(),
215 }],
216 },
217 },
218 PlannedArchive {
219 ty: SnapshotComponentType::RocksdbIndices,
220 component: "RocksDB Indices".to_string(),
221 archive: SnapshotArchive {
222 url: "u2".to_string(),
223 file_name: "rocksdb_indices.tar.zst".to_string(),
224 size: 1,
225 blake3: None,
226 output_files: vec![OutputFileChecksum {
227 path: "b".to_string(),
228 size: 1,
229 blake3: "y".to_string(),
230 }],
231 },
232 },
233 PlannedArchive {
234 ty: SnapshotComponentType::State,
235 component: "State (mdbx)".to_string(),
236 archive: SnapshotArchive {
237 url: "u1".to_string(),
238 file_name: "state.tar.zst".to_string(),
239 size: 1,
240 blake3: None,
241 output_files: vec![OutputFileChecksum {
242 path: "c".to_string(),
243 size: 1,
244 blake3: "z".to_string(),
245 }],
246 },
247 },
248 ];
249
250 planned.sort_by(|a, b| {
251 archive_priority_rank(a.ty)
252 .cmp(&archive_priority_rank(b.ty))
253 .then_with(|| a.component.cmp(&b.component))
254 .then_with(|| a.archive.file_name.cmp(&b.archive.file_name))
255 });
256
257 assert_eq!(planned[0].ty, SnapshotComponentType::State);
258 assert_eq!(planned[1].ty, SnapshotComponentType::RocksdbIndices);
259 assert_eq!(planned[2].ty, SnapshotComponentType::Transactions);
260 }
261
262 #[test]
263 fn collect_planned_archives_tracks_download_and_output_totals() {
264 let mut components = BTreeMap::new();
265 components.insert(
266 "state".to_string(),
267 ComponentManifest::Single(SingleArchive {
268 file: "state.tar.zst".to_string(),
269 size: 10,
270 decompressed_size: 100,
271 blake3: None,
272 output_files: vec![OutputFileChecksum {
273 path: "db/mdbx.dat".to_string(),
274 size: 100,
275 blake3: "h0".to_string(),
276 }],
277 }),
278 );
279 components.insert(
280 "transactions".to_string(),
281 ComponentManifest::Chunked(ChunkedArchive {
282 blocks_per_file: 500_000,
283 total_blocks: 1_000_000,
284 chunk_sizes: vec![20, 30],
285 chunk_decompressed_sizes: vec![200, 300],
286 chunk_output_files: vec![
287 vec![OutputFileChecksum {
288 path: "static_files/tx-0".to_string(),
289 size: 200,
290 blake3: "h1".to_string(),
291 }],
292 vec![OutputFileChecksum {
293 path: "static_files/tx-1".to_string(),
294 size: 300,
295 blake3: "h2".to_string(),
296 }],
297 ],
298 }),
299 );
300
301 let manifest = SnapshotManifest {
302 block: 1_000_000,
303 chain_id: 1,
304 storage_version: 2,
305 timestamp: 0,
306 base_url: Some("https://example.com".to_string()),
307 reth_version: None,
308 components,
309 };
310
311 let selections = BTreeMap::from([
312 (SnapshotComponentType::State, ComponentSelection::All),
313 (SnapshotComponentType::Transactions, ComponentSelection::Distance(500_000)),
314 ]);
315
316 let planned = collect_planned_archives(&manifest, &selections).unwrap();
317
318 assert_eq!(planned.total_download_size, 40);
319 assert_eq!(planned.total_output_size, 400);
320 assert_eq!(planned.archives.len(), 2);
321 }
322}