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::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]"#;