Fix swap quote row selection and close TAO→BTC crown loophole#420
Merged
Conversation
Two related issues in the quote/scoring pipeline: quote.py (issue #393): - Sort is now direction-aware: ascending for TAO→BTC (lower TAO/BTC = better user price), matching `swap now` and validator crown. Previously sorted descending unconditionally. - Removed the up-front bounds abort that used `available[0]`'s rate to derive the TAO leg. For BTC→TAO, the TAO leg depends on the miner's rate, so a single sentinel top row was killing the quote even when other miners were viable. Per-miner viability already covers it; the aggregate end-of-table hint preserves the "Decrease/Increase --amount" cue. - Per-row `is_executable_rate` gate downgrades sentinel rates to `unexecutable rate` so they don't get labeled "available". utils/rate.py (issue #396): - `is_executable_rate` now gates TAO→BTC the same way it gates BTC→TAO. Sentinel-low rates (e.g. 1e-8 TAO/BTC) imply absurd BTC payouts; the symmetric inverse check (treat 1/r as a BTC→TAO rate, run the same integer-overshoot logic) catches them without introducing policy. - Closes the crown-gaming loophole PR #395 left open: a miner posting r=1e-8 was winning the TAO→BTC lowest-rate-wins crown every block without ever serving a swap. They no longer pass the predicate. Tests: - tests/test_swap_quote.py (new) — CliRunner-driven regressions for both the sort fix and the unexecutable-row labeling. - tests/test_rate.py — flipped the gap-documenting test, added 4 new boundary tests for the TAO→BTC symmetric branch.
entrius
approved these changes
May 28, 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.
Fixes #393 and #396.
Summary
Two related fixes in the quote / scoring pipeline. Both were exploitable by miners posting sentinel rates that won't actually serve users.
#393 —
alw swap quoterow selectionquote.pywas sortingreverse=Trueunconditionally. Correct for BTC→TAO (higher rate = better user price); wrong for TAO→BTC, where lower TAO/BTC is the better price. The fix matches the pattern already used byswap now(swap.py:773) and validator crown (scoring.py:567).available[0]'s rate to derive the TAO leg and aborted with "No miner can accept this" when it failed. For BTC→TAO the TAO leg depends on the miner's rate, so a single sentinel top row (e.g.1.797e+308) was hiding viable lower rows like UID 189. Per-miner viability already covers the bounds check; an aggregate end-of-table hint preserves the "Decrease/Increase--amount" cue when every routable miner fails the same bound.available.is_executable_rate(the predicate from PR Filter unexecutable rates from crown eligibility #395) is now consulted per row in the quote table so sentinel BTC→TAO rates render asunexecutable rate, notavailable.#396 — TAO→BTC crown loophole
PR #395 closed BTC→TAO sentinel rates from earning crown but explicitly left TAO→BTC open. The code and tests documented this gap:
A miner posting
r=1e-8TAO/BTC was winning the lowest-rate-wins TAO→BTC crown every block without ever serving a swap.This PR closes the gap with a symmetric inverse check: for TAO→BTC, treat
1/ras if it were a BTC→TAO rate and apply the same integer-overshoot logic. If1/r's smallest positive sat would overshootmax_swap_rao, the original rate is sentinel-low by symmetry. Concretely with current SN7 bounds (min_swap=0.1 TAO,max_swap=0.5 TAO):r(TAO/BTC)The check is policy-free — it uses only
min_swap/max_swap, which validators already agree on — and consistent with #395's posture: it's the same kind of integer-overshoot guard, applied symmetrically to the other direction.What this does NOT change
Files changed
allways/utils/rate.py—is_executable_ratenow gates both directions symmetrically; docstring rewritten; the gap-documenting comment block is gone.allways/cli/swap_commands/quote.py— direction-aware sort, removedsample_pairabort, per-rowis_executable_rate, aggregate bounds hint, unexecutable-aware best-miner hint.tests/test_rate.py— flippedtest_tao_to_btc_lowball_rate_passes_predicate→test_tao_to_btc_lowball_rate_rejected; added 4 new boundary tests for the symmetric TAO→BTC branch.tests/test_swap_quote.py(new) —CliRunner-driven regressions: sentinel top row doesn't hide a viable lower row; sentinel row labeled unexecutable; aggregate "decrease --amount" hint; TAO→BTC ascending sort; TAO→BTC sentinel-low labeled unexecutable.Test plan
pytest tests/test_rate.py— 52 passed (4 new TAO→BTC boundary tests)pytest tests/test_swap_quote.py— 5 passed (new file)pytest tests/test_scoring_v1.py— 117 passed (crown gating unaffected for sane rates)embit-module failures unrelated to this PR): 564 passedruff checkclean