diff --git a/docs/plans/pool.md b/docs/plans/pool.md
index adc603fb6..8070d39aa 100644
--- a/docs/plans/pool.md
+++ b/docs/plans/pool.md
@@ -1,7 +1,9 @@
# 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
+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. 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.
@@ -24,8 +26,8 @@ template and worker code it reuses lives there.
| 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 |
+| 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 |
@@ -33,7 +35,7 @@ template and worker code it reuses lives there.
| 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 |
+| 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/
`, `/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 | |
@@ -196,3 +198,41 @@ in pool mode. Not implemented tonight.
`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=`); `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//vendor/igneum-node`), the one step the scripts do not do.
diff --git a/igneum-pow/tests/recheck.rs b/igneum-pow/tests/recheck.rs
new file mode 100644
index 000000000..d23977b40
--- /dev/null
+++ b/igneum-pow/tests/recheck.rs
@@ -0,0 +1,137 @@
+//! The miner's CPU re-check against the worker's reference path, class v3 and class v4 (6 October 2026).
+//!
+//! The fleet's 10-member pool run (18:14Z to 18:24Z): 0 shares accepted in ten minutes, every GPU answer refused as
+//! `WORKER MISMATCH`, because the pool fork's miner re-hashed each share with a fixed class v2 program while the 0.3.14
+//! CUDA worker hashed the class v3 program of the epoch. The rule since: the re-check builds its program through the
+//! chain seam (`Epoch::chain_program` and `Epoch::chain_dataset_day`, what the node's `IgneumEngine` calls) from the
+//! template's `(epoch seed, day, program class, era seed)`, never from a fixed class.
+//!
+//! This test pins that seam against the worker's reference for both classes the packs carry: the program the pack's
+//! kernels were emitted from (program.json, generator 3 or 4, with its id) and the pack's 96 vectors; then one known
+//! nonce through the worker's path (`block_init_words` plus `interpret_warp_init` on the pack's program) and through
+//! the re-check's path (the same on the seam's program and dataset) must agree, and the other class's program and
+//! the fixed-v2 program of the same seed must disagree (the mismatch the fleet saw).
+
+use igneum_pow::bind::{block_init_words, lane_nonce, unhex};
+use igneum_pow::verify::DatasetSource;
+use igneum_pow::{interpret_warp_init, DatasetMode, Epoch, ProgramClass, Shape};
+use serde_json::Value;
+use std::path::PathBuf;
+
+fn pack_dir(rel: &str) -> PathBuf {
+ let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../proto-cuda").join(rel);
+ assert!(p.join("program.json").is_file(), "pack {} is missing; this test never skips", p.display());
+ p
+}
+
+fn json(dir: &std::path::Path, f: &str) -> Value {
+ serde_json::from_str(&std::fs::read_to_string(dir.join(f)).unwrap_or_else(|e| panic!("{}: {e}", dir.join(f).display()))).unwrap()
+}
+
+fn hex64(v: &Value) -> u64 {
+ u64::from_str_radix(v.as_str().unwrap().trim_start_matches("0x"), 16).unwrap()
+}
+
+struct PackRef {
+ class: ProgramClass,
+ seed_label: String,
+ seed_bytes: Vec,
+ era: Vec,
+ day_bytes: Vec,
+ log2: u32,
+ /// The worker's reference: the pack's program and day dataset, checked against the pack's vectors
+ reference: Epoch,
+}
+
+/// The pack as the worker sees it: program.json's program (generator, id and class as recorded) over the pack's
+/// day dataset, trusted only after its 96 vectors reproduce.
+fn load(rel: &str, want: ProgramClass) -> PackRef {
+ let dir = pack_dir(rel);
+ let j = json(&dir, "program.json");
+ let class = match j["program_class"].as_str().unwrap() {
+ "v3" => ProgramClass::V3,
+ "v4" => ProgramClass::V4,
+ other => panic!("{rel}: class {other}"),
+ };
+ assert_eq!(class, want, "{rel}: the pack's class");
+ let seed_label = j["seed"].as_str().unwrap().to_string();
+ let seed_bytes = unhex(j["seed_bytes"].as_str().unwrap()).unwrap();
+ let era = unhex(j["era_seed_bytes"].as_str().unwrap()).unwrap();
+ let day_bytes = unhex(j["dataset"]["day_bytes"].as_str().unwrap()).unwrap();
+ let log2 = j["dataset"]["log2_words"].as_u64().unwrap() as u32;
+ let program = igneum_pow::generate_from_seed_bytes_program_class(&seed_label, &seed_bytes, class, Some(&era));
+ assert_eq!(program.generator, j["generator"].as_u64().unwrap() as u32, "{rel}: generator");
+ assert_eq!(program.generator, class.generator_version(), "{rel}: the generator is the class's");
+ assert_eq!(program.program_id(), hex64(&j["program_id"]), "{rel}: program id");
+ let shape = Shape::for_class(&program.class);
+ let mut dataset = DatasetSource::from_key_shape(igneum_pow::seed::seed_words_from_bytes(&day_bytes), DatasetMode::MemoryHard, log2, shape);
+ dataset.key_bytes = day_bytes.clone();
+ let reference = Epoch { program, dataset };
+ let v = json(&dir, "vectors.json");
+ let warps = v["warps"].as_array().unwrap();
+ assert_eq!(warps.len(), 3, "{rel}: three vector warps");
+ for w in warps {
+ let base = w["base_nonce"].as_u64().unwrap() as u32;
+ let got = reference.hash_warp(base);
+ for (lane, e) in w["expected"].as_array().unwrap().iter().enumerate() {
+ assert_eq!(got[lane], hex64(e), "{rel}: vector base {base} lane {lane}");
+ }
+ }
+ PackRef { class, seed_label, seed_bytes, era, day_bytes, log2, reference }
+}
+
+/// The re-check's epoch: the chain seam the node engine and the miner call, from the template's four values.
+/// The packs are day-0 caches, so days since genesis is 0 and the genesis dataset size is the pack's.
+fn recheck_epoch(p: &PackRef) -> Epoch {
+ Epoch { program: Epoch::chain_program(&p.seed_bytes, Some(&p.era), p.class, &p.seed_label), dataset: Epoch::chain_dataset_day(&p.day_bytes, p.class, 0, p.log2) }
+}
+
+const PREHASH: [u8; 32] = [0x5a; 32];
+const NONCE: u64 = 0x1234_5678_0000_0bb7; // high word 0x12345678, lane 0x0bb7 (warp base 0x0ba0, lane 23)
+
+/// The bound lane hash of one nonce on an epoch, the way a worker computes it (init words from the prehash and the
+/// nonce's high word, the warp at the lane's base) and the way `EpochRef::hash_bound` re-checks it.
+fn bound(e: &Epoch, prehash: &[u8; 32], nonce: u64) -> u64 {
+ let init = block_init_words(prehash, nonce);
+ let lane = lane_nonce(nonce);
+ interpret_warp_init(&e.program, &init, lane & !31, &e.dataset).hashes[(lane & 31) as usize]
+}
+
+#[test]
+fn cpu_recheck_equals_the_worker_reference_for_class_v3_and_class_v4() {
+ let v3 = load("packs-ca2-mixer/mx8-devnet-epoch0", ProgramClass::V3);
+ let v4 = load("packs-ca3-v4/v4-devnet-epoch0", ProgramClass::V4);
+ assert_eq!(v3.seed_bytes, v4.seed_bytes, "the two packs share the epoch seed (devnet genesis), so only the class differs");
+ assert_eq!(v3.era, v4.era);
+ let mut hashes = Vec::new();
+ for p in [&v3, &v4] {
+ let re = recheck_epoch(p);
+ assert_eq!(re.program.program_id(), p.reference.program.program_id(), "{:?}: the seam's program is the pack's", p.class);
+ assert_eq!(re.program.generator, p.reference.program.generator);
+ assert_eq!(re.program.instrs.len(), p.reference.program.instrs.len());
+ let worker = bound(&p.reference, &PREHASH, NONCE);
+ let cpu = bound(&re, &PREHASH, NONCE);
+ assert_eq!(cpu, worker, "{:?}: CPU re-check {cpu:016x} against the worker's reference {worker:016x}", p.class);
+ // the same nonce through a second prehash, so the agreement is not one lucky lane
+ let (w2, c2) = (bound(&p.reference, &[0xa5; 32], NONCE ^ 0x1f), bound(&re, &[0xa5; 32], NONCE ^ 0x1f));
+ assert_eq!(c2, w2, "{:?}: second nonce", p.class);
+ hashes.push(cpu);
+ }
+ assert_ne!(hashes[0], hashes[1], "class v3 and class v4 programs of one seed hash a nonce differently");
+ // the fleet's mismatch: a fixed class v2 program on the class v3 epoch's seed
+ let v2 = Epoch { program: Epoch::chain_program(&v3.seed_bytes, None, ProgramClass::V2, &v3.seed_label), dataset: Epoch::chain_dataset_day(&v3.day_bytes, ProgramClass::V2, 0, v3.log2) };
+ assert_ne!(bound(&v2, &PREHASH, NONCE), hashes[0], "a class v2 re-check refuses a class v3 share");
+ assert_ne!(bound(&v2, &PREHASH, NONCE), hashes[1], "a class v2 re-check refuses a class v4 share");
+}
+
+/// The generator-4 program id rule (counter-asic-3-status.md, the blocker closed 6 October 2026): the v4 program of
+/// a seed carries another id than the v3 program of the same seed, so a stale worker across the activation sees the
+/// mismatch, while v2 and v3 ids stay as the packs pinned them.
+#[test]
+fn program_ids_differ_between_class_v3_and_class_v4_of_one_seed() {
+ let v3 = load("packs-ca2-mixer/mx8-devnet-epoch0", ProgramClass::V3);
+ let v4 = load("packs-ca3-v4/v4-devnet-epoch0", ProgramClass::V4);
+ assert_eq!(v3.reference.program.program_id(), 0x73bc_bfe8_ccf9_88f1, "the v3 control's id as pinned");
+ assert_eq!(v4.reference.program.program_id(), 0xc120_d796_3abd_cd96, "the v4 candidate's id as pinned");
+ assert_ne!(v3.reference.program.program_id(), v4.reference.program.program_id());
+}
diff --git a/pool/README.md b/pool/README.md
index 64e49d166..9d042524b 100644
--- a/pool/README.md
+++ b/pool/README.md
@@ -163,3 +163,10 @@ Captured responses from the measured run are the fixtures.
| `src/api.rs` | the HTTP server, the routes, the field contracts |
| `web/index.html` | the page |
| `tools/measure.mjs` | the private-network measurement |
+
+## Building on igneum-build-1 (6 October 2026)
+
+Every Linux build and suite runs on the box through `tools/build-remote.sh` from this directory (`IGNEUM_AGENT=pool`). The
+crate reads the fork through the `vendor/igneum-node` symlink; the build library syncs the fork worktree it points at as a
+whole repository, and the symlink is made once on the box by hand: `ln -s /srv/builds//vendor/igneum-node`.
+Artefacts land in `pool/target-remote/release/igneum-pool` (x86_64 Linux); the Mac builds no Linux binary.
diff --git a/pool/src/node.rs b/pool/src/node.rs
index 8cdf94e5f..513813aa3 100644
--- a/pool/src/node.rs
+++ b/pool/src/node.rs
@@ -9,7 +9,7 @@ use crate::vardiff::{share_target, Vardiff};
use crate::verify::JobKey;
use kaspa_consensus_core::block::Block;
use kaspa_consensus_core::header::Header;
-use kaspa_consensus_core::igneum::{block_subsidy, install_pow_schedule, pow_schedule, producer_share, PowSchedule};
+use kaspa_consensus_core::igneum::{block_subsidy, install_pow_genesis, install_pow_schedule, install_program_class_v3_activation, pow_genesis_dataset_log2, pow_genesis_day_index, pow_schedule, producer_share, program_class_v3_activation_daa, PowSchedule};
use kaspa_grpc_client::GrpcClient;
use kaspa_hashes::Hash;
use kaspa_notify::{listener::ListenerId, scope::NewBlockTemplateScope};
@@ -48,12 +48,25 @@ pub async fn fetch_job(pool: &Arc, member: &Arc) -> Result<(), Str
let block: Block = raw.clone().try_into().map_err(|e| format!("block convert: {e}"))?;
let header: Header = block.header.as_ref().clone();
let info = tmpl.pow_epoch.as_ref().ok_or("the node reports no pow_epoch; a devnet-v4 line node is needed")?;
+ // The node's PoW schedule, genesis day and dataset size, and class v3 activation are the pool's (what the solo
+ // miner's `template()` installs): the share verifier's day cache and program must be the node's exactly
let wanted = PowSchedule::clamped(info.epoch_blocks, info.epoch_lead, info.day_ms);
if wanted != pow_schedule() {
install_pow_schedule(wanted);
eprintln!("{} node PoW schedule: {} DAA per epoch, lead {}, day {} ms", now(), wanted.epoch_blocks, wanted.epoch_lead, wanted.day_ms);
}
- let seeds = EpochSeeds { epoch: info.epoch_seed, day: day_index(header.timestamp) };
+ if (info.genesis_day_index, info.genesis_dataset_log2) != (pow_genesis_day_index(), pow_genesis_dataset_log2()) {
+ install_pow_genesis(info.genesis_day_index, info.genesis_dataset_log2);
+ eprintln!("{} node genesis day index {} and genesis dataset 2^{} words: the day cache follows them", now(), info.genesis_day_index, info.genesis_dataset_log2);
+ }
+ if info.program_class_v3_activation_daa != program_class_v3_activation_daa() {
+ install_program_class_v3_activation(info.program_class_v3_activation_daa);
+ eprintln!("{} node program class v3 activation: {} (epoch {} is class {}, the next epoch class {})", now(), info.program_class_v3_activation_daa, info.epoch_index, info.class().name(), info.next_class().name());
+ }
+ // Counter ASIC 2.0: the class and the era seed of the epoch ride with the template; the verifier hashes the
+ // program they name, the same one the members' workers compile (6 October 2026: a fixed class here or on the
+ // member refused every GPU share of the fleet run)
+ let seeds = EpochSeeds { epoch: info.epoch_seed, day: day_index(header.timestamp), class: info.class(), era: info.era_seed.unwrap_or(kaspa_hashes::ZERO_HASH) };
let engine = pool.engine.clone();
let epoch = tokio::task::spawn_blocking(move || engine.epoch_for(&seeds)).await.map_err(|e| e.to_string())?;
let prehash = header_prehash(&header);
@@ -119,9 +132,17 @@ pub async fn fetch_job(pool: &Arc, member: &Arc) -> Result<(), Str
epoch_blocks: info.epoch_blocks,
epoch_lead: info.epoch_lead,
day_ms: info.day_ms,
+ program_class: info.program_class,
+ next_program_class: info.next_program_class,
+ era_seed: info.era_seed.map(|h| h.to_string()),
+ era_index: info.era_index,
+ genesis_day_index: info.genesis_day_index,
+ genesis_dataset_log2: info.genesis_dataset_log2,
+ program_class_v3_activation_daa: info.program_class_v3_activation_daa,
}
.line(),
);
+ println!("{} SEEDS for member {} ({}): epoch {} day {} class {} era {}", now(), member.id, member.worker, seeds.epoch, seeds.day, seeds.class.name(), seeds.era);
}
member.send(Msg::Template { template_id, mode: "A".into(), block: Box::new(raw), daa_score: header.daa_score, bits: header.bits, votes_carried: Vec::new() }.line());
member.send(
@@ -135,6 +156,8 @@ pub async fn fetch_job(pool: &Arc, member: &Arc) -> Result<(), Str
nonce_count: hex_u64(1 << 32),
epoch_seed: seeds.epoch.to_string(),
day: seeds.day,
+ program_class: seeds.class.generator_version(),
+ era_seed: (seeds.class != kaspa_consensus_core::igneum::ProgramClass::V2).then(|| seeds.era.to_string()),
clean,
}
.line(),
diff --git a/pool/src/protocol.rs b/pool/src/protocol.rs
index 85fc96d3a..77e7b4dac 100644
--- a/pool/src/protocol.rs
+++ b/pool/src/protocol.rs
@@ -3,7 +3,8 @@
//! v0 encoding notes (docs/plans/pool.md): a `template` carries the node's own `RpcRawBlock` JSON (mode A, the full
//! block, so a member with a node can submit it there as well); `authorize` carries the member's payout address and
//! worker name beside the key, because v0 pays by EVM address and the spec's message names the member by key only;
-//! `job` carries the seed pair the worker protocol wants (`epoch_seed`, `day`) so a member needs no `seeds` lookup
+//! `job` carries the seed pair the worker protocol wants (`epoch_seed`, `day`) and the program class and era seed
+//! (Counter ASIC 2.0) so a member needs no `seeds` lookup
//! per job. Unknown fields are ignored, as the spec requires.
use kaspa_rpc_core::RpcRawBlock;
@@ -66,6 +67,18 @@ pub enum Msg {
epoch_blocks: u64,
epoch_lead: u64,
day_ms: u64,
+ /// Counter ASIC 2.0 (added 6 October 2026, after the fleet's 0-share run): the program class of the current
+ /// epoch as the node's `pow_epoch.program_class` reports it (the generator number, 2 or 3), the next epoch's
+ /// class, and the era seed class v3 programs are drawn under. A member hashes the program these name.
+ program_class: u32,
+ next_program_class: u32,
+ era_seed: Option,
+ era_index: u64,
+ /// The node's genesis day index, genesis dataset size (log2 words) and class v3 activation DAA: what a
+ /// member without a verifier node installs so its day cache is built at the network's size
+ genesis_day_index: u64,
+ genesis_dataset_log2: u32,
+ program_class_v3_activation_daa: u64,
},
SetTarget {
shift: u32,
@@ -90,6 +103,10 @@ pub enum Msg {
nonce_count: String,
epoch_seed: String,
day: u64,
+ /// The program class (generator number) and era seed of the job's epoch: the member's CPU re-check and its
+ /// worker lines use exactly these (never a fixed class)
+ program_class: u32,
+ era_seed: Option,
clean: bool,
},
JobRefused {
diff --git a/pool/src/verify.rs b/pool/src/verify.rs
index b7aa8aca1..049d9cab2 100644
--- a/pool/src/verify.rs
+++ b/pool/src/verify.rs
@@ -83,7 +83,7 @@ mod tests {
#[test]
fn known_good_and_known_bad_shares() {
let engine = IgneumEngine::new();
- let seeds = EpochSeeds { epoch: Hash::from_bytes([5u8; 32]), day: 0 };
+ let seeds = EpochSeeds::v2(Hash::from_bytes([5u8; 32]), 0);
let epoch = engine.epoch_for(&seeds);
let prehash = [9u8; 32];
let nonce = 0x0123_4567_89ab_cdefu64;
@@ -111,7 +111,7 @@ mod tests {
#[test]
fn lanes_of_one_group_are_distinct_shares() {
let engine = IgneumEngine::new();
- let seeds = EpochSeeds { epoch: Hash::from_bytes([6u8; 32]), day: 0 };
+ let seeds = EpochSeeds::v2(Hash::from_bytes([6u8; 32]), 0);
let epoch = engine.epoch_for(&seeds);
let prehash = [1u8; 32];
let base = 0x0000_0001_0000_0020u64;
@@ -133,7 +133,7 @@ mod cost {
#[test]
fn share_check_cost_ms() {
let engine = IgneumEngine::new();
- let seeds = EpochSeeds { epoch: Hash::from_bytes([3u8; 32]), day: 0 };
+ let seeds = EpochSeeds::v2(Hash::from_bytes([3u8; 32]), 0);
let epoch = engine.epoch_for(&seeds);
let key = JobKey { prehash: [2u8; 32], target64: 0, share_target64: u64::MAX, seeds };
let mut v: Vec = (0..500u64).map(|i| check(&epoch, &key, i * 97, 0).cost_ms).collect();