x/inference: CollateralParams.DowntimeMissedPercentageThreshold is governance-settable but read by nothing, and SlashForDowntime's comment describes a check it does not perform #1726
CollateralParams.DowntimeMissedPercentageThreshold is declared, defaulted, validated, registered as a governance-settable param and live on chain at 0.05 — and no code reads it. A governance vote to tune it would pass, store the new value, and change nothing.
Evidence
Every non-generated, non-test reference in the tree is declaration or plumbing, all in one file:
| location | what it is |
|---|---|
x/inference/types/params.go:17 |
KeyDowntimeMissedPercentageThreshold |
x/inference/types/params.go:338 |
default DecimalFromFloat(0.05) |
x/inference/types/params.go:581 |
entry in the params string listing |
x/inference/types/params.go:636 |
NewParamSetPair registration |
x/inference/types/params.go:982 |
validatePercentage call |
Its four siblings under CollateralParams all have the same plumbing plus a consumer in x/inference/keeper/collateral.go:
| parameter | consumed at |
|---|---|
SlashFractionInvalid |
collateral.go:170 |
SlashFractionDowntime |
collateral.go:198 |
CollateralPerWeightUnit |
collateral.go:51, 108, 115 |
BaseWeightRatio |
collateral.go:46, 93, 103 |
DowntimeMissedPercentageThreshold |
nowhere |
Why this looks like residue rather than a bug
proposals/tokenomics-v2/collateral-todo.md specifies the consumer that would have read it: "calculates a participant's missed request percentage for the epoch and compares it to the DowntimeMissedPercentageThreshold parameter."
What shipped instead is a sequential probability ratio test. getInactiveStatus in x/inference/calculations/status.go:90 decides the INACTIVE transition from DowntimeGoodPercentage, DowntimeBadPercentage and DowntimeHThreshold — all ValidationParams — accumulating an InactiveLLR across epochs rather than comparing one epoch's ratio to a fixed cut-off.
That is a better test than the one the design doc described, and downtime slashing does work: deactiveParticipant (participant_status.go:75) calls SlashForDowntime on the transition into INACTIVE. So this is a parameter left behind when its mechanism was replaced, not a missing feature.
The part that actively misleads
SlashForDowntime's doc comment still describes the superseded design:
// inference-chain/x/inference/keeper/collateral.go:195
// SlashForDowntime checks a participant's performance for the completed epoch and
// slashes their collateral if their missed request percentage exceeds the threshold.
func (k Keeper) SlashForDowntime(ctx context.Context, participant *types.Participant, params types.Params) {
The function performs no such comparison — it reads SlashFractionDowntime and slashes. The threshold decision happened upstream, in the SPRT, against different parameters. A reader auditing the slashing path is pointed at a check that does not exist there, and proposals/tokenomics-v2/collateral.md still documents the parameter as "the epoch performance threshold that triggers a downtime slash".
Suggested fix
Either remove the parameter, or wire it. Removal is a state-breaking params change and so wants an upgrade handler, which may not be worth it on its own — in which case the cheap and useful half is documentation: correct the SlashForDowntime comment to say the INACTIVE transition already made the decision, and mark the parameter deprecated where it is declared and in proposals/tokenomics-v2/collateral.md, so nobody proposes a vote on a value with no effect.
Scope
Read from main at 379bebced6. I searched open issues and pull requests for DowntimeMissedPercentageThreshold and found nothing covering this. The reference counts above come from grepping the tree at that commit, excluding *.pb.go, *.pulsar.go and _test.go; I checked the four siblings as a control precisely because an initial search that found no consumer for any of them would have proved only that my search was wrong.
💬 Comments (2)
Validated this against the current codebase — the issue is fully valid.
DowntimeMissedPercentageThreshold is only declared, defaulted, serialized, and validated. It is not used by any runtime decision-making logic.
Downtime detection is handled by the SPRT using DowntimeGoodPercentage, DowntimeBadPercentage, and DowntimeHThreshold in getInactiveStatus.
Once a participant transitions to INACTIVE, SlashForDowntime applies SlashFractionDowntime directly and never reads or compares DowntimeMissedPercentageThreshold.
Therefore, changing this parameter through governance has no behavioral effect. We can safely remove it, together with the outdated documentation and comment, as part of an upgrade that handles the params/state migration.
🔄 Auto-synced from Issue #1726 every hour.
Correcting my own framing above, having since read the live parameters rather than the code defaults. The core claim is unaffected —
DowntimeMissedPercentageThresholdis still read by nothing — but the section headed "Why this looks like residue rather than a bug" describes the SPRT as what shipped in its place, and on mainnet that test cannot currently fire.Live
validation_params, epoch 386:downtime_good_percentagedowntime_bad_percentagedowntime_h_thresholdWith
P0 == P1, both increments inSPRT.UpdateCountsare exactly zero —logFail = ln(P1/P0) = ln(1) = 0andlogPass = ln((1-P1)/(1-P0)) = ln(1) = 0— so the LLR cannot move from zero regardless of the observation sequence, andDecision()returnsUndeterminedfor every possible input. The1e9threshold makes it unreachable a second time over.getInactiveStatustherefore never returnsFail, soParticipantStatus_INACTIVEwith reasonDowntimeis unreachable through the statistical path.That reads as deliberate rather than accidental, and the same params object carries its own control: the invalidation SPRT beside it is live and well formed —
false_positive_rate0.05,bad_participant_invalidation_rate0.18,invalidation_h_threshold40, givinglogFail = +1.2809andlogPass = -0.1472, so 32 consecutive failures condemn and 272 consecutive passes clear. One test is parameterised to work and the other to do nothing.Worth noting what
Hmeans here, since the implementation uses a symmetric±Hrather than Wald's asymmetric boundaries. Symmetric bounds forcealpha = beta, andA = ln((1-beta)/alpha) = Hthen givesalpha = 1/(1+e^H). So the live invalidation threshold of 40 encodes a false-invalidation rate of about4.2e-18, against roughly1.8e-2at the code default of 4 — the on-chain value is not a tweak of the default, it is a different regime.Downtime slashing itself is still reachable, so the doc-comment problem this issue raises remains live rather than moot:
getConfirmationPoCStatusalso returnsINACTIVE(reasonFailedConfirmationPoC),confirmation_poc_params.alpha_thresholdis0.5on chain, and that transition runs the samedeactiveParticipant->SlashForDowntimepath. So a participant can still be slashed for downtime — just never via the missed-request statistics thatSlashForDowntime's comment describes, which makes the stale comment more misleading in production than it looked from the code alone.