igneum/pool/src/verify.rs
igneum-labs 8088b55166 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>
2026-10-06 18:49:13 +00:00

145 lines
5.7 KiB
Rust

//! Share validation against the CPU warp verifier (spec 09 section 9.8 item 5): every share is evaluated exactly as a
//! node verifies a block, through the node's own `IgneumEngine` (the same program, cache and init words). The codes
//! are the spec's: `stale`, `duplicate`, `above_target`, `wrong_hash`, `unknown_job`, `ok`.
use kaspa_pow::igneum::{EpochRef, EpochSeeds, IgneumEngine};
use std::sync::Arc;
use std::time::Instant;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Code {
Ok,
Stale,
Duplicate,
AboveTarget,
WrongHash,
UnknownJob,
}
impl Code {
pub fn as_str(self) -> &'static str {
match self {
Code::Ok => "ok",
Code::Stale => "stale",
Code::Duplicate => "duplicate",
Code::AboveTarget => "above_target",
Code::WrongHash => "wrong_hash",
Code::UnknownJob => "unknown_job",
}
}
}
/// What a job fixes for its shares.
#[derive(Clone)]
pub struct JobKey {
pub prehash: [u8; 32],
pub target64: u64,
pub share_target64: u64,
pub seeds: EpochSeeds,
}
/// The verdict on one share, with the hash the pool computed and the time it took.
#[derive(Clone, Copy, Debug)]
pub struct Verdict {
pub code: Code,
pub hash: u64,
/// The share is also a block (hash at or below the block target)
pub block: bool,
pub cost_ms: f64,
}
/// Evaluates the share's lane on the CPU. `claimed` is the member's `hash` field.
pub fn check(epoch: &EpochRef, job: &JobKey, nonce: u64, claimed: u64) -> Verdict {
let t = Instant::now();
let hash = epoch.hash_bound(&job.prehash, nonce);
let cost_ms = t.elapsed().as_secs_f64() * 1e3;
let code = if hash != claimed {
Code::WrongHash
} else if hash > job.share_target64 {
Code::AboveTarget
} else {
Code::Ok
};
Verdict { code, hash, block: code == Code::Ok && hash <= job.target64, cost_ms }
}
/// The engine's program and cache for a job's seeds (built once per pair, shared afterwards).
pub fn epoch_for(engine: &IgneumEngine, seeds: &EpochSeeds) -> Arc<EpochRef> {
engine.epoch_for(seeds)
}
#[cfg(test)]
mod tests {
use super::*;
use kaspa_hashes::Hash;
fn job(epoch: &EpochRef, prehash: [u8; 32], nonce: u64, seeds: EpochSeeds) -> (JobKey, u64) {
let h = epoch.hash_bound(&prehash, nonce);
(JobKey { prehash, target64: h / 2, share_target64: h, seeds }, h)
}
/// A known-good share (the member's hash equals the pool's evaluation and sits at the share target) is accepted;
/// a known-bad one (a wrong hash claim, or a hash above the share target) is refused with the spec's code.
#[test]
fn known_good_and_known_bad_shares() {
let engine = IgneumEngine::new();
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;
let (key, h) = job(&epoch, prehash, nonce, seeds);
let good = check(&epoch, &key, nonce, h);
assert_eq!(good.code, Code::Ok);
assert_eq!(good.hash, h);
assert!(!good.block, "target64 is below the hash: a share, not a block");
assert!(good.cost_ms > 0.0 && good.cost_ms < 1000.0, "{}", good.cost_ms);
// wrong hash claim: a worker fault
assert_eq!(check(&epoch, &key, nonce, h ^ 1).code, Code::WrongHash);
// the right hash on a tighter share target
let tight = JobKey { share_target64: h.saturating_sub(1), ..key.clone() };
assert_eq!(check(&epoch, &tight, nonce, h).code, Code::AboveTarget);
// the same nonce on another template hashes differently and is refused as a wrong hash
let other = JobKey { prehash: [10u8; 32], ..key.clone() };
assert_eq!(check(&epoch, &other, nonce, h).code, Code::WrongHash);
// a share at or below the block target is a block
let block_key = JobKey { target64: h, ..key };
assert!(check(&epoch, &block_key, nonce, h).block);
}
/// Lane arithmetic: the pool evaluates the lane `nonce AND 31` of the group `nonce AND NOT 31` (spec 9.8 item 1);
/// the 32 lanes of one group hash differently, so a nonce from the next lane is not the same share.
#[test]
fn lanes_of_one_group_are_distinct_shares() {
let engine = IgneumEngine::new();
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;
let a = epoch.hash_bound(&prehash, base);
let b = epoch.hash_bound(&prehash, base + 1);
assert_ne!(a, b);
let key = JobKey { prehash, target64: 0, share_target64: u64::MAX, seeds };
assert_eq!(check(&epoch, &key, base + 1, a).code, Code::WrongHash);
assert_eq!(check(&epoch, &key, base + 1, b).code, Code::Ok);
}
}
#[cfg(test)]
mod cost {
use super::*;
use kaspa_hashes::Hash;
/// Prints the share check cost over 500 shares (run under the measure lock with --nocapture for the number).
#[test]
fn share_check_cost_ms() {
let engine = IgneumEngine::new();
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();
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
let mean = v.iter().sum::<f64>() / v.len() as f64;
println!("SHARE_CHECK_MS n={} mean={:.3} p50={:.3} p99={:.3} max={:.3}", v.len(), mean, v[250], v[495], v[499]);
assert!(mean < 50.0);
}
}