use anyhow::Context;
use decaf377_rdsa::{SigningKey, SpendAuth, VerificationKey};
use directories::UserDirs;
use penumbra_app::genesis::AppState;
use penumbra_custody::soft_kms::Config as SoftKmsConfig;
use penumbra_keys::keys::{SpendKey, SpendKeyBytes};
use rand::Rng;
use rand_core::OsRng;
use regex::{Captures, Regex};
use serde::Deserialize;
use std::{
env::current_dir,
fs::{self, File},
io::Write,
net::SocketAddr,
path::PathBuf,
str::FromStr,
};
use tendermint::{node::Id, Genesis, Moniker, PrivateKey};
use tendermint_config::{
net::Address as TendermintAddress, NodeKey, PrivValidatorKey, TendermintConfig,
};
use url::Url;
use crate::network::generate::NetworkValidator;
pub struct NetworkTendermintConfig(pub TendermintConfig);
impl NetworkTendermintConfig {
pub fn new(
node_name: &str,
peers: Vec<TendermintAddress>,
external_address: Option<TendermintAddress>,
tm_rpc_bind: Option<SocketAddr>,
tm_p2p_bind: Option<SocketAddr>,
) -> anyhow::Result<Self> {
tracing::debug!("List of CometBFT peers: {:?}", peers);
let moniker: Moniker = Moniker::from_str(node_name)?;
let mut tm_config = TendermintConfig::parse_toml(include_str!(
"../../../../../testnets/cometbft_config_template.toml"
))
.context("Failed to parse the TOML config template for CometBFT")?;
tm_config.moniker = moniker;
tm_config.p2p.seeds = peers;
tracing::debug!("External address looks like: {:?}", external_address);
tm_config.p2p.external_address = external_address;
if let Some(rpc) = tm_rpc_bind {
tm_config.rpc.laddr =
parse_tm_address(None, &Url::parse(format!("tcp://{}", rpc).as_str())?)?;
}
if let Some(p2p) = tm_p2p_bind {
tm_config.p2p.laddr =
parse_tm_address(None, &Url::parse(format!("tcp://{}", p2p).as_str())?)?;
}
Ok(Self(tm_config))
}
}
impl NetworkTendermintConfig {
pub fn write_config(
&self,
node_dir: PathBuf,
v: &NetworkValidator,
genesis: &Genesis<AppState>,
) -> anyhow::Result<()> {
let pd_dir = node_dir.clone().join("pd");
let cb_data_dir = node_dir.clone().join("cometbft").join("data");
let cb_config_dir = node_dir.clone().join("cometbft").join("config");
tracing::info!(config_dir = %node_dir.display(), "Writing validator configs to");
fs::create_dir_all(pd_dir)?;
fs::create_dir_all(&cb_data_dir)?;
fs::create_dir_all(&cb_config_dir)?;
let genesis_file_path = cb_config_dir.clone().join("genesis.json");
tracing::debug!(genesis_file_path = %genesis_file_path.display(), "writing genesis");
let mut genesis_file = File::create(genesis_file_path)?;
genesis_file.write_all(serde_json::to_string_pretty(&genesis)?.as_bytes())?;
let cb_config_filepath = cb_config_dir.clone().join("config.toml");
tracing::debug!(cometbft_config = %cb_config_filepath.display(), "writing cometbft config.toml");
let mut cb_config_file = File::create(cb_config_filepath)?;
cb_config_file.write_all(toml::to_string(&self.0)?.as_bytes())?;
let priv_key = tendermint::PrivateKey::Ed25519(
v.keys
.node_key_sk
.ed25519_signing_key()
.expect("node key has ed25519 signing key")
.clone(),
);
let node_key = NodeKey { priv_key };
let cb_node_key_filepath = cb_config_dir.clone().join("node_key.json");
tracing::debug!(cb_node_key_filepath = %cb_node_key_filepath.display(), "writing node key file");
let mut cb_node_key_file = File::create(cb_node_key_filepath)?;
cb_node_key_file.write_all(serde_json::to_string_pretty(&node_key)?.as_bytes())?;
let priv_validator_key_filepath = cb_config_dir.clone().join("priv_validator_key.json");
tracing::debug!(priv_validator_key_filepath = %priv_validator_key_filepath.display(), "writing validator private key");
let mut priv_validator_key_file = File::create(priv_validator_key_filepath)?;
let priv_validator_key: PrivValidatorKey = v.keys.priv_validator_key()?;
priv_validator_key_file
.write_all(serde_json::to_string_pretty(&priv_validator_key)?.as_bytes())?;
let priv_validator_state_filepath = cb_data_dir.clone().join("priv_validator_state.json");
tracing::debug!(priv_validator_state_filepath = %priv_validator_state_filepath.display(), "writing validator state");
let mut priv_validator_state_file = File::create(priv_validator_state_filepath)?;
priv_validator_state_file.write_all(NetworkValidator::initial_state().as_bytes())?;
let validator_spend_key_filepath = cb_config_dir.clone().join("validator_custody.json");
tracing::debug!(validator_spend_key_filepath = %validator_spend_key_filepath.display(), "writing validator custody file");
let mut validator_spend_key_file = File::create(validator_spend_key_filepath)?;
let validator_wallet = SoftKmsConfig::from(
SpendKey::try_from(v.keys.validator_spend_key.clone())
.expect("spend key should be valid"),
);
validator_spend_key_file
.write_all(toml::to_string_pretty(&validator_wallet)?.as_bytes())?;
Ok(())
}
}
pub fn parse_tm_address(
node_id: Option<&Id>,
node_address: &Url,
) -> anyhow::Result<TendermintAddress> {
let hostname = match node_address.host() {
Some(h) => h,
None => {
anyhow::bail!(format!("Could not find hostname in URL: {}", node_address))
}
};
let port = node_address.port().unwrap_or(26656);
match node_id {
Some(id) => Ok(format!("{id}@{hostname}:{port}").parse()?),
None => Ok(format!("{hostname}:{port}").parse()?),
}
}
#[derive(Deserialize)]
pub struct ValidatorKeys {
pub validator_id_sk: SigningKey<SpendAuth>,
pub validator_id_vk: VerificationKey<SpendAuth>,
pub validator_spend_key: SpendKeyBytes,
pub validator_cons_sk: tendermint::PrivateKey,
pub validator_cons_pk: tendermint::PublicKey,
pub node_key_sk: tendermint::PrivateKey,
#[allow(unused_variables, dead_code)]
pub node_key_pk: tendermint::PublicKey,
}
impl ValidatorKeys {
pub fn from_seed(seed: [u8; 32]) -> Self {
let seed = SpendKeyBytes(seed);
let spend_key = SpendKey::from(seed.clone());
let validator_id_sk = spend_key.spend_auth_key();
let validator_id_vk = VerificationKey::from(validator_id_sk);
let validator_cons_sk = ed25519_consensus::SigningKey::new(OsRng);
let validator_cons_sk = tendermint::PrivateKey::Ed25519(
validator_cons_sk
.as_bytes()
.as_slice()
.try_into()
.expect("32 bytes"),
);
let validator_cons_pk = validator_cons_sk.public_key();
let node_key_sk = ed25519_consensus::SigningKey::from(seed.0);
let signing_key_bytes = node_key_sk.as_bytes().as_slice();
let node_key_sk =
tendermint::PrivateKey::Ed25519(signing_key_bytes.try_into().expect("32 bytes"));
let node_key_pk = node_key_sk.public_key();
ValidatorKeys {
validator_id_sk: validator_id_sk.clone(),
validator_id_vk,
validator_cons_sk,
validator_cons_pk,
node_key_sk,
node_key_pk,
validator_spend_key: seed,
}
}
pub fn generate() -> Self {
let seed = SpendKeyBytes(OsRng.gen());
let spend_key = SpendKey::from(seed.clone());
let validator_id_sk = spend_key.spend_auth_key();
let validator_id_vk = VerificationKey::from(validator_id_sk);
let validator_cons_sk = ed25519_consensus::SigningKey::new(OsRng);
let validator_cons_sk = tendermint::PrivateKey::Ed25519(
validator_cons_sk
.as_bytes()
.as_slice()
.try_into()
.expect("32 bytes"),
);
let validator_cons_pk = validator_cons_sk.public_key();
let node_key_sk = ed25519_consensus::SigningKey::from(seed.0);
let signing_key_bytes = node_key_sk.as_bytes().as_slice();
let node_key_sk =
tendermint::PrivateKey::Ed25519(signing_key_bytes.try_into().expect("32 bytes"));
let node_key_pk = node_key_sk.public_key();
ValidatorKeys {
validator_id_sk: validator_id_sk.clone(),
validator_id_vk,
validator_cons_sk,
validator_cons_pk,
node_key_sk,
node_key_pk,
validator_spend_key: seed,
}
}
pub fn priv_validator_key(&self) -> anyhow::Result<PrivValidatorKey> {
let address: tendermint::account::Id = self.validator_cons_pk.into();
let priv_key = tendermint::PrivateKey::Ed25519(
self.validator_cons_sk
.ed25519_signing_key()
.ok_or_else(|| {
anyhow::anyhow!("Failed during loop of signing key for NetworkValidator")
})?
.clone(),
);
let priv_validator_key = PrivValidatorKey {
address,
pub_key: self.validator_cons_pk,
priv_key,
};
Ok(priv_validator_key)
}
}
impl Default for ValidatorKeys {
fn default() -> Self {
Self::generate()
}
}
#[derive(Deserialize)]
pub struct TendermintNodeKey {
pub id: String,
pub priv_key: TendermintPrivKey,
}
#[derive(Deserialize)]
pub struct TendermintPrivKey {
#[serde(rename(serialize = "type"))]
pub key_type: String,
pub value: PrivateKey,
}
pub fn canonicalize_path(input: &str) -> PathBuf {
let tilde = Regex::new(r"^~(/|$)").expect("tilde regex is valid");
if input.starts_with('/') {
input.into()
} else if tilde.is_match(input) {
PathBuf::from(&*tilde.replace(input, |c: &Captures| {
if let Some(user_dirs) = UserDirs::new() {
format!("{}{}", user_dirs.home_dir().to_string_lossy(), &c[1],)
} else {
c[0].to_string()
}
}))
} else {
PathBuf::from(format!(
"{}/{}",
current_dir()
.expect("current working dir is valid")
.display(),
input
))
}
}
pub fn get_network_dir(network_dir: Option<PathBuf>) -> PathBuf {
match network_dir {
Some(o) => o,
None => canonicalize_path("~/.penumbra/network_data"),
}
}
pub fn url_has_necessary_parts(url: &Url) -> bool {
url.scheme() != "" && url.has_host() && url.port().is_some()
}