Skip to content

dvb-projekt/SoloPool

BlackHole

A self-hosted Bitcoin solo mining pool designed for Umbrel home nodes. Zero fee — 100% of every block reward goes directly to the miner's own address.

ScreenShot ScreenShot ScreenShot


Architecture

Component Technology Port
Stratum server Rust + Tokio 2018
REST API Axum 8081 (host)
Dashboard React + Nginx 3334 (host)
Bitcoin node Bitcoin Core via RPC + ZMQ (external)
Database SQLite (Docker volume)

Quick Start — One Command

Umbrel Home

git clone https://github.com/BlackHole-Axe/BlackHolePool.git
cd BlackHolePool
bash setup-blackhole.sh

The installer auto-detects your Bitcoin Core container, reads RPC credentials and ZMQ settings, writes env/.env, builds, and starts everything.

Standard Linux / macOS (any Bitcoin Core node)

git clone https://github.com/BlackHole-Axe/BlackHolePool.git
cd BlackHolePool
bash setup-blackhole.sh

If Bitcoin Core is running locally (native or Docker) the installer finds it automatically. For a remote node it will ask for the IP and credentials.

Non-interactive / CI

bash setup-blackhole.sh --unattended
# Then edit env/.env and run: bash run.sh

What the installer does

Step Action
1 Checks Docker + Docker Compose
2 Detects Umbrel / standalone environment
3 Finds Bitcoin Core (Docker container or native)
4 Reads RPC credentials from bitcoin.conf / Umbrel .env
5 Tests RPC connectivity
6 Optionally adds ZMQ lines to bitcoin.conf and restarts Core
7 Asks for your payout Bitcoin address
8 Writes env/.env with all discovered values
9 Selects the correct docker-compose overlay for your platform
10 Builds pool + dashboard with git provenance
11 Starts services
12 Verifies health and prints a status report

Manual setup (if you prefer)

1. Clone the repo

git clone https://github.com/BlackHole-Axe/BlackHolePool.git
cd BlackHolePool

2. Create your config file

cp env/.env.example env/.env
# Edit env/.env — at minimum set RPC_URL, RPC_USER, RPC_PASS, PAYOUT_ADDRESS

3. Start the pool

Umbrel:

bash run.sh

Standalone:

docker compose -f docker-compose.yml -f docker-compose.standalone.yml up -d

4. Check logs

docker compose logs -f blackhole-pool

You should see:

INFO  stratum listening on 0.0.0.0:2018
INFO  ZMQ block connected: tcp://...
INFO  new template: height=... txs=... ...

5. Connect your miner

Configure your miner with:

Field Value
URL stratum+tcp://YOUR_POOL_HOST:2018
Username YOUR_BITCOIN_ADDRESS (e.g. bc1q...)
Password (anything — leave blank or type x)

Important: Set your Bitcoin address as the username. The pool automatically uses it as the coinbase payout address, so the block reward goes directly to your wallet. If the username is not a valid Bitcoin address, the pool falls back to the PAYOUT_ADDRESS set in env/.env.


Configuration (env/.env)

All settings live in env/.env. Copy env/.env.example to env/.env as shown above.

Finding your Bitcoin Core RPC credentials (Umbrel)

  1. Open your Umbrel dashboard → Bitcoin Node app
  2. Go to Connect or Advanced tab
  3. Or read them directly from your node:
# SSH into Umbrel, then:
cat ~/umbrel/app-data/bitcoin/data/bitcoin/bitcoin.conf | grep -E "rpc|zmq"
.env variable Where to find it
RPC_URL http://<umbrel-ip>:8332 — on Umbrel the internal Docker IP is 10.21.21.8
RPC_USER Usually umbrel
RPC_PASS The long base64 string in bitcoin.conf under rpcpassword=
ZMQ_BLOCKS tcp://<umbrel-ip>:28334
ZMQ_TXS tcp://<umbrel-ip>:28336

Enabling ZMQ on Bitcoin Core

ZMQ is required for low-latency block notifications (reduces stale shares). On Umbrel it is enabled by default. On a custom node, add to bitcoin.conf:

zmqpubhashblock=tcp://0.0.0.0:28334
zmqpubhashtx=tcp://0.0.0.0:28336

Then restart Bitcoin Core.

Network (Umbrel Docker)

The pool runs inside umbrel_main_network (the same Docker network as Umbrel's Bitcoin Core container). This is why the RPC/ZMQ IPs use 10.21.21.8 — that is Bitcoin Core's fixed IP inside the Umbrel network.

If you run outside Umbrel, change RPC_URL and ZMQ_* to your node's actual IP.

Payout address

The recommended approach is to set the miner's username to a Bitcoin address. The pool then uses that address automatically — no .env change needed per miner.

The PAYOUT_ADDRESS in .env is the fallback used when the miner's username is not a recognisable Bitcoin address (e.g. a plain worker name like rig1).

Supported address formats:

  • Bech32 (P2WPKH / P2WSH): bc1q... / bc1p...
  • P2SH: 3...
  • P2PKH (legacy): 1...

Dashboard

Open http://YOUR_POOL_HOST:3334 in a browser to see:

  • Live hashrate chart
  • Connected miners table
  • Shares accepted / rejected
  • Found blocks log
  • Network difficulty & estimated time to block

API

The REST API is available on port 8081:

Endpoint Description
GET /health Health check
GET /pool Pool stats (fee, connected miners, hashrate)
GET /miners Per-worker stats
GET /hashrate Hashrate history
GET /blocks Found blocks
GET /network Network difficulty, block height
GET /metrics Internal counters (ZMQ, jobs, stales…)

Example:

curl http://localhost:8081/pool | python3 -m json.tool

Resetting the database

To wipe all share/block data and restart clean:

bash reset-pool.sh

Run this from the BlackHole directory. You may need sudo depending on Docker volume permissions.


Troubleshooting

Pool starts but shows "initial template refresh failed"

Bitcoin Core is not reachable. Check:

  • RPC_URL, RPC_USER, RPC_PASS in env/.env
  • Bitcoin Core is fully synced
  • Firewall allows port 8332 from the pool container

Lots of stale shares

  • Verify ZMQ is enabled and ZMQ_BLOCKS points to the correct IP/port
  • docker compose logs pool | grep ZMQ should show "ZMQ block connected"

Miner connects but no jobs are sent

  • Check that mining.authorize succeeds in the pool logs
  • Verify the Stratum port (2018) is reachable from the miner

Block found but rejected

  • Check docker compose logs pool | grep SUBMIT for the rejection reason
  • Common causes: stale block (ZMQ latency), nTime out of range, witness commitment mismatch

Project structure

BlackHole/
├── docker-compose.yml        — Compose service definitions
├── env/
│   ├── .env                  — Your config (gitignored)
│   └── .env.example          — Template with instructions
├── pool/                     — Rust backend (Stratum + API)
│   ├── Dockerfile
│   └── src/
│       ├── main.rs
│       ├── config.rs         — Environment variable parsing
│       ├── stratum/mod.rs    — Stratum v1 server
│       ├── template/mod.rs   — GBT fetching + coinbase building
│       ├── share/mod.rs      — Share validation
│       ├── api/mod.rs        — REST API (Axum)
│       ├── metrics.rs        — In-memory stats
│       ├── vardiff.rs        — Auto difficulty adjustment
│       ├── rpc.rs            — Bitcoin Core RPC client
│       └── storage/          — SQLite + Redis (optional)
├── dashboard/                — React frontend
│   ├── Dockerfile
│   ├── nginx.conf
│   └── src/
└── reset-pool.sh             — Wipe DB and restart

Security notes

  • AUTH_TOKEN is empty by default — any miner that can reach port 2018 can connect. Set it in env/.env if you want password-protected access.
  • Set REQUIRE_AUTH_TOKEN=true to make the pool refuse to start if AUTH_TOKEN is not set — useful as a safety net in network-facing deployments.
  • The pool is designed for a personal home node. Do not expose port 2018 to the internet unless you understand the implications.
  • Never commit env/.env to version control — it is listed in .gitignore.

Operational tuning reference

Bitcoin Core version

Recommended: v30.2.0 or later.

Measured GBT tail latency (p95):

  • v30.0.0: ~80ms (occasionally spikes to 565ms)
  • v30.2.0: ~50ms (more consistent)

On Umbrel: upgrade via the Umbrel UI → Bitcoin Node → Update.

PERSIST_SHARES

Value Behaviour When to use
false (default) No share rows written; worker_best still persisted High hashrate, NVMe not a concern
true Every accepted share stored in SQLite Forensic audit trail, historical analysis

Disk budget: ~200 bytes/share → ~300 MB per million shares (~69 h at 240 TH/s).

ZMQ_DEBOUNCE_MS

Controls how often ZMQ TX events trigger a getblocktemplate call. The dedup layer absorbs ~86% of responses as identical, so miner job rate is largely independent of this value. The main effect is Bitcoin Core CPU load.

Value GBT calls/min Core data Use case
1000 ~66 ~3.2 MB/s Maximum fee freshness
1500 ~44 ~2.1 MB/s Balanced (recommended for Umbrel Home)
3000 ~22 ~1.1 MB/s Reduce Core CPU, minimal freshness cost

POST_BLOCK_SUPPRESS_MS

After a new block, ZMQ TX events are suppressed for this many ms to avoid hammering Bitcoin Core while it processes the new mempool burst. Wire-measured: mempool stabilises 10–12 s after a block. Default: 12000 ms. Increase to 15000–20000 if Core CPU spikes after blocks.

Timing synchronisation rule

For a perfectly coordinated job pipeline, keep these equal:

NOTIFY_BUCKET_REFILL_MS = ZMQ_DEBOUNCE_MS

This ensures every new template from Bitcoin Core finds a token ready in the per-miner bucket — no artificial delays, no wasted refills.


API Reference

The REST API is available on port 8081 (host-mapped from container port 8080).

Endpoint Description
GET /health Health check {"ok":true}
GET /pool Full pool stats (hashrate, miners, blocks, counters)
GET /miners Per-worker stats (hashrate, shares, best diff, RTT)
GET /blackhole/miners Enriched miner list with firmware, session info
GET /blackhole/connection-status RPC + ZMQ + Stratum live connectivity
GET /blackhole/template-info Current block template (height, txs, target)
GET /blackhole/mempool Live mempool info from Bitcoin Core
GET /blocks Found blocks history
GET /build-info Build provenance (git SHA, timestamp, image ID)

Quick check:

curl http://localhost:8081/pool | python3 -m json.tool
curl http://localhost:8081/blackhole/connection-status | python3 -m json.tool

Monitoring commands

# Live pool logs
docker compose logs -f blackhole-pool

# Container health
docker ps --filter "name=blackhole"

# Pool stats snapshot
curl -s http://localhost:8081/pool | python3 -m json.tool

# Miner list
curl -s http://localhost:8081/blackhole/miners | python3 -m json.tool

# Connection status (RPC + ZMQ + Stratum)
curl -s http://localhost:8081/blackhole/connection-status | python3 -m json.tool

# Check for errors in logs
docker logs blackhole-blackhole-pool-1 2>&1 | grep -E "ERROR|WARN|BLOCK"

Releases

No releases published

Packages

 
 
 

Contributors