class v5: the AP-F4-1 mixer-draw rule behind Shape.state (a mixer block with NAF sum under 163, a word under NAF weight 4 or under 4 distinct rotation amounts is redrawn from the next stream values; naf_weight, mixer_block_admissible; the known-failed case a block of two-adder multipliers, then a day scan that finds the census's 6.1e-4 redraws with v4's constants unmoved) and the AP-F1-1 shadow rule behind LoadClass.state (a block whose peephole-removable share, shadow_removable_count: xor-cancel, or-idempotence, rotate merges, sum-cancel, exceeds 3.0 percent is redrawn from the continuing stream; the known-failed case a block of or pairs, then a seed scan that finds the census's 4e-3 redraws with the base program unmoved)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 18:27:53 +00:00
parent f0f189903c
commit b46f4c82bc
2 changed files with 194 additions and 1 deletions

View file

@ -1293,6 +1293,41 @@ pub fn candidate_from_words(seed_string: &str, seed_bytes: &[u8], seed: [u32; 8]
/// [`candidate_from_words`] for a load class. For [`LoadClass::V2`] this is the version 2 draw stream exactly;
/// for any other class the slot count is the class's and every instruction takes a tenth draw, `below(100)`,
/// the width roll (used only on a load slot, drawn on every slot so the stream stays uniform).
/// Class v5's shadow rule (AP-F1-1): the peephole-removable share a block may have, per mille of its instructions.
pub const SHADOW_REMOVABLE_MAX_PERMILLE: usize = 30;
/// Class v5's shadow rule: redraws before the last block stands as drawn (never reached at 4e-3 per try).
pub const SHADOW_REDRAW_CAP: u32 = 64;
/// The instructions of a shadow block an honest compiler removes (the AP-F1-1 census's classes): an instruction whose
/// destination's last writer, with no write to the destination or to the source in between, is the same op on the
/// same source and immediates and the pair cancels (xor: `x ^= s` twice) or is idempotent (or: `x |= s` twice), or a
/// rotate of a register last written by a rotate (the two merge into one), or an add followed by a sub (or a sub by an
/// add) of the same source and immediate (sum-cancel). Counted per removable instruction, never across a pass.
pub fn shadow_removable_count(block: &[Instr]) -> usize {
let mut last: [Option<usize>; 8] = [None; 8];
let mut removable = 0;
for (k, ins) in block.iter().enumerate() {
let d = ins.dst as usize;
if let Some(j) = last[d] {
let prev = &block[j];
// the source must not have been written between j and k (its value is the same)
let src_untouched = !block[j + 1..k].iter().any(|i| i.dst == ins.src);
let same_operands = prev.src == ins.src && prev.imm == ins.imm && prev.imm2 == ins.imm2 && prev.src2 == ins.src2 && prev.rot == ins.rot && prev.bit == ins.bit && prev.mask == ins.mask;
let hit = match (prev.op, ins.op) {
(Op::Xor, Op::Xor) | (Op::Or, Op::Or) => same_operands && src_untouched,
(Op::Rotl, Op::Rotl) | (Op::Rotr, Op::Rotr) | (Op::Rotl, Op::Rotr) | (Op::Rotr, Op::Rotl) => true,
(Op::Add, Op::Sub) | (Op::Sub, Op::Add) => same_operands && src_untouched,
_ => false,
};
if hit {
removable += 1;
}
}
last[d] = Some(k);
}
removable
}
pub fn candidate_from_words_class(
seed_string: &str,
seed_bytes: &[u8],
@ -1447,7 +1482,14 @@ pub fn candidate_from_words_class(
// op from the non-load table, the source as on an ALU slot, the same per-instruction draws (the width roll and
// the era windows included when the class takes them, drawn and ignored) so the stream shape is the program's.
let mut shadow = Vec::new();
let mut shadow_redraws = 0u32;
if let Some(sh) = class.shadow {
// Class v5 (docs/design/class-v5-stored-state.md section 11, AP-F1-1): a shadow block whose peephole-removable
// share exceeds SHADOW_REMOVABLE_MAX_PERMILLE (3.0 percent of the block; the census's histogram puts 384 of 100,000
// draws there) is redrawn from the continuing stream, so an honest compiler's simplification cannot take more
// than 3 percent of the shadow's useful work. Every other class keeps its first draw.
loop {
shadow.clear();
for _ in 0..sh.instrs {
let mut roll = rng.below(75);
let mut op = Op::Add;
@ -1476,7 +1518,13 @@ pub fn candidate_from_words_class(
}
shadow.push(Instr { op, dst: dst as u8, src: src as u8, src2: b as u8, imm, imm2, rot, bit: bit as u8, mask, width: 1, win: 0, off: 0 });
}
if !class.state || shadow_removable_count(&shadow) * 1000 <= sh.instrs as usize * SHADOW_REMOVABLE_MAX_PERMILLE || shadow_redraws >= SHADOW_REDRAW_CAP {
break;
}
shadow_redraws += 1;
}
}
let _ = shadow_redraws;
Program {
seed_string: seed_string.to_string(),
seed_bytes: seed_bytes.to_vec(),
@ -1954,6 +2002,47 @@ mod tests {
assert!(!ProgramClass::V4.has_state() && ProgramClass::V5.has_state());
}
/// Class v5's shadow rule (AP-F1-1), the known-failed case first: a synthetic block of xor-cancel pairs is counted and
/// is over the bound; a scan of seeds finds a v5 draw whose first block was redrawn (the census's 4e-3), every v5
/// block is under 3.0 percent removable, and the same seed's class v4 block (never redrawn) is the first draw.
#[test]
fn class_v5_shadow_redundancy_rule() {
let mk = |op: Op, dst: u8, src: u8| Instr { op, dst, src, src2: 0, imm: 7, imm2: 9, rot: 3, bit: 0, mask: 1, width: 1, win: 0, off: 0 };
let pair = vec![mk(Op::Xor, 1, 2), mk(Op::Xor, 1, 2)];
assert_eq!(shadow_removable_count(&pair), 1, "the known-failed case: an xor-cancel pair");
let broken = vec![mk(Op::Xor, 1, 2), mk(Op::Add, 2, 3), mk(Op::Xor, 1, 2)];
assert_eq!(shadow_removable_count(&broken), 0, "the source moved between the two");
let rot = vec![mk(Op::Rotl, 4, 0), mk(Op::Rotr, 4, 5)];
assert_eq!(shadow_removable_count(&rot), 1, "two rotates merge");
let sum = vec![mk(Op::Add, 6, 7), mk(Op::Sub, 6, 7)];
assert_eq!(shadow_removable_count(&sum), 1, "sum-cancel");
let mut bad = Vec::new();
for _ in 0..128 {
bad.push(mk(Op::Or, 3, 5));
bad.push(mk(Op::Or, 3, 5));
}
assert!(shadow_removable_count(&bad) * 1000 > bad.len() * SHADOW_REMOVABLE_MAX_PERMILLE, "a block of or-idempotent pairs is over the bound");
let era = [7u8; 32];
let mut redrawn = 0;
let mut scanned = 0;
for i in 0..6_000u32 {
let seed = format!("igneum-shadow-scan/{i}");
let v5 = generate_from_seed_bytes_program_class(&seed, seed.as_bytes(), ProgramClass::V5, Some(&era));
assert!(shadow_removable_count(&v5.shadow) * 1000 <= v5.shadow.len() * SHADOW_REMOVABLE_MAX_PERMILLE, "{seed}: a v5 block over the bound");
let v4 = generate_from_seed_bytes_program_class(&seed, seed.as_bytes(), ProgramClass::V4, Some(&era));
assert_eq!(v5.instrs, v4.instrs, "{seed}: the base program never moves");
if v5.shadow != v4.shadow {
redrawn += 1;
assert!(shadow_removable_count(&v4.shadow) * 1000 > v4.shadow.len() * SHADOW_REMOVABLE_MAX_PERMILLE, "{seed}: v5 redrew a block the rule admits");
}
scanned += 1;
if redrawn >= 2 && scanned >= 1_000 {
break;
}
}
assert!(redrawn >= 1, "no redraw in {scanned} seeds (the census says about 4e-3 per draw)");
}
/// Class v5 (docs/design/class-v5-stored-state.md) takes the amended class v4 draw (AP-F8-1) as the chain draws it:
/// with an era present every load's source was last written by an injecting op or a rotate, the base program and
/// the shadow block equal the amended v4's of the same seed, and the same holds at a ladder rung; the state flag

View file

@ -206,6 +206,46 @@ pub struct MixParams {
pub shape: Shape,
/// The per-day derivation program when `shape.derive_len != 0`, else `None`.
pub derive: Option<DeriveProgram>,
/// Class v5: how many mixer blocks the AP-F4-1 rule redrew before this one (0 on every other class, and on most days).
pub redraws: u32,
}
/// Class v5's mixer-draw rule (AP-F4-1): the NAF sum of the 16 multipliers at least this.
pub const MIXER_NAF_SUM_MIN: u32 = 163;
/// Class v5's mixer-draw rule: every multiplier's NAF weight at least this.
pub const MIXER_NAF_WORD_MIN: u32 = 4;
/// Class v5's mixer-draw rule: at least this many distinct rotation amounts among the eight.
pub const MIXER_DISTINCT_ROT_MIN: usize = 4;
/// Class v5's mixer-draw rule: redraws before the last block stands as drawn (never reached at 6.1e-4 per try).
pub const MIXER_REDRAW_CAP: u32 = 64;
/// The non-adjacent-form weight of a 32-bit word: the number of non-zero digits of its NAF, the adders a
/// shift-and-add multiplier by that constant needs (the M1 metric of the weak-day census).
pub fn naf_weight(mut x: u64) -> u32 {
let mut w = 0;
while x != 0 {
if x & 1 == 1 {
w += 1;
// the digit is +1 or -1: take x to the nearest multiple of 4
if x & 3 == 3 {
x += 1;
} else {
x -= 1;
}
}
x >>= 1;
}
w
}
/// Whether a mixer block passes class v5's draw rule (AP-F4-1).
pub fn mixer_block_admissible(rot: &[u32; 8], mul: &[u32; 16]) -> bool {
let sum: u32 = mul.iter().map(|&m| naf_weight(m as u64)).sum();
let words = mul.iter().all(|&m| naf_weight(m as u64) >= MIXER_NAF_WORD_MIN);
let mut distinct = rot.to_vec();
distinct.sort_unstable();
distinct.dedup();
sum >= MIXER_NAF_SUM_MIN && words && distinct.len() >= MIXER_DISTINCT_ROT_MIN
}
impl MixParams {
@ -227,8 +267,28 @@ impl MixParams {
for c in rc.iter_mut() {
*c = rng.next() as u32;
}
let mut redraws = 0u32;
if shape.state {
// Class v5 (docs/design/class-v5-stored-state.md section 11, AP-F4-1, the attack-pass lane's weak-day census):
// a mixer block whose multipliers are cheap on an adder datapath (NAF sum under 163, a word under NAF weight 4)
// or whose rotations repeat (under 4 distinct amounts) is redrawn from the next stream values, so no day is a
// weak day for a per-day LUT-recompute FPGA (the worst calendar day of the census, chain day 29,337, was 1.121x).
// About 6.1e-4 of days redraw. The derive program's draws (none under v5) come after, as before.
while !mixer_block_admissible(&rot, &mul) && redraws < MIXER_REDRAW_CAP {
for r in rot.iter_mut() {
*r = 1 + rng.below(31) as u32;
}
for m in mul.iter_mut() {
*m = (rng.next() as u32) | 1;
}
for c in rc.iter_mut() {
*c = rng.next() as u32;
}
redraws += 1;
}
}
let derive = if shape.is_derived() { Some(DeriveProgram::draw(&mut rng, shape.derive_len)) } else { None };
Self { key, rot, mul, rc, shape, derive }
Self { key, rot, mul, rc, shape, derive, redraws }
}
/// Parameters for a day string: the key is `seed_words("day/" + day)`.
pub fn for_day(day: &str) -> Self {
@ -758,6 +818,50 @@ impl MemhardCpu {
mod tests {
use super::*;
/// Class v5's mixer-draw rule (AP-F4-1), the known-failed case first: a block of cheap multipliers (NAF sum under
/// 163) or repeated rotations is inadmissible; a scan of day keys finds days the rule redraws (the census's 6.1e-4),
/// every v5 block passes after the draw, and the v4 constants of the same keys never move.
#[test]
fn class_v5_mixer_draw_rule() {
assert_eq!(naf_weight(0), 0);
assert_eq!(naf_weight(1), 1);
assert_eq!(naf_weight(3), 2, "11 = 100 - 1");
assert_eq!(naf_weight(7), 2, "111 = 1000 - 1");
assert_eq!(naf_weight(0xffff_ffff), 2);
assert_eq!(naf_weight(0b1010_1010), 4);
let good_rot = [1u32, 5, 9, 13, 17, 21, 25, 29];
let cheap = [0x8000_0001u32; 16];
assert!(!mixer_block_admissible(&good_rot, &cheap), "the known-failed case: 16 two-adder multipliers");
let dense = [0xaaaa_aaabu32; 16];
assert!(mixer_block_admissible(&good_rot, &dense));
assert!(!mixer_block_admissible(&[7u32; 8], &dense), "one rotation amount");
assert!(!mixer_block_admissible(&[1u32, 2, 3, 3, 3, 3, 3, 3], &dense), "three distinct amounts");
let v5 = Shape { mixer_mult: 8, cache_log2_words: 26, derive_len: 0, state: true };
let v4 = Shape { mixer_mult: 8, cache_log2_words: 26, derive_len: 0, state: false };
let mut redrawn = 0;
let mut scanned = 0;
for d in 0..60_000u64 {
let key = crate::seed::seed_words_from_bytes(&crate::bind::day_bytes(20_000 + d));
let a = MixParams::with_shape(key, v5);
assert!(mixer_block_admissible(&a.rot, &a.mul), "day {d}: a v5 block fails the rule after the draw");
if a.redraws > 0 {
redrawn += 1;
let b = MixParams::with_shape(key, v4);
assert_eq!(b.redraws, 0, "v4 never redraws");
assert_ne!((a.rot, a.mul), (b.rot, b.mul), "day {d}: v5 redrew, v4 kept the block");
assert!(!mixer_block_admissible(&b.rot, &b.mul), "day {d}: the v4 block was the inadmissible one");
} else {
let b = MixParams::with_shape(key, v4);
assert_eq!((a.rot, a.mul, a.rc), (b.rot, b.mul, b.rc), "day {d}: an admissible day is byte for byte v4's");
}
scanned += 1;
if redrawn >= 3 && scanned >= 2_000 {
break;
}
}
assert!(redrawn >= 1, "no redraw in {scanned} days (the census says about 6.1e-4 per day)");
}
#[test]
fn mix_params_for_day() {
// MEMHARD.md section 1.4 and the igneum-genesis-mh pack.