From 29a03a0d744a7b831f4757e536c88312efaa9981 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:27:00 +0000 Subject: [PATCH] adv-mixer: integral degree-saturation test for Q1; note the re-scope in the plan Internal adversarial pass, not an independent review. Adds the cube-sum (higher-order differential) command to bound the algebraic degree of the applications: a low degree would admit a cheap polynomial batching of the 8 applications, so degree saturation at small d is the no-shortcut bound. Plan section 8 records main's three-lane re-scope: this lane is the algebraic structure of M_r. Co-Authored-By: Claude Fable 5.1 --- docs/plans/cryptanalysis/plan-mixer.md | 11 ++++ tools/attack/adv-mixer/src/main.rs | 91 +++++++++++++++++++++++++- 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/docs/plans/cryptanalysis/plan-mixer.md b/docs/plans/cryptanalysis/plan-mixer.md index c9749b7ba..53adbe834 100644 --- a/docs/plans/cryptanalysis/plan-mixer.md +++ b/docs/plans/cryptanalysis/plan-mixer.md @@ -213,3 +213,14 @@ Only these were read. Nothing else in the repository. - An algebraic-degree or integral-distinguisher measurement across the 8 applications, to tighten Q1 beyond the affinity probe. - The census at more than 2^24 days if any class sits near the gate. + +## 8. Re-scope (19:2x BST, 7 October 2026, from main) + +The mixer work split into three lanes. This lane, adv-mixer, is narrowed to the algebraic structure of M_r: the +fold or commutation of the multiply layer across applications (only rk changes), the algebraic form of the 8 +applications between two reads, and any composition cheaper than 8x one application, priced in ops per item +against the chip model. Sibling adv-mixer-2 owns Q3 (the day-key weakness census and the calendar). Sibling +adv-mixer-3 owns Q2 (differential, linear, rotational-XOR, SAT and MILP on reduced applications, the round +margin). The Q3 census and the Q2 diffusion sweep already run in this lane are kept and recorded as courtesy +runs, attributed to the owning lane. This lane's own deepening is the fold probe plus an algebraic-degree +(integral cube-sum) saturation test across the applications. First results by 00:00 BST tonight. diff --git a/tools/attack/adv-mixer/src/main.rs b/tools/attack/adv-mixer/src/main.rs index 229be22cc..306283a6c 100644 --- a/tools/attack/adv-mixer/src/main.rs +++ b/tools/attack/adv-mixer/src/main.rs @@ -316,6 +316,89 @@ fn fold(day: u64, trials: u64) { println!(" VERDICT: {}", verdict); } +// -------------------------------------------------------------------------------------------------------------- +// integral (Q1): algebraic-degree saturation across K keyed applications +// -------------------------------------------------------------------------------------------------------------- +// +// The cube-sum (higher-order differential) test. Pick d input-bit positions. Over a fixed random base state, XOR +// every one of the 2^d subsets of those positions into the base, apply K applications, and XOR-accumulate the 16 +// output words. For an output bit that is a GF(2) polynomial of the input of degree < d, the sum over the full +// d-cube is 0 (the d-th derivative of a degree < d polynomial is 0). A nonzero sum proves the output bit has +// algebraic degree >= d in those d variables. We report, per d, the fraction of (base, cube) placements whose +// accumulated 512-bit sum is nonzero on at least one output bit, and the mean number of output bits set. When +// nonzero sums appear at small d the algebraic degree is already high, so no low-degree algebraic batching of the +// applications exists: an attacker cannot evaluate the 8 applications through a cheap polynomial. A composition +// cheaper than 8x would need a low-degree form; its absence is the bound, priced against 9,360 ops per item. + +fn integral(day: u64, apps: usize, dmax: usize, placements: u64, threads: usize) { + let mp = Arc::new(params_of_day(day)); + let keys = Arc::new(app_keys()); + println!("integral day={} apps={} placements_per_d={} threads={}", day, apps, placements, threads); + println!(" d = cube dimension; 'nonzero' = placements whose cube-sum is nonzero on >=1 output bit (degree >= d reached)"); + for d in 1..=dmax { + let per = placements / threads as u64; + let mut handles = Vec::new(); + for t in 0..threads { + let mp = Arc::clone(&mp); + let keys = Arc::clone(&keys); + let count = if t as u64 == threads as u64 - 1 { placements - per * (threads as u64 - 1) } else { per }; + let seed = 0xa1b2_c3d4_e5f6_0718 ^ day.wrapping_mul(0x9E3779B97F4A7C15) ^ ((d as u64) << 40) ^ (t as u64 + 1); + handles.push(thread::spawn(move || { + let mut rng = Rng::new(seed); + let mut nonzero = 0u64; + let mut bits_set_total = 0u64; + for _ in 0..count { + let base = rng.state(); + // choose d distinct input bit positions in 0..512 + let mut pos = [0usize; 32]; + let mut chosen = 0usize; + while chosen < d { + let b = (rng.0.next() % 512) as usize; + if !pos[..chosen].contains(&b) { + pos[chosen] = b; + chosen += 1; + } + } + let mut acc = [0u32; 16]; + for mask in 0u32..(1u32 << d) { + let mut s = base; + for (bit, &p) in pos[..d].iter().enumerate() { + if (mask >> bit) & 1 == 1 { + flip_bit(&mut s, p); + } + } + let o = apply_n(s, &mp, &keys, 0, apps); + for i in 0..16 { + acc[i] ^= o[i]; + } + } + let set: u32 = acc.iter().map(|w| w.count_ones()).sum(); + bits_set_total += set as u64; + if set > 0 { + nonzero += 1; + } + } + (nonzero, bits_set_total, count) + })); + } + let (mut nonzero, mut bits, mut n) = (0u64, 0u64, 0u64); + for h in handles { + let (a, b, c) = h.join().unwrap(); + nonzero += a; + bits += b; + n += c; + } + let frac = nonzero as f64 / n as f64; + let mean_bits = bits as f64 / n as f64; + let note = if nonzero == 0 { "sum always 0: degree < d on every output bit (a low-degree relation)" } else { "degree >= d reached" }; + println!( + " d={:2} nonzero {}/{} = {:.4} mean out-bits set {:.1}/512 [{}]", + d, nonzero, n, frac, mean_bits, note + ); + } + println!(" READING: the smallest d at which nonzero is ~1.0 is the degree floor; high degree at small d means no low-degree algebraic shortcut across the applications"); +} + // -------------------------------------------------------------------------------------------------------------- // startup self-check: the genesis test vector of the spec // -------------------------------------------------------------------------------------------------------------- @@ -362,8 +445,14 @@ fn main() { let trials = arg_u64(&args, "--trials", 100_000); fold(day, trials); } + "integral" => { + let apps = arg_u64(&args, "--apps", 2) as usize; + let dmax = arg_u64(&args, "--dmax", 14) as usize; + let placements = arg_u64(&args, "--placements", 20000); + integral(day, apps, dmax, placements, threads); + } _ => { - eprintln!("usage: attack-adv-mixer diffusion|fold [--day D] [--apps K] [--states N] [--start-app A] [--plant weak|none] [--trials N] [--threads T]"); + eprintln!("usage: attack-adv-mixer diffusion|fold|integral [--day D] [--apps K] [--states N] [--start-app A] [--plant weak|none] [--trials N] [--dmax D] [--placements N] [--threads T]"); } } let _ = AtomicU64::new(0).fetch_add(0, Ordering::Relaxed);