feat(validator): pin miner rate + address to the reservation#338
Merged
Conversation
eb94920 to
c0473f1
Compare
Settlement keys off the rate pinned at reservation (and delivered amount), so the to_amount == f(rate) struct check was redundant with the reserve-time slippage gate and non-actionable at confirm — a mismatch could only time out and slash the miner for a decision already made at reserve. Removing it also lets within-slippage reservations complete instead of timing out.
Read the miner's commitment as of the reservation block (reserved_until - reservation_ttl) and send there, matching the address the validator pins and verifies against. Avoids a mismatch if the miner moves its deposit address between the quote and the reservation landing on-chain. Falls back to the quoted address on any read failure.
296e38d to
539532b
Compare
entrius
approved these changes
May 20, 2026
This was referenced 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.
Swap 79 — what spawned this
Diagnosing allways swap 79 surfaced that a reservation pins the swap amounts
(
to_amount/tao_amount/from_amount) but not the miner's rate or itsdeposit/fulfillment addresses. At
initiate(~20 min after reserve, gated byBTC confirmation) the validator re-reads the miner's live commitment — so a
miner can move its commitment in the reserve→initiate window and the swap still
settles against the moved values.
The issue
Two exploits follow from
handle_swap_confirmre-reading the live commitment(
allways/validator/axon_handlers.py—load_swap_commitmentat thecommitment = ...call inside theaxon_lockblock):selected_rate_strflowing intoscale_encode_initiate_hash_inputandvote_initiateis the moved rate, shortchanging the user.after the user has sent BTC.
verify_transactionis then called with the newminer_from_address(axon_handlers.py, theprovider.verify_transaction(...)call),
vote_initiatefails verification, no swap is created, no slash fires —the miner keeps the BTC.
The fix
A validator-side, event-driven pin index — no contract change, consensus-safe:
state_store.py— newReservationPindataclass +reservation_pinstable(purely additive, no migration) with
upsert/get/remove/purge/updatemethods and
delete_hotkeycleanup.event_watcher.py— the watcher gainsnetuid/subtensor; a newMinerReservedbranch readsread_miner_commitment(..., block=R)at thecanonical reservation block
Rand persists the commitment snapshot.SwapInitiated/SwapTimedOutclear the pin;ReservationExtensionFinalizedbumps its TTL.
axon_handlers.py—handle_swap_confirmresolves rate + addresses from thepin (synthesizing a
MinerPairand feeding the existingresolve_swap_direction), falling back to the live commitment when no pinexists.
forward.py—purge_expired_reservation_pins()beside the existingpending-confirm purge.
neurons/validator.py— wiresnetuid/subtensorinto the watcher.The index is keyed on the canonical block
Rand a canonical commitment read,so every validator derives a byte-identical pin → an identical
request_hash.A validator with no pin falls back; it never produces a wrong pin. On any
failure (transient RPC, pruned block) no pin is written.
Review notes
vote_initiatehash inputs.hash from the live commitment and upgraded ones from the pin; if a miner moved
its rate/address the fleet can split on
vote_initiate. The exploit closesonly once a quorum runs the pin.
watcher
MinerReserved/lifecycle, axon address-theft + rate-swingregressions). The plan's integration test (reserve, move the miner's
commitment, confirm) still needs a testnet run.
back to the live commitment — chosen over rejecting so in-flight swaps aren't
broken. The exploit window stays open only for reservations straddling the
deploy.