Skip to content

fix: a reservation must be able to fund its own refund - #601

Merged
Zyrtnin merged 2 commits into
mainfrom
fix/reserve-floor-must-cover-its-own-refund
Sep 4, 2026
Merged

fix: a reservation must be able to fund its own refund#601
Zyrtnin merged 2 commits into
mainfrom
fix/reserve-floor-must-cover-its-own-refund

Conversation

@Zyrtnin

@Zyrtnin Zyrtnin commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

pyrxd swap reserve prints this on the interactive consent screen:

* Reclaim at --expiry is GUARANTEEDpyrxd swap refund always works at/after that height (barring a lost key).

Its only amount guard refused below the 546-photon dust floor. But a refund is a one-in-one-out spend that pays its fee out of the covenant value and must still leave a non-dust output — so it needs the relay fee for its own size plus dust, about 2,000,000 photons at the mainnet floor.

Measured through the production builders, before the fix:

reserve accepted? refundable at any fee?
546 yes no
100,000 yes no
1,000,000 yes no
1,900,000 yes no
2,500,000 yes yes

Everything in that band was accepted, told it was guaranteed reclaimable, and unreachable by any pyrxd path. swap cancel — documented as "the ONLY hard revocation" — fails identically, so an unwanted advert could not be revoked either.

The guard existed and measured the wrong quantity

Its own comment named the right one: "a covenant UTXO the maker could not later fill or refund (audit F3, availability)". It is now sized to what a refund actually costs, and exported as minimum_reservable_photons so a caller sizing a reservation can ask for the same number the guard uses.

The size constant is measured, not guessed

radiant_relay_size of refunds built by the production builder across 12 fresh keys gave 192–193 bytes, the spread being DER signature length; 200 leaves headroom for the tail.

My first draft guessed 320 and would have refused honest reservations down to ~1.6× the real floor — a guard refusing valid work, in the fix for a guard that refused too little. Both directions are planted: too low re-opens the band (7 failures), too high refuses honest reservations (1 failure).

Why no test saw it

Every fixture in test_swap_and_nft_fee_floors.py uses _RESERVE = 500_000_000, commented "big enough to pay a real fee out of", and the only sub-floor case uses photons=100 — below dust, so it was refused for the other reason. The band between dust and the real floor was never expressible.

The new tests sweep the property — everything the guard accepts is refundable — rather than restating the constant.

Found by an assertive-prose audit of user-facing text. The finding was the guarantee; the fund-loss path was underneath it.

Full suite: 11,006 passed, 192 skipped, 1 xfailed.

🤖 Generated with Claude Code

Mudwood Labs and others added 2 commits September 3, 2026 21:07
`pyrxd swap reserve` printed, on the interactive consent screen:

    * Reclaim at --expiry is GUARANTEED — `pyrxd swap refund` always works
      at/after that height (barring a lost key).

Its only amount guard refused below the 546-photon DUST floor. A refund is a
one-in-one-out spend that pays its fee OUT OF the covenant value and must still
leave a non-dust output, so it needs the relay fee for its own size PLUS dust -
about 2,000,000 photons at the mainnet floor, not 546.

MEASURED through the production builders, before the fix:

    reserve         546 photons -> accepted, NO refund at any fee
    reserve     100,000 photons -> accepted, NO refund at any fee
    reserve   1,000,000 photons -> accepted, NO refund at any fee
    reserve   1,900,000 photons -> accepted, NO refund at any fee
    reserve   2,500,000 photons -> accepted, refundable

Everything in that band was accepted, told it was guaranteed reclaimable, and
unreachable by any pyrxd path. `swap cancel` - documented as "the ONLY hard
revocation" - fails identically, so an unwanted advert also could not be
revoked.

THE GUARD EXISTED AND MEASURED THE WRONG QUANTITY. Its own comment named the
right one: "a covenant UTXO the maker could not later fill or refund (audit F3,
availability)". It is now sized to what a refund actually costs, exported as
`minimum_reservable_photons` so a caller sizing a reservation can ask for the
same number the guard uses.

THE SIZE CONSTANT IS MEASURED, NOT GUESSED. `radiant_relay_size` of refunds built
by the production builder across 12 fresh keys gave 192-193, the spread being DER
signature length; 200 leaves headroom for the tail. My first draft guessed 320 and
would have refused honest reservations down to ~1.6x the real floor - a guard
refusing valid work, on the fix for a guard that refused too little. Both
directions are planted: too low re-opens the band (7 fail), too high refuses
honest reservations (1 fail).

WHY NO TEST SAW IT. Every fixture in test_swap_and_nft_fee_floors.py uses
_RESERVE = 500_000_000, commented "big enough to pay a real fee out of", and the
only sub-floor case uses photons=100 - below dust, so it was refused for the
other reason. The band between dust and the real floor was never expressible.
The new tests sweep the PROPERTY (everything the guard accepts is refundable)
rather than restating the constant.

One existing test asserted `match="below the dust floor"`; 100 photons is still
refused, now for the reason that was always meant, and its docstring records why
the message changed.

Found by an assertive-prose audit of user-facing text - the finding was the
GUARANTEE, and the fund-loss path was underneath it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both sides were right about different things. #608 established that the dust
floor is NOT a node rule - Radiant never reaches IsDust, since its only caller is
gated on fRequireStandard which is hardcoded false - and that the real reason is
economic. #601 established that the guard was sized to the wrong quantity: a
refund pays its fee out of the covenant value and needs the relay floor for its
own size PLUS dust, not dust alone.

#608's comment had already anticipated the fix in words ('cannot fund the refund
or fill that must later spend it - at the reference node's relay rate that spend
costs millions of photons'). This keeps that reasoning and gives it the threshold
that makes it true.
@Zyrtnin
Zyrtnin merged commit d8dcb1a into main Sep 4, 2026
14 checks passed
@Zyrtnin
Zyrtnin deleted the fix/reserve-floor-must-cover-its-own-refund branch September 4, 2026 08:20
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