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 #[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 #[arg(long, short, value_parser = chain_value_parser)]
65 chain: Option<Arc<TempoChainSpec>>,
66
67 #[arg(long = "consensus.datadir", value_name = "PATH")]
69 consensus_datadir: Option<PathBuf>,
70
71 #[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}