22 KiB
Pool v0: what it does, what it does not, what was measured, and the listing checklist
5 October 2026, branch pool-v0 (main worktree ../igneum-wt-pool, fork worktree vendor/igneum-node-pool on the
fork's pool-v0 from release-0.3.6 a24ab01a). Josh's ask: a public mining pool with the stats API WhatToMine and
HiveOS read, "the biggest step toward being listable". Built against docs/spec/09-pool-protocol.md, the miner's
template subscription and worker protocol (igneum/miner/src/main.rs), the Hive package's local mode
(packaging/hive), ledger F10 and G6 (pools and votes), and docs/plans/explorer.md section 6. Nothing is deployed.
1. Rust, and why
Spec 9.3 fixes the member protocol as newline-delimited JSON over TLS, not the node's gRPC. The miner is Rust and
talks gRPC to its node; the pool must verify every share on the CPU warp verifier (spec 9.8 item 5), which is the
node's own IgneumEngine in consensus/pow/src/igneum.rs (same program, cache and init words as block validation),
and it must build templates and submit blocks over the node's gRPC. Both are Rust crates by path. A Node service
would have had to reimplement the verifier or shell out per share; the Rust pool calls it in process at 0.44 ms per
share (measured, section 5). JSON lines are three serde derives. So: Rust, pool/, its own Cargo workspace reading
the fork by path. The miner side is a module in the fork (igneum/miner/src/pool.rs), because the identity, voter,
template and worker code it reuses lives there.
2. What v0 does
| Area | v0 | Spec section |
|---|---|---|
| Transport | newline JSON over plain TCP on 4463 (devnet), 4462, 4461; one object per line, "t" names the type, unknown fields ignored, 4 MiB line cap |
9.3, 9.5 |
| Session | hello / welcome (version, chain id, pool address, modes, vote_mode: member, share_scheme with fee, window and min payout), authorize with the member's BLS pubkey and proof of possession (verified with verify_pop), payout address and worker name / authorized |
9.3, 9.5 |
| Templates | mode A, one template per member per tip (NewBlockTemplate subscription, 1 s refresh, on demand): the member's vote_key_hash in the header, its key reveal and the pool's IGNA payout address in the coinbase extra data; sent as the node's RpcRawBlock JSON so a member with a node can submit it there |
9.4, 9.4.1 items 1 and 2, 9.6 item 2 |
| Seeds | seeds on connection and on every change, from the template's pow_epoch (current and next epoch seed, boundary DAA, day, the schedule) |
9.9 |
| Jobs | job with prehash, target64, share_target64, a random 2^32 nonce space, the seed pair, clean on a tip change |
9.5 |
| Shares | share = a lane hash at or below share_target64; the pool evaluates the lane on the CPU and answers share_result with ok, stale (superseded job past the 2,000 ms grace), duplicate, above_target, wrong_hash, unknown_job; a share at or below target64 is a block, submitted to every node |
9.8 items 1, 5, 7 |
| Vardiff | per member, one share per 10 s, shift s with share_target = target64 << s never saturated; first correction sized from the measured rate (up to 8 steps), then one step per 30 s at most; idle members eased one step per 3 intervals |
9.8 items 2 and 4 (one deviation: the sized first step, below) |
| Weight | 2^-s of a block per share |
9.8 item 3 |
| Payment | PPLNS over a window of N blocks of weight (default 2); the split is snapshotted when a block is found and credited when it is confirmed blue (a chain block or in a chain block's mergeset blues, the set the execution layer pays); orphans pay nobody; fee (default 1%) off the top | 9.8 item 8 |
| Payouts | EIP-1559 transfers from the pool's coinbase address through the node's eth_ JSON-RPC, one per payee at or above the minimum, at most 16 per round, receipts checked, a failed receipt re-credits; --dry-run records and sends nothing |
design 1.1 (execution layer rewards) |
| The 20% | untouched: the execution layer credits the proving escrow by rule from consensus data (executor.rs); the pool's templates name only its own address and receive the 80% |
2.5, 5.3 |
| Votes | the member votes through its own node with its own key, as in solo mode; the pool holds no key and names each member's key in its header | 9.6, 9.7 items 1 and 2 |
| Member checks | own key in the header, the pool's address and own key reveal in the coinbase (refused with vote_key, payout, reveal); the pool's epoch seed against the member's node (seeds); a custodial pool (vote_mode not member) refused by default |
9.4.1 items 1 and 2, 9.9 item 2, 9.6 item 5 |
| Found blocks | the member submits to its own node and sends solution; the pool submits to every node |
9.2 |
| API | /api/stats, /api/blocks, /api/miners/<address>, /api/payments, /api/pool-stats (Hive-style flat object); field contracts in pool/src/api.rs, fixtures from the measured run |
explorer.md section 6 |
| Page | pool/web/index.html under the site's tokens: tiles, connect card with the exact miner flags, address lookup, blocks, payments |
|
| Persistence | state.json snapshot every 15 s and at shutdown (balances, blocks, payments, PPLNS window, lifetime counters) |
|
| Miner | `igneum-miner mine <grpc url | none> --pool host:port --evm-address <0x..> --worker-name : CPU threads or a GPU worker fed the share target; the CPU re-check of every GPU found before it is sent; STATUS lines with the Hive tokens (accepted=, rejected=, now=<MH/s>`) |
| HiveOS | Flight Sheet pool URL pool://host:4463, NODE=local or NODE=grpc://... for a verifier node, h-run.sh passes --pool and --worker-name |
packaging/hive |
| Dev fee | none in pool mode: the pool issues the templates, so the 1-in-100 template mechanism does not exist; the pool's fee is the only fee and is in welcome, on the page and in the Hive README (consequence C6, 5 October 2026) |
|
| Proving | the member's stats line carries proving: true when it also proves (--proving; the app sets it when its prover runs); the pool shows it per worker and the page says a proving worker gives about 4% of its rate to the prover (bench-log "proving v1") and that proving income never passes through the pool (C6) |
9.8 item 8 |
3. What v0 does not
| Gap | Why | What closes it |
|---|---|---|
| TLS on the member port | spec 9.3 requires TLS 1.3 because a hijacked line could feed a member a checkpoint hash to sign. v0 relays no votes and the member signs nothing it got from the pool (votes go through its own node), so the hijack has nothing to feed; the gap is confidentiality of shares and the binding field. Plain TCP tonight; rustls is in the fork's dependency tree |
a --tls-cert/--tls-key pair and the TLS exporter binding in authorize |
| Mode B (commitment) and mode C (declared templates) | the spec requires A and C of a conforming pool; C needs the member to build its own template with the pool's tag, and a node RPC that validates a template without submitting it (O-9.4) | O-9.4 first, then declare_template / template_ack / template_refused |
| Vote relay and carriage by the pool | 9.7 items 3 to 6; v0 members vote through their own node (road three of the three roads), so a member without a node does not vote | vote / vote_ack / votes_carried, aggregation per index (O-3.12), the carriage ratio on the page |
| Stratum compatibility | none, and the spec is not stratum-like: Stratum V1 is JSON-RPC with mining.subscribe, mining.notify, mining.submit over plain TCP and a job that carries a coinbase split and merkle branches; Igneum's protocol carries a full block, a 64-bit lane target and a BLS key, and its share is a lane of a 32-nonce group. A Stratum V1 bridge would have to hide the vote key, which is the thing 9.6 keeps with the member, so none is planned. Mining operating systems add custom miners by command line (Hive's custom-miner contract, packaging/hive), which is how this ships |
|
| Sample verification at high shifts | spec 9.8 item 5 allows sampling above sample_shift 8; v0 verifies every share (2,270 per second per core keeps it cheap at any member count the first pool will see) |
a sampling switch when a core is short |
| One template fetch per member per second | at 1,000 members that is 1,000 getBlockTemplate calls a second on the pool's node and 10 MB/s of mode A templates |
a template RPC that takes a list of keys, or mode B |
| A database | state.json and an in-memory ledger; a crash between snapshots loses up to 15 s of shares |
SQLite or Postgres when the first real pool runs |
| Custodial mode | vote_mode: pool is not offered (allowed by 9.6 item 5, refused by members by default) |
not planned |
| Vardiff first step | the spec moves the shift one step per set_target; v0's first correction after 8 shares or one interval moves up to 8 steps from the measured rate, because a 100 MH/s card at the initial 2^20-hash share target sends 100 shares a second and one step per 30 s would take 5 minutes to reach one per 10 s |
spec 9.10 row "Vardiff step" to say "after the first correction" |
| Luck | luck_24h is blocks found over block-weights of shares in the day; MiningPoolStats reads luck as expected over actual, and the field says which in the API source line |
|
| App setting | design only (section 7) |
4. How it was measured
pool/tools/measure.mjs under the run lock: one igneumd 0.3.6 (vendor/igneum-node-036/target-integration,
needs no change for the pool) on ports 30400 to 30403 (gRPC, p2p, JSON, EVM), network id igneum-devnet-3040, the
60x fast-time profile (infra/fast-time/override-60x.json) with real proof of work and genesis_bits 0x1e400000
(2^18 expected hashes per block, the CPU devnet value of the bench log), the pool on 30463 (members) and 30480 (API),
and three CPU miners of different identities, payout addresses and thread counts (4, 2, 2) pointed at the pool with
the node as their verifier (seed checks, votes). Ten minutes in dry run, then the same pool restarted without
--dry-run and a 0.1 IGN minimum for the real payout, then 100 requests per second against the API for 30 s.
Commands (the main checkout's lock script, from the pool worktree):
tools/lock/with-lock.sh build nice -n 19 cargo build --release -j 4 # pool/
tools/lock/with-lock.sh build nice -n 19 cargo build --release -j 4 -p igneum-miner # vendor/igneum-node-pool
tools/lock/with-lock.sh build nice -n 19 cargo test --release -j 4 # pool/ (unit tests)
tools/lock/with-lock.sh run node pool/tools/measure.mjs --secs 600 --payout-secs 240 --rps 100
5. Measured
Two runs, both 5 October 2026 on this Mac (M-series, 16 cores) while other agents' builds and test networks ran on
the same machine: every millisecond below is a number taken under load and says so. Counts, shares, blocks, payments
and balances are not disturbed by load. Summary JSON: <scratch>/measure/summary.json and measure2/summary.json.
Run 1: the pool was the whole network (three CPU miners, 8 threads in all, no other miner). Run 2: an 8-thread solo
CPU miner ("carrier") mined beside the pool so the pool held about half the hashrate and its blocks competed. Run 1
also found a bug: the member sends a share and then a solution for the same nonce (spec 9.5), and the pool counted
the solution as a duplicate share (604 "rejected" of 617 accepted, every one a solution); fixed before run 2 (a
solution whose nonce the share already credited is acknowledged, not counted), run 2 shows 0 rejected.
| Number | Run 1 (pool = network) | Run 2 (pool about half) | Source |
|---|---|---|---|
| Run length | 600 s dry run, 240 s real payout, 30 s API load | same | measure.mjs |
| Shares accepted / stale / rejected | 617 / 0 / 604 (the solution bug) | 317 / 0 / 0 | /api/stats pool.shares |
| Shares per minute, pool | 61.7 | 31.7 | accepted / 600 s |
| rig1 (4 threads): shares per minute, hashrate 10 min, final shift | 31.2, 86.9 kH/s, 0 | 15.8, 47.5 kH/s, 0 | /api/miners/0x11.. |
| rig2 (2 threads) | 15.4, 40.9 kH/s, 0 | 7.8, 20.4 kH/s, 1 | /api/miners/0x22.. |
| rig3 (2 threads) | 15.1, 40.3 kH/s, 0 | 8.1, 21.5 kH/s, 0 | /api/miners/0x33.. |
| Vardiff changes over 10 min | 0 | 17 (rig3 to shift 1 at 25 s and back at 55 s; rig2 to 1 at 3 min; all at 0 at the end) | pool log VARDIFF lines |
| Share check cost, ms (mean / p50 / p99 / max) | 2.018 / 2.054 / 2.334 / 5.333 (n 617, under load) | 2.094 / 2.096 / 2.241 / 5.785 (n 317, under load) | /api/stats pool.share_check_ms, Instant around hash_bound |
| Pool hashrate from shares | 168 kH/s | 89 kH/s (miners' own STATUS: 0.088 + 0.043 + 0.043 = 0.174 MH/s in run 1; 0.061 + 0.030 + 0.030 = 0.121 MH/s in run 2, which the carrier's 8 threads depressed) | /api/stats, miner STATUS now= |
| Blocks found / confirmed / orphaned | 604 / 604 / 0 | 289 / 288 / 0 (one pending at the end) | /api/stats pool.blocks_* |
| Orphan rate | 0 of 604 | 0 of 289 at 49.1% of the network's 589 blocks (the carrier found 288 in the same window, 444 over its run) | ORPHAN lines, /api/blocks |
| Pool's share of network blocks | 100% | 49.1% | pool.blocks_total / network.block_count |
| First block: found to confirmed | 3.7 s (found 21:20:24.953, confirmed 21:20:28.678, daa 3, effort 1.00, one payee at fraction 1) | /api/blocks first_confirmed_block |
|
| Reward per block credited | 2.5351 IGN (80% of the ramp's 3.169 IGN at day 0) | same | block_subsidy(daa, 1) x 80% |
| PPLNS in dry run | 60 payment records computed, nothing sent, balances kept: 792.14 / 365.30 / 360.01 IGN after 600 s | 59 records; 389.16 / 164.54 / 169.85 IGN | /api/payments dry_run, /api/miners balance_ign |
| Pool fee kept | 15.33 IGN of 1,532.77 (1.00%) | 7.31 of 733.39 (1.00%) | pool.pool_fee_total_ign |
| Real payout, 240 s: transfers sent / confirmed / failed | 24 / 18 / 0 (6 sent, receipt not yet read at the end) | 24 / 18 / 0 | /api/payments status from eth_getTransactionReceipt |
| Pool EVM balance before / after | 1,532.77 / 180.18 IGN | 733.39 / 111.12 IGN | eth_getBalance on the private node |
| Miners' EVM balances received | 1,026.10 / 491.09 / 473.22 IGN | 481.99 / 208.06 / 252.36 IGN | eth_getBalance before and after |
| API at 100 requests per second for 30 s | 3,001 sent, 3,001 ok, 0 errors, p50 0.78 ms, p90 0.97, p99 1.17, max 5.69 | 3,001 / 3,001 / 0, p50 0.85, p90 1.05, p99 2.76, max 11.98 | fetch() from Node 22 on the same machine, four endpoints round-robin |
| Member checks | 0 jobs refused, 0 CPU mismatches, 16 epoch seed changes followed (60-DAA epochs) by every miner, seed check against its own node passed each time | same | miner logs POOL SEEDS, STATUS refused= |
| Votes | every miner voted through its own node as in solo mode (VOTE lines in the miner logs); the pool held no key |
same | miner logs |
What is unusual about this network and why it does not change the reading: at genesis_bits 0x1e400000 a block is
2^18 expected hashes, so a share worth 2^20 hashes (the vardiff's initial size) is harder than a block; the shift sat
at 0 and every share was a block. That is why blocks found equals shares accepted less the few found by two miners on
the same tip. Vardiff still ran (17 changes in run 2 when the carrier slowed the rigs) and the saturation cap held.
On a GPU network (devnet difficulty 0x1d100000, about 2^28 hashes a block, and more as cards join) shares sit far
below blocks and the shift moves into its working range; that run needs a GPU worker and is unverified (section 8).
Consequences, by tier (the standing rule of 5 October 2026):
| Tier | What the numbers mean | What is done about it |
|---|---|---|
| A pool operator on one Hetzner box (cx23 2 vCPU or cpx31 4 vCPU) | 2.1 ms per share check under load on an M-series core (0.44 ms idle, bench-log) is one core for 480 to 2,270 shares a second, which at one share per 10 s per member is 4,800 to 22,700 members per core; the API at 100 rps costs under 3 ms p99, and WhatToMine polls once a minute. The cost that scales first is the template per member per second (section 3): at 1,000 members that is 1,000 getBlockTemplate calls a second on the box's node |
--verify-threads sizes the check pool; the template RPC that takes a list of keys is the planned fix before a thousand members |
| A home miner on one card (8, 12, 16, 24 or 32 GB, NVIDIA, AMD or Apple, any OS) | pool mode changes nothing on the card: the same worker, the same 256 MiB cache plus program, the pool's share target in the job's target field. Income: 80% of each block over the PPLNS window less 1%, paid when the balance passes 1 IGN; at 2.5 IGN a block (ramp day 0) a miner holding 1% of a pool that finds a block a minute is paid about every 40 minutes, and a miner at 0.01% of a large pool every 3 days. A miner that also proves sends about 4% fewer shares and the page says so | the --proving flag and the Proving column; --min-payout is the operator's lever for small miners |
| A rig (several cards) | one igneum-miner --pool per card, one vote key per rig (IDENTITIES is ignored in pool mode), the worker name labels each card on the page |
the Hive hooks' pool:// mode |
| A pool user without a node of their own | hashes and is paid, does not vote and does not check the pool's seeds (spec 9.7 item 2); their finality weight still accrues to their own key through the blocks the pool finds under it | NODE=local in the Flight Sheet runs the bundled node as the verifier |
| The network | pool concentration does not become vote concentration: every block carried the finding member's key, 3 keys under one payout address in both runs (F10, O-9.1) | the explorer's keys-per-payout-address count is the public check |
Harness note: run 1's node "answered" nine minutes after it started because the harness waited for igneum-miner watch 1
with an 8 s timeout, and watch 1 takes one sample, sleeps 10 s and then prints: every attempt was killed and the loop ran
out. Fixed in measure.mjs (15 s timeout). The same shape elsewhere: packaging/hive/h-run.sh runs watch 1 with no
timeout (fine); no other script in tools/ wraps watch in a timeout under 11 s (grep, 5 October 2026).
6. Listing checklist: WhatToMine's form against this pool
WhatToMine's "add a coin" form (as Josh read it on 5 October 2026 and docs/plans/explorer.md summarises: an
explorer or pool with an API, the block reward and block time sources, a source for total coins) and what
MiningPoolStats asks of a pool (hashrate, miners, workers, blocks with heights and times, fee, minimum payout, luck):
| They ask for | Where it is | State |
|---|---|---|
| Algorithm name | igneum (/api/stats pool.algorithm, network.name) |
ready |
| Network hashrate and difficulty | /api/stats network.hashrate, network.difficulty; also the site's /api/stats |
ready |
| Block reward and block time | /api/stats network.block_reward_ign, miner_reward_ign (80%), block_time_target_s 1; the site's /api/stats block_reward with the halving table |
ready |
| Total and circulating supply | the site's /api/supply |
ready (explorer branch) |
| An explorer | the site's /explorer, /block/<hash>, /address/<addr> |
ready (explorer branch) |
| A pool with an API | this pool: /api/stats, /api/blocks, /api/miners/<address>, /api/payments, /api/pool-stats |
ready, not deployed |
| Pool hashrate, miners, workers | /api/stats pool.hashrate (10 min), pool.miners, pool.workers |
ready |
| Blocks found with heights and times | /api/blocks: DAA score, blue score, ISO time, status |
ready |
| Fee, minimum payout, scheme | /api/stats pool.fee_percent, pool.min_payout_ign, pool.scheme PPLNS |
ready |
| Luck | /api/stats pool.luck_24h, pool.effort_current |
ready, definition in section 3 |
| A public pool address and page | --public-url, the page at / |
needs a box and a hostname (pool.igneum.network is held) |
| Exchange or price | none; nothing here is a token sale | not applicable |
| Mining software | igneum-miner with --pool; HiveOS custom miner package with pool:// |
ready, HiveOS untested on a rig |
What is still missing for a listing: a deployed pool with a hostname and TLS on the page, a public testnet for it to
mine, and the explorer branch merged so /api/supply is live. In that order.
7. App: "Mine to a pool" (design only)
Settings, under the rewards address field (app/igneum-app/ui/index.html, the #settings panel), one new field:
<div class="field">
<div class="k">mine to a pool</div>
<label class="switch"><input type="checkbox" id="s-pool"><span class="track"></span><span>Mine to a pool instead of solo</span></label>
<div class="row"><input type="text" class="addr-input mono" id="s-pool-url" placeholder="pool host:4463" spellcheck="false"><button class="btn small" id="s-pool-save">Apply</button></div>
<p class="note" id="s-pool-note">Your vote key stays on this machine and the pool names it in every block you find. The pool pays your rewards address by its own rule and fee (shown here once connected). The software dev fee is off in pool mode. Your node keeps running as the verifier.</p>
<div class="box mono" id="s-pool-terms" hidden></div>
</div>
Engine side (config.rs Settings): pool_url: String (empty = solo). engine.rs passes --pool <url> --worker-name <display_name>-gpuN [--proving] to each card's miner instead of --identities/--dev-fee, keeps the
node as the first argument, and shows the pool's welcome terms (fee, scheme, minimum payout) in #s-pool-terms
from the miner's POOL WELCOME line. The dashboard's "found" counters become "shares accepted" and "blocks credited"
in pool mode. Not implemented tonight.
8. Unverified
- The GPU worker path in pool mode (
--worker): written against the worker protocol and the solo miner's re-check, not run on a card tonight (the measurement used CPU threads). The CUDA and OpenCL workers take the share target in the job line's target field unchanged, so nothing on their side changes. - HiveOS hooks in pool mode:
h-config.shexercised by hand in both modes (pool and solo),h-run.shsyntax-checked; never run on a Hive rig (the package as a whole is untested there,packaging/hive/README.md). - Testnet and mainnet ports and chain ids: set from the design, not run.
- The Hetzner deploy in
pool/README.md: written from the seed-node scripts, not executed. - The exact reward per block the execution layer credits against the pool's expected figure: the pool computes
producer_share(block_subsidy(block daa)); the executor uses the merging chain block's subsidy; equal on the private run (section 5), may differ by the ramp slope across a day on a live chain. The pool's EVM balance is the truth and the page shows both.