Pool v0 rebased onto master and the 0.3.14 fork: the program class and era seed ride with every seeds and job line; the re-check test for class v3 and class v4

The fleet's 10-member load run (6 October 2026, 18:14Z to 18:24Z) accepted 0 shares: the pool fork's miner re-checked GPU
shares with a fixed class v2 program while the 0.3.14 workers hashed class v3 (docs/plans/pool.md section 9).

pool: node.rs builds the share verifier's seeds from the template's pow_epoch with the class and the era, installs the
node's genesis day, dataset size and class v3 activation beside the schedule, and sends program_class, next_program_class,
era_seed, era_index, genesis_day_index, genesis_dataset_log2 and program_class_v3_activation_daa in `seeds` and
program_class and era_seed in `job` (protocol.rs, additive fields); verify.rs tests through EpochSeeds::v2. Protocol,
vardiff, PPLNS, stats API and page otherwise unchanged. Suite 22 of 22 on igneum-build-1.

igneum-pow tests/recheck.rs: for class v3 (packs-ca2-mixer/mx8-devnet-epoch0) and class v4 (packs-ca3-v4/v4-devnet-epoch0)
the pack's program reproduces the pack's 96 vectors (the worker's reference), the chain seam the node engine calls
(Epoch::chain_program, chain_dataset_day) builds the same program, one known nonce hashes equal through both paths, and
the fixed-v2 program of the same seed disagrees; the pinned v3 and v4 program ids differ. 2 of 2 on igneum-build-1.

packaging/hive: the pool:// mode merged with master's per-card IDENTITIES=auto and OVERRIDE handling (selftest passes).
pool/README.md and pool.md: building on igneum-build-1 (the vendor/igneum-node symlink on the box).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-06 18:49:13 +00:00
parent 8b837962cf
commit 4de1b3ba52
6 changed files with 234 additions and 10 deletions

View file

@ -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). the project lead'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. 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.
@ -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/<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 | |
@ -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=<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.

137
igneum-pow/tests/recheck.rs Normal file
View file

@ -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<u8>,
era: Vec<u8>,
day_bytes: Vec<u8>,
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());
}

View file

@ -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 <fork worktree name> /srv/builds/<worktree>/vendor/igneum-node`.
Artefacts land in `pool/target-remote/release/igneum-pool` (x86_64 Linux); the Mac builds no Linux binary.

View file

@ -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<Pool>, member: &Arc<Member>) -> 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<Pool>, member: &Arc<Member>) -> 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<Pool>, member: &Arc<Member>) -> 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(),

View file

@ -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<String>,
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<String>,
clean: bool,
},
JobRefused {

View file

@ -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<f64> = (0..500u64).map(|i| check(&epoch, &key, i * 97, 0).cost_ms).collect();