igneum/docs/plans/pool.md
igneum-labs 6b05d53162 Pool daemon: the confirmation walk on its own gRPC connection, never restarted from the pruning point, bounded per tick
7 October 2026, 06:01Z and 06:12:55Z on pool-1: confirm_loop restarted its chain walk from the pruning point on any
failed getVirtualChainFromBlock and then fetched every chain block since (about 120,000) over the one connection the
templates used; one timed-out request under four parallel template fetches started it, every template request after it
timed out, no job was issued, 105 blocks on stale templates were orphans. Now: a second GrpcClient (pool.walker) for the
walk and the network numbers; a failed chain call keeps its cursor (the sink only when the node no longer knows it;
cursor_after_failure, node::walk_tests); at most 600 chain blocks per tick with a line saying so; a start with pending
blocks walks from the sink and says that older ones resolve by the orphan rule. docs/plans/pool.md section 9.2: the
re-run's record (the class question closed on pm-1 and pm-2, 71,240 shares, 0 mismatches), the cause, what stays open.
Suite 24 of 24 on igneum-build-1. Protocol and API unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 11:42:50 +00:00

33 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.