Skip to content

fix: two wallet shapes that the transaction builder gets wrong - #117

Merged
nau merged 1 commit into
mainfrom
fix/wallet-utxo-shape-defeats-the-tx-builder
Sep 25, 2026
Merged

nau merged 1 commit into
mainfrom
fix/wallet-utxo-shape-defeats-the-tx-builder

Conversation

@nau

@nau nau commented Sep 24, 2026

Copy link
Copy Markdown
Member

Both bugs are in whisky-pallas 1.0.25 (and 1.0.28-beta.1, unchanged — there is no newer stable). Both hit spo4 on preprod on 2026-09-24 and between them stopped it posting anything to Cardano for a day.

Bug 1: assets stripped off an input

convert_value emits one (policy, [one asset]) entry per asset, and MultiassetPositiveCoin::new does policy_map.insert(policy_id, asset_map) — replace, not merge. A UTxO carrying two asset names under one policy loses all but the last before the builder ever sees it, so the change it computes is short that asset while the lovelace balances exactly:

supplied: ... 665b33b7… {"460e60cd…": 1, "8e7beff1…": 1} ...
expected: ... 665b33b7… {                "8e7beff1…": 1} ...

The ledger names an asset the node never left out, which makes it hard to trace back.

Bug 2: the fee ignores the change output's tokens

whisky prices a mock body assembled before the change carries its tokens. Measured on build_collateral_top_up, same amounts, only the token load varying:

change output tx size fee set minFee result
ada-only 402 174,565 173,069 +1,496
1 token 445 174,565 174,961 −396
2 tokens, 2 policies 511 174,565 177,865 −3,300

fee set never moves. This is why heimdall posted fine historically and then stopped: nothing regressed in our code, the wallet's largest UTxO simply started carrying tokens, and select_fee takes the largest.

Changes

  • dropped_policy — names the policy a UTxO would lose assets under. Wired into select_fee, build_collateral_top_up and register_pool::select_inputs; each refuses by name when nothing else can fund the transaction.
  • select_fee prefers ada-only UTxOs, falling back to token-bearing ones when no ada-only one can cover the outputs. A preference, not a rule — ensure-collateral has to spend what the wallet holds.
  • The collateral split spends ada-only dust before one fat token-bearing UTxO, for the same reason. It matters most there: that is the transaction that digs a wallet out of having no ada-only UTxOs.
  • Where the change must carry tokens, the split measures the signed transaction and rebuilds once with an explicit fee. Measuring what actually goes out is the only estimate that cannot drift from it.

Scope

treasury_bootstrap already refuses token-bearing inputs. Every other builder routes its fee input through select_fee. Mints are unaffected in practice — every mints: call site is a single ScriptMint — though MultiassetNonZeroInt::new has the same replace-not-merge hole upstream.

Tests

10 new tests, each watched fail first, including a characterisation test pinning that the stripping limit is per UTxO (Value::add merges correctly, so two names on separate UTxOs are fine). 943 pass, clippy clean.

Upstream

Bug 1 is fixed in sidan-lab/whisky#40 with a regression test for both affected functions. Both changes here stand regardless of whether that lands, since the wallet is ours to choose inputs from.

`whisky-pallas` mishandles native tokens twice, and both surfaced on spo4 on
2026-09-24. It converts each input's asset list on its own and inserts the
result per policy id instead of merging, so a UTxO carrying two asset names
under one policy loses all but the last, and the ledger refuses the transaction
as ValueNotConserved, naming a value the node never left out. Separately it
prices a transaction from a mock body assembled before the change output carries
its tokens: the fee it sets does not move with the token load at all, so an
ada-only change leaves 1,496 lovelace of surplus, one token turns that into a
396 lovelace shortfall, and two into 3,300, answered by FeeTooSmallUTxO. Between
them these stopped the key handoff on bridge epochs 1556 and 1557 and left
`ensure-collateral` unable to dig the wallet out.

Input selection now passes over a UTxO whose assets would be stripped, in
`select_fee`, in the collateral split and in `register_pool`, and names the
policy when nothing else can fund the transaction. Fee inputs prefer ada-only
UTxOs so the change carries nothing to underprice, and the collateral split
spends ada-only dust before one fat token-bearing UTxO for the same reason.
Where the change must carry tokens, the split measures the signed transaction
and rebuilds once with an explicit fee rather than trusting the estimate. Two
asset names on separate UTxOs remain fine, since adding two already-converted
values merges correctly; a test pins that the limit is per UTxO. The stripping
half is fixed upstream as sidan-lab/whisky#40, but both stand regardless, since
the wallet is ours to choose inputs from.
@nau
nau merged commit 54244de into main Sep 25, 2026
4 checks passed
@nau
nau deleted the fix/wallet-utxo-shape-defeats-the-tx-builder branch September 25, 2026 08:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant