From 87fcd5f8c4964e2cc291665768f35f3fb6e25e7a Mon Sep 17 00:00:00 2001 From: anderdc Date: Mon, 1 Jun 2026 12:36:53 -0500 Subject: [PATCH 1/3] Fix collateral-baseline zeroing that recycled emissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The in-process event watcher recorded a miner's collateral as 0 whenever it applied a fee/slash delta with no prior baseline: _apply_collateral_delta did prior + delta where _latest_collateral returned 0 for an unseen miner, and the result clipped to 0. The #409/#423 capacity / can_fund crown gate then read that 0 and dropped the miner from crown entirely, so honest active best-rate miners earned nothing and the pool recycled. - _apply_collateral_delta: skip when there is no known baseline (return None from _latest_collateral) instead of fabricating a 0. - reconcile_collateral_from_contract: resync active miners' collateral to on-chain truth; run at startup (heals the warm-restart path) and once per scoring round (corrects drift). Writes at the current block only, preserving the per-block capacity property. - scoring gate: fail open on unknown collateral (absent != zero) in both the replay and the live snapshot — the contract auto-deactivates anyone below min_collateral, so an active miner always holds enough. - diagnose_non_earner: direction-aware (tao->btc lower-wins) and collateral- aware, so it reports insufficient_collateral / unknown_collateral instead of mislabeling a better-rate miner as 'outbid'. Tests cover the no-baseline regression, reconcile (heal/skip/idempotent/rpc), fail-open vs known-zero, and the diagnosis reasons. --- allways/validator/event_watcher.py | 67 ++++++++++++- allways/validator/forward.py | 10 ++ allways/validator/scoring.py | 21 +++- allways/validator/scoring_trace.py | 73 ++++++++++++-- neurons/validator.py | 10 ++ tests/test_scoring_v1.py | 154 ++++++++++++++++++++++++++--- 6 files changed, 304 insertions(+), 31 deletions(-) diff --git a/allways/validator/event_watcher.py b/allways/validator/event_watcher.py index fadf553..fe3b52f 100644 --- a/allways/validator/event_watcher.py +++ b/allways/validator/event_watcher.py @@ -480,6 +480,49 @@ def cold_bootstrap( self._record_collateral_event(self.cursor, hotkey, collateral) self.state_store.set_event_cursor(self.cursor) + def reconcile_collateral_from_contract( + self, + current_block: int, + metagraph_hotkeys: List[str], + contract_client: Any, + ) -> int: + """Resync each active miner's collateral against the contract. + + The per-event series can drift from on-chain truth — a missed + CollateralPosted leaves a stale/absent baseline, and a fee/slash delta + on no baseline used to fabricate a 0 — and the capacity / can_fund gate + then reads that as low/zero collateral and drops the miner from crown. + Reading the contract for each active miner and recording a fresh event + when the value changed reconciles the series to truth. + + Called at startup (heals the warm-restart path, which does no contract + reads) and once per scoring round. Only active miners can hold crown, + so we skip the rest — both correct and cheap. Writes at + ``current_block`` only, never retroactively, preserving the #409 + no-post-window-top-up property. RPC failures skip that hotkey and keep + the prior value. Returns the count of miners updated.""" + if not metagraph_hotkeys or contract_client is None: + return 0 + updated = 0 + for hotkey in metagraph_hotkeys: + if hotkey not in self.active_miners: + continue + try: + collateral = int(contract_client.get_miner_collateral(hotkey)) + except Exception as e: + bt.logging.debug(f'EventWatcher reconcile: collateral read failed for {hotkey[:8]}: {e}') + continue + if collateral < 0 or self._latest_collateral(hotkey) == collateral: + continue + self._record_collateral_event(current_block, hotkey, collateral) + updated += 1 + if updated: + bt.logging.info( + f'EventWatcher: reconciled collateral for {updated} active miner(s) ' + f'from contract @ block {current_block}' + ) + return updated + def hydrate_from_db(self) -> None: """Rebuild every in-memory mirror from state.db. Called on warm restart when the persisted cursor is within one scoring window of head — the @@ -974,11 +1017,14 @@ def apply_busy_delta(self, block_num: int, hotkey: str, delta: int, swap_id: Opt self.busy_events.append(BusyEvent(hotkey=hotkey, delta=delta, block=block_num)) self.state_store.insert_busy_event(block_num, hotkey, delta, swap_id) - def _latest_collateral(self, hotkey: str) -> int: - """Last known collateral for ``hotkey`` (0 if no event has fired).""" + def _latest_collateral(self, hotkey: str) -> Optional[int]: + """Last known collateral for ``hotkey``, or ``None`` if no event has + fired. ``None`` means *unknown* (we never observed a baseline), which + callers must not conflate with a known zero — applying a fee/slash + delta against an unknown baseline would fabricate a spurious 0.""" history = self.collateral_events_by_hotkey.get(hotkey) if not history: - return 0 + return None return history[-1].collateral_rao def _record_collateral_event(self, block_num: int, hotkey: str, collateral_rao: int) -> None: @@ -999,8 +1045,21 @@ def _apply_collateral_delta(self, block_num: int, hotkey: str, delta_rao: int) - """Apply a signed delta against the last-known collateral. Used for fee (``confirm_swap``) and slash (``timeout_swap``) deductions that ``apply_collateral_penalty`` silently makes without emitting a - ``CollateralWithdrawn``. Clipped at zero.""" + ``CollateralWithdrawn``. Clipped at zero. + + With no known baseline the delta is meaningless — ``prior`` would be a + fabricated 0, so ``0 + (-fee)`` clips to 0 and permanently pins the + miner at zero collateral, dropping them from crown via the capacity / + can_fund gate. Skip instead; ``reconcile_collateral_from_contract`` + and the next genuine CollateralPosted/Withdrawn establish the baseline, + and the scoring gate fails open while collateral is unknown.""" prior = self._latest_collateral(hotkey) + if prior is None: + bt.logging.debug( + f'EventWatcher: skipping collateral delta {delta_rao} for {self._label(hotkey)} ' + f'@ block {block_num} — no known baseline (would fabricate 0)' + ) + return self._record_collateral_event(block_num, hotkey, prior + delta_rao) def prune_old_events(self, current_block: int) -> None: diff --git a/allways/validator/forward.py b/allways/validator/forward.py index fb7d43f..de2d0c2 100644 --- a/allways/validator/forward.py +++ b/allways/validator/forward.py @@ -105,6 +105,16 @@ async def forward(self: Validator) -> None: enforce_swap_timeouts(self, tracker, uncertain_swaps) if due_for_scoring(self.block, self.last_scored_block, self.initial_scoring_done): + # Resync active miners' collateral to on-chain truth before scoring so + # a drifted/absent baseline can't drop an active miner from crown via + # the capacity / can_fund gate. Writes at the current block only, so it + # never retroactively rescales capacity already earned in this window. + try: + self.event_watcher.reconcile_collateral_from_contract( + self.block, list(self.metagraph.hotkeys), self.contract_client + ) + except Exception as e: + bt.logging.warning(f'forward: collateral reconcile failed: {e}') score_and_reward_miners(self) self.initial_scoring_done = True bt.logging.info('forward: scoring done') diff --git a/allways/validator/scoring.py b/allways/validator/scoring.py index 7eaa2a0..0e34a6c 100644 --- a/allways/validator/scoring.py +++ b/allways/validator/scoring.py @@ -294,6 +294,8 @@ def calculate_miner_rewards(self: Validator) -> Tuple[np.ndarray, Set[int]]: distributed=distributed, recycled=recycled, weighting_traces=weighting_traces, + min_swap_rao=min_swap_amount, + max_swap_rao=max_swap_amount, ) if storage_enabled: @@ -577,8 +579,14 @@ def can_fund(hotkey: str, rate: float) -> bool: # Boundary-squat per-block gate: a miner whose own rate forces a TAO # leg larger than their collateral_at_block earns no crown for that # block. Cascades to the next-best rate via crown_holders_at_instant. + # Fail open when collateral is *unknown* (no event ever recorded): + # absent != zero. The contract auto-deactivates anyone below + # min_collateral, so an active miner always holds enough; treating a + # missing baseline as 0 would silently drop them from crown. + if hotkey not in collaterals: + return True min_leg = min_executable_tao_leg(rate, from_chain, to_chain, min_swap_rao, max_swap_rao) - return min_leg == 0 or collaterals.get(hotkey, 0) >= min_leg + return min_leg == 0 or collaterals[hotkey] >= min_leg def credit_interval(interval_start: int, interval_end: int) -> None: duration = interval_end - interval_start @@ -607,7 +615,9 @@ def credit_interval(interval_start: int, interval_end: int) -> None: split = duration / len(holders) for hk in holders: crown_blocks[hk] = crown_blocks.get(hk, 0.0) + split - cap = capacity_factor(collaterals.get(hk, 0), max_swap_rao) + # Unknown collateral (no event recorded) → capacity 1.0, matching + # can_fund's fail-open. Only a known value scales capacity down. + cap = capacity_factor(collaterals[hk], max_swap_rao) if hk in collaterals else 1.0 cap_weighted_blocks[hk] = cap_weighted_blocks.get(hk, 0.0) + split * cap def apply_event(event: ReplayEvent) -> None: @@ -704,9 +714,12 @@ def can_fund( ) -> 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. + # smallest legal leg, which the ledger drops. Fail open on unknown + # collateral (absent != zero) to match the scoring path exactly. + if hotkey not in collaterals: + return True 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 + return min_leg == 0 or collaterals[hotkey] >= min_leg holders = crown_holders_at_instant( rates, diff --git a/allways/validator/scoring_trace.py b/allways/validator/scoring_trace.py index 8358142..2fe67f3 100644 --- a/allways/validator/scoring_trace.py +++ b/allways/validator/scoring_trace.py @@ -10,12 +10,14 @@ import bittensor as bt import numpy as np +from allways.chains import canonical_pair from allways.constants import ( CREDIBILITY_MAX_TIMEOUTS, CREDIBILITY_RAMP_OBSERVATIONS, RECYCLE_UID, TAO_TO_RAO, ) +from allways.utils.rate import min_executable_tao_leg if TYPE_CHECKING: from allways.validator.scoring import DirectionTrace @@ -72,6 +74,8 @@ def log_scoring_trace( distributed: float, recycled: float, weighting_traces: Optional[Dict[str, 'WeightingTrace']] = None, + min_swap_rao: int = 0, + max_swap_rao: int = 0, ) -> None: hotkeys = self.metagraph.hotkeys recycle_uid = RECYCLE_UID if RECYCLE_UID < len(rewards) else 0 @@ -112,8 +116,24 @@ def log_scoring_trace( f' uid={uid} hotkey={hk[:8]}.. crown_blk={crown_blk:.0f} sr={sr:.3f}{extras} reward={crown_reward:.3f}' ) + # Collateral as-of window_start mirrors the scoring replay's starting + # state, so the non-earner diagnosis can tell "excluded by collateral" + # from "genuinely outbid". Absent hotkey == unknown (fail-open), per the + # gate in scoring.py. + collaterals = dict(self.event_watcher.get_miner_collaterals_at(window_start)) lines.extend( - non_earner_lines(self, window_start, window_end, rewards, success_rates, direction_traces, recycle_uid) + non_earner_lines( + self, + window_start, + window_end, + rewards, + success_rates, + direction_traces, + recycle_uid, + collaterals, + min_swap_rao, + max_swap_rao, + ) ) if recycled > 0: @@ -136,7 +156,11 @@ def non_earner_lines( success_rates: Dict[str, float], direction_traces: Dict[Tuple[str, str], DirectionTrace], recycle_uid: int, + collaterals: Optional[Dict[str, int]] = None, + min_swap_rao: int = 0, + max_swap_rao: int = 0, ) -> List[str]: + collaterals = collaterals or {} ever_active = set(self.event_watcher.get_active_miners_at(window_start)) for e in self.event_watcher.get_active_events_in_range(window_start, window_end): if e['active']: @@ -155,7 +179,9 @@ def non_earner_lines( if not latest_rates and hk not in ever_active: continue sr = success_rates.get(hk, 1.0) - reason = diagnose_non_earner(hk, latest_rates, sr, ever_active, direction_traces) + reason = diagnose_non_earner( + hk, latest_rates, sr, ever_active, direction_traces, collaterals, min_swap_rao, max_swap_rao + ) out.append(f' uid={uid} hotkey={hk[:8]}.. crown_blk=0 reason="{reason}" sr={sr:.3f}') if len(out) >= NON_EARNER_LINE_CAP: break @@ -168,16 +194,47 @@ def diagnose_non_earner( sr: float, ever_active: Set[str], direction_traces: Dict[Tuple[str, str], DirectionTrace], + collaterals: Optional[Dict[str, int]] = None, + min_swap_rao: int = 0, + max_swap_rao: int = 0, ) -> str: + """Best-effort reason a miner earned no crown. Direction-aware: tao→btc is + lower-rate-wins, btc→tao higher-wins, so "outbid" only fires when the + miner's own rate is genuinely worse than the winner's. A rate that is at + least as good as the winner's but still earned nothing was excluded by the + capacity / can_fund collateral gate — report that, not "outbid".""" + collaterals = collaterals or {} if not latest_rates: return 'no_rate_posted' if hotkey not in ever_active: return 'not_active_during_window' if sr <= 0: return 'credibility_zero' # zero observations OR all-timeout history - parts = [ - f'{direction[0]}→{direction[1]}: own={own:g} vs best={direction_traces[direction].best_rate:g}' - for direction, own in latest_rates.items() - if direction in direction_traces and direction_traces[direction].best_rate > 0 - ] - return 'outbid (' + '; '.join(parts) + ')' if parts else 'no_competing_winner' + + outbid_parts: List[str] = [] + for (from_c, to_c), own in latest_rates.items(): + trace = direction_traces.get((from_c, to_c)) + if trace is None or trace.best_rate <= 0: + continue + best = trace.best_rate + canon_from, _ = canonical_pair(from_c, to_c) + lower_wins = from_c != canon_from + competitive = own <= best if lower_wins else own >= best + if not competitive: + outbid_parts.append(f'{from_c}→{to_c}: own={own:g} vs best={best:g}') + continue + # Rate is at least as good as the winner's, yet earned nothing — a + # qualification gate dropped this miner. Collateral is the usual cause. + if hotkey not in collaterals: + return f'unknown_collateral ({from_c}→{to_c}: own={own:g} beats/ties best={best:g}, no baseline)' + min_leg = min_executable_tao_leg(own, from_c, to_c, min_swap_rao, max_swap_rao) + have = collaterals[hotkey] + if min_leg > 0 and have < min_leg: + return ( + f'insufficient_collateral ({from_c}→{to_c}: have={have / TAO_TO_RAO:g}t ' + f'need={min_leg / TAO_TO_RAO:g}t)' + ) + # Competitive and funded — lost to a tie split, busy, or active-flag timing. + return f'competitive_but_unfilled ({from_c}→{to_c}: own={own:g} vs best={best:g})' + + return 'outbid (' + '; '.join(outbid_parts) + ')' if outbid_parts else 'no_competing_winner' diff --git a/neurons/validator.py b/neurons/validator.py index cf2904e..4d3345d 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -138,6 +138,16 @@ def __init__(self, config=None): metagraph_hotkeys=list(self.metagraph.hotkeys), contract_client=self.contract_client, ) + # Heal collateral on startup: a warm restart hydrates from state.db and + # does no contract reads, so any baseline that drifted/zeroed (e.g. a + # fee/slash delta applied without a baseline) would persist and keep the + # miner out of crown. Reconcile active miners against the contract now. + try: + self.event_watcher.reconcile_collateral_from_contract( + self.block, list(self.metagraph.hotkeys), self.contract_client + ) + except Exception as e: + bt.logging.warning(f'startup collateral reconcile failed: {e}') # Separate subtensor/contract/providers for axon handlers (thread safety). # axon_lock serialises every call on axon_subtensor's websocket so two diff --git a/tests/test_scoring_v1.py b/tests/test_scoring_v1.py index cc4cd5b..038db3b 100644 --- a/tests/test_scoring_v1.py +++ b/tests/test_scoring_v1.py @@ -1798,14 +1798,13 @@ def test_over_max_caps_at_full(self, tmp_path: Path): v.state_store.close() def test_zero_collateral_zeros_reward(self, tmp_path: Path): - """Collateral 0 with max_swap set → factor 0 → no reward, full recycle.""" + """A *known* zero collateral event with max_swap set → factor 0 → no + reward, full recycle. Seeded as an explicit present-0 event (not an + absent series, which now fails open — see + test_unknown_collateral_fails_open).""" hotkeys = pad_hotkeys_to_cover_recycle(['hk_a']) - v = make_validator( - tmp_path, - hotkeys, - max_swap_amount=500_000_000, - collaterals={'hk_a': 0}, - ) + v = make_validator(tmp_path, hotkeys, max_swap_amount=500_000_000) + seed_collateral(v.event_watcher, 'hk_a', 0, block=0) # present, known zero self.seed_tao_btc_crown(v, 'hk_a') rewards, _ = calculate_miner_rewards(v) recycle_uid = RECYCLE_UID if RECYCLE_UID < len(rewards) else 0 @@ -1847,17 +1846,18 @@ def test_cold_start_max_swap_zero_is_fail_safe(self, tmp_path: Path): np.testing.assert_allclose(rewards[0], POOL_TAO_BTC, atol=1e-6) v.state_store.close() - def test_no_collateral_anchor_zeros_reward(self, tmp_path: Path): - """A miner with no collateral event in the watcher's series (e.g. cold- - bootstrap saw zero on-chain collateral, or the read failed) is treated - as zero collateral throughout the window → factor 0 → zero reward. - Scoring no longer reads collateral via RPC, so the failure mode is now - a missing series rather than a failing call.""" + def test_unknown_collateral_fails_open(self, tmp_path: Path): + """A miner with NO collateral event in the watcher's series (unknown, + not zero) must fail OPEN: capacity 1.0 and can_fund passes, so it earns + the full pool. The contract auto-deactivates anyone below + min_collateral, so an active miner always holds enough — treating a + missing baseline as zero would silently drop honest miners from crown + (the collateral-baseline bug). Absent != zero.""" hotkeys = pad_hotkeys_to_cover_recycle(['hk_a']) - v = make_validator(tmp_path, hotkeys, max_swap_amount=500_000_000) # no collaterals dict + v = make_validator(tmp_path, hotkeys, max_swap_amount=500_000_000) # no collaterals dict → absent self.seed_tao_btc_crown(v, 'hk_a') rewards, _ = calculate_miner_rewards(v) - assert rewards[0] == 0.0 + np.testing.assert_allclose(rewards[0], POOL_TAO_BTC, atol=1e-6) v.state_store.close() def test_scoring_does_not_call_contract_for_collateral(self, tmp_path: Path): @@ -2631,6 +2631,130 @@ def test_collateral_events_persist_across_hydrate(self, tmp_path: Path): assert watcher2.get_miner_collaterals_at(1_000) == {'hk_a': 200_000_000} store.close() + def test_fee_without_baseline_does_not_fabricate_zero(self, tmp_path: Path): + """SwapCompleted fee for a miner with NO collateral baseline must NOT + write ``0 + (-fee)`` clipped to 0 — that pinned the miner at zero and + dropped them from crown via the capacity / can_fund gate. With no + baseline the delta is skipped and the miner stays *unknown* (absent), + which the scoring gate fails open on.""" + store = ValidatorStateStore(db_path=tmp_path / 'state.db') + watcher = make_watcher(store, active={'hk_a'}) + # No CollateralPosted first → unknown baseline. + watcher.apply_event(200, 'SwapInitiated', {'swap_id': 1, 'miner': 'hk_a'}) + watcher.apply_event( + 300, 'SwapCompleted', {'swap_id': 1, 'miner': 'hk_a', 'tao_amount': 0, 'fee_amount': 50_000_000} + ) + assert 'hk_a' not in watcher.collateral_events_by_hotkey # no fabricated 0 row + assert watcher.get_miner_collaterals_at(300) == {} # absent == unknown + assert watcher._latest_collateral('hk_a') is None + store.close() + + def test_reconcile_collateral_from_contract(self, tmp_path: Path): + """Reconcile resyncs active miners to on-chain truth: heals a corrupted + present-0 (hk_a), seeds an unknown (hk_b), skips inactive miners + (hk_c), and is idempotent when values already match.""" + store = ValidatorStateStore(db_path=tmp_path / 'state.db') + watcher = make_watcher(store, active={'hk_a', 'hk_b'}) # hk_c not active + seed_collateral(watcher, 'hk_a', 0, block=100) # corrupted present-0 + contract = MagicMock() + vals = {'hk_a': 474_000_000, 'hk_b': 600_000_000, 'hk_c': 999_000_000} + contract.get_miner_collateral.side_effect = lambda hk: vals.get(hk, 0) + updated = watcher.reconcile_collateral_from_contract(9_000, ['hk_a', 'hk_b', 'hk_c'], contract) + assert updated == 2 + snap = watcher.get_miner_collaterals_at(9_000) + assert snap == {'hk_a': 474_000_000, 'hk_b': 600_000_000} # hk_c skipped (inactive) + # Idempotent: values now match → no new events. + assert watcher.reconcile_collateral_from_contract(9_100, ['hk_a', 'hk_b', 'hk_c'], contract) == 0 + store.close() + + def test_reconcile_skips_on_rpc_failure(self, tmp_path: Path): + """A contract read failure leaves the prior value untouched — a + transient RPC blip can't zero a miner's collateral.""" + store = ValidatorStateStore(db_path=tmp_path / 'state.db') + watcher = make_watcher(store, active={'hk_a'}) + seed_collateral(watcher, 'hk_a', 123_000_000, block=0) + contract = MagicMock() + contract.get_miner_collateral.side_effect = RuntimeError('rpc down') + assert watcher.reconcile_collateral_from_contract(9_000, ['hk_a'], contract) == 0 + assert watcher.get_miner_collaterals_at(9_000) == {'hk_a': 123_000_000} # unchanged + store.close() + + +class TestNonEarnerDiagnosis: + """diagnose_non_earner must report the true reason: direction-aware outbid + (tao→btc lower-wins, btc→tao higher-wins) and collateral exclusion, not a + blanket 'outbid' that hid the collateral-baseline bug.""" + + def _trace(self, best_rate: float): + from allways.validator.scoring import DirectionTrace + + t = DirectionTrace(pool=0.5) + t.best_rate = best_rate + return t + + def test_competitive_but_present_zero_is_insufficient_collateral(self): + from allways.validator.scoring_trace import diagnose_non_earner + + # tao→btc lower-wins: own 279.3 beats best 280 → competitive, but + # collateral is a known 0 → insufficient_collateral, NOT outbid. + reason = diagnose_non_earner( + 'hk', + {('tao', 'btc'): 279.3}, + sr=0.98, + ever_active={'hk'}, + direction_traces={('tao', 'btc'): self._trace(280.0)}, + collaterals={'hk': 0}, + min_swap_rao=100_000_000, + max_swap_rao=500_000_000, + ) + assert reason.startswith('insufficient_collateral'), reason + + def test_competitive_but_unknown_collateral(self): + from allways.validator.scoring_trace import diagnose_non_earner + + reason = diagnose_non_earner( + 'hk', + {('tao', 'btc'): 279.3}, + sr=0.98, + ever_active={'hk'}, + direction_traces={('tao', 'btc'): self._trace(280.0)}, + collaterals={}, # absent → unknown + min_swap_rao=100_000_000, + max_swap_rao=500_000_000, + ) + assert reason.startswith('unknown_collateral'), reason + + def test_genuinely_worse_rate_is_direction_aware_outbid(self): + from allways.validator.scoring_trace import diagnose_non_earner + + # tao→btc lower-wins: own 281 is worse than best 280 → outbid. + reason = diagnose_non_earner( + 'hk', + {('tao', 'btc'): 281.0}, + sr=0.98, + ever_active={'hk'}, + direction_traces={('tao', 'btc'): self._trace(280.0)}, + collaterals={'hk': 500_000_000}, + min_swap_rao=100_000_000, + max_swap_rao=500_000_000, + ) + assert reason.startswith('outbid'), reason + + def test_competitive_and_funded_is_unfilled_not_outbid(self): + from allways.validator.scoring_trace import diagnose_non_earner + + reason = diagnose_non_earner( + 'hk', + {('tao', 'btc'): 279.3}, + sr=0.98, + ever_active={'hk'}, + direction_traces={('tao', 'btc'): self._trace(280.0)}, + collaterals={'hk': 500_000_000}, + min_swap_rao=100_000_000, + max_swap_rao=500_000_000, + ) + assert reason.startswith('competitive_but_unfilled'), reason + class TestScoringCadenceAndWindow: """Block-based scoring gate + cursor-anchored, gap-free window tiling — From 744224efa566aa4f428ffb2d18c8d9c966d3e6ca Mon Sep 17 00:00:00 2001 From: anderdc Date: Mon, 1 Jun 2026 12:37:01 -0500 Subject: [PATCH 2/3] chore: bump version 1.0.8 -> 1.0.9 --- 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 657be2e..4542276 100644 --- a/allways/__init__.py +++ b/allways/__init__.py @@ -1,3 +1,3 @@ -__version__ = '1.0.8' +__version__ = '1.0.9' 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 d753fa6..53b65de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "allways" -version = "1.0.8" +version = "1.0.9" description = "Allways - Universal Transaction Layer: Trustless cross-chain swaps on Bittensor Subnet 7" license = "MIT" requires-python = ">=3.10,<3.15" From 8c1548f8536b749aedd2022c83b115896ca4188b Mon Sep 17 00:00:00 2001 From: anderdc <61125407+anderdc@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:44:18 +0000 Subject: [PATCH 3/3] style: auto-fix pre-commit hooks --- allways/validator/scoring_trace.py | 3 +-- uv.lock | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/allways/validator/scoring_trace.py b/allways/validator/scoring_trace.py index 2fe67f3..fa5c36d 100644 --- a/allways/validator/scoring_trace.py +++ b/allways/validator/scoring_trace.py @@ -231,8 +231,7 @@ def diagnose_non_earner( have = collaterals[hotkey] if min_leg > 0 and have < min_leg: return ( - f'insufficient_collateral ({from_c}→{to_c}: have={have / TAO_TO_RAO:g}t ' - f'need={min_leg / TAO_TO_RAO:g}t)' + f'insufficient_collateral ({from_c}→{to_c}: have={have / TAO_TO_RAO:g}t need={min_leg / TAO_TO_RAO:g}t)' ) # Competitive and funded — lost to a tie split, busy, or active-flag timing. return f'competitive_but_unfilled ({from_c}→{to_c}: own={own:g} vs best={best:g})' diff --git a/uv.lock b/uv.lock index 2762417..35bfa8e 100644 --- a/uv.lock +++ b/uv.lock @@ -164,7 +164,7 @@ wheels = [ [[package]] name = "allways" -version = "1.0.8" +version = "1.0.9" source = { editable = "." } dependencies = [ { name = "base58" },