partner_chains_cli/create_chain_spec/
mod.rs

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