Skip to content

gonkavip/payout276

Repository files navigation

Gonka Epoch 276 CPoC-Misfire Compensation

This directory contains a reference computation for compensating miners who lost confirmation weight in epoch 276 because the v0.2.13 upgrade did not suppress confirmation PoC the way its release notes described. Two unintended CPoCs ran inside epoch 276 after the upgrade height, knocking 7 active participants to INACTIVE (via FailedConfirmationPoC) and reducing the confirmation_weight of 12 others.

The output CSV maps each affected miner to the exact ngonka they should receive. All inputs are queried from an archive full node and written to reference JSON files alongside the script, so the computation is fully deterministic and reproducible without any off-chain or third-party data.

TL;DR

  • 19 miners lost confirmation_weight in epoch 276 due to the v0.2.13 CPoC misfire.
  • Total amount: 32 429.966 GNK of compensation.
  • 7 of the 19 were forced from ACTIVE to INACTIVE by the unintended CPoCs and received zero reward.
  • The remaining 12 stayed ACTIVE but had their confirmation_weight partially reduced.
  • Both snapshot block heights are derived at runtime from on-chain state — nothing is hardcoded.

What Was Promised — and Why "10 000 blocks"

Proposal #54 (passed 2026-05-22) scheduled the v0.2.13 software upgrade at block height 4 267 300. Its release notes promised:

Disables confirmation PoC triggers for the rest of the upgrade epoch via a grace-epoch UpgradeProtectionWindow of 10000 blocks. The new snapshot logic starts from the next epoch.

The 10 000-block window is not a release-notes invention. It is a hardcoded constant in the v0.2.13 upgrade handler in the chain repository:

inference-chain/app/upgrades/v0_2_13/upgrades.go — commit 1e24d1c7ce66df0096de295dace06f164ebad4f3

// Block window after the upgrade in which confirmation PoC is skipped.
// Same value as v0.2.10; covers the rest of the upgrade epoch on mainnet.
GraceUpgradeProtectionWindow int64 = 10000

The same handler then calls AddPunishmentGraceEpoch(epoch, ..., 10000) at upgrade execution, which in turn should override the chain-default ConfirmationPocParams.UpgradeProtectionWindow = 500 for the rest of the upgrade epoch via this branch in x/inference/module/confirmation_poc.go:110:

if graceParams, ok := am.keeper.GetPunishmentGraceEpoch(ctx, epochContext.EpochIndex);
   ok && graceParams.UpgradeProtectionWindow > 0 {
    upgradeProtectionWindow = graceParams.UpgradeProtectionWindow
    am.LogDebug("using grace UpgradeProtectionWindow", ...)
}

So when the proposal text mentions a "10 000 block grace-epoch window," it is referring to the value above — verifiable in the upgrade-handler source, the commit, and the governance artifact at proposals/governance-artifacts/update-v0.2.13/README.md:54-56.

Important note about the default 500: querying the live chain at the upgrade block returns confirmation_poc_params.upgrade_protection_window = 500. That is the chain-default for normal upgrades. The 10 000 lives only in the v0.2.13 upgrade handler and was meant to be written into the PunishmentGraceEpoch keyed by the current epoch at the moment the handler ran. Whether it was actually written, and whether the CPoC trigger checked the override correctly, is the bug — but the promise to do so is concretely traceable to the commit above.

This compensation proposal does not depend on the 10 000 number for its math. The 10 000 only motivates why it is reasonable to repair the participants that the misfire damaged: the chain promised the participants there would be no CPoC for the rest of the epoch, and the two CPoCs that did fire damaged participants the chain had committed not to test in this window.

What Actually Happened

In practice the protection did not take effect, and two additional CPoCs ran inside epoch 276 after the upgrade. Each CPoC re-sampled participants and re-computed confirmation_weight. Net result:

  • 7 participants who were ACTIVE at h_before were forced to INACTIVE by h_after, via the FailedConfirmationPoC path in x/inference/calculations/status.go:82. They received zero reward in epoch 276.
  • 12 more participants stayed ACTIVE but had their confirmation_weight reduced.
  • Total confirmation_weight in the epoch fell by 131 214 cw (845 946 → 714 732), a 15.5% drop.

Because reward distribution in SettleAccounts is scaled by per- participant effective weight, dropping participants and reducing their cw translated directly into lost rewards.

On-Chain Inputs (No Hardcoded Block Heights)

The script derives both snapshot block heights from on-chain state at runtime:

Value Source
upgrade_height gov/v1/proposals/54.messages[].plan.height → 4 267 300
h_before upgrade_height - 1 → 4 267 299
next_epoch_start epoch_group_data/277.effective_block_height → 4 275 062
h_after next_epoch_start - 1 → 4 275 061

h_after is the last block of epoch 276 itself, taken directly from the chain's own record of the epoch boundary. By this height all CPoCs that fired inside epoch 276 — intended or otherwise — have settled, so the snapshot captures the full extent of the damage caused inside the epoch, without bleeding into epoch 277.

We deliberately do not use the chain-default upgrade_protection_window = 500 to construct h_after (which would yield 4 267 800). That window falls between the two unintended CPoCs and would underestimate the damage. The epoch boundary is the authoritative end-of-damage point.

Eligibility and Per-Participant lost_cw

A participant is eligible only if status_before == ACTIVE. For eligible participants we classify each transition based on the chain's own calculations/status.go rules:

Transition Detection lost_cw Reason tag
ACTIVE → ACTIVE cw dropped cw_before - cw_after partial_cpoc_damage
ACTIVE → ACTIVE cw unchanged 0 no_loss
ACTIVE → INACTIVE, missed/inactiveLLR unchanged only path left is FailedConfirmationPoC (see status.go:82) cw_before failed_cpoc_full_drop
ACTIVE → INACTIVE, missed or inactiveLLR grew participant turned off — Downtime SPRT (status.go:75) 0 skipped_downtime
ACTIVE → INVALID separate slashing path 0 skipped_invalid
status_before != ACTIVE excluded outright 0 ineligible_not_active_before

Rationale for the INACTIVE split: x/inference/calculations/status.go has exactly two paths from ACTIVE into INACTIVEDowntime (line 75) and FailedConfirmationPoC (line 82). Downtime is driven by SPRT on missed_requests vs inference_count, which leaves a footprint in current_epoch_stats.missed_requests and inactiveLLR. If neither of those moved between snapshots, the only remaining path into INACTIVE is FailedConfirmationPoC, i.e. CPoC damage. This is how the script distinguishes voluntary node downtime from CPoC misfire damage on purely on-chain signals.

Compensation Math

The chain pays rewards in SettleAccounts via bitcoin_rewards.go:723-736:

// reward[addr] = (effective_weight[addr] * fixed_epoch_reward) / totalPoCWeightBeforeDowntime

where totalPoCWeightBeforeDowntime = totalFullWeight = sum(vw.Weight). Crucially, this denominator is the epoch-wide constant total_weight (the sum of base PoC weights), not the post-CPoC confirmation_weight sum. The chain explicitly avoids redistributing unclaimed shares: any CPoC-reduced or invalidated participant's share becomes a remainder that goes to the governance module, not to the remaining participants.

Therefore the per-unit price the chain actually paid is:

coefficient = total_rewarded / total_weight
            = 193,820,331,174,280 ngonka / 798,029
            = 242,873.794 ngonka per weight unit

We approximate the lost share by treating lost_cw as lost effective- weight units and multiply by the coefficient:

compensation = lost_cw * total_rewarded / total_weight   (integer ngonka)

All arithmetic is integer ngonka via fixed-point:

per_cw_q = total_rewarded * 1e18 // total_weight
compensation = lost_cw * per_cw_q // 1e18

No floating-point rounding.

Statistics

Value
Members of epoch 276 54
ACTIVE before upgrade (status_before) 46
Dropped to INACTIVE via failed_cpoc_full_drop 7
Stayed ACTIVE but lost cw (partial_cpoc_damage) 12
no_loss (eligible, untouched) 27
ineligible_not_active_before 8
Total eligible for compensation 19
Total confirmation_weight lost 131 214 cw
Total lost_cw after misfire-damage classification 133 526
total_weight (denominator from epoch_group_data.total_weight) 798 029
Total reward pool in epoch 276 193 820.331 GNK
coefficient (ngonka per weight unit) 242 873.794
Total compensation 32 429.966 GNK

Reproducing the Computation

pip install -r requirements.txt
python3 payout_276.py <ARCHIVE_NODE_IP>

The script:

  1. Fetches gov/v1/proposals/54 and reads messages[].plan.height to derive h_before.
  2. Fetches epoch_group_data/277 (current state) and reads effective_block_height to derive h_after.
  3. Fetches four snapshots at those two heights (epoch_group_data/276, participant?limit=10000) and the per-member rewards (epoch_performance_summary/276/<addr> for each member).
  4. Writes all eight raw responses to JSON files alongside the script.
  5. Classifies each participant, applies the compensation formula, and writes payout_276.csv.

Custom output path:

python3 payout_276.py <ARCHIVE_NODE_IP> --out my_payout.csv

Why an Archive Node Is Required

The detection queries historical state at past block heights:

  • epoch_group_data/276 and participant at h_before = 4 267 299
  • epoch_group_data/276 and participant at h_after = 4 275 061

A node that has pruned state for those heights cannot answer these queries. Any archive full node retaining state since at least h_before will work.

Reference Files Written by the Script

File Content
proposal_54.json Raw gov/v1/proposals/54 (source of h_before).
epoch277_group_data_current.json Raw epoch_group_data/277 (source of h_after).
epoch276_group_data_before.json Raw epoch_group_data/276 at h_before.
epoch276_group_data_after.json Raw epoch_group_data/276 at h_after.
epoch276_participant_before.json Raw participant?limit=10000 at h_before.
epoch276_participant_after.json Raw participant?limit=10000 at h_after.
epoch276_performance.json Raw epoch_performance_summary/276/<addr> for each member, keyed by address.
payout_276.csv Final compensation table (generated).

Reviewers can verify all numbers without running an archive node by inspecting these JSON files directly.

Output Schema

payout_276.csv:

address, status_before, status_after, cw_before, cw_after, lost_cw,
reason, rewarded_coins_received, compensation_ngonka, compensation_gnk
  • status_before, status_afterACTIVE / INACTIVE / INVALID read from participant.{index}.status at the two heights.
  • cw_before, cw_afterconfirmation_weight read from epoch_group_data/276.validation_weights[].confirmation_weight at the two heights.
  • lost_cw — input to the compensation formula, per the classification table above.
  • reason — one of the *_full_drop / *_damage / skipped_* / ineligible_* / no_loss tags from the classifier.
  • rewarded_coins_received — what the chain actually paid this participant in epoch 276 (zero for the 7 dropped ones).
  • compensation_ngonka, compensation_gnk — the refund amount.

Rows are sorted by compensation_ngonka descending. Only rows with compensation_ngonka > 0 are included.

Verification Checklist for Reviewers

  • Reproduce against any archive node. The CSV should match byte-for-byte; inputs are pure on-chain state at chain-derived block heights, no hardcoded values.
  • Open proposal_54.json and confirm messages[].plan.height == 4267300.
  • Open epoch277_group_data_current.json and confirm epoch_group_data.effective_block_height == 4275062. The script derives h_after = 4275061 from this.
  • Pick any row with reason == failed_cpoc_full_drop. Open epoch276_participant_before.json and confirm status == ACTIVE. Open epoch276_participant_after.json and confirm status == INACTIVE with missed_requests and inactiveLLR unchanged from before — meaning the only path that could move the participant out of ACTIVE is the FailedConfirmationPoC branch in calculations/status.go:82.
  • Pick any row with reason == partial_cpoc_damage. Confirm cw_after < cw_before in the two epoch276_group_data_*.json files. lost_cw should equal cw_before - cw_after.
  • Verify compensation_ngonka == lost_cw * total_rewarded // total_weight with total_rewarded summed from epoch276_performance.json and total_weight read from epoch276_group_data_before.json.epoch_group_data.total_weight.
  • Sum the compensation_ngonka column and confirm it matches the script's reported total (32 429.966 GNK).
  • Negative control: pick any address that was INACTIVE at h_before (e.g. gonka1ujnc662v6g69jm6fgxnr79a2m7ehzeut059239 — search for it in epoch276_participant_before.json). It must not appear in payout_276.csv regardless of its confirmation_weight values.

Files

  • payout_276.py — end-to-end script (archive node required).
  • payout_276.csv — output.
  • requirements.txt — single dependency (aiohttp).
  • Reference JSON files listed above.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages