From 220f4a100334cd2d50579bd9ea2e53690c83c618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lech=20G=C5=82owiak?= Date: Thu, 12 Dec 2024 14:33:37 +0100 Subject: [PATCH] feat: add init-reserve-management smart contract execution --- toolkit/offchain/src/csl.rs | 8 +- toolkit/offchain/src/d_param/mod.rs | 8 +- toolkit/offchain/src/init_governance/mod.rs | 19 +- toolkit/offchain/src/lib.rs | 1 + toolkit/offchain/src/plutus_script.rs | 2 +- toolkit/offchain/src/reserve/init.rs | 265 ++++++++++++++++++++ toolkit/offchain/src/reserve/mod.rs | 3 + toolkit/offchain/tests/integration_tests.rs | 31 +++ 8 files changed, 324 insertions(+), 13 deletions(-) create mode 100644 toolkit/offchain/src/reserve/init.rs create mode 100644 toolkit/offchain/src/reserve/mod.rs diff --git a/toolkit/offchain/src/csl.rs b/toolkit/offchain/src/csl.rs index 8a4f304f3..1971bbbca 100644 --- a/toolkit/offchain/src/csl.rs +++ b/toolkit/offchain/src/csl.rs @@ -184,7 +184,7 @@ impl ScriptExUnits { pub(crate) fn get_validator_budgets( mut responses: Vec, -) -> Result { +) -> ScriptExUnits { responses.sort_by_key(|r| r.validator.index); let (mint_ex_units, spend_ex_units) = responses .into_iter() @@ -192,7 +192,7 @@ pub(crate) fn get_validator_budgets( let mint_ex_units = mint_ex_units.into_iter().map(ex_units_from_response).collect(); let spend_ex_units = spend_ex_units.into_iter().map(ex_units_from_response).collect(); - Ok(ScriptExUnits { mint_ex_units, spend_ex_units }) + ScriptExUnits { mint_ex_units, spend_ex_units } } fn ex_units_from_response(resp: OgmiosEvaluateTransactionResponse) -> ExUnits { @@ -208,6 +208,10 @@ pub(crate) fn empty_asset_name() -> AssetName { AssetName::new(vec![]).expect("Hardcoded empty asset name is valid") } +pub fn zero_ex_units() -> ExUnits { + ExUnits::new(&BigNum::zero(), &BigNum::zero()) +} + pub(crate) trait OgmiosUtxoExt { fn to_csl_tx_input(&self) -> TransactionInput; fn to_csl_tx_output(&self) -> Result; diff --git a/toolkit/offchain/src/d_param/mod.rs b/toolkit/offchain/src/d_param/mod.rs index 0458700e9..f7405fea5 100644 --- a/toolkit/offchain/src/d_param/mod.rs +++ b/toolkit/offchain/src/d_param/mod.rs @@ -146,7 +146,7 @@ where hex::encode(tx.to_bytes()) ) })?; - let mut mint_witness_ex_units = get_validator_budgets(evaluate_response)?; + let mut mint_witness_ex_units = get_validator_budgets(evaluate_response); mint_witness_ex_units.mint_ex_units.reverse(); let tx = mint_d_param_token_tx( validator, @@ -206,7 +206,7 @@ where hex::encode(tx.to_bytes()) ) })?; - let spend_ex_units = get_validator_budgets(evaluate_response)?; + let spend_ex_units = get_validator_budgets(evaluate_response); let tx = update_d_param_tx( validator, @@ -272,8 +272,6 @@ fn mint_d_param_token_tx( .unwrap_or_else(|| panic!("Mint ex units not found")), )?; - let tx_hash = TransactionHash::from_bytes(gov_utxo.transaction.id.into())?; - let gov_tx_input = TransactionInput::new(&tx_hash, gov_utxo.index.into()); tx_builder.add_script_reference_input(&gov_tx_input, gov_policy.bytes.len()); tx_builder.add_required_signer(&ctx.payment_key_hash()); tx_builder.balance_update_and_build(ctx) @@ -327,8 +325,6 @@ fn update_d_param_tx( .unwrap_or_else(|| panic!("Mint ex units not found")), )?; - let tx_hash = TransactionHash::from_bytes(gov_utxo.transaction.id.into())?; - let gov_tx_input = TransactionInput::new(&tx_hash, gov_utxo.index.into()); tx_builder.add_script_reference_input(&gov_tx_input, gov_policy.bytes.len()); tx_builder.add_required_signer(&ctx.payment_key_hash()); tx_builder.balance_update_and_build(ctx) diff --git a/toolkit/offchain/src/init_governance/mod.rs b/toolkit/offchain/src/init_governance/mod.rs index 3efd6482a..7a92a933b 100644 --- a/toolkit/offchain/src/init_governance/mod.rs +++ b/toolkit/offchain/src/init_governance/mod.rs @@ -1,4 +1,5 @@ use crate::csl::OgmiosUtxoExt; +use crate::plutus_script; use crate::scripts_data; use crate::{ await_tx::{AwaitTx, FixedDelayRetries}, @@ -164,13 +165,20 @@ pub async fn get_governance_utxo ScriptHash { - self.policy_script.hash() + self.policy_script.csl_script_hash() + } + + pub(crate) fn utxo_id_as_tx_input(&self) -> TransactionInput { + TransactionInput::new( + &TransactionHash::from_bytes(self.utxo_id.tx_hash.0.to_vec()).unwrap(), + self.utxo_id.index.0.into(), + ) } } @@ -181,7 +189,10 @@ pub(crate) async fn get_governance_data anyhow::Result { Ok(Self::from_cbor(&unwrap_one_layer_of_cbor(cbor)?, language)) diff --git a/toolkit/offchain/src/reserve/init.rs b/toolkit/offchain/src/reserve/init.rs new file mode 100644 index 000000000..f579051ec --- /dev/null +++ b/toolkit/offchain/src/reserve/init.rs @@ -0,0 +1,265 @@ +//! Initialization of the reserve management is execution of three similar transaction to +//! initialize three scripts: Rerserve Management Validator, Reserve Management Policy and +//! Illiquid Circulation Supply Validator. +//! +//! Transaction for each of these scripts should have: +//! * an output to Version Oracle Validator address that should: +//! * * have script reference with the script being initialized attached, script should be applied with Version Oracle Policy Id +//! * * contain 1 of token Version Oracle Policy with "Version oracle" asset name, minted in this transaction +//! * * * mint redeemer should be Constr(1, [Int: SCRIPT_ID, Bytes: Applied Script Bytes]) +//! * * have Plutus Data that is [Int: SCRIPT_ID, Bytes: Version Oracle Policy Id] +//! * an output to the current governance that should: +//! * * contain a new Goveranance Policy token, minted in this transaction, +//! * * * mint redeemer should be empty contructor Plutus Data +//! * a script reference rnput of the current Goveranance UTXO +//! * signature of the current goveranance + +use crate::{ + await_tx::AwaitTx, + csl::{ + get_builder_config, get_validator_budgets, zero_ex_units, TransactionBuilderExt, + TransactionContext, + }, + init_governance::{get_governance_data, GovernanceData}, + plutus_script::PlutusScript, + scripts_data::{self, VersionOracleData}, +}; +use anyhow::anyhow; +use cardano_serialization_lib::{ + AssetName, Assets, BigNum, ConstrPlutusData, DataCost, ExUnits, Int, JsError, LanguageKind, + MinOutputAdaCalculator, MintBuilder, MintWitness, MultiAsset, PlutusData, PlutusList, + PlutusScriptSource, Redeemer, RedeemerTag, ScriptHash, ScriptRef, Transaction, + TransactionBuilder, TransactionOutputBuilder, +}; +use ogmios_client::{ + query_ledger_state::{QueryLedgerState, QueryUtxoByUtxoId}, + query_network::QueryNetwork, + transactions::{OgmiosEvaluateTransactionResponse, Transactions}, +}; +use raw_scripts::{ILLIQUID_CIRCULATION_SUPPLY_VALIDATOR, RESERVE_AUTH_POLICY, RESERVE_VALIDATOR}; +use sidechain_domain::{McTxHash, UtxoId}; +use std::collections::HashMap; + +pub async fn init_reserve_management< + T: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId, + A: AwaitTx, +>( + genesis_utxo: UtxoId, + payment_key: [u8; 32], + client: &T, + await_tx: &A, +) -> anyhow::Result> { + let reserve_validator = + ScriptData::new("Reserve Management Validator", RESERVE_VALIDATOR.to_vec(), 28); + let reserve_policy = + ScriptData::new("Reserve Management Policy", RESERVE_AUTH_POLICY.to_vec(), 29); + let ics_validator = ScriptData::new( + "Illiquid Circulation Validator", + ILLIQUID_CIRCULATION_SUPPLY_VALIDATOR.to_vec(), + 30, + ); + Ok(vec![ + initialize_script(reserve_validator, genesis_utxo, payment_key, client, await_tx).await?, + initialize_script(reserve_policy, genesis_utxo, payment_key, client, await_tx).await?, + initialize_script(ics_validator, genesis_utxo, payment_key, client, await_tx).await?, + ] + .into_iter() + .flatten() + .collect()) +} + +struct ScriptData { + name: String, + plutus_script: PlutusScript, + id: u16, +} + +impl ScriptData { + fn new(name: &str, raw_bytes: Vec, id: u16) -> Self { + let plutus_script = PlutusScript::from_wrapped_cbor(&raw_bytes, LanguageKind::PlutusV2) + .expect("Plutus script should be valid"); + Self { name: name.to_string(), plutus_script, id } + } + + fn applied_plutus_script( + &self, + version_oracle: &VersionOracleData, + ) -> Result { + let policy = version_oracle.policy.script_hash(); + self.plutus_script + .clone() + .apply_uplc_data(uplc::PlutusData::BoundedBytes(policy.to_vec().into())) + .map_err(|e| JsError::from_str(&e.to_string())) + } +} + +/// TODO: make it idempotent is the next step +async fn initialize_script< + T: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId, + A: AwaitTx, +>( + script: ScriptData, + genesis_utxo: UtxoId, + payment_key: [u8; 32], + client: &T, + await_tx: &A, +) -> anyhow::Result> { + let ctx = TransactionContext::for_payment_key(payment_key, client).await?; + let governance = get_governance_data(genesis_utxo, client).await?; + let version_oracle = scripts_data::version_oracle(genesis_utxo, ctx.network)?; + + let tx_to_evaluate = init_script_tx( + &script, + &governance, + zero_ex_units(), + &version_oracle, + zero_ex_units(), + &ctx, + )?; + let evaluate_response = client.evaluate_transaction(&tx_to_evaluate.to_bytes()).await?; + + let (version_oracle_ex_units, governance_ex_units) = match_costs( + &tx_to_evaluate, + &version_oracle.policy.csl_script_hash(), + &governance.policy_script_hash(), + evaluate_response, + )?; + + let tx = init_script_tx( + &script, + &governance, + governance_ex_units, + &version_oracle, + version_oracle_ex_units, + &ctx, + )?; + let signed_tx = ctx.sign(&tx).to_bytes(); + let res = client.submit_transaction(&signed_tx).await.map_err(|e| { + anyhow!( + "Initialize Versioned '{}' transaction request failed: {}, tx bytes: {}", + script.name, + e, + hex::encode(signed_tx) + ) + })?; + let tx_id = res.transaction.id; + log::info!( + "Initialized Versioned '{}' transaction submitted: {}", + script.name, + hex::encode(tx_id) + ); + await_tx.await_tx_output(client, UtxoId::new(tx_id, 0)).await?; + Ok(Some(McTxHash(tx_id))) +} + +fn init_script_tx( + script: &ScriptData, + governance: &GovernanceData, + governance_script_cost: ExUnits, + version_oracle: &VersionOracleData, + versioning_script_cost: ExUnits, + ctx: &TransactionContext, +) -> Result { + let mut tx_builder = TransactionBuilder::new(&get_builder_config(ctx)?); + + let applied_script = script.applied_plutus_script(version_oracle)?; + { + let mut mint_builder = tx_builder.get_mint_builder().unwrap_or(MintBuilder::new()); + let mint_witness = MintWitness::new_plutus_script( + &PlutusScriptSource::new(&version_oracle.policy.to_csl()), + &Redeemer::new( + &RedeemerTag::new_mint(), + &0u32.into(), + &PlutusData::new_constr_plutus_data(&ConstrPlutusData::new( + &BigNum::one(), + &version_oracle_plutus_list(script.id, &applied_script.script_hash()), + )), + &versioning_script_cost, + ), + ); + mint_builder.add_asset(&mint_witness, &version_oracle_asset_name(), &Int::new_i32(1))?; + tx_builder.set_mint_builder(&mint_builder); + } + { + let script_ref = ScriptRef::new_plutus_script(&applied_script.to_csl()); + let amount_builder = TransactionOutputBuilder::new() + .with_address(&version_oracle.validator.address(ctx.network)) + .with_plutus_data(&PlutusData::new_list(&version_oracle_plutus_list( + script.id, + &version_oracle.policy_id().0, + ))) + .with_script_ref(&script_ref) + .next()?; + let mut ma = MultiAsset::new(); + let mut assets = Assets::new(); + assets.insert(&version_oracle_asset_name(), &1u64.into()); + ma.insert(&version_oracle.policy_id().0.into(), &assets); + let output = amount_builder.with_coin_and_asset(&0u64.into(), &ma).build()?; + let min_ada = MinOutputAdaCalculator::new( + &output, + &DataCost::new_coins_per_byte( + &ctx.protocol_parameters.min_utxo_deposit_coefficient.into(), + ), + ) + .calculate_ada()?; + let output = amount_builder.with_coin_and_asset(&min_ada, &ma).build()?; + tx_builder.add_output(&output)?; + } + // Mint governance token + let gov_tx_input = governance.utxo_id_as_tx_input(); + tx_builder.add_mint_one_script_token_using_reference_script( + &governance.policy_script, + &gov_tx_input, + governance_script_cost, + )?; + + tx_builder.add_script_reference_input(&gov_tx_input, governance.policy_script.bytes.len()); + tx_builder.add_required_signer(&ctx.payment_key_hash()); + tx_builder.balance_update_and_build(ctx) +} + +fn version_oracle_asset_name() -> AssetName { + AssetName::new(b"Version oracle".to_vec()).unwrap() +} + +fn version_oracle_plutus_list(script_id: u16, script_hash: &[u8]) -> PlutusList { + let mut list = PlutusList::new(); + list.add(&PlutusData::new_integer(&script_id.into())); + list.add(&PlutusData::new_bytes(script_hash.to_vec())); + list +} + +fn match_costs( + evaluated_transaction: &Transaction, + version_oracle_policy: &ScriptHash, + governance_policy: &ScriptHash, + evaluate_response: Vec, +) -> Result<(ExUnits, ExUnits), anyhow::Error> { + let mint_keys = evaluated_transaction + .body() + .mint() + .expect("Every Init Reserve Management transaction should have two mints") + .keys(); + let script_to_index: HashMap = + vec![(mint_keys.get(0), 0), (mint_keys.get(1), 1)].into_iter().collect(); + let mint_ex_units = get_validator_budgets(evaluate_response).mint_ex_units; + if mint_ex_units.len() == 2 { + let version_policy_idx = script_to_index + .get(version_oracle_policy) + .expect("Version Oracle Policy script is present in transaction mints") + .clone(); + let version_oracle_ex_units = mint_ex_units + .get(version_policy_idx) + .expect("mint_ex_units have two items") + .clone(); + let gov_policy_idx = script_to_index + .get(governance_policy) + .expect("Governance Policy script is present in transaction mints") + .clone(); + let governance_ex_units = + mint_ex_units.get(gov_policy_idx).expect("mint_ex_units have two items").clone(); + Ok((version_oracle_ex_units, governance_ex_units)) + } else { + Err(anyhow!("Could not build transaction to submit, evaluate response has wrong number of mint keys.")) + } +} diff --git a/toolkit/offchain/src/reserve/mod.rs b/toolkit/offchain/src/reserve/mod.rs new file mode 100644 index 000000000..89296fb03 --- /dev/null +++ b/toolkit/offchain/src/reserve/mod.rs @@ -0,0 +1,3 @@ +//! All smart-contracts related to rewads token reserve management + +pub mod init; diff --git a/toolkit/offchain/tests/integration_tests.rs b/toolkit/offchain/tests/integration_tests.rs index a9f9bdd9a..34d7133e1 100644 --- a/toolkit/offchain/tests/integration_tests.rs +++ b/toolkit/offchain/tests/integration_tests.rs @@ -18,6 +18,7 @@ use partner_chains_cardano_offchain::{ await_tx::{AwaitTx, FixedDelayRetries}, d_param, init_governance, register::Register, + reserve, }; use sidechain_domain::{ AdaBasedStaking, AuraPublicKey, CandidateRegistration, DParameter, GrandpaPublicKey, @@ -75,6 +76,36 @@ async fn upsert_d_param() { assert!(run_upsert_d_param(genesis_utxo, 1, 1, &client).await.is_some()) } +#[tokio::test] +async fn init_reserve() { + let _ = env_logger::builder().is_test(true).try_init(); + let image = GenericImage::new(TEST_IMAGE, TEST_IMAGE_TAG); + let client = Cli::default(); + let container = client.run(image); + let client = initialize(&container).await; + let genesis_utxo = run_init_goveranance(&client).await; + let txs = reserve::init::init_reserve_management( + genesis_utxo, + GOVERNANCE_AUTHORITY_PAYMENT_KEY.0, + &client, + &FixedDelayRetries::new(Duration::from_millis(500), 100), + ) + .await + .unwrap(); + assert_eq!(txs.len(), 3); + + // TODO: Implement idempotency in the following PR + // let txs = reserve::init::init_reserve_management( + // genesis_utxo, + // GOVERNANCE_AUTHORITY_PAYMENT_KEY.0, + // &client, + // &FixedDelayRetries::new(Duration::from_millis(500), 100), + // ) + // .await + // .unwrap(); + // assert_eq!(txs.len(), 0) +} + #[tokio::test] async fn register() { let _ = env_logger::builder().is_test(true).try_init();