From e925b20f7ca10454e762cc7fe1c7d30e8ed7cd84 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:58:49 +0000 Subject: [PATCH 1/3] 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 --- .github/workflows/ci.yml | 2 + igneum-pow/src/emit.rs | 13 +- igneum-pow/src/generator.rs | 123 +++++++++-- igneum-pow/src/lib.rs | 2 +- igneum-pow/src/main.rs | 4 +- igneum-pow/src/packcheck.rs | 37 +++- infra/fast-time/README.md | 8 + infra/fast-time/class-v4.mjs | 305 +++++++++++++++++++++++++++ infra/fast-time/override-60x.json | 9 +- proto-cuda/nvrtc/emu/packfile-test.c | 18 ++ proto-cuda/nvrtc/packfile.h | 24 ++- proto-metal/main.swift | 57 ++--- tools/ci/override-json-check.sh | 29 +++ 13 files changed, 562 insertions(+), 69 deletions(-) create mode 100644 infra/fast-time/class-v4.mjs create mode 100755 tools/ci/override-json-check.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8fed26f9..e00181ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,6 +69,8 @@ jobs: run: bash tools/ci/no-conflict-markers.sh - name: copied sources are re-stamped before a build 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 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) diff --git a/igneum-pow/src/emit.rs b/igneum-pow/src/emit.rs index 104a5191..37437fe3 100644 --- a/igneum-pow/src/emit.rs +++ b/igneum-pow/src/emit.rs @@ -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 /// 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 { 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"); + if p.program_class() == ProgramClass::V4 { + 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=).\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=).\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)))); diff --git a/igneum-pow/src/generator.rs b/igneum-pow/src/generator.rs index 7cd022bd..7cbffc10 100644 --- a/igneum-pow/src/generator.rs +++ b/igneum-pow/src/generator.rs @@ -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)`). 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 /// 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`]. +/// 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)] pub enum ProgramClass { #[default] V2, V3, + V4, } /// 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`. 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 /// draw is consumed, so widening the set at genesis keeps the derivation). 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`). /// 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 { + 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)); - p.generator = GENERATOR_VERSION_V3; + p.generator = generator; p.era_bytes = Some(era_bytes.to_vec()); p } @@ -813,6 +832,7 @@ impl ProgramClass { match self { ProgramClass::V2 => LoadClass::V2, ProgramClass::V3 => V3_CLASS, + ProgramClass::V4 => V4_CLASS, } } @@ -821,23 +841,26 @@ impl ProgramClass { match self { ProgramClass::V2 => GENERATOR_VERSION, 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 { match generator { GENERATOR_VERSION => Some(ProgramClass::V2), GENERATOR_VERSION_V3 => Some(ProgramClass::V3), + GENERATOR_VERSION_V4 => Some(ProgramClass::V4), _ => 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 { match self { ProgramClass::V2 => "v2", ProgramClass::V3 => "v3", + ProgramClass::V4 => "v4", } } @@ -845,10 +868,16 @@ impl ProgramClass { match s.trim() { "v2" => Some(ProgramClass::V2), "v3" => Some(ProgramClass::V3), + "v4" => Some(ProgramClass::V4), _ => 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. pub fn as_u8(&self) -> 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, /// can never be mistaken for this one. pub fn program_id(&self) -> u64 { - 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 + 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)`, a class v4 program's + // `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) } 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). + /// The program class of this program, from its generator version (3 = v3, 4 = v4, everything else v2). 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). 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. - // Without era bytes (a template before the era is known) the bare V3_CLASS stands. - if let (ProgramClass::V3, Some(era)) = (class, era_bytes) { - return generate_era(seed_string, seed_bytes, V3_CLASS, era, &V3_ALLOWED); + // Class v4 (Counter ASIC 3.0): the same draw inside V4_CLASS (V3_CLASS plus the shadow block), generator 4. + // Without era bytes (a template before the era is known) the bare class stands. + 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()); 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 }; + // The era is a property of class v3 and v4 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.has_era() { era_bytes.map(|b| b.to_vec()) } else { None }; p } @@ -1644,16 +1681,68 @@ mod tests { 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] { + for c in [ProgramClass::V2, ProgramClass::V3, ProgramClass::V4] { 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::from_generator(5), None); + assert_eq!(ProgramClass::parse("v5"), None); assert_eq!(ProgramClass::default(), ProgramClass::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 diff --git a/igneum-pow/src/lib.rs b/igneum-pow/src/lib.rs index 8e66f7a3..1b398fab 100644 --- a/igneum-pow/src/lib.rs +++ b/igneum-pow/src/lib.rs @@ -34,7 +34,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, 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 seed::{fnv1a64, seed_words, SplitMix64}; pub use verify::{hash_warp, interpret_warp_init, verify_block, DatasetMode, DatasetSource, Epoch}; diff --git a/igneum-pow/src/main.rs b/igneum-pow/src/main.rs index 5af90796..022d0681 100644 --- a/igneum-pow/src/main.rs +++ b/igneum-pow/src/main.rs @@ -40,7 +40,7 @@ struct Args { class: LoadClass, /// Days since genesis for the cache growth rule of a class with `growth` (0: the genesis cache). 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, /// The era seed bytes a class v3 chain program records (`--era-hex`). era_hex: Option, @@ -96,7 +96,7 @@ fn usage() -> ! { \x20 --class C load class: v2 (default), mx4, mx8 (class v3: mixer x8, cache growth), dr (Counter ASIC 3.0 item 2: the per-day derivation program, dr736 = the x8-equivalent), w4, w16, w64, w64x4, p4,p16,p64[xN], m[g]\n\ \x20 also: w4, w16, w64, w64x4, p4,p16,p64[xN], m[g], +shx (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 --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/ or :<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)" ); diff --git a/igneum-pow/src/packcheck.rs b/igneum-pow/src/packcheck.rs index 5bd4240c..e8df4c84 100644 --- a/igneum-pow/src/packcheck.rs +++ b/igneum-pow/src/packcheck.rs @@ -23,7 +23,7 @@ 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). + /// `IGNEUM_GENERATOR` (2, 3 or 4; 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, @@ -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`] 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. +/// [`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; class v4 Counter ASIC 3.0, 6 October 2026). `want_class` `None` accepts any +/// 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( program_h: &str, seeds_txt: Option<&str>, @@ -177,7 +177,7 @@ pub fn verify_pack_texts_chain( ) -> 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)"))); + 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 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()))); } } - if let (Some(want), ProgramClass::V3) = (want_era, class) { + if let (Some(want), true) = (want_era, class.has_era()) { 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())), + 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()))?; @@ -393,5 +393,28 @@ mod tests { // 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(_)))); + // 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); } } diff --git a/infra/fast-time/README.md b/infra/fast-time/README.md index 10078f7c..3fdbed6a 100644 --- a/infra/fast-time/README.md +++ b/infra/fast-time/README.md @@ -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 `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 ` 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. 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`, @@ -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_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_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: diff --git a/infra/fast-time/class-v4.mjs b/infra/fast-time/class-v4.mjs new file mode 100644 index 00000000..6fda03ad --- /dev/null +++ b/infra/fast-time/class-v4.mjs @@ -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 ] [--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 --prepare-packs +// --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); diff --git a/infra/fast-time/override-60x.json b/infra/fast-time/override-60x.json index eccb27f8..8405a79d 100644 --- a/infra/fast-time/override-60x.json +++ b/infra/fast-time/override-60x.json @@ -55,15 +55,16 @@ "proving_v0_activation_daa": 18446744073709551615, "finality_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, "proving_v1_activation_daa": 18446744073709551615, "proving_v1_segment_blocks": 8, "proving_v1_unproven_daa": 10, "proving_v1_aggregator_share_bps": 1000, "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} } diff --git a/proto-cuda/nvrtc/emu/packfile-test.c b/proto-cuda/nvrtc/emu/packfile-test.c index f9cf9dfb..eea550e7 100644 --- a/proto-cuda/nvrtc/emu/packfile-test.c +++ b/proto-cuda/nvrtc/emu/packfile-test.c @@ -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("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"); + // 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); write_file(dir, "program.h", text); err[0] = 0; diff --git a/proto-cuda/nvrtc/packfile.h b/proto-cuda/nvrtc/packfile.h index d4f2bc19..77318617 100644 --- a/proto-cuda/nvrtc/packfile.h +++ b/proto-cuda/nvrtc/packfile.h @@ -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 uint32_t loadsPerHash, bytesPerHash, scratchOps, persistent, scratchWordsPerLane; char loadClass[64]; - char programClass[8]; /* IGNEUM_PROGRAM_CLASS: "v2" or "v3" (Counter ASIC 2.0); absent = the generator's class */ - char eraHex[65]; /* IGNEUM_ERA_SEED_HEX of a class v3 chain pack; empty otherwise */ + 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 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: // 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; @@ -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_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 - * program class v2 (the lottery hash of 4 October 2026), generator 3 is class v3 (Counter ASIC 2.0). */ - if (pk->generator != 2 && pk->generator != 3) { - 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); + * program class v2 (the lottery hash of 4 October 2026), generator 3 is class v3 (Counter ASIC 2.0), generator 4 + * is class v4 (Counter ASIC 3.0, 6 October 2026: class v3 plus the latency-shadow block, emitted in the pack's own + * 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); } - strcpy(pk->programClass, pk->generator == 3 ? "v3" : "v2"); + strcpy(pk->programClass, pk->generator == 4 ? "v4" : pk->generator == 3 ? "v3" : "v2"); { char named[8] = {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=` and `era=` 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 - * is the named class and, when an era is named, its era seed is that era. Empty wanted strings accept any pack. - * Returns 1 on a match, else 0 with the reason in `why`. */ +/* Counter ASIC 2.0 (5 October 2026): a job or prepare line may end with `class=` and `era=` tokens (sent + * 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 + * class is the named class and, when an era is named, its era seed is that era (every class after v2 carries one). + * 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) { 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); 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); 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; } diff --git a/proto-metal/main.swift b/proto-metal/main.swift index 085dbc74..07b1ddb3 100644 --- a/proto-metal/main.swift +++ b/proto-metal/main.swift @@ -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), /// which has no Swift instruction list and so never races variants 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 eraHex: String 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). func matches(cls: String, era: String) -> Bool { 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 } var compiled: CompiledHash { lock.lock(); defer { lock.unlock() }; return slot } @@ -2984,11 +2984,18 @@ final class ServeDataset { } func matches(cls: String, era: String) -> Bool { 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 } } +/// 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). final class ServeStore { 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 // 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 // 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) { @@ -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)"]) } 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)") } - let packClass = generator == 3 ? "v3" : "v2" + 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 = 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)") } 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 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))") } 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") } @@ -3128,11 +3136,11 @@ func servePackDataset(_ gpu: GPU, _ store: ServeStore, dayHex: String, dir: Stri return String(programH[Range(m.range(at: 1), in: programH)!]) } let generator = defineU32("IGNEUM_GENERATOR") ?? 1 - let packClass = generator == 3 ? "v3" : "v2" + let packClass = packClassOf(generator: generator) 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 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") } 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) @@ -3182,18 +3190,19 @@ func runServe(_ opts: Options) -> Never { if f.isEmpty { continue } 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=` - // (a class v2 line is the line of before). A class v2 program is generated here from the seed (the Swift - // version 2 generator); a class v3 program is compiled from the pack the prepare line names (servePackProgram), - // so a v3 job on seeds with no prepared v3 pack asks for one (`need`) instead of mining the wrong program. + // (a class v2 line is the line of before); Counter ASIC 3.0 (6 October 2026): a class v4 epoch's with + // `class=v4 era=`. A class v2 program is generated here from the seed (the Swift version 2 generator); a + // 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 = "" 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)) } f.removeLast() } - if wantClass != "" && wantClass != "v2" && wantClass != "v3" { + if wantClass != "" && wantClass != "v2" && !isPackClass(wantClass) { 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)") } - else { emit("error \(id) 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, v3 or v4)") } continue } 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) 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: " ") : "" - 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 )"); 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 )"); continue } prepareQueue.async { let t0 = nowNs() 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 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 - let (sd, dsMs) = wantClass == "v3" + // 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) = isPackClass(wantClass) ? try servePackDataset(gpu, store, dayHex: dayHex, dir: packDir, wantClass: wantClass, wantEra: wantEra) : serveDataset(gpu, store, dayHex: dayHex, day: daySeed, datasetLog2: datasetLog2) // 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() 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 } @@ -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 {} } let sp: ServeProgram let sd: ServeDataset - if wantClass == "v3" { - // a v3 job runs on the pack program and the pack-built day of its class and era, both prepared; nothing - // is compiled inline (the Swift generator and context are the version 2 construction) + if isPackClass(wantClass) { + // a v3 or v4 job runs on the pack program and the pack-built day of its class and era, both prepared; + // 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 { 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 } sp = resident; sd = day diff --git a/tools/ci/override-json-check.sh b/tools/ci/override-json-check.sh new file mode 100755 index 00000000..62d0ad2f --- /dev/null +++ b/tools/ci/override-json-check.sh @@ -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 From f0b3355a7a8e74ea4e47a27351859daf5efe30a1 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:10:00 +0000 Subject: [PATCH 2/3] Counter ASIC 3.0 gates (node): G4, G4b, G5 and G6 GREEN for class v4 (the three fast-time summaries incl. the known-failed case, node-gates.md, counter-asic-3-node.md) Co-Authored-By: Claude Fable 5.1 --- .../class-v4-20261006-1553Z-cpu.json | 384 +++++++++++++++ .../class-v4-20261006-metal.json | 447 ++++++++++++++++++ .../class-v4-20261006-never-failed-case.json | 245 ++++++++++ docs/plans/counter-asic-3-gate/node-gates.md | 12 + docs/plans/counter-asic-3-node.md | 77 +++ 5 files changed, 1165 insertions(+) create mode 100644 docs/plans/counter-asic-3-gate/class-v4-20261006-1553Z-cpu.json create mode 100644 docs/plans/counter-asic-3-gate/class-v4-20261006-metal.json create mode 100644 docs/plans/counter-asic-3-gate/class-v4-20261006-never-failed-case.json create mode 100644 docs/plans/counter-asic-3-gate/node-gates.md create mode 100644 docs/plans/counter-asic-3-node.md diff --git a/docs/plans/counter-asic-3-gate/class-v4-20261006-1553Z-cpu.json b/docs/plans/counter-asic-3-gate/class-v4-20261006-1553Z-cpu.json new file mode 100644 index 00000000..b81cb997 --- /dev/null +++ b/docs/plans/counter-asic-3-gate/class-v4-20261006-1553Z-cpu.json @@ -0,0 +1,384 @@ +{ + "pass": true, + "checks": { + "v4_switch_line_on_every_node": true, + "v4_switch_line_names_the_rounded_epoch": true, + "v3_switch_line_on_every_node": true, + "template_switched_at_the_first_v4_epoch": true, + "template_switched_at_the_first_v3_epoch": true, + "blocks_before_the_boundary": true, + "blocks_after_the_boundary": true, + "v3_and_v4_programs_seen": true, + "v2_programs_seen_when_v3_starts_later": true, + "program_ids_differ_across_the_switch": true, + "miners_agree_on_every_program": true, + "zero_rejected_by_miners": true, + "zero_rejected_by_nodes": true, + "sinks_agree": true, + "block_counts_agree": true + }, + "activation": 150, + "v3_activation": 60, + "epoch_blocks": 60, + "first_v3_epoch": 1, + "first_v4_epoch": 3, + "boundary_daa": 180, + "secs": 420, + "threads": 1, + "node": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/vendor/igneum-node-ca3v4/target-ca3v4/release/igneumd", + "miner": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/vendor/igneum-node-ca3v4/target-ca3v4/release/igneum-miner", + "metal_worker": null, + "genesis_bits": "0x1f010000", + "template_switch_v3": { + "epoch": 1, + "daa": 60, + "at": 25.7 + }, + "template_switch": { + "epoch": 3, + "daa": 180, + "at": 163.9 + }, + "run_ended_at_s": 285, + "final_daa": 304, + "blocks": { + "total": 309, + "before_boundary": 181, + "after_boundary": 128, + "chain_before": 177, + "chain_after": 127 + }, + "programs": [ + { + "epoch": 0, + "class": "v2", + "program_id": "8f8806638d59850f", + "seed": "234e082d653dc69d", + "miners": 3, + "disagree": false, + "ready_ms": [ + 198, + 199, + 199 + ] + }, + { + "epoch": 1, + "class": "v3", + "program_id": "29ced0d031cc121b", + "seed": "bb01d0cccbc2ea2e", + "miners": 3, + "disagree": false, + "ready_ms": [ + 187, + 186, + 187 + ] + }, + { + "epoch": 2, + "class": "v3", + "program_id": "341fd4076256cd7b", + "seed": "060caf3a3e006592", + "miners": 3, + "disagree": false, + "ready_ms": [ + 2, + 2, + 2 + ] + }, + { + "epoch": 3, + "class": "v4", + "program_id": "070ca0f94e7f8f88", + "seed": "cc9d1bb4443c3028", + "miners": 3, + "disagree": false, + "ready_ms": [ + 2, + 2, + 2 + ] + }, + { + "epoch": 4, + "class": "v4", + "program_id": "b8cbe1ef7ab1d748", + "seed": "54e2edcfe251360c", + "miners": 3, + "disagree": false, + "ready_ms": [ + 2, + 2, + 2 + ] + }, + { + "epoch": 5, + "class": "v4", + "program_id": "9321cbd9c54c0b83", + "seed": "53a2fe6ce1308fb3", + "miners": 3, + "disagree": false, + "ready_ms": [ + 2, + 2, + 2 + ] + } + ], + "accepted_per_miner": [ + 103, + 103, + 102 + ], + "rejected_by_miners": [ + 0, + 0, + 0 + ], + "rejected_by_nodes": [ + 0, + 0, + 0 + ], + "rejected_lines": [], + "sinks": [ + "4da9ac669effdfab", + "4da9ac669effdfab", + "4da9ac669effdfab" + ], + "block_counts": [ + 308, + 308, + 308 + ], + "tips_per_node": [ + 1, + 1, + 1 + ], + "switch_lines": [ + "Program class v4 from the override file: active from epoch 3 (DAA score 150 rounded up to the epoch boundary at 180, epochs of 60 DAA)", + "Program class v4 from the override file: active from epoch 3 (DAA score 150 rounded up to the epoch boundary at 180, epochs of 60 DAA)", + "Program class v4 from the override file: active from epoch 3 (DAA score 150 rounded up to the epoch boundary at 180, epochs of 60 DAA)" + ], + "v3_switch_lines": [ + "Program class v3 from the override file: active from epoch 1 (DAA score 60 rounded up to the epoch boundary at 60, epochs of 60 DAA)", + "Program class v3 from the override file: active from epoch 1 (DAA score 60 rounded up to the epoch boundary at 60, epochs of 60 DAA)", + "Program class v3 from the override file: active from epoch 1 (DAA score 60 rounded up to the epoch boundary at 60, epochs of 60 DAA)" + ], + "samples": [ + { + "t": 4.6, + "daa": 0, + "epoch": 0, + "class": 2, + "nodes": [ + "0/234e082d", + "0/234e082d", + "0/234e082d" + ] + }, + { + "t": 19.7, + "daa": 51, + "epoch": 0, + "class": 2, + "nodes": [ + "51/b6699ac7", + "51/b6699ac7", + "51/b6699ac7" + ] + }, + { + "t": 34.7, + "daa": 62, + "epoch": 1, + "class": 3, + "nodes": [ + "62/50a9cf7a", + "62/50a9cf7a", + "62/50a9cf7a" + ] + }, + { + "t": 49.7, + "daa": 68, + "epoch": 1, + "class": 3, + "nodes": [ + "68/e0145a0c", + "68/e0145a0c", + "68/e0145a0c" + ] + }, + { + "t": 64.7, + "daa": 76, + "epoch": 1, + "class": 3, + "nodes": [ + "76/c23ad948", + "76/c23ad948", + "76/c23ad948" + ] + }, + { + "t": 79.8, + "daa": 97, + "epoch": 1, + "class": 3, + "nodes": [ + "97/b039ce4c", + "97/b039ce4c", + "97/b039ce4c" + ] + }, + { + "t": 94.8, + "daa": 113, + "epoch": 1, + "class": 3, + "nodes": [ + "113/ae890f91", + "113/ae890f91", + "113/ae890f91" + ] + }, + { + "t": 109.8, + "daa": 128, + "epoch": 2, + "class": 3, + "nodes": [ + "128/3545bd70", + "128/3545bd70", + "128/3545bd70" + ] + }, + { + "t": 124.8, + "daa": 138, + "epoch": 2, + "class": 3, + "nodes": [ + "138/5e342f39", + "138/5e342f39", + "138/5e342f39" + ] + }, + { + "t": 139.8, + "daa": 152, + "epoch": 2, + "class": 3, + "nodes": [ + "152/f376cfe3", + "152/f376cfe3", + "152/f376cfe3" + ] + }, + { + "t": 154.9, + "daa": 168, + "epoch": 2, + "class": 3, + "nodes": [ + "168/18974e58", + "168/18974e58", + "168/18974e58" + ] + }, + { + "t": 169.9, + "daa": 189, + "epoch": 3, + "class": 4, + "nodes": [ + "189/2efab18a", + "189/2efab18a", + "189/2efab18a" + ] + }, + { + "t": 184.9, + "daa": 203, + "epoch": 3, + "class": 4, + "nodes": [ + "203/024556df", + "203/024556df", + "203/024556df" + ] + }, + { + "t": 199.9, + "daa": 215, + "epoch": 3, + "class": 4, + "nodes": [ + "215/6aa4e4c9", + "215/6aa4e4c9", + "215/6aa4e4c9" + ] + }, + { + "t": 214.9, + "daa": 230, + "epoch": 3, + "class": 4, + "nodes": [ + "230/54e2edcf", + "230/54e2edcf", + "230/54e2edcf" + ] + }, + { + "t": 230, + "daa": 245, + "epoch": 4, + "class": 4, + "nodes": [ + "245/f5fc54f8", + "245/f5fc54f8", + "245/f5fc54f8" + ] + }, + { + "t": 245, + "daa": 265, + "epoch": 4, + "class": 4, + "nodes": [ + "265/7fe10368", + "265/7fe10368", + "265/7fe10368" + ] + }, + { + "t": 260, + "daa": 278, + "epoch": 4, + "class": 4, + "nodes": [ + "278/d50d4e8e", + "278/d50d4e8e", + "278/d50d4e8e" + ] + }, + { + "t": 275, + "daa": 287, + "epoch": 4, + "class": 4, + "nodes": [ + "287/83dc90ba", + "287/83dc90ba", + "287/83dc90ba" + ] + } + ], + "metal": null +} \ No newline at end of file diff --git a/docs/plans/counter-asic-3-gate/class-v4-20261006-metal.json b/docs/plans/counter-asic-3-gate/class-v4-20261006-metal.json new file mode 100644 index 00000000..7848160e --- /dev/null +++ b/docs/plans/counter-asic-3-gate/class-v4-20261006-metal.json @@ -0,0 +1,447 @@ +{ + "pass": true, + "checks": { + "v4_switch_line_on_every_node": true, + "v4_switch_line_names_the_rounded_epoch": true, + "v3_switch_line_on_every_node": true, + "template_switched_at_the_first_v4_epoch": true, + "template_switched_at_the_first_v3_epoch": true, + "blocks_before_the_boundary": true, + "blocks_after_the_boundary": true, + "v3_and_v4_programs_seen": true, + "v2_programs_seen_when_v3_starts_later": true, + "program_ids_differ_across_the_switch": true, + "miners_agree_on_every_program": true, + "zero_rejected_by_miners": true, + "zero_rejected_by_nodes": true, + "sinks_agree": true, + "block_counts_agree": true, + "metal_prepare_sent_for_v4": true, + "metal_worker_prepared_v4_pack": true, + "metal_no_need_or_mismatch": true, + "metal_accepted_blocks_after_switch": true, + "metal_cpu_recheck_clean": true, + "metal_swapped_without_pause": true + }, + "activation": 150, + "v3_activation": 60, + "epoch_blocks": 60, + "first_v3_epoch": 1, + "first_v4_epoch": 3, + "boundary_daa": 180, + "secs": 480, + "threads": 1, + "node": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/vendor/igneum-node-ca3v4/target-ca3v4/release/igneumd", + "miner": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/vendor/igneum-node-ca3v4/target-ca3v4/release/igneum-miner", + "metal_worker": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/proto-metal/igneum-bench", + "genesis_bits": "0x1e010000", + "template_switch_v3": { + "epoch": 1, + "daa": 60, + "at": 83.1 + }, + "template_switch": { + "epoch": 3, + "daa": 180, + "at": 191.7 + }, + "run_ended_at_s": 317.5, + "final_daa": 300, + "blocks": { + "total": 304, + "before_boundary": 182, + "after_boundary": 122, + "chain_before": 93, + "chain_after": 63 + }, + "programs": [ + { + "epoch": 0, + "class": "v2", + "program_id": "3e974094b5ce543f", + "seed": "130e3b7d568db07a", + "miners": 2, + "disagree": false, + "ready_ms": [ + 179, + 180 + ] + }, + { + "epoch": 1, + "class": "v3", + "program_id": "8ef0132ff054616a", + "seed": "b515430faf46fda8", + "miners": 2, + "disagree": false, + "ready_ms": [ + 291, + 290 + ] + }, + { + "epoch": 2, + "class": "v3", + "program_id": "d3da91ec49682ce1", + "seed": "9cbe1982e1eaf9df", + "miners": 2, + "disagree": false, + "ready_ms": [ + 7, + 7 + ] + }, + { + "epoch": 3, + "class": "v4", + "program_id": "2bcecca6f867c3b5", + "seed": "ca4245b598fcbeb4", + "miners": 2, + "disagree": false, + "ready_ms": [ + 7, + 7 + ] + }, + { + "epoch": 4, + "class": "v4", + "program_id": "21b78e63fc0e9251", + "seed": "3b6cfced206edf85", + "miners": 2, + "disagree": false, + "ready_ms": [ + 8, + 7 + ] + }, + { + "epoch": 5, + "class": "v4", + "program_id": "3855570a5ba12fc9", + "seed": "343510d7149dcf50", + "miners": 2, + "disagree": false, + "ready_ms": [ + 7, + 7 + ] + } + ], + "accepted_per_miner": [ + 301, + 1, + 1 + ], + "rejected_by_miners": [ + 0, + 0, + 0 + ], + "rejected_by_nodes": [ + 0, + 0, + 0 + ], + "rejected_lines": [], + "sinks": [ + "4e1aa3fbf1f4a54b", + "4e1aa3fbf1f4a54b", + "4e1aa3fbf1f4a54b" + ], + "block_counts": [ + 303, + 303, + 303 + ], + "tips_per_node": [ + 2, + 2, + 2 + ], + "switch_lines": [ + "Program class v4 from the override file: active from epoch 3 (DAA score 150 rounded up to the epoch boundary at 180, epochs of 60 DAA)", + "Program class v4 from the override file: active from epoch 3 (DAA score 150 rounded up to the epoch boundary at 180, epochs of 60 DAA)", + "Program class v4 from the override file: active from epoch 3 (DAA score 150 rounded up to the epoch boundary at 180, epochs of 60 DAA)" + ], + "v3_switch_lines": [ + "Program class v3 from the override file: active from epoch 1 (DAA score 60 rounded up to the epoch boundary at 60, epochs of 60 DAA)", + "Program class v3 from the override file: active from epoch 1 (DAA score 60 rounded up to the epoch boundary at 60, epochs of 60 DAA)", + "Program class v3 from the override file: active from epoch 1 (DAA score 60 rounded up to the epoch boundary at 60, epochs of 60 DAA)" + ], + "samples": [ + { + "t": 4.6, + "daa": 0, + "epoch": 0, + "class": 2, + "nodes": [ + "0/130e3b7d", + "0/130e3b7d", + "0/130e3b7d" + ] + }, + { + "t": 19.7, + "daa": 2, + "epoch": 0, + "class": 2, + "nodes": [ + "2/a26633e3", + "2/a26633e3", + "2/a26633e3" + ] + }, + { + "t": 34.7, + "daa": 4, + "epoch": 0, + "class": 2, + "nodes": [ + "4/2e80b7f2", + "4/2e80b7f2", + "4/2e80b7f2" + ] + }, + { + "t": 49.7, + "daa": 14, + "epoch": 0, + "class": 2, + "nodes": [ + "14/b6297491", + "14/b6297491", + "14/b6297491" + ] + }, + { + "t": 64.9, + "daa": 33, + "epoch": 0, + "class": 2, + "nodes": [ + "33/41f3c99e", + "33/41f3c99e", + "33/41f3c99e" + ] + }, + { + "t": 80.1, + "daa": 54, + "epoch": 0, + "class": 2, + "nodes": [ + "54/e14baafe", + "54/e14baafe", + "54/e14baafe" + ] + }, + { + "t": 95.1, + "daa": 76, + "epoch": 1, + "class": 3, + "nodes": [ + "76/31410d82", + "76/31410d82", + "76/31410d82" + ] + }, + { + "t": 110.2, + "daa": 94, + "epoch": 1, + "class": 3, + "nodes": [ + "94/f17df5db", + "94/f17df5db", + "94/f17df5db" + ] + }, + { + "t": 125.3, + "daa": 111, + "epoch": 1, + "class": 3, + "nodes": [ + "111/9cbe1982", + "111/9cbe1982", + "111/9cbe1982" + ] + }, + { + "t": 140.5, + "daa": 125, + "epoch": 2, + "class": 3, + "nodes": [ + "125/185ebceb", + "125/185ebceb", + "125/185ebceb" + ] + }, + { + "t": 155.8, + "daa": 144, + "epoch": 2, + "class": 3, + "nodes": [ + "144/904395a2", + "144/904395a2", + "144/904395a2" + ] + }, + { + "t": 171.2, + "daa": 158, + "epoch": 2, + "class": 3, + "nodes": [ + "158/e5faf4c5", + "158/e5faf4c5", + "158/e5faf4c5" + ] + }, + { + "t": 186.6, + "daa": 174, + "epoch": 2, + "class": 3, + "nodes": [ + "174/1a963704", + "174/1a963704", + "174/1a963704" + ] + }, + { + "t": 201.8, + "daa": 190, + "epoch": 3, + "class": 4, + "nodes": [ + "190/51e300b8", + "190/51e300b8", + "190/51e300b8" + ] + }, + { + "t": 217, + "daa": 205, + "epoch": 3, + "class": 4, + "nodes": [ + "205/251d965c", + "205/251d965c", + "205/251d965c" + ] + }, + { + "t": 232.1, + "daa": 219, + "epoch": 3, + "class": 4, + "nodes": [ + "219/0ecdb99f", + "219/0ecdb99f", + "219/0ecdb99f" + ] + }, + { + "t": 247.3, + "daa": 234, + "epoch": 3, + "class": 4, + "nodes": [ + "234/9f6a3235", + "233/686123b5", + "233/686123b5" + ] + }, + { + "t": 262.4, + "daa": 247, + "epoch": 4, + "class": 4, + "nodes": [ + "247/da402167", + "247/da402167", + "247/da402167" + ] + }, + { + "t": 277.6, + "daa": 263, + "epoch": 4, + "class": 4, + "nodes": [ + "263/86fe7193", + "263/86fe7193", + "263/86fe7193" + ] + }, + { + "t": 292.8, + "daa": 278, + "epoch": 4, + "class": 4, + "nodes": [ + "278/6f950a2e", + "278/6f950a2e", + "278/6f950a2e" + ] + }, + { + "t": 308.3, + "daa": 293, + "epoch": 4, + "class": 4, + "nodes": [ + "293/c29bced3", + "293/c29bced3", + "293/c29bced3" + ] + } + ], + "metal": { + "worker": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/proto-metal/igneum-bench", + "prepares": [ + "PREPARE sent for epoch seed b515430faf46fda81a323a803d58c5f3ef6a6777d1ca2b83cbd41ca0e45fe51d day 1243960 class v3 (6 DAA blocks before the boundary at 60; CPU side ready in 2280 ms)", + "PREPARE sent for epoch seed 9cbe1982e1eaf9df5c1a9dbbf870ca7016987c4594cd3cd47505f9827d1bed70 day 1243960 class v3 (6 DAA blocks before the boundary at 120; CPU side ready in 2071 ms)", + "PREPARE sent for epoch seed ca4245b598fcbeb49449c5072ae0cc6d3803eeb21f75da1eed5a56e8439bdb91 day 1243960 class v4 (6 DAA blocks before the boundary at 180; CPU side ready in 2599 ms)", + "PREPARE sent for epoch seed 3b6cfced206edf85aa0e643ee81c4c01c9a21a3de16e0c2d7347da27540867fa day 1243960 class v4 (6 DAA blocks before the boundary at 240; CPU side ready in 2325 ms)", + "PREPARE sent for epoch seed 343510d7149dcf50f3d302d9e7ed8cd0526dcadf4904ceafa74506e438c49031 day 1243960 class v4 (5 DAA blocks before the boundary at 300; CPU side ready in 2168 ms)" + ], + "prepared": [ + "worker: prepared b515430faf46fda81a323a803d58c5f3ef6a6777d1ca2b83cbd41ca0e45fe51d 69676e65756d2d6461792f38fb120000000000 396.1 program 62.9 dataset 333.1 race 0.0 variant base class v3 loads/hash 128 cache-fill 0.8 build 174.9 resident 2 programs 2 datasets (prepare answered 0.4 s after it was sent)", + "worker: prepared 9cbe1982e1eaf9df5c1a9dbbf870ca7016987c4594cd3cd47505f9827d1bed70 69676e65756d2d6461792f38fb120000000000 61.7 program 61.7 dataset 0.0 race 0.0 variant base class v3 loads/hash 128 cache-fill 0.8 build 174.9 resident 2 programs 2 datasets (prepare answered 0.1 s after it was sent)", + "worker: prepared ca4245b598fcbeb49449c5072ae0cc6d3803eeb21f75da1eed5a56e8439bdb91 69676e65756d2d6461792f38fb120000000000 430.2 program 193.9 dataset 236.2 race 0.0 variant base class v4 loads/hash 128 cache-fill 1.4 build 70.1 resident 2 programs 3 datasets (prepare answered 0.4 s after it was sent)", + "worker: prepared 3b6cfced206edf85aa0e643ee81c4c01c9a21a3de16e0c2d7347da27540867fa 69676e65756d2d6461792f38fb120000000000 138.6 program 138.5 dataset 0.0 race 0.0 variant base class v4 loads/hash 128 cache-fill 1.4 build 70.1 resident 2 programs 3 datasets (prepare answered 0.1 s after it was sent)", + "worker: prepared 343510d7149dcf50f3d302d9e7ed8cd0526dcadf4904ceafa74506e438c49031 69676e65756d2d6461792f38fb120000000000 175.3 program 175.3 dataset 0.0 race 0.0 variant base class v4 loads/hash 128 cache-fill 1.4 build 70.1 resident 2 programs 3 datasets (prepare answered 0.2 s after it was sent)" + ], + "prepared_v4": [ + "worker: prepared ca4245b598fcbeb49449c5072ae0cc6d3803eeb21f75da1eed5a56e8439bdb91 69676e65756d2d6461792f38fb120000000000 430.2 program 193.9 dataset 236.2 race 0.0 variant base class v4 loads/hash 128 cache-fill 1.4 build 70.1 resident 2 programs 3 datasets (prepare answered 0.4 s after it was sent)", + "worker: prepared 3b6cfced206edf85aa0e643ee81c4c01c9a21a3de16e0c2d7347da27540867fa 69676e65756d2d6461792f38fb120000000000 138.6 program 138.5 dataset 0.0 race 0.0 variant base class v4 loads/hash 128 cache-fill 1.4 build 70.1 resident 2 programs 3 datasets (prepare answered 0.1 s after it was sent)", + "worker: prepared 343510d7149dcf50f3d302d9e7ed8cd0526dcadf4904ceafa74506e438c49031 69676e65756d2d6461792f38fb120000000000 175.3 program 175.3 dataset 0.0 race 0.0 variant base class v4 loads/hash 128 cache-fill 1.4 build 70.1 resident 2 programs 3 datasets (prepare answered 0.2 s after it was sent)" + ], + "prepared_v3": [ + "worker: prepared b515430faf46fda81a323a803d58c5f3ef6a6777d1ca2b83cbd41ca0e45fe51d 69676e65756d2d6461792f38fb120000000000 396.1 program 62.9 dataset 333.1 race 0.0 variant base class v3 loads/hash 128 cache-fill 0.8 build 174.9 resident 2 programs 2 datasets (prepare answered 0.4 s after it was sent)", + "worker: prepared 9cbe1982e1eaf9df5c1a9dbbf870ca7016987c4594cd3cd47505f9827d1bed70 69676e65756d2d6461792f38fb120000000000 61.7 program 61.7 dataset 0.0 race 0.0 variant base class v3 loads/hash 128 cache-fill 0.8 build 174.9 resident 2 programs 2 datasets (prepare answered 0.1 s after it was sent)" + ], + "need": 0, + "mismatch_lines": [], + "rebuild_lines_before_v4": [], + "refused": [], + "swaps": [ + "SEED CHANGE at daa 60: epoch seed 130e3b7d568db07aad638f67b2ac26e7cf93c51e4f564d45c6724fc4b2d3da65 -> b515430faf46fda81a323a803d58c5f3ef6a6777d1ca2b83cbd41ca0e45fe51d, day 1243960 -> 1243960 (epoch 1): swapped with no pause (prepared 2 s ago, prepare took 396 ms)", + "SEED CHANGE at daa 120: epoch seed b515430faf46fda81a323a803d58c5f3ef6a6777d1ca2b83cbd41ca0e45fe51d -> 9cbe1982e1eaf9df5c1a9dbbf870ca7016987c4594cd3cd47505f9827d1bed70, day 1243960 -> 1243960 (epoch 2): swapped with no pause (prepared 4 s ago, prepare took 62 ms)", + "SEED CHANGE at daa 180: epoch seed 9cbe1982e1eaf9df5c1a9dbbf870ca7016987c4594cd3cd47505f9827d1bed70 -> ca4245b598fcbeb49449c5072ae0cc6d3803eeb21f75da1eed5a56e8439bdb91, day 1243960 -> 1243960 (epoch 3): swapped with no pause (prepared 3 s ago, prepare took 430 ms)", + "SEED CHANGE at daa 240: epoch seed ca4245b598fcbeb49449c5072ae0cc6d3803eeb21f75da1eed5a56e8439bdb91 -> 3b6cfced206edf85aa0e643ee81c4c01c9a21a3de16e0c2d7347da27540867fa, day 1243960 -> 1243960 (epoch 4): swapped with no pause (prepared 4 s ago, prepare took 139 ms)", + "SEED CHANGE at daa 300: epoch seed 3b6cfced206edf85aa0e643ee81c4c01c9a21a3de16e0c2d7347da27540867fa -> 343510d7149dcf50f3d302d9e7ed8cd0526dcadf4904ceafa74506e438c49031, day 1243960 -> 1243960 (epoch 5): swapped with no pause (prepared 5 s ago, prepare took 175 ms)" + ], + "accepted_total": 301, + "accepted_after_switch": 123, + "found_lines": 0, + "cpu_recheck_mismatched": 0, + "last_status": "" + } +} \ No newline at end of file diff --git a/docs/plans/counter-asic-3-gate/class-v4-20261006-never-failed-case.json b/docs/plans/counter-asic-3-gate/class-v4-20261006-never-failed-case.json new file mode 100644 index 00000000..b3eb74b2 --- /dev/null +++ b/docs/plans/counter-asic-3-gate/class-v4-20261006-never-failed-case.json @@ -0,0 +1,245 @@ +{ + "pass": false, + "checks": { + "v4_switch_line_on_every_node": true, + "v4_switch_line_names_the_rounded_epoch": false, + "v3_switch_line_on_every_node": true, + "template_switched_at_the_first_v4_epoch": false, + "template_switched_at_the_first_v3_epoch": true, + "blocks_before_the_boundary": true, + "blocks_after_the_boundary": false, + "v3_and_v4_programs_seen": false, + "v2_programs_seen_when_v3_starts_later": true, + "program_ids_differ_across_the_switch": false, + "miners_agree_on_every_program": true, + "zero_rejected_by_miners": true, + "zero_rejected_by_nodes": true, + "sinks_agree": true, + "block_counts_agree": true + }, + "activation": "never", + "v3_activation": 60, + "epoch_blocks": 60, + "first_v3_epoch": 1, + "first_v4_epoch": null, + "boundary_daa": "never", + "secs": 150, + "threads": 1, + "node": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/vendor/igneum-node-ca3v4/target-ca3v4/release/igneumd", + "miner": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/vendor/igneum-node-ca3v4/target-ca3v4/release/igneum-miner", + "metal_worker": null, + "genesis_bits": "0x1f010000", + "template_switch_v3": { + "epoch": 1, + "daa": 60, + "at": 31.7 + }, + "template_switch": null, + "run_ended_at_s": null, + "final_daa": 164, + "blocks": { + "total": 167, + "before_boundary": 167, + "after_boundary": 0, + "chain_before": 165, + "chain_after": 0 + }, + "programs": [ + { + "epoch": 0, + "class": "v2", + "program_id": "8f8806638d59850f", + "seed": "234e082d653dc69d", + "miners": 3, + "disagree": false, + "ready_ms": [ + 209, + 203, + 208, + 206, + 209, + 203 + ] + }, + { + "epoch": 1, + "class": "v3", + "program_id": "89059149b7328e64", + "seed": "80127533e53a2df7", + "miners": 3, + "disagree": false, + "ready_ms": [ + 193, + 192, + 192 + ] + }, + { + "epoch": 2, + "class": "v3", + "program_id": "6da8a5eb4dff139a", + "seed": "0e93562ee5369f59", + "miners": 3, + "disagree": false, + "ready_ms": [ + 3, + 4, + 4 + ] + } + ], + "accepted_per_miner": [ + 46, + 59, + 61 + ], + "rejected_by_miners": [ + 0, + 0, + 0 + ], + "rejected_by_nodes": [ + 0, + 0, + 0 + ], + "rejected_lines": [], + "sinks": [ + "20e729b7e4ef8637", + "20e729b7e4ef8637", + "20e729b7e4ef8637" + ], + "block_counts": [ + 166, + 166, + 166 + ], + "tips_per_node": [ + 1, + 1, + 1 + ], + "switch_lines": [ + "Program class v4 from the override file: never", + "Program class v4 from the override file: never", + "Program class v4 from the override file: never" + ], + "v3_switch_lines": [ + "Program class v3 from the override file: active from epoch 1 (DAA score 60 rounded up to the epoch boundary at 60, epochs of 60 DAA)", + "Program class v3 from the override file: active from epoch 1 (DAA score 60 rounded up to the epoch boundary at 60, epochs of 60 DAA)", + "Program class v3 from the override file: active from epoch 1 (DAA score 60 rounded up to the epoch boundary at 60, epochs of 60 DAA)" + ], + "samples": [ + { + "t": 4.6, + "daa": 0, + "epoch": 0, + "class": 2, + "nodes": [ + "0/234e082d", + "0/234e082d", + "0/234e082d" + ] + }, + { + "t": 19.7, + "daa": 39, + "epoch": 0, + "class": 2, + "nodes": [ + "39/14b03ba6", + "39/14b03ba6", + "39/14b03ba6" + ] + }, + { + "t": 34.7, + "daa": 62, + "epoch": 1, + "class": 3, + "nodes": [ + "62/b22630fc", + "62/b22630fc", + "62/b22630fc" + ] + }, + { + "t": 49.7, + "daa": 68, + "epoch": 1, + "class": 3, + "nodes": [ + "68/c8bb2f04", + "68/c8bb2f04", + "68/c8bb2f04" + ] + }, + { + "t": 64.7, + "daa": 83, + "epoch": 1, + "class": 3, + "nodes": [ + "83/d2610654", + "83/d2610654", + "83/d2610654" + ] + }, + { + "t": 79.8, + "daa": 101, + "epoch": 1, + "class": 3, + "nodes": [ + "101/72beef65", + "101/72beef65", + "101/72beef65" + ] + }, + { + "t": 94.8, + "daa": 111, + "epoch": 1, + "class": 3, + "nodes": [ + "111/846008b8", + "111/846008b8", + "111/846008b8" + ] + }, + { + "t": 109.8, + "daa": 121, + "epoch": 2, + "class": 3, + "nodes": [ + "121/d4da8120", + "121/d4da8120", + "121/d4da8120" + ] + }, + { + "t": 124.8, + "daa": 134, + "epoch": 2, + "class": 3, + "nodes": [ + "134/c7e80324", + "134/c7e80324", + "134/c7e80324" + ] + }, + { + "t": 139.8, + "daa": 152, + "epoch": 2, + "class": 3, + "nodes": [ + "152/2a916f0d", + "152/2a916f0d", + "152/2a916f0d" + ] + } + ], + "metal": null +} \ No newline at end of file diff --git a/docs/plans/counter-asic-3-gate/node-gates.md b/docs/plans/counter-asic-3-gate/node-gates.md new file mode 100644 index 00000000..ce322a9b --- /dev/null +++ b/docs/plans/counter-asic-3-gate/node-gates.md @@ -0,0 +1,12 @@ +# Counter ASIC 3.0: the node-side gates for class v4 (`mx8+sh256x27`) + +6 October 2026, worker "ca3-v4-node". The rows in the shape of `docs/plans/counter-asic-2-rollout.md` section 7. Trees: main `ca3-v4-node` a522d04 (igneum-pow, the workers, the harness, on ca3-coord 3213ee9), fork `ca3-v4-node` 5f7e0543 (on release-0.3.13-node bb43e9a8, the current release tip, which contains release-0.3.12-node). Builds: the Mac built igneumd and igneum-miner (`target-ca3v4/release`, `--features kaspad/igneum-pow`, warm from an APFS clone of the 0.3.13 target dir, under `with-lock.sh build`, nice 19, 4 jobs) and `igneum-bench` (the Metal worker, `swiftc -O`); the suites went to PC 2. The Mac's installed app was not touched (its miner stays paused); nothing was published; the live devnet was not touched. The Mac's load average during the runs: 5 to 12 (other agents' builds); the gates here count blocks, lines and ids, not rates, so the load does not move them. + +| # | Gate | Evidence required | State | +|---|---|---|---| +| G4 | the fast-time 3-node network mining across a v4 activation | 0 rejected blocks, 0 forks, every node's first lines show the switch, blocks on both sides of the boundary; the gate fires on one finished and one failed case | GREEN. Run 1 (15:54:32 to 15:59:21Z, `node infra/fast-time/class-v4.mjs --secs 420 --activation 150 --v3-activation 60 --epochs-after 2` under `with-lock.sh run`; three nodes, one real CPU miner each with `--engine igneum-pow`, every node verifying the other two): 3 of 3 nodes print both switch lines (`Program class v4 from the override file: active from epoch 3 (DAA score 150 rounded up to the epoch boundary at 180, epochs of 60 DAA)` and the v3 line for epoch 1 at DAA 60); templates class 2 for epoch 0, class 3 for epochs 1 and 2, class 4 for epochs 3 to 5; the switch seen at DAA 180, 163.9 s wall; 181 blocks before and 128 after DAA 180 (309 in all, selected chain 177 / 127); program ids agree on all three miners (e0 v2 `8f8806638d59850f`, the same v2 id as 2.0's three runs; e1 v3 `29ced0d031cc121b`, e2 v3 `341fd4076256cd7b`, e3 v4 `070ca0f94e7f8f88`, e4 v4 `b8cbe1ef7ab1d748`, e5 v4 `9321cbd9c54c0b83`; no v2 or v3 id reappears under v4); rejected 0/0/0 on the miners and 0/0/0 on the nodes (103, 103, 102 accepted); one sink `4da9ac669effdfab` on all three at 308/308/308 blocks, one tip each; digest `59a3eb5383bfbf23...` on all three; program and cache ready on one core: v2 epoch 0 199 ms, the first v3 epoch 187 ms (its own day cache), the first v4 epoch 2 ms (the v3 day cache reused across the N5 boundary, no rebuild). Summary `class-v4-20261006-1553Z-cpu.json`. Run 2, the known-failed case (15:59:46 to 16:02:17Z, `--activation never --v3-activation 60 --secs 150`): every node prints `Program class v4 from the override file: never`; the harness reports `SUMMARY FAIL: NO v4 epoch seen` with five checks down (`v4_switch_line_names_the_rounded_epoch`, `template_switched_at_the_first_v4_epoch`, `blocks_after_the_boundary`, `v3_and_v4_programs_seen`, `program_ids_differ_across_the_switch`), exit 1; the v3 side of that run was clean (epochs 0 v2, 1 and 2 v3; 0 rejected; one sink 166/166/166). Summary `class-v4-20261006-never-failed-case.json` | +| G4b | the Mac mines v4: a real Metal miner across a v4 boundary through the miner's `--prepare-packs` flow, and the app passes that flag to the Metal worker | the prepare lines on both sides, the worker's `prepared` line for the v4 pack, found or accepted blocks on v4, no `need` or mismatch line, no exit 42 or 44; the app's `miner_args` pushes `--prepare-packs` for every worker | GREEN. Run 3 (16:02:37 to 16:07:58Z, `--metal proto-metal/igneum-bench --genesis-bits 0x1e010000 --secs 480 --activation 150 --v3-activation 60`; node 0's miner is `igneum-miner --worker igneum-bench --prepare-packs --exit-on-seed-change`, the app's own shape; `igneum-bench` built from this tree at a522d04's `main.swift`, sha256 30c70754...; CPU miners on nodes 1 and 2): 5 PREPARE lines, each 5 to 6 DAA before its boundary, two `class v3` (DAA 60, 120) and three `class v4` with the pack directory and `class=v4 era=` (DAA 180, 240, 300); the worker's `prepared` lines for the three v4 packs: `prepared ca4245b5... 430.2 program 193.9 dataset 236.2 race 0.0 variant base class v4 loads/hash 128 cache-fill 1.4 build 70.1` (the first, the v4 day built from the pack's memhard.metal on the GPU: 1.4 ms cache fill, 70.1 ms build), then 138.6 and 175.3 ms with the day resident (program compile alone, the 256-instruction shadow block in the kernel text; a v3 prepare was 61.7 ms); every swap `swapped with no pause` at DAA 60, 120, 180, 240, 300 (prepared 2 to 5 s earlier); 301 blocks accepted by the Metal miner, 123 after the switch, every one re-checked on the CPU: `mismatched=0`; `need` 0; no class or era mismatch line, no `PACK OUT OF DATE`, no prepare-failed, no exit 42 or 44 (0 such lines in the miner's log); the chain 182 / 122 blocks across DAA 180 (304), rejected 0/0/0 and 0/0/0, one sink `4e1aa3fbf1f4a54b` on all three at 303/303/303, 3 of 3 v4 switch lines; program ids e3 v4 `2bcecca6f867c3b5`, e4 `21b78e63fc0e9251`, e5 `3855570a5ba12fc9` agreed by the Metal miner and the CPU miners; the Metal miner's STATUS at 309 s: 18.58 MH/s wall, 293 accepted, 0 rejected (a rate under a load average of 5 to 12 with the CPU miners and other agents' builds beside it: an order of magnitude, not a measurement). The app side: `app/igneum-app/src/engine.rs` `miner_args` pushes `--prepare-packs` for every worker (line 1478, the 0.3.11 fix; unit test `every_worker_gets_the_prepare_directory_in_the_platform_form`), confirmed unchanged on ca3-coord 3213ee9. Summary `class-v4-20261006-metal.json` | +| G5 | the PC-built Windows workers and the Mac workers from the same commit | every sha256 listed; the two Windows workers carry the resource block; no packaging, no DMG, no manifest | GREEN, with one gap named: the build job builds the node and the app only (`push-build-inputs.sh` writes two build units, `jobbuild.rs` has no worker unit), so the Windows workers were cross-built on the Mac from the committed tree a522d04 with `proto-cuda/nvrtc/build-windows.sh` under the build lock (mingw-w64, static, windres resource block), as 0.3.11's G5 row was: igneum-worker-cuda.exe e563126ed1d8ab8f6803ead89acca5b6fcb03203e6b87b70e0aec85e8e1046e4 (1,536,512 bytes), igneum-worker-opencl.exe 7fce1249d443aaf73a29b7474c1ef4084f3529b5bec3a2dc6bb97e624561a5e8 (478,208), `verify-exe.py`: both carry the coin icon and the version block (a first build of the same sources before the commit gave 26400edf... and 438e608a...: mingw stamps a link time, so the pair is not bit-reproducible; the sizes match the 0.3.11 pair, 1,536,512 and 478,208, and the sha256s differ from 0.3.11's 2b3b8c92... and edc4a75d...: the generator-4 rule is in them). The Mac worker from the same tree: igneum-bench 30c70754097dafbc3144a0c0f192526a0638089e5dde49ce46abb7a948dceade (665,128 bytes, built 16:47Z from `main.swift` as committed in a522d04, the binary G4b mined with). The same build job's (build-20261006-155958) node and app outputs from fork 5f7e0543, every sha256 verified against the PC's lines on fetch: igneumd.exe 140be25e486fe1634500b6e66eedfaca9fa6561d652f3e219b5a7a2bc377e766 (51,752,448), igneum-miner.exe ceb1e691ad57f58d09d5a0ff4cb5658aea0604d23ceb90b6a847a2c36a0bf81c (11,118,080), igneum-app.exe bfa3b871f17b69fd0c0c01a6f9fcac8964efb54dc10fd25db13422ba8f96d51f (3,065,344, PE and resource block ok); Linux igneumd d44b735869b129401d68f18786b4a5f16c22d00b682bd41a8e5022a01f7d7527 (49,603,112), igneum-miner a7985111d4972f4b67a9409b831a8c7af746508ebb3b19fc927aaddcfa2e2413, igneum-app 222f30c00bb9725f3127452f3d52403b380e25abaf9fba5c925839574cf1dabc; kept under the job's --out dir with --no-place. No packaging, no DMG, no manifest | +| G6 | the node change on a fork branch from the current release tip with suites green on PC 2, with the igneum-pow feature | the build job id and its SUMMARY line; the v4 engine test named in the output | GREEN. PC 2 job `build-20261006-155958` (fork ca3-v4-node 5f7e0543, main a522d04; published 15:59:58Z after the clear file and under the mkdir lock, taken 15:58:50Z, released 16:08:06Z after the closing report; the installed app untouched, the prover left on), `node tools/build-job.mjs run --target 1ccfe586 --node vendor/igneum-node-ca3v4 --node-tests "kaspa-consensus kaspa-consensus-core igneum-exec kaspa-pow igneum-miner kaspa-p2p-flows" --app-tests igneum-app --no-place`: `done exit 0 after 392 s: node ca3-v4-node 5f7e0543 app 0.3.11: every stage ok; 9 files uploaded (46 MB); 6 min of 40`; the test stage 50 s, exit 0 on both units: kaspa-consensus 98 passed (3 ignored; `ban_is_decided_by_the_carrying_block_so_nodes_agree_on_every_voter_list ... ok`, so 2.0's flake did not recur and no re-run was needed), kaspa-consensus-core 109 + 7 (`override_params_carry_the_program_class_v4_activation ... ok`), igneum-exec ?, kaspa-pow 15 (`program_class_v3_seeds_hash_their_own_program_over_their_own_cache ... ok` and `program_class_v4_seeds_hash_the_shadow_program_over_the_v3_day_cache ... ok`, both `#[cfg(feature = "igneum-pow")]`), igneum-miner ?, kaspa-p2p-flows 33, igneum-app 112 + 26 + 8. The feature: `build-job.mjs` and `push-build-inputs.sh` carry no `--features` for the test units (the manifest's `tests` entries are `dir` and `packages` only; the build units have `kaspad/igneum-pow`), and the PC's `jobbuild.rs` runs `cargo test --release -p ... ` in ONE invocation per unit, where `igneum-miner` depends on `kaspa-pow` with `features = ["igneum-pow"]`, so cargo's feature unification turns the feature on for kaspa-pow's test target in that invocation: the two engine tests above ran on PC 2 with the feature, named in the report, which is the evidence main asked for. The same two suites also pass on the Mac with the flag explicit (`cargo test --release -p kaspa-pow --features igneum-pow`: 15; `-p kaspa-consensus-core`: 109 + 7). What the tool lacks: a `features` key on a test unit; not added here (a change to the PC's installed app would be needed to honour it) | + +Summary files in this directory: `class-v4-20261006-1553Z-cpu.json` (G4 run 1), `class-v4-20261006-never-failed-case.json` (G4 run 2, the failed case), `class-v4-20261006-metal.json` (G4b). diff --git a/docs/plans/counter-asic-3-node.md b/docs/plans/counter-asic-3-node.md new file mode 100644 index 00000000..3e4e0849 --- /dev/null +++ b/docs/plans/counter-asic-3-node.md @@ -0,0 +1,77 @@ +# Counter ASIC 3.0: the node side (program class v4 as a second height switch) + +6 October 2026, worker "ca3-v4-node". Branches: `ca3-v4-node` (main repository: igneum-pow, the workers, the fast-time gate) and `ca3-v4-node` (the fork, from the 0.3.13 tip bb43e9a8). The 2.0 twin of `docs/plans/counter-asic-2-node.md`: every piece of its section 1 has its v4 twin below; v2 and v3 are byte for byte what they were (the pinned packs, `igneum-pow/tests/packs.rs`). The class: `mx8+sh256x27` (`docs/analysis/latency-shadow-2026-10-06.md`), gated here as THE class v4 candidate on the project lead's word of 16:50 UTC. Nothing is published; the Mac's app is untouched (its miner stays paused); the live devnet is untouched. Gate rows: `docs/plans/counter-asic-3-gate/node-gates.md`. + +## 1. What changed, where + +| Piece | What | +|---|---| +| igneum-pow `generator.rs` | `ProgramClass::V4`, `V4_CLASS = LoadClass { shadow: Some(ShadowClass { instrs: 256, reps: 27 }), ..V3_CLASS }` (name `mx8+sh256x27`), `GENERATOR_VERSION_V4 = 4`, `generate_era_generator` (the era draw of class v3 over `V4_CLASS`, generator 4 stamped) used by `generate_from_seed_bytes_program_class` for `(V4, Some(era))`; `ProgramClass::{load_class, generator_version, from_generator, name, parse}` take v4; `ProgramClass::has_era` (v3 and v4); a v4 program's id is `program_id(4, seed, attempt)` (`Program::program_id`), its class `Program::program_class` = V4 from generator 4. Tests: `program_classes` (v4 round trip), new `program_class_v4_is_class_v3_with_the_shadow_block` (the base instructions, attempt and era draw equal class v3's of the same seed and era; shadow 256 x 27, 55,296 per hash; the id differs; the hash differs; v2's id `bcc1248b10cc90f2` unchanged) | +| igneum-pow `lib.rs` | exports `GENERATOR_VERSION_V4`, `V4_CLASS` | +| igneum-pow `emit.rs` | program.h: `IGNEUM_GENERATOR 4`, `IGNEUM_PROGRAM_CLASS "v4"`, `IGNEUM_ERA_SEED_HEX` (the v3 comment lines are byte for byte the 0.3.11 ones, so the pinned v3 packs do not change); the shadow lines (`IGNEUM_SHADOW_INSTRS`, `IGNEUM_SHADOW_REPS`, the `for (sh ...)` block in every kernel body) come from the class as before; program.json `"program_class": "v4"` | +| igneum-pow `packcheck.rs` | `verify_pack_texts_chain` accepts generator 2, 3 or 4 (`PackFault::WrongClass` otherwise, "2, 3 or 4" in the message); the era rule applies to every class with `has_era` (v3 and v4); test `program_class_and_era_are_checked` extended (a v4 pack with its era and shadow lines passes when v4 is wanted, is refused when v2 or v3 is wanted; a v3 or v2 pack is refused when v4 is wanted; the v4 era mismatch and the missing-era cases; the v3 header unchanged) | +| igneum-pow `main.rs` | `--program-class v2|v3|v4` | +| `proto-cuda/nvrtc/packfile.h` | `pf_load` accepts generator 2, 3 or 4 and names the class from it (`"v4"`); `pf_pack_class_ok` applies the era rule to every class that is not v2; `pf_class_token` unchanged (reads `class=v4`). `emu/packfile-test.c`: 7 new checks (a generator 4 pack loads as v4 with its era; v3 refuses it; it refuses another era; v4 refuses a v3 pack; the `class=v4` token; a v3 class line on a generator 4 pack is refused); `emu/packfile-test.sh` 0 failures on the Mac | +| `proto-cuda/nvrtc/worker.cpp`, `proto-opencl/host.c` | no text change: both read the class and era through `packfile.h` (`pairIsClass`, `takeClassTokens`), so a pair's identity carries `v4` and its era, a v4 job against a resident v3 pair of the same seeds answers `need` and `program class mismatch`, and the prepared v4 pair wins; `host.c` compiles clean (`cc -fsyntax-only`) against the new header | +| `proto-metal/main.swift` | `isPackClass` (v3 or v4) and `packClassOf(generator)`: `servePackProgram` and `servePackDataset` accept generator 2, 3 or 4 and refuse the rest in plain words; a `class=v4` line is compiled only from the prepared pack (its `program_bound.metal` carries the shadow block) and its day built from the pack's `memhard.metal`, keyed by (day, class, era); an inline `class=v4` job with no prepared pair answers `need` and the class-mismatch error (2.0's G4b path); a class that is not v2, v3 or v4 is refused | +| fork `consensus/core/src/igneum.rs` | `ProgramClass::V4` (generator 4, `"v4"`), `has_era`, `dataset_class` (V4 -> V3: the v4 day cache is the v3 day cache), `program_class_for_epoch_at(epoch, N4, N5, L)` with two switches (v4 from the first epoch at or above N5, else v3 from N4, else v2; N5 at or below N4 is v4 from N5), `program_class_first_epoch_at`, `program_class_v4_first_epoch_at`, the process-wide `install_program_class_v4_activation` / `program_class_v4_activation_daa` / `program_class_v4_first_epoch`; `PowEpochInfo.program_class_v4_activation_daa` (serde default never); the switch test sweeps both heights | +| fork `consensus/core/src/config/params.rs` | `program_class_v4_activation_daa` in `Params` and `OverrideParams` (never on devnet, simnet, mainnet; 0 on the testnet), `override_params`, `From`, the digest (unconditionally, right after `program_class_v3_activation_daa`), `install_program_class_v4_activation`, `program_class_v4_first_epoch`; tests: `override_params_carry_the_program_class_v4_activation`, the digest test's 15th edit, the pinned devnet digest re-pinned (section 2), `fast_time_60x_file_is_the_devnet_at_60x` (every field) | +| fork `kaspad/src/daemon.rs` | `Program class v4 from the override file: active from epoch E (DAA score N5 rounded up to the epoch boundary at L*E, epochs of L DAA)` beside the v3 line; `install_program_class_v4_activation` next to the v3 install | +| fork `consensus/pow/src/igneum.rs` | `EpochSeeds::day_key` keys the day cache on `(day, class.dataset_class())`, so a v4 epoch after a v3 one builds no new cache; `era_bytes` for every class with an era; `pow_class_of` maps V4; `build_day` builds the dataset class's day. Test `program_class_v4_seeds_hash_the_shadow_program_over_the_v3_day_cache` (one cache for v3 and v4 seeds of one day; the v4 program is generator 4, `V4_CLASS` with the era inside, the base instructions and attempt class v3's; the engine's pow equals the standalone epoch's; another era keeps the id and changes the program) | +| fork `consensus/src/consensus/mod.rs` | `get_pow_epoch_info` reports `program_class_v4_activation_daa`; the class of this and the next epoch come from `program_class_for_epoch` (both switches) | +| fork `consensus/src/pipeline/header_processor/pre_ghostdag_validation.rs`, `processes/pruning_proof/igneum_pow.rs` | unchanged: `program_class_at(daa)` answers v4 under the installed switches; the era seed is read for every class after v2 | +| fork `rpc/core/src/model/message.rs`, `rpc/grpc/core/proto/rpc.proto` (field 19), `rpc/grpc/core/src/convert/message.rs` | `RpcPowEpochInfo.program_class_v4_activation_daa` (serde default never); proto `optional uint64 programClassV4ActivationDaa = 19` so a 0.3.13 node, which sends nothing, reads as never and not as 0 (v4 from genesis); `program_class` and `next_program_class` already carry a generator number, so 4 needs no field | +| fork `igneum/miner/src/main.rs` | the v4 activation installed from the template beside the v3 one (`node program class v4 activation: ...`); `class_tokens`: a v3 or v4 epoch's job and prepare lines end with `class= era=`; `seeds.txt` carries `program_class v4` and `era_seed_hex`; `write_pack_checked` checks class and era through `verify_pack_dir_chain` (generator 4 accepted); the "program and 256 MiB cache ready" line prints `class v4` and the id; `next_pair` takes the next epoch's class from the template (`next_program_class` 4 at the boundary) | +| `app/igneum-app/src/engine.rs` | confirmed, no change: `miner_args` pushes `--prepare-packs` for every worker (line 1478, since 0.3.11; the unit test `every_worker_gets_the_prepare_directory_in_the_platform_form`), so the Mac's Metal worker takes a v4 pack the way it takes a v3 one | +| `infra/fast-time/override-60x.json` | `program_class_v4_activation_daa` at never right after the v3 field. Two faults of the base file fixed on the way: the proving v1 block was in the file TWICE (two merges each appended it; the node refuses a duplicate field, so neither class harness could start a node from the file as committed), and the four 0.3.12 and 0.3.13 fields (`proving_v1_fresh_rule_daa`, `exec_restart_number`, `exec_restart_hash`, `exec_restart_trust_daa`) were missing (the fork's every-field test skips on the PCs, where the file is not beside the node). New CI check `tools/ci/override-json-check.sh` (every override file parses with no duplicate key; self-tested on a duplicate), registered in `.github/workflows/ci.yml` | +| `infra/fast-time/class-v4.mjs`, `infra/fast-time/README.md` | the G4 gate (section 3): class-v3.mjs's shape with two switches (`--v3-activation 60`, `--activation 150|never`), ports 29660, suffix 966, `/tmp/igneum-fast-time-v4`; the known-failed case is `--activation never` | + +## 2. The epoch-boundary rule with two switches + +One epoch has one program, so both switches key on the EPOCH: epoch `e` is v4 when `L * e >= N5`, else v3 when `L * e >= N4`, else v2 (`program_class_for_epoch_at`); each height rounds UP to the next boundary and never splits an epoch. A block's class is a function of its DAA score alone (`program_class_at`). + +| N4 | N5 | L | epochs | Source | +|---|---|---|---|---| +| 60 | 150 | 60 | e0 v2; e1, e2 v3; e3 (DAA 180) v4 | the gate run, section 3; the unit test | +| 60 | 181 | 60 | e3 v3; e4 (DAA 240) v4 | the unit test | +| 154,800 | 180,001 | 3,600 | e50 v3; e51 (183,600) v4 | the unit test (the devnet shape: v3 live from epoch 43, v4 at tip + 14,400 rounded up) | +| 150 | 120 | 60 | e2 v4 (N5 below N4: v4 from N5) | the unit test | +| any | never | any | the 2.0 answers, unchanged | the unit test sweeps N4 0..399 with N5 never | +| any | 0 | any | v4 from genesis (the testnet) | the unit test | + +The digest: `program_class_v4_activation_daa` enters `consensus_digest` unconditionally right after the v3 field, so the digest flips the moment a binary carrying the field runs, as the v3 field did on 0.3.11. Measured on the fork (the pinned test `consensus_digest_keeps_the_0_3_5_value_until_the_fee_switch_is_set`): the devnet digest with no override file moves from `c562d70e1428c9789823cc40067623b4767f7c555ce7ff4ea11c1498f013ef6c` (0.3.11 to 0.3.13) to `3c50502174b031b9afe8aad3758aa55eb32e3ee20ffa6f9760b2e958547c0690`. Consequence: a node on this binary is refused at the handshake by every 0.3.13 node and refuses them, which is the rollout's order step 1 (every node carries the object before any node reaches the height); the cut that carries it is a one-sweep binary rollout like 0.3.11's, and the Mac's app, PC 2 and the seeds all move in that sweep, nothing earlier. + +The day cache: the v4 dataset is the v3 dataset (the same mixer x8 construction, growth rule and era layout; the shadow block touches no load and no item), so the node's engine keys its day caches on `(day, dataset_class)` and a v4 epoch reuses the v3 cache across the N5 boundary: no 1 GiB rebuild on the node at the switch (the kaspa-pow test: `cached_days` 1, `cache_builds` 1 for v3 and v4 seeds of one day). The Metal worker keeps separate (day, class, era) entries, so it builds the v4 day once at the boundary from the pack's own `memhard.metal` (the same bytes; about 41 ms on the GPU by the 2.0 figure). + +## 3. The gates + +See `docs/plans/counter-asic-3-gate/node-gates.md` for the rows in the rollout's section 7 shape and the summary files. The results, in short: + +| Gate | State | The line that decides it | +|---|---|---| +| G4 | GREEN | run 1: 3 of 3 switch lines, 181 / 128 blocks across DAA 180, ids agree on all three miners, rejected 0/0/0, one sink 308/308/308; run 2 (`--activation never`): `SUMMARY FAIL: NO v4 epoch seen`, exit 1 | +| G4b | GREEN | three `prepared ... class v4` lines (430, 139, 175 ms), every swap with no pause, 123 blocks accepted on v4, re-check mismatched 0, need 0, no mismatch, no exit 42 or 44 | +| G5 | GREEN | the two Windows workers e563126e... and 7fce1249... with the resource block, the Mac worker 30c70754..., the job's igneumd.exe 140be25e..., every sha256 listed | +| G6 | GREEN | PC 2 job build-20261006-155958, every stage ok, 392 s; kaspa-pow 15 with the v4 engine test, no flake | + +## 4. Tests + +| Where | What | State | +|---|---|---| +| igneum-pow `cargo test --release` | 60 unit (the v4 class test, the v4 pack checks) + 7 derive + 4 mixer + 19 packs (the pinned v2 and v3 packs byte-identical) + 7 scratch | pass (Mac, 6 Oct 2026, 15:44Z) | +| `proto-cuda/nvrtc/emu/packfile-test.sh` | 13 + 7 checks: the generator rule with 4, the v4 pack with its era, the token matcher | 0 failures (Mac) | +| `proto-opencl/host.c` | `cc -fsyntax-only` against the new packfile.h | clean (Mac) | +| `proto-metal/main.swift` | `swiftc -O` (the G4b worker) | built (Mac) | +| fork `cargo test --release -p kaspa-consensus-core` | the two-switch rounding sweep, the v4 params, the digest's 15th edit, the pinned digest, the fast-time every-field test | pass (Mac, 109 + 7, 2 ignored; PC 2 job build-20261006-155958: 109 + 7) | +| fork `cargo test --release -p kaspa-pow --features igneum-pow` | the v4 engine test beside the v3 one | pass (Mac, 15; PC 2 job: 15, the feature on through unification with igneum-miner) | +| PC 2 `build-job.mjs` suites | `kaspa-consensus kaspa-consensus-core igneum-exec kaspa-pow igneum-miner kaspa-p2p-flows` + `igneum-app` | G6 row | + +## 5. Unverified, and what is owed + +- The AMD vendor (G1 on the RX 9070 XT) and G2 (1,000 random hashes per card re-hashed on the CPU) are the hash side's gates, not this worker's; the OpenCL worker's generator-4 rule was checked by the C test and a syntax check, not by a live worker on a v4 pack (as 2.0's rollout read before its G1). +- The Metal worker's rate in G4b (18.6 MH/s wall) was taken under a load average of 5 to 12 with CPU miners beside it: not a measurement (the measured v4 rate is the shadow analysis's 26.5 to 26.9 MH/s on packbench under the measure lock). +- The era walk for era >= 1 and the pruning-proof era witness: unchanged from 2.0 section 7 (the v4 switch adds nothing there). +- `next_pair` at an epoch boundary that is also an era boundary: the 2.0 note stands for v4 as for v3. +- Wire: `RpcPowEpochInfo` gained one Borsh field (wRPC, versioned by `GetBlockTemplateResponse`) and one optional proto field; a 0.3.13 miner against this node reads the v4 height as never (the field absent) and would key epochs on the v3 switch alone, so every miner moves in the same binary sweep as the node (the rollout order of 2.0). +- The build job has no `features` key for test units (G6 row): the feature was on through unification; adding the key needs the PC's installed app to honour it, a 0.3.14 app item. +- The 48 GiB APFS clone of the 0.3.13 target dir (`vendor/igneum-node-ca3v4/target-ca3v4`, untracked) can be deleted after the cut. From ed53e96fd8677400d1d20ae14e8ca4e7c550a701 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:10:30 +0000 Subject: [PATCH 3/3] Counter ASIC 3.0 gates (node): G6 row, the two crates whose lines the relay upload does not carry Co-Authored-By: Claude Fable 5.1 --- docs/plans/counter-asic-3-gate/node-gates.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plans/counter-asic-3-gate/node-gates.md b/docs/plans/counter-asic-3-gate/node-gates.md index ce322a9b..35082478 100644 --- a/docs/plans/counter-asic-3-gate/node-gates.md +++ b/docs/plans/counter-asic-3-gate/node-gates.md @@ -7,6 +7,6 @@ | G4 | the fast-time 3-node network mining across a v4 activation | 0 rejected blocks, 0 forks, every node's first lines show the switch, blocks on both sides of the boundary; the gate fires on one finished and one failed case | GREEN. Run 1 (15:54:32 to 15:59:21Z, `node infra/fast-time/class-v4.mjs --secs 420 --activation 150 --v3-activation 60 --epochs-after 2` under `with-lock.sh run`; three nodes, one real CPU miner each with `--engine igneum-pow`, every node verifying the other two): 3 of 3 nodes print both switch lines (`Program class v4 from the override file: active from epoch 3 (DAA score 150 rounded up to the epoch boundary at 180, epochs of 60 DAA)` and the v3 line for epoch 1 at DAA 60); templates class 2 for epoch 0, class 3 for epochs 1 and 2, class 4 for epochs 3 to 5; the switch seen at DAA 180, 163.9 s wall; 181 blocks before and 128 after DAA 180 (309 in all, selected chain 177 / 127); program ids agree on all three miners (e0 v2 `8f8806638d59850f`, the same v2 id as 2.0's three runs; e1 v3 `29ced0d031cc121b`, e2 v3 `341fd4076256cd7b`, e3 v4 `070ca0f94e7f8f88`, e4 v4 `b8cbe1ef7ab1d748`, e5 v4 `9321cbd9c54c0b83`; no v2 or v3 id reappears under v4); rejected 0/0/0 on the miners and 0/0/0 on the nodes (103, 103, 102 accepted); one sink `4da9ac669effdfab` on all three at 308/308/308 blocks, one tip each; digest `59a3eb5383bfbf23...` on all three; program and cache ready on one core: v2 epoch 0 199 ms, the first v3 epoch 187 ms (its own day cache), the first v4 epoch 2 ms (the v3 day cache reused across the N5 boundary, no rebuild). Summary `class-v4-20261006-1553Z-cpu.json`. Run 2, the known-failed case (15:59:46 to 16:02:17Z, `--activation never --v3-activation 60 --secs 150`): every node prints `Program class v4 from the override file: never`; the harness reports `SUMMARY FAIL: NO v4 epoch seen` with five checks down (`v4_switch_line_names_the_rounded_epoch`, `template_switched_at_the_first_v4_epoch`, `blocks_after_the_boundary`, `v3_and_v4_programs_seen`, `program_ids_differ_across_the_switch`), exit 1; the v3 side of that run was clean (epochs 0 v2, 1 and 2 v3; 0 rejected; one sink 166/166/166). Summary `class-v4-20261006-never-failed-case.json` | | G4b | the Mac mines v4: a real Metal miner across a v4 boundary through the miner's `--prepare-packs` flow, and the app passes that flag to the Metal worker | the prepare lines on both sides, the worker's `prepared` line for the v4 pack, found or accepted blocks on v4, no `need` or mismatch line, no exit 42 or 44; the app's `miner_args` pushes `--prepare-packs` for every worker | GREEN. Run 3 (16:02:37 to 16:07:58Z, `--metal proto-metal/igneum-bench --genesis-bits 0x1e010000 --secs 480 --activation 150 --v3-activation 60`; node 0's miner is `igneum-miner --worker igneum-bench --prepare-packs --exit-on-seed-change`, the app's own shape; `igneum-bench` built from this tree at a522d04's `main.swift`, sha256 30c70754...; CPU miners on nodes 1 and 2): 5 PREPARE lines, each 5 to 6 DAA before its boundary, two `class v3` (DAA 60, 120) and three `class v4` with the pack directory and `class=v4 era=` (DAA 180, 240, 300); the worker's `prepared` lines for the three v4 packs: `prepared ca4245b5... 430.2 program 193.9 dataset 236.2 race 0.0 variant base class v4 loads/hash 128 cache-fill 1.4 build 70.1` (the first, the v4 day built from the pack's memhard.metal on the GPU: 1.4 ms cache fill, 70.1 ms build), then 138.6 and 175.3 ms with the day resident (program compile alone, the 256-instruction shadow block in the kernel text; a v3 prepare was 61.7 ms); every swap `swapped with no pause` at DAA 60, 120, 180, 240, 300 (prepared 2 to 5 s earlier); 301 blocks accepted by the Metal miner, 123 after the switch, every one re-checked on the CPU: `mismatched=0`; `need` 0; no class or era mismatch line, no `PACK OUT OF DATE`, no prepare-failed, no exit 42 or 44 (0 such lines in the miner's log); the chain 182 / 122 blocks across DAA 180 (304), rejected 0/0/0 and 0/0/0, one sink `4e1aa3fbf1f4a54b` on all three at 303/303/303, 3 of 3 v4 switch lines; program ids e3 v4 `2bcecca6f867c3b5`, e4 `21b78e63fc0e9251`, e5 `3855570a5ba12fc9` agreed by the Metal miner and the CPU miners; the Metal miner's STATUS at 309 s: 18.58 MH/s wall, 293 accepted, 0 rejected (a rate under a load average of 5 to 12 with the CPU miners and other agents' builds beside it: an order of magnitude, not a measurement). The app side: `app/igneum-app/src/engine.rs` `miner_args` pushes `--prepare-packs` for every worker (line 1478, the 0.3.11 fix; unit test `every_worker_gets_the_prepare_directory_in_the_platform_form`), confirmed unchanged on ca3-coord 3213ee9. Summary `class-v4-20261006-metal.json` | | G5 | the PC-built Windows workers and the Mac workers from the same commit | every sha256 listed; the two Windows workers carry the resource block; no packaging, no DMG, no manifest | GREEN, with one gap named: the build job builds the node and the app only (`push-build-inputs.sh` writes two build units, `jobbuild.rs` has no worker unit), so the Windows workers were cross-built on the Mac from the committed tree a522d04 with `proto-cuda/nvrtc/build-windows.sh` under the build lock (mingw-w64, static, windres resource block), as 0.3.11's G5 row was: igneum-worker-cuda.exe e563126ed1d8ab8f6803ead89acca5b6fcb03203e6b87b70e0aec85e8e1046e4 (1,536,512 bytes), igneum-worker-opencl.exe 7fce1249d443aaf73a29b7474c1ef4084f3529b5bec3a2dc6bb97e624561a5e8 (478,208), `verify-exe.py`: both carry the coin icon and the version block (a first build of the same sources before the commit gave 26400edf... and 438e608a...: mingw stamps a link time, so the pair is not bit-reproducible; the sizes match the 0.3.11 pair, 1,536,512 and 478,208, and the sha256s differ from 0.3.11's 2b3b8c92... and edc4a75d...: the generator-4 rule is in them). The Mac worker from the same tree: igneum-bench 30c70754097dafbc3144a0c0f192526a0638089e5dde49ce46abb7a948dceade (665,128 bytes, built 16:47Z from `main.swift` as committed in a522d04, the binary G4b mined with). The same build job's (build-20261006-155958) node and app outputs from fork 5f7e0543, every sha256 verified against the PC's lines on fetch: igneumd.exe 140be25e486fe1634500b6e66eedfaca9fa6561d652f3e219b5a7a2bc377e766 (51,752,448), igneum-miner.exe ceb1e691ad57f58d09d5a0ff4cb5658aea0604d23ceb90b6a847a2c36a0bf81c (11,118,080), igneum-app.exe bfa3b871f17b69fd0c0c01a6f9fcac8964efb54dc10fd25db13422ba8f96d51f (3,065,344, PE and resource block ok); Linux igneumd d44b735869b129401d68f18786b4a5f16c22d00b682bd41a8e5022a01f7d7527 (49,603,112), igneum-miner a7985111d4972f4b67a9409b831a8c7af746508ebb3b19fc927aaddcfa2e2413, igneum-app 222f30c00bb9725f3127452f3d52403b380e25abaf9fba5c925839574cf1dabc; kept under the job's --out dir with --no-place. No packaging, no DMG, no manifest | -| G6 | the node change on a fork branch from the current release tip with suites green on PC 2, with the igneum-pow feature | the build job id and its SUMMARY line; the v4 engine test named in the output | GREEN. PC 2 job `build-20261006-155958` (fork ca3-v4-node 5f7e0543, main a522d04; published 15:59:58Z after the clear file and under the mkdir lock, taken 15:58:50Z, released 16:08:06Z after the closing report; the installed app untouched, the prover left on), `node tools/build-job.mjs run --target 1ccfe586 --node vendor/igneum-node-ca3v4 --node-tests "kaspa-consensus kaspa-consensus-core igneum-exec kaspa-pow igneum-miner kaspa-p2p-flows" --app-tests igneum-app --no-place`: `done exit 0 after 392 s: node ca3-v4-node 5f7e0543 app 0.3.11: every stage ok; 9 files uploaded (46 MB); 6 min of 40`; the test stage 50 s, exit 0 on both units: kaspa-consensus 98 passed (3 ignored; `ban_is_decided_by_the_carrying_block_so_nodes_agree_on_every_voter_list ... ok`, so 2.0's flake did not recur and no re-run was needed), kaspa-consensus-core 109 + 7 (`override_params_carry_the_program_class_v4_activation ... ok`), igneum-exec ?, kaspa-pow 15 (`program_class_v3_seeds_hash_their_own_program_over_their_own_cache ... ok` and `program_class_v4_seeds_hash_the_shadow_program_over_the_v3_day_cache ... ok`, both `#[cfg(feature = "igneum-pow")]`), igneum-miner ?, kaspa-p2p-flows 33, igneum-app 112 + 26 + 8. The feature: `build-job.mjs` and `push-build-inputs.sh` carry no `--features` for the test units (the manifest's `tests` entries are `dir` and `packages` only; the build units have `kaspad/igneum-pow`), and the PC's `jobbuild.rs` runs `cargo test --release -p ... ` in ONE invocation per unit, where `igneum-miner` depends on `kaspa-pow` with `features = ["igneum-pow"]`, so cargo's feature unification turns the feature on for kaspa-pow's test target in that invocation: the two engine tests above ran on PC 2 with the feature, named in the report, which is the evidence main asked for. The same two suites also pass on the Mac with the flag explicit (`cargo test --release -p kaspa-pow --features igneum-pow`: 15; `-p kaspa-consensus-core`: 109 + 7). What the tool lacks: a `features` key on a test unit; not added here (a change to the PC's installed app would be needed to honour it) | +| G6 | the node change on a fork branch from the current release tip with suites green on PC 2, with the igneum-pow feature | the build job id and its SUMMARY line; the v4 engine test named in the output | GREEN. PC 2 job `build-20261006-155958` (fork ca3-v4-node 5f7e0543, main a522d04; published 15:59:58Z after the clear file and under the mkdir lock, taken 15:58:50Z, released 16:08:06Z after the closing report; the installed app untouched, the prover left on), `node tools/build-job.mjs run --target 1ccfe586 --node vendor/igneum-node-ca3v4 --node-tests "kaspa-consensus kaspa-consensus-core igneum-exec kaspa-pow igneum-miner kaspa-p2p-flows" --app-tests igneum-app --no-place`: `done exit 0 after 392 s: node ca3-v4-node 5f7e0543 app 0.3.11: every stage ok; 9 files uploaded (46 MB); 6 min of 40`; the test stage 50 s, exit 0 on both units: kaspa-consensus 98 passed (3 ignored; `ban_is_decided_by_the_carrying_block_so_nodes_agree_on_every_voter_list ... ok`, so 2.0's flake did not recur and no re-run was needed), kaspa-consensus-core 109 + 7 (`override_params_carry_the_program_class_v4_activation ... ok`), igneum-exec (its per-crate line is not in the relay's upload of the report, which keeps the stage's tail; the unit exited 0, so it passed), kaspa-pow 15 (`program_class_v3_seeds_hash_their_own_program_over_their_own_cache ... ok` and `program_class_v4_seeds_hash_the_shadow_program_over_the_v3_day_cache ... ok`, both `#[cfg(feature = "igneum-pow")]`), igneum-miner (the same: not in the upload, in the exit-0 unit), kaspa-p2p-flows 33, igneum-app 112 + 26 + 8. The feature: `build-job.mjs` and `push-build-inputs.sh` carry no `--features` for the test units (the manifest's `tests` entries are `dir` and `packages` only; the build units have `kaspad/igneum-pow`), and the PC's `jobbuild.rs` runs `cargo test --release -p ... ` in ONE invocation per unit, where `igneum-miner` depends on `kaspa-pow` with `features = ["igneum-pow"]`, so cargo's feature unification turns the feature on for kaspa-pow's test target in that invocation: the two engine tests above ran on PC 2 with the feature, named in the report, which is the evidence main asked for. The same two suites also pass on the Mac with the flag explicit (`cargo test --release -p kaspa-pow --features igneum-pow`: 15; `-p kaspa-consensus-core`: 109 + 7). What the tool lacks: a `features` key on a test unit; not added here (a change to the PC's installed app would be needed to honour it) | Summary files in this directory: `class-v4-20261006-1553Z-cpu.json` (G4 run 1), `class-v4-20261006-never-failed-case.json` (G4 run 2, the failed case), `class-v4-20261006-metal.json` (G4b).