The fleet agent's row from the 6 October night: the daemon ran 20 minutes as a process serving nothing while its node
answered no template and its member port had been held. Now main binds the member and API listeners before anything
else (server::bind_listener; a failure is "POOL NOT STARTED: cannot bind the members listener on <addr>: <os error>",
exit 2), probes the node for a template with pow_epoch, retried for --node-wait-secs (default 60) with a line per
attempt, else exit 3, and prints a STATUS line every 30 s (members, jobs issued, shares, blocks, template failures, the
node's state: ok, NODE NOT ANSWERING, NO TEMPLATE YET). fetch_job counts template failures and the last success.
Test server::bind_tests: a held port is refused with the address in the message, the freed port binds. Suite 23 of 23
on igneum-build-1. Protocol and API unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit
|
||
|---|---|---|
| .. | ||
| src | ||
| tests/fixtures | ||
| tools | ||
| web | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
igneum-pool: the Igneum mining pool, version 0
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 |
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.
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 15 s; 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) |
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 |
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.
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.