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>
145 lines
5.7 KiB
Rust
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);
|
|
}
|
|
}
|