A small, single-binary solo-mining stratum server in pure C11. It accepts
miner connections on TCP :3334, builds block templates via bitcoind's
getblocktemplate, submits found blocks via submitblock, and records every
accepted share into a local SQLite database. A separate Node.js dashboard
reads a snapshot of that file for stats.
It is a solo pool with direct payouts: every coinbase has two outputs —
the miner who found the block gets the reward (minus a small operator
fee), and the configured operator_address gets the rest (default 1% =
100 basis points, configurable via fee_bps). Each connected miner gets
its own coinbase rendered against the miner's own address; the merkle
branches, prev-hash, ntime, etc. are shared.
There is no PPS, no inter-miner reward sharing, and no
difficulty-weighted accounting. If your miner finds the block, your
address gets ~99% of the subsidy + fees on-chain in the same coinbase
transaction; if it doesn't, nobody on this proxy gets anything for that
height. The shares and workers tables exist purely so the dashboard
can show a leaderboard, per-worker drilldown, and historical "blocks
found by the pool" view.
The mining.authorize username must start with the miner's Bitcoin
address. Format:
<bitcoin_address>[.<rig_label>]
bitcoin_addressis required and must be a valid bech32 (P2WPKH) or base58check (P2PKH / P2SH) address. It is parsed at authorize time and an invalid address is rejected with a clear error and logged in therejectstable.rig_labelis optional and lets a single miner (same address) have multiple rigs in the leaderboard as separate rows. Use anything alphanumeric plus_-.- The password is discarded entirely. There are no accounts and no auth.
Examples: bc1qabc…, bc1qabc….basement-rig, bcrt1q…test.alice.
This is a sibling project to the Rust mining pool that lives elsewhere in
this same monorepo. The two share nothing in code or goals: the Rust pool is
a production-style PPS pool with payouts; simplepool is intentionally minimal
and exists for solo mining + observability only.
Status: wired. The main binary loads config, connects to bitcoind,
opens the SQLite store, builds an initial job from getblocktemplate,
serves stratum on the configured port, and watches for new tips on a
background thread.
Dependencies: sqlite3, libcurl, pthread, plus a C11 compiler.
macOS:
brew install sqlite curl
make
Debian / Ubuntu:
sudo apt install build-essential libsqlite3-dev libcurl4-openssl-dev
make
The binary lands at build/simplepool.
cp proxy.conf.example proxy.conf
# edit proxy.conf
./build/simplepool proxy.conf
Initialise the SQLite database from the shipped schema:
mkdir -p data
sqlite3 data/shares.db < schema.sql
The proxy is the only writer. The database lives at data/shares.db and
runs in WAL mode, so a read-only consumer cannot block writes or corrupt
the file.
For the dashboard we still recommend pointing it at a separate snapshot file rather than the live DB. This isolates the dashboard's query load from the proxy's writer and means a future code change on the dashboard side can never accidentally open the live file read-write.
Use SQLite's online backup — it is atomic and safe to run while the proxy
is writing. A plain cp of a WAL'd database is not safe; always use
.backup:
# one-shot
sqlite3 data/shares.db ".backup data/shares.snapshot.db"
Run it on a timer (cron / systemd-timer / launchd), e.g. every minute:
* * * * * sqlite3 /path/to/data/shares.db ".backup /path/to/data/shares.snapshot.db"
The dashboard reads data/shares.snapshot.db by default — see
dashboard/README.md.
operator_address = bc1q... # required: recipient of the fee_bps cut
fee_bps = 100 # 100 = 1%; valid range 0..1000 (max 10%)
coinbase_tag = /simplepool/ # short string baked into the coinbase scriptSig
fee_bps = 0 disables the fee output (single-payout coinbase, all to
the miner). If the computed fee would be below the relay dust threshold
(~546 sats) the operator output is dropped automatically and the miner
gets the full reward.
One accepted share = one row in the shares table, tagged with the
worker_id resolved from the (sanitized) stratum username — which now
encodes the miner's payout address. The workers row stores
payout_address separately so the dashboard can also roll up by
address across multiple rigs.
If a share also satisfies the network target, it is additionally
recorded in blocks_found with height, hash, finder_id,
finder_address, reward_sats (paid to the miner), and fee_sats
(paid to operator_address). The matching shares row has
is_block = 1 and the block hash.
The repo ships a best-effort integration test that exercises the proxy
end-to-end against a regtest bitcoind:
# bitcoind must already be running with -regtest, RPC on 127.0.0.1:18443,
# user/password "drivepool"/"drivepool" (or override with env vars).
chmod +x tests/test_integration.sh
./tests/test_integration.sh
The script:
- Skips with exit 0 if
bitcoin-cli,nc, orsqlite3are missing, or if no regtest node is reachable. - Mines 101 blocks if needed so templates are non-empty.
- Writes
tests/integration.proxy.confand starts./build/simplepoolon127.0.0.1:13334with the DB at/tmp/simplepool-int.db. - Sends a tiny
mining.subscribe/mining.authorize/ stalemining.submitsequence overnc, thenSIGINTs the proxy. - Asserts that
workershas at least one row,workers.payout_addressis populated, andrejectshas at least one row.
For the broader stack flow (Docker compose, Rust pool, dashboard) see
../docs/TESTING.md.
Makefile # build / clean / test / format / install
schema.sql # SQLite schema (WAL, 4 tables — workers, shares,
# rejects, blocks_found)
proxy.conf.example # key = value config
src/
main.c # entry point: config + bitcoind + store + stratum + tip watcher
config.{c,h} # tiny key=value config parser
coinbase.{c,h} # BIP34 coinbase tx builder; bech32 + base58check decoders
log.{c,h} # tiny pthread-safe stderr logger
share.{c,h} # share-validation math
sha256.{c,h} # vendored SHA-256
stratum.{c,h} # stratum v1 server
store.{c,h} # SQLite writer with batching
bitcoind.{c,h} # libcurl-based JSON-RPC client
cjson/ # vendored cJSON (MIT) — see src/cjson/README.md
include/ # public headers (empty for now)
tests/ # unit tests + integration shell script