ffcfb79caba9460924ae0a16f84cfd13b8fb48e6
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ffcfb79cab
|
feat(attribution): carry a miner's name across chains by reward preimage
Live telemetry names the miners who win often and leaves everyone else as a hex string. Measured on mainnet: of 22 miners, 2 were named and 14 had no name available anywhere — but 6 already had one on Planck, under the same reward preimage. Those 6 are exactly the small operators the naming is worst for, because a name needs a block whose first reporter the feed could separate and they win few blocks. The preimage is derived from a secret only its owner holds, so the same preimage on two chains is the same operator. That is an inference the site can stand behind, unlike guessing from timing — and it is the only lever that works without waiting on the feed at all. It is a separate `AttributionSource` and renders as one. `Carried` names carry no confidence and no votes, because they assert nothing about this chain: only that this is who the operator was last known to be. Any live attribution here outranks one, and the row shows an `elsewhere` chip rather than the telemetry `node` chip so the two can never be read as the same claim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5 |
||
|
|
bfd3470840
|
feat(attribution): offer a best-effort name, and say how thin the evidence is
A name now appears as soon as one node leads the vote, rather than waiting for three. On a chain that resolves a first reporter for about one block in seven, the old bar left almost every miner permanently anonymous — and since a later block can overturn a mapping, a guess that corrects itself is more useful than a blank that never fills in. What is refused is a dead heat. Choosing between two equally-voted nodes would be a coin toss wearing a telemetry badge, which is a different thing from a thin but real lead. The honesty moves to the presentation instead of being dropped. A percentage alone cannot separate "100% of 1" from "86% of 7", so `Attribution` and `LeaderboardRow` now carry the number of votes behind the name. The leaderboard marks a mapping resting on a single block or a narrow lead with `node?` and a dashed chip, and the tooltip states both figures and that a later block can correct it. Dashed rather than a second colour: the accent hue is reserved for identity and alarm, and this is neither — the same claim held more loosely. The footer now says node names are inferred from which node reported a block to telemetry first, never something the miner asserted, and that the reward preimage beside them is the only identity the chain itself vouches for. `a_single_lucky_vote_still_names_nobody` asserted the old policy and is replaced by `one_vote_names_but_says_it_is_only_one`, which pins the new contract: it names, and it reports `votes == 1` so the UI can mark it. A new `a_dead_heat_names_nobody` keeps the case that is still refused. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5 |
||
|
|
ef3a4b2689
|
fix(attribution): a held name survives silence, and only a clear lead replaces it
Reported from the live site: a name would appear and then be "often forgotten if it doesn't mine a new block within the window". `attribute` dropped the held key the moment the vote window contained no vote for it. That reads silence as contradiction — the same mistake as counting abstentions in the confidence denominator, one layer further on. The window holds twenty observations and mainnet resolves a first reporter on roughly one block in seven, so an incumbent's votes routinely roll out before any replacement earns one. The name vanished at that moment and the row fell back to the raw preimage until three fresh votes rebuilt it. A held name is now replaced rather than dropped: it stands until a different node leads it by `TAKEOVER_MARGIN`. The margin is the other half of the same problem — a window carrying about three cast votes flips on a single vote under a bare majority rule, and pools demonstrably run several nodes that all report their blocks (this miner has answered to both `QUANPOOL - quanpool-com` and `quanpool-payout-mainnet`). Requiring a lead of two rides that out instead of oscillating between siblings. Takeover after a restart or rename still works, and is still tested: a node that genuinely owns the blocks accumulates votes and passes the margin within a few blocks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5 |
||
|
|
057937e9cc
|
fix(attribution): measure confidence over votes cast, not the whole window
Mainnet has shown no node names since launch. Every miner renders as its raw preimage while Planck, on the same code and the same feed, names twenty. `votes` holds one slot per block checked and a `None` for every block whose first reporter the feed could not separate from its second. Those are abstentions — the feed did not say who led — but confidence divided agreement by `votes.len()`, counting them as votes against. Measured on the live feed, mainnet resolves a clean lead on about a quarter of blocks, so the best score any author could reach was 0.25 against a `MIN_CONFIDENCE` of 0.6. No name could ever be held, however unanimous the votes that were actually cast. Planck cleared the bar only because 41-87% of its blocks produced usable votes. Confidence is now agreement among cast votes, which is what it always meant: "of the blocks where we could tell who was first, this fraction pointed here". `MIN_VOTES` guards the other side, so a single vote in an otherwise silent window is not read as 100% agreement. `a_held_name_survives_a_confidence_dip` asserted `confidence < 1.0` after five unresolvable blocks. That assertion encoded the bug. It now asserts the opposite for abstentions and keeps a real dip — disagreeing votes — as the case that lowers confidence. Two false leads are recorded in CLAUDE.md so the next person does not spend the afternoon on them: the feed delivers a 190-node roster before any live block traffic, so `ImportedBlock` does not appear for ~40 s after subscribing and a short probe concludes the feed is dead; and the roster's `AddedNode` entries carry a height-0 placeholder block with a zero hash, which inflates the apparent count of nodes claiming to be first if counted as import reports. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5 |
||
|
|
7e5c2de936
|
feat: sparkline history under the headline tiles
Adds `GET /v1/chains/{chain}/series` and a sparkline under network hashrate,
difficulty, block time and miners. Height gets none — it only goes up, and a
straight diagonal reports nothing.
The chart is bucketed by height, not by time: every point is the same number of
blocks, so every point carries equal statistical weight and a stretch nobody
recorded stays the same width on the axis as one that was. A bucket is the
window over fifty, which makes a full window exactly fifty buckets — that is
what lets the rolling miner count at the last point be the same number printed
in the tile above it, rather than a per-bucket count that would sit well below
it. Buckets are anchored to absolute height for the same reason `miner_series`
bins from the epoch: anchored to the tip instead, every boundary slides by one
on every block and the chart shifts under a reader watching it.
Interval comes from `authored_at`, never `observed_at`. A gap fill writes a
whole batch of observation times within the same second, so deltas taken from
it read as a chain producing hundreds of blocks a second — the trap
`RollingWindow`'s `at_tip` flag exists to avoid, which until now lived only in
memory. It is a column as of `0002`, so the distinction survives a restart and
`observed_at` is interpretable at all.
Each point's interval is a three-bucket moving average. The mean of one
bucket's gaps still carries real Poisson error — 72 blocks of a 15 s interval
lands at ±1.8 s — and across fifty points that draws a chain that appears to
change size every few minutes and does not.
Fixes a wrong number found on the way in: `pallet_qpow` retargets in
`on_finalize`, so `QPoWApi_get_difficulty` at the latest state is the
difficulty for the *next* block, not the one just seen. Every stored difficulty
was off by one block — invisible, because a gradual retarget still looks
plausible. It is now asked at `header.parent_hash`, the same call the miner
made when it built the block. A node that has pruned that state gets `None`
rather than the current value: substituting it is exactly how a gap fill would
stamp today's difficulty across a stretch of old heights. The CLI backfill had
the same bug at greater scale, copying one reading across an entire range.
Unknowns are drawn as breaks in the line, never interpolated: an outage, a
difficulty a pruned node cannot supply, a point without a full window of
authors behind it. A chart that guesses across what was never recorded is the
same class of lie as a hashrate divided by a sync speed, and just as hard to
catch afterwards.
Read against the dataviz skill first, per CLAUDE.md. Single series, one hue;
emphasis on the newest point is `--data-bright`, a lighter step of the same
bronze, because the usual "current period in the accent" convention is the one
change that would break this palette. The hover readout borrows each tile's
note line — five floating tooltips across a row of 180px tiles is a pile, not a
hover layer — and the same readout is on keyboard focus and in the aria-label,
since the line does not start at zero and its height is not a quantity anyone
should be estimating.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
|
||
|
|
110fbc3631
|
feat: blackbeard.observer — live Quantus mining leaderboard
Cargo workspace plus a Vite frontend, following ~/git/architecture/generic.md. Every block header carries its author's wormhole reward preimage in a `pow_` PreRuntime digest, so authorship for the whole network is derivable from headers alone — no indexer, no registration, no way for a miner to be left out. That decoding, the hashrate maths and the telemetry name attribution live in blackbeard-core with no I/O at all, so the parts that are easy to get subtly wrong are exercised by unit tests rather than only against a live chain. The browser holds one WebSocket: snapshot on subscribe, deltas thereafter. The head stream is itself a push (chain_subscribeNewHeads), so a block reaches the page the moment the node imports it. Messages are serialised once per broadcast, and leaderboards are recomputed only for windows a socket is actually watching. No RxJS — useSyncExternalStore is React's own contract for this. Verified against the live Planck testnet: 12/12 headers decoded, telemetry names attributed (quanpool-planck, baba-gorchitsa, …), warm start restoring 84 blocks and 5 held names across a restart. Three findings worth recording, all in CLAUDE.md: - substrate-telemetry sends its JSON in *binary* frames. A text-only client connects, subscribes, reports healthy and receives nothing at all — and a Python probe hides it, because json.loads accepts bytes. - Difficulty is a little-endian U512; decoding it big-endian gives a number wrong by ~10^150 that still renders fine. - Planck's real block interval is ~13-15s against a 6s target with enormous variance, so a measured interval needs 20 tip samples before it is publishable. Deploy assets, the Gitea Actions workflow and script/infra-setup.sh are included; port 25864 is registered in architecture/port-allocations.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSDYiibCtELsrjQq6KXnoi |