diff --git a/igneum-pow/src/generator.rs b/igneum-pow/src/generator.rs index a02a91a4..999918f7 100644 --- a/igneum-pow/src/generator.rs +++ b/igneum-pow/src/generator.rs @@ -54,6 +54,18 @@ pub const LOAD_SLOTS: usize = 16; /// Attempts before an implementation may treat the seed as a consensus fault (spec 01 section 1.4.6). At the /// measured 5.14 percent rejection rate the chance of 32 consecutive rejections is below 2^-136. pub const MAX_ATTEMPTS: u32 = 32; + +/// The attempt cap of class v4 sub-version 2 (AP-F8-2, 7 October 2026): rules (a') and (c') reject about two thirds of +/// candidates, so 32 attempts exhaust with probability about (2/3)^32, 2e-6 per epoch seed, one epoch no node could +/// draw every few decades at one epoch an hour (seen at chain-shaped seed igneum-f9/331672). At 256 attempts the +/// exhaustion probability is (2/3)^256, under 1e-45; the cost of a rejected attempt is one draw and the 64-unit check, +/// about 2 ms on one core, so the worst case is half a second. Keyed on the class v4 shape, so v2 and v3 keep 32. +pub const MAX_ATTEMPTS_V4: u32 = 256; + +/// The attempt cap of a class: [`MAX_ATTEMPTS_V4`] for the class v4 shape, [`MAX_ATTEMPTS`] otherwise. +pub fn max_attempts_for(class: &LoadClass) -> u32 { + if crate::accept::is_class_v4_shape(class) { MAX_ATTEMPTS_V4 } else { MAX_ATTEMPTS } +} /// Domain tag of the program id. pub const PROGRAM_ID_TAG: &[u8] = b"igneum-program/"; @@ -1418,14 +1430,38 @@ pub fn try_generate_from_seed_bytes(seed_string: &str, seed_bytes: &[u8]) -> Res /// [`try_generate_from_seed_bytes`] for a load class. pub fn try_generate_class(seed_string: &str, seed_bytes: &[u8], class: LoadClass) -> Result { let mut last = None; - for attempt in 0..MAX_ATTEMPTS { + let cap = max_attempts_for(&class); + for attempt in 0..cap { let p = candidate_class(seed_string, seed_bytes, attempt, class); match check(&p) { Ok(_) => return Ok(p), Err(r) => last = Some(r), } } - Err(Exhausted { seed_string: seed_string.to_string(), attempts: MAX_ATTEMPTS, last: last.unwrap() }) + 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 + 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() }) +} + +/// The last-resort program of a class v4 seed whose [`MAX_ATTEMPTS_V4`] candidates were all rejected (AP-F8-2): +/// the candidate at attempt [`MAX_ATTEMPTS_V4`] with every `or`, `mul` and `mulhi` of its base program and its shadow +/// block rewritten to `xor` (dst, src and the other fields kept). With no lossy op left every register stays fresh by dataflow from the +/// init words on, so rule (a') holds by construction; the program is the seed's consensus program as drawn, with no +/// further check, so the draw is total. It is reached with probability about (2/3)^256 per epoch seed (the measured +/// (a') plus (c') rejection rate of about two thirds per attempt), under 1e-45: the chain never sees it, and a test +/// walks it on real rejected candidates so the path is known to run. +pub fn last_resort_v4(mut p: Program) -> Program { + // the shadow block runs at the end of every iteration and its own lossy ops feed the next iteration's loads + // (rule (a') walks base then shadow to its fixpoint), so both are rewritten + for i in p.instrs.iter_mut().chain(p.shadow.iter_mut()) { + if matches!(i.op, Op::Or | Op::Mul | Op::MulHi) { + i.op = Op::Xor; + } + } + p } /// [`try_generate_from_seed_bytes`], treating exhaustion as the consensus fault it is. @@ -1828,6 +1864,42 @@ mod tests { crate::accept::check_fresh_sources_v4(p).is_ok() } + /// AP-F8-2: the class v4 draw is total. The last resort turns real (a')-rejected candidates into programs every + /// load of which reads a fresh register, the cap is 256 for the v4 shape and 32 for every other class, and the + /// chain path of a seed whose first candidates are rejected yields a program without a panic. + #[test] + fn class_v4_draw_is_total_with_the_last_resort() { + assert_eq!(max_attempts_for(&V4_CLASS), MAX_ATTEMPTS_V4); + assert_eq!(max_attempts_for(&LoadClass::era(V4_CLASS, &[7u8; 32], &V3_ALLOWED)), MAX_ATTEMPTS_V4); + assert_eq!(max_attempts_for(&V3_CLASS), MAX_ATTEMPTS); + assert_eq!(max_attempts_for(&LoadClass::V2), MAX_ATTEMPTS); + let class = LoadClass::era(V4_CLASS, &EraParams::test_era_bytes("igneum-era-test/0"), &V3_ALLOWED); + // real candidates that rule (a') rejects (the exhausting shape of seed igneum-f9/331672 at 07a809a7: 32 in a + // row), repaired by the last resort: every load's source fresh in the loop's steady state + let mut rejected = 0; + for i in 0..64u32 { + let seed = format!("igneum-ca3-v4-amend/total/{i}"); + for attempt in 0..4u32 { + let c = candidate_class(&seed, seed.as_bytes(), attempt, class); + if matches!(crate::accept::check_fresh_sources_v4(&c), Err(crate::accept::Reject::UnfreshLoadSource { .. })) { + rejected += 1; + let fixed = last_resort_v4(c.clone()); + assert!(crate::accept::check_fresh_sources_v4(&fixed).is_ok(), "{seed} attempt {attempt}: the last resort is fresh at every load"); + assert!(fixed.instrs.iter().chain(fixed.shadow.iter()).all(|i| !matches!(i.op, Op::Or | Op::Mul | Op::MulHi))); + assert_eq!((fixed.instrs.len(), fixed.shadow.len()), (c.instrs.len(), c.shadow.len())); + } + } + } + assert!(rejected > 0, "the sample holds (a')-rejected candidates (about two thirds of attempts do)"); + // the chain path is total: 64 seeds, every one a program + for i in 0..64u32 { + let seed = format!("igneum-ca3-v4-amend/total/{i}"); + let p = try_generate_class(&seed, seed.as_bytes(), class).expect("a class v4 seed always draws"); + assert!(crate::accept::check_fresh_sources_v4(&p).is_ok()); + assert!(p.attempt < MAX_ATTEMPTS_V4 || p.instrs.iter().chain(p.shadow.iter()).all(|i| !matches!(i.op, Op::Or | Op::Mul | Op::MulHi))); + } + } + #[test] fn program_class_v4_is_class_v3_with_the_shadow_block() { assert_eq!(V4_CLASS, V3_CLASS.with_shadow(256, 27));