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}