Counter ASIC 3.0 gates (node): class v4 = mx8+sh256x27 through the stack (igneum-pow ProgramClass::V4, V4_CLASS, generator 4, program_id(4, seed, attempt), the era composed as v3's; program.h and program.json class v4; packcheck, packfile.h, the CUDA and OpenCL identity rule and the Metal worker accept generator 4 and the class=v4 token, the Metal worker takes v4 from the prepared pack only; v2 and v3 byte-identical, the pinned packs diffed); the G4 harness class-v4.mjs (two switches, the never case); override-60x.json: the v4 field at never, the duplicated proving v1 block removed, the four 0.3.12/0.3.13 fields added; CI check override-json-check.sh (no duplicate key in any override file)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-josh 2026-10-06 16:58:49 +01:00
parent 3213ee9942
commit a522d046e1
13 changed files with 562 additions and 69 deletions

View file

@ -69,6 +69,8 @@ jobs:
run: bash tools/ci/no-conflict-markers.sh run: bash tools/ci/no-conflict-markers.sh
- name: copied sources are re-stamped before a build - name: copied sources are re-stamped before a build
run: bash tools/ci/copied-sources-check.sh run: bash tools/ci/copied-sources-check.sh
- name: override params files parse with no duplicate key (the duplicate-field class, 6 October 2026)
run: bash tools/ci/override-json-check.sh
- name: the signer is never piped into head - name: the signer is never piped into head
run: bash tools/ci/signer-pipe-check.sh run: bash tools/ci/signer-pipe-check.sh
- name: bash bodies in PowerShell job scripts pass bash -n, the lost-quote class (self-test first, then the tree) - name: bash bodies in PowerShell job scripts pass bash -n, the lost-quote class (self-test first, then the tree)

View file

@ -157,14 +157,21 @@ fn wide_load_stmt(dialect: CoreDialect, d: &str, idx: &str, width: u8, src: Wide
/// The program class lines of program.h (Counter ASIC 2.0): `IGNEUM_PROGRAM_CLASS` and, when the program was drawn /// 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 /// 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). /// worker reads an absent line as class v2. The generator version is `IGNEUM_GENERATOR` as before (3 for class v3,
/// 4 for class v4; the v3 comment lines are byte for byte the 0.3.11 ones, so the pinned v3 packs do not change).
fn program_class_header_lines(p: &Program) -> String { fn program_class_header_lines(p: &Program) -> String {
if p.program_class() == ProgramClass::V2 { if p.program_class() == ProgramClass::V2 {
return String::new(); return String::new();
} }
let mut s = 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"); if p.program_class() == ProgramClass::V4 {
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("// Program class v4 (Counter ASIC 3.0, docs/plans/counter-asic-3-node.md): generator version 4, class v3 plus the\n");
s.push_str("// latency-shadow block (IGNEUM_SHADOW_INSTRS x IGNEUM_SHADOW_REPS per iteration); a worker that runs another class\n");
s.push_str("// refuses this pack, and a job line names the class it wants (class=v4 era=<hex>).\n");
} else {
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()))); s.push_str(&format!("#define IGNEUM_PROGRAM_CLASS {}\n", jstr(p.program_class().name())));
if let Some(era) = &p.era_bytes { if let Some(era) = &p.era_bytes {
s.push_str(&format!("#define IGNEUM_ERA_SEED_HEX {}\n", jstr(&hex_bytes(era)))); s.push_str(&format!("#define IGNEUM_ERA_SEED_HEX {}\n", jstr(&hex_bytes(era))));

View file

@ -774,15 +774,21 @@ impl Default for LoadClass {
/// Generator version of a class v3 program (spec 01 section 1.4.6: `program_id(3, seed, attempt)`). /// 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; pub const GENERATOR_VERSION_V3: u32 = 3;
/// Generator version of a class v4 program (Counter ASIC 3.0, 6 October 2026, PROPOSED: `program_id(4, seed, attempt)`).
pub const GENERATOR_VERSION_V4: u32 = 4;
/// The program class of an epoch (Counter ASIC 2.0, 5 October 2026, `docs/plans/counter-asic-2-rollout.md`): one /// 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 /// 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. /// 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`]. /// V3 is generator version 3: its program id carries `generator = 3` and its load class is [`V3_CLASS`].
/// V4 (Counter ASIC 3.0, 6 October 2026, the candidate `mx8+sh256x27` behind `program_class_v4_activation_daa`) is
/// generator version 4: its program id carries `generator = 4` and its load class is [`V4_CLASS`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum ProgramClass { pub enum ProgramClass {
#[default] #[default]
V2, V2,
V3, V3,
V4,
} }
/// The load class of program class v3, decided 5 October 2026 (Counter ASIC 2.0, `docs/plans/counter-asic-2-status.md` /// The load class of program class v3, decided 5 October 2026 (Counter ASIC 2.0, `docs/plans/counter-asic-2-status.md`
@ -793,6 +799,13 @@ pub enum ProgramClass {
/// [`generate_from_seed_bytes_program_class`] (`LoadClass::era(V3_CLASS, era, &V3_ALLOWED)`); here `era` is `None`. /// [`generate_from_seed_bytes_program_class`] (`LoadClass::era(V3_CLASS, era, &V3_ALLOWED)`); here `era` is `None`.
pub const V3_CLASS: LoadClass = LoadClass { era: None, hot: None, ..LoadClass::MX8 }; pub const V3_CLASS: LoadClass = LoadClass { era: None, hot: None, ..LoadClass::MX8 };
/// The load class of program class v4 (Counter ASIC 3.0 item 8, `docs/analysis/latency-shadow-2026-10-06.md`, the
/// candidate of 6 October 2026, gated by `docs/plans/counter-asic-3-node.md`): class v3 plus the latency-shadow block
/// of 256 ALU instructions run 27 times per iteration ("mx8+sh256x27", 55,296 shadow instructions per hash). The
/// base program, the 16 loads, the item construction, the cache growth rule and the era draw are class v3's, draw
/// for draw, so a v4 epoch's day cache and dataset are the v3 day's. Composed with the era exactly as V3 is.
pub const V4_CLASS: LoadClass = LoadClass { shadow: Some(ShadowClass { instrs: 256, reps: 27 }), ..V3_CLASS };
/// The width set class v3's era draw chooses from: 4 bytes only (the read-width decision of 5 October 2026; the /// The width set class v3's era draw chooses from: 4 bytes only (the read-width decision of 5 October 2026; the
/// draw is consumed, so widening the set at genesis keeps the derivation). /// draw is consumed, so widening the set at genesis keeps the derivation).
pub const V3_ALLOWED: [u8; 1] = [1]; pub const V3_ALLOWED: [u8; 1] = [1];
@ -801,8 +814,14 @@ pub const V3_ALLOWED: [u8; 1] = [1];
/// over the width set `allowed`, generator 3 stamped and the era bytes recorded (`docs/plans/era-layout.md`). /// over the width set `allowed`, generator 3 stamped and the era bytes recorded (`docs/plans/era-layout.md`).
/// The chain's path is this with `base = V3_CLASS` and `allowed = V3_ALLOWED`. /// The chain's path is this with `base = V3_CLASS` and `allowed = V3_ALLOWED`.
pub fn generate_era(seed_string: &str, seed_bytes: &[u8], base: LoadClass, era_bytes: &[u8], allowed: &[u8]) -> Program { pub fn generate_era(seed_string: &str, seed_bytes: &[u8], base: LoadClass, era_bytes: &[u8], allowed: &[u8]) -> Program {
generate_era_generator(seed_string, seed_bytes, base, era_bytes, allowed, GENERATOR_VERSION_V3)
}
/// [`generate_era`] with the generator version stamped by the caller: 3 for class v3 over [`V3_CLASS`], 4 for
/// class v4 over [`V4_CLASS`] (Counter ASIC 3.0; the shadow block of the base rides through `LoadClass::era`).
pub fn generate_era_generator(seed_string: &str, seed_bytes: &[u8], base: LoadClass, era_bytes: &[u8], allowed: &[u8], generator: u32) -> Program {
let mut p = generate_from_seed_bytes_class(seed_string, seed_bytes, LoadClass::era(base, era_bytes, allowed)); let mut p = generate_from_seed_bytes_class(seed_string, seed_bytes, LoadClass::era(base, era_bytes, allowed));
p.generator = GENERATOR_VERSION_V3; p.generator = generator;
p.era_bytes = Some(era_bytes.to_vec()); p.era_bytes = Some(era_bytes.to_vec());
p p
} }
@ -813,6 +832,7 @@ impl ProgramClass {
match self { match self {
ProgramClass::V2 => LoadClass::V2, ProgramClass::V2 => LoadClass::V2,
ProgramClass::V3 => V3_CLASS, ProgramClass::V3 => V3_CLASS,
ProgramClass::V4 => V4_CLASS,
} }
} }
@ -821,23 +841,26 @@ impl ProgramClass {
match self { match self {
ProgramClass::V2 => GENERATOR_VERSION, ProgramClass::V2 => GENERATOR_VERSION,
ProgramClass::V3 => GENERATOR_VERSION_V3, ProgramClass::V3 => GENERATOR_VERSION_V3,
ProgramClass::V4 => GENERATOR_VERSION_V4,
} }
} }
/// The class of a generator version: 2 and 3 are the two classes, anything else is no class this crate runs. /// The class of a generator version: 2, 3 and 4 are the three classes, anything else is no class this crate runs.
pub fn from_generator(generator: u32) -> Option<ProgramClass> { pub fn from_generator(generator: u32) -> Option<ProgramClass> {
match generator { match generator {
GENERATOR_VERSION => Some(ProgramClass::V2), GENERATOR_VERSION => Some(ProgramClass::V2),
GENERATOR_VERSION_V3 => Some(ProgramClass::V3), GENERATOR_VERSION_V3 => Some(ProgramClass::V3),
GENERATOR_VERSION_V4 => Some(ProgramClass::V4),
_ => None, _ => None,
} }
} }
/// "v2" or "v3": the `IGNEUM_PROGRAM_CLASS` string of a pack and the `class=` token of a job line. /// "v2", "v3" or "v4": the `IGNEUM_PROGRAM_CLASS` string of a pack and the `class=` token of a job line.
pub fn name(&self) -> &'static str { pub fn name(&self) -> &'static str {
match self { match self {
ProgramClass::V2 => "v2", ProgramClass::V2 => "v2",
ProgramClass::V3 => "v3", ProgramClass::V3 => "v3",
ProgramClass::V4 => "v4",
} }
} }
@ -845,10 +868,16 @@ impl ProgramClass {
match s.trim() { match s.trim() {
"v2" => Some(ProgramClass::V2), "v2" => Some(ProgramClass::V2),
"v3" => Some(ProgramClass::V3), "v3" => Some(ProgramClass::V3),
"v4" => Some(ProgramClass::V4),
_ => None, _ => None,
} }
} }
/// Whether the class reads the era seed (every class after v2: the era layout is drawn inside the class).
pub fn has_era(&self) -> bool {
*self != ProgramClass::V2
}
/// The generator version as a byte, for wire formats that carry the class as a number. /// The generator version as a byte, for wire formats that carry the class as a number.
pub fn as_u8(&self) -> u8 { pub fn as_u8(&self) -> u8 {
self.generator_version() as u8 self.generator_version() as u8
@ -957,18 +986,23 @@ impl Program {
/// || attempt_le32`. Written into every pack so a version 1 program, or another attempt of the same seed, /// || 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. /// can never be mistaken for this one.
pub fn program_id(&self) -> u64 { pub fn program_id(&self) -> u64 {
if self.class.is_v2() || self.generator == GENERATOR_VERSION_V3 { if self.class.is_v2() || self.generator == GENERATOR_VERSION_V3 || self.generator == GENERATOR_VERSION_V4 {
// Spec 01 section 1.4.6: a class v3 program's id is `program_id(3, seed, attempt)`; the generator version // Spec 01 section 1.4.6: a class v3 program's id is `program_id(3, seed, attempt)`, a class v4 program's
// in the preimage separates it from every version 2 program of the same seed // `program_id(4, seed, attempt)` (Counter ASIC 3.0); the generator version in the preimage separates
// them from every version 2 program of the same seed
program_id(self.generator, &self.seed, self.attempt) program_id(self.generator, &self.seed, self.attempt)
} else { } else {
program_id_class(self.generator, &self.seed, self.attempt, &self.class) 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). /// The program class of this program, from its generator version (3 = v3, 4 = v4, everything else v2).
pub fn program_class(&self) -> ProgramClass { pub fn program_class(&self) -> ProgramClass {
if self.generator == GENERATOR_VERSION_V3 { ProgramClass::V3 } else { ProgramClass::V2 } match self.generator {
GENERATOR_VERSION_V3 => ProgramClass::V3,
GENERATOR_VERSION_V4 => ProgramClass::V4,
_ => ProgramClass::V2,
}
} }
} }
@ -1306,15 +1340,18 @@ pub fn generate_from_seed_bytes_class(seed_string: &str, seed_bytes: &[u8], clas
/// program, so its packs and its id say generator 3 (spec 01 sections 1.4.5 and 1.4.6). /// 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 { pub fn generate_from_seed_bytes_program_class(seed_string: &str, seed_bytes: &[u8], class: ProgramClass, era_bytes: Option<&[u8]>) -> Program {
// Class v3 with an era seed: the era layout (docs/plans/era-layout.md) drawn from the era bytes inside V3_CLASS. // Class v3 with an era seed: the era layout (docs/plans/era-layout.md) drawn from the era bytes inside V3_CLASS.
// Without era bytes (a template before the era is known) the bare V3_CLASS stands. // Class v4 (Counter ASIC 3.0): the same draw inside V4_CLASS (V3_CLASS plus the shadow block), generator 4.
if let (ProgramClass::V3, Some(era)) = (class, era_bytes) { // Without era bytes (a template before the era is known) the bare class stands.
return generate_era(seed_string, seed_bytes, V3_CLASS, era, &V3_ALLOWED); match (class, era_bytes) {
(ProgramClass::V3, Some(era)) => return generate_era(seed_string, seed_bytes, V3_CLASS, era, &V3_ALLOWED),
(ProgramClass::V4, Some(era)) => return generate_era_generator(seed_string, seed_bytes, V4_CLASS, era, &V3_ALLOWED, GENERATOR_VERSION_V4),
_ => {}
} }
let mut p = generate_from_seed_bytes_class(seed_string, seed_bytes, class.load_class()); let mut p = generate_from_seed_bytes_class(seed_string, seed_bytes, class.load_class());
p.generator = class.generator_version(); 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 // The era is a property of class v3 and v4 programs; a version 2 program never records one, so the pinned v2
// and every v2 export stay byte-identical whatever the chain reports for the era // 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.era_bytes = if class.has_era() { era_bytes.map(|b| b.to_vec()) } else { None };
p p
} }
@ -1644,16 +1681,68 @@ mod tests {
assert_eq!(v3.program_id(), program_id(GENERATOR_VERSION_V3, &v3.seed, v3.attempt)); 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(), program_id(GENERATOR_VERSION, &v3.seed, v3.attempt));
assert_ne!(v3.program_id(), v2.program_id()); assert_ne!(v3.program_id(), v2.program_id());
for c in [ProgramClass::V2, ProgramClass::V3] { for c in [ProgramClass::V2, ProgramClass::V3, ProgramClass::V4] {
assert_eq!(ProgramClass::parse(c.name()), Some(c)); assert_eq!(ProgramClass::parse(c.name()), Some(c));
assert_eq!(ProgramClass::from_generator(c.generator_version()), 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_u8(c.as_u8()), Some(c));
} }
assert_eq!(ProgramClass::from_generator(1), None); assert_eq!(ProgramClass::from_generator(1), None);
assert_eq!(ProgramClass::from_generator(4), None); assert_eq!(ProgramClass::from_generator(5), None);
assert_eq!(ProgramClass::parse("v4"), None); assert_eq!(ProgramClass::parse("v5"), None);
assert_eq!(ProgramClass::default(), ProgramClass::V2); assert_eq!(ProgramClass::default(), ProgramClass::V2);
assert_eq!(ProgramClass::V2.load_class(), LoadClass::V2); assert_eq!(ProgramClass::V2.load_class(), LoadClass::V2);
assert!(!ProgramClass::V2.has_era() && ProgramClass::V3.has_era() && ProgramClass::V4.has_era());
}
/// Counter ASIC 3.0 (6 October 2026): class v4 is class v3 with the latency-shadow block `sh256x27`, drawn after
/// the base program, so a v4 program's base instructions, attempt and era draw are the v3 program's of the same
/// seed and era, draw for draw; its generator is 4, its id `program_id(4, seed, attempt)`, its era recorded;
/// v2 and v3 are untouched.
#[test]
fn program_class_v4_is_class_v3_with_the_shadow_block() {
assert_eq!(V4_CLASS, V3_CLASS.with_shadow(256, 27));
assert_eq!(V4_CLASS.name(), "mx8+sh256x27");
assert_eq!(LoadClass::parse("mx8+sh256x27"), Some(V4_CLASS));
assert_eq!(LoadClass { shadow: None, ..V4_CLASS }, V3_CLASS, "v4 differs from v3 in the shadow alone");
assert_eq!(V4_CLASS.shadow_instrs_per_hash(), 8 * 256 * 27);
assert_eq!(ProgramClass::V4.load_class(), V4_CLASS);
assert_eq!(ProgramClass::V4.generator_version(), GENERATOR_VERSION_V4);
let era = [7u8; 32];
let v3 = generate_from_seed_bytes_program_class("igneum-genesis", b"igneum-genesis", ProgramClass::V3, Some(&era));
let v4 = generate_from_seed_bytes_program_class("igneum-genesis", b"igneum-genesis", ProgramClass::V4, Some(&era));
assert_eq!(v4.generator, GENERATOR_VERSION_V4);
assert_eq!(v4.program_class(), ProgramClass::V4);
assert_eq!(v4.era_bytes.as_deref(), Some(&era[..]));
assert_eq!(v4.instrs, v3.instrs, "the base program is class v3's, draw for draw");
assert_eq!(v4.attempt, v3.attempt);
assert_eq!(v4.seed, v3.seed);
assert!(v3.shadow.is_empty() && !v3.has_shadow());
assert_eq!(v4.shadow.len(), 256);
assert_eq!(v4.shadow_reps(), 27);
assert_eq!(v4.shadow_instrs_per_hash(), 55_296);
assert_eq!(LoadClass { era: None, ..v4.class }, V4_CLASS, "the era rides inside V4_CLASS");
assert_eq!(v4.class.era, v3.class.era, "the same era draw as class v3");
assert_eq!(v4.class, LoadClass::era(V4_CLASS, &era, &V3_ALLOWED));
assert!(check(&v4).is_ok());
assert_eq!(v4.program_id(), program_id(GENERATOR_VERSION_V4, &v4.seed, v4.attempt));
assert_ne!(v4.program_id(), v3.program_id());
assert_ne!(v4.program_id(), program_id(GENERATOR_VERSION, &v4.seed, v4.attempt));
// without an era (a template before the era is known) the bare V4_CLASS stands, generator 4
let bare = generate_from_seed_bytes_program_class("igneum-genesis", b"igneum-genesis", ProgramClass::V4, None);
assert_eq!(bare.class, V4_CLASS);
assert_eq!(bare.generator, GENERATOR_VERSION_V4);
assert_eq!(bare.era_bytes, None);
// the block changes the hash over the same dataset; the same seed and era derive the same block again
let ds = crate::verify::DatasetSource::new("2026-10-06", crate::verify::DatasetMode::ClosedForm, 20);
let h3 = crate::verify::hash_warp(&v3, 0, &ds);
let h4 = crate::verify::hash_warp(&v4, 0, &ds);
assert_ne!(h3, h4);
let again = generate_from_seed_bytes_program_class("igneum-genesis", b"igneum-genesis", ProgramClass::V4, Some(&era));
assert_eq!(again.shadow, v4.shadow);
assert_eq!(crate::verify::hash_warp(&again, 0, &ds), h4);
// v2 and v3 are byte for byte what they were (the pinned packs are diffed in tests/packs.rs)
assert_eq!(V3_CLASS.shadow, None);
assert_eq!(generate("igneum-genesis").program_id(), 0xbcc1248b10cc90f2);
} }
/// Era layout: the draw is deterministic, within bounds, and the six test eras are pinned; an era program has /// Era layout: the draw is deterministic, within bounds, and the six test eras are pinned; an era program has

View file

@ -34,7 +34,7 @@ pub mod verify;
pub use bind::{block_init_words, day_bytes, pow256_from_lane, target64_from_le256}; 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 accept::{check as accept_program, AcceptReport, Reject};
pub use generator::{generate, generate_from_seed_bytes, generate_from_seed_bytes_program_class, Instr, LoadClass, Op, Program, ProgramClass, GENERATOR_VERSION, GENERATOR_VERSION_V3, V3_CLASS}; pub use generator::{generate, generate_from_seed_bytes, generate_from_seed_bytes_program_class, Instr, LoadClass, Op, Program, ProgramClass, GENERATOR_VERSION, GENERATOR_VERSION_V3, GENERATOR_VERSION_V4, V3_CLASS, V4_CLASS};
pub use memhard::{cache_log2_words, dataset_log2_words, days_since_genesis, growth_doublings, Cache, MemhardCpu, MixParams, Shape}; pub use memhard::{cache_log2_words, dataset_log2_words, days_since_genesis, growth_doublings, Cache, MemhardCpu, MixParams, Shape};
pub use seed::{fnv1a64, seed_words, SplitMix64}; pub use seed::{fnv1a64, seed_words, SplitMix64};
pub use verify::{hash_warp, interpret_warp_init, verify_block, DatasetMode, DatasetSource, Epoch}; pub use verify::{hash_warp, interpret_warp_init, verify_block, DatasetMode, DatasetSource, Epoch};

View file

@ -40,7 +40,7 @@ struct Args {
class: LoadClass, class: LoadClass,
/// Days since genesis for the cache growth rule of a class with `growth` (0: the genesis cache). /// Days since genesis for the cache growth rule of a class with `growth` (0: the genesis cache).
days: u64, days: u64,
/// The program class (Counter ASIC 2.0 seam): v2 (default) or v3, which draws from V3_CLASS with generator 3. /// The program class (Counter ASIC 2.0 seam): v2 (default), v3 (V3_CLASS, generator 3) or v4 (V4_CLASS, generator 4).
program_class: Option<ProgramClass>, program_class: Option<ProgramClass>,
/// The era seed bytes a class v3 chain program records (`--era-hex`). /// The era seed bytes a class v3 chain program records (`--era-hex`).
era_hex: Option<String>, era_hex: Option<String>,
@ -96,7 +96,7 @@ fn usage() -> ! {
\x20 --class C load class: v2 (default), mx4, mx8 (class v3: mixer x8, cache growth), dr<len> (Counter ASIC 3.0 item 2: the per-day derivation program, dr736 = the x8-equivalent), w4, w16, w64, w64x4, p4,p16,p64[xN], <class>m<mult>[g]\n\ \x20 --class C load class: v2 (default), mx4, mx8 (class v3: mixer x8, cache growth), dr<len> (Counter ASIC 3.0 item 2: the per-day derivation program, dr736 = the x8-equivalent), w4, w16, w64, w64x4, p4,p16,p64[xN], <class>m<mult>[g]\n\
\x20 also: w4, w16, w64, w64x4, p4,p16,p64[xN], <class>m<mult>[g], <class>+sh<S>x<R> (latency-shadow block of S ALU instructions x R passes per iteration, Counter ASIC 3.0 item 8)\n\ \x20 also: w4, w16, w64, w64x4, p4,p16,p64[xN], <class>m<mult>[g], <class>+sh<S>x<R> (latency-shadow block of S ALU instructions x R passes per iteration, Counter ASIC 3.0 item 8)\n\
\x20 --days N days since genesis for the cache growth rule of a class with it (default 0: the 2^26-word cache)\n\ \x20 --days N days since genesis for the cache growth rule of a class with it (default 0: the 2^26-word cache)\n\
\x20 --program-class v2|v3 the program class of the seam (v3 = generator 3 on V3_CLASS, the chain's own derivation; --era-hex records the era seed)\n\ \x20 --program-class v2|v3|v4 the program class of the seam (v3 = generator 3 on V3_CLASS, v4 = generator 4 on V4_CLASS = mx8+sh256x27, the chain's own derivation; --era-hex records the era seed)\n\
\x20 --era E era layout over --class: igneum-era-test/<n> or <n>:<64 hex> (the 32-byte era seed E_n)\n\ \x20 --era E era layout over --class: igneum-era-test/<n> or <n>:<64 hex> (the 32-byte era seed E_n)\n\
\x20 --era-widths 4[,16,64] the width set the era draws from, in bytes (default 4: pinned; more lets the era draw it)" \x20 --era-widths 4[,16,64] the width set the era draws from, in bytes (default 4: pinned; more lets the era draw it)"
); );

View file

@ -23,7 +23,7 @@ pub struct PackIdentity {
pub attempt: u32, pub attempt: u32,
pub seedw: [u32; 8], pub seedw: [u32; 8],
pub keyw: [u32; 8], pub keyw: [u32; 8],
/// `IGNEUM_GENERATOR` (2 or 3; a pack without the line is generator 1, which no worker runs). /// `IGNEUM_GENERATOR` (2, 3 or 4; a pack without the line is generator 1, which no worker runs).
pub generator: u32, pub generator: u32,
/// The program class the generator version names (Counter ASIC 2.0). /// The program class the generator version names (Counter ASIC 2.0).
pub class: ProgramClass, pub class: ProgramClass,
@ -164,9 +164,9 @@ pub fn verify_pack_texts(program_h: &str, seeds_txt: Option<&str>, want_epoch: &
verify_pack_texts_chain(program_h, seeds_txt, want_epoch, want_day, None, None) 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 /// [`verify_pack_texts`] that also demands a program class and, for class v3 and v4, 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. /// (Counter ASIC 2.0, 5 October 2026; class v4 Counter ASIC 3.0, 6 October 2026). `want_class` `None` accepts any
/// A pack whose `IGNEUM_GENERATOR` is neither 2 nor 3 is refused whatever is wanted. /// class; `want_era` `None` skips the era. A pack whose `IGNEUM_GENERATOR` is not 2, 3 or 4 is refused whatever is wanted.
pub fn verify_pack_texts_chain( pub fn verify_pack_texts_chain(
program_h: &str, program_h: &str,
seeds_txt: Option<&str>, seeds_txt: Option<&str>,
@ -177,7 +177,7 @@ pub fn verify_pack_texts_chain(
) -> Result<PackIdentity, PackFault> { ) -> Result<PackIdentity, PackFault> {
let generator = define_u32(program_h, "IGNEUM_GENERATOR").unwrap_or(1); let generator = define_u32(program_h, "IGNEUM_GENERATOR").unwrap_or(1);
let Some(class) = ProgramClass::from_generator(generator) else { 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)"))); return Err(PackFault::WrongClass(format!("IGNEUM_GENERATOR {generator} is not a generator version this software runs (2, 3 or 4)")));
}; };
// IGNEUM_PROGRAM_CLASS, when present, must name the class the generator version names // 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 let Some(named) = define_str(program_h, "IGNEUM_PROGRAM_CLASS") {
@ -191,12 +191,12 @@ pub fn verify_pack_texts_chain(
return Err(PackFault::WrongClass(format!("the pack is program class {} (generator {generator}), the chain is on class {}", class.name(), want.name()))); 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) { if let (Some(want), true) = (want_era, class.has_era()) {
let want_hex = hex(want); let want_hex = hex(want);
match &era_hex { match &era_hex {
Some(h) if *h == want_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)))), 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())), None => return Err(PackFault::WrongClass(format!("a class {} pack without IGNEUM_ERA_SEED_HEX; the job names an era seed", class.name()))),
} }
} }
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 seedw = define_words(program_h, "IGNEUM_SEEDW_INIT").ok_or_else(|| PackFault::Unreadable("program.h has no IGNEUM_SEEDW_INIT with 8 words".into()))?;
@ -393,5 +393,28 @@ mod tests {
// a class line that contradicts the generator // a class line that contradicts the generator
let bad = h3.replace("#define IGNEUM_PROGRAM_CLASS \"v3\"\n", "#define IGNEUM_PROGRAM_CLASS \"v2\"\n"); 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(_)))); assert!(matches!(verify_pack_texts(&bad, None, &e, &d), Err(PackFault::Disagree(_))));
// Counter ASIC 3.0: a class v4 chain pack carries generator 4, the class line and the era seed; it is refused
// when v3 (or v2) is wanted, and a v3 pack is refused when v4 is wanted; the era rule applies to v4 as to v3
let v4 = Epoch::from_chain_seeds(&e, &d, Some(&era), ProgramClass::V4, "class test");
let h4 = program_header(&v4.program, "test day", &v4.dataset);
assert!(h4.contains("#define IGNEUM_GENERATOR 4\n"));
assert!(h4.contains("#define IGNEUM_PROGRAM_CLASS \"v4\"\n"));
assert!(h4.contains(&format!("#define IGNEUM_ERA_SEED_HEX \"{}\"\n", hex(&era))));
assert!(h4.contains("#define IGNEUM_SHADOW_INSTRS 256\n") && h4.contains("#define IGNEUM_SHADOW_REPS 27\n"), "{h4}");
let id = verify_pack_texts_chain(&h4, None, &e, &d, Some(ProgramClass::V4), Some(&era)).unwrap();
assert_eq!((id.generator, id.class, id.era_hex.as_deref()), (4, ProgramClass::V4, Some(hex(&era).as_str())));
assert_eq!(id.attempt, v3.program.attempt, "the v4 attempt is the v3 attempt of the same seed");
assert!(verify_pack_texts(&h4, None, &e, &d).is_ok(), "no class wanted: any class passes");
assert!(matches!(verify_pack_texts_chain(&h4, None, &e, &d, Some(ProgramClass::V3), None), Err(PackFault::WrongClass(_))));
assert!(matches!(verify_pack_texts_chain(&h4, None, &e, &d, Some(ProgramClass::V2), None), Err(PackFault::WrongClass(_))));
assert!(matches!(verify_pack_texts_chain(&h3, None, &e, &d, Some(ProgramClass::V4), None), Err(PackFault::WrongClass(_))));
assert!(matches!(verify_pack_texts_chain(&h2, None, &e, &d, Some(ProgramClass::V4), None), Err(PackFault::WrongClass(_))));
let err = verify_pack_texts_chain(&h4, None, &e, &d, Some(ProgramClass::V4), Some(&[1u8; 32])).unwrap_err();
assert!(err.to_string().contains("era seed"), "{err}");
let no_era = h4.replace(&format!("#define IGNEUM_ERA_SEED_HEX \"{}\"\n", hex(&era)), "");
let err = verify_pack_texts_chain(&no_era, None, &e, &d, Some(ProgramClass::V4), Some(&era)).unwrap_err();
assert!(err.to_string().contains("class v4 pack without IGNEUM_ERA_SEED_HEX"), "{err}");
// the v3 pack of the same seeds is unchanged by the v4 class existing
assert_eq!(program_header(&v3.program, "test day", &v3.dataset), h3);
} }
} }

View file

@ -30,6 +30,12 @@ every node, `program_class_v3_activation_daa` a few epochs ahead; reports blocks
program id and class before and after, rejected blocks, forks, and every node's switch line; see program id and class before and after, rejected blocks, forks, and every node's switch line; see
`docs/plans/counter-asic-2-node.md`). `docs/plans/counter-asic-2-node.md`).
Class v4 switch gate (Counter ASIC 3.0 G4): `node infra/fast-time/class-v4.mjs` (the same shape on ports 29660 and up,
`igneum-devnet-966`, data `/tmp/igneum-fast-time-v4`, `program_class_v3_activation_daa` at 60 and
`program_class_v4_activation_daa` at 150 by default, so one run crosses v2, v3 and v4; `--activation never` is the
known-failed case, which must report FAIL with no v4 epoch; `--metal <igneum-bench>` puts a real Metal miner on node 0
for G4b; see `docs/plans/counter-asic-3-node.md`).
Miners: `igneum-miner` follows the epoch length and lead its node reports in every template (`pow_epoch`), no flag. Miners: `igneum-miner` follows the epoch length and lead its node reports in every template (`pow_epoch`), no flag.
The dataset day is not in the template, so a real-hash miner on a fast-time network takes `IGNEUM_POW_DAY_MS=1440000` The dataset day is not in the template, so a real-hash miner on a fast-time network takes `IGNEUM_POW_DAY_MS=1440000`
in its environment (the node reads it from the file). The environment variables `IGNEUM_POW_EPOCH_BLOCKS`, in its environment (the node reads it from the file). The environment variables `IGNEUM_POW_EPOCH_BLOCKS`,
@ -62,6 +68,8 @@ Time parameters, divided by 60 (devnet value, 60x value):
| `proving_v1_segment_blocks`, `proving_v1_aggregator_share_bps` | 8, 1,000 | 8, 1,000 | a block count and a share | | `proving_v1_segment_blocks`, `proving_v1_aggregator_share_bps` | 8, 1,000 | 8, 1,000 | a block count and a share |
| `proving_v1_unproven_daa` | 600 | 10 | a DAA clock (the unproven allowance), divided by 60; the proving agent confirms the value | | `proving_v1_unproven_daa` | 600 | 10 | a DAA clock (the unproven allowance), divided by 60; the proving agent confirms the value |
| `program_class_v3_activation_daa` | never | never | a height, not a clock: the lottery hash draws programs from class v3 (Counter ASIC 2.0, 5 Oct 2026) from the first EPOCH whose start is at or above this DAA score (rounded up to an epoch boundary: at 60 DAA per epoch, 150 means epoch 3 at DAA 180); `infra/fast-time/class-v3.mjs` sets it a few epochs ahead in its merged file | | `program_class_v3_activation_daa` | never | never | a height, not a clock: the lottery hash draws programs from class v3 (Counter ASIC 2.0, 5 Oct 2026) from the first EPOCH whose start is at or above this DAA score (rounded up to an epoch boundary: at 60 DAA per epoch, 150 means epoch 3 at DAA 180); `infra/fast-time/class-v3.mjs` sets it a few epochs ahead in its merged file |
| `program_class_v4_activation_daa` | never | never | a height, not a clock: the lottery hash draws programs from class v4 (Counter ASIC 3.0, 6 Oct 2026: class v3 plus the latency-shadow block) from the first EPOCH whose start is at or above this DAA score, rounded up like the v3 switch; `infra/fast-time/class-v4.mjs` sets it a few epochs ahead in its merged file |
| `proving_v1_fresh_rule_daa`, `exec_restart_number`, `exec_restart_trust_daa`, `exec_restart_hash` | never, never, never, "" | the same | heights and a hash, not clocks (the 0.3.12 and 0.3.13 switches); present so the fork's every-field test (`fast_time_60x_file_is_the_devnet_at_60x`) holds; added 6 Oct 2026 with the class v4 field |
Unchanged, and why: Unchanged, and why:

View file

@ -0,0 +1,305 @@
#!/usr/bin/env node
// Counter ASIC 3.0 gate G4 (docs/plans/counter-asic-3-node.md; the 2.0 shape: docs/plans/counter-asic-2-rollout.md
// section 7): the fast-time 3-node network mining across a program class v4 activation. A private network on
// 127.0.0.1 ports 29660 and up, data under /tmp/igneum-fast-time-v4, network id igneum-devnet-966, every node on
// infra/fast-time/override-60x.json merged with a CPU genesis difficulty (genesis_bits 0x1f010000, 2^16 hashes per
// block, as sim/difficulty/testnet_v2.py), `program_class_v3_activation_daa` at --v3-activation (default 60: epoch 1,
// so the run crosses v2 -> v3 -> v4 and the two switches are both exercised) and `program_class_v4_activation_daa`
// at --activation (default 150: inside epoch 2 at 60 DAA per epoch, so the switch rounds UP to epoch 3 at DAA 180,
// the boundary rule under test). One real CPU miner per node (igneum-miner --engine igneum-pow, 1 thread) follows
// its node's templates, so every block of the run is a real lottery-hash solution and every node verifies every
// block of the other two under the class of its epoch.
//
// Reports, from the nodes' RPC and the logs: blocks on each side of the v4 boundary, the program class and id of
// every epoch (before and after), rejected blocks (the miners' submit answers and the nodes' "PoW rejected" lines),
// forks (every node's sink, selected tip and block count at the end), and every node's two switch lines. Exit 0 when
// every check passes. Never touches the live devnet (26610/26611, 26640/26641, 28640), the class-v3 harness (29600)
// or the simnet.mjs ports.
//
// The known-failed case (the gate rule: a gate is trusted only after it fires on one finished and one failed case):
// `--activation never` leaves the v4 switch at never; the harness must then report FAIL with no v4 epoch seen.
//
// node infra/fast-time/class-v4.mjs [--secs 420] [--activation 150|never] [--v3-activation 60|never] [--epochs-after 2]
// [--threads 1] [--metal <igneum-bench>] [--genesis-bits 0x1f010000]
// IGNEUMD and IGNEUM_MINER name the binaries (default: the ca3 fork worktree's target-ca3v4/release).
// --metal: node 0's miner is a real Metal GPU worker (igneum-miner --worker <igneum-bench> --prepare-packs <dir>
// --exit-on-seed-change, the app's own shape), so the Metal worker's class v4 path (the prepare line with the pack
// directory and the class and era tokens, the pack-built program and day) mines across the boundary (gate G4b);
// the report then adds the miner's PREPARE lines, the worker's prepared lines, any need / mismatch / refusal line,
// the accepted blocks on each side and the swap time. --genesis-bits raises the CPU difficulty for a GPU miner
// (0x1e010000 = 2^24 hashes per block: under a second on an M5 Max; the CPU miners on nodes 1 and 2 then verify
// and rarely find).
import { spawn } from 'node:child_process';
import { mkdirSync, rmSync, writeFileSync, readFileSync, openSync, existsSync } from 'node:fs';
import { connectRpc } from '../../tools/finality-attacks/lib/rpc.mjs';
import { devAddress } from '../../tools/harness/lib/address.mjs';
const ROOT = new URL('../../', import.meta.url).pathname;
const FILE = `${ROOT}infra/fast-time/override-60x.json`;
const BIN = process.env.IGNEUM_CA3_BIN || `${ROOT}vendor/igneum-node-ca3v4/target-ca3v4/release`;
const IGNEUMD = process.env.IGNEUMD || `${BIN}/igneumd`;
const CPU_MINER = process.env.IGNEUM_MINER || `${BIN}/igneum-miner`;
const TMP = '/tmp/igneum-fast-time-v4';
const BASE = 29660, SUFFIX = 966;
const NEVER = '18446744073709551615';
const args = process.argv.slice(2);
const flag = (name, dflt) => { const i = args.indexOf(`--${name}`); return i >= 0 ? +args[i + 1] : dflt; };
const sflag = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : null; };
// an activation flag: a DAA score, or "never" (the known-failed case for the v4 switch; a v2-only chain for the v3 one)
const actflag = (name, dflt) => { const v = sflag(name); if (v == null) return dflt; return v === 'never' ? null : +v; };
const GENESIS_BITS = flag('genesis-bits', 0x1f010000);
const METAL = sflag('metal');
const SECS = flag('secs', 420);
const ACTIVATION = actflag('activation', 150); // class v4 (N5); null = never
const V3_ACTIVATION = actflag('v3-activation', 60); // class v3 (N4); null = never
const EPOCHS_AFTER = flag('epochs-after', 2);
const THREADS = flag('threads', 1);
const started = [];
const log = (...a) => console.log(new Date().toISOString().slice(11, 23), ...a);
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
for (const b of [IGNEUMD, CPU_MINER, ...(METAL ? [METAL] : [])]) if (!existsSync(b)) { console.error(`missing ${b}`); process.exit(2); }
rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true });
// The file is merged as TEXT, never through JSON.parse: a `never` height is 18446744073709551615, which JavaScript
// rounds to 1.8446744073709552e+19 and the node refuses ("expected u64"; the first gate run, 5 October 2026 21:30Z).
// The merged fields are appended after the file's last field; a field already in the file is removed first.
const baseText = readFileSync(FILE, 'utf8');
const field = (name) => { const m = new RegExp(`"${name}":\\s*([0-9]+)`).exec(baseText); return m ? +m[1] : undefined; };
const EPOCH = field('pow_epoch_blocks');
const DAY_MS = field('pow_day_ms');
const FIRST_V4_EPOCH = ACTIVATION == null ? null : Math.ceil(ACTIVATION / EPOCH);
const FIRST_V3_EPOCH = V3_ACTIVATION == null ? null : Math.ceil(V3_ACTIVATION / EPOCH);
const BOUNDARY = FIRST_V4_EPOCH == null ? Infinity : FIRST_V4_EPOCH * EPOCH;
const override = `${TMP}/override.json`;
export function mergeOverrideText(text, fields) {
let out = text;
for (const k of Object.keys(fields)) out = out.replace(new RegExp(`\\s*"${k}":\\s*[^,}\\n]+,?`), '');
// a string of digits is written raw (a never height is the u64 max, beyond a JavaScript number); other strings quoted
const extra = Object.entries(fields).map(([k, v]) => `"${k}": ${typeof v === 'string' && !/^\d+$/.test(v) ? JSON.stringify(v) : v}`).join(', ');
return out.replace(/,?\s*}\s*$/, `,\n ${extra}\n}\n`);
}
// the two heights as digit text (the file's own two fields, both at never, are removed first by the merge)
const asText = (v) => v == null ? NEVER : String(v);
writeFileSync(override, mergeOverrideText(baseText, { genesis_bits: GENESIS_BITS, skip_proof_of_work: false, program_class_v3_activation_daa: asText(V3_ACTIVATION), program_class_v4_activation_daa: asText(ACTIVATION) }));
log(`v3 activation ${V3_ACTIVATION ?? 'never'} (first v3 epoch ${FIRST_V3_EPOCH ?? 'none'}), v4 activation ${ACTIVATION ?? 'never'} at ${EPOCH} DAA per epoch: the first v4 epoch is ${FIRST_V4_EPOCH ?? 'none'} (DAA ${Number.isFinite(BOUNDARY) ? BOUNDARY : 'never'}); run ${SECS} s, CPU genesis bits 0x${GENESIS_BITS.toString(16)}`);
class Node {
constructor(i, connect = []) {
this.i = i; this.grpcPort = BASE + i * 10; this.p2pPort = BASE + i * 10 + 1; this.jsonPort = BASE + i * 10 + 2;
this.connect = connect; this.dir = `${TMP}/n${i}`; this.logFile = `${this.dir}/node.log`;
}
get grpc() { return `grpc://127.0.0.1:${this.grpcPort}`; }
async start() {
mkdirSync(this.dir, { recursive: true });
const a = ['--devnet', `--devnet-suffix=${SUFFIX}`, '--nodnsseed', '--disable-upnp', '--nologfiles', '--enable-unsynced-mining', '--utxoindex',
`--appdir=${this.dir}`, `--rpclisten=127.0.0.1:${this.grpcPort}`, `--rpclisten-json=127.0.0.1:${this.jsonPort}`,
`--listen=127.0.0.1:${this.p2pPort}`, `--override-params-file=${override}`, '--loglevel=info', '--yes'];
if (this.connect.length) a.push(`--connect=${this.connect.join(',')}`); else a.push('--outpeers=0');
const out = openSync(this.logFile, 'a');
this.proc = spawn(IGNEUMD, a, { stdio: ['ignore', out, out] });
started.push(this.proc);
await sleep(1200);
this.rpc = await connectRpc(`ws://127.0.0.1:${this.jsonPort}`);
log(`n${this.i} up pid ${this.proc.pid} json ${this.jsonPort} p2p ${this.p2pPort}`);
return this;
}
grepLog(re) { try { return readFileSync(this.logFile, 'utf8').split('\n').filter(l => re.test(l)); } catch { return []; } }
}
function miner(bin, argv, name, env = {}) {
const out = openSync(`${TMP}/${name}.log`, 'a');
const p = spawn(bin, argv, { stdio: ['ignore', out, out], env: { ...process.env, ...env } });
started.push(p);
return p;
}
async function stopAll() {
for (const p of started.reverse()) { try { p.kill('SIGINT'); } catch { } }
await sleep(1500);
for (const p of started) { try { p.kill('SIGKILL'); } catch { } }
}
process.on('SIGINT', async () => { await stopAll(); process.exit(130); });
process.on('unhandledRejection', async (e) => { log(`FAILED: ${e?.stack || e}`); await stopAll(); process.exit(3); }); // a thrown start leaves no node behind
const minerName = (i) => (METAL && i === 0) ? 'metal0' : `cpu${i}`;
const minerLog = (i) => { try { return readFileSync(`${TMP}/${minerName(i)}.log`, 'utf8').split('\n'); } catch { return []; } };
const V4_LINE = /Program class v4 from the override file/, V3_LINE = /Program class v3 from the override file/;
const tail = (l, what) => l.replace(new RegExp(`^.*?(Program class ${what})`), '$1');
const t0 = Date.now();
const since = () => ((Date.now() - t0) / 1000).toFixed(1);
const n0 = await new Node(0).start();
const n1 = await new Node(1, [`127.0.0.1:${n0.p2pPort}`]).start();
const n2 = await new Node(2, [`127.0.0.1:${n0.p2pPort}`]).start(); // --connect takes one address
const nodes = [n0, n1, n2];
for (const n of nodes) {
const v3 = n.grepLog(V3_LINE)[0], v4 = n.grepLog(V4_LINE)[0];
log(`n${n.i} switch lines: ${v3 ? tail(v3, 'v3') : '(no v3 line)'} | ${v4 ? tail(v4, 'v4') : '(no v4 line)'}`);
}
log(`n0 PoW schedule: ${n0.grepLog(/PoW schedule from the override file/).map(l => l.replace(/^.*?(PoW schedule)/, '$1')).join(' | ') || '(no line)'}`);
log(`n0 digest: ${n0.grepLog(/Consensus params digest/).map(l => l.replace(/^.*?digest: /, '').slice(0, 16)).join(' ')}`);
// one real CPU miner per node, 1 thread, 2^16 expected hashes per block at genesis; with --metal, node 0's miner drives
// the Metal worker the way the app does (the pack for every prepared pair under packs/prepare, exit 42/44 on a refusal)
const PACKS = `${TMP}/packs/prepare`;
mkdirSync(PACKS, { recursive: true });
nodes.forEach((n, i) => {
if (METAL && i === 0) {
miner(CPU_MINER, ['mine', n.grpc, '1', String(SECS), 'metal0', '--engine', 'igneum-pow', '--worker', METAL, '--prepare-packs', PACKS, '--exit-on-seed-change', '--payout-label', 'metal0', '--status-secs', '30', '--no-vote'], 'metal0', { IGNEUM_POW_DAY_MS: String(DAY_MS) });
log(`n0 miner: Metal worker ${METAL}, packs under ${PACKS}`);
} else {
miner(CPU_MINER, ['mine', n.grpc, String(THREADS), String(SECS), `cpu${i}`, '--engine', 'igneum-pow', '--payout-label', `cpu${i}`, '--status-secs', '30', '--no-vote'], `cpu${i}`, { IGNEUM_POW_DAY_MS: String(DAY_MS) });
}
});
const pay = devAddress('fast-time-v4');
const epochs = new Map(); // epoch index -> { class, firstSeenDaa, at }
let firstV4 = null, firstV3 = null, lastEpoch = -1, lastReport = 0, lastDaa = 0, switchEnd = null;
const samples = [];
while (Date.now() - t0 < SECS * 1000) {
await sleep(1000);
let daa = null, epoch = null, cls = null, nextCls = null, era = null, eraSeed = null, act3 = null, act4 = null;
try {
const t = await n0.rpc.call('getBlockTemplate', { payAddress: pay, extraData: [] });
const pe = t.powEpoch || t.pow_epoch || {};
daa = pe.virtualDaaScore ?? t.block?.header?.daaScore; epoch = pe.epochIndex; cls = pe.programClass; nextCls = pe.nextProgramClass;
era = pe.eraIndex; eraSeed = pe.eraSeed; act3 = pe.programClassV3ActivationDaa; act4 = pe.programClassV4ActivationDaa;
} catch (e) { log(`template: ${e.message}`); }
if (epoch != null && epoch !== lastEpoch) {
epochs.set(epoch, { class: cls, firstSeenDaa: daa, at: +since() });
log(`epoch ${lastEpoch} -> ${epoch} at daa ${daa}, ${since()} s: template class ${cls} (generator), next epoch class ${nextCls}, era ${era} seed ${String(eraSeed).slice(0, 16)}, activation v3 ${act3} v4 ${act4}`);
if (firstV3 == null && cls === 3) { firstV3 = { epoch, daa, at: +since() }; log(`CLASS SWITCH: the template is class v3 from epoch ${epoch} (daa ${daa}) at ${since()} s wall`); }
if (firstV4 == null && cls === 4) { firstV4 = { epoch, daa, at: +since() }; log(`CLASS SWITCH: the template is class v4 from epoch ${epoch} (daa ${daa}) at ${since()} s wall`); }
lastEpoch = epoch;
}
lastDaa = daa ?? lastDaa;
if (Date.now() - lastReport > 15000) {
lastReport = Date.now();
const counts = await Promise.all(nodes.map(async n => { try { const d = await n.rpc.call('getBlockDagInfo'); return `${d.blockCount}/${String(d.sink).slice(0, 8)}`; } catch { return '?'; } }));
log(`t=${since()} s daa ${daa} epoch ${epoch} class ${cls} blocks/sink per node ${counts.join(' ')}`);
samples.push({ t: +since(), daa, epoch, class: cls, nodes: counts });
}
if (firstV4 != null && daa != null && daa >= BOUNDARY + EPOCHS_AFTER * EPOCH) { switchEnd = +since(); break; }
}
await sleep(3000); // let the last blocks relay before the end-of-run reads
// ---- the end-of-run reads -----------------------------------------------------------------------------------------
const dag = await Promise.all(nodes.map(async n => { try { return await n.rpc.call('getBlockDagInfo'); } catch (e) { return { error: e.message }; } }));
const genesis = dag[0].pruningPointHash;
// every block the first node holds, by DAA score, from genesis
async function allBlocks(n) {
const out = []; let low = genesis; const seen = new Set();
for (let round = 0; round < 500; round++) {
const r = await n.rpc.call('getBlocks', { lowHash: low, includeBlocks: true, includeTransactions: false });
const blocks = r.blocks || [];
let added = 0;
for (const b of blocks) { const h = b.verboseData?.hash || b.header?.hash; if (seen.has(h)) continue; seen.add(h); out.push({ hash: h, daa: +b.header.daaScore, chain: !!b.verboseData?.isChainBlock, blue: +(b.verboseData?.blueScore ?? 0) }); added++; }
if (!blocks.length || added === 0) break;
low = (r.blockHashes || []).at(-1) || blocks.at(-1).verboseData?.hash; if (!low) break;
}
return out;
}
let blocks = [];
try { blocks = await allBlocks(n0); } catch (e) { log(`getBlocks: ${e.message}`); }
const before = blocks.filter(b => b.daa < BOUNDARY), after = blocks.filter(b => b.daa >= BOUNDARY);
const chainBefore = before.filter(b => b.chain).length, chainAfter = after.filter(b => b.chain).length;
// program ids and classes per epoch from the miners' "program and 256 MiB cache ready" lines
const programs = new Map(); // epoch seed -> { class, id, miners: Set }
for (const i of [0, 1, 2]) for (const l of minerLog(i)) {
const m = /epoch seed ([0-9a-f]{64}) day (\d+) \(daa (\d+)\): program and 256 MiB cache ready in ([\d.]+) ms; class (v\d) program id ([0-9a-f]{16})/.exec(l);
if (!m) continue;
const k = m[1]; const e = programs.get(k) || { seed: k.slice(0, 16), epoch: Math.floor(+m[3] / EPOCH), class: m[5], id: m[6], miners: new Set(), ready_ms: [] };
if (e.id !== m[6] || e.class !== m[5]) e.disagree = true;
e.miners.add(i); e.ready_ms.push(+m[4]); programs.set(k, e);
}
// ready_ms: the program generation plus the day's cache and dataset build on one CPU core (the first epoch of a day
// pays the cache; a v4 epoch after a v3 one reuses the v3 day cache, so its number is the program alone)
const programRows = [...programs.values()].sort((a, b) => a.epoch - b.epoch).map(p => ({ epoch: p.epoch, class: p.class, program_id: p.id, seed: p.seed, miners: p.miners.size, disagree: !!p.disagree, ready_ms: p.ready_ms }));
const idsOf = (c) => programRows.filter(p => p.class === c).map(p => p.program_id);
const v2Ids = idsOf('v2'), v3Ids = idsOf('v3'), v4Ids = idsOf('v4');
const olderIds = [...v2Ids, ...v3Ids];
// rejections: the miners' submit answers and the nodes' PoW lines
const accepted = [0, 1, 2].map(i => minerLog(i).filter(l => /ACCEPTED block/.test(l)).length);
const rejectedMiner = [0, 1, 2].map(i => minerLog(i).filter(l => /rejected nonce=|submit error/.test(l)));
const rejectedNode = nodes.map(n => n.grepLog(/PoW rejected|Rejected block|rejected block/i));
const switchLines = nodes.map(n => n.grepLog(V4_LINE).map(l => tail(l, 'v4'))[0] || null);
const v3SwitchLines = nodes.map(n => n.grepLog(V3_LINE).map(l => tail(l, 'v3'))[0] || null);
const sinks = dag.map(d => String(d.sink || '?').slice(0, 16));
const counts = dag.map(d => d.blockCount ?? '?');
const tips = dag.map(d => (d.tipHashes || []).length);
// the Metal miner's protocol lines (gate G4b): PREPARE sent (the miner), prepared / need / error (the worker), the swap
let metal = null;
if (METAL) {
const L = minerLog(0);
const t = (l) => { const m = /^(\d+\.\d+) /.exec(l); return m ? +m[1] * 1000 : null; };
const switchAt = firstV4 ? t0 + firstV4.at * 1000 : null;
const prepares = L.filter(l => /PREPARE sent for epoch seed/.test(l)).map(l => l.replace(/^\S+ /, ''));
const prepared = L.filter(l => /worker: prepared /.test(l)).map(l => l.replace(/^\S+ /, ''));
const preparedV4 = prepared.filter(l => / class v4 /.test(l));
const need = L.filter(l => /worker: need |^\S+ worker error:.*need /.test(l) || /\bneed [0-9a-f]{64}/.test(l));
// a PACK OUT OF DATE before the first class v4 prepare is the v2/v3 rebuild path at work (a seed that flipped inside
// the quarter-lead confirm window, 3 DAA on the 60x profile); the gate judges the v4 path from its first prepare on
const firstV4Prepare = L.findIndex(l => /PREPARE sent for epoch seed .* class v4/.test(l));
const sinceV4 = (l, i) => firstV4Prepare < 0 || i >= firstV4Prepare;
const mismatch = L.filter((l, i) => sinceV4(l, i) && /program class mismatch|era seed mismatch|PACK OUT OF DATE|pack .*: program pack/.test(l));
const mismatchBefore = L.filter((l, i) => !sinceV4(l, i) && /program class mismatch|era seed mismatch|PACK OUT OF DATE/.test(l));
const refused = L.filter(l => /exiting with code 4[24]|refused the program pack|prepare-failed/.test(l));
const swaps = L.filter(l => /swapped with no pause|compiles inline|worker without prepare support/.test(l)).map(l => l.replace(/^\S+ /, ''));
const accepted = L.filter(l => /ACCEPTED block/.test(l));
const acceptedAfter = switchAt ? accepted.filter(l => (t(l) || 0) >= switchAt).length : 0;
const found = L.filter(l => /worker: found |^\S+ found /.test(l)).length;
const status = L.filter(l => /miner 'metal0' \[igneum-pow\]/.test(l)).map(l => l.replace(/^\S+ /, ''));
const lastStatus = status.at(-1) || '';
const mismatched = +(/mismatched=(\d+)/.exec(lastStatus)?.[1] ?? 0);
const rate = /hash=([\d.]+) MH\/s/.exec(lastStatus)?.[1];
metal = { worker: METAL, prepares, prepared, prepared_v4: preparedV4, prepared_v3: prepared.filter(l => / class v3 /.test(l)), need: need.length, mismatch_lines: mismatch, rebuild_lines_before_v4: mismatchBefore, refused, swaps, accepted_total: accepted.length, accepted_after_switch: acceptedAfter, found_lines: found, cpu_recheck_mismatched: mismatched, rate_mh_s: rate, last_status: lastStatus };
}
const checks = {
v4_switch_line_on_every_node: switchLines.every(Boolean),
v4_switch_line_names_the_rounded_epoch: switchLines.every(l => l && FIRST_V4_EPOCH != null && l.includes(`active from epoch ${FIRST_V4_EPOCH} `)),
v3_switch_line_on_every_node: v3SwitchLines.every(Boolean),
template_switched_at_the_first_v4_epoch: firstV4 != null && firstV4.epoch === FIRST_V4_EPOCH,
template_switched_at_the_first_v3_epoch: FIRST_V3_EPOCH == null ? firstV3 == null : (firstV3 != null && firstV3.epoch === FIRST_V3_EPOCH),
blocks_before_the_boundary: before.length > 0,
blocks_after_the_boundary: after.length > 0,
v3_and_v4_programs_seen: v3Ids.length > 0 && v4Ids.length > 0,
v2_programs_seen_when_v3_starts_later: FIRST_V3_EPOCH == null || FIRST_V3_EPOCH === 0 || v2Ids.length > 0,
program_ids_differ_across_the_switch: v4Ids.length > 0 && !olderIds.some(id => v4Ids.includes(id)),
miners_agree_on_every_program: programRows.every(p => !p.disagree),
zero_rejected_by_miners: rejectedMiner.every(r => r.length === 0),
zero_rejected_by_nodes: rejectedNode.every(r => r.length === 0),
sinks_agree: new Set(sinks).size === 1,
block_counts_agree: new Set(counts.map(String)).size === 1,
...(METAL ? {
metal_prepare_sent_for_v4: metal.prepares.some(l => / class v4 /.test(l)),
metal_worker_prepared_v4_pack: metal.prepared_v4.length > 0,
metal_no_need_or_mismatch: metal.need === 0 && metal.mismatch_lines.length === 0 && metal.refused.length === 0,
metal_accepted_blocks_after_switch: metal.accepted_after_switch > 0,
metal_cpu_recheck_clean: metal.cpu_recheck_mismatched === 0,
metal_swapped_without_pause: metal.swaps.some(l => /swapped with no pause/.test(l)),
} : {}),
};
const pass = Object.values(checks).every(Boolean);
const summary = {
pass, checks, activation: ACTIVATION ?? 'never', v3_activation: V3_ACTIVATION ?? 'never', epoch_blocks: EPOCH, first_v3_epoch: FIRST_V3_EPOCH, first_v4_epoch: FIRST_V4_EPOCH, boundary_daa: Number.isFinite(BOUNDARY) ? BOUNDARY : 'never', secs: SECS, threads: THREADS,
node: IGNEUMD, miner: CPU_MINER, metal_worker: METAL, genesis_bits: `0x${GENESIS_BITS.toString(16)}`,
template_switch_v3: firstV3, template_switch: firstV4, run_ended_at_s: switchEnd, final_daa: lastDaa,
blocks: { total: blocks.length, before_boundary: before.length, after_boundary: after.length, chain_before: chainBefore, chain_after: chainAfter },
programs: programRows, accepted_per_miner: accepted,
rejected_by_miners: rejectedMiner.map(r => r.length), rejected_by_nodes: rejectedNode.map(r => r.length),
rejected_lines: [...rejectedMiner.flat(), ...rejectedNode.flat()].slice(0, 20),
sinks, block_counts: counts, tips_per_node: tips, switch_lines: switchLines, v3_switch_lines: v3SwitchLines, samples, metal,
};
writeFileSync(`${TMP}/summary.json`, JSON.stringify(summary, null, 2));
log(`SUMMARY ${pass ? 'PASS' : 'FAIL'}: ${firstV4 ? `v4 from epoch ${firstV4.epoch} at DAA ${firstV4.daa}` : 'NO v4 epoch seen'}; blocks ${before.length} before / ${after.length} after the boundary at DAA ${Number.isFinite(BOUNDARY) ? BOUNDARY : 'never'} (chain ${chainBefore} / ${chainAfter}); programs ${programRows.map(p => `e${p.epoch}:${p.class}:${p.program_id}:${Math.max(...p.ready_ms)}ms`).join(' ')}; rejected miners ${rejectedMiner.map(r => r.length).join('/')} nodes ${rejectedNode.map(r => r.length).join('/')}; sinks ${sinks.join(' ')} (${checks.sinks_agree ? 'agree' : 'DIFFER'}); block counts ${counts.join('/')}; v4 switch lines ${switchLines.filter(Boolean).length}/3, v3 switch lines ${v3SwitchLines.filter(Boolean).length}/3`);
if (metal) {
log(`METAL: ${metal.prepares.length} PREPARE lines (${metal.prepares.filter(l => / class v4 /.test(l)).length} class v4, ${metal.prepares.filter(l => / class v3 /.test(l)).length} class v3), ${metal.prepared.length} prepared (${metal.prepared_v4.length} v4, ${metal.prepared_v3.length} v3), need ${metal.need}, mismatch/refusal lines ${metal.mismatch_lines.length + metal.refused.length}, accepted ${metal.accepted_total} (${metal.accepted_after_switch} after the switch), cpu re-check mismatched ${metal.cpu_recheck_mismatched}, rate ${metal.rate_mh_s} MH/s`);
for (const l of [...metal.prepares, ...metal.prepared, ...metal.swaps]) log(` ${l.slice(0, 260)}`);
for (const l of [...metal.mismatch_lines, ...metal.refused].slice(0, 10)) log(` BAD ${l.slice(0, 260)}`);
}
for (const [k, v] of Object.entries(checks)) if (!v) log(`FAILED CHECK ${k}`);
log(`summary: ${TMP}/summary.json`);
await stopAll();
process.exit(pass ? 0 : 1);

View file

@ -55,15 +55,16 @@
"proving_v0_activation_daa": 18446744073709551615, "proving_v0_activation_daa": 18446744073709551615,
"finality_v3_activation_daa": 18446744073709551615, "finality_v3_activation_daa": 18446744073709551615,
"program_class_v3_activation_daa": 18446744073709551615, "program_class_v3_activation_daa": 18446744073709551615,
"program_class_v4_activation_daa": 18446744073709551615,
"proving_v1_fresh_rule_daa": 18446744073709551615,
"exec_restart_number": 18446744073709551615,
"exec_restart_hash": "",
"exec_restart_trust_daa": 18446744073709551615,
"pow_genesis_dataset_log2": 28, "pow_genesis_dataset_log2": 28,
"proving_v1_activation_daa": 18446744073709551615, "proving_v1_activation_daa": 18446744073709551615,
"proving_v1_segment_blocks": 8, "proving_v1_segment_blocks": 8,
"proving_v1_unproven_daa": 10, "proving_v1_unproven_daa": 10,
"proving_v1_aggregator_share_bps": 1000, "proving_v1_aggregator_share_bps": 1000,
"fees_v1_activation_daa": 0, "fees_v1_activation_daa": 0,
"proving_v1_activation_daa": 18446744073709551615,
"proving_v1_segment_blocks": 8,
"proving_v1_unproven_daa": 10,
"proving_v1_aggregator_share_bps": 1000,
"fees": {"pgas": {"version": 1, "cycles_per_pgas": 1000, "intrinsic_pgas_per_tx": 300, "modexp_base": 10, "modexp_per_byte_numer": 1, "modexp_per_byte_denom": 10}, "block_proving_gas_limit": 120000, "shard_proving_gas_budget": 30000, "min_execution_base_fee_wei": 100000000000, "min_proving_base_fee_wei": 10000000000000, "initial_execution_base_fee_wei": 100000000000, "initial_proving_base_fee_wei": 10000000000000, "base_fee_change_denominator": 8} "fees": {"pgas": {"version": 1, "cycles_per_pgas": 1000, "intrinsic_pgas_per_tx": 300, "modexp_base": 10, "modexp_per_byte_numer": 1, "modexp_per_byte_denom": 10}, "block_proving_gas_limit": 120000, "shard_proving_gas_budget": 30000, "min_execution_base_fee_wei": 100000000000, "min_proving_base_fee_wei": 10000000000000, "initial_execution_base_fee_wei": 100000000000, "initial_proving_base_fee_wei": 10000000000000, "base_fee_change_denominator": 8}
} }

View file

@ -130,6 +130,24 @@ int main(int argc, char** argv) {
CHECK(pf_pack_class_ok(pk.programClass, pk.eraHex, "v3", EPOCH_34, why, sizeof(why)) == 0 && strstr(why, "era seed mismatch") == why, "a job naming another era refuses the v3 pack"); CHECK(pf_pack_class_ok(pk.programClass, pk.eraHex, "v3", EPOCH_34, why, sizeof(why)) == 0 && strstr(why, "era seed mismatch") == why, "a job naming another era refuses the v3 pack");
CHECK(pf_pack_class_ok("v2", "", "v3", EPOCH_33, why, sizeof(why)) == 0, "a job naming class v3 refuses a v2 pack"); CHECK(pf_pack_class_ok("v2", "", "v3", EPOCH_33, why, sizeof(why)) == 0, "a job naming class v3 refuses a v2 pack");
CHECK(pf_pack_class_ok("v2", "", "v2", EPOCH_33, why, sizeof(why)) == 1, "an era named on a v2 job is ignored"); CHECK(pf_pack_class_ok("v2", "", "v2", EPOCH_33, why, sizeof(why)) == 1, "an era named on a v2 job is ignored");
// Counter ASIC 3.0 (6 October 2026): a generator 4 pack is class v4 with its era seed; a job naming class v3
// refuses it and a job naming class v4 refuses a v3 pack; the era rule applies to v4 as to v3
snprintf(text, sizeof(text), "#define IGNEUM_SEED_BYTES_HEX \"%s\"\n#define IGNEUM_DAY_BYTES_HEX \"%s\"\n#define IGNEUM_GENERATOR 4\n#define IGNEUM_PROGRAM_CLASS \"v4\"\n#define IGNEUM_ERA_SEED_HEX \"%s\"\n#define IGNEUM_DATASET_LOG2 28\n#define IGNEUM_DATASET_MODE 1\n#define IGNEUM_SEEDW_INIT { %s }\n#define IGNEUM_KEY_INIT { %s }\n#define IGNEUM_CACHE_LOG2_WORDS 26\n#define IGNEUM_CACHE_SEGMENTS 4096u\n", EPOCH_34, DAY_20731, EPOCH_33, sw, kw);
write_file(dir, "program.h", text);
err[0] = 0;
CHECK(pf_load(dir, &pk, err, sizeof(err)) == 1 && strcmp(pk.programClass, "v4") == 0 && strcmp(pk.eraHex, EPOCH_33) == 0, "a generator 4 pack loads as class v4 with its era seed");
CHECK(pf_pack_class_ok(pk.programClass, pk.eraHex, "v4", EPOCH_33, why, sizeof(why)) == 1, "the v4 pack matches a job naming class v4 and its era");
CHECK(pf_pack_class_ok(pk.programClass, pk.eraHex, "v3", EPOCH_33, why, sizeof(why)) == 0 && strstr(why, "program class mismatch") == why, "a job naming class v3 refuses the v4 pack");
CHECK(pf_pack_class_ok(pk.programClass, pk.eraHex, "v4", EPOCH_34, why, sizeof(why)) == 0 && strstr(why, "era seed mismatch") == why, "a job naming another era refuses the v4 pack");
CHECK(pf_pack_class_ok("v3", EPOCH_33, "v4", EPOCH_33, why, sizeof(why)) == 0, "a job naming class v4 refuses a v3 pack");
{
char c[8] = {0}, e[65] = {0};
CHECK(pf_class_token("class=v4", c, sizeof(c), e, sizeof(e)) == 1 && strcmp(c, "v4") == 0, "the class=v4 token is read");
}
snprintf(text, sizeof(text), "#define IGNEUM_SEED_BYTES_HEX \"%s\"\n#define IGNEUM_DAY_BYTES_HEX \"%s\"\n#define IGNEUM_GENERATOR 4\n#define IGNEUM_PROGRAM_CLASS \"v3\"\n#define IGNEUM_DATASET_LOG2 28\n#define IGNEUM_DATASET_MODE 1\n#define IGNEUM_SEEDW_INIT { %s }\n#define IGNEUM_KEY_INIT { %s }\n#define IGNEUM_CACHE_LOG2_WORDS 26\n#define IGNEUM_CACHE_SEGMENTS 4096u\n", EPOCH_34, DAY_20731, sw, kw);
write_file(dir, "program.h", text);
err[0] = 0;
CHECK(pf_load(dir, &pk, err, sizeof(err)) == 0 && strstr(err, "does not match IGNEUM_GENERATOR 4") != NULL, "a v3 class line on a generator 4 pack is refused");
snprintf(text, sizeof(text), "#define IGNEUM_SEED_BYTES_HEX \"%s\"\n#define IGNEUM_DAY_BYTES_HEX \"%s\"\n#define IGNEUM_GENERATOR 3\n#define IGNEUM_PROGRAM_CLASS \"v2\"\n#define IGNEUM_DATASET_LOG2 28\n#define IGNEUM_DATASET_MODE 1\n#define IGNEUM_SEEDW_INIT { %s }\n#define IGNEUM_KEY_INIT { %s }\n#define IGNEUM_CACHE_LOG2_WORDS 26\n#define IGNEUM_CACHE_SEGMENTS 4096u\n", EPOCH_34, DAY_20731, sw, kw); snprintf(text, sizeof(text), "#define IGNEUM_SEED_BYTES_HEX \"%s\"\n#define IGNEUM_DAY_BYTES_HEX \"%s\"\n#define IGNEUM_GENERATOR 3\n#define IGNEUM_PROGRAM_CLASS \"v2\"\n#define IGNEUM_DATASET_LOG2 28\n#define IGNEUM_DATASET_MODE 1\n#define IGNEUM_SEEDW_INIT { %s }\n#define IGNEUM_KEY_INIT { %s }\n#define IGNEUM_CACHE_LOG2_WORDS 26\n#define IGNEUM_CACHE_SEGMENTS 4096u\n", EPOCH_34, DAY_20731, sw, kw);
write_file(dir, "program.h", text); write_file(dir, "program.h", text);
err[0] = 0; err[0] = 0;

View file

@ -29,8 +29,8 @@ typedef struct {
// read-width experiment (5 October 2026): the load class (0 when absent), bytes per hash, variant 5's scratch // read-width experiment (5 October 2026): the load class (0 when absent), bytes per hash, variant 5's scratch
uint32_t loadsPerHash, bytesPerHash, scratchOps, persistent, scratchWordsPerLane; uint32_t loadsPerHash, bytesPerHash, scratchOps, persistent, scratchWordsPerLane;
char loadClass[64]; char loadClass[64];
char programClass[8]; /* IGNEUM_PROGRAM_CLASS: "v2" or "v3" (Counter ASIC 2.0); absent = the generator's class */ char programClass[8]; /* IGNEUM_PROGRAM_CLASS: "v2", "v3" (Counter ASIC 2.0) or "v4" (Counter ASIC 3.0); absent = the generator's class */
char eraHex[65]; /* IGNEUM_ERA_SEED_HEX of a class v3 chain pack; empty otherwise */ char eraHex[65]; /* IGNEUM_ERA_SEED_HEX of a class v3 or v4 chain pack; empty otherwise */
// Counter ASIC 2.0 (5 October 2026): the mixer multiplier of the item derivation (IGNEUM_MIXER_MULT, 1 when absent: // Counter ASIC 2.0 (5 October 2026): the mixer multiplier of the item derivation (IGNEUM_MIXER_MULT, 1 when absent:
// version 2; 4 under class v3). The emitted memhard.h / kernel.cl carry it in their text; this is for the log lines. // version 2; 4 under class v3). The emitted memhard.h / kernel.cl carry it in their text; this is for the log lines.
uint32_t mixerMult; uint32_t mixerMult;
@ -284,12 +284,14 @@ static int pf_load(const char* dir, PfPack* pk, char* err, size_t cap) {
if (!pf_define_u32(prog, "IGNEUM_CACHE_SEGMENTS", &pk->cacheSegments)) { free(prog); return pf_fail(err, cap, "program.h has no IGNEUM_CACHE_SEGMENTS"); } if (!pf_define_u32(prog, "IGNEUM_CACHE_SEGMENTS", &pk->cacheSegments)) { free(prog); return pf_fail(err, cap, "program.h has no IGNEUM_CACHE_SEGMENTS"); }
if (!pf_define_u32(prog, "IGNEUM_GENERATOR", &pk->generator)) pk->generator = 1; if (!pf_define_u32(prog, "IGNEUM_GENERATOR", &pk->generator)) pk->generator = 1;
/* Spec 01 section 1.4.5: a pack whose generator version is not one this worker runs is refused. Generator 2 is /* Spec 01 section 1.4.5: a pack whose generator version is not one this worker runs is refused. Generator 2 is
* program class v2 (the lottery hash of 4 October 2026), generator 3 is class v3 (Counter ASIC 2.0). */ * program class v2 (the lottery hash of 4 October 2026), generator 3 is class v3 (Counter ASIC 2.0), generator 4
if (pk->generator != 2 && pk->generator != 3) { * is class v4 (Counter ASIC 3.0, 6 October 2026: class v3 plus the latency-shadow block, emitted in the pack's own
char m[200]; snprintf(m, sizeof(m), "program pack generator %u is not a generator version this worker runs (2 or 3)", (unsigned)pk->generator); * kernel text, so this loader needs nothing new beyond the number and the class token). */
if (pk->generator != 2 && pk->generator != 3 && pk->generator != 4) {
char m[200]; snprintf(m, sizeof(m), "program pack generator %u is not a generator version this worker runs (2, 3 or 4)", (unsigned)pk->generator);
free(prog); return pf_fail(err, cap, m); free(prog); return pf_fail(err, cap, m);
} }
strcpy(pk->programClass, pk->generator == 3 ? "v3" : "v2"); strcpy(pk->programClass, pk->generator == 4 ? "v4" : pk->generator == 3 ? "v3" : "v2");
{ {
char named[8] = {0}; char named[8] = {0};
if (pf_define_str(prog, "IGNEUM_PROGRAM_CLASS", named, sizeof(named)) && strcmp(named, pk->programClass) != 0) { if (pf_define_str(prog, "IGNEUM_PROGRAM_CLASS", named, sizeof(named)) && strcmp(named, pk->programClass) != 0) {
@ -429,16 +431,16 @@ static int pf_selftest(const PfPack* pk, const uint32_t* cacheHead, const uint32
} }
/* Counter ASIC 2.0 (5 October 2026): a job or prepare line may end with `class=<v2|v3>` and `era=<hex>` tokens (sent /* Counter ASIC 2.0 (5 October 2026): a job or prepare line may end with `class=<v2|v3|v4>` and `era=<hex>` tokens (sent
* only when the chain is on class v3, so every v2 line is the line of before). A pack matches the line when its class * only when the chain is on class v3 or v4, so every v2 line is the line of before). A pack matches the line when its
* is the named class and, when an era is named, its era seed is that era. Empty wanted strings accept any pack. * class is the named class and, when an era is named, its era seed is that era (every class after v2 carries one).
* Returns 1 on a match, else 0 with the reason in `why`. */ * Empty wanted strings accept any pack. Returns 1 on a match, else 0 with the reason in `why`. */
static int pf_pack_class_ok(const char* packClass, const char* packEra, const char* wantClass, const char* wantEra, char* why, size_t cap) { static int pf_pack_class_ok(const char* packClass, const char* packEra, const char* wantClass, const char* wantEra, char* why, size_t cap) {
if (wantClass && wantClass[0] && strcmp(wantClass, packClass) != 0) { if (wantClass && wantClass[0] && strcmp(wantClass, packClass) != 0) {
snprintf(why, cap, "program class mismatch: this pack is class %s, the job names class %s (export the pack again)", packClass, wantClass); snprintf(why, cap, "program class mismatch: this pack is class %s, the job names class %s (export the pack again)", packClass, wantClass);
return 0; return 0;
} }
if (wantEra && wantEra[0] && strcmp(packClass, "v3") == 0) { if (wantEra && wantEra[0] && strcmp(packClass, "v2") != 0) {
size_t i; int eq = strlen(packEra) == strlen(wantEra); size_t i; int eq = strlen(packEra) == strlen(wantEra);
for (i = 0; eq && packEra[i]; ++i) if (tolower((unsigned char)packEra[i]) != tolower((unsigned char)wantEra[i])) eq = 0; for (i = 0; eq && packEra[i]; ++i) if (tolower((unsigned char)packEra[i]) != tolower((unsigned char)wantEra[i])) eq = 0;
if (!eq) { snprintf(why, cap, "era seed mismatch: this pack was drawn under era %.16s, the job names era %.16s (export the pack again)", packEra[0] ? packEra : "(none)", wantEra); return 0; } if (!eq) { snprintf(why, cap, "era seed mismatch: this pack was drawn under era %.16s, the job names era %.16s (export the pack again)", packEra[0] ? packEra : "(none)", wantEra); return 0; }

View file

@ -2784,7 +2784,7 @@ final class ServeProgram {
/// The Swift-generated program (class v2); nil for a program compiled from a pack (class v3, Counter ASIC 2.0), /// The Swift-generated program (class v2); nil for a program compiled from a pack (class v3, Counter ASIC 2.0),
/// which has no Swift instruction list and so never races variants /// which has no Swift instruction list and so never races variants
let program: Program? let program: Program?
/// Counter ASIC 2.0: the class ("v2" or "v3") and, for a pack program, its era seed hex and loads per hash /// Counter ASIC 2.0: the class ("v2", "v3" or, Counter ASIC 3.0, "v4") and, for a pack program, its era seed hex and loads per hash
let programClass: String let programClass: String
let eraHex: String let eraHex: String
let loadsPerHash: Int let loadsPerHash: Int
@ -2804,7 +2804,7 @@ final class ServeProgram {
/// Whether this program is the one a line naming `cls` (and `era`) wants; empty names accept any (class v2 lines). /// Whether this program is the one a line naming `cls` (and `era`) wants; empty names accept any (class v2 lines).
func matches(cls: String, era: String) -> Bool { func matches(cls: String, era: String) -> Bool {
if cls != "" && cls != programClass { return false } if cls != "" && cls != programClass { return false }
if era != "" && programClass == "v3" && era.lowercased() != eraHex.lowercased() { return false } if era != "" && isPackClass(programClass) && era.lowercased() != eraHex.lowercased() { return false }
return true return true
} }
var compiled: CompiledHash { lock.lock(); defer { lock.unlock() }; return slot } var compiled: CompiledHash { lock.lock(); defer { lock.unlock() }; return slot }
@ -2984,11 +2984,18 @@ final class ServeDataset {
} }
func matches(cls: String, era: String) -> Bool { func matches(cls: String, era: String) -> Bool {
if cls != "" && cls != programClass { return false } if cls != "" && cls != programClass { return false }
if era != "" && programClass == "v3" && era.lowercased() != eraHex.lowercased() { return false } if era != "" && isPackClass(programClass) && era.lowercased() != eraHex.lowercased() { return false }
return true return true
} }
} }
/// Counter ASIC 2.0 and 3.0: the classes this worker runs from a prepared pack only (never from the Swift version 2
/// generator): class v3 (generator 3) and class v4 (generator 4, class v3 plus the latency-shadow block, which is in
/// the pack's own program_bound.metal). Each carries an era seed.
func isPackClass(_ cls: String) -> Bool { cls == "v3" || cls == "v4" }
/// The class name of a pack's IGNEUM_GENERATOR (packfile.h's rule).
func packClassOf(generator: UInt32) -> String { generator == 4 ? "v4" : generator == 3 ? "v3" : "v2" }
// The resident programs and datasets, shared by the job loop (main thread) and the prepare queue (background). // The resident programs and datasets, shared by the job loop (main thread) and the prepare queue (background).
final class ServeStore { final class ServeStore {
private let lock = NSLock() private let lock = NSLock()
@ -3053,7 +3060,8 @@ func serveProgram(_ gpu: GPU, _ store: ServeStore, seedHex: String, seed: [UInt8
// Counter ASIC 2.0 (5 October 2026): a class v3 program comes from the pack igneum-miner export-pack / --prepare-packs // Counter ASIC 2.0 (5 October 2026): a class v3 program comes from the pack igneum-miner export-pack / --prepare-packs
// wrote (program_bound.metal, the Rust emitter's text; program.h for the identity), never from the Swift generator. // wrote (program_bound.metal, the Rust emitter's text; program.h for the identity), never from the Swift generator.
// The pack is checked the way the one-click workers check it (proto-cuda/nvrtc/packfile.h): IGNEUM_GENERATOR 2 or 3, // Counter ASIC 3.0 (6 October 2026): a class v4 program (generator 4) the same way; its shadow block is in that text.
// The pack is checked the way the one-click workers check it (proto-cuda/nvrtc/packfile.h): IGNEUM_GENERATOR 2, 3 or 4,
// the class line against the generator, the seed bytes against the line's epoch seed, IGNEUM_SEEDW_INIT against the // the class line against the generator, the seed bytes against the line's epoch seed, IGNEUM_SEEDW_INIT against the
// words of IGNEUM_PROGRAM_ATTEMPT of that seed, and the class and era against what the line names. // words of IGNEUM_PROGRAM_ATTEMPT of that seed, and the class and era against what the line names.
func servePackProgram(_ gpu: GPU, _ store: ServeStore, seedHex: String, seed: [UInt8], dir: String, wantClass: String, wantEra: String) throws -> (ServeProgram, Double) { func servePackProgram(_ gpu: GPU, _ store: ServeStore, seedHex: String, seed: [UInt8], dir: String, wantClass: String, wantEra: String) throws -> (ServeProgram, Double) {
@ -3077,12 +3085,12 @@ func servePackProgram(_ gpu: GPU, _ store: ServeStore, seedHex: String, seed: [U
} }
func refuse(_ why: String) -> NSError { NSError(domain: "pack", code: 2, userInfo: [NSLocalizedDescriptionKey: "pack \(dir): \(why)"]) } func refuse(_ why: String) -> NSError { NSError(domain: "pack", code: 2, userInfo: [NSLocalizedDescriptionKey: "pack \(dir): \(why)"]) }
let generator = defineU32("IGNEUM_GENERATOR") ?? 1 let generator = defineU32("IGNEUM_GENERATOR") ?? 1
guard generator == 2 || generator == 3 else { throw refuse("program pack generator \(generator) is not a generator version this worker runs (2 or 3)") } guard generator == 2 || generator == 3 || generator == 4 else { throw refuse("program pack generator \(generator) is not a generator version this worker runs (2, 3 or 4)") }
let packClass = generator == 3 ? "v3" : "v2" let packClass = packClassOf(generator: generator)
if let named = defineStr("IGNEUM_PROGRAM_CLASS"), named != packClass { throw refuse("program pack IGNEUM_PROGRAM_CLASS \"\(named)\" does not match IGNEUM_GENERATOR \(generator)") } if let named = defineStr("IGNEUM_PROGRAM_CLASS"), named != packClass { throw refuse("program pack IGNEUM_PROGRAM_CLASS \"\(named)\" does not match IGNEUM_GENERATOR \(generator)") }
let eraHex = defineStr("IGNEUM_ERA_SEED_HEX") ?? "" let eraHex = defineStr("IGNEUM_ERA_SEED_HEX") ?? ""
if wantClass != "" && wantClass != packClass { throw refuse("program class mismatch: this pack is class \(packClass), the line names class \(wantClass) (export the pack again)") } if wantClass != "" && wantClass != packClass { throw refuse("program class mismatch: this pack is class \(packClass), the line names class \(wantClass) (export the pack again)") }
if wantEra != "" && packClass == "v3" && wantEra.lowercased() != eraHex.lowercased() { throw refuse("era seed mismatch: this pack was drawn under era \(eraHex.isEmpty ? "(none)" : String(eraHex.prefix(16))), the line names era \(wantEra.prefix(16)) (export the pack again)") } if wantEra != "" && isPackClass(packClass) && wantEra.lowercased() != eraHex.lowercased() { throw refuse("era seed mismatch: this pack was drawn under era \(eraHex.isEmpty ? "(none)" : String(eraHex.prefix(16))), the line names era \(wantEra.prefix(16)) (export the pack again)") }
if let packSeed = defineStr("IGNEUM_SEED_BYTES_HEX"), packSeed.lowercased() != seedHex.lowercased() { throw refuse("the pack is for epoch \(packSeed.prefix(16)), not \(seedHex.prefix(16))") } if let packSeed = defineStr("IGNEUM_SEED_BYTES_HEX"), packSeed.lowercased() != seedHex.lowercased() { throw refuse("the pack is for epoch \(packSeed.prefix(16)), not \(seedHex.prefix(16))") }
let attempt = defineU32("IGNEUM_PROGRAM_ATTEMPT") ?? 0 let attempt = defineU32("IGNEUM_PROGRAM_ATTEMPT") ?? 0
guard let seedw = defineWords("IGNEUM_SEEDW_INIT") else { throw refuse("program.h has no IGNEUM_SEEDW_INIT with 8 words") } guard let seedw = defineWords("IGNEUM_SEEDW_INIT") else { throw refuse("program.h has no IGNEUM_SEEDW_INIT with 8 words") }
@ -3128,11 +3136,11 @@ func servePackDataset(_ gpu: GPU, _ store: ServeStore, dayHex: String, dir: Stri
return String(programH[Range(m.range(at: 1), in: programH)!]) return String(programH[Range(m.range(at: 1), in: programH)!])
} }
let generator = defineU32("IGNEUM_GENERATOR") ?? 1 let generator = defineU32("IGNEUM_GENERATOR") ?? 1
let packClass = generator == 3 ? "v3" : "v2" let packClass = packClassOf(generator: generator)
let eraHex = defineStr("IGNEUM_ERA_SEED_HEX") ?? "" let eraHex = defineStr("IGNEUM_ERA_SEED_HEX") ?? ""
if let packDay = defineStr("IGNEUM_DAY_BYTES_HEX"), packDay.lowercased() != dayHex.lowercased() { throw refuse("the pack's day is \(packDay), not \(dayHex)") } if let packDay = defineStr("IGNEUM_DAY_BYTES_HEX"), packDay.lowercased() != dayHex.lowercased() { throw refuse("the pack's day is \(packDay), not \(dayHex)") }
if wantClass != "" && wantClass != packClass { throw refuse("program class mismatch: this pack is class \(packClass), the line names class \(wantClass)") } if wantClass != "" && wantClass != packClass { throw refuse("program class mismatch: this pack is class \(packClass), the line names class \(wantClass)") }
if wantEra != "" && packClass == "v3" && wantEra.lowercased() != eraHex.lowercased() { throw refuse("era seed mismatch: the pack's era is \(eraHex.prefix(16)), the line names \(wantEra.prefix(16))") } if wantEra != "" && isPackClass(packClass) && wantEra.lowercased() != eraHex.lowercased() { throw refuse("era seed mismatch: the pack's era is \(eraHex.prefix(16)), the line names \(wantEra.prefix(16))") }
if (defineU32("IGNEUM_HOT_MB") ?? 0) > 0 { throw refuse("a hot-table pack (IGNEUM_HOT_MB) is not served by this worker") } if (defineU32("IGNEUM_HOT_MB") ?? 0) > 0 { throw refuse("a hot-table pack (IGNEUM_HOT_MB) is not served by this worker") }
guard (defineU32("IGNEUM_DATASET_MODE") ?? 0) == 1 else { throw refuse("not a memory-hard pack (IGNEUM_DATASET_MODE 1)") } guard (defineU32("IGNEUM_DATASET_MODE") ?? 0) == 1 else { throw refuse("not a memory-hard pack (IGNEUM_DATASET_MODE 1)") }
let datasetLog2 = Int(defineU32("IGNEUM_DATASET_LOG2") ?? 28) let datasetLog2 = Int(defineU32("IGNEUM_DATASET_LOG2") ?? 28)
@ -3182,18 +3190,19 @@ func runServe(_ opts: Options) -> Never {
if f.isEmpty { continue } if f.isEmpty { continue }
if f[0] == "quit" { break } if f[0] == "quit" { break }
// Counter ASIC 2.0 (5 October 2026): a job or prepare line of a class v3 epoch ends with `class=v3 era=<hex>` // Counter ASIC 2.0 (5 October 2026): a job or prepare line of a class v3 epoch ends with `class=v3 era=<hex>`
// (a class v2 line is the line of before). A class v2 program is generated here from the seed (the Swift // (a class v2 line is the line of before); Counter ASIC 3.0 (6 October 2026): a class v4 epoch's with
// version 2 generator); a class v3 program is compiled from the pack the prepare line names (servePackProgram), // `class=v4 era=<hex>`. A class v2 program is generated here from the seed (the Swift version 2 generator); a
// so a v3 job on seeds with no prepared v3 pack asks for one (`need`) instead of mining the wrong program. // class v3 or v4 program is compiled from the pack the prepare line names (servePackProgram), so a v3 or v4
// job on seeds with no prepared pack of that class asks for one (`need`) instead of mining the wrong program.
var wantClass = "", wantEra = "" var wantClass = "", wantEra = ""
while let last = f.last, last.hasPrefix("class=") || last.hasPrefix("era=") { while let last = f.last, last.hasPrefix("class=") || last.hasPrefix("era=") {
if last.hasPrefix("class=") { wantClass = String(last.dropFirst(6)) } else { wantEra = String(last.dropFirst(4)) } if last.hasPrefix("class=") { wantClass = String(last.dropFirst(6)) } else { wantEra = String(last.dropFirst(4)) }
f.removeLast() f.removeLast()
} }
if wantClass != "" && wantClass != "v2" && wantClass != "v3" { if wantClass != "" && wantClass != "v2" && !isPackClass(wantClass) {
let id = f.count > 1 ? f[1] : "0" let id = f.count > 1 ? f[1] : "0"
if f[0] == "prepare" { emit("prepare-failed \(id) \(f.count > 2 ? f[2] : "0") program class \(wantClass) is not one this worker runs (v2 or v3)") } if f[0] == "prepare" { emit("prepare-failed \(id) \(f.count > 2 ? f[2] : "0") program class \(wantClass) is not one this worker runs (v2, v3 or v4)") }
else { emit("error \(id) program class \(wantClass) is not one this worker runs (v2 or v3)") } else { emit("error \(id) program class \(wantClass) is not one this worker runs (v2, v3 or v4)") }
continue continue
} }
if f[0] == "prepare" { if f[0] == "prepare" {
@ -3206,15 +3215,15 @@ func runServe(_ opts: Options) -> Never {
let have = (store.program(epochHex, cls: wantClass, era: wantEra) != nil, store.dataset(dayHex, cls: wantClass, era: wantEra) != nil) let have = (store.program(epochHex, cls: wantClass, era: wantEra) != nil, store.dataset(dayHex, cls: wantClass, era: wantEra) != nil)
if have.0 && have.1 { emit("prepared \(epochHex) \(dayHex) 0 program 0 dataset 0 (already resident)"); continue } if have.0 && have.1 { emit("prepared \(epochHex) \(dayHex) 0 program 0 dataset 0 (already resident)"); continue }
let packDir = f.count > 3 ? f[3...].joined(separator: " ") : "" let packDir = f.count > 3 ? f[3...].joined(separator: " ") : ""
if wantClass == "v3" && packDir.isEmpty { emit("prepare-failed \(epochHex) \(dayHex) a class v3 program is compiled from a pack: send prepare with a pack directory (igneum-miner --prepare-packs <dir>)"); continue } if isPackClass(wantClass) && packDir.isEmpty { emit("prepare-failed \(epochHex) \(dayHex) a class \(wantClass) program is compiled from a pack: send prepare with a pack directory (igneum-miner --prepare-packs <dir>)"); continue }
prepareQueue.async { prepareQueue.async {
let t0 = nowNs() let t0 = nowNs()
do { do {
let (sp, progMs) = wantClass == "v3" let (sp, progMs) = isPackClass(wantClass)
? try servePackProgram(gpu, store, seedHex: epochHex, seed: epochSeed, dir: packDir, wantClass: wantClass, wantEra: wantEra) ? try servePackProgram(gpu, store, seedHex: epochHex, seed: epochSeed, dir: packDir, wantClass: wantClass, wantEra: wantEra)
: try serveProgram(gpu, store, seedHex: epochHex, seed: epochSeed, datasetLog2: datasetLog2) : try serveProgram(gpu, store, seedHex: epochHex, seed: epochSeed, datasetLog2: datasetLog2)
// a class v3 day is built from the pack (its construction and layout), a v2 day by the Swift context // a class v3 or v4 day is built from the pack (its construction and layout), a v2 day by the Swift context
let (sd, dsMs) = wantClass == "v3" let (sd, dsMs) = isPackClass(wantClass)
? try servePackDataset(gpu, store, dayHex: dayHex, dir: packDir, wantClass: wantClass, wantEra: wantEra) ? try servePackDataset(gpu, store, dayHex: dayHex, dir: packDir, wantClass: wantClass, wantEra: wantEra)
: serveDataset(gpu, store, dayHex: dayHex, day: daySeed, datasetLog2: datasetLog2) : serveDataset(gpu, store, dayHex: dayHex, day: daySeed, datasetLog2: datasetLog2)
// The race: the prepared program against its own dataset, exclusive windows between jobs // The race: the prepared program against its own dataset, exclusive windows between jobs
@ -3226,7 +3235,7 @@ func runServe(_ opts: Options) -> Never {
} }
let (np, nd) = store.counts() let (np, nd) = store.counts()
emit("prepared \(epochHex) \(dayHex) \(fmt(ms(t0, nowNs()), 1)) program \(fmt(progMs, 1)) dataset \(fmt(dsMs, 1)) race \(fmt(raceMs, 1)) variant \(sp.compiled.variant) class \(sp.programClass) loads/hash \(sp.loadsPerHash) cache-fill \(fmt(sd.cacheFillGPUms, 1)) build \(fmt(sd.buildGPUms, 1)) resident \(np) programs \(nd) datasets") emit("prepared \(epochHex) \(dayHex) \(fmt(ms(t0, nowNs()), 1)) program \(fmt(progMs, 1)) dataset \(fmt(dsMs, 1)) race \(fmt(raceMs, 1)) variant \(sp.compiled.variant) class \(sp.programClass) loads/hash \(sp.loadsPerHash) cache-fill \(fmt(sd.cacheFillGPUms, 1)) build \(fmt(sd.buildGPUms, 1)) resident \(np) programs \(nd) datasets")
} catch { emit("prepare-failed \(epochHex) \(dayHex) \(wantClass == "v3" ? "" : "Metal compile failed: ")\(error.localizedDescription)") } } catch { emit("prepare-failed \(epochHex) \(dayHex) \(isPackClass(wantClass) ? "" : "Metal compile failed: ")\(error.localizedDescription)") }
} }
continue continue
} }
@ -3251,12 +3260,12 @@ func runServe(_ opts: Options) -> Never {
if store.program(pair.0, cls: wantClass, era: wantEra) == nil || store.dataset(pair.1, cls: wantClass, era: wantEra) == nil { prepareQueue.sync {} } if store.program(pair.0, cls: wantClass, era: wantEra) == nil || store.dataset(pair.1, cls: wantClass, era: wantEra) == nil { prepareQueue.sync {} }
let sp: ServeProgram let sp: ServeProgram
let sd: ServeDataset let sd: ServeDataset
if wantClass == "v3" { if isPackClass(wantClass) {
// a v3 job runs on the pack program and the pack-built day of its class and era, both prepared; nothing // a v3 or v4 job runs on the pack program and the pack-built day of its class and era, both prepared;
// is compiled inline (the Swift generator and context are the version 2 construction) // nothing is compiled inline (the Swift generator and context are the version 2 construction)
guard let resident = store.program(pair.0, cls: wantClass, era: wantEra), let day = store.dataset(pair.1, cls: wantClass, era: wantEra) else { guard let resident = store.program(pair.0, cls: wantClass, era: wantEra), let day = store.dataset(pair.1, cls: wantClass, era: wantEra) else {
emit("need \(pair.0) \(pair.1)") emit("need \(pair.0) \(pair.1)")
emit("error \(jobId) pack (metal): program class mismatch: no class v3 program and day for epoch \(pair.0.prefix(16)) day \(pair.1) is resident (this worker builds v3 from a pack); send prepare with a pack directory") emit("error \(jobId) pack (metal): program class mismatch: no class \(wantClass) program and day for epoch \(pair.0.prefix(16)) day \(pair.1) is resident (this worker builds \(wantClass) from a pack); send prepare with a pack directory")
continue continue
} }
sp = resident; sd = day sp = resident; sd = day

29
tools/ci/override-json-check.sh Executable file
View file

@ -0,0 +1,29 @@
#!/usr/bin/env bash
# The duplicate-field class (6 October 2026): two merges each appended the proving v1 block to
# infra/fast-time/override-60x.json, so the file carried `proving_v1_activation_daa` twice; the node refuses such a file
# ("duplicate field") and the class-v3 and class-v4 fast-time gates could not start a node from it, while the fork's
# every-field test skips when the file is not beside it (it is not on the PCs). Rule: every override params file the
# harnesses and the node read parses as JSON with no duplicate key. This check fails CI when one does not.
set -euo pipefail
cd "$(dirname "$0")/../.."
fail=0
while IFS= read -r f; do
if ! python3 - "$f" <<'PY'
import json, sys, collections
def pairs(p):
c = collections.Counter(k for k, _ in p)
dups = sorted(k for k, n in c.items() if n > 1)
if dups:
raise SystemExit(f"duplicate key(s) {dups}")
return dict(p)
try:
json.load(open(sys.argv[1]), object_pairs_hook=pairs)
except SystemExit as e:
print(f"override-json: {sys.argv[1]}: {e}"); sys.exit(1)
except Exception as e:
print(f"override-json: {sys.argv[1]}: does not parse: {e}"); sys.exit(1)
PY
then fail=1; fi
done < <(git ls-files 'infra/fast-time/*.json' 'tools/finality-attacks/redteam/*.json' 'infra/**/override*.json' | sort -u)
[ "$fail" = 0 ] && echo "override-json: every override params file parses with no duplicate key"
exit $fail