diff --git a/igneum-pow/src/emit.rs b/igneum-pow/src/emit.rs index 77029f280..a788c25a2 100644 --- a/igneum-pow/src/emit.rs +++ b/igneum-pow/src/emit.rs @@ -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=).\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()))); diff --git a/igneum-pow/src/generator.rs b/igneum-pow/src/generator.rs index bcd8bec25..ce68973ec 100644 --- a/igneum-pow/src/generator.rs +++ b/igneum-pow/src/generator.rs @@ -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>, pub instrs: Vec, } @@ -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 { + 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 { + 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 { + 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"); diff --git a/igneum-pow/src/lib.rs b/igneum-pow/src/lib.rs index 2c51a6208..1f95e11a6 100644 --- a/igneum-pow/src/lib.rs +++ b/igneum-pow/src/lib.rs @@ -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}; diff --git a/igneum-pow/src/packcheck.rs b/igneum-pow/src/packcheck.rs index fdcd185ab..5bd4240c2 100644 --- a/igneum-pow/src/packcheck.rs +++ b/igneum-pow/src/packcheck.rs @@ -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, } /// 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 { /// 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 { + 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, + want_era: Option<&[u8]>, +) -> Result { + 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 { + 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, want_era: Option<&[u8]>) -> Result { 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(_)))); + } } diff --git a/igneum-pow/src/verify.rs b/igneum-pow/src/verify.rs index fedfe1b91..2758d58fe 100644 --- a/igneum-pow/src/verify.rs +++ b/igneum-pow/src/verify.rs @@ -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)