igneum/docs/plans/pool.md

54 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). REBASED 6 October 2026 (section 9): branch pool-v0-rebase on master, fork branch pool-v0-rebase on release-0.3.14-node 4c6b129d (worktree ../igneum-wt-pool-rebase, fork worktree vendor/igneum-node-pr); the share re-check hashes the template's program class, never a fixed one. the project lead'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; since 6 October 2026 also program_class, next_program_class, era_seed, era_index, genesis_day_index, genesis_dataset_log2, program_class_v3_activation_daa, so a member without a node builds the same program and day cache as the pool's node) 9.9
Jobs job with prehash, target64, share_target64, a random 2^32 nonce space, the seed pair, program_class and era_seed (6 October 2026: the member refuses a job without a class, code seeds, and never hashes a guessed one), 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, program class and era seed against the member's node (seeds; the class and era joined the check on 6 October 2026); 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 [--proving] [--worker ]: 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
Share check cost in isolation, under the measure lock (every build and run slot held), 500 checks 1.347 mean / 1.313 p50 / 2.074 p99 / 2.638 max ms same binary cargo test --release share_check_cost_ms -- --nocapture in pool/, SHARE_CHECK_MS line
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) 1.35 ms per share check in isolation and 2.1 ms under load on an M-series core (the crate's tight-loop bench is 0.44 ms per warp; the check adds the init words and the engine's shared cache path) is one core for 480 to 740 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 the project lead 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.sh exercised by hand in both modes (pool and solo), h-run.sh syntax-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.

9. 6 October 2026: the rebase onto 0.3.14 and the program class

The fleet agent's load run (18:14Z to 18:24Z, ten GPU members, vardiff working): 0 shares accepted in ten minutes. Every member's log: WORKER MISMATCH nonce=... gpu=... cpu=...: share not sent. The pool fork's miner re-checked every GPU share with its own CPU hasher, and that hasher (release-0.3.6's kaspa-pow) predates program class v3: it hashed a fixed class v2 program while the 0.3.13 and 0.3.14 CUDA workers hashed the epoch's class v3 program, so the two never agreed and the member refused every share before it left the machine. Everything else in the pool worked.

What changed (branch pool-v0-rebase on master, fork pool-v0-rebase on release-0.3.14-node 4c6b129d):

Where Change Forced by the rebase?
Fork igneum/miner/src/pool.rs EpochSeeds carries class and era on 0.3.14; the member builds them from the job's program_class and era_seed (job_seeds), refuses a job without a class or with one this fork does not know (job_refused, code seeds), checks class and era against its own node's template beside the epoch seed (seeds_agree, through crate::template, which installs the node's schedule, genesis day and class v3 activation), installs the same three from the pool's seeds line when it has no node (install_from_pool_line), and ends every worker job and prepare line with the class tokens (class=v3 era=<hex>); prepare writes a checked pack under --prepare-packs as the solo miner does; a worker error ... program class mismatch gets a fresh prepare yes (the struct changed) and the fix itself
Fork igneum/miner/src/main.rs class_tokens, write_pack_checked, seeds_from_info are pub(crate) yes
Pool node.rs seeds from the template with the class and the era; the node's genesis day, dataset size and class v3 activation installed beside the schedule; seeds and job carry the new fields yes (the struct changed)
Pool protocol.rs Seeds + 7 fields, Job + 2 fields (additive: a member ignores unknown fields, so the line still parses on the old build; the old build then hashes class v2 as before and keeps mismatching, so every member moves to this build) yes
Pool verify.rs the tests build seeds through EpochSeeds::v2 yes
Protocol, vardiff, PPLNS, stats API, page unchanged
packaging/hive the pool:// mode merged with master's per-card IDENTITIES=auto and OVERRIDE handling; selftest.sh passes, the pool-mode config exercised by hand (NODE=local, OVERRIDE set) merge only

Tests (every suite on igneum-build-1, IGNEUM_AGENT=pool):

Suite Result What it pins
igneum-pow tests/recheck.rs 2 of 2 class v3 (packs-ca2-mixer/mx8-devnet-epoch0) and class v4 (packs-ca3-v4/v4-devnet-epoch0): the pack's program (generator, id) reproduces the pack's 96 vectors (the worker's reference), the chain seam the engine calls (Epoch::chain_program, chain_dataset_day) builds the same program, and one known nonce (prehash 0x5a..., nonce 0x12345678_00000bb7) hashes equal through both paths for each class; v3 and v4 differ from each other and from the fixed-v2 program of the same seed (the fleet's mismatch); the v3 id 73bcbfe8ccf988f1 and v4 id c120d7963abdcd96 as pinned
fork igneum-miner 21 of 21 pool::tests::cpu_recheck_equals_the_worker_reference_for_class_v3: a job line of the pool's shape through job_seeds, then the real IgneumEngine::epoch_for(seeds).hash_bound against the pack's reference on the known nonce, the v2 re-check shown to disagree, the worker tokens checked; a_class_v4_job_is_refused_until_the_fork_knows_class_v4: this fork's ProgramClass has no V4, so a class 4 job is refused rather than hashed on a guess, and the test FAILS the day V4 lands (ca3-v4-node), which is when it becomes the v3 test's shape on the v4 pack; pool_seeds_are_checked_against_the_verifier_class_and_era
pool igneum-pool 22 of 22 unchanged suites on the new seeds

Consequences per tier: a pool member on any card hashes the program its pool's node names, so the fix costs no hash rate and no memory on any tier (the day cache size is the node's, 1 GiB on the devnet); a member without a verifier node now gets the genesis day and dataset size from the pool's seeds line, which it had no source for before (it would have built a wrong-size cache and mismatched silently); a pool on a class v4 network needs a fork with ProgramClass::V4 (0.3.15, ca3-v4-node); until then its members refuse class 4 jobs loudly (JOB ... REFUSED (seeds)), never hash on a guess.

Building the pool crate on igneum-build-1: the pool reads the fork through the vendor/igneum-node symlink; lib.sh syncs the fork worktree it points at (vendor/igneum-node-pr) as a whole repository, and the symlink itself is created once on the box by hand (ln -s igneum-node-pr /srv/builds/<worktree>/vendor/igneum-node), the one step the scripts do not do.

9.1 7 October 2026: the daemon refuses to start half-alive

The fleet agent's night (6 October, 23:01Z to 23:32Z): the rebased pair never ran as a test. The daemon was started while the node still held its member port, then four members (the wind-down's weight rule freed four pods, not ten) sat connected with no job while every template fetch timed out; nothing in the daemon's own log said so except one line per member. The row from it, now code (pool 269364a+): the member and API listeners are bound in main before anything else and a port that cannot be bound ends the start with exit code 2 and POOL NOT STARTED: cannot bind the members listener on <addr>: <os error>; the node must answer a template with its pow_epoch before the pool serves anyone, retried for --node-wait-secs (default 60, each attempt logged), else exit code 3; a STATUS line every 30 s (members= jobs_issued= shares_accepted= shares_rejected= blocks= template_failures=; node ok | NODE NOT ANSWERING | NO TEMPLATE YET). Test: server::bind_tests (a held port refused with the address in the message, the freed port binds). The 10-member run is still owed: a clean 30-minute window, the daemon bound and its node ... answers templates line printed before the first member, on the next pods the weight rule frees (the table must read over 75 percent signed first).

9.2 7 October 2026, 06:00Z to 06:33Z: the re-run (five members), what it settled and what it found

Settled: the class question. pm-1 and pm-2 (RTX 3070, grpc url none) at +7 min: 36,917 and 34,323 shares accepted, 0 rejected, 0 WORKER MISMATCH, 0 refused, on the class v3 program of the pool's node's epoch (5530a50d..., era = the devnet genesis hash), the genesis day (20729) and dataset size (2^28) installed from the daemon's seeds line. The 6 October failure does not reproduce.

Found: the daemon stalls on a real chain. From 06:01Z (the first block found) no job was issued for seven minutes, pm-3 and pm-4 never got one, every block found on the stale templates (55, then 105, 480 DAA behind the virtual) was an orphan, vardiff could not act (a new share target rides with a job) so the two hashing members sent about 90 shares a second each and the share check read 9.8 ms on pool-1's cores (1.35 ms on the Mac); after a swap to a 20 s template timeout (f808b3f3) with the data dir kept, the stall returned within a minute. The node built templates in 0.6 ms all the while (its prewarm line) and served its own solo miner.

The cause, from the daemon's code: confirm_loop restarted its chain walk from the PRUNING POINT on any failed getVirtualChainFromBlock, and then called get_block for every chain block since (about 120,000 on the devnet) over the ONE gRPC connection the templates used (the client is one request stream per connection, 5 s request timeout); one timed-out request under four parallel template fetches started the walk, every later request waited behind it and timed out, which restarted the walk again. The kept state file (105 pending blocks) restarted it after the swap. The private measurement run (section 5) never saw it: a 600-block chain walks in a moment.

Fixed (pool commit after bd49c2a9): the walk and the network numbers run on a second gRPC connection (pool.walker); a failed chain call keeps its cursor (moved to the sink only when the node no longer knows it, never to the pruning point; test node::walk_tests); a tick walks at most 600 chain blocks and says how many; a start with pending blocks walks from the sink and says that older ones resolve by the orphan rule. --template-timeout-s stays (default 20).

Still open from the night, for the next window: the node log showed a new gRPC connection about every 0.8 s with the count steady at 5 (something opens and closes one each time; the daemon's re-subscribe loop is the suspect, the subscribe-line count in the daemon log decides it); vardiff's new target should apply to the member's current work without waiting for a job (a one-line member change, needs a miner rebuild); the 9.8 ms share check on the pool box's cores against 1.35 ms on the Mac (the verifier's day cache is 1 GiB and random reads on a rented box's memory are the cost; two verify threads saturate at about 200 shares a second, which vardiff must keep far away); and the 10-member load figure itself, not taken.

Consequences per tier: a pool operator needs a box whose memory serves the 1 GiB cache at speed (a rented 2 vCPU box verifies about 100 shares a second per core at 9.8 ms; at one share per 10 s per member that is 1,000 members per core, so the verifier is not the limit once vardiff holds); a member on any card is unaffected by any of this; the chain walk now costs the node at most 600 get_block calls per 5 s on its own connection.

9.3 The re-run's close (06:33:33Z) and the member rows from its logs

After 2f6c0358 (06:20:34Z to 06:33:33Z, 13 minutes, four members): 207 blocks found, 144 confirmed, 69 orphaned (13 inherited pending at the swap, 56 on its own jobs: a 27 percent residual at 1 bps), 20,404 shares accepted, 0 rejected, 0 mismatches, 0 refused, every STATUS node ok, the gRPC connection churn gone (ids #3 to #5 once each in a minute; the 193 connections in 150 s were under f808b3f3's pruning-point walk), re-subscribe lines 2 (one per daemon start: the re-subscribe loop was never the churn). Before it, f808b3f3 had worked through the walk by about 06:14Z: 245 blocks, 69 confirmed, 163 orphaned. First CONFIRMED line 06:24:32Z, 4.80 IGN to the finder. Logs: ~/Desktop/fleet/pool-run-0607/.

The residual orphans are template latency: the daemon's STATUS reads last template 1 s ago and the solo miner on the same node reads template_ms=1632; pool-1's node answers a template request in 1 to 1.6 s although its mining manager answers a per-member request from its cache by a coinbase rewrite (modify_block_template) and its prewarm builds in 0.6 ms. At 1 bps a template that arrives 1.6 s old loses about a quarter of the blocks found on it, pool or solo. That latency is the node's RPC path, a row for the node lane (measure get_block_template round trips on a devnet node under a miner and a pool; the suspect is the RPC service's pow_epoch derivation per request). 82 "RPC request timeout" lines after the 2f6c0358 swap are the gRPC client's own 5 s request timeout on that path; --template-timeout-s cannot lengthen it.

From pm-1's log (1.09 GB): 4,342,515 lines are worker: error N epoch seed mismatch: this worker holds epoch eea66ce7..., one per re-queued job, for the 40 s the worker compiled the new epoch's pack (NVRTC 40,327 ms) and again after every reconnect, because a session respawned the worker (a second process beside the first, the same compile again) and the member fed jobs for a pair the worker did not hold yet. Vardiff from the same log: shift 10 to 11 (idle easing while the worker compiled, which consumed the sized first correction), then 11 down to 0 one step per 30 s over 5.5 minutes at about 190 shares a second, the load that put the share check at 10 to 11 ms on pool-1's cores.

Fixed in the member (fork commit after b8070476): one worker process per run, jobs held for a pair whose prepare is pending and never re-prepared once held (WorkerMemory, test jobs_are_held_while_a_pair_is_being_prepared), worker error lines summarised (one per class, then a count per 1,000), set_target applied to the current work at once. Fixed in the pool (vardiff.rs): the idle easing no longer consumes the sized first correction (test an_idle_easing_does_not_consume_the_sized_first_correction).

Consequences per tier: a member on a 3070-class card loses 40 s of hash at every epoch roll to the NVRTC compile unless its pack is prepared ahead (the pool's seeds line carries next_epoch_seed for that, the member's prepare-ahead is the next member row); a member on a reconnect now keeps its worker and its compiled pair; a pool operator's verifier sees the sized correction within 10 s of a member's first shares (one share per 10 s per member from then on) instead of 5 minutes of a 190-share-a-second flood; the 10-member load figure is still owed and now has a clean form to run in.

10. 7 October 2026: pool-0 finished, TLS, and the open pool (mission item 11)

Branch pool-finish on release-0.3.19 44eee05b with the four daemon commits of pool-v0-rebase cherry-picked (the node fork pinned for the pool work is 0.3.19, whose kaspa-pow needs the latency ladder's igneum-pow; master b92a5fd4 does not carry it, so the branch sits on the release tree); fork branch pool-finish-node on release-0.3.19-node dc141409 with pool-v0-rebase merged (the pool-mode miner). Ordered by the project lead on 7 October 2026, 10:1x UK, built in the order of mission 2.11: pool-0, TLS and the page rows, the share sidechain.

10.1 Pool-0

What 2.11 asks of pool-0 was in v0 (section 2): the member's vote key in every header, the pool with no key and no vote, PPLNS, a payout round every 60 s, a 1 percent fee. What was missing was the fee published beside the dev fee (reinvent 3.5): welcome.share_scheme now carries software_dev_fee_percent (0 in pool mode: the pool's fee is the only fee), the page's Fees row says both, and site/miner.html's dev-fee card says pool-0 charges the same 1 percent so solo and pool cost the same and the choice is about variance alone. The jobs and seeds lines carry the latency ladder's rung (shadow_reps) beside the class and the era, which 0.3.19's program needs; a member checks it against its own node as it checks the seed, the class and the era (a pool that could choose the rung could choose the program).

10.2 TLS (spec 9.3, O-9.7 closed, Q67)

pool/src/tls.rs: rustls 0.23 with ring, TLS 1.3 only. --tls-cert/--tls-key (a chain from a trusted root) or --tls-self-signed (a P-256 pair made under the data dir on first start, read back afterwards; the pin BLAKE2b("igneum-pool-cert-pin-v1" || DER) printed at start, shown on the page and in /api/stats). The member (igneum/miner/src/pool.rs, fork): --pool-tls (the Mozilla roots, the pool's host as the server name) or --pool-pin <hex> (a verifier that accepts exactly the pinned certificate). The binding: both sides export 32 bytes with the label EXPORTER-igneum-pool-binding and the chain id (8 bytes LE) as the context; the member signs igneum-pool-binding-v1/ || chain id LE || exporter under its vote key with its own tag (DST_BINDING, consensus/core/src/finality.rs); the pool verifies it against the member's key and refuses the authorize with bye otherwise. Tests: finality.rs (the signature holds for one exporter and one chain id, is no proof of possession); tls.rs (a self-signed pair read back with one pin, a pinned handshake, both sides' exporters equal, the binding verified on its own connection and refused on a second, a wrong pin never handshakes). Testnet and mainnet refuse to start in the clear without --allow-plain; the devnet's local daemons stay plain. HiveOS: pools:// or POOL_PIN= (packaging/hive).

10.3 The page rows (polish Q68 to Q73)

Row Done
Q68 the page's node line: "node ok, ", "node syncing", "no template", "node unreachable since
Q69 one formatter set in the page: en-GB separators on every count, hash rate to one decimal on a kH to PH ladder, difficulty one spelling, IGN 4 decimals on tiles and 6 in tables; luck in MiningPoolStats' convention (expected over actual, over 100 percent is lucky), stated in the API source line
Q70 the hashrate samples and the check costs are persisted in state.json (one snapshot interval, 15 s, is the loss window; the README says so); a restart keeps the rate tiles and the luck
Q71 the payments of a looked-up address under its workers
Q72 hourly buckets per address kept 7 days (history in /api/miners/<address>, a sparkline on the page); --alert-webhook POSTed once per address silent for 10 minutes; /metrics Prometheus text; /health 200 only when the node is synced and answered a template in the last 30 s
Q73 the network's finality state from getFinalityCheckpoints (active, paused, window filling, unknown) with the latest locked index and its age, on the page and in /api/stats, beside the sentence "payouts follow blue confirmation, not finality" (network.payout_rule)

10.4 The open pool: the share sidechain with no operator

The design is spec 09 section 9.12 (written with the code). In one paragraph: the member runs its own node and igneum-pool --open beside it; the daemon holds no key, builds the member's 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, re-stamps without a node call when the chain tip moves (restamp_extra_data: the coinbase payload rewritten and the merkle root re-derived as the body check derives it), and gossips shares with the other members' daemons. A share is a template at the chain's target; a block is a share at the block target; the executor pays a blue block's producer share by its split from pool_split_activation_daa on (evm::split_producer: integer parts by weight, the dust to the finder), so every node computes the credits from the block alone, as it does the 20 percent. The chain: one parent per share, heaviest work wins, a fork's loser is stale and pays nothing, a 10-second target by default held by a 30-share retarget (the DAA's shape: the window's mean target times actual over expected span, clamped x4 per share and ten doublings above the first target; the first version compounded the last target by the window's ratio at every share and ran to the saturation cap in thirty shares, every hash a share: the first smoke run), a first target that is a chain constant (the network's genesis block target eight times easier, never a node's current template, which differs between members), a PPLNS window of 2,160 shares including the share itself, the dev fee as one split entry (the weights scaled by 2^32 before the fee's division, or a genesis share's few dozen hashes of work rounded the dev entry to a third of its share: the unit test).

File What
consensus/core/src/evm.rs (fork) IGNS and IGNP encode and parse, split_producer, the switch install_pool_split_activation
consensus/core/src/config/params.rs (fork) pool_split_activation_daa: never by default, in the digest once set, the fees' pattern; the override test
igneum/exec/src/executor.rs, service.rs (fork) SegmentBlock.split; the reward loop pays by the split at or above the switch; the test with an odd subsidy (the dust)
kaspad/src/daemon.rs (fork) installs the switch from the params beside the fees
pool/src/sidechain.rs the share, the chain, the target, the window's split, fork choice, verify-share
pool/src/open.rs the daemon side: stamping, accepting (structure, the seeds, the PoW), the shares log, orphans waiting for a parent, the API. The seeds check cost two gate runs on the box: the first keyed the node's epochs by DAA / epoch_blocks (not how the node numbers them: every share past epoch 1 refused), the second required the daemon's own node's seed exactly, and four nodes on the 60x profile named three different seed blocks for epoch 1 with no node refusing any block (a node checks a block's PoW under the seed of the block's own ancestry); the rule now is the node's seed, or a block the node holds at the epoch's seed depth with the epoch's class, era and rung
pool/src/p2p.rs the gossip: hello, share, get_shares, the relay, the sync when a peer is ahead
pool/tools/open-gate.mjs the gate

What the node fork needs merged (for the shipper, 0.3.20): the three commits of pool-v0-rebase (the pool-mode miner, already on pool-finish-node), plus pool-finish-node's own: the binding in finality.rs, the split codec and switch in evm.rs, the params field (the digest moves only once the switch is set, so the live devnet's digest does not move), the executor's split, the miner's TLS and rung. Nothing activates on the live devnet: pool_split_activation_daa stays never until a cut sets it through the P2 mechanism or the override, as the fee switch was.

10.5 The gate

Run: node pool/tools/open-gate.mjs on igneum-build-1 from /srv/builds/igneum-wt-pool-finish with the box-built binaries (the standing rule of 7 October 2026 afternoon: nothing builds or runs on the Mac; the first attempt ran on the Mac at load 113 and the Mac crashed under it, section 10.4's retarget note): 4 nodes on the 60x fast-time profile with real proof of work at genesis_bits 0x1e400000, the genesis dataset at 2^24 words (64 MiB per process against the devnet's 1 GiB, so 100 CPU members, 10 daemons and 4 nodes fit 64 GB), pool_split_activation_daa 0; 10 open daemons (one per node in turn, peered in a ring with two chords, the last one withholding its members' shares); the nodes started one after another with --addpeer and their peer counts read by getConnectedPeerInfo before anything else starts (the first box run started four nodes at once with --connect, nodes 2 and 3 never peered, and the one DAG was three partitions with three epoch seeds, which the share chain's seeds check then reported as "not the node's": the harness verifies the chain-side fact now, the standing rule); 100 CPU members (one key and one address each, one thread each, niced); the chain at 0.1 s per share (at 1 block a second a 10-second chain is sub-block work only for a pool under a tenth of the network, spec 9.12 item 8), window 2,160.

Run 1 of record: 7 October 2026, 12:53:51Z to 13:09:09Z on igneum-build-1, pool binary from pool-finish d48d9dd2 (the seeds-check line of the last fix overlaid), node and miner from pool-finish-node b0444f51 (strings igneumd carries b0444f51; the box's 96 threads also carried the proving lane's builds, load 50 to 60). Summary: /srv/builds/igneum-wt-pool-finish/pl-gate-1/run/summary.json and gate.log (prefix pl-*, spared).

Number Value Source
Nodes, peers, DAA at the end 4 nodes, peers 1/2/2/1, DAA 953/953/953/953 (one DAG; the 30-s samples agree throughout) getConnectedPeerInfo, getBlockDagInfo on every node
Members, daemons 100 keys and addresses on 10 daemons; daemon 9 (members 9, 19, ..., 99) withheld its shares the harness
Honest members with a share, paid by the coinbase rule 90 of 90, 90 of 90 the daemons' /api/open/shares; eth_getBalance before and after on node 0
Members paid over time 53 at 30 s, 71 at 60 s, 84 at 90 s, 91 at 120 s, 98 at 180 s, 100 at 211 s the 30-s samples
First payout after the first share, honest members n 90, p50 3.6 s, p90 7.0 s, max 8.3 s; 90 of 90 under 120 s the 5-s watcher: the member's first share's header time against its first chain credit
Blocks found 948 on the chain (DAA 948 at the last sample); 864 by honest daemons, 84 by the withholder the daemons' /api/blocks; getBlockDagInfo
Honest blocks paying a withheld address 0 of 864 the honest daemons' block splits
Withheld shares seen by any honest daemon 0 the honest daemons' /api/open/shares
Withheld members' credits 0.01 to 0.81 IGN each, from their own daemon's 84 blocks alone (that daemon pays its own window) against 2,383 IGN credited in all eth_getBalance
Share chain at the end height 1,814 on all nine honest daemons (1,816 on the withholder, its own branch), 2,007 shares accepted each, 193 stale, 8 to 15 reorgs, 0 refused /api/open on every daemon
Stale rate, honest daemons 9.6 percent (193 of 2,007) at 2 shares a second across 10 daemons on one box /api/open
Stale rate, the withholder 17.8 percent (its branch loses to the nine) /api/open on daemon 9
Share check cost, daemon 0 p50 3.8 ms, p99 7.3 ms, max 10.2 ms over 1,044 checks (the box under other lanes' builds, nice 15 miners) the daemon's STATUS line
Drop proof verify-share on daemon 0's first logged share: SHARE OK 530b509c... height 7 ... (work 32,768 hashes; cache 2^24 words), exit 0; on the 12-member smoke the same tool printed BLOCK PAYS <address> weight 970,456,567 of 4,294,967,292 (22.60%) against the first confirmed block after it igneum-pool verify-share
Verdict PASS the harness

Two runs before it failed and taught: the first (12:16Z) refused every share past epoch 1 because the seeds check keyed the node's epochs by DAA / epoch_blocks; the second (12:34Z) refused shares from members on other nodes because four nodes named three epoch seeds, and the reason was the harness, not the chain: --connect raced the earlier node's listener and nodes 2 and 3 never peered (three partitions). The seeds check is now the rule of spec 9.12 item 3, and the harness starts the nodes in sequence with --addpeer and reads their peers before anything else starts.

Consequences by tier, from these numbers (the standing rule of 5 October):

Tier What the gate's numbers mean What is done
A home miner with one card (8 to 32 GB, any vendor, any OS) on the open pool the first payout follows the first block found on the chain after its first share: 3.6 s median here at 1 block a second with the pool as the whole network; on the devnet at 100 GH/s with the open pool at a tenth of the network it is the pool's block interval, about 10 s, plus the share's wait; the member's share wait is the chain's interval times the member count over its hash share (100 members at 0.5 s: 50 s; at the spec's 10 s per share with 100 members of one card: 17 minutes, which is O-9.10's row) the chain's rate is a chain constant a public open chain sets from its hashrate (spec 9.12 item 8, O-9.10); the gate ran at 0.5 s
The same card on pool-0 unchanged by this round: a share every 10 s and a payout round every 60 s TLS on the member port, the page rows
A rig one open daemon per rig beside its node; the daemon's cost is the share checks (3.8 ms each here) at the chain's rate, under a core at 2 shares a second nothing more
A pool user without a node cannot join the open pool; pool-0 as before the page says so
The network the open pool's stale rate was 9.6 percent at 2 shares a second with ten daemons on one host; a public chain at the same rate across the internet loses more to forks, which is the uncles row (10.6); every block's coinbase grew by 48 bytes per payee, at most 256 payees, under the devnet's 16,384-byte cap the uncles row stays open; the public chain's rate is O-9.10

10.6 Open after this round

Item Why it is open What closes it
Uncles a fork's loser is stale; on the public internet at 100 ms between members a 10-share-a-second chain would lose several percent of shares to forks P2Pool's uncle rule (a share may name up to N recent stale shares; they are paid at a discount), after the stale rate of a public chain is measured
Share bodies a share carries the full template; on a chain with full blocks that is the block's size per share the transaction ids plus the coinbase, the merkle root rebuilt from them (mode B's shape)
A late joiner a share of an epoch the member's node no longer reports cannot be checked, so the sync is bounded to those epochs (O-9.11) a node RPC for past epochs' seeds, or the seeds in the share chain's headers
The chain's rate the public chain's seconds per share against its hashrate (O-9.10) one chain per decade of hashrate (P2Pool's main, mini, nano), or uncles
The Hive package, the app neither starts an open daemon yet; the app's "mine to a pool" setting (section 7) is still design the app spawns igneum-pool --open beside its node when the setting says open
Vote relay and mode C unchanged from section 3 unchanged

10.7 Consequences by tier

Tier Pool-0 The open pool
A home miner on one card (8, 12, 16, 24, 32 GB; NVIDIA, AMD, Apple; Windows, Linux, macOS) nothing changes on the card; the member port is TLS and the miner's --pool-pin or --pool-tls is one flag; the 1 percent is the same money as solo's dev fee runs its own node (the app already does) and one more process, the open daemon, whose memory is the day cache (1 GiB on the devnet, as the miner's); the 1 percent is the split entry; the first payout follows the first block found on the chain after its first share, seconds at devnet scale; at a public chain's rate its share interval is the chain's rate times the members' count, the row O-9.10 is about
A rig one daemon per rig, every card's miner on it; one key per operator as before the same
A pool user without a node pool-0 as before (hashes, does not vote) cannot join the open pool: the daemon builds templates from a node; a user without one takes pool-0
The pool operator (pool-0) TLS on the member port, /metrics, /health, the alert webhook, the persisted ledger none exists
The network pool concentration is not vote concentration, as before; an open pool's blocks carry as many keys as it has members and pay as many addresses as the window holds, so the coinbase grows by 48 bytes per payee (at most 256) the executor's split is the one new consensus-visible rule, behind a switch that is never on the devnet