This directory contains a reference computation for refunding rewards that
the chain's x/inference module assigned to participants but
dropped at the SetSettleAmount step — a known on-chain bug where
SettleAccounts calls continue on participants whose
GetBitcoinSettleAmounts returned settleError (typically
ErrNegativeCoinBalance). When this happens the participant has a
positive rewarded_coins in EpochPerformanceSummary, but no
SettleAmount record is ever written, so their MsgClaimRewards
returns "No rewards for this address" and the coins are eventually
swept into the gov module account by
TransferOldSettleAmountsToGovernance.
The output CSV maps each affected participant to the exact amount of
ngonka they should receive, with per-epoch breakdown. The numbers are
deterministic and verifiable against the on-chain state of any gonka
archive node.
- 19 miners are victims of the settle-drop bug across the chain's history.
- Total amount: 1 075.336 GNK of rewards that were computed and owed
but never written to a
SettleAmountrecord. - The proposal refunds each affected miner exactly what the chain computed for them, per epoch.
- Detection uses two standard chain endpoints only — no off-chain data, no third-party services, no log scraping.
Many participants have rewarded_coins > 0 and claimed = false in
their EpochPerformanceSummary. Most of those are NOT bugs. The chain
has a one-epoch claim window: after epoch N's payouts are settled, the
participant must submit MsgClaimRewards during epoch N+1 with a valid
seed signature, sufficient successful validations, and the correct
epoch_index. If they miss that window — for any operational reason
(node downtime, lost keys, wrong seed, bad validations) — the rewards
expire and are swept into gov by design.
This proposal explicitly does not refund those cases. The participant had a chance to claim and didn't. That is correct protocol behaviour, not a bug.
What this proposal does refund is the case where the participant had no chance to claim because the chain itself skipped writing their claim ticket. That is unambiguously on-chain malfunction, and the only party that can correct it is governance.
SettleAccounts in inference-chain/x/inference/keeper/accountsettle.go
runs as an EndBlocker at every epoch boundary. The relevant sequence:
GetBitcoinSettleAmounts(inbitcoin_rewards.go) computes per- participantSettleAmountandsettleError. If the participant hasCoinBalance < 0and the computedRewardCoinsare smaller than the debt, the code setsRewardCoins = 0and returnssettleError = types.ErrNegativeCoinBalance.SettleAccountswritesEpochPerformanceSummaryfor every participant unconditionally, copyingWorkCoinsandRewardCoins.- A second loop iterates the same amounts and calls
SetSettleAmountWithGovernanceTransferonly for those withamount.Error == nil. Participants withsettleErrorhitcontinueand never get aSettleAmountrecord.
Net result: the chain's books say the participant earned X ngonka, but
no claim ticket exists. The participant's wallet sees the rewarded
amount in epoch_performance_summary and submits MsgClaimRewards,
which validateRequest rejects because GetSettleAmount returns
not found.
A participant is a refund candidate if and only if both are true at
the moment epoch N+1 begins (i.e. at block height
epoch_meta[N+1].effective_block_height):
-
GET /productscience/inference/inference/epoch_performance_summary/{N}/{addr}returnsrewarded_coins > 0. -
The full pagination of
GET /productscience/inference/inference/settle_amount(with headerx-cosmos-block-height: <eff_{N+1}>) does NOT contain an entry withparticipant == addrandepoch_index == N.
Both endpoints are part of the standard inference module proto schema.
Any archive full node serves them. No third-party indexer, no
tx_search, no event-log parsing, no proto-decoding of authz-wrapped
messages.
This same pair of conditions correctly identifies victims regardless of
how the participant runs their claim flow (direct from cold key, or via
authz-granted warm key, or any other variation). Tracker / explorer
parsers are not required.
The computation is a 4-step pipeline implemented in unclaimed.py.
Inputs come exclusively from a gonka archive full node via standard
Cosmos REST endpoints.
For every epoch in [FROM_EPOCH..LAST_EPOCH+1], fetch
/inference/inference/epoch_group_data/{n}:
validation_weights[*].member_addressis the participant set for epoch n.effective_block_heightis the start block of epoch n. We useeffective_block_heightof epochn+1as the snapshot height for epoch n (the block at whichSettleAccountsfor epoch n has just finished).
For each (epoch, addr) pair, fetch
/inference/inference/epoch_performance_summary/{epoch}/{addr} and
record rewarded_coins and claimed.
For each epoch n, fetch the full paginated list
/inference/inference/settle_amount?pagination.limit=500 with the
header x-cosmos-block-height: eff_{n+1}. Record every
(epoch_index, participant) pair present at that height.
Keep only pairs (epoch=N, addr=X) where:
rewarded_coins[N][X] > 0 AND (N, X) NOT IN settle_present
For each kept pair, write the ngonka amount into the per-address row at
the column corresponding to N. Sum across all kept pairs into
total_ngonka and total_gnk.
All arithmetic is exact integer ngonka.
A single CSV (unclaimed.csv by default):
address, 1, 2, 3, ..., N, total_ngonka, total_gnk
gonka1...,0,0,...,150749450599,...,0,150749450599,150.749450599
gonka1...,0,0,...,156696892692,...,0,156696892692,156.696892692
...Each row is one affected miner. The epoch columns hold the per-epoch
amounts. The two right-most columns are per-address totals. Sum of the
total_ngonka column equals the total proposal payout.
Both detection steps query historical state:
epoch_performance_summaryfor past epochs.settle_amountat a past block height (viax-cosmos-block-height).
Most public RPC nodes prune historical state after a few weeks, so older epochs will return 404. To compute this list back to the start of the chain, you need an archive node — one that retains all historical state since block 1.
A run against a pruned node will silently skip the older epochs and miss most of the affected miners.
- Affected miners: 19
- Total unclaimed (settle-drop bug only): 1 075.336 GNK
- Mechanism:
SettleAccountssettleError →SetSettleAmountskipped →MsgClaimRewardsreturns"No rewards for this address".
pip install -r requirements.txt
python3 unclaimed.py <ARCHIVE_NODE_IP> --out unclaimed.csvThe script accepts either a hostname/IP (default port 8000) or a full
URL. A SQLite cache is created in cache_unclaimed_<NODE>/ so
subsequent runs are incremental — re-running takes seconds rather than
the ~30 minutes a full cold run takes.
To bound the range, use --from-epoch and --to-epoch:
python3 unclaimed.py <ARCHIVE_NODE_IP> --from-epoch 1 --to-epoch 273 --out unclaimed.csvunclaimed.py— end-to-end script (archive node required).requirements.txt— single dependency (aiohttp).cache_unclaimed_<NODE>/cache.db— SQLite cache. Safe to delete; it will be rebuilt on the next run.unclaimed.csv— output (generated). One row per affected miner with per-epoch breakdown and totals.
- The script connects to an archive node and successfully returns
epoch_performance_summaryfor the oldest epoch in scope. - Pick any row in the CSV. For one of its non-zero epoch columns
N, queryepoch_performance_summary/{N}/{addr}—rewarded_coinsmust match the column value andclaimed = false. - For the same
(N, addr), querysettle_amount/{addr}with headerx-cosmos-block-height: <eff of epoch N+1>and confirm the response is404 not found(i.e. SettleAmount was never written). - As a positive control: pick any address with
claimed = truefor epoch N. The samesettle_amountquery at the same block must return a record (and that address must NOT appear in the CSV). - Sum of the
total_ngonkacolumn equals the script's reported total. - No address has
total_ngonka == 0(rows are only written for addresses with at least one settle-dropped pair).