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