Counter ASIC 3.0 gates (hash): AP-F8-2, the class v4 draw is total (main's ruling: no consensus path panics). Sub-version 2 unchanged: every seed that accepts within the bound draws the same program (re-export diff 0 on v4-devnet-epoch0, v4-era-0 and v4-era-5; the id a788661687db4bb3 and the seven fingerprints stand). The attempt cap of the class v4 shape is 256 (MAX_ATTEMPTS_V4; v2 and v3 keep 32): rules (a') and (c') reject about two thirds of candidates, so 32 attempts exhausted at about 2e-6 per epoch seed (seed igneum-f9/331672, the fdac338d panic), 256 at under 1e-45, half a second of draw in the worst case. After the cap the seed takes the last-resort program, deterministic and accepted as drawn: the candidate at attempt 256 with every or, mul and mulhi of the base program and the shadow block rewritten to xor, so every register stays fresh from the init words on and rule (a') holds by construction. A unit test walks the last resort on real (a')-rejected candidates (every load fresh after it, no lossy op left, the shapes kept) and the chain path over 64 seeds with no panic; the cap per class is asserted

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 13:30:33 +00:00
parent e56085e3a5
commit fbb0032067

View file

@ -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<Program, Exhausted> {
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));