From ecec82b971a06c293c63b52bb497c73f2e1ac48b Mon Sep 17 00:00:00 2001 From: Krisztian Pinter <159046756+kpinter-iohk@users.noreply.github.com> Date: Wed, 22 Jan 2025 20:46:32 +0100 Subject: [PATCH] ETCM-9222 ExUnit calculation (#414) --- Cargo.lock | 4 +- Cargo.toml | 2 +- toolkit/offchain/src/csl.rs | 105 ++++++++++++++++++++++- toolkit/offchain/src/plutus_script.rs | 27 ++---- toolkit/offchain/src/reserve/handover.rs | 93 ++++---------------- 5 files changed, 132 insertions(+), 99 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 77cbd3724..ed0eb1cb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1008,9 +1008,9 @@ dependencies = [ [[package]] name = "cardano-serialization-lib" -version = "13.2.0" +version = "13.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44ab8700b9a4bc42f48a06f9555ea28c7cb958a3cb470de5727b8e0d76c2a702" +checksum = "c65a355e3e660ce3493b499fadf40ae13b535ea49fbcfebb34359f0868b34c82" dependencies = [ "bech32 0.7.3", "cbor_event", diff --git a/Cargo.toml b/Cargo.toml index f5fdae4b2..7a6592025 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,7 +60,7 @@ codegen-units = 1 anyhow = "1.0.81" async-trait = "0.1" assert_cmd = "2.0.14" -cardano-serialization-lib = { default-features = false, version = "13.2.0" } +cardano-serialization-lib = { default-features = false, version = "13.2.1" } cbor_event = { version = "2.4.0" } clap = { version = "4.5.10", features = ["derive"] } ed25519-zebra = { version = "4.0.3" } diff --git a/toolkit/offchain/src/csl.rs b/toolkit/offchain/src/csl.rs index ca9bf1c55..04229ae5a 100644 --- a/toolkit/offchain/src/csl.rs +++ b/toolkit/offchain/src/csl.rs @@ -2,6 +2,7 @@ use crate::plutus_script::PlutusScript; use cardano_serialization_lib::*; use fraction::{FromPrimitive, Ratio}; use ogmios_client::query_ledger_state::ReferenceScriptsCosts; +use ogmios_client::transactions::Transactions; use ogmios_client::{ query_ledger_state::{PlutusCostModels, ProtocolParametersResponse, QueryLedgerState}, query_network::QueryNetwork, @@ -9,6 +10,7 @@ use ogmios_client::{ types::{OgmiosUtxo, OgmiosValue}, }; use sidechain_domain::{AssetId, MainchainAddressHash, MainchainPrivateKey, NetworkType, UtxoId}; +use std::collections::HashMap; pub(crate) fn plutus_script_hash(script_bytes: &[u8], language: Language) -> [u8; 28] { // Before hashing the script, we need to prepend with byte denoting the language. @@ -201,6 +203,105 @@ fn ex_units_from_response(resp: OgmiosEvaluateTransactionResponse) -> ExUnits { ExUnits::new(&resp.budget.memory.into(), &resp.budget.cpu.into()) } +pub struct CostLookup { + mints: HashMap, + spends: HashMap, +} + +pub enum Costs { + Costs(CostLookup), + ZeroCosts, +} + +pub trait CostStore { + fn get_mint(&self, script: &PlutusScript) -> ExUnits; + fn get_spend(&self, spend_ix: u32) -> ExUnits; + fn get_one_spend(&self) -> ExUnits; +} + +impl CostStore for Costs { + fn get_mint(&self, script: &PlutusScript) -> ExUnits { + match self { + Costs::ZeroCosts => zero_ex_units(), + Costs::Costs(cost_lookup) => cost_lookup + .mints + .get(&script.csl_script_hash()) + .expect("should not be called with an unknown script") + .clone(), + } + } + fn get_spend(&self, spend_ix: u32) -> ExUnits { + match self { + Costs::ZeroCosts => zero_ex_units(), + Costs::Costs(cost_lookup) => cost_lookup + .spends + .get(&spend_ix) + .expect("should not be called with an unknown script") + .clone(), + } + } + fn get_one_spend(&self) -> ExUnits { + match self { + Costs::ZeroCosts => zero_ex_units(), + Costs::Costs(cost_lookup) => { + match cost_lookup.spends.values().collect::>()[..] { + [x] => x.clone(), + _ => panic!( + "should only be called when exacly one spend is expected to be present" + ), + } + }, + } + } +} + +impl Costs { + pub async fn calculate_costs( + make_tx: F, + client: &T, + ) -> anyhow::Result + where + F: Fn(Costs) -> Result, + { + let tx = make_tx(Costs::ZeroCosts)?; + // stage 1 + let costs = Self::from_ogmios(&tx, client).await?; + + let tx = make_tx(costs)?; + // stage 2 + let costs = Self::from_ogmios(&tx, client).await?; + + Ok(make_tx(costs)?) + } + + async fn from_ogmios(tx: &Transaction, client: &T) -> anyhow::Result { + let evaluate_response = client.evaluate_transaction(&tx.to_bytes()).await?; + + let mut mints = HashMap::new(); + let mut spends = HashMap::new(); + for er in evaluate_response { + match er.validator.purpose.as_str() { + "mint" => { + mints.insert( + tx.body() + .mint() + .expect("tx.body.mint() should not be empty if we received a 'mint' response from Ogmios") + .keys() + .get(er.validator.index as usize), + ex_units_from_response(er), + ); + }, + "spend" => { + spends.insert(er.validator.index, ex_units_from_response(er)); + }, + _ => {}, + } + } + + Ok(Costs::Costs(CostLookup { mints, spends })) + } +} + /// Conversion of ogmios-client budget to CSL execution units pub(crate) fn convert_ex_units(v: &OgmiosBudget) -> ExUnits { ExUnits::new(&v.memory.into(), &v.cpu.into()) @@ -489,7 +590,7 @@ impl TransactionBuilderExt for TransactionBuilder { &validator_source, &Redeemer::new(&RedeemerTag::new_mint(), &0u32.into(), &unit_plutus_data(), ex_units), ); - mint_builder.add_asset(&mint_witness, &empty_asset_name(), &amount)?; + mint_builder.add_asset(&mint_witness, &empty_asset_name(), amount)?; self.set_mint_builder(&mint_builder); Ok(()) } @@ -598,7 +699,7 @@ impl TransactionOutputAmountBuilderExt for TransactionOutputAmountBuilder { ctx: &TransactionContext, ) -> Result { let min_ada = self.with_coin_and_asset(&0u64.into(), ma).get_minimum_ada(ctx)?; - Ok(self.with_coin_and_asset(&min_ada, &ma)) + Ok(self.with_coin_and_asset(&min_ada, ma)) } } diff --git a/toolkit/offchain/src/plutus_script.rs b/toolkit/offchain/src/plutus_script.rs index 36f270a91..ba98719b3 100644 --- a/toolkit/offchain/src/plutus_script.rs +++ b/toolkit/offchain/src/plutus_script.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Context}; +use anyhow::{anyhow, Context, Error}; use cardano_serialization_lib::{ Address, JsError, Language, LanguageKind, NetworkIdKind, PlutusData, ScriptHash, }; @@ -32,18 +32,7 @@ impl PlutusScript { pub fn from_ogmios(ogmios_script: OgmiosScript) -> anyhow::Result { if let Plutus(script) = ogmios_script { - let language_kind = match script.language.as_str() { - "plutus:v1" => Language::new_plutus_v1(), - "plutus:v2" => Language::new_plutus_v2(), - "plutus:v3" => Language::new_plutus_v3(), - _ => { - return Err(anyhow!( - "Unsupported Plutus language version: {}", - script.language - )); - }, - }; - Ok(Self { bytes: script.cbor, language: language_kind }) + script.try_into() } else { Err(anyhow!("Expected Plutus script, got something else.")) } @@ -129,29 +118,29 @@ impl PlutusScript { } impl TryFrom for PlutusScript { - type Error = ogmios_client::types::PlutusScript; + type Error = Error; fn try_from(script: ogmios_client::types::PlutusScript) -> Result { let language = match script.language.as_str() { "plutus:v1" => Language::new_plutus_v1(), "plutus:v2" => Language::new_plutus_v2(), "plutus:v3" => Language::new_plutus_v3(), - _ => return Err(script), + _ => return Err(anyhow!("Unsupported Plutus language version: {}", script.language)), }; Ok(Self::from_cbor(&script.cbor, language)) } } -impl Into for PlutusScript { - fn into(self) -> ogmios_client::types::PlutusScript { +impl From for ogmios_client::types::PlutusScript { + fn from(val: PlutusScript) -> Self { ogmios_client::types::PlutusScript { - language: match self.language.kind() { + language: match val.language.kind() { LanguageKind::PlutusV1 => "plutus:v1", LanguageKind::PlutusV2 => "plutus:v2", LanguageKind::PlutusV3 => "plutus:v3", } .to_string(), - cbor: self.bytes, + cbor: val.bytes, } } } diff --git a/toolkit/offchain/src/reserve/handover.rs b/toolkit/offchain/src/reserve/handover.rs index 76915c004..1efd1c872 100644 --- a/toolkit/offchain/src/reserve/handover.rs +++ b/toolkit/offchain/src/reserve/handover.rs @@ -22,12 +22,10 @@ use super::{reserve_utxo_input_with_validator_script_reference, ReserveUtxo, Tok use crate::{ await_tx::AwaitTx, csl::{ - get_builder_config, get_validator_budgets, zero_ex_units, AssetIdExt, - OgmiosUtxoExt, ScriptExUnits, TransactionBuilderExt, TransactionContext, - TransactionOutputAmountBuilderExt, + get_builder_config, AssetIdExt, CostStore, Costs, OgmiosUtxoExt, TransactionBuilderExt, + TransactionContext, TransactionOutputAmountBuilderExt, }, init_governance::{get_governance_data, GovernanceData}, - plutus_script::PlutusScript, reserve::ReserveData, scripts_data::ReserveScripts, }; @@ -36,7 +34,7 @@ use cardano_serialization_lib::*; use ogmios_client::{ query_ledger_state::{QueryLedgerState, QueryUtxoByUtxoId}, query_network::QueryNetwork, - transactions::{OgmiosEvaluateTransactionResponse, Transactions}, + transactions::Transactions, types::OgmiosUtxo, }; use partner_chains_plutus_data::reserve::ReserveRedeemer; @@ -60,25 +58,11 @@ pub async fn handover_reserve< let amount = get_amount_to_release(&reserve_utxo).ok_or_else(|| anyhow!("Internal Error. Reserve Validator has UTXO with Reserve Auth Policy Token, but without other asset."))?; let utxo = reserve_utxo.reserve_utxo; - let tx_to_evaluate = - build_tx(&amount, &utxo, &reserve, &governance, ScriptsCosts::zero(), &ctx)?; - let evaluate_response = client.evaluate_transaction(&tx_to_evaluate.to_bytes()).await?; - let script_costs = ScriptsCosts::from_ogmios( - evaluate_response, - &reserve.scripts.auth_policy, - &governance.policy_script, - )?; - - // ETCM-9222 - this transaction manifests problem that input selection after the first evaluation can affects the cost of the transaction. - let tx = build_tx(&amount, &utxo, &reserve, &governance, script_costs, &ctx)?; - let evaluate_response = client.evaluate_transaction(&tx.to_bytes()).await?; - let script_costs = ScriptsCosts::from_ogmios( - evaluate_response, - &reserve.scripts.auth_policy, - &governance.policy_script, - )?; - - let tx = build_tx(&amount, &utxo, &reserve, &governance, script_costs, &ctx)?; + let tx = Costs::calculate_costs( + |costs| build_tx(&amount, &utxo, &reserve, &governance, costs, &ctx), + client, + ) + .await?; let signed_tx = ctx.sign(&tx).to_bytes(); let res = client.submit_transaction(&signed_tx).await.map_err(|e| { @@ -105,24 +89,28 @@ fn build_tx( reserve_utxo: &OgmiosUtxo, reserve: &ReserveData, governance: &GovernanceData, - costs: ScriptsCosts, + costs: Costs, ctx: &TransactionContext, ) -> Result { let mut tx_builder = TransactionBuilder::new(&get_builder_config(ctx)?); + let reserve_auth_policy_spend_cost = costs.get_one_spend(); + let reserve_auth_policy_burn_cost = costs.get_mint(&reserve.scripts.auth_policy); + let governance_mint_cost = costs.get_mint(&governance.policy_script); + // mint goveranance token tx_builder.add_mint_one_script_token_using_reference_script( &governance.policy_script, &governance.utxo_id_as_tx_input(), - &costs.governance_mint, + &governance_mint_cost, )?; // Spends UTXO with Reserve Auth Policy Token and Reserve (Reward) tokens tx_builder.set_inputs(&reserve_utxo_input_with_validator_script_reference( - &reserve_utxo, - &reserve, + reserve_utxo, + reserve, ReserveRedeemer::Handover { governance_version: 1 }, - &costs.reserve_auth_policy_spend, + &reserve_auth_policy_spend_cost, )?); // burn reserve auth policy token @@ -130,7 +118,7 @@ fn build_tx( &reserve.scripts.auth_policy, &reserve.auth_policy_version_utxo.to_csl_tx_input(), &Int::new_i32(-1), - &costs.reserve_auth_policy_burn, + &reserve_auth_policy_burn_cost, )?; tx_builder.add_output(&illiquid_supply_validator_output( @@ -162,48 +150,3 @@ fn illiquid_supply_validator_output( fn illiquid_supply_validator_redeemer() -> PlutusData { PlutusData::new_empty_constr_plutus_data(&BigNum::zero()) } - -struct ScriptsCosts { - reserve_auth_policy_spend: ExUnits, - reserve_auth_policy_burn: ExUnits, - governance_mint: ExUnits, -} - -impl ScriptsCosts { - fn zero() -> Self { - Self { - reserve_auth_policy_spend: zero_ex_units(), - reserve_auth_policy_burn: zero_ex_units(), - governance_mint: zero_ex_units(), - } - } - - fn from_ogmios( - response: Vec, - reserve_auth_policy_script: &PlutusScript, - governance_policy_script: &PlutusScript, - ) -> Result { - let ScriptExUnits { mut mint_ex_units, mut spend_ex_units } = - get_validator_budgets(response); - let reserve_auth_policy_spend = spend_ex_units - .pop() - .ok_or_else(|| anyhow!("Evaluate response does not have expected 'spend' cost"))?; - let mint_1 = mint_ex_units - .pop() - .ok_or_else(|| anyhow!("Evaluate response does not have expected 'mint' costs"))?; - let mint_0 = mint_ex_units - .pop() - .ok_or_else(|| anyhow!("Evaluate response does not have expected 'mint' costs"))?; - let (reserve_auth_policy_mint, governance_mint) = - if reserve_auth_policy_script.script_hash() < governance_policy_script.script_hash() { - (mint_0, mint_1) - } else { - (mint_1, mint_0) - }; - Ok(Self { - reserve_auth_policy_spend, - reserve_auth_policy_burn: reserve_auth_policy_mint, - governance_mint, - }) - } -}