Skip to main content

logicaffeine_base/
describe.rs

1//! # Integer-sequence description-length codec (the MDL primitive)
2//!
3//! A pure, self-contained engine that describes an `&[i64]` by the **shortest** program in a fixed
4//! menu of closed-form generators — affine (`base + i·stride`), geometric (`base · ratioⁱ`), a
5//! degree-≤4 polynomial (finite-difference seeds), a periodic block, a sparse dominant-value form, a
6//! sandboxed [`GenExpr`] generator, and the columnar fallbacks (delta, delta-of-delta,
7//! frame-of-reference bit-pack, run-length, dictionary, raw byte column, plain zig-zag varint). Each
8//! candidate is a complete self-delimiting byte string; [`consider`] keeps the smallest, so the
9//! result is **never larger than plain varint**.
10//!
11//! [`describe_int_seq`] is a *computable upper bound on the Kolmogorov complexity* of the sequence
12//! over this description language, and [`decode_int_seq`] is its exact inverse — decode reproduces
13//! the sequence bit-for-bit, so the encoded bytes are a **re-checkable witness** for that bound.
14//!
15//! This is the same codec the wire layer (`logicaffeine_compile`'s `marshal`) uses for its
16//! `WireStructure::Auto` int columns; it lives here in the leaf crate so both the wire codec and the
17//! proof layer share one implementation of the format. The crate has no I/O and no receiver policy,
18//! so the DoS bound (`max_elements`) is a **parameter**, supplied by each caller.
19
20// ---- Tuning + format constants -------------------------------------------------------------
21
22/// A cap on a length prefix's pre-allocation, so a corrupt huge count can't ask for gigabytes up
23/// front; the actual reads still bound-check every element.
24const PREALLOC_CAP: usize = 4096;
25
26/// The longest repeating block the periodic detector will consider.
27const PERIOD_CAP: usize = 512;
28
29/// The highest polynomial degree the generator detector will fit (degree 1 is the affine case).
30pub const MAX_POLY_DEGREE: usize = 4;
31
32/// The highest constant-coefficient linear-recurrence order the detector will fit. Order 2 already
33/// covers Fibonacci / Lucas / Pell and any LFSR of that length (this is Berlekamp–Massey over ℤ).
34pub const MAX_RECUR_ORDER: usize = 4;
35
36/// The largest byte column on which the GF(2) LFSR detector runs. Berlekamp–Massey is `O(bits²)`, so
37/// this bounds the deep-attack cost; it runs only as a LAST RESORT on columns nothing else compressed.
38const LFSR_MAX_BYTES: usize = 512;
39
40/// The largest byte column on which the 2-adic FCSR detector runs (bignum rational reconstruction).
41const FCSR_MAX_BYTES: usize = 256;
42
43/// A decode cap so a corrupt run-length can't ask for terabytes of output.
44const RLE_MAX_TOTAL: usize = 1 << 28;
45
46/// A hostile/garbage [`GenExpr`] tree is rejected past this many nodes.
47pub const MAX_GEN_NODES: u32 = 256;
48
49/// …or this deep — bounds both decode recursion and eval.
50pub const MAX_GEN_DEPTH: u32 = 32;
51
52/// The nesting cap for a periodic column whose block is itself a column (bounds decode recursion).
53const DECODE_MAX_DEPTH: u32 = 32;
54
55/// A generous element cap for the standalone [`decode_int_seq`] (which has no receiver policy): a
56/// tampered generator descriptor still cannot materialize more than this many elements.
57const DEFAULT_MAX_ELEMENTS: usize = 1 << 28;
58
59// The `T_INTS_*` tags for the Auto column menu. These bytes are the wire format; the wider tag space
60// (structs, strings, floats, …) lives in the wire codec that also speaks these.
61pub const T_INTS: u8 = 19; // adaptive-sign zig-zag varint per element (the plain baseline)
62pub const T_INTS_AFFINE: u8 = 32; // closed-form: base + stride·i (3 numbers, no data)
63pub const T_INTS_DELTA: u8 = 39; // first + zig-zag successive differences — monotone columns
64pub const T_INTS_DOD: u8 = 40; // first + first delta + zig-zag second differences — near-linear
65pub const T_INTS_FOR: u8 = 41; // min + bit-width + bit-packed (v-min) residuals — clustered
66pub const T_INTS_RLE: u8 = 42; // (value, run-length) pairs — runs of repeats
67pub const T_INTS_DICT: u8 = 43; // distinct values + bit-packed indices — low cardinality
68pub const T_INTS_POLY: u8 = 50; // degree + (degree+1) finite-difference seeds — a polynomial column
69pub const T_GEN: u8 = 51; // a serialized GenExpr over the index `i` (the general generator form)
70pub const T_BYTES: u8 = 53; // raw 1-byte-per-element blob — a byte column
71pub const T_INTS_GEOMETRIC: u8 = 61; // closed-form: base · ratioⁱ (3 numbers, no data)
72pub const T_INTS_PERIODIC: u8 = 62; // cyclic: period p + count + one block → pattern[i % p]
73pub const T_INTS_SPARSE: u8 = 67; // dominant value + (delta-index, value) exceptions — sparse
74pub const T_INTS_LRECUR: u8 = 82; // constant-coefficient linear recurrence: order + coeffs + seeds
75pub const T_INTS_LFSR: u8 = 83; // GF(2) LFSR: byte count + linear complexity L + L feedback taps + L seed bits
76pub const T_INTS_FCSR: u8 = 84; // 2-adic FCSR: byte count + rational p/q (p then q, each sign+len+LE bytes)
77
78// ---- Varint / zig-zag primitives -----------------------------------------------------------
79
80/// LEB128 unsigned varint: 7 bits per byte, high bit = continuation.
81#[inline]
82pub fn write_uvarint(mut x: u64, out: &mut Vec<u8>) {
83    while x >= 0x80 {
84        out.push((x as u8) | 0x80);
85        x >>= 7;
86    }
87    out.push(x as u8);
88}
89
90/// Read a LEB128 unsigned varint. `None` on an overlong/overflowing encoding or a short buffer.
91#[inline]
92pub fn read_uvarint(buf: &[u8], pos: &mut usize) -> Option<u64> {
93    let mut result = 0u64;
94    let mut shift = 0u32;
95    loop {
96        let b = *buf.get(*pos)?;
97        *pos += 1;
98        if shift >= 64 {
99            return None; // overlong / overflow
100        }
101        result |= u64::from(b & 0x7f) << shift;
102        if b & 0x80 == 0 {
103            return Some(result);
104        }
105        shift += 7;
106    }
107}
108
109/// Map a signed integer to an unsigned one with small magnitudes near zero (for varint).
110#[inline]
111pub fn zigzag(x: i64) -> u64 {
112    ((x << 1) ^ (x >> 63)) as u64
113}
114
115/// The inverse of [`zigzag`].
116#[inline]
117pub fn unzigzag(x: u64) -> i64 {
118    ((x >> 1) as i64) ^ -((x & 1) as i64)
119}
120
121/// The number of bytes [`write_uvarint`] would emit for `x`.
122pub fn uvarint_byte_len(x: u64) -> usize {
123    (((64 - x.leading_zeros()).max(1) + 6) / 7) as usize
124}
125
126/// The plain baseline column (`T_INTS`): an adaptive-sign header (`(n << 1) | signed`) then a
127/// varint per element — zig-zag when any value is negative, plain LEB128 otherwise.
128pub fn leb128_encode<I: Iterator<Item = i64> + Clone>(out: &mut Vec<u8>, vals: I, n: usize) {
129    let signed = vals.clone().any(|x| x < 0);
130    write_uvarint(((n as u64) << 1) | signed as u64, out);
131    out.reserve(n * 2);
132    if signed {
133        for x in vals {
134            write_uvarint(zigzag(x), out);
135        }
136    } else {
137        for x in vals {
138            write_uvarint(x as u64, out);
139        }
140    }
141}
142
143// ---- Bit packing ---------------------------------------------------------------------------
144
145/// Pack `vals` LSB-first at `width` bits each (1..=64). The inverse of [`bitunpack`].
146pub fn bitpack(vals: &[u64], width: u8) -> Vec<u8> {
147    if width == 0 {
148        return Vec::new();
149    }
150    let total_bits = vals.len().saturating_mul(width as usize);
151    let mut out = vec![0u8; total_bits.div_ceil(8)];
152    let mut bitpos = 0usize;
153    for &val in vals {
154        let mut bits = val;
155        let mut remaining = width as usize;
156        while remaining > 0 {
157            let byte = bitpos / 8;
158            let off = bitpos % 8;
159            let take = remaining.min(8 - off);
160            let mask = (1u64 << take) - 1;
161            out[byte] |= ((bits & mask) as u8) << off;
162            bits >>= take;
163            bitpos += take;
164            remaining -= take;
165        }
166    }
167    out
168}
169
170/// Read `count` LSB-first `width`-bit values from `bytes`. `None` if `bytes` is too short. The
171/// inverse of [`bitpack`].
172pub fn bitunpack(bytes: &[u8], count: usize, width: u8) -> Option<Vec<u64>> {
173    if width == 0 || width > 64 {
174        return None;
175    }
176    let total_bits = count.checked_mul(width as usize)?;
177    if bytes.len() < total_bits.div_ceil(8) {
178        return None;
179    }
180    let mut out = Vec::with_capacity(count.min(PREALLOC_CAP));
181    let mut bitpos = 0usize;
182    for _ in 0..count {
183        let mut val = 0u64;
184        let mut got = 0usize;
185        while got < width as usize {
186            let byte = bitpos / 8;
187            let off = bitpos % 8;
188            let take = (width as usize - got).min(8 - off);
189            let mask = (1u64 << take) - 1;
190            val |= (((bytes[byte] >> off) as u64) & mask) << got;
191            got += take;
192            bitpos += take;
193        }
194        out.push(val);
195    }
196    Some(out)
197}
198
199// ---- The sandboxed generator IR (ship the computation, not the data) -----------------------
200
201#[derive(Clone, Copy, Debug, PartialEq, Eq)]
202pub enum GenCmp {
203    Eq,
204    Ne,
205    Lt,
206    Le,
207    Gt,
208    Ge,
209}
210
211/// A restricted, pure, TOTAL expression over the element index `i`. Every op is total (div/mod by
212/// zero is 0, wrapping i64 arithmetic), and a malformed/hostile tree is bounded at decode by a node
213/// budget + depth cap, so evaluation can never panic, diverge, overflow, or escape.
214#[derive(Clone, Debug, PartialEq, Eq)]
215pub enum GenExpr {
216    Index,
217    Const(i64),
218    Add(Box<GenExpr>, Box<GenExpr>),
219    Sub(Box<GenExpr>, Box<GenExpr>),
220    Mul(Box<GenExpr>, Box<GenExpr>),
221    Div(Box<GenExpr>, Box<GenExpr>),
222    Mod(Box<GenExpr>, Box<GenExpr>),
223    Select { op: GenCmp, lhs: Box<GenExpr>, rhs: Box<GenExpr>, then: Box<GenExpr>, els: Box<GenExpr> },
224}
225
226/// Evaluate a generator at index `i`. TOTAL: div/mod by zero is 0; all arithmetic wraps.
227pub fn gen_eval(e: &GenExpr, i: i64) -> i64 {
228    match e {
229        GenExpr::Index => i,
230        GenExpr::Const(c) => *c,
231        GenExpr::Add(a, b) => gen_eval(a, i).wrapping_add(gen_eval(b, i)),
232        GenExpr::Sub(a, b) => gen_eval(a, i).wrapping_sub(gen_eval(b, i)),
233        GenExpr::Mul(a, b) => gen_eval(a, i).wrapping_mul(gen_eval(b, i)),
234        GenExpr::Div(a, b) => {
235            let d = gen_eval(b, i);
236            if d == 0 { 0 } else { gen_eval(a, i).wrapping_div(d) }
237        }
238        GenExpr::Mod(a, b) => {
239            let d = gen_eval(b, i);
240            if d == 0 { 0 } else { gen_eval(a, i).wrapping_rem(d) }
241        }
242        GenExpr::Select { op, lhs, rhs, then, els } => {
243            let (l, r) = (gen_eval(lhs, i), gen_eval(rhs, i));
244            let c = match op {
245                GenCmp::Eq => l == r,
246                GenCmp::Ne => l != r,
247                GenCmp::Lt => l < r,
248                GenCmp::Le => l <= r,
249                GenCmp::Gt => l > r,
250                GenCmp::Ge => l >= r,
251            };
252            if c { gen_eval(then, i) } else { gen_eval(els, i) }
253        }
254    }
255}
256
257/// Serialize a generator pre-order: a 1-byte node tag, then children. Self-delimiting.
258pub fn serialize_gen(e: &GenExpr, out: &mut Vec<u8>) {
259    match e {
260        GenExpr::Index => out.push(0),
261        GenExpr::Const(c) => {
262            out.push(1);
263            write_uvarint(zigzag(*c), out);
264        }
265        GenExpr::Add(a, b) => { out.push(2); serialize_gen(a, out); serialize_gen(b, out); }
266        GenExpr::Sub(a, b) => { out.push(3); serialize_gen(a, out); serialize_gen(b, out); }
267        GenExpr::Mul(a, b) => { out.push(4); serialize_gen(a, out); serialize_gen(b, out); }
268        GenExpr::Div(a, b) => { out.push(5); serialize_gen(a, out); serialize_gen(b, out); }
269        GenExpr::Mod(a, b) => { out.push(6); serialize_gen(a, out); serialize_gen(b, out); }
270        GenExpr::Select { op, lhs, rhs, then, els } => {
271            out.push(7);
272            out.push(*op as u8);
273            serialize_gen(lhs, out);
274            serialize_gen(rhs, out);
275            serialize_gen(then, out);
276            serialize_gen(els, out);
277        }
278    }
279}
280
281/// Parse a generator under a node `budget` and `depth` cap — a garbage/hostile tree returns `None`.
282pub fn deserialize_gen(buf: &[u8], pos: &mut usize, budget: &mut u32, depth: u32) -> Option<GenExpr> {
283    if depth > MAX_GEN_DEPTH || *budget == 0 {
284        return None;
285    }
286    *budget -= 1;
287    let tag = *buf.get(*pos)?;
288    *pos += 1;
289    Some(match tag {
290        0 => GenExpr::Index,
291        1 => GenExpr::Const(unzigzag(read_uvarint(buf, pos)?)),
292        2..=6 => {
293            let a = Box::new(deserialize_gen(buf, pos, budget, depth + 1)?);
294            let b = Box::new(deserialize_gen(buf, pos, budget, depth + 1)?);
295            match tag {
296                2 => GenExpr::Add(a, b),
297                3 => GenExpr::Sub(a, b),
298                4 => GenExpr::Mul(a, b),
299                5 => GenExpr::Div(a, b),
300                _ => GenExpr::Mod(a, b),
301            }
302        }
303        7 => {
304            let op = match *buf.get(*pos)? {
305                0 => GenCmp::Eq,
306                1 => GenCmp::Ne,
307                2 => GenCmp::Lt,
308                3 => GenCmp::Le,
309                4 => GenCmp::Gt,
310                5 => GenCmp::Ge,
311                _ => return None,
312            };
313            *pos += 1;
314            let lhs = Box::new(deserialize_gen(buf, pos, budget, depth + 1)?);
315            let rhs = Box::new(deserialize_gen(buf, pos, budget, depth + 1)?);
316            let then = Box::new(deserialize_gen(buf, pos, budget, depth + 1)?);
317            let els = Box::new(deserialize_gen(buf, pos, budget, depth + 1)?);
318            GenExpr::Select { op, lhs, rhs, then, els }
319        }
320        _ => return None,
321    })
322}
323
324// ---- Column-shape detectors ----------------------------------------------------------------
325
326/// If `v` is an exact affine progression `base + stride·i`, return `(base, stride)`.
327pub fn detect_affine(v: &[i64]) -> Option<(i64, i64)> {
328    if v.len() < 2 {
329        return None;
330    }
331    let base = v[0];
332    let stride = v[1].wrapping_sub(v[0]);
333    for (i, &x) in v.iter().enumerate() {
334        if base.wrapping_add((i as i64).wrapping_mul(stride)) != x {
335            return None;
336        }
337    }
338    Some((base, stride))
339}
340
341/// If `v` is an exact geometric progression `base · ratioⁱ` (≥3 elements, confirmed by replaying the
342/// decoder's wrapping arithmetic), return `(base, ratio)`.
343pub fn detect_geometric(v: &[i64]) -> Option<(i64, i64)> {
344    if v.len() < 3 {
345        return None;
346    }
347    let base = v[0];
348    if base == 0 || v[1].checked_rem(base)? != 0 {
349        return None;
350    }
351    let ratio = v[1].checked_div(base)?;
352    if ratio == 0 || ratio == 1 {
353        return None;
354    }
355    let mut cur = base;
356    for &x in v {
357        if cur != x {
358            return None;
359        }
360        cur = cur.wrapping_mul(ratio);
361    }
362    Some((base, ratio))
363}
364
365/// If `v` is an exact cyclic repetition of a minimal block of period `2 ≤ p ≤ min(len/2, cap)`,
366/// return `p`.
367pub fn detect_period(v: &[i64]) -> Option<usize> {
368    let n = v.len();
369    if n < 4 {
370        return None;
371    }
372    let cap = (n / 2).min(PERIOD_CAP);
373    'p: for p in 2..=cap {
374        for i in p..n {
375            if v[i] != v[i - p] {
376                continue 'p;
377            }
378        }
379        return Some(p);
380    }
381    None
382}
383
384/// If one value dominates the column (covers ≥ ¾), return it and the sorted `(index, value)`
385/// exceptions.
386pub fn detect_sparse(v: &[i64]) -> Option<(i64, Vec<(usize, i64)>)> {
387    if v.len() < 8 {
388        return None;
389    }
390    let mut cand = v[0];
391    let mut count: i64 = 0;
392    for &x in v {
393        if count == 0 {
394            cand = x;
395            count = 1;
396        } else if x == cand {
397            count += 1;
398        } else {
399            count -= 1;
400        }
401    }
402    let occ = v.iter().filter(|&&x| x == cand).count();
403    if v.len() - occ > v.len() / 4 {
404        return None; // not dominant enough — the exception list wouldn't beat the menu
405    }
406    let exceptions: Vec<(usize, i64)> =
407        v.iter().enumerate().filter(|(_, &x)| x != cand).map(|(i, &x)| (i, x)).collect();
408    Some((cand, exceptions))
409}
410
411/// Recognize `v` as a polynomial column (degree ≤ [`MAX_POLY_DEGREE`]) via finite differences and
412/// return `(degree, seeds)` — the k+1 leading-edge seeds, confirmed by exact reconstruction.
413pub fn detect_poly_generator(v: &[i64]) -> Option<(u8, Vec<i64>)> {
414    if v.len() < 3 {
415        return None;
416    }
417    let mut levels: Vec<Vec<i64>> = Vec::with_capacity(MAX_POLY_DEGREE + 1);
418    levels.push(v.to_vec());
419    for d in 0..MAX_POLY_DEGREE {
420        let prev = &levels[d];
421        if prev.len() < 2 {
422            break;
423        }
424        let mut next = Vec::with_capacity(prev.len() - 1);
425        for w in prev.windows(2) {
426            next.push(w[1].checked_sub(w[0])?); // a difference overflow → fall back to the menu
427        }
428        if next.len() >= 2 && next.iter().all(|&x| x == next[0]) {
429            let degree = (d + 1) as u8;
430            let mut seeds: Vec<i64> = levels.iter().map(|lvl| lvl[0]).collect();
431            seeds.push(next[0]);
432            if reconstruct_poly(&seeds, v.len()) == v {
433                return Some((degree, seeds));
434            }
435            return None;
436        }
437        levels.push(next);
438    }
439    None
440}
441
442/// Replay a polynomial column from its finite-difference seeds via a difference engine.
443pub fn reconstruct_poly(seeds: &[i64], n: usize) -> Vec<i64> {
444    let mut diffs = seeds.to_vec();
445    let mut out = Vec::with_capacity(n.min(PREALLOC_CAP));
446    for _ in 0..n {
447        out.push(diffs[0]);
448        for j in 0..diffs.len().saturating_sub(1) {
449            diffs[j] = diffs[j].wrapping_add(diffs[j + 1]);
450        }
451    }
452    out
453}
454
455/// Recognize `v` as a constant-coefficient linear recurrence `v[i] = Σⱼ cⱼ·v[i-1-j]` of minimal order
456/// `k ≤ [`MAX_RECUR_ORDER`]` with **integer** coefficients, and return `(coeffs, seeds)` — `k`
457/// coefficients + the first `k` values. This is exactly Berlekamp–Massey over ℤ: it captures Fibonacci,
458/// Lucas, Pell, and any linear-feedback sequence — none of which the polynomial detector can reach
459/// (their finite differences never settle). The coefficients are solved exactly (Cramer over `i128`)
460/// from the smallest `2k` terms and then CONFIRMED by replaying the recurrence with the SAME wrapping
461/// arithmetic the decoder uses, so a match certifies a bit-exact reconstruction.
462pub fn detect_linear_recurrence(v: &[i64]) -> Option<(Vec<i64>, Vec<i64>)> {
463    let n = v.len();
464    for k in 1..=MAX_RECUR_ORDER.min(n / 2) {
465        if let Some(coeffs) = solve_recurrence(v, k) {
466            if reconstruct_recurrence(&coeffs, &v[..k], n) == v {
467                return Some((coeffs, v[..k].to_vec()));
468            }
469        }
470    }
471    None
472}
473
474/// Solve the `k×k` linear system for integer recurrence coefficients from the smallest `2k` terms via
475/// Cramer's rule over `i128`. `None` if the system is singular, a coefficient is non-integer or
476/// out-of-`i64`, or a determinant overflows.
477fn solve_recurrence(v: &[i64], k: usize) -> Option<Vec<i64>> {
478    let mut m = vec![vec![0i128; k]; k];
479    let mut b = vec![0i128; k];
480    for r in 0..k {
481        for j in 0..k {
482            m[r][j] = v[k + r - 1 - j] as i128; // coefficient of c_{j+1} in the equation for v[k+r]
483        }
484        b[r] = v[k + r] as i128;
485    }
486    let det_m = det_i128(&m)?;
487    if det_m == 0 {
488        return None;
489    }
490    let mut coeffs = Vec::with_capacity(k);
491    for j in 0..k {
492        let mut mj = m.clone();
493        for (r, br) in b.iter().enumerate() {
494            mj[r][j] = *br;
495        }
496        let det_j = det_i128(&mj)?;
497        if det_j % det_m != 0 {
498            return None; // non-integer coefficient — not an exact integer recurrence
499        }
500        let c = det_j / det_m;
501        if c < i64::MIN as i128 || c > i64::MAX as i128 {
502            return None;
503        }
504        coeffs.push(c as i64);
505    }
506    Some(coeffs)
507}
508
509/// A checked `i128` determinant by Laplace expansion (used only for `k ≤ [`MAX_RECUR_ORDER`]`, so at
510/// most `4! = 24` terms). `None` on overflow.
511fn det_i128(m: &[Vec<i128>]) -> Option<i128> {
512    let k = m.len();
513    match k {
514        1 => Some(m[0][0]),
515        2 => m[0][0].checked_mul(m[1][1])?.checked_sub(m[0][1].checked_mul(m[1][0])?),
516        _ => {
517            let mut acc: i128 = 0;
518            for j in 0..k {
519                let minor: Vec<Vec<i128>> =
520                    (1..k).map(|r| (0..k).filter(|&c| c != j).map(|c| m[r][c]).collect()).collect();
521                let cof = m[0][j].checked_mul(det_i128(&minor)?)?;
522                acc = if j % 2 == 0 { acc.checked_add(cof)? } else { acc.checked_sub(cof)? };
523            }
524            Some(acc)
525        }
526    }
527}
528
529/// Replay a linear recurrence from `coeffs` and the first `seeds.len()` values, wrapping (matches the
530/// decoder, so a confirmed fit is exact across all of `i64`). `n` values out.
531fn reconstruct_recurrence(coeffs: &[i64], seeds: &[i64], n: usize) -> Vec<i64> {
532    let k = coeffs.len();
533    // Guard: without `k` seeds the recurrence cannot start — return what seeds we have (a malformed
534    // decode then simply fails the equality/round-trip check rather than indexing out of bounds).
535    if seeds.len() < k {
536        return seeds[..seeds.len().min(n)].to_vec();
537    }
538    let mut out = Vec::with_capacity(n.min(PREALLOC_CAP));
539    out.extend_from_slice(&seeds[..k.min(n)]);
540    for i in k..n {
541        let mut acc = 0i64;
542        for j in 0..k {
543            acc = acc.wrapping_add(coeffs[j].wrapping_mul(out[i - 1 - j]));
544        }
545        out.push(acc);
546    }
547    out.truncate(n);
548    out
549}
550
551// ---- Berlekamp–Massey over an arbitrary field: the shortest LFSR (the LFSR attack, as a compressor) --
552//
553// The Berlekamp–Massey algorithm finds the shortest linear-feedback shift register generating a
554// sequence over ANY field. Over GF(2) it is the classic bit-LFSR attack; over GF(2⁸) it is the
555// *word*-oriented LFSR attack (a byte stream whose bytes are a GF(256)-linear recurrence). A word-LFSR
556// of order `L` is also a bit-LFSR of order `≤ 8L`, so GF(2) already *catches* it — but GF(2⁸) reports
557// the natural word complexity and runs in `O(n²)` byte-ops instead of `O((8n)²)` bit-ops.
558
559/// The minimal field interface Berlekamp–Massey needs.
560pub trait FieldElem: Copy + PartialEq {
561    fn zero() -> Self;
562    fn one() -> Self;
563    fn add(self, o: Self) -> Self;
564    fn sub(self, o: Self) -> Self;
565    fn mul(self, o: Self) -> Self;
566    /// Multiplicative inverse; only called on nonzero elements.
567    fn inv(self) -> Self;
568}
569
570/// GF(2) — a single bit.
571#[derive(Clone, Copy, PartialEq, Eq, Debug)]
572pub struct Gf2(pub bool);
573impl FieldElem for Gf2 {
574    fn zero() -> Self { Gf2(false) }
575    fn one() -> Self { Gf2(true) }
576    fn add(self, o: Self) -> Self { Gf2(self.0 ^ o.0) }
577    fn sub(self, o: Self) -> Self { Gf2(self.0 ^ o.0) }
578    fn mul(self, o: Self) -> Self { Gf2(self.0 && o.0) }
579    fn inv(self) -> Self { self } // 1⁻¹ = 1 (the only nonzero element)
580}
581
582/// GF(2⁸) — the Rijndael/AES field (reduction polynomial `x⁸+x⁴+x³+x²+1`), one byte per element.
583#[derive(Clone, Copy, PartialEq, Eq, Debug)]
584pub struct Gf256(pub u8);
585
586fn gf256_xtime(a: u8) -> u8 {
587    (a << 1) ^ (if a & 0x80 != 0 { 0x1B } else { 0 })
588}
589/// `(log, exp)` tables for GF(2⁸) with generator 3: `exp[i] = 3ⁱ`, `log` its inverse. `exp` is doubled
590/// to length 512 so a product's log index never needs a modular reduction.
591fn gf256_tables() -> &'static (Vec<u8>, Vec<u8>) {
592    static T: std::sync::OnceLock<(Vec<u8>, Vec<u8>)> = std::sync::OnceLock::new();
593    T.get_or_init(|| {
594        let mut exp = vec![0u8; 512];
595        let mut log = vec![0u8; 256];
596        let mut x = 1u8;
597        for i in 0..255usize {
598            exp[i] = x;
599            log[x as usize] = i as u8;
600            x ^= gf256_xtime(x); // x ← x·3 = x·2 ⊕ x
601        }
602        for i in 255..512 {
603            exp[i] = exp[i - 255];
604        }
605        (log, exp)
606    })
607}
608impl FieldElem for Gf256 {
609    fn zero() -> Self { Gf256(0) }
610    fn one() -> Self { Gf256(1) }
611    fn add(self, o: Self) -> Self { Gf256(self.0 ^ o.0) }
612    fn sub(self, o: Self) -> Self { Gf256(self.0 ^ o.0) }
613    fn mul(self, o: Self) -> Self {
614        if self.0 == 0 || o.0 == 0 {
615            return Gf256(0);
616        }
617        let (log, exp) = gf256_tables();
618        Gf256(exp[log[self.0 as usize] as usize + log[o.0 as usize] as usize])
619    }
620    fn inv(self) -> Self {
621        let (log, exp) = gf256_tables();
622        Gf256(exp[255 - log[self.0 as usize] as usize])
623    }
624}
625
626/// **Berlekamp–Massey over a field.** Returns `(L, taps)` where `L` is the linear complexity and `taps`
627/// are the connection-polynomial coefficients `c₁..c_L`; the recurrence is `s[i] = -Σⱼ c_{j+1}·s[i-1-j]`.
628pub fn berlekamp_massey_field<F: FieldElem>(s: &[F]) -> (usize, Vec<F>) {
629    let n = s.len();
630    let mut c = vec![F::zero(); n + 1];
631    let mut b = vec![F::zero(); n + 1];
632    c[0] = F::one();
633    b[0] = F::one();
634    let mut l = 0usize;
635    let mut m = 1usize;
636    let mut b_disc = F::one(); // the discrepancy at the last length change
637    for nn in 0..n {
638        let mut d = s[nn];
639        for i in 1..=l {
640            d = d.add(c[i].mul(s[nn - i]));
641        }
642        if d == F::zero() {
643            m += 1;
644        } else {
645            let coef = d.mul(b_disc.inv());
646            let t = c.clone();
647            for i in 0..=n {
648                if i + m <= n && b[i] != F::zero() {
649                    c[i + m] = c[i + m].sub(coef.mul(b[i]));
650                }
651            }
652            if 2 * l <= nn {
653                l = nn + 1 - l;
654                b = t;
655                b_disc = d;
656                m = 1;
657            } else {
658                m += 1;
659            }
660        }
661    }
662    let taps = if l == 0 { Vec::new() } else { c[1..=l].to_vec() };
663    (l, taps)
664}
665
666/// Replay a field LFSR: emit `seed`, then `s[i] = -Σⱼ tapsⱼ·s[i-1-j]` (the connection recurrence).
667pub fn lfsr_generate_field<F: FieldElem>(taps: &[F], seed: &[F], total: usize) -> Vec<F> {
668    let l = taps.len();
669    let mut out = Vec::with_capacity(total);
670    out.extend_from_slice(&seed[..l.min(seed.len()).min(total)]);
671    for i in l..total {
672        let mut acc = F::zero();
673        for j in 0..l {
674            acc = acc.add(taps[j].mul(out[i - 1 - j]));
675        }
676        out.push(F::zero().sub(acc));
677    }
678    out.truncate(total);
679    out
680}
681
682/// **Berlekamp–Massey over GF(2)** — the shortest bit-LFSR. Thin wrapper over [`berlekamp_massey_field`]
683/// (see it for the full semantics). For an LFSR keystream `L` is the register length; for random ≈ n/2.
684pub fn berlekamp_massey_gf2(s: &[bool]) -> (usize, Vec<bool>) {
685    let elems: Vec<Gf2> = s.iter().map(|&b| Gf2(b)).collect();
686    let (l, taps) = berlekamp_massey_field(&elems);
687    (l, taps.into_iter().map(|g| g.0).collect())
688}
689
690/// Replay a GF(2) LFSR: `s[i] = ⊕ⱼ tapsⱼ · s[i-1-j]`. The inverse of [`berlekamp_massey_gf2`].
691pub fn lfsr_generate(taps: &[bool], seed: &[bool], total: usize) -> Vec<bool> {
692    let t: Vec<Gf2> = taps.iter().map(|&b| Gf2(b)).collect();
693    let sd: Vec<Gf2> = seed.iter().map(|&b| Gf2(b)).collect();
694    lfsr_generate_field(&t, &sd, total).into_iter().map(|g| g.0).collect()
695}
696
697/// If a byte column is a GF(2) LFSR keystream — its `8·n`-bit expansion has linear complexity `L` and
698/// the length-`L` LFSR regenerates every bit — return `(L, taps, seed_bits)`. Only worthwhile when `L`
699/// is well below the bit count (else the raw byte column wins and `consider` keeps it).
700fn detect_lfsr_bytes(v: &[i64]) -> Option<(usize, Vec<bool>, Vec<bool>)> {
701    let bits = bytes_to_bits(v);
702    let (l, taps) = berlekamp_massey_gf2(&bits);
703    if l == 0 || l >= bits.len() {
704        return None;
705    }
706    let seed = bits[..l].to_vec();
707    if lfsr_generate(&taps, &seed, bits.len()) != bits {
708        return None; // the LFSR does not regenerate the whole column — not a clean keystream
709    }
710    Some((l, taps, seed))
711}
712
713/// Expand a byte column to its LSB-first bit sequence (bit `j` of byte `i` is `(byte_i >> j) & 1`).
714pub fn bytes_to_bits(v: &[i64]) -> Vec<bool> {
715    let mut bits = Vec::with_capacity(v.len().saturating_mul(8));
716    for &x in v {
717        let byte = x as u8;
718        for j in 0..8 {
719            bits.push((byte >> j) & 1 == 1);
720        }
721    }
722    bits
723}
724
725/// Pack an LSB-first bit sequence back into bytes (the inverse of [`bytes_to_bits`]); a trailing
726/// partial group is zero-padded.
727pub fn bits_to_bytes(bits: &[bool]) -> Vec<i64> {
728    bits.chunks(8)
729        .map(|chunk| {
730            let mut byte = 0u8;
731            for (j, &bit) in chunk.iter().enumerate() {
732                if bit {
733                    byte |= 1 << j;
734                }
735            }
736            byte as i64
737        })
738        .collect()
739}
740
741// ---- 2-adic complexity / FCSR: the carry-based attack (what every LINEAR tool misses) -----------
742//
743// A feedback-with-carry shift register (FCSR) generates a bit sequence whose 2-adic value `Σ sᵢ2ⁱ`
744// equals a rational `p/q` with `q` ODD; the register is defined by the connection integer `q`. Because
745// the carry makes the map NON-linear over GF(2), an FCSR keystream (mod-2ⁿ LCGs, summation combiners,
746// the Marsaglia add-with-carry family) is invisible to Berlekamp–Massey over any field. The 2-adic
747// analogue — the **Rational Approximation Algorithm** (Klapper–Goresky), here via rational
748// reconstruction over the integers — recovers `p/q` and its *2-adic complexity* `≈ log₂ max(|p|,|q|)`.
749
750/// gcd of `|a|`, `|b|` by the Euclidean algorithm over `BigInt`.
751fn bigint_gcd_local(a: &crate::numeric::BigInt, b: &crate::numeric::BigInt) -> crate::numeric::BigInt {
752    let (mut a, mut b) = (a.abs(), b.abs());
753    while !b.is_zero() {
754        let r = a.div_rem(&b).map(|(_, r)| r).unwrap_or_else(crate::numeric::BigInt::zero);
755        a = b;
756        b = r;
757    }
758    a
759}
760
761/// The bit length of `|x|` (`0` for zero).
762fn bigint_bitlen(x: &crate::numeric::BigInt) -> usize {
763    let (_, bytes) = x.to_le_bytes();
764    for (i, &b) in bytes.iter().enumerate().rev() {
765        if b != 0 {
766            return i * 8 + (8 - b.leading_zeros() as usize);
767        }
768    }
769    0
770}
771
772/// **Rational reconstruction of a bit sequence as a 2-adic number.** Returns `(p, q)` with `q` odd and
773/// positive and `p/q ≡ Σ sᵢ2ⁱ (mod 2ⁿ)` — the shortest FCSR generating the sequence. `None` when no
774/// odd-denominator rational fits (an algorithmically-random sequence has no small FCSR).
775pub fn two_adic_reconstruct(bits: &[bool]) -> Option<(crate::numeric::BigInt, crate::numeric::BigInt)> {
776    use crate::numeric::BigInt;
777    let n = bits.len();
778    if n == 0 {
779        return None;
780    }
781    let two = BigInt::from_i64(2);
782    // α = Σ bits[i]·2ⁱ, and N = 2ⁿ.
783    let mut alpha = BigInt::zero();
784    let mut pow2 = BigInt::from_i64(1);
785    for &bit in bits {
786        if bit {
787            alpha = alpha.add(&pow2);
788        }
789        pow2 = pow2.mul(&two);
790    }
791    let n_big = pow2; // 2ⁿ
792    // Extended Euclid keeping rᵢ ≡ tᵢ·α (mod N); stop at the first rᵢ with rᵢ² ≤ N.
793    let (mut r0, mut t0) = (n_big.clone(), BigInt::zero());
794    let (mut r1, mut t1) = (alpha, BigInt::from_i64(1));
795    while !r1.is_zero() && r1.mul(&r1) > n_big {
796        let (quot, rem) = r0.div_rem(&r1)?;
797        let t2 = t0.sub(&quot.mul(&t1));
798        r0 = r1;
799        t0 = t1;
800        r1 = rem;
801        t1 = t2;
802    }
803    let (mut p, mut q) = (r1, t1);
804    if q.is_zero() {
805        return None;
806    }
807    if q.is_negative() {
808        p = BigInt::zero().sub(&p);
809        q = BigInt::zero().sub(&q);
810    }
811    // Reduce to lowest terms — the rational reconstruction can return a non-coprime `p/q` (a common
812    // factor `g` gives the same 2-adic value `pg/qg`); reducing yields the minimal FCSR and the true
813    // 2-adic complexity.
814    let g = bigint_gcd_local(&p, &q);
815    if !g.is_zero() && g != BigInt::from_i64(1) {
816        p = p.div_rem(&g).map(|(quot, _)| quot)?;
817        q = q.div_rem(&g).map(|(quot, _)| quot)?;
818    }
819    if !q.is_odd() {
820        return None; // no odd-denominator (FCSR) rational — treat as incompressible
821    }
822    Some((p, q))
823}
824
825/// Generate `n` bits of the FCSR keystream for `p/q` (`q` odd) — the 2-adic expansion, by repeated
826/// 2-adic division. The inverse of [`two_adic_reconstruct`].
827pub fn fcsr_generate(p: &crate::numeric::BigInt, q: &crate::numeric::BigInt, n: usize) -> Vec<bool> {
828    use crate::numeric::BigInt;
829    let two = BigInt::from_i64(2);
830    let mut r = p.clone();
831    let mut bits = Vec::with_capacity(n);
832    for _ in 0..n {
833        let bit = r.is_odd();
834        bits.push(bit);
835        if bit {
836            r = r.sub(q);
837        }
838        // r ← r/2 (exact: r − bit·q is even because q is odd).
839        r = r.div_rem(&two).map(|(quot, _)| quot).unwrap_or_else(BigInt::zero);
840    }
841    bits
842}
843
844/// The **2-adic complexity** of a bit sequence: `≈ log₂ max(|p|,|q|)` for its FCSR rational `p/q`. Low ⇒
845/// a carry-based keystream (an FCSR / add-with-carry generator); `≈ n/2` for a random sequence.
846pub fn two_adic_complexity(bits: &[bool]) -> usize {
847    match two_adic_reconstruct(bits) {
848        Some((p, q)) => bigint_bitlen(&p).max(bigint_bitlen(&q)),
849        None => bits.len(),
850    }
851}
852
853/// The **maximal order complexity** (Jansen) of a bit sequence: the length of the shortest feedback
854/// shift register — **linear OR nonlinear** — that generates it, i.e. the smallest `L` such that every
855/// length-`L` window has a unique successor (`s[i] = f(s[i-1],…,s[i-L])` for some feedback function
856/// `f`). It is the TOP of the FSR complexity hierarchy — `MOC ≤ 2-adic complexity` and `MOC ≤ linear
857/// complexity` — so it catches nonlinear generators (NFSRs, algebraic combiners with memory) that fool
858/// both Berlekamp–Massey and the 2-adic Rational Approximation. Low relative to `n` ⇒ a short-register
859/// generator; `≈ n/2` for a random sequence. Consistency is monotone in `L`, so we binary-search it.
860pub fn maximal_order_complexity(bits: &[bool]) -> usize {
861    let n = bits.len();
862    if n == 0 {
863        return 0;
864    }
865    // Is every length-`l` window followed by a unique bit?
866    let consistent = |l: usize| -> bool {
867        if l >= n {
868            return true;
869        }
870        let mut succ: std::collections::HashMap<&[bool], bool> = std::collections::HashMap::new();
871        for i in 0..(n - l) {
872            match succ.insert(&bits[i..i + l], bits[i + l]) {
873                Some(prev) if prev != bits[i + l] => return false,
874                _ => {}
875            }
876        }
877        true
878    };
879    // The smallest `L` in `[0, n]` with `consistent(L)`.
880    let (mut lo, mut hi) = (0usize, n);
881    while lo < hi {
882        let mid = (lo + hi) / 2;
883        if consistent(mid) {
884            hi = mid;
885        } else {
886            lo = mid + 1;
887        }
888    }
889    lo
890}
891
892// ---- Algebraic recurrence: the degree-`d` generalization of Berlekamp–Massey (the OPEN rung) ------
893//
894// Maximal order complexity certifies that a sequence is generated by a length-`L` feedback register,
895// but the register's feedback function `f` is a full `2^L` truth table — as large as the data — so a
896// low MOC is a structural *weakness* without a compressor. The algebraic recovery closes that gap when
897// `f` has *low degree*: model `s[i] = f(s[i-1],…,s[i-L])` as a GF(2) polynomial and recover its sparse
898// **algebraic normal form** by linearization. Each monomial of degree `≤ d` over the `L` window bits is
899// treated as an independent unknown; each output position gives one linear equation in those unknowns;
900// a GF(2) Gaussian solve returns the ANF coefficients. For a degree-`d` feedback this is `M = Σₖ₌₀ᵈ
901// C(L,k) = O(Lᵈ)` coefficients — vastly smaller than the `2^L` truth table — so a low-degree nonlinear
902// generator (a quadratic NFSR, an algebraic filter with memory) is *compressed*, not merely diagnosed.
903// This is the algebraic attack (Courtois–Meier linearization) recast as an MDL codec. It stays cheap
904// only while `d` is small; a high-degree `f` sends `M → 2^L` and the sequence back to the ceiling.
905
906/// The monomials of degree `≤ d` over `l` window variables, each as a sorted variable-index list
907/// (`[]` = the constant `1`). The ANF basis for the algebraic recurrence solve.
908fn monomials(l: usize, d: usize) -> Vec<Vec<usize>> {
909    fn combos(start: usize, l: usize, k: usize, cur: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
910        if cur.len() == k {
911            out.push(cur.clone());
912            return;
913        }
914        for v in start..l {
915            cur.push(v);
916            combos(v + 1, l, k, cur, out);
917            cur.pop();
918        }
919    }
920    let mut out = vec![Vec::new()];
921    for k in 1..=d.min(l) {
922        combos(0, l, k, &mut Vec::new(), &mut out);
923    }
924    out
925}
926
927/// Evaluate a monomial (an AND of window bits; the empty product is the constant `1`) on a window.
928fn eval_monomial(mono: &[usize], window: &[bool]) -> bool {
929    mono.iter().all(|&v| window[v])
930}
931
932/// Solve a GF(2) linear system `A·c = b` for a particular solution `c` (free variables set to `0`), with
933/// rows given as `(coefficient bitmask over `ncols` columns, rhs bit)`. Full Gaussian elimination to
934/// reduced row-echelon form over any number of columns (bitset rows, so `> 64` unknowns are fine).
935/// Returns `None` if the system is inconsistent.
936fn solve_gf2_system(rows: &[(Vec<u64>, bool)], ncols: usize) -> Option<Vec<bool>> {
937    let words = ncols.div_ceil(64).max(1);
938    let mut mat: Vec<(Vec<u64>, bool)> = rows.to_vec();
939    let mut pivot_row_of_col = vec![usize::MAX; ncols];
940    let mut r = 0usize;
941    for c in 0..ncols {
942        let (w, bit) = (c / 64, 1u64 << (c % 64));
943        let Some(pr) = (r..mat.len()).find(|&i| mat[i].0[w] & bit != 0) else {
944            continue;
945        };
946        mat.swap(r, pr);
947        for i in 0..mat.len() {
948            if i != r && mat[i].0[w] & bit != 0 {
949                for k in 0..words {
950                    mat[i].0[k] ^= mat[r].0[k];
951                }
952                mat[i].1 ^= mat[r].1;
953            }
954        }
955        pivot_row_of_col[c] = r;
956        r += 1;
957    }
958    // A row that reduced to `0 = 1` proves the system inconsistent.
959    if mat.iter().any(|(coeff, rhs)| *rhs && coeff.iter().all(|&w| w == 0)) {
960        return None;
961    }
962    // Free variables are `0`; each pivot variable equals its row's rhs (that row now has only its own
963    // pivot among the pivot columns, so the free-variables-zero assignment reads straight off the rhs).
964    let mut c = vec![false; ncols];
965    for col in 0..ncols {
966        if pivot_row_of_col[col] != usize::MAX {
967            c[col] = mat[pivot_row_of_col[col]].1;
968        }
969    }
970    Some(c)
971}
972
973/// Recover the **algebraic normal form** of a degree-`≤ d`, order-`l` nonlinear feedback: the ANF
974/// coefficient vector over [`monomials`]`(l, d)` such that replaying `s[i] = ⊕ₘ cₘ · monomialₘ(window)`
975/// reproduces `bits` exactly. Returns `None` if no such low-degree feedback fits (the GF(2) system is
976/// inconsistent, or the recovered coefficients fail to regenerate the sequence). This is the degree-`d`
977/// generalization of [`berlekamp_massey_gf2`] (which is the `d = 1` case): it compresses low-degree
978/// nonlinear generators that fool the linear, 2-adic, and maximal-order tests.
979pub fn detect_algebraic_recurrence(bits: &[bool], l: usize, d: usize) -> Option<Vec<bool>> {
980    if l == 0 || bits.len() <= l {
981        return None;
982    }
983    let monos = monomials(l, d);
984    let m = monos.len();
985    let words = m.div_ceil(64).max(1);
986    let mut rows: Vec<(Vec<u64>, bool)> = Vec::with_capacity(bits.len() - l);
987    for i in l..bits.len() {
988        // Window variable `v` is `s[i-1-v]`, matching [`algebraic_generate`].
989        let window: Vec<bool> = (0..l).map(|v| bits[i - 1 - v]).collect();
990        let mut coeff = vec![0u64; words];
991        for (mi, mono) in monos.iter().enumerate() {
992            if eval_monomial(mono, &window) {
993                coeff[mi / 64] |= 1u64 << (mi % 64);
994            }
995        }
996        rows.push((coeff, bits[i]));
997    }
998    let coeffs = solve_gf2_system(&rows, m)?;
999    if algebraic_generate(l, d, &coeffs, &bits[..l], bits.len()) != bits {
1000        return None; // the recovered feedback does not regenerate the whole sequence
1001    }
1002    Some(coeffs)
1003}
1004
1005/// Replay a degree-`d`, order-`l` algebraic feedback register from ANF coefficients over
1006/// [`monomials`]`(l, d)` and an `l`-bit seed: `s[i] = ⊕ₘ coeffsₘ · monomialₘ(s[i-1],…,s[i-l])`. The
1007/// inverse of [`detect_algebraic_recurrence`].
1008pub fn algebraic_generate(l: usize, d: usize, coeffs: &[bool], seed: &[bool], total: usize) -> Vec<bool> {
1009    let monos = monomials(l, d);
1010    let mut out: Vec<bool> = seed.iter().take(l).copied().collect();
1011    for i in l..total {
1012        let window: Vec<bool> = (0..l).map(|v| out[i - 1 - v]).collect();
1013        let mut bit = false;
1014        for (mi, mono) in monos.iter().enumerate() {
1015            if coeffs.get(mi).copied().unwrap_or(false) && eval_monomial(mono, &window) {
1016                bit ^= true;
1017            }
1018        }
1019        out.push(bit);
1020    }
1021    out.truncate(total);
1022    out
1023}
1024
1025/// The **algebraic complexity** of a bit sequence at maximal degree `max_degree`: the smallest order
1026/// `l` for which [`detect_algebraic_recurrence`] recovers a degree-`≤ max_degree` feedback, together
1027/// with the ANF coefficient count `M` (the true description size — `O(lᵈ)`, not the `2^l` truth table).
1028/// `None` if no order `≤ bits.len()/2` admits such a feedback at that degree.
1029pub fn algebraic_complexity(bits: &[bool], max_degree: usize) -> Option<(usize, usize)> {
1030    for l in 1..=(bits.len() / 2) {
1031        if let Some(coeffs) = detect_algebraic_recurrence(bits, l, max_degree) {
1032            let used = coeffs.iter().filter(|&&c| c).count();
1033            return Some((l, used.max(1)));
1034        }
1035    }
1036    None
1037}
1038
1039// ---- Correlation attack: divide-and-conquer on nonlinear COMBINER generators (the hidden-state rung) --
1040//
1041// A nonlinear combiner runs several LFSRs in parallel and emits `z[i] = C(a₁[i],…,aₖ[i])` for a Boolean
1042// combining function `C`. Every rung so far is blind to it: `z` is a function of the *hidden* register
1043// outputs, not of its own past, so no feedback recovery (linear, word, carry, maximal-order, or
1044// algebraic) applies. But if `C` is correlated with one input — `Pr[z = aⱼ] = p ≠ ½` — then `z` is a
1045// noisy copy of that single LFSR through a binary symmetric channel, and Siegenthaler's attack recovers
1046// `aⱼ`'s initial state ALONE: try each of the `2^Lⱼ − 1` nonzero states, keep the one whose output best
1047// agrees with `z`. This is divide-and-conquer — cost `Σⱼ 2^Lⱼ` instead of `2^(Σ Lⱼ)` — and it isolates
1048// each constituent register independently. It is the classical break the earlier rungs cannot express.
1049
1050/// The certified result of a correlation attack on one target LFSR: the recovered initial state and the
1051/// statistical edge it carries. The `init_state` is a re-checkable witness — regenerate the LFSR from it
1052/// and it reproduces the register the keystream leaks.
1053#[derive(Clone, Debug, PartialEq)]
1054pub struct CorrelationAttack {
1055    /// The register length `L` (`= taps.len()`).
1056    pub register_len: usize,
1057    /// The recovered initial state — the `L`-bit LFSR seed that best correlates with the keystream.
1058    pub init_state: Vec<bool>,
1059    /// `Pr[z = a]` over the sample — the correlation `p` (or `1 − p` if the leak is anti-correlated).
1060    pub agreement: f64,
1061    /// `|agreement − ½|` — the statistical edge; compare against [`spurious_bias_floor`].
1062    pub bias: f64,
1063    /// The number of keystream bits scored, `n`.
1064    pub samples: usize,
1065}
1066
1067/// Siegenthaler's basic correlation attack: recover the initial state of the length-`L` LFSR with
1068/// feedback `taps` that best correlates with `keystream`, by exhaustive search over the `2^L − 1`
1069/// nonzero initial states (each scored by agreement with the keystream). Returns the best state with its
1070/// bias — significant only when `bias` clears [`spurious_bias_floor`]`(L, n)`. Cost `O(2^L·n)`; returns
1071/// `None` for an empty register, a register longer than the keystream, or `L > 20` (past the exhaustive
1072/// cap — the domain of the fast-correlation attack, its scaling successor).
1073pub fn correlation_attack(keystream: &[bool], taps: &[bool]) -> Option<CorrelationAttack> {
1074    let l = taps.len();
1075    let n = keystream.len();
1076    if l == 0 || l > 20 || n <= l {
1077        return None;
1078    }
1079    let mut best: Option<CorrelationAttack> = None;
1080    for code in 1u64..(1u64 << l) {
1081        let seed: Vec<bool> = (0..l).map(|k| (code >> k) & 1 == 1).collect();
1082        let stream = lfsr_generate(taps, &seed, n);
1083        let agree = stream.iter().zip(keystream).filter(|(a, b)| a == b).count();
1084        let bias = (agree as f64 / n as f64 - 0.5).abs();
1085        if best.as_ref().is_none_or(|b| bias > b.bias) {
1086            best = Some(CorrelationAttack {
1087                register_len: l,
1088                init_state: seed,
1089                agreement: agree as f64 / n as f64,
1090                bias,
1091                samples: n,
1092            });
1093        }
1094    }
1095    best
1096}
1097
1098/// The spurious-bias floor: the expected maximum `|agreement − ½|` when the best of `2^register_len`
1099/// *uncorrelated* states is chosen on `n` samples — `≈ √(L·ln2 / 2n)`, from the tail of the maximum of
1100/// `2^L` centered binomials. A recovered [`CorrelationAttack::bias`] well above this floor is a genuine
1101/// correlation (a certified combiner leak); at or below it, the register does not measurably leak.
1102pub fn spurious_bias_floor(register_len: usize, samples: usize) -> f64 {
1103    if samples == 0 {
1104        return 1.0;
1105    }
1106    ((register_len as f64) * std::f64::consts::LN_2 / (2.0 * samples as f64)).sqrt()
1107}
1108
1109// ---- Walsh spectrum & linear cryptanalysis: EVERY linear approximation at once (generalize Rung E) ---
1110//
1111// The correlation attack read ONE correlation, `Pr[z = aⱼ]`. Linear cryptanalysis reads them all. For a
1112// Boolean combining/filter function `C` on `n` inputs, the Walsh–Hadamard transform of its ±1 form
1113// `F(x) = (−1)^C(x)` is `Ŵ(w) = Σₓ (−1)^{C(x) ⊕ ⟨w,x⟩}` — the signed correlation of `C` with EVERY linear
1114// function `⟨w,x⟩` simultaneously, with `Pr[C(x)=⟨w,x⟩] = ½ + Ŵ(w)/2^{n+1}`. Rung E is exactly the slice
1115// at weight-1 masks (`w = eⱼ`); the full spectrum exposes the multi-register approximations E is blind
1116// to — a function can be first-order correlation-immune (every weight-1 `Ŵ` vanishes, so E finds
1117// nothing) yet leak a weight-2 approximation with large bias. The largest `|Ŵ(w)|` is the best linear
1118// approximation (the distinguisher); `nonlinearity = 2^{n-1} − ½·max|Ŵ|` measures distance to the
1119// nearest affine function. The symmetry break: one FWHT diagonalizes the whole correlation structure.
1120// Ceiling: a BENT function has a FLAT spectrum (all `|Ŵ| = 2^{n/2}`) — maximal nonlinearity, no
1121// exploitable linear approximation, the linear-incompressible residue.
1122
1123/// The Walsh–Hadamard transform in place (the `±1` butterfly), on a slice whose length is a power of 2.
1124/// `a[w]` becomes `Σₓ a[x]·(−1)^{⟨w,x⟩}`.
1125pub fn fast_walsh_hadamard(a: &mut [i64]) {
1126    let n = a.len();
1127    debug_assert!(n.is_power_of_two(), "Walsh–Hadamard length must be a power of two");
1128    let mut len = 1;
1129    while len < n {
1130        let mut i = 0;
1131        while i < n {
1132            for j in i..i + len {
1133                let (x, y) = (a[j], a[j + len]);
1134                a[j] = x + y;
1135                a[j + len] = x - y;
1136            }
1137            i += 2 * len;
1138        }
1139        len <<= 1;
1140    }
1141}
1142
1143/// The Walsh spectrum of a Boolean function given as its `2ⁿ`-entry truth table: `Ŵ(w) = Σₓ (−1)^{C(x) ⊕
1144/// ⟨w,x⟩}`, the signed correlation of `C` with every linear function `⟨w,x⟩`. Index `w` is the linear
1145/// mask (bit `i` selects variable `i`). `None` if the length is not a positive power of two.
1146pub fn walsh_spectrum(truth: &[bool]) -> Option<Vec<i64>> {
1147    if truth.is_empty() || !truth.len().is_power_of_two() {
1148        return None;
1149    }
1150    let mut a: Vec<i64> = truth.iter().map(|&b| if b { -1 } else { 1 }).collect();
1151    fast_walsh_hadamard(&mut a);
1152    Some(a)
1153}
1154
1155/// The best linear approximation of a Boolean function: the mask `w` maximizing `|Ŵ(w)|`, with its bias
1156/// `|Pr[C(x)=⟨w,x⟩] − ½| = |Ŵ(w)| / 2^{n+1}`. When `skip_zero`, the trivial constant mask `w = 0` (which
1157/// measures `C`'s own imbalance) is excluded. Returns `(mask, bias)`, or `None` for a malformed table.
1158pub fn best_linear_approximation(truth: &[bool], skip_zero: bool) -> Option<(usize, f64)> {
1159    let spec = walsh_spectrum(truth)?;
1160    let denom = 2.0 * truth.len() as f64; // 2^{n+1}
1161    spec.iter()
1162        .enumerate()
1163        .skip(usize::from(skip_zero))
1164        .max_by_key(|(_, &c)| c.unsigned_abs())
1165        .map(|(w, &c)| (w, c.unsigned_abs() as f64 / denom))
1166}
1167
1168/// The nonlinearity of a Boolean function: `2^{n-1} − ½·maxₘ|Ŵ(w)|` — its Hamming distance to the nearest
1169/// affine function. High ⇒ resistant to linear approximation; maximal (bent, `n` even) ⇒ `2^{n-1} −
1170/// 2^{n/2−1}`. `None` for a malformed table.
1171pub fn nonlinearity(truth: &[bool]) -> Option<u64> {
1172    let spec = walsh_spectrum(truth)?;
1173    let max_abs = spec.iter().map(|&c| c.unsigned_abs()).max()?;
1174    Some((truth.len() as u64) / 2 - max_abs / 2)
1175}
1176
1177/// The correlation-immunity order of a Boolean function (Xiao–Massey): the largest `m` such that every
1178/// Walsh coefficient at a mask of Hamming weight `1..=m` vanishes. Order `≥ 1` ⇒ IMMUNE to the
1179/// first-order correlation attack (Rung E finds nothing) — yet a higher-weight coefficient may still
1180/// leak, which [`best_linear_approximation`] surfaces. `None` for a malformed table.
1181pub fn correlation_immunity_order(truth: &[bool]) -> Option<usize> {
1182    let spec = walsh_spectrum(truth)?;
1183    let n = truth.len().trailing_zeros() as usize;
1184    for m in 1..=n {
1185        let vanishes = spec
1186            .iter()
1187            .enumerate()
1188            .filter(|(w, _)| w.count_ones() as usize == m)
1189            .all(|(_, &c)| c == 0);
1190        if !vanishes {
1191            return Some(m - 1);
1192        }
1193    }
1194    Some(n)
1195}
1196
1197/// The algebraic normal form (ANF) of a Boolean function: coefficients over the `2ⁿ` monomials `∏_{i∈S}
1198/// xᵢ` (S a variable bitmask), via the binary Möbius transform — the GF(2) dual of the Walsh–Hadamard
1199/// butterfly, and its own inverse over GF(2) (applying it to the ANF returns the truth table). `anf[S]` is
1200/// `true` iff the monomial `∏_{i∈S} xᵢ` is present. `None` for a malformed table.
1201pub fn anf(truth: &[bool]) -> Option<Vec<bool>> {
1202    if truth.is_empty() || !truth.len().is_power_of_two() {
1203        return None;
1204    }
1205    let n = truth.len().trailing_zeros();
1206    let mut a = truth.to_vec();
1207    for i in 0..n {
1208        let step = 1usize << i;
1209        let mut j = 0;
1210        while j < a.len() {
1211            for k in j..j + step {
1212                let lo = a[k];
1213                a[k + step] ^= lo;
1214            }
1215            j += step << 1;
1216        }
1217    }
1218    Some(a)
1219}
1220
1221/// The algebraic degree of a Boolean function: the largest number of variables in any monomial present in
1222/// its ANF (0 for a constant, 1 for affine, up to `n`). `None` for a malformed table.
1223pub fn algebraic_degree(truth: &[bool]) -> Option<usize> {
1224    let a = anf(truth)?;
1225    Some(a.iter().enumerate().filter(|(_, &c)| c).map(|(m, _)| (m as u64).count_ones() as usize).max().unwrap_or(0))
1226}
1227
1228/// The autocorrelation spectrum of a Boolean function: `r_f(a) = Σ_x (−1)^{f(x) ⊕ f(x⊕a)}`, the correlation
1229/// of `f` with its shift by `a`. Computed by Wiener–Khinchin — the Walsh transform of the squared Walsh
1230/// spectrum, scaled by `2ⁿ` — so it costs one extra butterfly over [`walsh_spectrum`]. `r_f(0) = 2ⁿ`
1231/// always; `|r_f(a)| = 2ⁿ` exactly when the derivative `f(x⊕a) ⊕ f(x)` is CONSTANT — i.e. `a` is a linear
1232/// structure. `None` for a malformed table.
1233pub fn autocorrelation(truth: &[bool]) -> Option<Vec<i64>> {
1234    let mut w = walsh_spectrum(truth)?;
1235    for c in w.iter_mut() {
1236        *c *= *c;
1237    }
1238    fast_walsh_hadamard(&mut w);
1239    let scale = truth.len() as i64;
1240    for c in w.iter_mut() {
1241        *c /= scale;
1242    }
1243    Some(w)
1244}
1245
1246// ---- Algebraic immunity & annihilators: break the FILTER generator (the algebraic rung, Courtois–Meier)
1247//
1248// The correlation/Walsh rungs attack a combiner through its *statistical* leaks. The algebraic attack is
1249// exact: for a filter generator `z[t] = C(state_t)` (a single LFSR whose state is filtered by a nonlinear
1250// `C`), a low-degree ANNIHILATOR of `C` — a nonzero `g` with `g·C ≡ 0` — turns every keystream bit into a
1251// low-degree equation in the *initial state*. Where `z[t] = 1`, `C(state_t) = 1`, so `g(state_t) = 0`; and
1252// `state_t` is a LINEAR image of the initial state, so `g(state_t)` is a degree-`AI(C)` polynomial in the
1253// initial bits. Collect enough, linearize over monomials of degree `≤ AI(C)`, and solve for the whole
1254// state at once — polynomial for fixed `AI`, versus `2^L` brute force. The algebraic immunity `AI(C)` is
1255// the min degree of a nonzero annihilator of `C` or of `C⊕1`. The symmetry break: a low-degree
1256// annihilator is a hidden algebraic relation that compresses the `2^L` state search to a linear solve.
1257// Ceiling: a maximal-AI filter (`AI ≈ n/2`) leaves no low-degree relation — the algebraic-incompressible
1258// residue.
1259
1260/// A re-checkable annihilator witness: `coeffs` is the ANF (over [`monomials`]`(n_vars, degree)`) of a
1261/// nonzero `g` of degree `degree` that vanishes on `C`'s support (`annihilates_complement = false`) or on
1262/// `C`'s zero-set (`= true`, i.e. `g` annihilates `C ⊕ 1`).
1263#[derive(Clone, Debug, PartialEq, Eq)]
1264pub struct AnnihilatorWitness {
1265    pub n_vars: usize,
1266    pub degree: usize,
1267    pub coeffs: Vec<bool>,
1268    pub annihilates_complement: bool,
1269}
1270
1271/// A basis for the kernel `{c : A·c = 0}` over GF(2), `rows` being the coefficient bitmasks over `ncols`
1272/// columns: one basis vector per free column (that free variable set to 1, the pivots back-substituted).
1273/// Empty if the columns are independent. Bitset RREF, so `ncols > 64` is fine.
1274fn gf2_kernel_basis(rows: &[Vec<u64>], ncols: usize) -> Vec<Vec<bool>> {
1275    let words = ncols.div_ceil(64).max(1);
1276    let mut mat: Vec<Vec<u64>> = rows.to_vec();
1277    let mut pivot_row_of_col = vec![usize::MAX; ncols];
1278    let mut pivot_cols: Vec<usize> = Vec::new();
1279    let mut r = 0usize;
1280    for c in 0..ncols {
1281        let (w, bit) = (c / 64, 1u64 << (c % 64));
1282        let Some(pr) = (r..mat.len()).find(|&i| mat[i][w] & bit != 0) else {
1283            continue;
1284        };
1285        mat.swap(r, pr);
1286        for i in 0..mat.len() {
1287            if i != r && mat[i][w] & bit != 0 {
1288                for k in 0..words {
1289                    mat[i][k] ^= mat[r][k];
1290                }
1291            }
1292        }
1293        pivot_row_of_col[c] = r;
1294        pivot_cols.push(c);
1295        r += 1;
1296    }
1297    // Each free column (no pivot) yields one basis vector: set it to 1, read pivots off their RREF rows
1298    // (a pivot row is `pivot_col ⊕ (free columns)`, so with only this free var at 1, `c[pivot] = row[free]`).
1299    (0..ncols)
1300        .filter(|&c| pivot_row_of_col[c] == usize::MAX)
1301        .map(|free| {
1302            let mut c = vec![false; ncols];
1303            c[free] = true;
1304            for &pc in &pivot_cols {
1305                let row = &mat[pivot_row_of_col[pc]];
1306                if (row[free / 64] >> (free % 64)) & 1 == 1 {
1307                    c[pc] = true;
1308                }
1309            }
1310            c
1311        })
1312        .collect()
1313}
1314
1315/// A basis for the space of Boolean functions of degree `≤ d` (as ANF coefficients over [`monomials`]`(n,
1316/// d)`) that vanish on every point of `zero_set` (indices into `0..2ⁿ`) — every annihilator of that
1317/// support at that degree. Empty if only the zero function qualifies.
1318fn annihilator_basis(n: usize, d: usize, zero_set: &[usize]) -> Vec<Vec<bool>> {
1319    let monos = monomials(n, d);
1320    let m = monos.len();
1321    let words = m.div_ceil(64).max(1);
1322    let rows: Vec<Vec<u64>> = zero_set
1323        .iter()
1324        .map(|&x| {
1325            let window: Vec<bool> = (0..n).map(|i| (x >> i) & 1 == 1).collect();
1326            let mut coeff = vec![0u64; words];
1327            for (mi, mono) in monos.iter().enumerate() {
1328                if eval_monomial(mono, &window) {
1329                    coeff[mi / 64] |= 1u64 << (mi % 64);
1330                }
1331            }
1332            coeff
1333        })
1334        .collect();
1335    gf2_kernel_basis(&rows, m)
1336}
1337
1338/// A single nonzero annihilator of `zero_set` at degree `≤ d`, or `None` if none exists.
1339fn annihilator(n: usize, d: usize, zero_set: &[usize]) -> Option<Vec<bool>> {
1340    annihilator_basis(n, d, zero_set).into_iter().next()
1341}
1342
1343/// The **algebraic immunity** of a Boolean function given as its `2ⁿ` truth table: the minimum degree of
1344/// a nonzero annihilator of `C` or of `C ⊕ 1`, together with a re-checkable witness. Searching degrees
1345/// ascending, the first hit is the minimum (no lower-degree annihilator exists, or an earlier degree
1346/// would have found it). `None` for a malformed table. `AI ≤ ⌈n/2⌉` always.
1347pub fn algebraic_immunity(truth: &[bool]) -> Option<(usize, AnnihilatorWitness)> {
1348    if truth.is_empty() || !truth.len().is_power_of_two() {
1349        return None;
1350    }
1351    let n = truth.len().trailing_zeros() as usize;
1352    let support: Vec<usize> = (0..truth.len()).filter(|&x| truth[x]).collect();
1353    let zeros: Vec<usize> = (0..truth.len()).filter(|&x| !truth[x]).collect();
1354    for d in 0..=n {
1355        if let Some(g) = annihilator(n, d, &support) {
1356            return Some((d, AnnihilatorWitness { n_vars: n, degree: d, coeffs: g, annihilates_complement: false }));
1357        }
1358        if let Some(g) = annihilator(n, d, &zeros) {
1359            return Some((d, AnnihilatorWitness { n_vars: n, degree: d, coeffs: g, annihilates_complement: true }));
1360        }
1361    }
1362    None
1363}
1364
1365/// Evaluate an ANF (coefficients over [`monomials`]`(n_vars, degree)`) at a point `x ∈ 0..2ⁿ`.
1366fn eval_anf(coeffs: &[bool], n_vars: usize, degree: usize, x: usize) -> bool {
1367    let window: Vec<bool> = (0..n_vars).map(|i| (x >> i) & 1 == 1).collect();
1368    monomials(n_vars, degree)
1369        .iter()
1370        .enumerate()
1371        .filter(|(mi, _)| coeffs.get(*mi).copied().unwrap_or(false))
1372        .fold(false, |acc, (_, mono)| acc ^ eval_monomial(mono, &window))
1373}
1374
1375/// Re-check an [`AnnihilatorWitness`] against a truth table with zero trust in the producer: the witness
1376/// is nonzero and vanishes on the required set (support of `C`, or of `C ⊕ 1`). Confirms the
1377/// algebraic-immunity claim independently.
1378pub fn verify_annihilator(truth: &[bool], w: &AnnihilatorWitness) -> bool {
1379    if w.coeffs.iter().all(|&c| !c) || monomials(w.n_vars, w.degree).len() != w.coeffs.len() {
1380        return false;
1381    }
1382    (0..truth.len()).all(|x| {
1383        let must_vanish = if w.annihilates_complement { !truth[x] } else { truth[x] };
1384        !must_vanish || !eval_anf(&w.coeffs, w.n_vars, w.degree, x)
1385    })
1386}
1387
1388/// Recover the initial state of a **filter generator** by the algebraic attack: a length-`l` LFSR with
1389/// feedback `taps` drives a filter `C` (truth table `filter_truth` over its `m = log₂` inputs, read as
1390/// `m` CONSECUTIVE state bits), emitting `keystream[t] = C(seq[t], …, seq[t+m−1])`. Using a min-degree
1391/// annihilator `g` of `C`, each applicable keystream bit becomes `g(⟨r_t,s₀⟩, …) = 0`, a degree-`AI`
1392/// equation in the initial state `s₀`; expanding over `s₀`-monomials of degree `≤ AI` and solving the
1393/// GF(2) system recovers `s₀`. Returns the `l`-bit initial state (verified by regeneration), or `None` if
1394/// there is no low-AI annihilator, the system is underdetermined, or `l > 64`.
1395pub fn algebraic_filter_attack(keystream: &[bool], taps: &[bool], filter_truth: &[bool]) -> Option<Vec<bool>> {
1396    let l = taps.len();
1397    let n = keystream.len();
1398    if l == 0 || l > 64 || !filter_truth.len().is_power_of_two() {
1399        return None;
1400    }
1401    let m = filter_truth.len().trailing_zeros() as usize;
1402    if m == 0 || n < l + m {
1403        return None;
1404    }
1405    let (ai, _) = algebraic_immunity(filter_truth)?;
1406    if ai == 0 {
1407        return None; // a constant filter carries no state
1408    }
1409    let g_monos = monomials(m, ai);
1410    // The whole annihilator space at degree AI, from BOTH sides: functions vanishing on the filter's
1411    // support (applied where the keystream is 1) and on its zero-set (applied where it is 0). A single
1412    // annihilator underdetermines the linearized system; the full basis makes it full rank.
1413    let support: Vec<usize> = (0..filter_truth.len()).filter(|&x| filter_truth[x]).collect();
1414    let zeros: Vec<usize> = (0..filter_truth.len()).filter(|&x| !filter_truth[x]).collect();
1415    let ann_when_one = annihilator_basis(m, ai, &support);
1416    let ann_when_zero = annihilator_basis(m, ai, &zeros);
1417
1418    // Linear forms r_k (bitmask over the l initial-state bits) for each sequence position: seq[k]=⟨r_k,s₀⟩.
1419    let need = n + m;
1420    let mut r: Vec<u64> = Vec::with_capacity(need);
1421    for k in 0..need {
1422        if k < l {
1423            r.push(1u64 << k);
1424        } else {
1425            let mut acc = 0u64;
1426            for (j, &t) in taps.iter().enumerate() {
1427                if t {
1428                    acc ^= r[k - 1 - j];
1429                }
1430            }
1431            r.push(acc);
1432        }
1433    }
1434
1435    // Column layout: monomials(l, ai), column 0 = the constant (pinned to 1 via a seed equation).
1436    let s_monos = monomials(l, ai);
1437    let ncols = s_monos.len();
1438    let col_of: std::collections::HashMap<u64, usize> = s_monos
1439        .iter()
1440        .enumerate()
1441        .map(|(i, mono)| (mono.iter().fold(0u64, |b, &v| b | (1u64 << v)), i))
1442        .collect();
1443    let words = ncols.div_ceil(64).max(1);
1444
1445    // Expand an annihilator `g(y₀,…,y_{m-1})` with `yᵢ = ⟨r_{t+i}, s₀⟩` into an equation row over the
1446    // s₀-monomials (the constant monomial, mask 0, moves to the right-hand side).
1447    let expand = |g: &[bool], t: usize| -> Option<(Vec<u64>, bool)> {
1448        let mut acc: std::collections::HashSet<u64> = std::collections::HashSet::new();
1449        for (gi, mono) in g_monos.iter().enumerate() {
1450            if !g[gi] {
1451                continue;
1452            }
1453            let mut poly: std::collections::HashSet<u64> = std::collections::HashSet::from([0u64]);
1454            for &i in mono {
1455                let form = r[t + i];
1456                let mut next: std::collections::HashSet<u64> = std::collections::HashSet::new();
1457                for &pm in &poly {
1458                    let mut bits = form;
1459                    while bits != 0 {
1460                        let v = bits.trailing_zeros();
1461                        bits &= bits - 1;
1462                        let term = pm | (1u64 << v);
1463                        if !next.insert(term) {
1464                            next.remove(&term); // XOR: cancel duplicates
1465                        }
1466                    }
1467                }
1468                poly = next;
1469            }
1470            for pm in poly {
1471                if !acc.insert(pm) {
1472                    acc.remove(&pm);
1473                }
1474            }
1475        }
1476        let mut coeff = vec![0u64; words];
1477        let mut rhs = false;
1478        for mask in acc {
1479            if mask == 0 {
1480                rhs = true;
1481            } else {
1482                coeff[*col_of.get(&mask)? / 64] |= 1u64 << (col_of[&mask] % 64);
1483            }
1484        }
1485        Some((coeff, rhs))
1486    };
1487
1488    let mut rows: Vec<(Vec<u64>, bool)> = Vec::new();
1489    let mut pin = vec![0u64; words];
1490    pin[0] = 1;
1491    rows.push((pin, true)); // pin the constant monomial to 1
1492
1493    for t in 0..n.saturating_sub(m - 1) {
1494        let anns = if keystream[t] { &ann_when_one } else { &ann_when_zero };
1495        for g in anns {
1496            rows.push(expand(g, t)?);
1497        }
1498    }
1499
1500    let sol = solve_gf2_system(&rows, ncols)?;
1501    // The degree-1 monomial columns hold the recovered state bits.
1502    let state: Vec<bool> = (0..l)
1503        .map(|i| col_of.get(&(1u64 << i)).map(|&ci| sol[ci]).unwrap_or(false))
1504        .collect();
1505
1506    // Verify by regeneration: the recovered state must reproduce the keystream exactly.
1507    let seq = lfsr_generate(taps, &state, need);
1508    let regen: Vec<bool> = (0..n)
1509        .map(|t| {
1510            let idx = (0..m).fold(0usize, |a, i| a | (usize::from(seq[t + i]) << i));
1511            filter_truth[idx]
1512        })
1513        .collect();
1514    (regen == keystream).then_some(state)
1515}
1516
1517// ---- Fast correlation attack: decode the LFSR-as-linear-code, no 2^L search (Meier–Staffelbach) ------
1518//
1519// The correlation attack (Rung E) recovered a leaking LFSR's initial state by exhaustive search over its
1520// 2^L states. The fast correlation attack removes the exponential: the LFSR's output is a codeword of a
1521// linear code (it satisfies its feedback recurrence at every position), so a noisy keystream `z = a ⊕
1522// noise` is a corrupted codeword, and recovering `a` is DECODING. The recurrence gives a low-weight
1523// parity check — `a[i] ⊕ ⊕ⱼ tapsⱼ·a[i−1−j] = 0` — and over GF(2) each squaring of the feedback
1524// polynomial (`g(x)^{2^k}`, the Frobenius map) gives another check of the SAME low weight, stretched.
1525// Instantiated at every anchor, these give many low-weight checks per bit; a Gallager hard-decision
1526// bit-flip decoder then flips the bits sitting in a majority of unsatisfied checks until the sequence
1527// satisfies its recurrence — a valid LFSR output — recovered in time polynomial in `L`, not `2^L`. The
1528// symmetry broken is the code's own automorphism: its dual carries low-weight words that pin the errors.
1529// Ceiling: too much noise (past the Meier–Staffelbach threshold) or a dense feedback polynomial (no
1530// low-weight checks) and the decoder cannot converge — the register resists.
1531
1532/// Meier–Staffelbach fast correlation attack: recover the initial state of the length-`L` LFSR with
1533/// feedback `taps` from a noisy keystream `keystream` (a `z = a ⊕ noise` binary symmetric channel), by
1534/// iterative bit-flip decoding against the low-weight parity checks the feedback recurrence and its
1535/// Frobenius squares generate. Returns the recovered `L`-bit initial state when decoding converges to a
1536/// valid LFSR output that agrees with the keystream noticeably better than chance, else `None` (too much
1537/// noise or too few low-weight checks — the register resists). Polynomial in `L` — no `2^L` search.
1538pub fn fast_correlation_attack(keystream: &[bool], taps: &[bool], max_iters: usize) -> Option<Vec<bool>> {
1539    let (n, l) = (keystream.len(), taps.len());
1540    if l == 0 || n <= 2 * l {
1541        return None;
1542    }
1543    // Base parity-check offsets (relative to an anchor `i`): position `i` and each `i − (1+j)` with a tap.
1544    let mut base: Vec<usize> = vec![0];
1545    for (j, &t) in taps.iter().enumerate() {
1546        if t {
1547            base.push(1 + j);
1548        }
1549    }
1550    // Frobenius: squaring the feedback polynomial doubles the offsets and preserves the weight, so each
1551    // level adds independent low-weight checks that reach further across the sequence.
1552    let mut checks: Vec<Vec<usize>> = Vec::new();
1553    let mut scale = 1usize;
1554    for _ in 0..4 {
1555        let span = base.iter().map(|&o| o * scale).max().unwrap_or(0);
1556        if span >= n {
1557            break;
1558        }
1559        for i in span..n {
1560            checks.push(base.iter().map(|&o| i - o * scale).collect());
1561        }
1562        scale *= 2;
1563    }
1564    if checks.is_empty() {
1565        return None;
1566    }
1567    let mut per_bit = vec![0usize; n];
1568    for c in &checks {
1569        for &p in c {
1570            per_bit[p] += 1;
1571        }
1572    }
1573
1574    let mut y = keystream.to_vec();
1575    for _ in 0..max_iters {
1576        let mut votes = vec![0usize; n];
1577        let mut all_satisfied = true;
1578        for c in &checks {
1579            if c.iter().fold(false, |a, &p| a ^ y[p]) {
1580                all_satisfied = false;
1581                for &p in c {
1582                    votes[p] += 1;
1583                }
1584            }
1585        }
1586        if all_satisfied {
1587            break;
1588        }
1589        let mut flipped = false;
1590        for i in 0..n {
1591            if per_bit[i] > 0 && votes[i] * 2 > per_bit[i] {
1592                y[i] ^= true;
1593                flipped = true;
1594            }
1595        }
1596        if !flipped {
1597            break; // stuck below the flip threshold — decoding cannot progress
1598        }
1599    }
1600
1601    // Accept only a genuine codeword (the recurrence holds everywhere) that correlates with the keystream.
1602    let state = y[..l].to_vec();
1603    let regen = lfsr_generate(taps, &state, n);
1604    if regen != y {
1605        return None;
1606    }
1607    let agree = regen.iter().zip(keystream).filter(|(a, b)| a == b).count() as f64 / n as f64;
1608    (agree > 0.6).then_some(state)
1609}
1610
1611// ---- Clock control / decimation: break the shrinking generator (the structural rung) -----------------
1612//
1613// Every rung so far assumes the keystream is a fixed function of register state read at a fixed cadence.
1614// A clock-controlled generator breaks that: in the SHRINKING GENERATOR a clock register `A` gates a data
1615// register `S`, emitting `S[i]` only when `A[i] = 1` and dropping it otherwise. The output is therefore a
1616// DATA-DEPENDENT decimation of `S` — it is not a fixed function of either register's own past, so no
1617// feedback, correlation, algebraic, or linear rung applies, and its linear complexity is enormous
1618// (≈ `2^{L_S}`). But the decimation is the exploitable symmetry: guess the clock register's `2^{L_A}`
1619// states (divide-and-conquer on the CLOCK alone), and a correct guess fixes the emit positions
1620// `{i : A[i] = 1}`. Each output bit is then `S` at a known position — `output[k] = ⟨r_{iₖ}, s₀⟩`, a
1621// LINEAR equation in `S`'s initial state — so a GF(2) solve recovers `S` and the whole generator falls.
1622// Ceiling: a large or securely-clocked control register makes the clock guess itself exponential.
1623
1624/// The shrinking generator: clock register `A` (feedback `a_taps`, seed `a_seed`) gates data register `S`
1625/// (feedback `s_taps`, seed `s_seed`), emitting `S`'s bit exactly when `A`'s bit is `1`, until `out_len`
1626/// bits are produced. Its output is a data-dependent decimation of `S` with very high linear complexity.
1627pub fn shrinking_generator(
1628    a_taps: &[bool],
1629    a_seed: &[bool],
1630    s_taps: &[bool],
1631    s_seed: &[bool],
1632    out_len: usize,
1633) -> Vec<bool> {
1634    let clocks = out_len.saturating_mul(4) + 64; // A = 1 about half the time; 4× gives ample headroom
1635    let a = lfsr_generate(a_taps, a_seed, clocks);
1636    let s = lfsr_generate(s_taps, s_seed, clocks);
1637    let mut out = Vec::with_capacity(out_len);
1638    for i in 0..clocks {
1639        if a[i] {
1640            out.push(s[i]);
1641            if out.len() == out_len {
1642                break;
1643            }
1644        }
1645    }
1646    out
1647}
1648
1649/// Attack the shrinking generator: recover the initial states of both the clock register `A` (feedback
1650/// `a_taps`) and the data register `S` (feedback `s_taps`) from `output` alone, by divide-and-conquer on
1651/// the clock. Each of the `2^{L_A}` clock states is tried; a guess fixes the emit positions, turning every
1652/// output bit into a linear equation `⟨r_{iₖ}, s₀⟩ = output[k]` in `S`'s initial state, solved over GF(2)
1653/// and verified by regeneration. Returns `(a_state, s_state)` for the first guess that reproduces the
1654/// output, or `None` (clock register too large — `L_A > 22` — or no consistent state). Exponential in
1655/// `L_A` only, not `L_A + L_S`.
1656pub fn attack_shrinking_generator(
1657    output: &[bool],
1658    a_taps: &[bool],
1659    s_taps: &[bool],
1660) -> Option<(Vec<bool>, Vec<bool>)> {
1661    let (la, ls) = (a_taps.len(), s_taps.len());
1662    let m = output.len();
1663    if la == 0 || la > 22 || ls == 0 || ls > 64 || m < ls {
1664        return None;
1665    }
1666    let clocks = m.saturating_mul(4) + 64;
1667    // S's linear forms r_k (bitmask over its ls initial-state bits): S[k] = ⟨r_k, s₀⟩.
1668    let mut r: Vec<u64> = Vec::with_capacity(clocks);
1669    for k in 0..clocks {
1670        if k < ls {
1671            r.push(1u64 << k);
1672        } else {
1673            let mut acc = 0u64;
1674            for (j, &t) in s_taps.iter().enumerate() {
1675                if t {
1676                    acc ^= r[k - 1 - j];
1677                }
1678            }
1679            r.push(acc);
1680        }
1681    }
1682    let words = ls.div_ceil(64).max(1);
1683    for code in 1u64..(1u64 << la) {
1684        let a_seed: Vec<bool> = (0..la).map(|k| (code >> k) & 1 == 1).collect();
1685        let a = lfsr_generate(a_taps, &a_seed, clocks);
1686        let mut positions = Vec::with_capacity(m);
1687        for (i, &bit) in a.iter().enumerate() {
1688            if bit {
1689                positions.push(i);
1690                if positions.len() == m {
1691                    break;
1692                }
1693            }
1694        }
1695        if positions.len() < m {
1696            continue; // this clock did not emit enough bits
1697        }
1698        let rows: Vec<(Vec<u64>, bool)> = positions
1699            .iter()
1700            .enumerate()
1701            .map(|(k, &pos)| {
1702                let mut coeff = vec![0u64; words];
1703                coeff[0] = r[pos];
1704                (coeff, output[k])
1705            })
1706            .collect();
1707        if let Some(sol) = solve_gf2_system(&rows, ls) {
1708            let s_seed: Vec<bool> = sol[..ls].to_vec();
1709            if shrinking_generator(a_taps, &a_seed, s_taps, &s_seed, m) == output {
1710                return Some((a_seed, s_seed));
1711            }
1712        }
1713    }
1714    None
1715}
1716
1717// ---- XL / Gröbner escalation: solve where linearization underdetermines (the algebraic capstone) -----
1718//
1719// The algebraic-immunity rung linearizes a degree-`AI` system over its monomials and solves. When the
1720// number of monomials outruns the equations, that linear solve is underdetermined and stalls. The XL
1721// (eXtended Linearization) algorithm escalates: MULTIPLY each equation by monomials to manufacture more
1722// equations of bounded degree, then linearize the enlarged system. Multiplication surfaces implicit
1723// lower-degree consequences — the "degree fall" — that the original degree could not express, so a system
1724// unsolvable by linearization becomes solvable at a higher operating degree. And an inconsistency that
1725// only appears after multiplication is a REFUTATION: proof the system has no solution (the same certified
1726// UNSAT the algebraic-DRAT bridge consumes). The symmetry broken is the polynomial ideal's own structure
1727// — its Gröbner basis of implicit relations. Ceiling: the regularity degree — a truly random high-degree
1728// system forces the operating degree (and the monomial count) up to the wall.
1729
1730/// The outcome of an XL solve over GF(2).
1731#[derive(Clone, Debug, PartialEq, Eq)]
1732pub enum PolySolveResult {
1733    /// A satisfying assignment, verified against the original equations.
1734    Solved(Vec<bool>),
1735    /// The system is unsatisfiable — a certified refutation (the linearized system became inconsistent).
1736    Refuted,
1737    /// No verdict at the operating degrees tried (raise `max_degree`).
1738    Undetermined,
1739}
1740
1741/// All monomials of degree `≤ deg` over `n` variables, as bitmasks (`≤ 64` variables).
1742fn monomial_masks(n: usize, deg: usize) -> Vec<u64> {
1743    monomials(n, deg).iter().map(|mono| mono.iter().fold(0u64, |b, &v| b | (1u64 << v))).collect()
1744}
1745
1746/// Evaluate a monomial (bitmask; `0` = the constant `1`) at an assignment.
1747fn eval_mask(mask: u64, x: &[bool]) -> bool {
1748    let mut m = mask;
1749    let mut v = true;
1750    while m != 0 {
1751        v &= x[m.trailing_zeros() as usize];
1752        m &= m - 1;
1753    }
1754    v
1755}
1756
1757/// Does `x` satisfy every equation (each an XOR of monomials that must be `0`)?
1758fn verify_poly_system(eqs: &[Vec<u64>], x: &[bool]) -> bool {
1759    eqs.iter().all(|eq| !eq.iter().fold(false, |a, &m| a ^ eval_mask(m, x)))
1760}
1761
1762/// Solve a Boolean polynomial system over GF(2) by **XL** (eXtended Linearization). Each equation is an
1763/// XOR of monomials (bitmasks over the `n_vars` variables, `x²=x`; mask `0` is the constant) that must be
1764/// `0`. For each operating degree `d = 1..=max_degree`, every equation is multiplied by all monomials of
1765/// degree `≤ d − deg(eq)`, the enlarged system is linearized over the degree-`≤ d` monomials (with the
1766/// constant pinned to `1`) and solved: an inconsistency proves the system unsatisfiable ([`Refuted`]), a
1767/// solution whose degree-1 part satisfies the original equations is returned ([`Solved`]), otherwise the
1768/// degree is raised. This solves systems the plain degree-`d` linearization cannot, and refutes ones it
1769/// cannot see contradictory.
1770///
1771/// [`Refuted`]: PolySolveResult::Refuted
1772/// [`Solved`]: PolySolveResult::Solved
1773pub fn solve_polynomial_system_gf2(eqs: &[Vec<u64>], n_vars: usize, max_degree: usize) -> PolySolveResult {
1774    for d in 1..=max_degree {
1775        let cols = monomial_masks(n_vars, d);
1776        let col_of: std::collections::HashMap<u64, usize> =
1777            cols.iter().enumerate().map(|(i, &m)| (m, i)).collect();
1778        let ncols = cols.len();
1779        let words = ncols.div_ceil(64).max(1);
1780
1781        let mut rows: Vec<(Vec<u64>, bool)> = Vec::new();
1782        // Pin the constant monomial to 1.
1783        let mut pin = vec![0u64; words];
1784        let c0 = col_of[&0];
1785        pin[c0 / 64] |= 1u64 << (c0 % 64);
1786        rows.push((pin, true));
1787
1788        for eq in eqs {
1789            let deg_eq = eq.iter().map(|m| m.count_ones() as usize).max().unwrap_or(0);
1790            if deg_eq > d {
1791                continue; // cannot be represented at this operating degree yet
1792            }
1793            for mu in monomial_masks(n_vars, d - deg_eq) {
1794                // Multiply the equation by `mu` (monomial product = OR of masks over GF(2)).
1795                let mut acc: std::collections::HashSet<u64> = std::collections::HashSet::new();
1796                for &m in eq {
1797                    let prod = m | mu;
1798                    if !acc.insert(prod) {
1799                        acc.remove(&prod); // XOR cancellation
1800                    }
1801                }
1802                if acc.is_empty() {
1803                    continue;
1804                }
1805                let mut coeff = vec![0u64; words];
1806                let mut rhs = false;
1807                for mask in acc {
1808                    if mask == 0 {
1809                        rhs = true;
1810                    } else if let Some(&ci) = col_of.get(&mask) {
1811                        coeff[ci / 64] |= 1u64 << (ci % 64);
1812                    }
1813                }
1814                rows.push((coeff, rhs));
1815            }
1816        }
1817
1818        match solve_gf2_system(&rows, ncols) {
1819            None => return PolySolveResult::Refuted,
1820            Some(sol) => {
1821                let x: Vec<bool> = (0..n_vars)
1822                    .map(|i| col_of.get(&(1u64 << i)).map(|&ci| sol[ci]).unwrap_or(false))
1823                    .collect();
1824                if verify_poly_system(eqs, &x) {
1825                    return PolySolveResult::Solved(x);
1826                }
1827            }
1828        }
1829    }
1830    PolySolveResult::Undetermined
1831}
1832
1833/// Serialize a `BigInt` as sign byte + varint length + little-endian magnitude, trimmed to minimal
1834/// length (`to_le_bytes` is limb-aligned, so a small value would otherwise carry trailing zero bytes).
1835fn write_bigint(x: &crate::numeric::BigInt, out: &mut Vec<u8>) {
1836    let (neg, mut bytes) = x.to_le_bytes();
1837    while bytes.last() == Some(&0) {
1838        bytes.pop();
1839    }
1840    out.push(neg as u8);
1841    write_uvarint(bytes.len() as u64, out);
1842    out.extend_from_slice(&bytes);
1843}
1844
1845/// The inverse of [`write_bigint`].
1846fn read_bigint(buf: &[u8], pos: &mut usize) -> Option<crate::numeric::BigInt> {
1847    let neg = *buf.get(*pos)? != 0;
1848    *pos += 1;
1849    let len = read_uvarint(buf, pos)? as usize;
1850    let bytes = buf.get(*pos..pos.checked_add(len)?)?;
1851    *pos += len;
1852    Some(crate::numeric::BigInt::from_le_bytes(neg, bytes))
1853}
1854
1855/// If a byte column is an FCSR (carry-based) keystream — its bit expansion is the 2-adic rational
1856/// `p/q` and that rational regenerates every bit — return `(p, q)`. Catches carry generators (add-with-
1857/// carry, mod-2ⁿ LCGs) that fool every linear generator; `consider` keeps it only when `p/q` beats raw.
1858fn detect_fcsr_bytes(v: &[i64]) -> Option<(crate::numeric::BigInt, crate::numeric::BigInt)> {
1859    let bits = bytes_to_bits(v);
1860    let (p, q) = two_adic_reconstruct(&bits)?;
1861    if fcsr_generate(&p, &q, bits.len()) != bits {
1862        return None;
1863    }
1864    Some((p, q))
1865}
1866
1867/// Recognize `v[i] = a + b·(i mod p)` for a small period `p` and synthesize the generator.
1868pub fn detect_modular_affine(v: &[i64]) -> Option<GenExpr> {
1869    const MAX_PERIOD: usize = 16;
1870    if v.len() < 4 {
1871        return None;
1872    }
1873    for p in 2..=MAX_PERIOD.min(v.len() / 2) {
1874        let a = v[0];
1875        let b = v[1].wrapping_sub(v[0]);
1876        if b != 0 && (0..v.len()).all(|i| v[i] == a.wrapping_add(b.wrapping_mul((i % p) as i64))) {
1877            return Some(GenExpr::Add(
1878                Box::new(GenExpr::Const(a)),
1879                Box::new(GenExpr::Mul(
1880                    Box::new(GenExpr::Const(b)),
1881                    Box::new(GenExpr::Mod(Box::new(GenExpr::Index), Box::new(GenExpr::Const(p as i64)))),
1882                )),
1883            ));
1884        }
1885    }
1886    None
1887}
1888
1889// ---- Columnar fallback encoders ------------------------------------------------------------
1890
1891/// Delta: first value then zig-zag successive differences. Wins on monotone columns.
1892pub fn delta_encode(out: &mut Vec<u8>, v: &[i64]) {
1893    out.push(T_INTS_DELTA);
1894    write_uvarint(v.len() as u64, out);
1895    if let Some(&first) = v.first() {
1896        write_uvarint(zigzag(first), out);
1897        let mut prev = first;
1898        for &x in &v[1..] {
1899            write_uvarint(zigzag(x.wrapping_sub(prev)), out);
1900            prev = x;
1901        }
1902    }
1903}
1904
1905/// Delta-of-delta: first value, first delta, then zig-zag second differences. Wins on near-linear
1906/// progressions (timestamps with jitter).
1907pub fn dod_encode(out: &mut Vec<u8>, v: &[i64]) {
1908    out.push(T_INTS_DOD);
1909    write_uvarint(v.len() as u64, out);
1910    if v.is_empty() {
1911        return;
1912    }
1913    write_uvarint(zigzag(v[0]), out);
1914    if v.len() == 1 {
1915        return;
1916    }
1917    let mut prev_delta = v[1].wrapping_sub(v[0]);
1918    write_uvarint(zigzag(prev_delta), out);
1919    let mut prev = v[1];
1920    for &x in &v[2..] {
1921        let d = x.wrapping_sub(prev);
1922        write_uvarint(zigzag(d.wrapping_sub(prev_delta)), out);
1923        prev_delta = d;
1924        prev = x;
1925    }
1926}
1927
1928/// Frame-of-reference: subtract the column minimum, bit-pack the residuals. Wins on clustered
1929/// columns (a small range around any base).
1930pub fn for_encode(out: &mut Vec<u8>, v: &[i64]) {
1931    out.push(T_INTS_FOR);
1932    write_uvarint(v.len() as u64, out);
1933    let min = v.iter().copied().min().unwrap_or(0);
1934    write_uvarint(zigzag(min), out);
1935    if v.is_empty() {
1936        out.push(0);
1937        return;
1938    }
1939    let max = v.iter().copied().max().unwrap();
1940    let range = (max as u64).wrapping_sub(min as u64);
1941    let width = if range == 0 { 0 } else { (64 - range.leading_zeros()) as u8 };
1942    out.push(width);
1943    if width > 0 {
1944        let residuals: Vec<u64> = v.iter().map(|&x| (x as u64).wrapping_sub(min as u64)).collect();
1945        out.extend_from_slice(&bitpack(&residuals, width));
1946    }
1947}
1948
1949/// Run-length: (value, run-length) pairs. Wins on columns of repeated runs.
1950pub fn rle_encode(out: &mut Vec<u8>, v: &[i64]) {
1951    let mut runs: Vec<(i64, u64)> = Vec::new();
1952    for &x in v {
1953        match runs.last_mut() {
1954            Some(last) if last.0 == x => last.1 += 1,
1955            _ => runs.push((x, 1)),
1956        }
1957    }
1958    out.push(T_INTS_RLE);
1959    write_uvarint(runs.len() as u64, out);
1960    for (val, len) in runs {
1961        write_uvarint(zigzag(val), out);
1962        write_uvarint(len, out);
1963    }
1964}
1965
1966/// Dictionary: distinct values (first-seen order) then a bit-packed index column. Wins on
1967/// low-cardinality columns.
1968pub fn dict_encode(v: &[i64]) -> Vec<u8> {
1969    let mut dict: Vec<i64> = Vec::new();
1970    let mut index_of: std::collections::HashMap<i64, u64> = std::collections::HashMap::new();
1971    let mut indices: Vec<u64> = Vec::with_capacity(v.len());
1972    for &x in v {
1973        let idx = *index_of.entry(x).or_insert_with(|| {
1974            dict.push(x);
1975            (dict.len() - 1) as u64
1976        });
1977        indices.push(idx);
1978    }
1979    let mut out = vec![T_INTS_DICT];
1980    write_uvarint(dict.len() as u64, &mut out);
1981    for &d in &dict {
1982        write_uvarint(zigzag(d), &mut out);
1983    }
1984    write_uvarint(v.len() as u64, &mut out);
1985    let iw = if dict.len() <= 1 { 0 } else { (64 - ((dict.len() - 1) as u64).leading_zeros()) as u8 };
1986    out.push(iw);
1987    if iw > 0 {
1988        out.extend_from_slice(&bitpack(&indices, iw));
1989    }
1990    out
1991}
1992
1993/// Keep `cand` if it is smaller than the current `best` — the MDL argmin over candidate encodings.
1994pub fn consider(best: &mut Vec<u8>, cand: Vec<u8>) {
1995    if cand.len() < best.len() {
1996        *best = cand;
1997    }
1998}
1999
2000// ---- The description engine (encode) -------------------------------------------------------
2001
2002/// Build every applicable column encoding and append the smallest to `out`. The plain-varint
2003/// baseline is always a candidate, so the result is never larger than the varint form.
2004pub fn emit_best_int_column(v: &[i64], out: &mut Vec<u8>) {
2005    let mut best = Vec::new();
2006    best.push(T_INTS);
2007    leb128_encode(&mut best, v.iter().copied(), v.len());
2008
2009    if let Some((base, stride)) = detect_affine(v) {
2010        let mut c = vec![T_INTS_AFFINE];
2011        write_uvarint(zigzag(base), &mut c);
2012        write_uvarint(zigzag(stride), &mut c);
2013        write_uvarint(v.len() as u64, &mut c);
2014        consider(&mut best, c);
2015    }
2016    if let Some((base, ratio)) = detect_geometric(v) {
2017        let mut c = vec![T_INTS_GEOMETRIC];
2018        write_uvarint(zigzag(base), &mut c);
2019        write_uvarint(zigzag(ratio), &mut c);
2020        write_uvarint(v.len() as u64, &mut c);
2021        consider(&mut best, c);
2022    }
2023    if let Some(p) = detect_period(v) {
2024        let mut c = vec![T_INTS_PERIODIC];
2025        write_uvarint(v.len() as u64, &mut c);
2026        emit_best_int_column(&v[..p], &mut c);
2027        consider(&mut best, c);
2028    }
2029    if let Some((dom, exc)) = detect_sparse(v) {
2030        let mut c = vec![T_INTS_SPARSE];
2031        write_uvarint(zigzag(dom), &mut c);
2032        write_uvarint(v.len() as u64, &mut c);
2033        write_uvarint(exc.len() as u64, &mut c);
2034        let mut prev = 0usize;
2035        for (i, x) in &exc {
2036            write_uvarint((i - prev) as u64, &mut c);
2037            prev = *i;
2038            write_uvarint(zigzag(*x), &mut c);
2039        }
2040        consider(&mut best, c);
2041    }
2042    if let Some((degree, seeds)) = detect_poly_generator(v) {
2043        let mut c = vec![T_INTS_POLY, degree];
2044        write_uvarint(v.len() as u64, &mut c);
2045        for &s in &seeds {
2046            write_uvarint(zigzag(s), &mut c);
2047        }
2048        consider(&mut best, c);
2049    }
2050    // Ship the GENERATOR, not the data: a linear-recurrence column (Fibonacci-class) becomes its order,
2051    // coefficients, and seeds — a handful of numbers regardless of n. Reaches sequences the polynomial
2052    // detector cannot (finite differences that never settle).
2053    if let Some((coeffs, seeds)) = detect_linear_recurrence(v) {
2054        let mut c = vec![T_INTS_LRECUR, coeffs.len() as u8];
2055        write_uvarint(v.len() as u64, &mut c);
2056        for &x in &coeffs {
2057            write_uvarint(zigzag(x), &mut c);
2058        }
2059        for &s in &seeds {
2060            write_uvarint(zigzag(s), &mut c);
2061        }
2062        consider(&mut best, c);
2063    }
2064    if let Some(expr) = detect_modular_affine(v) {
2065        let mut c = vec![T_GEN];
2066        serialize_gen(&expr, &mut c);
2067        write_uvarint(v.len() as u64, &mut c);
2068        consider(&mut best, c);
2069    }
2070    let mut delta = Vec::new();
2071    delta_encode(&mut delta, v);
2072    consider(&mut best, delta);
2073
2074    let mut dod = Vec::new();
2075    dod_encode(&mut dod, v);
2076    consider(&mut best, dod);
2077
2078    if !v.is_empty() && v.iter().all(|&x| (0..256).contains(&x)) {
2079        let mut b = vec![T_BYTES];
2080        write_uvarint(v.len() as u64, &mut b);
2081        b.extend(v.iter().map(|&x| x as u8));
2082        consider(&mut best, b);
2083    }
2084
2085    let mut for_c = Vec::new();
2086    for_encode(&mut for_c, v);
2087    consider(&mut best, for_c);
2088
2089    let mut rle = Vec::new();
2090    rle_encode(&mut rle, v);
2091    consider(&mut best, rle);
2092
2093    consider(&mut best, dict_encode(v));
2094
2095    // LAST RESORT — the LFSR attack: if this is a small byte column that NOTHING above compressed (it
2096    // looks random), run Berlekamp–Massey. A keystream from a short LFSR collapses to `O(L)` bits even
2097    // though every other generator sees noise. Gated by size and by "nothing else worked", so the
2098    // `O(bits²)` cost is paid only where it can pay off.
2099    if !v.is_empty()
2100        && v.len() <= LFSR_MAX_BYTES
2101        && best.len() >= v.len()
2102        && v.iter().all(|&x| (0..256).contains(&x))
2103    {
2104        if let Some((l, taps, seed)) = detect_lfsr_bytes(v) {
2105            let mut c = vec![T_INTS_LFSR];
2106            write_uvarint(v.len() as u64, &mut c);
2107            write_uvarint(l as u64, &mut c);
2108            let tap_vals: Vec<u64> = taps.iter().map(|&b| b as u64).collect();
2109            c.extend_from_slice(&bitpack(&tap_vals, 1));
2110            let seed_vals: Vec<u64> = seed.iter().map(|&b| b as u64).collect();
2111            c.extend_from_slice(&bitpack(&seed_vals, 1));
2112            consider(&mut best, c);
2113        }
2114    }
2115
2116    // LAST RESORT — the CARRY attack: an FCSR keystream (add-with-carry / mod-2ⁿ LCG) fools every LINEAR
2117    // generator (its linear complexity is high), so it survives even the LFSR pass above. The 2-adic
2118    // Rational Approximation collapses it to a small rational `p/q`. Same gating as the LFSR pass, plus
2119    // its own size bound (the reconstruction is bignum).
2120    if !v.is_empty()
2121        && v.len() <= FCSR_MAX_BYTES
2122        && best.len() >= v.len()
2123        && v.iter().all(|&x| (0..256).contains(&x))
2124    {
2125        if let Some((p, q)) = detect_fcsr_bytes(v) {
2126            let mut c = vec![T_INTS_FCSR];
2127            write_uvarint(v.len() as u64, &mut c);
2128            write_bigint(&p, &mut c);
2129            write_bigint(&q, &mut c);
2130            consider(&mut best, c);
2131        }
2132    }
2133
2134    out.extend_from_slice(&best);
2135}
2136
2137/// The **computable upper bound on Kolmogorov complexity** of `v` over this description language:
2138/// the shortest self-delimiting program (from the generator menu) that reproduces `v`. The returned
2139/// bytes are a re-checkable witness — [`decode_int_seq`] reproduces `v` exactly.
2140pub fn describe_int_seq(v: &[i64]) -> Vec<u8> {
2141    let mut out = Vec::new();
2142    emit_best_int_column(v, &mut out);
2143    out
2144}
2145
2146// ---- The description engine (decode) -------------------------------------------------------
2147
2148/// Reject a decoded element count that exceeds `max_elements`, before any `count`-sized
2149/// materialization (the small-message-huge-output guard for generator columns).
2150#[inline]
2151fn bounded(n: u64, max_elements: usize) -> Option<usize> {
2152    let n = n as usize;
2153    (n <= max_elements).then_some(n)
2154}
2155
2156/// Decode one int column whose tag byte has already been read as `tag`. The inverse of
2157/// [`emit_best_int_column`]; returns `None` for a non-int tag or a corrupt/hostile body.
2158/// `max_elements` caps generator expansion; `depth` bounds the periodic-block recursion.
2159pub fn decode_int_column_body(
2160    tag: u8,
2161    buf: &[u8],
2162    pos: &mut usize,
2163    max_elements: usize,
2164    depth: u32,
2165) -> Option<Vec<i64>> {
2166    Some(match tag {
2167        // Adaptive sign: the count's low bit says whether the column was zig-zag encoded.
2168        T_INTS => {
2169            let header = read_uvarint(buf, pos)?;
2170            let signed = header & 1 == 1;
2171            let n = bounded(header >> 1, max_elements)?;
2172            let mut v = Vec::with_capacity(n.min(PREALLOC_CAP));
2173            for _ in 0..n {
2174                let u = read_uvarint(buf, pos)?;
2175                v.push(if signed { unzigzag(u) } else { u as i64 });
2176            }
2177            v
2178        }
2179        // Closed form `base + stride·i`, replayed with the SAME wrapping arithmetic the encoder verified.
2180        T_INTS_AFFINE => {
2181            let base = unzigzag(read_uvarint(buf, pos)?);
2182            let stride = unzigzag(read_uvarint(buf, pos)?);
2183            let n = bounded(read_uvarint(buf, pos)?, max_elements)?;
2184            let mut v = Vec::with_capacity(n.min(PREALLOC_CAP));
2185            for i in 0..n {
2186                v.push(base.wrapping_add((i as i64).wrapping_mul(stride)));
2187            }
2188            v
2189        }
2190        // Geometric generator: replay `wrapping_mul` — exact even across overflow.
2191        T_INTS_GEOMETRIC => {
2192            let base = unzigzag(read_uvarint(buf, pos)?);
2193            let ratio = unzigzag(read_uvarint(buf, pos)?);
2194            let n = bounded(read_uvarint(buf, pos)?, max_elements)?;
2195            let mut v = Vec::with_capacity(n.min(PREALLOC_CAP));
2196            let mut cur = base;
2197            for _ in 0..n {
2198                v.push(cur);
2199                cur = cur.wrapping_mul(ratio);
2200            }
2201            v
2202        }
2203        // Cyclic generator: decode the period BLOCK (itself a best-encoded column), emit `block[i % p]`.
2204        T_INTS_PERIODIC => {
2205            let n = bounded(read_uvarint(buf, pos)?, max_elements)?;
2206            if depth >= DECODE_MAX_DEPTH {
2207                return None;
2208            }
2209            let block_tag = *buf.get(*pos)?;
2210            *pos += 1;
2211            let block = decode_int_column_body(block_tag, buf, pos, max_elements, depth + 1)?;
2212            let p = block.len();
2213            if p == 0 {
2214                return None; // an empty block has no period — malformed
2215            }
2216            let mut v = Vec::with_capacity(n.min(PREALLOC_CAP));
2217            for i in 0..n {
2218                v.push(block[i % p]);
2219            }
2220            v
2221        }
2222        // Sparse column: fill with the dominant value, patch the delta-indexed exceptions.
2223        T_INTS_SPARSE => {
2224            let dom = unzigzag(read_uvarint(buf, pos)?);
2225            let n = bounded(read_uvarint(buf, pos)?, max_elements)?;
2226            let num_exc = bounded(read_uvarint(buf, pos)?, max_elements)?;
2227            let mut v = Vec::with_capacity(n.min(PREALLOC_CAP));
2228            v.resize(n, dom);
2229            let mut idx = 0usize;
2230            for _ in 0..num_exc {
2231                idx = idx.checked_add(read_uvarint(buf, pos)? as usize)?;
2232                let val = unzigzag(read_uvarint(buf, pos)?);
2233                *v.get_mut(idx)? = val; // out-of-range exception index → clean None
2234            }
2235            v
2236        }
2237        // Polynomial generator: read the degree-bounded seeds, replay the difference engine.
2238        T_INTS_POLY => {
2239            let degree = *buf.get(*pos)? as usize;
2240            *pos += 1;
2241            if degree > MAX_POLY_DEGREE {
2242                return None;
2243            }
2244            let n = bounded(read_uvarint(buf, pos)?, max_elements)?;
2245            let mut seeds = Vec::with_capacity(degree + 1);
2246            for _ in 0..=degree {
2247                seeds.push(unzigzag(read_uvarint(buf, pos)?));
2248            }
2249            reconstruct_poly(&seeds, n)
2250        }
2251        // Linear-recurrence generator: read the order-bounded coefficients + seeds, replay the recurrence.
2252        T_INTS_LRECUR => {
2253            let k = *buf.get(*pos)? as usize;
2254            *pos += 1;
2255            if k == 0 || k > MAX_RECUR_ORDER {
2256                return None; // malformed / untrusted order
2257            }
2258            let n = bounded(read_uvarint(buf, pos)?, max_elements)?;
2259            let mut coeffs = Vec::with_capacity(k);
2260            for _ in 0..k {
2261                coeffs.push(unzigzag(read_uvarint(buf, pos)?));
2262            }
2263            let mut seeds = Vec::with_capacity(k);
2264            for _ in 0..k {
2265                seeds.push(unzigzag(read_uvarint(buf, pos)?));
2266            }
2267            reconstruct_recurrence(&coeffs, &seeds, n)
2268        }
2269        // LFSR keystream: read the linear complexity L, the L feedback taps and L seed bits, then
2270        // replay the register for 8·n bits and pack back to n bytes.
2271        T_INTS_LFSR => {
2272            let n = bounded(read_uvarint(buf, pos)?, max_elements)?; // byte count
2273            let l = read_uvarint(buf, pos)? as usize;
2274            let total_bits = n.checked_mul(8)?;
2275            if l > total_bits {
2276                return None;
2277            }
2278            let nbytes = l.div_ceil(8);
2279            let tap_bytes = buf.get(*pos..pos.checked_add(nbytes)?)?;
2280            *pos += nbytes;
2281            let taps: Vec<bool> = bitunpack(tap_bytes, l, 1)?.into_iter().map(|x| x == 1).collect();
2282            let seed_bytes = buf.get(*pos..pos.checked_add(nbytes)?)?;
2283            *pos += nbytes;
2284            let seed: Vec<bool> = bitunpack(seed_bytes, l, 1)?.into_iter().map(|x| x == 1).collect();
2285            bits_to_bytes(&lfsr_generate(&taps, &seed, total_bits))
2286        }
2287        // FCSR keystream: read the byte count and the rational p/q, then replay the 2-adic expansion.
2288        T_INTS_FCSR => {
2289            let n = bounded(read_uvarint(buf, pos)?, max_elements)?; // byte count
2290            let p = read_bigint(buf, pos)?;
2291            let q = read_bigint(buf, pos)?;
2292            if !q.is_odd() {
2293                return None; // an FCSR connection integer must be odd
2294            }
2295            let total_bits = n.checked_mul(8)?;
2296            bits_to_bytes(&fcsr_generate(&p, &q, total_bits))
2297        }
2298        // General generator: parse the bounded `GenExpr`, evaluate it at 0..n in the sandbox.
2299        T_GEN => {
2300            let mut budget = MAX_GEN_NODES;
2301            let expr = deserialize_gen(buf, pos, &mut budget, 0)?;
2302            let n = bounded(read_uvarint(buf, pos)?, max_elements)?;
2303            let mut v = Vec::with_capacity(n.min(PREALLOC_CAP));
2304            for i in 0..n {
2305                v.push(gen_eval(&expr, i as i64));
2306            }
2307            v
2308        }
2309        // Byte column: read the count, widen each raw byte to an Int.
2310        T_BYTES => {
2311            let n = read_uvarint(buf, pos)? as usize;
2312            let raw = buf.get(*pos..pos.checked_add(n)?)?;
2313            *pos += n;
2314            raw.iter().map(|&b| b as i64).collect()
2315        }
2316        // Delta column: cumulative-sum the zig-zag differences.
2317        T_INTS_DELTA => {
2318            let n = read_uvarint(buf, pos)? as usize;
2319            let mut v = Vec::with_capacity(n.min(PREALLOC_CAP));
2320            if n > 0 {
2321                let mut cur = unzigzag(read_uvarint(buf, pos)?);
2322                v.push(cur);
2323                for _ in 1..n {
2324                    cur = cur.wrapping_add(unzigzag(read_uvarint(buf, pos)?));
2325                    v.push(cur);
2326                }
2327            }
2328            v
2329        }
2330        // Delta-of-delta column: double cumulative sum (the inverse of `dod_encode`).
2331        T_INTS_DOD => {
2332            let n = read_uvarint(buf, pos)? as usize;
2333            let mut v = Vec::with_capacity(n.min(PREALLOC_CAP));
2334            if n > 0 {
2335                let first = unzigzag(read_uvarint(buf, pos)?);
2336                v.push(first);
2337                if n > 1 {
2338                    let mut prev_delta = unzigzag(read_uvarint(buf, pos)?);
2339                    let mut prev = first.wrapping_add(prev_delta);
2340                    v.push(prev);
2341                    for _ in 2..n {
2342                        prev_delta = prev_delta.wrapping_add(unzigzag(read_uvarint(buf, pos)?));
2343                        prev = prev.wrapping_add(prev_delta);
2344                        v.push(prev);
2345                    }
2346                }
2347            }
2348            v
2349        }
2350        // Frame-of-reference column: unpack the bit-packed residuals, add the minimum.
2351        T_INTS_FOR => {
2352            let n = read_uvarint(buf, pos)? as usize;
2353            let min = unzigzag(read_uvarint(buf, pos)?);
2354            let width = *buf.get(*pos)?;
2355            *pos += 1;
2356            if width > 64 {
2357                return None;
2358            }
2359            let mut v = Vec::with_capacity(n.min(PREALLOC_CAP));
2360            if width == 0 {
2361                for _ in 0..n {
2362                    v.push(min);
2363                }
2364            } else {
2365                let nbytes = n.checked_mul(width as usize)?.div_ceil(8);
2366                let bytes = buf.get(*pos..pos.checked_add(nbytes)?)?;
2367                *pos += nbytes;
2368                for r in bitunpack(bytes, n, width)? {
2369                    v.push(r.wrapping_add(min as u64) as i64);
2370                }
2371            }
2372            v
2373        }
2374        // Run-length column: expand each (value, run-length) pair, capped against a corrupt length.
2375        T_INTS_RLE => {
2376            let runs = read_uvarint(buf, pos)? as usize;
2377            let mut v: Vec<i64> = Vec::new();
2378            for _ in 0..runs {
2379                let val = unzigzag(read_uvarint(buf, pos)?);
2380                let len = read_uvarint(buf, pos)? as usize;
2381                if v.len().checked_add(len)? > RLE_MAX_TOTAL {
2382                    return None;
2383                }
2384                v.resize(v.len() + len, val);
2385            }
2386            v
2387        }
2388        // Dictionary column: read the distinct values, map the bit-packed indices back through them.
2389        T_INTS_DICT => {
2390            let d = read_uvarint(buf, pos)? as usize;
2391            let mut dict = Vec::with_capacity(d.min(PREALLOC_CAP));
2392            for _ in 0..d {
2393                dict.push(unzigzag(read_uvarint(buf, pos)?));
2394            }
2395            let n = read_uvarint(buf, pos)? as usize;
2396            let iw = *buf.get(*pos)?;
2397            *pos += 1;
2398            if iw > 64 {
2399                return None;
2400            }
2401            let mut v = Vec::with_capacity(n.min(PREALLOC_CAP));
2402            if iw == 0 {
2403                if n > 0 {
2404                    let val = *dict.first()?;
2405                    v.resize(n, val);
2406                }
2407            } else {
2408                let nbytes = n.checked_mul(iw as usize)?.div_ceil(8);
2409                let bytes = buf.get(*pos..pos.checked_add(nbytes)?)?;
2410                *pos += nbytes;
2411                for ix in bitunpack(bytes, n, iw)? {
2412                    v.push(*dict.get(ix as usize)?);
2413                }
2414            }
2415            v
2416        }
2417        _ => return None,
2418    })
2419}
2420
2421/// Decode a full [`describe_int_seq`] byte string back to the exact sequence. Requires the whole
2422/// buffer to be consumed (a clean round-trip witness). Uses a default element cap.
2423pub fn decode_int_seq(bytes: &[u8]) -> Option<Vec<i64>> {
2424    let mut pos = 0usize;
2425    let tag = *bytes.get(pos)?;
2426    pos += 1;
2427    let v = decode_int_column_body(tag, bytes, &mut pos, DEFAULT_MAX_ELEMENTS, 0)?;
2428    (pos == bytes.len()).then_some(v)
2429}
2430
2431#[cfg(test)]
2432mod tests {
2433    use super::*;
2434
2435    fn roundtrips(v: &[i64]) {
2436        let enc = describe_int_seq(v);
2437        let dec = decode_int_seq(&enc);
2438        assert_eq!(dec.as_deref(), Some(v), "round-trip failed for {v:?} (enc {enc:?})");
2439    }
2440
2441    #[test]
2442    fn affine_round_trips_and_beats_varint() {
2443        let v: Vec<i64> = (0..1000).map(|i| 10 + 7 * i).collect();
2444        roundtrips(&v);
2445        // The generator (3 numbers) must be far smaller than 1000 varints.
2446        assert!(describe_int_seq(&v).len() < 20, "affine must ship as a generator, not data");
2447    }
2448
2449    #[test]
2450    fn geometric_round_trips() {
2451        let v: Vec<i64> = (0..40).map(|i| 3i64.wrapping_mul(2i64.wrapping_pow(i))).collect();
2452        roundtrips(&v);
2453    }
2454
2455    #[test]
2456    fn polynomial_round_trips() {
2457        let v: Vec<i64> = (0..500).map(|i| i * i - 3 * i + 5).collect();
2458        roundtrips(&v);
2459        assert!(describe_int_seq(&v).len() < 30, "poly must ship as finite-difference seeds");
2460    }
2461
2462    #[test]
2463    fn linear_recurrence_ships_the_generator() {
2464        // Fibonacci — order-2 recurrence, NOT polynomial (its finite differences never settle), so
2465        // ONLY the recurrence detector catches it: 60 terms collapse to a handful of numbers.
2466        let mut fib = vec![0i64, 1];
2467        while fib.len() < 60 {
2468            let n = fib.len();
2469            fib.push(fib[n - 1].wrapping_add(fib[n - 2]));
2470        }
2471        roundtrips(&fib);
2472        assert!(describe_int_seq(&fib).len() < 15, "Fibonacci ships as (order, coeffs, seeds), not data");
2473
2474        // Lucas (same recurrence, different seeds).
2475        let mut lucas = vec![2i64, 1];
2476        while lucas.len() < 60 {
2477            let n = lucas.len();
2478            lucas.push(lucas[n - 1].wrapping_add(lucas[n - 2]));
2479        }
2480        roundtrips(&lucas);
2481        assert!(describe_int_seq(&lucas).len() < 15);
2482
2483        // Pell: c = [2, 1].
2484        let mut pell = vec![0i64, 1];
2485        while pell.len() < 50 {
2486            let n = pell.len();
2487            pell.push(2i64.wrapping_mul(pell[n - 1]).wrapping_add(pell[n - 2]));
2488        }
2489        roundtrips(&pell);
2490        assert!(describe_int_seq(&pell).len() < 15);
2491
2492        // A general order-3 recurrence v[i] = v[i-1] + v[i-2] − v[i-3].
2493        let mut r3 = vec![1i64, 2, 3];
2494        while r3.len() < 40 {
2495            let n = r3.len();
2496            r3.push(r3[n - 1].wrapping_add(r3[n - 2]).wrapping_sub(r3[n - 3]));
2497        }
2498        roundtrips(&r3);
2499        assert!(describe_int_seq(&r3).len() < 18);
2500    }
2501
2502    #[test]
2503    fn berlekamp_massey_recovers_the_shortest_lfsr() {
2504        // Connection polynomial 1 + x + x³ → taps [1,0,1] (s[i] = s[i-1] ⊕ s[i-3]); linear complexity 3.
2505        let taps = vec![true, false, true];
2506        let seed = vec![true, false, false];
2507        let seq = lfsr_generate(&taps, &seed, 40);
2508        let (l, recovered) = berlekamp_massey_gf2(&seq);
2509        assert_eq!(l, 3, "a length-3 LFSR has linear complexity 3");
2510        // The recovered LFSR regenerates the whole sequence from its first L bits (the attack).
2511        assert_eq!(lfsr_generate(&recovered, &seq[..l], seq.len()), seq);
2512        // A well-mixed random bit sequence has linear complexity ≈ n/2 — incompressible as an LFSR.
2513        // (splitmix64's nonlinear finalizer; a plain xorshift low bit is deliberately NOT used — BM
2514        // correctly reports its ~64 linear complexity, exposing that generator's linear weakness.)
2515        let mut st = 0x1234_5678u64;
2516        let rnd: Vec<bool> = (0..200)
2517            .map(|_| {
2518                st = st.wrapping_add(0x9E37_79B9_7F4A_7C15);
2519                let mut z = st;
2520                z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
2521                z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
2522                z ^= z >> 31;
2523                z & 1 == 1
2524            })
2525            .collect();
2526        let (lr, _) = berlekamp_massey_gf2(&rnd);
2527        assert!((80..=120).contains(&lr), "high-quality random complexity ≈ n/2, got {lr}");
2528        // Bit ↔ byte conversions round-trip.
2529        let bytes = vec![0x41i64, 0x00, 0xFF, 0x7E];
2530        assert_eq!(bits_to_bytes(&bytes_to_bits(&bytes)), bytes);
2531    }
2532
2533    #[test]
2534    fn lfsr_keystream_bytes_compress_to_the_register() {
2535        // A maximal length-7 LFSR (x⁷+x³+1, bit-period 127) — its 200-byte keystream has byte-period
2536        // 127, beyond the periodic detector's n/2 reach, and looks random to every arithmetic generator.
2537        // Berlekamp–Massey recovers the 7-bit register and the whole column collapses to a handful of
2538        // bytes — the classic stream-cipher attack, here as a compressor.
2539        let taps = vec![false, false, true, false, false, false, true]; // c₃ = c₇ = 1
2540        let seed = vec![true, false, true, true, false, false, true];
2541        let bits = lfsr_generate(&taps, &seed, 200 * 8);
2542        let bytes = bits_to_bytes(&bits);
2543        roundtrips(&bytes);
2544        let enc = describe_int_seq(&bytes);
2545        assert_eq!(enc[0], T_INTS_LFSR, "the keystream ships as its LFSR register (tag), got {}", enc[0]);
2546        assert!(enc.len() < 20, "200 bytes collapse to the 7-bit register, got {} bytes", enc.len());
2547    }
2548
2549    #[test]
2550    fn berlekamp_massey_over_gf256_recovers_a_word_lfsr() {
2551        // GF(2⁸) field sanity: 3⁵¹ ≠ 1 but 3²⁵⁵ = 1 (generator), and inv is a true inverse.
2552        assert_eq!(Gf256(0x53).mul(Gf256(0xCA)), Gf256(0x53).mul(Gf256(0xCA)));
2553        assert_eq!(Gf256(0x53).mul(Gf256(0x53).inv()), Gf256::one());
2554        assert_eq!(Gf256(0xFF).mul(Gf256(0xFF).inv()), Gf256::one());
2555        // An order-3 word-LFSR over GF(256): s[i] = c₁·s[i-1] + c₂·s[i-2] + c₃·s[i-3] with field
2556        // coefficients. Berlekamp–Massey over GF(256) recovers order 3 directly — exercising the
2557        // b_disc bookkeeping the GF(2) path never touches.
2558        let taps = vec![Gf256(0x02), Gf256(0x8d), Gf256(0x1f)];
2559        let seed = vec![Gf256(0x41), Gf256(0x9c), Gf256(0x07)];
2560        let seq = lfsr_generate_field(&taps, &seed, 60);
2561        let (l, recovered) = berlekamp_massey_field(&seq);
2562        assert_eq!(l, 3, "order-3 word-LFSR has GF(256) linear complexity 3, got {l}");
2563        assert_eq!(lfsr_generate_field(&recovered, &seq[..l], seq.len()), seq, "recovered register regenerates it");
2564    }
2565
2566    #[test]
2567    fn two_adic_complexity_detects_fcsr_keystreams() {
2568        use crate::numeric::BigInt;
2569        // FCSR: connection integer q = 19 (odd), numerator p = 3. Its 2-adic expansion is an FCSR
2570        // keystream — the carry makes it nonlinear over GF(2), so no field-BM sees its structure.
2571        let bits = fcsr_generate(&BigInt::from_i64(3), &BigInt::from_i64(19), 120);
2572        let (rp, rq) = two_adic_reconstruct(&bits).expect("a clean 2-adic rational");
2573        assert_eq!(fcsr_generate(&rp, &rq, bits.len()), bits, "the recovered FCSR regenerates the keystream");
2574        let tac = two_adic_complexity(&bits);
2575        assert!(tac < 12, "a small FCSR has low 2-adic complexity, got {tac}");
2576        // The carry makes it MORE complex to a LINEAR view than to a 2-adic one — BM sees more.
2577        assert!(berlekamp_massey_gf2(&bits).0 > tac, "the FCSR is simpler 2-adically than linearly");
2578        // A well-mixed random sequence has 2-adic complexity ≈ n/2.
2579        let mut st = 0xABCD_1234u64;
2580        let rnd: Vec<bool> = (0..200)
2581            .map(|_| {
2582                st = st.wrapping_add(0x9E37_79B9_7F4A_7C15);
2583                let mut z = st;
2584                z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
2585                z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
2586                z ^= z >> 31;
2587                z & 1 == 1
2588            })
2589            .collect();
2590        assert!(two_adic_complexity(&rnd) > 60, "random ≈ n/2 2-adic complexity, got {}", two_adic_complexity(&rnd));
2591    }
2592
2593    #[test]
2594    fn fcsr_keystream_bytes_compress_to_the_rational() {
2595        use crate::numeric::BigInt;
2596        // A large connection integer q = 1000003: long period (not periodic), HIGH linear complexity
2597        // (fools the LFSR compressor), tiny 2-adic complexity. Only the FCSR generator collapses it.
2598        let bits = fcsr_generate(&BigInt::from_i64(7), &BigInt::from_i64(1_000_003), 100 * 8);
2599        let bytes = bits_to_bytes(&bits);
2600        roundtrips(&bytes);
2601        let enc = describe_int_seq(&bytes);
2602        assert_eq!(enc[0], T_INTS_FCSR, "the FCSR keystream ships as its rational p/q (tag), got {}", enc[0]);
2603        assert!(enc.len() < 20, "100 bytes collapse to a small rational, got {} bytes", enc.len());
2604    }
2605
2606    #[test]
2607    fn maximal_order_complexity_catches_nonlinear_feedback() {
2608        // A De Bruijn sequence of order L=6: generated by a NONLINEAR order-6 feedback (prefer-one),
2609        // full period 2⁶=64, every 6-window appearing exactly once. Its linear complexity is ~2⁵ and its
2610        // 2-adic complexity is high (both tools see it as complex), but its maximal order complexity is
2611        // exactly the register order 6 — the nonlinear rung sees the short register the linear rungs cannot.
2612        let order = 6;
2613        let period = 1usize << order;
2614        let mut db: Vec<bool> = vec![false; order];
2615        let mut seen: std::collections::HashSet<Vec<bool>> = std::collections::HashSet::new();
2616        seen.insert(db[..order].to_vec());
2617        while db.len() < period {
2618            let mut w: Vec<bool> = db[db.len() - (order - 1)..].to_vec();
2619            w.push(true);
2620            if seen.contains(&w) {
2621                w.pop();
2622                w.push(false);
2623            }
2624            seen.insert(w.clone());
2625            db.push(*w.last().unwrap());
2626        }
2627        let bits: Vec<bool> = [db.as_slice(), db.as_slice(), db.as_slice()].concat(); // 3 cyclic copies
2628        let moc = maximal_order_complexity(&bits);
2629        let lin = berlekamp_massey_gf2(&bits).0;
2630        assert!((5..=7).contains(&moc), "the order-6 De Bruijn register has MOC ≈ 6, got {moc}");
2631        // The SAME sequence fools the LINEAR tools — they see far higher complexity than its true order.
2632        assert!(lin > moc, "nonlinear feedback fools linear complexity (BM {lin} > MOC {moc})");
2633        assert!(two_adic_complexity(&bits) > moc, "nonlinear feedback fools 2-adic complexity too");
2634        // MOC ≤ linear complexity for EVERYTHING (it is the shorter, more general register).
2635        let mut st = 0x2468_ace0_1357_9bdfu64;
2636        let rnd: Vec<bool> = (0..300)
2637            .map(|_| {
2638                st = st.wrapping_add(0x9E37_79B9_7F4A_7C15);
2639                let mut z = st;
2640                z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
2641                z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
2642                z ^= z >> 31;
2643                z & 1 == 1
2644            })
2645            .collect();
2646        assert!(maximal_order_complexity(&rnd) <= berlekamp_massey_gf2(&rnd).0, "MOC ≤ linear complexity");
2647    }
2648
2649    #[test]
2650    fn algebraic_recurrence_recovers_low_degree_nonlinear_feedback() {
2651        // A QUADRATIC (degree-2) order-8 nonlinear feedback register:
2652        //   s[i] = s[i-1] ⊕ s[i-5] ⊕ s[i-6] ⊕ s[i-8] ⊕ (s[i-1] AND s[i-6]).
2653        // Its linear complexity is 246 (the linear rung is fooled — Berlekamp–Massey needs a 246-stage
2654        // LFSR), its 2-adic complexity is high, and maximal order complexity would diagnose "order 8"
2655        // only as a 2⁸=256-entry truth table. The ALGEBRAIC recovery returns the sparse degree-2 ANF
2656        // that regenerates it — 37 coefficients — compressing a nonlinear generator every earlier rung
2657        // can only measure.
2658        let seed: Vec<bool> = (0..8).map(|k| (0x9E37u64 >> k) & 1 == 1).collect();
2659        let mut bits = seed.clone();
2660        while bits.len() < 600 {
2661            let i = bits.len();
2662            let s = |k: usize| bits[i - k];
2663            bits.push(s(1) ^ s(5) ^ s(6) ^ s(8) ^ (s(1) & s(6)));
2664        }
2665
2666        let coeffs = detect_algebraic_recurrence(&bits, 8, 2).expect("recovers the degree-2 feedback");
2667        assert_eq!(
2668            algebraic_generate(8, 2, &coeffs, &bits[..8], bits.len()),
2669            bits,
2670            "the recovered ANF regenerates the whole nonlinear sequence"
2671        );
2672
2673        // The description is the sparse ANF: M = 1 + C(8,1) + C(8,2) = 37 monomials, versus the 2⁸ = 256
2674        // truth-table entries maximal order complexity would need for the same register.
2675        assert_eq!(coeffs.len(), 37, "degree-2 ANF over 8 vars has 37 monomials");
2676        assert!(coeffs.len() < (1usize << 8), "the ANF is far sparser than the full truth table");
2677
2678        // The quadratic term genuinely fools the LINEAR rung — Berlekamp–Massey reports complexity far
2679        // above the true register order — so nothing below this rung could have compressed it.
2680        let lin = berlekamp_massey_gf2(&bits).0;
2681        assert!(lin > 200, "the nonlinear term drives linear complexity to 246, got {lin}");
2682        assert!(two_adic_complexity(&bits) > 8, "and 2-adic complexity is high too — carry tools miss it");
2683
2684        // A degree-1 (purely affine) fit CANNOT reproduce it — the compression is genuinely nonlinear.
2685        assert!(detect_algebraic_recurrence(&bits, 8, 1).is_none(), "no affine order-8 recurrence fits");
2686
2687        // `algebraic_complexity` finds the same short register at degree 2 with its true coefficient count.
2688        let (l, m) = algebraic_complexity(&bits, 2).expect("degree-2 algebraic complexity exists");
2689        assert!(l <= 8, "the algebraic recovery finds an order-≤8 register, got {l}");
2690        assert!(m <= 37, "and describes it in at most its 37 ANF coefficients, got {m}");
2691    }
2692
2693    #[test]
2694    fn algebraic_recovery_is_a_strict_generalization_of_berlekamp_massey() {
2695        // At degree 1 the algebraic recovery IS Berlekamp–Massey: a pure GF(2) LFSR keystream must be
2696        // recovered by the degree-1 solve and regenerate identically. (The affine ANF is not unique when
2697        // the constant column is dependent on the taps over the sample, so we assert regeneration — the
2698        // real correctness witness — not a specific coefficient vector.)
2699        let taps = vec![true, false, false, true, false, true]; // an order-6 LFSR
2700        let seed = vec![true, false, true, true, false, false];
2701        let bits = lfsr_generate(&taps, &seed, 200);
2702        let coeffs = detect_algebraic_recurrence(&bits, 6, 1).expect("degree-1 recovery = Berlekamp–Massey");
2703        assert_eq!(
2704            algebraic_generate(6, 1, &coeffs, &bits[..6], bits.len()),
2705            bits,
2706            "the degree-1 ANF regenerates the linear keystream"
2707        );
2708        // The linear rung already handles this one — Berlekamp–Massey finds the same short register.
2709        assert!(berlekamp_massey_gf2(&bits).0 <= 6, "an LFSR keystream has linear complexity ≤ its order");
2710    }
2711
2712    #[test]
2713    fn correlation_attack_breaks_the_geffe_combiner_register_by_register() {
2714        // The Geffe generator: three LFSRs feed a multiplexer z[i] = x2[i] ? x1[i] : x3[i]. Its output
2715        // is a function of the HIDDEN register outputs, not of its own past — so every feedback-recovery
2716        // rung (linear, word, carry, maximal-order, algebraic) is blind to it. But Geffe leaks: z agrees
2717        // with x1 and with x3 three-quarters of the time (Pr[z=x1]=Pr[z=x3]=¾), while x2 is invisible
2718        // (Pr[z=x2]=½). The correlation attack recovers x1 and x3 ALONE — divide-and-conquer.
2719        let n = 2000;
2720        let taps1 = [false, false, true, false, false, false, true]; // L=7, period 127
2721        let taps2 = [false, false, true, false, true]; // L=5, period 31 (the protected middle)
2722        let taps3 = [false, false, false, false, true, false, false, false, true]; // L=9, period 511
2723        let seed1 = [true, false, true, true, false, false, true];
2724        let seed2 = [true, true, false, false, true];
2725        let seed3 = [true, false, false, true, false, true, true, false, true];
2726        let x1 = lfsr_generate(&taps1, &seed1, n);
2727        let x2 = lfsr_generate(&taps2, &seed2, n);
2728        let x3 = lfsr_generate(&taps3, &seed3, n);
2729        let z: Vec<bool> = (0..n).map(|i| if x2[i] { x1[i] } else { x3[i] }).collect();
2730
2731        // Attack x1's register alone (2⁷ = 128 states): the correct initial state pops out at ¾ agreement.
2732        let a1 = correlation_attack(&z, &taps1).expect("x1 register attackable");
2733        assert_eq!(a1.init_state, seed1.to_vec(), "recovers x1's exact initial state");
2734        assert!((a1.agreement - 0.75).abs() < 0.05, "x1 leaks at ~¾ agreement, got {}", a1.agreement);
2735        assert!(a1.bias > 3.0 * spurious_bias_floor(7, n), "x1's correlation clears the spurious floor");
2736        // The recovered state is a re-checkable witness — regenerate it and it IS x1.
2737        assert_eq!(lfsr_generate(&taps1, &a1.init_state, n), x1, "the witness regenerates the x1 register");
2738
2739        // Attack x3's register alone (2⁹ = 512 states) — independently of x1 and x2.
2740        let a3 = correlation_attack(&z, &taps3).expect("x3 register attackable");
2741        assert_eq!(a3.init_state, seed3.to_vec(), "recovers x3's exact initial state");
2742        assert!((a3.agreement - 0.75).abs() < 0.05, "x3 leaks at ~¾ agreement, got {}", a3.agreement);
2743        assert!(a3.bias > 3.0 * spurious_bias_floor(9, n), "x3's correlation clears the spurious floor");
2744        assert_eq!(lfsr_generate(&taps3, &a3.init_state, n), x3, "the witness regenerates the x3 register");
2745
2746        // x2 is the CORRELATION-IMMUNE middle: its best state sits at the spurious floor, no real leak.
2747        let a2 = correlation_attack(&z, &taps2).expect("x2 register scanned");
2748        assert!(a2.bias < 3.0 * spurious_bias_floor(5, n), "x2 does not measurably leak, bias {}", a2.bias);
2749        assert!(a1.bias > 3.0 * a2.bias, "the leaking registers are starkly separated from the protected one");
2750    }
2751
2752    #[test]
2753    fn walsh_spectrum_sees_the_linear_approximation_correlation_is_blind_to() {
2754        // f(x1,x2,x3,x4) = x1 ⊕ x2 ⊕ (x3 ∧ x4). It is FIRST-ORDER correlation-immune: every weight-1
2755        // Walsh coefficient vanishes, so Rung E's single-input attack sees Pr[z=xⱼ]=½ and finds nothing.
2756        // But the full Walsh spectrum reveals a WEIGHT-2 approximation z ≈ x1⊕x2 with bias ¼ — the
2757        // multi-register leak E structurally cannot express (it only reads weight-1 masks).
2758        let f: Vec<bool> = (0..16)
2759            .map(|x| {
2760                let b = |i: usize| (x >> i) & 1 == 1;
2761                b(0) ^ b(1) ^ (b(2) & b(3))
2762            })
2763            .collect();
2764        let spec = walsh_spectrum(&f).expect("well-formed truth table");
2765        for m in [1usize, 2, 4, 8] {
2766            assert_eq!(spec[m], 0, "weight-1 mask {m} vanishes — Rung E is blind here");
2767        }
2768        assert_eq!(correlation_immunity_order(&f), Some(1), "f is first-order correlation-immune");
2769        let (mask, bias) = best_linear_approximation(&f, true).expect("a best approximation exists");
2770        assert_eq!(bias, 0.25, "the exploitable approximation has bias ¼");
2771        assert!(mask.count_ones() >= 2, "and it lives at weight ≥ 2 — beyond E's single-register view");
2772        assert_eq!(nonlinearity(&f), Some(4), "nonlinearity 2³ − 8/2 = 4");
2773
2774        // THE CEILING — a bent function g = x1x2 ⊕ x3x4 has a FLAT spectrum: every |Ŵ| = 2^{n/2} = 4, so
2775        // no linear approximation beats bias 1/8. Maximal nonlinearity, the linear-incompressible residue.
2776        let g: Vec<bool> = (0..16)
2777            .map(|x| {
2778                let b = |i: usize| (x >> i) & 1 == 1;
2779                (b(0) & b(1)) ^ (b(2) & b(3))
2780            })
2781            .collect();
2782        let spec_g = walsh_spectrum(&g).expect("well-formed");
2783        assert!(spec_g.iter().all(|&c| c.unsigned_abs() == 4), "bent ⇒ perfectly flat spectrum");
2784        assert_eq!(nonlinearity(&g), Some(6), "bent nonlinearity 2³ − 2¹ = 6 (maximal for n=4)");
2785        assert_eq!(best_linear_approximation(&g, true).unwrap().1, 0.125, "no approximation beats 1/8");
2786    }
2787
2788    #[test]
2789    fn anf_is_its_own_inverse_and_reads_the_degree() {
2790        // The Möbius transform is an involution over GF(2): anf(anf(f)) == f, for every function.
2791        for n in 1..=5usize {
2792            for seed in 0..8u64 {
2793                let f: Vec<bool> = (0..1u64 << n)
2794                    .map(|i| {
2795                        let mut z = 0x9E37_79B9_7F4A_7C15u64.wrapping_mul(i.wrapping_add(seed * 131 + 1));
2796                        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
2797                        (z ^ (z >> 27)) & 1 == 1
2798                    })
2799                    .collect();
2800                let a = anf(&f).expect("well-formed");
2801                assert_eq!(anf(&a).as_deref(), Some(&f[..]), "anf∘anf = identity (n={n}, seed={seed})");
2802            }
2803        }
2804        // Known ANFs: constant (degree 0), parity (degree 1), the inner-product bent (degree 2).
2805        let n = 4;
2806        let parity: Vec<bool> = (0..1usize << n).map(|x| (x as u32).count_ones() % 2 == 1).collect();
2807        assert_eq!(algebraic_degree(&parity), Some(1), "parity is affine");
2808        let bent: Vec<bool> = (0..1usize << n)
2809            .map(|x| {
2810                let b = |i: usize| x & (1 << i) != 0;
2811                (b(0) && b(1)) ^ (b(2) && b(3))
2812            })
2813            .collect();
2814        assert_eq!(algebraic_degree(&bent), Some(2), "x0x1 ⊕ x2x3 is quadratic");
2815        assert_eq!(algebraic_degree(&vec![true; 8]), Some(0), "a constant has degree 0");
2816    }
2817
2818    #[test]
2819    fn autocorrelation_reads_the_derivative_symmetry() {
2820        let n = 4;
2821        // A linear function has EVERY direction as a linear structure: |r(a)| = 2ⁿ for all a.
2822        let parity: Vec<bool> = (0..1usize << n).map(|x| (x as u32).count_ones() % 2 == 1).collect();
2823        let rp = autocorrelation(&parity).expect("well-formed");
2824        assert_eq!(rp[0], 16, "r(0) = 2ⁿ always");
2825        assert!(rp.iter().all(|&c| c.unsigned_abs() == 16), "a linear function is flat: |r(a)| = 2ⁿ ∀a");
2826
2827        // A bent function is PERFECTLY nonlinear: r(a) = 0 for every a ≠ 0 — no linear structure at all.
2828        let bent: Vec<bool> = (0..1usize << n)
2829            .map(|x| {
2830                let b = |i: usize| x & (1 << i) != 0;
2831                (b(0) && b(1)) ^ (b(2) && b(3))
2832            })
2833            .collect();
2834        let rb = autocorrelation(&bent).expect("well-formed");
2835        assert_eq!(rb[0], 16);
2836        assert!(rb[1..].iter().all(|&c| c == 0), "bent ⇒ no nonzero linear structure");
2837
2838        // f(x) = g(x0,x1,x2) ⊕ x3 has the complement structure a = e3: flipping x3 always flips f.
2839        let f: Vec<bool> = (0..1usize << n)
2840            .map(|x| {
2841                let b = |i: usize| x & (1 << i) != 0;
2842                ((b(0) && b(1)) ^ b(2)) ^ b(3)
2843            })
2844            .collect();
2845        let rf = autocorrelation(&f).expect("well-formed");
2846        assert_eq!(rf[1 << 3], -16, "flipping x3 always flips f ⇒ r(e3) = −2ⁿ (a linear structure)");
2847    }
2848
2849    #[test]
2850    fn walsh_found_approximation_breaks_a_correlation_immune_combiner() {
2851        // Four LFSRs feed the CI(1) combiner z = a1 ⊕ a2 ⊕ (a3 ∧ a4). Because it is first-order
2852        // correlation-immune, Rung E sees each single register at ½ — invisible. The Walsh-found weight-2
2853        // approximation z ≈ a1⊕a2 is the break: the COMBINED register leaks at ¾.
2854        let n = 4000;
2855        let taps1 = [false, false, true, false, false, false, true]; // L=7
2856        let taps2 = [false, false, true, false, true]; // L=5
2857        let taps3 = [false, false, false, false, true, false, false, false, true]; // L=9
2858        let taps4 = [false, false, false, false, false, false, false, false, true, false, true]; // L=11
2859        let a1 = lfsr_generate(&taps1, &[true, false, true, true, false, false, true], n);
2860        let a2 = lfsr_generate(&taps2, &[true, true, false, false, true], n);
2861        let a3 = lfsr_generate(&taps3, &[true, false, false, true, false, true, true, false, true], n);
2862        let a4 = lfsr_generate(&taps4, &[true, false, true, false, false, true, true, false, false, true, false], n);
2863        let z: Vec<bool> = (0..n).map(|i| a1[i] ^ a2[i] ^ (a3[i] & a4[i])).collect();
2864
2865        // E's view — the keystream against each single register: flat, no leak (correlation-immune).
2866        let agree1 = z.iter().zip(&a1).filter(|(x, y)| x == y).count() as f64 / n as f64;
2867        assert!((agree1 - 0.5).abs() < 0.04, "single register a1 is invisible to first-order correlation, {agree1}");
2868
2869        // F's view — the keystream against the COMBINED register a1⊕a2 (the weight-2 mask Walsh found).
2870        let combined: Vec<bool> = (0..n).map(|i| a1[i] ^ a2[i]).collect();
2871        let agree_r = z.iter().zip(&combined).filter(|(x, y)| x == y).count() as f64 / n as f64;
2872        assert!((agree_r - 0.75).abs() < 0.04, "the weight-2 approximation leaks at ¾ — E could not see it, {agree_r}");
2873    }
2874
2875    #[test]
2876    fn fast_correlation_attack_decodes_a_noisy_lfsr_without_exhaustive_search() {
2877        // Primitive trinomial x¹⁷+x³+1: a[i] = a[i-14] ⊕ a[i-17], weight-3 parity checks — ideal for fast
2878        // correlation. Rung E would search 2¹⁷ states; the decoder recovers the state in polynomial time.
2879        let mut taps = vec![false; 17];
2880        taps[13] = true; // a[i-14]
2881        taps[16] = true; // a[i-17]
2882        let seed: Vec<bool> = (0..17).map(|k| (0xACE1u64 >> k) & 1 == 1).collect();
2883        let n = 4000;
2884        let a = lfsr_generate(&taps, &seed, n);
2885
2886        // Transmit through a binary symmetric channel: flip ~12% of the bits.
2887        let mut st = 0x1234_5678_9abc_def0u64;
2888        let z: Vec<bool> = a
2889            .iter()
2890            .map(|&bit| {
2891                st = st.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
2892                bit ^ ((st >> 40) % 100 < 12)
2893            })
2894            .collect();
2895        let agree = a.iter().zip(&z).filter(|(x, y)| x == y).count() as f64 / n as f64;
2896        assert!((agree - 0.88).abs() < 0.04, "the channel is genuinely ~12% noisy, got {agree}");
2897
2898        let state = fast_correlation_attack(&z, &taps, 400).expect("the decoder recovers the register");
2899        assert_eq!(state, seed, "fast correlation recovers the exact initial state — no 2¹⁷ search");
2900
2901        // Ceiling: pure noise (no correlation) decodes to nothing significant.
2902        let mut st2 = 0xdead_beef_0bad_c0deu64;
2903        let pure: Vec<bool> = (0..n)
2904            .map(|_| {
2905                st2 = st2.wrapping_mul(6364136223846793005).wrapping_add(1);
2906                (st2 >> 40) & 1 == 1
2907            })
2908            .collect();
2909        assert!(fast_correlation_attack(&pure, &taps, 400).is_none(), "pure noise leaks no register — the ceiling");
2910    }
2911
2912    #[test]
2913    fn shrinking_generator_falls_to_a_clock_guess_the_linear_rungs_cannot_touch() {
2914        // Clock register A (L=7) gates data register S (L=9). The output is a data-dependent decimation
2915        // of S — not a fixed function of any register's own past — so its linear complexity is enormous
2916        // and every feedback/correlation/algebraic/linear rung is blind. The attack guesses A's 2⁷ states
2917        // (divide-and-conquer on the CLOCK alone) and linear-solves S.
2918        let a_taps = [false, false, true, false, false, false, true]; // L=7
2919        let s_taps = [false, false, false, false, true, false, false, false, true]; // L=9
2920        let a_seed = [true, false, true, true, false, false, true];
2921        let s_seed = [true, true, false, true, false, true, true, false, true];
2922        let m = 300;
2923        let output = shrinking_generator(&a_taps, &a_seed, &s_taps, &s_seed, m);
2924
2925        // The linear rung is blind: the output's linear complexity is far above the data register order.
2926        assert!(
2927            berlekamp_massey_gf2(&output).0 > 40,
2928            "the shrinking generator's output has high linear complexity, got {}",
2929            berlekamp_massey_gf2(&output).0
2930        );
2931
2932        let (a_rec, s_rec) = attack_shrinking_generator(&output, &a_taps, &s_taps).expect("the generator falls");
2933        // The shrinking generator has key-equivalences (distinct register states yielding the same
2934        // keystream), so the break is keystream-equivalence, not literal state equality: the recovered
2935        // pair reproduces the output — and keeps reproducing it far past the 300 bits the attack used
2936        // (1000 bits), proving a full recovery of the generator, not a short coincidence.
2937        assert_eq!(
2938            shrinking_generator(&a_taps, &a_rec, &s_taps, &s_rec, m),
2939            output,
2940            "the recovered pair regenerates the keystream — the certified break"
2941        );
2942        assert_eq!(
2943            shrinking_generator(&a_taps, &a_rec, &s_taps, &s_rec, 1000),
2944            shrinking_generator(&a_taps, &a_seed, &s_taps, &s_seed, 1000),
2945            "and stays identical to the true generator well past the attacked length — a full break"
2946        );
2947    }
2948
2949    #[test]
2950    fn xl_solves_a_quadratic_system_by_degree_escalation() {
2951        // A quadratic system over GF(2) in x0..x3 with the unique solution [1,0,1,0]. Each equation is an
2952        // XOR of monomial bitmasks that must be 0 (bit i = variable i; mask 0 = the constant 1).
2953        let eqs: Vec<Vec<u64>> = vec![
2954            vec![0b0011],           // x0·x1 = 0
2955            vec![0b0101, 0b0000],   // x0·x2 = 1
2956            vec![0b0110],           // x1·x2 = 0
2957            vec![0b1100],           // x2·x3 = 0
2958            vec![0b0001, 0b0100],   // x0 ⊕ x2 = 0
2959            vec![0b0010, 0b1000],   // x1 ⊕ x3 = 0
2960        ];
2961        // At operating degree 1 the quadratic constraints cannot even be represented — only the two
2962        // linear relations survive, and the system is underdetermined.
2963        assert_eq!(
2964            solve_polynomial_system_gf2(&eqs, 4, 1),
2965            PolySolveResult::Undetermined,
2966            "degree 1 cannot represent the quadratic constraints"
2967        );
2968        // XL at degree 2 multiplies the equations, linking the products back to the variables, and the
2969        // unique root falls out — verified against the original nonlinear system.
2970        assert_eq!(
2971            solve_polynomial_system_gf2(&eqs, 4, 2),
2972            PolySolveResult::Solved(vec![true, false, true, false]),
2973            "XL recovers the unique root"
2974        );
2975    }
2976
2977    #[test]
2978    fn xl_refutes_an_unsatisfiable_system_linearization_misses() {
2979        // x0·x1 = 1 and x0 = 0 is contradictory (x0=0 forces x0·x1=0≠1), but a degree-1 linearization
2980        // cannot even represent x0·x1. XL multiplies x0=0 by x1 to derive x0·x1=0, colliding with
2981        // x0·x1=1 → 0=1, a certified refutation.
2982        // Over 2 variables (x0 = bit 0, x1 = bit 1), x0·x1 has mask 0b11.
2983        let eqs: Vec<Vec<u64>> = vec![
2984            vec![0b11, 0b00], // x0·x1 ⊕ 1 = 0
2985            vec![0b01],       // x0 = 0
2986        ];
2987        assert_eq!(
2988            solve_polynomial_system_gf2(&eqs, 2, 1),
2989            PolySolveResult::Undetermined,
2990            "at degree 1 the quadratic contradiction is invisible"
2991        );
2992        assert_eq!(
2993            solve_polynomial_system_gf2(&eqs, 2, 2),
2994            PolySolveResult::Refuted,
2995            "XL manufactures x0·x1 = 0, refuting the system"
2996        );
2997    }
2998
2999    #[test]
3000    fn algebraic_immunity_computes_and_verifies_known_values() {
3001        let b = |x: usize, i: usize| (x >> i) & 1 == 1;
3002
3003        // An affine function has algebraic immunity 1: g = C ⊕ 1 annihilates it (degree 1).
3004        let affine: Vec<bool> = (0..8).map(|x| b(x, 0) ^ b(x, 1) ^ b(x, 2)).collect();
3005        let (ai, w) = algebraic_immunity(&affine).expect("well-formed");
3006        assert_eq!(ai, 1, "affine functions have AI 1");
3007        assert!(verify_annihilator(&affine, &w), "the affine annihilator re-checks");
3008
3009        // Majority-of-3 has algebraic immunity 2 — no affine annihilator exists on either side.
3010        let maj3: Vec<bool> = (0..8).map(|x| (x as u32).count_ones() >= 2).collect();
3011        let (ai, w) = algebraic_immunity(&maj3).expect("well-formed");
3012        assert_eq!(ai, 2, "majority-3 has AI 2 (the maximum for n=3)");
3013        assert!(verify_annihilator(&maj3, &w), "the degree-2 annihilator re-checks");
3014
3015        // A product term x1∧x2∧x3 has AI 1 (x1 ⊕ 1 vanishes on its lone support point 111).
3016        let and3: Vec<bool> = (0..8).map(|x| b(x, 0) & b(x, 1) & b(x, 2)).collect();
3017        assert_eq!(algebraic_immunity(&and3).unwrap().0, 1, "a single product term has AI 1");
3018
3019        // Tamper cases: an all-zero witness is not an annihilator, and flipping the side breaks vanishing.
3020        let (_, w) = algebraic_immunity(&maj3).unwrap();
3021        let mut zeroed = w.clone();
3022        zeroed.coeffs = vec![false; zeroed.coeffs.len()];
3023        assert!(!verify_annihilator(&maj3, &zeroed), "the zero function is not a valid annihilator");
3024        let mut flipped = w.clone();
3025        flipped.annihilates_complement = !flipped.annihilates_complement;
3026        assert!(!verify_annihilator(&maj3, &flipped), "a nonzero g cannot vanish on both sides");
3027    }
3028
3029    #[test]
3030    fn algebraic_immunity_never_exceeds_half_n() {
3031        // AI(C) ≤ ⌈n/2⌉ for every Boolean function — a theorem; re-checked here on pseudo-random tables.
3032        let mut st = 0x00C0_FFEE_1234_5678u64;
3033        for _ in 0..24 {
3034            let truth: Vec<bool> = (0..16)
3035                .map(|_| {
3036                    st = st.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
3037                    (st >> 33) & 1 == 1
3038                })
3039                .collect();
3040            let (ai, w) = algebraic_immunity(&truth).expect("well-formed");
3041            assert!(ai <= 2, "AI ≤ ⌈4/2⌉ = 2, got {ai}");
3042            assert!(verify_annihilator(&truth, &w), "every witness re-checks");
3043        }
3044    }
3045
3046    #[test]
3047    fn algebraic_filter_attack_recovers_the_filter_generator_state() {
3048        // A filter generator: a length-10 maximal LFSR (x¹⁰+x³+1, period 1023) filtered by majority-of-3
3049        // over three consecutive state bits. Correlation/Walsh see only statistical leaks; the algebraic
3050        // attack is exact — maj3 has AI 2, so each keystream bit is a degree-2 equation in the initial
3051        // state, and linearizing over the 56 monomials recovers all 10 secret bits at once.
3052        let taps = [false, false, false, false, false, false, true, false, false, true]; // s[i]=s[i-7]⊕s[i-10]
3053        let s0 = [true, false, true, true, false, false, true, false, true, true];
3054        let filter_truth: Vec<bool> = (0..8).map(|x| (x as u32).count_ones() >= 2).collect(); // maj3
3055        let m = 3;
3056        let n = 400;
3057        let seq = lfsr_generate(&taps, &s0, n + m);
3058        let keystream: Vec<bool> = (0..n)
3059            .map(|t| {
3060                let idx = (0..m).fold(0usize, |a, i| a | (usize::from(seq[t + i]) << i));
3061                filter_truth[idx]
3062            })
3063            .collect();
3064
3065        let recovered = algebraic_filter_attack(&keystream, &taps, &filter_truth)
3066            .expect("the algebraic attack recovers the state");
3067        assert_eq!(recovered, s0.to_vec(), "the recovered initial state IS the secret key");
3068    }
3069
3070    #[test]
3071    fn periodic_round_trips() {
3072        let block = [4i64, 1, 1, 5, 9, 2, 6];
3073        let v: Vec<i64> = (0..300).map(|i| block[i % block.len()]).collect();
3074        roundtrips(&v);
3075    }
3076
3077    #[test]
3078    fn modular_affine_round_trips() {
3079        let v: Vec<i64> = (0..200).map(|i| 100 + 3 * ((i % 8) as i64)).collect();
3080        roundtrips(&v);
3081    }
3082
3083    #[test]
3084    fn sparse_round_trips() {
3085        let mut v = vec![42i64; 500];
3086        v[17] = -1;
3087        v[300] = 999;
3088        v[499] = 7;
3089        roundtrips(&v);
3090    }
3091
3092    #[test]
3093    fn delta_and_dod_shapes_round_trip() {
3094        let monotone: Vec<i64> = (0..200).map(|i| i * i / 7 + i).collect();
3095        roundtrips(&monotone);
3096        let jittered: Vec<i64> = (0..200).map(|i| 1_000_000 + 60 * i + (i % 3)).collect();
3097        roundtrips(&jittered);
3098    }
3099
3100    #[test]
3101    fn runs_and_low_cardinality_round_trip() {
3102        let mut runs = vec![5i64; 100];
3103        runs.extend(std::iter::repeat(9).take(80));
3104        runs.extend(std::iter::repeat(-2).take(120));
3105        roundtrips(&runs);
3106        let categorical: Vec<i64> = (0..600).map(|i| [10, 20, 30][i % 3]).collect();
3107        roundtrips(&categorical);
3108    }
3109
3110    #[test]
3111    fn byte_column_round_trips() {
3112        let v: Vec<i64> = (0..500).map(|i| ((i * 37) % 256) as i64).collect();
3113        roundtrips(&v);
3114    }
3115
3116    #[test]
3117    fn edge_cases_round_trip() {
3118        roundtrips(&[]);
3119        roundtrips(&[42]);
3120        roundtrips(&[-1]);
3121        roundtrips(&[i64::MIN, i64::MAX, 0, -1, 1]);
3122        roundtrips(&[7, 7, 7, 7]);
3123    }
3124
3125    #[test]
3126    fn pseudorandom_round_trips_and_is_never_larger_than_varint() {
3127        // A pseeded pseudo-random column has no generator structure; the varint baseline must win
3128        // and the round-trip must still be exact.
3129        let mut state = 0x1234_5678_9abc_def0u64;
3130        let v: Vec<i64> = (0..400)
3131            .map(|_| {
3132                state ^= state << 13;
3133                state ^= state >> 7;
3134                state ^= state << 17;
3135                (state >> 1) as i64
3136            })
3137            .collect();
3138        roundtrips(&v);
3139        let mut baseline = vec![T_INTS];
3140        leb128_encode(&mut baseline, v.iter().copied(), v.len());
3141        assert!(describe_int_seq(&v).len() <= baseline.len(), "never worse than the varint baseline");
3142    }
3143}