Repository navigation
fix(tx-builder): evaluate the final transaction and settle fee and change to a fixed point - #861
Merged
lisicky merged 2 commits intoNov 1, 2026
Conversation
…ange to a fixed point Coin selection prices redeemers on a skeleton with a 1 000 ADA placeholder fee and a change output that still holds the fee, so a script that reads the fee or the change can need other units on chain than the ones declared. complete() now evaluates the final body, recomputes the fee, moves the difference into the change output with the most lovelace and repeats until one more pass changes nothing (at most 20 passes, then it throws). With a change output, the first pass sets the minimum fee; after that the fee only rises, so a script that gets cheaper with a higher fee cannot flip between two fees. The collateral is then checked against the settled fee. Without an evaluator or Plutus scripts nothing changes; a manual fee is kept.
…ted fee The largest-first selector priced every pass with the 1 000 ADA placeholder fee and dropped the redeemers it evaluated, so complete() always had to re-evaluate the final transaction at least twice. Passes after the first now use the fee already computed, whose change already pays it, and the selector returns its redeemers. When that pass priced the final transaction, the final check costs one evaluation.
This was referenced Oct 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds on #849: this PR targets its branch, so it shows only its own two commits, and should merge after it. Supersedes #851.
Two points first
complete()with Plutus scripts. It comes on top of perf: cache provider calls and reduce fetcher and evaluator calls in MeshTxBuilder #849, which reduces calls elsewhere. For a remote evaluator (Blockfrost, Ogmios), that is one more HTTP call. Builds without an evaluator or without Plutus scripts make no extra call. When the selection has already priced the final transaction (the common case: scripts that do not read the fee or the change), the check is exactly that one call.Problem
As @lisicky said on #851, balancing belongs in coin selection, and
computeMinimumCostalready re-evaluates the redeemers on a cloned builder with the change outputs present. Two gaps remain:Repro: a V3 spending validator
\ctx -> if txInfoFee < 100 ADA then loop 1000 else (), spent withMeshTxBuilder+ScalusEmulatoras the evaluator:CardanoSdkInputSelector(default), beforeLargestFirstInputSelector, beforeBefore the change, the default selector's tx fails phase 2: the fee matches the declared units, but those units are about 400 times too low. The largest-first selector never returned the redeemers it evaluated, so its transactions declared the default ex-units with a fee priced for the evaluated ones. The first commit fixes both rows.
Commits
1.
fix(tx-builder): evaluate the final transaction and settle fee and change to a fixed pointcomplete(): afterupdateByTxPrototype(..., true),settleFeeAndChangeevaluates the final body and recomputes the fee (getActualFee). It moves the difference into a change output and repeats until one more pass changes neither the fee nor the units. It stops after 20 passes with an error, and never returns a transaction it could not settle. Evaluation errors carry the same "Evaluate redeemers failed: ..." prefix as in selection.TxBuilderloop floors the fee at the initial fee. That floor is not enough here. A script that is cheaper with a higher fee (fee < X ? expensive : cheap, with X between the two fees) would flip between two fees until the cap. A fee that only rises after the first pass settles it, at the cost of a fee a little above the minimum in that case.maxValSize), the change output with the most lovelace takes the difference. The cardano-sdk selector puts the excess lovelace in the first split output, largest-first in the last one; the others hold their minimum.setFeeis kept: only the units are refreshed.computeMinimumCostcannot see the final units.collateral * 100 >= fee * collateralPercent. The collateral istotalCollateralwhen it is set, else the lovelace of the collateral inputs. It throws with the numbers.CardanoSdkInputSelector: the placeholder stays, with a comment. For the size estimate it is exact: it encodes in 5 bytes, like any fee from 0.065 to 4 294 ADA.2.
perf(tx-builder): price largest-first selection passes with the computed feeKnown limitations
updateRedeemerandgetRedeemerCostsskip them. This was already the case on perf: cache provider calls and reduce fetcher and evaluator calls in MeshTxBuilder #849.MeshTxBuilderwithoutparamsuses default limits that can be lower than mainnet's.#849 interaction
#849 caches fetcher results (UTxOs by tx hash, cost models per epoch) and skips evaluation without Plutus witnesses. It does not memoize evaluation results, so the final evaluation sees the final body.
Tests
packages/mesh-transaction/test/mesh-tx-builder/exact-balancing.test.ts, with two hand-written V3 validators that read the fee (UPLC source next to the flat hex). 13 of its 14 tests fail on #849 without this PR:complete()throw;totalCollateralor the collateral inputs) does not cover the fee.The 14th test is a guard and passes on #849 too: a manual fee is kept and the units are exact.
npm test -w packages/mesh-transaction: 180 tests, 176 passed, 4 failed. The same 4 fail on #849 without this PR (166 tests, 162 passed): 3 intx.test.ts("Cannot find module 'scalus'") and 1 inprovider-calls.test.ts("fetches missing script bytes for a locally known Plutus reference UTxO").npm test -w packages/mesh-core-cst: 6 + 179 passed.