igneum/igneum-pow/tests/packs.rs
igneum-labs fdcab858e3 Lottery hash: generator version 2 (16 load slots, fresh sources, acceptance rule), every vector re-cut, packs regenerated, three workers re-checked, 20,000-program census
igneum-pow 0.2.0: generator v2 draws exactly 16 load slots from instructions 1..63, a
load's source from the registers written earlier and not read by a load since, the other
48 ops from the ten non-load weights; accept.rs is spec 01 section 1.4.6 (static: no
stale load source, every register injected; dynamic: 64 units on the seed-keyed
closed-form dataset, no constant bit, no lane-constant site, under 164 saturated, bias
within 136 of 1024, distinct addresses above 245,760); a rejected candidate is replaced
by the next attempt of the seed (seed || k_le32), 32 a consensus fault. Packs carry the
generator version, attempt and program id. Version 1 kept as generate_v1 for the census.

Packs: igneum-genesis, igneum-hourly, igneum-genesis-mh regenerated by igneum-pow export;
new igneum-devnet-v4-epoch0 (devnet genesis hash, day bytes 20730). Checks: Rust 39 of
39 tests; Metal natively via the Swift port (export cross-check 3 of 3 warps, identical
programs and vectors on five seeds incl. three with attempt 1, fuzz 2,000 of 2,000);
CUDA emu 4 of 4 packs; OpenCL emu 2 packs x 2 configurations; Apple OpenCL 4 of 4 packs
at 27.9 Mhash/s. Census 20,000: 5.225 percent rejected, accepted distinct mean 127.887.

Spec 01 0.2 (1.4.2, 1.4.3, 1.4.6, 1.11, 1.15, 1.16, 1.17), igneum-pow README, the CUDA,
OpenCL and Metal test notes, bench-log entry, ledger M5 and M6 Fixed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 07:52:40 +00:00

352 lines
15 KiB
Rust

//! The checked-in packs under proto-cuda/packs/ against this crate (their source since generator version 2,
//! 4 October 2026): every program instruction by instruction from its seed bytes, the generator version, attempt
//! and program id, the dataset and cache self-test words, the 96 hash vectors per pack, and every emitted file
//! byte for byte. A pack that drifts from the emitters, or a generator change that moves a vector, fails here.
//!
//! Packs: igneum-genesis-mh and igneum-devnet-v4-epoch0 (memory-hard; the latter from the devnet genesis hash as
//! the epoch seed and the day bytes of 2026-10-04), igneum-genesis and igneum-hourly (closed-form dataset,
//! interpreter regression only).
use igneum_pow::accept;
use igneum_pow::emit::{
cuda_kernel, cuda_kernel_bound, cuda_memhard_header, export_pack, metal_memhard, metal_program,
metal_program_bound, opencl_kernel, opencl_kernel_bound, program_header, program_json, LoadSource,
};
use igneum_pow::generator::{generate_from_seed_bytes, Op, GENERATOR_VERSION, LOAD_SLOTS};
use igneum_pow::memhard::CACHE_WORDS;
use igneum_pow::verify::{DatasetMode, DatasetSource, Epoch};
use serde_json::Value;
use std::path::PathBuf;
use std::sync::OnceLock;
const PACKS: [&str; 4] = ["igneum-genesis-mh", "igneum-devnet-v4-epoch0", "igneum-genesis", "igneum-hourly"];
fn packs_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../proto-cuda/packs")
}
fn read(pack: &str, file: &str) -> String {
let p = packs_dir().join(pack).join(file);
std::fs::read_to_string(&p).unwrap_or_else(|e| panic!("read {}: {e}", p.display()))
}
fn json(pack: &str, file: &str) -> Value {
serde_json::from_str(&read(pack, file)).unwrap_or_else(|e| panic!("{pack}/{file}: {e}"))
}
fn hex32(v: &Value) -> u32 {
u32::from_str_radix(v.as_str().unwrap().trim_start_matches("0x"), 16).unwrap()
}
fn hex64(v: &Value) -> u64 {
u64::from_str_radix(v.as_str().unwrap().trim_start_matches("0x"), 16).unwrap()
}
fn unhex(v: &Value) -> Vec<u8> {
igneum_pow::bind::unhex(v.as_str().unwrap()).unwrap()
}
/// The epoch a pack describes, rebuilt from program.json alone: the program from `seed_bytes`, the dataset from
/// `dataset.day_bytes` in the pack's mode and size. Memory-hard packs fill a 256 MiB cache (about 0.2 s), so
/// each is built once.
fn epoch(pack: &str) -> &'static Epoch {
static E: OnceLock<Vec<(String, Epoch)>> = OnceLock::new();
let all = E.get_or_init(|| {
PACKS
.iter()
.map(|p| {
let j = json(p, "program.json");
let seed = j["seed"].as_str().unwrap();
let seed_bytes = unhex(&j["seed_bytes"]);
let day_bytes = unhex(&j["dataset"]["day_bytes"]);
let mode = match j["dataset_mode"].as_str().unwrap() {
"memory-hard" => DatasetMode::MemoryHard,
_ => DatasetMode::ClosedForm,
};
let log2 = j["dataset"]["log2_words"].as_u64().unwrap() as u32;
let program = generate_from_seed_bytes(seed, &seed_bytes);
let mut dataset =
DatasetSource::from_key(igneum_pow::seed::seed_words_from_bytes(&day_bytes), mode, log2);
dataset.key_bytes = day_bytes;
(p.to_string(), Epoch { program, dataset })
})
.collect()
});
&all.iter().find(|(n, _)| n == pack).unwrap().1
}
fn day_label(pack: &str) -> String {
json(pack, "program.json")["dataset"]["day"].as_str().unwrap().to_string()
}
fn check_program_json(pack: &str) {
let j = json(pack, "program.json");
let p = &epoch(pack).program;
assert_eq!(j["format"].as_str().unwrap(), "igneum-program-pack-3");
assert_eq!(j["generator"].as_u64().unwrap() as u32, GENERATOR_VERSION, "{pack}: generator version");
assert_eq!(j["attempt"].as_u64().unwrap() as u32, p.attempt, "{pack}: attempt");
assert_eq!(hex64(&j["program_id"]), p.program_id(), "{pack}: program id");
let sw: Vec<u32> = j["seed_words"].as_array().unwrap().iter().map(hex32).collect();
assert_eq!(p.seed.to_vec(), sw, "{pack}: seed words");
assert_eq!(p.loads_per_hash() as u64, j["loads_per_hash"].as_u64().unwrap(), "{pack}: loads per hash");
assert_eq!(p.loads_per_hash(), 8 * LOAD_SLOTS);
assert!(accept::check(p).is_ok(), "{pack}: the pack's program passes the acceptance rule");
let instrs = j["instructions"].as_array().unwrap();
assert_eq!(instrs.len(), p.instrs.len(), "{pack}: instruction count");
for (k, (ins, ji)) in p.instrs.iter().zip(instrs).enumerate() {
assert_eq!(ji["i"].as_u64().unwrap() as usize, k);
assert_eq!(Op::from_name(ji["op"].as_str().unwrap()).unwrap(), ins.op, "{pack} #{k} op");
assert_eq!(ji["dst"].as_u64().unwrap(), ins.dst as u64, "{pack} #{k} dst");
assert_eq!(ji["src"].as_u64().unwrap(), ins.src as u64, "{pack} #{k} src");
assert_eq!(ji["src2"].as_u64().unwrap(), ins.src2 as u64, "{pack} #{k} src2");
assert_eq!(hex32(&ji["imm"]), ins.imm, "{pack} #{k} imm");
assert_eq!(hex32(&ji["imm2"]), ins.imm2, "{pack} #{k} imm2");
assert_eq!(ji["rot"].as_u64().unwrap(), ins.rot as u64, "{pack} #{k} rot");
assert_eq!(ji["bit"].as_u64().unwrap(), ins.bit as u64, "{pack} #{k} bit");
assert_eq!(ji["mask"].as_u64().unwrap(), ins.mask as u64, "{pack} #{k} mask");
}
let mix = j["op_mix"].as_object().unwrap();
for (name, count) in p.histogram() {
assert_eq!(mix[name].as_u64().unwrap() as usize, count, "{pack}: op_mix {name}");
}
assert_eq!(mix.len(), p.histogram().len());
}
#[test]
fn program_json_matches_all_packs() {
for pack in PACKS {
check_program_json(pack);
}
}
/// The genesis program of generator v2 (spec 01 section 1.4.3 vector).
#[test]
fn genesis_program_shape() {
let p = &epoch("igneum-genesis-mh").program;
assert_eq!(p.attempt, 0);
assert_eq!(p.op_mix(), "load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1");
assert_eq!(p.program_id(), 0xbcc1248b10cc90f2);
assert_eq!(&epoch("igneum-genesis").program, p, "closed-form and memory-hard packs share the program");
}
#[test]
fn mixer_params_match_pack() {
for pack in ["igneum-genesis-mh", "igneum-devnet-v4-epoch0"] {
let j = json(pack, "program.json");
let mp = &epoch(pack).dataset.memhard().unwrap().params;
let key: Vec<u32> = j["dataset"]["key"].as_array().unwrap().iter().map(hex32).collect();
assert_eq!(mp.key.to_vec(), key);
let rot: Vec<u32> =
j["dataset"]["mixer"]["rot"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap() as u32).collect();
assert_eq!(mp.rot.to_vec(), rot);
let mul: Vec<u32> = j["dataset"]["mixer"]["mul"].as_array().unwrap().iter().map(hex32).collect();
assert_eq!(mp.mul.to_vec(), mul);
let rc: Vec<u32> = j["dataset"]["mixer"]["rc"].as_array().unwrap().iter().map(hex32).collect();
assert_eq!(mp.rc.to_vec(), rc);
assert_eq!(hex32(&j["dataset"]["d0"]), mp.key[0]);
assert_eq!(hex32(&j["dataset"]["d1"]), mp.key[1]);
}
}
#[test]
fn cache_matches_vectors() {
let v = json("igneum-genesis-mh", "vectors.json");
let m = epoch("igneum-genesis-mh").dataset.memhard().unwrap();
let w = m.cache.words();
assert_eq!(w.len(), CACHE_WORDS);
let head: Vec<u32> = v["cache_head"].as_array().unwrap().iter().map(hex32).collect();
assert_eq!(&w[..16], &head[..]);
let last: Vec<u32> = v["cache_last_line"].as_array().unwrap().iter().map(hex32).collect();
assert_eq!(&w[CACHE_WORDS - 16..], &last[..]);
assert_eq!(m.cache.fnv1a64(), 0x48c4f5bf24166b2e, "cache FNV-1a 64 of day 2026-10-03 (MEMHARD.md), unchanged by v2");
assert_eq!(m.cache.fnv1a64(), hex64(&v["cache_fnv1a64"]));
let v = json("igneum-devnet-v4-epoch0", "vectors.json");
let m = epoch("igneum-devnet-v4-epoch0").dataset.memhard().unwrap();
assert_eq!(m.cache.fnv1a64(), hex64(&v["cache_fnv1a64"]));
assert_eq!(m.cache.fnv1a64(), 0x448274a57f508cbc, "cache FNV-1a 64 of day bytes igneum-day/20730");
}
fn check_dataset_words(pack: &str) {
let v = json(pack, "vectors.json");
let ds = &epoch(pack).dataset;
let head: Vec<u32> = v["dataset_head"].as_array().unwrap().iter().map(hex32).collect();
for (i, h) in head.iter().enumerate() {
assert_eq!(ds.word(i as u32), *h, "{pack}: dataset[{i}]");
}
let last_index = v["dataset_last_index"].as_u64().unwrap() as u32;
assert_eq!(last_index, ds.mask);
assert_eq!(ds.word(last_index), hex32(&v["dataset_last"]), "{pack}: dataset[MASK]");
let samples = v["dataset_samples"].as_array().unwrap();
assert_eq!(samples.len(), 64);
for s in samples {
let idx = s["index"].as_u64().unwrap() as u32;
assert_eq!(ds.word(idx), hex32(&s["value"]), "{pack}: dataset[{idx}]");
}
}
#[test]
fn dataset_words_match_all_packs() {
for pack in PACKS {
check_dataset_words(pack);
}
}
/// Returns the number of hashes compared (3 warps x 32 lanes = 96).
fn check_vectors(pack: &str) -> usize {
let v = json(pack, "vectors.json");
let e = epoch(pack);
assert_eq!(v["dataset_mode"].as_str().unwrap(), e.dataset.mode().name());
assert_eq!(v["dataset_log2_words"].as_u64().unwrap() as u32, e.dataset.log2_words);
let warps = v["warps"].as_array().unwrap();
assert_eq!(warps.len(), 3);
let mut n = 0;
for w in warps {
let base = w["base_nonce"].as_u64().unwrap() as u32;
let expected: Vec<u64> = w["expected"].as_array().unwrap().iter().map(hex64).collect();
let got = e.hash_warp(base);
for lane in 0..32 {
assert_eq!(got[lane], expected[lane], "{pack}: base {base} lane {lane}");
n += 1;
}
// The single-nonce API agrees with the warp.
assert_eq!(e.hash(base + 7), expected[7]);
assert!(e.verify_block(base + 7, expected[7]));
assert!(!e.verify_block(base + 7, expected[7] - 1));
}
n
}
#[test]
fn vectors_96_per_pack() {
for pack in PACKS {
assert_eq!(check_vectors(pack), 96, "{pack}");
}
}
/// The spec 01 section 1.17 vectors (igneum-genesis-mh, generator v2).
#[test]
fn spec_vectors_genesis_mh() {
let e = epoch("igneum-genesis-mh");
let w = e.hash_warp(0);
assert_eq!(w[0], 0x42246ba99fc58e4f);
assert_eq!(w[31], 0xb08446b1f2de7793);
assert_eq!(e.hash_warp(4096)[0], 0x3d3903e310ca038f);
assert_eq!(e.hash_warp(1_000_000)[0], 0xf218c1bd58e6dfe0);
}
fn assert_same_text(pack: &str, file: &str, got: &str) {
let want = read(pack, file);
if got != want {
let (gl, wl): (Vec<&str>, Vec<&str>) = (got.lines().collect(), want.lines().collect());
for i in 0..gl.len().max(wl.len()) {
let g = gl.get(i).copied().unwrap_or("<eof>");
let w = wl.get(i).copied().unwrap_or("<eof>");
if g != w {
panic!("{pack}/{file} differs at line {}:\n pack: {w}\n rust: {g}", i + 1);
}
}
panic!("{pack}/{file} differs only in trailing bytes (len {} vs {})", got.len(), want.len());
}
}
fn check_sources(pack: &str) {
let e = epoch(pack);
let p = &e.program;
let day = day_label(pack);
let mp = e.dataset.memhard().map(|m| &m.params);
assert_same_text(pack, "kernel.cu", &cuda_kernel(p, mp));
assert_same_text(pack, "kernel_bound.cu", &cuda_kernel_bound(p, mp));
assert_same_text(pack, "program.metal", &metal_program(p, e.dataset.log2_words, LoadSource::Stored));
assert_same_text(pack, "program_bound.metal", &metal_program_bound(p, e.dataset.log2_words));
assert_same_text(pack, "kernel.cl", &opencl_kernel(p, mp));
assert_same_text(pack, "kernel_bound.cl", &opencl_kernel_bound(p, mp));
assert_same_text(pack, "program.h", &program_header(p, &day, &e.dataset));
if let Some(mp) = mp {
assert_same_text(pack, "memhard.h", &cuda_memhard_header(p, mp));
assert_same_text(pack, "memhard.metal", &metal_memhard(mp));
}
let got = program_json(p, &day, &e.dataset);
assert_same_text(pack, "program.json", &got);
let _: Value = serde_json::from_str(&got).expect("program.json is valid JSON");
// Every load in every emitted hash kernel has the masked form, and there are exactly 16 of them.
for (file, load, masked) in [
("kernel.cu", "ds[r", " & mask]"),
("kernel_bound.cu", "ds[r", " & mask]"),
("program.metal", "dataset[r", " & MASK]"),
("program_bound.metal", "dataset[r", " & MASK]"),
] {
let text = read(pack, file);
assert_eq!(text.matches(load).count(), LOAD_SLOTS, "{pack}/{file}: 16 loads");
assert_eq!(text.matches(masked).count(), LOAD_SLOTS, "{pack}/{file}: 16 masked loads");
}
}
#[test]
fn emitted_sources_match_all_packs() {
for pack in PACKS {
check_sources(pack);
}
}
/// The whole pack as `export` writes it: vectors.json and vectors.h match, and the file list is the full set.
fn check_export(pack: &str) {
let e = epoch(pack);
let v = json(pack, "vectors.json");
let source = v["source"].as_str().unwrap();
let out = export_pack(e, &day_label(pack), source);
let file = |name: &str| -> &str { &out.files.iter().find(|(n, _)| n == name).unwrap().1 };
assert_same_text(pack, "vectors.json", file("vectors.json"));
assert_same_text(pack, "vectors.h", file("vectors.h"));
let mut expected = vec![
"program.json",
"vectors.json",
"kernel.cu",
"kernel.cl",
"program.h",
"vectors.h",
"program.metal",
"program_bound.metal",
"kernel_bound.cu",
"kernel_bound.cl",
];
if e.dataset.mode() == DatasetMode::MemoryHard {
expected.extend(["memhard.h", "memhard.metal"]);
}
assert_eq!(out.files.iter().map(|(n, _)| n.as_str()).collect::<Vec<_>>(), expected);
let mut on_disk: Vec<String> = std::fs::read_dir(packs_dir().join(pack))
.unwrap()
.map(|d| d.unwrap().file_name().to_string_lossy().to_string())
.filter(|n| !n.starts_with('.'))
.collect();
on_disk.sort();
let mut want: Vec<String> = expected.iter().map(|s| s.to_string()).collect();
want.sort();
assert_eq!(on_disk, want, "{pack}: no stale file in the pack directory");
}
#[test]
fn export_pack_matches_all_packs() {
for pack in PACKS {
check_export(pack);
}
}
#[test]
fn item_is_independent_of_dataset_size() {
// MEMHARD.md 1.7: a smaller dataset is a prefix of items, so dataset[w] is the same at every size.
let big = &epoch("igneum-genesis-mh").dataset;
let m = big.memhard().unwrap();
for w in [0u32, 1, 15, 16, 17, 0x00ff_ffff, 0x03ff_ffff] {
assert_eq!(big.word(w), m.word(w));
}
}
/// The devnet pack is the chain's own derivation: epoch seed = the devnet genesis hash, day bytes = `bind::day_bytes(20730)`.
#[test]
fn devnet_pack_is_the_chain_derivation() {
let j = json("igneum-devnet-v4-epoch0", "program.json");
let genesis = igneum_pow::bind::unhex("edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07").unwrap();
assert_eq!(unhex(&j["seed_bytes"]), genesis);
assert_eq!(unhex(&j["dataset"]["day_bytes"]), igneum_pow::bind::day_bytes(20_730).to_vec());
let e = Epoch::from_seed_bytes(&genesis, &igneum_pow::bind::day_bytes(20_730), "devnet");
assert_eq!(e.program.instrs, epoch("igneum-devnet-v4-epoch0").program.instrs);
assert_eq!(e.hash_warp(0), epoch("igneum-devnet-v4-epoch0").hash_warp(0));
}