reth_cli_commands/download/
manifest_cmd.rs1use crate::download::manifest::{generate_manifest, SnapshotManifest};
2use clap::Parser;
3use eyre::{Result, WrapErr};
4use reth_db::{mdbx::DatabaseArguments, open_db_read_only, tables, Database};
5use reth_db_api::transaction::DbTx;
6use reth_primitives_traits::FastInstant as Instant;
7use reth_stages_types::StageId;
8use reth_static_file_types::DEFAULT_BLOCKS_PER_STATIC_FILE;
9use std::path::PathBuf;
10use tracing::{info, warn};
11
12#[derive(Debug, Parser)]
17pub struct SnapshotManifestCommand {
18 #[arg(long, short = 'd')]
20 source_datadir: PathBuf,
21
22 #[arg(long)]
24 base_url: Option<String>,
25
26 #[arg(long, short = 'o')]
28 output_dir: PathBuf,
29
30 #[arg(long)]
34 block: Option<u64>,
35
36 #[arg(long, default_value = "1")]
38 chain_id: u64,
39
40 #[arg(long, value_parser = clap::value_parser!(u64).range(1..))]
44 blocks_per_file: Option<u64>,
45
46 #[arg(long)]
52 previous_manifest: Option<PathBuf>,
53}
54
55impl SnapshotManifestCommand {
56 pub fn execute(self) -> Result<()> {
58 let block = match self.block {
59 Some(block) => block,
60 None => infer_snapshot_block(&self.source_datadir)?,
61 };
62 let blocks_per_file = match self.blocks_per_file {
63 Some(blocks_per_file) => blocks_per_file,
64 None => infer_blocks_per_file(&self.source_datadir)?,
65 };
66
67 info!(target: "reth::cli",
68 dir = ?self.source_datadir,
69 output = ?self.output_dir,
70 block,
71 blocks_per_file,
72 "Packaging modular snapshot archives"
73 );
74 let previous = self
75 .previous_manifest
76 .as_ref()
77 .map(|path| -> Result<SnapshotManifest> {
78 let json = reth_fs_util::read_to_string(path)?;
79 serde_json::from_str(&json)
80 .wrap_err_with(|| format!("Invalid previous manifest: {}", path.display()))
81 })
82 .transpose()?;
83
84 let start = Instant::now();
85 let manifest = generate_manifest(
86 &self.source_datadir,
87 &self.output_dir,
88 self.base_url.as_deref(),
89 block,
90 self.chain_id,
91 blocks_per_file,
92 previous.as_ref(),
93 )?;
94
95 let num_components = manifest.components.len();
96 let json = serde_json::to_string_pretty(&manifest)?;
97 let output = self.output_dir.join("manifest.json");
98 reth_fs_util::write(&output, &json)?;
99 info!(target: "reth::cli",
100 path = ?output,
101 components = num_components,
102 block = manifest.block,
103 elapsed = ?start.elapsed(),
104 "Manifest written"
105 );
106
107 Ok(())
108 }
109}
110
111fn infer_snapshot_block(source_datadir: &std::path::Path) -> Result<u64> {
113 if let Ok(block) = infer_snapshot_block_from_db(source_datadir) {
114 return Ok(block);
115 }
116
117 let block = infer_snapshot_block_from_headers(source_datadir)?;
118 warn!(
119 target: "reth::cli",
120 block,
121 "Could not read Finish stage checkpoint from source DB, using header static-file tip"
122 );
123 Ok(block)
124}
125
126fn infer_snapshot_block_from_db(source_datadir: &std::path::Path) -> Result<u64> {
128 let candidates = [source_datadir.join("db"), source_datadir.to_path_buf()];
129
130 for db_path in candidates {
131 if !db_path.exists() {
132 continue;
133 }
134
135 let db = match open_db_read_only(&db_path, DatabaseArguments::default()) {
136 Ok(db) => db,
137 Err(_) => continue,
138 };
139
140 let tx = db.tx()?;
141 if let Some(checkpoint) = tx.get::<tables::StageCheckpoints>(StageId::Finish.to_string())? {
142 return Ok(checkpoint.block_number);
143 }
144 }
145
146 eyre::bail!(
147 "Could not infer --block from source DB (Finish checkpoint missing); pass --block manually"
148 );
149}
150
151fn infer_snapshot_block_from_headers(source_datadir: &std::path::Path) -> Result<u64> {
153 let max_end = header_ranges(source_datadir)?
154 .into_iter()
155 .map(|(_, end)| end)
156 .max()
157 .ok_or_else(|| eyre::eyre!("No header static files found to infer --block"))?;
158 Ok(max_end)
159}
160
161fn infer_blocks_per_file(source_datadir: &std::path::Path) -> Result<u64> {
163 let mut inferred = None;
164 for (start, end) in header_ranges(source_datadir)? {
165 let span = end.saturating_sub(start).saturating_add(1);
166 if span == 0 {
167 continue;
168 }
169
170 if let Some(existing) = inferred {
171 if existing != span {
172 eyre::bail!(
173 "Inconsistent header static file ranges; pass --blocks-per-file manually"
174 );
175 }
176 } else {
177 inferred = Some(span);
178 }
179 }
180
181 inferred.ok_or_else(|| {
182 eyre::eyre!(
183 "Could not infer --blocks-per-file from header static files; pass it manually (default is {DEFAULT_BLOCKS_PER_STATIC_FILE})"
184 )
185 })
186}
187
188fn header_ranges(source_datadir: &std::path::Path) -> Result<Vec<(u64, u64)>> {
190 let static_files_dir = source_datadir.join("static_files");
191 let static_files_dir =
192 if static_files_dir.exists() { static_files_dir } else { source_datadir.to_path_buf() };
193
194 let entries = std::fs::read_dir(&static_files_dir).wrap_err_with(|| {
195 format!("Failed to read static files directory: {}", static_files_dir.display())
196 })?;
197
198 let mut ranges = Vec::new();
199 for entry in entries {
200 let entry = entry?;
201 let file_name = entry.file_name();
202 let file_name = file_name.to_string_lossy();
203 if let Some(range) = parse_headers_range(&file_name) {
204 ranges.push(range);
205 }
206 }
207
208 Ok(ranges)
209}
210
211fn parse_headers_range(file_name: &str) -> Option<(u64, u64)> {
213 let remainder = file_name.strip_prefix("static_file_headers_")?;
214 let (start, end_with_suffix) = remainder.split_once('_')?;
215
216 let start = start.parse::<u64>().ok()?;
217 let end_digits: String = end_with_suffix.chars().take_while(|ch| ch.is_ascii_digit()).collect();
218 let end = end_digits.parse::<u64>().ok()?;
219
220 Some((start, end))
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use tempfile::tempdir;
227
228 #[test]
229 fn parse_headers_range_works_with_suffixes() {
230 assert_eq!(parse_headers_range("static_file_headers_0_499999"), Some((0, 499_999)));
231 assert_eq!(
232 parse_headers_range("static_file_headers_500000_999999.jar"),
233 Some((500_000, 999_999))
234 );
235 assert_eq!(parse_headers_range("static_file_transactions_0_499999"), None);
236 }
237
238 #[test]
239 fn infer_blocks_per_file_from_header_ranges() {
240 let dir = tempdir().unwrap();
241 let sf = dir.path().join("static_files");
242 std::fs::create_dir_all(&sf).unwrap();
243 std::fs::write(sf.join("static_file_headers_0_499999"), []).unwrap();
244 std::fs::write(sf.join("static_file_headers_500000_999999.jar"), []).unwrap();
245
246 assert_eq!(infer_blocks_per_file(dir.path()).unwrap(), 500_000);
247 }
248
249 #[test]
250 fn infer_snapshot_block_from_headers_uses_max_end() {
251 let dir = tempdir().unwrap();
252 let sf = dir.path().join("static_files");
253 std::fs::create_dir_all(&sf).unwrap();
254 std::fs::write(sf.join("static_file_headers_0_499999"), []).unwrap();
255 std::fs::write(sf.join("static_file_headers_500000_999999"), []).unwrap();
256
257 assert_eq!(infer_snapshot_block_from_headers(dir.path()).unwrap(), 999_999);
258 }
259}