fix(inference): power cap zeros network when zero-weight participants are in settlement #1395
Problem
On gonka-testnet-4, epoch 15 settlement zeroed all participant weights and cascaded into epochs 16–20 with total_weight=0 and no active epoch members.
Chain logs at settlement (block ~5305):
weight_group model=Qwen/Qwen2.5-7B-Instruct raw_total=81702 scale=1.0
weight_pipeline addr=gonka1e8yj… consensus=34658 final=0 modes=7B:DIRECT
weight_pipeline addr=gonka1m6s8… consensus=0 final=0 modes=7B:NONE
Universal power capping applied originalTotalPower=81702 cappedTotalPower=0
Adding member … weight=0 (all 4 participants)
Consensus weights were computed correctly (~81k total on 7B). Universal power capping then collapsed everything to zero.
Root cause
Why a zero-weight host is still in settlement
ComputeNewWeights includes participants who passed PoC on any model (or are preserved). There is no filter for “will have non-zero consensus weight on an eligible group.”
Later, ComputeConsensusWeights overwrites p.Weight from eligible groups only. A host that mined 4B but where only 7B is eligible gets consensus=0 — but stays in activeParticipants.
Why power capping zeroed everyone
applyEpochPowerCapping passes the full activeParticipants slice to CalculateOptimalCap with no Weight > 0 filter.
The algorithm used participant headcount (len(participants) == 4) in the 30% threshold formula, including the zero-weight guardian. Sorted powers: [0, 10238, 34658, 36806].
When the first positive weight triggered the cap with sumPrev=0:
Every host was then capped to 0.
Collateral was not involved (grace_period_end_epoch=180, grace skip logged).
Contributing testnet conditions (not required to trigger the bug)
- Botched 4B→7B transition: PoC mined on 4B, hardware reassigned join hosts to 7B
- Only 7B eligible at settlement (4B failed VMin after join hosts moved)
- Guardian had
modes=7B:NONE(no delegation tx) andconsensus=0on 7B
On mainnet steady state, a delegating non-7B host would still have consensus=0 on the sole eligible group but often positive weight from another eligible model. The bug also applies during model bootstrap when old-model miners sit in activeParticipants with Weight=0.
Proposed fix
Files to change:
| File | Change |
|---|---|
inference-chain/x/inference/keeper/bitcoin_rewards.go |
CalculateOptimalCap: skip Weight <= 0 in threshold math; use positiveCount instead of headcount; bail out if cap <= 0 && totalPower > 0 |
inference-chain/x/inference/keeper/bitcoin_rewards_test.go |
Add regression test for epoch-15 scenario |
Reference commit: 7f3c00e95 on branch do/guardian-tiebreaker-weightless-votes (PR #1394 was closed; re-apply from this commit).
bitcoin_rewards.go — key changes in CalculateOptimalCap
- Build
participantPowersonly from entries withWeight > 0 - Early return if
positiveCount <= 1 - Use
positiveCount(notlen(participants)) in threshold loop and cap formula - Safety net: if computed
cap <= 0whiletotalPower > 0, return unchanged (no capping) - Apply step unchanged: still iterates all participants; zero-weight entries stay zero
participantPowers := make([]ParticipantPowerInfo, 0, participantCount)
for i, participant := range participants {
if participant.Weight <= 0 {
continue
}
participantPowers = append(participantPowers, ParticipantPowerInfo{...})
}
positiveCount := len(participantPowers)
if positiveCount <= 1 {
return participants, totalPower, false
}
// threshold loop uses positiveCount instead of participantCount
// ...
if cap <= 0 && totalPower > 0 {
return participants, totalPower, false
}
bitcoin_rewards_test.go — regression test
func TestApplyPowerCappingForWeights_SkipsZeroWeightParticipants(t *testing.T) {
participants := []*types.ActiveParticipant{
{Index: "guardian", Weight: 0},
{Index: "host-a", Weight: 10238},
{Index: "host-b", Weight: 34658},
{Index: "host-c", Weight: 36806},
}
capped, wasCapped := ApplyPowerCappingForWeights(participants)
// total must remain positive; guardian stays at 0
}
Test plan
go test ./inference-chain/x/inference/keeper/ -run TestApplyPowerCappingForWeights_SkipsZeroWeightParticipants -count=1
go test ./inference-chain/x/inference/module/ -run TestApplyPowerCapping -count=1
Related code paths (for reviewers)
- Settlement:
inference-chain/x/inference/module/module.go—onEndOfPoCValidationStage→applyEpochPowerCapping - Cap entry:
inference-chain/x/inference/module/power_capping.go→ApplyPowerCapping - Cap algorithm:
inference-chain/x/inference/keeper/bitcoin_rewards.go—ApplyPowerCappingForWeights,CalculateOptimalCap - Design (original, no zero-weight filter):
proposals/early-network-protection/early-network-protection-plan.md
Optional follow-ups (separate from this fix)
- Filter or document zero-weight
activeParticipantsbefore power capping at the call site - Harden multi-model transition: build delegation groups from PoC-proven models before
setModelsForParticipants(model_assignment.go) - Require bootstrap delegation before sole-eligible-group settlement during model rollout
💬 Comments (1)
🔄 Auto-synced from Issue #1395 every hour.
fixed, thank you @DimaOrekhovPS