//! 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 { 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 = (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::() / 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); } }