Skip to content

Say which pool a miner is actually looking at - #41

Merged
rsantacroce merged 1 commit into
mainfrom
2026-08-22-pool-identity-banner
Aug 22, 2026
Merged

Say which pool a miner is actually looking at#41
rsantacroce merged 1 commit into
mainfrom
2026-08-22-pool-identity-banner

Conversation

@rsantacroce

Copy link
Copy Markdown
Collaborator

The problem

The dashboard header says the same thing on every simplepool ever deployed:

simplepool   STRATUM STATS    Overview  Blocks  Templates

Four things a miner has no other way to learn are left unstated:

  • which Bitcoin network the coinbase is being built for — a stratum port looks identical on mainnet and on regtest
  • solo or pps-classic — whether a block pays its finder or the pool's wallet, and whether shares accrue anything in between
  • the coinbase tag
  • the operator fee address, and under pps-classic the pool wallet

What it looks like now

simplepool   SIGNET · PPS-CLASSIC    Overview  Blocks  Templates
─────────────────────────────────────────────────────────────────
 NETWORK signet   MODE pps-classic  fee 1.00%   TAG /simplepool/
 OPERATOR tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx
 POOL WALLET tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3

Addresses are shown in full. A truncated address verifies nothing, and
verifying where the money goes is the entire point. A non-mainnet pool gets a
warn-coloured rule — "why has my payout not arrived" and "this pool is mining
signet" are frequently the same question.

The strip renders on the admin nav too.

Where the facts come from

pool_meta, which the proxy now writes at startup — not the dashboard's
environment. That is the rule the PPS rate already follows, for the reason
already written into schema.sql: a second copy of the config is a copy that
can disagree with the pool it claims to describe.

The practical consequence is that a proxy that has not restarted leaves the
strip reading unknown. That is deliberate. A banner asserting the wrong
network is worse than one admitting it does not know.

The identity columns are read by their own guarded SELECT, so a dashboard
deployed ahead of the proxy — the upgrade order that actually happens — keeps
its rate figures instead of losing poolMeta entirely to add a banner. There
is a test pinning exactly that.

Determining the network

getblockchaininfo is authoritative but is not always available: the CUSF
enforcer serves exactly getblocktemplate and submitblock, and that
enforcer is precisely the backend a drivechain pool must point at. So:

  1. ask the node — network_source = node
  2. fall back to the network encoded in operator_addressnetwork_source = inferred, and the strip labels it, because an address cannot tell testnet from signet
  3. failing both, unknown

A footgun caught for free

Comparing the two answers catches something worth catching: a mainnet
operator address on a test chain, or the reverse
, pays the fee to a script
nobody on that chain controls. Valid block, plausible-looking coinbase, money
gone. That now gets a warning in the journal at startup:

operator_address 'bc1q…' is a main address but the node is on 'signet' —
the 100 bps fee would pay a script nobody on this chain controls.
Fix operator_address before mining a block.

Also

/api/status returns the same five fields under pool, so a monitor does not
have to scrape HTML to notice a pool restarted onto a different network.

Changes

Area
src/bitcoind.[ch] bitcoind_get_chain()getblockchaininfochain
src/coinbase.[ch] coinbase_address_network(), coinbase_network_is_mainnet()
src/main.c resolve_network(), mismatch warning, identity written at startup
schema.sql, src/store.[ch] 5 new pool_meta columns + migrations, store_record_pool_identity()
dashboard/ partial/pool-identity.ejs, both navs, stats.poolIdentity(), /api/status, CSS, README

store_record_pool_identity() touches only its own columns, so it cannot
collide with the per-template store_record_pool_meta() write in either
order — including the write-once credited_from stamp. It deliberately does
not touch updated_at, which means "when the rate was last refreshed"; a
stalled template path must not look alive because the process restarted.
Solo mode stores pool_btc_address as NULL rather than "", so "not
applicable in this mode" reads differently from "configured blank".

Testing

  • make test — all suites pass, including new test_pool_identity (store) and address -> network (coinbase). Builds clean under -Werror.
  • npm test in dashboard/ — 84 tests, 82 pass / 2 pre-existing /proc skips, including 6 new ones covering full-address rendering, solo mode hiding the pool wallet, inferred labelling, the non-mainnet flag, a pre-migration DB, and a null pool_meta.
  • Rendered by hand against a live dashboard on a seeded DB in all four states: pps-classic/signet, solo/main-inferred, un-migrated DB, and no row at all.

Not in this PR

The user picked the dashboard header as the surface to fix. The same identity
gap exists in simplepoolctl status, the proxy's startup log (partly closed
here — it now logs one pool identity: line), and the hardcoded "About the
numbers on this page" card on /, which still claims "PPS-classic build"
and "1 000 sats × share difficulty" regardless of the actual mode and
rate. That card is wrong on a solo pool today and is worth a follow-up.

The dashboard header read "simplepool · stratum stats" on every page. That
is true of every simplepool ever deployed, and it left four things a miner
has no other way to learn unstated: which Bitcoin network the coinbase is
being built for, whether the pool pays solo or pps-classic, the coinbase
tag, and the addresses the money goes to. A stratum port looks identical
on mainnet and on regtest.

The header chip now carries network and mode, and a strip under it names
the tag, the operator fee address and — under pps-classic — the pool
wallet, in full. Truncated addresses verify nothing, and verifying where
the money goes is the point.

The facts come from pool_meta, which the proxy now writes at startup, not
from the dashboard's environment. Same rule the PPS rate already follows,
for the same reason: a second copy of the config is a copy that can
disagree with the pool it claims to describe. The practical consequence is
that an un-restarted proxy leaves the strip reading "unknown" — deliberate,
since a banner asserting the wrong network is worse than one admitting it
does not know. The identity columns are read by their own guarded SELECT so
a dashboard deployed ahead of the proxy keeps its rate figures.

Determining the network needed care. getblockchaininfo is authoritative but
is not always available: the CUSF enforcer serves exactly getblocktemplate
and submitblock, and that enforcer is precisely the backend a drivechain
pool must point at. So ask the node first, fall back to the network encoded
in operator_address, and record which answered — an address cannot tell
testnet from signet, and "inferred" says so rather than guessing.

Comparing the two also catches a live footgun for free: a mainnet operator
address on a test chain, or the reverse, pays the fee to a script nobody on
that chain controls. Valid block, plausible coinbase, money gone. It now
gets a warning in the journal at startup.

/api/status returns the same five fields, so a monitor does not have to
scrape HTML to notice a pool restarted onto a different network.
@rsantacroce
rsantacroce merged commit 28554bc into main Aug 22, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant