partner_chains_cli/create_chain_spec/
mod.rs1use crate::config::ConfigFieldDefinition;
2use crate::io::IOContext;
3use crate::permissioned_candidates::{ParsedPermissionedCandidatesKeys, PermissionedCandidateKeys};
4use crate::runtime_bindings::PartnerChainRuntime;
5use crate::{CmdRun, config::config_fields};
6use anyhow::anyhow;
7use authority_selection_inherents::MaybeFromCandidateKeys;
8use sidechain_domain::{AssetName, MainchainAddress, PolicyId, UtxoId};
9use sp_core::ecdsa;
10use sp_runtime::{AccountId32, DeserializeOwned};
11use std::marker::PhantomData;
12
13#[cfg(test)]
14mod tests;
15
16#[derive(Clone, Debug, Default, clap::Parser)]
17pub struct CreateChainSpecCmd<T: PartnerChainRuntime> {
18 #[clap(skip)]
19 _phantom: PhantomData<T>,
20}
21
22impl<T: PartnerChainRuntime> CmdRun for CreateChainSpecCmd<T> {
23 fn run<C: IOContext>(&self, context: &C) -> anyhow::Result<()> {
24 let config = CreateChainSpecConfig::load(context)?;
25 context.print("This wizard will create a chain spec JSON file according to the provided configuration, using WASM runtime code from the compiled node binary.");
26 Self::print_config(context, &config);
27 if context.prompt_yes_no("Do you want to continue?", true) {
28 let content = T::create_chain_spec(&config);
29 context.write_file("chain-spec.json", &serde_json::to_string_pretty(&content)?);
30 context.print("chain-spec.json file has been created.");
31 context.print(
32 "If you are the governance authority, you can distribute it to the validators.",
33 );
34 Ok(())
35 } else {
36 context.print("Aborted.");
37 Ok(())
38 }
39 }
40}
41
42impl<T: PartnerChainRuntime> CreateChainSpecCmd<T> {
43 fn print_config<C: IOContext>(context: &C, config: &CreateChainSpecConfig<T::Keys>) {
44 context.print("Chain parameters:");
45 context.print(format!("- Genesis UTXO: {}", config.genesis_utxo).as_str());
46 context.print("SessionValidatorManagement Main Chain Configuration:");
47 context.print(
48 format!("- committee_candidate_address: {}", config.committee_candidate_address)
49 .as_str(),
50 );
51 context.print(
52 format!("- d_parameter_policy_id: {}", config.d_parameter_policy_id.to_hex_string())
53 .as_str(),
54 );
55 context.print(
56 format!(
57 "- permissioned_candidates_policy_id: {}",
58 config.permissioned_candidates_policy_id.to_hex_string()
59 )
60 .as_str(),
61 );
62 context.print("Bridge Configuration (unused if empty):");
63 context.print(&format!("- asset name: {}", config.bridge_token_asset_name.to_hex_string()));
64 context
65 .print(&format!("- asset policy ID: {}", config.bridge_token_policy.to_hex_string()));
66 context.print(&format!(
67 "- illiquid circulation supply validator address: {}",
68 config.illiquid_circulation_supply_validator_address
69 ));
70 context.print("Governed Map Configuration:");
71 context.print(&format!(
72 "- validator address: {}",
73 config.governed_map_validator_address.clone().unwrap_or_default()
74 ));
75 context.print(&format!(
76 "- asset policy ID: {}",
77 config.governed_map_asset_policy_id.clone().unwrap_or_default().to_hex_string()
78 ));
79 use colored::Colorize;
80 if config.initial_permissioned_candidates_parsed.is_empty() {
81 context.print("WARNING: The list of initial permissioned candidates is empty. Generated chain spec will not allow the chain to start.".red().to_string().as_str());
82 let update_msg = format!(
83 "Update 'initial_permissioned_candidates' field of {} file with keys of initial committee.",
84 context
85 .config_file_path(config_fields::INITIAL_PERMISSIONED_CANDIDATES.config_file)
86 );
87 context.print(update_msg.red().to_string().as_str());
88 context.print(INITIAL_PERMISSIONED_CANDIDATES_EXAMPLE.yellow().to_string().as_str());
89 } else {
90 context.print("Initial permissioned candidates:");
91 for candidate in config.initial_permissioned_candidates_raw.iter() {
92 context.print(format!("- {}", candidate).as_str());
93 }
94 }
95 }
96}
97
98#[allow(missing_docs)]
99#[derive(Debug)]
100pub struct CreateChainSpecConfig<Keys> {
102 pub bootnodes: Vec<String>,
103 pub genesis_utxo: UtxoId,
104 pub initial_permissioned_candidates_raw: Vec<PermissionedCandidateKeys>,
105 pub initial_permissioned_candidates_parsed: Vec<ParsedPermissionedCandidatesKeys<Keys>>,
106 pub committee_candidate_address: MainchainAddress,
107 pub d_parameter_policy_id: PolicyId,
108 pub permissioned_candidates_policy_id: PolicyId,
109 pub bridge_token_policy: PolicyId,
110 pub bridge_token_asset_name: AssetName,
111 pub illiquid_circulation_supply_validator_address: MainchainAddress,
112 pub governed_map_validator_address: Option<MainchainAddress>,
113 pub governed_map_asset_policy_id: Option<PolicyId>,
114}
115
116impl<Keys: MaybeFromCandidateKeys> CreateChainSpecConfig<Keys> {
117 pub(crate) fn load<C: IOContext>(c: &C) -> Result<Self, anyhow::Error> {
118 let initial_permissioned_candidates_raw =
119 load_config_field(c, &config_fields::INITIAL_PERMISSIONED_CANDIDATES)?;
120 let initial_permissioned_candidates_parsed: Vec<ParsedPermissionedCandidatesKeys<Keys>> =
121 initial_permissioned_candidates_raw
122 .iter()
123 .map(TryFrom::try_from)
124 .collect::<Result<Vec<ParsedPermissionedCandidatesKeys<Keys>>, anyhow::Error>>()?;
125 Ok(Self {
126 bootnodes: load_config_field(c, &config_fields::BOOTNODES)?,
127 genesis_utxo: load_config_field(c, &config_fields::GENESIS_UTXO)?,
128 initial_permissioned_candidates_raw,
129 initial_permissioned_candidates_parsed,
130 committee_candidate_address: load_config_field(
131 c,
132 &config_fields::COMMITTEE_CANDIDATES_ADDRESS,
133 )?,
134 d_parameter_policy_id: load_config_field(c, &config_fields::D_PARAMETER_POLICY_ID)?,
135 permissioned_candidates_policy_id: load_config_field(
136 c,
137 &config_fields::PERMISSIONED_CANDIDATES_POLICY_ID,
138 )?,
139 bridge_token_policy: load_config_field(c, &config_fields::BRIDGE_TOKEN_POLICY)?,
140 bridge_token_asset_name: load_config_field(c, &config_fields::BRIDGE_TOKEN_ASSET_NAME)?,
141 illiquid_circulation_supply_validator_address: load_config_field(
142 c,
143 &config_fields::ILLIQUID_SUPPLY_ADDRESS,
144 )?,
145 governed_map_validator_address: config_fields::GOVERNED_MAP_VALIDATOR_ADDRESS
146 .load_from_file(c),
147 governed_map_asset_policy_id: config_fields::GOVERNED_MAP_POLICY_ID.load_from_file(c),
148 })
149 }
150
151 pub fn pallet_sidechain_config<T: pallet_sidechain::Config>(
153 &self,
154 slots_per_epoch: sidechain_slots::SlotsPerEpoch,
155 ) -> pallet_sidechain::GenesisConfig<T> {
156 pallet_sidechain::GenesisConfig {
157 genesis_utxo: self.genesis_utxo,
158 slots_per_epoch,
159 _config: PhantomData,
160 }
161 }
162
163 pub fn pallet_partner_chains_session_config<T: pallet_session::Config>(
166 &self,
167 ) -> pallet_session::GenesisConfig<T>
168 where
169 T::ValidatorId: From<AccountId32>,
170 T::Keys: From<Keys>,
171 T::AccountId: From<AccountId32>,
172 {
173 pallet_session::GenesisConfig {
174 keys: self
175 .initial_permissioned_candidates_parsed
176 .iter()
177 .map(|c| {
178 (c.account_id_32().into(), c.account_id_32().into(), c.keys.clone().into())
179 })
180 .collect::<Vec<_>>(),
181 ..Default::default()
182 }
183 }
184
185 pub fn pallet_session_validator_management_config<
188 T: pallet_session_validator_management::Config,
189 >(
190 &self,
191 ) -> pallet_session_validator_management::GenesisConfig<T>
192 where
193 T::AuthorityId: From<ecdsa::Public>,
194 T::AuthorityKeys: From<Keys>,
195 T::CommitteeMember:
196 From<authority_selection_inherents::CommitteeMember<T::AuthorityId, T::AuthorityKeys>>,
197 {
198 pallet_session_validator_management::GenesisConfig {
199 initial_authorities: self
200 .initial_permissioned_candidates_parsed
201 .iter()
202 .map(|c| {
203 authority_selection_inherents::CommitteeMember::permissioned(
204 c.sidechain.into(),
205 c.keys.clone().into(),
206 )
207 .into()
208 })
209 .collect::<Vec<_>>(),
210 main_chain_scripts: sp_session_validator_management::MainChainScripts {
211 committee_candidate_address: self.committee_candidate_address.clone(),
212 d_parameter_policy_id: self.d_parameter_policy_id.clone(),
213 permissioned_candidates_policy_id: self.permissioned_candidates_policy_id.clone(),
214 },
215 }
216 }
217
218 pub fn bridge_config<T: pallet_partner_chains_bridge::Config>(
220 &self,
221 ) -> pallet_partner_chains_bridge::GenesisConfig<T> {
222 pallet_partner_chains_bridge::GenesisConfig {
223 main_chain_scripts: Some(sp_partner_chains_bridge::MainChainScripts {
224 token_policy_id: self.bridge_token_policy.clone(),
225 token_asset_name: self.bridge_token_asset_name.clone(),
226 illiquid_circulation_supply_validator_address: self
227 .illiquid_circulation_supply_validator_address
228 .clone(),
229 }),
230 initial_checkpoint: Some(self.genesis_utxo),
231 _marker: PhantomData,
232 }
233 }
234
235 pub fn governed_map_config<T: pallet_governed_map::Config>(
237 &self,
238 ) -> pallet_governed_map::GenesisConfig<T> {
239 pallet_governed_map::GenesisConfig {
240 main_chain_scripts: self.governed_map_validator_address.as_ref().and_then(|addr| {
241 self.governed_map_asset_policy_id.as_ref().map(|policy| {
242 sp_governed_map::MainChainScriptsV1 {
243 validator_address: addr.clone(),
244 asset_policy_id: policy.clone(),
245 }
246 })
247 }),
248 _marker: PhantomData,
249 }
250 }
251}
252
253impl<T> Default for CreateChainSpecConfig<T> {
254 fn default() -> Self {
255 Self {
256 bootnodes: Default::default(),
257 genesis_utxo: Default::default(),
258 initial_permissioned_candidates_raw: Default::default(),
259 initial_permissioned_candidates_parsed: Default::default(),
260 committee_candidate_address: Default::default(),
261 d_parameter_policy_id: Default::default(),
262 permissioned_candidates_policy_id: Default::default(),
263 bridge_token_policy: Default::default(),
264 bridge_token_asset_name: Default::default(),
265 illiquid_circulation_supply_validator_address: Default::default(),
266 governed_map_validator_address: Default::default(),
267 governed_map_asset_policy_id: Default::default(),
268 }
269 }
270}
271
272fn load_config_field<C: IOContext, T: DeserializeOwned>(
273 context: &C,
274 field: &ConfigFieldDefinition<T>,
275) -> Result<T, anyhow::Error> {
276 field.load_from_file(context).ok_or_else(|| {
277 context.eprint(format!("The '{}' configuration file is missing or invalid.\nIf you are the governance authority, please make sure you have run the `prepare-configuration` command to generate the chain configuration file.\nIf you are a validator, you can obtain the chain configuration file from the governance authority.", context.config_file_path(field.config_file)).as_str());
278 anyhow!("failed to read '{}'", field.path.join("."))
279 })
280}
281
282pub const INITIAL_PERMISSIONED_CANDIDATES_EXAMPLE: &str = r#"Example of 'initial_permissioned_candidates' field with 2 permissioned candidates:
283"initial_permissioned_candidates": [
284 {
285 "partner_chains_key": "0x020a1091341fe5664bfa1782d5e0477968906ac916b04cb365ec3153755684d9a1",
286 "keys": {
287 "aura": "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde49a5684e7a56da27d",
288 "gran": "0x88dc3417d5058ec4b4503e0c12ea1a0a89be200f498922423d4334014fa6b0ee"
289 }
290 },
291 {
292 "partner_chains_key": "0x0390084fdbf27d2b79d26a4f13f0cdd982cb755a661969143c37cbc49ef5b91f27",
293 "keys": {
294 "aura": "0x8eaf04151687736326c9fea17e25fc5287613698c912909cb226aa4794f26a48",
295 "gran": "0xd17c2d7823ebf260fd138f2d7e27d114cb145d968b5ff5006125f2414fadae69"
296 }
297 }
298]"#;