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
4 changes: 4 additions & 0 deletions allways/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@
VOLUME_WEIGHT_ALPHA: float = 0.5
# Closed swaps required for full credibility (0 → 100% linear ramp).
CREDIBILITY_RAMP_OBSERVATIONS: int = 10
# More than this many timed-out swaps within CREDIBILITY_WINDOW_BLOCKS hard-zeros
# a miner's credibility (and thus their whole reward) until the old timeouts age
# out of the rolling window. 0-2 tolerated; the 3rd timeout zeros credibility.
CREDIBILITY_MAX_TIMEOUTS: int = 2

# ─── Emission Recycling ────────────────────────────────────
RECYCLE_UID = 53 # Subnet owner UID
Expand Down
9 changes: 8 additions & 1 deletion allways/validator/scoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from allways.chains import canonical_pair
from allways.constants import (
CREDIBILITY_MAX_TIMEOUTS,
CREDIBILITY_RAMP_OBSERVATIONS,
CREDIBILITY_WINDOW_BLOCKS,
DIRECTION_POOLS,
Expand Down Expand Up @@ -249,6 +250,7 @@ def calculate_miner_rewards(self: Validator) -> Tuple[np.ndarray, Set[int]]:
wt.record_credibility(
closed_swaps=sum(success_stats.get(hotkey, (0, 0))),
ramp_target=CREDIBILITY_RAMP_OBSERVATIONS,
timed_out=success_stats.get(hotkey, (0, 0))[1],
)
crown_share_dir = blocks / total_crown_dir
vol_dir = volumes_dir.get(hotkey, 0)
Expand Down Expand Up @@ -384,9 +386,14 @@ def success_rate(stats: Optional[Tuple[int, int]]) -> float:

def credibility_ramp(stats: Optional[Tuple[int, int]]) -> float:
"""Linear ramp to full credibility at CREDIBILITY_RAMP_OBSERVATIONS closed
swaps. Applied linearly to the reward, not cubed. Zero observations → 0."""
swaps. Applied linearly to the reward, not cubed. Zero observations → 0.
More than CREDIBILITY_MAX_TIMEOUTS timed-out swaps in the window hard-zeros
credibility — a rolling penalty that recovers as old timeouts age out."""
if not stats or stats == (0, 0):
return 0.0
_completed, timed_out = stats
if timed_out > CREDIBILITY_MAX_TIMEOUTS:
return 0.0
return min(1.0, sum(stats) / CREDIBILITY_RAMP_OBSERVATIONS)


Expand Down
14 changes: 11 additions & 3 deletions allways/validator/scoring_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
import bittensor as bt
import numpy as np

from allways.constants import CREDIBILITY_RAMP_OBSERVATIONS, RECYCLE_UID, TAO_TO_RAO
from allways.constants import (
CREDIBILITY_MAX_TIMEOUTS,
CREDIBILITY_RAMP_OBSERVATIONS,
RECYCLE_UID,
TAO_TO_RAO,
)

if TYPE_CHECKING:
from allways.validator.scoring import DirectionTrace
Expand Down Expand Up @@ -45,9 +50,12 @@ def record_volume(self, vol_rao: int, total_volume_rao: int, crown_share: float,
self.participation = min(1.0, self.volume_share / crown_share) if crown_share > 0 else 1.0
self.volume_factor = factor

def record_credibility(self, closed_swaps: int, ramp_target: int) -> None:
def record_credibility(self, closed_swaps: int, ramp_target: int, timed_out: int = 0) -> None:
self.closed_swaps = closed_swaps
self.credibility_ramp = min(1.0, closed_swaps / ramp_target) if ramp_target > 0 else 1.0
if timed_out > CREDIBILITY_MAX_TIMEOUTS:
self.credibility_ramp = 0.0
else:
self.credibility_ramp = min(1.0, closed_swaps / ramp_target) if ramp_target > 0 else 1.0


def log_scoring_trace(
Expand Down
46 changes: 40 additions & 6 deletions tests/test_scoring_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,20 @@ def test_none_is_zero(self):
assert credibility_ramp((0, 0)) == 0.0

def test_scales_linearly_below_threshold(self):
"""1 closed swap → ramp 0.1; 5 closed → 0.5. Counts timeouts too."""
"""1 closed swap → ramp 0.1; 5 closed → 0.5. Counts tolerated timeouts too."""
assert credibility_ramp((1, 0)) == 0.1
assert credibility_ramp((5, 0)) == 0.5
assert credibility_ramp((2, 3)) == 0.5
assert credibility_ramp((3, 2)) == 0.5 # 2 timeouts tolerated

def test_caps_at_full_ramp(self):
assert credibility_ramp((10, 0)) == 1.0
assert credibility_ramp((90, 10)) == 1.0

def test_excess_timeouts_hard_zero(self):
"""More than CREDIBILITY_MAX_TIMEOUTS (2) timeouts → 0, regardless of volume."""
assert credibility_ramp((8, 2)) == 1.0 # 2 tolerated, fully ramped
assert credibility_ramp((8, 3)) == 0.0 # 3rd timeout zeros it
assert credibility_ramp((90, 10)) == 0.0 # high volume can't rescue it
assert credibility_ramp((0, 3)) == 0.0


class TestCrownHoldersHelper:
Expand Down Expand Up @@ -1693,6 +1699,15 @@ def test_record_credibility_zero_target_is_unity(self):
wt.record_credibility(closed_swaps=5, ramp_target=0)
assert wt.credibility_ramp == 1.0

def test_record_credibility_excess_timeouts_zero(self):
"""Trace mirrors the reward rule: >3 timeouts → ramp shown as 0."""
from allways.validator.scoring_trace import WeightingTrace

wt = WeightingTrace()
wt.record_credibility(closed_swaps=100, ramp_target=10, timed_out=4)
assert wt.closed_swaps == 100
assert wt.credibility_ramp == 0.0


class TestVolumeWeighting:
"""End-to-end volume weighting via calculate_miner_rewards."""
Expand Down Expand Up @@ -2036,8 +2051,27 @@ def test_full_ramp_at_threshold(self, tmp_path: Path):
np.testing.assert_allclose(rewards[0], POOL_BTC_TAO, atol=1e-6)
v.state_store.close()

def test_timeouts_advance_ramp_but_hurt_raw_rate(self, tmp_path: Path):
"""5 completed + 5 timed_out → ramp = 1.0, raw = 0.5 → 0.5³ × 1.0 = 0.125."""
def test_tolerated_timeouts_advance_ramp_but_hurt_raw_rate(self, tmp_path: Path):
"""8 completed + 2 timed_out (within the 2-timeout tolerance) → ramp = 1.0,
raw = 0.8 → 0.8³ × 1.0."""
hotkeys = pad_hotkeys_to_cover_recycle(['hk_a'])
v = make_validator(tmp_path, hotkeys, baseline_credibility=False)
self.seed_btc_tao_crown(v, 'hk_a')
for i in range(8):
v.state_store.insert_swap_outcome(
swap_id=i + 1, miner_hotkey='hk_a', completed=True, resolved_block=100 + i
)
for i in range(2):
v.state_store.insert_swap_outcome(
swap_id=100 + i, miner_hotkey='hk_a', completed=False, resolved_block=200 + i
)
rewards, _ = calculate_miner_rewards(v)
np.testing.assert_allclose(rewards[0], POOL_BTC_TAO * 0.8**3, atol=1e-6)
v.state_store.close()

def test_excess_timeouts_zero_reward(self, tmp_path: Path):
"""5 completed + 5 timed_out → 5 timeouts > 3 → credibility hard-zeroed →
reward 0 despite a high raw fulfillment count."""
hotkeys = pad_hotkeys_to_cover_recycle(['hk_a'])
v = make_validator(tmp_path, hotkeys, baseline_credibility=False)
self.seed_btc_tao_crown(v, 'hk_a')
Expand All @@ -2050,7 +2084,7 @@ def test_timeouts_advance_ramp_but_hurt_raw_rate(self, tmp_path: Path):
swap_id=100 + i, miner_hotkey='hk_a', completed=False, resolved_block=200 + i
)
rewards, _ = calculate_miner_rewards(v)
np.testing.assert_allclose(rewards[0], POOL_BTC_TAO * 0.5**3, atol=1e-6)
np.testing.assert_allclose(rewards[0], 0.0, atol=1e-6)
v.state_store.close()

def test_unearned_ramp_portion_recycles(self, tmp_path: Path):
Expand Down
Loading