Skip to main content

tempo/
snapshot_download.rs

1use 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    /// Skip encoding consensus state. This will pass-through directly to Reth.
29    #[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    /// Consensus storage directory. If not set, this will be derived from --datadir.
39    #[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        // Order interleaves tempo + reth flags to exercise both schemas in
147        // the same parse pass.
148        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}