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
22pub 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
91pub 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 ®ister_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 ®ister_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 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 assert_eq!(used_inputs_value, outputs_lovelace_sum + fee);
417 }
418
419 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 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}