pd/network/
config.rs

1use anyhow::Context;
2use decaf377_rdsa::{SigningKey, SpendAuth, VerificationKey};
3use directories::UserDirs;
4use penumbra_sdk_app::genesis::AppState;
5use penumbra_sdk_custody::soft_kms::Config as SoftKmsConfig;
6use penumbra_sdk_keys::keys::{SpendKey, SpendKeyBytes};
7use rand::Rng;
8use rand_core::OsRng;
9use regex::{Captures, Regex};
10use serde::Deserialize;
11use std::{
12    env::current_dir,
13    fs::{self, File},
14    io::Write,
15    net::SocketAddr,
16    path::PathBuf,
17    str::FromStr,
18};
19use tendermint::{node::Id, Genesis, Moniker, PrivateKey};
20use tendermint_config::{
21    net::Address as TendermintAddress, NodeKey, PrivValidatorKey, TendermintConfig,
22};
23use url::Url;
24
25use crate::network::generate::NetworkValidator;
26
27/// Wrapper for a [TendermintConfig], with a constructor for convenient defaults.
28pub struct NetworkTendermintConfig(pub TendermintConfig);
29
30impl NetworkTendermintConfig {
31    /// Use a hard-coded Tendermint config as a base template, substitute
32    /// values via a typed interface, and rerender as TOML.
33    pub fn new(
34        node_name: &str,
35        peers: Vec<TendermintAddress>,
36        external_address: Option<TendermintAddress>,
37        tm_rpc_bind: Option<SocketAddr>,
38        tm_p2p_bind: Option<SocketAddr>,
39    ) -> anyhow::Result<Self> {
40        tracing::debug!("List of CometBFT peers: {:?}", peers);
41        let moniker: Moniker = Moniker::from_str(node_name)?;
42        let mut tm_config = TendermintConfig::parse_toml(include_str!(
43            "../../../../../testnets/cometbft_config_template.toml"
44        ))
45        .context("Failed to parse the TOML config template for CometBFT")?;
46        tm_config.moniker = moniker;
47        tm_config.p2p.seeds = peers;
48        tracing::debug!("External address looks like: {:?}", external_address);
49        tm_config.p2p.external_address = external_address;
50        // The CometBFT config wants URLs, not SocketAddrs, so we'll prepend protocol.
51        if let Some(rpc) = tm_rpc_bind {
52            tm_config.rpc.laddr =
53                parse_tm_address(None, &Url::parse(format!("tcp://{}", rpc).as_str())?)?;
54        }
55        if let Some(p2p) = tm_p2p_bind {
56            tm_config.p2p.laddr =
57                parse_tm_address(None, &Url::parse(format!("tcp://{}", p2p).as_str())?)?;
58        }
59
60        Ok(Self(tm_config))
61    }
62}
63
64impl NetworkTendermintConfig {
65    /// Write Tendermint config files to disk. This includes not only the `config.toml` file,
66    /// but also all keypairs required for node and/or validator identity.
67    pub fn write_config(
68        &self,
69        node_dir: PathBuf,
70        v: &NetworkValidator,
71        genesis: &Genesis<AppState>,
72    ) -> anyhow::Result<()> {
73        // We'll also create the pd state directory here, since it's convenient.
74        let pd_dir = node_dir.clone().join("pd");
75        let cb_data_dir = node_dir.clone().join("cometbft").join("data");
76        let cb_config_dir = node_dir.clone().join("cometbft").join("config");
77
78        tracing::info!(config_dir = %node_dir.display(), "Writing validator configs to");
79
80        fs::create_dir_all(pd_dir)?;
81        fs::create_dir_all(&cb_data_dir)?;
82        fs::create_dir_all(&cb_config_dir)?;
83
84        let genesis_file_path = cb_config_dir.clone().join("genesis.json");
85        tracing::debug!(genesis_file_path = %genesis_file_path.display(), "writing genesis");
86        let mut genesis_file = File::create(genesis_file_path)?;
87        genesis_file.write_all(serde_json::to_string_pretty(&genesis)?.as_bytes())?;
88
89        let cb_config_filepath = cb_config_dir.clone().join("config.toml");
90        tracing::debug!(cometbft_config = %cb_config_filepath.display(), "writing cometbft config.toml");
91        let mut cb_config_file = File::create(cb_config_filepath)?;
92        cb_config_file.write_all(toml::to_string(&self.0)?.as_bytes())?;
93
94        // Write this node's node_key.json
95        // the underlying type doesn't implement Copy or Clone (for the best)
96        let priv_key = tendermint::PrivateKey::Ed25519(
97            v.keys
98                .node_key_sk
99                .ed25519_signing_key()
100                .expect("node key has ed25519 signing key")
101                .clone(),
102        );
103
104        let node_key = NodeKey { priv_key };
105        let cb_node_key_filepath = cb_config_dir.clone().join("node_key.json");
106        tracing::debug!(cb_node_key_filepath = %cb_node_key_filepath.display(), "writing node key file");
107        let mut cb_node_key_file = File::create(cb_node_key_filepath)?;
108        cb_node_key_file.write_all(serde_json::to_string_pretty(&node_key)?.as_bytes())?;
109
110        // Write this node's priv_validator_key.json
111        let priv_validator_key_filepath = cb_config_dir.clone().join("priv_validator_key.json");
112        tracing::debug!(priv_validator_key_filepath = %priv_validator_key_filepath.display(), "writing validator private key");
113        let mut priv_validator_key_file = File::create(priv_validator_key_filepath)?;
114        let priv_validator_key: PrivValidatorKey = v.keys.priv_validator_key()?;
115        priv_validator_key_file
116            .write_all(serde_json::to_string_pretty(&priv_validator_key)?.as_bytes())?;
117
118        // Write the initial validator state:
119        let priv_validator_state_filepath = cb_data_dir.clone().join("priv_validator_state.json");
120        tracing::debug!(priv_validator_state_filepath = %priv_validator_state_filepath.display(), "writing validator state");
121        let mut priv_validator_state_file = File::create(priv_validator_state_filepath)?;
122        priv_validator_state_file.write_all(NetworkValidator::initial_state().as_bytes())?;
123
124        // Write the validator's spend key:
125        let validator_spend_key_filepath = cb_config_dir.clone().join("validator_custody.json");
126        tracing::debug!(validator_spend_key_filepath = %validator_spend_key_filepath.display(), "writing validator custody file");
127        let mut validator_spend_key_file = File::create(validator_spend_key_filepath)?;
128        let validator_wallet = SoftKmsConfig::from(
129            SpendKey::try_from(v.keys.validator_spend_key.clone())
130                .expect("spend key should be valid"),
131        );
132        validator_spend_key_file
133            .write_all(toml::to_string_pretty(&validator_wallet)?.as_bytes())?;
134
135        Ok(())
136    }
137}
138
139/// Construct a [`tendermint_config::net::Address`] from an optional node [`Id`] and `node_address`.
140/// The `node_address` can be an IP address or a hostname. Supports custom ports, defaulting
141/// to 26656 if not specified.
142pub fn parse_tm_address(
143    node_id: Option<&Id>,
144    node_address: &Url,
145) -> anyhow::Result<TendermintAddress> {
146    let hostname = match node_address.host() {
147        Some(h) => h,
148        None => {
149            anyhow::bail!(format!("Could not find hostname in URL: {}", node_address))
150        }
151    };
152    // Default to 26656 for Tendermint port, if not specified.
153    let port = node_address.port().unwrap_or(26656);
154    match node_id {
155        Some(id) => Ok(format!("{id}@{hostname}:{port}").parse()?),
156        None => Ok(format!("{hostname}:{port}").parse()?),
157    }
158}
159
160/// Collection of all keypairs required for a Penumbra validator.
161/// Used to generate a stable identity for a [`NetworkValidator`].
162#[derive(Deserialize)]
163pub struct ValidatorKeys {
164    /// Penumbra spending key and viewing key for this node.
165    pub validator_id_sk: SigningKey<SpendAuth>,
166    pub validator_id_vk: VerificationKey<SpendAuth>,
167    pub validator_spend_key: SpendKeyBytes,
168    /// Consensus key for tendermint.
169    pub validator_cons_sk: tendermint::PrivateKey,
170    pub validator_cons_pk: tendermint::PublicKey,
171    /// P2P auth key for tendermint.
172    pub node_key_sk: tendermint::PrivateKey,
173    #[allow(unused_variables, dead_code)]
174    pub node_key_pk: tendermint::PublicKey,
175}
176
177impl ValidatorKeys {
178    /// Use a hard-coded seed to generate a new set of validator keys.
179    pub fn from_seed(seed: [u8; 32]) -> Self {
180        // Create the spend key for this node.
181        let seed = SpendKeyBytes(seed);
182        let spend_key = SpendKey::from(seed.clone());
183
184        // Create signing key and verification key for this node.
185        let validator_id_sk = spend_key.spend_auth_key();
186        let validator_id_vk = VerificationKey::from(validator_id_sk);
187
188        let validator_cons_sk = ed25519_consensus::SigningKey::new(OsRng);
189
190        // generate consensus key for tendermint.
191        let validator_cons_sk = tendermint::PrivateKey::Ed25519(
192            validator_cons_sk
193                .as_bytes()
194                .as_slice()
195                .try_into()
196                .expect("32 bytes"),
197        );
198        let validator_cons_pk = validator_cons_sk.public_key();
199
200        // generate P2P auth key for tendermint.
201        let node_key_sk = ed25519_consensus::SigningKey::from(seed.0);
202        let signing_key_bytes = node_key_sk.as_bytes().as_slice();
203
204        // generate consensus key for tendermint.
205        let node_key_sk =
206            tendermint::PrivateKey::Ed25519(signing_key_bytes.try_into().expect("32 bytes"));
207        let node_key_pk = node_key_sk.public_key();
208
209        ValidatorKeys {
210            validator_id_sk: validator_id_sk.clone(),
211            validator_id_vk,
212            validator_cons_sk,
213            validator_cons_pk,
214            node_key_sk,
215            node_key_pk,
216            validator_spend_key: seed,
217        }
218    }
219
220    pub fn generate() -> Self {
221        // Create the spend key for this node.
222        // TODO: change to use seed phrase
223        let seed = SpendKeyBytes(OsRng.gen());
224        let spend_key = SpendKey::from(seed.clone());
225
226        // Create signing key and verification key for this node.
227        let validator_id_sk = spend_key.spend_auth_key();
228        let validator_id_vk = VerificationKey::from(validator_id_sk);
229
230        let validator_cons_sk = ed25519_consensus::SigningKey::new(OsRng);
231
232        // generate consensus key for tendermint.
233        let validator_cons_sk = tendermint::PrivateKey::Ed25519(
234            validator_cons_sk
235                .as_bytes()
236                .as_slice()
237                .try_into()
238                .expect("32 bytes"),
239        );
240        let validator_cons_pk = validator_cons_sk.public_key();
241
242        // generate P2P auth key for tendermint.
243        let node_key_sk = ed25519_consensus::SigningKey::from(seed.0);
244        let signing_key_bytes = node_key_sk.as_bytes().as_slice();
245
246        // generate consensus key for tendermint.
247        let node_key_sk =
248            tendermint::PrivateKey::Ed25519(signing_key_bytes.try_into().expect("32 bytes"));
249        let node_key_pk = node_key_sk.public_key();
250
251        ValidatorKeys {
252            validator_id_sk: validator_id_sk.clone(),
253            validator_id_vk,
254            validator_cons_sk,
255            validator_cons_pk,
256            node_key_sk,
257            node_key_pk,
258            validator_spend_key: seed,
259        }
260    }
261    /// Format the p2p consensus keypair into a struct suitable for serialization
262    /// directly as `priv_validator_key.json` for Tendermint config.
263    pub fn priv_validator_key(&self) -> anyhow::Result<PrivValidatorKey> {
264        let address: tendermint::account::Id = self.validator_cons_pk.into();
265        let priv_key = tendermint::PrivateKey::Ed25519(
266            self.validator_cons_sk
267                .ed25519_signing_key()
268                .ok_or_else(|| {
269                    anyhow::anyhow!("Failed during loop of signing key for NetworkValidator")
270                })?
271                .clone(),
272        );
273        let priv_validator_key = PrivValidatorKey {
274            address,
275            pub_key: self.validator_cons_pk,
276            priv_key,
277        };
278        Ok(priv_validator_key)
279    }
280}
281
282impl Default for ValidatorKeys {
283    fn default() -> Self {
284        Self::generate()
285    }
286}
287
288#[derive(Deserialize)]
289pub struct TendermintNodeKey {
290    pub id: String,
291    pub priv_key: TendermintPrivKey,
292}
293
294#[derive(Deserialize)]
295pub struct TendermintPrivKey {
296    #[serde(rename(serialize = "type"))]
297    pub key_type: String,
298    pub value: PrivateKey,
299}
300
301/// Expand tildes in a path.
302/// Modified from `<https://stackoverflow.com/a/68233480>`
303pub fn canonicalize_path(input: &str) -> PathBuf {
304    let tilde = Regex::new(r"^~(/|$)").expect("tilde regex is valid");
305    if input.starts_with('/') {
306        // if the input starts with a `/`, we use it as is
307        input.into()
308    } else if tilde.is_match(input) {
309        // if the input starts with `~` as first token, we replace
310        // this `~` with the user home directory
311        PathBuf::from(&*tilde.replace(input, |c: &Captures| {
312            if let Some(user_dirs) = UserDirs::new() {
313                format!("{}{}", user_dirs.home_dir().to_string_lossy(), &c[1],)
314            } else {
315                c[0].to_string()
316            }
317        }))
318    } else {
319        PathBuf::from(format!(
320            "{}/{}",
321            current_dir()
322                .expect("current working dir is valid")
323                .display(),
324            input
325        ))
326    }
327}
328
329/// Convert an optional CLI arg into a [`PathBuf`], defaulting to
330/// `~/.penumbra/network_data`.
331pub fn get_network_dir(network_dir: Option<PathBuf>) -> PathBuf {
332    // By default output directory will be in `~/.penumbra/network_data/`
333    match network_dir {
334        Some(o) => o,
335        None => canonicalize_path("~/.penumbra/network_data"),
336    }
337}
338
339/// Check that a [Url] has all the necessary parts defined for use as a CLI arg.
340pub fn url_has_necessary_parts(url: &Url) -> bool {
341    url.scheme() != "" && url.has_host() && url.port().is_some()
342}