Skip to main content

tempo_xtask/
generate_shadowfork.rs

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/// Generates artifacts for a private shadow fork from a local Tempo execution datadir.
39///
40/// The source node must be stopped before using its datadir. The resulting network is expected to
41/// run private consensus from the generated artifacts and not follow the source chain after startup.
42#[derive(Debug, clap::Parser)]
43pub(crate) struct GenerateShadowfork {
44    /// The target directory that will be populated.
45    ///
46    /// If this directory exists but is not empty the operation will fail unless `--force`
47    /// is specified. In this case the target directory will be first cleaned.
48    #[arg(long, short, value_name = "DIR")]
49    output: PathBuf,
50
51    /// Whether to overwrite `output`.
52    #[arg(long)]
53    force: bool,
54
55    /// Human-readable source chain name recorded in the manifest.
56    #[arg(long)]
57    source_chain: String,
58
59    /// Source execution datadir, chain datadir, or db directory to fork from.
60    #[arg(long, value_name = "DIR")]
61    execution_datadir: PathBuf,
62
63    /// Source block to fork from: `latest`, a decimal block number, or a block hash.
64    ///
65    /// The selected block must be the local datadir tip.
66    #[arg(long, default_value = "latest")]
67    block: BlockTarget,
68
69    /// Keep the source chain ID in the generated genesis.
70    #[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(&registry_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}