From dbfb5e3bf8d54ed601461f9ea31a54462d2c95a5 Mon Sep 17 00:00:00 2001 From: anderdc Date: Sun, 31 May 2026 12:39:16 -0500 Subject: [PATCH 1/6] ci(docker): tag pushed image with git sha as well as latest Mirrors gittensor's docker-publish so every main build is pullable by its exact commit sha (entrius/allways:), not just :latest. Makes pinning/rolling back to a known-good build a direct image pull instead of a source rebuild. --- .github/workflows/docker-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 8651cbb8..4b0032e7 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -24,4 +24,4 @@ jobs: context: . file: Dockerfile push: true - tags: entrius/allways:latest + tags: entrius/allways:latest,entrius/allways:${{ github.sha }} From ac49e436b5bd9ffbd1e21d8c8c8b487ad7d10234 Mon Sep 17 00:00:00 2001 From: anderdc Date: Sun, 31 May 2026 12:51:09 -0500 Subject: [PATCH 2/6] fix(scoring): unpack collateral + apply squat gate in live crown snapshot snapshot_current_crown_holders unpacked reconstruct_window_start_state into 4 names, but #423 made it return 5 (added collaterals). On a fresh process the forced first scoring pass hit this and threw 'too many values to unpack (expected 4)' every forward step, blocking weight-setting. Unpack collaterals and feed the same can_fund boundary-squat gate the ledger path uses, so the live crown table no longer credits a holder whose collateral can't fund their own smallest legal leg. Adds the first tests for this function (crash regression + squat exclusion). --- allways/validator/scoring.py | 11 +++++++- tests/test_scoring_v1.py | 54 ++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/allways/validator/scoring.py b/allways/validator/scoring.py index c526a594..7a56becb 100644 --- a/allways/validator/scoring.py +++ b/allways/validator/scoring.py @@ -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, @@ -698,6 +699,13 @@ 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, @@ -705,6 +713,7 @@ def executable_check(rate: float, from_chain=from_chain, to_chain=to_chain) -> b 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) diff --git a/tests/test_scoring_v1.py b/tests/test_scoring_v1.py index 5d4d04e6..cc4cd5ba 100644 --- a/tests/test_scoring_v1.py +++ b/tests/test_scoring_v1.py @@ -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 @@ -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.""" From 7c83d97139b9de5990acc48526b42ed5a59b1e9f Mon Sep 17 00:00:00 2001 From: anderdc Date: Sun, 31 May 2026 12:51:09 -0500 Subject: [PATCH 3/6] fix(validator): construct bounds_cache before bootstrap_miner_rates #437 moved bounds_cache creation after the bootstrap call, so bootstrap_miner_rates read a not-yet-set attribute and logged 'no attribute bounds_cache', falling back to unbounded (min/max=0) commitment reads on cold start. Move the axon/bounds_cache block above the bootstrap call so the bounds are available. --- neurons/validator.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/neurons/validator.py b/neurons/validator.py index 94e200ae..cf2904e2 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -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 @@ -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() From 0c08a30fa03f6476f052e75ce54bfe77cf6f6a35 Mon Sep 17 00:00:00 2001 From: anderdc <61125407+anderdc@users.noreply.github.com> Date: Sun, 31 May 2026 17:51:32 +0000 Subject: [PATCH 4/6] style: auto-fix pre-commit hooks --- allways/validator/scoring.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/allways/validator/scoring.py b/allways/validator/scoring.py index 7a56becb..7eaa2a07 100644 --- a/allways/validator/scoring.py +++ b/allways/validator/scoring.py @@ -699,7 +699,9 @@ 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: + 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. From 458286f428e8b6bf7f74f529da977e9b8736bebf Mon Sep 17 00:00:00 2001 From: anderdc Date: Sun, 31 May 2026 13:06:24 -0500 Subject: [PATCH 5/6] chore: bump version 1.0.7 -> 1.0.8 --- allways/__init__.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/allways/__init__.py b/allways/__init__.py index 8d9040a4..657be2e0 100644 --- a/allways/__init__.py +++ b/allways/__init__.py @@ -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])) diff --git a/pyproject.toml b/pyproject.toml index 7bbe497b..d753fa6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" From 56244be581ed93846b257ca90ddde9cd9fd122c4 Mon Sep 17 00:00:00 2001 From: anderdc <61125407+anderdc@users.noreply.github.com> Date: Sun, 31 May 2026 18:06:45 +0000 Subject: [PATCH 6/6] style: auto-fix pre-commit hooks --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index f6427db6..27624178 100644 --- a/uv.lock +++ b/uv.lock @@ -164,7 +164,7 @@ wheels = [ [[package]] name = "allways" -version = "1.0.7" +version = "1.0.8" source = { editable = "." } dependencies = [ { name = "base58" },