igneum-pow: the program class seam for Counter ASIC 2.0 (ProgramClass V2/V3, V3_CLASS placeholder, Epoch::from_chain_seeds, class and era in packs)

ProgramClass { V2, V3 }: V2 is the lottery hash exactly; V3 is generator version 3 on V3_CLASS, a PLACEHOLDER
set to w16 (LoadClass::fixed(4, 16)) that the integration agent replaces with the decided class. A v3 program's
id is program_id(3, seed, attempt) (spec 01 section 1.4.6). Epoch::from_chain_seeds(epoch, day, era, class,
label) and Epoch::chain_program build what the node's engine and the miner's export use; a v3 program records
the era seed bytes (Program::era_bytes). program.h and program.json of a v3 pack carry IGNEUM_PROGRAM_CLASS and
IGNEUM_ERA_SEED_HEX beside IGNEUM_GENERATOR 3; a v2 pack is unchanged (tests/packs.rs diffs the pinned packs).
packcheck: verify_pack_texts_chain / verify_pack_dir_chain refuse a pack of the wrong class, the wrong era, or
a generator other than 2 or 3 (PackFault::WrongClass); PackIdentity carries generator, class and era.

Tests: program_classes (generator.rs), program_class_and_era_are_checked (packcheck.rs); 42 + 11 pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-05 20:25:06 +00:00
parent d28a32e403
commit 176b6f4e40
5 changed files with 273 additions and 7 deletions

View file

@ -9,7 +9,7 @@
//! One deliberate difference from the Swift: `program_json` writes the cache line mask inside the `"item"` string
//! as a bare `0x003fffff`. The Swift writes it quoted (`jhex`), which is not valid JSON.
use crate::generator::{Instr, Op, Program, GENERATOR_VERSION, INSTR_COUNT, ITERATIONS, LOAD_SLOTS};
use crate::generator::{Instr, Op, Program, ProgramClass, GENERATOR_VERSION, INSTR_COUNT, ITERATIONS, LOAD_SLOTS};
use crate::memhard::{
MixParams, CACHE_LINES_PER_SEGMENT, CACHE_LINE_MASK, CACHE_LOG2_WORDS, CACHE_SEGMENTS, CACHE_SEGMENT_LOG2_LINES,
CACHE_TAG, CACHE_WORDS, CHACHA_ROUNDS, CHACHA_SIGMA, ITEM_ROUNDS,
@ -77,6 +77,23 @@ fn wide_load_stmt(dialect: CoreDialect, d: &str, a: &str, width: u8, src: WideSo
s
}
/// The program class lines of program.h (Counter ASIC 2.0): `IGNEUM_PROGRAM_CLASS` and, when the program was drawn
/// on the chain, `IGNEUM_ERA_SEED_HEX`. Empty for every version 2 program, so the pinned packs do not change; a
/// worker reads an absent line as class v2. The generator version is `IGNEUM_GENERATOR` as before (3 for class v3).
fn program_class_header_lines(p: &Program) -> String {
if p.program_class() == ProgramClass::V2 {
return String::new();
}
let mut s = String::new();
s.push_str("// Program class v3 (Counter ASIC 2.0, docs/plans/counter-asic-2-rollout.md): generator version 3; a worker that\n");
s.push_str("// runs another class refuses this pack, and a job line names the class it wants (class=v3 era=<hex>).\n");
s.push_str(&format!("#define IGNEUM_PROGRAM_CLASS {}\n", jstr(p.program_class().name())));
if let Some(era) = &p.era_bytes {
s.push_str(&format!("#define IGNEUM_ERA_SEED_HEX {}\n", jstr(&hex_bytes(era))));
}
s
}
/// The load class lines of program.h (empty for the lottery hash, so the pinned packs do not change).
fn class_header_lines(p: &Program) -> String {
if p.class.is_v2() {
@ -1090,6 +1107,7 @@ pub fn program_header(p: &Program, day: &str, ds: &DatasetSource) -> String {
s.push_str(&format!("#define IGNEUM_LOADS_PER_HASH {}\n", p.loads_per_hash()));
s.push_str(&format!("#define IGNEUM_WIDE_LOADS_PER_HASH {}\n", p.wide_loads_per_hash()));
s.push_str(&format!("#define IGNEUM_OP_MIX {}\n", jstr(&p.op_mix())));
s.push_str(&program_class_header_lines(p));
s.push_str(&class_header_lines(p));
s.push_str(&scratch_header_lines(p));
s.push_str("// 0 = closed-form dataset (ds_elem), 1 = memory-hard cache construction (MEMHARD.md, memhard.h)\n");
@ -1273,6 +1291,12 @@ pub fn program_json(p: &Program, day: &str, ds: &DatasetSource) -> String {
s.push_str(&format!(" \"iterations\": {ITERATIONS},\n"));
s.push_str(&format!(" \"instruction_count\": {INSTR_COUNT},\n"));
s.push_str(&format!(" \"loads_per_hash\": {},\n", p.loads_per_hash()));
if p.program_class() != ProgramClass::V2 {
s.push_str(&format!(" \"program_class\": {},\n", jstr(p.program_class().name())));
if let Some(era) = &p.era_bytes {
s.push_str(&format!(" \"era_seed_bytes\": {},\n", jstr(&hex_bytes(era))));
}
}
if !p.class.is_v2() {
let c = p.width_counts();
s.push_str(&format!(" \"load_class\": {},\n", jstr(&p.class.name())));

View file

@ -158,6 +158,11 @@ pub struct Program {
pub attempt: u32,
/// The load class: [`LoadClass::V2`] for the lottery hash, another for the read-width experiment.
pub class: LoadClass,
/// The era seed bytes a class v3 chain program was drawn under (`E_n` of spec 04 section 4.4, the devnet stand-in
/// of `docs/plans/era-layout.md` section 2), recorded in the pack so a worker can check it carries the era the
/// job names. `None` for every version 2 program and every string-seed pack. The placeholder [`V3_CLASS`] does
/// not read it; the era draw of the integration branch will.
pub era_bytes: Option<Vec<u8>>,
pub instrs: Vec<Instr>,
}
@ -314,6 +319,79 @@ impl Default for LoadClass {
}
}
/// Generator version of a class v3 program (spec 01 section 1.4.6: `program_id(3, seed, attempt)`).
pub const GENERATOR_VERSION_V3: u32 = 3;
/// The program class of an epoch (Counter ASIC 2.0, 5 October 2026, `docs/plans/counter-asic-2-rollout.md`): one
/// height switch in the node, `program_class_v3_activation_daa`, rounded up to an epoch boundary, decides which
/// class an epoch's program is drawn from. V2 is the lottery hash as adopted on 4 October 2026, byte for byte.
/// V3 is generator version 3: its program id carries `generator = 3` and its load class is [`V3_CLASS`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum ProgramClass {
#[default]
V2,
V3,
}
/// PLACEHOLDER (ca2-node worker, 5 October 2026): the load class of program class v3 is set to w16 (16 loads of
/// four words, `LoadClass::fixed(4, 16)`) so the node, the miner, the packs and the fast-time gate can be built and
/// run before the Counter ASIC 2.0 measurements decide the width, the per-load mix and the scratch share
/// (`counter-asic-2-rollout.md` section 6). The integration agent on branch ca2-v3 replaces this constant with the
/// decided class; nothing else in the seam names the class, so it is one edit.
pub const V3_CLASS: LoadClass = LoadClass { mix: [0, 100, 0], load_slots: LOAD_SLOTS as u8, scratch: None, scratch_kb: 0 };
impl ProgramClass {
/// The load class this program class draws from.
pub fn load_class(&self) -> LoadClass {
match self {
ProgramClass::V2 => LoadClass::V2,
ProgramClass::V3 => V3_CLASS,
}
}
/// The generator version written into every pack and program id of this class.
pub fn generator_version(&self) -> u32 {
match self {
ProgramClass::V2 => GENERATOR_VERSION,
ProgramClass::V3 => GENERATOR_VERSION_V3,
}
}
/// The class of a generator version: 2 and 3 are the two classes, anything else is no class this crate runs.
pub fn from_generator(generator: u32) -> Option<ProgramClass> {
match generator {
GENERATOR_VERSION => Some(ProgramClass::V2),
GENERATOR_VERSION_V3 => Some(ProgramClass::V3),
_ => None,
}
}
/// "v2" or "v3": the `IGNEUM_PROGRAM_CLASS` string of a pack and the `class=` token of a job line.
pub fn name(&self) -> &'static str {
match self {
ProgramClass::V2 => "v2",
ProgramClass::V3 => "v3",
}
}
pub fn parse(s: &str) -> Option<ProgramClass> {
match s.trim() {
"v2" => Some(ProgramClass::V2),
"v3" => Some(ProgramClass::V3),
_ => None,
}
}
/// The generator version as a byte, for wire formats that carry the class as a number.
pub fn as_u8(&self) -> u8 {
self.generator_version() as u8
}
pub fn from_u8(v: u8) -> Option<ProgramClass> {
Self::from_generator(v as u32)
}
}
impl Program {
pub fn loads_per_hash(&self) -> usize {
self.instrs.iter().filter(|i| i.op.is_load()).count() * ITERATIONS
@ -370,12 +448,19 @@ impl Program {
/// || attempt_le32`. Written into every pack so a version 1 program, or another attempt of the same seed,
/// can never be mistaken for this one.
pub fn program_id(&self) -> u64 {
if self.class.is_v2() {
if self.class.is_v2() || self.generator == GENERATOR_VERSION_V3 {
// Spec 01 section 1.4.6: a class v3 program's id is `program_id(3, seed, attempt)`; the generator version
// in the preimage separates it from every version 2 program of the same seed
program_id(self.generator, &self.seed, self.attempt)
} else {
program_id_class(self.generator, &self.seed, self.attempt, &self.class)
}
}
/// The program class of this program, from its generator version (3 = v3, everything else v2).
pub fn program_class(&self) -> ProgramClass {
if self.generator == GENERATOR_VERSION_V3 { ProgramClass::V3 } else { ProgramClass::V2 }
}
}
pub fn program_id(generator: u32, seed: &[u32; 8], attempt: u32) -> u64 {
@ -555,6 +640,7 @@ pub fn candidate_from_words_class(
generator: GENERATOR_VERSION,
attempt,
class,
era_bytes: None,
instrs,
}
}
@ -615,6 +701,18 @@ pub fn generate_from_seed_bytes_class(seed_string: &str, seed_bytes: &[u8], clas
try_generate_class(seed_string, seed_bytes, class).unwrap_or_else(|e| panic!("{e}"))
}
/// The program of a program class (what the chain calls through `Epoch::from_chain_seeds`): class v2 is
/// [`generate_from_seed_bytes`] exactly; class v3 draws from [`V3_CLASS`] and stamps generator version 3 on the
/// program, so its packs and its id say generator 3 (spec 01 sections 1.4.5 and 1.4.6).
pub fn generate_from_seed_bytes_program_class(seed_string: &str, seed_bytes: &[u8], class: ProgramClass, era_bytes: Option<&[u8]>) -> Program {
let mut p = generate_from_seed_bytes_class(seed_string, seed_bytes, class.load_class());
p.generator = class.generator_version();
// The era is a property of class v3 programs; a version 2 program never records one, so the pinned v2 packs
// and every v2 export stay byte-identical whatever the chain reports for the era
p.era_bytes = if class == ProgramClass::V3 { era_bytes.map(|b| b.to_vec()) } else { None };
p
}
/// The program of a seed string (its UTF-8 bytes are the program seed).
pub fn generate(seed_string: &str) -> Program {
generate_from_seed_bytes(seed_string, seed_string.as_bytes())
@ -731,6 +829,7 @@ pub fn generate_v1_from_words(seed_string: &str, seed: [u32; 8], cfg: &Generator
generator: 1,
attempt: 0,
class: LoadClass::V2,
era_bytes: None,
instrs,
}
}
@ -912,6 +1011,41 @@ mod tests {
assert!((counts[2] as f64 / total - 0.15).abs() < 0.05, "{counts:?}");
}
/// Counter ASIC 2.0 seam: class v2 is the lottery hash exactly; class v3 is generator 3 on the placeholder load
/// class, with an id that no version 2 program of the seed can carry; the class round-trips through its name and
/// its generator number.
#[test]
fn program_classes() {
let v2 = generate_from_seed_bytes_program_class("igneum-genesis", b"igneum-genesis", ProgramClass::V2, Some(&[7u8; 32]));
let plain = generate("igneum-genesis");
assert_eq!(v2, plain, "a v2 program never records an era");
assert_eq!(v2.era_bytes, None);
assert_eq!(v2.program_class(), ProgramClass::V2);
assert_eq!(v2.generator, GENERATOR_VERSION);
let v3 = generate_from_seed_bytes_program_class("igneum-genesis", b"igneum-genesis", ProgramClass::V3, Some(&[7u8; 32]));
assert_eq!(v3.generator, GENERATOR_VERSION_V3);
assert_eq!(v3.era_bytes.as_deref(), Some(&[7u8; 32][..]));
let v3_no_era = generate_from_seed_bytes_program_class("igneum-genesis", b"igneum-genesis", ProgramClass::V3, None);
assert_eq!(v3_no_era.instrs, v3.instrs, "the placeholder class does not read the era");
assert_eq!(v3_no_era.program_id(), v3.program_id());
assert_eq!(v3.program_class(), ProgramClass::V3);
assert_eq!(v3.class, V3_CLASS);
assert!(check(&v3).is_ok());
assert_eq!(v3.program_id(), program_id(GENERATOR_VERSION_V3, &v3.seed, v3.attempt));
assert_ne!(v3.program_id(), program_id(GENERATOR_VERSION, &v3.seed, v3.attempt));
assert_ne!(v3.program_id(), v2.program_id());
for c in [ProgramClass::V2, ProgramClass::V3] {
assert_eq!(ProgramClass::parse(c.name()), Some(c));
assert_eq!(ProgramClass::from_generator(c.generator_version()), Some(c));
assert_eq!(ProgramClass::from_u8(c.as_u8()), Some(c));
}
assert_eq!(ProgramClass::from_generator(1), None);
assert_eq!(ProgramClass::from_generator(4), None);
assert_eq!(ProgramClass::parse("v4"), None);
assert_eq!(ProgramClass::default(), ProgramClass::V2);
assert_eq!(ProgramClass::V2.load_class(), LoadClass::V2);
}
#[test]
fn generate_returns_an_accepted_program() {
let p = generate("igneum-genesis");

View file

@ -32,7 +32,7 @@ pub mod verify;
pub use bind::{block_init_words, day_bytes, pow256_from_lane, target64_from_le256};
pub use accept::{check as accept_program, AcceptReport, Reject};
pub use generator::{generate, generate_from_seed_bytes, Instr, Op, Program, GENERATOR_VERSION};
pub use generator::{generate, generate_from_seed_bytes, generate_from_seed_bytes_program_class, Instr, Op, Program, ProgramClass, GENERATOR_VERSION, GENERATOR_VERSION_V3, V3_CLASS};
pub use memhard::{Cache, MemhardCpu, MixParams};
pub use seed::{fnv1a64, seed_words, SplitMix64};
pub use verify::{hash_warp, interpret_warp_init, verify_block, DatasetMode, DatasetSource, Epoch};

View file

@ -10,7 +10,7 @@
//! a later attempt. This module is the one place that rule is written in Rust; the miner checks every pack it
//! writes with it before a worker sees the pack, and the tests pin the attempt vectors the C side also pins.
use crate::generator::attempt_words;
use crate::generator::{attempt_words, ProgramClass};
use crate::seed::seed_words_from_bytes;
use std::fmt;
use std::path::Path;
@ -23,6 +23,12 @@ pub struct PackIdentity {
pub attempt: u32,
pub seedw: [u32; 8],
pub keyw: [u32; 8],
/// `IGNEUM_GENERATOR` (2 or 3; a pack without the line is generator 1, which no worker runs).
pub generator: u32,
/// The program class the generator version names (Counter ASIC 2.0).
pub class: ProgramClass,
/// `IGNEUM_ERA_SEED_HEX` when the pack carries one (class v3 chain packs).
pub era_hex: Option<String>,
}
/// Why a pack is not the one a worker should mine with. `Display` is the plain-words line the logs carry.
@ -35,6 +41,9 @@ pub enum PackFault {
/// The files of one pack contradict each other (seeds.txt against program.h, or the init words against the
/// seeds and the attempt): a half-written or hand-edited pack, or a worker and an exporter on different rules.
Disagree(String),
/// The pack is of another program class than the one the chain is on (spec 01 section 1.4.5: an implementation
/// refuses a pack whose generator version is not its own), or its era seed is not the era the job names.
WrongClass(String),
}
impl fmt::Display for PackFault {
@ -50,6 +59,7 @@ impl fmt::Display for PackFault {
day_label(want_day)
),
PackFault::Disagree(w) => write!(f, "program pack and its seeds disagree: {w}"),
PackFault::WrongClass(w) => write!(f, "program pack of the wrong class: {w}"),
}
}
}
@ -151,6 +161,44 @@ fn seeds_line(text: &str, key: &str) -> Option<String> {
/// Checks the texts of a pack (program.h, and seeds.txt when it exists) against the seeds a worker will be asked
/// to mine with. Pure: the miner and the tests call it with file contents.
pub fn verify_pack_texts(program_h: &str, seeds_txt: Option<&str>, want_epoch: &[u8], want_day: &[u8]) -> Result<PackIdentity, PackFault> {
verify_pack_texts_chain(program_h, seeds_txt, want_epoch, want_day, None, None)
}
/// [`verify_pack_texts`] that also demands a program class and, for class v3, the era seed the chain is on
/// (Counter ASIC 2.0, 5 October 2026). `want_class` `None` accepts either class; `want_era` `None` skips the era.
/// A pack whose `IGNEUM_GENERATOR` is neither 2 nor 3 is refused whatever is wanted.
pub fn verify_pack_texts_chain(
program_h: &str,
seeds_txt: Option<&str>,
want_epoch: &[u8],
want_day: &[u8],
want_class: Option<ProgramClass>,
want_era: Option<&[u8]>,
) -> Result<PackIdentity, PackFault> {
let generator = define_u32(program_h, "IGNEUM_GENERATOR").unwrap_or(1);
let Some(class) = ProgramClass::from_generator(generator) else {
return Err(PackFault::WrongClass(format!("IGNEUM_GENERATOR {generator} is not a generator version this software runs (2 or 3)")));
};
// IGNEUM_PROGRAM_CLASS, when present, must name the class the generator version names
if let Some(named) = define_str(program_h, "IGNEUM_PROGRAM_CLASS") {
if ProgramClass::parse(&named) != Some(class) {
return Err(PackFault::Disagree(format!("IGNEUM_PROGRAM_CLASS {named:?} does not match IGNEUM_GENERATOR {generator}")));
}
}
let era_hex = define_str(program_h, "IGNEUM_ERA_SEED_HEX").map(|h| h.to_ascii_lowercase());
if let Some(want) = want_class {
if want != class {
return Err(PackFault::WrongClass(format!("the pack is program class {} (generator {generator}), the chain is on class {}", class.name(), want.name())));
}
}
if let (Some(want), ProgramClass::V3) = (want_era, class) {
let want_hex = hex(want);
match &era_hex {
Some(h) if *h == want_hex => {}
Some(h) => return Err(PackFault::WrongClass(format!("the pack's era seed {} is not the era seed {} the job names", short(h), short(&want_hex)))),
None => return Err(PackFault::WrongClass("a class v3 pack without IGNEUM_ERA_SEED_HEX; the job names an era seed".into())),
}
}
let seedw = define_words(program_h, "IGNEUM_SEEDW_INIT").ok_or_else(|| PackFault::Unreadable("program.h has no IGNEUM_SEEDW_INIT with 8 words".into()))?;
let keyw = define_words(program_h, "IGNEUM_KEY_INIT").ok_or_else(|| PackFault::Unreadable("program.h has no IGNEUM_KEY_INIT with 8 words".into()))?;
let attempt = define_u32(program_h, "IGNEUM_PROGRAM_ATTEMPT").unwrap_or(0);
@ -194,14 +242,19 @@ pub fn verify_pack_texts(program_h: &str, seeds_txt: Option<&str>, want_epoch: &
if epoch_hex != want_epoch_hex || day_hex != want_day_hex {
return Err(PackFault::OutOfDate { pack_epoch: epoch_hex, pack_day: day_hex, want_epoch: want_epoch_hex, want_day: want_day_hex });
}
Ok(PackIdentity { epoch_hex, day_hex, attempt, seedw, keyw })
Ok(PackIdentity { epoch_hex, day_hex, attempt, seedw, keyw, generator, class, era_hex })
}
/// [`verify_pack_texts`] over a pack directory.
pub fn verify_pack_dir(dir: &Path, want_epoch: &[u8], want_day: &[u8]) -> Result<PackIdentity, PackFault> {
verify_pack_dir_chain(dir, want_epoch, want_day, None, None)
}
/// [`verify_pack_texts_chain`] over a pack directory.
pub fn verify_pack_dir_chain(dir: &Path, want_epoch: &[u8], want_day: &[u8], want_class: Option<ProgramClass>, want_era: Option<&[u8]>) -> Result<PackIdentity, PackFault> {
let program_h = std::fs::read_to_string(dir.join("program.h")).map_err(|e| PackFault::Unreadable(format!("cannot read {}/program.h: {e}", dir.display())))?;
let seeds = std::fs::read_to_string(dir.join("seeds.txt")).ok();
verify_pack_texts(&program_h, seeds.as_deref(), want_epoch, want_day)
verify_pack_texts_chain(&program_h, seeds.as_deref(), want_epoch, want_day, want_class, want_era)
}
#[cfg(test)]
@ -301,4 +354,44 @@ mod tests {
assert_eq!(day_label(DAY_20731), "20731");
assert_eq!(day_label("abcd"), "abcd");
}
/// Counter ASIC 2.0: a class v3 chain pack carries generator 3, the class line and the era seed; it is refused
/// when the chain wants class v2, when the era differs, and a v2 pack is refused when the chain wants v3; a
/// generator this software does not run is refused whatever is wanted.
#[test]
fn program_class_and_era_are_checked() {
let e = bytes(EPOCH_34);
let d = bytes(DAY_20731);
let era = [0x5au8; 32];
let v3 = Epoch::from_chain_seeds(&e, &d, Some(&era), ProgramClass::V3, "class test");
let h3 = program_header(&v3.program, "test day", &v3.dataset);
assert!(h3.contains("#define IGNEUM_GENERATOR 3\n"));
assert!(h3.contains("#define IGNEUM_PROGRAM_CLASS \"v3\"\n"));
assert!(h3.contains(&format!("#define IGNEUM_ERA_SEED_HEX \"{}\"\n", hex(&era))));
let id = verify_pack_texts_chain(&h3, None, &e, &d, Some(ProgramClass::V3), Some(&era)).unwrap();
assert_eq!((id.generator, id.class, id.era_hex.as_deref()), (3, ProgramClass::V3, Some(hex(&era).as_str())));
assert_eq!(id.attempt, v3.program.attempt);
assert!(verify_pack_texts(&h3, None, &e, &d).is_ok(), "no class wanted: either class passes");
let err = verify_pack_texts_chain(&h3, None, &e, &d, Some(ProgramClass::V2), None).unwrap_err();
assert!(matches!(err, PackFault::WrongClass(_)), "{err}");
assert!(err.to_string().contains("program pack of the wrong class"), "{err}");
let err = verify_pack_texts_chain(&h3, None, &e, &d, Some(ProgramClass::V3), Some(&[1u8; 32])).unwrap_err();
assert!(err.to_string().contains("era seed"), "{err}");
// the v2 pack of the same seeds: generator 2, no class line, no era line, refused when v3 is wanted
let v2 = Epoch::from_chain_seeds(&e, &d, Some(&era), ProgramClass::V2, "class test");
let h2 = program_header(&v2.program, "test day", &v2.dataset);
assert!(h2.contains("#define IGNEUM_GENERATOR 2\n"));
assert!(!h2.contains("IGNEUM_PROGRAM_CLASS") && !h2.contains("IGNEUM_ERA_SEED_HEX"));
let plain = Epoch::from_seed_bytes(&e, &d, "class test");
assert_eq!(program_header(&plain.program, "test day", &plain.dataset), h2, "class v2 from the chain is the v2 export byte for byte");
let id = verify_pack_texts_chain(&h2, None, &e, &d, Some(ProgramClass::V2), Some(&era)).unwrap();
assert_eq!((id.generator, id.class, id.era_hex), (2, ProgramClass::V2, None));
assert!(matches!(verify_pack_texts_chain(&h2, None, &e, &d, Some(ProgramClass::V3), None), Err(PackFault::WrongClass(_))));
// a generator nobody runs
let h9 = h2.replace("#define IGNEUM_GENERATOR 2\n", "#define IGNEUM_GENERATOR 9\n");
assert!(matches!(verify_pack_texts(&h9, None, &e, &d), Err(PackFault::WrongClass(_))));
// a class line that contradicts the generator
let bad = h3.replace("#define IGNEUM_PROGRAM_CLASS \"v3\"\n", "#define IGNEUM_PROGRAM_CLASS \"v2\"\n");
assert!(matches!(verify_pack_texts(&bad, None, &e, &d), Err(PackFault::Disagree(_))));
}
}

View file

@ -1,7 +1,7 @@
//! The CPU reference interpreter for one 32-lane warp (`cpuWarpTraced` in the Swift) and the API the node
//! calls. Dataset words come from the memory-hard cache (default) or from the closed form (old packs).
use crate::generator::{generate, generate_class, Instr, LoadClass, Op, Program, ITERATIONS, LANES};
use crate::generator::{generate, generate_class, Instr, LoadClass, Op, Program, ProgramClass, ITERATIONS, LANES};
use crate::memhard::MemhardCpu;
use crate::seed::day_key;
@ -449,6 +449,21 @@ impl Epoch {
Self { program, dataset }
}
/// The chain's shape with the program class (Counter ASIC 2.0, 5 October 2026): what the node's engine and the
/// miner's pack export build from the seeds a block template carries. Class v2 is [`Epoch::from_seed_bytes`]
/// exactly (the era bytes are ignored and not recorded); class v3 draws from [`crate::generator::V3_CLASS`]
/// with generator version 3 and records the era seed bytes (`E_n`) in the program for the pack.
pub fn from_chain_seeds(epoch_seed: &[u8], day_bytes: &[u8], era_bytes: Option<&[u8]>, class: ProgramClass, label: &str) -> Self {
let mut epoch = Self::from_seed_bytes_class(epoch_seed, day_bytes, label, class.load_class());
epoch.program = Self::chain_program(epoch_seed, era_bytes, class, label);
epoch
}
/// The program alone of [`Epoch::from_chain_seeds`] (no cache fill): for an engine that shares the day's cache.
pub fn chain_program(epoch_seed: &[u8], era_bytes: Option<&[u8]>, class: ProgramClass, label: &str) -> Program {
crate::generator::generate_from_seed_bytes_program_class(label, epoch_seed, class, era_bytes)
}
/// The 32 hashes of the warp starting at `base_nonce`.
pub fn hash_warp(&self, base_nonce: u32) -> [u64; LANES] {
hash_warp(&self.program, base_nonce, &self.dataset)