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>
352 lines
15 KiB
Rust
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));
|
|
}
|