igneum/igneum-pow/tests/recheck.rs

143 lines
8.6 KiB
Rust

//! 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");
// the amended class v4 (AP-F8-1, AP-F8-3): sub-version 3 (object byte 7; the acceptance executes the shadow block)
// is the pinned id below; c120d7963abdcd96 (the 6 October stream, byte 4), 1a4230699a6b9c60 (sub-version 1, the
// one-writer rule, 0.3.20's and 0.3.21's byte 5) and a788661687db4bb3 (sub-version 2, never shipped) must differ
assert_ne!(v4.reference.program.program_id(), 0xc120_d796_3abd_cd96, "the pre-amendment v4 id must differ");
assert_ne!(v4.reference.program.program_id(), 0x1a42_3069_9a6b_9c60, "the sub-version-1 id must differ");
assert_ne!(v4.reference.program.program_id(), 0xa788_6616_87db_4bb3, "the sub-version-2 id must differ");
assert_eq!(v4.reference.program.program_id(), 0xa785_0016_87d8_688a, "the sub-version-3 v4 id as pinned");
assert_ne!(v3.reference.program.program_id(), v4.reference.program.program_id());
}