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            // Full MSB-to-LSB scan; once a differing limb is seen the
89            // `decided` mask stops later limbs from overturning it. Mirrors
90            // `FixedUInt`'s `const_cmp_ct`. result: 2 = Greater, 1 = Less.
91            PersonalityTag::Ct => {
92                let mut result: u8 = 0;
93                let mut decided: u8 = 0;
94                let mut i = n;
95                while i > 0 {
96                    i -= 1;
97                    let gt = (self.limbs[i] > other.limbs[i]) as u8;
98                    let lt = (self.limbs[i] < other.limbs[i]) as u8;
99                    let here = (gt << 1) | lt;
100                    let undecided_mask = core::hint::black_box(!decided);
101                    result |= undecided_mask & here;
102                    let here_nz_mask = core::hint::black_box(((here != 0) as u8).wrapping_neg());
103                    decided |= here_nz_mask;
104                }
105                match result {
106                    2 => core::cmp::Ordering::Greater,
107                    1 => core::cmp::Ordering::Less,
108                    _ => core::cmp::Ordering::Equal,
109                }
110            }
111        }
112    }
113}
114
115// ── Hash (consistent with value-based Eq) ──
116
117impl<T: MachineWord + core::hash::Hash, const CAP: usize, P: Personality> core::hash::Hash
118    for HeaplessBigInt<T, CAP, P>
119{
120    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
121        // Hash up to the highest non-zero limb, ignoring `len` so equal
122        // values at different shapes hash alike (value-based). Scans
123        // `0..len`; limbs beyond `len` are zero by invariant, so `CAP`
124        // need not enter. NCT-implicit — this scans content.
125        let mut top = 0usize;
126        let mut i = 0;
127        while i < self.len as usize {
128            if !super::is_zero(&self.limbs[i]) {
129                top = i + 1;
130            }
131            i += 1;
132        }
133        state.write_usize(top);
134        for limb in &self.limbs[..top] {
135            limb.hash(state);
136        }
137    }
138}
139
140// ── subtle::ConstantTimeEq (Ct-safe equality) ──
141
142impl<T, const CAP: usize, P: Personality> subtle::ConstantTimeEq for HeaplessBigInt<T, CAP, P>
143where
144    T: MachineWord + subtle::ConstantTimeEq,
145{
146    fn ct_eq(&self, other: &Self) -> subtle::Choice {
147        // Iterate `max(len)` — a public shape parameter. Both operands
148        // walk the same public count regardless of value.
149        let n = core::cmp::max(self.len, other.len) as usize;
150        let mut acc = subtle::Choice::from(1u8);
151        let mut i = 0;
152        while i < n {
153            let per_limb = self.limbs[i].ct_eq(&other.limbs[i]);
154            acc &= per_limb;
155            i += 1;
156        }
157        // `black_box` on the accumulator so LLVM can't recognise the
158        // AND-fold as a short-circuit — same defence as
159        // FixedUInt's `const_ct_select`.
160        subtle::Choice::from(core::hint::black_box(acc.unwrap_u8()))
161    }
162}
163
164// ── subtle::ConditionallySelectable ──
165
166impl<T, const CAP: usize, P: Personality> subtle::ConditionallySelectable
167    for HeaplessBigInt<T, CAP, P>
168where
169    T: MachineWord + subtle::ConditionallySelectable,
170{
171    fn conditional_select(a: &Self, b: &Self, choice: subtle::Choice) -> Self {
172        // Output `len = max(a.len, b.len)`. Both operand lens are public
173        // shape parameters, so their `max` is public — never derived
174        // from `choice`. Per-limb select up to that bound; the tails
175        // beyond each operand's own len are zero (zero-tail invariant),
176        // so per-limb select on the tails yields zero regardless of
177        // `choice`, and the output's tail past `out_len` stays zero.
178        let out_len = core::cmp::max(a.len, b.len);
179        let mut limbs = [super::zero::<T>(); CAP];
180        let mut i = 0;
181        while i < out_len as usize {
182            limbs[i] = <T as subtle::ConditionallySelectable>::conditional_select(
183                &a.limbs[i],
184                &b.limbs[i],
185                choice,
186            );
187            i += 1;
188        }
189        Self {
190            limbs,
191            len: out_len,
192            _p: PhantomData,
193        }
194    }
195}
196
197// ── const_num_traits::CtIsZero ──
198
199impl<T, const CAP: usize, P: Personality> const_num_traits::ops::ct::CtIsZero
200    for HeaplessBigInt<T, CAP, P>
201where
202    T: MachineWord + subtle::ConstantTimeEq,
203{
204    fn ct_is_zero(&self) -> subtle::Choice {
205        let n = self.len as usize;
206        let mut acc = subtle::Choice::from(1u8);
207        let mut i = 0;
208        while i < n {
209            acc &= self.limbs[i].ct_eq(&<T as const_num_traits::ConstZero>::ZERO);
210            i += 1;
211        }
212        subtle::Choice::from(core::hint::black_box(acc.unwrap_u8()))
213    }
214}
215
216// ── subtle::ConstantTimeGreater / ConstantTimeLess ──
217
218impl<T, const CAP: usize, P: Personality> subtle::ConstantTimeGreater for HeaplessBigInt<T, CAP, P>
219where
220    T: MachineWord + subtle::ConstantTimeEq + subtle::ConstantTimeGreater,
221{
222    fn ct_gt(&self, other: &Self) -> subtle::Choice {
223        // MSB-to-LSB scan across `max(a.len, b.len)`. `undecided` locks
224        // the answer at the first differing limb without a data-dependent
225        // branch — every iteration always executes.
226        let n = core::cmp::max(self.len, other.len) as usize;
227        let mut gt = subtle::Choice::from(0u8);
228        let mut undecided = subtle::Choice::from(1u8);
229        let mut i = n;
230        while i > 0 {
231            i -= 1;
232            let gt_here = self.limbs[i].ct_gt(&other.limbs[i]);
233            let eq_here = self.limbs[i].ct_eq(&other.limbs[i]);
234            gt |= undecided & gt_here;
235            undecided &= eq_here;
236        }
237        gt
238    }
239}
240
241// `ConstantTimeLess` is derived from `ConstantTimeEq` + `ConstantTimeGreater`
242// via its default methods; the empty impl is enough to opt in.
243impl<T, const CAP: usize, P: Personality> subtle::ConstantTimeLess for HeaplessBigInt<T, CAP, P> where
244    T: MachineWord + subtle::ConstantTimeEq + subtle::ConstantTimeGreater
245{
246}