tempo/
snapshot_download.rs1use std::{
2 fs,
3 path::{Path, PathBuf},
4 time::Instant,
5};
6
7use clap::{ArgMatches, FromArgMatches, Parser};
8use eyre::{Context as _, OptionExt, bail};
9use reth_cli_commands::download::DownloadCommand;
10use reth_cli_runner::CliRunner;
11use tempo_chainspec::spec::TempoChainSpecParser;
12use tempo_telemetry_util::display_duration;
13use tracing::info;
14
15use crate::snapshot_manifest::{TEMPO_CONSENSUS_MANIFEST_KEY, TempoConsensusManifest};
16
17const BOOTSTRAP_FINALIZATION_FILE: &str = "bootstrap/finalization.cert";
18
19#[derive(Debug, Parser)]
20#[command(
21 name = "download",
22 about = "Downloads snapshot archives produced by `tempo snapshot-manifest`."
23)]
24pub(crate) struct Args {
25 #[command(flatten)]
26 inner: DownloadCommand<TempoChainSpecParser>,
27
28 #[arg(
30 long,
31 default_value_t = true,
32 default_missing_value = "true",
33 num_args = 0..=1,
34 require_equals = true
35 )]
36 skip_consensus: bool,
37
38 #[arg(long = "consensus.datadir", value_name = "PATH")]
40 consensus_datadir: Option<PathBuf>,
41}
42
43pub(crate) fn run_with_runner(matches: &ArgMatches, runner: CliRunner) -> eyre::Result<()> {
44 let args = Args::from_arg_matches(matches).wrap_err("failed to parse args")?;
45
46 let datadir = matches
47 .get_raw("datadir")
48 .and_then(|mut v| v.next())
49 .map(PathBuf::from)
50 .expect("--datadir must be set");
51
52 let manifest_url = matches.get_one::<String>("manifest_url").cloned();
53 let manifest_path = matches.get_one::<PathBuf>("manifest_path").cloned();
54
55 runner.block_on(async move {
56 info!("running execution layer download...");
57
58 let start = Instant::now();
59 args.inner
60 .execute::<tempo_node::node::TempoNode>()
61 .await
62 .wrap_err("execution layer download failed")?;
63
64 info!(
65 "execution layer download finished in {}",
66 display_duration(start.elapsed())
67 );
68
69 if args.skip_consensus {
70 info!("--skip-consensus set. skipping consensus layer");
71 return Ok(());
72 }
73
74 let consensus_dir = args
75 .consensus_datadir
76 .unwrap_or_else(|| datadir.join("consensus"));
77
78 let consensus_manifest = load_consensus_manifest(manifest_url, manifest_path).await?;
79 write_bootstrap_finalization(&consensus_dir, &consensus_manifest)?;
80
81 Ok(())
82 })
83}
84
85async fn load_consensus_manifest(
86 manifest_url: Option<String>,
87 manifest_path: Option<PathBuf>,
88) -> eyre::Result<TempoConsensusManifest> {
89 let manifest_bytes = match (manifest_path, (manifest_url)) {
90 (None, None) => bail!("--manifest-url or --manifest-path must be set"),
91 (Some(path), _) => fs::read(path).wrap_err("failed to read manifest file")?,
92 (_, Some(url)) => {
93 let client = reqwest::Client::new();
94 let resp = client
95 .get(url)
96 .send()
97 .await
98 .wrap_err("failed to fetch from manifest url")?
99 .error_for_status()
100 .wrap_err("invalid response from manifest url")?;
101
102 resp.bytes()
103 .await
104 .wrap_err("failed to parse manifest from url")?
105 .to_vec()
106 }
107 };
108
109 let value: serde_json::Value =
110 serde_json::from_slice(&manifest_bytes).wrap_err("failed to parse manifest.json")?;
111
112 let consensus_manifest = value
113 .get(TEMPO_CONSENSUS_MANIFEST_KEY)
114 .map(|value| serde_json::from_value(value.clone()))
115 .transpose()
116 .wrap_err("failed to parse TempoConsensusManifest extension")?
117 .ok_or_eyre("missing consensus in manifest")?;
118
119 Ok(consensus_manifest)
120}
121
122fn write_bootstrap_finalization(
123 consensus_dir: &Path,
124 consensus_manifest: &TempoConsensusManifest,
125) -> eyre::Result<()> {
126 let path = consensus_dir.join(BOOTSTRAP_FINALIZATION_FILE);
127 if let Some(parent) = path.parent() {
128 fs::create_dir_all(parent)
129 .wrap_err_with(|| format!("failed to create dir: {}", parent.display()))?;
130 }
131
132 fs::write(&path, consensus_manifest.finalization.as_ref())
133 .wrap_err_with(|| format!("failed to write finalization to {}", path.display()))?;
134
135 info!(path = %path.display(), "persisted bootstrap finalization");
136 Ok(())
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use alloy_primitives::{B256, Bytes};
143
144 #[test]
145 fn args_parses_mixed_reth_and_tempo_flags() {
146 let args = Args::try_parse_from([
149 "tempo",
150 "--manifest-url",
151 "https://snap/manifest.json",
152 "--datadir",
153 "/d",
154 "--consensus.datadir",
155 "/c",
156 "--skip-consensus",
157 ])
158 .unwrap();
159
160 assert!(args.skip_consensus);
161 assert_eq!(args.consensus_datadir.as_deref(), Some(Path::new("/c")));
162 }
163
164 #[test]
165 fn args_accepts_explicit_skip_consensus_false() {
166 let args = Args::try_parse_from([
167 "tempo",
168 "--manifest-url",
169 "https://snap/manifest.json",
170 "--datadir",
171 "/d",
172 "--skip-consensus=false",
173 ])
174 .unwrap();
175
176 assert!(!args.skip_consensus);
177 }
178
179 #[test]
180 fn load_manifest_reads_tempo_consensus_extension_from_path() {
181 let dir = tempfile::tempdir().unwrap();
182 let path = dir.path().join("manifest.json");
183 let bytes = br#"{
184 "block": 42,
185 "chain_id": 1,
186 "storage_version": 2,
187 "timestamp": 0,
188 "components": {},
189 "consensus": {
190 "height": 42,
191 "digest": "0x000000000000000000000000000000000000000000000000000000000000002a",
192 "finalization": "0xaabbcc"
193 }
194 }"#;
195
196 fs::write(&path, bytes).unwrap();
197
198 let manifest =
199 futures::executor::block_on(load_consensus_manifest(None, Some(path))).unwrap();
200
201 assert_eq!(manifest.height, 42);
202 assert_eq!(manifest.digest, B256::with_last_byte(0x2a));
203 assert_eq!(manifest.finalization, Bytes::from(vec![0xaa, 0xbb, 0xcc]));
204 }
205
206 #[test]
207 fn write_finalization_writes_raw_bytes() {
208 let dir = tempfile::tempdir().unwrap();
209 let tempo_consensus = TempoConsensusManifest {
210 height: 42,
211 digest: B256::with_last_byte(0x2a),
212 finalization: Bytes::from(vec![0x00, 0x01, 0x02, 0xff]),
213 };
214
215 write_bootstrap_finalization(dir.path(), &tempo_consensus).unwrap();
216
217 let bytes = fs::read(dir.path().join(BOOTSTRAP_FINALIZATION_FILE)).unwrap();
218 assert_eq!(bytes, [0x00, 0x01, 0x02, 0xff]);
219 }
220}