Architecture
dvb-WarpPool is a Rust workspace with 18 library crates + 6 binaries. A clear layering — from pure schema crates without async at the bottom up to the async-orchestrated binaries on top — keeps the system testable and lets sub-crates run standalone (e.g. the translator as a sidecar without the pool daemon).
Crate Overview
Schema + foundation (no async)
| Crate | What |
|---|---|
warppool-profiles | admin profiles — capacity hints + defaults. TOML values klein / mittel / gross / enterprise (Small ≤5 / Medium ≤20 / Large ≤100 / Enterprise >100 miners). These are recommendations for tuning threads, RAM target, retention and poll intervals — not hard caps; all four are hot-switchable at runtime |
warppool-config | TOML schema (PoolConfig + sub-configs + secrets) |
warppool-hwdetect | hardware detection via sysinfo → profile recommendation |
Core mining layer
| Crate | What |
|---|---|
warppool-bitcoin-rpc | JSON-RPC + ZMQ subscribe for Bitcoin Core |
warppool-bitcoin-ipc | opt-in Cap’n Proto mining client for Bitcoin Core >= 31 (low-latency template source; TemplateSource trait shared with the GBT path). Selected via [node].template_source = "ipc"; GBT stays the default, and Core’s own IPC mining interface is still experimental |
warppool-job-builder | coinbase tx (BIP34 height, SegWit commitment, BIP54-forward-compatible locktime) + merkle tree + Stratum job construction |
warppool-share-validator | share PoW check + block-found detection |
warppool-stratum-v1 | TCP listener, session state machine, VarDiff |
warppool-stratum-v2 | NOISE handshake, binary framing, mining subprotocol |
warppool-translator | V1↔V2 sidecar (a pool can offer Sv2 only; V1 miners connect via the translator) |
Storage + API
| Crate | What |
|---|---|
warppool-storage | SQLite via sqlx, all tables + migrations |
warppool-api | axum HTTP API (REST + SSE stream + static UI serving) |
Operational subsystems
| Crate | What |
|---|---|
warppool-health | Bitcoin Core multi-RPC health + bitcoin.conf parser + snippet generator |
warppool-autoupdate | version parser + forge release client + atomic_swap + cosign hook |
warppool-notifier | push sinks (ntfy/Telegram/Discord/Slack/email) + counter metrics |
warppool-telemetry | vendor API probes (AxeOS, NerdNOS, Avalon, …) + mDNS discovery + HTTP sweep + PoolMetrics |
warppool-uninstall | cross-platform uninstall plans (per-OS path tables, marker-gated Bitcoin Core targets) |
Tools
| Crate | What |
|---|---|
warppool-simulator | sim miners (vendor personas) + sim node + scenarios |
Binaries
| Binary | Purpose |
|---|---|
dvb-warppool-daemon | the pool — orchestrates all subsystems |
dvb-warppool-cli | operator tools (hash-password, token-create, set-profile, check-update, …) |
dvb-warppool-setup | first-run wizard (axum, embedded HTML) |
dvb-warppool-translator | V1→V2 sidecar (clap CLI, can run as a systemd service) |
dvb-warppool-sim | simulation runner (scenario list / run) |
dvb-warppool-mac-launcher | macOS app-bundle launcher (caffeinate wrapper, daemon supervision) |
Data Flow (happy path)
+---------------------+
| Bitcoin Core |
| (mainnet / regtest) |
+---------------------+
| RPC | ZMQ pub
v v
+---------------------+
| bitcoin-rpc |
| - getblocktemplate |
| - submitblock |
| - hashblock watch |
+---------------------+
|
v
+---------------------+
| job-builder |
| - coinbase splits |
| - merkle tree |
| - StratumJob |
+---------------------+
|
+-----------+-----------+
v v
+------------------+ +------------------+
| stratum-v1 | | stratum-v2 |
| TCP / TCP+TLS | | TCP+NOISE |
+------------------+ +------------------+
| |
v v
V1 miners (Bitaxe, V2 miners / translator
NerdNOS, AntminerS23)
| |
v v
submit (extranonce, nonce, ntime)
|
v
+---------------------+
| share-validator |
| - PoW check |
| - block detection |
+---------------------+
|
+-----------+-----------+
v v
+------------------+ +------------------+
| storage | | block-submit-loop|
| - record_share | | - assemble block|
| - vardiff_state | | - submitblock |
+------------------+ +------------------+
|
v on success
+---------------------+
| notifier |
| fire BlockFound |
+---------------------+
| | | | |
v v v v v
ntfy tg disc slack email
Daemon Task Topology
dvb-warppool-daemon is a Tokio multi-reactor with roughly a dozen
long-lived tasks plus per-connection tasks. All tasks share one
CancellationToken for graceful shutdown.
+------- main() async -------+
| |
+- spawn ---------+----------+
| |
+-------+-------+ +------------------+
| Stratum-V1 | Stratum-V2 |
| accept loop | accept loop |
| (+ TLS) | (+ NOISE) |
+----------------+ +-----------------+
| |
per connection: session task
| |
v v
+---------------------------------+
| block_found_tx broadcast |
+---------------------------------+
|
+-------+--------+
| block_submit_ |
| loop |
+----------------+
|
Bitcoin Core RPC submitblock
|
+-------+--------+
| notifier.notify(BlockFound) |
+- spawn ---------+----------+
| job_refresh_loop |
| - poll getblocktemplate (60s) |
| - ZMQ hashblock fast path |
| - drain & build job |
| - push to stratum handles |
+---------------------------------+
+- spawn ---------+----------+
| aggregate_loop (60s tick) |
| - storage.aggregate_5min |
| - SharesAccepted SSE event |
+---------------------------------+
+- spawn ---------+----------+
| health_check_loop (60s tick) |
| - check_bitcoin_health |
| - SSE HealthSnapshot |
| - RpcDown/Recovered notify |
+---------------------------------+
+- spawn ---------+----------+
| periodic_update_check (24h) |
| - Forge Releases API |
| - SSE UpdateAvailable |
+---------------------------------+
+- spawn ---------+----------+
| miner_poll_loop (vendor probes) |
+---------------------------------+
+- spawn ---------+----------+
| HTTP API (axum on :18334) |
| - REST endpoints |
| - SSE /api/events |
| - static UI from --ui-dir |
+---------------------------------+
(Later phases added further opt-in loops on the same pattern: UPnP lease renewal, the Anker-bridge supervisor, and the daily energy aggregation.)
Shared State
Tokio tasks share Arc<...> handles instead of global statics:
| Handle | Type | Used by |
|---|---|---|
notifier: Arc<Notifier> | shared via clone | block_submit_loop, health_check_loop, periodic_update_check, NotifierConnectionSink, API |
pool_metrics: Arc<PoolMetrics> | atomic counters | NotifierConnectionSink, BitcoinRpc, API /metrics handler |
event_bus: Arc<PoolEventBus> | broadcast::Sender | all subsystems publish; API → subscribers (SSE) |
storage: Arc<Storage> | sqlx pool | share recording, audit log, vardiff state, settings |
snapshot: Arc<RwLock<NetworkSnapshot>> | RwLock snapshot | job_refresh_loop writes; API reads |
profile_kind: Arc<RwLock<ProfileKind>> | hot-switchable | API admin route + display |
cancel: CancellationToken | propagation | all tasks (graceful shutdown) |
Storage Schema
Core tables from phases 1-15; the schema is created programmatically from a
bundled SCHEMA_SQL constant (CREATE TABLE IF NOT EXISTS …, run by
init_schema() in crates/storage/src/lib.rs) — there are no sqlx file
migrations. Later phases added the device tables
(miners, miner-telemetry raw/5min), the per-day energy history
(daily_aggregates) and goPool-import bookkeeping on the same pattern.
| Table | Phase | What |
|---|---|---|
workers | 1 | worker list (user, last_seen_at, shares_accepted/rejected, blocks_found) |
shares_raw | 1 | last hour of raw shares — basis for hashrate computation |
shares_agg_5min | 1 | aggregated 5-minute buckets — hashrate chart data |
blocks_found | 1 | block history (worker, height, hash, accepted, reject_reason) |
wallet_lifetime | 8 | per-wallet lifetime accumulator (sum_difficulty, expected_blocks, since) backing the odds panel’s expected_blocks/p_zero; written batched from the aggregate loop, watermarked by pool_settings.wallet_lifetime_last_rowid |
pool_settings | 2.5 | generic KV store (active_profile_kind, donation addresses, the v1.21.0 marquee banner text + expiry, etc.) |
vardiff_state | 2.5 | per-worker VarDiff snapshots (current_diff, ema, last_share_unix) |
audit_log | 3 | admin actions (actor, action, target, peer_ip, ok, details) |
api_tokens | 3.2 | persistent bearer tokens (token_hash, name, scope, last_used_at, revoked_at) |
admin_2fa | 3.3 | TOTP secrets per user (secret_base32, enabled) |
push_subscriptions | 1 | web-push subscriptions (PWA) |
Timestamp convention. DATETIME columns are written by SQLite itself
(CURRENT_TIMESTAMP → YYYY-MM-DD HH:MM:SS, space separator, UTC);
pool_settings string values are written by Rust as RFC3339 (T
separator). The two are not lexically comparable ('T' > ' '), so
cutoffs against DATETIME columns must be computed inside SQL
(datetime('now', '-N seconds')), never bound as Rust-formatted
strings — a v1.0.16 bug filtered out every active worker exactly this
way. The full rules live in the warppool-storage module docs.
The Sv2 Stack in Detail
Stratum V2 is a binary-framed protocol with a NOISE handshake in front. Two subprotocols matter in the pool context:
- Mining subprotocol (implemented) — channel-based, extranonce-aware, version rolling per BIP-320, per-channel set-target.
- Template Distribution Protocol (TDP; foundation in phase 7.6a,
wiring in 7.6b deferred) — replaces
getblocktemplatewith push-driven template updates straight from the Bitcoin node.
Standard vs. extended channels. Both are supported; pick by role.
Standard is header-only mining for end devices: the pool computes the
merkle root and ships ready-to-hash work — smaller frames, no merkle
work on the device. Extended hands the miner coinbase_prefix/suffix
plus the merkle path and full extranonce rolling, which is what proxies,
translators and farm aggregators need to manage their own search space
(our own translator opens an extended channel upstream for exactly that
reason). A single Bitaxe-class device gains nothing from extended —
nonce + version rolling is ample search space for its hash rate — and
pays with bigger jobs and per-job merkle work on the ESP32.
Recommendation: standard channel for solo devices, extended for
proxy/aggregation setups. VarDiff converges per connection either
way; a difficulty difference between the two modes is just convergence
state, not quality.
V1 miner ----TCP plain----> stratum-v1 server ----------+
|
V1 miner ----TCP+TLS------> stratum-v1 TLS server -------+
|
v
+------------------+
| job-builder |
| share-validator |
+------------------+
^
|
V2 miner ----TCP+NOISE----> stratum-v2 server (sv2_listen, +
reference deployment :3336)
|
V1 miner ----TCP--+ |
| |
v |
dvb-warppool-translator (sidecar) --TCP+NOISE--------+
- SetupConnection
- OpenExtendedMiningChannel with user_identity
- SubmitSharesExtended with miner extranonce
- receives NewExtendedMiningJob + SetNewPrevHash
- maps to V1 mining.notify + slushpool prev_hash + BIP-320 version rolling
The concrete wire format lives in crates/stratum-v2/src/messages.rs with
roundtrip tests. The NOISE handshake uses the SRI noise_sv2 stack
(secp256k1 + EllSwift + Schnorr-signed certificates, spec-conformant since
the Sv2 modernisation) — pure Rust, no OpenSSL.
Connection Lifecycle Hooks
Both Stratum servers expose an optional ConnectionSink trait. For
authenticated workers (V1: mining.authorize, V2: OpenChannel with
user_identity) on_authorized fires; on connection end on_disconnect.
The daemon implements a NotifierConnectionSink struct that implements
both traits. A per-worker debounce (default 30s) keeps flapping miners
from firing a notification on every reconnect. The same hooks feed the
live worker sets (TLS/Sv2 badges) and the peer-IP refcount map used by the
energy accounting (“Disconnected” devices are excluded from watts/kWh).
v1.Session::run
|
handle_authorize → connection_sink.on_authorized
|
... shares ...
|
loop exit → connection_sink.on_disconnect
v2.handle_connection
|
process_frame → channels().iter() → new user_identity?
| → on_authorized
... shares ...
|
loop exit → for each notified_user → on_disconnect
Event Bus + SSE
PoolEventBus is a tokio broadcast channel with PoolEvent variants:
| Event | Source |
|---|---|
BlockFound | block_submit_loop |
NewJob | job_refresh_loop |
SharesAccepted | aggregate_loop |
HealthSnapshot | health_check_loop (phase 13b) |
UpdateAvailable | periodic_update_check (phase 8e) |
ProfileSwitched | API admin profile hot-switch |
Heartbeat | SSE keepalive with the current subscriber count |
The API’s /api/events opens one SSE stream per client with the current
subscription set. The UI uses it for HealthBanner + UpdateBanner +
toast events.
Configuration Layers
/etc/dvb-warppool/
├── config.toml # non-sensitive settings
├── secrets.toml # chmod 600 — admin hash, jwt secret, rpc pass, sv2 key
└── pool.db # SQLite (path configurable)
Env vars for opt-in subsystems (see the
Configuration Reference):
WARPPOOL_HEALTH_CHECK_INTERVAL_SECONDS, WARPPOOL_AUTOUPDATE_REPO,
WARPPOOL_DISCONNECT_DEBOUNCE_SECS, sink-specific tokens, etc.
Lifecycle (daemon boot)
- Parse CLI (clap) + ENV
- Load
config.toml+ (optional)secrets.toml - Validate config (
mining.payout_addressrequired,ratelimitconstraints, …) - Init tracing subscriber (JSON when
logging.json) - Open SQLite + run migrations
- Resolve admin profile (persisted value in
pool_settingsbeats the config default) - Construct
BitcoinRpcwithwith_metrics(pool_metrics) - Probe RPC (warning on failure; the daemon still starts)
- Construct
Notifierfrom config - Construct
PoolMetrics(Arc-shared) - Build the initial job (or skip when RPC failed)
- Spawn the Stratum V1 listener (+ TLS listener when cert/key configured)
- Spawn the Stratum V2 listener when
sv2_listenis configured (the authority key is auto-generated intosecrets.tomlif missing) - Spawn job_refresh_loop (poll + optional ZMQ)
- Spawn block_submit_loop
- Spawn aggregate_loop (60s)
- Spawn health_check_loop when
WARPPOOL_HEALTH_CHECK_INTERVAL_SECONDS > 0 - Spawn periodic_update_check when
WARPPOOL_AUTOUPDATE_REPO+ interval > 0 - Spawn the HTTP API (axum on
status_listen) - Install signal handlers (SIGTERM → cancel.cancel() → all tasks shut down)
- The
tokio::mainevent loop runs until cancel
Crate Dependencies (DAG)
Simplified crate graph (not every edge; no cyclic deps):
profiles --+
hwdetect --+--> config
|
+--> storage
+--> bitcoin-rpc --+
+--> bitcoin-ipc |
+--> job-builder |
+--> share-validator
+--> stratum-v1 -----+
+--> stratum-v2 -----+
+--> translator -----+
+--> telemetry -----+
+--> notifier ------+
+--> autoupdate ----+
+--> health --------+
+--> uninstall -----+
+--> api -----------+
+--> simulator -----+
|
v
dvb-warppool-daemon
dvb-warppool-cli
dvb-warppool-setup
dvb-warppool-translator
dvb-warppool-sim
dvb-warppool-mac-launcher
Crates and their one-directional deps:
telemetry← none (foundation for metrics)bitcoin-rpc→telemetry(for the RPC latency histogram, phase 16.3)api→notifier+autoupdate+telemetry+storage+ …daemon→ all library crates plus runtime deps (tokio, sqlx, …)
Testing Strategy
Three layers, plus one extra operator-driven layer:
| Layer | What | Tool |
|---|---|---|
| Unit | pure logic per crate | cargo test -p warppool-<crate> |
| Integration | multiple crates in-process, axum mock servers | tests/ per crate |
| Sim | against the real daemon process, vendor personas | dvb-warppool-sim scenario <name> |
| Regtest E2E | against a real bitcoind regtest | scripts/regtest-up.sh + --ignored (4 tests, opt-in) |
As of v1.24.0: 1201 tests + 4 ignored, all green. UI side:
pnpm svelte-check → 374 files, 0 errors.
See TESTING.md for details + the simulator workflow.