AUDIT4-2 fix: invert plutus collateral/funding utxo picker
build_signed_plutus_spend was picking the LARGEST ada-only utxo for collateral and the next-largest for funding. Wallets with one big change utxo + a small leftover (the typical shape after any send) hit this with funding=tiny, collateral=huge — funding+locked couldn't cover payout + script-execution fee + change min_utxo even with billions of lovelace sitting unused in collateral. Fix: pick the SMALLEST ada-only utxo that still qualifies (≥5 ADA) for collateral, and the LARGEST for funding. Collateral never gets consumed on the happy path, so its size beyond the 5-ADA floor is wasted budget; funding has to cover real spend. Surfaced 2026-05-04 audit-4 phase F2 on the deployed the build host container against the always-succeeds Aiken validator. New regression test picks_smallest_qualifying_collateral_largest_funding covers the mixed-size-utxo scenario the prior tests missed (both old utxos were 50-100M ada, so the inversion didn't show).
This commit is contained in:
parent
7cb70db12b
commit
2fc4474639
1 changed files with 72 additions and 5 deletions
|
|
@ -147,9 +147,22 @@ pub fn build_signed_plutus_spend(
|
||||||
.collect();
|
.collect();
|
||||||
ada_only.sort_by_key(|u| std::cmp::Reverse(u.lovelace));
|
ada_only.sort_by_key(|u| std::cmp::Reverse(u.lovelace));
|
||||||
|
|
||||||
// Pick collateral — largest ADA-only UTXO ≥ 5 ADA.
|
// AUDIT4-2 fix: pick the SMALLEST ADA-only UTXO that still
|
||||||
|
// qualifies for collateral (≥ 5 ADA), so the LARGEST stays
|
||||||
|
// available for funding the spend. Previously we did the
|
||||||
|
// inverse — collateral got the biggest utxo, funding got
|
||||||
|
// whatever scrap was next, and a typical wallet (one big
|
||||||
|
// change utxo + a tiny self-send leftover) couldn't cover
|
||||||
|
// payout + fee + min_utxo even with billions of lovelace
|
||||||
|
// sitting in the change. Surfaced 2026-05-04 audit-4 phase F2.
|
||||||
|
//
|
||||||
|
// Collateral is NEVER consumed on the happy path — it's only
|
||||||
|
// seized if the script fails — so its size beyond the 5-ADA
|
||||||
|
// floor is wasted budget. Funding, by contrast, must cover
|
||||||
|
// (payout + fee + change_min_utxo), so we want it big.
|
||||||
let collateral = ada_only
|
let collateral = ada_only
|
||||||
.iter()
|
.iter()
|
||||||
|
.rev()
|
||||||
.find(|u| u.lovelace >= MIN_COLLATERAL_LOVELACE)
|
.find(|u| u.lovelace >= MIN_COLLATERAL_LOVELACE)
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
WalletError::Derivation(format!(
|
WalletError::Derivation(format!(
|
||||||
|
|
@ -163,10 +176,9 @@ pub fn build_signed_plutus_spend(
|
||||||
// path — it's only seized if the script fails. So we need a
|
// path — it's only seized if the script fails. So we need a
|
||||||
// SEPARATE regular input to fund payout + fee + change.
|
// SEPARATE regular input to fund payout + fee + change.
|
||||||
//
|
//
|
||||||
// Pick the next-largest ADA-only UTXO that's not the collateral.
|
// Pick the LARGEST ADA-only UTXO that's not the collateral —
|
||||||
// Could ease this constraint (use any ADA-only UTXO of sufficient
|
// funding has to cover payout + script-execution fee + change
|
||||||
// size) but largest-first matches the rest of the wallet's
|
// min_utxo, which can run several ADA at default budgets.
|
||||||
// selection ergonomics.
|
|
||||||
let funding = ada_only
|
let funding = ada_only
|
||||||
.iter()
|
.iter()
|
||||||
.find(|u| {
|
.find(|u| {
|
||||||
|
|
@ -532,6 +544,61 @@ mod tests {
|
||||||
assert!(witness.redeemer.is_some(), "redeemer witness present");
|
assert!(witness.redeemer.is_some(), "redeemer witness present");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// AUDIT4-2 regression. A wallet with one tiny qualifying UTXO
|
||||||
|
/// alongside one huge UTXO must pick the tiny one for collateral
|
||||||
|
/// and the huge one for funding (not the inverse). Pre-fix, the
|
||||||
|
/// huge UTXO became collateral and funding fell back to the
|
||||||
|
/// tiny 5-ADA scrap, too small to cover payout, script-exec
|
||||||
|
/// fee, and change min_utxo. Surfaced 2026-05-04 audit-4 phase F2.
|
||||||
|
#[test]
|
||||||
|
fn picks_smallest_qualifying_collateral_largest_funding() {
|
||||||
|
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: 6_000_000,
|
||||||
|
};
|
||||||
|
// Two utxos: one barely-qualifying-for-collateral (5 ADA),
|
||||||
|
// one huge (1000 ADA). Pre-fix would put the 1000 ADA into
|
||||||
|
// collateral and the 5 ADA into funding — too small to
|
||||||
|
// cover a 4 ADA payout + ~3 ADA fee+min_utxo. Post-fix,
|
||||||
|
// collateral=5 ADA, funding=1000 ADA, builds cleanly.
|
||||||
|
let utxos = vec![
|
||||||
|
InputUtxo {
|
||||||
|
tx_hash_hex: "aaaa".repeat(16),
|
||||||
|
output_index: 0,
|
||||||
|
lovelace: 1_000_000_000, // 1000 ADA
|
||||||
|
assets: Default::default(),
|
||||||
|
},
|
||||||
|
InputUtxo {
|
||||||
|
tx_hash_hex: "bbbb".repeat(16),
|
||||||
|
output_index: 0,
|
||||||
|
lovelace: 5_000_000, // exactly the floor
|
||||||
|
assets: Default::default(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let cbor = build_signed_plutus_spend(
|
||||||
|
&payment,
|
||||||
|
Network::Preprod,
|
||||||
|
&locked,
|
||||||
|
PlutusVersion::V3,
|
||||||
|
&ALWAYS_TRUE_PLUTUS_V3_CBOR,
|
||||||
|
&UNIT_REDEEMER_CBOR,
|
||||||
|
None,
|
||||||
|
&utxos,
|
||||||
|
&change,
|
||||||
|
&payout,
|
||||||
|
4_000_000,
|
||||||
|
DEFAULT_EX_UNITS,
|
||||||
|
&ProtocolParams::default(),
|
||||||
|
)
|
||||||
|
.expect("post-fix plutus spend builds with mixed-size utxos");
|
||||||
|
assert!(cbor.len() > 200);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_signed_plutus_spend_rejects_no_collateral() {
|
fn build_signed_plutus_spend_rejects_no_collateral() {
|
||||||
let payment = payment_from_canonical();
|
let payment = payment_from_canonical();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue