f9ea292a70ca50c2a1aa19de7ad63c05a1ade095
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f9ea292a70
|
fix: a hashrate estimate now carries its error bar
The leaderboard credited a miner with "1.55 GH/s" on the strength of one block, last seen four hours earlier, to three significant figures. Block finding is Poisson: k blocks carries a relative standard error of 1/√k, so one block is ±100% and it takes twenty-five to reach ±20%. The estimator was unbiased and the presentation was not honest, which is the half that matters to a reader. `miner_hashrate` now returns the count's uncertainty with the figure, and the leaderboard and miner page render it — dimmed below four blocks, where the error bar is wider than half the value. Checked first that the arithmetic itself is sound, because the obvious suspicion was the formula. It is the chain's own: `qpow-math::is_valid_nonce` accepts when `hash < U512::MAX / difficulty`, `verify_nonce_internal` passes it what `get_difficulty()` returns, and the hash is uniform — 200,000 samples of `hash_squeeze_twice` gave 0.4995 of 2^512 against a uniform 0.5, and 12.48% below 2^509 against 12.5%. Acceptance is 1/difficulty, so expected trials is difficulty, exactly. The two network estimators — difficulty over tip interval, and summed work over window span — also agree within 5% on live data. What that leaves is recorded in CLAUDE.md and is not an observer bug: our three miners measure 1.66 GH/s (1.39 instantaneous), while the chain credited the same preimage 5.3 GH/s over 29 blocks. Sampling does not explain 14 sigma. `miner_cpu_hash_rate` is 0 on all three — every hash is on GPU — and the miner's CPU path calls `qpow_math::get_nonce_hash` directly while the CUDA kernel is a separate implementation with its own counter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5 |
||
|
|
a3eb670d11
|
feat: identify miners by the address they are paid at, not the preimage
A reward preimage is exact and unrecognisable — no miner knows theirs by sight. The address is the string they configured and the one their balance shows against, and the two are related by a derivation the chain performs anyway: `pallets/mining-rewards` pays `qp_wormhole::derive_wormhole_address`, which is `qp_poseidon_core::rehash_to_bytes`. Running the same function reproduces the address exactly, with no RPC and no lookup table. The derivation takes the preimage and nothing else — no genesis, no chain parameters — so one preimage is one account on every Quantus chain. The unit test pins it against a real pair (this observer's own node and the address it was paid at), so a dependency upgrade that changed the derivation fails the build rather than quietly relabelling every miner on the site. Unnamed miners now render as their address. The miner panel shows all three identifiers, because they answer different questions: the node name is an inference and may be absent, the address is what the miner recognises, and the preimage is what the header actually carries and the only one the chain asserts. SS58 is written out rather than pulled from `sp-core`, which would bring a substrate runtime's worth of dependencies to format 36 bytes — the same trade the readme records for declining subxt. Base58 is hand-rolled because it is long division and nothing else; blake2 is not, because checksums are not worth hand-rolling. Both are covered by the address test vector. `RecentBlock` gains the attribution source: the ticker cannot tell a node name from an address by looking, since `quanpool-payout-mainnet` and `quantus-radar` both begin with the letter Quantus addresses do. Two tests that asserted the old display policy now assert the new one. 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 |
||
|
|
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 |