igneum-pow at the 0.3.20 line (ca3-v4-amend 8d9d6859: 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

This commit is contained in:
igneum-labs 2026-10-07 15:24:22 +00:00
parent 9f543aa970
commit 6142100524
5 changed files with 84 additions and 10 deletions

View file

@ -9,7 +9,7 @@
//! One deliberate difference from the Swift: `program_json` writes the cache line mask inside the `"item"` string
//! as a bare `0x003fffff`. The Swift writes it quoted (`jhex`), which is not valid JSON.
use crate::generator::{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=<hex>).\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))));
}

View file

@ -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::<Vec<_>>(),
v3.instrs.iter().map(|i| i.op.is_load()).collect::<Vec<_>>(),
"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);

View file

@ -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)")))
}

View file

@ -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);
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

View file

@ -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());
}