A decentralized Bitcoin mining pool in Go, inspired by the original p2pool. Every miner runs their own node and shares work over a peer-to-peer network — no central server, no trust required.
p2pool-go maintains a sharechain — a parallel blockchain of lower-difficulty "shares" that tracks each miner's contributions. When a share also meets Bitcoin's full network difficulty, it becomes a real Bitcoin block whose coinbase pays all recent contributors proportionally via PPLNS (Pay Per Last N Shares).
- Decentralized — No pool operator. Every miner runs their own node.
- Trustless — Payouts are enforced by sharechain consensus.
- Fair — PPLNS rewards discourage pool-hopping and reward consistent miners.
- Go 1.22+
- Bitcoin Core (bitcoind) with RPC enabled
Add to ~/.bitcoin/bitcoin.conf:
server=1
rpcuser=your_rpc_user
rpcpassword=your_rpc_password
rpcallowip=127.0.0.1Start and wait for sync:
bitcoind -daemon
bitcoin-cli getblockchaininfogit clone https://github.com/djkazic/p2pool-go.git
cd p2pool-go
make build
./build/p2pool \
-address bc1qyourmainnetaddress \
-rpc-user your_rpc_user \
-rpc-password your_rpc_passwordAny Stratum v1 miner works. Example with cpuminer:
cpuminer -a sha256d -o stratum+tcp://127.0.0.1:3333 -u worker1 -p xThe -u username is just a worker label — payout addresses are set on the node with -address, not the miner.
Navigate to http://localhost:3333 in your browser. The web UI is served on the same port as the stratum server.
Create a .env file:
PAYOUT_ADDRESS=bc1pks29cz7w4hj265aanmyp8uwrl9hzykhwf5hl5gf40hlvj5hcs2es0sgm7z
BITCOIN_RPC_HOST=your_bitcoind_ip
BITCOIN_RPC_USER=your_rpc_user
BITCOIN_RPC_PASSWORD=your_rpc_password
LOG_LEVEL=info
| Variable | Description |
|---|---|
PAYOUT_ADDRESS |
Your mainnet Bitcoin address (bc1...) |
BITCOIN_RPC_HOST |
IP/hostname of your bitcoind |
BITCOIN_RPC_USER |
bitcoind RPC username |
BITCOIN_RPC_PASSWORD |
bitcoind RPC password |
LOG_LEVEL |
Log verbosity: debug, info, warn, error (default: info) |
The container defaults to mainnet on RPC port 8332.
docker compose up --build -dSame as above — stratum at stratum+tcp://localhost:3333, dashboard at http://localhost:3333.
docker compose logs -f
docker compose downA real-time monitoring dashboard served directly on the stratum port (no extra HTTP port needed). HTTP and Stratum are multiplexed on the same TCP connection by peeking the first byte.
- Stat cards — Pool hashrate, local hashrate, connected miners, peers, shares, difficulty, estimated time to block, last block found
- Live graphs — Pool hashrate and share count history rendered on canvas (60-point rolling window)
- Recent shares table — Full untruncated hashes, clickable for details, golden highlight for shares that mined a Bitcoin block
- Share detail modal — Click any share hash to view full header fields (version, prev block hash, merkle root, bits, nonce, prev share hash, difficulty)
- PPLNS distribution — Bar chart showing each miner's proportional contribution
- Auto-refresh — Polls
/api/statusevery 5 seconds - Web UI — GitHub-inspired dark UI, responsive layout
| Endpoint | Description |
|---|---|
GET / |
Web dashboard |
GET /api/status |
Pool status JSON (2s cache) |
GET /api/share/{hash} |
Share details by hex hash |
GET /metrics |
Prometheus metrics |
Exposed at /metrics on the stratum port. Gauges are updated every 30 seconds; counters increment in real time.
Gauges:
p2pool_sharechain_height, p2pool_miners_connected, p2pool_peers_connected, p2pool_share_difficulty, p2pool_pool_hashrate, p2pool_local_hashrate, p2pool_uptime_seconds
Counters:
p2pool_stratum_shares_accepted_total, p2pool_stratum_shares_rejected_total, p2pool_blocks_found_total, p2pool_block_submissions_total{result="success|rejected|failed"}
- Stratum v1 protocol over newline-delimited JSON-RPC
- Variable difficulty (vardiff) — Adjusts per-miner difficulty targeting 10s between shares, retargets every 60s
- BIP 310 version rolling — Miners can roll bits 13–28 of the block version for extra nonce space
- Grace period — After a vardiff retarget, shares at the previous difficulty are still accepted
- Rate limiting — Per-session submit rate limiter prevents flooding
- Session limit — Max 1,000 concurrent miner sessions
- TCP keepalive — 30s keepalive probes detect dead connections
- Read timeout — 5-minute idle timeout per connection
- Difficulty adjustment — 72-share window (~36 min), max 4x step, window trimming excludes stale-difficulty outliers
- Heaviest-chain fork choice — Cumulative work determines the best tip; ties broken by lowest hash
- Validation — Timestamp bounds (±2 min of now, ±10 min of parent), PoW check, parent existence, address validation
- Pruning — Orphans pruned every 5 minutes; old shares beyond 2x PPLNS window removed
- Persistent storage — BoltDB-backed store (
sharechain.db) survives restarts - Events —
NewTip,NewBlock,Reorgevents drive job regeneration and logging
- Window — 8,640 shares (~3 days at 30s target)
- Weighted — Shares weighted by difficulty (higher-diff shares count more)
- Finder fee — 0.5% bonus to the miner whose share becomes a block
- Dust consolidation — Payouts below 546 satoshis are redistributed to avoid unspendable outputs
- Deterministic — Same window always produces identical payout sets
- libp2p with Noise encryption and Yamux multiplexing
- GossipSub for share propagation (
/p2pool/shares/1.0.0) - Discovery — mDNS for LAN, Kademlia DHT for WAN, static bootnodes
- Locator-based sync — New nodes sync via exponentially-spaced hash locators (like Bitcoin's
getheaders) - CBOR encoding — Compact binary serialization for all P2P messages
- Hardened — 30s stream deadlines, message size caps (1MB sync, 100KB coinbase), field length validation
On a LAN, nodes find each other automatically via mDNS. For WAN (internet) discovery, at least one public bootnode is needed:
-
Run a bootnode — Start p2pool on a server with a public IP (or port-forwarded port 9171). On startup it logs its multiaddr:
listening on {"addr": "/ip4/203.0.113.5/tcp/9171/p2p/12D3KooWABC..."} -
Connect other nodes — Pass that multiaddr to other nodes:
./build/p2pool -address tb1q... -bootnodes /ip4/203.0.113.5/tcp/9171/p2p/12D3KooWABC...
-
Multiple bootnodes — Comma-separate for redundancy:
-bootnodes /ip4/1.2.3.4/tcp/9171/p2p/12D3KooW...,/ip4/5.6.7.8/tcp/9171/p2p/12D3KooW...
Once connected to any peer, the Kademlia DHT propagates peer info so nodes discover each other transitively — the bootnode is only needed for the initial introduction.
- Retry with backoff — Transient RPC failures retry up to 3 times (1s → 2s → 4s)
- No retry on rejection — Explicit consensus rejections from bitcoind are not retried
- Merkle root verification — Pre-submission sanity check catches reconstruction bugs
- Full block reconstruction — 80-byte header + coinbase + all template transactions
| Flag | Default | Description |
|---|---|---|
-address |
(required) | Payout address (bech32: bc1... for mainnet, tb1... for testnet) |
-rpc-host |
127.0.0.1 |
bitcoind RPC host |
-rpc-port |
8332 |
bitcoind RPC port |
-rpc-user |
user |
bitcoind RPC username |
-rpc-password |
pass |
bitcoind RPC password |
-network |
mainnet |
Bitcoin network (mainnet, testnet3, regtest) |
-stratum-port |
3333 |
Stratum server port (also serves HTTP dashboard) |
-start-difficulty |
100000 |
Initial stratum difficulty (vardiff adjusts from here) |
-p2p-port |
9171 |
P2P listen port |
-bootnodes |
(none) | Comma-separated bootnode multiaddrs for WAN discovery |
-mdns |
true |
Enable mDNS LAN discovery |
-data-dir |
.p2pool |
Persistent data directory |
-log-level |
info |
Log level (debug, info, warn, error) |
Environment variables override flags (useful for containers):
| Variable | Overrides |
|---|---|
BITCOIN_RPC_HOST |
-rpc-host |
BITCOIN_RPC_USER |
-rpc-user |
BITCOIN_RPC_PASSWORD |
-rpc-password |
P2POOL_DATA_DIR |
-data-dir |
P2POOL_BOOTNODES |
-bootnodes |
LOG_LEVEL |
-log-level |
Defaults are configured for mainnet. For testnet:
./build/p2pool \
-address tb1qyourtestnetaddress \
-network testnet3 \
-rpc-port 18332 \
-rpc-user your_rpc_user \
-rpc-password your_rpc_password| Port | Protocol | Description |
|---|---|---|
| 3333 | TCP | Stratum v1 + HTTP dashboard + Prometheus metrics |
| 9171 | TCP | P2P network (libp2p) |
+-------------------+
| Bitcoin Core |
| (bitcoind) |
+---------+---------+
| JSON-RPC
| (getblocktemplate / submitblock)
+---------v---------+
Stratum | | libp2p
Miners <----------->| p2pool-go Node |<-----------> Other Nodes
(TCP:3333) | | (TCP:9171)
| Web Dashboard |
HTTP <---->| /api + /metrics |
(TCP:3333) +---------+---------+
|
+---------v---------+
| Sharechain |
| (BoltDB) |
+-------------------+
| Package | Description |
|---|---|
cmd/p2pool |
CLI entrypoint and flag parsing |
internal/node |
Central event loop coordinating all subsystems |
internal/sharechain |
Share storage, validation, difficulty adjustment, fork choice |
internal/pplns |
PPLNS payout calculation with finder fee and dust consolidation |
internal/stratum |
Stratum v1 TCP server, sessions, vardiff, version rolling |
internal/work |
Block template → Stratum job conversion, header reconstruction |
internal/p2p |
libp2p host, GossipSub, mDNS/DHT discovery, locator sync |
internal/bitcoin |
Bitcoin Core JSON-RPC client |
internal/web |
HTTP dashboard, JSON API, Prometheus metrics endpoint |
internal/metrics |
Prometheus gauge/counter definitions |
internal/types |
Share, ShareHeader, coinbase builder, payout types |
internal/config |
Configuration with defaults and validation |
pkg/util |
Double-SHA256, compact/target conversions, encoding helpers |
- Work generation — The node polls bitcoind for block templates, computes PPLNS payouts from the sharechain, builds a coinbase, and sends Stratum jobs to miners.
- Share submission — When a miner submits work, the node reconstructs and hashes the 80-byte block header, then checks against three thresholds:
- Stratum difficulty (per-miner, vardiff-adjusted) — accepted as valid work
- Sharechain difficulty (global, ~30s target) — added to sharechain and broadcast via P2P
- Bitcoin difficulty (full network) — submitted to bitcoind as a real block
- P2P propagation — Valid shares are broadcast via GossipSub. New peers sync the full sharechain using locator-based requests.
- Payouts — When a share meets Bitcoin's difficulty, its embedded coinbase transaction pays all miners in the PPLNS window proportionally.
make build # Build to build/p2pool
make test # Run tests
make test-race # Run with race detector
make test-verbose # Verbose test output
make vet # go vet
make fmt # go fmt
make clean # Remove build artifacts
make run ARGS="-address bc1q..." # Build and runMIT