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
@@ -0,0 +1,43 @@
{
"db_name": "PostgreSQL",
"query": "\n select date_bin($3, observed_at, timestamptz 'epoch') as \"bucket!\",\n count(*) filter (where miner = $2) as \"mine!\",\n count(*) as \"total!\",\n -- Expected hashes this miner spent in the bucket. Over the\n -- bucket's own duration that is its hashrate directly, with\n -- no reference to the network's size — which is what makes\n -- the series survive difficulty moving underneath it.\n coalesce(sum(difficulty) filter (where miner = $2), 0)::text as \"work!\"\n from block\n where chain = $1 and observed_at >= $4\n group by 1\n order by 1 asc\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "bucket!",
"type_info": "Timestamptz"
},
{
"ordinal": 1,
"name": "mine!",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "total!",
"type_info": "Int8"
},
{
"ordinal": 3,
"name": "work!",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Interval",
"Timestamptz"
]
},
"nullable": [
null,
null,
null,
null
]
},
"hash": "089d2d81d7a50482dd988c5807cee6981dd854ef9984f280c3b644ded2a7fb4f"
}
@@ -1,37 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n select date_bin($3, observed_at, timestamptz 'epoch') as \"bucket!\",\n count(*) filter (where miner = $2) as \"mine!\",\n count(*) as \"total!\"\n from block\n where chain = $1 and observed_at >= $4\n group by 1\n order by 1 asc\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "bucket!",
"type_info": "Timestamptz"
},
{
"ordinal": 1,
"name": "mine!",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "total!",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Interval",
"Timestamptz"
]
},
"nullable": [
null,
null,
null
]
},
"hash": "41f694ddea9ed92237677559e524a8f8c0e28d31217a80094b5d096214dc4e37"
}
@@ -1,35 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n select height, miner, observed_at\n from (\n select height, miner, observed_at\n from block\n where chain = $1\n order by height desc\n limit $2\n ) recent\n order by height asc\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "height",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "miner",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "observed_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text",
"Int8"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "461853e4ec42eaf95c83f6ed8e177ffd527f1b59461c1e4fa582f1d935c7b6a3"
}
@@ -0,0 +1,47 @@
{
"db_name": "PostgreSQL",
"query": "\n select height, miner, observed_at, authored_at, difficulty\n from (\n select height, miner, observed_at, authored_at, difficulty\n from block\n where chain = $1\n order by height desc\n limit $2\n ) recent\n order by height asc\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "height",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "miner",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "observed_at",
"type_info": "Timestamptz"
},
{
"ordinal": 3,
"name": "authored_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "difficulty",
"type_info": "Numeric"
}
],
"parameters": {
"Left": [
"Text",
"Int8"
]
},
"nullable": [
false,
false,
false,
true,
true
]
},
"hash": "613fcaadc85b9d3870abd6d158661b90e4133110744c383e8a20eee87cce5556"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n select height, miner, observed_at, at_tip\n from block\n where chain = $1\n order by height desc\n limit $2\n ",
"query": "\n select height, miner, observed_at, at_tip, authored_at, difficulty\n from block\n where chain = $1\n order by height desc\n limit $2\n ",
"describe": {
"columns": [
{
@@ -22,6 +22,16 @@
"ordinal": 3,
"name": "at_tip",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "authored_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "difficulty",
"type_info": "Numeric"
}
],
"parameters": {
@@ -34,8 +44,10 @@
false,
false,
false,
false
false,
true,
true
]
},
"hash": "594f0b138422b4f15986e92d07deadaac1dce80502a2f8b3e35d490e8ff148d5"
"hash": "ecadeb2897735698673b754462d9b537048bf4fa503ae8df878e2681393e0ab7"
}