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 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("Native Token Management Configuration (unused if empty):");
63		context.print(&format!("- asset name: {}", config.native_token_asset_name.to_hex_string()));
64		context
65			.print(&format!("- asset policy ID: {}", config.native_token_policy.to_hex_string()));
66		context.print(&format!("- illiquid supply address: {}", config.illiquid_supply_address));
67		context.print("Governed Map Configuration:");
68		context.print(&format!(
69			"- validator address: {}",
70			config.governed_map_validator_address.clone().unwrap_or_default()
71		));
72		context.print(&format!(
73			"- asset policy ID: {}",
74			config.governed_map_asset_policy_id.clone().unwrap_or_default().to_hex_string()
75		));
76		use colored::Colorize;
77		if config.initial_permissioned_candidates_parsed.is_empty() {
78			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());
79			let update_msg = format!(
80				"Update 'initial_permissioned_candidates' field of {} file with keys of initial committee.",
81				context
82					.config_file_path(config_fields::INITIAL_PERMISSIONED_CANDIDATES.config_file)
83			);
84			context.print(update_msg.red().to_string().as_str());
85			context.print(INITIAL_PERMISSIONED_CANDIDATES_EXAMPLE.yellow().to_string().as_str());
86		} else {
87			context.print("Initial permissioned candidates:");
88			for candidate in config.initial_permissioned_candidates_raw.iter() {
89				context.print(format!("- {}", candidate).as_str());
90			}
91		}
92	}
93}
94
95#[allow(missing_docs)]
96#[derive(Debug)]
97/// Configuration that contains all Partner Chain specific data required to create the chain spec
98pub struct CreateChainSpecConfig<Keys> {
99	pub genesis_utxo: UtxoId,
100	pub initial_permissioned_candidates_raw: Vec<PermissionedCandidateKeys>,
101	pub initial_permissioned_candidates_parsed: Vec<ParsedPermissionedCandidatesKeys<Keys>>,
102	pub committee_candidate_address: MainchainAddress,
103	pub d_parameter_policy_id: PolicyId,
104	pub permissioned_candidates_policy_id: PolicyId,
105	pub native_token_policy: PolicyId,
106	pub native_token_asset_name: AssetName,
107	pub illiquid_supply_address: MainchainAddress,
108	pub governed_map_validator_address: Option<MainchainAddress>,
109	pub governed_map_asset_policy_id: Option<PolicyId>,
110}
111
112impl<Keys: MaybeFromCandidateKeys> CreateChainSpecConfig<Keys> {
113	pub(crate) fn load<C: IOContext>(c: &C) -> Result<Self, anyhow::Error> {
114		let initial_permissioned_candidates_raw =
115			load_config_field(c, &config_fields::INITIAL_PERMISSIONED_CANDIDATES)?;
116		let initial_permissioned_candidates_parsed: Vec<ParsedPermissionedCandidatesKeys<Keys>> =
117			initial_permissioned_candidates_raw
118				.iter()
119				.map(TryFrom::try_from)
120				.collect::<Result<Vec<ParsedPermissionedCandidatesKeys<Keys>>, anyhow::Error>>()?;
121		Ok(Self {
122			genesis_utxo: load_config_field(c, &config_fields::GENESIS_UTXO)?,
123			initial_permissioned_candidates_raw,
124			initial_permissioned_candidates_parsed,
125			committee_candidate_address: load_config_field(
126				c,
127				&config_fields::COMMITTEE_CANDIDATES_ADDRESS,
128			)?,
129			d_parameter_policy_id: load_config_field(c, &config_fields::D_PARAMETER_POLICY_ID)?,
130			permissioned_candidates_policy_id: load_config_field(
131				c,
132				&config_fields::PERMISSIONED_CANDIDATES_POLICY_ID,
133			)?,
134			native_token_policy: load_config_field(c, &config_fields::NATIVE_TOKEN_POLICY)?,
135			native_token_asset_name: load_config_field(c, &config_fields::NATIVE_TOKEN_ASSET_NAME)?,
136			illiquid_supply_address: load_config_field(c, &config_fields::ILLIQUID_SUPPLY_ADDRESS)?,
137			governed_map_validator_address: config_fields::GOVERNED_MAP_VALIDATOR_ADDRESS
138				.load_from_file(c),
139			governed_map_asset_policy_id: config_fields::GOVERNED_MAP_POLICY_ID.load_from_file(c),
140		})
141	}
142
143	/// Returns [pallet_sidechain::GenesisConfig] derived from the config
144	pub fn pallet_sidechain_config<T: pallet_sidechain::Config>(
145		&self,
146		slots_per_epoch: sidechain_slots::SlotsPerEpoch,
147	) -> pallet_sidechain::GenesisConfig<T> {
148		pallet_sidechain::GenesisConfig {
149			genesis_utxo: self.genesis_utxo,
150			slots_per_epoch,
151			_config: PhantomData,
152		}
153	}
154
155	/// Returns [pallet_partner_chains_session::GenesisConfig] derived from the config, using initial permissioned candidates
156	/// as initial validators
157	pub fn pallet_partner_chains_session_config<T: pallet_partner_chains_session::Config>(
158		&self,
159	) -> pallet_partner_chains_session::GenesisConfig<T>
160	where
161		T::ValidatorId: From<AccountId32>,
162		T::Keys: From<Keys>,
163	{
164		pallet_partner_chains_session::GenesisConfig {
165			initial_validators: self
166				.initial_permissioned_candidates_parsed
167				.iter()
168				.map(|c| (c.account_id_32().into(), c.keys.clone().into()))
169				.collect::<Vec<_>>(),
170		}
171	}
172
173	/// Returns [pallet_session_validator_management::GenesisConfig] derived from the config using initial permissioned candidates
174	/// as initial authorities
175	pub fn pallet_session_validator_management_config<
176		T: pallet_session_validator_management::Config,
177	>(
178		&self,
179	) -> pallet_session_validator_management::GenesisConfig<T>
180	where
181		T::AuthorityId: From<ecdsa::Public>,
182		T::AuthorityKeys: From<Keys>,
183		T::CommitteeMember:
184			From<authority_selection_inherents::CommitteeMember<T::AuthorityId, T::AuthorityKeys>>,
185	{
186		pallet_session_validator_management::GenesisConfig {
187			initial_authorities: self
188				.initial_permissioned_candidates_parsed
189				.iter()
190				.map(|c| {
191					authority_selection_inherents::CommitteeMember::permissioned(
192						c.sidechain.into(),
193						c.keys.clone().into(),
194					)
195					.into()
196				})
197				.collect::<Vec<_>>(),
198			main_chain_scripts: sp_session_validator_management::MainChainScripts {
199				committee_candidate_address: self.committee_candidate_address.clone(),
200				d_parameter_policy_id: self.d_parameter_policy_id.clone(),
201				permissioned_candidates_policy_id: self.permissioned_candidates_policy_id.clone(),
202			},
203		}
204	}
205
206	/// Returns [pallet_native_token_management::GenesisConfig] derived from the config
207	pub fn native_token_management_config<T: pallet_native_token_management::Config>(
208		&self,
209	) -> pallet_native_token_management::GenesisConfig<T> {
210		pallet_native_token_management::GenesisConfig {
211			main_chain_scripts: Some(sp_native_token_management::MainChainScripts {
212				native_token_policy_id: self.native_token_policy.clone(),
213				native_token_asset_name: self.native_token_asset_name.clone(),
214				illiquid_supply_validator_address: self.illiquid_supply_address.clone(),
215			}),
216			_marker: PhantomData,
217		}
218	}
219
220	/// Returns [pallet_governed_map::GenesisConfig] derived from the config
221	pub fn governed_map_config<T: pallet_governed_map::Config>(
222		&self,
223	) -> pallet_governed_map::GenesisConfig<T> {
224		pallet_governed_map::GenesisConfig {
225			main_chain_scripts: self.governed_map_validator_address.as_ref().and_then(|addr| {
226				self.governed_map_asset_policy_id.as_ref().map(|policy| {
227					sp_governed_map::MainChainScriptsV1 {
228						validator_address: addr.clone(),
229						asset_policy_id: policy.clone(),
230					}
231				})
232			}),
233			_marker: PhantomData,
234		}
235	}
236}
237
238impl<T> Default for CreateChainSpecConfig<T> {
239	fn default() -> Self {
240		Self {
241			genesis_utxo: Default::default(),
242			initial_permissioned_candidates_raw: Default::default(),
243			initial_permissioned_candidates_parsed: Default::default(),
244			committee_candidate_address: Default::default(),
245			d_parameter_policy_id: Default::default(),
246			permissioned_candidates_policy_id: Default::default(),
247			native_token_policy: Default::default(),
248			native_token_asset_name: Default::default(),
249			illiquid_supply_address: Default::default(),
250			governed_map_validator_address: Default::default(),
251			governed_map_asset_policy_id: Default::default(),
252		}
253	}
254}
255
256fn load_config_field<C: IOContext, T: DeserializeOwned>(
257	context: &C,
258	field: &ConfigFieldDefinition<T>,
259) -> Result<T, anyhow::Error> {
260	field.load_from_file(context).ok_or_else(|| {
261		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());
262		anyhow!("failed to read '{}'", field.path.join("."))
263	})
264}
265
266pub const INITIAL_PERMISSIONED_CANDIDATES_EXAMPLE: &str = r#"Example of 'initial_permissioned_candidates' field with 2 permissioned candidates:
267"initial_permissioned_candidates": [
268	{
269		"partner_chains_keys": "0x020a1091341fe5664bfa1782d5e0477968906ac916b04cb365ec3153755684d9a1"
270		"keys": {
271			"aura": "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde49a5684e7a56da27d",
272			"gran": "0x88dc3417d5058ec4b4503e0c12ea1a0a89be200f498922423d4334014fa6b0ee"
273		}
274	},
275	{
276		"partner_chains_keys": "0x0390084fdbf27d2b79d26a4f13f0cdd982cb755a661969143c37cbc49ef5b91f27"
277		"keys": {
278			"aura": "0x8eaf04151687736326c9fea17e25fc5287613698c912909cb226aa4794f26a48",
279			"gran": "0xd17c2d7823ebf260fd138f2d7e27d114cb145d968b5ff5006125f2414fadae69"
280		}
281	}
282]"#;