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();