diff --git a/tools/attack/adv-mixer/src/main.rs b/tools/attack/adv-mixer/src/main.rs index 306283a6c..3c5bd0c61 100644 --- a/tools/attack/adv-mixer/src/main.rs +++ b/tools/attack/adv-mixer/src/main.rs @@ -238,76 +238,49 @@ fn xor16(a: &[u32; 16], b: &[u32; 16]) -> [u32; 16] { o } -fn fold(day: u64, trials: u64) { - let mp = params_of_day(day); - let keys = app_keys(); - let rk1 = keys[0]; - let rk2 = keys[1]; - let mut rng = Rng::new(0xfeed_face_cafe_babe ^ day.wrapping_mul(0x9E3779B97F4A7C15)); - - // (a) GF(2) affinity of the two-application map g(s) = M(M(s,rk1),rk2). An affine map over GF(2)^512 obeys - // g(a) ^ g(b) ^ g(c) ^ g(a^b^c) = g(0^...) summed; exactly, g(a)^g(b)^g(c)^g(a^b^c) is constant for an - // affine g. We test the 4-point relation g(a)^g(b)^g(c)^g(a^b^c) == g(d0)^g(d0)... using the zero anchor: - // for affine g, g(a)^g(b)^g(c)^g(a^b^c) == g(0) (four points a,b,c,a^b^c vs the origin). Count nonzero. - let g = |s: [u32; 16]| -> [u32; 16] { - let s1 = apply_n(s, &mp, &keys, 0, 1); - apply_n(s1, &mp, &keys, 1, 1) - }; +/// Probe (a) affinity and (c) key-order commutation on the adjacent application pair (i, i+1). +fn fold_pair(mp: &MixParams, keys: &[u32; 72], i: usize, trials: u64, rng: &mut Rng) -> (u64, u64) { + let (rk1, rk2) = (keys[i], keys[i + 1]); + let g = |s: [u32; 16]| -> [u32; 16] { let mut t = s; mixer(&mut t, rk1, mp); mixer(&mut t, rk2, mp); t }; let g0 = g([0u32; 16]); - let mut affine_violations = 0u64; + let mut violations = 0u64; + let mut agree = 0u64; for _ in 0..trials { - let a = rng.state(); - let b = rng.state(); - let c = rng.state(); + let a = rng.state(); let b = rng.state(); let c = rng.state(); let abc = xor16(&xor16(&a, &b), &c); let lhs = xor16(&xor16(&g(a), &g(b)), &xor16(&g(c), &g(abc))); - if lhs != g0 { - affine_violations += 1; - } + if lhs != g0 { violations += 1; } + let s = rng.state(); + let mut s1 = s; mixer(&mut s1, rk2, mp); mixer(&mut s1, rk1, mp); + if g(s) == s1 { agree += 1; } } + (violations, agree) +} - // (b) word separability: flip each input word fully (xor 0xffffffff) and see whether every output word of the - // full 8-application block moves on at least one probe. A dead (in_word -> out_word) pair over all probes - // is a broken dependency a shortcut could exploit. - let full = |s: [u32; 16]| apply_n(s, &mp, &keys, 0, 8); - let mut dep = [[false; 16]; 16]; // dep[iw][ow] = out word ow ever changed when in word iw flipped +/// Probe (b) word separability over the full 8-application block of round 0. +fn fold_block(mp: &MixParams, keys: &[u32; 72], trials: u64, rng: &mut Rng) -> u64 { + let full = |s: [u32; 16]| apply_n(s, mp, keys, 0, 8); + let mut dep = [[false; 16]; 16]; for _ in 0..trials { let base = rng.state(); let o0 = full(base); for iw in 0..16 { - let mut s = base; - s[iw] ^= 0xffff_ffff; + let mut s = base; s[iw] ^= 0xffff_ffff; let o1 = full(s); - for ow in 0..16 { - if o0[ow] != o1[ow] { - dep[iw][ow] = true; - } - } - } - } - let mut dead_pairs = 0u64; - for iw in 0..16 { - for ow in 0..16 { - if !dep[iw][ow] { - dead_pairs += 1; - } - } - } - - // (c) key-order commutation: M(M(s,rk1),rk2) vs M(M(s,rk2),rk1). Agreement would let keys fold. - let mut commute_agree = 0u64; - for _ in 0..trials { - let s = rng.state(); - let ab = apply_n(apply_n(s, &mp, &keys, 0, 1), &mp, &keys, 1, 1); - // apply rk2 then rk1 by temporarily swapping via explicit keys - let mut s1 = s; - mixer(&mut s1, rk2, &mp); - mixer(&mut s1, rk1, &mp); - if ab == s1 { - commute_agree += 1; + for ow in 0..16 { if o0[ow] != o1[ow] { dep[iw][ow] = true; } } } } + let mut dead = 0u64; + for iw in 0..16 { for ow in 0..16 { if !dep[iw][ow] { dead += 1; } } } + dead +} +fn fold(day: u64, trials: u64) { + let mp = params_of_day(day); + let keys = app_keys(); + let mut rng = Rng::new(0xfeed_face_cafe_babe ^ day.wrapping_mul(0x9E3779B97F4A7C15)); + let (affine_violations, commute_agree) = fold_pair(&mp, &keys, 0, trials, &mut rng); + let dead_pairs = fold_block(&mp, &keys, trials, &mut rng); println!("fold day={} trials={}", day, trials); println!(" (a) GF(2) affinity violations of the 2-application map: {} of {} (0 would be a BREAK: the map is affine)", affine_violations, trials); println!(" (b) dead (in_word -> out_word) pairs over the full 8-application block: {} of 256 (any would be a broken dependency)", dead_pairs); @@ -316,6 +289,46 @@ fn fold(day: u64, trials: u64) { println!(" VERDICT: {}", verdict); } +/// The fold probes over `days` consecutive chain days and all 71 adjacent application-key pairs per day. +fn fold_sweep(day0: u64, days: u64, trials: u64, threads: usize) { + let keys = Arc::new(app_keys()); + let per = (days + threads as u64 - 1) / threads as u64; + let mut handles = Vec::new(); + for t in 0..threads { + let keys = Arc::clone(&keys); + let lo = day0 + t as u64 * per; + let hi = (lo + per).min(day0 + days); + handles.push(thread::spawn(move || { + let (mut viol, mut tot, mut agree, mut dead, mut n_days) = (0u64, 0u64, 0u64, 0u64, 0u64); + let mut worst_viol_frac = 1.0f64; + for d in lo..hi { + let mp = params_of_day(d); + let mut rng = Rng::new(0xfeed_face_cafe_babe ^ d.wrapping_mul(0x9E3779B97F4A7C15)); + for i in 0..71 { + let (v, a) = fold_pair(&mp, &keys, i, trials, &mut rng); + viol += v; tot += trials; agree += a; + let f = v as f64 / trials as f64; + if f < worst_viol_frac { worst_viol_frac = f; } + } + dead += fold_block(&mp, &keys, trials, &mut rng); + n_days += 1; + } + (viol, tot, agree, dead, n_days, worst_viol_frac) + })); + } + let (mut viol, mut tot, mut agree, mut dead, mut n_days) = (0u64, 0u64, 0u64, 0u64, 0u64); + let mut worst = 1.0f64; + for h in handles { + let (v, t, a, dd, nd, w) = h.join().unwrap(); + viol += v; tot += t; agree += a; dead += dd; n_days += nd; if w < worst { worst = w; } + } + println!("fold-sweep from_day={} days={} pairs_per_day=71 trials_per_pair={}", day0, n_days, trials); + println!(" (a) affinity violations: {} of {} pair-trials; lowest per-pair violation fraction {:.6} (1.0 = never affine)", viol, tot, worst); + println!(" (b) dead word pairs summed over {} days: {} (0 = complete dependency every day)", n_days, dead); + println!(" (c) key-order agreements: {} of {} pair-trials", agree, tot); + println!(" VERDICT: {}", if viol < tot || dead > 0 || agree > 0 { "FINDING" } else { "BOUND: no fold on any day or pair probed" }); +} + // -------------------------------------------------------------------------------------------------------------- // integral (Q1): algebraic-degree saturation across K keyed applications // -------------------------------------------------------------------------------------------------------------- @@ -399,6 +412,333 @@ fn integral(day: u64, apps: usize, dmax: usize, placements: u64, threads: usize) 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"); } +// -------------------------------------------------------------------------------------------------------------- +// linrel (Q1): exact affine-relation search over the full 32-bit map by GF(2) elimination +// -------------------------------------------------------------------------------------------------------------- +// +// Collect N random (x, y = K applications of x) pairs. Each pair is a GF(2) row over the 1025 columns +// [512 input bits | 512 output bits | 1]. Any nonzero v with A v = 0 is an exact affine relation +// a.x XOR b.y = c that holds on every sample; with N far above 1025 a surviving relation is genuine (a chance +// survivor has probability 2^-(N - 1025)). Kernel dimension 1025 - rank(A). Rank 1025 means NO affine relation +// exists between the input bits and the output bits of the composed applications, at full width with the real +// day constants. This is the decidable algebraic probe in place of a SAT solve (no solver reaches the box). +// `--addr` restricts the output columns to the 22 line-index bits of s[0] (the bits a chip prefetches on). + +const LR_IN: usize = 512; + +fn gf2_rank(rows: &mut Vec>, ncols: usize) -> usize { + let words = (ncols + 63) / 64; + let mut rank = 0usize; + let mut r = 0usize; + for col in 0..ncols { + let (w, b) = (col / 64, col % 64); + let mut piv = None; + for i in r..rows.len() { + if (rows[i][w] >> b) & 1 == 1 { + piv = Some(i); + break; + } + } + let Some(p) = piv else { continue }; + rows.swap(r, p); + let pr = rows[r].clone(); + for i in 0..rows.len() { + if i != r && (rows[i][w] >> b) & 1 == 1 { + for k in 0..words { + rows[i][k] ^= pr[k]; + } + } + } + rank += 1; + r += 1; + if r == rows.len() { + break; + } + } + rank +} + +fn linrel(day: u64, apps: usize, samples: usize, addr_only: bool) { + let mp = params_of_day(day); + let keys = app_keys(); + let mut rng = Rng::new(0x5a5a_1234_9e37_79b9 ^ day.wrapping_mul(0x9E3779B97F4A7C15) ^ ((apps as u64) << 48)); + let nout = if addr_only { 22 } else { 512 }; + let ncols = LR_IN + nout + 1; + let words = (ncols + 63) / 64; + let mut rows: Vec> = Vec::with_capacity(samples); + for _ in 0..samples { + let x = rng.state(); + let y = apply_n(x, &mp, &keys, 0, apps); + let mut row = vec![0u64; words]; + for b in 0..LR_IN { + if bit_of(&x, b) == 1 { + row[b / 64] |= 1u64 << (b % 64); + } + } + for b in 0..nout { + if bit_of(&y, b) == 1 { + let c = LR_IN + b; + row[c / 64] |= 1u64 << (c % 64); + } + } + let c = LR_IN + nout; + row[c / 64] |= 1u64 << (c % 64); + rows.push(row); + } + let rank = gf2_rank(&mut rows, ncols); + let kernel = ncols - rank; + println!( + "linrel day={} apps={} samples={} columns={} ({} in + {} out + 1) rank={} kernel_dim={}", + day, apps, samples, ncols, LR_IN, nout, rank, kernel + ); + let margin = samples as i64 - ncols as i64; + if kernel == 0 { + println!(" BOUND: no exact affine relation a.x XOR b.y = c over the {} samples (false-survivor odds 2^-{})", samples, margin); + } else { + println!(" FINDING: {} independent affine relations survive all {} samples (chance survivor odds 2^-{} each)", kernel, samples, margin); + } +} + +// -------------------------------------------------------------------------------------------------------------- +// lineindex (Q1): the 22 line-index bits of s[0] across applications, avalanche with a tight band +// -------------------------------------------------------------------------------------------------------------- +// +// The cache read uses s[0] AND (2^22 - 1). A chip that could predict those 22 bits from fewer than K +// applications could issue the read early and hide the mixer behind the memory latency. We tally the flip +// probability of each of the 22 address bits for each of the 512 input bits over N states after K applications, +// report holes and cells beyond 8 sigma, and the worst cell. + +fn lineindex(day: u64, apps: usize, states: u64, threads: usize) { + let mp = Arc::new(params_of_day(day)); + let keys = Arc::new(app_keys()); + let per = states / 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 { states - per * (threads as u64 - 1) } else { per }; + let seed = 0x7777_0000_abcd_ef01 ^ day.wrapping_mul(0x9E3779B97F4A7C15) ^ ((apps as u64) << 40) ^ (t as u64 + 1); + handles.push(thread::spawn(move || { + let mut rng = Rng::new(seed); + let mut counts = vec![0u64; 512 * 22]; + for _ in 0..count { + let base = rng.state(); + let o0 = apply_n(base, &mp, &keys, 0, apps)[0] & 0x003f_ffff; + for ib in 0..512 { + let mut s = base; + flip_bit(&mut s, ib); + let o1 = apply_n(s, &mp, &keys, 0, apps)[0] & 0x003f_ffff; + let d = o0 ^ o1; + for ab in 0..22 { + if (d >> ab) & 1 == 1 { + counts[ib * 22 + ab] += 1; + } + } + } + } + (counts, count) + })); + } + let mut counts = vec![0u64; 512 * 22]; + let mut n = 0u64; + for h in handles { + let (c, k) = h.join().unwrap(); + for (a, b) in counts.iter_mut().zip(c.iter()) { + *a += b; + } + n += k; + } + let nf = n as f64; + let sigma = 0.5 / nf.sqrt(); + let band = 8.0 * sigma; + let (mut holes, mut strong, mut worst_dev, mut worst) = (0u64, 0u64, 0.0f64, (0usize, 0usize, 0.0f64)); + let mut total = 0u64; + for ib in 0..512 { + for ab in 0..22 { + let c = counts[ib * 22 + ab]; + total += c; + let p = c as f64 / nf; + if c == 0 || c == n { + holes += 1; + } else { + let dev = (p - 0.5).abs(); + if dev > band { + strong += 1; + } + if dev > worst_dev { + worst_dev = dev; + worst = (ib, ab, p); + } + } + } + } + println!("lineindex day={} apps={} states={} threads={} (22 address bits x 512 input bits = 11264 cells)", day, apps, n, threads); + println!(" mean address-bit flip probability: {:.6} (ideal 0.5); band 8 sigma = {:.6}", total as f64 / (nf * 11264.0), band); + println!(" holes: {} of 11264; strong-bias cells: {} of 11264", holes, strong); + println!(" worst cell: in_bit {} -> addr_bit {} p = {:.6} ({:.1} sigma)", worst.0, worst.1, worst.2, worst_dev / sigma); + println!(" VERDICT: {}", if holes > 0 || strong > 0 { "FINDING (address bits predictable at this K)" } else { "no address-bit distinguisher at this K" }); +} + +// -------------------------------------------------------------------------------------------------------------- +// cnf (Q1): DIMACS export of two keyed applications with the real day constants, the commutation instance +// -------------------------------------------------------------------------------------------------------------- +// +// Bit-exact Tseitin encoding of M(M(x, rk1), rk2) and M(M(x, rk2), rk1) on a shared 512-variable input, with the +// constraint that the two outputs are equal. SAT = a state on which the two key orders commute; UNSAT = none +// exists (a proof of fold probe (c) over all 2^512 states). Adds are ripple-carry full adders, a constant +// multiply is the shift-add chain over the set bits of MUL, a constant XOR is a literal flip, a rotation is +// wiring. No solver reaches the box (crates.io is refused and none is installed), so this writes the model for a +// solver elsewhere and the row stays BLOCKED on the solve. + +struct Cnf { + nvars: i32, + clauses: Vec>, +} +const LTRUE: i32 = i32::MAX; +const LFALSE: i32 = i32::MIN + 1; +impl Cnf { + fn new() -> Self { + Cnf { nvars: 0, clauses: Vec::new() } + } + fn var(&mut self) -> i32 { + self.nvars += 1; + self.nvars + } + fn neg(l: i32) -> i32 { + if l == LTRUE { LFALSE } else if l == LFALSE { LTRUE } else { -l } + } + fn xor(&mut self, a: i32, b: i32) -> i32 { + if a == LFALSE { return b; } + if b == LFALSE { return a; } + if a == LTRUE { return Self::neg(b); } + if b == LTRUE { return Self::neg(a); } + let o = self.var(); + self.clauses.push(vec![-a, -b, -o]); + self.clauses.push(vec![a, b, -o]); + self.clauses.push(vec![a, -b, o]); + self.clauses.push(vec![-a, b, o]); + o + } + fn and(&mut self, a: i32, b: i32) -> i32 { + if a == LFALSE || b == LFALSE { return LFALSE; } + if a == LTRUE { return b; } + if b == LTRUE { return a; } + let o = self.var(); + self.clauses.push(vec![-o, a]); + self.clauses.push(vec![-o, b]); + self.clauses.push(vec![o, -a, -b]); + o + } + fn or(&mut self, a: i32, b: i32) -> i32 { + if a == LTRUE || b == LTRUE { return LTRUE; } + if a == LFALSE { return b; } + if b == LFALSE { return a; } + let o = self.var(); + self.clauses.push(vec![o, -a]); + self.clauses.push(vec![o, -b]); + self.clauses.push(vec![-o, a, b]); + o + } + /// 32-bit ripple-carry add of two literal words. + fn add32(&mut self, a: &[i32; 32], b: &[i32; 32]) -> [i32; 32] { + let mut out = [LFALSE; 32]; + let mut carry = LFALSE; + for i in 0..32 { + let t = self.xor(a[i], b[i]); + out[i] = self.xor(t, carry); + let c1 = self.and(a[i], b[i]); + let c2 = self.and(t, carry); + carry = self.or(c1, c2); + } + out + } + fn xor32(&mut self, a: &[i32; 32], b: &[i32; 32]) -> [i32; 32] { + let mut o = [LFALSE; 32]; + for i in 0..32 { o[i] = self.xor(a[i], b[i]); } + o + } + fn const32(v: u32) -> [i32; 32] { + let mut o = [LFALSE; 32]; + for i in 0..32 { o[i] = if (v >> i) & 1 == 1 { LTRUE } else { LFALSE }; } + o + } + fn rotl32(a: &[i32; 32], n: u32) -> [i32; 32] { + let mut o = [LFALSE; 32]; + for i in 0..32 { o[((i as u32 + n) % 32) as usize] = a[i]; } + o + } + fn shl32(a: &[i32; 32], n: u32) -> [i32; 32] { + let mut o = [LFALSE; 32]; + for i in 0..32 { if i as u32 + n < 32 { o[(i as u32 + n) as usize] = a[i]; } } + o + } + /// Multiply by a constant: shift-add over the set bits of `c`. + fn mulc32(&mut self, a: &[i32; 32], c: u32) -> [i32; 32] { + let mut acc: Option<[i32; 32]> = None; + for j in 0..32 { + if (c >> j) & 1 == 1 { + let sh = Self::shl32(a, j); + acc = Some(match acc { None => sh, Some(p) => self.add32(&p, &sh) }); + } + } + acc.unwrap_or(Self::const32(0)) + } + fn qr(&mut self, s: &mut [[i32; 32]; 16], a: usize, b: usize, c: usize, d: usize, r: [u32; 4]) { + s[a] = self.add32(&s[a], &s[b]); s[d] = self.xor32(&s[d], &s[a]); s[d] = Self::rotl32(&s[d], r[0]); + s[c] = self.add32(&s[c], &s[d]); s[b] = self.xor32(&s[b], &s[c]); s[b] = Self::rotl32(&s[b], r[1]); + s[a] = self.add32(&s[a], &s[b]); s[d] = self.xor32(&s[d], &s[a]); s[d] = Self::rotl32(&s[d], r[2]); + s[c] = self.add32(&s[c], &s[d]); s[b] = self.xor32(&s[b], &s[c]); s[b] = Self::rotl32(&s[b], r[3]); + } + /// One mixer application, symbolic, the same wiring as memhard::mixer. + fn mixer(&mut self, s: &mut [[i32; 32]; 16], rk: u32, mp: &MixParams) { + for i in 0..16 { + let k = Self::const32(mp.rc[i].wrapping_add(rk)); + let x = self.xor32(&s[i], &k); + s[i] = self.mulc32(&x, mp.mul[i]); + } + let r = &mp.rot; + let col = [r[0], r[1], r[2], r[3]]; + let dia = [r[4], r[5], r[6], r[7]]; + self.qr(s, 0, 4, 8, 12, col); self.qr(s, 1, 5, 9, 13, col); self.qr(s, 2, 6, 10, 14, col); self.qr(s, 3, 7, 11, 15, col); + self.qr(s, 0, 5, 10, 15, dia); self.qr(s, 1, 6, 11, 12, dia); self.qr(s, 2, 7, 8, 13, dia); self.qr(s, 3, 4, 9, 14, dia); + } + fn assert_eq_lit(&mut self, a: i32, b: i32) { + let is_const = |l: i32| l == LTRUE || l == LFALSE; + match (a, b) { + (LTRUE, LTRUE) | (LFALSE, LFALSE) => {} + (x, y) if is_const(x) && is_const(y) => self.clauses.push(vec![]), // constant mismatch: UNSAT + (LTRUE, x) | (x, LTRUE) => self.clauses.push(vec![x]), + (LFALSE, x) | (x, LFALSE) => self.clauses.push(vec![-x]), + _ => { self.clauses.push(vec![-a, b]); self.clauses.push(vec![a, -b]); } + } + } +} + +fn cnf_export(day: u64, path: &str) { + let mp = params_of_day(day); + let keys = app_keys(); + let (rk1, rk2) = (keys[0], keys[1]); + let mut c = Cnf::new(); + let mut x = [[LFALSE; 32]; 16]; + for w in 0..16 { for b in 0..32 { x[w][b] = c.var(); } } + let mut s1 = x; c.mixer(&mut s1, rk1, &mp); c.mixer(&mut s1, rk2, &mp); + let mut s2 = x; c.mixer(&mut s2, rk2, &mp); c.mixer(&mut s2, rk1, &mp); + for w in 0..16 { for b in 0..32 { c.assert_eq_lit(s1[w][b], s2[w][b]); } } + let mut out = String::new(); + let _ = writeln!(out, "c adv-mixer commutation instance: exists x with M(M(x,rk1),rk2) == M(M(x,rk2),rk1), day {} rk1 {:#010x} rk2 {:#010x}", day, rk1, rk2); + let _ = writeln!(out, "c internal adversarial pass, not an independent review; frozen mixer 017e7037; input vars 1..512 = s[w] bit b at 1 + 32 w + b"); + let _ = writeln!(out, "p cnf {} {}", c.nvars, c.clauses.len()); + for cl in &c.clauses { + for &l in cl { let _ = write!(out, "{} ", l); } + let _ = writeln!(out, "0"); + } + std::fs::write(path, out).expect("write cnf"); + println!("cnf day={} vars={} clauses={} written {}", day, c.nvars, c.clauses.len(), path); + println!(" BLOCKED on the solve: no SAT solver reaches the box (crates.io refused, none installed); the model is the artefact"); +} + +use std::fmt::Write as _; + // -------------------------------------------------------------------------------------------------------------- // startup self-check: the genesis test vector of the spec // -------------------------------------------------------------------------------------------------------------- @@ -449,10 +789,32 @@ fn main() { 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); + let days = arg_u64(&args, "--days", 1); + for d in day..day + days { integral(d, apps, dmax, placements, threads); } + } + "fold-sweep" => { + let trials = arg_u64(&args, "--trials", 20_000); + let days = arg_u64(&args, "--days", 64); + fold_sweep(day, days, trials, threads); + } + "linrel" => { + let apps = arg_u64(&args, "--apps", 2) as usize; + let samples = arg_u64(&args, "--samples", 8192) as usize; + let addr = args.iter().any(|a| a == "--addr"); + let days = arg_u64(&args, "--days", 1); + for d in day..day + days { linrel(d, apps, samples, addr); } + } + "lineindex" => { + let apps = arg_u64(&args, "--apps", 2) as usize; + let states = arg_u64(&args, "--states", 500_000); + lineindex(day, apps, states, threads); + } + "cnf" => { + let path = arg_str(&args, "--out", "adv-mixer-commute.cnf").to_string(); + cnf_export(day, &path); } _ => { - 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]"); + eprintln!("usage: attack-adv-mixer diffusion|fold|fold-sweep|integral|linrel|lineindex|cnf [--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);