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}