Skip to main content

tempo/
snapshot_manifest.rs

1use std::{
2    fs,
3    path::{Path, PathBuf},
4    sync::Arc,
5    time::Instant,
6};
7
8use alloy_primitives::{B256, Bytes};
9use clap::{ArgMatches, FromArgMatches, Parser};
10use commonware_codec::Encode as _;
11use commonware_consensus::simplex::{scheme::bls12381_threshold::vrf::Scheme, types::Finalization};
12use commonware_cryptography::{bls12381::primitives::variant::MinSig, ed25519::PublicKey};
13use commonware_runtime::Runner as _;
14use eyre::{Context as _, OptionExt, ensure};
15use reth_chainspec::EthChainSpec as _;
16use reth_cli_commands::download::{
17    manifest::SnapshotManifest, manifest_cmd::SnapshotManifestCommand,
18};
19use reth_cli_runner::CliRunner;
20use reth_db::DatabaseEnv;
21use reth_node_builder::NodeTypesWithDBAdapter;
22use reth_provider::{
23    BlockIdReader,
24    providers::{BlockchainProvider, ReadOnlyConfig},
25};
26use serde::{Deserialize, Serialize};
27use tempo_chainspec::spec::{TempoChainSpec, chain_value_parser, chainspec_from_chain_id};
28use tempo_consensus::{consensus::Digest, find_last_finalized_marker};
29use tempo_node::node::TempoNode;
30use tempo_telemetry_util::display_duration;
31
32pub(crate) const TEMPO_CONSENSUS_MANIFEST_KEY: &str = "consensus";
33
34type TempoFinalization = Finalization<Scheme<PublicKey, MinSig>, Digest>;
35type TempoExecutionProvider = BlockchainProvider<NodeTypesWithDBAdapter<TempoNode, DatabaseEnv>>;
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub(crate) struct TempoConsensusManifest {
39    pub(crate) height: u64,
40    pub(crate) digest: B256,
41    pub(crate) finalization: Bytes,
42}
43
44#[derive(Debug, Parser)]
45#[command(
46    name = "snapshot-manifest",
47    about = "Generate snapshot archives and a manifest for the EL plus consensus floor certificate."
48)]
49pub(crate) struct Args {
50    #[command(flatten)]
51    inner: SnapshotManifestCommand,
52
53    /// Skip encoding consensus state. This will pass-through directly to Reth.
54    #[arg(
55        long,
56        default_value_t = true,
57        default_missing_value = "true",
58        num_args = 0..=1,
59        require_equals = true
60    )]
61    skip_consensus: bool,
62
63    /// Chain spec override for local/unknown chains.
64    #[arg(long, short, value_parser = chain_value_parser)]
65    chain: Option<Arc<TempoChainSpec>>,
66
67    /// Consensus storage directory. If not set, this will be derived from --datadir.
68    #[arg(long = "consensus.datadir", value_name = "PATH")]
69    consensus_datadir: Option<PathBuf>,
70
71    /// Maximum blocks behind the finalized execution tip to inspect.
72    #[arg(long = "consensus.finalization-search-depth", default_value_t = 100)]
73    finalization_search_depth: u64,
74}
75
76pub(crate) fn run(matches: &ArgMatches) -> eyre::Result<()> {
77    let args = Args::from_arg_matches(matches).wrap_err("failed to parse args")?;
78
79    let source_datadir = matches
80        .get_one::<PathBuf>("source_datadir")
81        .cloned()
82        .expect("--source-dir must be set");
83
84    let output_dir = matches
85        .get_one::<PathBuf>("output_dir")
86        .cloned()
87        .expect("--output-dir must be set");
88
89    args.execute(&source_datadir, &output_dir)
90}
91
92impl Args {
93    fn execute(self, source_datadir: &Path, output_dir: &Path) -> eyre::Result<()> {
94        let Self {
95            inner,
96            skip_consensus,
97            consensus_datadir,
98            finalization_search_depth,
99            chain,
100        } = self;
101
102        fs::create_dir_all(output_dir)
103            .wrap_err_with(|| format!("failed to create output dir: {}", output_dir.display()))?;
104
105        eprintln!("packaging execution layer");
106
107        let start = Instant::now();
108        inner
109            .execute()
110            .wrap_err("reth snapshot-manifest (EL packaging) failed")?;
111
112        eprintln!(
113            "execution layer snapshot finished in {}",
114            display_duration(start.elapsed())
115        );
116
117        if skip_consensus {
118            eprintln!("--skip-consensus set. skipping consensus layer");
119            return Ok(());
120        }
121
122        let manifest_path = output_dir.join("manifest.json");
123        let manifest = read_manifest(&manifest_path)
124            .wrap_err_with(|| format!("failed reading manifest: {}", manifest_path.display()))?;
125
126        let chainspec = resolve_chainspec(chain, manifest.chain_id)?;
127        let execution_provider = execution_provider(chainspec, source_datadir)?;
128
129        let consensus_dir = consensus_datadir.unwrap_or_else(|| source_datadir.join("consensus"));
130        eprintln!(
131            "reading snapshot finalization. consensus dir: {}, search depth: {}",
132            consensus_dir.display(),
133            finalization_search_depth,
134        );
135
136        let (height, finalization) = find_snapshot_finalization(
137            &consensus_dir,
138            execution_provider,
139            finalization_search_depth,
140        )
141        .wrap_err("failed to read finalization state")?;
142
143        let digest = finalization.proposal.payload;
144        let consensus_manifest = TempoConsensusManifest {
145            height,
146            digest: digest.0,
147            finalization: finalization.encode().into(),
148        };
149
150        let manifest_height = manifest.block;
151        ensure!(
152            manifest_height >= height,
153            "finalization marker must be at or below execution"
154        );
155
156        let mut manifest_json =
157            serde_json::to_value(&manifest).wrap_err("failed to serialize merged manifest")?;
158
159        manifest_json
160            .as_object_mut()
161            .ok_or_eyre("serialized manifest was not a JSON object")?
162            .insert(
163                TEMPO_CONSENSUS_MANIFEST_KEY.to_string(),
164                serde_json::to_value(consensus_manifest)
165                    .wrap_err("failed to serialize Tempo consensus manifest extension")?,
166            );
167
168        let manifest_json = serde_json::to_string_pretty(&manifest_json)
169            .wrap_err("failed to serialize manifest")?;
170        fs::write(&manifest_path, manifest_json)
171            .wrap_err_with(|| format!("failed to write {}", manifest_path.display()))?;
172
173        eprintln!("embedded finalization for height `{height}`; execution=`{manifest_height}`",);
174        Ok(())
175    }
176}
177
178fn read_manifest(manifest_path: &Path) -> eyre::Result<SnapshotManifest> {
179    let manifest_bytes = fs::read(manifest_path).wrap_err("failed to read file")?;
180    serde_json::from_slice(&manifest_bytes).wrap_err("failed to parse manifest")
181}
182
183fn resolve_chainspec(
184    chain: Option<Arc<TempoChainSpec>>,
185    manifest_chain_id: u64,
186) -> eyre::Result<Arc<TempoChainSpec>> {
187    match chain {
188        None => chainspec_from_chain_id(manifest_chain_id).ok_or_eyre(format!(
189            "unknown chain id `{manifest_chain_id}`; pass --chain explicitly"
190        )),
191        Some(spec) => {
192            let chain_id = spec.chain_id();
193            ensure!(
194                chain_id == manifest_chain_id,
195                "mismatch in --chain id `{chain_id}` and manifest chain id `{manifest_chain_id}`",
196            );
197            Ok(spec)
198        }
199    }
200}
201
202fn find_snapshot_finalization(
203    consensus_dir: &Path,
204    execution_provider: TempoExecutionProvider,
205    max_depth: u64,
206) -> eyre::Result<(u64, TempoFinalization)> {
207    let runtime_config =
208        commonware_runtime::tokio::Config::default().with_storage_directory(consensus_dir);
209
210    let tip = execution_provider
211        .finalized_block_number()
212        .wrap_err("failed to read finalized block number")?
213        .ok_or_eyre("no finalized execution state")?;
214
215    let runner = commonware_runtime::tokio::Runner::new(runtime_config);
216    runner.start(|context| async move {
217        find_last_finalized_marker(&context, &execution_provider, max_depth)
218            .await?
219            .ok_or_eyre(format!(
220                "no finalization marker found; finalized tip `{tip}` with {max_depth} block lookback"
221            ))
222    })
223}
224
225fn execution_provider(
226    chainspec: Arc<TempoChainSpec>,
227    source_datadir: &Path,
228) -> eyre::Result<TempoExecutionProvider> {
229    let runner = CliRunner::try_default_runtime().wrap_err("failed to fetch execution runtime")?;
230
231    let read_cfg = ReadOnlyConfig::from_datadir(source_datadir);
232    let factory = TempoNode::provider_factory_builder()
233        .open_read_only(chainspec, read_cfg, runner.runtime())
234        .wrap_err("failed to open execution provider")?;
235
236    BlockchainProvider::new(factory).wrap_err("failed to create execution provider")
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn args_defaults_to_skip_consensus() {
245        let args = Args::try_parse_from([
246            "tempo",
247            "--source-datadir",
248            "/source",
249            "--output-dir",
250            "/output",
251        ])
252        .unwrap();
253
254        assert!(args.skip_consensus);
255    }
256
257    #[test]
258    fn args_accepts_bare_skip_consensus() {
259        let args = Args::try_parse_from([
260            "tempo",
261            "--source-datadir",
262            "/source",
263            "--output-dir",
264            "/output",
265            "--skip-consensus",
266        ])
267        .unwrap();
268
269        assert!(args.skip_consensus);
270    }
271
272    #[test]
273    fn args_accepts_explicit_skip_consensus_false() {
274        let args = Args::try_parse_from([
275            "tempo",
276            "--source-datadir",
277            "/source",
278            "--output-dir",
279            "/output",
280            "--skip-consensus=false",
281        ])
282        .unwrap();
283
284        assert!(!args.skip_consensus);
285    }
286
287    #[test]
288    fn consensus_manifest_serializes_binary_fields_as_hex() {
289        let manifest = TempoConsensusManifest {
290            height: 42,
291            digest: B256::with_last_byte(0x2a),
292            finalization: Bytes::from(vec![0x00, 0x01, 0x02, 0xff]),
293        };
294
295        let value = serde_json::to_value(manifest).unwrap();
296
297        assert_eq!(value["height"], 42);
298        assert_eq!(
299            value["digest"],
300            "0x000000000000000000000000000000000000000000000000000000000000002a"
301        );
302        assert_eq!(value["finalization"], "0x000102ff");
303    }
304}