chain-spec-generator 13.0.1-beta.196

Generates a chainspec artifact for use in genesis block creation of a Xand network.
Documentation
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,
}

/// Filename of chain spec template stored in ZIP files downloaded
const CHAIN_SPEC_SOURCE_JSON_FILENAME: &str = "chainSpecSource.json";

/// Filename the validator expects to be present in some folder when booting up
const CHAIN_SPEC_FILENAME_CONVENTION: &str = "chainSpecModified.json";

/// Initial Chain Spec settings (refer to the thermite repo for more context)
const BLOCKS_UNTIL_AUTO_EXIT: usize = 30 * 24 * 60 * 60 / 6; // secs / (secs/block)
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<()> {
        // Unzip into working_dir
        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(())
    }

    /// Generates a chain spec from the template zip published by thermite and the
    /// context available to `self`, which currently comes from a YAML file.
    ///
    /// The target directory must already exist.
    ///
    /// Returns path to generated chain spec.
    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 {
    // The Chain Spec requires the cidr block list to contain 10 elements
    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)
        // Remaining elements in the list are padded with null
        .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);
    }
}