• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kaidokert / modmath-rs / 28725835473

05 Jul 2026 01:35AM UTC coverage: 98.698% (+0.1%) from 98.55%
28725835473

Pull #23

github

web-flow
Merge 3d73b5eb0 into 32599a022
Pull Request #23: v0.4.0: const-num-traits migration, CT typestate discipline, safegcd CT inverse

1735 of 1761 new or added lines in 14 files covered. (98.52%)

15 existing lines in 2 files now uncovered.

5989 of 6068 relevant lines covered (98.7%)

3089.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.4
/modmath/src/inv/safegcd.rs
1
// Clippy's `clone_on_copy` / `op_ref` lints misfire on generic code that
2
// uses `.clone()` or `&x + &T::zero()` as portable "produce owned T"
3
// idioms across trait-bound flavors where T may or may not be Copy.
4
#![allow(clippy::clone_on_copy, clippy::op_ref)]
5

6
//! Bernstein-Yang constant-time modular inverse via "safegcd" divsteps.
7
//!
8
//! Computes `a⁻¹ mod n` in constant time over the value being inverted,
9
//! for **any modulus** — composite or prime. Used by RSA private-key
10
//! blinding (where `n = p·q` is composite, so Fermat's little theorem
11
//! doesn't apply) and anywhere else CT inversion of arbitrary residues
12
//! is needed.
13
//!
14
//! Reference: Bernstein & Yang, *"Fast constant-time gcd computation
15
//! and modular inversion"*, IACR ePrint 2019/266
16
//! (<https://gcd.cr.yp.to/>).
17
//!
18
//! ## Algorithm shape
19
//!
20
//! Operates on:
21
//! - `f, g`: signed two's-complement values stored in unsigned `T`.
22
//!   `f` starts as `modulus`, `g` starts as `value`. They evolve via
23
//!   divsteps until `g == 0` and `f == ±1` (iff `gcd == 1`).
24
//! - `d, e`: modular coefficients kept in `[0, modulus)` throughout,
25
//!   such that `d * value ≡ f mod modulus` and `e * value ≡ g mod
26
//!   modulus`. At convergence, `d` is `±value⁻¹ mod modulus`.
27
//! - `delta`: small signed integer that tracks the divsteps state
28
//!   machine. Stored as `i64`.
29
//!
30
//! ## Why this is "signed" without naming a signed bigint type
31
//!
32
//! The algorithm conceptually operates on signed values `f`, `g` that
33
//! range over `(-modulus, modulus)`. **The implementation never types
34
//! a signed bigint.** Every "signed" op is two's-complement-on-unsigned:
35
//!
36
//! - signed `f + g`  ≡ `T::wrapping_add(f, g)`         on unsigned `T`
37
//! - signed `-f`     ≡ `T::wrapping_neg(f)`            on unsigned `T`
38
//! - signed `f < 0`  ≡ MSB of unsigned `f` is set
39
//! - arithmetic shr  ≡ `(x >> 1) | sign_extend_mask`   on unsigned `T`
40
//!
41
//! ## Bound precondition
42
//!
43
//! `modulus` must fit in `T` with at least one bit of headroom — i.e.
44
//! `2 * modulus` does not overflow `T`. The algorithm maintains
45
//! `|d|, |e| < modulus` strictly, so intermediate sums `d + e` are
46
//! bounded by `2 * modulus`. A modulus that fully occupies the
47
//! carrier width violates this — pick a carrier at least one limb
48
//! wider (a 2048-bit RSA modulus needs a 2080-bit carrier at 32-bit
49
//! limbs).
50
//!
51
//! ## Implementation note
52
//!
53
//! This is a **per-step (unbatched) implementation**: each divstep
54
//! does multi-precision arithmetic on the full `T`. Asymptotically
55
//! `O(n²)` for an `n`-bit modulus. The batched ("jumpdivstep")
56
//! variant brings this to `O(n²/W)` where `W` is the limb width — a
57
//! follow-up optimization. The unbatched version is **correct** and
58
//! suitable for cases where the latency budget allows it (RSA blinding
59
//! at 2048 bits is dominated by the main exponentiation anyway).
60

61
use const_num_traits::{CtIsZero, CtParity, One, WrappingAdd, WrappingSub, Zero};
62
use modmath_cios::CiosRowOps;
63
use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, ConstantTimeLess, CtOption};
64

65
/// Total number of divsteps for an `n`-bit operand. Per Theorem 11.2
66
/// of the Bernstein-Yang paper, for the *integer* case with δ₀=1 (our
67
/// initialization at [`safegcd_inv_ct`]) the proven sufficient count
68
/// is `⌊(49·n + 80)/17⌋` for `n < 46` and `⌊(49·n + 57)/17⌋` for
69
/// `n ≥ 46`. Under-iterating causes worst-case coprime pairs to exit
70
/// divsteps with `g ≠ 0`, silently masking valid inverses to `None`.
71
/// (The `(45·n + 64)/19` bound is the *polynomial* case from Section
72
/// 7 — not applicable here.)
73
pub(crate) const fn divsteps_total(modulus_bits: usize) -> usize {
582✔
74
    if modulus_bits < 46 {
582✔
75
        (49 * modulus_bits + 80) / 17
558✔
76
    } else {
77
        (49 * modulus_bits + 57) / 17
24✔
78
    }
79
}
582✔
80

81
/// Returns `Choice::from(1)` iff the low bit of `value` is set.
82
///
83
/// Delegates to cnt's [`CtParity`] on the low limb. The CT contract
84
/// for "is this primitive odd" lives upstream; we compose by
85
/// extracting `word(0)` (which is a primitive once the limb level is
86
/// reached).
87
#[inline]
88
fn ct_low_bit<T>(value: &T) -> Choice
114,935✔
89
where
114,935✔
90
    T: CiosRowOps,
114,935✔
91
    T::Word: CtParity,
114,935✔
92
{
93
    value.word(0).ct_is_odd()
114,935✔
94
}
114,935✔
95

96
/// Returns `Choice::from(1)` iff the most-significant bit of `value`'s
97
/// bit pattern is set (i.e. value is "negative" in two's-complement
98
/// interpretation).
99
///
100
/// Exposed at crate visibility so `Field::inv_safegcd_ct` can fold the
101
/// "modulus has one bit of headroom" precondition into its CtOption
102
/// instead of relying on documentation alone.
103
#[inline]
104
pub(crate) fn ct_msb_set<T>(value: &T) -> Choice
57,210✔
105
where
57,210✔
106
    T: CiosRowOps,
57,210✔
107
    T::Word: core::ops::BitAnd<Output = T::Word>
57,210✔
108
        + core::ops::Shl<usize, Output = T::Word>
57,210✔
109
        + One
57,210✔
110
        + CtIsZero,
57,210✔
111
{
112
    let n = value.word_count();
57,210✔
113
    let word_bits = core::mem::size_of::<T::Word>() * 8;
57,210✔
114
    let msb_mask = T::Word::one() << (word_bits - 1);
57,210✔
115
    let masked = value.word(n - 1) & msb_mask;
57,210✔
116
    // Delegates to cnt's CtIsZero rather than ct_eq(&T::Word::zero()).
117
    // Identical semantics; the dedicated trait is cnt-tested upstream.
118
    !masked.ct_is_zero()
57,210✔
119
}
57,210✔
120

121
/// Constant-time `delta > 0` for the i64 state variable.
122
#[inline]
123
fn ct_i64_positive(delta: i64) -> Choice {
57,178✔
124
    let delta_u = delta as u64;
57,178✔
125
    // nonzero = ((x | -x) >> 63) — top bit set iff x != 0
126
    let nonzero_top = (delta_u | delta_u.wrapping_neg()) >> 63;
57,178✔
127
    // sign_bit_clear = 1 iff x's high bit is 0 (i.e. x >= 0 as i64)
128
    let sign_bit_clear = (!delta_u) >> 63;
57,178✔
129
    Choice::from(((nonzero_top & sign_bit_clear) & 1) as u8)
57,178✔
130
}
57,178✔
131

132
/// Arithmetic right shift by 1 (sign-extending). For T interpreted as
133
/// two's-complement, returns `value / 2` with floor rounding for
134
/// negative values.
135
#[inline]
136
fn arithmetic_shr_one<T>(value: &T) -> T
57,178✔
137
where
57,178✔
138
    T: CiosRowOps
57,178✔
139
        + Clone
57,178✔
140
        + ConditionallySelectable
57,178✔
141
        + One
57,178✔
142
        + Zero
57,178✔
143
        + core::ops::Shr<usize, Output = T>
57,178✔
144
        + core::ops::Shl<usize, Output = T>
57,178✔
145
        + core::ops::BitOr<Output = T>,
57,178✔
146
    T::Word: core::ops::BitAnd<Output = T::Word>
57,178✔
147
        + core::ops::Shl<usize, Output = T::Word>
57,178✔
148
        + One
57,178✔
149
        + CtIsZero,
57,178✔
150
{
151
    let logical = value.clone() >> 1;
57,178✔
152
    let msb_set = ct_msb_set(value);
57,178✔
153
    let n_bits = value.word_count() * core::mem::size_of::<T::Word>() * 8;
57,178✔
154
    let top_bit_mask = T::one() << (n_bits - 1);
57,178✔
155
    let with_sign_ext = logical.clone() | top_bit_mask;
57,178✔
156
    T::conditional_select(&logical, &with_sign_ext, msb_set)
57,178✔
157
}
57,178✔
158

159
/// Reduce `sum` to `[0, m)` when `sum` is already in `[0, 2·m)`.
160
/// One conditional subtract suffices.
161
#[inline]
162
fn reduce_lt_2m_ct<T>(sum: T, m: &T) -> T
57,178✔
163
where
57,178✔
164
    T: Clone + ConditionallySelectable + WrappingSub<Output = T> + ConstantTimeLess,
57,178✔
165
{
166
    let sum_minus_m = sum.clone().wrapping_sub(m.clone());
57,178✔
167
    let need_sub = !sum.ct_lt(m);
57,178✔
168
    T::conditional_select(&sum, &sum_minus_m, need_sub)
57,178✔
169
}
57,178✔
170

171
/// Modular add: `(a + b) mod m`, assuming `a, b ∈ [0, m)` so the sum
172
/// is in `[0, 2·m)`. Precondition: `2·m` fits in `T` (one bit of
173
/// headroom over `m`).
174
#[inline]
175
fn add_mod_ct<T>(a: &T, b: &T, m: &T) -> T
57,178✔
176
where
57,178✔
177
    T: Clone
57,178✔
178
        + ConditionallySelectable
57,178✔
179
        + WrappingAdd<Output = T>
57,178✔
180
        + WrappingSub<Output = T>
57,178✔
181
        + ConstantTimeLess,
57,178✔
182
{
183
    let sum = a.clone().wrapping_add(b.clone());
57,178✔
184
    reduce_lt_2m_ct(sum, m)
57,178✔
185
}
57,178✔
186

187
/// Modular negate: `(m - x) mod m`, preserving the `[0, m)` invariant.
188
/// Returns 0 when `x == 0` (since `-0 mod m = 0`).
189
///
190
/// Delegates to cnt's [`CtIsZero`] for the masked zero check rather
191
/// than hand-rolling `ct_eq(&T::zero())`. Identical semantically; the
192
/// dedicated trait is cnt-tested.
193
#[inline]
194
fn neg_mod_ct<T>(x: &T, m: &T) -> T
57,757✔
195
where
57,757✔
196
    T: Clone + Zero + ConditionallySelectable + WrappingSub<Output = T> + CtIsZero,
57,757✔
197
{
198
    let zero = T::zero();
57,757✔
199
    let x_is_zero = x.ct_is_zero();
57,757✔
200
    let neg = m.clone().wrapping_sub(x.clone());
57,757✔
201
    T::conditional_select(&neg, &zero, x_is_zero)
57,757✔
202
}
57,757✔
203

204
/// Modular halving: returns `y ∈ [0, m)` such that `2·y ≡ x mod m`.
205
/// Precondition: `x ∈ [0, m)`, `m` is odd.
206
///
207
/// For odd `m`, the inverse of 2 mod `m` is `(m + 1) / 2`; computing
208
/// `x / 2 mod m` via the standard branch-free trick:
209
/// - if `x` even: result is `x >> 1`.
210
/// - if `x` odd: result is `(x + m) >> 1` (since `x + m` is even when
211
///   `m` is odd, and the sum is in `[m, 2m)` which fits with one bit
212
///   of headroom).
213
#[inline]
214
fn half_mod_ct<T>(x: &T, m: &T) -> T
57,178✔
215
where
57,178✔
216
    T: CiosRowOps
57,178✔
217
        + Clone
57,178✔
218
        + ConditionallySelectable
57,178✔
219
        + WrappingAdd<Output = T>
57,178✔
220
        + core::ops::Shr<usize, Output = T>,
57,178✔
221
    T::Word: CtParity,
57,178✔
222
{
223
    let x_odd = ct_low_bit(x);
57,178✔
224
    let x_plus_m = x.clone().wrapping_add(m.clone());
57,178✔
225
    let candidate_odd = x_plus_m >> 1;
57,178✔
226
    let candidate_even = x.clone() >> 1;
57,178✔
227
    T::conditional_select(&candidate_even, &candidate_odd, x_odd)
57,178✔
228
}
57,178✔
229

230
/// Compute `value⁻¹ mod modulus` in constant time over `value`. Works
231
/// for any modulus (composite or prime), provided the modulus is odd
232
/// and `2 * modulus` fits in `T`.
233
///
234
/// Returns `CtOption::Some(inv)` when `gcd(value, modulus) == 1`, or
235
/// `CtOption::None` masked when no inverse exists. The failure path
236
/// timing is independent of input magnitudes.
237
///
238
/// The `modulus is odd` and `modulus > 1` preconditions are folded
239
/// into the returned mask: passing an even modulus or `1` yields
240
/// `CtOption::None`, matching the behavior for "no inverse exists" —
241
/// the divsteps loop still runs the full step count in constant time.
242
///
243
/// # Preconditions
244
///
245
/// - `value < modulus` (caller should reduce first)
246
/// - `2 * modulus` fits in `T` without overflow (i.e. `T` is at least
247
///   one bit wider than `modulus`)
248
pub fn safegcd_inv_ct<T>(value: &T, modulus: &T) -> CtOption<T>
579✔
249
where
579✔
250
    T: CiosRowOps
579✔
251
        + Clone
579✔
252
        + ConditionallySelectable
579✔
253
        + ConstantTimeEq
579✔
254
        + ConstantTimeLess
579✔
255
        + CtIsZero
579✔
256
        + Zero
579✔
257
        + One
579✔
258
        + WrappingAdd<Output = T>
579✔
259
        + WrappingSub<Output = T>
579✔
260
        + core::ops::Shr<usize, Output = T>
579✔
261
        + core::ops::Shl<usize, Output = T>
579✔
262
        + core::ops::BitOr<Output = T>,
579✔
263
    T::Word: Copy
579✔
264
        + ConditionallySelectable
579✔
265
        + ConstantTimeEq
579✔
266
        + CtIsZero
579✔
267
        + CtParity
579✔
268
        + One
579✔
269
        + Zero
579✔
270
        + core::ops::BitAnd<Output = T::Word>
579✔
271
        + core::ops::Shl<usize, Output = T::Word>,
579✔
272
{
273
    let n_bits = value.word_count() * core::mem::size_of::<T::Word>() * 8;
579✔
274
    let total_steps = divsteps_total(n_bits);
579✔
275

276
    let mut f = modulus.clone();
579✔
277
    let mut g = value.clone();
579✔
278
    let mut d = T::zero();
579✔
279
    let mut e = T::one();
579✔
280
    let mut delta: i64 = 1;
579✔
281

282
    for _ in 0..total_steps {
57,178✔
283
        let delta_pos = ct_i64_positive(delta);
57,178✔
284
        let g_odd = ct_low_bit(&g);
57,178✔
285
        let swap = delta_pos & g_odd;
57,178✔
286

57,178✔
287
        // swap_choice: (f, g) ← (g, -f); (d, e) ← (e, -d mod m); delta ← -delta
57,178✔
288
        let new_f_if_swap = g.clone();
57,178✔
289
        let new_g_if_swap = f.clone().wrapping_neg_two_complement();
57,178✔
290
        let new_d_if_swap = e.clone();
57,178✔
291
        let new_e_if_swap = neg_mod_ct(&d, modulus);
57,178✔
292

57,178✔
293
        f = T::conditional_select(&f, &new_f_if_swap, swap);
57,178✔
294
        g = T::conditional_select(&g, &new_g_if_swap, swap);
57,178✔
295
        d = T::conditional_select(&d, &new_d_if_swap, swap);
57,178✔
296
        e = T::conditional_select(&e, &new_e_if_swap, swap);
57,178✔
297

57,178✔
298
        let neg_delta = (delta as u64).wrapping_neg() as i64;
57,178✔
299
        delta = i64::conditional_select(&delta, &neg_delta, swap);
57,178✔
300
        delta = delta.wrapping_add(1);
57,178✔
301

57,178✔
302
        // g_odd was determined before swap; after swap+negate the parity
57,178✔
303
        // of g equals that of the original g (both old g and -old_f are
57,178✔
304
        // odd when the swap fired, since old f is odd by loop invariant).
57,178✔
305
        // In the non-swap case g_odd tracks current g's parity directly.
57,178✔
306
        let g_odd_now = g_odd;
57,178✔
307
        let to_add_to_g = T::conditional_select(&T::zero(), &f, g_odd_now);
57,178✔
308
        g = g.wrapping_add(to_add_to_g);
57,178✔
309

57,178✔
310
        let add_to_e = add_mod_ct(&e, &d, modulus);
57,178✔
311
        e = T::conditional_select(&e, &add_to_e, g_odd_now);
57,178✔
312

57,178✔
313
        g = arithmetic_shr_one(&g);
57,178✔
314
        e = half_mod_ct(&e, modulus);
57,178✔
315
    }
57,178✔
316

317
    // After total_steps divsteps, g == 0 and f == ±gcd. For the
318
    // invertible case (gcd == 1), f is either +1 (bit pattern T::one())
319
    // or -1 (bit pattern T::one().wrapping_neg()).
320
    let one = T::one();
579✔
321
    let neg_one_pattern = one.clone().wrapping_neg_two_complement();
579✔
322

323
    let f_is_one = f.ct_eq(&one);
579✔
324
    let f_is_neg_one = f.ct_eq(&neg_one_pattern);
579✔
325
    let has_inverse = f_is_one | f_is_neg_one;
579✔
326

327
    // Result: d if f == 1; (m - d) = -d mod m if f == -1.
328
    let neg_d = neg_mod_ct(&d, modulus);
579✔
329
    let result = T::conditional_select(&d, &neg_d, f_is_neg_one);
579✔
330

331
    // Fold in the modulus preconditions. `half_mod_ct`'s (x + m) >> 1
332
    // trick is invalid unless m is odd; modulus = 1 collapses residues
333
    // to a single class. Divsteps still ran (constant time), the
334
    // resulting `d`/`e` are garbage in those cases, and the mask
335
    // discards them.
336
    let modulus_is_odd = ct_low_bit(modulus);
579✔
337
    let modulus_is_one = modulus.ct_eq(&one);
579✔
338
    let modulus_ok = modulus_is_odd & !modulus_is_one;
579✔
339

340
    CtOption::new(result, has_inverse & modulus_ok)
579✔
341
}
579✔
342

343
// Inline WrappingNeg-style two's-complement negate. const-num-traits'
344
// WrappingNeg has an `Output` associated type that doesn't necessarily
345
// equal T for generic T; this trait gives a clean `T -> T` shape as
346
// `0 - x` via wrapping_sub.
347
trait WrappingNegT: Sized {
348
    fn wrapping_neg_two_complement(self) -> Self;
349
}
350

351
impl<T> WrappingNegT for T
352
where
353
    T: Zero + WrappingSub<Output = T>,
354
{
355
    #[inline]
356
    fn wrapping_neg_two_complement(self) -> Self {
57,757✔
357
        T::zero().wrapping_sub(self)
57,757✔
358
    }
57,757✔
359
}
360

361
#[cfg(test)]
362
mod tests {
363
    use super::*;
364
    use crate::inv::basic_mod_inv;
365

366
    #[test]
367
    fn divsteps_total_matches_paper_bound() {
1✔
368
        assert!(divsteps_total(256) >= (45usize * 256 + 64).div_ceil(19));
1✔
369
        assert!(divsteps_total(2048) >= (45usize * 2048 + 64).div_ceil(19));
1✔
370
        assert!(divsteps_total(4096) >= (45usize * 4096 + 64).div_ceil(19));
1✔
371
    }
1✔
372

373
    /// Exhaustive cross-check against `basic_mod_inv` over a small
374
    /// odd-prime modulus. T = u32 gives plenty of headroom for the
375
    /// `2 * modulus` precondition.
376
    #[test]
377
    fn matches_basic_mod_inv_mod_small_primes_u32() {
1✔
378
        for &m in &[3u32, 5, 7, 11, 13, 17, 19, 23, 29, 31, 97, 251] {
12✔
379
            for v in 1..m {
494✔
380
                let got = safegcd_inv_ct::<u32>(&v, &m);
494✔
381
                let want = basic_mod_inv(v, m);
494✔
382
                match (got.into_option(), want) {
494✔
383
                    (Some(g), Some(w)) => {
494✔
384
                        assert_eq!(
494✔
385
                            g, w,
386
                            "value={v} modulus={m}: safegcd gave {g}, basic_mod_inv gave {w}"
387
                        );
388
                        assert_eq!((g as u64 * v as u64) % m as u64, 1, "inv check failed");
494✔
389
                    }
390
                    (Some(_), None) | (None, Some(_)) => {
NEW
391
                        panic!("disagreement on value={v} modulus={m}");
×
392
                    }
NEW
393
                    (None, None) => {}
×
394
                }
395
            }
396
        }
397
    }
1✔
398

399
    /// Composite modulus — the load-bearing case for RSA blinding,
400
    /// where Fermat's little theorem doesn't apply but safegcd still
401
    /// computes the inverse correctly.
402
    #[test]
403
    fn handles_composite_modulus_u32() {
1✔
404
        // n = 3 * 5 = 15. Values coprime to 15: 1, 2, 4, 7, 8, 11, 13, 14.
405
        let m: u32 = 15;
1✔
406
        let coprime_values = [1u32, 2, 4, 7, 8, 11, 13, 14];
1✔
407
        for &v in &coprime_values {
8✔
408
            let got = safegcd_inv_ct::<u32>(&v, &m).into_option();
8✔
409
            let want = basic_mod_inv(v, m);
8✔
410
            assert_eq!(got, want, "value={v} modulus=15: composite case mismatch");
8✔
411
            if let Some(inv) = got {
8✔
412
                assert_eq!((inv as u64 * v as u64) % 15, 1);
8✔
NEW
413
            }
×
414
        }
415
        for &v in &[3u32, 5, 6, 9, 10, 12] {
6✔
416
            assert!(
6✔
417
                safegcd_inv_ct::<u32>(&v, &m).into_option().is_none(),
6✔
418
                "value={v} modulus=15: expected None (not coprime)"
419
            );
420
        }
421
    }
1✔
422

423
    /// FixedUInt end-to-end test. Confirms the algorithm works for
424
    /// multi-limb T as it does for primitive T. This is the
425
    /// load-bearing case — RSA uses FixedUInt, not primitives.
426
    #[test]
427
    fn fixed_bigint_smoke_test() {
1✔
428
        use const_num_traits::Ct;
429
        use fixed_bigint::FixedUInt;
430
        type U64Ct = FixedUInt<u32, 2, Ct>;
431
        type U64Nct = FixedUInt<u32, 2>;
432

433
        let m_raw: u64 = 0x7FFF_FFFF_FFFF_FFE7; // 2^63 - 25, prime
1✔
434
        let m = U64Ct::from(m_raw);
1✔
435
        let m_nct = U64Nct::from(m_raw);
1✔
436
        let test_vals = [1u64, 2, 7, 42, 0xDEAD_BEEF];
1✔
437
        for &v_raw in &test_vals {
5✔
438
            let v = U64Ct::from(v_raw);
5✔
439
            let v_nct = U64Nct::from(v_raw);
5✔
440
            let got = safegcd_inv_ct(&v, &m).into_option();
5✔
441
            assert!(got.is_some(), "expected inverse to exist for v={v_raw}");
5✔
442
            let inv = got.unwrap();
5✔
443
            // Cross-check by converting Ct → Nct and running basic_mod_inv
444
            let baseline = basic_mod_inv(v_nct, m_nct).expect("baseline inv");
5✔
445
            let inv_nct: U64Nct = inv.forget_ct();
5✔
446
            assert_eq!(
5✔
447
                inv_nct, baseline,
448
                "FixedUInt v={v_raw}: mismatch with basic_mod_inv"
449
            );
450
        }
451
    }
1✔
452

453
    /// u64 composite modulus. Coprime values must invert; 0xDEAD_BEEF
454
    /// (shares factor 11 with n = 0x100000707000031) must return
455
    /// CtOption::None.
456
    #[test]
457
    fn u64_composite_coprime() {
1✔
458
        let n: u64 = 0x1_0000_0007 * 0x100_0007;
1✔
459
        for v in [1u64, 2, 3, 0xCAFE_BABE, 0xFEED_FACE] {
5✔
460
            let got = safegcd_inv_ct::<u64>(&v, &n).into_option();
5✔
461
            assert!(got.is_some(), "u64 case: v={v} mod {n:#x} expected Some");
5✔
462
            let inv = got.unwrap();
5✔
463
            let prod = (inv as u128 * v as u128) % n as u128;
5✔
464
            assert_eq!(prod, 1, "u64 v={v}: inv*v mod n != 1");
5✔
465
        }
466
        // Non-coprime: 0xDEAD_BEEF shares factor 11 with n
467
        assert!(
1✔
468
            safegcd_inv_ct::<u64>(&0xDEAD_BEEF, &n)
1✔
469
                .into_option()
1✔
470
                .is_none()
1✔
471
        );
472
    }
1✔
473

474
    /// Larger primitive: u64 modulus, sanity check on a handful of pairs.
475
    /// Uses a modulus < 2^63 to satisfy the `2 * modulus fits in T`
476
    /// precondition.
477
    #[test]
478
    fn u64_smoke_test() {
1✔
479
        let m: u64 = 0x7FFF_FFFF_FFFF_FFE7; // 2^63 - 25, a prime
1✔
480
        let test_vals = [1u64, 2, 7, 0xDEAD_BEEF, 0xCAFE_BABE];
1✔
481
        for &v in &test_vals {
5✔
482
            let got = safegcd_inv_ct::<u64>(&v, &m).into_option();
5✔
483
            let want = basic_mod_inv(v, m);
5✔
484
            assert_eq!(got, want, "value={v} modulus={m}: u64 case mismatch");
5✔
485
            if let Some(inv) = got {
5✔
486
                let prod = (inv as u128 * v as u128) % m as u128;
5✔
487
                assert_eq!(prod, 1, "u64 inv * value mod m != 1");
5✔
NEW
488
            }
×
489
        }
490
    }
1✔
491

492
    /// Even modulus and modulus = 1 violate the algorithm's precondition
493
    /// (`half_mod_ct`'s trick assumes odd m; modulus = 1 has a single
494
    /// residue class). The mask folds those into CtOption::None.
495
    #[test]
496
    fn modulus_precondition_masks() {
1✔
497
        for m in [2u32, 4, 6, 100, 0xFFFF_FFFE] {
5✔
498
            for v in [1u32, 3, 7, 0xCAFE_BABE] {
20✔
499
                assert!(
20✔
500
                    safegcd_inv_ct::<u32>(&v, &m).into_option().is_none(),
20✔
501
                    "even modulus {m:#x} with value {v:#x}: expected None"
502
                );
503
            }
504
        }
505
        for v in [0u32, 1, 7] {
3✔
506
            assert!(
3✔
507
                safegcd_inv_ct::<u32>(&v, &1u32).into_option().is_none(),
3✔
508
                "modulus = 1 with value {v}: expected None"
509
            );
510
        }
511
    }
1✔
512
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc