Two standalone scripts that rebuild the subsidy (Bitcoin-style) reward
distribution for a Gonka epoch by reading historical chain state from a
chain-api endpoint (an archive node). They require no chain binary, no .env,
and no third-party packages — just Python 3 and network access to a node.
| Script | What it answers |
|---|---|
forecast_epoch_reward.py |
What did / would the chain pay this epoch? (fast approximation) |
forecast_epoch_reward_nocap.py |
What would the chain have paid if the ComputeGroupCap did not exist, and how does that compare to the actual on-chain payout? (exact settlement math) |
Both write a per-participant CSV and print a summary.
Both scripts use only the Python 3 standard library — there is nothing to
pip install. You need:
- Python >= 3.10
- network access to a Gonka chain-api endpoint (an archive node for old epochs)
A requirements.txt is included for completeness (it lists no packages):
pip install -r requirements.txt # no-op; here only to make the dependency story explicitRun from inside this scripts/ directory:
# actual / approximate distribution
python3 forecast_epoch_reward.py node1.gonka.ai 267
# counterfactual "no group cap" distribution, with a column comparing to
# what the chain really paid
python3 forecast_epoch_reward_nocap.py node1.gonka.ai 267 --comparenode1.gonka.ai is the node host; the chain-api URL is built automatically as
http://<node>:8000/chain-api. You can also pass a full http://host:port or
a .../chain-api URL. An archive node is required for old epochs whose
state has been pruned on regular nodes.
Output CSV defaults to ./epoch_<N>_reward.csv (capped) or
./epoch_<N>_reward_nocap.csv (no-cap). Override with --out PATH.
The two scripts answer different questions and use different math. This matters: they disagree by ~1% on the actual distribution, and that gap is exactly the part that determines a fair restitution number.
A fast reconstruction of the reward path:
reward_i = subsidy_pool × weight_i × ratio_i / Σ weight
ratio_i = active_i ? min(confirmation_weight_i / scaled_weight_to_confirm_i, 1) : 0
active_i = ratio_i ≥ 0.5 × 0.909 (0.4545)
It uses the capped weight already stored in EpochGroupData and an
activity ratio. It is a good approximation but drifts ~1% from what the chain
actually paid, because it does not faithfully model the effective-weight
rescale or the burned (un-redistributed) share.
A faithful port of the on-chain settlement path in
x/inference/keeper/bitcoin_rewards.go (lines 597–675), with the
ComputeGroupCap (delegation_weight_calculator.go:330–350) lifted.
effectiveWeight_i = confirmation_weight_i
if weight_i < rawTotal_i: effectiveWeight_i = CW_i × weight_i / rawTotal_i # rescale into consensus scale
effectiveWeight_i = min(effectiveWeight_i, fullWeight_i)
if INACTIVE/INVALID: effectiveWeight_i = 0 # earns nothing...
reward_i = subsidy_pool × effectiveWeight_i / Σ fullWeight_j # ...but still counts in the denominator
Three things the simplified model glossed over, and which this script gets
exactly right (validated to 0.0000% against actual rewarded_coins for
e267 when run in capped mode):
-
Effective-weight rescale. When a member's consensus
weightwas reduced below their raw PoC contribution (rawTotal), the chain rescalesconfirmation_weightbyweight / rawTotaland clamps it toweight. This is the precise numerator the reward formula uses. -
Burned / unconfirmed weight is kept in the denominator. Inactive, invalid, and partially-unconfirmed participants do not get their share redistributed to everyone else — it is burned to governance. The denominator is
Σ fullWeightover all members, so removing a member's reward does not inflate everyone else's. Ignoring this is the source of the ~1% drift. -
Status is derived, not looked up. A member is INACTIVE/INVALID exactly when their CPoC ratio
confirmation_weight / rawTotal < 0.4545(0.5 × pocDeviationCoeff). This reproduces the chain's ACTIVE/INACTIVE/INVALID split with zero mismatches on e267, so the script needs no per-participant status query.
ComputeGroupCap only ever shrinks a non-initial model group, by applying a
single scaleFactor = cap / rawTotal to every member's contribution. The
no-cap script sets that factor to 1.0, so each capped member's consensus
weight becomes its full uncapped contribution:
weight_i(no-cap) = fullWeight_i(no-cap) = rawTotal_i = Σ_models floor(coeff × Σ poc_weight)
Everything else (CW, the rescale branch, status zeroing, the burned-share denominator) is left identical to the chain — so the result isolates the cap's effect and nothing else.
A naive "uncap the weights" calculation (e.g. dividing each Kimi member's confirmation weight by the old, capped network total) double-counts: it raises the numerators without raising the denominator, so the "fair shares" sum to well over 100% of the pool. The exact model recomputes the denominator too. As a result, the corrected per-epoch compensation comes out several times smaller than the naive figure (for e267: ~56k GNK of positive shortfall vs ~246k from the naive method), because lifting Kimi's cap also dilutes every other group's share of the same fixed pool.
Columns (--compare adds the last two):
| Column | Meaning |
|---|---|
status |
ACTIVE or INACTIVE_THRESHOLD (derived from the CPoC ratio) |
weight_chain |
consensus weight the chain actually stored (post-cap) |
full_weight_nocap |
consensus weight with the cap removed (= raw_total for capped members) |
raw_total |
uncapped PoC contribution Σ floor(coeff × poc_weight) |
confirmation_weight |
CPoC-confirmed work (the reward numerator base) |
effective_weight |
final numerator after rescale/clamp/status |
share_pct |
effective_weight / Σ full_weight_nocap |
reward_nocap_gnk |
reward under the no-cap counterfactual |
actual_rewarded_gnk |
what the chain really paid (--compare only) |
diff_gnk |
reward_nocap − actual; positive = underpaid (owed), negative = overpaid |
The summary line reports total_distributed_gnk, burned_to_governance_gnk,
and (with --compare) two restitution totals:
Σ extra owed (no-cap − actual)— net difference across all members (can be negative: lifting the cap shifts pool share between groups).Σ extra owed (positive only)— sum of positivediffonly, i.e. paying the underpaid without clawing back from the overpaid. This is the figure to use for a restitution that only ever pays out.
forecast_epoch_reward_nocap.py <node> <epoch> [options]
<node> node host (node1.gonka.ai), or full http://host:port[/chain-api] URL
<epoch> epoch number, e.g. 267
--compare also fetch actual rewarded_coins and append diff columns
(one extra request per participant — slower)
--height-end H pin the snapshot block height (skips auto-discovery;
use the epoch's last block for a settled snapshot)
--initial-model M founding model id exempt from the cap
(default: first sub_group_models entry; cap removal is a
no-op for it, so this rarely needs setting)
--out PATH output CSV path
--quiet suppress progress logs
forecast_epoch_reward.py takes the same <node> <epoch> plus
--height-end, --out, --quiet.
503 Service Temporarily Unavailable— the node proxy is briefly overloaded. The no-cap script retries transient 5xx up to 4× with backoff; if it still fails, re-run. Pinning--height-endreduces the number of probe requests during height discovery.--compareis slower — it issues oneepoch_performance_summaryrequest per participant (~50 per epoch). Drop it if you only need the no-cap distribution.- Use an archive node for pruned historical epochs; a regular node only serves recent state.
- Numbers are GNK (1 GNK = 10⁹ ngonka). The chain stores ngonka; the scripts print/convert to GNK.
| Step | Chain source |
|---|---|
subsidy pool initial × exp(decay × (epoch−genesis)) |
bitcoin_reward_params |
group cap scaleFactor = cap / rawTotal |
delegation_weight_calculator.go:330–350 |
rawTotal = Σ floor(coeff × poc_weight) |
delegation_pipeline.go:149, bitcoin_rewards.go:181 (CoefficientAdjustedWeight) |
| effective-weight rescale + clamp | bitcoin_rewards.go:617–630 |
| status zeroing, full-weight denominator (burn) | bitcoin_rewards.go:597–675 |
activity threshold 0.5 × pocDeviationCoeff |
pocDeviationCoeff = 0.909 |