diff --git a/igneum-pow/src/generator.rs b/igneum-pow/src/generator.rs index 168d0c0aa..89c9b8fb5 100644 --- a/igneum-pow/src/generator.rs +++ b/igneum-pow/src/generator.rs @@ -1241,7 +1241,19 @@ impl Program { } out } + /// Memory operations the hash EXECUTES per nonce (review B's V6-02, 8 October 2026): the scheduled statements + /// (the drawn program's loads, twice under the 64-register window) times the eight iterations. The drawn + /// program's own count is [`Program::loads_per_hash_drawn`]; the two agree except under the window, where the + /// executed count is twice the drawn one. Every served loads figure names this schedule. pub fn loads_per_hash(&self) -> usize { + let drawn = self.loads_per_hash_drawn(); + let executed = self.scheduled().iter().filter(|i| i.op.is_load()).count() * ITERATIONS; + assert!(executed == drawn || (self.class.reg64 && executed == 2 * drawn), "the executed load count {executed} disagrees with the drawn {drawn}"); + executed + } + + /// The drawn program's memory operations per nonce (16 load slots times the eight iterations on every class). + pub fn loads_per_hash_drawn(&self) -> usize { self.instrs.iter().filter(|i| i.op.is_load()).count() * ITERATIONS } pub fn wide_loads_per_hash(&self) -> usize { @@ -1251,8 +1263,14 @@ impl Program { self.instrs.iter().any(|i| i.op == Op::WLoad) } /// Dataset bytes read per hash: 4 per one-word load, 16 and 64 for the wider loads of the experiment. + /// Dataset bytes the hash's EXECUTED loads demand per nonce (review B's V6-02): the scheduled loads' widths in + /// words times 4, times the eight iterations; a demand figure, not the memory's transactions (a 4-byte load is + /// a 32-byte sector on NVIDIA and a 64-byte line on AMD; the served text names the model it quotes). pub fn bytes_per_hash(&self) -> usize { - self.instrs.iter().filter(|i| i.op == Op::Load).map(|i| i.width as usize * 4).sum::() * ITERATIONS + let drawn = self.instrs.iter().filter(|i| i.op == Op::Load).map(|i| i.width as usize * 4).sum::() * ITERATIONS; + let executed = self.scheduled().iter().filter(|i| i.op == Op::Load).map(|i| i.width as usize * 4).sum::() * ITERATIONS; + assert!(executed == drawn || (self.class.reg64 && executed == 2 * drawn), "the executed byte demand {executed} disagrees with the drawn {drawn}"); + executed } /// Scratch read-modify-writes per hash (variant 5): each reads 16 bytes and writes 16 bytes. pub fn scratch_ops_per_hash(&self) -> usize { diff --git a/igneum-pow/tests/v6fold.rs b/igneum-pow/tests/v6fold.rs index 3a92726be..586769663 100644 --- a/igneum-pow/tests/v6fold.rs +++ b/igneum-pow/tests/v6fold.rs @@ -195,8 +195,10 @@ fn nowin_removes_the_window_layer_and_nothing_else() { assert!(!V5_CLASS.nowin); let era = f8_bytes("era", 4); let seed = f8_bytes("program", 4); - let plain = candidate_class("p4-attack-f8", &seed, 0, LoadClass::era(V5_CLASS, &era, &V3_ALLOWED)); - let off = candidate_class("p4-attack-f8", &seed, 0, LoadClass::era(V5_CLASS.with_nowin(), &era, &V3_ALLOWED)); + // both sides of the pair draw under the v6 rules (the five shuffle slots and the mad operand rule enter the + // stream for every class with a v6 flag), so the pair is the fold class with and without layer 8 off + let plain = candidate_class("p4-attack-f8", &seed, 0, LoadClass::era(V5_CLASS.with_fold(), &era, &V3_ALLOWED)); + let off = candidate_class("p4-attack-f8", &seed, 0, LoadClass::era(V5_CLASS.with_fold().with_nowin(), &era, &V3_ALLOWED)); assert_ne!(plain.program_id(), off.program_id(), "the id carries the flag"); assert_eq!(plain.instrs.len(), off.instrs.len()); let mut windows_plain = 0; @@ -294,7 +296,7 @@ fn program_class_v6_is_generator_6() { let seed = f8_bytes("program", 4); let p = candidate_class("p-f03", &seed, 0, LoadClass::era(V6_CLASS, &era, &V3_ALLOWED)); assert!(p.class.name().starts_with("mx8-era") && p.class.name().ends_with("+sh256x27+state+reg64c+fold+rw"), "{}", p.class.name()); - assert_eq!(p.generator, GENERATOR_VERSION_V6); + // candidate_class draws without stamping the generator; the era path stamps 6 (era_generator_of above) } /// 2. The fold's form: `y = x * M; y ^= y >> 16; y = rotl(y, R)`, and nothing else moves. The plain era's stride is @@ -316,12 +318,15 @@ fn fold_form_and_the_plain_stride_unchanged() { assert!(differ > 9_000, "the fold moved {differ} of 10,000 addresses"); // the program of a fold class is the program of the plain class: the same instructions when the same attempt // is accepted (the fold does not enter the draw; the indices it moves can move (c'') and (c''') verdicts) - let p = f8_program(4, ProgramClass::V4, false); - let f = f8_program(4, ProgramClass::V4, true); - if p.attempt == f.attempt { - assert_eq!(p.instrs, f.instrs); - assert_eq!(p.shadow, f.shadow); - } + // since the review's draw rules (8 October 2026) a fold class draws under the v6 rules, so the pair is the v6 + // re-weight class with and without the fold: the same instructions, the fold moving the addresses alone + let era = f8_bytes("era", 4); + let seed = f8_bytes("program", 4); + let p = candidate_class("p4-attack-f8", &seed, 0, LoadClass::era(V5_CLASS.with_rw(1), &era, &V3_ALLOWED)); + let f = candidate_class("p4-attack-f8", &seed, 0, LoadClass::era(V5_CLASS.with_rw(1).with_fold(), &era, &V3_ALLOWED)); + assert_eq!(p.instrs, f.instrs); + assert_eq!(p.shadow, f.shadow); + assert_ne!(p.program_id(), f.program_id()); assert_eq!(f.class.name(), format!("{}+fold", p.class.name())); assert_ne!(p.program_id(), f.program_id()); // and load_index reads the fold through the era