partner_chains_cardano_offchain/
versioning_system.rs

1use crate::{
2	await_tx::AwaitTx,
3	cardano_keys::CardanoPaymentSigningKey,
4	csl::{
5		CostStore, Costs, InputsBuilderExt, MultiAssetExt, OgmiosUtxoExt, TransactionBuilderExt,
6		TransactionContext, TransactionExt, TransactionOutputAmountBuilderExt, get_builder_config,
7	},
8	governance::GovernanceData,
9	multisig::{MultiSigSmartContractResult, submit_or_create_tx_to_sign},
10	plutus_script::PlutusScript,
11	scripts_data::{self, PlutusScriptData},
12};
13use anyhow::anyhow;
14use cardano_serialization_lib::{
15	AssetName, BigInt, MultiAsset, PlutusData, ScriptRef, Transaction, TransactionBuilder,
16	TransactionOutputBuilder, TxInputsBuilder,
17};
18use ogmios_client::{
19	query_ledger_state::{QueryLedgerState, QueryUtxoByUtxoId},
20	query_network::QueryNetwork,
21	transactions::Transactions,
22	types::OgmiosUtxo,
23};
24use partner_chains_plutus_data::version_oracle::{VersionOracleDatum, VersionOraclePolicyRedeemer};
25use raw_scripts::ScriptId;
26use sidechain_domain::UtxoId;
27
28pub(crate) struct ScriptData {
29	name: String,
30	plutus_script: PlutusScript,
31	id: ScriptId,
32}
33
34impl ScriptData {
35	pub(crate) fn new(name: &str, raw_bytes: Vec<u8>, id: ScriptId) -> Self {
36		let plutus_script =
37			PlutusScript::v2_from_cbor(&raw_bytes).expect("Plutus script should be valid");
38		Self { name: name.to_string(), plutus_script, id }
39	}
40}
41
42pub(crate) async fn initialize_script<
43	T: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId,
44	A: AwaitTx,
45>(
46	script: ScriptData,
47	genesis_utxo: UtxoId,
48	payment_key: &CardanoPaymentSigningKey,
49	client: &T,
50	await_tx: &A,
51) -> anyhow::Result<Option<MultiSigSmartContractResult>> {
52	let payment_ctx = TransactionContext::for_payment_key(payment_key, client).await?;
53	let governance = GovernanceData::get(genesis_utxo, client).await?;
54	let version_oracle = scripts_data::version_oracle(genesis_utxo, payment_ctx.network)?;
55
56	if script_is_initialized(&script, &version_oracle, &payment_ctx, client).await? {
57		log::info!("Script '{}' is already initialized", script.name);
58		return Ok(None);
59	}
60	Ok(Some(
61		submit_or_create_tx_to_sign(
62			&governance,
63			payment_ctx,
64			|costs, ctx| init_script_tx(&script, &governance, &version_oracle, costs, &ctx),
65			&format!("Init {}", script.name),
66			client,
67			await_tx,
68		)
69		.await?,
70	))
71}
72
73pub(crate) async fn update_script<
74	T: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId,
75	A: AwaitTx,
76>(
77	script: ScriptData,
78	genesis_utxo: UtxoId,
79	old_versioned_utxo: OgmiosUtxo,
80	payment_key: &CardanoPaymentSigningKey,
81	client: &T,
82	await_tx: &A,
83) -> anyhow::Result<Option<MultiSigSmartContractResult>> {
84	let payment_ctx = TransactionContext::for_payment_key(payment_key, client).await?;
85	let governance = GovernanceData::get(genesis_utxo, client).await?;
86	let version_oracle = scripts_data::version_oracle(genesis_utxo, payment_ctx.network)?;
87
88	if !script_is_initialized(&script, &version_oracle, &payment_ctx, client).await? {
89		log::info!("Script '{}' isn't initialized yet", script.name);
90		return Ok(None);
91	}
92	Ok(Some(
93		submit_or_create_tx_to_sign(
94			&governance,
95			payment_ctx,
96			|costs, ctx| {
97				update_script_tx(
98					&script,
99					&governance,
100					&version_oracle,
101					&old_versioned_utxo,
102					costs,
103					&ctx,
104				)
105			},
106			&format!("Update {}", script.name),
107			client,
108			await_tx,
109		)
110		.await?,
111	))
112}
113
114/// Upserts a Plutus script into the versioning system
115pub async fn upsert_script<
116	T: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId,
117	A: AwaitTx,
118>(
119	plutus_script: PlutusScript,
120	script_id: u32,
121	genesis_utxo: UtxoId,
122	payment_key: &CardanoPaymentSigningKey,
123	client: &T,
124	await_tx: &A,
125) -> anyhow::Result<Option<MultiSigSmartContractResult>> {
126	let payment_ctx = TransactionContext::for_payment_key(payment_key, client).await?;
127	let version_oracle = scripts_data::version_oracle(genesis_utxo, payment_ctx.network)?;
128
129	let script_id = script_id.try_into().map_err(|_| anyhow!("Invalid script id"))?;
130	let versioned_utxo = find_script_utxo(script_id, &version_oracle, &payment_ctx, client).await?;
131	let script_data = ScriptData { name: format!("{:?}", script_id), plutus_script, id: script_id };
132
133	match versioned_utxo {
134		Some(utxo) => {
135			update_script(script_data, genesis_utxo, utxo, payment_key, client, await_tx).await
136		},
137
138		None => initialize_script(script_data, genesis_utxo, payment_key, client, await_tx).await,
139	}
140}
141
142fn init_script_tx(
143	script: &ScriptData,
144	governance: &GovernanceData,
145	version_oracle: &PlutusScriptData,
146	costs: Costs,
147	ctx: &TransactionContext,
148) -> anyhow::Result<Transaction> {
149	let mut tx_builder = TransactionBuilder::new(&get_builder_config(ctx)?);
150	{
151		tx_builder.add_mint_one_script_token(
152			&version_oracle.policy,
153			&version_oracle_asset_name(),
154			&VersionOraclePolicyRedeemer::MintVersionOracle(
155				script.id.into(),
156				script.plutus_script.script_hash().into(),
157			)
158			.into(),
159			&costs.get_mint(&version_oracle.policy.clone()),
160		)?;
161	}
162	{
163		let script_ref = ScriptRef::new_plutus_script(&script.plutus_script.to_csl());
164		let amount_builder = TransactionOutputBuilder::new()
165			.with_address(&version_oracle.validator.address(ctx.network))
166			.with_plutus_data(
167				&VersionOracleDatum {
168					version_oracle: script.id.into(),
169					currency_symbol: version_oracle.policy.script_hash().into(),
170				}
171				.into(),
172			)
173			.with_script_ref(&script_ref)
174			.next()?;
175		let ma = MultiAsset::new()
176			.with_asset_amount(&version_oracle.policy.asset(version_oracle_asset_name())?, 1u64)?;
177		let output = amount_builder.with_minimum_ada_and_asset(&ma, ctx)?.build()?;
178		tx_builder.add_output(&output)?;
179	}
180	// Mint governance token
181	let gov_tx_input = governance.utxo_id_as_tx_input();
182	tx_builder.add_mint_one_script_token_using_reference_script(
183		&governance.policy.script(),
184		&gov_tx_input,
185		&costs,
186	)?;
187	Ok(tx_builder.balance_update_and_build(ctx)?.remove_native_script_witnesses())
188}
189
190fn update_script_tx(
191	script: &ScriptData,
192	governance: &GovernanceData,
193	version_oracle: &PlutusScriptData,
194	old_versioned_utxo: &OgmiosUtxo,
195	costs: Costs,
196	ctx: &TransactionContext,
197) -> anyhow::Result<Transaction> {
198	let mut tx_builder = TransactionBuilder::new(&get_builder_config(ctx)?);
199	{
200		tx_builder.set_inputs(&{
201			let mut inputs = TxInputsBuilder::new();
202			inputs.add_script_utxo_input(
203				&old_versioned_utxo,
204				&version_oracle.validator,
205				&PlutusData::new_integer(&BigInt::from(script.id as u32)),
206				&costs.get_one_spend(),
207			)?;
208
209			inputs
210		});
211	}
212	{
213		let script_ref = ScriptRef::new_plutus_script(&script.plutus_script.to_csl());
214		let amount_builder = TransactionOutputBuilder::new()
215			.with_address(&version_oracle.validator.address(ctx.network))
216			.with_plutus_data(
217				&VersionOracleDatum {
218					version_oracle: script.id.into(),
219					currency_symbol: version_oracle.policy.script_hash().into(),
220				}
221				.into(),
222			)
223			.with_script_ref(&script_ref)
224			.next()?;
225		let ma = MultiAsset::new()
226			.with_asset_amount(&version_oracle.policy.asset(version_oracle_asset_name())?, 1u64)?;
227		let output = amount_builder.with_minimum_ada_and_asset(&ma, ctx)?.build()?;
228		tx_builder.add_output(&output)?;
229	}
230	// Mint governance token
231	let gov_tx_input = governance.utxo_id_as_tx_input();
232	tx_builder.add_mint_one_script_token_using_reference_script(
233		&governance.policy.script(),
234		&gov_tx_input,
235		&costs,
236	)?;
237	Ok(tx_builder.balance_update_and_build(ctx)?.remove_native_script_witnesses())
238}
239
240fn version_oracle_asset_name() -> AssetName {
241	AssetName::new(b"Version oracle".to_vec()).unwrap()
242}
243
244// There exist UTXO at Version Oracle Validator with Datum that contains
245// * script id of the script being initialized
246// * Version Oracle Policy Id
247async fn script_is_initialized<T: QueryLedgerState>(
248	script: &ScriptData,
249	version_oracle: &PlutusScriptData,
250	ctx: &TransactionContext,
251	client: &T,
252) -> Result<bool, anyhow::Error> {
253	Ok(find_script_utxo(script.id, version_oracle, ctx, client).await?.is_some())
254}
255
256/// Finds an UTXO at Version Oracle Validator with Datum that contains
257/// * given script id
258/// * Version Oracle Policy Id
259pub(crate) async fn find_script_utxo<T: QueryLedgerState>(
260	script_id: ScriptId,
261	version_oracle: &PlutusScriptData,
262	ctx: &TransactionContext,
263	client: &T,
264) -> Result<Option<OgmiosUtxo>, anyhow::Error> {
265	let validator_address = version_oracle.validator.address(ctx.network).to_bech32(None)?;
266	let validator_utxos = client.query_utxos(&[validator_address]).await?;
267	// Decode datum from utxos and check if it contains script id
268	Ok(validator_utxos.into_iter().find(|utxo| {
269		utxo.get_plutus_data()
270			.and_then(|data| TryInto::<VersionOracleDatum>::try_into(data).ok())
271			.is_some_and(|datum| {
272				datum.version_oracle.script_id == script_id as u32
273					&& datum.currency_symbol == version_oracle.policy.script_hash().into()
274			})
275	}))
276}
277
278/// Gets an UTXO at Version Oracle Validator with correct Datum and reference script
279pub async fn get_script_utxo<T: QueryLedgerState + QueryNetwork>(
280	script_id: ScriptId,
281	version_oracle: &PlutusScriptData,
282	payment_key: &CardanoPaymentSigningKey,
283	client: &T,
284) -> Result<Option<OgmiosUtxo>, anyhow::Error> {
285	let ctx = TransactionContext::for_payment_key(payment_key, client).await?;
286	find_script_utxo(script_id, version_oracle, &ctx, client).await
287}
288
289#[cfg(test)]
290mod tests {
291	use super::{ScriptData, init_script_tx};
292	use crate::{
293		csl::{Costs, OgmiosUtxoExt, TransactionContext, unit_plutus_data},
294		governance::GovernanceData,
295		plutus_script,
296		scripts_data::{self, PlutusScriptData},
297		test_values::{
298			make_utxo, payment_addr, payment_key, protocol_parameters, test_governance_policy,
299		},
300	};
301	use cardano_serialization_lib::{
302		AssetName, BigNum, ExUnits, Int, NetworkIdKind, PlutusData, RedeemerTag, ScriptHash,
303		Transaction,
304	};
305	use ogmios_client::types::{OgmiosTx, OgmiosUtxo};
306	use partner_chains_plutus_data::version_oracle::VersionOraclePolicyRedeemer;
307	use raw_scripts::ScriptId;
308	use sidechain_domain::UtxoId;
309
310	#[test]
311	fn init_script_tx_version_oracle_output_test() {
312		let outputs = make_init_script_tx().body().outputs();
313		let voo = outputs
314			.into_iter()
315			.find(|o| o.address() == version_oracle_validator_address())
316			.expect("Init Script Transaction should have output to Version Oracle Validator");
317		let voo_script_ref = voo
318			.script_ref()
319			.expect("Version Oracle Validator output should have script reference");
320		let voo_plutus_script = voo_script_ref
321			.plutus_script()
322			.expect("Script reference should be Plutus script");
323		assert_eq!(voo_plutus_script, test_initialized_script().plutus_script.to_csl());
324		let amount = voo
325			.amount()
326			.multiasset()
327			.and_then(|ma| ma.get(&version_oracle_policy_csl_script_hash()))
328			.and_then(|vo_ma| vo_ma.get(&AssetName::new(b"Version oracle".to_vec()).unwrap()))
329			.expect("Version Oracle Validator output has a token of Version Oracle Policy");
330
331		assert_eq!(amount, 1u64.into());
332
333		let voo_plutus_data = voo
334			.plutus_data()
335			.and_then(|pd| pd.as_list())
336			.expect("Version Oracle Validator output should have Plutus Data of List type");
337		assert_eq!(
338			voo_plutus_data.get(0),
339			PlutusData::new_integer(&(test_initialized_script().id as u32).into())
340		);
341		assert_eq!(
342			voo_plutus_data.get(1),
343			PlutusData::new_bytes(version_oracle_data().policy.script_hash().to_vec())
344		);
345	}
346
347	#[test]
348	fn init_script_tx_change_output_test() {
349		let outputs = make_init_script_tx().body().outputs();
350		let change_output = outputs
351			.into_iter()
352			.find(|o| o.address() == payment_addr())
353			.expect("Change output has to be present to keep governance token")
354			.clone();
355		let gov_token_amount = change_output
356			.amount()
357			.multiasset()
358			.and_then(|ma| ma.get(&test_governance_data().policy.script().script_hash().into()))
359			.and_then(|gov_ma| gov_ma.get(&AssetName::new(vec![]).unwrap()))
360			.unwrap();
361		assert_eq!(gov_token_amount, BigNum::one(), "Change contains one governance token");
362	}
363
364	#[test]
365	fn init_script_tx_reference_input() {
366		// a script reference input of the current Governance UTXO
367		let ref_input = make_init_script_tx()
368			.body()
369			.reference_inputs()
370			.expect("Init transaction should have reference input")
371			.get(0);
372		assert_eq!(ref_input, test_governance_input().to_csl_tx_input());
373	}
374
375	#[test]
376	fn init_script_tx_mints() {
377		let tx = make_init_script_tx();
378		let vo_mint = tx
379			.body()
380			.mint()
381			.and_then(|mint| mint.get(&version_oracle_policy_csl_script_hash()))
382			.and_then(|assets| assets.get(0))
383			.and_then(|assets| assets.get(&AssetName::new(b"Version oracle".to_vec()).unwrap()))
384			.expect("Transaction should have a mint of Version Oracle Policy token");
385		assert_eq!(vo_mint, Int::new_i32(1));
386
387		let gov_mint = tx
388			.body()
389			.mint()
390			.and_then(|mint| mint.get(&test_governance_data().policy.script().script_hash().into()))
391			.and_then(|assets| assets.get(0))
392			.and_then(|assets| assets.get(&AssetName::new(vec![]).unwrap()))
393			.expect("Transaction should have a mint of Governance Policy token");
394		assert_eq!(gov_mint, Int::new_i32(1));
395	}
396
397	#[test]
398	fn init_script_tx_redeemers() {
399		// This is so cumbersome, because of the CSL interface for Redeemers
400		let ws = make_init_script_tx()
401			.witness_set()
402			.redeemers()
403			.expect("Transaction has two redeemers");
404		let redeemers = vec![ws.get(0), ws.get(1)];
405
406		let expected_vo_redeemer_data = {
407			let script_hash = test_initialized_script().plutus_script.script_hash();
408			VersionOraclePolicyRedeemer::MintVersionOracle(
409				test_initialized_script().id.into(),
410				script_hash.into(),
411			)
412			.into()
413		};
414
415		let _ = redeemers
416			.iter()
417			.find(|r| {
418				r.tag() == RedeemerTag::new_mint()
419					&& r.data() == expected_vo_redeemer_data
420					&& r.ex_units() == versioning_script_cost()
421			})
422			.expect("Transaction should have a mint redeemer for Version Oracle Policy");
423		let _ = redeemers
424			.iter()
425			.find(|r| {
426				r.tag() == RedeemerTag::new_mint()
427					&& r.data() == unit_plutus_data()
428					&& r.ex_units() == governance_script_cost()
429			})
430			.expect("Transaction should have a mint redeemer for Governance Policy");
431	}
432
433	fn make_init_script_tx() -> Transaction {
434		init_script_tx(
435			&test_initialized_script(),
436			&test_governance_data(),
437			&version_oracle_data(),
438			test_costs(),
439			&test_transaction_context(),
440		)
441		.unwrap()
442	}
443
444	fn test_initialized_script() -> ScriptData {
445		ScriptData::new(
446			"Test Script",
447			plutus_script![
448				raw_scripts::RESERVE_VALIDATOR,
449				version_oracle_data().policy_id_as_plutus_data()
450			]
451			.unwrap()
452			.bytes
453			.to_vec(),
454			ScriptId::ReserveValidator,
455		)
456	}
457
458	fn test_governance_input() -> OgmiosUtxo {
459		OgmiosUtxo { transaction: OgmiosTx { id: [16; 32] }, index: 0, ..Default::default() }
460	}
461
462	fn test_governance_data() -> GovernanceData {
463		GovernanceData { policy: test_governance_policy(), utxo: test_governance_input() }
464	}
465
466	fn version_oracle_data() -> PlutusScriptData {
467		scripts_data::version_oracle(UtxoId::new([255u8; 32], 0), NetworkIdKind::Testnet).unwrap()
468	}
469
470	fn version_oracle_validator_address() -> cardano_serialization_lib::Address {
471		version_oracle_data().validator.address(NetworkIdKind::Testnet)
472	}
473
474	fn version_oracle_policy_csl_script_hash() -> ScriptHash {
475		version_oracle_data().policy.csl_script_hash()
476	}
477
478	fn test_transaction_context() -> TransactionContext {
479		TransactionContext {
480			payment_key: payment_key(),
481			payment_key_utxos: vec![make_utxo(121u8, 3, 996272387, &payment_addr())],
482			network: NetworkIdKind::Testnet,
483			protocol_parameters: protocol_parameters(),
484			change_address: payment_addr(),
485		}
486	}
487
488	fn governance_script_cost() -> ExUnits {
489		ExUnits::new(&100u64.into(), &200u64.into())
490	}
491
492	fn versioning_script_cost() -> ExUnits {
493		ExUnits::new(&300u64.into(), &400u64.into())
494	}
495
496	fn test_costs() -> Costs {
497		Costs::new(
498			vec![
499				(test_governance_policy().script().script_hash().into(), governance_script_cost()),
500				(version_oracle_policy_csl_script_hash(), versioning_script_cost()),
501			]
502			.into_iter()
503			.collect(),
504			std::collections::HashMap::new(),
505		)
506	}
507}