fix: a miner's balance is a receipt total, and the supply figures were wrong

Two claims this repository shipped, both disproved by measuring mainnet.

**A mining address is keyless, so its balance can only rise.** `retained`
was documented here as "the one measure privacy cannot hide", reasoning that
a payment debits the account whichever route it takes. It does not.
`mining-rewards` pays `derive_wormhole_address(preimage)`, which no Dilithium
keypair signs for, and `Wormhole::credit_and_record` credits an exit's
destination with `Unbalanced::increase_balance` while never debiting the
note's source. QUANPOOL's balance rose at all twelve samples between blocks
1,000 and 42,400 and fell at none, nonce 0 throughout — as every miner's must.

So `retained` read ~100% for everyone because it had to, and a number
identical for all reads as an accusation aimed at whoever mined most. The
column is gone, `balance` is `received`, and the table says plainly that a
pool paying out in full and one paying nothing produce the same row.

**Four empty lock maps are not "nothing is withheld".** Locks, Holds,
Freezes and Reserves are all empty — counted, not assumed — and the network
page concluded circulating equals total issuance. Wrong by a factor of 436:
this runtime vests from a *pot*, not with locks, and `modlqvesting` holds
5,669,940 QTC, 99.54% of every unit of balance on the chain. The page now
reports the pot and issuance-minus-pot, and still declines the word
"circulating" because the release schedule is not read here.

The pot address is derived from `Vesting::PalletId` in metadata rather than
a hard-coded `qvesting`: it is a consequence of a runtime constant, and a
runtime that changed it would leave the site reporting an empty pot while
looking perfectly healthy. Both steps are pinned against the real mainnet
metadata fixture.

Also recorded: never sum account balances and call it supply. They exceed
`TotalIssuance` by 13,427 QTC at block 42,400 and by exactly zero at genesis,
bisecting to block 4,800 and a `verify_public_batch` — private value is
counted twice by any such total. Single blocks reconcile exactly, so a spot
check says everything is fine.

Refs #17
Closes #19

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 11:43:39 +03:00
co-authored by Claude Opus 5
parent e3c95d4945
commit 4d9d41888a
13 changed files with 484 additions and 91 deletions
+23 -40
View File
@@ -30,34 +30,6 @@ import { height as fmtHeight, share as fmtShare, shortAddress, tokens } from '..
import { href } from '../lib/routes'
import type { Window as WindowName } from '../api/generated/Window'
/**
* Balance as a percentage of everything the chain has paid an address.
*
* **The one measure here that privacy cannot hide.** A wormhole transfer does
* not record its sender, so `sent visibly` is blind to the very mechanism a
* wormhole-held balance would use to pay out — 9,894 such transfers reached 654
* recipients on mainnet with no sender recorded at all. But a payment still
* debits the account, so a balance that has not fallen is a balance nothing has
* left, by any route, private or otherwise.
*
* Above 100% means the address also received from somewhere besides mining.
*/
function retained(mined: string | null, balance: string | null): string {
if (mined === null || balance === null) return '—'
let held: bigint
let paid: bigint
try {
held = BigInt(balance)
paid = BigInt(mined)
} catch {
return '—'
}
// Nothing mined is not zero retention, it is a ratio with no denominator —
// and most accounts that distribute widely on this chain mined nothing at all.
if (paid === 0n) return '—'
return `${Number((held * 1000n) / paid) / 10}%`
}
export function DistributionPanel({
chain,
window: windowName,
@@ -117,12 +89,11 @@ export function DistributionPanel({
</th>
<th scope="col">Share</th>
<th scope="col">Mined</th>
<th scope="col">Holds</th>
<th
scope="col"
title="Balance as a percentage of everything the chain has paid this address. The one column privacy cannot hide: a payout debits the account whether or not its sender was recorded."
title="Everything ever credited to this address. A mining address is keyless, so nothing can transfer out of it and this number can only rise — it is a receipt total, not a holding."
>
Retained
Received
</th>
<th
scope="col"
@@ -153,8 +124,7 @@ export function DistributionPanel({
</td>
<td className="numeral">{fmtShare(m.share)}</td>
<td className="numeral">{amount(m.mined)}</td>
<td className="numeral">{amount(m.balance)}</td>
<td className="numeral">{retained(m.mined, m.balance)}</td>
<td className="numeral">{amount(m.received)}</td>
<td className="numeral">{amount(m.flows?.sent)}</td>
<td className="numeral">{recipients === null ? '—' : fmtHeight(recipients)}</td>
</tr>
@@ -174,17 +144,30 @@ export function DistributionPanel({
transactions, and the privacy is a feature of the chain rather than a gap in this table.
</p>
<p className="panel-note">
<em>Retained</em> is the column privacy cannot hide. A payment debits the account whichever
route it takes, so a balance that has not fallen below what the chain paid in is a balance
nothing has left. Above 100% means the address also received from somewhere other than
mining. It still cannot tell you whether an operator pays from a different wallet, settles
off-chain, or is simply a solo miner keeping what it earned — which is the ordinary case,
and which every mining address on this chain currently resembles.
<strong>
There is no column here that can tell you whether a miner is paying out, and there cannot
be.
</strong>{' '}
The chain pays a mining reward to an address derived from the miner&apos;s own reward
preimage — a Poseidon hash that no key signs for. Nothing can transfer out of it. The only
way to move the value is a wormhole exit, which credits its destination{' '}
<em>without debiting the source</em>. So <em>received</em> is a running total of everything
ever credited, it rises and never falls, and it does that identically for an operator who
pays out in full and one who pays out nothing. Measured over all of mainnet&apos;s history,
the largest miner&apos;s balance rose at every sample and fell at none.
</p>
<p className="panel-note">
That is also why this table no longer carries a “retained” percentage. It read close to 100%
for every miner on the chain, because it had to, and a number that is the same for everyone
reads as an accusation aimed at whoever happens to be at the top — which here is simply
whoever mined the most. The place any of this is observable is the{' '}
<Link to={href({ chain, index: 'wormhole' })}>wormhole page</Link>, which counts what has
gone in and come out without claiming to know who moved it.
</p>
<p className="panel-note">
The names are the weakest column and come from telemetry, not the chain — an operator
chooses what its node calls itself, so a name containing “pool” is a claim rather than a
fact. <em>Holds</em> is chain state and is true now, while <em>mined</em>,{' '}
fact. <em>Received</em> is chain state and is true now, while <em>mined</em>,{' '}
<em>sent visibly</em> and <em>recipients</em> are counted over the indexed range above; on a
chain read from genesis those agree, on one still being indexed backwards they do not.
</p>
+43 -9
View File
@@ -85,6 +85,15 @@ export function NetworkPanel({
locks.freezes === 0 &&
locks.reserves === 0
// The vesting pot, which on this chain holds almost everything. Rendered as a
// percentage because two eighteen-digit strings do not compare at a glance,
// and 99.5% is the whole point.
const pot = summary.vesting_pot
const potShare =
pot && pot.share_bps !== null
? `${(pot.share_bps / 100).toFixed(pot.share_bps > 9900 ? 2 : 1)}%`
: null
const spark = (pick: (d: NetworkSummary['days'][number]) => number, unit: string): SparkPoint[] =>
summary.days.map((d) => ({
value: pick(d),
@@ -120,6 +129,20 @@ export function NetworkPanel({
value={amount(summary.mined_since_genesis)}
note="issuance less the endowment"
/>
<Field
label="Held in the vesting pot"
value={pot ? amount(pot.balance) : '—'}
note={
potShare === null
? 'the vesting pallet\u2019s own account'
: `${potShare} of issuance`
}
/>
<Field
label="Issuance outside the pot"
value={amount(summary.issuance_outside_pot)}
note="the ceiling on what has been distributed"
/>
<Field label="Fees collected" value={amount(summary.collected_fees)} />
<Field label="Inactive issuance" value={amount(summary.inactive_issuance)} />
<Field
@@ -129,13 +152,22 @@ export function NetworkPanel({
/>
</div>
<p className="panel-note">
{pot && potShare !== null && (
<>
<strong>{potShare} of all issuance sits in one account</strong> — {pot.address}, which
the vesting pallet owns and nobody holds a key to. This runtime vests from a pot
rather than with locks: undistributed funds wait there and are paid out from it.{' '}
</>
)}
{nothingLocked ? (
<>
<strong>Nothing is locked.</strong> All four of the balances pallet&apos;s
immobilising maps — locks, holds, freezes and reserves — are empty, counted rather
than assumed. Which is why there is no separate “circulating supply” figure here: it
would equal total issuance exactly, and printing it would assert a distinction this
chain does not currently make.{' '}
All four of the balances pallet&apos;s immobilising maps — locks, holds, freezes and
reserves — are empty, counted rather than assumed.{' '}
<strong>That is not the same as nothing being withheld</strong>: this chain withholds
with the pot above, which no lock records. There is still no “circulating supply”
figure here, because funds leave the pot on a schedule this observer does not read and
an address outside it may be unspendable for other reasons — but issuance alone would
overstate what is in play by a factor of several hundred.{' '}
</>
) : (
locks && (
@@ -147,12 +179,14 @@ export function NetworkPanel({
)}
{summary.vesting_schedules !== null && summary.vesting_schedules > 0 && (
<>
There are {count(summary.vesting_schedules)} vesting schedules, shown as a count
rather than a sum: this runtime&apos;s vesting does not touch the balances
pallet&apos;s locks, so what it holds and when it releases would be a guess without
reading the pallet&apos;s own arithmetic.
The {count(summary.vesting_schedules)} vesting schedules are a count rather than a
sum; the sum that matters is the pot, and it is read directly.{' '}
</>
)}
<strong>Account balances are not added up anywhere on this site.</strong> On this chain
they exceed total issuance and the gap grows: a wormhole exit credits its destination
without debiting the note&apos;s source, so value that moves privately appears twice in
any such total. Total issuance is the figure that counts each unit once.
</p>
</section>