igneum/igneum-pow/tests/derivation.rs

142 lines
8.3 KiB
Rust

//! Every pinned pack's program id re-derived from its own `program_id_derivation` text (the documentation finding of
//! 7 October 2026, the in-house pass adv-accept-3: program.json and spec 1.4.6 stated the plain form while generator 4
//! appends `'sub/' || sub_version_le16`, so a client written from the text derived 30956569d8f3d8d7 for Devnet 3's epoch
//! 0 where the chain says fce15bf61030be57). The text is parsed part by part and the bytes come from the pack's own fields
//! (generator, attempt, seed_words, sub_version, load_class), never from the code's id function, so the test fails the
//! moment the printed derivation and the hashed bytes disagree.
use igneum_pow::generator::LoadClass;
use serde_json::Value;
use std::path::{Path, PathBuf};
fn fnv1a64(b: &[u8]) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for &x in b {
h ^= x as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
h
}
fn hex64(v: &Value) -> u64 {
u64::from_str_radix(v.as_str().unwrap().trim_start_matches("0x"), 16).unwrap()
}
/// The bytes a derivation part names, from the pack's own fields.
fn part_bytes(part: &str, j: &Value, class: Option<&LoadClass>) -> Vec<u8> {
if let Some(lit) = part.strip_prefix('\'').and_then(|p| p.strip_suffix('\'')) {
return lit.as_bytes().to_vec();
}
let c = || class.unwrap_or_else(|| panic!("part {part} needs the pack's load_class"));
match part {
"generator_le32" => (j["generator"].as_u64().unwrap() as u32).to_le_bytes().to_vec(),
"attempt_le32" => (j["attempt"].as_u64().unwrap() as u32).to_le_bytes().to_vec(),
"seed_words as little-endian bytes" => j["seed_words"].as_array().unwrap().iter().flat_map(|w| (hex64(w) as u32).to_le_bytes()).collect(),
"sub_version_le16" => (j["sub_version"].as_u64().expect("a generator-4 pack carries sub_version") as u16).to_le_bytes().to_vec(),
"mix[3]" => c().mix.to_vec(),
"load_slots_u8" => vec![c().load_slots],
"scratch_k_u8" => vec![c().scratch.unwrap()],
"scratch_kb_u8" => vec![c().scratch_kb],
"mixer_mult_u8" => vec![c().mixer_mult],
"growth_u8" => vec![c().growth as u8],
"derive_len_le16" => c().derive_len.to_le_bytes().to_vec(),
"shadow_instrs_le16" => c().shadow.unwrap().instrs.to_le_bytes().to_vec(),
"shadow_reps_le16" => c().shadow.unwrap().reps.to_le_bytes().to_vec(),
"hot_mb_u8" => vec![c().hot.unwrap().mb],
"hot_k_u8" => vec![c().hot.unwrap().k],
other => panic!("derivation part {other:?} is not one this test knows; teach it the field before pinning such a pack"),
}
}
fn rederive(dir: &Path) -> (u64, u64, String) {
let j: Value = serde_json::from_str(&std::fs::read_to_string(dir.join("program.json")).unwrap()).unwrap();
let text = j["program_id_derivation"].as_str().unwrap().to_string();
let parts = text.strip_prefix("FNV-1a 64 over ").unwrap_or_else(|| panic!("{}: the derivation does not start with 'FNV-1a 64 over '", dir.display()));
let class = j["load_class"].as_str().and_then(LoadClass::parse);
let mut b = Vec::new();
for part in parts.split(" || ") {
b.extend(part_bytes(part.trim(), &j, class.as_ref()));
}
(fnv1a64(&b), hex64(&j["program_id"]), text)
}
fn pinned_packs() -> Vec<PathBuf> {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../proto-cuda");
let mut out = Vec::new();
for sub in ["packs-ca3-v4", "packs-ca3-shadow"] {
let mut v: Vec<_> = std::fs::read_dir(root.join(sub)).unwrap().map(|e| e.unwrap().path()).filter(|p| p.join("program.json").is_file()).collect();
v.sort();
out.extend(v);
}
out.push(root.join("packs/igneum-devnet-v4-epoch0"));
assert!(out.len() >= 18, "the pinned packs are missing; this test never skips ({} found)", out.len());
out
}
#[test]
fn every_pinned_pack_id_rederives_from_its_own_derivation_text() {
let mut seen_sub = false;
for dir in pinned_packs() {
let (derived, pinned, text) = rederive(&dir);
assert_eq!(derived, pinned, "{}: the id re-derived from \"{text}\" is {derived:016x}, the pack says {pinned:016x}", dir.display());
if text.contains("'sub/'") {
seen_sub = true;
}
}
assert!(seen_sub, "at least one generator-4 pack states the 'sub/' || sub_version_le16 suffix");
}
#[test]
fn the_class_v4_derivation_states_the_suffix_and_the_plain_text_derives_another_id() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../proto-cuda/packs-ca3-v4/v4-devnet-epoch0");
let j: Value = serde_json::from_str(&std::fs::read_to_string(root.join("program.json")).unwrap()).unwrap();
let text = j["program_id_derivation"].as_str().unwrap();
assert!(text.ends_with("attempt_le32 || 'sub/' || sub_version_le16"), "{text}");
// the known-failed case: the text as printed before 7 October 2026 (no suffix) derives a different id
let plain = "FNV-1a 64 over 'igneum-program/' || generator_le32 || seed_words as little-endian bytes || attempt_le32";
let mut b = Vec::new();
for part in plain.strip_prefix("FNV-1a 64 over ").unwrap().split(" || ") {
b.extend(part_bytes(part, &j, None));
}
assert_ne!(fnv1a64(&b), hex64(&j["program_id"]), "the plain text must not derive the pinned id (it did before the fix)");
assert_eq!(hex64(&j["program_id"]), 0xa785_0016_87d8_688a);
}
/// The record maker (ignored in the suite): prints `DERIVATION <dir> <id> <text>` for every pack directory under proto-cuda,
/// the program rebuilt as the pack tests rebuild it (generator 3 with era bytes under class v2's era layout for packs-ca2-era,
/// generator 3 through the v3 seam otherwise, generator 2 with its load class, plain generator 2), so program.json's line can
/// be set from the code's own recipe after a derivation change. `cargo test --release --test derivation -- --ignored --nocapture`.
#[test]
#[ignore]
fn print_every_pack_derivation() {
use igneum_pow::generator::{generate_era, generate_from_seed_bytes, generate_from_seed_bytes_class, generate_from_seed_bytes_program_class, ProgramClass, GENERATOR_VERSION_V3};
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../proto-cuda");
let mut dirs: Vec<PathBuf> = std::fs::read_dir(&root).unwrap().map(|e| e.unwrap().path()).filter(|p| p.file_name().unwrap().to_str().unwrap().starts_with("packs")).flat_map(|g| std::fs::read_dir(g).unwrap().map(|e| e.unwrap().path())).filter(|p| p.join("program.json").is_file()).collect();
dirs.sort();
for dir in dirs {
let j: Value = serde_json::from_str(&std::fs::read_to_string(dir.join("program.json")).unwrap()).unwrap();
let seed = j["seed"].as_str().unwrap();
let seed_bytes = igneum_pow::bind::unhex(j["seed_bytes"].as_str().unwrap()).unwrap();
let gen = j["generator"].as_u64().unwrap() as u32;
let era = j.get("era_seed_bytes").and_then(|v| v.as_str()).map(|h| igneum_pow::bind::unhex(h).unwrap());
let lc = j.get("load_class").and_then(|c| c.as_str());
let program = if gen == GENERATOR_VERSION_V3 && dir.parent().unwrap().file_name().unwrap() == "packs-ca2-era" {
let allowed: Vec<u8> = j["era"]["allowed_widths"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap() as u8).collect();
generate_era(seed, &seed_bytes, LoadClass::V2, era.as_deref().unwrap(), &allowed)
} else if gen == GENERATOR_VERSION_V3 {
generate_from_seed_bytes_program_class(seed, &seed_bytes, ProgramClass::V3, era.as_deref())
} else if gen == 4 {
generate_from_seed_bytes_program_class(seed, &seed_bytes, ProgramClass::V4, era.as_deref())
} else if let Some(c) = lc {
let Some(class) = LoadClass::parse(c) else {
println!("SKIP {} load_class {c:?} is a name the parser no longer accepts (an old experiment's pack); its derivation line stands as written", dir.strip_prefix(&root).unwrap().display());
continue;
};
generate_from_seed_bytes_class(seed, &seed_bytes, class)
} else {
generate_from_seed_bytes(seed, &seed_bytes)
};
println!("DERIVATION {} {:016x} {}", dir.strip_prefix(&root).unwrap().display(), program.program_id(), program.program_id_derivation());
assert_eq!(program.program_id(), hex64(&j["program_id"]), "{}: the rebuilt program's id is the pack's", dir.display());
}
}