chain_spec_generator/
generation.rs

1mod confidentiality_values;
2pub(crate) mod json;
3
4use crate::generation::confidentiality_values::{
5    PENDING_CREATE_EXPIRE_TIME, USED_CORRELATION_ID_EXPIRE_TIME,
6};
7use crate::public_models::domain::egress_ip_range::EgressIpRange;
8use crate::{
9    error::Error, output::shell_out, EmissionRate, LimitedAgentIdentity, MemberIdentity,
10    TrustIdentity, ValidatorIdentity,
11};
12use json::{JsonMap, JsonTryAsObjectMut, JsonTryGetMut, JsonValue};
13use serde_json::json;
14use std::path::{Path, PathBuf};
15
16#[derive(Debug)]
17pub struct ChainSpec {
18    pub trust: TrustIdentity,
19    pub limited_agent: LimitedAgentIdentity,
20    pub members: Vec<MemberIdentity>,
21    pub validators: Vec<ValidatorIdentity>,
22    pub validator_emission_rate: EmissionRate,
23}
24
25/// Filename of chain spec template stored in ZIP files downloaded
26const CHAIN_SPEC_SOURCE_JSON_FILENAME: &str = "chainSpecSource.json";
27
28/// Filename the validator expects to be present in some folder when booting up
29const CHAIN_SPEC_FILENAME_CONVENTION: &str = "chainSpecModified.json";
30
31/// Initial Chain Spec settings (refer to the thermite repo for more context)
32const BLOCKS_UNTIL_AUTO_EXIT: usize = 30 * 24 * 60 * 60 / 6; // secs / (secs/block)
33const TOTAL_CREATED: usize = 0;
34const TOTAL_REDEEMED: usize = 0;
35const EPOCH_DURATION: usize = 100;
36
37impl ChainSpec {
38    fn unzip_chainspec_template(
39        chain_spec_template_zip: &Path,
40        workdir: &Path,
41    ) -> crate::Result<()> {
42        // Unzip into working_dir
43        let cmd = format!(
44            "unzip -o {} -d {}",
45            chain_spec_template_zip
46                .to_str()
47                .ok_or_else(|| Error::OsStringIsNotValidString(chain_spec_template_zip.into()))?,
48            &workdir
49                .to_str()
50                .ok_or_else(|| Error::OsStringIsNotValidString(workdir.into()))?
51        );
52        let _res = shell_out(&cmd)?;
53
54        let chainspec_source_path = Path::join(workdir, CHAIN_SPEC_SOURCE_JSON_FILENAME);
55        if !std::path::Path::exists(&chainspec_source_path) {
56            return Err(Error::ChainSpecSourceFileMissing(chainspec_source_path));
57        }
58
59        Ok(())
60    }
61
62    /// Generates a chain spec from the template zip published by thermite and the
63    /// context available to `self`, which currently comes from a YAML file.
64    ///
65    /// The target directory must already exist.
66    ///
67    /// Returns path to generated chain spec.
68    pub fn generate_into<P: AsRef<Path> + std::fmt::Debug>(
69        &self,
70        chain_spec_template_zip: P,
71        target_dir: P,
72    ) -> crate::Result<PathBuf> {
73        let workdir = std::fs::canonicalize(&target_dir).map_err(|e| {
74            Error::IoError(
75                e,
76                format!("Failed to canonicalize working dir path {:?}", target_dir),
77            )
78        })?;
79
80        let chain_spec_template_zip =
81            std::fs::canonicalize(&chain_spec_template_zip).map_err(|e| {
82                Error::IoError(
83                    e,
84                    format!(
85                        "Failed to canonicalize chain spec template path {:?}",
86                        chain_spec_template_zip
87                    ),
88                )
89            })?;
90        Self::unzip_chainspec_template(&chain_spec_template_zip, &workdir)?;
91
92        let chainspec_source_path = Path::join(&workdir, CHAIN_SPEC_SOURCE_JSON_FILENAME);
93        let outfile_path = Path::join(&workdir, CHAIN_SPEC_FILENAME_CONVENTION);
94
95        self.fill_chain_spec(&chainspec_source_path, &outfile_path)?;
96        Ok(outfile_path)
97    }
98
99    fn fill_chain_spec(
100        &self,
101        chainspec_source_path: &Path,
102        outfile_path: &Path,
103    ) -> crate::Result<()> {
104        let template: JsonValue =
105            serde_json::from_reader(std::fs::File::open(chainspec_source_path).unwrap()).unwrap();
106        let output = self.patch_template(template)?;
107
108        std::fs::write(outfile_path, output.to_string()).map_err(|e| {
109            Error::IoError(
110                e,
111                format!("Failed to write chain spec to {:?}", outfile_path),
112            )
113        })
114    }
115
116    fn patch_template(&self, template: JsonValue) -> crate::Result<JsonValue> {
117        let mut output = template;
118        self.set_boot_nodes(&mut output)?;
119        self.set_runtime(&mut output)?;
120        Ok(output)
121    }
122
123    fn set_boot_nodes(&self, output: &mut JsonValue) -> crate::Result<()> {
124        let boot_nodes = output.try_get_mut("bootNodes")?;
125        *boot_nodes = json!([]);
126        Ok(())
127    }
128
129    fn set_runtime(&self, output: &mut JsonValue) -> crate::Result<()> {
130        let runtime = output
131            .try_get_mut("genesis")?
132            .try_get_mut("runtime")?
133            .try_as_object_mut()?;
134
135        self.insert_xandstrate_values(runtime);
136        self.insert_xandvalidators(runtime);
137        self.set_pallet_session_keys(runtime)?;
138        self.set_confidentiality(runtime)?;
139        self.set_validator_emissions(runtime)?;
140
141        Ok(())
142    }
143
144    fn insert_xandstrate_values(&self, runtime: &mut JsonMap) {
145        let encryption_keys = json!(self
146            .members
147            .iter()
148            .map(|m| json!([&m.address, &m.pub_key]))
149            .chain(
150                self.validators
151                    .iter()
152                    .map(|v| json!([&v.key_gen_summary.val_kp_pub, &v.key_gen_summary.pub_key]))
153            )
154            .chain(std::iter::once(json!([
155                &self.trust.address,
156                &self.trust.pub_key
157            ])))
158            .collect::<Vec<_>>());
159        let registered_members = json!(self
160            .members
161            .iter()
162            .map(|m| json!([m.address, true]))
163            .collect::<Vec<_>>());
164
165        let allow_listed_cidr_blocks = json!(self
166            .members
167            .iter()
168            .map(|m| format_cidr_blocks(&m.address.to_string(), &m.cidr_blocks))
169            .chain(self.validators.iter().map(|v| {
170                format_cidr_blocks(&v.key_gen_summary.val_kp_pub.to_string(), &v.cidr_blocks)
171            }))
172            .chain(std::iter::once(format_cidr_blocks(
173                &self.trust.address.to_string(),
174                &self.trust.cidr_blocks,
175            )))
176            .chain(std::iter::once(format_cidr_blocks(
177                &self.limited_agent.address.to_string(),
178                &self.limited_agent.cidr_blocks,
179            )))
180            .collect::<Vec<_>>());
181
182        let values = json!({
183            "bannedMembers": [],
184            "blocksUntilAutoExit": BLOCKS_UNTIL_AUTO_EXIT,
185            "limitedAgentId": self.limited_agent.address,
186            "nodeEncryptionKey": encryption_keys,
187            "registeredMembers": registered_members,
188            "trustNodeId": self.trust.address,
189            "allowListedCidrBlocks": allow_listed_cidr_blocks
190        });
191
192        runtime.insert("xandstrate".to_string(), values);
193    }
194
195    fn insert_xandvalidators(&self, runtime: &mut JsonMap) {
196        runtime.insert("xandvalidators".to_string(), json!({
197            "epochDuration": EPOCH_DURATION,
198            "validators": self.validators.iter().map(|v| v.key_gen_summary.val_kp_pub.to_string()).collect::<Vec<_>>()
199        }));
200    }
201
202    fn set_pallet_session_keys(&self, runtime: &mut JsonMap) -> crate::Result<()> {
203        let keys = runtime.try_get_mut("palletSession")?.try_get_mut("keys")?;
204
205        *keys = json!(self
206            .validators
207            .iter()
208            .map(|v| {
209                let keys = &v.key_gen_summary;
210                let authority = &keys.val_kp_pub;
211
212                json!([
213                    authority.to_string(),
214                    authority.to_string(),
215                    {
216                        "aura": &keys.produce_blocks_kp_pub.to_string(),
217                        "grandpa": &keys.finalize_blocks_kp_pub.to_string(),
218                    }
219                ])
220            })
221            .collect::<Vec<JsonValue>>());
222
223        Ok(())
224    }
225
226    fn set_confidentiality(&self, runtime: &mut JsonMap) -> crate::Result<()> {
227        let confidentiality = runtime.try_get_mut("confidentiality")?;
228        *confidentiality = json!({
229            "totalCreated": TOTAL_CREATED,
230            "totalRedeemed": TOTAL_REDEEMED,
231            "pendingCreateExpireTime": PENDING_CREATE_EXPIRE_TIME,
232            "usedKeyImages": [],
233            "txos": [],
234            "identityTags": [],
235            "pendingCreates": [],
236            "pendingRedeems": [],
237            "clearUtxos": [],
238            "usedCorrelationIdExpireTime": USED_CORRELATION_ID_EXPIRE_TIME,
239        });
240        Ok(())
241    }
242
243    fn set_validator_emissions(&self, runtime: &mut JsonMap) -> crate::Result<()> {
244        let emissions = runtime.try_get_mut("validatorEmissions")?;
245        *emissions = json!({
246            "emissionRateSetting": {
247                "minor_units_per_emission": self.validator_emission_rate.minor_units_per_validator_emission,
248                "block_quota": self.validator_emission_rate.block_quota,
249            }
250        });
251        Ok(())
252    }
253}
254
255fn format_cidr_blocks(address: &str, cidr_blocks: &[EgressIpRange]) -> JsonValue {
256    // The Chain Spec requires the cidr block list to contain 10 elements
257    const MAX_CIDR_BLOCKS: usize = 10;
258
259    if cidr_blocks.len() > MAX_CIDR_BLOCKS {
260        panic!(
261            "Chainspec only supports allowlisting up to {} CIDR blocks per network participant",
262            MAX_CIDR_BLOCKS
263        );
264    }
265
266    let cidr_arrays = cidr_blocks
267        .iter()
268        .map(EgressIpRange::as_allowlist_json)
269        // Remaining elements in the list are padded with null
270        .chain(std::iter::repeat(JsonValue::Null))
271        .take(MAX_CIDR_BLOCKS)
272        .collect::<Vec<_>>();
273
274    json!([address, cidr_arrays])
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use crate::cli::commands::generate::config::{ConfigEmissionRate, ConfigurationFile};
281    use std::fs;
282    use std::num::NonZeroU64;
283    use temp_dir::TempDir;
284
285    use insta::assert_json_snapshot;
286
287    #[derive(Default)]
288    struct TestScenario {
289        pub emssion_rate: ConfigEmissionRate,
290    }
291
292    impl TestScenario {
293        pub fn with_validator_emissions(
294            minor_units_per_validator_emission: u64,
295            block_quota: NonZeroU64,
296        ) -> Self {
297            Self {
298                emssion_rate: ConfigEmissionRate {
299                    minor_units_per_validator_emission,
300                    block_quota,
301                },
302            }
303        }
304    }
305
306    fn chainspec_from_sample_config(test_scenario: Option<&TestScenario>) -> ChainSpec {
307        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("sample_config.yaml");
308        let file = std::fs::File::open(path).unwrap();
309
310        let config = if let Some(test_scenario) = test_scenario {
311            ConfigurationFile {
312                validator_emission_rate: test_scenario.emssion_rate.clone(),
313                ..serde_yaml::from_reader(file).unwrap()
314            }
315        } else {
316            ConfigurationFile {
317                ..serde_yaml::from_reader(file).unwrap()
318            }
319        };
320
321        config.build_chain_spec_data().unwrap()
322    }
323
324    fn snapshot_generated_output(test_scenario: Option<&TestScenario>) {
325        let chainspec = chainspec_from_sample_config(test_scenario);
326
327        let version = "16.0.0";
328        let template_path: PathBuf = Path::new(&format!(
329            "{}/.test-assets/chain-spec-template.{}.zip",
330            env!("CARGO_MANIFEST_DIR"),
331            version
332        ))
333        .into();
334        let tempdir = TempDir::new().unwrap();
335        let temp_target = tempdir.path().into();
336        let template_path = chainspec.generate_into(template_path, temp_target).unwrap();
337        let template = fs::read_to_string(template_path).unwrap();
338        let as_json: JsonValue = serde_json::from_str(&template).unwrap();
339        assert_json_snapshot!(as_json, {".genesis.runtime.system.code" => "[blob]"});
340    }
341
342    #[test]
343    fn generate_with_validator_emissions_disabled() {
344        let zero_emission_scenario =
345            TestScenario::with_validator_emissions(0, 1.try_into().unwrap());
346        snapshot_generated_output(Some(&zero_emission_scenario));
347    }
348
349    #[test]
350    fn generate_with_validator_emissions_enabled() {
351        snapshot_generated_output(None);
352    }
353}