[BUG] cPoC rotation can shrink the final challenge segment below the punishable minimum #2015
Summary
How it is supposed to work
When a challenge is created, the chain checks that enough blocks remain until the safety window:
safetyHeight − startHeight ≥ min_punishable_segment_blocks
This guarantees the target has at least min_punishable_segment_blocks blocks (100 on testnet‑4, 300 on mainnet) to generate PoC before the segment is evaluated. The segment runs from startHeight to finish = safetyHeight.
What actually happens when a confirmation PoC fires
A confirmation PoC (cPoC) can be triggered at any block of the Inference phase. When it does, the open challenge is handled like this:
- The current challenge segment is cut short: its finish becomes the cPoC exchange end + 1 (ChallengeFinish), and that shortened segment is evaluated.
- When the cPoC completes, the challenge is restarted with a new segment (RotateChallengeSegment): startHeight becomes the current height, finish stays at the original safetyHeight.
The check "finish − start ≥ min" is only performed at creation time. It is not repeated at rotation. So the new segment can be arbitrarily short — it is simply whatever is left between the cPoC's end and the safety window.
What I observed (testnet‑4, epoch 52) min_punishable_segment_blocks was 100. Both segments were below it, so neither was ever evaluated (see #2013), and the challenge defaulted to PASSED: the challenger's locked payment was paid to a target that had produced zero commits.
Why it matters on mainnet too
With mainnet params (safety window 500, cPoC cycle 35+0+5+240+120 = 400 blocks, min 300), any cPoC triggered in roughly the last ~200 blocks of the cPoC trigger window leaves a final segment shorter than 300 blocks. With ~4 random cPoC triggers per epoch, that is a few percent of epochs with an open challenge.
Combined with issue 2013 that converts a valid challenge into a free payout. The rotation should either guarantee finish − newStart ≥ min (else abort+refund), or the final decision should evaluate across all segments of the epoch.
💬 Comments (3)
https://github.com/gonka-ai/gonka/commit/24a4204de78cefbe14d3e1dda2784ab4fbebc65d
solved, thank you
🔄 Auto-synced from Issue #2015 every hour.
There is a chance of this happening on mainnet: module/confirmation_poc.go:139-147 and the rotation rule (new start = cPoC validation end + 1):
cPoC trigger window ends at nextPoC − 2 − (35+0+5+240+120+500) = nextPoC − 902. A cPoC triggered at height T starts generating ~20 blocks later and finishes validation at T + 20 + 34 + 5 + 239 ≈ T + 298; the challenge segment restarts at ≈ T + 299. Safety (= challenge finish) is nextPoC − 500, so the final segment is nextPoC − 500 − (T + 299). It is < 300 whenever T > nextPoC − 1099, i.e. for any cPoC triggered in the last ~197 blocks of the trigger window. Latest possible trigger gives a final segment of ≈ 103 blocks.
Mainnet does 4 cPoCs per epoch at effectively random heights across a ~14 090‑block trigger window, so per epoch ≈ 1 − (1 − 197/14090)^4 ≈ 5–6 % chance that the last cPoC lands in that zone.