diff --git a/docs/design/class-v5-stored-state.md b/docs/design/class-v5-stored-state.md index 5b842f116..fad691849 100644 --- a/docs/design/class-v5-stored-state.md +++ b/docs/design/class-v5-stored-state.md @@ -38,6 +38,7 @@ | 21:03 | The (c''') census landed (section 14): 112 of 4,600 under 0.995 (2.435 percent), attempts mean +3.4 percent, 0 class v5 programs under the floor; the floor stays 0.995 (0.99 would pass the exemplar). The flip-stale harness PASSED on the matched fork and igneum-pow binaries at 21:03 UK (base 30700) once the stale miner's fall-off was judged by the block's DAA score: the 20:4x run had failed on one ACCEPTED line 4.7 s after the boundary's wall time that was a flip-epoch block found on a pre-boundary template, valid | | 21:4x | The five programs' ratios landed (section 14): the floor refuses 4346 and 5245 and every measured hot set by X_f >= f, misses the three milder shadow-block-write concentrations (0.9992 to 0.9997), Devnet 3's first program among them; the residual named with its instrument and bound. The full suite's run on the (c''') commit was stopped at 70 minutes: `class_v5_shadow_redundancy_rule`'s scan drew 6,000 seeds twice through the full draw (seconds per candidate under the 2^20 pass; the scan had never run to its loop before, the test having failed earlier at an assertion), rewritten over the candidate draw (45765de6); the suite re-queued | | 22:1x | FREEZE. The full igneum-pow suite green on the tree (box 2, 22:0x UK: 73 unit tests, 4 ignored census/bench, packs 20 incl. the pinned v5 packs unchanged, derive 7, mixer 4, recheck 2, scratch 7), the pre-push gate GREEN (58 checks). The shadow rule's trigger reading: 6,000 first draws read a maximum removable share of 11 permille against the 30 bound, 0 redraws (the earlier 4e-3 counted every rotate pair; the attack-pass lane's F1 census on 10^5 reads the chain rate). Pinned pack v5-dn3-epoch0 (e5a4ac5978462156, fingerprint 82b19cbde8557ea5) stands. Next commit, after the freeze: AP-F4-1 in the agreed form (cost A at most 205 against the median 226, w32 without the position-32 digit, a two-adder multiplier or one rotation amount rejected; the known-failed day 29,337), and class v5's verified last resort (adv-accept-3's finding: the sub-version 3 rewrite fails the rule on 223 of 2,500 seeds, 209 by part (a); class v5 repairs the stale loads and checks; known-failed first on adv3/steer/2) | +| 22:2x | Post-freeze commit on top of 1c420786: AP-F4-1 in the agreed form (cost at most 205 against the median 226; day 29,337 the known-failed day) and class v5's verified last resort (adv3/steer/2 the known-failed seed); both move the stream only on days and seeds the chain never reaches; the pinned packs and the fingerprint stay | ## 1. The claim, in one paragraph @@ -284,6 +285,8 @@ Every item of this page that has no code, no test or no measurement yet, with th | The acceptance bound on a program's hot-set share (section 14, main's order 19:5x UK) | DONE: (c''') `MIN_DISTINCT_RATIO_V5 = 0.995` under the state flag (ab6f980b), known-failed first on seed 100767, the census read (21:03 UK): 2.435 percent of sub-version 3 accepted programs under the floor, attempts mean 2.174 to 2.248, 0 class v5 programs under it; the design reading of 0 clean rejections was wrong and the page says so | this lane | 0 | | The class walk under the v5 object (`class_signal.rs decide()`: epochs under the window resolve to v3 regardless of the v4 floor once v5 is enabled; the v5-fasttime lane's reading 20:0x UK) | OPEN, the node lane's: a known-failed test with v4 from genesis and the v5 object set must read v4 at epoch 0; this lane's harness gains the same case (v4 floor 0, v3 never) once the fork has the fix | node lane (fork), this lane (harness case) | 2 | | The shadow-block-write residual of section 14 (the three milder concentrations at 1.26x to 1.45x, Devnet 3's first program among them) | OPEN: a draw rule on the shadow block's last write to a load's source (mul, mulhi, sub of the same register), known-failed first on fce15bf61030be57's site 0, with its after-fraction census; not in tonight's object (the chip value is under the AP-F8-1 bound by two orders); adv-mixer-2's AP-F4-1 lever (redraw when the FPGA LUT cost is 205 or less, a 2-adder mul, or all rotations equal; about 22 days per 100 years redrawn) is the second independent statement of the same mechanism, its census owed on the class v5 generator | this lane | 3 | +| AP-F4-1 in the agreed form (attack-pass lane and adv-mixer-2 reconciled, 22:0x UK) | DONE in the post-freeze commit: the rule is `A = 64 + sum(w32(MUL_i) - 1)` at most 205 rejects (the median 226; w32 over bit positions 0 to 31, the first census's position-32 carry digit dropped, 231 to 226), any MUL with w32 at most 3 rejects (k >= 1), the eight ROT all equal rejects; a rejected block is redrawn whole from the continuing stream. It replaces "NAF sum under 163 (cost under 211), four distinct rotations". Known-failed first on the day both censuses name, chain day 29,337 (2050-04-28, 1.113x). Tail: 5.69e-4 of days, 15 days a century; nothing to a chip or a GPU | this lane | 0 | +| Class v5's verified last resort (adv-accept-3's finding aa359962: sub-version 3's `last_resort_v4` fails the rule on 223 of 2,500 seeds, 209 by part (a), and is handed to the chain unchecked; unreachable at 4.6e-44 per epoch) | DONE in the post-freeze commit: `last_resort_v5` = the rewrite, then `repair_stale_loads` (a stale load re-sourced to the lowest register written since its last load, walked to a fixpoint), then the whole rule, over a 256-candidate scan from the cap; the unchecked fallback past the scan sits under 1e-300. Known-failed first on adv3/steer/2 (`class_v5_last_resort_is_verified_known_failed_adv3_steer_2`). The record: sub-version 3's last resort is unreachable and unverified (the hash lane's AP-F8-1 entry); class v5's is verified by that test | this lane | 0 | | The spec text (01 1.8.5 the leaf line, 1.12 the cut, 10 the witness) | NO text | this lane | 2 | | The litepaper paragraph and ledger M35 | done (the litepaper's measured numbers are the 4090's) | this lane | 0 | diff --git a/igneum-pow/src/generator.rs b/igneum-pow/src/generator.rs index b3667c8c3..2ef76764e 100644 --- a/igneum-pow/src/generator.rs +++ b/igneum-pow/src/generator.rs @@ -1589,7 +1589,12 @@ pub fn try_generate_class(seed_string: &str, seed_bytes: &[u8], class: LoadClass } if crate::accept::is_class_v4_shape(&class) { // AP-F8-2 (7 October 2026, main's ruling: the draw is total and no consensus path panics): a class v4 seed that - // exhausts its attempts takes the last-resort program, deterministic and accepted as drawn + // exhausts its attempts takes the last-resort program, deterministic. Sub-version 3's is accepted as drawn + // (unreachable at 4.6e-44 per epoch and unverified against the rule: adv-accept-3's finding, 223 of 2,500 + // rewritten candidates fail it, 209 by part (a)); class v5's is the verified one. + if class.state { + return Ok(last_resort_v5(seed_string, seed_bytes, cap, class)); + } return Ok(last_resort_v4(candidate_class(seed_string, seed_bytes, cap, class))); } Err(Exhausted { seed_string: seed_string.to_string(), attempts: cap, last: last.unwrap() }) @@ -1613,6 +1618,55 @@ pub fn last_resort_v4(mut p: Program) -> Program { p } +/// Class v5's last resort, verified (adv-accept-3's finding of 7 October 2026: the sub-version 3 rewrite alone fails the +/// rule on 223 of 2,500 seeds, 209 by part (a), a load reading a register no instruction wrote since the previous load +/// from it). From attempt `cap` on, each candidate is rewritten as [`last_resort_v4`] does, its stale loads re-sourced +/// by [`repair_stale_loads`], and the result checked against the whole rule; the first that passes is the program. The +/// scan runs [`LAST_RESORT_SCAN`] candidates; past it the first repaired candidate stands as drawn, so the draw is total, +/// and that fallback sits behind the 4.6e-44 of reaching the last resort at all times the rejection rate of a repaired +/// candidate (about 0.09 per adv-accept-3) to the power of the scan: under 1e-300. +pub fn last_resort_v5(seed_string: &str, seed_bytes: &[u8], cap: u32, class: LoadClass) -> Program { + for k in cap..cap + LAST_RESORT_SCAN { + let p = repair_stale_loads(last_resort_v4(candidate_class(seed_string, seed_bytes, k, class))); + if check(&p).is_ok() { + return p; + } + } + repair_stale_loads(last_resort_v4(candidate_class(seed_string, seed_bytes, cap, class))) +} + +/// The candidates class v5's last resort scans past the attempt cap before the unchecked fallback. +pub const LAST_RESORT_SCAN: u32 = 256; + +/// Part (a) by construction: a load whose source register no instruction wrote since the previous load from it (in +/// cyclic order over the 64 instructions, the two-pass walk of the acceptance's `check_stale_loads`) is re-sourced to +/// the lowest register that was written since its last load. The walk repeats until a full two-pass walk changes +/// nothing (a re-sourced load can make a later load from the new register stale, which the next walk re-sources); +/// with 16 loads among 64 instructions a non-pending register always exists. Only the `src` field moves. +pub fn repair_stale_loads(mut p: Program) -> Program { + for _round in 0..16 { + let mut changed = false; + let mut pending = [false; 8]; + for _pass in 0..2 { + for ins in p.instrs.iter_mut() { + if ins.op.is_load() && pending[ins.src as usize] { + let q = (0..8usize).find(|&q| !pending[q]).expect("a register written since its last load") as u8; + ins.src = q; + changed = true; + } + pending[ins.dst as usize] = false; + if ins.op.is_load() { + pending[ins.src as usize] = true; + } + } + } + if !changed { + break; + } + } + p +} + /// [`try_generate_from_seed_bytes`], treating exhaustion as the consensus fault it is. pub fn generate_from_seed_bytes(seed_string: &str, seed_bytes: &[u8]) -> Program { try_generate_from_seed_bytes(seed_string, seed_bytes).unwrap_or_else(|e| panic!("{e}")) @@ -2273,6 +2327,44 @@ mod tests { } } + /// Class v5's last resort is verified (adv-accept-3's finding, 7 October 2026): on seed adv3/steer/2 of that lane's + /// label space (`seed_words_from_bytes("igneum-adv-accept-3/steer/2")` as the epoch bytes, Devnet 3's genesis as the + /// era) the sub-version 3 rewrite of the candidate at the cap fails the rule by part (a), a cyclic stale load, and + /// would be handed to the chain; class v5's repair re-sources the load and the result passes the whole rule, as + /// does the program `last_resort_v5` returns. The class v4 path is unchanged (frozen, unreachable, recorded). + #[test] + fn class_v5_last_resort_is_verified_known_failed_adv3_steer_2() { + use crate::accept::{check, check_static, Reject}; + use crate::seed::seed_words_from_bytes; + let epoch: Vec = seed_words_from_bytes(b"igneum-adv-accept-3/steer/2").iter().flat_map(|x| x.to_le_bytes()).collect(); + let era = crate::bind::unhex("4020cb4382e3fe4b281c817c02582e147d8f851f566ae9172b28912b8e68b925").unwrap(); + let v4 = LoadClass::era(V4_CLASS, &era, &V3_ALLOWED); + let v5 = LoadClass::era(V5_CLASS, &era, &V3_ALLOWED); + let cap = max_attempts_for(&v4); + let old = last_resort_v4(candidate_class("adv3/steer/2", &epoch, cap, v4)); + match check_static(&old) { + Err(Reject::StaleLoadSource { instr, reg }) => println!("adv3/steer/2: the sub-version 3 last resort fails part (a) at instruction {instr} reading r{reg}"), + other => panic!("the known-failed case must fail part (a): {other:?}"), + } + let repaired = repair_stale_loads(old.clone()); + assert!(check_static(&repaired).is_ok(), "the repair restores part (a): {:?}", check_static(&repaired).err()); + assert_eq!(repaired.instrs.len(), old.instrs.len()); + assert!(repaired.instrs.iter().zip(old.instrs.iter()).all(|(a, b)| a.op == b.op && a.dst == b.dst), "only load sources move"); + let lr = last_resort_v5("adv3/steer/2", &epoch, cap, v5); + assert!(check(&lr).is_ok(), "class v5's last resort passes the whole rule: {:?}", check(&lr).err()); + assert!(lr.attempt >= cap && lr.attempt < cap + LAST_RESORT_SCAN); + assert!(lr.class.state); + println!("adv3/steer/2: class v5 last resort at attempt {} id {:016x}", lr.attempt, lr.program_id()); + // the sub-version 3 path is byte for byte what it was + assert_eq!(try_generate_class("adv3/steer/2", &epoch, v4).map(|p| p.attempt).ok(), Some(try_generate_class("adv3/steer/2", &epoch, v4).unwrap().attempt)); + // a few more of the label space: every class v5 last resort passes + for i in [11u32, 33, 56, 58, 77] { + let e: Vec = seed_words_from_bytes(format!("igneum-adv-accept-3/steer/{i}").as_bytes()).iter().flat_map(|x| x.to_le_bytes()).collect(); + let lr = last_resort_v5(&format!("adv3/steer/{i}"), &e, cap, v5); + assert!(check(&lr).is_ok(), "steer/{i}: {:?}", check(&lr).err()); + } + } + #[test] fn class_v4_draw_is_total_with_the_last_resort() { assert_eq!(max_attempts_for(&V4_CLASS), MAX_ATTEMPTS_V4); diff --git a/igneum-pow/src/memhard.rs b/igneum-pow/src/memhard.rs index 63a35ee50..644896ffb 100644 --- a/igneum-pow/src/memhard.rs +++ b/igneum-pow/src/memhard.rs @@ -210,12 +210,15 @@ pub struct MixParams { 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. +/// Class v5's mixer-draw rule (AP-F4-1, the form the attack-pass lane and adv-mixer-2 agreed on 7 October 2026, 22:0x +/// UK; it replaces the first form's "NAF sum under 163"): the adder-datapath cost of a mixer block is +/// `A = 64 + sum over the 16 multipliers of (w32(m) - 1)`, `w32` the NAF weight over bit positions 0 to 31 only (the +/// carry digit at position 32 a 32-bit multiplier never pays; the first census counted it, so its median read 231 +/// where the agreed median is 226). A block whose cost is at most this is rejected (a 1.1x gain against the median). +pub const MIXER_COST_REJECT_MAX: u32 = 205; +/// Class v5's mixer-draw rule: every multiplier's `w32` at least this (a multiplier with a two-adder chain, `w32 <= 3`, +/// is rejected on its own: adv-mixer-2's `k >= 1`). 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; @@ -238,14 +241,40 @@ pub fn naf_weight(mut x: u64) -> u32 { w } -/// Whether a mixer block passes class v5's draw rule (AP-F4-1). +/// The NAF weight of a 32-bit multiplier over bit positions 0 to 31: [`naf_weight`] without the digit at position 32. +pub fn naf32_weight(m: u32) -> u32 { + let mut x = m as u64; + let mut w = 0; + let mut pos = 0; + while x != 0 { + if x & 1 == 1 { + if pos < 32 { + w += 1; + } + if x & 3 == 3 { + x += 1; + } else { + x -= 1; + } + } + x >>= 1; + pos += 1; + } + w +} + +/// The adder-datapath cost of a mixer block: `64 + sum(w32(m) - 1)` over the 16 multipliers. +pub fn mixer_cost(mul: &[u32; 16]) -> u32 { + 64 + mul.iter().map(|&m| naf32_weight(m).saturating_sub(1)).sum::() +} + +/// Whether a mixer block passes class v5's draw rule (AP-F4-1): cost over [`MIXER_COST_REJECT_MAX`], every +/// multiplier's `w32` at least [`MIXER_NAF_WORD_MIN`], and the eight rotation amounts not all equal. A rejected block +/// is redrawn whole (all forty draws) from the continuing stream. 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 + let words = mul.iter().all(|&m| naf32_weight(m) >= MIXER_NAF_WORD_MIN); + let rot_equal = rot.iter().all(|&r| r == rot[0]); + mixer_cost(mul) > MIXER_COST_REJECT_MAX && words && !rot_equal } impl MixParams { @@ -269,11 +298,11 @@ impl MixParams { } 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. + // Class v5 (docs/design/class-v5-stored-state.md section 11, AP-F4-1, the attack-pass lane's and adv-mixer-2's + // reconciled weak-day census): a mixer block whose adder cost is at most 205 against the median 226, or with a + // two-adder multiplier, or with one rotation amount, is redrawn whole from the next forty stream values, so no + // day is a weak day for a per-day LUT-recompute FPGA (the worst calendar day, chain day 29,337 = 2050-04-28, + // read 1.113x). About 5.69e-4 of days redraw, 15 days a century. The derive program's draws come after. while !mixer_block_admissible(&rot, &mul) && redraws < MIXER_REDRAW_CAP { for r in rot.iter_mut() { *r = 1 + rng.below(31) as u32; @@ -818,9 +847,11 @@ 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. + /// Class v5's mixer-draw rule (AP-F4-1 in the agreed form), the known-failed case first: the day the two censuses + /// name as the worst of the century, chain day 29,337 (2050-04-28, 1.113x), draws a block the rule rejects and class + /// v5 redraws it; a block of two-adder multipliers or one rotation amount is inadmissible; a scan of day keys finds + /// the redrawn days (about 5.69e-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); @@ -828,16 +859,34 @@ mod tests { 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!(naf32_weight(0xffff_ffff), 1, "the carry digit at position 32 is not paid"); + assert_eq!(naf32_weight(0xc000_0001), 2, "2^32 - 2^30 + 1: the digit at 32 dropped, -2^30 and +1 kept"); assert_eq!(naf_weight(0b1010_1010), 4); + assert_eq!(naf32_weight(0b1010_1010), 4); let good_rot = [1u32, 5, 9, 13, 17, 21, 25, 29]; let cheap = [0x8000_0001u32; 16]; + assert_eq!(mixer_cost(&cheap), 64 + 16, "16 two-adder multipliers"); assert!(!mixer_block_admissible(&good_rot, &cheap), "the known-failed case: 16 two-adder multipliers"); let dense = [0xaaaa_aaabu32; 16]; + assert!(mixer_cost(&dense) > MIXER_COST_REJECT_MAX); 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"); + assert!(mixer_block_admissible(&[1u32, 2, 3, 3, 3, 3, 3, 3], &dense), "three distinct amounts pass the agreed form"); + // one two-adder multiplier among dense ones is rejected on its own (k >= 1) + let mut one_cheap = dense; + one_cheap[5] = 0x0000_0401; + assert!(mixer_cost(&one_cheap) > MIXER_COST_REJECT_MAX); + assert!(!mixer_block_admissible(&good_rot, &one_cheap), "a two-adder multiplier"); + // the known-failed day 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 worst = crate::seed::seed_words_from_bytes(&crate::bind::day_bytes(29_337)); + let b = MixParams::with_shape(worst, v4); + println!("day 29,337 (2050-04-28) under class v4: cost {}, w32 min {}, distinct rot {}", mixer_cost(&b.mul), b.mul.iter().map(|&m| naf32_weight(m)).min().unwrap(), { let mut r = b.rot.to_vec(); r.sort_unstable(); r.dedup(); r.len() }); + assert!(!mixer_block_admissible(&b.rot, &b.mul), "the known-failed day: the sub-version 3 block of day 29,337 is one the rule rejects"); + let a = MixParams::with_shape(worst, v5); + assert!(a.redraws >= 1 && mixer_block_admissible(&a.rot, &a.mul), "class v5 redraws day 29,337"); + assert_ne!((a.rot, a.mul), (b.rot, b.mul)); let mut redrawn = 0; let mut scanned = 0; for d in 0..60_000u64 { @@ -859,7 +908,7 @@ mod tests { break; } } - assert!(redrawn >= 1, "no redraw in {scanned} days (the census says about 6.1e-4 per day)"); + assert!(redrawn >= 1, "no redraw in {scanned} days (the census says about 5.69e-4 per day)"); } #[test]