Skip to content

feat(validator): pin miner rate + address to the reservation#338

Merged
entrius merged 3 commits into
testfrom
feat/pin-reservation-rate-address
May 20, 2026
Merged

feat(validator): pin miner rate + address to the reservation#338
entrius merged 3 commits into
testfrom
feat/pin-reservation-rate-address

Conversation

@anderdc

@anderdc anderdc commented May 18, 2026

Copy link
Copy Markdown
Collaborator

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 its
deposit/fulfillment addresses. At initiate (~20 min after reserve, gated by
BTC 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_confirm re-reading the live commitment
(allways/validator/axon_handlers.pyload_swap_commitment at the
commitment = ... call inside the axon_lock block):

  • Rate-swing — a miner moves its rate between reserve and confirm; the
    selected_rate_str flowing into scale_encode_initiate_hash_input and
    vote_initiate is the moved rate, shortchanging the user.
  • Address theft (total loss) — a miner changes its committed deposit address
    after the user has sent BTC. verify_transaction is then called with the new
    miner_from_address (axon_handlers.py, the provider.verify_transaction(...)
    call), vote_initiate fails 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 — new ReservationPin dataclass + reservation_pins table
    (purely additive, no migration) with upsert/get/remove/purge/update
    methods and delete_hotkey cleanup.
  • event_watcher.py — the watcher gains netuid/subtensor; a new
    MinerReserved branch reads read_miner_commitment(..., block=R) at the
    canonical reservation block R and persists the commitment snapshot.
    SwapInitiated/SwapTimedOut clear the pin; ReservationExtensionFinalized
    bumps its TTL.
  • axon_handlers.pyhandle_swap_confirm resolves rate + addresses from the
    pin (synthesizing a MinerPair and feeding the existing
    resolve_swap_direction), falling back to the live commitment when no pin
    exists.
  • forward.pypurge_expired_reservation_pins() beside the existing
    pending-confirm purge.
  • neurons/validator.py — wires netuid/subtensor into the watcher.

The index is keyed on the canonical block R and 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

  • This is consensus-path code — it changes the provenance of the
    vote_initiate hash inputs.
  • Needs a coordinated validator upgrade: mid-rollout, un-upgraded validators
    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 closes
    only once a quorum runs the pin.
  • Not yet testnet-tested — unit-tested only (state-store round-trip/purge,
    watcher MinerReserved/lifecycle, axon address-theft + rate-swing
    regressions). The plan's integration test (reserve, move the miner's
    commitment, confirm) still needs a testnet run.
  • Pre-existing reservations made before this deploys have no pin and fall
    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.

@xiao-xiao-mao xiao-xiao-mao Bot added the bug Something isn't working label May 18, 2026
@anderdc anderdc force-pushed the feat/pin-reservation-rate-address branch from eb94920 to c0473f1 Compare May 20, 2026 16:54
anderdc and others added 3 commits May 20, 2026 14:47
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.
@LandynDev LandynDev force-pushed the feat/pin-reservation-rate-address branch from 296e38d to 539532b Compare May 20, 2026 19:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SwapConfirm uses live commitment instead of reserved terms

3 participants