fix: a miner's hashrate is the work it did, not its share of today's network
deploy / build (push) Successful in 6m31s
deploy / deploy-web (push) Successful in 4s
deploy / deploy-api (push) Successful in 14s

Reported against a Grafana panel showing roughly a sixth of what the site did.
Grafana was closer to the truth.

The estimate was `share of blocks in the window × current network hashrate`.
That is only correct while the network is the same size across the whole
window. Measured on mainnet's launch day it was nowhere near: difficulty rose
346-fold inside one 14,400-block window, from 96.4B to 33.4T. A miner that won
its blocks early — when a block cost a three-hundredth of what it costs now —
had that share multiplied by today's network. For this observer's own node the
site reported 25.4 GH/s against an actual 2.5 GH/s.

Difficulty is expected hashes per block by definition, so the difficulty of the
blocks a miner won, summed and divided by the time they spanned, is its
hashrate directly — with no reference to the network's size, and immune to that
size changing underneath, because every block carries the difficulty in force
when it was won. The observer has recorded per-block difficulty correctly since
the parent-state fix, so the data was already there.

`miner_series` had the same flaw and is fixed the same way: summed work per
bucket over the bucket's own duration.

The window's duration is taken from `authored_at`, not `observed_at`. A stretch
this process caught up on carries observation times seconds apart for blocks
minutes apart, and dividing real work by that would invent hashrate — the same
trap the history series was built to avoid.

The headline network figure is unchanged. It divides current difficulty by the
measured interval because it answers a different question: how hard is a block
to win right now, not what did this miner do over the last twelve hours. The
two no longer sum to each other, which is correct and is what the footer now
says.

`standings_rank_by_blocks_then_recency` asserted 400.0 from the old formula;
its third argument is now the window's duration, so it asserts the work-based
figure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
This commit is contained in:
2026-09-09 15:45:34 +03:00
co-authored by Claude Opus 5
parent 17ad5b1f74
commit 9eb4bee649
15 changed files with 295 additions and 117 deletions
+4 -2
View File
@@ -240,8 +240,10 @@ export default function App() {
header — no registration, no opt-in, no way to be left out.
</span>
<span>
Hashrates are estimates from blocks won over a finite window, not measurements of anyone's
hardware.
Hashrates are estimates over a finite window, not measurements of anyone's hardware. A
miner's figure is the difficulty of the blocks it won — expected hashes, by definition —
divided by the time they spanned, so a miner that won while the network was smaller is
credited with the work it actually did rather than a share of what the network is now.
</span>
<span>
Node names are a best-effort mapping, inferred from which node reported a block to
+1 -1
View File
@@ -135,7 +135,7 @@ export function MinerPanel({
<div className="stat">
<div className="stat-label">Est. hashrate</div>
<div className="stat-value">{hashrate(row?.hashrate_estimate)}</div>
<div className="stat-note">from share of blocks won</div>
<div className="stat-note">from work its blocks cost</div>
</div>
<div className="stat">
<div className="stat-label">Blocks observed</div>