Setup Health-Checks
Since Phase 9, the first-run wizard (dvb-warppool-setup) doesn’t just check
the Bitcoin Core RPC connection itself — it runs a diagnostic of:
- Bitcoin Core health — everything reachable over RPC (
/api/bitcoin-health) - Connectivity snapshot — RPC + ZMQ endpoint reachability, pre-filled from
your
bitcoin.conf(/api/connectivity-snapshot)
Both checks are entirely local — the wizard never contacts a third party.
Privacy matrix
| Check | What it does | Privacy |
|---|---|---|
| Bitcoin health (RPC calls) | Localhost → Bitcoin Core | ✅ Local |
| Connectivity snapshot | TCP-dials the configured RPC + ZMQ endpoints | ✅ Local |
There are no opt-in external probes and no third-party calls. The wizard
reads your bitcoin.conf (when a path is given) purely locally — to pre-fill
the form and to filter the config recommendations.
Bitcoin health in detail
A click on “Bitcoin Core health check” makes 5 RPC calls in parallel:
| RPC | What we extract |
|---|---|
getnetworkinfo | version, subversion |
getblockchaininfo | ibd, chain, blocks, headers, verification_progress, size_on_disk, pruned |
getindexinfo | txindex (on/off) |
getpeerinfo | inbound/outbound peer count |
getzmqnotifications | pubhashblock/pubrawblock endpoints |
Plus automatic warnings:
| Condition | Warning |
|---|---|
ibd=true | Mining not yet possible; the pool should only start after sync |
peers.total < 8 | Tip latency increased; new blocks arrive later |
peers.inbound = 0 on mainnet | Port 8333 is probably not forwarded |
zmq.hashblock=None | Pool falls back to RPC polling (job refresh latency) |
pruned=true | Some features (txindex-based) are limited |
verification_progress < 99.9% (and not IBD) | Still syncing |
Why inbound peers matter for a pool.
peers.inbound = 0is not just a good-citizen nag: a node that nobody can dial reaches the network’s new tip only through its own outbound peers, which adds latency to every block change — and every millisecond your pool keeps handing out a stale job is hashrate spent on a block that can no longer be won. For an internet-facing pool, forward TCP 8333 to the node and keeplisten=1on. The live reference deployment (pool.warppool.org) runs a pruned Bitcoin Core 31.1 with 8333 open, sitting at roughly 30 peers. The warning is skipped when the daemon already sees a LAN peer on 8333 (lan_peer_at_8333), so a node fronted by another host on the same network doesn’t warn spuriously.
Connectivity snapshot
On load (and via “↻ Re-check connection”) the wizard fetches
/api/connectivity-snapshot, which:
- Reads the RPC + ZMQ settings — from your
bitcoin.confif a path is given, otherwise mainnet defaults (:8332) — and pre-fills the form. - TCP-dials each endpoint locally (RPC URL,
zmqpubhashblock,zmqpubrawblock) with a short timeout and reports each as reachable / unreachable / not-set. Nothing is sent beyond the dial. - When no
bitcoin.confis found, it offers a one-click “Create mainnet config” (/api/create-bitcoinconf— writes a config with ZMQ + a random RPC password).
There is no port-bind smoke test, no UPnP discovery, and no external
reachability probe in the wizard. Reachability of your public node is a
Bitcoin Core concern (see the peers.inbound warning above); persistent UPnP
forwarding lives in the daemon ([upnp], see below).
What this looks like in code
Backend in apps/dvb-warppool-setup/src/main.rs:
bitcoin_health(BitcoinHealthReq)— the 5 RPC calls + the warnings (delegates towarppool_health)connectivity_snapshot()— TCP-dials the RPC + ZMQ endpoints and reports each
The warning logic lives in the warppool-health
crate (collect_bitcoin_warnings_v2_with_conf) and has 15 unit tests in
crates/health/src/bitcoin.rs covering every warning edge case (IBD,
low-peers, no-inbound, no-zmq, pruned, verification-progress,
no-double-warning-on-IBD, healthy-zero-warnings, plus the inbound / listen /
private-LAN-peer cases).
UI in apps/dvb-warppool-setup/src/index.html:
- Bitcoin card with RPC test + health-check button
- Connectivity card with per-endpoint status pills + override boxes
- Status cards with icons (✅⚠️❌), key-value rows, warning list with ⚠ bullet
Phase 10a — bitcoin.conf snippet generator
Instead of just emitting warnings, since Phase 10a the health check generates a copy-paste-ready bitcoin.conf snippet that fixes the identified problems. No direct write — the operator decides what (if anything) to adopt.
What ends up in the snippet
| Health finding | Snippet line |
|---|---|
zmq.hashblock=None | zmqpubhashblock=tcp://127.0.0.1:28332 |
zmq.hashblock=None AND zmq.rawblock=None | + zmqpubrawblock=tcp://127.0.0.1:28333 |
peers.total < 8 AND peers.outbound < 8 | maxconnections=125 |
peers.inbound == 0 AND chain="main" | listen=1 + comment about port forwarding |
What we deliberately don’t suggest:
txindex=1— heavy, many pools don’t need it (pruned is fine for solo)server=1— if the RPC test got through at all, this must already be truebind=— the defaultall-interfacesfits almost every case
Format
# ===== dvb-WarpPool Setup-Wizard Recommendations =====
# ZMQ block notifications for immediate job refresh
zmqpubhashblock=tcp://127.0.0.1:28332
zmqpubrawblock=tcp://127.0.0.1:28333
# More peer slots so Bitcoin Core gets more stable peer discovery
maxconnections=125
# enable listening so incoming peers can connect
# (additionally: forward port 8333/tcp on the router to this machine)
listen=1
# ===== End Recommendations =====
Header + footer as wrapper comments so the operator spots the section immediately — no risk of accidentally overwriting existing settings (all recommendations sit inside a marked block).
UI
In the browser:
- 📋 box containing the snippet as monospace code
- “Copy” button (
navigator.clipboard.writeText) - OS-aware path hint:
~/.bitcoin/bitcoin.conf(Linux),~/Library/Application Support/Bitcoin/bitcoin.conf(macOS),%APPDATA%\Bitcoin\bitcoin.conf(Windows) — vianavigator.platform - Fallback alert if the clipboard API is unavailable (e.g. http context without a TLS cert)
Daemon UPnP renew loop (Phase 27)
The wizard itself does not open router ports — it has no UPnP feature.
Persistent port forwarding is a daemon concern: an opt-in renew loop in the
daemon refreshes the forwards listed in the [upnp] block every
renew_interval_secs. Opt-in (default enabled = false).
Configuration:
[upnp]
enabled = true
renew_interval_secs = 3000 # 50min, safely below the 3600s lease
[[upnp.forwards]]
port = 3333
protocol = "tcp"
lease_seconds = 3600
description = "Stratum V1"
UPnP add_port is idempotent on the router side (same port + protocol =
extend lease, don’t create a second entry). The first renew happens 10s
after daemon start (covers crash recovery), then every
renew_interval_secs seconds (range 60..=86400). Errors are only
logged — the loop keeps running so transient problems don’t permanently
kill the forward.
Typical router quirks
- FritzBox default: UPnP-discoverable = yes, but
add_port= blocked. The operator has to enable “Allow UPnP status changes via UPnP” in the web UI. We then returnadd_port: The client is not authorized to map this port. - OpenWrt: UPnP off by default, has to be installed as a package
(
miniupnpd). - ISP routers (Telekom Speedport etc.): UPnP often completely disabled; some models have no UPnP at all.
In all of these cases the wizard shows a clear error with the exact router reason. The operator knows what to do.
Phase 12 — bitcoin.conf parse-existing
Instead of suggesting every recommendation, since Phase 12 the
snippet generator filters out lines that are already configured — if
the operator supplies the path to their bitcoin.conf.
How it works
- UI field “bitcoin.conf path” (optional, OS-aware placeholder)
- Path is sent along with the
/api/bitcoin-healthPOST - Server parses the file with
parse_bitcoin_conf_str(&str) -> BTreeMap<String, Vec<String>>— line-based, tolerant of:- Comments (
# ...) - Empty lines + whitespace
- Section headers
[main][test](ignored, all keys flattened) - Multiple values per key (e.g. two
zmqpubhashblock=lines) - Malformed lines (no
=→ ignored, no crash)
- Comments (
generate_bitcoinconf_snippet(&BitcoinHealth, parsed.as_ref())filters per recommendation:if !parsed.contains_key("zmqpubhashblock") → push recommendation
Default behavior is preserved
If the path is not set: all recommendations as before. If the path is set but the file isn’t readable: all recommendations + warning “bitcoin.conf at … not readable”.
Example
bitcoin.conf before the health check:
server=1
rpcuser=warppool
zmqpubhashblock=tcp://127.0.0.1:28332
maxconnections=200
What previously would have been suggested in the snippet:
zmqpubhashblock=tcp://127.0.0.1:28332(even though it’s already there!)zmqpubrawblock=tcp://127.0.0.1:28333(may be missing)maxconnections=125(even though 200 is already set!)listen=1(may be missing)
What with Phase 12 gets suggested:
- only
zmqpubrawblock=tcp://127.0.0.1:28333andlisten=1— the rest is already configured.
What we do NOT do
- No value-compare: if the operator has
maxconnections=10(bad), we still don’t suggestmaxconnections=125— we trust that the operator chose that value deliberately. We only fix “missing keys”, not “bad values”. - No direct write: we only parse read-only, no backup risk.
- No TOML/JSON format: bitcoin.conf has its own line-based format, not TOML.
Phase 13 — daemon periodic health check
The setup wizard runs once at first-run. The daemon runs continuous checks in the running pool — and pushes every change as an SSE event so the UI can show a dashboard widget.
Wiring
13a — crate extraction: The health logic was extracted out of the
setup wizard into its own crate
warppool-health. Since then the setup
wizard is just a thin axum wrapper; the daemon can call the same
functions directly without code duplication.
13b — periodic task: At startup, the daemon spawns a tokio task that runs the health check every N seconds (default 60). Activated via env:
WARPPOOL_HEALTH_CHECK_INTERVAL_SECONDS=60 dvb-warppool-daemon
# 0 = off (default on when env not set → 60s)
Per tick:
- Re-resolve RPC auth from
config.node.rpc_cookie_path(preferred) orsecrets.rpc_user/rpc_pass(the cookie gets rotated on Bitcoin Core restart — we read fresh every time) warppool_health::check_bitcoin_health(&client, &url, &auth).awaitcollect_bitcoin_warnings(&h)event_bus.publish(PoolEvent::HealthSnapshot { ... })
SSE event format
{
"type": "health_snapshot",
"at": "2026-05-27T12:00:00Z",
"rpc_ok": true,
"peers_total": 12,
"peers_inbound": 3,
"ibd": false,
"pruned": false,
"zmq_hashblock_ok": true,
"warnings": [
"Few peers (3/8 recommended) — tip latency increased"
],
"warning_keys": [
{ "key": "bitcoin_few_peers", "message": "…", "params": { "total": "3" } }
]
}
warnings is the legacy array of ready-made strings; warning_keys carries the
same warnings structured (stable key + params) so the UI can localise
them into all supported languages and so an operator can suppress a warning by
key (see below). The UI prefers warning_keys and falls back to warnings.
The UI subscribes via EventSource('/api/events'), dispatches on
event.type === 'health_snapshot', and renders a banner/badge with
the warnings (HealthBanner.svelte).
What it deliberately does NOT do
- No bitcoin.conf parse in the periodic check — that’s setup-wizard-specific because it generates snippet recommendations. The daemon doesn’t need it.
- No UPnP check in 13b — UPnP state changes rarely and is operator-side. The setup wizard checks once.
- No delta detection — we push every snapshot; the UI does the client-side compare if needed. Saves server state. (Still true today.)
- No read-only API endpoint
GET /api/admin/health— the UI uses SSE. The only admin health routes that exist are the suppression ones below. A pull endpoint for CLI scripting is still open; for now,/metricscovers scripted monitoring.
Operator example
# Filter the live SSE stream down to health events
curl -N http://localhost:18334/api/events 2>/dev/null \
| grep -A1 'event: health_snapshot'
# event: health_snapshot
# data: {"type":"health_snapshot","at":"...","rpc_ok":true,...}
The operator sees, for example, when after weeks of uptime
zmq_hashblock_ok=false suddenly shows up (Bitcoin Core restarted
without ZMQ config) — and can react without anyone having to monitor
hands-on.
Phase 14 — health banner in the dashboard (shipped)
The health_snapshot events are rendered by HealthBanner.svelte as a
persistent banner below the header, localised from warning_keys (falling back
to the raw warnings strings for older daemons). The banner is dismissable
per session on purpose — no localStorage, so a reload brings it back and
nothing gets permanently hidden by accident.
Suppressing a warning permanently
Some warnings are correct but irrelevant for a given deployment — a deliberately pruned node, for example, does not need to be told it is pruned on every reload. A logged-in operator can therefore suppress a warning by its stable key:
- In the UI: Admin → Health, or the “hide permanently” control on the banner.
- Over the API:
GET /api/admin/health/suppressions,POST /api/admin/health/suppress{ "key": "bitcoin_pruned" },DELETE /api/admin/health/suppress{ "key": "bitcoin_pruned" }— all admin-authenticated.
The stable keys are bitcoin_ibd, bitcoin_few_peers,
bitcoin_no_inbound_listen, bitcoin_no_inbound_no_listen,
bitcoin_no_zmq_hashblock, bitcoin_pruned, bitcoin_verification_progress
and ipc_template_stale.
The suppression list lives in the database and is re-read on every health
tick, so changes take effect without restarting the daemon. Suppressed keys are
filtered out before the legacy warnings strings are built, so a suppressed
warning disappears from both fields. Only warnings that carry a key can be
suppressed.
IPC template-stale warning
When the pool takes its templates from Bitcoin Core’s IPC interface
([node] template_source = "ipc" — note that Core’s IPC interface is itself
experimental), the health loop additionally watches whether templates are
still arriving. If none has arrived for longer than 3 job-refresh intervals (at
least 90 s), it raises the suppressible ipc_template_stale warning — a
permanently dead socket (node downgraded below Core 31, IPC toggle off, mount
gone) would otherwise fail silently as “no new jobs”. It is deliberately only a
warning: there is no automatic fallback to GBT; the way back is setting
[node] template_source = "gbt" and restarting.
See also
- Notifications —
rpc-down/rpc-recoveredevents are fired by the same health loop - Observability —
warppool_rpc_ready+ RPC latency histogram - Troubleshooting — when the wizard shows “cookie not readable” or UPnP finds no gateway