From 67d40c9b97e83002d5eb3b48716c08683ab91178 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:47:44 +0000 Subject: [PATCH] adv-accept: attempts command (row 90: per-part rejections, cap, last resort, forced-exhaust plant) Co-Authored-By: Claude Fable 5.1 --- tools/attack/adv-accept/src/main.rs | 125 ++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/tools/attack/adv-accept/src/main.rs b/tools/attack/adv-accept/src/main.rs index a8456298c..11df7f997 100644 --- a/tools/attack/adv-accept/src/main.rs +++ b/tools/attack/adv-accept/src/main.rs @@ -328,6 +328,7 @@ fn main() { "sweep" => sweep(&args[2..]), "plant" => plant(&args[2..]), "gap" => gap_cmd(&args[2..]), + "attempts" => attempts_cmd(&args[2..]), _ => { eprintln!("unknown command {}", args[1]); std::process::exit(2); @@ -582,3 +583,127 @@ fn gap_cmd(args: &[String]) { gap::run(&p, day, threads, zero); } } + +// -------------------------------------------------------------------------------------------------------------- +// Row 90 (owner adv-accept-3, unspawned; claimed by this lane): the attempt loop walked through the real rule, +// attempt by attempt, recording WHICH part rejected each candidate (the first failing test in the module's +// order), the attempt count of the accepted program, any seed reaching the 256 cap, and the last-resort program. +// The draw and every verdict are the library's (candidate_class, accept::check, last_resort_v4). +// Known-failed shape: `--force-exhaust` treats every verdict as a rejection, so the loop MUST hit the cap and +// print the last-resort program's shape (what a rule that rejects everything would hand the chain). +// -------------------------------------------------------------------------------------------------------------- + +fn attempts_cmd(args: &[String]) { + use igneum_pow::generator::{candidate_class, last_resort_v4, LoadClass, GENERATOR_VERSION_V4, MAX_ATTEMPTS_V4}; + let get = |k: &str, d: &str| -> String { args.windows(2).find(|w| w[0] == k).map(|w| w[1].clone()).unwrap_or_else(|| d.to_string()) }; + let seed_start: u64 = get("--seed-start", "2").parse().unwrap(); + let seeds: u64 = get("--seeds", "1000").parse().unwrap(); + let threads: usize = get("--threads", "32").parse().unwrap(); + let force_exhaust = args.iter().any(|a| a == "--force-exhaust"); + let out = get("--out", ""); + println!("adv-accept attempts: seeds {seed_start}..{} ({seeds}), {threads} threads, cap {MAX_ATTEMPTS_V4}, force-exhaust {force_exhaust}", seed_start + seeds); + let t0 = Instant::now(); + let next = AtomicUsize::new(0); + // per attempt index (0..=MAX): counts of each reject class; the accepted attempt histogram; exhaustions + const CLASSES: usize = 11; + fn class_of(r: &Reject) -> usize { + match r { + Reject::StaleLoadSource { .. } => 0, + Reject::NoInjectingWrite { .. } => 1, + Reject::UnfreshLoadSource { .. } => 2, + Reject::ConstantBit { .. } => 3, + Reject::LaneConstantSite { .. } => 4, + Reject::Saturated { .. } => 5, + Reject::SaturatedSource { .. } => 6, + Reject::RepeatedSource { .. } => 7, + Reject::LowEntropySite { .. } => 8, + Reject::OutputBias { .. } => 9, + Reject::DistinctAddresses { .. } => 10, + } + } + const NAMES: [&str; CLASSES] = ["(a) stale", "(b) no-inject", "(a') unfresh", "(c) const-bit", "(c) lane-const", "(c) saturated", "(c') sat-source", "(c'') repeated", "(c'') low-entropy", "(c) bias", "(c) distinct"]; + let parts: Vec<(Vec<[u64; CLASSES]>, Vec, u64, Vec, Vec<(u64, u32, u64, f64)>)> = std::thread::scope(|sc| { + let mut hs = Vec::new(); + for _ in 0..threads { + let next = &next; + hs.push(sc.spawn(move || { + let mut by_attempt = vec![[0u64; CLASSES]; MAX_ATTEMPTS_V4 as usize + 1]; + let mut accepted_at = vec![0u64; MAX_ATTEMPTS_V4 as usize + 1]; + let mut exhausted = 0u64; + let mut notes = Vec::new(); + let mut rows = Vec::new(); + loop { + let i = next.fetch_add(1, Ordering::Relaxed) as u64; + if i >= seeds { break; } + let k = seed_start + i; + let epoch = seed_bytes("epoch", k); + let era = seed_bytes("era", k); + let class = LoadClass::era(V4_CLASS, &era, &V3_ALLOWED); + let label = format!("igneum-attack-f8/{k}"); + let mut done = false; + let cap = if force_exhaust { 8 } else { MAX_ATTEMPTS_V4 }; // under the plant a short cap shows the path + for a in 0..cap { + let mut p = candidate_class(&label, &epoch, a, class); + p.generator = GENERATOR_VERSION_V4; + match check(&p) { + Ok(r) if !force_exhaust => { + accepted_at[a as usize] += 1; + rows.push((k, a, p.program_id(), r.distinct_mean())); + done = true; + break; + } + Ok(_) => { by_attempt[a as usize][3] += 0; } // forced: counted as a rejection of no class + Err(r) => { by_attempt[a as usize][class_of(&r)] += 1; } + } + } + if !done { + exhausted += 1; + let mut lr = candidate_class(&label, &epoch, cap, class); + lr.generator = GENERATOR_VERSION_V4; + let lr = last_resort_v4(lr); + let v = check(&lr); + let (lossy_last, n_or, n_mul, n_mulhi, regs) = structure(&lr); + notes.push(format!("seed {k}: EXHAUSTED at cap {cap}; last resort id {:016x} attempt {} op mix {} shadow mix {}; rule verdict on it {:?}; structure lossy_last {lossy_last} or {n_or} mul {n_mul} mulhi {n_mulhi} regs {regs}", lr.program_id(), lr.attempt, lr.op_mix(), lr.shadow_op_mix(), v.as_ref().map(|r| format!("accept distinct_mean {:.3} sat {} bias {}", r.distinct_mean(), r.saturated, r.bias_max)).map_err(|e| e.to_string()))); + } + } + (by_attempt, accepted_at, exhausted, notes, rows) + })); + } + hs.into_iter().map(|h| h.join().unwrap()).collect() + }); + let mut by_attempt = vec![[0u64; CLASSES]; MAX_ATTEMPTS_V4 as usize + 1]; + let mut accepted_at = vec![0u64; MAX_ATTEMPTS_V4 as usize + 1]; + let mut exhausted = 0u64; + let mut rows = Vec::new(); + for (b, acc, ex, notes, r) in parts { + for a in 0..b.len() { for c in 0..CLASSES { by_attempt[a][c] += b[a][c]; } accepted_at[a] += acc[a]; } + exhausted += ex; + for n in notes { println!("{n}"); } + rows.extend(r); + } + let n = rows.len() as u64 + exhausted; + let total_rej: [u64; CLASSES] = { let mut t = [0u64; CLASSES]; for a in &by_attempt { for c in 0..CLASSES { t[c] += a[c]; } } t }; + let rejections: u64 = total_rej.iter().sum(); + println!("attempts: {n} seeds in {:.0} s; {rejections} rejected candidates, {} accepted, {exhausted} exhausted (cap {})", t0.elapsed().as_secs_f64(), rows.len(), MAX_ATTEMPTS_V4); + println!("rejection by part over all candidates: {}", NAMES.iter().zip(total_rej.iter()).map(|(nm, c)| format!("{nm}={c} ({:.1}%)", *c as f64 * 100.0 / rejections.max(1) as f64)).collect::>().join(" ")); + let max_a = accepted_at.iter().rposition(|&c| c > 0).unwrap_or(0); + println!("accepted-attempt histogram 0..{max_a}: {:?}", &accepted_at[..=max_a]); + for a in 0..=max_a.min(3) { + let tot: u64 = by_attempt[a].iter().sum::() + accepted_at[a]; + println!("attempt {a}: {tot} candidates, rejected {} ({:.1}%): {}", by_attempt[a].iter().sum::(), by_attempt[a].iter().sum::() as f64 * 100.0 / tot.max(1) as f64, NAMES.iter().zip(by_attempt[a].iter()).filter(|(_, c)| **c > 0).map(|(nm, c)| format!("{nm}={c}")).collect::>().join(" ")); + } + let mean_a = accepted_at.iter().enumerate().map(|(a, &c)| a as f64 * c as f64).sum::() / rows.len().max(1) as f64; + let p_rej = rejections as f64 / (rejections + rows.len() as u64) as f64; + println!("mean accepted attempt {mean_a:.3}; per-candidate rejection probability {p_rej:.4}; P(256 consecutive rejections) if independent = {:.3e}", p_rej.powi(256)); + if rows.len() > 0 { + let dm: Vec = rows.iter().map(|r| r.3).collect(); + println!("closed-form distinct-item mean of accepted programs: mean {:.4} min {:.4}", dm.iter().sum::() / dm.len() as f64, dm.iter().cloned().fold(f64::MAX, f64::min)); + } + if !out.is_empty() { + let mut f = std::fs::File::create(&out).unwrap(); + writeln!(f, "# seed accepted_attempt program_id distinct_mean").unwrap(); + for (k, a, id, d) in &rows { writeln!(f, "{k} {a} {id:016x} {d:.5}").unwrap(); } + println!("wrote {out}"); + } + println!("attempts DONE"); +}