Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 8fb1117dd7)
17 KiB
igneum-pool: the Igneum mining pool (pool-0, and the open pool)
A public mining pool for Igneum (docs/spec/09-pool-protocol.md, docs/plans/pool.md). It talks to one or more
igneumd nodes, builds a template per member with the member's own vote key in the header, hands out jobs over the
spec 09 protocol (newline JSON over TCP), sets a share target per member so each sends about one share every 10 s,
verifies every share on the CPU warp verifier the node uses, submits found blocks to every node, pays PPLNS from the
pool's coinbase address on the EVM side with a configurable fee (default 1%), and serves the stats API that
WhatToMine and MiningPoolStats read plus a page.
Built 5 October 2026 on branch pool-v0. Nothing is deployed. Measured on a private fast-time network
(docs/plans/pool.md section 5): three CPU miners of different identities, 0 rejected and 0 stale shares in the
second run, 289 blocks found and 0 orphaned at 49% of the network, PPLNS paid in dry run and then for real (24
transfers, 0 failed, the miners' EVM balances rose by what the ledger said), the API at 100 requests per second with
p99 under 3 ms, share checks 2.1 ms each under load (0.44 ms idle, the crate bench).
Build
The crate reads the node fork by path from ../vendor/igneum-node (the fork checkout, or a symlink to the fork
worktree that carries the pool-mode miner, branch pool-v0 of the fork) and ../igneum-pow from this repository.
cd pool
cargo build --release # target/release/igneum-pool, about 2 min warm on the Mac
cargo test --release # vardiff, PPLNS, share validation against the warp verifier, the API fixture
The miner with pool mode: cargo build --release -p igneum-miner in the fork worktree (branch pool-v0).
Run
igneum-pool --node grpc://127.0.0.1:26610 --evm-rpc http://127.0.0.1:26790 \
--listen 0.0.0.0:4463 --http 127.0.0.1:4480 --data-dir /var/lib/igneum-pool \
--network devnet --fee-percent 1 --min-payout 1 --pplns-window 2 --payout-interval-s 60 \
--name "Igneum pool" --public-url pool.example.net:4463 [--dry-run]
| Flag | Default | Meaning |
|---|---|---|
--node |
grpc://127.0.0.1:26610 |
gRPC of a node; repeat for more. The first builds templates and confirms blocks; every one receives found blocks |
--evm-rpc |
http://127.0.0.1:26790 (devnet), 26890 testnet, 8545 mainnet |
the first node's eth_ JSON-RPC, for payouts |
--listen |
0.0.0.0:4463 (devnet), 4462 testnet, 4461 mainnet |
where members connect (spec 9.3 ports) |
--http |
127.0.0.1:4480 |
the stats API and the page; put a TLS terminator in front |
--data-dir |
data |
state.json (the ledger snapshot) and the payout key |
--payout-key |
<data-dir>/payout-key.json |
the pool's secp256k1 key, created on first start, mode 0600 |
--fee-percent |
1 |
the pool fee, taken from each confirmed block's reward before the PPLNS split |
--min-payout |
1.0 IGN |
a member's balance is paid once it reaches this |
--pplns-window |
2.0 blocks |
the window, in blocks of expected work (shares weigh 2^-s) |
--payout-interval-s |
60 |
a payout round every so often, at most 16 transfers per round |
--dry-run |
off | compute and record payouts, send nothing |
--network |
devnet |
devnet, testnet or mainnet: chain id (4463, 4462, 4461), default ports |
--share-interval-s |
10 |
vardiff target: one share per member per interval |
--min-shift, --max-shift |
0, 60 |
bounds on the share shift; the saturation cap always applies |
--stale-grace-ms |
2000 |
a share on a superseded job inside the grace still pays |
--orphan-after-daa |
120 |
a pending block this far behind the virtual and not blue is an orphan |
--verify-threads |
2 |
share checks running at once on the CPU |
--tls-cert, --tls-key |
none | TLS 1.3 on the member port (spec 9.3) from a PEM chain and key |
--tls-self-signed |
off | TLS 1.3 from a self-signed pair made under the data dir on first start; the pool prints the certificate's pin, members pass it as --pool-pin |
--allow-plain |
off | plain TCP members on testnet or mainnet (the devnet allows plain by default; the public networks refuse to start in the clear without this) |
--alert-webhook |
none | Q72: an http:// URL POSTed once per address that sent shares before and none for ten minutes |
--open |
off | the open pool: the share sidechain with no operator (below) |
--p2p-listen, --peer |
0.0.0.0:<member port + 10>, none |
the open pool's share gossip: where to listen, who to connect to |
--chain-share-s, --window-shares, --open-dev-fee-percent, --open-chain, --chain-genesis-target64 |
10, 2160, 1, igneum-open-v1, the network's genesis block target eight times easier |
the share chain's constants; every member of one chain holds the same or its shares are refused |
At start the pool prints its payout address. That address is the pool's coinbase address: every template names it in
the IGNA field, so the execution layer credits it 80% of every blue block the pool finds. The 20% proving share goes
to the proving escrow by the chain's own rule (consensus/core/src/igneum.rs, igneum/exec/src/executor.rs); the
pool never touches it and cannot.
TLS on the member port (spec 9.3, 7 October 2026)
--tls-self-signed makes tls-cert.pem and tls-key.pem under the data directory on first start and prints the
certificate's pin (BLAKE2b("igneum-pool-cert-pin-v1" || DER), 64 hex); a member connects with
igneum-miner ... --pool host:4463 --pool-pin <pin>. A public pool with a certificate from a root the members'
systems trust runs --tls-cert chain.pem --tls-key key.pem, and members use --pool-tls. Over TLS every authorize
carries the binding: the member's BLS signature over this connection's TLS exporter (label
EXPORTER-igneum-pool-binding, context the chain id), so an authorize replayed on another connection is refused
(src/tls.rs, the test self_signed_tls_pins_exports_and_binds_one_connection). HiveOS: pools://host:4463 in the
Flight Sheet for --pool-tls, POOL_PIN=<pin> in the extra config for a pinned pool. Testnet and mainnet pools refuse to
start in the clear unless --allow-plain says so.
The open pool (--open, 7 October 2026; docs/plans/pool.md section 10; spec 09 section 9.12)
The same binary beside a member's own node, with no payout key: the member's miners connect to it on loopback (or a
rig's LAN), it builds their templates from the member's node with the member's key in the header and the member's own
address in the coinbase, stamps the share chain's parent (IGNS) and the window's split (IGNP) into every coinbase,
and gossips shares with the other members' daemons (--peer). A block found on the chain pays the window from its own
coinbase by the execution layer's split rule (pool_split_activation_daa in the node's override file; blocks below it
pay the finder alone). Nobody holds a balance; the only fee is the software dev fee as one entry of the split (the solo
miner's 1 percent).
igneum-pool --open --node grpc://127.0.0.1:26610 --listen 127.0.0.1:4463 --http 127.0.0.1:4480 --data-dir ~/.igneum-open \
--p2p-listen 0.0.0.0:4473 --peer other-member.example:4473 --network devnet
igneum-miner mine grpc://127.0.0.1:26610 1 100000000 me --pool 127.0.0.1:4463 --evm-address 0xYOURADDRESS --worker-name gpu0 --worker ./igneum-worker-cuda
Every share the daemon's own members find is one line of <data-dir>/shares.log, self-certifying; igneum-pool verify-share '<line>' [--block '<raw block json>'] re-hashes it and, against a block (its raw JSON from
/api/open/share/<hash>), says whether that block's split pays the share's address (BLOCK PAYS, exit 0; BLOCK DROPS,
exit 3). /api/open is the chain (height, tip, target, the window's split), /api/open/shares?from=&limit= the shares,
/api/payments the coinbase credits by block. The gate: pool/tools/open-gate.mjs (100 members, 10 daemons, 4 nodes on
a fast-time network; pool.md section 10.5 has the numbers).
What the operator must secure
- The payout key (
payout-key.json). It holds every reward the pool earns until the payout round sends it on. Back it up off the box before the first block. Anyone with the file can empty the pool. Keep the data directory at mode 0700, the file at 0600 (the pool sets 0600 on creation), and never put it in a repository or a log. - The pool's HTTP port behind a TLS terminator (Caddy or nginx) with the page on one hostname and the member port
(4463) reachable; v0 speaks plain TCP to members, so the terminator does not cover shares yet (TLS for the member
port is the v0 gap named in
docs/plans/pool.md). - The node's RPC (
--rpclisten) and EVM RPC (--evm-rpclisten) on loopback only. The pool talks to them locally. state.json: the ledger. It is rewritten every--snapshot-interval-s(15 s): balances, blocks, payments, the hashrate samples and the check costs (Q70: a restart keeps the rate tiles and the luck; the loss window is one interval), and the hourly history per address (7 days). A lost file loses unpaid balances since the last payout, so back it up with the key.
Deploy on one Hetzner box (the seed-node pattern)
The seed nodes (infra/seed-nodes/README.md) are Hetzner VMs with a fixed IPv4, igneumd as a systemd unit, RPC on
loopback, built by the cross-compile on the Mac (infra/cross/build-linux.sh). A pool box is the same plus this
binary and two more ports.
- Create the VM as a seed would be created:
SEED_NAME=igneum-pool-1 SEED_TYPE=cpx31 ./create-seed.shfrominfra/seed-nodes(the firewall script opens 22 and 26611; add 4463 and 443). The seeds run on cx23 (2 vCPU, 4 GB, USD 6.49 a month net plus the IPv4, approximate from the seed plan); a pool wants a core for the node, a core for share checks (one share per 10 s per member, 0.44 ms each on an M5 Max core, so a server core at roughly half that rate covers thousands of members, approximate) and 8 GB for the node plus the pool's 256 MiB cache per day seed. A cpx31 (4 vCPU, 8 GB) is about USD 14 a month net (approximate, price list not re-read tonight). - Provision the node exactly as
provision-seed.shdoes, with--utxoindexadded and--evm-rpclisten=127.0.0.1:26790(the pool needs the EVM RPC for payouts). The node must be the devnet-v4 line or later (it reportspow_epochwith every template; the pool refuses to run against one that does not). - Cross-compile
igneum-poolwith the samecargo-zigbuildtarget as the node (x86_64-unknown-linux-gnu.2.36) frompool/, copy it to/opt/igneum/bin/igneum-pool, and install the unit below. Re-stamp copied sources withtouchbefore any build on the far side (the stale-build rule in CLAUDE.md); nothing is built on the server. caddy(or nginx) in front of127.0.0.1:4480forhttps://pool.<domain>/; the member port 4463 open in the Hetzner firewall.- First start with
--dry-runfor a day, read/api/blocksand/api/payments, then restart without it.
# /etc/systemd/system/igneum-pool.service
[Unit]
Description=Igneum mining pool
After=network-online.target igneumd.service
Requires=igneumd.service
[Service]
User=igneum
EnvironmentFile=/etc/igneum/pool.env
ExecStart=/opt/igneum/bin/igneum-pool --node grpc://127.0.0.1:26610 --evm-rpc http://127.0.0.1:26790 \
--listen 0.0.0.0:4462 --http 127.0.0.1:4480 --data-dir /var/lib/igneum-pool --network testnet \
--fee-percent 1 --min-payout 1 --name "Igneum pool" --public-url pool.igneum.network:4462
Restart=always
RestartSec=5
UMask=0077
[Install]
WantedBy=multi-user.target
Costs, approximate: the VM (USD 6.49 to 14 a month net by type), the IPv4 (Hetzner's primary IPv4 price, a few dollars), outbound traffic inside Hetzner's included allowance at a few hundred members (a template is a few KB per member per second in mode A; 1,000 members at 10 KB/s each is 10 MB/s, which is where mode B or a template RPC that takes a list of keys comes in, O-9.4). Measure before quoting.
Connect a miner
igneum-miner mine grpc://127.0.0.1:26610 1 100000000 rig1 --pool pool.example.net:4463 \
--evm-address 0xYOURADDRESS --worker-name rig1-gpu0 --worker ./igneum-worker-cuda --worker-args "--device 0 --pack packs/devnet"
igneum-miner mine none 4 100000000 rig1 --pool pool.example.net:4463 --evm-address 0xYOURADDRESS --worker-name rig1-cpu
The first argument is the member's own node (its verifier: seed checks, voting with its own key, a second road for
found blocks) or none (hashes, does not vote). --evm-address is the payout address, --worker-name the label on the
page. HiveOS: Flight Sheet pool URL pool://pool.example.net:4463 (packaging/hive/README.md). In pool mode the miner
software takes no dev fee; the pool's fee is in the welcome line the miner prints.
The API
| Path | What |
|---|---|
/api/stats |
pool (hashrate, miners, workers, shares, blocks 24 h and total, last block, fee, min payout, luck, effort, paid, share check cost) and network (chain id, difficulty, hashrate, DAA, reward) |
/api/blocks?limit=N |
blocks found: hash, DAA, time, finder, status (pending, confirmed, orphan), reward, effort, the PPLNS split |
/api/miners/<address> |
hashrate (10 min, 1 h, reported), shares, workers (with shift, proving flag, online), balance, paid, payments |
/api/payments?limit=N |
every payout with its transaction hash and receipt status |
/api/pool-stats |
the flat camelCase object pool dashboards poll (hashrate, miners, workers, blocks, lastBlock, fee, minPayout) |
/metrics |
Prometheus text (Q72) |
/health |
200 when the node is synced and answered a template in the last 30 s, else 503 with the node's state (Q72) |
/api/open, /api/open/shares, /api/open/share/<hash> |
the open pool's share chain |
The page (Q69 to Q73): every number formatted as the site's are (en-GB separators, hash rate to one decimal on a kH to PH ladder, IGN to 4 decimals on tiles and 6 in tables), luck in MiningPoolStats' convention (expected over actual), the payments of a looked-up address under its workers with a 7-day hourly sparkline, the node's state in words, and the network's finality state beside the sentence "payouts follow blue confirmation, not finality".
The field lists are constants in src/api.rs; cargo test checks the fixtures in tests/fixtures/ against them.
Captured responses from the measured run are the fixtures.
Files
| File | What |
|---|---|
src/main.rs |
start-up, tasks, the status line, shutdown snapshot |
src/config.rs |
flags and defaults |
src/protocol.rs |
the spec 09 messages, newline JSON |
src/server.rs |
member connections: hello, authorize (proof of possession checked), shares, solutions, stats |
src/node.rs |
templates per member, jobs, block submission, the blue-set walk that confirms or orphans, network numbers, vardiff ticks |
src/verify.rs |
the share check on the warp verifier, the spec's codes |
src/vardiff.rs |
the share shift per member |
src/pplns.rs |
the window, the split, the distribution with the fee |
src/payout.rs |
the payout key, EIP-1559 transfers through eth_ JSON-RPC, receipts, dry run |
src/state.rs |
the ledger and its snapshot |
src/api.rs |
the HTTP server, the routes, the field contracts, /metrics, /health |
src/tls.rs |
TLS 1.3 on the member port, the self-signed pair, the exporter of the binding |
src/sidechain.rs |
the open pool's share chain: shares, the target, the window's split, fork choice, verify-share |
src/open.rs |
the open pool's daemon side: stamping templates, accepting shares, the shares log, the API |
src/p2p.rs |
the share gossip between members |
tools/open-gate.mjs |
the open pool's gate on a fast-time network |
web/index.html |
the page |
tools/measure.mjs |
the private-network measurement |
Building on igneum-build-1 (6 October 2026)
Every Linux build and suite runs on the box through tools/build-remote.sh from this directory (IGNEUM_AGENT=pool). The
crate reads the fork through the vendor/igneum-node symlink; the build library syncs the fork worktree it points at as a
whole repository, and the symlink is made once on the box by hand: ln -s <fork worktree name> /srv/builds/<worktree>/vendor/igneum-node,
with the line vendor/igneum-node in /srv/builds/<worktree>/.igneum-scratch-spare so the mirror's clean before every build
keeps it (7 October 2026: without the line the first box build of the pool crate removed it and cargo found no fork).
Artefacts land in pool/target-remote/release/igneum-pool (x86_64 Linux); the Mac builds no Linux binary.
Start-up contract (7 October 2026): the daemon exits 2 when --listen or --http cannot be bound, exits 3 when the node
answers no template with pow_epoch within --node-wait-secs (default 60), and prints node <url> answers templates: ...
before it accepts a member; a STATUS line every 30 s names the member count, jobs issued, shares and the node's state.