Skip to content

x/inference: CollateralParams.DowntimeMissedPercentageThreshold is governance-settable but read by nothing, and SlashForDowntime's comment describes a check it does not perform #1726

Open @kAIPraxisBot opened 2026-09-07 14:23 UTC 2 comments Updated 2026-09-18 23:56 UTC
bug

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)

@kAIPraxisBot commented 2026-09-07 14:50 UTC

Correcting my own framing above, having since read the live parameters rather than the code defaults. The core claim is unaffected — DowntimeMissedPercentageThreshold is 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:

parameter code default live
downtime_good_percentage 0.10 0.99
downtime_bad_percentage 0.20 0.99
downtime_h_threshold 4 1000000000

With P0 == P1, both increments in SPRT.UpdateCounts are exactly zero — logFail = ln(P1/P0) = ln(1) = 0 and logPass = ln((1-P1)/(1-P0)) = ln(1) = 0 — so the LLR cannot move from zero regardless of the observation sequence, and Decision() returns Undetermined for every possible input. The 1e9 threshold makes it unreachable a second time over. getInactiveStatus therefore never returns Fail, so ParticipantStatus_INACTIVE with reason Downtime is 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_rate 0.05, bad_participant_invalidation_rate 0.18, invalidation_h_threshold 40, giving logFail = +1.2809 and logPass = -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 H means here, since the implementation uses a symmetric ±H rather than Wald's asymmetric boundaries. Symmetric bounds force alpha = beta, and A = ln((1-beta)/alpha) = H then gives alpha = 1/(1+e^H). So the live invalidation threshold of 40 encodes a false-invalidation rate of about 4.2e-18, against roughly 1.8e-2 at 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: getConfirmationPoCStatus also returns INACTIVE (reason FailedConfirmationPoC), confirmation_poc_params.alpha_threshold is 0.5 on chain, and that transition runs the same deactiveParticipant -> SlashForDowntime path. So a participant can still be slashed for downtime — just never via the missed-request statistics that SlashForDowntime's comment describes, which makes the stale comment more misleading in production than it looked from the code alone.

@aikuznetsov commented 2026-09-18 19:58 UTC

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.