1use logicaffeine_base::describe;
22
23use crate::cdcl::{Lit, Var};
24use crate::proof::Perm;
25use std::collections::BTreeSet;
26
27#[derive(Clone, Debug, PartialEq, Eq)]
30pub enum Descriptor {
31 IntSeq { encoded: Vec<u8> },
34 BooleanFunction { num_vars: usize, tree: StructureTree },
37}
38
39#[derive(Clone, Debug, PartialEq, Eq)]
46pub struct DescriptionBound {
47 pub object_hash: u64,
49 pub bytes: usize,
51 pub descriptor: Descriptor,
53}
54
55impl DescriptionBound {
56 pub fn of_int_seq(v: &[i64]) -> DescriptionBound {
59 let encoded = describe::describe_int_seq(v);
60 DescriptionBound { object_hash: hash_ints(v), bytes: encoded.len(), descriptor: Descriptor::IntSeq { encoded } }
61 }
62
63 pub fn of_boolean(truth: &[bool]) -> Option<DescriptionBound> {
67 let num_vars = truth.len().trailing_zeros() as usize;
68 let tree = structure_tree(truth)?;
69 let bytes = tree.total_description_bits().div_ceil(8).max(1);
70 Some(DescriptionBound {
71 object_hash: hash_bools(truth),
72 bytes,
73 descriptor: Descriptor::BooleanFunction { num_vars, tree },
74 })
75 }
76
77 pub fn verify(&self) -> bool {
81 match &self.descriptor {
82 Descriptor::IntSeq { encoded } => {
83 encoded.len() == self.bytes
84 && describe::decode_int_seq(encoded).map_or(false, |v| hash_ints(&v) == self.object_hash)
85 }
86 Descriptor::BooleanFunction { num_vars, tree } => {
87 tree.total_description_bits().div_ceil(8).max(1) == self.bytes
88 && tree.reconstruct().map_or(false, |t| {
89 t.len() == (1usize << num_vars) && hash_bools(&t) == self.object_hash
90 })
91 }
92 }
93 }
94}
95
96fn hash_bools(v: &[bool]) -> u64 {
98 const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
99 const PRIME: u64 = 0x0000_0100_0000_01b3;
100 let mut h = OFFSET;
101 let mut mix = |b: u8| {
102 h ^= b as u64;
103 h = h.wrapping_mul(PRIME);
104 };
105 for b in (v.len() as u64).to_le_bytes() {
106 mix(b);
107 }
108 for &x in v {
109 mix(x as u8);
110 }
111 h
112}
113
114fn hash_ints(v: &[i64]) -> u64 {
117 const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
118 const PRIME: u64 = 0x0000_0100_0000_01b3;
119 let mut h = OFFSET;
120 let mut mix = |bytes: [u8; 8]| {
121 for b in bytes {
122 h ^= b as u64;
123 h = h.wrapping_mul(PRIME);
124 }
125 };
126 mix((v.len() as u64).to_le_bytes());
127 for &x in v {
128 mix(x.to_le_bytes());
129 }
130 h
131}
132
133pub const GROUP_DECODER_OVERHEAD: usize = 8;
144
145#[derive(Clone, Debug)]
151pub struct StructuralBound {
152 pub num_vars: usize,
154 pub whole: DescriptionBound,
156 pub rep: DescriptionBound,
158 pub gens: DescriptionBound,
160 pub group_entropy_bits: f64,
162}
163
164impl StructuralBound {
165 pub fn group_bytes(&self) -> usize {
167 self.rep.bytes + self.gens.bytes + GROUP_DECODER_OVERHEAD
168 }
169
170 pub fn is_compression(&self) -> bool {
173 self.group_bytes() < self.whole.bytes
174 }
175
176 pub fn best_bytes(&self) -> usize {
178 self.whole.bytes.min(self.group_bytes())
179 }
180
181 pub fn verify(&self) -> bool {
186 if !(self.whole.verify() && self.rep.verify() && self.gens.verify()) {
188 return false;
189 }
190 let (nv_f, f_clauses) = match decode_cnf(&self.whole.descriptor) {
192 Some(x) => x,
193 None => return false,
194 };
195 let (_, rep_clauses) = match decode_cnf(&self.rep.descriptor) {
196 Some(x) => x,
197 None => return false,
198 };
199 let (nv_g, gens) = match decode_gens(&self.gens.descriptor) {
200 Some(x) => x,
201 None => return false,
202 };
203 if nv_f != self.num_vars || nv_g != self.num_vars {
204 return false;
205 }
206 if !gens.iter().all(|p| crate::symmetry_detect::perm_is_automorphism(&f_clauses, p)) {
208 return false;
209 }
210 reconstructs(&f_clauses, &rep_clauses, &gens)
212 }
213}
214
215fn decode_cnf(d: &Descriptor) -> Option<(usize, Vec<Vec<Lit>>)> {
217 let Descriptor::IntSeq { encoded } = d else { return None };
218 unflatten_cnf(&describe::decode_int_seq(encoded)?)
219}
220
221fn decode_gens(d: &Descriptor) -> Option<(usize, Vec<Perm>)> {
223 let Descriptor::IntSeq { encoded } = d else { return None };
224 unflatten_gens(&describe::decode_int_seq(encoded)?)
225}
226
227pub fn structural_bound(num_vars: usize, clauses: &[Vec<Lit>], generators: &[Perm]) -> Option<StructuralBound> {
231 if !generators.iter().all(|g| crate::symmetry_detect::perm_is_automorphism(clauses, g)) {
232 return None;
233 }
234 let orbits = crate::hypercube::clause_orbits(clauses, generators);
235 let rep_clauses: Vec<Vec<Lit>> = orbits.iter().filter_map(|o| o.first().map(|&i| clauses[i].clone())).collect();
236 if !reconstructs(clauses, &rep_clauses, generators) {
238 return None;
239 }
240 Some(StructuralBound {
241 num_vars,
242 whole: DescriptionBound::of_int_seq(&flatten_cnf(num_vars, clauses)),
243 rep: DescriptionBound::of_int_seq(&flatten_cnf(num_vars, &rep_clauses)),
244 gens: DescriptionBound::of_int_seq(&flatten_gens(num_vars, generators)),
245 group_entropy_bits: crate::hypercube::symmetry_entropy_bits(num_vars, clauses),
246 })
247}
248
249fn reconstructs(f_clauses: &[Vec<Lit>], rep_clauses: &[Vec<Lit>], gens: &[Perm]) -> bool {
255 let f_keys = clause_set(f_clauses);
256 let rep_keys = clause_set(rep_clauses);
257 if !rep_keys.is_subset(&f_keys) {
258 return false; }
260 let orbits = crate::hypercube::clause_orbits(f_clauses, gens);
261 orbits.iter().all(|orbit| {
262 orbit.iter().any(|&i| rep_keys.contains(&crate::symmetry_detect::clause_key(&f_clauses[i])))
263 })
264}
265
266fn clause_set(clauses: &[Vec<Lit>]) -> BTreeSet<Vec<u32>> {
269 clauses.iter().map(|c| crate::symmetry_detect::clause_key(c)).collect()
270}
271
272fn lit_code(l: Lit) -> i64 {
274 (l.var() as i64) * 2 + if l.is_positive() { 0 } else { 1 }
275}
276
277fn lit_from_code(code: i64) -> Option<Lit> {
279 if code < 0 {
280 return None;
281 }
282 Some(Lit::new((code / 2) as Var, code % 2 == 0))
283}
284
285fn flatten_cnf(num_vars: usize, clauses: &[Vec<Lit>]) -> Vec<i64> {
287 let mut out = vec![num_vars as i64, clauses.len() as i64];
288 for c in clauses {
289 out.push(c.len() as i64);
290 for &l in c {
291 out.push(lit_code(l));
292 }
293 }
294 out
295}
296
297fn unflatten_cnf(flat: &[i64]) -> Option<(usize, Vec<Vec<Lit>>)> {
299 let mut it = flat.iter().copied();
300 let num_vars = usize::try_from(it.next()?).ok()?;
301 let num_clauses = usize::try_from(it.next()?).ok()?;
302 let mut clauses = Vec::with_capacity(num_clauses.min(4096));
303 for _ in 0..num_clauses {
304 let len = usize::try_from(it.next()?).ok()?;
305 let mut c = Vec::with_capacity(len.min(4096));
306 for _ in 0..len {
307 c.push(lit_from_code(it.next()?)?);
308 }
309 clauses.push(c);
310 }
311 if it.next().is_some() {
312 return None; }
314 Some((num_vars, clauses))
315}
316
317fn flatten_gens(num_vars: usize, gens: &[Perm]) -> Vec<i64> {
319 let mut out = vec![gens.len() as i64, num_vars as i64];
320 for g in gens {
321 for v in 0..num_vars {
322 out.push(lit_code(g.apply(Lit::pos(v as Var))));
323 }
324 }
325 out
326}
327
328fn unflatten_gens(flat: &[i64]) -> Option<(usize, Vec<Perm>)> {
330 let mut it = flat.iter().copied();
331 let num_gens = usize::try_from(it.next()?).ok()?;
332 let num_vars = usize::try_from(it.next()?).ok()?;
333 let mut gens = Vec::with_capacity(num_gens.min(4096));
334 for _ in 0..num_gens {
335 let mut images = Vec::with_capacity(num_vars.min(4096));
336 for _ in 0..num_vars {
337 images.push(lit_from_code(it.next()?)?);
338 }
339 gens.push(Perm::from_images(images));
340 }
341 if it.next().is_some() {
342 return None;
343 }
344 Some((num_vars, gens))
345}
346
347const RIGIDITY_MAX_VARS: usize = 64;
360
361#[derive(Clone, Copy, Debug)]
368pub struct Budget {
369 pub max_gaussian_dim: usize,
371}
372
373impl Budget {
374 pub fn standard() -> Budget {
376 Budget { max_gaussian_dim: RIGIDITY_MAX_VARS }
377 }
378}
379
380#[derive(Clone, Debug, PartialEq, Eq)]
384pub enum Refusal {
385 OverBudgetGaussian { dim: usize, cap: usize },
387 NoLinearStructure,
389}
390
391#[derive(Clone, Debug, PartialEq, Eq)]
395pub struct LinearRigidityCert {
396 pub num_vars: usize,
398 pub rows: Vec<u64>,
401 pub rank: usize,
403 pub kernel_basis: Vec<Vec<bool>>,
405 pub solution_count_log2: u32,
407}
408
409pub fn certify_linear_rigidity(num_vars: usize, clauses: &[Vec<Lit>]) -> Option<LinearRigidityCert> {
413 certify_linear_rigidity_within(num_vars, clauses, &Budget::standard()).ok()
414}
415
416pub fn certify_linear_rigidity_within(
420 num_vars: usize,
421 clauses: &[Vec<Lit>],
422 budget: &Budget,
423) -> Result<LinearRigidityCert, Refusal> {
424 if num_vars > budget.max_gaussian_dim {
425 return Err(Refusal::OverBudgetGaussian { dim: num_vars, cap: budget.max_gaussian_dim });
426 }
427 let eqs = crate::lyapunov::extract_xor(num_vars, clauses);
428 if eqs.is_empty() {
429 return Err(Refusal::NoLinearStructure);
430 }
431 let rows = eqs_to_rows(&eqs, num_vars);
432 let sol = crate::gf2::solve_gf2(num_vars, &rows, &vec![false; rows.len()]).ok_or(Refusal::NoLinearStructure)?;
433 let rank = num_vars - sol.kernel_basis.len();
434 Ok(LinearRigidityCert {
435 num_vars,
436 rows,
437 rank,
438 solution_count_log2: sol.kernel_basis.len() as u32,
439 kernel_basis: sol.kernel_basis,
440 })
441}
442
443pub fn check_linear_rigidity(cert: &LinearRigidityCert, clauses: &[Vec<Lit>]) -> bool {
447 if cert.num_vars > RIGIDITY_MAX_VARS {
448 return false;
449 }
450 let eqs = crate::lyapunov::extract_xor(cert.num_vars, clauses);
452 if eqs.is_empty() {
453 return false;
454 }
455 let rows = eqs_to_rows(&eqs, cert.num_vars);
456 let recovered: BTreeSet<u64> = rows.iter().copied().collect();
458 let claimed: BTreeSet<u64> = cert.rows.iter().copied().collect();
459 if recovered != claimed {
460 return false;
461 }
462 let sol = match crate::gf2::solve_gf2(cert.num_vars, &rows, &vec![false; rows.len()]) {
464 Some(s) => s,
465 None => return false,
466 };
467 let true_kernel_dim = sol.kernel_basis.len();
468 if cert.rank != cert.num_vars - true_kernel_dim
469 || cert.solution_count_log2 as usize != true_kernel_dim
470 || cert.kernel_basis.len() != true_kernel_dim
471 {
472 return false;
473 }
474 for v in &cert.kernel_basis {
476 if v.len() != cert.num_vars || rows.iter().any(|&row| gf2_dot(row, v)) {
477 return false;
478 }
479 }
480 independent_gf2(&cert.kernel_basis)
482}
483
484#[derive(Clone, Debug, PartialEq, Eq)]
490pub struct LinearStructureCert {
491 pub num_vars: usize,
493 pub num_xor_eqs: usize,
495 pub rank: usize,
497 pub kernel_dim: usize,
499}
500
501pub fn certify_linear_structure(num_vars: usize, clauses: &[Vec<Lit>]) -> Option<LinearStructureCert> {
504 let eqs = crate::lyapunov::extract_xor(num_vars, clauses);
505 if eqs.is_empty() {
506 return None;
507 }
508 let inc = crate::xor_engine::IncXor::new(num_vars, &eqs);
509 Some(LinearStructureCert {
510 num_vars,
511 num_xor_eqs: eqs.len(),
512 rank: inc.rank(),
513 kernel_dim: inc.kernel_dim(),
514 })
515}
516
517pub fn check_linear_structure(cert: &LinearStructureCert, clauses: &[Vec<Lit>]) -> bool {
520 let eqs = crate::lyapunov::extract_xor(cert.num_vars, clauses);
521 if eqs.len() != cert.num_xor_eqs {
522 return false;
523 }
524 let inc = crate::xor_engine::IncXor::new(cert.num_vars, &eqs);
525 inc.rank() == cert.rank && inc.kernel_dim() == cert.kernel_dim
526}
527
528#[derive(Clone, Debug)]
533pub enum LinearShortcut {
534 None { rigidity: Option<LinearRigidityCert>, structure: LinearStructureCert },
539 NoLinearStructure,
542}
543
544pub fn linear_shortcut_verdict(num_vars: usize, clauses: &[Vec<Lit>]) -> LinearShortcut {
546 match certify_linear_structure(num_vars, clauses) {
547 None => LinearShortcut::NoLinearStructure,
548 Some(structure) => {
549 let rigidity = certify_linear_rigidity(num_vars, clauses);
550 LinearShortcut::None { rigidity, structure }
551 }
552 }
553}
554
555const GATE_SYMMETRY_MAX_VARS: usize = 48;
558
559pub fn incompressibility_gate(num_vars: usize, clauses: &[Vec<Lit>]) -> Option<LinearStructureCert> {
566 let structure = certify_linear_structure(num_vars, clauses)?;
567 if num_vars > GATE_SYMMETRY_MAX_VARS {
568 return None; }
570 if crate::hypercube::symmetry_entropy_bits(num_vars, clauses) == 0.0 {
571 Some(structure) } else {
573 None
574 }
575}
576
577fn eqs_to_rows(eqs: &[crate::xorsat::XorEquation], num_vars: usize) -> Vec<u64> {
579 eqs.iter()
580 .map(|e| e.vars.iter().filter(|&&v| v < num_vars.min(64)).fold(0u64, |m, &v| m | (1u64 << v)))
581 .collect()
582}
583
584fn gf2_dot(row: u64, v: &[bool]) -> bool {
587 let mut acc = false;
588 let mut r = row;
589 while r != 0 {
590 let i = r.trailing_zeros() as usize;
591 if i < v.len() {
592 acc ^= v[i];
593 }
594 r &= r - 1;
595 }
596 acc
597}
598
599pub fn incompressible_string_exists(n: u32) -> Option<crate::pigeonhole::CountingCert> {
612 if n == 0 || n > 127 {
613 return None; }
615 let strings: u128 = 1u128 << n;
616 let shorter_programs: u128 = strings - 1; crate::pigeonhole::certify_pigeonhole_unsat(strings, shorter_programs)
618}
619
620pub fn certified_incompressible_function_exists(n: u32) -> Option<crate::pigeonhole::CountingCert> {
628 if n == 0 || n > 6 {
629 return None;
630 }
631 incompressible_string_exists(1u32 << n)
632}
633
634#[derive(Clone, Debug)]
646pub enum CryptoStrength {
647 Weak { witness: DescriptionBound, ratio: f64 },
650 IncompressibleInClass { ratio: f64 },
654}
655
656pub fn incompressibility_ratio(data: &[u8]) -> f64 {
661 if data.is_empty() {
662 return 1.0;
663 }
664 let ints: Vec<i64> = data.iter().map(|&b| b as i64).collect();
665 describe::describe_int_seq(&ints).len() as f64 / data.len() as f64
666}
667
668pub fn gf256_word_complexity(data: &[u8]) -> usize {
673 let elems: Vec<describe::Gf256> = data.iter().map(|&b| describe::Gf256(b)).collect();
674 describe::berlekamp_massey_field(&elems).0
675}
676
677pub fn two_adic_complexity_of_bytes(data: &[u8]) -> usize {
682 let bits: Vec<bool> = data.iter().flat_map(|&b| (0..8).map(move |j| (b >> j) & 1 == 1)).collect();
683 describe::two_adic_complexity(&bits)
684}
685
686pub fn maximal_order_complexity_of_bytes(data: &[u8]) -> usize {
694 let bits: Vec<bool> = data.iter().flat_map(|&b| (0..8).map(move |j| (b >> j) & 1 == 1)).collect();
695 describe::maximal_order_complexity(&bits)
696}
697
698#[derive(Clone, Debug, PartialEq, Eq)]
704pub struct AlgebraicAttack {
705 pub order: usize,
707 pub degree: usize,
709 pub anf_terms: usize,
711 pub anf: Vec<bool>,
713 pub truth_table: usize,
715}
716
717pub fn algebraic_attack_on_bytes(data: &[u8], max_degree: usize, max_order: usize) -> Option<AlgebraicAttack> {
723 let bits: Vec<bool> = data.iter().flat_map(|&b| (0..8).map(move |j| (b >> j) & 1 == 1)).collect();
724 let cap = max_order.min(bits.len() / 2);
725 for l in 1..=cap {
726 if let Some(anf) = describe::detect_algebraic_recurrence(&bits, l, max_degree) {
727 return Some(AlgebraicAttack {
728 order: l,
729 degree: max_degree,
730 anf_terms: anf.iter().filter(|&&c| c).count().max(1),
731 anf,
732 truth_table: 1usize.checked_shl(l as u32).unwrap_or(usize::MAX),
733 });
734 }
735 }
736 None
737}
738
739#[derive(Clone, Debug, PartialEq)]
742pub struct CombinerLeak {
743 pub candidate_index: usize,
745 pub attack: describe::CorrelationAttack,
747 pub floor: f64,
749 pub margin: f64,
751}
752
753pub fn scan_for_combiner_leaks(keystream: &[u8], candidates: &[Vec<bool>], significance: f64) -> Vec<CombinerLeak> {
762 let bits: Vec<bool> = keystream.iter().flat_map(|&b| (0..8).map(move |j| (b >> j) & 1 == 1)).collect();
763 let mut leaks = Vec::new();
764 for (i, taps) in candidates.iter().enumerate() {
765 let Some(attack) = describe::correlation_attack(&bits, taps) else {
766 continue;
767 };
768 let floor = describe::spurious_bias_floor(taps.len(), attack.samples);
769 if floor > 0.0 && attack.bias > significance * floor {
770 leaks.push(CombinerLeak { candidate_index: i, margin: attack.bias / floor, floor, attack });
771 }
772 }
773 leaks
774}
775
776pub fn fast_correlation_attack(keystream: &[bool], taps: &[bool], max_iters: usize) -> Option<Vec<bool>> {
781 describe::fast_correlation_attack(keystream, taps, max_iters)
782}
783
784pub fn attack_shrinking_generator(output: &[bool], a_taps: &[bool], s_taps: &[bool]) -> Option<(Vec<bool>, Vec<bool>)> {
789 describe::attack_shrinking_generator(output, a_taps, s_taps)
790}
791
792#[derive(Clone, Debug, PartialEq, Eq)]
804pub struct LensCoverage {
805 pub lens: &'static str,
806 pub description_bits: usize,
807}
808
809#[derive(Clone, Debug)]
811pub struct LensReport {
812 pub length_bits: usize,
813 pub coverage: Vec<LensCoverage>,
815 pub best_lens: &'static str,
817 pub best_description_bits: usize,
818 pub covered: bool,
821}
822
823pub fn lens_report(bits: &[bool]) -> LensReport {
829 let n = bits.len();
830 let mut coverage = Vec::new();
831
832 let lc = describe::berlekamp_massey_gf2(bits).0;
834 coverage.push(LensCoverage { lens: "linear (LFSR / Berlekamp–Massey)", description_bits: lc.saturating_mul(2) });
835
836 let tc = describe::two_adic_complexity(bits);
838 coverage.push(LensCoverage { lens: "2-adic (FCSR)", description_bits: tc.saturating_mul(2) });
839
840 let moc = describe::maximal_order_complexity(bits);
843 let moc_bits = if moc < 20 { (1usize << moc).saturating_add(moc) } else { usize::MAX };
844 coverage.push(LensCoverage { lens: "maximal-order (nonlinear FSR)", description_bits: moc_bits });
845
846 let alg = (1..=12)
848 .find_map(|l| describe::detect_algebraic_recurrence(bits, l, 2).map(|c| l + c.iter().filter(|&&b| b).count()));
849 coverage.push(LensCoverage { lens: "algebraic-recurrence (deg-2 ANF)", description_bits: alg.unwrap_or(usize::MAX) });
850
851 let bytes = describe::bits_to_bytes(bits);
853 let enc = describe::describe_int_seq(&bytes).len().saturating_mul(8);
854 coverage.push(LensCoverage { lens: "MDL codec menu", description_bits: enc });
855
856 let best = coverage.iter().min_by_key(|c| c.description_bits).expect("nonempty");
857 let covered = best.description_bits.saturating_mul(2) < n;
858 LensReport {
859 length_bits: n,
860 best_lens: best.lens,
861 best_description_bits: best.description_bits,
862 covered,
863 coverage,
864 }
865}
866
867#[derive(Clone, Debug)]
870pub struct CoverageCensus {
871 pub total: usize,
872 pub covered: usize,
874 pub uncovered: usize,
876 pub by_lens: Vec<(&'static str, usize)>,
878}
879
880pub fn census(corpus: &[Vec<bool>]) -> CoverageCensus {
883 let mut counts: std::collections::BTreeMap<&'static str, usize> = std::collections::BTreeMap::new();
884 let mut covered = 0;
885 for seq in corpus {
886 let r = lens_report(seq);
887 if r.covered {
888 covered += 1;
889 *counts.entry(r.best_lens).or_insert(0) += 1;
890 }
891 }
892 CoverageCensus {
893 total: corpus.len(),
894 covered,
895 uncovered: corpus.len() - covered,
896 by_lens: counts.into_iter().collect(),
897 }
898}
899
900pub fn exhaustive_coverage(len: usize) -> CoverageCensus {
907 let corpus: Vec<Vec<bool>> =
908 (0u64..(1u64 << len)).map(|code| (0..len).map(|i| (code >> i) & 1 == 1).collect()).collect();
909 census(&corpus)
910}
911
912#[derive(Clone, Debug)]
914pub struct RecursiveReduction {
915 pub sizes: Vec<usize>,
917 pub depth: usize,
919 pub irreducible_bytes: usize,
921 pub compressed: bool,
923}
924
925pub fn recursive_reduce(bytes: &[u8]) -> RecursiveReduction {
936 let mut cur: Vec<i64> = bytes.iter().map(|&b| b as i64).collect();
937 let mut sizes = vec![cur.len()];
938 loop {
939 let enc = describe::describe_int_seq(&cur);
940 if enc.len() >= cur.len() {
941 break; }
943 cur = enc.iter().map(|&b| b as i64).collect();
944 sizes.push(cur.len());
945 }
946 RecursiveReduction {
947 depth: sizes.len() - 1,
948 irreducible_bytes: *sizes.last().expect("nonempty"),
949 compressed: sizes.len() > 1,
950 sizes,
951 }
952}
953
954#[derive(Clone, Debug, PartialEq)]
957pub struct LinearDistinguisher {
958 pub mask: usize,
960 pub bias: f64,
962 pub mask_weight: u32,
964 pub nonlinearity: u64,
966 pub immunity_order: usize,
968}
969
970pub fn linear_cryptanalysis(truth: &[bool]) -> Option<LinearDistinguisher> {
976 let (mask, bias) = describe::best_linear_approximation(truth, true)?;
977 Some(LinearDistinguisher {
978 mask,
979 bias,
980 mask_weight: mask.count_ones(),
981 nonlinearity: describe::nonlinearity(truth)?,
982 immunity_order: describe::correlation_immunity_order(truth)?,
983 })
984}
985
986#[derive(Clone, Debug, PartialEq, Eq)]
989pub struct AlgebraicImmunityReport {
990 pub immunity: usize,
992 pub max_possible: usize,
994 pub witness: describe::AnnihilatorWitness,
996 pub is_maximal: bool,
998}
999
1000pub fn algebraic_immunity_of(truth: &[bool]) -> Option<AlgebraicImmunityReport> {
1006 let (immunity, witness) = describe::algebraic_immunity(truth)?;
1007 let n = truth.len().trailing_zeros() as usize;
1008 let max_possible = n.div_ceil(2);
1009 Some(AlgebraicImmunityReport { immunity, max_possible, witness, is_maximal: immunity == max_possible })
1010}
1011
1012pub fn algebraic_filter_attack(keystream: &[bool], taps: &[bool], filter_truth: &[bool]) -> Option<Vec<bool>> {
1017 describe::algebraic_filter_attack(keystream, taps, filter_truth)
1018}
1019
1020#[derive(Clone, Debug, PartialEq, Eq)]
1043pub enum CubeStructure {
1044 Constant(bool),
1046 Junta { relevant: Vec<usize>, subtable: Vec<bool> },
1049 Affine { coeffs: Vec<bool>, constant: bool },
1051 Symmetric { by_weight: Vec<bool> },
1054 LowDegree { degree: usize, anf: Vec<bool> },
1056 ResistedArsenal { nonlinearity: u64, degree: usize },
1059}
1060
1061impl CubeStructure {
1062 pub fn reconstruct(&self, n: usize) -> Option<Vec<bool>> {
1065 let size = 1usize << n;
1066 match self {
1067 CubeStructure::Constant(b) => Some(vec![*b; size]),
1068 CubeStructure::Junta { relevant, subtable } => {
1069 if subtable.len() != 1usize << relevant.len() {
1070 return None;
1071 }
1072 Some(
1073 (0..size)
1074 .map(|x| {
1075 let c = relevant.iter().enumerate().fold(0usize, |acc, (bit, &v)| {
1076 if x & (1 << v) != 0 { acc | (1 << bit) } else { acc }
1077 });
1078 subtable[c]
1079 })
1080 .collect(),
1081 )
1082 }
1083 CubeStructure::Affine { coeffs, constant } => {
1084 if coeffs.len() != n {
1085 return None;
1086 }
1087 Some(
1088 (0..size)
1089 .map(|x| {
1090 coeffs.iter().enumerate().fold(*constant, |acc, (i, &a)| acc ^ (a && (x & (1 << i) != 0)))
1091 })
1092 .collect(),
1093 )
1094 }
1095 CubeStructure::Symmetric { by_weight } => {
1096 if by_weight.len() != n + 1 {
1097 return None;
1098 }
1099 Some((0..size).map(|x| by_weight[(x as u64).count_ones() as usize]).collect())
1100 }
1101 CubeStructure::LowDegree { anf, .. } => {
1103 if anf.len() != size {
1104 return None;
1105 }
1106 describe::anf(anf)
1107 }
1108 CubeStructure::ResistedArsenal { .. } => None,
1109 }
1110 }
1111}
1112
1113#[derive(Clone, Debug)]
1116pub struct StructureReport {
1117 pub num_vars: usize,
1118 pub class: CubeStructure,
1119 pub raw_bits: usize,
1121 pub description_bits: usize,
1123 pub compressed: bool,
1125}
1126
1127fn var_index_bits(n: usize) -> usize {
1129 if n <= 1 { 1 } else { (usize::BITS - (n - 1).leading_zeros()) as usize }
1130}
1131
1132fn partial_binomial_sum(n: usize, d: usize) -> usize {
1135 let mut sum = 0usize;
1136 let mut c = 1usize;
1137 for k in 0..=d.min(n) {
1138 sum = sum.saturating_add(c);
1139 c = c.saturating_mul(n - k) / (k + 1);
1140 }
1141 sum
1142}
1143
1144fn symmetric_profile(truth: &[bool], n: usize) -> Option<Vec<bool>> {
1146 let mut by_weight: Vec<Option<bool>> = vec![None; n + 1];
1147 for (x, &val) in truth.iter().enumerate() {
1148 let w = (x as u64).count_ones() as usize;
1149 match by_weight[w] {
1150 None => by_weight[w] = Some(val),
1151 Some(v) if v != val => return None,
1152 _ => {}
1153 }
1154 }
1155 Some(by_weight.into_iter().map(|o| o.unwrap_or(false)).collect())
1156}
1157
1158pub fn find_structure(truth: &[bool]) -> Option<StructureReport> {
1165 if truth.is_empty() || !truth.len().is_power_of_two() {
1166 return None;
1167 }
1168 let n = truth.len().trailing_zeros() as usize;
1169 let raw = truth.len();
1170 let mut cands: Vec<(CubeStructure, usize)> = Vec::new();
1171
1172 if truth.iter().all(|&b| b == truth[0]) {
1174 cands.push((CubeStructure::Constant(truth[0]), 1));
1175 }
1176
1177 let relevant: Vec<usize> =
1179 (0..n).filter(|&i| (0..truth.len()).any(|x| truth[x] != truth[x ^ (1 << i)])).collect();
1180 if !relevant.is_empty() && relevant.len() < n {
1181 let k = relevant.len();
1182 let subtable: Vec<bool> = (0..1usize << k)
1183 .map(|c| {
1184 let x = relevant.iter().enumerate().fold(0usize, |acc, (bit, &v)| {
1185 if c & (1 << bit) != 0 { acc | (1 << v) } else { acc }
1186 });
1187 truth[x]
1188 })
1189 .collect();
1190 let bits = k * var_index_bits(n) + (1usize << k);
1191 cands.push((CubeStructure::Junta { relevant, subtable }, bits));
1192 }
1193
1194 if describe::nonlinearity(truth) == Some(0) {
1196 let constant = truth[0];
1197 let coeffs: Vec<bool> = (0..n).map(|i| truth[1 << i] ^ constant).collect();
1198 cands.push((CubeStructure::Affine { coeffs, constant }, n + 1));
1199 }
1200
1201 if let Some(by_weight) = symmetric_profile(truth, n) {
1203 cands.push((CubeStructure::Symmetric { by_weight }, n + 1));
1204 }
1205
1206 if let Some(anf) = describe::anf(truth) {
1209 let weight = anf.iter().filter(|&&c| c).count();
1210 let degree =
1211 anf.iter().enumerate().filter(|(_, &c)| c).map(|(m, _)| (m as u64).count_ones() as usize).max().unwrap_or(0);
1212 let dense = if degree + 2 <= n { partial_binomial_sum(n, degree) } else { usize::MAX };
1215 let bits = weight.saturating_mul(n).min(dense).max(1);
1216 cands.push((CubeStructure::LowDegree { degree, anf }, bits));
1217 }
1218
1219 let (class, description_bits) = cands
1220 .into_iter()
1221 .min_by_key(|(_, b)| *b)
1222 .filter(|(_, b)| *b < raw)
1223 .unwrap_or_else(|| {
1224 let degree = describe::algebraic_degree(truth).unwrap_or(n);
1225 let nonlinearity = describe::nonlinearity(truth).unwrap_or(0);
1226 (CubeStructure::ResistedArsenal { nonlinearity, degree }, raw)
1227 });
1228
1229 Some(StructureReport { num_vars: n, compressed: description_bits < raw, description_bits, raw_bits: raw, class })
1230}
1231
1232#[derive(Clone, Debug)]
1240pub struct StructureCover {
1241 pub num_vars: usize,
1242 pub monomials_by_degree: Vec<usize>,
1244 pub total_monomials: usize,
1246 pub residue_degree: usize,
1248 pub residue_monomials: usize,
1250 pub description_bits: usize,
1252 pub raw_bits: usize,
1253 pub compressed: bool,
1255}
1256
1257pub fn structure_cover(truth: &[bool]) -> Option<StructureCover> {
1262 let anf = describe::anf(truth)?;
1263 let n = truth.len().trailing_zeros() as usize;
1264 let mut by_degree = vec![0usize; n + 1];
1265 for (m, &c) in anf.iter().enumerate() {
1266 if c {
1267 by_degree[(m as u64).count_ones() as usize] += 1;
1268 }
1269 }
1270 let total: usize = by_degree.iter().sum();
1271 let residue_degree = by_degree.iter().rposition(|&c| c > 0).unwrap_or(0);
1272 let residue_monomials = by_degree[residue_degree];
1273 let raw = truth.len();
1274 let description_bits = if total == 0 { 1 } else { total * n };
1275 Some(StructureCover {
1276 num_vars: n,
1277 compressed: description_bits < raw,
1278 monomials_by_degree: by_degree,
1279 total_monomials: total,
1280 residue_degree,
1281 residue_monomials,
1282 description_bits,
1283 raw_bits: raw,
1284 })
1285}
1286
1287#[derive(Clone, Debug)]
1300pub struct LinearStructureReport {
1301 pub num_vars: usize,
1302 pub basis: Vec<usize>,
1304 pub derivative: Vec<bool>,
1307}
1308
1309impl LinearStructureReport {
1310 pub fn dim(&self) -> usize {
1312 self.basis.len()
1313 }
1314 pub fn is_reducible(&self) -> bool {
1316 !self.basis.is_empty()
1317 }
1318 pub fn verify(&self, truth: &[bool]) -> bool {
1321 if self.basis.len() != self.derivative.len() {
1322 return false;
1323 }
1324 if gf2_echelon_basis(&self.basis).len() != self.basis.len() {
1325 return false; }
1327 for (&a, &c) in self.basis.iter().zip(&self.derivative) {
1328 if a == 0 || a >= truth.len() {
1329 return false;
1330 }
1331 if !(0..truth.len()).all(|x| (truth[x] ^ truth[x ^ a]) == c) {
1332 return false;
1333 }
1334 }
1335 true
1336 }
1337}
1338
1339fn gf2_echelon_basis(vectors: &[usize]) -> Vec<usize> {
1342 let mut basis: Vec<usize> = Vec::new();
1343 for &v in vectors {
1344 let mut x = v;
1345 for &b in &basis {
1346 x = x.min(x ^ b);
1347 }
1348 if x != 0 {
1349 basis.push(x);
1350 basis.sort_unstable_by(|a, b| b.cmp(a));
1351 }
1352 }
1353 basis
1354}
1355
1356fn reduce_by_basis(mut x: usize, basis: &[usize]) -> usize {
1359 for &b in basis {
1360 let hb = 1usize << (usize::BITS - 1 - b.leading_zeros());
1361 if x & hb != 0 {
1362 x ^= b;
1363 }
1364 }
1365 x
1366}
1367
1368pub fn linear_structures(truth: &[bool]) -> Option<LinearStructureReport> {
1371 if truth.is_empty() || !truth.len().is_power_of_two() {
1372 return None;
1373 }
1374 let n = truth.len().trailing_zeros() as usize;
1375 let r = describe::autocorrelation(truth)?;
1376 let full = truth.len() as i64;
1377 let structs: Vec<usize> = (1..truth.len()).filter(|&a| r[a].abs() == full).collect();
1378 let basis = gf2_echelon_basis(&structs);
1379 let derivative = basis.iter().map(|&a| truth[a] ^ truth[0]).collect();
1380 Some(LinearStructureReport { num_vars: n, basis, derivative })
1381}
1382
1383#[derive(Clone, Debug)]
1387pub struct InvarianceReduction {
1388 pub original_vars: usize,
1389 pub reduced_vars: usize,
1390 pub free_positions: Vec<usize>,
1392 pub reduced_truth: Vec<bool>,
1394}
1395
1396impl InvarianceReduction {
1397 pub fn verify(&self, truth: &[bool], invariance_basis: &[usize]) -> bool {
1400 if self.reduced_truth.len() != 1 << self.reduced_vars {
1401 return false;
1402 }
1403 (0..truth.len()).all(|x| {
1404 let rep = reduce_by_basis(x, invariance_basis);
1405 let y = self
1406 .free_positions
1407 .iter()
1408 .enumerate()
1409 .fold(0usize, |acc, (i, &p)| if rep & (1 << p) != 0 { acc | (1 << i) } else { acc });
1410 self.reduced_truth[y] == truth[x]
1411 })
1412 }
1413}
1414
1415fn gf2_rref(basis: &[usize]) -> Vec<usize> {
1418 let mut b = basis.to_vec();
1419 let pivots: Vec<usize> = b.iter().map(|&v| 1usize << (usize::BITS - 1 - v.leading_zeros())).collect();
1420 for i in 0..b.len() {
1421 for j in 0..b.len() {
1422 if i != j && b[j] & pivots[i] != 0 {
1423 b[j] ^= b[i];
1424 }
1425 }
1426 }
1427 b
1428}
1429
1430#[derive(Clone, Debug, PartialEq, Eq)]
1435pub struct AffineReduction {
1436 pub num_vars: usize,
1437 pub linear_form: usize,
1439 pub invariance_basis: Vec<usize>,
1440 pub free_positions: Vec<usize>,
1441 pub reduced_truth: Vec<bool>,
1443}
1444
1445impl AffineReduction {
1446 pub fn reconstruct(&self) -> Option<Vec<bool>> {
1448 if self.reduced_truth.len() != 1 << self.free_positions.len() {
1449 return None;
1450 }
1451 Some(
1452 (0..1usize << self.num_vars)
1453 .map(|x| {
1454 let rep = reduce_by_basis(x, &self.invariance_basis);
1455 let y = self
1456 .free_positions
1457 .iter()
1458 .enumerate()
1459 .fold(0usize, |acc, (j, &p)| if rep & (1 << p) != 0 { acc | (1 << j) } else { acc });
1460 self.reduced_truth[y] ^ ((self.linear_form & x).count_ones() % 2 == 1)
1461 })
1462 .collect(),
1463 )
1464 }
1465}
1466
1467pub fn affine_reduce(truth: &[bool]) -> Option<AffineReduction> {
1472 if truth.is_empty() || !truth.len().is_power_of_two() {
1473 return None;
1474 }
1475 let n = truth.len().trailing_zeros() as usize;
1476 let ls = linear_structures(truth)?;
1477 if !ls.derivative.iter().any(|&d| d) {
1478 return None; }
1480 let rref = gf2_rref(&ls.basis);
1482 let mut c = 0usize;
1483 for &b in &rref {
1484 if truth[b] ^ truth[0] {
1485 c |= 1usize << (usize::BITS - 1 - b.leading_zeros());
1486 }
1487 }
1488 let h: Vec<bool> = (0..truth.len()).map(|x| truth[x] ^ ((c & x).count_ones() % 2 == 1)).collect();
1489 let (red, inv_basis) = reduce_by_invariance(&h)?;
1490 let out = AffineReduction {
1491 num_vars: n,
1492 linear_form: c,
1493 invariance_basis: inv_basis,
1494 free_positions: red.free_positions,
1495 reduced_truth: red.reduced_truth,
1496 };
1497 (out.reconstruct().as_deref() == Some(truth)).then_some(out)
1498}
1499
1500pub fn reduce_by_invariance(truth: &[bool]) -> Option<(InvarianceReduction, Vec<usize>)> {
1504 if truth.is_empty() || !truth.len().is_power_of_two() {
1505 return None;
1506 }
1507 let n = truth.len().trailing_zeros() as usize;
1508 let r = describe::autocorrelation(truth)?;
1509 let full = truth.len() as i64;
1510 let invariances: Vec<usize> = (1..truth.len()).filter(|&a| r[a] == full).collect();
1511 let basis = gf2_echelon_basis(&invariances);
1512 if basis.is_empty() {
1513 return None;
1514 }
1515 let pivots: Vec<usize> = basis.iter().map(|&b| (usize::BITS - 1 - b.leading_zeros()) as usize).collect();
1516 let free_positions: Vec<usize> = (0..n).filter(|p| !pivots.contains(p)).collect();
1517 let reduced_vars = free_positions.len();
1518 let reduced_truth: Vec<bool> = (0..1usize << reduced_vars)
1519 .map(|y| {
1520 let x = free_positions
1521 .iter()
1522 .enumerate()
1523 .fold(0usize, |acc, (i, &p)| if y & (1 << i) != 0 { acc | (1 << p) } else { acc });
1524 truth[x]
1525 })
1526 .collect();
1527 Some((InvarianceReduction { original_vars: n, reduced_vars, free_positions, reduced_truth }, basis))
1528}
1529
1530#[derive(Clone, Debug, PartialEq, Eq)]
1543pub struct AffineSignature {
1544 pub num_vars: usize,
1545 pub walsh_abs_distribution: Vec<(u64, usize)>,
1547 pub is_plateaued: bool,
1549 pub amplitude: Option<u64>,
1551 pub is_bent: bool,
1553}
1554
1555impl AffineSignature {
1556 pub fn implied_linear_dim(&self) -> Option<usize> {
1559 let amp = self.amplitude?;
1560 if !amp.is_power_of_two() {
1561 return None;
1562 }
1563 let log = amp.trailing_zeros() as usize;
1564 (2 * log).checked_sub(self.num_vars)
1565 }
1566}
1567
1568pub fn affine_signature(truth: &[bool]) -> Option<AffineSignature> {
1571 let n = truth.len().trailing_zeros() as usize;
1572 let spec = describe::walsh_spectrum(truth)?;
1573 let mut counts: std::collections::BTreeMap<u64, usize> = std::collections::BTreeMap::new();
1574 for &c in &spec {
1575 *counts.entry(c.unsigned_abs()).or_default() += 1;
1576 }
1577 let nonzero: Vec<u64> = counts.keys().copied().filter(|&v| v != 0).collect();
1578 let is_plateaued = nonzero.len() == 1;
1579 let amplitude = is_plateaued.then(|| nonzero[0]);
1580 let bent_amp = (n % 2 == 0).then(|| 1u64 << (n / 2));
1581 let is_bent = amplitude.is_some() && amplitude == bent_amp;
1582 Some(AffineSignature {
1583 num_vars: n,
1584 walsh_abs_distribution: counts.into_iter().collect(),
1585 is_plateaued,
1586 amplitude,
1587 is_bent,
1588 })
1589}
1590
1591#[derive(Clone, Debug)]
1603pub struct SeparableDecomposition {
1604 pub num_vars: usize,
1605 pub constant: bool,
1607 pub blocks: Vec<Vec<usize>>,
1609 pub block_truths: Vec<Vec<bool>>,
1611}
1612
1613impl SeparableDecomposition {
1614 pub fn is_separable(&self) -> bool {
1616 self.blocks.len() >= 2
1617 }
1618 pub fn table_bits(&self) -> usize {
1620 self.block_truths.iter().map(|t| t.len()).sum()
1621 }
1622 pub fn reconstruct(&self, n: usize) -> Option<Vec<bool>> {
1624 let size = 1usize << n;
1625 let mut out = vec![self.constant; size];
1626 for (b, t) in self.blocks.iter().zip(&self.block_truths) {
1627 if t.len() != 1 << b.len() {
1628 return None;
1629 }
1630 for (x, slot) in out.iter_mut().enumerate() {
1631 let y = b.iter().enumerate().fold(0usize, |acc, (j, &i)| {
1632 if x & (1 << i) != 0 { acc | (1 << j) } else { acc }
1633 });
1634 *slot ^= t[y];
1635 }
1636 }
1637 Some(out)
1638 }
1639}
1640
1641fn uf_find(parent: &mut [usize], mut x: usize) -> usize {
1642 while parent[x] != x {
1643 parent[x] = parent[parent[x]];
1644 x = parent[x];
1645 }
1646 x
1647}
1648
1649pub fn separable_decomposition(truth: &[bool]) -> Option<SeparableDecomposition> {
1652 if truth.is_empty() || !truth.len().is_power_of_two() {
1653 return None;
1654 }
1655 let n = truth.len().trailing_zeros() as usize;
1656 let anf = describe::anf(truth)?;
1657 let constant = anf[0];
1658 let mut parent: Vec<usize> = (0..n).collect();
1659 let mut relevant = vec![false; n];
1660 for (m, &c) in anf.iter().enumerate() {
1661 if !c || m == 0 {
1662 continue;
1663 }
1664 let bits: Vec<usize> = (0..n).filter(|&i| m & (1 << i) != 0).collect();
1665 for &i in &bits {
1666 relevant[i] = true;
1667 }
1668 for w in bits.windows(2) {
1669 let (ra, rb) = (uf_find(&mut parent, w[0]), uf_find(&mut parent, w[1]));
1670 parent[ra] = rb;
1671 }
1672 }
1673 let mut comp: std::collections::BTreeMap<usize, Vec<usize>> = std::collections::BTreeMap::new();
1674 for i in 0..n {
1675 if relevant[i] {
1676 let r = uf_find(&mut parent, i);
1677 comp.entry(r).or_default().push(i);
1678 }
1679 }
1680 let blocks: Vec<Vec<usize>> = comp.into_values().collect();
1681 let mut block_truths = Vec::with_capacity(blocks.len());
1682 for b in &blocks {
1683 let bmask: usize = b.iter().fold(0, |acc, &i| acc | (1 << i));
1684 let mut sub = vec![false; 1usize << b.len()];
1685 for (m, &c) in anf.iter().enumerate() {
1686 if !c || m == 0 || m & !bmask != 0 {
1687 continue;
1688 }
1689 let sub_m = b.iter().enumerate().fold(0usize, |acc, (j, &i)| {
1690 if m & (1 << i) != 0 { acc | (1 << j) } else { acc }
1691 });
1692 sub[sub_m] = true;
1693 }
1694 block_truths.push(describe::anf(&sub)?); }
1696 Some(SeparableDecomposition { num_vars: n, constant, blocks, block_truths })
1697}
1698
1699#[derive(Clone, Debug, PartialEq, Eq)]
1712pub struct VariableSymmetry {
1713 pub num_vars: usize,
1714 pub generators: Vec<Vec<usize>>,
1716 pub order: u128,
1718 pub orbit_count: usize,
1720 pub orbit_values: Vec<bool>,
1722}
1723
1724impl VariableSymmetry {
1725 pub fn is_nontrivial(&self) -> bool {
1727 self.order > 1
1728 }
1729 pub fn reconstruct(&self, n: usize) -> Option<Vec<bool>> {
1732 let size = 1usize << n;
1733 if self.orbit_values.len() != self.orbit_count {
1734 return None;
1735 }
1736 let mut orbs = self.input_orbits(size)?;
1737 orbs.sort_by_key(|o| *o.iter().min().expect("nonempty orbit"));
1738 if orbs.len() != self.orbit_count {
1739 return None;
1740 }
1741 let mut out = vec![false; size];
1742 for (k, o) in orbs.iter().enumerate() {
1743 for &x in o {
1744 out[x] = self.orbit_values[k];
1745 }
1746 }
1747 Some(out)
1748 }
1749 pub fn verify(&self, truth: &[bool]) -> bool {
1751 for s in &self.generators {
1752 if s.len() != self.num_vars || !is_variable_automorphism(truth, s) {
1753 return false;
1754 }
1755 }
1756 self.reconstruct(self.num_vars).as_deref() == Some(truth)
1757 }
1758 fn input_orbits(&self, size: usize) -> Option<Vec<Vec<usize>>> {
1759 let lifted: Vec<Vec<usize>> =
1760 self.generators.iter().map(|s| (0..size).map(|x| permute_input_bits(x, s)).collect()).collect();
1761 Some(if lifted.is_empty() { (0..size).map(|x| vec![x]).collect() } else { crate::permgroup::orbits(size, &lifted) })
1762 }
1763}
1764
1765fn permute_input_bits(x: usize, sigma: &[usize]) -> usize {
1767 sigma.iter().enumerate().fold(0usize, |acc, (i, &si)| if x & (1 << i) != 0 { acc | (1 << si) } else { acc })
1768}
1769
1770fn is_variable_automorphism(truth: &[bool], sigma: &[usize]) -> bool {
1772 (0..truth.len()).all(|x| truth[x] == truth[permute_input_bits(x, sigma)])
1773}
1774
1775pub fn variable_symmetry(truth: &[bool]) -> Option<VariableSymmetry> {
1779 if truth.is_empty() || !truth.len().is_power_of_two() {
1780 return None;
1781 }
1782 let n = truth.len().trailing_zeros() as usize;
1783 let mut generators: Vec<Vec<usize>> = Vec::new();
1784 for i in 0..n {
1785 for j in i + 1..n {
1786 let mut s: Vec<usize> = (0..n).collect();
1787 s.swap(i, j);
1788 if is_variable_automorphism(truth, &s) {
1789 generators.push(s);
1790 }
1791 }
1792 }
1793 for a in 0..n {
1796 for b in a + 1..n {
1797 for c in 0..n {
1798 if c == a || c == b {
1799 continue;
1800 }
1801 for d in c + 1..n {
1802 if d == a || d == b {
1803 continue;
1804 }
1805 let mut s: Vec<usize> = (0..n).collect();
1806 s.swap(a, b);
1807 s.swap(c, d);
1808 if is_variable_automorphism(truth, &s) {
1809 generators.push(s);
1810 }
1811 }
1812 }
1813 }
1814 }
1815 if n > 1 {
1816 let rho: Vec<usize> = (0..n).map(|i| (i + 1) % n).collect();
1817 if is_variable_automorphism(truth, &rho) {
1818 generators.push(rho);
1819 }
1820 }
1821 let order = crate::permgroup::schreier_sims(n, &generators).order();
1822 let size = 1usize << n;
1823 let sym = VariableSymmetry { num_vars: n, generators, order, orbit_count: 0, orbit_values: Vec::new() };
1824 let mut orbs = sym.input_orbits(size)?;
1825 orbs.sort_by_key(|o| *o.iter().min().expect("nonempty orbit"));
1826 let orbit_values: Vec<bool> = orbs.iter().map(|o| truth[*o.iter().min().expect("nonempty")]).collect();
1827 Some(VariableSymmetry { orbit_count: orbs.len(), orbit_values, ..sym })
1828}
1829
1830#[derive(Clone, Debug)]
1841pub struct DeepStructureReport {
1842 pub num_vars: usize,
1843 pub raw_bits: usize,
1844 pub description_bits: usize,
1845 pub winner: &'static str,
1848 pub compressed: bool,
1849}
1850
1851pub fn find_structure_deep(truth: &[bool]) -> Option<DeepStructureReport> {
1854 if truth.is_empty() || !truth.len().is_power_of_two() {
1855 return None;
1856 }
1857 let n = truth.len().trailing_zeros() as usize;
1858 let raw = truth.len();
1859 let mut cands: Vec<(&'static str, usize)> = Vec::new();
1860
1861 if let Some(r) = find_structure(truth) {
1862 if r.compressed {
1863 let name = match r.class {
1864 CubeStructure::Constant(_) => "coordinate:constant",
1865 CubeStructure::Junta { .. } => "coordinate:junta",
1866 CubeStructure::Affine { .. } => "coordinate:affine",
1867 CubeStructure::Symmetric { .. } => "coordinate:symmetric",
1868 CubeStructure::LowDegree { .. } => "coordinate:low-degree",
1869 CubeStructure::ResistedArsenal { .. } => "coordinate:residue",
1870 };
1871 cands.push((name, r.description_bits));
1872 }
1873 }
1874 if let Some(d) = separable_decomposition(truth) {
1875 if d.is_separable() {
1876 cands.push(("separable", d.table_bits()));
1877 }
1878 }
1879 if let Some(s) = variable_symmetry(truth) {
1880 if s.is_nontrivial() {
1881 cands.push(("permutation", s.orbit_count));
1882 }
1883 }
1884 if let Some((red, _)) = reduce_by_invariance(truth) {
1885 cands.push(("linear-invariance", 1usize << red.reduced_vars));
1886 }
1887 if let Some(ar) = affine_reduce(truth) {
1888 cands.push(("affine", ar.reduced_truth.len() + n));
1889 }
1890
1891 let (winner, description_bits) =
1892 cands.into_iter().filter(|(_, b)| *b < raw).min_by_key(|(_, b)| *b).unwrap_or(("residue", raw));
1893 Some(DeepStructureReport { num_vars: n, raw_bits: raw, description_bits, winner, compressed: description_bits < raw })
1894}
1895
1896#[derive(Clone, Debug, PartialEq, Eq)]
1908pub enum StructureTree {
1909 Coordinate { class: CubeStructure, num_vars: usize, bits: usize },
1911 Permutation { sym: VariableSymmetry, num_vars: usize },
1913 Residue { truth: Vec<bool> },
1915 Separable { num_vars: usize, constant: bool, blocks: Vec<(Vec<usize>, StructureTree)> },
1917 LinearReduced { num_vars: usize, invariance_basis: Vec<usize>, free_positions: Vec<usize>, inner: Box<StructureTree> },
1919 AffineReduced {
1921 num_vars: usize,
1922 linear_form: usize,
1923 invariance_basis: Vec<usize>,
1924 free_positions: Vec<usize>,
1925 inner: Box<StructureTree>,
1926 },
1927}
1928
1929impl StructureTree {
1930 pub fn depth(&self) -> usize {
1932 match self {
1933 StructureTree::Coordinate { .. } | StructureTree::Permutation { .. } | StructureTree::Residue { .. } => 0,
1934 StructureTree::Separable { blocks, .. } => 1 + blocks.iter().map(|(_, t)| t.depth()).max().unwrap_or(0),
1935 StructureTree::LinearReduced { inner, .. } | StructureTree::AffineReduced { inner, .. } => {
1936 1 + inner.depth()
1937 }
1938 }
1939 }
1940 pub fn total_description_bits(&self) -> usize {
1942 match self {
1943 StructureTree::Coordinate { bits, .. } => *bits,
1944 StructureTree::Permutation { sym, .. } => sym.orbit_count,
1945 StructureTree::Residue { truth } => truth.len(),
1946 StructureTree::Separable { blocks, .. } => {
1947 1 + blocks.iter().map(|(v, t)| v.len() + t.total_description_bits()).sum::<usize>()
1948 }
1949 StructureTree::LinearReduced { invariance_basis, inner, .. } => {
1950 invariance_basis.len() + inner.total_description_bits()
1951 }
1952 StructureTree::AffineReduced { num_vars, inner, .. } => *num_vars + inner.total_description_bits(),
1953 }
1954 }
1955 pub fn reconstruct(&self) -> Option<Vec<bool>> {
1957 match self {
1958 StructureTree::Coordinate { class, num_vars, .. } => class.reconstruct(*num_vars),
1959 StructureTree::Permutation { sym, num_vars } => sym.reconstruct(*num_vars),
1960 StructureTree::Residue { truth } => Some(truth.clone()),
1961 StructureTree::Separable { num_vars, constant, blocks } => {
1962 let size = 1usize << num_vars;
1963 let mut out = vec![*constant; size];
1964 for (vars, sub) in blocks {
1965 let bt = sub.reconstruct()?;
1966 for (x, slot) in out.iter_mut().enumerate() {
1967 let y = vars.iter().enumerate().fold(0usize, |acc, (j, &i)| {
1968 if x & (1 << i) != 0 { acc | (1 << j) } else { acc }
1969 });
1970 *slot ^= bt[y];
1971 }
1972 }
1973 Some(out)
1974 }
1975 StructureTree::LinearReduced { num_vars, invariance_basis, free_positions, inner } => {
1976 let inner_truth = inner.reconstruct()?;
1977 Some(
1978 (0..1usize << num_vars)
1979 .map(|x| {
1980 let rep = reduce_by_basis(x, invariance_basis);
1981 let y = free_positions.iter().enumerate().fold(0usize, |acc, (j, &p)| {
1982 if rep & (1 << p) != 0 { acc | (1 << j) } else { acc }
1983 });
1984 inner_truth[y]
1985 })
1986 .collect(),
1987 )
1988 }
1989 StructureTree::AffineReduced { num_vars, linear_form, invariance_basis, free_positions, inner } => {
1990 let inner_truth = inner.reconstruct()?;
1991 Some(
1992 (0..1usize << num_vars)
1993 .map(|x| {
1994 let rep = reduce_by_basis(x, invariance_basis);
1995 let y = free_positions.iter().enumerate().fold(0usize, |acc, (j, &p)| {
1996 if rep & (1 << p) != 0 { acc | (1 << j) } else { acc }
1997 });
1998 inner_truth[y] ^ ((linear_form & x).count_ones() % 2 == 1)
1999 })
2000 .collect(),
2001 )
2002 }
2003 }
2004 }
2005}
2006
2007pub fn structure_tree(truth: &[bool]) -> Option<StructureTree> {
2011 if truth.is_empty() || !truth.len().is_power_of_two() {
2012 return None;
2013 }
2014 let n = truth.len().trailing_zeros() as usize;
2015 let deep = find_structure_deep(truth)?;
2016 let tree = match deep.winner {
2017 "separable" => {
2018 let d = separable_decomposition(truth)?;
2019 let mut blocks = Vec::with_capacity(d.blocks.len());
2020 for (vars, bt) in d.blocks.iter().zip(&d.block_truths) {
2021 blocks.push((vars.clone(), structure_tree(bt)?));
2022 }
2023 StructureTree::Separable { num_vars: n, constant: d.constant, blocks }
2024 }
2025 "linear-invariance" => {
2026 let (red, basis) = reduce_by_invariance(truth)?;
2027 StructureTree::LinearReduced {
2028 num_vars: n,
2029 invariance_basis: basis,
2030 free_positions: red.free_positions.clone(),
2031 inner: Box::new(structure_tree(&red.reduced_truth)?),
2032 }
2033 }
2034 "affine" => {
2035 let ar = affine_reduce(truth)?;
2036 StructureTree::AffineReduced {
2037 num_vars: n,
2038 linear_form: ar.linear_form,
2039 invariance_basis: ar.invariance_basis,
2040 free_positions: ar.free_positions,
2041 inner: Box::new(structure_tree(&ar.reduced_truth)?),
2042 }
2043 }
2044 "permutation" => StructureTree::Permutation { sym: variable_symmetry(truth)?, num_vars: n },
2045 "residue" => StructureTree::Residue { truth: truth.to_vec() },
2046 _ => StructureTree::Coordinate { class: find_structure(truth)?.class, num_vars: n, bits: deep.description_bits },
2047 };
2048 Some(tree)
2049}
2050
2051#[derive(Clone, Debug)]
2054pub struct BooleanCensus {
2055 pub num_vars: usize,
2056 pub total: usize,
2058 pub by_winner: Vec<(&'static str, usize)>,
2060 pub compressed: usize,
2062 pub residue: usize,
2064}
2065
2066pub fn boolean_function_census(n: usize) -> Option<BooleanCensus> {
2071 if n == 0 || n > 4 {
2072 return None;
2073 }
2074 let dim = 1usize << n;
2075 let total = 1u64 << dim;
2076 let mut counts: std::collections::BTreeMap<&'static str, usize> = std::collections::BTreeMap::new();
2077 for code in 0..total {
2078 let truth: Vec<bool> = (0..dim).map(|i| (code >> i) & 1 == 1).collect();
2079 *counts.entry(find_structure_deep(&truth)?.winner).or_default() += 1;
2080 }
2081 let residue = counts.get("residue").copied().unwrap_or(0);
2082 Some(BooleanCensus {
2083 num_vars: n,
2084 total: total as usize,
2085 by_winner: counts.into_iter().collect(),
2086 compressed: total as usize - residue,
2087 residue,
2088 })
2089}
2090
2091#[derive(Clone, Debug)]
2093pub struct SampledCensus {
2094 pub num_vars: usize,
2095 pub samples: usize,
2096 pub residue: usize,
2098 pub by_winner: Vec<(&'static str, usize)>,
2099}
2100
2101impl SampledCensus {
2102 pub fn residue_fraction(&self) -> f64 {
2104 self.residue as f64 / self.samples.max(1) as f64
2105 }
2106}
2107
2108pub fn sampled_boolean_census(n: usize, samples: usize, seed: u64) -> Option<SampledCensus> {
2113 if n == 0 || n > 12 {
2114 return None;
2115 }
2116 let dim = 1usize << n;
2117 let mut counts: std::collections::BTreeMap<&'static str, usize> = std::collections::BTreeMap::new();
2118 for s in 0..samples {
2119 let truth: Vec<bool> = (0..dim)
2120 .map(|i| {
2121 let mut z =
2122 seed.wrapping_add((s as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15)).wrapping_add(i as u64 + 1);
2123 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
2124 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
2125 (z ^ (z >> 31)) & 1 == 1
2126 })
2127 .collect();
2128 *counts.entry(find_structure_deep(&truth)?.winner).or_default() += 1;
2129 }
2130 let residue = counts.get("residue").copied().unwrap_or(0);
2131 Some(SampledCensus { num_vars: n, samples, residue, by_winner: counts.into_iter().collect() })
2132}
2133
2134#[derive(Clone, Debug)]
2139pub struct KolmogorovBound {
2140 pub num_vars: usize,
2141 pub bits: usize,
2143 pub raw_bits: usize,
2145 pub tree: StructureTree,
2147}
2148
2149impl KolmogorovBound {
2150 pub fn ratio(&self) -> f64 {
2152 self.bits as f64 / self.raw_bits.max(1) as f64
2153 }
2154 pub fn is_compressed(&self) -> bool {
2156 self.bits < self.raw_bits
2157 }
2158 pub fn verify(&self, truth: &[bool]) -> bool {
2160 self.tree.reconstruct().as_deref() == Some(truth)
2161 }
2162}
2163
2164pub fn kolmogorov_bound(truth: &[bool]) -> Option<KolmogorovBound> {
2167 let n = truth.len().trailing_zeros() as usize;
2168 let tree = structure_tree(truth)?;
2169 Some(KolmogorovBound { num_vars: n, bits: tree.total_description_bits(), raw_bits: truth.len(), tree })
2170}
2171
2172#[derive(Clone, Debug, PartialEq, Eq)]
2185pub struct SboxProfile {
2186 pub in_bits: usize,
2187 pub out_bits: usize,
2188 pub differential_uniformity: u32,
2191 pub linearity: u64,
2193 pub min_degree: usize,
2195 pub is_affine: bool,
2197 pub is_bijective: bool,
2199 pub is_apn: bool,
2201}
2202
2203pub fn sbox_profile(sbox: &[u32], out_bits: usize) -> Option<SboxProfile> {
2207 if sbox.is_empty() || !sbox.len().is_power_of_two() {
2208 return None;
2209 }
2210 let n = sbox.len().trailing_zeros() as usize;
2211 let m = out_bits;
2212 let size = sbox.len();
2213
2214 let mut differential_uniformity = 0u32;
2215 for a in 1..size {
2216 let mut count = vec![0u32; 1usize << m];
2217 for x in 0..size {
2218 count[(sbox[x] ^ sbox[x ^ a]) as usize] += 1;
2219 }
2220 differential_uniformity = differential_uniformity.max(*count.iter().max().unwrap_or(&0));
2221 }
2222
2223 let mut linearity = 0u64;
2224 let mut min_degree = usize::MAX;
2225 let mut is_affine = true;
2226 for b in 1..(1usize << m) {
2227 let comp: Vec<bool> = sbox.iter().map(|&y| (b as u32 & y).count_ones() % 2 == 1).collect();
2228 let spec = describe::walsh_spectrum(&comp)?;
2229 linearity = linearity.max(spec.iter().map(|&c| c.unsigned_abs()).max().unwrap_or(0));
2230 let deg = describe::algebraic_degree(&comp)?;
2231 min_degree = min_degree.min(deg);
2232 if deg > 1 {
2233 is_affine = false;
2234 }
2235 }
2236
2237 let mut seen = vec![false; 1usize << m];
2238 let is_bijective = n == m
2239 && sbox.iter().all(|&y| {
2240 let u = y as usize;
2241 u < seen.len() && !std::mem::replace(&mut seen[u], true)
2242 });
2243
2244 Some(SboxProfile {
2245 in_bits: n,
2246 out_bits: m,
2247 differential_uniformity,
2248 linearity,
2249 min_degree: if min_degree == usize::MAX { 0 } else { min_degree },
2250 is_affine,
2251 is_bijective,
2252 is_apn: n == m && differential_uniformity == 2,
2253 })
2254}
2255
2256#[derive(Clone, Debug, PartialEq, Eq)]
2258pub struct SboxSpectra {
2259 pub in_bits: usize,
2260 pub out_bits: usize,
2261 pub differential_spectrum: Vec<(u32, usize)>,
2264 pub walsh_spectrum: Vec<(u64, usize)>,
2266}
2267
2268pub fn sbox_spectra(sbox: &[u32], out_bits: usize) -> Option<SboxSpectra> {
2273 if sbox.is_empty() || !sbox.len().is_power_of_two() {
2274 return None;
2275 }
2276 let n = sbox.len().trailing_zeros() as usize;
2277 let m = out_bits;
2278 let size = sbox.len();
2279
2280 let mut ddt_hist: std::collections::BTreeMap<u32, usize> = std::collections::BTreeMap::new();
2281 for a in 1..size {
2282 let mut count = vec![0u32; 1usize << m];
2283 for x in 0..size {
2284 count[(sbox[x] ^ sbox[x ^ a]) as usize] += 1;
2285 }
2286 for &c in &count {
2287 *ddt_hist.entry(c).or_default() += 1;
2288 }
2289 }
2290
2291 let mut walsh_hist: std::collections::BTreeMap<u64, usize> = std::collections::BTreeMap::new();
2292 for b in 1..(1usize << m) {
2293 let comp: Vec<bool> = sbox.iter().map(|&y| (b as u32 & y).count_ones() % 2 == 1).collect();
2294 for &w in &describe::walsh_spectrum(&comp)? {
2295 *walsh_hist.entry(w.unsigned_abs()).or_default() += 1;
2296 }
2297 }
2298
2299 Some(SboxSpectra {
2300 in_bits: n,
2301 out_bits: m,
2302 differential_spectrum: ddt_hist.into_iter().collect(),
2303 walsh_spectrum: walsh_hist.into_iter().collect(),
2304 })
2305}
2306
2307pub fn boomerang_uniformity(sbox: &[u32]) -> Option<u32> {
2312 if sbox.is_empty() || !sbox.len().is_power_of_two() {
2313 return None;
2314 }
2315 let size = sbox.len();
2316 let mut inv = vec![u32::MAX; size];
2317 for (x, &y) in sbox.iter().enumerate() {
2318 let u = y as usize;
2319 if u >= size || inv[u] != u32::MAX {
2320 return None; }
2322 inv[u] = x as u32;
2323 }
2324 let mut bu = 0u32;
2325 for a in 1..size {
2326 for b in 1..size {
2327 let mut count = 0u32;
2328 for x in 0..size {
2329 let lhs = inv[(sbox[x] as usize) ^ b] ^ inv[(sbox[x ^ a] as usize) ^ b];
2330 if lhs as usize == a {
2331 count += 1;
2332 }
2333 }
2334 bu = bu.max(count);
2335 }
2336 }
2337 Some(bu)
2338}
2339
2340#[derive(Clone, Debug, PartialEq, Eq)]
2345pub enum SboxVerdict {
2346 Affine,
2348 LinearComponent,
2351 DeterministicDifference,
2353 Quadratic,
2355 NoStructuralWeaknessFound {
2357 differential_uniformity: u32,
2358 linearity: u64,
2359 min_degree: usize,
2360 boomerang_uniformity: Option<u32>,
2361 },
2362}
2363
2364pub fn sbox_full_audit(sbox: &[u32], out_bits: usize) -> Option<SboxVerdict> {
2369 let p = sbox_profile(sbox, out_bits)?;
2370 let full = 1u64 << p.in_bits;
2371 if p.is_affine {
2372 return Some(SboxVerdict::Affine);
2373 }
2374 if p.linearity == full {
2375 return Some(SboxVerdict::LinearComponent);
2376 }
2377 if p.differential_uniformity as u64 == full {
2378 return Some(SboxVerdict::DeterministicDifference);
2379 }
2380 if p.min_degree == 2 {
2381 return Some(SboxVerdict::Quadratic);
2382 }
2383 Some(SboxVerdict::NoStructuralWeaknessFound {
2384 differential_uniformity: p.differential_uniformity,
2385 linearity: p.linearity,
2386 min_degree: p.min_degree,
2387 boomerang_uniformity: if p.is_bijective { boomerang_uniformity(sbox) } else { None },
2388 })
2389}
2390
2391#[derive(Clone, Debug, PartialEq, Eq)]
2403pub enum RsaStrength {
2404 Factored { p: logicaffeine_base::BigInt, q: logicaffeine_base::BigInt, method: &'static str },
2406 SoundAgainstStructuralAttacks,
2409}
2410
2411pub fn rsa_structural_audit(n: &logicaffeine_base::BigInt) -> RsaStrength {
2415 match crate::factor::structural_factor(n, Default::default()) {
2416 Some(w) => RsaStrength::Factored { p: w.p, q: w.q, method: w.method },
2417 None => RsaStrength::SoundAgainstStructuralAttacks,
2418 }
2419}
2420
2421#[derive(Clone, Debug, PartialEq, Eq)]
2423pub enum RsaAuditVerdict {
2424 Factored { method: &'static str, p: logicaffeine_base::BigInt, q: logicaffeine_base::BigInt },
2426 WeakExponent,
2428 CompressibleModulus(CompressibilityClass),
2430 ResistsFullArsenal,
2434}
2435
2436pub fn rsa_full_audit(n: &logicaffeine_base::BigInt, e: &logicaffeine_base::BigInt) -> RsaAuditVerdict {
2442 use crate::factor;
2443 if let Some(w) = factor::structural_factor(n, Default::default()) {
2444 return RsaAuditVerdict::Factored { method: w.method, p: w.p, q: w.q };
2445 }
2446 if factor::wiener(e, n).is_some() {
2447 return RsaAuditVerdict::WeakExponent;
2448 }
2449 let (_, bytes) = n.to_le_bytes();
2450 let report = classify_bytes(&bytes);
2451 if report.class != CompressibilityClass::Incompressible {
2452 return RsaAuditVerdict::CompressibleModulus(report.class);
2453 }
2454 RsaAuditVerdict::ResistsFullArsenal
2455}
2456
2457pub fn assess_key_material(data: &[u8]) -> CryptoStrength {
2460 let ints: Vec<i64> = data.iter().map(|&b| b as i64).collect();
2461 let witness = DescriptionBound::of_int_seq(&ints);
2462 let ratio = if data.is_empty() { 1.0 } else { witness.bytes as f64 / data.len() as f64 };
2463 if witness.bytes < data.len() {
2465 CryptoStrength::Weak { witness, ratio }
2466 } else {
2467 CryptoStrength::IncompressibleInClass { ratio }
2468 }
2469}
2470
2471#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2481pub enum CompressibilityClass {
2482 Generated,
2485 Periodic,
2487 LowEntropy,
2489 Smooth,
2491 Incompressible,
2493}
2494
2495#[derive(Clone, Debug)]
2497pub struct CompressibilityReport {
2498 pub class: CompressibilityClass,
2499 pub ratio: f64,
2501 pub described_bytes: usize,
2502 pub raw_bytes: usize,
2503}
2504
2505pub fn classify_bytes(data: &[u8]) -> CompressibilityReport {
2507 let ints: Vec<i64> = data.iter().map(|&b| b as i64).collect();
2508 let described = describe::describe_int_seq(&ints);
2509 let raw = data.len();
2510 let ratio = if raw == 0 { 1.0 } else { described.len() as f64 / raw as f64 };
2511 let class = if described.len() >= raw.max(1) {
2512 CompressibilityClass::Incompressible
2514 } else {
2515 class_of_tag(described.first().copied().unwrap_or(describe::T_INTS))
2516 };
2517 CompressibilityReport { class, ratio, described_bytes: described.len(), raw_bytes: raw }
2518}
2519
2520pub fn classify_text(text: &str) -> CompressibilityReport {
2522 classify_bytes(text.as_bytes())
2523}
2524
2525pub fn classify_int_seq(data: &[i64]) -> CompressibilityReport {
2529 let described = describe::describe_int_seq(data);
2530 let mut baseline = vec![describe::T_INTS];
2531 describe::leb128_encode(&mut baseline, data.iter().copied(), data.len());
2532 let raw = baseline.len();
2533 let ratio = if raw == 0 { 1.0 } else { described.len() as f64 / raw as f64 };
2534 let class = if described.len() >= raw {
2535 CompressibilityClass::Incompressible
2536 } else {
2537 class_of_tag(described.first().copied().unwrap_or(describe::T_INTS))
2538 };
2539 CompressibilityReport { class, ratio, described_bytes: described.len(), raw_bytes: raw }
2540}
2541
2542fn class_of_tag(tag: u8) -> CompressibilityClass {
2544 use describe::*;
2545 match tag {
2546 t if t == T_INTS_AFFINE
2547 || t == T_INTS_GEOMETRIC
2548 || t == T_INTS_POLY
2549 || t == T_GEN
2550 || t == T_INTS_LRECUR =>
2551 {
2552 CompressibilityClass::Generated
2553 }
2554 t if t == T_INTS_PERIODIC => CompressibilityClass::Periodic,
2555 t if t == T_INTS_SPARSE || t == T_INTS_RLE || t == T_INTS_DICT => CompressibilityClass::LowEntropy,
2556 t if t == T_INTS_DELTA || t == T_INTS_DOD || t == T_INTS_FOR => CompressibilityClass::Smooth,
2557 _ => CompressibilityClass::Incompressible, }
2559}
2560
2561fn independent_gf2(vectors: &[Vec<bool>]) -> bool {
2564 let mut rows: Vec<u64> = vectors
2565 .iter()
2566 .map(|v| v.iter().enumerate().fold(0u64, |m, (i, &b)| if b && i < 64 { m | (1u64 << i) } else { m }))
2567 .collect();
2568 let mut rank = 0usize;
2569 for col in 0..64u32 {
2570 if let Some(piv) = (rank..rows.len()).find(|&r| rows[r] & (1u64 << col) != 0) {
2571 rows.swap(rank, piv);
2572 let pr = rows[rank];
2573 for r in 0..rows.len() {
2574 if r != rank && rows[r] & (1u64 << col) != 0 {
2575 rows[r] ^= pr;
2576 }
2577 }
2578 rank += 1;
2579 }
2580 }
2581 rank == vectors.len()
2582}
2583
2584#[cfg(test)]
2585mod tests {
2586 use super::*;
2587
2588 #[test]
2589 fn description_bound_decode_witness_round_trips() {
2590 let v: Vec<i64> = (0..200).map(|i| 10 + 7 * i).collect();
2593 let db = DescriptionBound::of_int_seq(&v);
2594 assert!(db.verify(), "the decode witness must reproduce the described sequence");
2595 let baseline = describe::describe_int_seq(&v); let mut plain = vec![19u8]; describe::leb128_encode(&mut plain, v.iter().copied(), v.len());
2598 assert!(db.bytes <= plain.len(), "K̄ is never larger than the varint baseline");
2599 assert!(db.bytes < 12, "an affine column ships as a generator (a handful of bytes)");
2600 assert_eq!(db.bytes, baseline.len());
2601 }
2602
2603 #[test]
2604 fn description_bound_rejects_tampered_witness() {
2605 let v: Vec<i64> = (0..50).map(|i| i * i - 3 * i + 5).collect();
2606 let mut db = DescriptionBound::of_int_seq(&v);
2607 assert!(db.verify(), "the honest witness verifies");
2608 if let Descriptor::IntSeq { encoded } = &mut db.descriptor {
2611 let last = encoded.len() - 1;
2612 encoded[last] ^= 0xff;
2613 }
2614 assert!(!db.verify(), "a tampered decode witness must be rejected");
2615 }
2616
2617 #[test]
2618 fn compression_is_never_over_claimed() {
2619 let mut s = 0x9e37_79b9_7f4a_7c15u64;
2625 let v: Vec<i64> = (0..300)
2626 .map(|_| {
2627 s ^= s << 13;
2628 s ^= s >> 7;
2629 s ^= s << 17;
2630 (s >> 1) as i64
2631 })
2632 .collect();
2633 let db = DescriptionBound::of_int_seq(&v);
2634 assert!(db.verify(), "any reported bound must decode back to the exact sequence");
2635 let mut plain = vec![19u8];
2636 describe::leb128_encode(&mut plain, v.iter().copied(), v.len());
2637 assert!(db.bytes <= plain.len(), "K̄ is never larger than the varint baseline");
2638 }
2639
2640 fn splitmix_bytes(n: usize, mut s: u64) -> Vec<u8> {
2644 (0..n)
2645 .map(|_| {
2646 s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
2647 let mut z = s;
2648 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
2649 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
2650 z ^= z >> 31;
2651 (z >> 24) as u8
2652 })
2653 .collect()
2654 }
2655
2656 #[test]
2657 fn structured_key_material_is_certified_weak() {
2658 let weak_key: Vec<u8> = (0..300).map(|i| (i % 7) as u8).collect();
2661 match assess_key_material(&weak_key) {
2662 CryptoStrength::Weak { witness, ratio } => {
2663 assert!(witness.verify(), "the compression witness reproduces the key (the attack)");
2664 assert!(ratio < 0.5, "a predictable key is far shorter than its raw bytes, got {ratio}");
2665 }
2666 CryptoStrength::IncompressibleInClass { .. } => panic!("a periodic key must be flagged weak"),
2667 }
2668 }
2669
2670 #[test]
2671 fn lfsr_keystream_key_is_certified_weak() {
2672 let taps = [false, false, true, false, false, false, true]; let seed = [true, false, true, true, false, false, true];
2677 let bits = describe::lfsr_generate(&taps, &seed, 200 * 8);
2678 let key: Vec<u8> = describe::bits_to_bytes(&bits).iter().map(|&x| x as u8).collect();
2679 match assess_key_material(&key) {
2680 CryptoStrength::Weak { witness, ratio } => {
2681 assert!(witness.verify(), "the recovered register reproduces the keystream (the attack)");
2682 assert!(ratio < 0.2, "the key collapses to its register, ratio {ratio}");
2683 }
2684 CryptoStrength::IncompressibleInClass { .. } => panic!("an LFSR keystream key must be weak"),
2685 }
2686 }
2687
2688 #[test]
2689 fn word_and_two_adic_complexity_detect_their_keystreams() {
2690 let taps = [describe::Gf256(0x02), describe::Gf256(0x8d), describe::Gf256(0x1f)];
2692 let seed = [describe::Gf256(0x41), describe::Gf256(0x9c), describe::Gf256(0x07)];
2693 let word_key: Vec<u8> = describe::lfsr_generate_field(&taps, &seed, 120).iter().map(|g| g.0).collect();
2694 assert_eq!(gf256_word_complexity(&word_key), 3, "word-LFSR keystream has word complexity 3");
2695
2696 let bits = describe::fcsr_generate(
2698 &logicaffeine_base::BigInt::from_i64(7),
2699 &logicaffeine_base::BigInt::from_i64(19),
2700 8 * 60,
2701 );
2702 let fcsr_key: Vec<u8> = describe::bits_to_bytes(&bits).iter().map(|&x| x as u8).collect();
2703 assert!(two_adic_complexity_of_bytes(&fcsr_key) < 12, "FCSR key has low 2-adic complexity");
2704
2705 let random = splitmix_bytes(120, 0x1357_9bdf_2468_ace0);
2707 assert!(gf256_word_complexity(&random) > 30, "random has high word complexity");
2708 assert!(two_adic_complexity_of_bytes(&random) > 200, "random ≈ n/2 2-adic complexity");
2709 let bits: Vec<bool> = random.iter().flat_map(|&b| (0..8).map(move |j| (b >> j) & 1 == 1)).collect();
2711 assert!(
2712 maximal_order_complexity_of_bytes(&random) <= describe::berlekamp_massey_gf2(&bits).0,
2713 "MOC ≤ linear complexity for everything (it is the shorter, more general register)"
2714 );
2715 }
2716
2717 #[test]
2718 fn algebraic_attack_breaks_a_nonlinear_keystream_that_maximal_order_only_measures() {
2719 let seed: Vec<bool> = (0..8).map(|k| (0x9E37u64 >> k) & 1 == 1).collect();
2725 let mut bits = seed.clone();
2726 while bits.len() < 8 * 80 {
2727 let i = bits.len();
2728 let s = |k: usize| bits[i - k];
2729 bits.push(s(1) ^ s(5) ^ s(6) ^ s(8) ^ (s(1) & s(6)));
2730 }
2731 let key: Vec<u8> = describe::bits_to_bytes(&bits).iter().map(|&x| x as u8).collect();
2732
2733 assert!(gf256_word_complexity(&key) > 30, "word-LFSR rung fooled by the nonlinear feedback");
2735 assert!(two_adic_complexity_of_bytes(&key) > 100, "2-adic rung fooled too");
2736 assert!(maximal_order_complexity_of_bytes(&key) <= 8, "maximal order complexity sees the order-8 register");
2737
2738 let attack = algebraic_attack_on_bytes(&key, 2, 16).expect("degree-2 attack recovers the register");
2740 assert!(attack.order <= 8, "the shortest register found is order ≤ 8, got {}", attack.order);
2741 assert_eq!(attack.truth_table, 1 << attack.order, "truth-table size is 2^order");
2742 assert!(
2743 attack.anf_terms < attack.truth_table,
2744 "the ANF ({} terms) is smaller than the truth table ({})",
2745 attack.anf_terms,
2746 attack.truth_table
2747 );
2748
2749 let regen = describe::algebraic_generate(attack.order, attack.degree, &attack.anf, &bits[..attack.order], bits.len());
2751 assert_eq!(regen, bits, "the recovered ANF regenerates the whole nonlinear keystream — the attack");
2752 }
2753
2754 #[test]
2755 fn combiner_scan_breaks_geffe_but_not_a_cryptographic_keystream() {
2756 let n = 8 * 250; let taps1 = [false, false, true, false, false, false, true]; let taps2 = [false, false, true, false, true]; let taps3 = [false, false, false, false, true, false, false, false, true]; let seed1 = [true, false, true, true, false, false, true];
2762 let seed2 = [true, true, false, false, true];
2763 let seed3 = [true, false, false, true, false, true, true, false, true];
2764 let x1 = describe::lfsr_generate(&taps1, &seed1, n);
2765 let x2 = describe::lfsr_generate(&taps2, &seed2, n);
2766 let x3 = describe::lfsr_generate(&taps3, &seed3, n);
2767 let z: Vec<bool> = (0..n).map(|i| if x2[i] { x1[i] } else { x3[i] }).collect();
2768 let key: Vec<u8> = describe::bits_to_bytes(&z).iter().map(|&x| x as u8).collect();
2769
2770 let candidates = vec![taps1.to_vec(), taps2.to_vec(), taps3.to_vec()];
2771 let leaks = scan_for_combiner_leaks(&key, &candidates, 3.0);
2772
2773 let leaking: Vec<usize> = leaks.iter().map(|l| l.candidate_index).collect();
2775 assert_eq!(leaking, vec![0, 2], "Geffe leaks its outer registers, not the protected middle");
2776 for leak in &leaks {
2778 let taps = &candidates[leak.candidate_index];
2779 let regen = describe::lfsr_generate(taps, &leak.attack.init_state, n);
2780 let planted = if leak.candidate_index == 0 { &x1 } else { &x3 };
2781 assert_eq!(®en, planted, "the recovered initial state regenerates the hidden register");
2782 assert!(leak.margin > 3.0, "the leak clears the significance threshold, margin {}", leak.margin);
2783 }
2784 let random = splitmix_bytes(250, 0xfeed_face_0bad_c0de);
2788 assert!(
2789 scan_for_combiner_leaks(&random, &candidates, 3.0).is_empty(),
2790 "a strong keystream leaks no register to first-order correlation — the ceiling"
2791 );
2792 }
2793
2794 #[test]
2795 fn linear_cryptanalysis_reads_the_whole_spectrum_where_correlation_reads_one_bit() {
2796 let f: Vec<bool> = (0..16)
2799 .map(|x| {
2800 let b = |i: usize| (x >> i) & 1 == 1;
2801 b(0) ^ b(1) ^ (b(2) & b(3))
2802 })
2803 .collect();
2804 let d = linear_cryptanalysis(&f).expect("well-formed");
2805 assert_eq!(d.immunity_order, 1, "first-order correlation-immune — E finds nothing");
2806 assert_eq!(d.bias, 0.25, "but a linear approximation leaks with bias ¼");
2807 assert!(d.mask_weight >= 2, "at weight ≥ 2 — the multi-register leak beyond E's reach");
2808
2809 let g: Vec<bool> = (0..16)
2811 .map(|x| {
2812 let b = |i: usize| (x >> i) & 1 == 1;
2813 (b(0) & b(1)) ^ (b(2) & b(3))
2814 })
2815 .collect();
2816 let dg = linear_cryptanalysis(&g).expect("well-formed");
2817 assert_eq!(dg.nonlinearity, 6, "bent ⇒ maximal nonlinearity 6 for n=4");
2818 assert_eq!(dg.bias, 0.125, "no linear approximation beats the flat-spectrum floor 1/8 — the ceiling");
2819 }
2820
2821 #[test]
2822 fn algebraic_immunity_report_grades_filters_and_the_attack_recovers_state() {
2823 let weak: Vec<bool> = (0..16).map(|x| (x >> 0) & 1 == 1).collect(); let wr = algebraic_immunity_of(&weak).expect("well-formed");
2826 assert_eq!(wr.immunity, 1, "an affine filter has AI 1");
2827 assert!(!wr.is_maximal, "AI 1 < ⌈4/2⌉ = 2 — exploitable");
2828 assert!(describe::verify_annihilator(&weak, &wr.witness), "the annihilator re-checks");
2829
2830 let bent: Vec<bool> = (0..16)
2831 .map(|x| {
2832 let b = |i: usize| (x >> i) & 1 == 1;
2833 (b(0) & b(1)) ^ (b(2) & b(3))
2834 })
2835 .collect();
2836 let br = algebraic_immunity_of(&bent).expect("well-formed");
2837 assert_eq!(br.immunity, 2, "the bent filter has AI 2");
2838 assert!(br.is_maximal, "AI 2 = ⌈4/2⌉ — maximal algebraic immunity, the ceiling");
2839
2840 let taps = [false, false, false, false, false, false, true, false, false, true]; let s0 = [true, true, false, true, false, true, true, false, false, true];
2843 let filter: Vec<bool> = (0..8).map(|x| (x as u32).count_ones() >= 2).collect(); let (m, n) = (3usize, 400usize);
2845 let seq = describe::lfsr_generate(&taps, &s0, n + m);
2846 let keystream: Vec<bool> = (0..n)
2847 .map(|t| {
2848 let idx = (0..m).fold(0usize, |a, i| a | (usize::from(seq[t + i]) << i));
2849 filter[idx]
2850 })
2851 .collect();
2852 let recovered = algebraic_filter_attack(&keystream, &taps, &filter).expect("attack succeeds");
2853 assert_eq!(recovered, s0.to_vec(), "the algebraic attack recovers the exact secret state");
2854 }
2855
2856 #[test]
2857 fn fast_correlation_scales_the_correlation_break_past_exhaustive_search() {
2858 let mut taps = vec![false; 17];
2861 taps[13] = true;
2862 taps[16] = true;
2863 let seed: Vec<bool> = (0..17).map(|k| (0x5A5Au64 >> k) & 1 == 1).collect();
2864 let n = 4000;
2865 let a = describe::lfsr_generate(&taps, &seed, n);
2866 let mut st = 0x0f1e_2d3c_4b5a_6978u64;
2867 let z: Vec<bool> = a
2868 .iter()
2869 .map(|&bit| {
2870 st = st.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
2871 bit ^ ((st >> 40) % 100 < 12)
2872 })
2873 .collect();
2874 let state = fast_correlation_attack(&z, &taps, 400).expect("decodes the leaking register");
2875 assert_eq!(state, seed, "the recovered state IS the register key — no 2¹⁷ search");
2876 }
2877
2878 #[test]
2879 fn shrinking_generator_falls_to_clock_control_inversion() {
2880 let a_taps = [false, false, true, false, false, false, true];
2883 let s_taps = [false, false, false, false, true, false, false, false, true];
2884 let a_seed = [true, true, false, false, true, false, true];
2885 let s_seed = [false, true, true, false, true, true, false, false, true];
2886 let m = 300;
2887 let output = describe::shrinking_generator(&a_taps, &a_seed, &s_taps, &s_seed, m);
2888 let (a_rec, s_rec) = attack_shrinking_generator(&output, &a_taps, &s_taps).expect("the generator falls");
2889 assert_eq!(
2890 describe::shrinking_generator(&a_taps, &a_rec, &s_taps, &s_rec, 800),
2891 describe::shrinking_generator(&a_taps, &a_seed, &s_taps, &s_seed, 800),
2892 "the recovered registers reproduce the keystream well past the attacked length — a full break"
2893 );
2894 }
2895
2896 #[test]
2897 fn full_arsenal_audit_flags_weak_keys_and_clears_a_sound_one() {
2898 use crate::factor;
2899 use logicaffeine_base::BigInt;
2900 let big = |s: &str| BigInt::parse_decimal(s).unwrap();
2901 let one = BigInt::from_i64(1);
2902 let e = BigInt::from_i64(65537);
2903
2904 let p = factor::next_prime(&big("1000000000000000000000000000057"));
2906 let q = factor::next_prime(&p.add(&BigInt::from_i64(100)));
2907 let n = p.mul(&q);
2908 assert!(matches!(rsa_full_audit(&n, &e), RsaAuditVerdict::Factored { .. }), "close primes caught");
2909
2910 let p = factor::next_prime(&big("1000000000000000"));
2912 let q = factor::next_prime(&big("3000000000000000"));
2913 let n = p.mul(&q);
2914 let phi = p.sub(&one).mul(&q.sub(&one));
2915 let small_d = BigInt::from_i64(7919);
2916 let big_e = factor::mod_inverse(&small_d, &phi).expect("d coprime to φ");
2917 assert_eq!(rsa_full_audit(&n, &big_e), RsaAuditVerdict::WeakExponent, "small d caught by Wiener");
2918
2919 let p = factor::next_prime(&big("1000000000000000000000000000057"));
2922 let q = factor::next_prime(&big("9000000000000000000000000000000"));
2923 let n = p.mul(&q);
2924 assert_eq!(
2925 rsa_full_audit(&n, &e),
2926 RsaAuditVerdict::ResistsFullArsenal,
2927 "a sound RSA key resists the entire arsenal — we are not breaking RSA"
2928 );
2929 }
2930
2931 #[test]
2932 fn recursive_reduction_reaches_the_incompressible_fixed_point() {
2933 let structured: Vec<u8> = (0..300).map(|i| [3u8, 1, 4, 1, 5, 9, 2, 6][i % 8]).collect();
2935 let r = recursive_reduce(&structured);
2936 assert!(r.compressed, "the periodic sequence is symmetry-broken down");
2937 assert!(r.irreducible_bytes < structured.len() / 2, "and reaches a small fixed point, {:?}", r.sizes);
2938
2939 let random: Vec<u8> = (0..300u32)
2942 .map(|i| {
2943 let mut z = 0x9E37_79B9_7F4A_7C15u64.wrapping_mul(i as u64 + 1);
2944 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
2945 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
2946 ((z ^ (z >> 31)) & 0xFF) as u8
2947 })
2948 .collect();
2949 let r = recursive_reduce(&random);
2950 assert_eq!(r.depth, 0, "random has no structure any lens breaks — the fixed point is the object");
2951 assert!(!r.compressed, "no lens fires — the residue, which we cannot prove is truly structureless");
2952 }
2953
2954 fn pseudorandom_truth(n: usize) -> Vec<bool> {
2957 (0..1u64 << n)
2958 .map(|i| {
2959 let mut z = 0x9E37_79B9_7F4A_7C15u64.wrapping_mul(i + 1);
2960 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
2961 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
2962 (z ^ (z >> 31)) & 1 == 1
2963 })
2964 .collect()
2965 }
2966
2967 #[test]
2968 fn find_structure_reads_a_constant_off_the_cube() {
2969 let truth = vec![true; 8]; let r = find_structure(&truth).expect("a well-formed cube");
2971 assert_eq!(r.num_vars, 3);
2972 assert!(matches!(r.class, CubeStructure::Constant(true)));
2973 assert!(r.compressed && r.description_bits == 1, "the whole cube in one bit");
2974 assert_eq!(r.class.reconstruct(3).as_deref(), Some(&truth[..]));
2975 }
2976
2977 #[test]
2978 fn find_structure_reads_a_junta_off_the_coordinate_walk() {
2979 let n = 8;
2982 let truth: Vec<bool> = (0..1usize << n).map(|x| (x & 0b111).count_ones() >= 2).collect();
2983 let r = find_structure(&truth).unwrap();
2984 match &r.class {
2985 CubeStructure::Junta { relevant, .. } => assert_eq!(relevant, &vec![0, 1, 2]),
2986 other => panic!("expected a junta, got {other:?}"),
2987 }
2988 assert!(r.compressed);
2989 assert_eq!(r.class.reconstruct(n).as_deref(), Some(&truth[..]));
2990 }
2991
2992 #[test]
2993 fn find_structure_reads_affine_parity_off_the_walsh_walk() {
2994 let n = 4;
2996 let truth: Vec<bool> = (0..1usize << n).map(|x| (x as u32).count_ones() % 2 == 1).collect();
2997 let r = find_structure(&truth).unwrap();
2998 match &r.class {
2999 CubeStructure::Affine { coeffs, constant } => {
3000 assert_eq!(coeffs, &vec![true; 4]);
3001 assert!(!constant);
3002 }
3003 other => panic!("expected affine, got {other:?}"),
3004 }
3005 assert!(r.compressed && r.description_bits == n + 1);
3006 assert_eq!(r.class.reconstruct(n).as_deref(), Some(&truth[..]));
3007 }
3008
3009 #[test]
3010 fn find_structure_reads_a_symmetric_function_off_the_weight_shells() {
3011 let n = 3;
3013 let truth: Vec<bool> = (0..1usize << n).map(|x| (x as u32).count_ones() >= 2).collect();
3014 let r = find_structure(&truth).unwrap();
3015 assert!(matches!(r.class, CubeStructure::Symmetric { .. }), "got {:?}", r.class);
3016 assert!(r.compressed);
3017 assert_eq!(r.class.reconstruct(n).as_deref(), Some(&truth[..]));
3018 }
3019
3020 #[test]
3021 fn find_structure_reads_low_degree_off_the_mobius_walk() {
3022 let n = 6;
3025 let truth: Vec<bool> = (0..1usize << n)
3026 .map(|x| {
3027 let b = |i: usize| x & (1usize << i) != 0;
3028 (b(0) && b(1)) ^ (b(2) && b(3)) ^ (b(4) && b(5))
3029 })
3030 .collect();
3031 let r = find_structure(&truth).unwrap();
3032 match &r.class {
3033 CubeStructure::LowDegree { degree, .. } => assert_eq!(*degree, 2),
3034 other => panic!("expected low-degree, got {other:?}"),
3035 }
3036 assert!(r.compressed);
3037 assert_eq!(r.class.reconstruct(n).as_deref(), Some(&truth[..]));
3038 }
3039
3040 #[test]
3041 fn find_structure_finds_no_lens_for_a_pseudorandom_function() {
3042 let n = 6;
3044 let truth = pseudorandom_truth(n);
3045 let r = find_structure(&truth).unwrap();
3046 assert!(matches!(r.class, CubeStructure::ResistedArsenal { .. }), "got {:?}", r.class);
3047 assert!(!r.compressed, "the residue does not beat storing the truth table");
3048 assert!(r.class.reconstruct(n).is_none(), "the residue carries no compressed witness");
3049 }
3050
3051 #[test]
3052 fn structure_witness_is_re_checkable_and_rejects_tampering() {
3053 let n = 4;
3054 let truth: Vec<bool> = (0..1usize << n).map(|x| (x as u32).count_ones() % 2 == 1).collect();
3055 let r = find_structure(&truth).unwrap();
3056 assert_eq!(r.class.reconstruct(n).as_deref(), Some(&truth[..]), "the witness reproduces the function");
3057 let tampered = match r.class {
3058 CubeStructure::Affine { mut coeffs, constant } => {
3059 coeffs[0] = !coeffs[0];
3060 CubeStructure::Affine { coeffs, constant }
3061 }
3062 other => panic!("expected affine, got {other:?}"),
3063 };
3064 assert_ne!(tampered.reconstruct(n).as_deref(), Some(&truth[..]), "a tampered witness does not");
3065 }
3066
3067 #[test]
3068 fn structure_cover_peels_a_sparse_function_to_a_small_residue() {
3069 let n = 6;
3071 let truth: Vec<bool> = (0..1usize << n)
3072 .map(|x| {
3073 let b = |i: usize| x & (1usize << i) != 0;
3074 (b(0) && b(1)) ^ (b(2) && b(3)) ^ (b(4) && b(5))
3075 })
3076 .collect();
3077 let c = structure_cover(&truth).unwrap();
3078 assert_eq!(c.monomials_by_degree, vec![0, 0, 3, 0, 0, 0, 0], "only the degree-2 slice is populated");
3079 assert_eq!(c.total_monomials, 3);
3080 assert_eq!((c.residue_degree, c.residue_monomials), (2, 3), "the residue is three quadratic terms");
3081 assert_eq!(c.monomials_by_degree.iter().sum::<usize>(), c.total_monomials, "the slices sum to the whole");
3082 assert!(c.compressed, "a sparse ANF covers all 2^n corners cheaply");
3083 }
3084
3085 #[test]
3086 fn structure_cover_of_a_constant_is_the_degree_zero_slice() {
3087 let c = structure_cover(&vec![true; 8]).unwrap(); assert_eq!(c.monomials_by_degree, vec![1, 0, 0, 0], "just the constant term");
3089 assert_eq!((c.total_monomials, c.residue_degree), (1, 0));
3090 assert!(c.compressed);
3091 }
3092
3093 #[test]
3094 fn structure_cover_leaves_a_dense_high_degree_residue_for_a_pseudorandom_function() {
3095 let n = 6;
3098 let truth = pseudorandom_truth(n);
3099 let c = structure_cover(&truth).unwrap();
3100 assert_eq!(c.monomials_by_degree.iter().sum::<usize>(), c.total_monomials);
3101 assert!(c.residue_degree >= 4, "structure reaches high degree, got {}", c.residue_degree);
3102 assert!(c.total_monomials > n, "a dense ANF, not a sparse low-degree cover");
3103 assert!(!c.compressed, "the peel does not beat storing the truth table — the incompressible core");
3104 }
3105
3106 fn rotated_junta(n_free: usize) -> Vec<bool> {
3109 let h = pseudorandom_truth(n_free); (0..1usize << (n_free + 1))
3111 .map(|x| {
3112 let u0 = ((x & 1) ^ ((x >> 1) & 1)) & 1; let rest = x >> 2; h[u0 | (rest << 1)]
3115 })
3116 .collect()
3117 }
3118
3119 #[test]
3120 fn linear_structures_finds_none_in_a_pseudorandom_function() {
3121 let truth = pseudorandom_truth(6);
3123 let r = linear_structures(&truth).unwrap();
3124 assert_eq!(r.dim(), 0, "a random function has trivial linear space, got basis {:?}", r.basis);
3125 assert!(!r.is_reducible());
3126 assert!(reduce_by_invariance(&truth).is_none(), "nothing to peel");
3127 assert!(r.verify(&truth), "the empty witness trivially re-checks");
3128 }
3129
3130 #[test]
3131 fn linear_structures_peel_a_rotated_junta_the_coordinate_lenses_miss() {
3132 let n = 6;
3133 let truth = rotated_junta(5);
3134 let base = find_structure(&truth).unwrap();
3136 assert!(matches!(base.class, CubeStructure::ResistedArsenal { .. }), "base arsenal: {:?}", base.class);
3137 let ls = linear_structures(&truth).unwrap();
3139 assert!(ls.is_reducible(), "the rotated junta has a nontrivial linear space");
3140 assert_eq!(reduce_by_basis(0b11, &ls.basis), 0, "a = e0 ⊕ e1 lies in the linear space");
3141 assert!(ls.verify(&truth), "the linear-structure witness re-checks");
3142 let (red, basis) = reduce_by_invariance(&truth).expect("a rotated junta reduces");
3144 assert!(red.reduced_vars < n, "peeled at least one dimension: {} → {}", n, red.reduced_vars);
3145 assert!(red.verify(&truth, &basis), "the reduced function reproduces the original on every corner");
3146 }
3147
3148 #[test]
3149 fn affine_reduce_peels_a_residue_plus_a_linear_form() {
3150 let n = 6;
3153 let g = pseudorandom_truth(5);
3154 let truth: Vec<bool> = (0..1usize << n).map(|x| g[x >> 1] ^ (x & 1 != 0)).collect();
3155 assert!(matches!(find_structure(&truth).unwrap().class, CubeStructure::ResistedArsenal { .. }));
3156 assert!(reduce_by_invariance(&truth).is_none(), "no pure invariance to peel");
3157 let ar = affine_reduce(&truth).expect("the linear form peels off");
3158 assert!(ar.reduced_truth.len() < truth.len(), "reduced onto fewer coordinates");
3159 assert_eq!(ar.reconstruct().as_deref(), Some(&truth[..]), "h ⊕ ℓ reconstructs f exactly");
3160 }
3161
3162 #[test]
3163 fn linear_structure_witness_rejects_tampering() {
3164 let truth = rotated_junta(5);
3165 let r = linear_structures(&truth).unwrap();
3166 assert!(r.verify(&truth));
3167 let mut bad = r.clone();
3169 bad.derivative[0] = !bad.derivative[0];
3170 assert!(!bad.verify(&truth), "a tampered derivative is caught");
3171 let mut bad2 = r.clone();
3173 bad2.basis[0] ^= 0b100; assert!(!bad2.verify(&truth), "a tampered basis vector is caught");
3175 }
3176
3177 fn apply_linear_input_map(truth: &[bool], rows: &[usize]) -> Vec<bool> {
3180 (0..truth.len())
3181 .map(|x| {
3182 let ax = rows
3183 .iter()
3184 .enumerate()
3185 .fold(0usize, |acc, (i, &r)| if (r & x).count_ones() % 2 == 1 { acc | (1 << i) } else { acc });
3186 truth[ax]
3187 })
3188 .collect()
3189 }
3190
3191 #[test]
3192 fn affine_signature_is_invariant_under_a_linear_change_of_basis() {
3193 let rows = vec![0b0001usize, 0b0011, 0b0111, 0b1111];
3195 assert_eq!(gf2_echelon_basis(&rows).len(), 4, "the map must be invertible");
3196 let bent: Vec<bool> = (0..16)
3197 .map(|x| {
3198 let b = |i: usize| x & (1 << i) != 0;
3199 (b(0) && b(1)) ^ (b(2) && b(3))
3200 })
3201 .collect();
3202 for f in [bent, pseudorandom_truth(4), (0..16).map(|x: usize| (x as u32).count_ones() % 2 == 1).collect()] {
3203 let g = apply_linear_input_map(&f, &rows);
3204 assert_eq!(
3205 affine_signature(&f).unwrap().walsh_abs_distribution,
3206 affine_signature(&g).unwrap().walsh_abs_distribution,
3207 "the Walsh multiset is an affine invariant — a rotation cannot change the class"
3208 );
3209 }
3210 }
3211
3212 #[test]
3213 fn affine_signature_recognizes_bent_and_plateaued_and_reads_the_linear_dimension() {
3214 let bent: Vec<bool> = (0..16)
3216 .map(|x| {
3217 let b = |i: usize| x & (1 << i) != 0;
3218 (b(0) && b(1)) ^ (b(2) && b(3))
3219 })
3220 .collect();
3221 let sb = affine_signature(&bent).unwrap();
3222 assert!(sb.is_plateaued && sb.is_bent && sb.amplitude == Some(4));
3223 assert_eq!(sb.implied_linear_dim(), Some(0));
3224 assert_eq!(sb.implied_linear_dim(), Some(linear_structures(&bent).unwrap().dim()));
3225
3226 let pb: Vec<bool> = (0..8)
3228 .map(|x| {
3229 let b = |i: usize| x & (1 << i) != 0;
3230 (b(0) && b(1)) ^ b(2)
3231 })
3232 .collect();
3233 let sp = affine_signature(&pb).unwrap();
3234 assert!(sp.is_plateaued && !sp.is_bent && sp.amplitude == Some(4));
3235 assert_eq!(sp.implied_linear_dim(), Some(1), "amplitude 2^{{(n+k)/2}} reads off k=1");
3236 assert_eq!(sp.implied_linear_dim(), Some(linear_structures(&pb).unwrap().dim()), "the two rungs agree");
3237
3238 let rnd = pseudorandom_truth(6);
3240 let sr = affine_signature(&rnd).unwrap();
3241 assert!(!sr.is_plateaued && sr.amplitude.is_none(), "the residue has many Walsh amplitudes");
3242 assert!(sr.walsh_abs_distribution.len() >= 3, "a genuine spread of amplitudes");
3243 }
3244
3245 fn split_dense(seed_lo: u64, seed_hi: u64) -> Vec<bool> {
3248 let bit = |i: u64, s: u64| {
3249 let mut z = 0x9E37_79B9_7F4A_7C15u64.wrapping_mul(i + s);
3250 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
3251 (z ^ (z >> 27)) & 1 == 1
3252 };
3253 (0..64usize).map(|x| bit((x & 0b111) as u64, seed_lo) ^ bit(((x >> 3) & 0b111) as u64, seed_hi)).collect()
3254 }
3255
3256 #[test]
3257 fn separable_decomposition_splits_independent_blocks() {
3258 let n = 6;
3260 let truth: Vec<bool> = (0..1usize << n)
3261 .map(|x| {
3262 let b = |i: usize| x & (1usize << i) != 0;
3263 (b(0) && b(1)) ^ (b(2) && b(3)) ^ (b(4) && b(5))
3264 })
3265 .collect();
3266 let d = separable_decomposition(&truth).unwrap();
3267 assert_eq!(d.blocks, vec![vec![0, 1], vec![2, 3], vec![4, 5]], "the three pairs split apart");
3268 assert!(d.is_separable());
3269 assert!(d.table_bits() < truth.len(), "Σ 2^|B| = 12 beats 2ⁿ = 64");
3270 assert_eq!(d.reconstruct(n).as_deref(), Some(&truth[..]), "the blocks XOR back to the whole");
3271 }
3272
3273 #[test]
3274 fn separable_decomposition_peels_dense_blocks_apart() {
3275 let n = 6;
3277 let truth = split_dense(1, 999);
3278 let d = separable_decomposition(&truth).unwrap();
3279 assert!(d.is_separable(), "the two dense halves are independent blocks");
3280 for b in &d.blocks {
3281 let lo = b.iter().any(|&i| i < 3);
3282 let hi = b.iter().any(|&i| i >= 3);
3283 assert!(!(lo && hi), "no block bridges the two independent halves: {b:?}");
3284 }
3285 assert_eq!(d.reconstruct(n).as_deref(), Some(&truth[..]));
3286 }
3287
3288 #[test]
3289 fn a_connected_function_is_one_irreducible_block() {
3290 let n = 5;
3292 let truth: Vec<bool> = (0..1usize << n)
3293 .map(|x| {
3294 let b = |i: usize| x & (1usize << i) != 0;
3295 (b(0) && b(1)) ^ (b(1) && b(2)) ^ (b(2) && b(3)) ^ (b(3) && b(4))
3296 })
3297 .collect();
3298 let d = separable_decomposition(&truth).unwrap();
3299 assert_eq!(d.blocks, vec![vec![0, 1, 2, 3, 4]], "one connected block");
3300 assert!(!d.is_separable());
3301 assert_eq!(d.reconstruct(n).as_deref(), Some(&truth[..]));
3302 }
3303
3304 #[test]
3305 fn separable_reconstruct_rejects_tampering() {
3306 let truth = split_dense(7, 42);
3307 let mut d = separable_decomposition(&truth).unwrap();
3308 assert_eq!(d.reconstruct(6).as_deref(), Some(&truth[..]));
3309 d.block_truths[0][0] = !d.block_truths[0][0]; assert_ne!(d.reconstruct(6).as_deref(), Some(&truth[..]), "a tampered block is caught");
3311 }
3312
3313 fn rotation_symmetric(n: usize, seed: u64) -> Vec<bool> {
3316 let mask = (1usize << n) - 1;
3317 let rot = |x: usize| ((x << 1) | (x >> (n - 1))) & mask;
3318 (0..1usize << n)
3319 .map(|x| {
3320 let (mut m, mut y) = (x, x);
3321 for _ in 0..n {
3322 y = rot(y);
3323 m = m.min(y);
3324 }
3325 let mut z = 0x9E37_79B9_7F4A_7C15u64.wrapping_mul(m as u64 + seed);
3326 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
3327 (z ^ (z >> 27)) & 1 == 1
3328 })
3329 .collect()
3330 }
3331
3332 #[test]
3333 fn variable_symmetry_generalizes_the_symmetric_class() {
3334 let n = 4;
3337 let truth: Vec<bool> = (0..1usize << n).map(|x| (x as u32).count_ones() >= 2).collect();
3338 let s = variable_symmetry(&truth).unwrap();
3339 assert_eq!(s.order, 24, "Aut(MAJ4) = S_4");
3340 assert_eq!(s.orbit_count, n + 1, "the orbits are the weight classes");
3341 assert!(s.verify(&truth));
3342 assert_eq!(s.reconstruct(n).as_deref(), Some(&truth[..]));
3343 }
3344
3345 #[test]
3346 fn variable_symmetry_finds_rotation_symmetry_the_linear_ladder_misses() {
3347 let n = 5;
3349 let truth = rotation_symmetric(n, 12345);
3350 let s = variable_symmetry(&truth).unwrap();
3351 assert!(s.is_nontrivial() && s.order % 5 == 0, "C_5 ≤ Aut(f), order {}", s.order);
3352 assert!(s.order < 120 && s.orbit_count > n + 1, "partial symmetry: order {} orbits {}", s.order, s.orbit_count);
3355 assert!(s.orbit_count < 1 << n, "yet still compressed: {} < {}", s.orbit_count, 1 << n);
3356 assert!(s.verify(&truth));
3357 assert_eq!(s.reconstruct(n).as_deref(), Some(&truth[..]), "the orbit painting reproduces f");
3358 }
3359
3360 #[test]
3361 fn variable_symmetry_is_trivial_for_a_pseudorandom_function() {
3362 let truth = pseudorandom_truth(6);
3364 let s = variable_symmetry(&truth).unwrap();
3365 assert_eq!(s.order, 1, "a random function has no variable symmetry");
3366 assert!(!s.is_nontrivial());
3367 assert_eq!(s.orbit_count, 1 << 6, "every input is its own orbit — the residue");
3368 assert!(s.verify(&truth), "the trivial group trivially reproduces f");
3369 }
3370
3371 #[test]
3372 fn variable_symmetry_witness_rejects_tampering() {
3373 let truth = rotation_symmetric(5, 999);
3374 let mut s = variable_symmetry(&truth).unwrap();
3375 assert!(s.verify(&truth));
3376 s.orbit_values[0] = !s.orbit_values[0]; assert!(!s.verify(&truth), "a tampered orbit value is caught");
3378 let mut s2 = variable_symmetry(&truth).unwrap();
3380 if let Some(g) = s2.generators.first_mut() {
3381 g.swap(0, 1); }
3383 s2.orbit_values[1] = !s2.orbit_values[1];
3386 assert!(!s2.verify(&truth));
3387 }
3388
3389 #[test]
3390 fn deep_finder_routes_each_function_to_its_tightest_axis() {
3391 let sep = split_dense(1, 999);
3394 assert_eq!(find_structure_deep(&sep).unwrap().winner, "separable");
3395
3396 let rot = rotation_symmetric(5, 12345);
3398 assert_eq!(find_structure_deep(&rot).unwrap().winner, "permutation");
3399
3400 let rj = rotated_junta(5);
3402 assert_eq!(find_structure_deep(&rj).unwrap().winner, "linear-invariance");
3403
3404 let mono: Vec<bool> = (0..16usize).map(|x| (x & 1 != 0) && (x & 2 != 0) && (x & 4 != 0)).collect();
3406 assert_eq!(find_structure_deep(&mono).unwrap().winner, "coordinate:low-degree");
3407
3408 let parity: Vec<bool> = (0..16usize).map(|x| (x as u32).count_ones() % 2 == 1).collect();
3411 assert_eq!(find_structure_deep(&parity).unwrap().winner, "linear-invariance");
3412
3413 let rnd = pseudorandom_truth(6);
3415 let d = find_structure_deep(&rnd).unwrap();
3416 assert_eq!(d.winner, "residue");
3417 assert!(!d.compressed && d.description_bits == d.raw_bits);
3418 }
3419
3420 #[test]
3421 fn structure_tree_recurses_to_a_nested_fixed_point() {
3422 let n = 6;
3425 let truth: Vec<bool> = (0..1usize << n)
3426 .map(|x| {
3427 let b = |i: usize| x & (1usize << i) != 0;
3428 ((b(0) ^ b(1)) && b(2)) ^ (b(3) && b(4) && b(5))
3429 })
3430 .collect();
3431 let tree = structure_tree(&truth).unwrap();
3432 match &tree {
3434 StructureTree::Separable { blocks, .. } => {
3435 assert_eq!(blocks.len(), 2, "the two halves split apart");
3436 assert!(
3438 blocks.iter().any(|(_, t)| matches!(t, StructureTree::LinearReduced { .. })),
3439 "a block peels deeper: {:?}",
3440 blocks.iter().map(|(_, t)| t.depth()).collect::<Vec<_>>()
3441 );
3442 }
3443 other => panic!("expected a separable top level, got {other:?}"),
3444 }
3445 assert!(tree.depth() >= 2, "a genuinely nested decomposition, depth {}", tree.depth());
3446 assert_eq!(tree.reconstruct().as_deref(), Some(&truth[..]), "the whole tree reconstructs f exactly");
3447 assert!(tree.total_description_bits() < truth.len(), "and it is a real compression of the cube");
3448 }
3449
3450 #[test]
3451 fn structure_tree_bottoms_out_at_the_residue() {
3452 let truth = pseudorandom_truth(6);
3453 let tree = structure_tree(&truth).unwrap();
3454 assert!(matches!(tree, StructureTree::Residue { .. }), "no axis peels a random function");
3455 assert_eq!(tree.depth(), 0);
3456 assert_eq!(tree.reconstruct().as_deref(), Some(&truth[..]), "the residue is stored and reproduced raw");
3457 }
3458
3459 #[test]
3460 fn census_residue_is_certified_nonempty_by_counting() {
3461 for nv in 2..=4u32 {
3464 let census = boolean_function_census(nv as usize).unwrap();
3465 assert!(census.residue > 0, "the arsenal empirically leaves a residue at n={nv}");
3466 let cert = certified_incompressible_function_exists(nv).expect("a counting certificate exists");
3467 assert!(crate::pigeonhole::check_counting_cert(&cert), "the counting certificate re-checks");
3468 assert_eq!(cert.pigeons, 1u128 << (1u128 << nv), "one pigeon per Boolean function");
3470 assert_eq!(cert.holes, (1u128 << (1u128 << nv)) - 1, "one hole per shorter program");
3471 }
3472 assert!(certified_incompressible_function_exists(6).is_some());
3474 assert!(certified_incompressible_function_exists(7).is_none());
3475 }
3476
3477 #[test]
3478 fn sampled_census_shows_the_residue_approaching_one() {
3479 let fracs: Vec<f64> =
3482 (4..=8).map(|n| sampled_boolean_census(n, 1200, 12345).unwrap().residue_fraction()).collect();
3483 assert!((fracs[0] - 0.62).abs() < 0.08, "n=4 sample ≈ exhaustive census: got {}", fracs[0]);
3485 for w in fracs.windows(2) {
3487 assert!(w[1] >= w[0] - 0.01, "the residue fraction climbs toward 1 with n: {fracs:?}");
3488 }
3489 assert!(*fracs.last().unwrap() > 0.99, "by n=8 almost every function is incompressible: {fracs:?}");
3490 }
3491
3492 #[test]
3493 fn boolean_census_maps_the_space_and_the_residue_grows() {
3494 let (c2, c3, c4) = (
3496 boolean_function_census(2).unwrap(),
3497 boolean_function_census(3).unwrap(),
3498 boolean_function_census(4).unwrap(),
3499 );
3500 for c in [&c2, &c3, &c4] {
3501 assert_eq!(c.total, 1usize << (1usize << c.num_vars), "2^{{2ⁿ}} functions");
3502 assert_eq!(c.by_winner.iter().map(|(_, k)| *k).sum::<usize>(), c.total, "every function is classified");
3503 assert_eq!(c.compressed + c.residue, c.total);
3504 }
3505 let (f3, f4) = (c3.residue as f64 / c3.total as f64, c4.residue as f64 / c4.total as f64);
3508 assert!(f4 > f3 + 0.3, "the residue fraction jumps with n (n=3 → {f3:.3}, n=4 → {f4:.3})");
3509 assert!(c4.residue * 2 > c4.total, "by n=4 the incompressible residue is already the majority");
3510 for axis in
3513 ["affine", "coordinate:constant", "coordinate:low-degree", "linear-invariance", "permutation", "separable"]
3514 {
3515 assert!(c4.by_winner.iter().any(|(w, k)| *w == axis && *k > 0), "axis {axis} wins somewhere");
3516 }
3517 }
3518
3519 #[test]
3520 fn description_bound_covers_boolean_functions_in_the_unified_framework() {
3521 let structured: Vec<bool> = (0..64usize)
3523 .map(|x| {
3524 let b = |i: usize| x & (1usize << i) != 0;
3525 ((b(0) ^ b(1)) && b(2)) ^ (b(3) && b(4) && b(5))
3526 })
3527 .collect();
3528 let db = DescriptionBound::of_boolean(&structured).unwrap();
3529 assert!(matches!(db.descriptor, Descriptor::BooleanFunction { .. }));
3530 assert!(db.verify(), "the tree decode witness re-checks inside DescriptionBound");
3531 assert!(db.bytes < structured.len() / 8 + 1, "K̄ beats the packed truth table, {} bytes", db.bytes);
3532
3533 let mut bad = DescriptionBound::of_boolean(&structured).unwrap();
3535 bad.bytes += 1;
3536 assert!(!bad.verify(), "an inconsistent bound is rejected");
3537
3538 let seq = DescriptionBound::of_int_seq(&(0..100).map(|i| 3 * i + 1).collect::<Vec<_>>());
3540 assert!(seq.verify());
3541 }
3542
3543 #[test]
3544 fn kolmogorov_bound_certifies_compression_and_the_honest_residue() {
3545 let structured: Vec<bool> = (0..64usize)
3547 .map(|x| {
3548 let b = |i: usize| x & (1usize << i) != 0;
3549 ((b(0) ^ b(1)) && b(2)) ^ (b(3) && b(4) && b(5))
3550 })
3551 .collect();
3552 let kb = kolmogorov_bound(&structured).unwrap();
3553 assert!(kb.is_compressed() && kb.ratio() < 1.0, "structure gives K̄ < 2ⁿ, ratio {}", kb.ratio());
3554 assert!(kb.verify(&structured), "the decode witness re-checks the bound");
3555
3556 let rnd = pseudorandom_truth(6);
3558 let kr = kolmogorov_bound(&rnd).unwrap();
3559 assert_eq!(kr.bits, kr.raw_bits, "no axis beats the truth table — the residue, K̄ = 2ⁿ");
3560 assert!(!kr.is_compressed() && kr.verify(&rnd));
3561
3562 let mut bad = kolmogorov_bound(&structured).unwrap();
3564 bad.tree = StructureTree::Residue { truth: rnd.clone() };
3565 assert!(!bad.verify(&structured), "a witness for a different function does not verify");
3566 }
3567
3568 #[test]
3569 fn sbox_profile_flags_a_linear_sbox_as_weak() {
3570 let id: Vec<u32> = (0..16u32).collect();
3572 let p = sbox_profile(&id, 4).unwrap();
3573 assert!(p.is_affine && p.min_degree == 1, "a linear S-box has affine components");
3574 assert_eq!(p.differential_uniformity, 16, "S(x⊕a)⊕S(x)=a is constant — maximally weak");
3575 assert_eq!(p.linearity, 16, "a perfect linear approximation exists");
3576 assert!(p.is_bijective && !p.is_apn);
3577 }
3578
3579 #[test]
3580 fn sbox_profile_certifies_the_aes_sbox_is_strong() {
3581 #[rustfmt::skip]
3582 const AES: [u8; 256] = [
3583 0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
3584 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
3585 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
3586 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
3587 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
3588 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
3589 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
3590 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
3591 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
3592 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
3593 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
3594 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
3595 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
3596 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
3597 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
3598 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16,
3599 ];
3600 let sbox: Vec<u32> = AES.iter().map(|&b| b as u32).collect();
3601 let p = sbox_profile(&sbox, 8).unwrap();
3602 assert_eq!(p.differential_uniformity, 4, "AES S-box differential uniformity");
3604 assert_eq!(p.linearity, 32, "AES S-box linearity (nonlinearity 128 − 16 = 112)");
3605 assert_eq!(p.min_degree, 7, "AES S-box algebraic degree");
3606 assert!(p.is_bijective && !p.is_affine && !p.is_apn, "strong, invertible, not linear, not APN");
3607 }
3608
3609 fn apply_input_linear(sbox: &[u32], rows: &[usize]) -> Vec<u32> {
3611 (0..sbox.len())
3612 .map(|x| {
3613 let ax = rows
3614 .iter()
3615 .enumerate()
3616 .fold(0usize, |acc, (i, &r)| if (r & x).count_ones() % 2 == 1 { acc | (1 << i) } else { acc });
3617 sbox[ax]
3618 })
3619 .collect()
3620 }
3621 fn apply_output_linear(sbox: &[u32], rows: &[usize]) -> Vec<u32> {
3623 sbox.iter()
3624 .map(|&y| {
3625 rows.iter().enumerate().fold(0u32, |acc, (i, &r)| {
3626 if (r & y as usize).count_ones() % 2 == 1 { acc | (1 << i) } else { acc }
3627 })
3628 })
3629 .collect()
3630 }
3631
3632 #[test]
3633 fn sbox_spectra_are_affine_invariants() {
3634 let present: Vec<u32> = vec![0xC, 5, 6, 0xB, 9, 0, 0xA, 0xD, 3, 0xE, 0xF, 8, 4, 7, 1, 2];
3636 let rows = vec![0b0001usize, 0b0011, 0b0111, 0b1111]; assert_eq!(gf2_echelon_basis(&rows).len(), 4);
3638 let base = sbox_spectra(&present, 4).unwrap();
3639 let g = apply_input_linear(&present, &rows);
3640 assert_eq!(sbox_spectra(&g, 4).unwrap(), base, "spectra invariant under an input linear change");
3641 let h = apply_output_linear(&present, &rows);
3642 assert_eq!(sbox_spectra(&h, 4).unwrap(), base, "spectra invariant under an output linear change");
3643 }
3644
3645 #[test]
3646 fn sbox_spectra_distinguish_inequivalent_boxes() {
3647 let id: Vec<u32> = (0..16u32).collect();
3648 let present: Vec<u32> = vec![0xC, 5, 6, 0xB, 9, 0, 0xA, 0xD, 3, 0xE, 0xF, 8, 4, 7, 1, 2];
3649 assert_ne!(
3650 sbox_spectra(&id, 4).unwrap().differential_spectrum,
3651 sbox_spectra(&present, 4).unwrap().differential_spectrum,
3652 "a linear box and a nonlinear box cannot be affine-equivalent"
3653 );
3654 }
3655
3656 #[test]
3657 fn sbox_profile_recognizes_an_apn_gold_function() {
3658 fn gf8_mul(mut a: u32, mut b: u32) -> u32 {
3660 let mut p = 0;
3661 for _ in 0..3 {
3662 if b & 1 == 1 {
3663 p ^= a;
3664 }
3665 b >>= 1;
3666 let hi = a & 0b100;
3667 a = (a << 1) & 0b111;
3668 if hi != 0 {
3669 a ^= 0b011; }
3671 }
3672 p
3673 }
3674 let sbox: Vec<u32> = (0..8u32).map(|x| gf8_mul(gf8_mul(x, x), x)).collect();
3675 let p = sbox_profile(&sbox, 3).unwrap();
3676 assert_eq!(p.differential_uniformity, 2, "the Gold function is APN");
3677 assert!(p.is_apn && p.is_bijective, "an APN permutation on an odd number of bits");
3678 assert_eq!(boomerang_uniformity(&sbox), Some(2), "APN ⟹ boomerang uniformity 2");
3680 assert_eq!(sbox_full_audit(&sbox, 3), Some(SboxVerdict::Quadratic), "optimal differential, weak algebra");
3683 }
3684
3685 #[test]
3686 fn sbox_audit_flags_weakness_and_clears_aes() {
3687 let id: Vec<u32> = (0..16u32).collect();
3689 assert_eq!(sbox_full_audit(&id, 4), Some(SboxVerdict::Affine));
3690
3691 #[rustfmt::skip]
3692 const AES: [u8; 256] = [
3693 0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
3694 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
3695 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
3696 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
3697 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
3698 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
3699 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
3700 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
3701 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
3702 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
3703 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
3704 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
3705 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
3706 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
3707 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
3708 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16,
3709 ];
3710 let aes: Vec<u32> = AES.iter().map(|&b| b as u32).collect();
3711 assert_eq!(
3713 sbox_full_audit(&aes, 8),
3714 Some(SboxVerdict::NoStructuralWeaknessFound {
3715 differential_uniformity: 4,
3716 linearity: 32,
3717 min_degree: 7,
3718 boomerang_uniformity: Some(6),
3719 }),
3720 "AES resists the structural arsenal — not broken"
3721 );
3722 }
3723
3724 #[test]
3725 fn boomerang_uniformity_matches_the_aes_published_value() {
3726 #[rustfmt::skip]
3727 const AES: [u8; 256] = [
3728 0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
3729 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
3730 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
3731 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
3732 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
3733 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
3734 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
3735 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
3736 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
3737 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
3738 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
3739 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
3740 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
3741 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
3742 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
3743 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16,
3744 ];
3745 let sbox: Vec<u32> = AES.iter().map(|&b| b as u32).collect();
3746 assert_eq!(boomerang_uniformity(&sbox), Some(6), "AES S-box boomerang uniformity (Cid et al. 2018)");
3747 }
3748
3749 #[test]
3750 fn coverage_census_shows_covered_families_and_the_uncovered_residue() {
3751 use logicaffeine_base::BigInt;
3752 let n = 300;
3753
3754 let mut structured: Vec<Vec<bool>> = Vec::new();
3756 structured.push(describe::lfsr_generate(
3757 &[false, false, true, false, false, false, true],
3758 &[true, false, true, true, false, false, true],
3759 n,
3760 )); structured.push(describe::fcsr_generate(&BigInt::from_i64(7), &BigInt::from_i64(19), n)); structured.push(describe::fcsr_generate(&BigInt::from_i64(11), &BigInt::from_i64(23), n));
3763 structured.push((0..n).map(|i| [true, false, false, true, true, false][i % 6]).collect()); let sc = census(&structured);
3765 assert_eq!(sc.uncovered, 0, "every structured sequence is covered; map = {:?}", sc.by_lens);
3766
3767 let random: Vec<Vec<bool>> = (0..40u64)
3769 .map(|i| {
3770 let mut st = 0x9E37_79B9_7F4A_7C15u64.wrapping_mul(i.wrapping_add(1));
3771 (0..n)
3772 .map(|_| {
3773 st = st.wrapping_add(0x9E37_79B9_7F4A_7C15);
3774 let mut z = st;
3775 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
3776 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
3777 z ^= z >> 31;
3778 z & 1 == 1
3779 })
3780 .collect()
3781 })
3782 .collect();
3783 assert_eq!(census(&random).covered, 0, "cryptographic-random sequences are the uncovered residue");
3784
3785 let ex = exhaustive_coverage(14);
3787 eprintln!(
3788 "exhaustive len=14: covered {}/{} = {:.2}%, residue {} = {:.2}%",
3789 ex.covered,
3790 ex.total,
3791 100.0 * ex.covered as f64 / ex.total as f64,
3792 ex.uncovered,
3793 100.0 * ex.uncovered as f64 / ex.total as f64,
3794 );
3795 assert!(ex.covered * 2 < ex.total, "the covered families are a minority sliver — most of the space is residue");
3796 }
3797
3798 #[test]
3799 fn rsa_structural_audit_breaks_weak_moduli_and_certifies_the_ceiling() {
3800 use crate::factor;
3801 use logicaffeine_base::BigInt;
3802 let big = |s: &str| BigInt::parse_decimal(s).unwrap();
3803
3804 let p = factor::next_prime(&big("1000000000000000000"));
3806 let q = factor::next_prime(&p.add(&BigInt::from_i64(2)));
3807 let n = p.mul(&q);
3808 match rsa_structural_audit(&n) {
3809 RsaStrength::Factored { p: a, q: b, method } => {
3810 assert!(factor::verify_factorization(&n, &a, &b), "the witness re-multiplies to N");
3811 assert!(method.contains("Fermat"), "close primes are caught by Fermat, got {method}");
3812 }
3813 RsaStrength::SoundAgainstStructuralAttacks => panic!("close primes must be broken"),
3814 }
3815
3816 let p = factor::next_prime(&big("1000000000000000000"));
3818 let q = factor::next_prime(&big("9000000000000000000"));
3819 let n = p.mul(&q);
3820 assert_eq!(
3821 rsa_structural_audit(&n),
3822 RsaStrength::SoundAgainstStructuralAttacks,
3823 "a sound modulus is the number-theoretic incompressible residue"
3824 );
3825 }
3826
3827 #[test]
3828 fn algebraic_attack_stops_at_the_ceiling_on_random_bytes() {
3829 let random = splitmix_bytes(120, 0xdead_beef_cafe_babe);
3832 assert!(
3833 algebraic_attack_on_bytes(&random, 2, 20).is_none(),
3834 "no degree-2 register regenerates a cryptographic keystream — the Chaitin ceiling"
3835 );
3836 }
3837
3838 #[test]
3839 fn random_key_material_is_incompressible_in_class() {
3840 let key = splitmix_bytes(400, 0x1234_5678_9abc_def0);
3842 match assess_key_material(&key) {
3843 CryptoStrength::IncompressibleInClass { ratio } => {
3844 assert!(ratio >= 0.95, "random bytes are ~incompressible, got ratio {ratio}");
3845 }
3846 CryptoStrength::Weak { ratio, .. } => panic!("random key wrongly flagged weak, ratio {ratio}"),
3847 }
3848 }
3849
3850 #[test]
3851 fn incompressibility_ratio_separates_structure_from_randomness() {
3852 let structured: Vec<u8> = (0..300).map(|i| (i % 8) as u8).collect(); let random = splitmix_bytes(300, 0x9e37_79b9_7f4a_7c15);
3854 assert!(
3855 incompressibility_ratio(&structured) < 0.3 * incompressibility_ratio(&random),
3856 "structured key ({}) must be far more compressible than random ({})",
3857 incompressibility_ratio(&structured),
3858 incompressibility_ratio(&random),
3859 );
3860 }
3861
3862 #[test]
3863 fn classify_recognizes_the_ordered_to_random_spectrum() {
3864 let counter: Vec<u8> = (0..200u16).map(|i| i as u8).collect();
3866 assert_eq!(classify_bytes(&counter).class, CompressibilityClass::Generated);
3867 let periodic: Vec<u8> = (0..300).map(|i| (i % 8) as u8).collect();
3869 assert_eq!(classify_bytes(&periodic).class, CompressibilityClass::Periodic);
3870 let random = splitmix_bytes(400, 0xDEAD_BEEF_CAFE_1234);
3872 assert_eq!(classify_bytes(&random).class, CompressibilityClass::Incompressible);
3873 assert!(classify_bytes(&counter).ratio < 0.2, "a counter is nearly free to describe");
3875 assert!(classify_bytes(&periodic).ratio < 0.2, "a short period is nearly free to describe");
3876 assert!(classify_bytes(&random).ratio >= 0.95, "random bytes cost ~their full length");
3877 }
3878
3879 #[test]
3880 fn classify_recognizes_fibonacci_as_a_generator() {
3881 let mut fib = vec![0i64, 1];
3885 while fib.len() < 60 {
3886 let n = fib.len();
3887 fib.push(fib[n - 1].wrapping_add(fib[n - 2]));
3888 }
3889 let report = classify_int_seq(&fib);
3890 assert_eq!(report.class, CompressibilityClass::Generated, "Fibonacci is a generator, not random");
3891 assert!(report.ratio < 0.1, "60 Fibonacci terms collapse to a handful, ratio {}", report.ratio);
3892 }
3893
3894 #[test]
3895 fn classify_text_places_inputs_on_the_compressibility_spectrum() {
3896 let repeated = "the quick brown fox jumps over the lazy dog. ".repeat(30);
3898 let rep = classify_text(&repeated);
3899 assert_ne!(rep.class, CompressibilityClass::Incompressible, "repeated text has structure");
3900 assert!(rep.ratio < 0.5, "repeated text compresses well, ratio {}", rep.ratio);
3901 let random_bytes = splitmix_bytes(400, 0x0102_0304_0506_0708);
3903 let rand_ratio = classify_bytes(&random_bytes).ratio;
3904 assert_eq!(classify_bytes(&random_bytes).class, CompressibilityClass::Incompressible);
3905 let ascii: String =
3909 splitmix_bytes(400, 0x0a0b_0c0d_0e0f_1011).iter().map(|&b| char::from(33 + b % 94)).collect();
3910 let ascii_ratio = classify_text(&ascii).ratio;
3911 assert!(
3912 rep.ratio < ascii_ratio && ascii_ratio < rand_ratio,
3913 "spectrum: repetition ({}) < narrow-alphabet text ({ascii_ratio}) < full randomness ({rand_ratio})",
3914 rep.ratio,
3915 );
3916 }
3917
3918 #[test]
3919 fn structural_bound_certifies_symmetry_and_realizes_compression() {
3920 let (cnf, _) = crate::families::php(6);
3926 let gens = crate::hypercube::php_perm_symmetries(6);
3927 let sb = structural_bound(cnf.num_vars, &cnf.clauses, &gens).expect("php is symmetric");
3928 assert!(sb.verify(), "the group description must re-check: automorphisms + full reconstruction");
3929 assert!(sb.group_entropy_bits > 0.0, "a symmetric formula has positive symmetry-entropy");
3930 assert!(sb.best_bytes() <= sb.whole.bytes, "the certificate never worsens the upper bound");
3931 assert!(sb.is_compression(), "at scale, PHP compresses via its symmetry group (group < flat)");
3932 assert_eq!(sb.best_bytes(), sb.group_bytes(), "the group description is the better bound here");
3933 }
3934
3935 #[test]
3936 fn structural_bound_declines_a_non_automorphism() {
3937 let (cnf, _) = crate::families::php(3);
3939 let mut imgs: Vec<Lit> = (0..cnf.num_vars).map(|v| Lit::pos(v as Var)).collect();
3940 imgs[0] = Lit::pos(1); let bogus = Perm::from_images(imgs);
3942 assert!(structural_bound(cnf.num_vars, &cnf.clauses, &[bogus]).is_none());
3943 }
3944
3945 fn xor_gadget(vars: &[usize], rhs: bool) -> Vec<Vec<Lit>> {
3948 let k = vars.len();
3949 let target = if rhs { 0 } else { 1 }; let mut clauses = Vec::new();
3951 for mask in 0u32..(1u32 << k) {
3952 if (mask.count_ones() % 2) as u32 == target {
3953 let clause = vars
3954 .iter()
3955 .enumerate()
3956 .map(|(i, &v)| Lit::new(v as Var, mask & (1 << i) == 0))
3957 .collect();
3958 clauses.push(clause);
3959 }
3960 }
3961 clauses
3962 }
3963
3964 #[test]
3965 fn linear_rigidity_kernel_is_rechecked() {
3966 let mut clauses = xor_gadget(&[0, 1, 2], true);
3970 clauses.extend(xor_gadget(&[1, 2, 3], true));
3971 let cert = certify_linear_rigidity(4, &clauses).expect("has parity structure");
3972 assert_eq!(cert.rank, 2, "two independent parity rows");
3973 assert_eq!(cert.solution_count_log2, 2, "2-dimensional kernel ⇒ 2² solutions");
3974 assert_eq!(cert.kernel_basis.len(), 2);
3975 assert_eq!(cert.kernel_basis.len(), cert.num_vars - cert.rank, "rank–nullity");
3976 assert!(check_linear_rigidity(&cert, &clauses), "the exposed linear structure re-checks");
3977 }
3978
3979 #[test]
3980 fn linear_rigidity_rejects_tampered_kernel() {
3981 let mut clauses = xor_gadget(&[0, 1, 2], true);
3982 clauses.extend(xor_gadget(&[1, 2, 3], true));
3983 let mut cert = certify_linear_rigidity(4, &clauses).expect("has parity structure");
3984 assert!(check_linear_rigidity(&cert, &clauses));
3985 cert.kernel_basis[0][0] ^= true;
3987 assert!(!check_linear_rigidity(&cert, &clauses), "a non-solution kernel vector is rejected");
3988 }
3989
3990 #[test]
3991 fn certify_linear_rigidity_declines_without_parity() {
3992 let clauses = vec![vec![Lit::pos(0), Lit::pos(1)], vec![Lit::neg(0), Lit::pos(2)]];
3994 assert!(certify_linear_rigidity(3, &clauses).is_none());
3995 }
3996
3997 #[test]
3998 fn linear_structure_certified_at_par32_scale() {
3999 let (_eqs, cnf, _) = crate::families::tseitin_expander(60, 0x51A7);
4002 assert!(cnf.num_vars > 64, "the par32-scale regime, beyond gf2's u64 rows");
4003 assert!(certify_linear_rigidity(cnf.num_vars, &cnf.clauses).is_none());
4005 let cert = certify_linear_structure(cnf.num_vars, &cnf.clauses).expect("has parity structure");
4007 assert!(cert.rank > 0 && cert.num_xor_eqs > 0, "a non-trivial recovered parity system");
4008 assert!(check_linear_structure(&cert, &cnf.clauses), "rank + kernel dimension re-check");
4009 let mut bad = cert.clone();
4011 bad.rank += 1;
4012 assert!(!check_linear_structure(&bad, &cnf.clauses), "an inflated rank is rejected");
4013 }
4014
4015 #[test]
4016 fn certify_linear_structure_declines_without_parity() {
4017 let clauses = vec![vec![Lit::pos(0), Lit::pos(1)], vec![Lit::neg(0), Lit::pos(2)]];
4018 assert!(certify_linear_structure(3, &clauses).is_none());
4019 }
4020
4021 #[test]
4022 fn linear_shortcut_verdict_reports_rigidity_with_a_rechecking_certificate() {
4023 let mut small = xor_gadget(&[0, 1, 2], true);
4026 small.extend(xor_gadget(&[1, 2, 3], true));
4027 match linear_shortcut_verdict(4, &small) {
4028 LinearShortcut::None { rigidity, structure } => {
4029 assert!(check_linear_structure(&structure, &small));
4030 let rig = rigidity.expect("≤64 vars ⇒ an explicit kernel basis is available");
4031 assert!(check_linear_rigidity(&rig, &small));
4032 }
4033 LinearShortcut::NoLinearStructure => panic!("the parity system has linear structure"),
4034 }
4035
4036 let (_e, cnf, _) = crate::families::tseitin_expander(60, 0x51A7);
4039 assert!(cnf.num_vars > 64);
4040 match linear_shortcut_verdict(cnf.num_vars, &cnf.clauses) {
4041 LinearShortcut::None { rigidity, structure } => {
4042 assert!(rigidity.is_none(), "past the u64 budget the explicit basis is withheld");
4043 assert!(check_linear_structure(&structure, &cnf.clauses));
4044 }
4045 LinearShortcut::NoLinearStructure => panic!("the Tseitin expander has linear structure"),
4046 }
4047
4048 let plain = vec![vec![Lit::pos(0), Lit::pos(1)]];
4050 assert!(matches!(linear_shortcut_verdict(2, &plain), LinearShortcut::NoLinearStructure));
4051 }
4052
4053 #[test]
4054 fn incompressibility_gate_fires_on_rigid_linear_and_declines_on_symmetric() {
4055 let (php, _) = crate::families::php(4);
4058 assert!(incompressibility_gate(php.num_vars, &php.clauses).is_none(), "PHP is symmetric → decline");
4059 assert!(incompressibility_gate(3, &[vec![Lit::pos(0), Lit::pos(1)]]).is_none());
4061 let (_e, tseitin, _) = crate::families::tseitin_expander(20, 0x51A7);
4064 assert!(certify_linear_structure(tseitin.num_vars, &tseitin.clauses).is_some(), "has parity structure");
4065 assert!(incompressibility_gate(tseitin.num_vars, &tseitin.clauses).is_none(), "but not rigid → decline");
4066
4067 let mut fired = false;
4072 for seed in [0x51A7u64, 0xBEEF, 0x1234, 0xF00D, 0xCAFE, 0xABCD, 0x9E37, 0x2718] {
4073 let base = crate::families::random_3sat(12, 40, seed);
4074 let mut clauses = base.clauses.clone();
4075 clauses.extend(xor_gadget(&[0, 1, 2], true));
4076 if let Some(cert) = incompressibility_gate(base.num_vars, &clauses) {
4077 assert!(check_linear_structure(&cert, &clauses), "the attached structure cert re-checks");
4078 fired = true;
4079 break;
4080 }
4081 }
4082 assert!(fired, "a rigid random-3SAT + an XOR gadget is linear AND rigid — the gate must fire");
4083 }
4084
4085 #[test]
4086 fn incompressibility_lemma_is_certified_by_counting() {
4087 for n in 1..=100u32 {
4090 let cert = incompressible_string_exists(n).expect("2ⁿ strings > 2ⁿ − 1 shorter programs");
4091 assert_eq!(cert.pigeons, 1u128 << n, "2ⁿ strings of length n");
4092 assert_eq!(cert.holes, (1u128 << n) - 1, "2ⁿ − 1 programs shorter than n");
4093 assert!(crate::pigeonhole::check_counting_cert(&cert), "the counting refutation re-checks");
4094 }
4095 let c = incompressible_string_exists(10).unwrap();
4097 assert_eq!((c.pigeons, c.holes), (1024, 1023));
4098 assert!(incompressible_string_exists(0).is_none());
4100 assert!(incompressible_string_exists(200).is_none());
4101 }
4102
4103 #[test]
4104 fn budget_refuses_oversized_gaussian_fail_closed() {
4105 let mut clauses = xor_gadget(&[0, 1, 2], true);
4108 clauses.extend(xor_gadget(&[1, 2, 3], true));
4109 let tiny = Budget { max_gaussian_dim: 2 };
4110 assert_eq!(
4111 certify_linear_rigidity_within(4, &clauses, &tiny),
4112 Err(Refusal::OverBudgetGaussian { dim: 4, cap: 2 })
4113 );
4114 let cert = certify_linear_rigidity_within(4, &clauses, &Budget::standard()).expect("within budget");
4116 assert!(check_linear_rigidity(&cert, &clauses));
4117 let plain = vec![vec![Lit::pos(0), Lit::pos(1)]];
4119 assert_eq!(
4120 certify_linear_rigidity_within(2, &plain, &Budget::standard()),
4121 Err(Refusal::NoLinearStructure)
4122 );
4123 }
4124
4125 #[test]
4126 fn structural_bound_rejects_tampered_generators() {
4127 let (cnf, _) = crate::families::php(3);
4128 let gens = crate::hypercube::php_perm_symmetries(3);
4129 let mut sb = structural_bound(cnf.num_vars, &cnf.clauses, &gens).expect("php is symmetric");
4130 assert!(sb.verify());
4131 if let Descriptor::IntSeq { encoded } = &mut sb.gens.descriptor {
4133 let mid = encoded.len() / 2;
4134 encoded[mid] ^= 0xff;
4135 }
4136 assert!(!sb.verify(), "a tampered generator description must be rejected");
4137 }
4138}