Skip to content

feat: emission scoring — capacity, volume, credibility ramp#324

Merged
anderdc merged 5 commits into
testfrom
feat/emission-scoring-weights
May 13, 2026
Merged

feat: emission scoring — capacity, volume, credibility ramp#324
anderdc merged 5 commits into
testfrom
feat/emission-scoring-weights

Conversation

@LandynDev
Copy link
Copy Markdown
Collaborator

Problem → Solution

Problem 1 — Idle crown farming. A miner can post just-above-market rates, self-reserve to lock themselves out, and earn full emission for "being available" without ever serving swaps. We can't simply stop paying reserved miners (inverse exploit: attackers force-reserve real miners to zero their emissions).

Capacity + volume weighting on per-direction rewards. Independent multipliers on the existing crown-time × success_rate³ formula:

reward_i = Σ_dir [pool_d × crown_share_id × sr_i³ × capacity_i] × volume_factor_i
  • capacity_factor = min(1.0, collateral / max_swap_amount). A miner at the 0.1 TAO collateral floor with a 0.5 TAO max swap only covers 20% of the swap-size band, so they earn 20% of what they'd otherwise win. Cold-start fail-safe to 1.0 when bounds aren't readable.
  • volume_factor = (1 - α) + α × min(1.0, vol_share / crown_share), α=0.5. Idle crown holders lose half; over-serving is capped at 1.0 — the cap is the anti-wash-trade guarantee (no incentive to force volume past parity with your crown share). Quiet network (zero volume) → factor 1.0, no penalty.

Each shortfall recycles to RECYCLE_UID; nothing transfers between miners.

Problem 2 — New-miner free emission. success_rate(stats) defaulted to 1.0 for zero-outcome miners, so a fresh hotkey holding crown earned the full cubed multiplier (1.0³ = 1.0) before serving anything. Cheap sybil farming surface.

Linear credibility ramp.

success_rate = (completed / total) × min(1.0, total / CREDIBILITY_RAMP_OBSERVATIONS)

CREDIBILITY_RAMP_OBSERVATIONS = 10. Zero observations → zero credibility. Each closed swap (completed or timed-out) climbs the ramp by 10%. At ≥10 closed swaps the ramp is a no-op and the function returns the raw success rate exactly as before. A miner with 8/10 still gets 0.8³ = 0.512 — no behavior change at steady state, only the bootstrap window changes.

This is the right shape for the network: it incentivizes brand-new miners to actually compete for crown and serve swaps (each successful fulfillment is a measurable credibility gain), instead of dropping in and farming from day one.

Recycling

All four shortfall sources flow to RECYCLE_UID:

  • Success rate (sr³ < 1)
  • Capacity (collateral / max_swap < 1)
  • Volume (vol_share / crown_share < 1)
  • Crown unfilled (no qualifying miner)

None redirect to other miners. Pool conservation is invariant (rewards.sum() == 1.0 always).

Surfacing

The per-round scoring trace now reads per crown earner:

uid=12 hotkey=5C1a.. crown_blk=400 sr=0.500 (5/10 closed, ramp=0.50) cap=0.20 (col=0.1t) vol=0.000t vol_share=0.00 crown_share=1.00 vol_f=0.50 reward=0.013

A miner sees exactly which factor zeroed them: collateral too low → raise; volume too low → fulfill more; closed swaps too few → keep grinding.

Test plan

  • ruff check allways/ tests/ — clean
  • ruff format --check — all formatted
  • pytest tests/ — 461/461 passing locally (97 in scoring, including 19 new for capacity/volume/credibility behavior)
  • Algebraic spot-checks on every new factor (helpers + integration)
  • Pool-conservation regression: rewards.sum() == 1.0 across mixed-factor scenarios
  • Schema migration is idempotent (duplicate column caught)
  • Cold-start fail-safe verified: bounds_cache.max_swap_amount() returns 0 → capacity_factor = 1.0 (no zeroing on first pass post-restart)
  • After merge: pull on lena, restart aw-vali, watch the scoring trace surface the new factors on the next pass

LandynDev added 2 commits May 13, 2026 14:48
Two new multipliers on top of crown_share × success_rate³:

- capacity_factor = min(1.0, collateral / max_swap_amount).
  A miner at the collateral floor (0.1 of 0.5 max) only covers 20% of the
  swap-size band; emission scales accordingly. Cold-start fail-safe to 1.0.

- volume_factor = (1 - α) + α · min(1.0, vol_share / crown_share), α=0.5.
  Idle crown holders lose half their reward. Over-serving is capped at 1.0,
  so wash swaps never amplify reward — the anti-self-route guarantee.

Both shortfalls recycle to RECYCLE_UID; nothing transfers to other miners.
Aggregation windows: capacity is a snapshot at scoring time, volume is the
600-block scoring window. Same window as crown_share — apples-to-apples.

Wiring:
- swap_outcomes.tao_amount column (idempotent ALTER).
- event_watcher passes SwapCompleted tao_amount through to state_store.
- new state_store.get_volume_since aggregator.
- scoring trace surfaces cap, vol_share, vol_factor per miner.
Zero-outcome miners earn 0 instead of the legacy 1.0 optimistic default —
the free-emission hole that let fresh hotkeys farm crown without serving
swaps. Credibility climbs linearly with closed swaps and tops out at
CREDIBILITY_RAMP_OBSERVATIONS (10), so a real new miner crosses the ramp
in 10 fulfillments and is then indistinguishable from a long-running one.

  success_rate = raw_rate × min(1.0, total / CREDIBILITY_RAMP_OBSERVATIONS)

Per-miner scoring trace surfaces the ramp position so operators can see
exactly how many more swaps they need:

  uid=12 hotkey=5C1a.. crown_blk=400 sr=0.500 (5/10 closed, ramp=0.50) ...

Tests get a baseline_credibility=True kwarg on the make_validator fixture
(default True) so every test that isn't specifically exercising the ramp
gets a fully-credible miner for free. New TestCredibilityRamp class
exercises the ramp directly with baseline_credibility=False.
@xiao-xiao-mao xiao-xiao-mao Bot added the enhancement New feature or request label May 13, 2026
LandynDev added 3 commits May 13, 2026 15:45
…ancy

- Inline bounds_cache + collateral reads into calculate_miner_rewards;
  drop the read_max_swap_amount / read_miner_collateral helpers (they
  were scoring-internal one-shot wrappers, not first-class concepts).
- Move WeightingTrace to scoring_trace.py where it belongs and add
  record_capacity / record_volume / record_credibility methods so
  scoring.py never touches trace fields directly.
- Push the idle-network short-circuit into volume_factor so the callsite
  has one branch instead of two; signature now takes (vol_rao,
  total_volume_rao, crown_share) — pure-math, no callsite duplication.
- closed_swaps + credibility_ramp live on WeightingTrace, so
  log_scoring_trace no longer needs success_stats threaded through.
- Refresh the module docstring to the actual reward equation.
- Direct unit tests for WeightingTrace.record_capacity / record_volume /
  record_credibility (math + edge cases: idle network, over-serve cap,
  ramp_target=0).
- max_swap_amount RPC failure → capacity_factor fail-safes to 1.0
  (previously only the collateral failure path was covered).
- Reword diagnose_non_earner reason: 'slashed_credibility_zero' is
  misleading post-ramp (new miners also land at sr=0) → 'credibility_zero'
  with a comment.
@anderdc anderdc merged commit e88cdd5 into test May 13, 2026
3 checks passed
@anderdc anderdc deleted the feat/emission-scoring-weights branch May 13, 2026 21:19
@LandynDev LandynDev mentioned this pull request May 13, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants