Skip to main content

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}