mod confidentiality_values;
pub(crate) mod json;
use crate::generation::confidentiality_values::{
PENDING_CREATE_EXPIRE_TIME, USED_CORRELATION_ID_EXPIRE_TIME,
};
use crate::public_models::domain::egress_ip_range::EgressIpRange;
use crate::{
error::Error, output::shell_out, EmissionRate, LimitedAgentIdentity, MemberIdentity,
TrustIdentity, ValidatorIdentity,
};
use json::{JsonMap, JsonTryAsObjectMut, JsonTryGetMut, JsonValue};
use serde_json::json;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct ChainSpec {
pub trust: TrustIdentity,
pub limited_agent: LimitedAgentIdentity,
pub members: Vec<MemberIdentity>,
pub validators: Vec<ValidatorIdentity>,
pub validator_emission_rate: EmissionRate,
}
const CHAIN_SPEC_SOURCE_JSON_FILENAME: &str = "chainSpecSource.json";
const CHAIN_SPEC_FILENAME_CONVENTION: &str = "chainSpecModified.json";
const BLOCKS_UNTIL_AUTO_EXIT: usize = 30 * 24 * 60 * 60 / 6; const TOTAL_CREATED: usize = 0;
const TOTAL_REDEEMED: usize = 0;
const EPOCH_DURATION: usize = 100;
impl ChainSpec {
fn unzip_chainspec_template(
chain_spec_template_zip: &Path,
workdir: &Path,
) -> crate::Result<()> {
let cmd = format!(
"unzip -o {} -d {}",
chain_spec_template_zip
.to_str()
.ok_or_else(|| Error::OsStringIsNotValidString(chain_spec_template_zip.into()))?,
&workdir
.to_str()
.ok_or_else(|| Error::OsStringIsNotValidString(workdir.into()))?
);
let _res = shell_out(&cmd)?;
let chainspec_source_path = Path::join(workdir, CHAIN_SPEC_SOURCE_JSON_FILENAME);
if !std::path::Path::exists(&chainspec_source_path) {
return Err(Error::ChainSpecSourceFileMissing(chainspec_source_path));
}
Ok(())
}
pub fn generate_into<P: AsRef<Path> + std::fmt::Debug>(
&self,
chain_spec_template_zip: P,
target_dir: P,
) -> crate::Result<PathBuf> {
let workdir = std::fs::canonicalize(&target_dir).map_err(|e| {
Error::IoError(
e,
format!("Failed to canonicalize working dir path {:?}", target_dir),
)
})?;
let chain_spec_template_zip =
std::fs::canonicalize(&chain_spec_template_zip).map_err(|e| {
Error::IoError(
e,
format!(
"Failed to canonicalize chain spec template path {:?}",
chain_spec_template_zip
),
)
})?;
Self::unzip_chainspec_template(&chain_spec_template_zip, &workdir)?;
let chainspec_source_path = Path::join(&workdir, CHAIN_SPEC_SOURCE_JSON_FILENAME);
let outfile_path = Path::join(&workdir, CHAIN_SPEC_FILENAME_CONVENTION);
self.fill_chain_spec(&chainspec_source_path, &outfile_path)?;
Ok(outfile_path)
}
fn fill_chain_spec(
&self,
chainspec_source_path: &Path,
outfile_path: &Path,
) -> crate::Result<()> {
let template: JsonValue =
serde_json::from_reader(std::fs::File::open(chainspec_source_path).unwrap()).unwrap();
let output = self.patch_template(template)?;
std::fs::write(outfile_path, output.to_string()).map_err(|e| {
Error::IoError(
e,
format!("Failed to write chain spec to {:?}", outfile_path),
)
})
}
fn patch_template(&self, template: JsonValue) -> crate::Result<JsonValue> {
let mut output = template;
self.set_boot_nodes(&mut output)?;
self.set_runtime(&mut output)?;
Ok(output)
}
fn set_boot_nodes(&self, output: &mut JsonValue) -> crate::Result<()> {
let boot_nodes = output.try_get_mut("bootNodes")?;
*boot_nodes = json!([]);
Ok(())
}
fn set_runtime(&self, output: &mut JsonValue) -> crate::Result<()> {
let runtime = output
.try_get_mut("genesis")?
.try_get_mut("runtime")?
.try_as_object_mut()?;
self.insert_xandstrate_values(runtime);
self.insert_xandvalidators(runtime);
self.set_pallet_session_keys(runtime)?;
self.set_confidentiality(runtime)?;
self.set_validator_emissions(runtime)?;
Ok(())
}
fn insert_xandstrate_values(&self, runtime: &mut JsonMap) {
let encryption_keys = json!(self
.members
.iter()
.map(|m| json!([&m.address, &m.pub_key]))
.chain(
self.validators
.iter()
.map(|v| json!([&v.key_gen_summary.val_kp_pub, &v.key_gen_summary.pub_key]))
)
.chain(std::iter::once(json!([
&self.trust.address,
&self.trust.pub_key
])))
.collect::<Vec<_>>());
let registered_members = json!(self
.members
.iter()
.map(|m| json!([m.address, true]))
.collect::<Vec<_>>());
let allow_listed_cidr_blocks = json!(self
.members
.iter()
.map(|m| format_cidr_blocks(&m.address.to_string(), &m.cidr_blocks))
.chain(self.validators.iter().map(|v| {
format_cidr_blocks(&v.key_gen_summary.val_kp_pub.to_string(), &v.cidr_blocks)
}))
.chain(std::iter::once(format_cidr_blocks(
&self.trust.address.to_string(),
&self.trust.cidr_blocks,
)))
.chain(std::iter::once(format_cidr_blocks(
&self.limited_agent.address.to_string(),
&self.limited_agent.cidr_blocks,
)))
.collect::<Vec<_>>());
let values = json!({
"bannedMembers": [],
"blocksUntilAutoExit": BLOCKS_UNTIL_AUTO_EXIT,
"limitedAgentId": self.limited_agent.address,
"nodeEncryptionKey": encryption_keys,
"registeredMembers": registered_members,
"trustNodeId": self.trust.address,
"allowListedCidrBlocks": allow_listed_cidr_blocks
});
runtime.insert("xandstrate".to_string(), values);
}
fn insert_xandvalidators(&self, runtime: &mut JsonMap) {
runtime.insert("xandvalidators".to_string(), json!({
"epochDuration": EPOCH_DURATION,
"validators": self.validators.iter().map(|v| v.key_gen_summary.val_kp_pub.to_string()).collect::<Vec<_>>()
}));
}
fn set_pallet_session_keys(&self, runtime: &mut JsonMap) -> crate::Result<()> {
let keys = runtime.try_get_mut("palletSession")?.try_get_mut("keys")?;
*keys = json!(self
.validators
.iter()
.map(|v| {
let keys = &v.key_gen_summary;
let authority = &keys.val_kp_pub;
json!([
authority.to_string(),
authority.to_string(),
{
"aura": &keys.produce_blocks_kp_pub.to_string(),
"grandpa": &keys.finalize_blocks_kp_pub.to_string(),
}
])
})
.collect::<Vec<JsonValue>>());
Ok(())
}
fn set_confidentiality(&self, runtime: &mut JsonMap) -> crate::Result<()> {
let confidentiality = runtime.try_get_mut("confidentiality")?;
*confidentiality = json!({
"totalCreated": TOTAL_CREATED,
"totalRedeemed": TOTAL_REDEEMED,
"pendingCreateExpireTime": PENDING_CREATE_EXPIRE_TIME,
"usedKeyImages": [],
"txos": [],
"identityTags": [],
"pendingCreates": [],
"pendingRedeems": [],
"clearUtxos": [],
"usedCorrelationIdExpireTime": USED_CORRELATION_ID_EXPIRE_TIME,
});
Ok(())
}
fn set_validator_emissions(&self, runtime: &mut JsonMap) -> crate::Result<()> {
let emissions = runtime.try_get_mut("validatorEmissions")?;
*emissions = json!({
"emissionRateSetting": {
"minor_units_per_emission": self.validator_emission_rate.minor_units_per_validator_emission,
"block_quota": self.validator_emission_rate.block_quota,
}
});
Ok(())
}
}
fn format_cidr_blocks(address: &str, cidr_blocks: &[EgressIpRange]) -> JsonValue {
const MAX_CIDR_BLOCKS: usize = 10;
if cidr_blocks.len() > MAX_CIDR_BLOCKS {
panic!(
"Chainspec only supports allowlisting up to {} CIDR blocks per network participant",
MAX_CIDR_BLOCKS
);
}
let cidr_arrays = cidr_blocks
.iter()
.map(EgressIpRange::as_allowlist_json)
.chain(std::iter::repeat(JsonValue::Null))
.take(MAX_CIDR_BLOCKS)
.collect::<Vec<_>>();
json!([address, cidr_arrays])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::commands::generate::config::{ConfigEmissionRate, ConfigurationFile};
use std::fs;
use std::num::NonZeroU64;
use temp_dir::TempDir;
use insta::assert_json_snapshot;
#[derive(Default)]
struct TestScenario {
pub emssion_rate: ConfigEmissionRate,
}
impl TestScenario {
pub fn with_validator_emissions(
minor_units_per_validator_emission: u64,
block_quota: NonZeroU64,
) -> Self {
Self {
emssion_rate: ConfigEmissionRate {
minor_units_per_validator_emission,
block_quota,
},
}
}
}
fn chainspec_from_sample_config(test_scenario: Option<&TestScenario>) -> ChainSpec {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("sample_config.yaml");
let file = std::fs::File::open(path).unwrap();
let config = if let Some(test_scenario) = test_scenario {
ConfigurationFile {
validator_emission_rate: test_scenario.emssion_rate.clone(),
..serde_yaml::from_reader(file).unwrap()
}
} else {
ConfigurationFile {
..serde_yaml::from_reader(file).unwrap()
}
};
config.build_chain_spec_data().unwrap()
}
fn snapshot_generated_output(test_scenario: Option<&TestScenario>) {
let chainspec = chainspec_from_sample_config(test_scenario);
let version = "16.0.0";
let template_path: PathBuf = Path::new(&format!(
"{}/.test-assets/chain-spec-template.{}.zip",
env!("CARGO_MANIFEST_DIR"),
version
))
.into();
let tempdir = TempDir::new().unwrap();
let temp_target = tempdir.path().into();
let template_path = chainspec.generate_into(template_path, temp_target).unwrap();
let template = fs::read_to_string(template_path).unwrap();
let as_json: JsonValue = serde_json::from_str(&template).unwrap();
assert_json_snapshot!(as_json, {".genesis.runtime.system.code" => "[blob]"});
}
#[test]
fn generate_with_validator_emissions_disabled() {
let zero_emission_scenario =
TestScenario::with_validator_emissions(0, 1.try_into().unwrap());
snapshot_generated_output(Some(&zero_emission_scenario));
}
#[test]
fn generate_with_validator_emissions_enabled() {
snapshot_generated_output(None);
}
}