143 lines
8.6 KiB
Rust
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());
|
|
}
|