Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Bitcoin Core health — everything reachable over RPC (/api/bitcoin-health)
  2. 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

CheckWhat it doesPrivacy
Bitcoin health (RPC calls)Localhost → Bitcoin Core✅ Local
Connectivity snapshotTCP-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:

RPCWhat we extract
getnetworkinfoversion, subversion
getblockchaininfoibd, chain, blocks, headers, verification_progress, size_on_disk, pruned
getindexinfotxindex (on/off)
getpeerinfoinbound/outbound peer count
getzmqnotificationspubhashblock/pubrawblock endpoints

Plus automatic warnings:

ConditionWarning
ibd=trueMining not yet possible; the pool should only start after sync
peers.total < 8Tip latency increased; new blocks arrive later
peers.inbound = 0 on mainnetPort 8333 is probably not forwarded
zmq.hashblock=NonePool falls back to RPC polling (job refresh latency)
pruned=trueSome features (txindex-based) are limited
verification_progress < 99.9% (and not IBD)Still syncing

Why inbound peers matter for a pool. peers.inbound = 0 is 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 keep listen=1 on. 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:

  1. Reads the RPC + ZMQ settings — from your bitcoin.conf if a path is given, otherwise mainnet defaults (:8332) — and pre-fills the form.
  2. 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.
  3. When no bitcoin.conf is 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 to warppool_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 findingSnippet line
zmq.hashblock=Nonezmqpubhashblock=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 < 8maxconnections=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 true
  • bind= — the default all-interfaces fits 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) — via navigator.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 return add_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

  1. UI field “bitcoin.conf path” (optional, OS-aware placeholder)
  2. Path is sent along with the /api/bitcoin-health POST
  3. 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)
  4. 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:28333 and listen=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 suggest maxconnections=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:

  1. Re-resolve RPC auth from config.node.rpc_cookie_path (preferred) or secrets.rpc_user/rpc_pass (the cookie gets rotated on Bitcoin Core restart — we read fresh every time)
  2. warppool_health::check_bitcoin_health(&client, &url, &auth).await
  3. collect_bitcoin_warnings(&h)
  4. 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, /metrics covers 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

  • Notificationsrpc-down / rpc-recovered events are fired by the same health loop
  • Observabilitywarppool_rpc_ready + RPC latency histogram
  • Troubleshooting — when the wizard shows “cookie not readable” or UPnP finds no gateway