From e792962bc007a078866e23f43d388ebfdffbf7e2 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:24:22 +0000 Subject: [PATCH] igneum-pow at the 0.3.20 line (ca3-v4-amend d4eea255: the ladder, the amended class v4 as object 5 and the rung-keyed source rule), the pair the 0.3.20 node fork links against; replaces the 0.3.18-line copy on this branch until ca3-v4-amend reaches master --- igneum-pow/src/emit.rs | 9 ++++++- igneum-pow/src/generator.rs | 54 ++++++++++++++++++++++++++++++++----- igneum-pow/src/packcheck.rs | 9 +++++++ igneum-pow/tests/mixer.rs | 17 ++++++++++-- igneum-pow/tests/recheck.rs | 5 +++- 5 files changed, 84 insertions(+), 10 deletions(-) diff --git a/igneum-pow/src/emit.rs b/igneum-pow/src/emit.rs index 37437fe37..8d93a59a3 100644 --- a/igneum-pow/src/emit.rs +++ b/igneum-pow/src/emit.rs @@ -9,7 +9,7 @@ //! One deliberate difference from the Swift: `program_json` writes the cache line mask inside the `"item"` string //! as a bare `0x003fffff`. The Swift writes it quoted (`jhex`), which is not valid JSON. -use crate::generator::{EraParams, Instr, Op, Program, ProgramClass, GENERATOR_VERSION, INSTR_COUNT, ITERATIONS, LOAD_SLOTS}; +use crate::generator::{EraParams, Instr, Op, Program, ProgramClass, GENERATOR_VERSION, INSTR_COUNT, ITERATIONS, LOAD_SLOTS, PROGRAM_SUBVERSION_V4}; use crate::derive::{instr_line as derive_instr_line, instr_text as derive_instr_text, DERIVE_PROGRAMS}; use crate::memhard::{ hot_key, hot_segments, hot_words, Layout, MixParams, Shape, CACHE_LINES_PER_SEGMENT, CACHE_SEGMENT_LOG2_LINES, CACHE_TAG, @@ -173,6 +173,10 @@ fn program_class_header_lines(p: &Program) -> String { 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 p.program_class() == ProgramClass::V4 { + // the class v4 stream sub-version (AP-F8-1 amendment): a worker ignores it, packcheck requires it + s.push_str(&format!("#define IGNEUM_PROGRAM_SUBVERSION {}\n", PROGRAM_SUBVERSION_V4)); + } if let Some(era) = &p.era_bytes { s.push_str(&format!("#define IGNEUM_ERA_SEED_HEX {}\n", jstr(&hex_bytes(era)))); } @@ -1825,6 +1829,9 @@ pub fn program_json(p: &Program, day: &str, ds: &DatasetSource) -> String { s.push_str(&format!(" \"loads_per_hash\": {},\n", p.loads_per_hash())); if p.program_class() != ProgramClass::V2 { s.push_str(&format!(" \"program_class\": {},\n", jstr(p.program_class().name()))); + if p.program_class() == ProgramClass::V4 { + s.push_str(&format!(" \"sub_version\": {},\n", PROGRAM_SUBVERSION_V4)); + } if let Some(era) = &p.era_bytes { s.push_str(&format!(" \"era_seed_bytes\": {},\n", jstr(&hex_bytes(era)))); } diff --git a/igneum-pow/src/generator.rs b/igneum-pow/src/generator.rs index 8bbc72c9f..13ac517a5 100644 --- a/igneum-pow/src/generator.rs +++ b/igneum-pow/src/generator.rs @@ -1076,16 +1076,29 @@ impl Program { } pub fn program_id(generator: u32, seed: &[u32; 8], attempt: u32) -> u64 { - let mut b = Vec::with_capacity(PROGRAM_ID_TAG.len() + 4 + 32 + 4); + let mut b = Vec::with_capacity(PROGRAM_ID_TAG.len() + 4 + 32 + 4 + 6); b.extend_from_slice(PROGRAM_ID_TAG); b.extend_from_slice(&generator.to_le_bytes()); for w in seed { b.extend_from_slice(&w.to_le_bytes()); } b.extend_from_slice(&attempt.to_le_bytes()); + if generator == GENERATOR_VERSION_V4 { + // The class v4 sub-version (AP-F8-1 amendment, 7 October 2026): `"sub/" || sub_version as little-endian u16` + // appended for generator 4 only, so a binary from before the load-source rule (sub-version 0, no suffix) and + // one after it never share a program id for one seed; the node's id check then catches a split. v2 and v3 + // ids are byte-identical. The node reads the sub-version from [`PROGRAM_SUBVERSION_V4`]; packs carry it as + // IGNEUM_PROGRAM_SUBVERSION and program.json "sub_version". + b.extend_from_slice(b"sub/"); + b.extend_from_slice(&PROGRAM_SUBVERSION_V4.to_le_bytes()); + } fnv1a64(&b) } +/// The sub-version of class v4's program stream, in every generator-4 program id and pack (AP-F8-1: 1 = the +/// load-source rule of `candidate_from_words_class`; 0 was the stream of 6 October 2026, never stamped). +pub const PROGRAM_SUBVERSION_V4: u16 = 1; + /// Domain tag of the program id of a read-width class (never collides with [`PROGRAM_ID_TAG`]). pub const PROGRAM_ID_TAG_RW: &[u8] = b"igneum-program-rw/"; @@ -1224,7 +1237,19 @@ pub fn candidate_from_words_class( is_hot[slot as usize] = true; } // (2) The instructions. `fresh[r]`: r was written by an earlier instruction and no load has read it since. + // Class v4's chain draw (AP-F8-1, 7 October 2026, `docs/analysis/ca3-v4-uniform.md`): a load's source is drawn + // only from registers whose last writer injects or is a rotate (`entropy_kept[r]`), never from one last written + // by `or`, `mul` or `mulhi` (an `or`-written source is all-ones with probability (3/4)^32 per read and made the + // 153x item of the finding). Keyed on the era-composed V4_CLASS so the generator-2 ladder packs keep their stream; + // v2 and v3 take no part. The draw order and the stream are otherwise the same, draw for draw. + // Keyed on the class with the shadow's pass count set aside (the latency ladder draws the same base program at + // every rung: `of_load_class` compares the pass count too and answered None at every rung but 27, found by the + // fork's ladder test, 7 October 2026 10:06Z), the same comparison the fork's "v4 is v3 plus the shadow" test makes. + let source_rule_v4 = class.era.is_some() + && matches!(class.shadow, Some(ShadowClass { instrs: V4_SHADOW_INSTRS, .. })) + && LoadClass { era: None, shadow: None, ..class } == LoadClass { shadow: None, ..V4_CLASS }; let mut fresh = [false; 8]; + let mut entropy_kept = [false; 8]; let mut instrs = Vec::with_capacity(INSTR_COUNT); for k in 0..INSTR_COUNT { let mut roll = rng.below(75); @@ -1250,7 +1275,7 @@ pub fn candidate_from_words_class( let mut eligible = [0u64; 8]; let mut n = 0usize; for r in 0..8u64 { - if r != dst && fresh[r as usize] { + if r != dst && fresh[r as usize] && (!source_rule_v4 || entropy_kept[r as usize]) { eligible[n] = r; n += 1; } @@ -1298,6 +1323,7 @@ pub fn candidate_from_words_class( fresh[src as usize] = false; } fresh[dst as usize] = true; + entropy_kept[dst as usize] = op.injects() || matches!(op, Op::Rotl | Op::Rotr); instrs.push(Instr { op, dst: dst as u8, src: src as u8, src2: b as u8, imm, imm2, rot, bit: bit as u8, mask, width, win, off }); } // (3) The latency-shadow block (Counter ASIC 3.0 item 8): drawn after the base program from the same stream, so @@ -1785,8 +1811,9 @@ mod tests { } /// 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; + /// the base program; since the AP-F8-1 amendment (7 October 2026) its base program draws a load's source only + /// from registers whose last writer keeps entropy, so it is its own stream over class v3's load slots and era + /// draw; its generator is 4, its id `program_id(4, seed, attempt)` with the sub-version suffix, its era recorded; /// v2 and v3 are untouched. #[test] fn program_class_v4_is_class_v3_with_the_shadow_block() { @@ -1803,9 +1830,24 @@ mod tests { 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); + // The AP-F8-1 amendment (7 October 2026): class v4's chain draw takes a load's source only from registers + // whose last writer injects or rotates, so its base program is its own stream (the 6 October stream, equal + // to class v3's draw for draw, is sub-version 0 and never stamped); the load slots, the op draws and the era + // draw are still class v3's, and every load site obeys the rule assert_eq!(v4.seed, v3.seed); + assert_eq!( + v4.instrs.iter().map(|i| i.op.is_load()).collect::>(), + v3.instrs.iter().map(|i| i.op.is_load()).collect::>(), + "the load slots are class v3's" + ); + let mut kept = [false; 8]; + for (k, i) in v4.instrs.iter().enumerate() { + if i.op.is_load() { + assert!(kept[i.src as usize], "load #{k} reads r{} whose last writer does not keep entropy", i.src); + } + kept[i.dst as usize] = i.op.injects() || matches!(i.op, Op::Rotl | Op::Rotr); + } + assert_ne!(v4.instrs, v3.instrs, "the amended v4 base program is not class v3's (a lossy-sourced load was redrawn)"); assert!(v3.shadow.is_empty() && !v3.has_shadow()); assert_eq!(v4.shadow.len(), 256); assert_eq!(v4.shadow_reps(), 27); diff --git a/igneum-pow/src/packcheck.rs b/igneum-pow/src/packcheck.rs index 177d74b3a..ee0490662 100644 --- a/igneum-pow/src/packcheck.rs +++ b/igneum-pow/src/packcheck.rs @@ -195,6 +195,15 @@ pub fn verify_pack_texts_chain( let shadow = define_u32(program_h, "IGNEUM_SHADOW_INSTRS").unwrap_or(0); match (class, shadow > 0) { (ProgramClass::V4, false) => return Err(PackFault::Disagree("IGNEUM_GENERATOR 4 (class v4) without IGNEUM_SHADOW_INSTRS: not a class v4 pack".into())), + // the class v4 stream sub-version (AP-F8-1 amendment, 7 October 2026): a generator 4 pack from before the + // load-source rule carries no IGNEUM_PROGRAM_SUBVERSION and its program id is another stream's; refused + (ProgramClass::V4, true) if define_u32(program_h, "IGNEUM_PROGRAM_SUBVERSION") != Some(u32::from(crate::generator::PROGRAM_SUBVERSION_V4)) => { + return Err(PackFault::Disagree(format!( + "IGNEUM_GENERATOR 4 (class v4) with IGNEUM_PROGRAM_SUBVERSION {}: this software runs sub-version {} (a class v4 pack from before the load-source rule, or after another amendment)", + define_u32(program_h, "IGNEUM_PROGRAM_SUBVERSION").map(|v| v.to_string()).unwrap_or_else(|| "absent".into()), + crate::generator::PROGRAM_SUBVERSION_V4 + ))) + } (ProgramClass::V3, true) => { return Err(PackFault::Disagree(format!("IGNEUM_GENERATOR 3 (class v3) with a shadow block (IGNEUM_SHADOW_INSTRS {shadow}): a class v4 program is generator 4 (export the pack as class v4)"))) } diff --git a/igneum-pow/tests/mixer.rs b/igneum-pow/tests/mixer.rs index eebda7f47..6230ee115 100644 --- a/igneum-pow/tests/mixer.rs +++ b/igneum-pow/tests/mixer.rs @@ -92,8 +92,21 @@ fn contract(p: &Program, seed: &str, class: LoadClass, era: Option<[u8; 32]>) { assert_eq!((i.width, i.win, i.off), (1, 0, 0), "shadow #{k}: no load fields"); } let base = program_of(seed, LoadClass { shadow: None, ..class }, era); - assert_eq!(p.instrs, base.instrs, "{seed}: the base program is the class's without the shadow"); - assert_eq!(p.attempt, base.attempt); + if p.generator == GENERATOR_VERSION_V4 { + // the amended class v4 (AP-F8-1): the chain draw takes a load's source only from registers whose + // last writer injects or rotates, so its base program is its own stream, not class v3's; what holds + // is the rule itself, checked here on every load site in draw order + let mut kept = [false; 8]; + for (k, i) in p.instrs.iter().enumerate() { + if i.op.is_load() { + assert!(kept[i.src as usize], "{seed}: load #{k} reads r{} whose last writer does not keep entropy", i.src); + } + kept[i.dst as usize] = i.op.injects() || matches!(i.op, Op::Rotl | Op::Rotr); + } + } else { + assert_eq!(p.instrs, base.instrs, "{seed}: the base program is the class's without the shadow"); + assert_eq!(p.attempt, base.attempt); + } if era.is_none() || p.generator == GENERATOR_VERSION_V4 { // a generator-2 program carries the shadow in its id bytes; a class v4 program is generator 4 // (ca3-v4-node 7c22d0d), so its id differs from the generator-3 id of the same seeds diff --git a/igneum-pow/tests/recheck.rs b/igneum-pow/tests/recheck.rs index d23977b40..ad0210b15 100644 --- a/igneum-pow/tests/recheck.rs +++ b/igneum-pow/tests/recheck.rs @@ -132,6 +132,9 @@ fn program_ids_differ_between_class_v3_and_class_v4_of_one_seed() { let v3 = load("packs-ca2-mixer/mx8-devnet-epoch0", ProgramClass::V3); let v4 = load("packs-ca3-v4/v4-devnet-epoch0", ProgramClass::V4); assert_eq!(v3.reference.program.program_id(), 0x73bc_bfe8_ccf9_88f1, "the v3 control's id as pinned"); - assert_eq!(v4.reference.program.program_id(), 0xc120_d796_3abd_cd96, "the v4 candidate's id as pinned"); + // the amended class v4 (AP-F8-1, sub-version 1): the id of 6 October 2026, c120d7963abdcd96, is the must-differ + // vector (a binary from before the load-source rule), the pinned id below the must-equal one + assert_ne!(v4.reference.program.program_id(), 0xc120_d796_3abd_cd96, "the pre-amendment v4 id must differ"); + assert_eq!(v4.reference.program.program_id(), 0x1a42_3069_9a6b_9c60, "the amended v4 candidate's id as pinned"); assert_ne!(v3.reference.program.program_id(), v4.reference.program.program_id()); }