tempo_xtask/
generate_devnet.rs

1use std::{net::SocketAddr, path::PathBuf};
2
3use alloy_primitives::Address;
4use eyre::{Context, OptionExt as _, ensure};
5use rand::SeedableRng as _;
6use reth_network_peers::pk2id;
7use secp256k1::SECP256K1;
8use serde::Serialize;
9
10use crate::genesis_args::GenesisArgs;
11
12/// Generates a config file to run a bunch of validators locally.
13///
14/// This includes generating a genesis.
15#[derive(Debug, clap::Parser)]
16pub(crate) struct GenerateDevnet {
17    /// The target directory that will be populated with the
18    ///
19    /// If this directory exists but is not empty the operation will fail unless `--force`
20    /// is specified. In this case the target directory will be first cleaned.
21    #[arg(long, short, value_name = "DIR")]
22    output: PathBuf,
23
24    /// Whether to overwrite `output`.
25    #[arg(long)]
26    force: bool,
27
28    #[arg(long)]
29    image_tag: String,
30
31    /// The URL at which genesis will be found.
32    #[arg(long)]
33    genesis_url: String,
34
35    #[clap(flatten)]
36    genesis_args: GenesisArgs,
37}
38
39impl GenerateDevnet {
40    pub(crate) async fn run(self) -> eyre::Result<()> {
41        let Self {
42            output,
43            force,
44            image_tag,
45            genesis_url,
46            genesis_args,
47        } = self;
48
49        let seed = genesis_args.seed;
50        let (genesis, consensus_config) = genesis_args
51            .generate_genesis()
52            .await
53            .wrap_err("failed to generate genesis")?;
54
55        let consensus_config = consensus_config
56            .ok_or_eyre("no consensus config generated; did you provide --validators?")?;
57
58        std::fs::create_dir_all(&output).wrap_err_with(|| {
59            format!("failed creating target directory at `{}`", output.display())
60        })?;
61
62        if force {
63            eprintln!(
64                "--force was specified: deleting all files in target directory `{}`",
65                output.display()
66            );
67            // XXX: this first removes the directory and then recreates it. Small workaround
68            // so that one doesn't have to iterate through the entire thing recursively.
69            std::fs::remove_dir_all(&output)
70                .and_then(|_| std::fs::create_dir(&output))
71                .wrap_err_with(|| {
72                    format!("failed clearing target directory at `{}`", output.display())
73                })?;
74        } else {
75            let target_is_empty = std::fs::read_dir(&output)
76                .wrap_err_with(|| {
77                    format!(
78                        "failed reading target directory `{}` to determine if it is empty",
79                        output.display()
80                    )
81                })?
82                .next()
83                .is_none();
84            ensure!(
85                target_is_empty,
86                "target directory `{}` is not empty; delete all its contents or rerun command with --force",
87                output.display(),
88            );
89        }
90
91        let mut rng = rand::rngs::StdRng::seed_from_u64(seed.unwrap_or_else(rand::random::<u64>));
92        let mut execution_peers = vec![];
93
94        let devmode = consensus_config.validators.len() == 1;
95
96        let mut all_configs = vec![];
97        for validator in consensus_config.validators {
98            let (execution_p2p_signing_key, execution_p2p_identity) = {
99                let (sk, pk) = SECP256K1.generate_keypair(&mut rng);
100                (sk, pk2id(&pk))
101            };
102
103            let consensus_p2p_port = validator.addr.port();
104            let execution_p2p_port = consensus_p2p_port + 1;
105            let consensus_metrics_port = consensus_p2p_port + 2;
106
107            execution_peers.push(format!(
108                "enode://{execution_p2p_identity:x}@{}",
109                SocketAddr::new(validator.addr.ip(), execution_p2p_port),
110            ));
111
112            all_configs.push((
113                validator.clone(),
114                ConfigOutput {
115                    execution_genesis_url: genesis_url.clone(),
116
117                    devmode,
118                    node_image_tag: image_tag.clone(),
119
120                    consensus_on_disk_signing_key: validator.signing_key.to_string(),
121                    consensus_on_disk_signing_share: validator.signing_share.to_string(),
122
123                    // FIXME(janis): this should not be zero
124                    consensus_fee_recipient: Address::ZERO,
125
126                    consensus_p2p_port,
127                    consensus_metrics_port,
128                    execution_p2p_port,
129
130                    execution_p2p_disc_key: execution_p2p_signing_key.display_secret().to_string(),
131
132                    // set in next loop, before writing.
133                    execution_peers: vec![],
134                },
135            ));
136
137            println!("created a config for validator `{}`", validator.addr);
138        }
139
140        for (validator, mut config) in all_configs {
141            config.execution_peers = execution_peers.clone();
142            let config_json = serde_json::to_string_pretty(&config)
143                .wrap_err("failed to convert config to json")?;
144            // TODO: use Path::with_added_extension once we are on 1.91
145            let dst = output.join(format!("{}.json", validator.addr));
146            std::fs::write(&dst, config_json).wrap_err_with(|| {
147                format!("failed to write deployment config to `{}`", dst.display())
148            })?;
149            println!("wrote config to `{}`", dst.display());
150        }
151        eprintln!("config files written");
152
153        let genesis_ser = serde_json::to_string_pretty(&genesis)
154            .wrap_err("failed serializing genesis as json")?;
155        let dst = output.join("genesis.json");
156        std::fs::write(&dst, &genesis_ser)
157            .wrap_err_with(|| format!("failed writing genesis to `{}`", dst.display()))?;
158        println!("wrote genesis to `{}`", dst.display());
159        Ok(())
160    }
161}
162
163#[derive(Debug, Serialize)]
164pub(crate) struct ConfigOutput {
165    devmode: bool,
166    consensus_on_disk_signing_key: String,
167    consensus_on_disk_signing_share: String,
168    consensus_p2p_port: u16,
169    consensus_fee_recipient: Address,
170    consensus_metrics_port: u16,
171    node_image_tag: String,
172    execution_genesis_url: String,
173    execution_p2p_port: u16,
174    execution_peers: Vec<String>,
175    execution_p2p_disc_key: String,
176}