fix: a reservation must be able to fund its own refund - #601
Merged
Conversation
`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.
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.
pyrxd swap reserveprints this on the interactive consent screen: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:
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_photonsso a caller sizing a reservation can ask for the same number the guard uses.The size constant is measured, not guessed
radiant_relay_sizeof 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.pyuses_RESERVE = 500_000_000, commented "big enough to pay a real fee out of", and the only sub-floor case usesphotons=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