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

Public 0% Solo Pool — Per-Worker Setup & Hardening

This guide turns dvb-WarpPool into a public, non-custodial, 0%-fee solo pool: any miner who connects mines to their own BTC address (taken from the Stratum login), and the full block reward goes straight to the finder on-chain. The pool never holds funds — there is nothing to withdraw, nothing to trust. It is the natural next tier above the friends-only pool in Scaling Tiers.

How it differs from single mode: in the default single mode the whole reward pays one operator address (Solo + Friends). In per_worker mode every miner supplies its own address as the Stratum username, and the coinbase is built per-connection to pay exactly that address — one output, full subsidy, no fee. See the design notes in docs/design/per-worker-coinbase.md.

Honesty: WarpPool has not yet found a mainnet block. Advertise the pool as what it is — a fresh, open, non-custodial solo pool — never as “battle-tested”. The consensus path (per-worker coinbase → real Bitcoin Core accepts the block → reward pays the finder) is verified end-to-end on regtest, but the first mainnet block is still “not yet”.

The project runs one such pool itself — pool.warppool.org, per-worker, 0% fee, non-custodial, currently around a dozen miners. Where this guide says “the reference deployment”, that is the box it means. It has not found a mainnet block either.


Contents

  1. Turn on per-worker mode
  2. What miners enter
  3. Public hardening profile
  4. TLS
  5. Behind a reverse proxy
  6. Deployment checklist

1. Turn on per-worker mode

In config.toml:

[mining]
payout_mode = "per_worker"
# payout_address is IGNORED in per_worker mode (each miner brings its own).
# pool_fee_percent / operator_donation_percent MUST be 0 — the daemon
# refuses to start otherwise (full reward goes to the finder).

That single flag is the whole feature switch. The daemon fails closed on startup if a fee or donation is configured together with per_worker, so you cannot accidentally run a public pool that skims the reward.

Setup wizard shortcut. If you install through the setup wizard (dvb-warppool-setup), the Mining card offers a Pool type choice — Private solo pool vs Public 0% pool. Picking Public 0% pool writes payout_mode = "per_worker" for you, hides the pool-wide payout address (it is unused here), and forces the fee to 0. No hand-editing required.

An invalid or wrong-network login address is rejected at mining.authorize (V1) / channel-open (Sv2) — there is no fallback to an operator address, so a typo never silently pays the wrong person.

2. What miners enter

Miners set their worker/username to their own address, optionally with a rig suffix after a dot:

Username:  bc1qyour-own-address.rig1
Password:  x
  • Everything before the first . is the payout address; the rest is a free rig label used only for stats.
  • Supported address types: P2PKH, P2SH, P2WPKH, P2WSH, P2TR (Taproot). Any valid mainnet address works.

Public wallet pages & the optional owner panel

Every miner gets a login-free per-wallet page at /users/<address> — the same “look up my address” flow a public solo pool is expected to have. It shows that address’s aggregate hashrate (1 m / 5 m / 1 h / 1 d / 7 d), a per-rig breakdown, lifetime accepted shares, best-ever share difficulty, and the first-seen time. No account, no operator involvement — anyone with the address can open it, and a miner can bookmark their own.

The same numbers are served as ckpool-compatible JSON at GET /api/users/<address> — the exact field names (hashrate1m, hashrate1hr, bestshare, worker[], …) that firmware displays and stats tools like ckstats already parse. Both routes are plain HTTP under the status port, so they sit behind the same reverse proxy as the dashboard (Reverse Proxy).

By default a wallet page is public. A miner who wants more control can prove ownership of the address and unlock an opt-in owner panel — entirely per-miner, nothing the operator has to configure:

  1. The page hands out a short one-time challenge message.
  2. The miner signs it in their own wallet’s Sign Message box (BIP-137 signed-message — the standard “verify address” feature in Electrum, Sparrow, Bitcoin Core, hardware wallets). No private key leaves the wallet, no funds move, no account is created — the pool only checks the signature.
  3. A valid signature sets a short-lived owner cookie for that address and reveals two controls:
    • Privacy — hide the address from the public wallet list and blank its /users/<address> data for everyone but the verified owner.
    • Notifications — point a personal ntfy topic or webhook at this address’s milestones: block-found (this address found a block), personal-best (a new best-ever share across all its rigs), top-100 (first entry into the pool’s all-time top-100 best shares) and rig-offline (one of its rigs dropped). Each event type has its own cooldown, and a found block sends a single headline instead of overlapping alerts.

Ownership signing covers single-key P2PKH / P2WPKH / P2SH-P2WPKH addresses; Taproot (P2TR) can mine but cannot use the owner panel, because the legacy signed-message format has no Taproot variant. Full walk-through in Notifications → Per-wallet miner notifications; the owner panel needs no operator config — see the Configuration Reference note.

Operator-wide directory & leaderboard privacy

The per-miner owner panel above is opt-in and only the miner controls it. As the operator you can additionally set the default exposure of the whole UI, to match the convention of established solo pools (solo.ckpool.org publishes no browsable directory; AtlasPool masks leaderboard addresses). Both default to the open behaviour, so this is opt-in:

[privacy]
# Hide the browsable Workers/wallet directory from anonymous visitors. Each
# /users/<address> page still works by direct link; the logged-in operator still
# sees the full directory (so keep admin auth enabled).
wallet_directory_public = false
# Mask the Bitcoin address in the public best-shares leaderboard (bc1qsx…dvng),
# server-side. The logged-in operator still sees full addresses.
leaderboard_addresses_public = false

This is the recommended posture for a public mainnet pool: miners reach their own page by link, but the pool doesn’t hand out an enumerable list of everyone’s address and rig setup. It does not touch payouts or block construction. See the Configuration Reference.

Accepting donations (optional)

Because a per-worker pool takes 0% of every reward, the only way to fund hosting is a voluntary donation. The dashboard has built-in support for this, and it appears only in per_worker mode — a private solo pool never shows it.

Set the addresses in the admin UI under Admin → Donations (the nav entry is hidden unless the pool runs in per-worker mode):

  • On-chain — any valid Bitcoin address. It is validated on save and shown as a bitcoin: QR the visitor’s wallet can open directly.
  • Lightning — an LN address / LNURL / BOLT12 string (stored as-is).

Either may be left blank to hide that option. When at least one is set, a Support this pool card with copy buttons and QR codes appears at the bottom of the public dashboard. The addresses are plain operator metadata — they never touch coinbase construction, so setting one can’t affect the fail-closed payout_mode invariant (a donation here is a visitor sending sats to a displayed address, not a share of the block reward).

Under the hood the addresses are stored as pool settings (not config.toml), so no restart is needed; POST /api/admin/donations is admin-authenticated and GET /api/donation-qr/{onchain,lightning} renders the QR SVGs.

3. Public hardening profile

A pool exposed to the internet needs the abuse controls a friends-only pool can skip. Every key below already exists; none is per-worker-specific — they apply to any public deployment. Full detail in the Configuration Reference.

[stratum]
# Bound how many simultaneous V1 connections one source IP may hold. On a
# public pool this stops a single host from hogging slots. Leave 0 only if you
# expect many miners behind one NAT.
max_connections_per_ip = 50

[ratelimit]
enabled = true
connects_per_sec = 5
connect_burst = 20
auths_per_sec = 1.0
auth_burst = 10

# Auto-ban IPs that keep tripping the connect limit (escalating). Loopback and
# private LAN are never banned.
autoban_enabled = true
autoban_strikes = 3
autoban_base_secs = 60
autoban_max_secs = 86400
autoban_window_secs = 600

# Ban a worker that floods invalid shares (share-flood defence). Off by
# default; turn on for a public pool.
reject_ban_enabled = true
reject_ban_min_samples = 100
reject_ban_ratio = 0.9

# Optional subnet access control. `ban_cidrs` denies ranges outright;
# `allow_only = true` flips to a strict whitelist (only allow_cidrs connect).
# ban_cidrs = ["203.0.113.0/24"]
# allow_only = false
# allow_cidrs = ["192.168.0.0/16"]

Optional invite-only variant — combine per-worker payouts with an address allowlist so only known addresses may mine:

[mining]
payout_mode = "per_worker"
address_allowlist = ["bc1qalice...", "bc1qbob..."]

Also switch on automated snapshots before you go public — a public pool’s stats and best-share history are worth keeping:

[backup]
enabled = true
interval_secs = 86400   # daily; the daemon clamps to >= 60
dir = "backups"         # relative paths resolve against the working directory
keep = 7                # 0 = keep everything

These are JSON ledger snapshots. They contain no secrets and no raw shares, so they are safe to copy off-box to ordinary storage.

Sizing profile

A public pool normally runs the gross profile (its defaults are tuned for roughly 21-100 workers): connection cap, VarDiff bounds, memory budget and retention in one bundle. The range in the name is guidance, not a limit — nothing rejects the 101st miner, and profiles hot-switch at runtime without a restart (Admin → Profile).

Resist the urge to move to enterprise the moment you pass 100 miners: its VarDiff defaults are calibrated for PH/s-class farms and will start far too high for Bitaxe/NerdMiner-class hardware, which is exactly what an open 0% pool attracts. Compare the profiles side by side in the admin UI first. See Scaling Tiers.

The node behind the pool

The pool can only mine on the tip it knows about, so the node’s own network position is an operational concern, not a detail:

  • Accept inbound connections. Forward TCP 8333 and keep listen=1 on. A node nobody can dial learns about each new block only through its own outbound peers, and every extra millisecond of tip latency is time your miners spend on a job that can no longer win. The health check warns when peers.inbound = 0 on mainnet — take that warning seriously here (Setup Health-Checks).
  • Pruned is fine. Solo mining needs no txindex; the reference deployment runs a pruned Bitcoin Core 31.1 with 8333 open at roughly 30 peers.
  • Consider a backup node. [[node.backup]] entries make a primary outage a non-event, and solved blocks are submitted to every configured node in parallel — see Resilience.

4. TLS

Firmware that only trusts public CAs needs a real certificate. The Solo + Friends guide walks through DuckDNS + Let’s Encrypt end-to-end; the relevant keys are:

[stratum]
stratum_tls_listen = ":3334"
tls_cert_path = "/etc/dvb-warppool/tls/cert.pem"
tls_key_path  = "/etc/dvb-warppool/tls/key.pem"

Leave the two paths unset to use the daemon’s auto-generated self-signed certificate (fine for NerdMiner/Bitaxe-class firmware that lets you pin it).

5. Behind a reverse proxy

When the status UI is served over HTTPS through nginx/Caddy:

[server]
status_public_url = "https://pool.example.com"
trust_proxy_headers = true    # so per-IP limits see the real client IP
cookie_secure = true          # UI is HTTPS
# status_tls_listen = ":18443"   # or terminate TLS at the proxy instead

See Reverse Proxy for ready-to-paste nginx/Caddy configs (TLS termination, X-Forwarded-For, and the /api/events SSE passthrough).

With trust_proxy_headers = true the daemon trusts X-Forwarded-For. Only enable it when a proxy you control actually sets that header — otherwise a client could spoof its IP past the per-IP limits.

6. Deployment checklist

  • payout_mode = "per_worker", no pool fee / donation (daemon starts).
  • Bitcoin Core reachable and inbound-reachable (TCP 8333 forwarded, listen=1) so tip changes arrive fast; health check shows no peers.inbound = 0 warning.
  • Optionally [node] template_source = "ipc" for lower template latency (needs Core ≥ 31 built with IPC; Core’s IPC interface is itself experimental — GBT stays the default and is the way back).
  • [backup] enabled = true with a keep count you’re happy with.
  • [ratelimit] enabled = true with autoban_enabled and reject_ban_enabled on.
  • max_connections_per_ip set (unless miners sit behind one NAT).
  • TLS in front of Stratum (own cert for public-CA-only firmware).
  • Reverse proxy: trust_proxy_headers + cookie_secure set correctly.
  • Test a miner with your-address.rig1; confirm it appears at /users/<address> and shares are accepted.
  • If you publish a landing page, keep the honesty guardrails: no “battle-tested”; mainnet block “not yet”.
  • Optional [legal] footer (terms/privacy links) for a public/commercial deployment.