Skip to content

Commit

Permalink
ETCM-9222 ExUnit calculation (#414)
Browse files Browse the repository at this point in the history
  • Loading branch information
kpinter-iohk authored Jan 22, 2025
1 parent 53e588c commit ecec82b
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 99 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
105 changes: 103 additions & 2 deletions toolkit/offchain/src/csl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ 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,
transactions::{OgmiosBudget, OgmiosEvaluateTransactionResponse},
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.
Expand Down Expand Up @@ -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<cardano_serialization_lib::ScriptHash, ExUnits>,
spends: HashMap<u32, ExUnits>,
}

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::<Vec<_>>()[..] {
[x] => x.clone(),
_ => panic!(
"should only be called when exacly one spend is expected to be present"
),
}
},
}
}
}

impl Costs {
pub async fn calculate_costs<T: Transactions, F>(
make_tx: F,
client: &T,
) -> anyhow::Result<Transaction>
where
F: Fn(Costs) -> Result<Transaction, JsError>,
{
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<T: Transactions>(tx: &Transaction, client: &T) -> anyhow::Result<Costs> {
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())
Expand Down Expand Up @@ -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(())
}
Expand Down Expand Up @@ -598,7 +699,7 @@ impl TransactionOutputAmountBuilderExt for TransactionOutputAmountBuilder {
ctx: &TransactionContext,
) -> Result<Self, JsError> {
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))
}
}

Expand Down
27 changes: 8 additions & 19 deletions toolkit/offchain/src/plutus_script.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use anyhow::{anyhow, Context};
use anyhow::{anyhow, Context, Error};
use cardano_serialization_lib::{
Address, JsError, Language, LanguageKind, NetworkIdKind, PlutusData, ScriptHash,
};
Expand Down Expand Up @@ -32,18 +32,7 @@ impl PlutusScript {

pub fn from_ogmios(ogmios_script: OgmiosScript) -> anyhow::Result<Self> {
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."))
}
Expand Down Expand Up @@ -129,29 +118,29 @@ impl PlutusScript {
}

impl TryFrom<ogmios_client::types::PlutusScript> for PlutusScript {
type Error = ogmios_client::types::PlutusScript;
type Error = Error;

fn try_from(script: ogmios_client::types::PlutusScript) -> Result<Self, Self::Error> {
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<ogmios_client::types::PlutusScript> for PlutusScript {
fn into(self) -> ogmios_client::types::PlutusScript {
impl From<PlutusScript> 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,
}
}
}
Expand Down
93 changes: 18 additions & 75 deletions toolkit/offchain/src/reserve/handover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand All @@ -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;
Expand All @@ -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| {
Expand All @@ -105,32 +89,36 @@ fn build_tx(
reserve_utxo: &OgmiosUtxo,
reserve: &ReserveData,
governance: &GovernanceData,
costs: ScriptsCosts,
costs: Costs,
ctx: &TransactionContext,
) -> Result<Transaction, JsError> {
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
tx_builder.add_mint_script_token_using_reference_script(
&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(
Expand Down Expand Up @@ -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<OgmiosEvaluateTransactionResponse>,
reserve_auth_policy_script: &PlutusScript,
governance_policy_script: &PlutusScript,
) -> Result<Self, anyhow::Error> {
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,
})
}
}

0 comments on commit ecec82b

Please sign in to comment.