logicaffeine_kernel/field_algebra.rs
1//! Kernel algebra for the prime field 𝔽_q of ML-KEM / ML-DSA — the certified arithmetic
2//! substrate (F2 seed).
3//!
4//! Every identity here is discharged by the kernel's OWN decision procedures — the [`ring`]
5//! canonicalizer for polynomial identities (which hold for ALL values, not a ≤16-bit
6//! sample), and Fourier-Motzkin [`lia`] for the modular-reduction range bounds — with no Z3.
7//! This is the layer the NTT's correctness and its symmetry-derived *speed* rewrites build
8//! on: a rewrite that cuts multiplies (or collapses a symmetric butterfly) is only safe once
9//! the kernel certifies it computes the same polynomial.
10//!
11//! [`ring`]: crate::ring
12//! [`lia`]: crate::lia
13
14use crate::lia::{fourier_motzkin_unsat, Constraint, LinearExpr, Rational};
15use crate::ring::Polynomial;
16
17/// Kyber / ML-KEM's modulus — prime, so ℤ/qℤ is the field 𝔽_q.
18pub const KYBER_Q: i64 = 3329;
19
20/// Gauss's three-multiplication identity — the symmetry that turns a 4-multiply bilinear
21/// form `ad + bc` into 3 multiplies `(a+b)(c+d) − ac − bd`. It is the archetypal
22/// "spend an add to save a multiply" rewrite behind fast complex / polynomial / NTT-butterfly
23/// multiplication. Certified as a polynomial identity over any commutative ring by the
24/// kernel's `ring` canonicalizer, so a codegen pass may apply it knowing it is sound.
25pub fn gauss_three_multiply_identity() -> bool {
26 let a = Polynomial::var(0);
27 let b = Polynomial::var(1);
28 let c = Polynomial::var(2);
29 let d = Polynomial::var(3);
30 let lhs = a.mul(&d).add(&b.mul(&c)); // ad + bc — 4 multiplies, naive
31 let ac = a.mul(&c);
32 let bd = b.mul(&d);
33 let rhs = a.add(&b).mul(&c.add(&d)).sub(&ac).sub(&bd); // (a+b)(c+d) − ac − bd — 3 multiplies
34 lhs.canonical_eq(&rhs)
35}
36
37/// A constraint `coeff·a + d OP 0` over the single variable `a` (de Bruijn 0).
38fn con(coeff: i64, d: i64, strict: bool) -> Constraint {
39 let expr = LinearExpr::constant(Rational::from_i64(d))
40 .add(&LinearExpr::var(0).scale(&Rational::from_i64(coeff)));
41 Constraint { expr, strict }
42}
43
44/// Certify that conditional-subtract reduction keeps a modular-ADD result in range. After an
45/// addition in 𝔽_q the value `a` lies in `[0, 2q)`; the reduction is
46/// `csub(a) = if a ≥ q { a − q } else { a }`, and the result must lie in `[0, q)`. The two
47/// non-trivial bounds (the `a ≥ q` branch) are discharged by Fourier-Motzkin: a goal is valid
48/// iff its negation's constraints are UNSAT.
49pub fn conditional_subtract_in_range(q: i64) -> bool {
50 // Lower bound `a − q ≥ 0`: hyp `a ≥ q` (−a + q ≤ 0) ∧ ¬goal `a − q < 0` → UNSAT.
51 let lower = fourier_motzkin_unsat(&[con(-1, q, false), con(1, -q, true)]);
52 // Upper bound `a − q < q` (a − 2q < 0): hyp `a < 2q` (a − 2q < 0) ∧ ¬goal `a − 2q ≥ 0`
53 // (−a + 2q ≤ 0) → UNSAT.
54 let upper = fourier_motzkin_unsat(&[con(1, -2 * q, true), con(-1, 2 * q, false)]);
55 lower && upper
56}
57
58/// Montgomery radix `R = 2¹⁶` and `qinv' = −q⁻¹ mod R = 3327`, with the exact integer cofactor
59/// `(qinv'·q + 1) / R = 169`. These are the constants of the division-free reduction
60/// `redc(x) = (x + ((x·qinv') mod R)·q) / R` used by the (SIMD) ML-KEM NTT.
61pub const MONT_R: i64 = 65536;
62pub const NEG_QINV_MOD_R: i64 = 3327;
63const MONT_COFACTOR: i64 = 169; // (NEG_QINV_MOD_R·KYBER_Q + 1) / MONT_R
64
65/// Certify the Montgomery DIVISIBILITY: `x + (x·qinv')·q = x·169·R`, i.e. the reduction's
66/// numerator (with the un-reduced `lo = x·qinv'`) is an EXACT multiple of `R` — so `redc`'s
67/// `/ R` is an exact shift, never a rounding division. This is what makes `qinv' = 3327` the
68/// RIGHT constant (`qinv'·q ≡ −1 mod R`): the kernel `ring` canonicalizer reduces both sides to
69/// `x·11075584`, certifying it for ALL `x`, not a sampled width.
70pub fn montgomery_reduction_divisibility() -> bool {
71 let x = Polynomial::var(0);
72 let lhs = x.add(
73 &x.mul(&Polynomial::constant(NEG_QINV_MOD_R))
74 .mul(&Polynomial::constant(KYBER_Q)),
75 );
76 let rhs = x
77 .mul(&Polynomial::constant(MONT_COFACTOR))
78 .mul(&Polynomial::constant(MONT_R));
79 lhs.canonical_eq(&rhs)
80}
81
82/// Certify the Montgomery CONGRUENCE: `(x + lo·q) − x = q·lo`, so the reduction numerator differs
83/// from `x` by a multiple of `q` ⇒ `redc·R ≡ x (mod q)` ⇒ `redc ≡ x·R⁻¹ (mod q)` (`R` is a unit
84/// in 𝔽_q). A ring identity in the free variables `x`, `lo`, certified for all values.
85pub fn montgomery_reduction_congruence() -> bool {
86 let x = Polynomial::var(0);
87 let lo = Polynomial::var(1);
88 let lhs = x.add(&lo.mul(&Polynomial::constant(KYBER_Q))).sub(&x);
89 let rhs = Polynomial::constant(KYBER_Q).mul(&lo);
90 lhs.canonical_eq(&rhs)
91}
92
93/// The full Montgomery reduction is kernel-certified: the constant divides exactly (so `/R` is a
94/// shift), the result is `x·R⁻¹ mod q` (congruence), and the final conditional subtract keeps it
95/// in `[0, q)` (range). A single gate over the three procedures.
96pub fn montgomery_reduction_certified() -> bool {
97 montgomery_reduction_divisibility()
98 && montgomery_reduction_congruence()
99 && conditional_subtract_in_range(KYBER_Q)
100}
101
102// ── ML-DSA-65 (Dilithium3) prime field 𝔽_q, q = 8380417, 32-bit Montgomery radix ────────────────
103
104/// Dilithium's modulus — prime, so ℤ/qℤ is the field 𝔽_q.
105pub const MLDSA_Q: i64 = 8_380_417;
106/// Montgomery radix `R = 2³²` and `qinv = q⁻¹ mod R = 58728449` (the SUBTRACT convention: the
107/// reduction is `redc(a) = (a − ((a·qinv) mod R)·q) / R`, matching `mldsa::montgomery_reduce`). The
108/// exact integer cofactor `(qinv·q − 1) / R` makes the numerator a clean multiple of `R`.
109pub const MLDSA_MONT_R: i64 = 1 << 32;
110pub const MLDSA_QINV: i64 = 58_728_449;
111const MLDSA_MONT_COFACTOR: i64 = (MLDSA_QINV * MLDSA_Q - 1) / MLDSA_MONT_R;
112
113/// Certify the ML-DSA Montgomery DIVISIBILITY: `a·qinv·q − a = cofactor·R·a`, i.e. the reduction's
114/// numerator `a − (a·qinv)·q = −cofactor·R·a` is an EXACT multiple of `R` (so `/R` is a shift, never
115/// a rounding divide). This holds because `qinv·q ≡ 1 (mod R)`; the `ring` canonicalizer reduces both
116/// sides to `a·(qinv·q − 1)`, certifying it for ALL `a`.
117pub fn mldsa_montgomery_divisibility() -> bool {
118 let a = Polynomial::var(0);
119 let lhs = a
120 .mul(&Polynomial::constant(MLDSA_QINV))
121 .mul(&Polynomial::constant(MLDSA_Q))
122 .sub(&a);
123 let rhs = a
124 .mul(&Polynomial::constant(MLDSA_MONT_COFACTOR))
125 .mul(&Polynomial::constant(MLDSA_MONT_R));
126 lhs.canonical_eq(&rhs)
127}
128
129/// Certify the ML-DSA Montgomery CONGRUENCE: `(a − lo·q) − a = −q·lo`, so the reduction numerator
130/// `redc·R = a − lo·q` differs from `a` by a multiple of `q` ⇒ `redc·R ≡ a (mod q)` ⇒
131/// `redc ≡ a·R⁻¹ (mod q)`. A ring identity in the free variables `a`, `lo`, certified for all values.
132pub fn mldsa_montgomery_congruence() -> bool {
133 let a = Polynomial::var(0);
134 let lo = Polynomial::var(1);
135 let lhs = a.sub(&lo.mul(&Polynomial::constant(MLDSA_Q))).sub(&a);
136 let rhs = Polynomial::constant(-MLDSA_Q).mul(&lo);
137 lhs.canonical_eq(&rhs)
138}
139
140/// The full ML-DSA Montgomery reduction is kernel-certified: the numerator is an exact multiple of
141/// `R` (divisibility) and the result is `a·R⁻¹ mod q` (congruence). Together these prove
142/// `mldsa::montgomery_reduce` — hence the AVX2 `montmul32` that shares its formula — computes the
143/// right field element, with no Z3, for ALL inputs (not a sampled bit-width).
144pub fn mldsa_montgomery_reduction_certified() -> bool {
145 mldsa_montgomery_divisibility() && mldsa_montgomery_congruence()
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn mldsa_montgomery_reduction_is_kernel_certified() {
154 assert_eq!(
155 MLDSA_QINV * MLDSA_Q - 1,
156 MLDSA_MONT_COFACTOR * MLDSA_MONT_R,
157 "qinv·q − 1 must be an exact multiple of R = 2³² (qinv = q⁻¹ mod R)"
158 );
159 assert!(mldsa_montgomery_divisibility(), "a·qinv·q − a is an exact multiple of R");
160 assert!(mldsa_montgomery_congruence(), "redc·R ≡ a (mod q)");
161 assert!(mldsa_montgomery_reduction_certified(), "the combined ML-DSA Montgomery gate");
162 }
163
164 #[test]
165 fn mldsa_montgomery_certification_is_non_vacuous() {
166 // A wrong cofactor must NOT certify — otherwise qinv would be invalid.
167 let a = Polynomial::var(0);
168 let lhs = a.mul(&Polynomial::constant(MLDSA_QINV)).mul(&Polynomial::constant(MLDSA_Q)).sub(&a);
169 let wrong = a
170 .mul(&Polynomial::constant(MLDSA_MONT_COFACTOR + 1))
171 .mul(&Polynomial::constant(MLDSA_MONT_R));
172 assert!(!lhs.canonical_eq(&wrong), "cofactor+1 must be rejected");
173 }
174
175 #[test]
176 fn montgomery_reduction_is_kernel_certified() {
177 assert!(montgomery_reduction_divisibility(), "qinv'=3327 makes x+(x·qinv')·q an exact multiple of R");
178 assert!(montgomery_reduction_congruence(), "redc·R ≡ x (mod q)");
179 assert!(montgomery_reduction_certified(), "the combined Montgomery-reduction gate");
180 assert_eq!(NEG_QINV_MOD_R * KYBER_Q + 1, MONT_COFACTOR * MONT_R, "qinv'·q + 1 = 169·R");
181 }
182
183 #[test]
184 fn montgomery_certification_is_non_vacuous() {
185 let x = Polynomial::var(0);
186 let lhs = x.add(&x.mul(&Polynomial::constant(NEG_QINV_MOD_R)).mul(&Polynomial::constant(KYBER_Q)));
187 let wrong = x.mul(&Polynomial::constant(168)).mul(&Polynomial::constant(MONT_R)); // 168 ≠ 169
188 assert!(!lhs.canonical_eq(&wrong), "a wrong cofactor must not be certified — qinv' would be invalid");
189 let lo = Polynomial::var(1);
190 let cong_lhs = x.add(&lo.mul(&Polynomial::constant(KYBER_Q))).sub(&x);
191 assert!(!cong_lhs.canonical_eq(&lo), "redc·R − x is q·lo, not lo");
192 }
193
194 #[test]
195 fn gauss_three_multiply_is_kernel_certified() {
196 assert!(
197 gauss_three_multiply_identity(),
198 "the ring canonicalizer must certify Gauss's 3-multiply butterfly identity"
199 );
200 }
201
202 #[test]
203 fn gauss_identity_is_non_vacuous() {
204 // A WRONG rewrite (dropping `− bd`) must NOT canonicalize equal — the prover genuinely
205 // distinguishes the identity rather than calling everything equal.
206 let a = Polynomial::var(0);
207 let b = Polynomial::var(1);
208 let c = Polynomial::var(2);
209 let d = Polynomial::var(3);
210 let lhs = a.mul(&d).add(&b.mul(&c));
211 let wrong = a.add(&b).mul(&c.add(&d)).sub(&a.mul(&c)); // missing − bd
212 assert!(!lhs.canonical_eq(&wrong), "a wrong rewrite must not be certified equal");
213 }
214
215 #[test]
216 fn conditional_subtract_in_range_for_kyber_q() {
217 assert!(
218 conditional_subtract_in_range(KYBER_Q),
219 "csub must keep a modular-add result of 𝔽_3329 in [0, q)"
220 );
221 }
222
223 #[test]
224 fn reduction_range_is_non_vacuous() {
225 // The procedure must NOT "prove" a FALSE range claim. Under the too-weak hypothesis
226 // `a < 3q`, the negation of `a − q < q` (i.e. `a ≥ 2q`) is SATISFIABLE (a ≈ 2.5q), so
227 // `fourier_motzkin_unsat` must return FALSE — the bound genuinely does not hold.
228 let too_weak =
229 fourier_motzkin_unsat(&[con(1, -3 * KYBER_Q, true), con(-1, 2 * KYBER_Q, false)]);
230 assert!(!too_weak, "a < 3q does NOT bound a − q below q — the prover must not claim it does");
231 }
232}