Skip to content

[BUG] cPoC rotation can shrink the final challenge segment below the punishable minimum #2015

Closed @maria-mitina opened 2026-10-15 09:54 UTC 3 comments Updated 2026-10-21 09:37 UTC
bug

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:

  1. The current challenge segment is cut short: its finish becomes the cPoC exchange end + 1 (ChallengeFinish), and that shortened segment is evaluated.
  2. 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)

@maria-mitina commented 2026-10-15 11:21 UTC

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.

@x0152 commented 2026-10-15 21:23 UTC

https://github.com/gonka-ai/gonka/commit/24a4204de78cefbe14d3e1dda2784ab4fbebc65d

@maria-mitina commented 2026-10-21 09:37 UTC

solved, thank you


🔄 Auto-synced from Issue #2015 every hour.