use anyhow::{bail, ensure, format_err, Error};
use exonum::crypto::PublicKey;
use exonum_node::{ConnectInfo, ConnectListConfig, NodeApiConfig};
use serde_derive::{Deserialize, Serialize};
use structopt::StructOpt;
use std::{collections::BTreeMap, net::SocketAddr, path::PathBuf};
use crate::{
command::{ExonumCommand, StandardResult},
config::{NodeConfig, NodePrivateConfig, NodePublicConfig},
io::{load_config_file, save_config_file},
};
#[derive(StructOpt, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Finalize {
pub private_config_path: PathBuf,
pub output_config_path: PathBuf,
#[structopt(long, short = "p")]
pub public_configs: Vec<PathBuf>,
#[structopt(long)]
pub public_api_address: Option<SocketAddr>,
#[structopt(long)]
pub private_api_address: Option<SocketAddr>,
#[structopt(long)]
pub public_allow_origin: Option<String>,
#[structopt(long)]
pub private_allow_origin: Option<String>,
}
struct ValidatedConfigs {
common: NodePublicConfig,
public_configs: Vec<NodePublicConfig>,
}
impl Finalize {
fn validate_configs(configs: Vec<NodePublicConfig>) -> Result<ValidatedConfigs, Error> {
let mut config_iter = configs.into_iter();
let mut public_configs = BTreeMap::new();
let first = config_iter
.next()
.ok_or_else(|| format_err!("Expected at least one config in <public-configs>"))?;
let consensus_key = Self::get_consensus_key(&first)?;
public_configs.insert(consensus_key, first.clone());
for config in config_iter {
ensure!(
first.consensus == config.consensus,
"Found public configs with different consensus configuration.\
Make sure the same template config was used for generation.\
{:#?} \nnot equal to\n {:#?}",
first.consensus,
config.consensus
);
ensure!(
first.general == config.general,
"Found public configs with different general configuration.\
Make sure the same template config was used for generation.\
{:#?} \nnot equal to\n {:#?}",
first.general,
config.general
);
let consensus_key = Self::get_consensus_key(&config)?;
if public_configs.insert(consensus_key, config).is_some() {
bail!(
"Found duplicated consensus keys in <public-configs>: {:?}",
consensus_key
);
}
}
Ok(ValidatedConfigs {
common: first,
public_configs: public_configs.values().cloned().collect(),
})
}
fn get_consensus_key(config: &NodePublicConfig) -> anyhow::Result<PublicKey> {
Ok(config
.validator_keys
.ok_or_else(|| format_err!("Expected validator keys in public config: {:#?}", config))?
.consensus_key)
}
fn create_connect_list_config(
public_configs: &[NodePublicConfig],
key_to_skip: &PublicKey,
) -> ConnectListConfig {
let peers = public_configs
.iter()
.filter_map(|config| {
let public_key = Self::get_consensus_key(config).unwrap();
if public_key == *key_to_skip {
None
} else {
Some(ConnectInfo {
public_key,
address: config.address.clone().unwrap(),
})
}
})
.collect();
ConnectListConfig { peers }
}
}
impl ExonumCommand for Finalize {
fn execute(self) -> Result<StandardResult, Error> {
let private_config: NodePrivateConfig = load_config_file(&self.private_config_path)?;
let public_configs: Vec<NodePublicConfig> = self
.public_configs
.into_iter()
.map(load_config_file)
.collect::<Result<_, _>>()?;
let public_allow_origin = self.public_allow_origin.map(|s| s.parse().unwrap());
let private_allow_origin = self.private_allow_origin.map(|s| s.parse().unwrap());
let ValidatedConfigs {
common,
public_configs,
} = Self::validate_configs(public_configs)?;
let validators_count = common.general.validators_count as usize;
ensure!(
validators_count == public_configs.len(),
"The number of validators ({}) does not match the number of validators keys ({}).",
validators_count,
public_configs.len()
);
let validator_keys = public_configs
.iter()
.flat_map(|public_config| public_config.validator_keys)
.collect();
let consensus = common.consensus.with_validator_keys(validator_keys);
let connect_list =
Self::create_connect_list_config(&public_configs, &private_config.consensus_public_key);
let private_config = NodePrivateConfig {
api: NodeApiConfig {
public_api_address: self.public_api_address,
private_api_address: self.private_api_address,
public_allow_origin,
private_allow_origin,
..private_config.api
},
connect_list,
..private_config
};
let public_config = NodePublicConfig {
consensus,
general: common.general,
validator_keys: None,
address: None,
};
let config = NodeConfig {
private_config,
public_config,
};
save_config_file(&config, &self.output_config_path)?;
Ok(StandardResult::Finalize {
node_config_path: self.output_config_path,
})
}
}