Skip to content

feat(resolver): size collateral inputs from the collateral percentage - #354

Draft
scarmuega wants to merge 2 commits into
mainfrom
feat/resolver-collateral-sizing
Draft

feat(resolver): size collateral inputs from the collateral percentage#354
scarmuega wants to merge 2 commits into
mainfrom
feat/resolver-collateral-sizing

Conversation

@scarmuega

@scarmuega scarmuega commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Plan: plans/lang-collateral-sizing-min-ada.md (tx3 Trellis root, type: code-change-request, repo: solution/lang/tx3).

Draft — item 1 of the plan is implemented and verified in-repo; item 2 needs a chain reproduction this session could not run. See "Done criterion" below.

The defect

A .tx3 collateral block declares what the author knows. Asteria's is representative:

collateral {
    from: Player,
    min_amount: fees,
}

The ledger asks for collateralPercentage of the fee — 150% on every Cardano network since Alonzo — so a UTxO that satisfies the declared query is a third short of what the ledger accepts. Nothing in the resolution path closed the gap: compile_collateral (crates/tx3-cardano/src/compile/mod.rs:450) only passes through the UTxOs the TIR names, and the resolver's input pipeline selected against the declared min_amount verbatim. Result: Asteria create-ship CBOR with insufficient collateral.

What changed

All of it in crates/tx3-resolver:

  • CanonicalQuery::raise_lovelace_floor — widens a query's lovelace requirement, never narrows it, and leaves other asset requirements alone.
  • required_collateral(fees, percentage)ceil(fees * percentage / 100). The ceiling matters: the ledger rejects a collateral balance short by one lovelace.
  • ResolveJob::resolve_inputs applies the floor to every query with collateral: true, before narrow → approximate → assign. Selection then picks a UTxO that actually covers the requirement, and when none does, resolution fails with InputNotResolved instead of emitting a tx the ledger will reject.
  • The fee for the current pass is tracked on the job (ResolveJob::fees), set where eval_pass already reads it. On the first pass the fee is 0, so sizing is a no-op and the fixpoint loop still gets its first fee estimate.
  • New ResolveOptions { max_optimize_rounds, collateral_percentage }, with collateral_percentage defaulting to DEFAULT_COLLATERAL_PERCENTAGE = 150.

Non-breaking. resolve_tx keeps its exact signature and picks up the default, so dolos/crates/trp and tx3-hydra build unchanged and get the fix on their next bump. Callers holding live protocol parameters can pass their own percentage through the new resolve_tx_with_options. (ResolveJob::execute — the lower-level entry point — now takes &ResolveOptions; no in-org caller uses it.)

Why the percentage is a resolver option rather than a PParams read

Sizing has to happen where UTxO selection happens: the resolver. The resolver is chain-agnostic and reaches the compiler only through tx3_tir::compile::Compiler, whose surface is compile + reduce_op — it cannot read tx3_cardano::PParams, and CompilerOp lives in the published tx3-tir crate (core/tir, a different repository), so a ComputeCollateral op is out of scope for a one-repo change. Threading the live value therefore has to come from the caller, which is exactly what ResolveOptions is for. Adding a collateral_percentage field to tx3_cardano::PParams in this PR would only have broken the struct literals in dolos and tx3-hydra without anything reading it yet; it is filed as follow-up work instead.

Verification

cargo test --workspace — 350 tests, 0 failures. cargo clippy --all-targets --all-features clean, cargo fmt --all applied.

New tests:

  • inputs::canonical::tests (7) — the percentage arithmetic including the ceiling, and the floor's widen / never-narrow / absent / zero-fee / other-assets-preserved behaviour.
  • inputs::tests::test_collateral_sized_from_percentage_of_fee — a TIR carrying the Asteria collateral shape (min_amount: fees) against a pool holding a UTxO that covers the fee but not 150% of it and one that covers both: the ample one is selected.
  • inputs::tests::test_collateral_percentage_is_configurable — at 100% the tight UTxO is chosen, so the floor really is driven by the option and not hardcoded.
  • inputs::tests::test_collateral_short_of_percentage_does_not_resolve — no candidate reaches 150%: InputNotResolved, not a bad tx.
  • inputs::tests::test_collateral_untouched_on_the_zero_fee_pass — the first pass is unaffected.

The simple_transfer golden fixture is unchanged (it declares no collateral), and no existing hash or fee assertion moved.

Done criterion — partially met

The plan's criterion has three parts:

  1. Asteria create-ship collateral is sufficient — the mechanism is implemented and covered by the tests above, driven by Asteria's exact collateral shape. The live resolve of solution/protocols/txpipe/asteria against a devnet/TRP was not run: this session has no chain access, no funded wallet, and no Asteria game state. Someone with a devnet should confirm before this leaves draft.
  2. Bodega (-32004) change-output min-adanot addressed. The plan requires a reproduction first ("if it no longer reproduces, record that and change nothing"), and that reproduction is not runnable here. Recorded as a blocker on the plan rather than guessed at. Static finding, for whoever picks it up: there is no min-ada backfill or check anywhere on the output path — compile_output_block (crates/tx3-cardano/src/compile/mod.rs:193) compiles the amount the author wrote, and min_utxo(...) is an explicit language-level call the author must make. So the gap is real in code; what is undecided is whether a resolver-side backfill is wanted at all, since silently raising an output amount breaks the balance the author wrote.
  3. Unit/regression tests for the sizing computation — met.

Escalations

Blocker filed on the plan (--by coder, to org/founder): who runs the Asteria and Bodega chain reproductions, on what network, before this can leave draft — and whether a change-output min-ada backfill is wanted at all if Bodega still reproduces.

Follow-up filed

plans/collateral-percentage-from-live-pparams.md (draft, tx3 root) — carry the live collateralPercentage protocol parameter into resolution end to end: add the field to tx3_cardano::PParams and have dolos/crates/trp and tx3-hydra pass it through resolve_tx_with_options. Crosses three repositories, so it is not this plan's to land.

🤖 Generated with Claude Code

A `.tx3` collateral block declares what the author knows — typically
`min_amount: fees`. The ledger asks for `collateralPercentage` of the fee
(150% on every Cardano network since Alonzo), so a UTxO that satisfies the
declared query is a third short of what the ledger accepts, and the tx is
rejected for insufficient collateral (seen on Asteria create-ship).

Nothing in the resolution path sized collateral: `compile_collateral` only
passes through whatever the TIR named. Raise the lovelace floor of every
collateral query to `ceil(fees * collateral_percentage / 100)` before the
narrow → approximate → assign pipeline runs, so selection picks a UTxO that
actually covers the requirement — or fails loudly with `InputNotResolved`
rather than emitting a tx the ledger will reject.

The percentage is a knob on the new `ResolveOptions`, defaulting to 150.
`resolve_tx` keeps its signature and picks up the default; callers holding
live protocol parameters can pass their own through
`resolve_tx_with_options`.

Ref: plans/lang-collateral-sizing-min-ada.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The configurability test asserted which of two candidates the ranker picks
at 100%, which the mock store's randomized refs make flaky — green locally,
red on CI. Assert the option's actual effect instead: the same pool that
fails at 150% resolves at 100%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant