fix(swap_reserve): reject reserves that over-commit source balance (#…#307
Open
thomascolden585-svg wants to merge 1 commit intoentrius:testfrom
Open
Conversation
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.
Problem
Closes #295
handle_swap_reservevalidated the requestor's source-chain balanceagainst the current reserve request only. The smart contract(smart-contracts/ink/lib.rs::reservations) keys reservations byminer hotkey and exposes no source-address index, so neither sideconsidered the user's other still-live reservations from the samesource address.A user with funds for a single swap could therefore reserve multipleminers in parallel, locking honest capacity behind reservations thatcould never all settle and starving real liquidity.
Fix
Introduce a validator-side mirror of the on-chain reservations tableand consult it during reserve so the cumulative committed amount per source address is bounded by the user's visible balance.
allways/validator/reservation_index.py— thread-safe(miner_hotkey -> Reservation)mirror withcommitted_amount_for_address(from_address, from_chain, current_block, exclude_miner)that filters by source chain (BTC funds don't back a TAO reserve) and ignores expired rows.
event_watcher.py— keep the index in sync incrementally:MinerReserved→ fetch the fullReservationrow via contract_client.get_reservation(miner)andupsert`.ReservationCancelled→remove.SwapInitiated→remove(reservation consumed into a swap;contract clears it in
clear_confirmed_reservation).once at startup so the first reserve check is already accurate.
axon_handlers.py::handle_swap_reserve— after the per-request balance/collateral gates, additionally reject whenbalance < committed + synapse.from_amount, with the rejection message naming the amount alreadycommitted across other live reservations.
neurons/validator.py— constructReservationIndexand inject it (plus thecontract_client)into
ContractEventWatcherso the handler and watcher share the same instance.The handler degrades gracefully when no index is wired (legacy /test paths) — the original per-request check still runs.
Files changed
allways/validator/reservation_index.pyallways/validator/event_watcher.pyMinerReserved/ReservationCancelled/ drop onSwapInitiated, hydrate at bootstrapallways/validator/axon_handlers.pyhandle_swap_reserveneurons/validator.pyReservationIndextests/test_reservation_index.pyTest plan
uv run pytest tests/test_reservation_index.py— 19 new testscovering:
committed_amount_for_address: sums across miners for the same(from_addr, from_chain),ignores other addresses, other chains,expired rows, and the
exclude_minerself-row; returns 0 forempty address/chain.
removeclears entries;hydrate_from_contractloads only live rows and swallows per-hotkey RPC errors.MinerReservedupserts via contract read (and survives a failed read),ReservationCancelledandSwapInitiateddrop the row.handle_swap_reserveend-to-end: rejects when other livereservations would exhaust the balance, accepts when the total fits, ignores commitments from other addresses
or expired reservations, and falls through cleanly when no index is attached.
uv run pytest tests/— full suite green.uv run ruff check allways/ neurons/— clean.uv run pre-commit run --all-files— clean.Risk / rollout
so steady- state minersee no behavior change.
contract_clientandreservation_indexare bothOptionalonContractEventWatcher, so existing callers/teststhat don't pass them continue to work.