Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ jobs:
context: .
file: Dockerfile
push: true
tags: entrius/allways:latest
tags: entrius/allways:latest,entrius/allways:${{ github.sha }}
2 changes: 1 addition & 1 deletion allways/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__version__ = '1.0.7'
__version__ = '1.0.8'
version_split = __version__.split('.')
__spec_version__ = (1000 * int(version_split[0])) + (10 * int(version_split[1])) + (1 * int(version_split[2]))
13 changes: 12 additions & 1 deletion allways/validator/scoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -677,8 +677,9 @@ def snapshot_current_crown_holders(
bt.logging.warning(f'swap-bounds read failed in live snapshot: {e}')
min_swap_amount = max_swap_amount = 0
rows_by_direction: Dict[Tuple[str, str], List[Tuple[str, str, str, float, float, int]]] = {}
bounds_set = min_swap_amount > 0 or max_swap_amount > 0
for from_chain, to_chain in DIRECTION_POOLS:
rates, busy_count, active_set, pinned_rates = reconstruct_window_start_state(
rates, busy_count, active_set, pinned_rates, collaterals = reconstruct_window_start_state(
self.state_store,
self.event_watcher,
from_chain,
Expand All @@ -698,13 +699,23 @@ def snapshot_current_crown_holders(
def executable_check(rate: float, from_chain=from_chain, to_chain=to_chain) -> bool:
return is_executable_rate(rate, from_chain, to_chain, min_swap_amount, max_swap_amount)

def can_fund(
hotkey: str, rate: float, from_chain=from_chain, to_chain=to_chain, collaterals=collaterals
) -> bool:
# Mirror the scoring path's boundary-squat gate so the live table
# never credits a holder whose collateral can't fund their own
# smallest legal leg, which the ledger drops.
min_leg = min_executable_tao_leg(rate, from_chain, to_chain, min_swap_amount, max_swap_amount)
return min_leg == 0 or collaterals.get(hotkey, 0) >= min_leg

holders = crown_holders_at_instant(
rates,
rewardable_hotkeys,
busy=busy_set,
active=active_set,
lower_rate_wins=lower_rate_wins,
executable_rate_check=executable_check,
can_fund_at_rate=can_fund if bounds_set else None,
)
if holders:
share = 1.0 / len(holders)
Expand Down
33 changes: 18 additions & 15 deletions neurons/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,21 +138,6 @@ def __init__(self, config=None):
metagraph_hotkeys=list(self.metagraph.hotkeys),
contract_client=self.contract_client,
)
self.bootstrap_miner_rates()

self.swap_tracker = SwapTracker(client=self.contract_client, metagraph=self.metagraph)
self.swap_tracker.initialize()
# Late-bind the tracker so TimeoutExtensionFinalized events can write
# the new timeout_block straight into the in-memory active swap.
self.event_watcher.swap_tracker = self.swap_tracker
bt.logging.debug(f'Validator components: fee_divisor={self.fee_divisor}, timeout={timeout_blocks}')

self.swap_verifier = SwapVerifier(
chain_providers=self.chain_providers,
fee_divisor=self.fee_divisor,
metagraph=self.metagraph,
state_store=self.state_store,
)

# Separate subtensor/contract/providers for axon handlers (thread safety).
# axon_lock serialises every call on axon_subtensor's websocket so two
Expand All @@ -174,6 +159,24 @@ def __init__(self, config=None):
lock=self.axon_lock,
)

# bootstrap_miner_rates reads bounds_cache, so the block above must
# run first.
self.bootstrap_miner_rates()

self.swap_tracker = SwapTracker(client=self.contract_client, metagraph=self.metagraph)
self.swap_tracker.initialize()
# Late-bind the tracker so TimeoutExtensionFinalized events can write
# the new timeout_block straight into the in-memory active swap.
self.event_watcher.swap_tracker = self.swap_tracker
bt.logging.debug(f'Validator components: fee_divisor={self.fee_divisor}, timeout={timeout_blocks}')

self.swap_verifier = SwapVerifier(
chain_providers=self.chain_providers,
fee_divisor=self.fee_divisor,
metagraph=self.metagraph,
state_store=self.state_store,
)

# Attach synapse handlers to axon
self.attach_axon_handlers()

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "allways"
version = "1.0.7"
version = "1.0.8"
description = "Allways - Universal Transaction Layer: Trustless cross-chain swaps on Bittensor Subnet 7"
license = "MIT"
requires-python = ">=3.10,<3.15"
Expand Down
54 changes: 54 additions & 0 deletions tests/test_scoring_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
replay_crown_time_window,
score_and_reward_miners,
scoring_window_bounds,
snapshot_current_crown_holders,
success_rate,
)
from allways.validator.state_store import ValidatorStateStore
Expand Down Expand Up @@ -729,6 +730,59 @@ def test_busy_state_at_window_start_is_reconstructed(self, tmp_path: Path):
store.close()


class TestSnapshotCurrentCrownHolders:
"""The per-forward-step live-crown snapshot must stay consistent with the
scoring ledger: it reconstructs the same 5-tuple window state and applies
the same boundary-squat gate."""

def _seed_rate(self, store: ValidatorStateStore, hotkey: str, rate: float) -> None:
conn = store.require_connection()
conn.execute(
'INSERT INTO rate_events (hotkey, from_chain, to_chain, rate, block) VALUES (?, ?, ?, ?, ?)',
(hotkey, 'btc', 'tao', rate, 0),
)
conn.commit()

def test_runs_and_credits_funded_holder(self, tmp_path: Path):
"""Regression for the 5-vs-4 unpack crash: reconstruct_window_start_state
returns 5 values and this caller must unpack all of them. A funded miner
with an executable rate shows up as the live crown holder."""
v = make_validator(
tmp_path,
['hk_funded'],
min_swap_amount=100_000_000,
max_swap_amount=500_000_000,
collaterals={'hk_funded': 500_000_000},
)
self._seed_rate(v.state_store, 'hk_funded', 326.0)

rows = snapshot_current_crown_holders(v)

holders = [row[2] for row in rows[('btc', 'tao')]]
assert holders == ['hk_funded']
v.state_store.close()

def test_boundary_squat_excluded_from_live_table(self, tmp_path: Path):
"""The squatter posts the best, executable rate but their 0.15 TAO
collateral can't fund the 0.5 TAO leg it forces. The live table must
drop them to the funded runner-up, matching the ledger."""
v = make_validator(
tmp_path,
['hk_squat', 'hk_funded'],
min_swap_amount=100_000_000,
max_swap_amount=500_000_000,
collaterals={'hk_squat': 150_000_000, 'hk_funded': 500_000_000},
)
self._seed_rate(v.state_store, 'hk_squat', 50000.0) # best rate, can't fund
self._seed_rate(v.state_store, 'hk_funded', 326.0) # runner-up, can fund

rows = snapshot_current_crown_holders(v)

holders = [row[2] for row in rows[('btc', 'tao')]]
assert holders == ['hk_funded']
v.state_store.close()


class TestPinnedRateDuringReservation:
"""Crown calculation must use the pinned rate during the reserved-not-busy
window, not the live rate. Closes the bump-after-pin loophole."""
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.