990 lines
40 KiB
Rust
990 lines
40 KiB
Rust
//! Attack pass F4: the weak-day census (`docs/plans/cryptanalysis.md` section 4.2 row F4, `funding.md` B2 rank 3 and
|
|
//! B5 rank 3). Every chain day `d` has the key `seed_words_from_bytes("igneum-day/" || d_le64)` (`bind::day_bytes`,
|
|
//! the interim day rule) and its mixer constants come from `MixParams::with_shape`, a SplitMix64 stream seeded from
|
|
//! `key[0] | key[1] << 32`: `ROT[0..7]` in 1..31, `MUL[0..15]` odd, `RC[0..15]`. This harness walks consecutive chain
|
|
//! days through the real draw code (the `igneum-pow` path dependency, nothing re-implemented) and classifies each
|
|
//! day's draw.
|
|
//!
|
|
//! The gain metrics, all exact and structural (what a datapath built for the day pays, in adder-equivalents per
|
|
//! mixer application; the verifier and every GPU pay the same ops on every day, so wall time cannot move):
|
|
//!
|
|
//! * M1, the per-day LUT datapath (an FPGA bitstream synthesised for the day, the only per-day attacker that
|
|
//! exists: a constant XOR is absorbed into the next LUT, a rotation by a constant is routing, a 32-bit add or
|
|
//! XOR is one 32-bit adder-equivalent, a multiply by a constant is `NAF(MUL) - 1` adders in canonical signed-digit
|
|
//! shift-add form): `cost = 32 adds + 32 xors + sum_i (naf(MUL_i) - 1)`. Gain of a day = the census median cost
|
|
//! over the day's cost. This is the generous bound: optimal single-constant multiplication is cheaper than NAF for
|
|
//! every constant, and a DSP-block multiply does not depend on the value at all.
|
|
//! * M2, the DSP-bound datapath (the multiplies in DSP blocks, value-independent): a word whose constant has NAF
|
|
//! weight at most 3 (two adders) moves to LUTs and frees its DSP, so gain = 16 / (16 - k) for k such words.
|
|
//! * ROT and RC classes: a constant rotation is wiring and a constant XOR is inverters on a per-day datapath, so
|
|
//! their value hands a datapath exactly 0 ops. They are censused as structure (counts against the analytic
|
|
//! expectation) and the worst members are measured for diffusion (`avalanche`), which bounds the only other
|
|
//! thing a rotation draw could move. A bit-exact verifier never lets a chip skip an application, however weak its
|
|
//! diffusion, so diffusion is reported and is not a gain.
|
|
//!
|
|
//! Commands:
|
|
//! attack-f4 census --from 20729 --count 16777216 --threads 12 [--out file] [--dedupe]
|
|
//! attack-f4 day --index 20729 one day's draw and classification
|
|
//! attack-f4 plant alleq|mul1|mul1all|rc0|rcrk0 the known-fail firings: the day 20729 draw with the planted field
|
|
//! attack-f4 expect --threads 12 exact per-word tables (NAF weight, popcount) over all 2^31 odd
|
|
//! constants, and the 16-fold convolution: the expected M1 tail
|
|
//! attack-f4 avalanche --index 20729 [--states 4096] single-application and two-application diffusion of a day
|
|
|
|
use igneum_pow::bind::day_bytes;
|
|
use igneum_pow::generator::V4_CLASS;
|
|
use igneum_pow::memhard::{mixer, round_key_mult, MixParams, Shape, ITEM_ROUNDS};
|
|
use igneum_pow::seed::{seed_words_from_bytes, SplitMix64};
|
|
use std::fmt::Write as _;
|
|
use std::io::Write as _;
|
|
|
|
/// The chain's genesis day index (`bind.rs` tests: day_index(0x1a0ff0f7c00) = 20,729, 3 October 2026).
|
|
const GENESIS_DAY: u64 = 20_729;
|
|
/// Mixer applications per item under class v4 (`Shape::mixers_per_item`): 9 x 8.
|
|
const APPLICATIONS_PER_ITEM: u64 = (ITEM_ROUNDS as u64 + 1) * 8;
|
|
/// Adds and XORs of one application outside the multiply layer: 8 quarter rounds x (4 adds + 4 xors).
|
|
const QR_ADDS_XORS: u32 = 64;
|
|
/// The gate's gain threshold (plan 1.4 (3), B5 rank 3).
|
|
const GAIN_GATE: f64 = 1.1;
|
|
/// M2: a constant of NAF weight at most this is cheaper in LUTs than in a DSP block.
|
|
const M2_NAF_CEIL: u32 = 3;
|
|
|
|
// ------------------------------------------------------------------------------------------------------------
|
|
// The day
|
|
// ------------------------------------------------------------------------------------------------------------
|
|
|
|
fn v4_shape() -> Shape {
|
|
let s = Shape::for_class(&V4_CLASS);
|
|
assert_eq!(s.mixer_mult, 8, "class v4 is the x8 mixer");
|
|
assert_eq!(s.derive_len, 0, "class v4 has no derivation program: the fixed mixer with drawn constants");
|
|
s
|
|
}
|
|
|
|
/// The chain day's key and its draw through the real code.
|
|
fn params_of_day(d: u64) -> MixParams {
|
|
MixParams::with_shape(seed_words_from_bytes(&day_bytes(d)), v4_shape())
|
|
}
|
|
|
|
fn seed64(key: &[u32; 8]) -> u64 {
|
|
key[0] as u64 | ((key[1] as u64) << 32)
|
|
}
|
|
|
|
/// Non-adjacent-form weight of a 32-bit constant (the number of nonzero signed digits).
|
|
fn naf_weight(v: u32) -> u32 {
|
|
let mut n = v as u64;
|
|
let mut w = 0;
|
|
while n != 0 {
|
|
if n & 1 == 1 {
|
|
// digit +1 when n = 1 mod 4, -1 when n = 3 mod 4
|
|
if n & 3 == 3 {
|
|
n += 1;
|
|
} else {
|
|
n -= 1;
|
|
}
|
|
w += 1;
|
|
}
|
|
n >>= 1;
|
|
}
|
|
w
|
|
}
|
|
|
|
/// Round keys of the 72 applications of an item under m = 8: `round_key(r * 8 + j)`, r in 0..=8, j in 0..8.
|
|
fn round_keys() -> [u32; 72] {
|
|
let mut k = [0u32; 72];
|
|
for r in 0..=ITEM_ROUNDS {
|
|
for j in 0..8 {
|
|
k[r * 8 + j] = round_key_mult(r, j, 8);
|
|
}
|
|
}
|
|
k
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
struct DayClass {
|
|
// ROT
|
|
rot_distinct: u32,
|
|
rot_all_equal: bool,
|
|
rot_max_mult: u32,
|
|
rot_comp_same_word: bool,
|
|
rot_comp_any: bool,
|
|
rot_small: u32,
|
|
rot_byte: u32,
|
|
// MUL
|
|
naf: [u32; 16],
|
|
naf_sum: u32,
|
|
naf_min: u32,
|
|
mul_one: u32,
|
|
mul_minus_one: u32,
|
|
mul_pop_le2: u32,
|
|
mul_pop_le4: u32,
|
|
mul_pop_le8: u32,
|
|
mul_naf_le2: u32,
|
|
mul_naf_le3: u32,
|
|
mul_naf_le4: u32,
|
|
mul_small: u32,
|
|
mul_dup: bool,
|
|
// RC
|
|
rc_zero: u32,
|
|
rc_pop_ext: u32,
|
|
rc_rk_zero: u32,
|
|
rc_dup: bool,
|
|
// gains
|
|
cost_m1: u32,
|
|
m2_k: u32,
|
|
}
|
|
|
|
fn classify(mp: &MixParams, rks: &[u32; 72]) -> DayClass {
|
|
let mut c = DayClass::default();
|
|
// ROT
|
|
let mut seen = [0u32; 32];
|
|
for &r in &mp.rot {
|
|
assert!((1..=31).contains(&r));
|
|
seen[r as usize] += 1;
|
|
if matches!(r, 1 | 2 | 30 | 31) {
|
|
c.rot_small += 1;
|
|
}
|
|
if matches!(r, 8 | 16 | 24) {
|
|
c.rot_byte += 1;
|
|
}
|
|
}
|
|
c.rot_distinct = seen.iter().filter(|&&n| n > 0).count() as u32;
|
|
c.rot_max_mult = *seen.iter().max().unwrap();
|
|
c.rot_all_equal = c.rot_distinct == 1;
|
|
// the same-word pairs of a quarter round: s[d] takes ROT[0] then ROT[2], s[b] takes ROT[1] then ROT[3]; the
|
|
// diagonal round the same with ROT[4..7]
|
|
for (a, b) in [(0, 2), (1, 3), (4, 6), (5, 7)] {
|
|
if mp.rot[a] + mp.rot[b] == 32 {
|
|
c.rot_comp_same_word = true;
|
|
}
|
|
}
|
|
for a in 0..8 {
|
|
for b in a + 1..8 {
|
|
if mp.rot[a] + mp.rot[b] == 32 {
|
|
c.rot_comp_any = true;
|
|
}
|
|
}
|
|
}
|
|
// MUL
|
|
c.naf_min = u32::MAX;
|
|
for i in 0..16 {
|
|
let m = mp.mul[i];
|
|
assert!(m & 1 == 1);
|
|
let w = naf_weight(m);
|
|
c.naf[i] = w;
|
|
c.naf_sum += w;
|
|
c.naf_min = c.naf_min.min(w);
|
|
let p = m.count_ones();
|
|
if m == 1 {
|
|
c.mul_one += 1;
|
|
}
|
|
if m == u32::MAX {
|
|
c.mul_minus_one += 1;
|
|
}
|
|
if p <= 2 {
|
|
c.mul_pop_le2 += 1;
|
|
}
|
|
if p <= 4 {
|
|
c.mul_pop_le4 += 1;
|
|
}
|
|
if p <= 8 {
|
|
c.mul_pop_le8 += 1;
|
|
}
|
|
if w <= 2 {
|
|
c.mul_naf_le2 += 1;
|
|
}
|
|
if w <= 3 {
|
|
c.mul_naf_le3 += 1;
|
|
}
|
|
if w <= 4 {
|
|
c.mul_naf_le4 += 1;
|
|
}
|
|
if m < 256 {
|
|
c.mul_small += 1;
|
|
}
|
|
for j in 0..i {
|
|
if mp.mul[j] == m {
|
|
c.mul_dup = true;
|
|
}
|
|
}
|
|
}
|
|
c.cost_m1 = QR_ADDS_XORS + c.naf_sum - 16;
|
|
c.m2_k = c.mul_naf_le3;
|
|
// RC
|
|
for i in 0..16 {
|
|
let r = mp.rc[i];
|
|
if r == 0 {
|
|
c.rc_zero += 1;
|
|
}
|
|
let p = r.count_ones();
|
|
if p <= 4 || p >= 28 {
|
|
c.rc_pop_ext += 1;
|
|
}
|
|
for &rk in rks.iter() {
|
|
if r.wrapping_add(rk) == 0 {
|
|
c.rc_rk_zero += 1;
|
|
}
|
|
}
|
|
for j in 0..i {
|
|
if mp.rc[j] == r {
|
|
c.rc_dup = true;
|
|
}
|
|
}
|
|
}
|
|
c
|
|
}
|
|
|
|
fn m2_gain(k: u32) -> f64 {
|
|
16.0 / (16.0 - k as f64)
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------------------
|
|
// The tally
|
|
// ------------------------------------------------------------------------------------------------------------
|
|
|
|
/// The worst member of a class: the lowest M1 cost among the days in it (ties: the earliest day).
|
|
#[derive(Clone, Copy, Debug)]
|
|
struct Worst {
|
|
day: u64,
|
|
cost: u32,
|
|
}
|
|
|
|
impl Worst {
|
|
fn none() -> Self {
|
|
Worst { day: u64::MAX, cost: u32::MAX }
|
|
}
|
|
fn offer(&mut self, day: u64, cost: u32) {
|
|
if cost < self.cost || (cost == self.cost && day < self.day) {
|
|
*self = Worst { day, cost };
|
|
}
|
|
}
|
|
fn merge(&mut self, o: &Worst) {
|
|
if o.day != u64::MAX {
|
|
self.offer(o.day, o.cost);
|
|
}
|
|
}
|
|
}
|
|
|
|
const CLASSES: &[&str] = &[
|
|
"ROT all equal",
|
|
"ROT distinct <= 3",
|
|
"ROT distinct <= 4",
|
|
"ROT max multiplicity >= 4",
|
|
"ROT same-word pair sums to 32",
|
|
"ROT any pair sums to 32",
|
|
"ROT all 8 in {1,2,30,31}",
|
|
"ROT >= 6 in {1,2,30,31}",
|
|
"ROT >= 4 in {1,2,30,31}",
|
|
"ROT >= 4 in {8,16,24}",
|
|
"MUL any = 1",
|
|
"MUL any = 2^32-1",
|
|
"MUL any popcount <= 2",
|
|
"MUL any popcount <= 4",
|
|
"MUL any popcount <= 8",
|
|
"MUL any NAF weight <= 2",
|
|
"MUL any NAF weight <= 3",
|
|
"MUL any NAF weight <= 4",
|
|
"MUL any < 256",
|
|
"MUL two equal",
|
|
"MUL M2 k >= 2 (gain >= 1.14x)",
|
|
"RC any = 0",
|
|
"RC any popcount <= 4 or >= 28",
|
|
"RC + rk = 0 for any of the 72 keys",
|
|
"RC two equal",
|
|
];
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct Tally {
|
|
n: u64,
|
|
class_count: Vec<u64>,
|
|
class_worst: Vec<Worst>,
|
|
cost_hist: Vec<u64>,
|
|
naf_sum_hist: Vec<u64>,
|
|
naf_min_hist: Vec<u64>,
|
|
rot_distinct_hist: [u64; 9],
|
|
rot_small_hist: [u64; 9],
|
|
rot_byte_hist: [u64; 9],
|
|
rot_mult_hist: [u64; 9],
|
|
m2_k_hist: [u64; 17],
|
|
/// The 16 lowest-cost days seen (cost, day), sorted ascending.
|
|
lowest: Vec<(u32, u64)>,
|
|
highest: Vec<(u32, u64)>,
|
|
seeds: Vec<u64>,
|
|
}
|
|
|
|
impl Tally {
|
|
fn new(dedupe: bool, cap: usize) -> Self {
|
|
Tally {
|
|
n: 0,
|
|
class_count: vec![0; CLASSES.len()],
|
|
class_worst: vec![Worst::none(); CLASSES.len()],
|
|
cost_hist: vec![0; 400],
|
|
naf_sum_hist: vec![0; 400],
|
|
naf_min_hist: vec![0; 40],
|
|
rot_distinct_hist: [0; 9],
|
|
rot_small_hist: [0; 9],
|
|
rot_byte_hist: [0; 9],
|
|
rot_mult_hist: [0; 9],
|
|
m2_k_hist: [0; 17],
|
|
lowest: Vec::new(),
|
|
highest: Vec::new(),
|
|
seeds: if dedupe { Vec::with_capacity(cap) } else { Vec::new() },
|
|
}
|
|
}
|
|
|
|
fn flags(c: &DayClass) -> [bool; 25] {
|
|
[
|
|
c.rot_all_equal,
|
|
c.rot_distinct <= 3,
|
|
c.rot_distinct <= 4,
|
|
c.rot_max_mult >= 4,
|
|
c.rot_comp_same_word,
|
|
c.rot_comp_any,
|
|
c.rot_small == 8,
|
|
c.rot_small >= 6,
|
|
c.rot_small >= 4,
|
|
c.rot_byte >= 4,
|
|
c.mul_one > 0,
|
|
c.mul_minus_one > 0,
|
|
c.mul_pop_le2 > 0,
|
|
c.mul_pop_le4 > 0,
|
|
c.mul_pop_le8 > 0,
|
|
c.mul_naf_le2 > 0,
|
|
c.mul_naf_le3 > 0,
|
|
c.mul_naf_le4 > 0,
|
|
c.mul_small > 0,
|
|
c.mul_dup,
|
|
c.m2_k >= 2,
|
|
c.rc_zero > 0,
|
|
c.rc_pop_ext > 0,
|
|
c.rc_rk_zero > 0,
|
|
c.rc_dup,
|
|
]
|
|
}
|
|
|
|
fn add(&mut self, day: u64, mp: &MixParams, c: &DayClass, dedupe: bool) {
|
|
self.n += 1;
|
|
let f = Self::flags(c);
|
|
assert_eq!(f.len(), CLASSES.len());
|
|
for (i, &on) in f.iter().enumerate() {
|
|
if on {
|
|
self.class_count[i] += 1;
|
|
self.class_worst[i].offer(day, c.cost_m1);
|
|
}
|
|
}
|
|
self.cost_hist[c.cost_m1 as usize] += 1;
|
|
self.naf_sum_hist[c.naf_sum as usize] += 1;
|
|
self.naf_min_hist[c.naf_min as usize] += 1;
|
|
self.rot_distinct_hist[c.rot_distinct as usize] += 1;
|
|
self.rot_small_hist[c.rot_small as usize] += 1;
|
|
self.rot_byte_hist[c.rot_byte as usize] += 1;
|
|
self.rot_mult_hist[c.rot_max_mult as usize] += 1;
|
|
self.m2_k_hist[c.m2_k as usize] += 1;
|
|
push_sorted(&mut self.lowest, (c.cost_m1, day), 16, true);
|
|
push_sorted(&mut self.highest, (c.cost_m1, day), 4, false);
|
|
if dedupe {
|
|
self.seeds.push(seed64(&mp.key));
|
|
}
|
|
}
|
|
|
|
fn merge(&mut self, o: Tally) {
|
|
self.n += o.n;
|
|
for i in 0..CLASSES.len() {
|
|
self.class_count[i] += o.class_count[i];
|
|
self.class_worst[i].merge(&o.class_worst[i]);
|
|
}
|
|
for (a, b) in self.cost_hist.iter_mut().zip(o.cost_hist.iter()) {
|
|
*a += b;
|
|
}
|
|
for (a, b) in self.naf_sum_hist.iter_mut().zip(o.naf_sum_hist.iter()) {
|
|
*a += b;
|
|
}
|
|
for (a, b) in self.naf_min_hist.iter_mut().zip(o.naf_min_hist.iter()) {
|
|
*a += b;
|
|
}
|
|
for i in 0..9 {
|
|
self.rot_distinct_hist[i] += o.rot_distinct_hist[i];
|
|
self.rot_small_hist[i] += o.rot_small_hist[i];
|
|
self.rot_byte_hist[i] += o.rot_byte_hist[i];
|
|
self.rot_mult_hist[i] += o.rot_mult_hist[i];
|
|
}
|
|
for i in 0..17 {
|
|
self.m2_k_hist[i] += o.m2_k_hist[i];
|
|
}
|
|
for e in o.lowest {
|
|
push_sorted(&mut self.lowest, e, 16, true);
|
|
}
|
|
for e in o.highest {
|
|
push_sorted(&mut self.highest, e, 4, false);
|
|
}
|
|
self.seeds.extend(o.seeds);
|
|
}
|
|
}
|
|
|
|
/// Keep the `cap` smallest (ascending) or largest (descending) entries.
|
|
fn push_sorted(v: &mut Vec<(u32, u64)>, e: (u32, u64), cap: usize, ascending: bool) {
|
|
if v.len() == cap {
|
|
let last = *v.last().unwrap();
|
|
let keep = if ascending { e < last } else { e > last };
|
|
if !keep {
|
|
return;
|
|
}
|
|
v.pop();
|
|
}
|
|
let pos = if ascending { v.partition_point(|x| *x < e) } else { v.partition_point(|x| *x > e) };
|
|
v.insert(pos, e);
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------------------
|
|
// Analytic expectations (per day, independent draws; the modulo-31 bias of `below` is 2^-64 per value and ignored)
|
|
// ------------------------------------------------------------------------------------------------------------
|
|
|
|
fn ln_choose(n: u64, k: u64) -> f64 {
|
|
let lg = |x: u64| -> f64 { (1..=x).map(|i| (i as f64).ln()).sum() };
|
|
lg(n) - lg(k) - lg(n - k)
|
|
}
|
|
|
|
fn binom_tail(n: u64, p: f64, k_min: u64) -> f64 {
|
|
(k_min..=n).map(|k| (ln_choose(n, k) + (k as f64) * p.ln() + ((n - k) as f64) * (1.0 - p).ln()).exp()).sum()
|
|
}
|
|
|
|
/// P(d distinct values among 8 uniform draws from 31): S(8, d) x 31 falling d / 31^8.
|
|
fn p_rot_distinct(d: u32) -> f64 {
|
|
// Stirling numbers of the second kind S(8, d)
|
|
let mut s = vec![vec![0f64; 9]; 9];
|
|
s[0][0] = 1.0;
|
|
for n in 1..=8 {
|
|
for k in 1..=n {
|
|
s[n][k] = (k as f64) * s[n - 1][k] + s[n - 1][k - 1];
|
|
}
|
|
}
|
|
let mut falling = 1.0;
|
|
for i in 0..d {
|
|
falling *= (31 - i) as f64;
|
|
}
|
|
s[8][d as usize] * falling / 31f64.powi(8)
|
|
}
|
|
|
|
/// P(max multiplicity >= 4 among 8 draws from 31), by enumeration of the multiplicity patterns is long; the
|
|
/// census gives the exact count, so this is the first-order bound: C(8,4) x 31 x 31^-4 (approximate).
|
|
fn p_rot_mult4_approx() -> f64 {
|
|
70.0 * 31.0 / 31f64.powi(4)
|
|
}
|
|
|
|
fn p_any_of(n: u64, p: f64) -> f64 {
|
|
1.0 - (1.0 - p).powi(n as i32)
|
|
}
|
|
|
|
fn one_in(p: f64) -> String {
|
|
if p <= 0.0 {
|
|
"0".into()
|
|
} else {
|
|
format!("1 in {:.3e}", 1.0 / p)
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------------------
|
|
// Printing a day
|
|
// ------------------------------------------------------------------------------------------------------------
|
|
|
|
fn hex_bytes(b: &[u8]) -> String {
|
|
b.iter().map(|x| format!("{x:02x}")).collect()
|
|
}
|
|
|
|
fn describe(day: u64, mp: &MixParams, c: &DayClass, median_cost: Option<u32>) -> String {
|
|
let mut s = String::new();
|
|
let _ = writeln!(s, "day index {day} (genesis + {}), day bytes {} , seed64 {:016x}", day as i64 - GENESIS_DAY as i64, hex_bytes(&day_bytes(day)), seed64(&mp.key));
|
|
let _ = writeln!(s, "key {}", mp.key.iter().map(|w| format!("{w:08x}")).collect::<Vec<_>>().join(" "));
|
|
let _ = writeln!(s, "ROT {:?} distinct {} max multiplicity {} small {} byte-aligned {} same-word pair 32 {} any pair 32 {}", mp.rot, c.rot_distinct, c.rot_max_mult, c.rot_small, c.rot_byte, c.rot_comp_same_word, c.rot_comp_any);
|
|
let _ = writeln!(s, "MUL {}", mp.mul.iter().map(|w| format!("{w:08x}")).collect::<Vec<_>>().join(" "));
|
|
let _ = writeln!(s, "NAF {:?} sum {} min {} =1 {} =-1 {} pop<=4 {} naf<=3 {} <256 {} dup {}", c.naf, c.naf_sum, c.naf_min, c.mul_one, c.mul_minus_one, c.mul_pop_le4, c.mul_naf_le3, c.mul_small, c.mul_dup);
|
|
let _ = writeln!(s, "RC {}", mp.rc.iter().map(|w| format!("{w:08x}")).collect::<Vec<_>>().join(" "));
|
|
let _ = writeln!(s, "RC zero {} pop<=4|>=28 {} rc+rk=0 {} dup {}", c.rc_zero, c.rc_pop_ext, c.rc_rk_zero, c.rc_dup);
|
|
let _ = writeln!(s, "M1 cost {} adder-equivalents per application ({} per item, 72 applications); M2 k {} (gain {:.3}x)", c.cost_m1, c.cost_m1 as u64 * APPLICATIONS_PER_ITEM, c.m2_k, m2_gain(c.m2_k));
|
|
if let Some(m) = median_cost {
|
|
let _ = writeln!(s, "M1 gain against the census median {m}: {:.4}x", m as f64 / c.cost_m1 as f64);
|
|
}
|
|
let flags: Vec<&str> = Tally::flags(c).iter().zip(CLASSES.iter()).filter(|(f, _)| **f).map(|(_, n)| *n).collect();
|
|
let _ = writeln!(s, "classes: {}", if flags.is_empty() { "none".to_string() } else { flags.join("; ") });
|
|
s
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------------------
|
|
// Commands
|
|
// ------------------------------------------------------------------------------------------------------------
|
|
|
|
fn arg(args: &[String], name: &str) -> Option<String> {
|
|
args.iter().position(|a| a == name).and_then(|i| args.get(i + 1).cloned())
|
|
}
|
|
|
|
fn census(args: &[String]) {
|
|
let from: u64 = arg(args, "--from").map(|v| v.parse().unwrap()).unwrap_or(GENESIS_DAY);
|
|
let count: u64 = arg(args, "--count").map(|v| v.parse().unwrap()).unwrap_or(1 << 24);
|
|
let threads: usize = arg(args, "--threads").map(|v| v.parse().unwrap()).unwrap_or(12);
|
|
let out = arg(args, "--out");
|
|
let dedupe = args.iter().any(|a| a == "--dedupe");
|
|
let shape = v4_shape();
|
|
let rks = round_keys();
|
|
let t0 = std::time::Instant::now();
|
|
let chunk = count.div_ceil(threads as u64);
|
|
let tallies: Vec<Tally> = std::thread::scope(|sc| {
|
|
let hs: Vec<_> = (0..threads)
|
|
.map(|t| {
|
|
let rks = &rks;
|
|
sc.spawn(move || {
|
|
let lo = from + chunk * t as u64;
|
|
let hi = (lo + chunk).min(from + count);
|
|
let mut tally = Tally::new(dedupe, (hi.saturating_sub(lo)) as usize);
|
|
for d in lo..hi {
|
|
let mp = params_of_day(d);
|
|
debug_assert_eq!(mp.shape, shape);
|
|
let c = classify(&mp, rks);
|
|
tally.add(d, &mp, &c, dedupe);
|
|
}
|
|
tally
|
|
})
|
|
})
|
|
.collect();
|
|
hs.into_iter().map(|h| h.join().unwrap()).collect()
|
|
});
|
|
let mut all = Tally::new(false, 0);
|
|
for t in tallies {
|
|
all.merge(t);
|
|
}
|
|
let secs = t0.elapsed().as_secs_f64();
|
|
assert_eq!(all.n, count);
|
|
|
|
// the median cost and the gate fractions
|
|
let total = all.n;
|
|
let mut acc = 0u64;
|
|
let mut median = 0u32;
|
|
for (c, &n) in all.cost_hist.iter().enumerate() {
|
|
acc += n;
|
|
if acc * 2 >= total {
|
|
median = c as u32;
|
|
break;
|
|
}
|
|
}
|
|
let mean = all.cost_hist.iter().enumerate().map(|(c, &n)| c as f64 * n as f64).sum::<f64>() / total as f64;
|
|
let var = all.cost_hist.iter().enumerate().map(|(c, &n)| (c as f64 - mean).powi(2) * n as f64).sum::<f64>() / total as f64;
|
|
let gate_cost = |reference: f64| -> u32 { (reference / GAIN_GATE).floor() as u32 }; // cost <= this gives gain >= 1.1x... strictly over: cost < reference/1.1
|
|
let over = |reference: f64| -> u64 {
|
|
all.cost_hist.iter().enumerate().filter(|(c, _)| (*c as f64) * GAIN_GATE < reference).map(|(_, &n)| n).sum()
|
|
};
|
|
let over_median = over(median as f64);
|
|
let over_mean = over(mean);
|
|
let m2_over: u64 = all.m2_k_hist.iter().enumerate().filter(|(k, _)| m2_gain(*k as u32) > GAIN_GATE).map(|(_, &n)| n).sum();
|
|
|
|
let mut r = String::new();
|
|
let _ = writeln!(r, "# attack-f4 census: {count} chain days from day index {from} (genesis {GENESIS_DAY}), class v4 shape (mixer x{}, cache 2^{}), {threads} threads, {secs:.1} s", shape.mixer_mult, shape.cache_log2_words);
|
|
let _ = writeln!(r, "draw: MixParams::with_shape(seed_words_from_bytes(bind::day_bytes(d)), Shape::for_class(&V4_CLASS)); seed64 = key[0] | key[1] << 32");
|
|
let _ = writeln!(r);
|
|
let _ = writeln!(r, "## Gate (plan 1.4 (3), B5 rank 3): fraction of days with any gain over {GAIN_GATE}x under 2^-20 = {:.3e}", 2f64.powi(-20));
|
|
let _ = writeln!(r);
|
|
let _ = writeln!(r, "| Metric | Reference | Days over {GAIN_GATE}x | Fraction | Against 2^-20 |");
|
|
let _ = writeln!(r, "|---|---|---|---|---|");
|
|
let frac = |n: u64| n as f64 / total as f64;
|
|
let vs = |n: u64| if frac(n) < 2f64.powi(-20) { "under" } else { "OVER" };
|
|
let _ = writeln!(r, "| M1 per-day LUT datapath, adders per application | median cost {median} (gain over {GAIN_GATE}x = cost under {}) | {over_median} | {:.3e} | {} |", gate_cost(median as f64) + 1, frac(over_median), vs(over_median));
|
|
let _ = writeln!(r, "| M1 against the mean cost {mean:.2} (sd {:.2}) | cost under {:.2} | {over_mean} | {:.3e} | {} |", var.sqrt(), mean / GAIN_GATE, frac(over_mean), vs(over_mean));
|
|
let _ = writeln!(r, "| M2 DSP-bound datapath, 16/(16-k) with k = words of NAF weight <= {M2_NAF_CEIL} | k >= 2 | {m2_over} | {:.3e} | {} |", frac(m2_over), vs(m2_over));
|
|
let _ = writeln!(r, "| ROT value (wiring) and RC value (inverters) on a per-day datapath | exact 0 ops moved on every day | 0 | 0 | under |");
|
|
let _ = writeln!(r);
|
|
let _ = writeln!(r, "M1 cost = {QR_ADDS_XORS} + sum(NAF(MUL_i) - 1); mean {mean:.3}, sd {:.3}, median {median}, min {} (day {}), max {} (day {})", var.sqrt(), all.lowest[0].0, all.lowest[0].1, all.highest[0].0, all.highest[0].1);
|
|
let _ = writeln!(r);
|
|
|
|
// the classes
|
|
let p_mul1 = p_any_of(16, 2f64.powi(-31));
|
|
let p_small = p_any_of(16, 128.0 / 2f64.powi(31));
|
|
let p_rc0 = p_any_of(16, 2f64.powi(-32));
|
|
let p_rcrk = p_any_of(16, 72.0 / 2f64.powi(32));
|
|
let pop_le = |k: u32| -> f64 { (0..=k).map(|i| ln_choose(32, i as u64).exp()).sum::<f64>() };
|
|
let p_rc_pop = p_any_of(16, 2.0 * pop_le(4) / 2f64.powi(32));
|
|
// odd constants with popcount <= k: the low bit is set, so C(31, i) for the other i < k bits
|
|
let odd_pop_le = |k: u32| -> f64 { (0..k).map(|i| ln_choose(31, i as u64).exp()).sum::<f64>() / 2f64.powi(31) };
|
|
let p_dup16 = |space: f64| -> f64 { 1.0 - (1..16).map(|i| 1.0 - i as f64 / space).product::<f64>() };
|
|
let expectations: Vec<Option<f64>> = vec![
|
|
Some(31f64.powi(-7)),
|
|
Some((1..=3).map(p_rot_distinct).sum()),
|
|
Some((1..=4).map(p_rot_distinct).sum()),
|
|
Some(p_rot_mult4_approx()),
|
|
Some(p_any_of(4, 1.0 / 31.0)),
|
|
Some(p_any_of(28, 1.0 / 31.0)),
|
|
Some((4f64 / 31.0).powi(8)),
|
|
Some(binom_tail(8, 4.0 / 31.0, 6)),
|
|
Some(binom_tail(8, 4.0 / 31.0, 4)),
|
|
Some(binom_tail(8, 3.0 / 31.0, 4)),
|
|
Some(p_mul1),
|
|
Some(p_mul1),
|
|
Some(p_any_of(16, odd_pop_le(2))),
|
|
Some(p_any_of(16, odd_pop_le(4))),
|
|
Some(p_any_of(16, odd_pop_le(8))),
|
|
None,
|
|
None,
|
|
None,
|
|
Some(p_small),
|
|
Some(p_dup16(2f64.powi(31))),
|
|
None,
|
|
Some(p_rc0),
|
|
Some(p_rc_pop),
|
|
Some(p_rcrk),
|
|
Some(p_dup16(2f64.powi(32))),
|
|
];
|
|
let _ = writeln!(r, "## Classes over {count} days");
|
|
let _ = writeln!(r);
|
|
let _ = writeln!(r, "| Class | Count | Fraction | Expected per day (analytic) | Expected count | Worst member (day, M1 cost, M1 gain vs median, M2 gain) |");
|
|
let _ = writeln!(r, "|---|---|---|---|---|---|");
|
|
for (i, name) in CLASSES.iter().enumerate() {
|
|
let n = all.class_count[i];
|
|
let w = all.class_worst[i];
|
|
let worst = if w.day == u64::MAX {
|
|
"none".to_string()
|
|
} else {
|
|
let c = classify(¶ms_of_day(w.day), &rks);
|
|
format!("day {} , cost {} , {:.4}x , {:.3}x", w.day, w.cost, median as f64 / w.cost as f64, m2_gain(c.m2_k))
|
|
};
|
|
let (ep, ec) = match expectations[i] {
|
|
Some(p) => (format!("{p:.3e} ({})", one_in(p)), format!("{:.3}", p * total as f64)),
|
|
None => ("see `expect`".to_string(), "see `expect`".to_string()),
|
|
};
|
|
let _ = writeln!(r, "| {name} | {n} | {:.3e} | {ep} | {ec} | {worst} |", frac(n));
|
|
}
|
|
let _ = writeln!(r);
|
|
let _ = writeln!(r, "## Histograms");
|
|
let _ = writeln!(r);
|
|
let _ = writeln!(r, "| ROT distinct amounts | Count | Fraction | Expected (S(8,d) 31_d / 31^8) |");
|
|
let _ = writeln!(r, "|---|---|---|---|");
|
|
for d in 1..=8 {
|
|
let _ = writeln!(r, "| {d} | {} | {:.4e} | {:.4e} |", all.rot_distinct_hist[d], frac(all.rot_distinct_hist[d]), p_rot_distinct(d as u32));
|
|
}
|
|
let _ = writeln!(r);
|
|
let _ = writeln!(r, "| ROT amounts in {{1,2,30,31}} | Count | Expected Binomial(8, 4/31) | ROT amounts in {{8,16,24}} | Count | Expected Binomial(8, 3/31) | ROT max multiplicity | Count |");
|
|
let _ = writeln!(r, "|---|---|---|---|---|---|---|---|");
|
|
for k in 0..=8 {
|
|
let e1 = binom_tail(8, 4.0 / 31.0, k) - binom_tail(8, 4.0 / 31.0, k + 1);
|
|
let e2 = binom_tail(8, 3.0 / 31.0, k) - binom_tail(8, 3.0 / 31.0, k + 1);
|
|
let _ = writeln!(r, "| {k} | {} | {:.1} | {k} | {} | {:.1} | {k} | {} |", all.rot_small_hist[k as usize], e1 * total as f64, all.rot_byte_hist[k as usize], e2 * total as f64, all.rot_mult_hist[k as usize]);
|
|
}
|
|
let _ = writeln!(r);
|
|
let _ = writeln!(r, "| M2 k (words of NAF weight <= {M2_NAF_CEIL}) | Count | Gain 16/(16-k) |");
|
|
let _ = writeln!(r, "|---|---|---|");
|
|
for k in 0..=16 {
|
|
if all.m2_k_hist[k] > 0 || k <= 3 {
|
|
let _ = writeln!(r, "| {k} | {} | {:.3}x |", all.m2_k_hist[k], m2_gain(k as u32));
|
|
}
|
|
}
|
|
let _ = writeln!(r);
|
|
let _ = writeln!(r, "| Minimum NAF weight over the 16 MUL | Count |");
|
|
let _ = writeln!(r, "|---|---|");
|
|
for (w, &n) in all.naf_min_hist.iter().enumerate() {
|
|
if n > 0 {
|
|
let _ = writeln!(r, "| {w} | {n} |");
|
|
}
|
|
}
|
|
let _ = writeln!(r);
|
|
let _ = writeln!(r, "| M1 cost per application | Count | Cumulative fraction | M1 gain vs median |");
|
|
let _ = writeln!(r, "|---|---|---|---|");
|
|
let mut cum = 0u64;
|
|
for (c, &n) in all.cost_hist.iter().enumerate() {
|
|
if n > 0 {
|
|
cum += n;
|
|
let _ = writeln!(r, "| {c} | {n} | {:.4e} | {:.4}x |", frac(cum), median as f64 / c as f64);
|
|
}
|
|
}
|
|
let _ = writeln!(r);
|
|
let _ = writeln!(r, "## The 16 lowest-cost days (M1)");
|
|
let _ = writeln!(r);
|
|
for &(cost, day) in &all.lowest {
|
|
let mp = params_of_day(day);
|
|
let c = classify(&mp, &rks);
|
|
let _ = writeln!(r, "- day {day}: cost {cost}, gain {:.4}x vs median, NAF sum {}, M2 k {}, day-hex {}", median as f64 / cost as f64, c.naf_sum, c.m2_k, hex_bytes(&day_bytes(day)));
|
|
}
|
|
if dedupe {
|
|
all.seeds.sort_unstable();
|
|
let before = all.seeds.len();
|
|
all.seeds.dedup();
|
|
let _ = writeln!(r);
|
|
let _ = writeln!(r, "## 64-bit seeding: {} days, {} distinct 64-bit stream seeds ({} collisions; expected C(n,2)/2^64 = {:.3e})", before, all.seeds.len(), before - all.seeds.len(), (before as f64) * (before as f64 - 1.0) / 2.0 / 2f64.powi(64));
|
|
}
|
|
let _ = writeln!(r);
|
|
let _ = writeln!(r, "## Worst member of every class, in full");
|
|
let _ = writeln!(r);
|
|
let mut shown: Vec<u64> = Vec::new();
|
|
for (i, name) in CLASSES.iter().enumerate() {
|
|
let w = all.class_worst[i];
|
|
if w.day == u64::MAX || shown.contains(&w.day) {
|
|
continue;
|
|
}
|
|
shown.push(w.day);
|
|
let mp = params_of_day(w.day);
|
|
let c = classify(&mp, &rks);
|
|
let _ = writeln!(r, "### {name}: day {}", w.day);
|
|
let _ = writeln!(r, "```");
|
|
let _ = write!(r, "{}", describe(w.day, &mp, &c, Some(median)));
|
|
let _ = writeln!(r, "```");
|
|
}
|
|
print!("{r}");
|
|
if let Some(p) = out {
|
|
std::fs::File::create(&p).unwrap().write_all(r.as_bytes()).unwrap();
|
|
eprintln!("written {p}");
|
|
}
|
|
}
|
|
|
|
fn day_cmd(args: &[String]) {
|
|
let d: u64 = arg(args, "--index").map(|v| v.parse().unwrap()).unwrap_or(GENESIS_DAY);
|
|
let median: Option<u32> = arg(args, "--median").map(|v| v.parse().unwrap());
|
|
let mp = params_of_day(d);
|
|
let c = classify(&mp, &round_keys());
|
|
print!("{}", describe(d, &mp, &c, median));
|
|
}
|
|
|
|
/// The known-fail firings: the genesis day's draw with one field forced through this crate's own hook (the
|
|
/// `MixParams` fields are public; `igneum-pow` is untouched). Exit 0 when the classifier flags the plant and the
|
|
/// gain metric that the plant moves reads over the gate.
|
|
fn plant(args: &[String]) {
|
|
let what = args.get(2).cloned().unwrap_or_default();
|
|
let median: u32 = arg(args, "--median").map(|v| v.parse().unwrap()).unwrap_or(221);
|
|
let rks = round_keys();
|
|
let mut mp = params_of_day(GENESIS_DAY);
|
|
let before = classify(&mp, &rks);
|
|
println!("before the plant (day {GENESIS_DAY}):");
|
|
print!("{}", describe(GENESIS_DAY, &mp, &before, Some(median)));
|
|
let (flag_name, gain_metric): (&str, &str) = match what.as_str() {
|
|
"alleq" => {
|
|
mp.rot = [7; 8];
|
|
("ROT all equal", "structure (0 ops on a per-day datapath by construction; diffusion in `avalanche`)")
|
|
}
|
|
"mul1" => {
|
|
mp.mul[5] = 1;
|
|
("MUL any = 1", "M1")
|
|
}
|
|
"mul1all" => {
|
|
mp.mul = [1; 16];
|
|
("MUL any = 1", "M1")
|
|
}
|
|
"mulnaf" => {
|
|
// the lightest realistic plant: four words at NAF weight 3 (M2 k = 4), the rest untouched
|
|
for i in 0..4 {
|
|
mp.mul[i] = (1u32 << 20) + (1u32 << 9) + 1;
|
|
}
|
|
("MUL any NAF weight <= 3", "M1 and M2")
|
|
}
|
|
"rc0" => {
|
|
mp.rc[3] = 0;
|
|
("RC any = 0", "structure (0 ops on a per-day datapath by construction)")
|
|
}
|
|
"rcrk0" => {
|
|
mp.rc[3] = 0u32.wrapping_sub(round_key_mult(2, 5, 8));
|
|
("RC + rk = 0 for any of the 72 keys", "structure (one xor of 10,368 ops per item on a generic datapath: 1.0001x)")
|
|
}
|
|
_ => {
|
|
eprintln!("plant alleq|mul1|mul1all|mulnaf|rc0|rcrk0");
|
|
std::process::exit(2);
|
|
}
|
|
};
|
|
let after = classify(&mp, &rks);
|
|
println!("\nafter the plant `{what}`:");
|
|
print!("{}", describe(GENESIS_DAY, &mp, &after, Some(median)));
|
|
let idx = CLASSES.iter().position(|n| *n == flag_name).unwrap();
|
|
let flagged = Tally::flags(&after)[idx] && !Tally::flags(&before)[idx];
|
|
let gain_m1 = median as f64 / after.cost_m1 as f64;
|
|
let gain_m2 = m2_gain(after.m2_k);
|
|
println!("\nplant `{what}`: classifier flag `{flag_name}` {} (was {} before); M1 gain {gain_m1:.4}x, M2 gain {gain_m2:.4}x; gain metric for this plant: {gain_metric}", if flagged { "FIRED" } else { "did NOT fire" }, Tally::flags(&before)[idx]);
|
|
let gain_fired = gain_m1 > GAIN_GATE || gain_m2 > GAIN_GATE;
|
|
println!("gain over {GAIN_GATE}x: {}", if gain_fired { "FIRED" } else { "not over the gate" });
|
|
if !flagged {
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
/// Exact per-word tables over every odd 32-bit constant (2^31 of them): the NAF weight distribution and the
|
|
/// popcount distribution; then the 16-fold convolution of the NAF-weight distribution gives the expected M1 cost
|
|
/// distribution and the expected fraction of days under any cost.
|
|
fn expect(args: &[String]) {
|
|
let threads: usize = arg(args, "--threads").map(|v| v.parse().unwrap()).unwrap_or(12);
|
|
let median: u32 = arg(args, "--median").map(|v| v.parse().unwrap()).unwrap_or(221);
|
|
let t0 = std::time::Instant::now();
|
|
let per: Vec<([u64; 40], [u64; 33])> = std::thread::scope(|sc| {
|
|
let hs: Vec<_> = (0..threads)
|
|
.map(|t| {
|
|
sc.spawn(move || {
|
|
let mut naf = [0u64; 40];
|
|
let mut pop = [0u64; 33];
|
|
let lo = ((1u64 << 32) * t as u64 / threads as u64) | 1;
|
|
let hi = (1u64 << 32) * (t as u64 + 1) / threads as u64;
|
|
let mut v = lo;
|
|
while v < hi {
|
|
naf[naf_weight(v as u32) as usize] += 1;
|
|
pop[(v as u32).count_ones() as usize] += 1;
|
|
v += 2;
|
|
}
|
|
(naf, pop)
|
|
})
|
|
})
|
|
.collect();
|
|
hs.into_iter().map(|h| h.join().unwrap()).collect()
|
|
});
|
|
let mut naf = [0u64; 40];
|
|
let mut pop = [0u64; 33];
|
|
for (a, b) in per {
|
|
for i in 0..40 {
|
|
naf[i] += a[i];
|
|
}
|
|
for i in 0..33 {
|
|
pop[i] += b[i];
|
|
}
|
|
}
|
|
let total: u64 = naf.iter().sum();
|
|
assert_eq!(total, 1 << 31);
|
|
println!("# attack-f4 expect: all {total} odd 32-bit constants, {threads} threads, {:.1} s", t0.elapsed().as_secs_f64());
|
|
println!();
|
|
println!("| NAF weight | Odd constants | Fraction | Cumulative | Any of 16 per day | x 2^24 days |");
|
|
println!("|---|---|---|---|---|---|");
|
|
let mut cum = 0u64;
|
|
for w in 0..40 {
|
|
if naf[w] > 0 {
|
|
cum += naf[w];
|
|
let p = cum as f64 / total as f64;
|
|
println!("| {w} | {} | {:.4e} | {:.4e} | {:.4e} | {:.3} |", naf[w], naf[w] as f64 / total as f64, p, p_any_of(16, p), p_any_of(16, p) * 2f64.powi(24));
|
|
}
|
|
}
|
|
println!();
|
|
println!("| Popcount | Odd constants | Cumulative fraction | Any of 16 per day |");
|
|
println!("|---|---|---|---|");
|
|
cum = 0;
|
|
for w in 0..33 {
|
|
if pop[w] > 0 {
|
|
cum += pop[w];
|
|
let p = cum as f64 / total as f64;
|
|
println!("| {w} | {} | {:.4e} | {:.4e} |", pop[w], p, p_any_of(16, p));
|
|
}
|
|
}
|
|
// the 16-fold convolution of the NAF-weight distribution: the exact expected distribution of the NAF sum
|
|
let pw: Vec<f64> = naf.iter().map(|&n| n as f64 / total as f64).collect();
|
|
let mut dist = vec![0f64; 1];
|
|
dist[0] = 1.0;
|
|
for _ in 0..16 {
|
|
let mut next = vec![0f64; dist.len() + 39];
|
|
for (i, &a) in dist.iter().enumerate() {
|
|
if a == 0.0 {
|
|
continue;
|
|
}
|
|
for (w, &b) in pw.iter().enumerate() {
|
|
next[i + w] += a * b;
|
|
}
|
|
}
|
|
dist = next;
|
|
}
|
|
let mean: f64 = dist.iter().enumerate().map(|(s, &p)| s as f64 * p).sum();
|
|
let var: f64 = dist.iter().enumerate().map(|(s, &p)| (s as f64 - mean).powi(2) * p).sum();
|
|
println!();
|
|
println!("Expected NAF sum over 16 words: mean {mean:.4}, sd {:.4}; expected M1 cost mean {:.4}", var.sqrt(), mean + QR_ADDS_XORS as f64 - 16.0);
|
|
println!();
|
|
println!("| M1 cost | NAF sum | Expected fraction of days at this cost | Expected cumulative fraction | Gain vs median {median} |");
|
|
println!("|---|---|---|---|---|");
|
|
let mut c = 0f64;
|
|
for (s, &p) in dist.iter().enumerate() {
|
|
let cost = s as i64 + QR_ADDS_XORS as i64 - 16;
|
|
if cost < 0 {
|
|
continue;
|
|
}
|
|
c += p;
|
|
if p > 1e-12 && (cost as f64) <= median as f64 {
|
|
println!("| {cost} | {s} | {p:.4e} | {c:.4e} | {:.4}x |", median as f64 / cost as f64);
|
|
}
|
|
}
|
|
let gate: f64 = dist.iter().enumerate().filter(|(s, _)| ((*s as f64) + QR_ADDS_XORS as f64 - 16.0) * GAIN_GATE < median as f64).map(|(_, &p)| p).sum();
|
|
println!();
|
|
println!("Expected fraction of days with M1 gain over {GAIN_GATE}x against median {median}: {gate:.4e} ({} ; x 2^24 = {:.1}); 2^-20 = {:.4e}", one_in(gate), gate * 2f64.powi(24), 2f64.powi(-20));
|
|
}
|
|
|
|
/// Diffusion of one day's mixer: for `states` random 16-word states and each of the 512 input bits, the fraction of
|
|
/// the 512 output bits that flip after k = 1 and k = 2 applications (keys `round_key_mult(0, 0, 8)` and `(0, 1, 8)`),
|
|
/// mean over all, and the minimum per-output-bit flip probability. An ideal mixer reads 0.5 mean and about 0.5 min.
|
|
fn avalanche(args: &[String]) {
|
|
let d: u64 = arg(args, "--index").map(|v| v.parse().unwrap()).unwrap_or(GENESIS_DAY);
|
|
let states: usize = arg(args, "--states").map(|v| v.parse().unwrap()).unwrap_or(4096);
|
|
let plant_alleq: Option<u32> = arg(args, "--plant-alleq").map(|v| v.parse().unwrap());
|
|
let mut mp = params_of_day(d);
|
|
if let Some(r) = plant_alleq {
|
|
mp.rot = [r; 8];
|
|
}
|
|
let c = classify(&mp, &round_keys());
|
|
println!("avalanche of day {d}{}: ROT {:?}, NAF sum {}, {states} states x 512 input bits", plant_alleq.map(|r| format!(" with ROT planted all {r}")).unwrap_or_default(), mp.rot, c.naf_sum);
|
|
let mut rng = SplitMix64::new(0xF4F4_F4F4 ^ d);
|
|
for k in 1..=3usize {
|
|
let apply = |s: &mut [u32; 16]| {
|
|
for j in 0..k {
|
|
mixer(s, round_keys()[j], &mp);
|
|
}
|
|
};
|
|
let mut flips = [0u64; 512];
|
|
let mut total_flips = 0u64;
|
|
let mut trials = 0u64;
|
|
for _ in 0..states {
|
|
let mut base = [0u32; 16];
|
|
for w in base.iter_mut() {
|
|
*w = rng.next() as u32;
|
|
}
|
|
let mut y0 = base;
|
|
apply(&mut y0);
|
|
for bit in 0..512 {
|
|
let mut x = base;
|
|
x[bit / 32] ^= 1 << (bit % 32);
|
|
apply(&mut x);
|
|
for o in 0..512 {
|
|
let f = ((x[o / 32] ^ y0[o / 32]) >> (o % 32)) & 1;
|
|
flips[o] += f as u64;
|
|
total_flips += f as u64;
|
|
}
|
|
trials += 1;
|
|
}
|
|
}
|
|
let mean = total_flips as f64 / (trials as f64 * 512.0);
|
|
let min = flips.iter().map(|&f| f as f64 / trials as f64).fold(1.0, f64::min);
|
|
let max = flips.iter().map(|&f| f as f64 / trials as f64).fold(0.0, f64::max);
|
|
println!("| {k} application{} | mean flip {mean:.4} | min per output bit {min:.4} | max {max:.4} |", if k > 1 { "s" } else { "" });
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
let args: Vec<String> = std::env::args().collect();
|
|
match args.get(1).map(|s| s.as_str()) {
|
|
Some("census") => census(&args),
|
|
Some("day") => day_cmd(&args),
|
|
Some("plant") => plant(&args),
|
|
Some("expect") => expect(&args),
|
|
Some("avalanche") => avalanche(&args),
|
|
_ => {
|
|
eprintln!("attack-f4 census|day|plant|expect|avalanche (see the module doc)");
|
|
std::process::exit(2);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn naf_weights() {
|
|
assert_eq!(naf_weight(0), 0);
|
|
assert_eq!(naf_weight(1), 1);
|
|
assert_eq!(naf_weight(3), 2); // 4 - 1
|
|
assert_eq!(naf_weight(7), 2); // 8 - 1
|
|
assert_eq!(naf_weight(0xFFFF_FFFF), 2); // 2^32 - 1
|
|
assert_eq!(naf_weight(0xAAAA_AAAB), 17); // alternating odd: the maximum for 32 bits
|
|
assert_eq!(naf_weight((1 << 20) + (1 << 9) + 1), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn genesis_day_draw_matches_memhard_md() {
|
|
// MEMHARD.md section 1.1 (string day "2026-10-03") is a different key from the chain's day index 20,729;
|
|
// what is checked here is that the chain-day path draws through the real code and stays in range.
|
|
let mp = params_of_day(GENESIS_DAY);
|
|
assert!(mp.rot.iter().all(|r| (1..=31).contains(r)));
|
|
assert!(mp.mul.iter().all(|m| m & 1 == 1));
|
|
assert_eq!(mp.shape, v4_shape());
|
|
let s = igneum_pow::seed::day_key("2026-10-03");
|
|
let mp2 = MixParams::with_shape(s, Shape::V2);
|
|
assert_eq!(mp2.rot, [20, 20, 19, 4, 26, 3, 3, 27], "MEMHARD.md 1.1 genesis-day ROT");
|
|
}
|
|
}
|