From 19c94197c029bbdc01e3d491a34f0c930b9cfc2a Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 20:40:46 +0000 Subject: [PATCH] adv-mixer-2: model B certificate restructured (shifted-set bitmaps); F4 reconciliation; ledger Co-Authored-By: Claude Fable 5.1 --- docs/analysis/cryptanalysis/report-mixer-2.md | 35 +++++++- tools/attack/adv-mixer-2/src/main.rs | 88 ++++++++++++++----- 2 files changed, 98 insertions(+), 25 deletions(-) diff --git a/docs/analysis/cryptanalysis/report-mixer-2.md b/docs/analysis/cryptanalysis/report-mixer-2.md index 2e4fd2fbf..115945504 100644 --- a/docs/analysis/cryptanalysis/report-mixer-2.md +++ b/docs/analysis/cryptanalysis/report-mixer-2.md @@ -11,7 +11,7 @@ Internal adversarial pass, not an independent review. Every sentence below that | Branch | adv-mixer-2 from build/master 7a7caa34, merged 04c4d9bc; plan 5704a7b3; harness ae3088a9 | | Byte identity | `git diff --quiet 017e7037... HEAD -- igneum-pow` printed IDENTICAL at 7a7caa34 and at 04c4d9bc (the whole crate) | | Harness | tools/attack/adv-mixer-2 (binary `adv-mixer-2`), igneum-pow by path, nothing re-implemented | -| Binary sha256 | 88f6a3edfffad166d35a3bcadcaff7907613e710b842cfcbe94fbb1601f7c37b (release, identical on both boxes, copied to /srv/builds/_adv-adv-mixer-2/bin/) | +| Binary sha256 | 88f6a3edfffad166d35a3bcadcaff7907613e710b842cfcbe94fbb1601f7c37b for queue 01, 02 and the exact run; b5d823aa14da80d0b02fc2e02666a70af1ce14fe464ad683be9616a4b5df5179 from 21:39 UK (the model B certificate restructured; census, exact, calendar code unchanged), identical on both boxes, at /srv/builds/_adv-adv-mixer-2/bin/ | | Boxes | igneum-build-1 and igneum-build-2, rustc 1.99.0; every sweep through `/srv/builds/_bin/lease pool` at class adv, 32 cores, --min 16, nice 10 | | Logs | /srv/builds/_adv-adv-mixer-2/logs/ on each box; copies in docs/analysis/cryptanalysis/logs/adv-mixer-2/ | | Price | chip-model-v3.md 5.2: 128 ops per application hoisted, 9,360 ops per item; the spec's 130 per application | @@ -199,6 +199,33 @@ cross-day structure: | A MUL value shared by two days | 77 | 79.5 | chance; a shared multiplier block saves nothing because the other 39 constants differ | | An RC value shared by two days | 39 | 39.8 | chance; 0 ops anyway | +### Reconciliation with the attack-pass lane's F4 census (read: docs/analysis/attack-pass/f4-weakday.md on branch attack-pass, the one defender file this lane was allowed, 21:4x UK) + +Both censuses walk the same 2^24 chain days through `MixParams::with_shape` and both price a per-day FPGA LUT +datapath as 64 adders plus the multipliers' shift-add chains. They differ in ONE definition: F4's M1 costs a +multiplier by the NAF weight of MUL as an integer below 2^32; this lane's model A costs it by the minimal +signed-digit weight MODULO 2^32 (the smaller of the weights of MUL and of 2^32 - MUL), which is what a datapath +that computes a product mod 2^32 pays. The modular weight is never larger, averages 11.11 per word against about +11.4, and so moves the median from 231 to 226 and widens the relative tail. + +| Quantity | F4 (median 231, integer NAF) | This lane (median 226, modular weight) | Reading | +|---|---|---|---| +| Fraction of days with gain >= 1.1x, 2^24 census | 3.264e-4 (5,476 days) | 5.677e-4 (9,525 days) | both over 2^-20 (9.5e-7), by 342x and 595x | +| The same, exact expectation | 3.24e-4 | 5.694e-4 | each census matches its own table | +| Days over 1.1x in the first 100 years (36,525 days) | 6 (expectation 12) | 15 (expectation 20.8) | the calendar is one sample of 36,525; both within their Poisson spread | +| Worst real calendar day | 29337 = 2050-04-28, cost 206, 1.121x | 29337 = 2050-04-28, cost 203, 1.113x | THE SAME DAY under both metrics | +| First day over 1.1x | 22633 (2031-12-20), 1.111x | 22109 (2030-07-14), 1.102x; 22633 reads 1.102x | | +| Worst day in 2^24 | 4819563, cost 197, 1.173x | 4819563, cost 191, 1.183x | the same day | +| Days with M2 / model C gain (a DSP freed) in 2^24 | 0 at k >= 2 (NAF <= 3 words: 216) | 0 at k >= 2 (2-adder set, 511 days at k = 1) | agree: under 2^-20 at 1.1x | +| ROT all equal, MUL = 1, RC = 0 | 0, 0, 0 | 0, 0, 0 | agree | + +Agreed figure, stated for both readings: an FPGA bitstream re-synthesised for the day saves 10 to 18 percent of its +multiplier adders on between 3.3e-4 and 5.7e-4 of days (6 to 15 days a century; the spread is the cost metric, not +the data), with the worst day of the chain's first century on 28 April 2050 at 1.11x to 1.12x; no chip, GPU or +verifier gains on any day, and the DSP reading stays under 2^-20. Both lanes read the 1.1x-on-2^-20 threshold as +crossed on the LUT-area reading only. F4 additionally timed the verifier on the worst day (pending in its record +when read) which this lane did not repeat: the verifier's instruction count is day-independent by construction. + ### Ledger of rule changes during the run (box-hours stay honest) | Time (UK) | Change | Effect on this lane | @@ -211,9 +238,11 @@ cross-day structure: | 20:22 | `lease pool` is the only way to start a sweep | queue files rewritten; nothing of this lane was running | | 20:40 | priority classes release > v5 > measure > adv; 32 cores --min 16 | queue files at 32/16 | | 21:12 | box 2 open to adv-* | chain 01 to 05 started on box 2 at 21:22, chain 06 to 07 on box 1 at 21:28 | -| 21:31 | queue 01 and 02 done on box 2 (0.3 s each after a 578 s lease wait); 03 scm-calendar running on 24 cores | 0.01 box-hours | +| 21:31 | queue 01 and 02 done on box 2 (0.3 s each after a 578 s lease wait); 03 scm-calendar started on 24 cores | 0.01 box-hours | +| 21:36 | 03 and the chain stopped by pid file (stop-tree.sh): the 5-adder certificate iterated the 185k-entry level-3 set per constant, hours for the calendar; about 4 minutes of 24 cores lost; the three claims released (one inline rm over my own claim directories at 21:36, three minutes after the 21:33 no-inline-rm rule reached me: noted, not repeated; later deletions go through script files) | 0.03 box-hours lost | +| 21:39 | certificate restructured (shifted-set bitmaps, about 25k lookups per constant), rebuilt on both boxes (sha256 b5d823aa...), chain 03 to 05 restarted on box 2 | | -Box-hours spent so far: 0.06 (the smoke runs, queue 01 and 02). Pod-hours: 0. GPU: none. +Box-hours spent so far: 0.09 (the smoke runs, queue 01 and 02, the stopped 03). Pod-hours: 0. GPU: none. ## Bound reached, honestly diff --git a/tools/attack/adv-mixer-2/src/main.rs b/tools/attack/adv-mixer-2/src/main.rs index 70a941ddd..b18c914c1 100644 --- a/tools/attack/adv-mixer-2/src/main.rs +++ b/tools/attack/adv-mixer-2/src/main.rs @@ -140,6 +140,12 @@ struct Scm { levels: Vec>, /// bitmap over odd values (index c >> 1) of cost <= 4 le4: Vec, + /// bitmap over odd values of cost exactly 3 + l3: Vec, + /// sh[k], k = 1..=3: bitmap over ALL 32-bit values of {+-(b << s) : b in L_k, s in 1..31} + sh: Vec>, + /// inverses of the level-2 values + inv2: Vec, counts: [u64; 5], } @@ -151,6 +157,15 @@ fn bit_get(bm: &[AtomicU64], c: u32) -> bool { let i = (c >> 1) as usize; bm[i >> 6].load(Ordering::Relaxed) >> (i & 63) & 1 == 1 } +/// Bitmaps over every 32-bit value (the shifted sets hold even values). +fn bit32_set(bm: &[AtomicU64], v: u32) { + let i = v as usize; + bm[i >> 6].fetch_or(1u64 << (i & 63), Ordering::Relaxed); +} +fn bit32_get(bm: &[AtomicU64], v: u32) -> bool { + let i = v as usize; + bm[i >> 6].load(Ordering::Relaxed) >> (i & 63) & 1 == 1 +} fn combine(a: u32, b: u32, out: &mut impl FnMut(u32)) { for s in 1..32u32 { @@ -161,6 +176,10 @@ fn combine(a: u32, b: u32, out: &mut impl FnMut(u32)) { } } +fn new_bitmap(words: usize) -> Vec { + (0..words).map(|_| AtomicU64::new(0)).collect() +} + impl Scm { fn build(threads: usize) -> Scm { let t0 = Instant::now(); @@ -199,7 +218,7 @@ impl Scm { levels.push(v); } // level 4 as a bitmap: (L3, L0), (L2, L1), (L1, L2), (L0, L3) combinations and the products L3 x L1, L2 x L2 - let le4: Vec = (0..(1usize << 25)).map(|_| AtomicU64::new(0)).collect(); + let le4 = new_bitmap(1usize << 25); for lv in &levels { for &c in lv { bit_set(&le4, c); @@ -253,7 +272,26 @@ impl Scm { } counts[4] = n4 - counts[..4].iter().sum::(); eprintln!("scm: level 4: {} values; cost <= 4: {} of 2^31 odd ({:.4}) ({:.1} s)", counts[4], n4, n4 as f64 / 2f64.powi(31), t0.elapsed().as_secs_f64()); - Scm { levels, le4, counts } + // the level-3 bitmap and the shifted sets + let l3 = new_bitmap(1usize << 25); + for &c in &levels[3] { + bit_set(&l3, c); + } + let mut sh: Vec> = vec![Vec::new()]; + for k in 1..=3usize { + let bm = new_bitmap(1usize << 26); + for &b in &levels[k] { + for s in 1..32u32 { + let v = b.wrapping_shl(s); + bit32_set(&bm, v); + bit32_set(&bm, 0u32.wrapping_sub(v)); + } + } + sh.push(bm); + } + let inv2: Vec = levels[2].iter().map(|&b| inv_mod(b)).collect(); + eprintln!("scm: shifted sets and inverses ready ({:.1} s)", t0.elapsed().as_secs_f64()); + Scm { levels, le4, l3, sh, inv2, counts } } fn in_level_le3(&self, c: u32) -> Option { @@ -265,7 +303,8 @@ impl Scm { None } - /// Certified adder count: exact for 0..=4; 5 when a decomposition over the exact sets exists; None otherwise. + /// Certified adder count: exact for 0..=4; 5 when a decomposition over the exact sets exists (the forms listed + /// below; `a = 1, b of cost 4 shifted` is not searched, so 5 is a partial certificate); None otherwise. fn certify(&self, c: u32) -> Option { if let Some(k) = self.in_level_le3(c) { return Some(k); @@ -273,7 +312,7 @@ impl Scm { if bit_get(&self.le4, c) { return Some(4); } - // 5 adders: c = a +- (1 << s) or (1 << s) - a with a of cost 4: a in {c -+ 2^s, 2^s - c} + // c = a +- (1 << s) or (1 << s) - a with a of cost 4 for s in 1..32u32 { let p = 1u32 << s; for a in [c.wrapping_sub(p), c.wrapping_add(p), p.wrapping_sub(c)] { @@ -282,32 +321,37 @@ impl Scm { } } } - // c = a * b, a of cost 4, b of cost 1: a = c * b^-1 + // c = a * b, a of cost 4, b of cost 1 for &b in &self.levels[1] { - let inv = inv_mod(b); - if bit_get(&self.le4, c.wrapping_mul(inv)) { + if bit_get(&self.le4, c.wrapping_mul(inv_mod(b))) { return Some(5); } } - // c = a +- (b << s) with a of cost 3 (set), b of cost 1; and a of cost 2, b of cost 2 - for (i, j) in [(3usize, 1usize), (2, 2), (1, 3)] { - for &b in &self.levels[j] { - for s in 1..32u32 { - let bs = b.wrapping_shl(s); - for a in [c.wrapping_sub(bs), c.wrapping_add(bs), bs.wrapping_sub(c)] { - if self.levels[i].binary_search(&a).is_ok() { - return Some(5); - } + // c = a +- (b << s) with a of cost i and b of cost j, i + j = 4: test (c - a) and (a - c) against the shifted + // set of level j, iterating the level-i values (90, 4,101 or 185,223 entries) + for (i, j) in [(1usize, 3usize), (2, 2)] { + for &a in &self.levels[i] { + if bit32_get(&self.sh[j], c.wrapping_sub(a)) || bit32_get(&self.sh[j], a.wrapping_sub(c)) { + return Some(5); + } + } + } + // (3, 1): iterate the shifted level-1 set instead (90 x 62 values) against the level-3 bitmap + for &b in &self.levels[1] { + for s in 1..32u32 { + let bs = b.wrapping_shl(s); + for a in [c.wrapping_sub(bs), c.wrapping_add(bs), bs.wrapping_sub(c)] { + if a & 1 == 1 && bit_get(&self.l3, a) { + return Some(5); } } } } - // c = a * b with a of cost 3, b of cost 2 (and 2, 3) - for (i, j) in [(3usize, 2usize), (2, 3)] { - for &b in &self.levels[j] { - if self.levels[i].binary_search(&c.wrapping_mul(inv_mod(b))).is_ok() { - return Some(5); - } + // c = a * b with a of cost 2, b of cost 3: b = c * a^-1 in the level-3 bitmap + for &ia in &self.inv2 { + let b = c.wrapping_mul(ia); + if bit_get(&self.l3, b) { + return Some(5); } } None