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 <noreply@anthropic.com>
This commit is contained in:
parent
5bddb289c3
commit
29a03a0d74
2 changed files with 101 additions and 1 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue