partner_chains_cardano_offchain/
permissioned_candidates.rs

1//!
2//! Permissioned candidates are stored on chain in an UTXO at the Permissioned Candidates Validator address.
3//! There should be at most one UTXO at the validator address and it should contain the permissioned candidates list.
4//! This UTXO should have 1 token of the Permissioned Candidates Policy with an empty asset name.
5//! The datum encodes Permissioned Candidates using VersionedGenericDatum envelope with the Permissioned Candidates stored
6//! in the `datum` field of it. Field should contain list of list, where each inner list is a triple of byte strings
7//! `[sidechain_public_key, aura_public_key, grandpa_publicKey]`.
8
9use crate::await_tx::AwaitTx;
10use crate::csl::{
11	CostStore, Costs, InputsBuilderExt, TransactionBuilderExt, TransactionContext, TransactionExt,
12	empty_asset_name, get_builder_config,
13};
14use crate::governance::GovernanceData;
15use crate::multisig::{MultiSigSmartContractResult, submit_or_create_tx_to_sign};
16use crate::plutus_script::PlutusScript;
17use crate::scripts_data::PlutusScriptData;
18use crate::{cardano_keys::CardanoPaymentSigningKey, scripts_data};
19use anyhow::anyhow;
20use cardano_serialization_lib::{
21	BigInt, NetworkIdKind, PlutusData, Transaction, TransactionBuilder, TxInputsBuilder,
22};
23use ogmios_client::query_ledger_state::{QueryLedgerState, QueryUtxoByUtxoId};
24use ogmios_client::query_network::QueryNetwork;
25use ogmios_client::transactions::Transactions;
26use ogmios_client::types::OgmiosUtxo;
27use partner_chains_plutus_data::permissioned_candidates::{
28	PermissionedCandidateDatums, permissioned_candidates_to_plutus_data,
29};
30use sidechain_domain::{PermissionedCandidateData, UtxoId};
31
32/// Upserts permissioned candidates list.
33/// Arguments:
34///  - `genesis_utxo`: Genesis UTxO identifying the Partner Chain.
35///  - `candidates`: List of permissioned candidates. The current list (if exists) will be overwritten by this list.
36///  - `payment_signing_key`: Signing key of the party paying fees.
37///  - `await_tx`: [AwaitTx] strategy.
38pub async fn upsert_permissioned_candidates<
39	C: QueryLedgerState + QueryNetwork + Transactions + QueryUtxoByUtxoId,
40	A: AwaitTx,
41>(
42	genesis_utxo: UtxoId,
43	candidates: &[PermissionedCandidateData],
44	payment_signing_key: &CardanoPaymentSigningKey,
45	client: &C,
46	await_tx: &A,
47) -> anyhow::Result<Option<MultiSigSmartContractResult>> {
48	let ctx = TransactionContext::for_payment_key(payment_signing_key, client).await?;
49	let scripts = scripts_data::permissioned_candidates_scripts(genesis_utxo, ctx.network)?;
50	let governance_data = GovernanceData::get(genesis_utxo, client).await?;
51	let validator_utxos = client.query_utxos(&[scripts.validator_address.clone()]).await?;
52	let mut candidates = candidates.to_owned();
53	candidates.sort();
54
55	let result_opt = match get_current_permissioned_candidates(validator_utxos, &scripts)? {
56		Some((_, current_permissioned_candidates))
57			if current_permissioned_candidates == *candidates =>
58		{
59			log::info!("Current permissioned candidates are equal to the one to be set.");
60			None
61		},
62		Some((current_utxo, _)) => {
63			log::info!(
64				"Current permissioned candidates are different to the one to be set. Preparing transaction to update."
65			);
66			Some(
67				update_permissioned_candidates(
68					&scripts.validator,
69					&scripts.policy,
70					&candidates,
71					&current_utxo,
72					&governance_data,
73					ctx,
74					client,
75					await_tx,
76				)
77				.await?,
78			)
79		},
80		None => {
81			log::info!(
82				"There aren't any permissioned candidates. Preparing transaction to insert."
83			);
84			Some(
85				insert_permissioned_candidates(
86					&scripts.validator,
87					&scripts.policy,
88					&candidates,
89					&governance_data,
90					ctx,
91					client,
92					await_tx,
93				)
94				.await?,
95			)
96		},
97	};
98	Ok(result_opt)
99}
100
101fn get_current_permissioned_candidates(
102	validator_utxos: Vec<OgmiosUtxo>,
103	scripts: &PlutusScriptData,
104) -> Result<Option<(OgmiosUtxo, Vec<PermissionedCandidateData>)>, anyhow::Error> {
105	let utxos_with_permissioned_candidates_token: Vec<OgmiosUtxo> = validator_utxos
106		.into_iter()
107		.filter(|utxo| utxo.value.native_tokens.get(&scripts.policy_id().0).is_some())
108		.collect();
109
110	if utxos_with_permissioned_candidates_token.len() > 1 {
111		return Err(anyhow!("Multiple UTXOs with permissioned candidates token found"));
112	}
113
114	if let Some(utxo) = utxos_with_permissioned_candidates_token.first() {
115		let datum = utxo.datum.clone().ok_or_else(|| {
116			anyhow!("Invalid state: an UTXO at the validator script address does not have a datum")
117		})?;
118		let datum_plutus_data = PlutusData::from_bytes(datum.bytes).map_err(|e| {
119			anyhow!(
120				"Internal error: could not decode datum of permissioned candidates validator script: {}",
121				e
122			)
123		})?;
124		let mut permissioned_candidates: Vec<PermissionedCandidateData> =
125			PermissionedCandidateDatums::try_from(datum_plutus_data)
126				.map_err(|e| {
127					anyhow!(
128						"Internal error: could not decode datum of permissioned candidates validator script: {}",
129						e
130					)
131				})?
132				.into();
133		permissioned_candidates.sort();
134		Ok(Some((utxo.clone(), permissioned_candidates)))
135	} else {
136		Ok(None)
137	}
138}
139
140async fn insert_permissioned_candidates<C, A>(
141	validator: &PlutusScript,
142	policy: &PlutusScript,
143	candidates: &[PermissionedCandidateData],
144	governance_data: &GovernanceData,
145	payment_ctx: TransactionContext,
146	client: &C,
147	await_tx: &A,
148) -> anyhow::Result<MultiSigSmartContractResult>
149where
150	C: Transactions + QueryLedgerState + QueryNetwork + QueryUtxoByUtxoId,
151	A: AwaitTx,
152{
153	submit_or_create_tx_to_sign(
154		governance_data,
155		payment_ctx,
156		|costs, ctx| {
157			mint_permissioned_candidates_token_tx(
158				validator,
159				policy,
160				candidates,
161				governance_data,
162				costs,
163				ctx,
164			)
165		},
166		"Insert Permissioned Candidates",
167		client,
168		await_tx,
169	)
170	.await
171}
172
173async fn update_permissioned_candidates<C, A>(
174	validator: &PlutusScript,
175	policy: &PlutusScript,
176	candidates: &[PermissionedCandidateData],
177	current_utxo: &OgmiosUtxo,
178	governance_data: &GovernanceData,
179	payment_ctx: TransactionContext,
180	client: &C,
181	await_tx: &A,
182) -> anyhow::Result<MultiSigSmartContractResult>
183where
184	C: Transactions + QueryNetwork + QueryLedgerState + QueryUtxoByUtxoId,
185	A: AwaitTx,
186{
187	submit_or_create_tx_to_sign(
188		governance_data,
189		payment_ctx,
190		|costs, ctx| {
191			update_permissioned_candidates_tx(
192				validator,
193				policy,
194				candidates,
195				current_utxo,
196				governance_data,
197				costs,
198				ctx,
199			)
200		},
201		"Update Permissioned Candidates",
202		client,
203		await_tx,
204	)
205	.await
206}
207
208/// Builds a transaction that mints a Permissioned Candidates token and also mint governance token
209fn mint_permissioned_candidates_token_tx(
210	validator: &PlutusScript,
211	policy: &PlutusScript,
212	permissioned_candidates: &[PermissionedCandidateData],
213	governance_data: &GovernanceData,
214	costs: Costs,
215	ctx: &TransactionContext,
216) -> anyhow::Result<Transaction> {
217	let mut tx_builder = TransactionBuilder::new(&get_builder_config(ctx)?);
218	// The essence of transaction: mint permissioned candidates token and set output with it, mint a governance token.
219	tx_builder.add_mint_one_script_token(
220		policy,
221		&empty_asset_name(),
222		&permissioned_candidates_policy_redeemer_data(),
223		&costs.get_mint(&policy.clone()),
224	)?;
225	tx_builder.add_output_with_one_script_token(
226		validator,
227		policy,
228		&permissioned_candidates_to_plutus_data(permissioned_candidates),
229		ctx,
230	)?;
231
232	let gov_tx_input = governance_data.utxo_id_as_tx_input();
233	tx_builder.add_mint_one_script_token_using_reference_script(
234		&governance_data.policy.script(),
235		&gov_tx_input,
236		&costs,
237	)?;
238
239	Ok(tx_builder.balance_update_and_build(ctx)?.remove_native_script_witnesses())
240}
241
242fn update_permissioned_candidates_tx(
243	validator: &PlutusScript,
244	policy: &PlutusScript,
245	permissioned_candidates: &[PermissionedCandidateData],
246	script_utxo: &OgmiosUtxo,
247	governance_data: &GovernanceData,
248	costs: Costs,
249	ctx: &TransactionContext,
250) -> anyhow::Result<Transaction> {
251	let mut tx_builder = TransactionBuilder::new(&get_builder_config(ctx)?);
252
253	{
254		let mut inputs = TxInputsBuilder::new();
255		inputs.add_script_utxo_input(
256			script_utxo,
257			validator,
258			&permissioned_candidates_policy_redeemer_data(),
259			&costs.get_one_spend(),
260		)?;
261		tx_builder.set_inputs(&inputs);
262	}
263
264	tx_builder.add_output_with_one_script_token(
265		validator,
266		policy,
267		&permissioned_candidates_to_plutus_data(permissioned_candidates),
268		ctx,
269	)?;
270
271	let gov_tx_input = governance_data.utxo_id_as_tx_input();
272	tx_builder.add_mint_one_script_token_using_reference_script(
273		&governance_data.policy.script(),
274		&gov_tx_input,
275		&costs,
276	)?;
277
278	Ok(tx_builder.balance_update_and_build(ctx)?.remove_native_script_witnesses())
279}
280
281fn permissioned_candidates_policy_redeemer_data() -> PlutusData {
282	PlutusData::new_integer(&BigInt::zero())
283}
284
285/// Returns all permissioned candidates.
286pub async fn get_permissioned_candidates<C>(
287	genesis_utxo: UtxoId,
288	network: NetworkIdKind,
289	client: &C,
290) -> anyhow::Result<Option<Vec<PermissionedCandidateData>>>
291where
292	C: QueryNetwork + QueryLedgerState,
293{
294	let scripts = scripts_data::permissioned_candidates_scripts(genesis_utxo, network)?;
295	let validator_utxos = client.query_utxos(&[scripts.validator_address.clone()]).await?;
296	Ok(get_current_permissioned_candidates(validator_utxos, &scripts)?
297		.map(|(_, candidates)| candidates))
298}
299
300#[cfg(test)]
301mod tests {
302	use super::{mint_permissioned_candidates_token_tx, update_permissioned_candidates_tx};
303	use crate::{
304		csl::{Costs, TransactionContext, empty_asset_name},
305		governance::GovernanceData,
306		test_values::*,
307	};
308	use cardano_serialization_lib::{Address, ExUnits, Int, NetworkIdKind, PlutusData};
309	use hex_literal::hex;
310	use ogmios_client::types::{Asset as OgmiosAsset, OgmiosTx, OgmiosUtxo, OgmiosValue};
311	use partner_chains_plutus_data::permissioned_candidates::permissioned_candidates_to_plutus_data;
312	use sidechain_domain::{
313		AuraPublicKey, CandidateKeys, GrandpaPublicKey, PermissionedCandidateData,
314		SidechainPublicKey,
315	};
316
317	#[test]
318	fn mint_permissioned_candiates_token_tx_regression_test() {
319		let tx = mint_permissioned_candidates_token_tx(
320			&test_validator(),
321			&test_policy(),
322			&input_candidates(),
323			&test_governance_data(),
324			test_costs_mint(),
325			&test_tx_context(),
326		)
327		.unwrap();
328
329		let body = tx.body();
330		let inputs = body.inputs();
331		// Both payment utxos are used as inputs
332		assert_eq!(
333			inputs.get(0).to_string(),
334			"0404040404040404040404040404040404040404040404040404040404040404#1"
335		);
336		assert_eq!(
337			inputs.get(1).to_string(),
338			"0707070707070707070707070707070707070707070707070707070707070707#0"
339		);
340		// The greater payment utxo is used as collateral
341		assert_eq!(
342			body.collateral().unwrap().get(0).to_string(),
343			"0404040404040404040404040404040404040404040404040404040404040404#1"
344		);
345		let outputs = body.outputs();
346		// There is a change for payment
347		let change_output = outputs.into_iter().find(|o| o.address() == payment_addr()).unwrap();
348		// There is 1 permissioned candidates token in the validator address output
349		let script_output = outputs.into_iter().find(|o| o.address() == validator_addr()).unwrap();
350		let coins_sum = change_output
351			.amount()
352			.coin()
353			.checked_add(&script_output.amount().coin())
354			.unwrap()
355			.checked_add(&body.fee())
356			.unwrap();
357		assert_eq!(
358			coins_sum,
359			(greater_payment_utxo().value.lovelace + lesser_payment_utxo().value.lovelace).into()
360		);
361		assert_eq!(
362			script_output
363				.amount()
364				.multiasset()
365				.unwrap()
366				.get_asset(&token_policy_id().into(), &empty_asset_name(),),
367			1u64.into()
368		);
369		assert_eq!(script_output.plutus_data().unwrap(), expected_plutus_data());
370		// This token is minted in the transaction
371		let mint = body.mint().unwrap();
372		assert_eq!(
373			mint.get(&token_policy_id().into())
374				.unwrap()
375				.get(0)
376				.unwrap()
377				.get(&empty_asset_name())
378				.unwrap(),
379			Int::new_i32(1)
380		);
381
382		// Collateral return must be set
383		let collateral_return = body.collateral_return().unwrap();
384		assert_eq!(collateral_return.address(), payment_addr());
385		let total_collateral = body.total_collateral().unwrap();
386		assert_eq!(
387			collateral_return.amount().coin().checked_add(&total_collateral).unwrap(),
388			greater_payment_utxo().value.lovelace.into()
389		);
390	}
391
392	#[test]
393	fn update_permissioned_candidates_tx_regression_test() {
394		let script_utxo_lovelace = 1952430;
395		let script_utxo = OgmiosUtxo {
396			transaction: OgmiosTx { id: [15; 32] },
397			index: 0,
398			value: OgmiosValue {
399				lovelace: script_utxo_lovelace,
400				native_tokens: vec![(
401					token_policy_id(),
402					vec![OgmiosAsset { name: vec![], amount: 1 }],
403				)]
404				.into_iter()
405				.collect(),
406			},
407			address: validator_addr().to_bech32(None).unwrap(),
408			..Default::default()
409		};
410
411		let tx = update_permissioned_candidates_tx(
412			&test_validator(),
413			&test_policy(),
414			&input_candidates(),
415			&script_utxo,
416			&test_governance_data(),
417			test_costs_update(),
418			&test_tx_context(),
419		)
420		.unwrap();
421		let body = tx.body();
422		let inputs = body.inputs();
423		// Script input goes to inputs
424		assert_eq!(
425			inputs.get(0).to_string(),
426			"0404040404040404040404040404040404040404040404040404040404040404#1"
427		);
428		// Payment input goes to inputs
429		assert_eq!(
430			inputs.get(1).to_string(),
431			"0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f#0"
432		);
433		// The greater payment utxo is used as collateral
434		assert_eq!(
435			body.collateral().unwrap().get(0).to_string(),
436			"0404040404040404040404040404040404040404040404040404040404040404#1"
437		);
438		let outputs = body.outputs();
439		// There is a change for payment
440		let change_output = outputs.into_iter().find(|o| o.address() == payment_addr()).unwrap();
441		// There is 1 permissioned candidates token in the validator address output
442		let script_output = outputs.into_iter().find(|o| o.address() == validator_addr()).unwrap();
443		let coins_sum = change_output
444			.amount()
445			.coin()
446			.checked_add(&script_output.amount().coin())
447			.unwrap()
448			.checked_add(&body.fee())
449			.unwrap();
450		assert_eq!(
451			coins_sum,
452			(greater_payment_utxo().value.lovelace + script_utxo_lovelace).into()
453		);
454		assert_eq!(
455			script_output
456				.amount()
457				.multiasset()
458				.unwrap()
459				.get_asset(&token_policy_id().into(), &empty_asset_name(),),
460			1u64.into()
461		);
462		assert_eq!(script_output.plutus_data().unwrap(), expected_plutus_data());
463
464		// Collateral return must be set
465		let collateral_return = body.collateral_return().unwrap();
466		assert_eq!(collateral_return.address(), payment_addr());
467		let total_collateral = body.total_collateral().unwrap();
468		assert_eq!(
469			collateral_return.amount().coin().checked_add(&total_collateral).unwrap(),
470			greater_payment_utxo().value.lovelace.into()
471		);
472	}
473
474	fn permissioned_candidates_ex_units() -> ExUnits {
475		ExUnits::new(&10000u32.into(), &200u32.into())
476	}
477	fn governance_ex_units() -> ExUnits {
478		ExUnits::new(&99999u32.into(), &999u32.into())
479	}
480
481	fn test_costs_mint() -> Costs {
482		Costs::new(
483			vec![
484				(test_policy().csl_script_hash(), permissioned_candidates_ex_units()),
485				(test_governance_policy().script().script_hash().into(), governance_ex_units()),
486			]
487			.into_iter()
488			.collect(),
489			vec![(0, permissioned_candidates_ex_units())].into_iter().collect(),
490		)
491	}
492
493	fn test_costs_update() -> Costs {
494		Costs::new(
495			vec![(test_governance_policy().script().script_hash().into(), governance_ex_units())]
496				.into_iter()
497				.collect(),
498			vec![(0, permissioned_candidates_ex_units())].into_iter().collect(),
499		)
500	}
501
502	fn test_goveranance_utxo() -> OgmiosUtxo {
503		OgmiosUtxo { transaction: OgmiosTx { id: [123; 32] }, index: 17, ..Default::default() }
504	}
505
506	fn test_governance_data() -> GovernanceData {
507		GovernanceData { policy: test_governance_policy(), utxo: test_goveranance_utxo() }
508	}
509
510	fn test_tx_context() -> TransactionContext {
511		TransactionContext {
512			payment_key: payment_key(),
513			payment_key_utxos: vec![
514				lesser_payment_utxo(),
515				greater_payment_utxo(),
516				make_utxo(14u8, 0, 400000, &payment_addr()),
517			],
518			network: NetworkIdKind::Testnet,
519			protocol_parameters: protocol_parameters(),
520			change_address: payment_addr(),
521		}
522	}
523
524	fn lesser_payment_utxo() -> OgmiosUtxo {
525		make_utxo(7u8, 0, 1700000, &payment_addr())
526	}
527
528	fn greater_payment_utxo() -> OgmiosUtxo {
529		make_utxo(4u8, 1, 1800000, &payment_addr())
530	}
531
532	fn validator_addr() -> Address {
533		Address::from_bech32("addr_test1wpha4546lvfcau5jsrwpht9h6350m3au86fev6nwmuqz9gqer2ung")
534			.unwrap()
535	}
536
537	fn token_policy_id() -> [u8; 28] {
538		hex!("f14241393964259a53ca546af364e7f5688ca5aaa35f1e0da0f951b2")
539	}
540
541	fn input_candidates() -> Vec<PermissionedCandidateData> {
542		vec![
543			PermissionedCandidateData {
544				sidechain_public_key: SidechainPublicKey(
545					hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
546						.into(),
547				),
548				keys: CandidateKeys(vec![
549					AuraPublicKey(
550						hex!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
551							.into(),
552					)
553					.into(),
554					GrandpaPublicKey(
555						hex!("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")
556							.into(),
557					)
558					.into(),
559				]),
560			},
561			PermissionedCandidateData {
562				sidechain_public_key: SidechainPublicKey(
563					hex!("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd")
564						.into(),
565				),
566				keys: CandidateKeys(vec![
567					AuraPublicKey(
568						hex!("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee")
569							.into(),
570					)
571					.into(),
572					GrandpaPublicKey(
573						hex!("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
574							.into(),
575					)
576					.into(),
577				]),
578			},
579		]
580	}
581
582	fn expected_plutus_data() -> PlutusData {
583		permissioned_candidates_to_plutus_data(&input_candidates())
584	}
585}