Files
observer/web/src/components/DistributionPanel.tsx
T
grenadeandClaude Opus 5 4d9d41888a 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>
2026-09-14 11:43:39 +03:00

177 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Who won the blocks, and what became of the rewards.
*
* **This table reports flows and does not judge them.** Every figure is
* something the chain recorded — direction comes from each transfer event's own
* `from` and `to` — but what the figures *mean* is a question the data cannot
* settle. An operator may hold several addresses, may settle off-chain, or may
* simply be a solo miner keeping what it earned, which is the ordinary case and
* unremarkable.
*
* So there is no badge, no flag, and no word like "delinquent" anywhere here.
* What earns the table its place is **comparison**: one miner's recipient count
* says almost nothing, and beside its neighbours' it says a great deal. The
* reader draws the conclusion; the site lays out the evidence and names its
* limits.
*
* The name column is the weakest thing on the page and is marked as such. There
* is no such thing as a "pool" on this chain — the word appears only inside a
* name an operator reports over telemetry, and a site that flagged entities as
* delinquent on the strength of a name they chose themselves would be one bad
* match away from libelling someone.
*/
import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import type { Distribution } from '../api/generated/Distribution'
import { RequestFailed, fetchDistribution } from '../api/rest'
import { height as fmtHeight, share as fmtShare, shortAddress, tokens } from '../lib/format'
import { href } from '../lib/routes'
import type { Window as WindowName } from '../api/generated/Window'
export function DistributionPanel({
chain,
window: windowName,
decimals,
symbol,
}: {
chain: string
window: WindowName
decimals: number
/** The unit, shown once in the header rather than on every cell: six columns
* of amounts each repeating "QTC" is noise, and they are all the same unit. */
symbol: string
}) {
const [data, setData] = useState<Distribution | null>(null)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const controller = new AbortController()
setData(null)
setError(null)
fetchDistribution(chain, windowName, controller.signal)
.then(setData)
.catch((e: unknown) => {
if (controller.signal.aborted) return
setError(e instanceof RequestFailed ? e.message : 'Could not reach the observer.')
})
return () => controller.abort()
}, [chain, windowName])
if (error) return <p className="empty">{error}</p>
if (!data) return <p className="empty">Following the rewards…</p>
const amount = (raw: string | null | undefined) =>
raw === null || raw === undefined ? '—' : tokens(raw, decimals, 1)
return (
<section className="panel" style={{ marginBottom: 26 }}>
<div className="panel-head">
<h2 className="panel-title">Where the rewards went</h2>
<span className="eyebrow">
{fmtHeight(data.window_blocks)}-block window · amounts in {symbol} ·{' '}
{data.indexed_from !== null && data.indexed_to !== null
? `blocks ${fmtHeight(data.indexed_from)}–${fmtHeight(data.indexed_to)}`
: 'nothing indexed yet'}
</span>
</div>
<div className="scroll-x">
<table className="board">
<caption className="visually-hidden">
Each miner&apos;s share of blocks, what it was paid, and what it has sent on
</caption>
<thead>
<tr>
<th scope="col" className="left">
Miner
</th>
<th scope="col">Share</th>
<th scope="col">Mined</th>
<th
scope="col"
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."
>
Received
</th>
<th
scope="col"
title="Transfers whose sender the chain recorded. Wormhole sends record none, so a zero here is not evidence that nothing left."
>
Sent visibly
</th>
<th scope="col">Recipients</th>
</tr>
</thead>
<tbody>
{data.miners.map((m) => {
const recipients = m.flows?.recipients ?? null
return (
<tr key={m.miner}>
<td className="left">
<div className="call-cell">
<Link to={href({ chain, miner: m.miner, window: windowName })}>
{m.display}
</Link>
{/* Only where the name is not already the address: an
unattributed miner renders as its payout address, and
repeating it underneath is one string printed twice. */}
{m.address && m.attribution !== 'preimage' && (
<span className="runtime-type">{shortAddress(m.address)}</span>
)}
</div>
</td>
<td className="numeral">{fmtShare(m.share)}</td>
<td className="numeral">{amount(m.mined)}</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>
)
})}
</tbody>
</table>
</div>
<p className="panel-note">
<strong>This chain hides who sends.</strong> A wormhole transfer records its recipient and
its amount but not its origin — on mainnet, 9,894 such transfers reached 654 recipients with
no sender recorded at all. So <em>sent visibly</em> and <em>recipients</em> count only the
transfers that named a sender, and{' '}
<strong>a zero in those columns is not evidence that nothing left</strong>. It is evidence
that nothing left by a route the chain writes down. Payouts here are very plausibly wormhole
transactions, and the privacy is a feature of the chain rather than a gap in this table.
</p>
<p className="panel-note">
<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>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>
</section>
)
}