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::runtime_bindings::PartnerChainRuntimeBindings;
6use crate::{CmdRun, config::config_fields};
7use anyhow::{Context, anyhow};
8use serde_json::Value as JValue;
9use sidechain_domain::UtxoId;
10use sp_runtime::DeserializeOwned;
11use std::marker::PhantomData;
12
13#[cfg(test)]
14mod tests;
15
16pub trait CreateChainSpecRuntimeBindings: PartnerChainRuntime {
17	fn initial_member(id: Self::AuthorityId, keys: Self::AuthorityKeys) -> Self::CommitteeMember;
18}
19impl<T: PartnerChainRuntimeBindings> CreateChainSpecRuntimeBindings for T {
20	fn initial_member(id: Self::AuthorityId, keys: Self::AuthorityKeys) -> Self::CommitteeMember {
21		<T as PartnerChainRuntimeBindings>::initial_member(id, keys)
22	}
23}
24
25#[derive(Clone, Debug, Default, clap::Parser)]
26pub struct CreateChainSpecCmd<T: CreateChainSpecRuntimeBindings> {
27	#[clap(skip)]
28	_phantom: PhantomData<T>,
29}
30
31const SESSION_INITIAL_VALIDATORS_PATH: &str =
32	"/genesis/runtimeGenesis/config/session/initialValidators";
33const SESSION_VALIDATOR_MANAGEMENT_INITIAL_AUTHORITIES_PATH: &str =
34	"/genesis/runtimeGenesis/config/sessionCommitteeManagement/initialAuthorities";
35const GOVERNED_MAP_VALIDATOR_ADDRESS_PATH: &str =
36	"/genesis/runtimeGenesis/config/governedMap/mainChainScripts/validator_address";
37const GOVERNED_MAP_ASSET_POLICY_ID_PATH: &str =
38	"/genesis/runtimeGenesis/config/governedMap/mainChainScripts/asset_policy_id";
39
40impl<T: CreateChainSpecRuntimeBindings> CmdRun for CreateChainSpecCmd<T> {
41	fn run<C: IOContext>(&self, context: &C) -> anyhow::Result<()> {
42		let config = CreateChainSpecConfig::load(context)?;
43		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.");
44		Self::print_config(context, &config);
45		if context.prompt_yes_no("Do you want to continue?", true) {
46			Self::run_build_spec_command(context, &config)?;
47			Self::update_chain_spec_field_not_filled_by_the_node(context, &config)?;
48			context.print("chain-spec.json file has been created.");
49			context.print(
50				"If you are the governance authority, you can distribute it to the validators.",
51			);
52			context.print("Run 'setup-main-chain-state' command to set D-parameter and permissioned candidates on Cardano.");
53			Ok(())
54		} else {
55			context.print("Aborted.");
56			Ok(())
57		}
58	}
59}
60
61impl<T: CreateChainSpecRuntimeBindings> CreateChainSpecCmd<T> {
62	fn print_config<C: IOContext>(context: &C, config: &CreateChainSpecConfig) {
63		context.print("Chain parameters:");
64		context.print(format!("- Genesis UTXO: {}", config.genesis_utxo).as_str());
65		context.print("SessionValidatorManagement Main Chain Configuration:");
66		context.print(
67			format!("- committee_candidate_address: {}", config.committee_candidate_address)
68				.as_str(),
69		);
70		context
71			.print(format!("- d_parameter_policy_id: {}", config.d_parameter_policy_id).as_str());
72		context.print(
73			format!(
74				"- permissioned_candidates_policy_id: {}",
75				config.permissioned_candidates_policy_id
76			)
77			.as_str(),
78		);
79		context.print("Native Token Management Configuration (unused if empty):");
80		context.print(&format!("- asset name: {}", config.native_token_asset_name));
81		context.print(&format!("- asset policy ID: {}", config.native_token_policy));
82		context.print(&format!("- illiquid supply address: {}", config.illiquid_supply_address));
83		context.print("Governed Map Configuration:");
84		context.print(&format!(
85			"- validator address: {}",
86			config.governed_map_validator_address.clone().unwrap_or_default()
87		));
88		context.print(&format!(
89			"- asset policy ID: {}",
90			config.governed_map_asset_policy_id.clone().unwrap_or_default()
91		));
92		use colored::Colorize;
93		if config.initial_permissioned_candidates_raw.is_empty() {
94			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());
95			let update_msg = format!(
96				"Update 'initial_permissioned_candidates' field of {} file with keys of initial committee.",
97				config_fields::INITIAL_PERMISSIONED_CANDIDATES.config_file
98			);
99			context.print(update_msg.red().to_string().as_str());
100			context.print(INITIAL_PERMISSIONED_CANDIDATES_EXAMPLE.yellow().to_string().as_str());
101		} else {
102			context.print("Initial permissioned candidates:");
103			for candidate in config.initial_permissioned_candidates_raw.iter() {
104				context.print(format!("- {}", candidate).as_str());
105			}
106		}
107	}
108
109	fn run_build_spec_command<C: IOContext>(
110		context: &C,
111		config: &CreateChainSpecConfig,
112	) -> anyhow::Result<String> {
113		let node_executable = context.current_executable()?;
114		context.set_env_var("GENESIS_UTXO", &config.genesis_utxo.to_string());
115		context.set_env_var(
116			"COMMITTEE_CANDIDATE_ADDRESS",
117			&config.committee_candidate_address.to_string(),
118		);
119		context.set_env_var("D_PARAMETER_POLICY_ID", &config.d_parameter_policy_id.to_string());
120		context.set_env_var(
121			"PERMISSIONED_CANDIDATES_POLICY_ID",
122			&config.permissioned_candidates_policy_id.to_string(),
123		);
124		context.set_env_var("NATIVE_TOKEN_POLICY_ID", &config.native_token_policy);
125		context.set_env_var("NATIVE_TOKEN_ASSET_NAME", &config.native_token_asset_name);
126		context.set_env_var("ILLIQUID_SUPPLY_VALIDATOR_ADDRESS", &config.illiquid_supply_address);
127		context.run_command(
128			format!("{node_executable} build-spec --disable-default-bootnode > chain-spec.json")
129				.to_string()
130				.as_str(),
131		)
132	}
133
134	fn update_chain_spec_field_not_filled_by_the_node<C: IOContext>(
135		context: &C,
136		config: &CreateChainSpecConfig,
137	) -> anyhow::Result<()> {
138		let json = context
139			.read_file("chain-spec.json")
140			.context("Could not read chain-spec.json file. File is expected to exists.")?;
141		let mut chain_spec: serde_json::Value = serde_json::from_str(&json)?;
142
143		let initial_validators = config
144			.initial_permissioned_candidates_parsed
145			.iter()
146			.map(|c| {
147				serde_json::to_value((c.account_id_32(), c.session_keys::<T::AuthorityKeys>()))
148			})
149			.collect::<Result<Vec<serde_json::Value>, _>>()?;
150		let initial_validators = serde_json::Value::Array(initial_validators);
151		Self::update_field(&mut chain_spec, SESSION_INITIAL_VALIDATORS_PATH, initial_validators)?;
152
153		let initial_authorities = config
154			.initial_permissioned_candidates_parsed
155			.iter()
156			.map(|c| -> anyhow::Result<serde_json::Value> {
157				let initial_member =
158					T::initial_member(c.sidechain.into(), c.session_keys::<T::AuthorityKeys>());
159				Ok(serde_json::to_value(initial_member)?)
160			})
161			.collect::<Result<Vec<serde_json::Value>, _>>()?;
162		let initial_authorities = serde_json::Value::Array(initial_authorities);
163		Self::update_field(
164			&mut chain_spec,
165			SESSION_VALIDATOR_MANAGEMENT_INITIAL_AUTHORITIES_PATH,
166			initial_authorities,
167		)?;
168		match config.governed_map_validator_address.clone() {
169			Some(address) => Self::update_field(
170				&mut chain_spec,
171				GOVERNED_MAP_VALIDATOR_ADDRESS_PATH,
172				serde_json::Value::String(format!("0x{}", hex::encode(address.as_bytes()))),
173			)?,
174			None => (),
175		}
176		match config.governed_map_asset_policy_id.clone() {
177			Some(policy_id) => Self::update_field(
178				&mut chain_spec,
179				GOVERNED_MAP_ASSET_POLICY_ID_PATH,
180				serde_json::Value::String(format!("0x{policy_id}")),
181			)?,
182			None => (),
183		}
184		context.write_file("chain-spec.json", serde_json::to_string_pretty(&chain_spec)?.as_str());
185		Ok(())
186	}
187
188	fn update_field(
189		chain_spec: &mut JValue,
190		field_name: &str,
191		value: JValue,
192	) -> Result<(), anyhow::Error> {
193		if let Some(field) = chain_spec.pointer_mut(field_name) {
194			*field = value;
195			Ok(())
196		} else {
197			Err(anyhow!(
198				"Internal error: Could not find {field_name} in chain spec file! Possibly this wizard does not support the current chain spec version."
199			))
200		}
201	}
202}
203
204#[derive(Debug)]
205struct CreateChainSpecConfig {
206	genesis_utxo: UtxoId,
207	initial_permissioned_candidates_raw: Vec<PermissionedCandidateKeys>,
208	initial_permissioned_candidates_parsed: Vec<ParsedPermissionedCandidatesKeys>,
209	committee_candidate_address: String,
210	d_parameter_policy_id: String,
211	permissioned_candidates_policy_id: String,
212	native_token_policy: String,
213	native_token_asset_name: String,
214	illiquid_supply_address: String,
215	governed_map_validator_address: Option<String>,
216	governed_map_asset_policy_id: Option<String>,
217}
218
219impl CreateChainSpecConfig {
220	pub fn load<C: IOContext>(c: &C) -> Result<Self, anyhow::Error> {
221		let initial_permissioned_candidates_raw =
222			load_config_field(c, &config_fields::INITIAL_PERMISSIONED_CANDIDATES)?;
223		let initial_permissioned_candidates_parsed: Vec<ParsedPermissionedCandidatesKeys> =
224			initial_permissioned_candidates_raw
225				.iter()
226				.map(TryFrom::try_from)
227				.collect::<Result<Vec<ParsedPermissionedCandidatesKeys>, anyhow::Error>>()?;
228		Ok(Self {
229			genesis_utxo: load_config_field(c, &config_fields::GENESIS_UTXO)?,
230			initial_permissioned_candidates_raw,
231			initial_permissioned_candidates_parsed,
232			committee_candidate_address: load_config_field(
233				c,
234				&config_fields::COMMITTEE_CANDIDATES_ADDRESS,
235			)?,
236			d_parameter_policy_id: load_config_field(c, &config_fields::D_PARAMETER_POLICY_ID)?,
237			permissioned_candidates_policy_id: load_config_field(
238				c,
239				&config_fields::PERMISSIONED_CANDIDATES_POLICY_ID,
240			)?,
241			native_token_policy: load_config_field(c, &config_fields::NATIVE_TOKEN_POLICY)?,
242			native_token_asset_name: load_config_field(c, &config_fields::NATIVE_TOKEN_ASSET_NAME)?,
243			illiquid_supply_address: load_config_field(c, &config_fields::ILLIQUID_SUPPLY_ADDRESS)?,
244			governed_map_validator_address: config_fields::GOVERNED_MAP_VALIDATOR_ADDRESS
245				.load_from_file(c),
246			governed_map_asset_policy_id: config_fields::GOVERNED_MAP_POLICY_ID.load_from_file(c),
247		})
248	}
249}
250
251fn load_config_field<C: IOContext, T: DeserializeOwned>(
252	context: &C,
253	field: &ConfigFieldDefinition<T>,
254) -> Result<T, anyhow::Error> {
255	field.load_from_file(context).ok_or_else(|| {
256		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.", field.config_file).as_str());
257		anyhow!("failed to read '{}'", field.path.join("."))
258	})
259}
260
261pub const INITIAL_PERMISSIONED_CANDIDATES_EXAMPLE: &str = r#"Example of 'initial_permissioned_candidates' field with 2 permissioned candidates:
262"initial_permissioned_candidates": [
263	{
264	  "aura_pub_key": "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde49a5684e7a56da27d",
265	  "grandpa_pub_key": "0x88dc3417d5058ec4b4503e0c12ea1a0a89be200f498922423d4334014fa6b0ee",
266	  "sidechain_pub_key": "0x020a1091341fe5664bfa1782d5e0477968906ac916b04cb365ec3153755684d9a1"
267	},
268	{
269	  "aura_pub_key": "0x8eaf04151687736326c9fea17e25fc5287613698c912909cb226aa4794f26a48",
270	  "grandpa_pub_key": "0xd17c2d7823ebf260fd138f2d7e27d114cb145d968b5ff5006125f2414fadae69",
271	  "sidechain_pub_key": "0x0390084fdbf27d2b79d26a4f13f0cdd982cb755a661969143c37cbc49ef5b91f27"
272	}
273]"#;