Skip to content

fix: finalize script tx fee, collateral, and script data hash consistently#2

Open
mgpai22 wants to merge 8 commits into
feat/evaluate-tx-additional-utxosfrom
feat/script-tx-finalization
Open

fix: finalize script tx fee, collateral, and script data hash consistently#2
mgpai22 wants to merge 8 commits into
feat/evaluate-tx-additional-utxosfrom
feat/script-tx-finalization

Conversation

@mgpai22

@mgpai22 mgpai22 commented Jun 23, 2026

Copy link
Copy Markdown
Member

Finalization fixes for script (Plutus) transactions built via Complete(). Each addresses a ledger rejection observed when submitting reward-withdrawal redeemer txs to preprod (Blockfrost/Ogmios).

Fixes

  • Fee sizing uses a non-zero placeholder fee. estimateFee and estimateExecutionUnits built their sizing/eval body with a zero fee. The body fee field (key 2) is omitempty, so a zero value is dropped from the CBOR: this undercounts the body size (off by the width of the real fee field, causing FeeTooSmallUTxO) and produces a fee-less body that strict evaluators reject (field fee with key 2, not decoded). Both now build with a placeholder fee from Context.MaxTxFee() (fallback 2 ADA).
  • Conway tiered reference-script fee. minfee omitted the reference-script fee for script txs, causing FeeTooSmallUTxO. Adds backend.TierRefScriptFee (matching the ledger tierRefScriptFee), protocol-param helpers (RefScriptFeePerByte/RefScriptSizeIncrement/RefScriptMultiplier), and a new MinFeeRefScriptCostPerByte field for providers (e.g. BlockFrost) that expose only the flat name. totalReferenceScriptSize resolves reference + spending input script_refs; only applied when the tx has scripts.
  • Collateral resized against the final fee. setCollateral() only had a preliminary (max-by-size) fee; finalizeCollateral recomputes total collateral and the collateral return from the actual fee (ceil(fee * collateralPercent / 100)), avoiding InsufficientCollateral. Explicit/manual collateral is left untouched.
  • Script data hash omits empty datums. ComputeScriptDataHash encoded an empty datum array (0x80) when there were no datums, shifting the hash by one byte and causing ScriptIntegrityHashMismatch for datum-less redeemer txs. Now appends nothing when there are no datums, matching Conway UtxoValidateScriptDataHash.

Tests

finalization_test.go: script-data-hash omission, tiered ref-script fee, collateral resizing from final fee, and explicit-collateral passthrough.

Validated end-to-end on preprod: a reward-withdrawal tx built through Complete() submits and confirms on-chain.

Targets feat/evaluate-tx-additional-utxos (this branch builds on it).

mgpai22 added 8 commits June 23, 2026 18:02
…ently

Several Complete() finalization steps used stale or zero values that the
ledger rejects for script transactions:

- estimateFee/estimateExecutionUnits built the sizing/eval body with a
  zero fee; since the fee field is omitempty a zero value is dropped,
  undercounting the body size (FeeTooSmallUTxO) and producing a fee-less
  body that strict evaluators reject. Use a non-zero placeholder fee.
- minfee omitted the Conway tiered reference-script fee for script txs.
  Add TierRefScriptFee plus protocol-param helpers, and a new
  MinFeeRefScriptCostPerByte field for providers (e.g. BlockFrost) that
  only expose the flat name.
- collateral was sized from the preliminary fee; resize total collateral
  and the collateral return against the final fee (finalizeCollateral),
  leaving explicit/manual collateral untouched.
- ComputeScriptDataHash encoded an empty datum array (0x80) when there
  were no datums, shifting the hash and causing ScriptIntegrityHashMismatch
  for datum-less redeemer txs; append nothing when there are no datums.
TierRefScriptFee accumulated the Conway tiered reference-script fee in float64
and floored at the end. The direction matches the ledger (which floors an exact
Rational), and for the integer base price the drift is far below a lovelace, so
this was correct in practice. Switch to big.Rat with the multiplier recovered
exactly as 6/5 so the result provably equals the ledger minimum for any size and
base, matching the reference implementations (CML, mesh) and removing any
float-boundary risk on large multi-tier reference scripts. Add oracle tests
(single-tier linear, exact match across 1..50 tiers).
…t price

Harden the Conway reference-script fee in estimateFee to match the ledger:
- count every reference script (native and Plutus), not only Plutus, since the
  ledger prices all reference scripts by raw byte size
- compute the size over the resolved input union unconditionally instead of
  gating on hasScripts(); totalReferenceScriptSize returns 0 when no reference
  scripts are present, so non-reference-script txs are unaffected
- return a hard error when reference scripts are present but the protocol
  parameters carry no per-byte price, instead of silently charging zero and
  building an underpriced transaction
A wallet with a single vkey UTxO previously could not build a script
transaction: setCollateral reserved a collateral UTxO out of the
coin-selection pool and validateCollateralDistinctFromInputs rejected any
overlap between inputs and collateral. This was stricter than the Cardano
ledger (and than mesh, lucid, and lucid-evolution), which allow a UTxO to
serve as both because collateral is consumed only on phase-2 script failure
and regular inputs only on success.

setCollateral still prefers a separate eligible vkey UTxO and reserves it,
so multi-UTxO wallets keep an identical transaction shape. When that
reservation starves coin selection, Complete releases the auto-selected
collateral for overlap and retries selection once, so the lone UTxO appears
exactly once in the inputs and once in the collateral.

validateCollateral keeps duplicate detection and enforces
MaxCollateralInputs but no longer rejects input/collateral overlap.
finalizeCollateral sizes total_collateral to ceil(fee * collateralPercent /
100) from the final fee and builds the collateral return from the collateral
inputs' value alone (excess ADA plus any native assets), never touching the
success-path balance. Script-address collateral stays forbidden and
caller-pinned collateral is never rewritten.
- validateCollateral now rejects script-address (non-vkey) collateral for
  caller-pinned AddCollateral, matching the ledger requirement.
- finalizeCollateral validates explicit total_collateral (SetCollateralAmount)
  against ceil(fee*collateralPercent/100) instead of skipping the check.
- size collateral inside the fee-convergence loop so the fee accounts for the
  final collateral total/return footprint.
- restore the collateral reservation when the overlap retry fails so a later
  Complete() starts from consistent state.
- prefer the smallest eligible dedicated collateral to reduce unnecessary
  overlap on multi-UTxO wallets.

Adds tests for manual script-address collateral rejection and below-required
explicit collateral amount.
- finalizeCollateral now emits total_collateral (body key 18) for an explicit
  SetCollateralAmount even when combined with AddCollateral, and recomputes the
  collateral return from the requested amount; previously the requested amount
  was silently dropped.
- reject an explicit collateral amount below the ledger minimum or above the
  collateral inputs instead of building an invalid tx.
- validate fully manual AddCollateral: reject asset-bearing manual collateral
  that has no return (implicit collateral cannot return assets) and under-funded
  collateral inputs, while keeping prior ADA-only implicit-collateral behavior.
- revert to first-eligible dedicated collateral selection so multi-UTxO wallets
  keep an identical tx shape.
- add a final fee verification after collateral finalization to cover the
  non-converged fee path.

Adds tests for explicit-amount emission, manual asset collateral rejection.
…check

- finalizeCollateral now rejects an explicit collateral amount that leaves a
  sub-min-ADA return instead of silently raising total_collateral above the
  requested value (which would forfeit the dust on phase-2 failure). Only the
  auto-sized path absorbs ADA dust.
- remove the post-loop fee re-verification: it relied on the same fee=0 size
  estimate as the main loop so it could not soundly detect underpay, and it
  added fee-adjacent logic outside the collateral scope. Collateral is sized
  inside the convergence loop before each estimate, so the converged fee already
  covers the finalized collateral footprint.

Adds a test for explicit-amount dust rejection.
Bring the collateral-input-overlap change onto the script-tx-finalization
branch. The overlap collateral code (setCollateral overlap fallback,
selectCollateral/collateralEligible helpers, releaseCollateralForOverlap,
restoreCollateralReservation, validateCollateral, the 3-mode
finalizeCollateral, the isUsed overlap handling, and the Complete two-pass
retry) supersedes the older finalizeCollateral and the
validateCollateralDistinctFromInputs check. The reference-script fee code
(TierRefScriptFee, estimateFee ref-script pricing, totalReferenceScriptSize,
placeholder eval fee) and additionalUtxos EvaluateTx support are preserved.

A single wallet UTxO can now serve as both a spending input and collateral.
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