partner_chains_cli/setup_main_chain_state/
mod.rs1use crate::cmd_traits::{
2 GetDParam, GetPermissionedCandidates, UpsertDParam, UpsertPermissionedCandidates,
3};
4use crate::config::config_fields::CARDANO_PAYMENT_SIGNING_KEY_FILE;
5use crate::config::{ChainConfig, ConfigFieldDefinition, ConfigFile, config_fields};
6use crate::io::IOContext;
7use crate::ogmios::config::prompt_ogmios_configuration;
8use crate::permissioned_candidates::{ParsedPermissionedCandidatesKeys, PermissionedCandidateKeys};
9use crate::{CmdRun, PartnerChainRuntime, cardano_key};
10use anyhow::Context;
11use anyhow::anyhow;
12use authority_selection_inherents::MaybeFromCandidateKeys;
13use ogmios_client::query_ledger_state::{QueryLedgerState, QueryUtxoByUtxoId};
14use ogmios_client::query_network::QueryNetwork;
15use ogmios_client::transactions::Transactions;
16use partner_chains_cardano_offchain::await_tx::FixedDelayRetries;
17use partner_chains_cardano_offchain::cardano_keys::CardanoPaymentSigningKey;
18use partner_chains_cardano_offchain::d_param::{get_d_param, upsert_d_param};
19use partner_chains_cardano_offchain::multisig::{
20 MultiSigSmartContractResult, MultiSigTransactionData,
21};
22use serde::de::DeserializeOwned;
23use sidechain_domain::{DParameter, PermissionedCandidateData, UtxoId};
24use std::marker::PhantomData;
25
26#[cfg(test)]
27mod tests;
28
29#[derive(Clone, Debug, clap::Parser)]
30pub struct SetupMainChainStateCmd<T: PartnerChainRuntime> {
31 #[clap(flatten)]
32 common_arguments: crate::CommonArguments,
33 #[clap(skip)]
34 _phantom: PhantomData<T>,
35}
36
37impl<Keys: MaybeFromCandidateKeys> TryFrom<PermissionedCandidateData>
38 for ParsedPermissionedCandidatesKeys<Keys>
39{
40 type Error = anyhow::Error;
41
42 fn try_from(value: PermissionedCandidateData) -> Result<Self, Self::Error> {
43 let keys: PermissionedCandidateKeys = (&value).into();
44 TryFrom::try_from(&keys)
45 }
46}
47
48#[derive(Debug, PartialEq)]
49struct SortedPermissionedCandidates(Vec<PermissionedCandidateData>);
50
51impl SortedPermissionedCandidates {
52 pub fn new(mut keys: Vec<PermissionedCandidateData>) -> Self {
53 keys.sort();
54 Self(keys)
55 }
56}
57
58impl<T: PartnerChainRuntime> CmdRun for SetupMainChainStateCmd<T> {
59 fn run<C: IOContext>(&self, context: &C) -> anyhow::Result<()> {
60 let chain_config = crate::config::load_chain_config(context)?;
61 context.print(
62 "This wizard will set or update D-Parameter and Permissioned Candidates on the main chain. Setting either of these costs ADA!",
63 );
64 let config_initial_authorities =
65 initial_permissioned_candidates_from_chain_config::<C, T::Keys>(context)?;
66 context.print("Will read the current D-Parameter and Permissioned Candidates from the main chain using Ogmios client.");
67 let ogmios_config = prompt_ogmios_configuration(context)?;
68 let offchain = context.offchain_impl(&ogmios_config)?;
69 let config_file_path = context.config_file_path(ConfigFile::Chain);
70
71 match get_permissioned_candidates::<C>(&offchain, &chain_config)? {
72 Some(candidates) if candidates == config_initial_authorities => {
73 context.print(&format!("Permissioned candidates in the {} file match the most recent on-chain initial permissioned candidates.", config_file_path));
74 },
75 candidates => {
76 print_on_chain_and_config_permissioned_candidates(
77 context,
78 candidates,
79 &config_initial_authorities,
80 );
81 set_candidates_on_main_chain(
82 self.common_arguments.retries(),
83 context,
84 &offchain,
85 config_initial_authorities,
86 chain_config.chain_parameters.genesis_utxo,
87 )?;
88 },
89 };
90 let d_parameter = get_d_parameter::<C>(&offchain, &chain_config)?;
91 print_on_chain_d_parameter(context, &d_parameter);
92 set_d_parameter_on_main_chain(
93 self.common_arguments.retries(),
94 context,
95 &offchain,
96 d_parameter.unwrap_or(DParameter {
97 num_permissioned_candidates: 0,
98 num_registered_candidates: 0,
99 }),
100 chain_config.chain_parameters.genesis_utxo,
101 )?;
102 context.print("Done. Please remember that any changes to the Cardano state can be observed immediately, but from the Partner Chain point of view they will be effective in two main chain epochs.");
103 Ok(())
104 }
105}
106
107fn initial_permissioned_candidates_from_chain_config<C: IOContext, Keys: MaybeFromCandidateKeys>(
108 context: &C,
109) -> anyhow::Result<SortedPermissionedCandidates> {
110 let candidates: Vec<PermissionedCandidateKeys> =
113 load_chain_config_field(context, &config_fields::INITIAL_PERMISSIONED_CANDIDATES)?;
114 let candidates = candidates
116 .iter()
117 .map(ParsedPermissionedCandidatesKeys::<Keys>::try_from)
118 .collect::<Result<Vec<_>, _>>()?;
119 let candidates = candidates.iter().map(PermissionedCandidateData::from).collect();
120 Ok(SortedPermissionedCandidates::new(candidates))
121}
122
123fn get_permissioned_candidates<C: IOContext>(
124 offchain: &C::Offchain,
125 chain_config: &ChainConfig,
126) -> anyhow::Result<Option<SortedPermissionedCandidates>> {
127 let tokio_runtime = tokio::runtime::Runtime::new().map_err(|e| anyhow::anyhow!(e))?;
128 let candidates_opt = tokio_runtime
129 .block_on(offchain.get_permissioned_candidates(chain_config.chain_parameters.genesis_utxo))
130 .context("Failed to read Permissioned Candidates from Ogmios")?;
131 Ok(candidates_opt.map(|candidates| SortedPermissionedCandidates::new(candidates)))
132}
133
134fn get_d_parameter<C: IOContext>(
135 offchain: &C::Offchain,
136 chain_config: &ChainConfig,
137) -> anyhow::Result<Option<DParameter>> {
138 let tokio_runtime = tokio::runtime::Runtime::new().map_err(|e| anyhow::anyhow!(e))?;
139 let d_param_opt = tokio_runtime
140 .block_on(offchain.get_d_param(chain_config.chain_parameters.genesis_utxo))
141 .context("Failed to get D-parameter from Ogmios")?;
142 Ok(d_param_opt)
143}
144
145fn print_on_chain_and_config_permissioned_candidates<C: IOContext>(
146 context: &C,
147 on_chain_candidates: Option<SortedPermissionedCandidates>,
148 config_candidates: &SortedPermissionedCandidates,
149) {
150 match on_chain_candidates {
151 Some(candidates) => {
152 context.print(&format!("Permissioned candidates in the {} file does not match the most recent on-chain initial permissioned candidates.", context.chain_config_file_path()));
153 context.print("The most recent on-chain initial permissioned candidates are:");
154 for candidate in candidates.0.iter() {
155 context.print(&format!("{}", PermissionedCandidateKeys::from(candidate)));
156 }
157 context.print("The permissioned candidates in the configuration file are:");
158 for candidate in config_candidates.0.iter() {
159 context.print(&format!("{}", PermissionedCandidateKeys::from(candidate)));
160 }
161 },
162 None => context.print("List of permissioned candidates is not set on Cardano yet."),
163 }
164}
165
166fn print_on_chain_d_parameter<C: IOContext>(
167 context: &C,
168 on_chain_d_parameter: &Option<DParameter>,
169) {
170 if let Some(d_parameter) = on_chain_d_parameter {
171 context.print(&format!(
172 "D-Parameter on the main chain is: (P={}, R={})",
173 d_parameter.num_permissioned_candidates, d_parameter.num_registered_candidates
174 ))
175 }
176}
177
178fn set_candidates_on_main_chain<C: IOContext>(
179 await_tx: FixedDelayRetries,
180 context: &C,
181 offchain: &C::Offchain,
182 candidates: SortedPermissionedCandidates,
183 genesis_utxo: UtxoId,
184) -> anyhow::Result<()> {
185 let update = context.prompt_yes_no("Do you want to set/update the permissioned candidates on the main chain with values from configuration file?", false);
186 if update {
187 let payment_signing_key_path =
188 CARDANO_PAYMENT_SIGNING_KEY_FILE.prompt_with_default_from_file_and_save(context);
189 let pkey =
190 cardano_key::get_mc_payment_signing_key_from_file(&payment_signing_key_path, context)?;
191 let tokio_runtime = tokio::runtime::Runtime::new().map_err(|e| anyhow::anyhow!(e))?;
192 let result = tokio_runtime
193 .block_on(offchain.upsert_permissioned_candidates(
194 await_tx,
195 genesis_utxo,
196 &candidates.0,
197 &pkey,
198 ))
199 .context("Permissioned candidates update failed")?;
200 match result {
201 None => context.print(
202 "Permissioned candidates on the Cardano are already equal to value from the config file.",
203 ),
204 Some(MultiSigSmartContractResult::TransactionSubmitted(_)) => context.print(
205 "Permissioned candidates updated. The change will be effective in two main chain epochs.",
206 ),
207 Some(MultiSigSmartContractResult::TransactionToSign(tx_data)) => {
208 print_tx_to_sign_and_instruction(
209 context,
210 "update permissioned candidates",
211 &tx_data,
212 )?
213 },
214 }
215 }
216 Ok(())
217}
218
219fn set_d_parameter_on_main_chain<C: IOContext>(
220 await_tx: FixedDelayRetries,
221 context: &C,
222 offchain: &C::Offchain,
223 default_d_parameter: DParameter,
224 genesis_utxo: UtxoId,
225) -> anyhow::Result<()> {
226 let update = context
227 .prompt_yes_no("Do you want to set/update the D-parameter on the main chain?", false);
228 if update {
229 let p = context.prompt(
230 "Enter P, the number of permissioned candidates seats, as a non-negative integer.",
231 Some(&default_d_parameter.num_permissioned_candidates.to_string()),
232 );
233 let num_permissioned_candidates: u16 = p.parse()?;
234 let r = context.prompt(
235 "Enter R, the number of registered candidates seats, as a non-negative integer.",
236 Some(&default_d_parameter.num_registered_candidates.to_string()),
237 );
238 let num_registered_candidates: u16 = r.parse()?;
239 let payment_signing_key_path =
240 CARDANO_PAYMENT_SIGNING_KEY_FILE.prompt_with_default_from_file_and_save(context);
241 let payment_signing_key =
242 cardano_key::get_mc_payment_signing_key_from_file(&payment_signing_key_path, context)?;
243 let d_parameter =
244 sidechain_domain::DParameter { num_permissioned_candidates, num_registered_candidates };
245 let tokio_runtime = tokio::runtime::Runtime::new().map_err(|e| anyhow::anyhow!(e))?;
246 let result = tokio_runtime.block_on(offchain.upsert_d_param(
247 await_tx,
248 genesis_utxo,
249 &d_parameter,
250 &payment_signing_key,
251 ))?;
252 match result {
253 None => context.print(&format!("D-parameter is set to ({}, {}) already.", p, r)),
254 Some(MultiSigSmartContractResult::TransactionSubmitted(_)) => context.print(&format!(
255 "D-parameter updated to ({}, {}). The change will be effective in two main chain epochs.",
256 p, r
257 )),
258 Some(MultiSigSmartContractResult::TransactionToSign(tx_data)) => {
259 print_tx_to_sign_and_instruction(context, "update D-parameter", &tx_data)?
260 },
261 }
262 }
263 Ok(())
264}
265
266fn load_chain_config_field<C: IOContext, T: DeserializeOwned>(
267 context: &C,
268 field: &ConfigFieldDefinition<T>,
269) -> Result<T, anyhow::Error> {
270 field.load_from_file(context).ok_or_else(|| {
271 context.eprint(&format!("The '{}' configuration file is missing or invalid.\nIt should have been created and updated with initial permissioned candidates before running this wizard.", context.chain_config_file_path()));
272 anyhow!("failed to read '{}'", field.path.join("."))
273 })
274}
275
276fn print_tx_to_sign_and_instruction<C: IOContext>(
277 context: &C,
278 tx_name: &str,
279 tx: &MultiSigTransactionData,
280) -> anyhow::Result<()> {
281 let json = serde_json::to_string_pretty(&tx)?;
282 context.print(&format!(
283 "The Partner chain is governed by MultiSig. Sign and submit the {tx_name} transaction:"
284 ));
285 context.print(&json);
286 context.print("Please find the instructions at: https://github.com/input-output-hk/partner-chains/blob/master/docs/user-guides/governance/governance.md#multi-signature-governance");
287 Ok(())
288}
289
290impl<C: QueryLedgerState + QueryNetwork + Transactions + QueryUtxoByUtxoId> UpsertDParam for C {
291 async fn upsert_d_param(
292 &self,
293 await_tx: FixedDelayRetries,
294 genesis_utxo: UtxoId,
295 d_parameter: &DParameter,
296 payment_signing_key: &CardanoPaymentSigningKey,
297 ) -> anyhow::Result<Option<MultiSigSmartContractResult>> {
298 upsert_d_param(genesis_utxo, d_parameter, payment_signing_key, self, &await_tx).await
299 }
300}
301
302impl<C: QueryLedgerState + QueryNetwork> GetDParam for C {
303 async fn get_d_param(&self, genesis_utxo: UtxoId) -> anyhow::Result<Option<DParameter>> {
304 get_d_param(genesis_utxo, self).await
305 }
306}