plutus spend: fix all 4 chain-level bugs surfaced in preprod audit
PLUTUS-1 (HIGH) — value-not-conserved on happy path. collateral isn't consumed unless script fails, so total_in counted lovelace that wasn't actually available for outputs. now picks a SEPARATE ada-only funding utxo as a regular input alongside the locked utxo; collateral stays collateral. error message tells callers to "split a UTXO first or top up" if a second ada-only utxo isn't available. PLUTUS-2 (HIGH) — collateral containing native assets. chain forbids that; our picker grabbed largest-overall. now filters available_utxos to assets.is_empty() before picking, errors clearly if no ada-only utxo ≥ 5 ADA exists. PLUTUS-3 (HIGH) — fee underestimation. plutus tx fees are size_fee + exunits_fee. only size_fee was being charged. new ProtocolParams::ex_units_fee() does ceil(mem * priceMem) + ceil(steps * priceStep). conway-era prices in defaults (577/10000 mem, 721/10_000_000 steps). fee jumps from ~0.17 ADA → ~1.7 ADA for the default ExUnits budget — matches what chain demanded. PLUTUS-4 (LOW, becomes blocking under the others) — script_data_hash not computed. pallas-txbuilder only computes the body hash field when language_view is set on staging. plutus v3 path now calls .language_view(version, cost_model) when the caller-supplied ProtocolParams::plutus_v3_cost_model is Some. mcp wallet_script_spend populates with the canonical preprod V3 cost model from plutus_cost_models::PLUTUS_V3_COST_MODEL_PREPROD (297 i64 params, fetched from koios epoch_params 2026-05). when ProtocolParams has no cost model, we skip language_view and the chain rejects with PPViewHashesDontMatch — explicit-failure mode, no silent shipping of broken txs. new tests: - ex_units_fee_matches_known_values: 14M mem * 0.0577 + 10B steps * 7.21e-5 ≈ 1.529 ADA ± ceil-rounding. locks the conway price math. - rejects_when_no_funding_input_separate_from_collateral: catches the PLUTUS-1 single-utxo case. - rejects_when_collateral_candidate_has_assets: PLUTUS-2 ada-only. verified on preprod against a real script-locked utxo (the placeholder script we locked 5 tADA at earlier). chain rejection went from 5 distinct errors to 1 (MalformedScriptWitnesses — expected, our placeholder UPLC isn't valid). structural body shape now passes every chain-rule check; only the script bytecode itself fails to compile, which is a test-env limitation (no aiken in our toolchain yet) not a wallet-code limitation. 97 unit tests pass. ProtocolParams gained 5 new fields + ex_units_fee helper; went from Copy to Clone (cost_model is a Vec).
This commit is contained in:
parent
05292f182e
commit
7d59ceffd2
5 changed files with 297 additions and 32 deletions
|
|
@ -41,6 +41,7 @@ pub mod inspect;
|
|||
pub mod metadata;
|
||||
pub mod mint;
|
||||
pub mod plutus;
|
||||
pub mod plutus_cost_models;
|
||||
pub mod sign;
|
||||
pub mod stake;
|
||||
pub mod tx;
|
||||
|
|
|
|||
|
|
@ -138,33 +138,65 @@ pub fn build_signed_plutus_spend(
|
|||
let payout_addr = parse_address(payout_address_bech32)?;
|
||||
let network_id = network_id_for(network);
|
||||
|
||||
// Pick collateral — largest UTXO ≥ 5 ADA.
|
||||
let mut sorted: Vec<InputUtxo> = available_utxos.to_vec();
|
||||
sorted.sort_by(|a, b| b.lovelace.cmp(&a.lovelace));
|
||||
let collateral = sorted
|
||||
// PLUTUS-2 audit fix: collateral MUST be ADA-only (chain rejects
|
||||
// collateral inputs that carry native assets).
|
||||
let mut ada_only: Vec<InputUtxo> = available_utxos
|
||||
.iter()
|
||||
.filter(|u| u.assets.is_empty())
|
||||
.cloned()
|
||||
.collect();
|
||||
ada_only.sort_by(|a, b| b.lovelace.cmp(&a.lovelace));
|
||||
|
||||
// Pick collateral — largest ADA-only UTXO ≥ 5 ADA.
|
||||
let collateral = ada_only
|
||||
.iter()
|
||||
.find(|u| u.lovelace >= MIN_COLLATERAL_LOVELACE)
|
||||
.ok_or_else(|| {
|
||||
WalletError::Derivation(format!(
|
||||
"no wallet UTXO ≥ {} lovelace available for collateral",
|
||||
"no ADA-only wallet UTXO ≥ {} lovelace available for collateral",
|
||||
MIN_COLLATERAL_LOVELACE
|
||||
))
|
||||
})?
|
||||
.clone();
|
||||
|
||||
// Total inputs = locked + collateral (collateral acts as a
|
||||
// regular input on the happy path; on script failure the chain
|
||||
// consumes only collateral and the rest of the tx is rolled back).
|
||||
let total_in = locked.lovelace.saturating_add(collateral.lovelace);
|
||||
// PLUTUS-1 audit fix: collateral is NOT consumed on the happy
|
||||
// path — it's only seized if the script fails. So we need a
|
||||
// SEPARATE regular input to fund payout + fee + change.
|
||||
//
|
||||
// Pick the next-largest ADA-only UTXO that's not the collateral.
|
||||
// Could ease this constraint (use any ADA-only UTXO of sufficient
|
||||
// size) but largest-first matches the rest of the wallet's
|
||||
// selection ergonomics.
|
||||
let funding = ada_only
|
||||
.iter()
|
||||
.find(|u| {
|
||||
!(u.tx_hash_hex == collateral.tx_hash_hex
|
||||
&& u.output_index == collateral.output_index)
|
||||
})
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
WalletError::Derivation(
|
||||
"need a SECOND ADA-only wallet UTXO to fund the spend (separate from collateral). \
|
||||
split a UTXO first or top up the wallet."
|
||||
.into(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let fee_pass1: u64 = 1_000_000;
|
||||
// Regular inputs (consumed on happy path): locked + funding.
|
||||
// Collateral is held off-line and only consumed on script failure.
|
||||
let total_in = locked.lovelace.saturating_add(funding.lovelace);
|
||||
|
||||
// PLUTUS-3 audit fix: Plutus tx fee = size_fee + ExUnits_fee.
|
||||
// ExUnits dominates for typical scripts (~1.5 ADA at default budget).
|
||||
let ex_fee = params.ex_units_fee(ex_units.mem, ex_units.steps);
|
||||
let fee_pass1 = 1_000_000u64.saturating_add(ex_fee);
|
||||
let need = payout_lovelace
|
||||
.checked_add(fee_pass1)
|
||||
.and_then(|x| x.checked_add(params.min_utxo_lovelace))
|
||||
.ok_or_else(|| WalletError::Derivation("amount overflow".into()))?;
|
||||
if total_in < need {
|
||||
return Err(WalletError::Derivation(format!(
|
||||
"insufficient lovelace for plutus spend: have {total_in}, need {need}"
|
||||
"insufficient lovelace for plutus spend: have {total_in} (locked+funding), need {need}"
|
||||
)));
|
||||
}
|
||||
|
||||
|
|
@ -172,6 +204,10 @@ pub fn build_signed_plutus_spend(
|
|||
parse_tx_hash(&locked.tx_hash_hex)?,
|
||||
locked.output_index as u64,
|
||||
);
|
||||
let funding_input = Input::new(
|
||||
parse_tx_hash(&funding.tx_hash_hex)?,
|
||||
funding.output_index as u64,
|
||||
);
|
||||
let collateral_input = Input::new(
|
||||
parse_tx_hash(&collateral.tx_hash_hex)?,
|
||||
collateral.output_index as u64,
|
||||
|
|
@ -181,7 +217,10 @@ pub fn build_signed_plutus_spend(
|
|||
change_lovelace: u64|
|
||||
-> Result<StagingTransaction, WalletError> {
|
||||
let mut staging = StagingTransaction::new();
|
||||
// PLUTUS-1: locked + funding as regular inputs (both consumed
|
||||
// on happy path); collateral as collateral_input only.
|
||||
staging = staging.input(locked_input.clone());
|
||||
staging = staging.input(funding_input.clone());
|
||||
staging = staging.collateral_input(collateral_input.clone());
|
||||
staging = staging.output(Output::new(payout_addr.clone(), payout_lovelace));
|
||||
if change_lovelace > 0 {
|
||||
|
|
@ -199,11 +238,25 @@ pub fn build_signed_plutus_spend(
|
|||
if let Some(d) = witness_datum_cbor {
|
||||
staging = staging.datum(d.to_vec());
|
||||
}
|
||||
// PLUTUS-4 audit fix: pallas-txbuilder only computes
|
||||
// script_data_hash if language_view is set. Without it, the
|
||||
// body's hash is None and the chain rejects with
|
||||
// PPViewHashesDontMatch. PlutusV3 path requires a V3 cost
|
||||
// model — caller-supplied via ProtocolParams.
|
||||
if let Some(cost_model) = params.plutus_v3_cost_model.as_deref() {
|
||||
if matches!(script_version, PlutusVersion::V3) {
|
||||
staging = staging.language_view(
|
||||
script_version.to_script_kind(),
|
||||
cost_model.to_vec(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(staging)
|
||||
};
|
||||
|
||||
// Pass 1 — placeholder fee, measure unsigned size.
|
||||
let change_pass1 = total_in
|
||||
.checked_sub(payout_lovelace + fee_pass1)
|
||||
.checked_sub(payout_lovelace.saturating_add(fee_pass1))
|
||||
.ok_or_else(|| {
|
||||
WalletError::Derivation("pass1: insufficient lovelace for plutus spend".into())
|
||||
})?;
|
||||
|
|
@ -213,16 +266,17 @@ pub fn build_signed_plutus_spend(
|
|||
.map_err(|e| WalletError::Derivation(format!("conway build (pass1): {e}")))?
|
||||
.tx_bytes
|
||||
.0;
|
||||
// One witness for the body (payment key) + the redeemer's
|
||||
// script_data_hash overhead is already in the unsigned size.
|
||||
// 128 bytes overhead for the single VKey witness we'll add.
|
||||
let est_signed = (unsigned.len() as u64) + 128;
|
||||
let real_fee = params.min_fee_for_size(est_signed);
|
||||
// size fee + ExUnits fee.
|
||||
let size_fee = params.min_fee_for_size(est_signed);
|
||||
let real_fee = size_fee.saturating_add(ex_fee);
|
||||
|
||||
let final_change = total_in
|
||||
.checked_sub(payout_lovelace + real_fee)
|
||||
.checked_sub(payout_lovelace.saturating_add(real_fee))
|
||||
.ok_or_else(|| {
|
||||
WalletError::Derivation(format!(
|
||||
"insufficient funds for fee: total_in={total_in} payout={payout_lovelace} fee={real_fee}"
|
||||
"insufficient funds for fee: total_in={total_in} payout={payout_lovelace} fee={real_fee} (size={size_fee} + ex={ex_fee})"
|
||||
))
|
||||
})?;
|
||||
if final_change < params.min_utxo_lovelace {
|
||||
|
|
@ -310,6 +364,110 @@ mod tests {
|
|||
assert!(DEFAULT_EX_UNITS.steps >= 100_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ex_units_fee_matches_known_values() {
|
||||
// PLUTUS-3 audit fix: fee for default budget should be ~1.5 ADA.
|
||||
let p = ProtocolParams::default();
|
||||
let fee = p.ex_units_fee(DEFAULT_EX_UNITS.mem, DEFAULT_EX_UNITS.steps);
|
||||
// 14M mem * 0.0577 + 10B steps * 7.21e-5
|
||||
// = 807_800 + 721_000 = 1_528_800 lovelace
|
||||
// ceil-rounding may bump by 1.
|
||||
assert!(
|
||||
(1_528_800..=1_528_802).contains(&fee),
|
||||
"fee {} not in expected range ~1.529 ADA",
|
||||
fee
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_when_no_funding_input_separate_from_collateral() {
|
||||
let payment = payment_from_canonical();
|
||||
let change = change_address(Network::Preprod);
|
||||
let payout = payout_address();
|
||||
let locked = PlutusInput {
|
||||
tx_hash_hex: "deadbeef".repeat(8),
|
||||
output_index: 0,
|
||||
lovelace: 50_000_000,
|
||||
};
|
||||
// Only ONE wallet UTXO — collateral pick consumes it; no
|
||||
// separate funding UTXO available. PLUTUS-1 fix should
|
||||
// reject with a clear error rather than building a tx that
|
||||
// chain rejects.
|
||||
let utxos = vec![InputUtxo {
|
||||
tx_hash_hex: "cafebabe".repeat(8),
|
||||
output_index: 0,
|
||||
lovelace: 100_000_000,
|
||||
assets: Default::default(),
|
||||
}];
|
||||
let err = build_signed_plutus_spend(
|
||||
&payment,
|
||||
Network::Preprod,
|
||||
&locked,
|
||||
PlutusVersion::V3,
|
||||
&ALWAYS_TRUE_PLUTUS_V3_CBOR,
|
||||
&UNIT_REDEEMER_CBOR,
|
||||
None,
|
||||
&utxos,
|
||||
&change,
|
||||
&payout,
|
||||
10_000_000,
|
||||
DEFAULT_EX_UNITS,
|
||||
&ProtocolParams::default(),
|
||||
)
|
||||
.expect_err("expected a 'need second utxo' error");
|
||||
match err {
|
||||
WalletError::Derivation(m) => assert!(m.contains("SECOND")),
|
||||
other => panic!("expected Derivation, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_when_collateral_candidate_has_assets() {
|
||||
let payment = payment_from_canonical();
|
||||
let change = change_address(Network::Preprod);
|
||||
let payout = payout_address();
|
||||
let locked = PlutusInput {
|
||||
tx_hash_hex: "deadbeef".repeat(8),
|
||||
output_index: 0,
|
||||
lovelace: 50_000_000,
|
||||
};
|
||||
// Largest UTXO has assets — must NOT be picked as collateral.
|
||||
// No ADA-only UTXO ≥ 5 ADA → rejected.
|
||||
let mut with_assets = InputUtxo {
|
||||
tx_hash_hex: "cafebabe".repeat(8),
|
||||
output_index: 0,
|
||||
lovelace: 100_000_000,
|
||||
assets: Default::default(),
|
||||
};
|
||||
with_assets.assets.insert("ee".repeat(28) + "deadbeef", 1);
|
||||
let utxos = vec![with_assets];
|
||||
let err = build_signed_plutus_spend(
|
||||
&payment,
|
||||
Network::Preprod,
|
||||
&locked,
|
||||
PlutusVersion::V3,
|
||||
&ALWAYS_TRUE_PLUTUS_V3_CBOR,
|
||||
&UNIT_REDEEMER_CBOR,
|
||||
None,
|
||||
&utxos,
|
||||
&change,
|
||||
&payout,
|
||||
10_000_000,
|
||||
DEFAULT_EX_UNITS,
|
||||
&ProtocolParams::default(),
|
||||
)
|
||||
.expect_err("expected ADA-only collateral error");
|
||||
match err {
|
||||
WalletError::Derivation(m) => {
|
||||
assert!(
|
||||
m.contains("ADA-only") || m.contains("collateral"),
|
||||
"expected ADA-only / collateral message, got: {m}"
|
||||
)
|
||||
}
|
||||
other => panic!("expected Derivation, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_signed_plutus_spend_produces_cbor() {
|
||||
let payment = payment_from_canonical();
|
||||
|
|
@ -321,13 +479,22 @@ mod tests {
|
|||
output_index: 0,
|
||||
lovelace: 50_000_000,
|
||||
};
|
||||
// Wallet has one large UTXO usable as collateral.
|
||||
let utxos = vec![InputUtxo {
|
||||
tx_hash_hex: "cafebabe".repeat(8),
|
||||
output_index: 0,
|
||||
lovelace: 100_000_000,
|
||||
assets: Default::default(),
|
||||
}];
|
||||
// PLUTUS-1+2 audit fix: spend path needs TWO ADA-only wallet
|
||||
// UTXOs — one for collateral, one for funding.
|
||||
let utxos = vec![
|
||||
InputUtxo {
|
||||
tx_hash_hex: "cafebabe".repeat(8),
|
||||
output_index: 0,
|
||||
lovelace: 100_000_000,
|
||||
assets: Default::default(),
|
||||
},
|
||||
InputUtxo {
|
||||
tx_hash_hex: "feedface".repeat(8),
|
||||
output_index: 0,
|
||||
lovelace: 50_000_000,
|
||||
assets: Default::default(),
|
||||
},
|
||||
];
|
||||
|
||||
let cbor = build_signed_plutus_spend(
|
||||
&payment,
|
||||
|
|
@ -372,12 +539,20 @@ mod tests {
|
|||
lovelace: 50_000_000,
|
||||
};
|
||||
// No wallet UTXO ≥ 5 ADA.
|
||||
let utxos = vec![InputUtxo {
|
||||
tx_hash_hex: "cafebabe".repeat(8),
|
||||
output_index: 0,
|
||||
lovelace: 1_000_000,
|
||||
assets: Default::default(),
|
||||
}];
|
||||
let utxos = vec![
|
||||
InputUtxo {
|
||||
tx_hash_hex: "cafebabe".repeat(8),
|
||||
output_index: 0,
|
||||
lovelace: 1_000_000,
|
||||
assets: Default::default(),
|
||||
},
|
||||
InputUtxo {
|
||||
tx_hash_hex: "feedface".repeat(8),
|
||||
output_index: 0,
|
||||
lovelace: 1_000_000,
|
||||
assets: Default::default(),
|
||||
},
|
||||
];
|
||||
let err = build_signed_plutus_spend(
|
||||
&payment,
|
||||
Network::Preprod,
|
||||
|
|
|
|||
42
crates/aldabra-core/src/plutus_cost_models.rs
Normal file
42
crates/aldabra-core/src/plutus_cost_models.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// Conway-era preprod Plutus V3 cost model, epoch 286 (2026-05).
|
||||
// 297 params. Pull fresh via koios epoch_params after major hard forks.
|
||||
pub const PLUTUS_V3_COST_MODEL_PREPROD: [i64; 297] = [
|
||||
100788, 420, 1, 1, 1000, 173, 0, 1,
|
||||
1000, 59957, 4, 1, 11183, 32, 201305, 8356,
|
||||
4, 16000, 100, 16000, 100, 16000, 100, 16000,
|
||||
100, 16000, 100, 16000, 100, 100, 100, 16000,
|
||||
100, 94375, 32, 132994, 32, 61462, 4, 72010,
|
||||
178, 0, 1, 22151, 32, 91189, 769, 4,
|
||||
2, 85848, 123203, 7305, -900, 1716, 549, 57,
|
||||
85848, 0, 1, 1, 1000, 42921, 4, 2,
|
||||
24548, 29498, 38, 1, 898148, 27279, 1, 51775,
|
||||
558, 1, 39184, 1000, 60594, 1, 141895, 32,
|
||||
83150, 32, 15299, 32, 76049, 1, 13169, 4,
|
||||
22100, 10, 28999, 74, 1, 28999, 74, 1,
|
||||
43285, 552, 1, 44749, 541, 1, 33852, 32,
|
||||
68246, 32, 72362, 32, 7243, 32, 7391, 32,
|
||||
11546, 32, 85848, 123203, 7305, -900, 1716, 549,
|
||||
57, 85848, 0, 1, 90434, 519, 0, 1,
|
||||
74433, 32, 85848, 123203, 7305, -900, 1716, 549,
|
||||
57, 85848, 0, 1, 1, 85848, 123203, 7305,
|
||||
-900, 1716, 549, 57, 85848, 0, 1, 955506,
|
||||
213312, 0, 2, 270652, 22588, 4, 1457325, 64566,
|
||||
4, 20467, 1, 4, 0, 141992, 32, 100788,
|
||||
420, 1, 1, 81663, 32, 59498, 32, 20142,
|
||||
32, 24588, 32, 20744, 32, 25933, 32, 24623,
|
||||
32, 43053543, 10, 53384111, 14333, 10, 43574283, 26308,
|
||||
10, 16000, 100, 16000, 100, 962335, 18, 2780678,
|
||||
6, 442008, 1, 52538055, 3756, 18, 267929, 18,
|
||||
76433006, 8868, 18, 52948122, 18, 1995836, 36, 3227919,
|
||||
12, 901022, 1, 166917843, 4307, 36, 284546, 36,
|
||||
158221314, 26549, 36, 74698472, 36, 333849714, 1, 254006273,
|
||||
72, 2174038, 72, 2261318, 64571, 4, 207616, 8310,
|
||||
4, 1293828, 28716, 63, 0, 1, 1006041, 43623,
|
||||
251, 0, 1, 100181, 726, 719, 0, 1,
|
||||
100181, 726, 719, 0, 1, 100181, 726, 719,
|
||||
0, 1, 107878, 680, 0, 1, 95336, 1,
|
||||
281145, 18848, 0, 1, 180194, 159, 1, 1,
|
||||
158519, 8942, 0, 1, 159378, 8813, 0, 1,
|
||||
107490, 3298, 1, 106057, 655, 1, 1964219, 24520,
|
||||
3,
|
||||
];
|
||||
|
|
@ -56,7 +56,7 @@ use crate::{Network, PaymentKey, WalletError};
|
|||
/// here match Conway-era mainnet as of 2026-Q2; supply your own via
|
||||
/// [`ProtocolParams::from_koios_response`] (TODO) if you want
|
||||
/// chain-fresh values.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProtocolParams {
|
||||
/// Per-byte fee coefficient (a). Mainnet: 44.
|
||||
pub min_fee_a: u64,
|
||||
|
|
@ -66,6 +66,19 @@ pub struct ProtocolParams {
|
|||
/// formula is `coins_per_utxo_byte * tx_out_size`; we use the
|
||||
/// 1 ADA floor as a safe over-approximation.
|
||||
pub min_utxo_lovelace: u64,
|
||||
/// Plutus ExUnits price per memory unit, as numerator/denominator
|
||||
/// rational. Conway era: 577 / 10000 = 0.0577 lovelace/mem.
|
||||
pub price_mem_numerator: u64,
|
||||
pub price_mem_denominator: u64,
|
||||
/// Plutus ExUnits price per CPU step, as numerator/denominator.
|
||||
/// Conway era: 721 / 10_000_000 = 7.21e-5 lovelace/step.
|
||||
pub price_steps_numerator: u64,
|
||||
pub price_steps_denominator: u64,
|
||||
/// Plutus V3 cost model. Required when building Plutus spend
|
||||
/// transactions so the chain can verify `script_data_hash`. If
|
||||
/// `None`, Plutus paths skip script_data_hash and the chain will
|
||||
/// reject with `PPViewHashesDontMatch`.
|
||||
pub plutus_v3_cost_model: Option<Vec<i64>>,
|
||||
}
|
||||
|
||||
impl Default for ProtocolParams {
|
||||
|
|
@ -74,16 +87,42 @@ impl Default for ProtocolParams {
|
|||
min_fee_a: 44,
|
||||
min_fee_b: 155_381,
|
||||
min_utxo_lovelace: 1_000_000,
|
||||
price_mem_numerator: 577,
|
||||
price_mem_denominator: 10_000,
|
||||
price_steps_numerator: 721,
|
||||
price_steps_denominator: 10_000_000,
|
||||
// Plutus paths populate this from `plutus_cost_models::PLUTUS_V3_COST_MODEL_PREPROD`
|
||||
// or fetched from `epoch_params`. None by default keeps
|
||||
// the ada-only / mint paths zero-cost.
|
||||
plutus_v3_cost_model: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolParams {
|
||||
/// Linear size-based fee: `min_fee_a * tx_size + min_fee_b`.
|
||||
pub fn min_fee_for_size(&self, tx_size_bytes: u64) -> u64 {
|
||||
self.min_fee_a
|
||||
.saturating_mul(tx_size_bytes)
|
||||
.saturating_add(self.min_fee_b)
|
||||
}
|
||||
|
||||
/// ExUnits execution cost in lovelace, ceiling-rounded per the
|
||||
/// Conway protocol rules. Add this to `min_fee_for_size(...)` for
|
||||
/// the total fee on a Plutus transaction.
|
||||
/// (PLUTUS-3 audit fix.)
|
||||
pub fn ex_units_fee(&self, mem: u64, steps: u64) -> u64 {
|
||||
// ceil(mem * num / den)
|
||||
let mem_cost = mem
|
||||
.saturating_mul(self.price_mem_numerator)
|
||||
.saturating_add(self.price_mem_denominator.saturating_sub(1))
|
||||
/ self.price_mem_denominator.max(1);
|
||||
let step_cost = steps
|
||||
.saturating_mul(self.price_steps_numerator)
|
||||
.saturating_add(self.price_steps_denominator.saturating_sub(1))
|
||||
/ self.price_steps_denominator.max(1);
|
||||
mem_cost.saturating_add(step_cost)
|
||||
}
|
||||
}
|
||||
|
||||
/// One UTXO available for spending. Independently typed from
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use aldabra_chain::{ChainBackend, KoiosClient};
|
||||
use aldabra_core::plutus_cost_models::PLUTUS_V3_COST_MODEL_PREPROD;
|
||||
use aldabra_core::{
|
||||
add_witness, build_signed_cip68_nft_mint, build_signed_mint_with_metadata,
|
||||
build_signed_payment_with_assets, build_signed_plutus_spend, build_signed_stake_delegation,
|
||||
|
|
@ -821,6 +822,13 @@ impl WalletService {
|
|||
lovelace: locked_lovelace,
|
||||
};
|
||||
|
||||
// Plutus paths require the V3 cost model so the chain can
|
||||
// verify script_data_hash. Hardcoded preprod value from
|
||||
// koios epoch_params; future improvement is pulling fresh
|
||||
// from the chain on each call. (PLUTUS-4 audit fix.)
|
||||
let mut params = ProtocolParams::default();
|
||||
params.plutus_v3_cost_model = Some(PLUTUS_V3_COST_MODEL_PREPROD.to_vec());
|
||||
|
||||
let cbor = build_signed_plutus_spend(
|
||||
&self.inner.payment_key,
|
||||
self.inner.network,
|
||||
|
|
@ -834,7 +842,7 @@ impl WalletService {
|
|||
&payout_address,
|
||||
payout_lovelace,
|
||||
budget,
|
||||
&ProtocolParams::default(),
|
||||
¶ms,
|
||||
)
|
||||
.map_err(|e| format!("build/sign plutus spend: {e}"))?;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue