partner_chains_cli/setup_main_chain_state/
mod.rs

1use 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	// Requirements state "read from 'chain config' (or chain-spec).
111	// It's easier to read from config than from chain-spec, because parsing is already present.
112	let candidates: Vec<PermissionedCandidateKeys> =
113		load_chain_config_field(context, &config_fields::INITIAL_PERMISSIONED_CANDIDATES)?;
114	// Use ParsedPermissionedCandidatesKeys to validate them
115	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}