partner_chains_cardano_offchain/
register.rs

1use crate::cardano_keys::CardanoPaymentSigningKey;
2use crate::csl::TransactionOutputAmountBuilderExt;
3use crate::csl::{
4	CostStore, Costs, InputsBuilderExt, TransactionBuilderExt, TransactionContext, unit_plutus_data,
5};
6use crate::{await_tx::AwaitTx, plutus_script::PlutusScript};
7use anyhow::anyhow;
8use cardano_serialization_lib::{
9	PlutusData, Transaction, TransactionBuilder, TransactionOutputBuilder, TxInputsBuilder,
10};
11use ogmios_client::{
12	query_ledger_state::{QueryLedgerState, QueryUtxoByUtxoId},
13	query_network::QueryNetwork,
14	transactions::Transactions,
15	types::OgmiosUtxo,
16};
17use partner_chains_plutus_data::registered_candidates::{
18	RegisterValidatorDatum, candidate_registration_to_plutus_data,
19};
20use sidechain_domain::*;
21
22/// Submits a transaction to register a registered candidate.
23/// Arguments:
24///  - `genesis_utxo`: UTxO identifying the Partner Chain.
25///  - `candidate_registration`: [CandidateRegistration] registration data.
26///  - `ogmios_client`: Ogmios client.
27///  - `await_tx`: [AwaitTx] strategy.
28pub async fn run_register<
29	C: QueryLedgerState + QueryNetwork + QueryUtxoByUtxoId + Transactions,
30	A: AwaitTx,
31>(
32	genesis_utxo: UtxoId,
33	candidate_registration: &CandidateRegistration,
34	payment_signing_key: &CardanoPaymentSigningKey,
35	client: &C,
36	await_tx: A,
37) -> anyhow::Result<Option<McTxHash>> {
38	let ctx = TransactionContext::for_payment_key(payment_signing_key, client).await?;
39	let validator = crate::scripts_data::registered_candidates_scripts(genesis_utxo)?;
40	let validator_address = validator.address_bech32(ctx.network)?;
41	let registration_utxo = ctx
42		.payment_key_utxos
43		.iter()
44		.find(|u| u.utxo_id() == candidate_registration.registration_utxo)
45		.ok_or(anyhow!("registration utxo not found at payment address"))?;
46	let all_registration_utxos = client.query_utxos(&[validator_address]).await?;
47	let own_registrations = get_own_registrations(
48		candidate_registration.own_pkh,
49		candidate_registration.stake_ownership.pub_key.clone(),
50		&all_registration_utxos,
51	);
52
53	if own_registrations.iter().any(|(_, existing_registration)| {
54		candidate_registration.matches_keys(existing_registration)
55	}) {
56		log::info!("✅ Candidate already registered with same keys.");
57		return Ok(None);
58	}
59	let own_registration_utxos = own_registrations.iter().map(|r| r.0.clone()).collect::<Vec<_>>();
60
61	let tx = Costs::calculate_costs(
62		|costs| {
63			register_tx(
64				&validator,
65				candidate_registration,
66				registration_utxo,
67				&own_registration_utxos,
68				costs,
69				&ctx,
70			)
71		},
72		client,
73	)
74	.await?;
75
76	let signed_tx = ctx.sign(&tx).to_bytes();
77	let result = client.submit_transaction(&signed_tx).await.map_err(|e| {
78		anyhow!(
79			"Submit candidate registration transaction request failed: {}, bytes: {}",
80			e,
81			hex::encode(tx.to_bytes())
82		)
83	})?;
84	let tx_id = result.transaction.id;
85	log::info!("✅ Transaction submitted. ID: {}", hex::encode(result.transaction.id));
86	await_tx.await_tx_output(client, McTxHash(tx_id)).await?;
87
88	Ok(Some(McTxHash(result.transaction.id)))
89}
90
91/// Submits a transaction to register a registered candidate.
92/// Arguments:
93///  - `genesis_utxo`: UTxO identifying the Partner Chain.
94///  - `stake_ownership_pub_key`: Stake pub key of the registered candidate to be deregistered.
95///  - `await_tx`: [AwaitTx] strategy.
96pub async fn run_deregister<
97	C: QueryLedgerState + QueryNetwork + QueryUtxoByUtxoId + Transactions,
98	A: AwaitTx,
99>(
100	genesis_utxo: UtxoId,
101	payment_signing_key: &CardanoPaymentSigningKey,
102	stake_ownership_pub_key: StakePoolPublicKey,
103	client: &C,
104	await_tx: A,
105) -> anyhow::Result<Option<McTxHash>> {
106	let ctx = TransactionContext::for_payment_key(payment_signing_key, client).await?;
107	let validator = crate::scripts_data::registered_candidates_scripts(genesis_utxo)?;
108	let validator_address = validator.address_bech32(ctx.network)?;
109	let all_registration_utxos = client.query_utxos(&[validator_address]).await?;
110	let own_registrations = get_own_registrations(
111		payment_signing_key.to_pub_key_hash(),
112		stake_ownership_pub_key.clone(),
113		&all_registration_utxos,
114	);
115
116	if own_registrations.is_empty() {
117		log::info!("✅ Candidate is not registered.");
118		return Ok(None);
119	}
120
121	let own_registration_utxos = own_registrations.iter().map(|r| r.0.clone()).collect::<Vec<_>>();
122
123	let tx = Costs::calculate_costs(
124		|costs| deregister_tx(&validator, &own_registration_utxos, costs, &ctx),
125		client,
126	)
127	.await?;
128
129	let signed_tx = ctx.sign(&tx).to_bytes();
130	let result = client.submit_transaction(&signed_tx).await.map_err(|e| {
131		anyhow!(
132			"Submit candidate deregistration transaction request failed: {}, bytes: {}",
133			e,
134			hex::encode(tx.to_bytes())
135		)
136	})?;
137	let tx_id = result.transaction.id;
138	log::info!("✅ Transaction submitted. ID: {}", hex::encode(result.transaction.id));
139	await_tx.await_tx_output(client, McTxHash(tx_id)).await?;
140
141	Ok(Some(McTxHash(result.transaction.id)))
142}
143
144fn get_own_registrations(
145	own_pkh: MainchainKeyHash,
146	spo_pub_key: StakePoolPublicKey,
147	validator_utxos: &[OgmiosUtxo],
148) -> Vec<(OgmiosUtxo, CandidateRegistration)> {
149	let mut own_registrations = Vec::new();
150	for validator_utxo in validator_utxos {
151		match get_candidate_registration(validator_utxo.clone()) {
152			Ok(candidate_registration) => {
153				if candidate_registration.stake_ownership.pub_key == spo_pub_key
154					&& candidate_registration.own_pkh == own_pkh
155				{
156					own_registrations.push((validator_utxo.clone(), candidate_registration.clone()))
157				}
158			},
159			Err(e) => log::debug!("Found invalid UTXO at validator address: {}", e),
160		}
161	}
162	own_registrations
163}
164
165fn get_candidate_registration(validator_utxo: OgmiosUtxo) -> anyhow::Result<CandidateRegistration> {
166	let datum = validator_utxo.datum.ok_or_else(|| anyhow!("UTXO does not have a datum"))?;
167	let datum_plutus_data = PlutusData::from_bytes(datum.bytes)
168		.map_err(|e| anyhow!("Could not decode datum of validator script: {}", e))?;
169	let register_validator_datum = RegisterValidatorDatum::try_from(datum_plutus_data)
170		.map_err(|e| anyhow!("Could not decode datum of validator script: {}", e))?;
171	Ok(register_validator_datum.into())
172}
173
174fn register_tx(
175	validator: &PlutusScript,
176	candidate_registration: &CandidateRegistration,
177	registration_utxo: &OgmiosUtxo,
178	own_registration_utxos: &[OgmiosUtxo],
179	costs: Costs,
180	ctx: &TransactionContext,
181) -> anyhow::Result<Transaction> {
182	let config = crate::csl::get_builder_config(ctx)?;
183	let mut tx_builder = TransactionBuilder::new(&config);
184
185	{
186		let mut inputs = TxInputsBuilder::new();
187		for own_registration_utxo in own_registration_utxos {
188			inputs.add_script_utxo_input(
189				own_registration_utxo,
190				validator,
191				&register_redeemer_data(),
192				&costs.get_one_spend(),
193			)?;
194		}
195		inputs.add_regular_inputs(&[registration_utxo.clone()])?;
196		tx_builder.set_inputs(&inputs);
197	}
198
199	{
200		let datum = candidate_registration_to_plutus_data(candidate_registration);
201		let amount_builder = TransactionOutputBuilder::new()
202			.with_address(&validator.address(ctx.network))
203			.with_plutus_data(&datum)
204			.next()?;
205		let output = amount_builder.with_minimum_ada(ctx)?.build()?;
206		tx_builder.add_output(&output)?;
207	}
208
209	Ok(tx_builder.balance_update_and_build(ctx)?)
210}
211
212fn deregister_tx(
213	validator: &PlutusScript,
214	own_registration_utxos: &[OgmiosUtxo],
215	costs: Costs,
216	ctx: &TransactionContext,
217) -> anyhow::Result<Transaction> {
218	let config = crate::csl::get_builder_config(ctx)?;
219	let mut tx_builder = TransactionBuilder::new(&config);
220
221	{
222		let mut inputs = TxInputsBuilder::new();
223		for own_registration_utxo in own_registration_utxos {
224			inputs.add_script_utxo_input(
225				own_registration_utxo,
226				validator,
227				&register_redeemer_data(),
228				&costs.get_one_spend(),
229			)?;
230		}
231		tx_builder.set_inputs(&inputs);
232	}
233
234	Ok(tx_builder.balance_update_and_build(ctx)?)
235}
236
237fn register_redeemer_data() -> PlutusData {
238	unit_plutus_data()
239}
240
241#[cfg(test)]
242mod tests {
243	use super::register_tx;
244	use crate::csl::{Costs, OgmiosUtxoExt, TransactionContext};
245	use crate::test_values::{self, *};
246	use cardano_serialization_lib::{Address, NetworkIdKind, Transaction, TransactionInputs};
247	use ogmios_client::types::OgmiosValue;
248	use ogmios_client::types::{OgmiosTx, OgmiosUtxo};
249	use partner_chains_plutus_data::registered_candidates::candidate_registration_to_plutus_data;
250	use proptest::{
251		array::uniform32,
252		collection::{hash_set, vec},
253		prelude::*,
254	};
255
256	use sidechain_domain::{
257		AdaBasedStaking, CandidateKeys, CandidateRegistration, MainchainKeyHash,
258		MainchainSignature, McTxHash, SidechainPublicKey, SidechainSignature, UtxoId, UtxoIndex,
259	};
260
261	fn sum_lovelace(utxos: &[OgmiosUtxo]) -> u64 {
262		utxos.iter().map(|utxo| utxo.value.lovelace).sum()
263	}
264
265	const MIN_UTXO_LOVELACE: u64 = 1000000;
266	const FIVE_ADA: u64 = 5000000;
267
268	fn own_pkh() -> MainchainKeyHash {
269		MainchainKeyHash([0; 28])
270	}
271	fn candidate_registration(registration_utxo: UtxoId) -> CandidateRegistration {
272		CandidateRegistration {
273			stake_ownership: AdaBasedStaking {
274				pub_key: test_values::stake_pool_pub_key(),
275				signature: MainchainSignature([0u8; 64]),
276			},
277			partner_chain_pub_key: SidechainPublicKey(Vec::new()),
278			partner_chain_signature: SidechainSignature(Vec::new()),
279			registration_utxo,
280			own_pkh: own_pkh(),
281			keys: CandidateKeys(vec![]),
282		}
283	}
284
285	fn lesser_payment_utxo() -> OgmiosUtxo {
286		make_utxo(1u8, 0, 1200000, &payment_addr())
287	}
288
289	fn greater_payment_utxo() -> OgmiosUtxo {
290		make_utxo(4u8, 1, 1200001, &payment_addr())
291	}
292
293	fn registration_utxo() -> OgmiosUtxo {
294		make_utxo(11u8, 0, 1000000, &payment_addr())
295	}
296
297	fn validator_addr() -> Address {
298		Address::from_bech32("addr_test1wpha4546lvfcau5jsrwpht9h6350m3au86fev6nwmuqz9gqer2ung")
299			.unwrap()
300	}
301
302	#[test]
303	fn register_tx_regression_test() {
304		let payment_key_utxos =
305			vec![lesser_payment_utxo(), greater_payment_utxo(), registration_utxo()];
306		let ctx = TransactionContext {
307			payment_key: payment_key(),
308			payment_key_utxos: payment_key_utxos.clone(),
309			network: NetworkIdKind::Testnet,
310			protocol_parameters: protocol_parameters(),
311			change_address: payment_addr(),
312		};
313		let own_registration_utxos = vec![payment_key_utxos.get(1).unwrap().clone()];
314		let registration_utxo = payment_key_utxos.first().unwrap();
315		let candidate_registration = candidate_registration(registration_utxo.utxo_id());
316		let tx = register_tx(
317			&test_values::test_validator(),
318			&candidate_registration,
319			registration_utxo,
320			&own_registration_utxos,
321			Costs::ZeroCosts,
322			&ctx,
323		)
324		.unwrap();
325
326		let body = tx.body();
327		let inputs = body.inputs();
328		// Both inputs are used to cover transaction
329		assert_eq!(
330			inputs.get(0).to_string(),
331			"0101010101010101010101010101010101010101010101010101010101010101#0"
332		);
333		assert_eq!(
334			inputs.get(1).to_string(),
335			"0404040404040404040404040404040404040404040404040404040404040404#1"
336		);
337		let outputs = body.outputs();
338
339		let script_output = outputs.into_iter().find(|o| o.address() == validator_addr()).unwrap();
340		let coins_sum = script_output.amount().coin().checked_add(&body.fee()).unwrap();
341		assert_eq!(
342			coins_sum,
343			(greater_payment_utxo().value.lovelace + lesser_payment_utxo().value.lovelace).into()
344		);
345		assert_eq!(
346			script_output.plutus_data().unwrap(),
347			candidate_registration_to_plutus_data(&candidate_registration)
348		);
349	}
350
351	fn register_transaction_balancing_test(payment_utxos: Vec<OgmiosUtxo>) {
352		let payment_key_utxos = payment_utxos.clone();
353		let ctx = TransactionContext {
354			payment_key: payment_key(),
355			payment_key_utxos: payment_key_utxos.clone(),
356			network: NetworkIdKind::Testnet,
357			protocol_parameters: protocol_parameters(),
358			change_address: payment_addr(),
359		};
360		let registration_utxo = payment_key_utxos.first().unwrap();
361		let candidate_registration = candidate_registration(registration_utxo.utxo_id());
362		let own_registration_utxos = if payment_utxos.len() >= 2 {
363			vec![payment_utxos.get(1).unwrap().clone()]
364		} else {
365			Vec::new()
366		};
367		let tx = register_tx(
368			&test_values::test_validator(),
369			&candidate_registration,
370			registration_utxo,
371			&own_registration_utxos,
372			Costs::ZeroCosts,
373			&ctx,
374		)
375		.unwrap();
376
377		let validator_address = &test_values::test_validator().address(ctx.network);
378
379		used_inputs_lovelace_equals_outputs_and_fee(&tx, &payment_key_utxos.clone());
380		fee_is_less_than_one_and_half_ada(&tx);
381		output_at_validator_has_register_candidate_datum(
382			&tx,
383			&candidate_registration,
384			validator_address,
385		);
386		spends_own_registration_utxos(&tx, &own_registration_utxos);
387	}
388
389	fn match_inputs(inputs: &TransactionInputs, payment_utxos: &[OgmiosUtxo]) -> Vec<OgmiosUtxo> {
390		inputs
391			.into_iter()
392			.map(|input| {
393				payment_utxos
394					.iter()
395					.find(|utxo| utxo.to_csl_tx_input() == *input)
396					.unwrap()
397					.clone()
398			})
399			.collect()
400	}
401
402	fn used_inputs_lovelace_equals_outputs_and_fee(tx: &Transaction, payment_utxos: &[OgmiosUtxo]) {
403		let used_inputs: Vec<OgmiosUtxo> = match_inputs(&tx.body().inputs(), payment_utxos);
404		let used_inputs_value: u64 = sum_lovelace(&used_inputs);
405		let outputs_lovelace_sum: u64 = tx
406			.body()
407			.outputs()
408			.into_iter()
409			.map(|output| {
410				let value: u64 = output.amount().coin().into();
411				value
412			})
413			.sum();
414		let fee: u64 = tx.body().fee().into();
415		// Used inputs are qual to the sum of the outputs plus the fee
416		assert_eq!(used_inputs_value, outputs_lovelace_sum + fee);
417	}
418
419	// Exact fee depends on inputs and outputs, but it definately is less than 1.5 ADA
420	fn fee_is_less_than_one_and_half_ada(tx: &Transaction) {
421		assert!(tx.body().fee() <= 1500000u64.into());
422	}
423
424	fn output_at_validator_has_register_candidate_datum(
425		tx: &Transaction,
426		candidate_registration: &CandidateRegistration,
427		validator_address: &Address,
428	) {
429		let outputs = tx.body().outputs();
430		let validator_output =
431			outputs.into_iter().find(|o| o.address() == *validator_address).unwrap();
432		assert_eq!(
433			validator_output.plutus_data().unwrap(),
434			candidate_registration_to_plutus_data(candidate_registration)
435		);
436	}
437
438	fn spends_own_registration_utxos(tx: &Transaction, own_registration_utxos: &[OgmiosUtxo]) {
439		let inputs = tx.body().inputs();
440		assert!(
441			own_registration_utxos
442				.iter()
443				.all(|p| inputs.into_iter().any(|i| *i == p.to_csl_tx_input()))
444		);
445	}
446
447	proptest! {
448		#[test]
449		fn spends_input_utxo_and_outputs_to_validator_address(payment_utxos in arb_payment_utxos(10)
450			.prop_filter("Inputs total lovelace too low", |utxos| sum_lovelace(utxos) > 4000000)) {
451			register_transaction_balancing_test(payment_utxos)
452		}
453	}
454
455	prop_compose! {
456		// Set is needed to be used, because we have to avoid UTXOs with the same id.
457		fn arb_payment_utxos(n: usize)
458			(utxo_ids in hash_set(arb_utxo_id(), 1..n))
459			(utxo_ids in Just(utxo_ids.clone()), values in vec(arb_utxo_lovelace(), utxo_ids.len())
460		) -> Vec<OgmiosUtxo> {
461			utxo_ids.into_iter().zip(values.into_iter()).map(|(utxo_id, value)| OgmiosUtxo {
462				transaction: OgmiosTx { id: utxo_id.tx_hash.0 },
463				index: utxo_id.index.0,
464				value,
465				address: PAYMENT_ADDR.into(),
466				..Default::default()
467			}).collect()
468		}
469	}
470
471	prop_compose! {
472		fn arb_utxo_lovelace()(value in MIN_UTXO_LOVELACE..FIVE_ADA) -> OgmiosValue {
473			OgmiosValue::new_lovelace(value)
474		}
475	}
476
477	prop_compose! {
478		fn arb_utxo_id()(tx_hash in uniform32(0u8..255u8), index in any::<u16>()) -> UtxoId {
479			UtxoId {
480				tx_hash: McTxHash(tx_hash),
481				index: UtxoIndex(index),
482			}
483		}
484	}
485}