Skip to main content

fixed_bigint/heapless/
cmp.rs

1//! `PartialEq` / `Eq` / `PartialOrd` / `Ord` for `HeaplessBigInt`, plus
2//! `subtle::ConstantTimeEq` and `subtle::ConditionallySelectable` for
3//! the Ct paths.
4//!
5//! All value-based, each dispatching on `P` like `FixedUInt`:
6//! - `Eq`: walks `max(a.len, b.len)` limbs. `Nct` short-circuits at the
7//!   first differing limb; `Ct` XOR/OR-folds every limb so timing does
8//!   not reveal the first mismatch (the returned `bool` is still
9//!   branchable — Ct-secure equality routes through `ConstantTimeEq`).
10//!   Under the zero-tail invariant, `HeaplessBigInt<u32, N, _>` with
11//!   `len = 0` compares equal to a `zero_full_cap()` (len = CAP).
12//! - `Ord`: MSB-to-LSB up to `max(a.len, b.len)`. `Nct` short-circuits;
13//!   `Ct` scans the full width with an `undecided` lock (no early return).
14//! - `subtle::ConstantTimeEq`: XOR-fold across `max(a.len, b.len)`.
15//!   `black_box` guards against LLVM re-branchifying the fold.
16//! - `subtle::ConditionallySelectable`: per-limb branchless select via
17//!   `T`'s subtle impl, iterating up to `max(a.len, b.len)`. Output len
18//!   is that same `max`, a public shape derived from two public shape
19//!   parameters — never from `choice`.
20//! - `const_num_traits::CtIsZero`: AND-fold `T::ct_eq(&ZERO)` across
21//!   `0..self.len`. Limbs beyond `len` are zero by invariant so
22//!   skipping them preserves the answer.
23//! - `subtle::ConstantTimeGreater` / `ConstantTimeLess`: MSB-to-LSB
24//!   scan up to `max(a.len, b.len)` with a running `undecided` bit —
25//!   the Montgomery conditional-subtract shape.
26
27use super::{HeaplessBigInt, is_zero, zero};
28use crate::MachineWord;
29use const_num_traits::{Personality, PersonalityTag};
30use core::marker::PhantomData;
31
32// ── PartialEq / Eq (value-based) ──
33
34impl<T: MachineWord, const CAP: usize, P: Personality> PartialEq for HeaplessBigInt<T, CAP, P> {
35    fn eq(&self, other: &Self) -> bool {
36        let n = core::cmp::max(self.len, other.len) as usize;
37        match P::TAG {
38            PersonalityTag::Nct => {
39                let mut i = 0;
40                while i < n {
41                    if self.limbs[i] != other.limbs[i] {
42                        return false;
43                    }
44                    i += 1;
45                }
46                true
47            }
48            // Fold every limb, no early return: timing is independent of
49            // where the first mismatch is.
50            PersonalityTag::Ct => {
51                let mut diff = zero::<T>();
52                let mut i = 0;
53                while i < n {
54                    diff |= self.limbs[i] ^ other.limbs[i];
55                    i += 1;
56                }
57                is_zero(&diff)
58            }
59        }
60    }
61}
62
63impl<T: MachineWord, const CAP: usize, P: Personality> Eq for HeaplessBigInt<T, CAP, P> {}
64
65// ── PartialOrd / Ord (value-based) ──
66
67impl<T: MachineWord, const CAP: usize, P: Personality> PartialOrd for HeaplessBigInt<T, CAP, P> {
68    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
69        Some(self.cmp(other))
70    }
71}
72
73impl<T: MachineWord, const CAP: usize, P: Personality> Ord for HeaplessBigInt<T, CAP, P> {
74    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
75        let n = core::cmp::max(self.len, other.len) as usize;
76        match P::TAG {
77            PersonalityTag::Nct => {
78                let mut i = n;
79                while i > 0 {
80                    i -= 1;
81                    match self.limbs[i].cmp(&other.limbs[i]) {
82                        core::cmp::Ordering::Equal => continue,
83                        ord => return ord,
84                    }
85                }
86                core::cmp::Ordering::Equal
87            }
88            // Shared full-width branchless scan (see `const_cmp_ct`); the two
89            // operand slices are equal length (`n`).
90            PersonalityTag::Ct => {
91                // `get(..n)` not `[..n]`: `n = max(len) <= CAP` by invariant,
92                // so `None` is unreachable, but the checked form keeps the
93                // array-slice off the panic path at MSRV/`-Oz`.
94                let a = self.limbs.get(..n).unwrap_or(&self.limbs);
95                let b = other.limbs.get(..n).unwrap_or(&other.limbs);
96                crate::fixeduint::const_cmp_ct(a, b)
97            }
98        }
99    }
100}
101
102// ── Hash (consistent with value-based Eq) ──
103
104impl<T: MachineWord + core::hash::Hash, const CAP: usize, P: Personality> core::hash::Hash
105    for HeaplessBigInt<T, CAP, P>
106{
107    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
108        // Hash up to the highest non-zero limb, ignoring `len` so equal
109        // values at different shapes hash alike (value-based). Scans
110        // `0..len`; limbs beyond `len` are zero by invariant, so `CAP`
111        // need not enter. NCT-implicit — this scans content.
112        let mut top = 0usize;
113        let mut i = 0;
114        while i < self.len as usize {
115            if !super::is_zero(&self.limbs[i]) {
116                top = i + 1;
117            }
118            i += 1;
119        }
120        state.write_usize(top);
121        for limb in &self.limbs[..top] {
122            limb.hash(state);
123        }
124    }
125}
126
127// ── subtle::ConstantTimeEq (Ct-safe equality) ──
128
129impl<T, const CAP: usize, P: Personality> subtle::ConstantTimeEq for HeaplessBigInt<T, CAP, P>
130where
131    T: MachineWord + subtle::ConstantTimeEq,
132{
133    fn ct_eq(&self, other: &Self) -> subtle::Choice {
134        // Iterate `max(len)` — a public shape parameter. Both operands
135        // walk the same public count regardless of value.
136        let n = core::cmp::max(self.len, other.len) as usize;
137        let mut acc = subtle::Choice::from(1u8);
138        let mut i = 0;
139        while i < n {
140            let per_limb = self.limbs[i].ct_eq(&other.limbs[i]);
141            acc &= per_limb;
142            i += 1;
143        }
144        // `black_box` on the accumulator so LLVM can't recognise the
145        // AND-fold as a short-circuit — same defence as
146        // FixedUInt's `const_ct_select`.
147        subtle::Choice::from(core::hint::black_box(acc.unwrap_u8()))
148    }
149}
150
151// ── subtle::ConditionallySelectable ──
152
153impl<T, const CAP: usize, P: Personality> subtle::ConditionallySelectable
154    for HeaplessBigInt<T, CAP, P>
155where
156    T: MachineWord + subtle::ConditionallySelectable,
157{
158    fn conditional_select(a: &Self, b: &Self, choice: subtle::Choice) -> Self {
159        // Output `len = max(a.len, b.len)`. Both operand lens are public
160        // shape parameters, so their `max` is public — never derived
161        // from `choice`. Per-limb select up to that bound; the tails
162        // beyond each operand's own len are zero (zero-tail invariant),
163        // so per-limb select on the tails yields zero regardless of
164        // `choice`, and the output's tail past `out_len` stays zero.
165        let out_len = core::cmp::max(a.len, b.len);
166        let mut limbs = [super::zero::<T>(); CAP];
167        let mut i = 0;
168        while i < out_len as usize {
169            limbs[i] = <T as subtle::ConditionallySelectable>::conditional_select(
170                &a.limbs[i],
171                &b.limbs[i],
172                choice,
173            );
174            i += 1;
175        }
176        Self {
177            limbs,
178            len: out_len,
179            _p: PhantomData,
180        }
181    }
182}
183
184/// Branchless select for the `Ct` arms of value-returning ops: returns
185/// `if_true` when `flag`, else `if_false`, with no branch on `flag`. A thin
186/// wrapper over `conditional_select` (the whole-value masked select above) —
187/// the runtime-impl analog of `FixedUInt::const_ct_select` (heapless doesn't
188/// need a `const fn` variant since its impls aren't const-evaluated). The
189/// result width is `max(if_false.len, if_true.len)`, a public shape, never
190/// derived from `flag`.
191#[inline]
192pub(crate) fn ct_select<T, const CAP: usize, P: Personality>(
193    if_false: &HeaplessBigInt<T, CAP, P>,
194    if_true: &HeaplessBigInt<T, CAP, P>,
195    flag: bool,
196) -> HeaplessBigInt<T, CAP, P>
197where
198    T: MachineWord + subtle::ConditionallySelectable,
199{
200    <HeaplessBigInt<T, CAP, P> as subtle::ConditionallySelectable>::conditional_select(
201        if_false,
202        if_true,
203        subtle::Choice::from(flag as u8),
204    )
205}
206
207// ── const_num_traits::CtIsZero ──
208
209impl<T, const CAP: usize, P: Personality> const_num_traits::ops::ct::CtIsZero
210    for HeaplessBigInt<T, CAP, P>
211where
212    T: MachineWord + subtle::ConstantTimeEq,
213{
214    fn ct_is_zero(&self) -> subtle::Choice {
215        let n = self.len as usize;
216        let mut acc = subtle::Choice::from(1u8);
217        let mut i = 0;
218        while i < n {
219            acc &= self.limbs[i].ct_eq(&<T as const_num_traits::ConstZero>::ZERO);
220            i += 1;
221        }
222        subtle::Choice::from(core::hint::black_box(acc.unwrap_u8()))
223    }
224}
225
226// ── subtle::ConstantTimeGreater / ConstantTimeLess ──
227
228impl<T, const CAP: usize, P: Personality> subtle::ConstantTimeGreater for HeaplessBigInt<T, CAP, P>
229where
230    T: MachineWord + subtle::ConstantTimeEq + subtle::ConstantTimeGreater,
231{
232    fn ct_gt(&self, other: &Self) -> subtle::Choice {
233        // MSB-to-LSB scan across `max(a.len, b.len)`. `undecided` locks
234        // the answer at the first differing limb without a data-dependent
235        // branch — every iteration always executes.
236        let n = core::cmp::max(self.len, other.len) as usize;
237        let mut gt = subtle::Choice::from(0u8);
238        let mut undecided = subtle::Choice::from(1u8);
239        let mut i = n;
240        while i > 0 {
241            i -= 1;
242            let gt_here = self.limbs[i].ct_gt(&other.limbs[i]);
243            let eq_here = self.limbs[i].ct_eq(&other.limbs[i]);
244            gt |= undecided & gt_here;
245            undecided &= eq_here;
246        }
247        gt
248    }
249}
250
251// `ConstantTimeLess` is derived from `ConstantTimeEq` + `ConstantTimeGreater`
252// via its default methods; the empty impl is enough to opt in.
253impl<T, const CAP: usize, P: Personality> subtle::ConstantTimeLess for HeaplessBigInt<T, CAP, P> where
254    T: MachineWord + subtle::ConstantTimeEq + subtle::ConstantTimeGreater
255{
256}