Skip to content

Replay collateral per block so capacity reflects window state (#409)#423

Merged
LandynDev merged 3 commits into
testfrom
fix/capacity-historical-collateral-replay
May 31, 2026
Merged

Replay collateral per block so capacity reflects window state (#409)#423
LandynDev merged 3 commits into
testfrom
fix/capacity-historical-collateral-replay

Conversation

@anderdc
Copy link
Copy Markdown
Collaborator

@anderdc anderdc commented May 28, 2026

Summary

Closes #409. Capacity weighting used to multiply already-replayed crown by the miner's current collateral, read once at scoring time via contract_client.get_miner_collateral. The replayed crown intervals are historical; the collateral read is live. A miner can hold crown through the window with low collateral, then top up before the next scoring pass and receive the higher capacity multiplier retroactively.

This PR treats collateral the same way active, busy, rate, and reservation_pin are already treated: a per-block event series replayed inside replay_crown_time_window, with the capacity multiplier evaluated at the block being credited.

Approach

  • New CollateralEvent in event_watcher.py, with its own in-memory list + _by_hotkey index, schema (collateral_events), CRUD, and prune helpers — same shape as ActiveEvent/BusyEvent.
  • apply_event handles CollateralPosted and CollateralWithdrawn (writing total / remaining respectively, which the contract emits as post-event balances).
  • SwapCompleted and SwapTimedOut also tap the series. The contract's apply_collateral_penalty silently deducts the fee/slash without emitting CollateralWithdrawn, so the watcher mirrors the deduction from SwapCompleted.fee_amount / SwapTimedOut.slash_amount. CollateralSlashed is deliberately skipped to avoid double-counting with SwapTimedOut.
  • Cold bootstrap anchors current collateral at cursor for every active miner. Same trade-off active_events already makes — a fresh validator can only read current state and treats it as constant across the first window.
  • replay_crown_time_window reconstructs collateral at window_start and applies a new EventKind.COLLATERAL. Inside credit_interval, each holder's split is multiplied by capacity_factor(collateral_at_block, max_swap_rao) and accumulated into a parallel cap_weighted_blocks dict that the trace exposes.
  • calculate_miner_rewards divides cap_weighted_blocks[hk] / crown_blocks[hk] to get the time-weighted effective capacity multiplier, then drops the contract read entirely.

Notes

  • Cold-start anchor uses current collateral; the first scoring window after a fresh boot treats current as constant. Documented in code.
  • Withdrawal is gated by MinerStillActive on chain, so during any crown-earning interval the series is monotone non-decreasing (post-only). The retroactive boost path is closed.
  • WeightingTrace.collateral dropped; the log line now shows only cap= since the effective multiplier is time-averaged and no single collateral value is the right thing to print.

Merged with test

Rebased onto latest test and resolved conflicts in scoring.py and tests/test_scoring_v1.py:

  • reconstruct_window_start_state keeps the collateral-aware 5-tuple signature and folds in test's batched-rate-read docstring.
  • Both added test classes (TestHistoricalCollateralReplay + TestScoringCadenceAndWindow) are retained.
  • test lowered SCORING_WINDOW_BLOCKS 600 → 300, so the window is now (9_700, 10_000]. test_mid_window_topup_blends_capacity's top-up moved 9_700 → 9_850 (new midpoint) to keep the 150/150 split; stale window-math comments updated. No behavior change to the fix itself.

Test plan

  • uv run pytest tests/test_scoring_v1.py — 131 pass
  • uv run pytest — 624 pass
  • uv run ruff check — clean
  • New regression: test_409_retroactive_topup_does_not_boost_window mirrors the proof in the issue
  • test_mid_window_topup_blends_capacity covers the time-weighted average path
  • test_swap_completed_deducts_fee_from_collateral_series / test_swap_timed_out_deducts_slash_from_collateral_series cover the silent deductions
  • test_prune_keeps_latest_collateral_event_per_hotkey covers the reconstruction anchor invariant
  • test_collateral_events_persist_across_hydrate covers warm restart round-trip

anderdc and others added 3 commits May 28, 2026 13:30
Capacity weighting used to multiply already-replayed crown by the miner's
*current* collateral, read once at scoring time. A miner could hold crown
through the window with low collateral and top up before the next scoring
pass to receive full capacity credit retroactively.

Mirror the active/busy/rate treatment: emit a per-block CollateralEvent
series from CollateralPosted, CollateralWithdrawn, and the silent fee/
slash deductions hidden inside SwapCompleted/SwapTimedOut. The crown
replay walks the series alongside everything else and accumulates a cap-
weighted-blocks counter that the scoring loop divides back into a time-
weighted capacity factor — no contract reads at scoring time.

Cold bootstrap anchors current collateral at cursor (same approximation
already used for the active set). Warm restart hydrates collateral_events
from state.db. Prune preserves the latest row per hotkey as a
reconstruction anchor, same rule as active_events.
Resolve conflicts in scoring.py and test_scoring_v1.py:
- reconstruct_window_start_state: keep collateral 5-tuple signature, fold
  in test's batched-rate-read docstring, drop stale rates={} init.
- test imports: combine CollateralEvent with new scoring-window constants.
- keep both added test classes (TestHistoricalCollateralReplay +
  TestScoringCadenceAndWindow).
- recalibrate collateral replay tests to SCORING_WINDOW_BLOCKS=300
  (window now (9_700, 10_000]; mid-window top-up moved to 9_850).
@LandynDev LandynDev merged commit 1039473 into test May 31, 2026
3 checks passed
@anderdc anderdc deleted the fix/capacity-historical-collateral-replay branch May 31, 2026 16:08
anderdc pushed a commit that referenced this pull request May 31, 2026
…shot

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).
LandynDev pushed a commit that referenced this pull request May 31, 2026
* 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:<sha>), not just :latest. Makes
pinning/rolling back to a known-good build a direct image pull instead
of a source rebuild.

* 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).

* 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.

* style: auto-fix pre-commit hooks

* chore: bump version 1.0.7 -> 1.0.8

* style: auto-fix pre-commit hooks

---------

Co-authored-by: anderdc <me@alexanderdc.com>
LandynDev pushed a commit that referenced this pull request Jun 1, 2026
* Fix collateral-baseline zeroing that recycled emissions

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.

* chore: bump version 1.0.8 -> 1.0.9

* style: auto-fix pre-commit hooks

---------

Co-authored-by: anderdc <me@alexanderdc.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Capacity weighting reads current collateral, so miners can retroactively boost rewards for crown time already earned

2 participants