Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 42 additions & 12 deletions allways/validator/event_watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -874,19 +874,49 @@ def record_reservation_pin(self, block_num: int, miner: str, reserved_until: int
f'{block_num} — no pin written, will fall back'
)
return
self.state_store.upsert_reservation_pin(
ReservationPin(
miner_hotkey=miner,
reserve_block=block_num,
from_chain=commitment.from_chain,
to_chain=commitment.to_chain,
rate_str=commitment.rate_str,
counter_rate_str=commitment.counter_rate_str,
miner_from_address=commitment.from_address,
miner_to_address=commitment.to_address,
reserved_until=reserved_until,
# Only BACKFILL the settlement pin — never overwrite one the reserve
# handler already wrote. ``handle_swap_reserve`` pins the commitment at
# the instant it validated the user's quote, which is the rate the
# on-chain ``to_amount`` was reserved against. Re-reading at ``block_num``
# can capture a DIFFERENT rate when the miner moved its commitment
# between the handler's read and the on-chain inclusion of
# ``vote_reserve`` — e.g. a miner oscillating its rate every few blocks
# lands the reservation's inclusion block on a stale tick. Overwriting
# then makes the settlement rate disagree with the reserved ``to_amount``
# and the user is short-changed at confirm (see swap 2405: reserved at
# 370, pinned to 280, settled 24% low). The synchronous pin is
# authoritative; this read only matters when that write failed.
#
# NOTE / TODO(contract-v2, multi-validator): with one validator the
# synchronous pin is always present and correct, so preferring it fully
# closes the divergence. Once multiple validators reserve, each one's
# synchronous pin is read at its own instant and they are NOT
# deterministic across the set. The real fix then is to bind
# (reserve_block, rate) into the reservation at quorum and verify
# rate == CommitmentOf(reserve_block) within the user's slippage band,
# so every validator derives an identical, quote-consistent settlement
# rate. That requires the reserve hash + Reservation struct to carry the
# rate, i.e. a smart-contract iteration. Until v2 lands, back off to the
# synchronous pin here.
if self.state_store.get_reservation_pin(miner) is None:
self.state_store.upsert_reservation_pin(
ReservationPin(
miner_hotkey=miner,
reserve_block=block_num,
from_chain=commitment.from_chain,
to_chain=commitment.to_chain,
rate_str=commitment.rate_str,
counter_rate_str=commitment.counter_rate_str,
miner_from_address=commitment.from_address,
miner_to_address=commitment.to_address,
reserved_until=reserved_until,
)
)
else:
bt.logging.info(
f'EventWatcher: reserve-time pin already present for {miner[:8]} '
f'at block {block_num} — preserving synchronous pin (not overwriting)'
)
)
# Emit pin lifecycle events into the scoring overlay. The reservation
# locks in BOTH offered directions for this miner (the contract takes
# the miner offline for any new swap until this reservation resolves),
Expand Down
36 changes: 36 additions & 0 deletions tests/test_event_watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,42 @@ def test_miner_reserved_writes_expected_pin(self, tmp_path: Path):
assert pin.miner_to_address == '5miner'
w.state_store.close()

def test_existing_synchronous_pin_is_not_overwritten(self, tmp_path: Path):
"""The reserve handler's synchronous pin captures the rate the user's
quote was validated against and is authoritative. A later MinerReserved
re-read at the inclusion block must NOT clobber it, even when the miner
moved its commitment in between — that divergence short-changed the user
in swap 2405 (reserved at 370, overwritten to 280)."""
from allways.validator.state_store import ReservationPin

w = make_watcher(tmp_path, netuid=2, subtensor=MagicMock())
# Synchronous pin as handle_swap_reserve writes it at quote time.
w.state_store.upsert_reservation_pin(
ReservationPin(
miner_hotkey='hk_a',
reserve_block=898,
from_chain='btc',
to_chain='tao',
rate_str='370',
counter_rate_str='0.0029',
miner_from_address='bc1-miner',
miner_to_address='5miner',
reserved_until=1000,
)
)
# The inclusion-block read sees the miner's oscillated-down rate.
moved = make_pinned_commitment(rate_str='280')
with patch(
'allways.validator.event_watcher.read_miner_commitment',
return_value=moved,
):
w.apply_event(900, 'MinerReserved', {'miner': 'hk_a', 'reserved_until': 1000})

pin = w.state_store.get_reservation_pin('hk_a')
assert pin.rate_str == '370' # preserved — not overwritten with 280
assert pin.reserve_block == 898
w.state_store.close()

def test_commitment_read_raising_writes_no_pin(self, tmp_path: Path):
"""A transient RPC error or a pruned block must not write a pin and
must not let the exception escape apply_event."""
Expand Down
Loading