1use std::{
2 collections::BTreeMap,
3 net::SocketAddr,
4 path::{Path, PathBuf},
5 str::FromStr,
6 time::{SystemTime, UNIX_EPOCH},
7};
8
9use alloy::primitives::{Address, B256};
10use alloy_consensus::Sealable as _;
11use commonware_codec::Encode as _;
12use commonware_consensus::types::Epoch;
13use commonware_cryptography::Signer as _;
14use eyre::{Context as _, OptionExt as _, ensure, eyre};
15use rand_08::SeedableRng as _;
16use reth_db::{mdbx::DatabaseArguments, open_db};
17use reth_db_api::{database::Database as _, tables, transaction::DbTx};
18use reth_network_peers::pk2id;
19use reth_provider::{
20 BlockHashReader as _, HeaderProvider as _, StaticFileProviderBuilder, StaticFileSegment,
21};
22use secp256k1::SECP256K1;
23use serde::Serialize;
24use tempo_contracts::precompiles::VALIDATOR_CONFIG_V2_ADDRESS;
25use tempo_precompiles::validator_config_v2::VALIDATOR_NS_ADD;
26use tempo_primitives::TempoPrimitives;
27use tempo_validator_config::ValidatorConfig;
28
29use crate::{
30 genesis_args::GenesisArgs,
31 shadowfork::{
32 SHADOW_CHAINSPEC_FILE, SHADOW_EPOCH, SHADOWFORK_SIGNING_KEY_SECRET, SourceExecutionDataDir,
33 resolve_source_execution_data_dir, source_chain_cli_arg, source_chain_id,
34 write_shadow_chainspec as write_shadow_chainspec_file,
35 },
36};
37
38#[derive(Debug, clap::Parser)]
43pub(crate) struct GenerateShadowfork {
44 #[arg(long, short, value_name = "DIR")]
49 output: PathBuf,
50
51 #[arg(long)]
53 force: bool,
54
55 #[arg(long)]
57 source_chain: String,
58
59 #[arg(long, value_name = "DIR")]
61 execution_datadir: PathBuf,
62
63 #[arg(long, default_value = "latest")]
67 block: BlockTarget,
68
69 #[arg(long, conflicts_with = "chain_id")]
71 preserve_chain_id: bool,
72
73 #[clap(flatten)]
74 genesis_args: GenesisArgs,
75}
76
77impl GenerateShadowfork {
78 pub(crate) async fn run(self) -> eyre::Result<()> {
79 let Self {
80 output,
81 force,
82 source_chain,
83 execution_datadir,
84 block,
85 preserve_chain_id,
86 mut genesis_args,
87 } = self;
88
89 prepare_output_dir(&output, force)?;
90
91 let source_chain_id = source_chain_id(&source_chain).ok_or_else(|| {
92 eyre!(
93 "cannot infer source chain id for source_chain `{source_chain}`; \
94 expected one of mainnet, presto, moderato, testnet, or dev"
95 )
96 })?;
97 let source_execution_chain = source_chain_cli_arg(&source_chain, source_chain_id)
98 .ok_or_else(|| {
99 eyre!(
100 "cannot infer source execution chain directory for source_chain `{source_chain}` and chain id `{source_chain_id}`"
101 )
102 })?
103 .to_string();
104 let source_execution_datadir =
105 resolve_source_execution_data_dir(&execution_datadir, &source_chain, source_chain_id)?;
106 let manifest_execution_datadir =
107 std::fs::canonicalize(&execution_datadir).unwrap_or_else(|_| execution_datadir.clone());
108
109 if preserve_chain_id {
110 genesis_args.set_chain_id(source_chain_id);
111 }
112
113 let source_block = block
114 .read_from_datadir(&source_execution_datadir)
115 .wrap_err("failed to read source block from local execution datadir")?;
116
117 let seed = genesis_args.seed;
118 let shadow_chain_id = genesis_args.chain_id();
119 let validator_onchain_addresses = genesis_args
120 .validator_onchain_addresses()
121 .wrap_err("failed resolving shadow validator onchain addresses")?;
122 let (genesis, consensus_config) = genesis_args
123 .generate_genesis()
124 .await
125 .wrap_err("failed to generate shadow genesis")?;
126 let consensus_config = consensus_config
127 .ok_or_eyre("no consensus config generated; did you provide --validators?")?;
128
129 let mut rng =
130 rand_08::rngs::StdRng::seed_from_u64(seed.unwrap_or_else(rand_08::random::<u64>));
131 let mut node_outputs = vec![];
132 for (idx, validator) in consensus_config.validators.iter().enumerate() {
133 let (execution_p2p_signing_key, execution_p2p_identity) = {
134 let (sk, pk) = SECP256K1.generate_keypair(&mut rng);
135 (sk, pk2id(&pk))
136 };
137
138 let consensus_p2p_port = validator.addr.port();
139 let execution_p2p_port = consensus_p2p_port + 1;
140 let validator_address = validator_onchain_addresses[idx];
141 let fee_recipient = validator_address;
142 let validator_public_key: B256 = validator
143 .public_key()
144 .encode()
145 .as_ref()
146 .try_into()
147 .expect("ed25519 public keys are 32 bytes");
148 let validator_config = ValidatorConfig {
149 chain_id: source_chain_id,
150 validator_address,
151 public_key: validator_public_key,
152 ingress: validator.addr,
153 egress: validator.addr.ip(),
154 };
155 let message = validator_config.add_validator_message_hash(fee_recipient);
156 let validator_add_signature = const_hex::encode_prefixed(
157 validator
158 .signing_key
159 .clone()
160 .into_inner()
161 .sign(VALIDATOR_NS_ADD, message.as_slice())
162 .encode(),
163 );
164
165 node_outputs.push(NodeOutput {
166 index: idx,
167 validator_addr: validator.addr,
168 validator_public_key,
169 validator_address,
170 fee_recipient,
171 validator_add_signature,
172 consensus_p2p_port,
173 consensus_metrics_port: consensus_p2p_port + 2,
174 execution_p2p_port,
175 authrpc_port: execution_p2p_port + 2,
176 execution_p2p_disc_key: execution_p2p_signing_key.display_secret().to_string(),
177 execution_p2p_identity: format!("{execution_p2p_identity:x}"),
178 });
179 }
180
181 let genesis_dst = output.join("genesis.json");
182 let genesis_ser = serde_json::to_string_pretty(&genesis)
183 .wrap_err("failed serializing genesis as json")?;
184 let shadow_validator_config_v2_storage = extract_validator_config_v2_storage(&genesis_ser)?;
185 std::fs::write(&genesis_dst, &genesis_ser)
186 .wrap_err_with(|| format!("failed writing genesis to `{}`", genesis_dst.display()))?;
187
188 let shadow_epoch_length = source_block
189 .number
190 .checked_add(1)
191 .ok_or_eyre("fork block number overflowed shadow epoch length")?;
192 let shadow_chainspec_path =
193 write_shadow_chainspec(&output, &source_chain, source_chain_id, shadow_epoch_length)?;
194 let mut shadow_dkg_outcome = consensus_config.to_genesis_dkg_outcome();
195 shadow_dkg_outcome.epoch = Epoch::new(SHADOW_EPOCH);
196 let shadow_dkg_outcome = const_hex::encode_prefixed(shadow_dkg_outcome.encode());
197
198 for (validator, node) in consensus_config.validators.iter().zip(node_outputs.iter()) {
199 let target_dir = node.dir(&output);
200 std::fs::create_dir(&target_dir).wrap_err_with(|| {
201 format!(
202 "failed creating target directory to store validator specific keys at `{}`",
203 target_dir.display()
204 )
205 })?;
206
207 let signing_key_dst = target_dir.join("signing.key");
208 validator
209 .signing_key
210 .write_to_file_encrypted(
211 &signing_key_dst,
212 tempo_consensus_config::SigningKeyPassphrase::from(
213 SHADOWFORK_SIGNING_KEY_SECRET,
214 ),
215 )
216 .wrap_err_with(|| {
217 format!(
218 "failed writing signing key to `{}`",
219 signing_key_dst.display()
220 )
221 })?;
222
223 let signing_share_dst = target_dir.join("signing.share");
224 std::fs::write(&signing_share_dst, validator.signing_share.to_string()).wrap_err_with(
225 || {
226 format!(
227 "failed writing signing share to `{}`",
228 signing_share_dst.display()
229 )
230 },
231 )?;
232
233 let enode_key_dst = target_dir.join("enode.key");
234 std::fs::write(&enode_key_dst, &node.execution_p2p_disc_key).wrap_err_with(|| {
235 format!("failed writing enode key to `{}`", enode_key_dst.display())
236 })?;
237
238 let enode_identity_dst = target_dir.join("enode.identity");
239 std::fs::write(&enode_identity_dst, &node.execution_p2p_identity).wrap_err_with(
240 || {
241 format!(
242 "failed writing enode identity to `{}`",
243 enode_identity_dst.display()
244 )
245 },
246 )?;
247 }
248
249 let manifest = ShadowForkManifest {
250 source_chain,
251 source_chain_id,
252 shadow_chain_id,
253 source_execution_datadir: manifest_execution_datadir,
254 source_execution_chain,
255 fork_block_number: source_block.number,
256 fork_block_hash: source_block.hash,
257 fork_parent_hash: source_block.parent_hash,
258 fork_state_root: source_block.state_root,
259 fork_timestamp: source_block.timestamp,
260 shadow_epoch: SHADOW_EPOCH,
261 shadow_epoch_length,
262 shadow_dkg_outcome,
263 shadow_validator_config_v2_storage,
264 shadow_chainspec: SHADOW_CHAINSPEC_FILE.to_string(),
265 created_at_unix_secs: SystemTime::now()
266 .duration_since(UNIX_EPOCH)
267 .wrap_err("system clock is before UNIX epoch")?
268 .as_secs(),
269 validator_count: node_outputs.len(),
270 validators: node_outputs,
271 notes: vec![
272 "This is a one-shot shadow fork preparation; generated nodes must not use --follow."
273 .to_string(),
274 "Local-datadir shadow forks use the source chainspec for execution because the database keeps the source genesis hash."
275 .to_string(),
276 "Run bootstrap-shadowfork against a stopped local execution datadir for each shadow node; bootstrap writes a shadow chainspec whose epochLength makes the fork block the epoch-0 boundary, patches ValidatorConfigV2 in the local execution DB, and seeds private DKG state for epoch 1."
277 .to_string(),
278 ],
279 };
280
281 let manifest_dst = output.join("manifest.json");
282 let manifest_json = serde_json::to_string_pretty(&manifest)
283 .wrap_err("failed serializing manifest as json")?;
284 std::fs::write(&manifest_dst, manifest_json).wrap_err_with(|| {
285 format!(
286 "failed writing shadow fork manifest to `{}`",
287 manifest_dst.display()
288 )
289 })?;
290
291 println!("wrote shadow genesis to `{}`", genesis_dst.display());
292 println!(
293 "wrote shadow chainspec to `{}`",
294 shadow_chainspec_path.display()
295 );
296 println!("wrote shadow fork manifest to `{}`", manifest_dst.display());
297
298 Ok(())
299 }
300}
301
302fn prepare_output_dir(output: &Path, force: bool) -> eyre::Result<()> {
303 std::fs::create_dir_all(output)
304 .wrap_err_with(|| format!("failed creating target directory at `{}`", output.display()))?;
305
306 if force {
307 eprintln!(
308 "--force was specified: deleting all files in target directory `{}`",
309 output.display()
310 );
311 std::fs::remove_dir_all(output)
312 .and_then(|_| std::fs::create_dir(output))
313 .wrap_err_with(|| {
314 format!("failed clearing target directory at `{}`", output.display())
315 })?;
316 } else {
317 let target_is_empty = std::fs::read_dir(output)
318 .wrap_err_with(|| {
319 format!(
320 "failed reading target directory `{}` to determine if it is empty",
321 output.display()
322 )
323 })?
324 .next()
325 .is_none();
326 ensure!(
327 target_is_empty,
328 "target directory `{}` is not empty; delete all its contents or rerun command with --force",
329 output.display(),
330 );
331 }
332
333 Ok(())
334}
335
336fn write_shadow_chainspec(
337 output: &Path,
338 source_chain: &str,
339 source_chain_id: u64,
340 shadow_epoch_length: u64,
341) -> eyre::Result<PathBuf> {
342 let path = output.join(SHADOW_CHAINSPEC_FILE);
343 write_shadow_chainspec_file(&path, source_chain, source_chain_id, shadow_epoch_length)?;
344 Ok(path)
345}
346
347fn extract_validator_config_v2_storage(
348 genesis_json: &str,
349) -> eyre::Result<BTreeMap<String, String>> {
350 let genesis: serde_json::Value =
351 serde_json::from_str(genesis_json).wrap_err("failed parsing generated genesis JSON")?;
352 let registry_address = VALIDATOR_CONFIG_V2_ADDRESS.to_string().to_ascii_lowercase();
353 let storage = genesis
354 .get("alloc")
355 .and_then(serde_json::Value::as_object)
356 .and_then(|alloc| alloc.get(®istry_address))
357 .and_then(|account| account.get("storage"))
358 .and_then(serde_json::Value::as_object)
359 .ok_or_else(|| {
360 eyre!(
361 "generated genesis does not contain ValidatorConfigV2 storage at `{registry_address}`"
362 )
363 })?;
364
365 storage
366 .iter()
367 .map(|(slot, value)| {
368 Ok((
369 slot.clone(),
370 value
371 .as_str()
372 .ok_or_else(|| {
373 eyre!("generated ValidatorConfigV2 storage value is not a string")
374 })?
375 .to_string(),
376 ))
377 })
378 .collect()
379}
380
381#[derive(Clone, Debug)]
382enum BlockTarget {
383 Latest,
384 Number(u64),
385 Hash(B256),
386}
387
388impl FromStr for BlockTarget {
389 type Err = eyre::Report;
390
391 fn from_str(value: &str) -> Result<Self, Self::Err> {
392 if value.eq_ignore_ascii_case("latest") {
393 return Ok(Self::Latest);
394 }
395
396 if let Ok(number) = value.parse::<u64>() {
397 return Ok(Self::Number(number));
398 }
399
400 if value.starts_with("0x") {
401 return Ok(Self::Hash(value.parse().wrap_err_with(|| {
402 format!("failed parsing block hash `{value}`")
403 })?));
404 }
405
406 Err(eyre!(
407 "invalid block target `{value}`; expected `latest`, a decimal block number, or a 0x-prefixed block hash"
408 ))
409 }
410}
411
412impl BlockTarget {
413 fn read_from_datadir(
414 &self,
415 source_datadir: &SourceExecutionDataDir,
416 ) -> eyre::Result<SourceBlock> {
417 ensure!(
418 source_datadir.static_files_path.exists(),
419 "execution static files directory `{}` does not exist",
420 source_datadir.static_files_path.display(),
421 );
422 let static_file_provider =
423 StaticFileProviderBuilder::read_only(&source_datadir.static_files_path)
424 .build::<TempoPrimitives>()
425 .wrap_err_with(|| {
426 format!(
427 "failed opening execution static files at `{}`",
428 source_datadir.static_files_path.display(),
429 )
430 })?;
431 let highest_header = static_file_provider
432 .get_highest_static_file_block(StaticFileSegment::Headers)
433 .ok_or_else(|| eyre!("execution static files contain no headers"))?;
434
435 let block_number = match self {
436 Self::Latest => highest_header,
437 Self::Number(number) => *number,
438 Self::Hash(hash) => {
439 let db = open_db(&source_datadir.db_path, DatabaseArguments::default())
440 .wrap_err_with(|| {
441 format!(
442 "failed opening execution database at `{}`",
443 source_datadir.db_path.display(),
444 )
445 })?;
446 let tx = db.tx()?;
447 tx.get::<tables::HeaderNumbers>(*hash)?
448 .ok_or_else(|| eyre!("block hash `{hash}` not found in local datadir"))?
449 }
450 };
451 ensure!(
452 block_number == highest_header,
453 "shadow fork generation can only use the local execution datadir tip. datadir tip is `{highest_header}`, but --block resolved to `{block_number}`",
454 );
455
456 let header = static_file_provider
457 .header_by_number(block_number)
458 .wrap_err_with(|| format!("failed reading boundary header at block `{block_number}`"))?
459 .ok_or_else(|| eyre!("boundary header at block `{block_number}` was not found"))?;
460 let hash = static_file_provider
461 .block_hash(block_number)
462 .wrap_err_with(|| {
463 format!("failed reading canonical hash for boundary block `{block_number}`")
464 })?
465 .unwrap_or_else(|| header.hash_slow());
466 if let Self::Hash(expected_hash) = self {
467 ensure!(
468 hash == *expected_hash,
469 "block hash `{expected_hash}` resolved to block `{block_number}`, but the local datadir canonical hash is `{hash}`",
470 );
471 }
472
473 Ok(SourceBlock {
474 number: block_number,
475 hash,
476 parent_hash: header.inner.parent_hash,
477 state_root: header.inner.state_root,
478 timestamp: header.inner.timestamp,
479 })
480 }
481}
482
483#[derive(Debug)]
484struct SourceBlock {
485 number: u64,
486 hash: B256,
487 parent_hash: B256,
488 state_root: B256,
489 timestamp: u64,
490}
491
492#[derive(Debug, Serialize)]
493struct ShadowForkManifest {
494 source_chain: String,
495 source_chain_id: u64,
496 shadow_chain_id: u64,
497 source_execution_datadir: PathBuf,
498 source_execution_chain: String,
499 fork_block_number: u64,
500 fork_block_hash: B256,
501 fork_parent_hash: B256,
502 fork_state_root: B256,
503 fork_timestamp: u64,
504 shadow_epoch: u64,
505 shadow_epoch_length: u64,
506 shadow_dkg_outcome: String,
507 shadow_validator_config_v2_storage: BTreeMap<String, String>,
508 shadow_chainspec: String,
509 created_at_unix_secs: u64,
510 validator_count: usize,
511 validators: Vec<NodeOutput>,
512 notes: Vec<String>,
513}
514
515#[derive(Debug, Serialize)]
516struct NodeOutput {
517 index: usize,
518 validator_addr: SocketAddr,
519 validator_public_key: B256,
520 validator_address: Address,
521 fee_recipient: Address,
522 validator_add_signature: String,
523 consensus_p2p_port: u16,
524 consensus_metrics_port: u16,
525 execution_p2p_port: u16,
526 authrpc_port: u16,
527 execution_p2p_disc_key: String,
528 execution_p2p_identity: String,
529}
530
531impl NodeOutput {
532 fn dir(&self, output: &Path) -> PathBuf {
533 output.join(format!("node-{}", self.index))
534 }
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540
541 #[test]
542 fn parses_block_targets() {
543 assert!(matches!(
544 "latest".parse::<BlockTarget>().unwrap(),
545 BlockTarget::Latest
546 ));
547 assert!(matches!(
548 "42".parse::<BlockTarget>().unwrap(),
549 BlockTarget::Number(42)
550 ));
551 assert!(
552 "0x1111111111111111111111111111111111111111111111111111111111111111"
553 .parse::<BlockTarget>()
554 .is_ok()
555 );
556 assert!("finalized".parse::<BlockTarget>().is_err());
557 }
558}