From 42827d16eb43d701be997db4eaffa4aff3531608 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:56:03 +0000 Subject: [PATCH] adv-cache-3: harness crate (skip, relations, image, pebble, cross) Co-Authored-By: Claude Fable 5.1 --- tools/attack/adv-cache-3/Cargo.toml | 21 + tools/attack/adv-cache-3/src/main.rs | 1261 ++++++++++++++++++++++++++ 2 files changed, 1282 insertions(+) create mode 100644 tools/attack/adv-cache-3/Cargo.toml create mode 100644 tools/attack/adv-cache-3/src/main.rs diff --git a/tools/attack/adv-cache-3/Cargo.toml b/tools/attack/adv-cache-3/Cargo.toml new file mode 100644 index 00000000..94dbf242 --- /dev/null +++ b/tools/attack/adv-cache-3/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "attack-adv-cache-3" +version = "0.1.0" +edition = "2021" +description = "Adversarial lane adv-cache-3: the chain break or skip of the memory-hard cache (template skip search, GF(2) rank, dependence, feed-forward relations, exhaustive image census at small word size, the pebbling curve); igneum-pow by path, nothing re-implemented but the restated chain, which is checked against the library" +license = "MIT" +publish = false + +[[bin]] +name = "adv-cache-3" +path = "src/main.rs" + +[dependencies] +igneum-pow = { path = "../../../igneum-pow" } + +[workspace] + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 diff --git a/tools/attack/adv-cache-3/src/main.rs b/tools/attack/adv-cache-3/src/main.rs new file mode 100644 index 00000000..38a94248 --- /dev/null +++ b/tools/attack/adv-cache-3/src/main.rs @@ -0,0 +1,1261 @@ +//! adv-cache-3: the chain break or skip of the memory-hard cache (internal adversarial pass, not an independent +//! review). Spec 01 section 1.8.3: segment `s`, line `j`: `x_j = line_{j-1} XOR c_j`, `line_j = B(x_j) = C(x_j) + x_j`, +//! `c_j = sigma || K || s || j || tag`, `prev_0 = 0`. The real `B` is the library's `chacha_block`; the chain is +//! restated here and checked word for word against `Cache::fill_segment` on every run. A reduced block `B(w, r)` +//! (16 words of `w` bits, `r` double rounds, rotations reduced mod `w` and floored at 1, the same layout truncated) +//! is the small-scale model; `w = 32, r = 6` with the feed-forward is the real block and is asserted equal to the +//! library's on start. +//! +//! Commands (one per plan row): +//! adv-cache-3 check --day 20730 [--day2 20733] the cache fingerprints and the chain restatement +//! adv-cache-3 skip --day D --segments S [--lines 64] [--w 32] [--rounds 6] [--plant none|no-xor|no-feedforward] +//! template skip search, GF(2) rank, dependence tables, inversion attempt (Q1, Q2 partial knowledge) +//! adv-cache-3 relations --day0 D --days N --lines-log2 L [--w 32] [--rounds 6] [--plant ...] +//! per-bit biases of the feed-forward relations and the 512 x 512 linear-correlation table (Q2) +//! adv-cache-3 image --w 2 [--rounds 6] [--depth 64] [--plant no-feedforward] exhaustive image census (Q1 (4)) +//! adv-cache-3 pebble [--lines 64] [--exhaustive-upto 16] [--mc 1000000] [--plant skip-edge 8] the pebbling curve (Q3) +//! adv-cache-3 cross --day D --segments S [--w 32] [--rounds 6] [--plant ...] cross-segment and cross-day tables (Q4) +//! Every command takes --threads T (default: the available parallelism). + +use igneum_pow::bind::day_bytes; +use igneum_pow::memhard::{chacha_block, Cache, CACHE_LINES_PER_SEGMENT, CACHE_SEGMENTS, CACHE_TAG, CHACHA_SIGMA}; +use igneum_pow::seed::{seed_words_from_bytes, SplitMix64}; +use std::io::Write; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{Instant, SystemTime, UNIX_EPOCH}; + +const ROT: [u32; 4] = [16, 12, 8, 7]; + +fn utc_now() -> String { + let s = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + let (d, t) = (s / 86400, s % 86400); + let z = d as i64 + 719468; + let era = z.div_euclid(146097); + let doe = z.rem_euclid(146097); + let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; + let y = yoe + era * 400; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + let mp = (5 * doy + 2) / 153; + let dd = doy - (153 * mp + 2) / 5 + 1; + let mm = if mp < 10 { mp + 3 } else { mp - 9 }; + let yy = if mm <= 2 { y + 1 } else { y }; + format!("{yy:04}-{mm:02}-{dd:02}T{:02}:{:02}:{:02}Z", t / 3600, (t / 60) % 60, t % 60) +} + +macro_rules! log { + ($($arg:tt)*) => {{ + println!("[{}] {}", utc_now(), format!($($arg)*)); + std::io::stdout().flush().ok(); + }}; +} + +// ------------------------------------------------------------------------------------------------------------ +// The block, generic in word width and double-round count +// ------------------------------------------------------------------------------------------------------------ + +#[derive(Clone, Copy, Debug)] +struct Blk { + w: u32, + mask: u32, + dr: u32, + ff: bool, + rot: [u32; 4], +} + +impl Blk { + fn new(w: u32, dr: u32, ff: bool) -> Blk { + assert!((2..=32).contains(&w)); + let mask = if w == 32 { u32::MAX } else { (1u32 << w) - 1 }; + let mut rot = ROT; + if w < 32 { + for r in rot.iter_mut() { + *r %= w; + if *r == 0 { + *r = 1; + } + } + } + Blk { w, mask, dr, ff, rot } + } + #[inline(always)] + fn rotl(&self, x: u32, r: u32) -> u32 { + if self.w == 32 { + x.rotate_left(r) + } else { + ((x << r) | (x >> (self.w - r))) & self.mask + } + } + #[inline(always)] + fn rotr(&self, x: u32, r: u32) -> u32 { + if self.w == 32 { + x.rotate_right(r) + } else { + ((x >> r) | (x << (self.w - r))) & self.mask + } + } + #[inline(always)] + fn add(&self, a: u32, b: u32) -> u32 { + a.wrapping_add(b) & self.mask + } + #[inline(always)] + fn sub(&self, a: u32, b: u32) -> u32 { + a.wrapping_sub(b) & self.mask + } + #[inline(always)] + fn qr(&self, s: &mut [u32; 16], a: usize, b: usize, c: usize, d: usize) { + let r = self.rot; + s[a] = self.add(s[a], s[b]); + s[d] ^= s[a]; + s[d] = self.rotl(s[d], r[0]); + s[c] = self.add(s[c], s[d]); + s[b] ^= s[c]; + s[b] = self.rotl(s[b], r[1]); + s[a] = self.add(s[a], s[b]); + s[d] ^= s[a]; + s[d] = self.rotl(s[d], r[2]); + s[c] = self.add(s[c], s[d]); + s[b] ^= s[c]; + s[b] = self.rotl(s[b], r[3]); + } + #[inline(always)] + fn qr_inv(&self, s: &mut [u32; 16], a: usize, b: usize, c: usize, d: usize) { + let r = self.rot; + s[b] = self.rotr(s[b], r[3]); + s[b] ^= s[c]; + s[c] = self.sub(s[c], s[d]); + s[d] = self.rotr(s[d], r[2]); + s[d] ^= s[a]; + s[a] = self.sub(s[a], s[b]); + s[b] = self.rotr(s[b], r[1]); + s[b] ^= s[c]; + s[c] = self.sub(s[c], s[d]); + s[d] = self.rotr(s[d], r[0]); + s[d] ^= s[a]; + s[a] = self.sub(s[a], s[b]); + } + /// The permutation `C`. + fn core(&self, x: &[u32; 16]) -> [u32; 16] { + let mut y = *x; + for _ in 0..self.dr { + self.qr(&mut y, 0, 4, 8, 12); + self.qr(&mut y, 1, 5, 9, 13); + self.qr(&mut y, 2, 6, 10, 14); + self.qr(&mut y, 3, 7, 11, 15); + self.qr(&mut y, 0, 5, 10, 15); + self.qr(&mut y, 1, 6, 11, 12); + self.qr(&mut y, 2, 7, 8, 13); + self.qr(&mut y, 3, 4, 9, 14); + } + y + } + /// `C^-1`. + fn core_inv(&self, y: &[u32; 16]) -> [u32; 16] { + let mut x = *y; + for _ in 0..self.dr { + self.qr_inv(&mut x, 3, 4, 9, 14); + self.qr_inv(&mut x, 2, 7, 8, 13); + self.qr_inv(&mut x, 1, 6, 11, 12); + self.qr_inv(&mut x, 0, 5, 10, 15); + self.qr_inv(&mut x, 3, 7, 11, 15); + self.qr_inv(&mut x, 2, 6, 10, 14); + self.qr_inv(&mut x, 1, 5, 9, 13); + self.qr_inv(&mut x, 0, 4, 8, 12); + } + x + } + /// `B(x) = C(x) + x` (or `C(x)` without the feed-forward). + #[inline] + fn block(&self, x: &[u32; 16]) -> [u32; 16] { + let mut y = self.core(x); + if self.ff { + for i in 0..16 { + y[i] = self.add(y[i], x[i]); + } + } + y + } + fn bits(&self) -> usize { + 16 * self.w as usize + } +} + +fn xor16(a: &[u32; 16], b: &[u32; 16]) -> [u32; 16] { + let mut r = [0u32; 16]; + for i in 0..16 { + r[i] = a[i] ^ b[i]; + } + r +} + +fn eq16(a: &[u32; 16], b: &[u32; 16]) -> bool { + a.iter().zip(b.iter()).all(|(x, y)| x == y) +} + +/// Pack 16 words of `w` bits into a 512-bit vector (word `i` at bits `i w .. (i + 1) w`). +fn pack(x: &[u32; 16], w: u32) -> [u64; 8] { + let mut out = [0u64; 8]; + if w == 32 { + for i in 0..8 { + out[i] = x[2 * i] as u64 | ((x[2 * i + 1] as u64) << 32); + } + return out; + } + let mut pos = 0usize; + for i in 0..16 { + for b in 0..w as usize { + if (x[i] >> b) & 1 == 1 { + out[pos >> 6] |= 1u64 << (pos & 63); + } + pos += 1; + } + } + out +} + +#[inline(always)] +fn bit(v: &[u64; 8], i: usize) -> u32 { + ((v[i >> 6] >> (i & 63)) & 1) as u32 +} + +// ------------------------------------------------------------------------------------------------------------ +// The chain, restated, with plants +// ------------------------------------------------------------------------------------------------------------ + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Plant { + None, + NoXor, + NoFeedforward, +} + +impl Plant { + fn parse(s: &str) -> Plant { + match s { + "none" => Plant::None, + "no-xor" => Plant::NoXor, + "no-feedforward" => Plant::NoFeedforward, + _ => panic!("unknown plant {s}"), + } + } + fn name(self) -> &'static str { + match self { + Plant::None => "none", + Plant::NoXor => "no-xor", + Plant::NoFeedforward => "no-feedforward", + } + } +} + +/// `c_j` of segment `seg`, every word truncated to the block's width. +fn consts(blk: &Blk, key: &[u32; 8], seg: u32, j: u32) -> [u32; 16] { + let mut c = [0u32; 16]; + c[..4].copy_from_slice(&CHACHA_SIGMA); + c[4..12].copy_from_slice(key); + c[12] = seg; + c[13] = j; + c[14] = CACHE_TAG[0]; + c[15] = CACHE_TAG[1]; + for w in c.iter_mut() { + *w &= blk.mask; + } + c +} + +/// Lines 0..n-1 of segment `seg` with their block inputs `x_j`. +fn chain(blk: &Blk, key: &[u32; 8], seg: u32, n: usize, plant: Plant) -> (Vec<[u32; 16]>, Vec<[u32; 16]>) { + let mut xs = Vec::with_capacity(n); + let mut lines = Vec::with_capacity(n); + let mut prev = [0u32; 16]; + for j in 0..n { + let c = consts(blk, key, seg, j as u32); + let x = if plant == Plant::NoXor { c } else { xor16(&c, &prev) }; + let line = blk.block(&x); + xs.push(x); + lines.push(line); + prev = line; + } + (xs, lines) +} + +fn key_of_day(d: u64) -> [u32; 8] { + seed_words_from_bytes(&day_bytes(d)) +} + +fn real_blk(dr: u32, plant: Plant) -> Blk { + Blk::new(32, dr, plant != Plant::NoFeedforward) +} + +/// The real block at w = 32, 6 double rounds, with the feed-forward, must equal the library's `chacha_block`; +/// `core_inv` must invert `core` at every width used. +fn self_test() { + let b = Blk::new(32, 6, true); + let mut rng = SplitMix64::new(0xADC3_0000); + for _ in 0..4096 { + let mut x = [0u32; 16]; + for w in x.iter_mut() { + *w = rng.next() as u32; + } + assert!(eq16(&b.block(&x), &chacha_block(&x)), "restated block differs from chacha_block"); + assert!(eq16(&b.core_inv(&b.core(&x)), &x), "core_inv is not the inverse of core at w = 32"); + } + for w in [2u32, 4, 8, 16] { + let b = Blk::new(w, 2, true); + for _ in 0..4096 { + let mut x = [0u32; 16]; + for v in x.iter_mut() { + *v = (rng.next() as u32) & b.mask; + } + assert!(eq16(&b.core_inv(&b.core(&x)), &x), "core_inv is not the inverse of core at w = {w}"); + } + } + // the restated chain equals the library's fill on three segments of day 20730 + let key = key_of_day(20730); + let b = Blk::new(32, 6, true); + for seg in [0usize, 21_859, CACHE_SEGMENTS - 1] { + let mut words = vec![0u32; CACHE_LINES_PER_SEGMENT * 16]; + let mut all = vec![0u32; (seg + 1) * CACHE_LINES_PER_SEGMENT * 16]; + Cache::fill_segment(&mut all, seg, &key); + words.copy_from_slice(&all[seg * 1024..seg * 1024 + 1024]); + let (_, lines) = chain(&b, &key, seg as u32, 64, Plant::None); + for j in 0..64 { + assert!(eq16(&lines[j], words[j * 16..j * 16 + 16].try_into().unwrap()), "chain restatement differs at segment {seg} line {j}"); + } + } + log!("self-test: restated B equals chacha_block on 4,096 inputs; core_inv inverts core at w = 2, 4, 8, 16, 32; the restated chain equals Cache::fill_segment on segments 0, 21859, 65535 of day 20730"); +} + +// ------------------------------------------------------------------------------------------------------------ +// Arguments and threads +// ------------------------------------------------------------------------------------------------------------ + +struct Args { + cmd: String, + kv: Vec<(String, String)>, +} + +impl Args { + fn parse() -> Args { + let a: Vec = std::env::args().collect(); + let cmd = a.get(1).cloned().unwrap_or_else(|| "help".into()); + let mut kv = Vec::new(); + let mut i = 2; + while i < a.len() { + let k = a[i].trim_start_matches("--").to_string(); + let v = if i + 1 < a.len() && !a[i + 1].starts_with("--") { i += 1; a[i].clone() } else { "1".into() }; + kv.push((k, v)); + i += 1; + } + Args { cmd, kv } + } + fn get(&self, k: &str) -> Option<&str> { + self.kv.iter().rev().find(|(a, _)| a == k).map(|(_, v)| v.as_str()) + } + fn u64(&self, k: &str, d: u64) -> u64 { + self.get(k).map(|v| v.parse().unwrap_or_else(|_| panic!("--{k} takes a number"))).unwrap_or(d) + } + fn usize(&self, k: &str, d: usize) -> usize { + self.u64(k, d as u64) as usize + } + fn u32(&self, k: &str, d: u32) -> u32 { + self.u64(k, d as u64) as u32 + } + fn plant(&self) -> Plant { + Plant::parse(self.get("plant").unwrap_or("none")) + } + fn threads(&self) -> usize { + self.usize("threads", std::thread::available_parallelism().map(|n| n.get()).unwrap_or(8)) + } +} + +/// Run `f(thread, lo, hi)` over `total` work items split across `threads`, returning each thread's result. +fn par(threads: usize, total: usize, f: impl Fn(usize, usize, usize) -> T + Sync) -> Vec { + let threads = threads.max(1).min(total.max(1)); + std::thread::scope(|s| { + let hs: Vec<_> = (0..threads) + .map(|t| { + let f = &f; + let lo = total * t / threads; + let hi = total * (t + 1) / threads; + s.spawn(move || f(t, lo, hi)) + }) + .collect(); + hs.into_iter().map(|h| h.join().unwrap()).collect() + }) +} + +fn z(count: u64, n: u64) -> f64 { + (count as f64 - n as f64 / 2.0) / (n as f64 / 4.0).sqrt() +} + +// ------------------------------------------------------------------------------------------------------------ +// GF(2) rank +// ------------------------------------------------------------------------------------------------------------ + +struct Gf2 { + pivots: Vec<[u64; 17]>, +} + +impl Gf2 { + fn new() -> Gf2 { + Gf2 { pivots: Vec::new() } + } + fn lead(r: &[u64; 17]) -> Option { + for i in (0..17).rev() { + if r[i] != 0 { + return Some(i * 64 + 63 - r[i].leading_zeros() as usize); + } + } + None + } + /// Add a row; returns true when it was independent of the rows so far. + fn add(&mut self, mut r: [u64; 17]) -> bool { + for p in &self.pivots { + let lp = Gf2::lead(p).unwrap(); + if (r[lp >> 6] >> (lp & 63)) & 1 == 1 { + for i in 0..17 { + r[i] ^= p[i]; + } + } + } + if Gf2::lead(&r).is_none() { + return false; + } + // keep pivots sorted by descending lead so the reduction above is a full reduction in one pass + let l = Gf2::lead(&r).unwrap(); + let pos = self.pivots.iter().position(|p| Gf2::lead(p).unwrap() < l).unwrap_or(self.pivots.len()); + self.pivots.insert(pos, r); + true + } + fn rank(&self) -> usize { + self.pivots.len() + } +} + +/// Row `(x bits || y bits || 1)` for the rank test, `nb` bits each. +fn rank_row(x: &[u64; 8], y: &[u64; 8], nb: usize) -> [u64; 17] { + let mut r = [0u64; 17]; + for i in 0..nb { + if bit(x, i) == 1 { + r[i >> 6] |= 1u64 << (i & 63); + } + if bit(y, i) == 1 { + let p = nb + i; + r[p >> 6] |= 1u64 << (p & 63); + } + } + let p = 2 * nb; + r[p >> 6] |= 1u64 << (p & 63); + r +} + +// ------------------------------------------------------------------------------------------------------------ +// Correlation table: counts of (u bit a = 1 AND v bit b = 1), with the marginals +// ------------------------------------------------------------------------------------------------------------ + +struct Corr { + nb: usize, + n: u64, + na: Vec, + nb_: Vec, + n11: Vec, + vbits: Vec, +} + +impl Corr { + fn new(nb: usize) -> Corr { + Corr { nb, n: 0, na: vec![0; nb], nb_: vec![0; nb], n11: vec![0; nb * nb], vbits: vec![0; nb] } + } + #[inline] + fn add(&mut self, u: &[u64; 8], v: &[u64; 8]) { + let nb = self.nb; + self.n += 1; + for b in 0..nb { + let vb = bit(v, b); + self.vbits[b] = vb; + self.nb_[b] += vb as u64; + } + for a in 0..nb { + if bit(u, a) == 1 { + self.na[a] += 1; + let row = &mut self.n11[a * nb..(a + 1) * nb]; + for b in 0..nb { + row[b] += self.vbits[b]; + } + } + } + } + fn merge(&mut self, o: &Corr) { + self.n += o.n; + for i in 0..self.nb { + self.na[i] += o.na[i]; + self.nb_[i] += o.nb_[i]; + } + for i in 0..self.nb * self.nb { + self.n11[i] += o.n11[i]; + } + } + /// The largest |z| of `u[a] XOR v[b]` against 1/2 over every cell, with its cell, and the count of cells beyond 6. + fn report(&self, label: &str) -> f64 { + let nb = self.nb; + let mut worst = 0f64; + let mut cell = (0usize, 0usize); + let mut over6 = 0usize; + let mut over5 = 0usize; + for a in 0..nb { + for b in 0..nb { + let xor1 = self.na[a] + self.nb_[b] - 2 * self.n11[a * nb + b] as u64; + let zz = z(xor1, self.n).abs(); + if zz > worst { + worst = zz; + cell = (a, b); + } + if zz > 6.0 { + over6 += 1; + } + if zz > 5.0 { + over5 += 1; + } + } + } + let cells = (nb * nb) as f64; + // expected count beyond 5 sigma under the normal tail: 5.7e-7 per cell + log!( + "{label}: n {} cells {} worst |z| {:.2} at (in bit {}, out bit {}) = (word {} bit {}, word {} bit {}); cells over 5 sigma {} (expected {:.3}); over 6 sigma {} (expected {:.5})", + self.n, + nb * nb, + worst, + cell.0, + cell.1, + cell.0 / (nb / 16), + cell.0 % (nb / 16), + cell.1 / (nb / 16), + cell.1 % (nb / 16), + over5, + cells * 5.7e-7, + over6, + cells * 2.0e-9 + ); + worst + } +} + +// ------------------------------------------------------------------------------------------------------------ +// check +// ------------------------------------------------------------------------------------------------------------ + +fn cmd_check(a: &Args) { + for (k, want) in [("day", 0x448274a57f508cbcu64), ("day2", 0x7334fa46e5d972ebu64)] { + let d = a.u64(k, if k == "day" { 20730 } else { 20733 }); + let t0 = Instant::now(); + let c = Cache::fill(key_of_day(d)); + let f = c.fnv1a64(); + let head = &c.words()[..16]; + log!("day {d}: cache FNV-1a 64 {f:#018x} (vectors.json {want:#018x}: {}), head word 0 {:#010x}, fill {:.2} s", if f == want { "MATCH" } else { "MISMATCH" }, head[0], t0.elapsed().as_secs_f64()); + assert_eq!(f, want, "cache fingerprint mismatch on day {d}"); + } +} + +// ------------------------------------------------------------------------------------------------------------ +// skip: the template search, the rank test, the dependence tables, the inversion attempt +// ------------------------------------------------------------------------------------------------------------ + +const TEMPLATES: [(&str, u32); 7] = [ + ("B(c_j)", 1), + ("B(c_j XOR c_i), i < j", 1), + ("B(c_j) XOR B(c_i), i < j", 2), + ("B(c_j) XOR c_i, i < j", 1), + ("B(B(c_j))", 2), + ("B(c_j) + c_i, i < j", 1), + ("Cinv(line_j) XOR c_j = line_{j-1} (up the chain)", 1), +]; + +fn cmd_skip(a: &Args) { + let day = a.u64("day", 20730); + let w = a.u32("w", 32); + let dr = a.u32("rounds", 6); + let n = a.usize("lines", 64); + let segs = a.usize("segments", 1024); + let plant = a.plant(); + let threads = a.threads(); + let blk = Blk::new(w, dr, plant != Plant::NoFeedforward); + let key = key_of_day(day); + let nb = blk.bits(); + log!("skip: day {day} w {w} double rounds {dr} lines {n} segments {segs} plant {} threads {threads}; rotations {:?}", plant.name(), blk.rot); + let seg_of = |p: usize| -> u32 { if segs >= CACHE_SEGMENTS { p as u32 } else { ((p as u64 * 0x9E37) & 0xffff) as u32 } & blk.mask }; + let t0 = Instant::now(); + + // (1) templates: match counts per template per j, and the chance expectation + let results = par(threads, segs, |_, lo, hi| { + let mut hits = vec![vec![0u64; n]; TEMPLATES.len()]; + let mut compares = 0u64; + let mut bc: Vec<[u32; 16]> = vec![[0u32; 16]; n]; + for p in lo..hi { + let s = seg_of(p); + let (_, lines) = chain(&blk, &key, s, n, plant); + let cs: Vec<[u32; 16]> = (0..n).map(|j| consts(&blk, &key, s, j as u32)).collect(); + for j in 0..n { + bc[j] = blk.block(&cs[j]); + } + for j in 0..n { + let l = &lines[j]; + compares += 2; + if eq16(&bc[j], l) { + hits[0][j] += 1; + } + if eq16(&blk.block(&bc[j]), l) { + hits[4][j] += 1; + } + for i in 0..j { + compares += 4; + if eq16(&blk.block(&xor16(&cs[j], &cs[i])), l) { + hits[1][j] += 1; + } + if eq16(&xor16(&bc[j], &bc[i]), l) { + hits[2][j] += 1; + } + if eq16(&xor16(&bc[j], &cs[i]), l) { + hits[3][j] += 1; + } + let mut sum = [0u32; 16]; + for k in 0..16 { + sum[k] = blk.add(bc[j][k], cs[i][k]); + } + if eq16(&sum, l) { + hits[5][j] += 1; + } + } + if j >= 1 { + compares += 1; + let up = xor16(&blk.core_inv(l), &cs[j]); + if eq16(&up, &lines[j - 1]) { + hits[6][j] += 1; + } + } + } + } + (hits, compares) + }); + let mut hits = vec![vec![0u64; n]; TEMPLATES.len()]; + let mut compares = 0u64; + for (h, c) in results { + compares += c; + for t in 0..TEMPLATES.len() { + for j in 0..n { + hits[t][j] += h[t][j]; + } + } + } + let chance = compares as f64 / 2f64.powi(nb as i32); + log!("templates: {} compares of {}-bit lines, chance matches expected {:.3e}, {:.1} s", compares, nb, chance, t0.elapsed().as_secs_f64()); + let mut under = 0u64; + for (t, (name, blocks)) in TEMPLATES.iter().enumerate() { + let total: u64 = hits[t].iter().sum(); + let js: Vec = (0..n).filter(|&j| hits[t][j] > 0).map(|j| format!("j{}:{}", j, hits[t][j])).collect(); + // a match at j derives line j in `blocks` evaluations (the inverse template: line j-1 from line j in 1) + let below: u64 = (0..n).map(|j| if (*blocks as usize) < j + 1 || (t == 6 && j >= 1) { hits[t][j] } else { 0 }).sum(); + under += below; + log!("template {t} [{name}] at {blocks} block(s): matches {total}{}", if js.is_empty() { String::new() } else { format!(" ({})", js.join(" ")) }); + } + log!("SKIP RESULT: lines derived under j + 1 blocks by any template: {under} of {} (gate 0{}){}", (segs * n) as u64, if plant == Plant::None { "" } else { "; a plant must read above 0" }, if (under == 0) == (plant == Plant::None) { " PASS" } else { " FIRE" }); + + // (2) the GF(2) rank test on (x_j, line_j) and (line_{j-1}, line_j) + let samples = 4096usize; + let js_test: Vec = [1usize, 2, n / 2, n - 1].into_iter().filter(|&j| j >= 1 && j < n).collect(); + let mut sample_lines: Vec<(Vec<[u32; 16]>, Vec<[u32; 16]>)> = Vec::new(); + for p in 0..(samples / n.max(1) + 1).max(samples / 63 + 1) { + let s = seg_of(p); + sample_lines.push(chain(&blk, &key, s, n, plant)); + } + for &j in &js_test { + let mut g_x = Gf2::new(); + let mut g_p = Gf2::new(); + let mut cnt = 0usize; + 'outer: for (xs, lines) in &sample_lines { + // every line j >= 1 of the segment within the pooled test, the fixed j for the per-j test + for jj in [j] { + g_x.add(rank_row(&pack(&xs[jj], w), &pack(&lines[jj], w), nb)); + g_p.add(rank_row(&pack(&lines[jj - 1], w), &pack(&lines[jj], w), nb)); + cnt += 1; + if cnt >= samples { + break 'outer; + } + } + } + log!("rank j={j}: samples {cnt}, columns {} (x bits, line bits, 1): rank(x_j, line_j) {} rank(line_j-1, line_j) {}{}", 2 * nb + 1, g_x.rank(), g_p.rank(), if g_x.rank() == 2 * nb + 1 && g_p.rank() == 2 * nb + 1 { " FULL" } else { " DEFICIENT: an affine relation exists" }); + } + { + let mut g = Gf2::new(); + let mut cnt = 0usize; + 'o2: for (xs, lines) in &sample_lines { + for jj in 1..n { + g.add(rank_row(&pack(&xs[jj], w), &pack(&lines[jj], w), nb)); + cnt += 1; + if cnt >= samples { + break 'o2; + } + } + } + log!("rank pooled j>=1: samples {cnt}, rank(x_j, line_j) {} of {}{}", g.rank(), 2 * nb + 1, if g.rank() == 2 * nb + 1 { " FULL" } else { " DEFICIENT" }); + } + + // (3) dependence: flip each bit of line_{j-1}, count flips of each bit of line_j; and the word table + let dep_samples = 4096usize.min(segs * (n - 1)); + let t1 = Instant::now(); + let dep = par(threads, dep_samples, |_, lo, hi| { + let mut cnt = vec![0u64; nb * nb]; + let mut wcnt = vec![0u64; 256]; + let mut rng = SplitMix64::new(0xADC3_1111 ^ lo as u64); + for q in lo..hi { + let s = seg_of(q / (n - 1)); + let j = 1 + q % (n - 1); + let (_, lines) = chain(&blk, &key, s, j + 1, plant); + let prev = lines[j - 1]; + let c = consts(&blk, &key, s, j as u32); + let base = lines[j]; + let basep = pack(&base, w); + for b in 0..nb { + let mut pf = prev; + pf[b / w as usize] ^= 1u32 << (b % w as usize); + let x = if plant == Plant::NoXor { c } else { xor16(&c, &pf) }; + let y = pack(&blk.block(&x), w); + for o in 0..nb { + cnt[b * nb + o] += (bit(&y, o) ^ bit(&basep, o)) as u64; + } + } + for i in 0..16 { + let mut pf = prev; + let mut r = (rng.next() as u32) & blk.mask; + if r == 0 { + r = 1; + } + pf[i] ^= r; + let x = if plant == Plant::NoXor { c } else { xor16(&c, &pf) }; + let y = blk.block(&x); + for o in 0..16 { + wcnt[i * 16 + o] += (y[o] != base[o]) as u64; + } + } + } + (cnt, wcnt) + }); + let mut cnt = vec![0u64; nb * nb]; + let mut wcnt = vec![0u64; 256]; + for (c, wc) in dep { + for i in 0..nb * nb { + cnt[i] += c[i]; + } + for i in 0..256 { + wcnt[i] += wc[i]; + } + } + let nn = dep_samples as u64; + let mut zmax = 0f64; + let mut zmin = 0f64; + let mut zeros = 0usize; + for &c in &cnt { + let zz = z(c, nn); + zmax = zmax.max(zz); + zmin = zmin.min(zz); + if c == 0 { + zeros += 1; + } + } + let wmin = wcnt.iter().copied().min().unwrap(); + log!("dependence: {dep_samples} lines, {nb} x {nb} flip table: worst cells z {:+.2} / {:+.2} (gate 6), zero cells {zeros} of {}; word table min {} of {} (every output word changes when any input word changes: {}); {:.1} s", zmax, zmin, nb * nb, wmin, nn, if wmin == nn { "YES" } else { "NO, a word is computable from fewer than 16 input words" }, t1.elapsed().as_secs_f64()); + // partial knowledge, stated from the word table + let mut determined = 0usize; + for o in 0..16 { + let full = (0..16).all(|i| wcnt[i * 16 + o] == nn); + if !full { + determined += 1; + } + } + log!("partial knowledge: output words computable from a proper subset of the 16 input words: {determined} of 16 (gate 0)"); + + // (4) inversion attempt: x <- Cinv(y - x) from x = 0, 64 steps, on 2^16 lines + let inv_samples = 65_536usize.min(segs * (n - 1)); + let t2 = Instant::now(); + let inv = par(threads, inv_samples, |_, lo, hi| { + let mut conv = 0u64; + let mut direct = 0u64; + for q in lo..hi { + let s = seg_of(q / (n - 1)); + let j = 1 + q % (n - 1); + let (xs, lines) = chain(&blk, &key, s, j + 1, plant); + let y = lines[j]; + if eq16(&blk.core_inv(&y), &xs[j]) { + direct += 1; + } + let mut x = [0u32; 16]; + for _ in 0..64 { + let mut d = [0u32; 16]; + for k in 0..16 { + d[k] = blk.sub(y[k], x[k]); + } + x = blk.core_inv(&d); + if eq16(&blk.block(&x), &y) { + conv += 1; + break; + } + } + } + (conv, direct) + }); + let (conv, direct) = inv.iter().fold((0u64, 0u64), |a, b| (a.0 + b.0, a.1 + b.1)); + log!("inversion: {inv_samples} lines: Cinv(line_j) = x_j directly {direct}; fixed-point iteration x <- Cinv(y - x) converged {conv} (gate 0{}); {:.1} s", if plant == Plant::NoFeedforward { "; the plant must read all" } else { "" }, t2.elapsed().as_secs_f64()); + log!("skip done in {:.1} s", t0.elapsed().as_secs_f64()); +} + +// ------------------------------------------------------------------------------------------------------------ +// relations: per-bit biases and the correlation table (Q2) +// ------------------------------------------------------------------------------------------------------------ + +fn cmd_relations(a: &Args) { + let day0 = a.u64("day0", 20730); + let days = a.u64("days", 4); + let l2 = a.u32("lines-log2", 20); + let w = a.u32("w", 32); + let dr = a.u32("rounds", 6); + let plant = a.plant(); + let threads = a.threads(); + let blk = Blk::new(w, dr, plant != Plant::NoFeedforward); + let nb = blk.bits(); + let n = 64usize; + let segs = (1usize << l2) / n; + log!("relations: days {day0}..{} lines per day 2^{l2} ({segs} segments x 64) w {w} double rounds {dr} plant {} threads {threads}", day0 + days - 1, plant.name()); + let t0 = Instant::now(); + let names = ["line_j XOR x_j", "line_j - x_j (= C(x_j))", "line_j XOR line_j-1", "line_j - line_j-1"]; + let mut bias = vec![vec![0u64; nb]; 4]; + let mut total = 0u64; + let mut corr = Corr::new(nb); + for d in day0..day0 + days { + let key = key_of_day(d); + let parts = par(threads, segs, |_, lo, hi| { + let mut bias = vec![vec![0u64; nb]; 4]; + let mut c = Corr::new(nb); + for p in lo..hi { + let s = (p as u32) & blk.mask; + let (xs, lines) = chain(&blk, &key, s, n, plant); + for j in 1..n { + let (x, l, pv) = (&xs[j], &lines[j], &lines[j - 1]); + let mut r = [[0u32; 16]; 4]; + for k in 0..16 { + r[0][k] = l[k] ^ x[k]; + r[1][k] = blk.sub(l[k], x[k]); + r[2][k] = l[k] ^ pv[k]; + r[3][k] = blk.sub(l[k], pv[k]); + } + for t in 0..4 { + let pk = pack(&r[t], w); + for b in 0..nb { + bias[t][b] += bit(&pk, b) as u64; + } + } + c.add(&pack(x, w), &pack(l, w)); + } + } + (bias, c) + }); + for (b, c) in parts { + for t in 0..4 { + for i in 0..nb { + bias[t][i] += b[t][i]; + } + } + corr.merge(&c); + } + total += (segs * (n - 1)) as u64; + log!("day {d} done, {total} lines so far, {:.1} s", t0.elapsed().as_secs_f64()); + } + let mut worst_all = 0f64; + for t in 0..4 { + let mut worst = 0f64; + let mut wb = 0usize; + let mut over6 = 0usize; + for b in 0..nb { + let zz = z(bias[t][b], total); + if zz.abs() > worst { + worst = zz.abs(); + wb = b; + } + if zz.abs() > 6.0 { + over6 += 1; + } + } + worst_all = worst_all.max(worst); + log!("bias [{}]: {nb} bits over {total} lines: worst |z| {:.2} at bit {wb} (word {} bit {}), bits over 6 sigma {over6}", names[t], worst, wb / w as usize, wb % w as usize); + } + let wc = corr.report("correlation x_j bits against line_j bits (the line_j-1 table is the same up to a sign per column, x_j = line_j-1 XOR c_j)"); + log!("RELATIONS RESULT: worst bias |z| {:.2}, worst correlation |z| {:.2}, gate 6 at {total} samples{}; {:.1} s", worst_all, wc, if worst_all.max(wc) <= 6.0 { " PASS" } else { " FIRE" }, t0.elapsed().as_secs_f64()); +} + +// ------------------------------------------------------------------------------------------------------------ +// image: the exhaustive census at w = 2 (a 32-bit state) +// ------------------------------------------------------------------------------------------------------------ + +fn cmd_image(a: &Args) { + let w = a.u32("w", 2); + assert_eq!(w, 2, "the exhaustive census enumerates 2^32 states: w = 2 only"); + let dr = a.u32("rounds", 6); + let depth = a.usize("depth", 64); + let day = a.u64("day", 20730); + let plant = a.plant(); + let threads = a.threads(); + let blk = Blk::new(w, dr, plant != Plant::NoFeedforward); + let key = key_of_day(day); + let seg = 1u32 & blk.mask; + log!("image: w {w} double rounds {dr} depth {depth} plant {} threads {threads}; rotations {:?}; all 2^32 states enumerated per step", plant.name(), blk.rot); + let words = 1usize << 26; + let mut cur: Vec = (0..words).map(|_| AtomicU64::new(u64::MAX)).collect(); + let mut nxt: Vec = (0..words).map(|_| AtomicU64::new(0)).collect(); + let unpack = |v: u32| -> [u32; 16] { + let mut x = [0u32; 16]; + for i in 0..16 { + x[i] = (v >> (2 * i)) & 3; + } + x + }; + let repack = |x: &[u32; 16]| -> u32 { + let mut v = 0u32; + for i in 0..16 { + v |= (x[i] & 3) << (2 * i); + } + v + }; + let n = 2f64.powi(32); + let mut tau = 1f64; + let t0 = Instant::now(); + for k in 1..=depth { + let c = consts(&blk, &key, seg, ((k - 1) & 3) as u32); + for x in nxt.iter() { + x.store(0, Ordering::Relaxed); + } + let (cur_r, nxt_r) = (&cur, &nxt); + let plant_c = plant; + let blk_c = blk; + par(threads, words, |_, lo, hi| { + for wi in lo..hi { + let mut m = cur_r[wi].load(Ordering::Relaxed); + while m != 0 { + let b = m.trailing_zeros(); + m &= m - 1; + let v = ((wi as u64) << 6 | b as u64) as u32; + let p = unpack(v); + let x = if plant_c == Plant::NoXor { c } else { xor16(&c, &p) }; + let y = repack(&blk_c.block(&x)); + nxt_r[(y >> 6) as usize].fetch_or(1u64 << (y & 63), Ordering::Relaxed); + } + } + }); + let size: u64 = par(threads, words, |_, lo, hi| (lo..hi).map(|i| nxt_r[i].load(Ordering::Relaxed).count_ones() as u64).sum::()).iter().sum(); + tau = 1.0 - (-tau).exp(); + log!("depth {k}: image size {size} = {:.6} of 2^32 (random-function recursion tau_k = 1 - exp(-tau_k-1): {:.6}; 2/k: {:.6}; a permutation: 1.000000); {:.0} s", size as f64 / n, tau, 2.0 / k as f64, t0.elapsed().as_secs_f64()); + std::mem::swap(&mut cur, &mut nxt); + } +} + +// ------------------------------------------------------------------------------------------------------------ +// pebble: the pebbling curve of the 64-line chain (Q3) +// ------------------------------------------------------------------------------------------------------------ + +/// Blocks to derive line `j` from the held set (bitmask over lines) on the chain with an optional skip edge +/// (`skip` = 0 for the plain path): shortest path from a held line or from nothing (`-1`, which reaches line 0 in 1). +fn costs(n: usize, held: u64, skip: usize) -> Vec { + let mut d = vec![0u32; n]; + for j in 0..n { + if (held >> j) & 1 == 1 { + d[j] = 0; + continue; + } + let mut best = if j == 0 { 1 } else { d[j - 1] + 1 }; + if skip > 0 && j >= skip { + best = best.min(d[j - skip] + 1); + } + if skip > 0 && j < skip && j > 0 { + // from nothing through the skip edge: nothing to skip from + } + d[j] = best; + } + d +} + +fn mean_cost(n: usize, held: u64, skip: usize) -> f64 { + costs(n, held, skip).iter().map(|&c| c as f64).sum::() / n as f64 +} + +/// The exact optimum over placements of `k` held lines on the plain path by dynamic programming. +fn dp_optimum(n: usize, k: usize) -> (f64, u64) { + // g[i][r] = min total cost of lines i+1..n-1 given line i is held (i = n means the virtual -1 shifted: use index 0 = virtual) + // positions: 0 = virtual (-1), 1..=n = lines 0..n-1 + let inf = f64::INFINITY; + let mut g = vec![vec![inf; k + 1]; n + 2]; + let mut choice = vec![vec![0usize; k + 1]; n + 2]; + // segment cost: lines strictly between held position p and the next held q (or the end): sum of (line - held line) + let seg_cost = |p: usize, q: usize| -> f64 { + // p, q positions; lines p+1..q-1 (position index) cost (pos - p) + (p + 1..q).map(|pos| (pos - p) as f64).sum() + }; + for p in (0..=n).rev() { + g[p][0] = seg_cost(p, n + 1); + for r in 1..=k { + let mut best = inf; + let mut bq = 0; + for q in p + 1..=n { + let v = seg_cost(p, q) + g[q][r - 1]; + if v < best { + best = v; + bq = q; + } + } + g[p][r] = best; + choice[p][r] = bq; + } + } + let mut held = 0u64; + let (mut p, mut r) = (0usize, k); + while r > 0 { + let q = choice[p][r]; + if q == 0 { + break; + } + held |= 1u64 << (q - 1); + p = q; + r -= 1; + } + (g[0][k] / n as f64, held) +} + +fn stride_held(n: usize, k: usize, offset: usize) -> u64 { + let step = n / k; + let mut h = 0u64; + for i in 0..k { + let pos = i * step + offset; + if pos < n { + h |= 1u64 << pos; + } + } + h +} + +fn cmd_pebble(a: &Args) { + let n = a.usize("lines", 64); + let exh = a.usize("exhaustive-upto", 16); + let mc = a.u64("mc", 1_000_000); + let skip = a.usize("skip-edge", 0); + log!("pebble: lines {n} exhaustive check up to {exh} lines, Monte Carlo {mc} trials per point, skip edge {}", if skip == 0 { "none (the plain path)".to_string() } else { format!("{skip} (PLANT: line j also from line j - {skip} in one block)") }); + // the DP against exhaustive search at small n + for m in [10usize, 12, 14, 16].into_iter().filter(|&m| m <= exh) { + for k in [1usize, 2, 4] { + let (dp, _) = dp_optimum(m, k); + let mut best = f64::INFINITY; + for held in 0u64..(1u64 << m) { + if held.count_ones() as usize == k { + best = best.min(mean_cost(m, held, 0)); + } + } + log!("exhaustive n={m} k={k}: optimum {best:.4} blocks per read, DP {dp:.4}{}", if (best - dp).abs() < 1e-9 { " AGREE" } else { " DISAGREE" }); + } + } + let fs: Vec = [1usize, 2, 4, 8, 16, 32, 64].into_iter().filter(|&k| k <= n).collect(); + log!("static curve (blocks per uniform read; ops per item = 9,360 + 8 x blocks x 608; SRAM = f x 128 mm^2 at the N5 headline of chip-model-v3 section 2):"); + log!("f=k/{n} | k held | DP optimum (plain path) | stride offset 0 | stride offset step-1 | best over the planted graph (k=1 exhaustive, else local search) | ops per item at the optimum | SRAM mm^2"); + let mut prev_opt = f64::INFINITY; + let mut monotone = true; + let mut below_honest = false; + for &k in &fs { + let (opt, held_opt) = dp_optimum(n, k); + let step = n / k; + let s0 = mean_cost(n, stride_held(n, k, 0), 0); + let s1 = mean_cost(n, stride_held(n, k, step - 1), 0); + // the planted graph: exhaustive at k = 1, a local search (1-swap descent from the DP placement) otherwise + let planted = if skip == 0 { + opt + } else if k == 1 { + (0..n).map(|j| mean_cost(n, 1u64 << j, skip)).fold(f64::INFINITY, f64::min) + } else { + let mut h = held_opt; + let mut cur = mean_cost(n, h, skip); + loop { + let mut improved = false; + for out in 0..n { + if (h >> out) & 1 == 0 { + continue; + } + for into in 0..n { + if (h >> into) & 1 == 1 { + continue; + } + let h2 = (h & !(1u64 << out)) | (1u64 << into); + let c = mean_cost(n, h2, skip); + if c < cur - 1e-12 { + cur = c; + h = h2; + improved = true; + } + } + } + if !improved { + break; + } + } + cur + }; + let ops = 9_360.0 + 8.0 * opt * 608.0; + let sram = 128.0 * k as f64 / n as f64; + if opt > prev_opt + 1e-12 { + monotone = false; + } + prev_opt = opt; + if opt < s0.min(s1) - 1e-12 { + // the optimum is allowed under the stride (it is the optimum); what the gate forbids is the honest curve + // sitting ABOVE a cheaper adversary strategy the attacker has and the honest miner lacks: there is none on a path + } + if planted < opt - 1e-12 { + below_honest = true; + } + log!("{k}/{n} | {k} | {opt:.4} | {s0:.4} | {s1:.4} | {planted:.4} | {ops:.0} | {sram:.1}"); + } + log!("static curve: monotone in f {}; f = 1 reads {} ops per item (gate 9,360); planted graph under the path optimum at some f: {}{}", if monotone { "YES" } else { "NO" }, 9_360, if below_honest { "YES" } else { "NO" }, if skip == 0 { if monotone { " PASS" } else { " FIRE" } } else if below_honest { " PLANT FIRES" } else { " PLANT DID NOT FIRE" }); + + // the amortising adversary: m requests per segment (fixed m, and Poisson(m) as a random spread of N items over 2^16 segments) + log!("amortising adversary (Monte Carlo {mc} trials): blocks per read with m requests in one segment, one walk per gap from the nearest held line to the deepest request; the stride store at offset step-1 (the DP optimum's shape for k >= 2)"); + log!("f=k/{n} | m=1 | m=2 | m=4 | m=8 | m=16 | m=64 | Poisson m=1 | Poisson m=8 | Poisson m=64"); + let mut rng = SplitMix64::new(0xADC3_2222); + let walk = |held: u64, reqs: &[usize]| -> u64 { + // deepest request per gap: for each request, cost from nearest held at or below; walks in one gap share the prefix + let mut deepest = [0i64; 65]; + let mut has = [false; 65]; + for &j in reqs { + // the nearest held line at or below j, or -1 + let below = held & ((2u64 << j) - 1); + let h: i64 = if below == 0 { -1 } else { 63 - below.leading_zeros() as i64 }; + let gi = (h + 1) as usize; + let depth = j as i64 - h; + if !has[gi] || depth > deepest[gi] { + has[gi] = true; + deepest[gi] = depth; + } + } + (0..65).filter(|&g| has[g]).map(|g| deepest[g] as u64).sum() + }; + for &k in &fs { + let held = if k == n { u64::MAX } else { stride_held(n, k, n / k - 1) }; + let mut row = Vec::new(); + for &m in &[1usize, 2, 4, 8, 16, 64] { + let mut blocks = 0u64; + let trials = (mc / m as u64).max(1000); + let mut reqs = vec![0usize; m]; + for _ in 0..trials { + for r in reqs.iter_mut() { + *r = (rng.next() % n as u64) as usize; + } + blocks += walk(held, &reqs); + } + row.push(format!("{:.3}", blocks as f64 / (trials as f64 * m as f64))); + } + for &lambda in &[1.0f64, 8.0, 64.0] { + let mut blocks = 0u64; + let mut reads = 0u64; + let trials = (mc as f64 / lambda).max(1000.0) as u64; + let mut reqs: Vec = Vec::new(); + for _ in 0..trials { + // Poisson by inversion + let mut cnt = 0usize; + let mut p = (-lambda).exp(); + let mut acc = p; + let u = (rng.next() >> 11) as f64 / (1u64 << 53) as f64; + while u > acc && cnt < 1000 { + cnt += 1; + p *= lambda / cnt as f64; + acc += p; + } + reqs.clear(); + for _ in 0..cnt { + reqs.push((rng.next() % n as u64) as usize); + } + reads += cnt as u64; + blocks += walk(held, &reqs); + } + row.push(format!("{:.3}", blocks as f64 / reads.max(1) as f64)); + } + log!("{k}/{n} | {}", row.join(" | ")); + } + log!("cross-check points: the plain path with nothing held at m = 1 must read 32.5 (E[j] + 1); sibling adv-cache measured 23.84, 7.06 and 0.99 blocks per read on real addresses at Poisson m = 1, 8, 64 with nothing held (its batch rows)"); +} + +// ------------------------------------------------------------------------------------------------------------ +// cross: cross-segment and cross-day correlation tables (Q4) +// ------------------------------------------------------------------------------------------------------------ + +fn cmd_cross(a: &Args) { + let day = a.u64("day", 20730); + let segs = a.usize("segments", 4096); + let w = a.u32("w", 32); + let dr = a.u32("rounds", 6); + let plant = a.plant(); + let threads = a.threads(); + let blk = Blk::new(w, dr, plant != Plant::NoFeedforward); + let nb = blk.bits(); + let n = 64usize; + let key = key_of_day(day); + let key2 = key_of_day(day + 1); + log!("cross: day {day} (and {}) segments {segs} w {w} double rounds {dr} plant {} threads {threads}", day + 1, plant.name()); + let t0 = Instant::now(); + let mut worst = 0f64; + for j in [0usize, 1, n - 1] { + let parts = par(threads, segs, |_, lo, hi| { + let mut c = Corr::new(nb); + for p in lo..hi { + let s = ((p as u64 * 0x9E37) & 0xffff) as u32 & blk.mask; + let s2 = s ^ (1u32 << (p % (16.min(w as usize)))); + let (_, l1) = chain(&blk, &key, s, j + 1, plant); + let (_, l2) = chain(&blk, &key, s2, j + 1, plant); + c.add(&pack(&l1[j], w), &pack(&l2[j], w)); + } + c + }); + let mut c = Corr::new(nb); + for p in parts { + c.merge(&p); + } + worst = worst.max(c.report(&format!("cross-segment j={j}: line_j(s) against line_j(s XOR 2^b)"))); + } + { + let parts = par(threads, segs, |_, lo, hi| { + let mut c = Corr::new(nb); + for p in lo..hi { + let s = ((p as u64 * 0x9E37) & 0xffff) as u32 & blk.mask; + let j = p % n; + let (_, l1) = chain(&blk, &key, s, j + 1, plant); + let (_, l2) = chain(&blk, &key2, s, j + 1, plant); + c.add(&pack(&l1[j], w), &pack(&l2[j], w)); + } + c + }); + let mut c = Corr::new(nb); + for p in parts { + c.merge(&p); + } + worst = worst.max(c.report("cross-day: line_j(s) of day d against day d + 1, same (s, j)")); + } + log!("known constants of x_j from the code: j = 0: 16 of 16 words (x_0 = c_0 is public); j >= 1: 0 of 16 (every word is XORed with the previous line)"); + log!("CROSS RESULT: worst |z| {worst:.2}, gate 6 at {segs} samples{}; {:.1} s", if worst <= 6.0 { " PASS" } else { " FIRE" }, t0.elapsed().as_secs_f64()); +} + +fn main() { + let a = Args::parse(); + log!("adv-cache-3 {} (internal adversarial pass, not an independent review)", a.cmd); + self_test(); + match a.cmd.as_str() { + "check" => cmd_check(&a), + "skip" => cmd_skip(&a), + "relations" => cmd_relations(&a), + "image" => cmd_image(&a), + "pebble" => cmd_pebble(&a), + "cross" => cmd_cross(&a), + _ => { + eprintln!("commands: check, skip, relations, image, pebble, cross (see the file header)"); + std::process::exit(2); + } + } +}