From e697e10762735d571603bfabe83b48a675c680a8 Mon Sep 17 00:00:00 2001 From: Gustavo Banegas Date: Fri, 24 Apr 2026 08:05:31 +0200 Subject: [PATCH 1/4] Order point as an "outside" function. --- ec/src/lib.rs | 4 + ec/src/point_order_ops.rs | 361 ++++++++++++++++++++++++++++++++ ec/src/point_twisted_hessian.rs | 11 +- ec/src/point_weierstrass.rs | 17 ++ 4 files changed, 385 insertions(+), 8 deletions(-) create mode 100644 ec/src/point_order_ops.rs diff --git a/ec/src/lib.rs b/ec/src/lib.rs index 8d729a2..303e04d 100644 --- a/ec/src/lib.rs +++ b/ec/src/lib.rs @@ -28,3 +28,7 @@ pub mod curve_legendre; pub mod curve_twisted_hessian; /// Projective points on twisted Hessian curves. pub mod point_twisted_hessian; +/// Shared small-integer utilities (sieve, trial factorization). +pub mod num_utils; +/// Order computations for points (Sutherland 2007). +pub mod point_order_ops; diff --git a/ec/src/point_order_ops.rs b/ec/src/point_order_ops.rs new file mode 100644 index 0000000..eec679f --- /dev/null +++ b/ec/src/point_order_ops.rs @@ -0,0 +1,361 @@ +//! Order computations for points on elliptic curves. +//! +//! # Problem +//! +//! Given a curve $E/\mathbb{F}_q$ and a point $P \in E(\mathbb{F}_q)$, we want +//! to compute the **order** of $P$: the smallest integer $n \ge 1$ such that +//! +//! $$ +//! [n] P = O. +//! $$ +//! +//! Following Sutherland (2007), we expose two regimes: +//! +//! | Regime | Input given | Cost | +//! |-----------------------|--------------------------------------------------------------------------------------|------------------------------------------------------------------| +//! | Known group order | $N = \#E(\mathbb{F}_q)$ and its factorization | $O(k \log N)$ scalar muls, where $k = \omega(N)$ | +//! | Generic (Hasse bound) | Interval $[\ell_{lo}, \ell_{hi}]$ with $\lvert P \rvert \in [\ell_{lo}, \ell_{hi}]$ | $\widetilde O\!\left(\sqrt{\ell_{hi} - \ell_{lo}}\right)$ | +//! +//! # When to use which +//! +//! - If you already know $N = \#E(\mathbb{F}_q)$ (e.g. via SEA in the isogeny +//! crate), use [`order_from_group_order`]. This is the usual cryptographic +//! case and runs in time polynomial in $\log q$. +//! - Otherwise, bracket $\lvert P \rvert$ by the Hasse interval +//! $[q + 1 - 2\sqrt q,\; q + 1 + 2\sqrt q]$ and call +//! [`order_in_interval`]. +//! +//! # On the complexity claim +//! +//! The present [`order_in_interval`] gives the textbook $\widetilde O(\sqrt W)$ +//! where $W = \ell_{hi} - \ell_{lo}$ is the Hasse width (so $W \in O(\sqrt q)$ +//! and the total cost is $\widetilde O(q^{1/4})$). +//! +//! Sutherland (2007, §5) shows this can be refined to +//! $\widetilde O\!\left(\sqrt{\ell_{\max}(\lvert P \rvert)}\right)$ by +//! peeling off the smooth part of $\lvert P \rvert$ before running BSGS, +//! which is strictly faster whenever $\lvert P \rvert$ has any nontrivial +//! smooth factor. That optimisation is non-trivial to get right — a naive +//! "test $\ell$-divisibility on the residual" does not work from a Hasse +//! interval alone — and is deferred to a follow-up implementation. See +//! the `TODO(sutherland-phase-1)` note at the bottom of this module. +//! +//! # Model coverage +//! +//! The algorithms are written against [`PointOps`] and [`PointAdd`] so they +//! work for every curve model in this crate that supports full group +//! addition: Weierstrass, Edwards, Hessian (and twisted Hessian), Jacobi +//! quartic, Jacobi intersection, and Legendre. +//! +//! [`order_in_interval`] additionally requires `P: Eq + Hash` for the +//! baby-step / giant-step table. Deriving `Hash` on the point structs is a +//! one-line change wherever the underlying field element type is `Hash` +//! (which is the case for `Uint`-backed `FpElement`). +//! +//! Montgomery / Kummer points (x-only) do **not** implement [`PointAdd`] and +//! need a dedicated x-only variant using the pseudo-addition ladder; that +//! specialisation is left for a follow-up. + +use crate::num_utils::{product_of_factors, trial_factor}; +use crate::point_ops::{PointAdd, PointOps}; +use crypto_bigint::{NonZero, Uint}; +use std::collections::HashMap; +use std::hash::Hash; + +// --------------------------------------------------------------------------- +// Case A: known group order (fast path, O(log q)) +// --------------------------------------------------------------------------- + +/// Given a point `P`, the group order `N = #E(𝔽_q)`, and the prime +/// factorization of `N` as `[(p₁, e₁), …, (p_k, e_k)]`, return the exact +/// order of `P`. +/// +/// # Algorithm +/// +/// Since $\lvert P \rvert \mid N$ (Lagrange), start from $n \leftarrow N$ +/// and, for each prime factor $p_i$ of $N$, strip as many copies of $p_i$ +/// from $n$ as possible while still satisfying $[n] P = O$: +/// +/// ```text +/// n ← N +/// for each (pᵢ, eᵢ) in factors: +/// for j = 1 .. eᵢ: +/// if [n / pᵢ] P = O: +/// n ← n / pᵢ +/// else: +/// break +/// return n +/// ``` +/// +/// The final `n` is the exact order of `P`. +/// +/// # Cost +/// +/// $O(k log N)$ scalar multiplications, where $k = \omega(N)$ is the number +/// of distinct prime factors of `N`. +/// +/// +pub fn order_from_group_order( + point: &P, + curve: &P::Curve, + n_group: &Uint, + factors: &[(Uint, u32)], +) -> Uint +where + P: PointOps, +{ + debug_assert_eq!(product_of_factors::(factors), *n_group); + + let mut n = *n_group; + + // For each (pᵢ, eᵢ), peel copies of pᵢ off n while [n/pᵢ] P = O. + // At most eᵢ successful peels per prime ⇒ O(∑ eᵢ) = O(log N) scalar + // multiplications total. + for (p, _e) in factors { + let p_nz = NonZero::new(*p).expect("prime factor must be nonzero"); + loop { + // q = n / p. If p does not divide n we stop (shouldn't + // happen for a prime factor of N, but guards bad input). + let (q, rem) = n.div_rem(&p_nz); + if !bool::from(rem.is_zero()) { + break; + } + let q_point = point.scalar_mul(q.as_words(), curve); + if q_point.is_identity() { + n = q; + } else { + break; + } + } + } + + n +} + +// --------------------------------------------------------------------------- +// Case B: generic order in a bracket (BSGS, O(√W)) +// --------------------------------------------------------------------------- + +/// Compute the order of `P` knowing only a bracket $[\ell_{lo}, \ell_{hi}]$ +/// that contains it. +/// +/// For an elliptic curve over $\mathbb{F}_q$ the natural bracket is the +/// **Hasse interval**: +/// +/// $$ +/// \lvert P \rvert \in [\,q + 1 - 2\sqrt q,\; q + 1 + 2\sqrt q\,]. +/// $$ +/// +/// # Algorithm (baby-step / giant-step) +/// +/// Let $W = \ell_{hi} - \ell_{lo}$ and $w = \lceil \sqrt W \rceil$. +/// We look for a pair $(i, j)$ with $0 \le i, j \le w$ satisfying +/// +/// $$ +/// [\ell_{lo} + i \cdot w - j] P = O, +/// $$ +/// +/// which is equivalent to the collision +/// +/// $$ +/// [\ell_{lo} + i \cdot w]\, P \;=\; [j]\, P +/// $$ +/// +/// in the group. The baby table $\{[j] P : 0 \le j \le w\}$ is built +/// once; giant steps walk from $[\ell_{lo}] P$ in strides of $[w] P$ +/// until a collision is found. +/// +/// ```text +/// w ← ⌈√(\ell_hi − \ell_lo)⌉ +/// table ← { [j] P ↦ j : 0 ≤ j ≤ w } +/// Q ← [\ell_lo] P +/// for i = 0 .. w: +/// if Q ∈ table with value j: +/// return reduce_order( \ell_lo + i·w − j ) +/// Q ← Q + [w] P +/// ``` +/// +/// The returned value $c = \ell_{lo} + i \cdot w - j$ need not itself be +/// the order; it is only guaranteed to be a multiple of it inside the +/// bracket. We finish by factoring $c$ and running the Case-A peel. +/// +/// # Cost +/// +/// `O(\sqrtW)` point additions and a hash table of the same size for the +/// BSGS itself. Phase 3 additionally trial-factors the resulting +/// witness `c ≤ hi`, which costs `O(\sqrtc · M)` big-int operations — +/// practical only when `hi` fits in roughly 80 bits. +/// +/// In cryptographic settings ($q \ge 2^{128}$) you should **always** +/// prefer [`order_from_group_order`], feeding in a factorization you +/// obtained separately (e.g. from SEA). `order_in_interval` is +/// intended for testing, for small-field experimentation, and as the +/// raw building block that the future Sutherland smoothness +/// optimisation will wrap. +/// +/// # Parameters +/// +/// - `point` — the point `P` whose order we want +/// - `curve` — the curve it lives on +/// - `lo, hi` — bracket containing `|P|` +/// +/// # Returns +/// +/// The exact order `|P|`. +/// +/// # Panics +/// +/// - If `[k] P ≠ O` for every `k ∈ [lo, hi]`, i.e. the bracket is wrong. +/// - If the BSGS table size would exceed `u64::MAX` (unreachable in +/// practice: for any Hasse interval of a curve over a field fitting in +/// `Uint`, $\sqrt W$ fits comfortably in 64 bits). +pub fn order_in_interval( + point: &P, + curve: &P::Curve, + lo: &Uint, + hi: &Uint, +) -> Uint +where + P: PointAdd + Eq + Hash, +{ + // Giant stride w = ⌈\sqrt(hi − lo)⌉ . + let width = hi.wrapping_sub(lo); + let w_floor = width.floor_sqrt_vartime(); + let w_uint = if w_floor.wrapping_mul(&w_floor) == width { + w_floor + } else { + w_floor.wrapping_add(&Uint::::ONE) + }; + + // BSGS table size fits in u64 for any realistic q — width ≲ 4\sqrtq + // so \sqrtwidth ≲ 2 · q^{1/4}, which is < 2^64 for q < 2^254. + debug_assert!( + w_uint.as_words()[1..].iter().all(|&w| w == 0), + "BSGS table size would exceed u64; bracket is unrealistically large" + ); + let w: u64 = w_uint.as_words()[0]; + + // BSGS: find candidate multiple of |P| in the bracket. + let candidate = bsgs_find_multiple::(point, curve, lo, w); + + // Reduce to the exact order by factoring and peeling. + let factors = trial_factor::(&candidate); + order_from_group_order::(point, curve, &candidate, &factors) +} + +// --------------------------------------------------------------------------- +// BSGS (Case-B helper) +// --------------------------------------------------------------------------- +// +// Given `P` and a bracket `[lo, lo + w²]` claimed to contain `|P|`, +// find a value c ∈ [lo, lo + w²] with [c] P = O. +// +// Standard BSGS: +// +// baby[j] = [j] P for j = 0, 1, …, w +// giant_i = [lo + i·w] P for i = 0, 1, …, w +// +// On a collision giant_i == baby[j] we have [lo + i·w - j] P = O, +// so `c = lo + i·w - j` is a valid multiple of the order within the +// bracket (Phase 3 reduces it to the exact order). + +fn bsgs_find_multiple( + point: &P, + curve: &P::Curve, + lo: &Uint, + w: u64, +) -> Uint +where + P: PointAdd + Eq + Hash, +{ + // ---- Baby steps: j · P for j ∈ [0, w] ---- + // + // When the true order |P| is smaller than w, the baby table contains + // duplicate keys (e.g. [0]P = [|P|]P = [2|P|]P = ...). We keep the + // smallest `j` on collision via `.or_insert`; a `HashMap::insert` that + // overwrote would also be correct, but produces slightly larger `c` + // values for Phase 3 to factor. + // + // Including j = w is needed so that the range of representable + // candidates `lo + i·w − j` covers every integer in [lo, lo + w²]. + + let mut table: HashMap = HashMap::with_capacity(w as usize + 1); + let mut jp = P::identity(curve); + for j in 0..=w { + table.entry(jp.clone()).or_insert(j); + jp = jp.add(point, curve); + } + + // ---- Giant steps: start at [lo] P, stride by [w] P ---- + // + // On a collision we compute c = lo + i·w − j. There are a few + // cases to handle carefully: + // + // (1) `lo + i·w < j`: the (signed) candidate is negative — this + // just means the multiple of |P| lying in the search lattice + // is below `lo`. Skip and keep walking. + // (2) `c = 0`: the collision witnesses a trivial relation like + // `[lo]P = [lo]P`. No information about the order. Skip. + // (3) `c > 0`: a valid positive multiple of |P| within at most + // `lo + w²` of `lo`. Return it; Phase 3 will reduce to the + // exact order by trial-factor + peel. + // + // Case (2) is common when |P| is small enough that the baby table + // contains duplicates: multiple `j` map to the same point. The + // `.or_insert` above keeps the smallest `j`, which then produces + // `c = 0` at `i = 0`; we simply keep walking until `i` is large + // enough that `lo + i·w` is a genuine multiple of |P|. + + let w_uint = Uint::::from(w); + let stride = point.scalar_mul(&[w], curve); + let mut cur = point.scalar_mul(lo.as_words(), curve); + + for i in 0..=w { + if let Some(&j) = table.get(&cur) { + // Candidate c = lo + i·w − j . + let i_w = Uint::::from(i).wrapping_mul(&w_uint); + let plus = lo.wrapping_add(&i_w); + let j_uint = Uint::::from(j); + + // Cases (1) and (2): c ≤ 0 . Skip. + if j_uint < plus { + let c = plus.wrapping_sub(&j_uint); + if c != Uint::::ZERO { + // Case (3): valid positive multiple of |P|. + return c; + } + } + // Fall through — collision did not yield a usable c. + } + cur = cur.add(&stride, curve); + } + + panic!("BSGS exhausted with no collision — bracket [lo, hi] is incorrect"); +} + +// --------------------------------------------------------------------------- +// Follow-up: Sutherland (2007, §5) smoothness optimisation +// --------------------------------------------------------------------------- +// +// The present `order_in_interval` does plain BSGS over the whole +// Hasse width and achieves O(q^{1/4}) in the worst case. +// +// Sutherland shows that by handling small-prime factors of |P| +// separately one can reduce the BSGS width to O(\sqrt\ell_max(|P|)) , +// which is much less than O(q^{1/4}) whenever |P| has any small +// prime factor. The algorithm splits |P| = m t with \ell_max(m) \leq B +// and searches for m and t independently. +// +// A correct implementation requires: (a) a divisibility oracle that +// does not assume |P| divides any fixed round number, (b) careful +// lifting between the \ell-torsion and the full group, and (c) the +// orchestration described in Sutherland's Algorithm 5.1. +// +// Tracked here so the TODO is discoverable from the module root. +// +// TODO(sutherland-phase-1): +// - Implement order_in_interval_smooth(...) +// - Match the I(sqrt(\elll)_max(|P|)) bound +// - Add reference tests against known-order curves (e.g. P-256, where +// |E(F_p)| is prime so the new algorithm degenerates to the current +// one and must agree bit-for-bit). diff --git a/ec/src/point_twisted_hessian.rs b/ec/src/point_twisted_hessian.rs index 44e52c6..be652a4 100644 --- a/ec/src/point_twisted_hessian.rs +++ b/ec/src/point_twisted_hessian.rs @@ -13,10 +13,9 @@ use core::fmt; use subtle::{Choice, ConditionallySelectable, ConstantTimeEq}; -use crate::curve_ops::Curve; use crate::curve_twisted_hessian::TwistedHessianCurve; use crate::point_ops::{PointAdd, PointOps}; -use fp::field_ops::{FieldOps, FieldRandom}; +use fp::field_ops::{FieldOps}; use fp::{ref_field_impl, ref_field_trait_impl}; /// A projective point `(X:Y:Z)` on a twisted Hessian curve. @@ -189,7 +188,7 @@ ref_field_trait_impl! { } ref_field_impl! { - impl TwistedHessianPoint { + impl TwistedHessianPoint { /// Negation on a twisted Hessian curve: /// /// $$-(X:Y:Z) = (X:Z:Y).$$ @@ -240,10 +239,6 @@ ref_field_impl! { if other.is_identity() { return self.clone(); } - - debug_assert!(curve.is_on_curve(self)); - debug_assert!(curve.is_on_curve(other)); - let r = self.add_formula_1(other); if !r.is_zero_projective() { return r; @@ -313,4 +308,4 @@ ref_field_trait_impl! { TwistedHessianPoint::::add(self, other, curve) } } -} +} \ No newline at end of file diff --git a/ec/src/point_weierstrass.rs b/ec/src/point_weierstrass.rs index 31cdfcb..a3632ad 100644 --- a/ec/src/point_weierstrass.rs +++ b/ec/src/point_weierstrass.rs @@ -93,6 +93,23 @@ where { } +impl core::hash::Hash for AffinePoint +where + F: FieldOps + ConstantTimeEq + core::hash::Hash, +{ + // Must agree with `PartialEq`: two identities compare equal regardless + // of their (unused) `x`, `y` fields, so they must hash the same. + // We fold `infinity` in first and only hash coordinates for finite + // points. + fn hash(&self, state: &mut H) { + self.infinity.hash(state); + if !self.infinity { + self.x.hash(state); + self.y.hash(state); + } + } +} + // --------------------------------------------------------------------------- // Constructors // --------------------------------------------------------------------------- From 8df841252abb100829602935c6232711cf6a8d43 Mon Sep 17 00:00:00 2001 From: Gustavo Banegas Date: Fri, 24 Apr 2026 08:06:35 +0200 Subject: [PATCH 2/4] Order point as an "outside" function. Added "hash" of elements. --- ec/src/num_utils.rs | 273 ++++++++++++++++ ec/tests/point_order_ops_tests.rs | 526 ++++++++++++++++++++++++++++++ fp/src/f2_element.rs | 2 +- fp/src/fp_element.rs | 14 + 4 files changed, 814 insertions(+), 1 deletion(-) create mode 100644 ec/src/num_utils.rs create mode 100644 ec/tests/point_order_ops_tests.rs diff --git a/ec/src/num_utils.rs b/ec/src/num_utils.rs new file mode 100644 index 0000000..a464d25 --- /dev/null +++ b/ec/src/num_utils.rs @@ -0,0 +1,273 @@ +//! Small-integer utilities shared by the `ec` crate. +//! +//! These are deliberately simple, `alloc`-using helpers intended for +//! auxiliary computations like order determination and point-count +//! post-processing — **not** for the hot inner loops of scalar +//! multiplication. They operate on `u64` for compactness and on +//! [`Uint`](crypto_bigint::Uint) for interop with the rest of the +//! stack. +//! +//! # Contents +//! +//! | Function | What it does | +//! |-------------------------|----------------------------------------------------| +//! | [`SmallPrimes::up_to`] | Sieve of Eratosthenes, lazy iterator over primes | +//! | [`trial_factor`] | Prime factorization by trial division | +//! | [`product_of_factors`] | Multiplies out `[(pᵢ, eᵢ)]` (debug-assert helper) | + +use crypto_bigint::{NonZero, Uint}; + +// --------------------------------------------------------------------------- +// SmallPrimes — sieve of Eratosthenes over [2, bound] +// --------------------------------------------------------------------------- + +/// Iterator yielding the primes `ℓ ≤ bound`, in increasing order. +/// +/// Construction is $O(B \log \log B)$ time and $O(B / 8)$ memory (one +/// bit per odd integer). The iterator itself then yields each prime +/// in amortised $O(1)$ by advancing a cursor over the sieve. +/// +/// Intended for `bound` up to roughly $2^{20}$; beyond that you should +/// prefer a segmented sieve. +/// +/// # Example +/// +/// ```text +/// let small: Vec = SmallPrimes::up_to(20).collect(); +/// // small == [2, 3, 5, 7, 11, 13, 17, 19] +/// ``` +pub struct SmallPrimes { + // `sieve[i] = true` ⇔ (2i + 1) is composite, except for the + // explicit special case of 2 handled by `yielded_two`. + // We sieve only odd numbers to halve memory. + sieve: Vec, + // Cursor in odd-number space: next candidate is 2·cursor + 1. + cursor: usize, + // Inclusive upper bound in odd-number space. + odd_limit: usize, + // Whether we have already emitted the prime 2. + yielded_two: bool, +} + +impl SmallPrimes { + /// Build the sieve for primes `≤ bound`. + pub fn up_to(bound: u64) -> Self { + let bound_us = bound as usize; + + // Odd-number space: index i ↔ value 2i + 1. + // Max odd value we need is `bound` (if bound is odd) or + // `bound - 1`. The index for (2i+1 = bound) is i = (bound-1)/2. + let odd_limit = if bound < 3 { 0 } else { (bound_us - 1) / 2 }; + + let mut sieve = vec![false; odd_limit + 1]; + // Index 0 ↔ value 1, which is not prime. + sieve[0] = true; + + // Cross out odd composites. For each prime p = 2i+1 with p·p ≤ bound, + // mark p·p, p·(p+2), p·(p+4), … . In odd-index space the step is p. + let mut i = 1usize; + while { + let p = 2 * i + 1; + p.saturating_mul(p) <= bound_us + } { + if !sieve[i] { + let p = 2 * i + 1; + // First composite to cross out is p*p; its index is (p*p - 1)/2. + let mut j = (p * p - 1) / 2; + while j <= odd_limit { + sieve[j] = true; + j += p; // step 2p in value-space = p in odd-index space + } + } + i += 1; + } + + Self { + sieve, + cursor: 0, + odd_limit, + yielded_two: bound < 2, + } + } +} + +impl Iterator for SmallPrimes { + type Item = u64; + + fn next(&mut self) -> Option { + // Emit 2 first. + if !self.yielded_two { + self.yielded_two = true; + return Some(2); + } + + // Then scan odd indices ≥ 1 (skipping index 0 which is the value 1). + if self.cursor == 0 { + self.cursor = 1; + } + while self.cursor <= self.odd_limit { + if !self.sieve[self.cursor] { + let value = (2 * self.cursor + 1) as u64; + self.cursor += 1; + return Some(value); + } + self.cursor += 1; + } + None + } +} + +// --------------------------------------------------------------------------- +// trial_factor — `n = ∏ pᵢ^eᵢ` via trial division +// --------------------------------------------------------------------------- + +/// Trial-factor `n` into `[(pᵢ, eᵢ)]` with `pᵢ` prime and `∏ pᵢ^eᵢ = n`. +/// +/// # Algorithm +/// +/// Sieve all primes up to $\sqrt n$ and divide them out one at a time. +/// Any residue greater than 1 after the loop is itself a prime factor +/// (because it has no prime factor $\le \sqrt n$). +/// +/// # Complexity +/// +/// $O(\pi(\sqrt n) \cdot M)$ where $M$ is the cost of a big-int division. +/// +/// # When this is acceptable +/// +/// Trial division is only sensible when $n$ is known to be small or +/// smooth — which is exactly the situation in order computations +/// (Sutherland's Phase 3 residue is bounded by $\sqrt{\text{Hasse width}}$). +/// +/// # Returns +/// +/// Empty vector for `n = 1`. Otherwise a list of `(prime, exponent)` +/// pairs with prime factors in ascending order. +pub fn trial_factor(n: &Uint) -> Vec<(Uint, u32)> { + if n == &Uint::::ONE { + return Vec::new(); + } + + let mut residue = *n; + let mut out: Vec<(Uint, u32)> = Vec::new(); + + // Bound the sieve by ⌈√n⌉ + 1, capped at u64::MAX so we can sieve + // with u64 primes. If n exceeds 2^128 this becomes impractical — + // but for the intended uses (factor residues of size ≈ √(Hasse width)) + // this is always fine. + let sqrt_n = n.floor_sqrt_vartime(); + let sqrt_cap: u64 = if sqrt_n.as_words()[1..].iter().all(|&w| w == 0) { + sqrt_n.as_words()[0].saturating_add(1) + } else { + u64::MAX + }; + + for p in SmallPrimes::up_to(sqrt_cap) { + let p_uint = Uint::::from(p); + let p_nz = NonZero::new(p_uint).expect("p ≥ 2 is nonzero"); + + let mut exp: u32 = 0; + loop { + let (q, r) = residue.div_rem(&p_nz); + if bool::from(r.is_zero()) { + residue = q; + exp += 1; + } else { + break; + } + } + if exp > 0 { + out.push((p_uint, exp)); + if residue == Uint::::ONE { + return out; + } + } + } + + // Any residue > 1 is itself prime. + if residue != Uint::::ONE { + out.push((residue, 1)); + } + out +} + +// --------------------------------------------------------------------------- +// product_of_factors — inverse of `trial_factor`, for consistency checks +// --------------------------------------------------------------------------- + +/// Multiplies out `[(pᵢ, eᵢ)]` to produce `∏ pᵢ^eᵢ`. +/// +/// Used mainly in `debug_assert!` blocks to check that a caller-supplied +/// factorization matches the claimed product. +pub fn product_of_factors(factors: &[(Uint, u32)]) -> Uint { + let mut acc = Uint::::ONE; + for (p, e) in factors { + for _ in 0..*e { + acc = acc.wrapping_mul(p); + } + } + acc +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use crypto_bigint::U64; + + #[test] + fn sieve_small() { + let got: Vec = SmallPrimes::up_to(20).collect(); + assert_eq!(got, vec![2, 3, 5, 7, 11, 13, 17, 19]); + } + + #[test] + fn sieve_edge_cases() { + assert_eq!(SmallPrimes::up_to(0).collect::>(), vec![]); + assert_eq!(SmallPrimes::up_to(1).collect::>(), vec![]); + assert_eq!(SmallPrimes::up_to(2).collect::>(), vec![2]); + assert_eq!(SmallPrimes::up_to(3).collect::>(), vec![2, 3]); + } + + #[test] + fn factor_small_composite() { + // 360 = 2^3 · 3^2 · 5 + let n = U64::from(360u64); + let f = trial_factor(&n); + assert_eq!( + f, + vec![ + (U64::from(2u64), 3), + (U64::from(3u64), 2), + (U64::from(5u64), 1), + ] + ); + assert_eq!(product_of_factors(&f), n); + } + + #[test] + fn factor_prime() { + // 101 is prime. + let n = U64::from(101u64); + let f = trial_factor(&n); + assert_eq!(f, vec![(U64::from(101u64), 1)]); + } + + #[test] + fn factor_one() { + let n = U64::ONE; + let f: Vec<(U64, u32)> = trial_factor(&n); + assert!(f.is_empty()); + } + + #[test] + fn factor_prime_squared() { + // 49 = 7^2 — trips the "residue > sqrt" branch if exp logic is off. + let n = U64::from(49u64); + let f = trial_factor(&n); + assert_eq!(f, vec![(U64::from(7u64), 2)]); + } +} diff --git a/ec/tests/point_order_ops_tests.rs b/ec/tests/point_order_ops_tests.rs new file mode 100644 index 0000000..ac6a88e --- /dev/null +++ b/ec/tests/point_order_ops_tests.rs @@ -0,0 +1,526 @@ +//! Integration tests for `ec::point_order_ops`. +//! +//! Covers Sutherland (2007): +//! +//! * [`order_from_group_order`] — fast path when `N = #E(F_q)` is known +//! * [`order_in_interval`] — generic BSGS on the Hasse interval +//! +//! # Fixture +//! +//! * Two distinct primes (`2` and `3`) with a squared factor → exercises +//! the inner peeling loop of [`order_from_group_order`]. +//! * Full order spectrum represented in the group: +//! +//! | order | count | role | +//! |-------|-------|----------------------| +//! | 1 | 1 | identity `O` | +//! | 2 | 1 | 2-torsion | +//! | 3 | 2 | 3-torsion | +//! | 6 | 2 | 6-torsion | +//! | 9 | 6 | inside Hasse bracket | +//! | 18 | 6 | generators | +//! +//! * Hasse interval `[q+1-⌈2√q⌉, q+1+⌈2√q⌉] = [10, 26]` for `q = 17`, +//! which contains orders `18` and `9` — so BSGS (`w = ⌈√16⌉ = 4`) has +//! real work to do. +//! +//! Every test that needs the "true" order of a point gets it via brute-force +//! repeated addition (legal here because |P| ≤ 18), so our reference is +//! independent of the algorithms under test. + +use crypto_bigint::{Uint, const_prime_monty_params}; + +use fp::fp_element::FpElement; + +use ec::curve_edwards::EdwardsCurve; +use ec::curve_weierstrass::WeierstrassCurve; +use ec::num_utils::{SmallPrimes, product_of_factors, trial_factor}; +use ec::point_edwards::EdwardsPoint; +use ec::point_order_ops::{order_from_group_order, order_in_interval}; +use ec::point_weierstrass::AffinePoint; + +// --------------------------------------------------------------------------- +// Test fixture +// --------------------------------------------------------------------------- + +const_prime_monty_params!(Fp17Mod, Uint<1>, "0000000000000011", 3); +type F17 = FpElement; + +fn fp(n: u64) -> F17 { + F17::from_u64(n) +} + +/// `y² = x³ + 1` over `F₁₇` +fn curve() -> WeierstrassCurve { + WeierstrassCurve::new_short(fp(0), fp(1)) +} + +/// Expected group order `#E(F₁₇) = 18` +fn group_order() -> Uint<1> { + Uint::<1>::from(18u64) +} + +/// Expected factorization `18 = 2 · 3²` +fn group_order_factors() -> Vec<(Uint<1>, u32)> { + vec![(Uint::<1>::from(2u64), 1), (Uint::<1>::from(3u64), 2)] +} + +/// Hasse bracket for `q = 17`: `[10, 26]` (width 16, `w = 4`). +fn hasse_interval() -> (Uint<1>, Uint<1>) { + (Uint::<1>::from(10u64), Uint::<1>::from(26u64)) +} + +/// All affine points on the fixture curve (brute force over F₁₇). +/// +/// Does **not** include the point at infinity — callers append it if +/// they want to test the identity too. +fn all_affine_points() -> Vec<(u64, u64)> { + let p = 17u64; + let mut pts = Vec::new(); + for x in 0..p { + // rhs = x³ + 1 (mod p) + let rhs = (x.wrapping_mul(x) % p * x % p + 1) % p; + for y in 0..p { + if (y.wrapping_mul(y)) % p == rhs { + pts.push((x, y)); + } + } + } + pts +} + +/// All points including `O`, as `AffinePoint` values. +fn all_points() -> Vec> { + let mut v: Vec> = all_affine_points() + .into_iter() + .map(|(x, y)| AffinePoint::new(fp(x), fp(y))) + .collect(); + v.push(AffinePoint::::identity()); + v +} + +/// Brute-force order: smallest `k ≥ 1` with `[k] P = O`, via repeated +/// addition. Only acceptable because |P| ≤ 18 on this fixture. +fn brute_force_order(p: &AffinePoint, c: &WeierstrassCurve) -> u64 { + if p.is_identity() { + return 1; + } + let mut acc = *p; + let mut k: u64 = 1; + while !acc.is_identity() { + acc = acc.add(p, c); + k += 1; + assert!(k < 100, "runaway in brute-force order"); + } + k +} + +// --------------------------------------------------------------------------- +// Fixture self-tests — fail early if the curve equation ever changes +// --------------------------------------------------------------------------- + +#[test] +fn fixture_has_expected_point_count() { + // 17 affine points on y² = x³ + 1 over F₁₇, plus the point at + // infinity, for a total of #E(F₁₇) = 18. + let n = all_affine_points().len() as u64 + 1; + assert_eq!( + n, 18, + "fixture curve should have exactly 18 points, got {}", + n + ); +} + +#[test] +fn fixture_has_expected_order_distribution() { + use std::collections::BTreeMap; + let c = curve(); + let mut dist: BTreeMap = BTreeMap::new(); + for p in all_points() { + *dist.entry(brute_force_order(&p, &c)).or_insert(0) += 1; + } + // Expected: {1:1, 2:1, 3:2, 6:2, 9:6, 18:6} + let expected: BTreeMap = [(1, 1), (2, 1), (3, 2), (6, 2), (9, 6), (18, 6)] + .into_iter() + .collect(); + assert_eq!(dist, expected, "order distribution regression"); +} + +// --------------------------------------------------------------------------- +// num_utils at the integration level +// --------------------------------------------------------------------------- + +#[test] +fn num_utils_sieve_agrees_with_hand_list() { + let primes_up_to_30: Vec = SmallPrimes::up_to(30).collect(); + assert_eq!(primes_up_to_30, vec![2, 3, 5, 7, 11, 13, 17, 19, 23, 29]); +} + +#[test] +fn num_utils_factors_group_order() { + let n = Uint::<1>::from(18u64); + let f = trial_factor(&n); + assert_eq!(f, group_order_factors()); + assert_eq!(product_of_factors(&f), n); +} + +#[test] +fn num_utils_round_trips_every_small_int() { + // For every n in [1, 50], trial_factor then product_of_factors must + // return n. Catches off-by-ones in either direction. + for n_u64 in 1u64..=50 { + let n = Uint::<1>::from(n_u64); + let f = trial_factor(&n); + assert_eq!( + product_of_factors(&f), + n, + "round-trip failed on n = {}", + n_u64 + ); + } +} + +// --------------------------------------------------------------------------- +// Case A: order_from_group_order +// --------------------------------------------------------------------------- + +#[test] +fn case_a_identity_has_order_one() { + let c = curve(); + let n = group_order(); + let factors = group_order_factors(); + + let id = AffinePoint::::identity(); + let ord = order_from_group_order(&id, &c, &n, &factors); + assert_eq!(ord, Uint::<1>::from(1u64), "order of O should be 1"); +} + +#[test] +fn case_a_every_point_matches_brute_force() { + let c = curve(); + let n = group_order(); + let factors = group_order_factors(); + + for p in all_points() { + let expected = brute_force_order(&p, &c); + let got = order_from_group_order(&p, &c, &n, &factors); + assert_eq!( + got, + Uint::<1>::from(expected), + "point {}: brute-force = {}, order_from_group_order = {:?}", + p, + expected, + got, + ); + // Sanity: every order must divide 18. + assert_eq!(18 % expected, 0, "order {} does not divide 18", expected); + } +} + +#[test] +fn case_a_finds_a_generator() { + // At least one point must be reported as order 18 (i.e. a generator). + let c = curve(); + let n = group_order(); + let factors = group_order_factors(); + + let mut found = false; + for p in all_points() { + let ord = order_from_group_order(&p, &c, &n, &factors); + if ord == Uint::<1>::from(18u64) { + found = true; + break; + } + } + assert!(found, "expected at least one generator of order 18"); +} + +const_prime_monty_params!( + Fp25519Mod, + Uint<4>, + "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed", + 2 +); +type F25519 = FpElement; + +fn fp25519(words: [u64; 4]) -> F25519 { + F25519::from_words(words) +} + +/// Untwisted Edwards model birationally equivalent to Curve25519: +/// +/// x² + y² = 1 + d x² y², d = (A - 2)/(A + 2), A = 486662. +/// +/// This is the odd-characteristic Edwards model implemented by +/// `ec::curve_edwards::EdwardsCurve`. +fn curve25519_edwards() -> EdwardsCurve { + // d = 121665 / 121666 mod p + EdwardsCurve::new(fp25519([ + 9949773421441615690, + 18415207549394364244, + 8302596497594521447, + 3313685130627776908, + ])) +} + +/// Prime-order subgroup size of the Curve25519 base point. +fn curve25519_subgroup_order() -> Uint<4> { + Uint::<4>::from_words([ + 6346243789798364141, + 1503914060200516822, + 0, + 1152921504606846976, + ]) +} + +/// Full Curve25519 group order = 8 · ℓ. +fn curve25519_group_order() -> Uint<4> { + Uint::<4>::from_words([ + 13876462170967809896, + 12031312481604134578, + 0, + 9223372036854775808, + ]) +} + +/// Factorization of `#E(F_p) = 2³ · ℓ`, with `ℓ` prime. +fn curve25519_group_order_factors() -> Vec<(Uint<4>, u32)> { + vec![ + (Uint::<4>::from(2u64), 3), + (curve25519_subgroup_order(), 1), + ] +} + +/// Curve25519 Montgomery base point `u = 9`, mapped into the untwisted +/// Edwards model above. +/// +/// We hard-code one of the two sign choices for `x`; both have the same +/// order. The `y` coordinate is `(u - 1)/(u + 1) = 4/5`. +fn curve25519_basepoint_edwards() -> EdwardsPoint { + EdwardsPoint::new( + fp25519([ + 14735460775765454235, + 6707206447363776329, + 440012832734768475, + 1556671287782030463, + ]), + fp25519([ + 7378697629483820632, + 7378697629483820646, + 7378697629483820646, + 7378697629483820646, + ]), + ) +} + +#[test] +fn curve25519_fixture_is_consistent() { + let c = curve25519_edwards(); + let p = curve25519_basepoint_edwards(); + let ell = curve25519_subgroup_order(); + + assert!( + c.contains(&p.x, &p.y), + "mapped Curve25519 base point must lie on the Edwards model", + ); + + let killed = p.scalar_mul(ell.as_words(), &c); + assert!( + killed.is_identity(), + "[ℓ]P must be O for the Curve25519 base point", + ); + + let small = p.scalar_mul(&[8u64], &c); + assert!( + !small.is_identity(), + "base point should not lie in the small cofactor subgroup", + ); +} + +#[test] +fn curve25519_case_a_recovers_prime_subgroup_order() { + let c = curve25519_edwards(); + let p = curve25519_basepoint_edwards(); + let n = curve25519_group_order(); + let factors = curve25519_group_order_factors(); + let ell = curve25519_subgroup_order(); + + let ord = order_from_group_order(&p, &c, &n, &factors); + assert_eq!( + ord, ell, + "Curve25519 base point should have prime order ℓ inside #E(F_p)=8·ℓ", + ); +} + +#[test] +fn curve25519_case_a_order_is_minimal() { + let c = curve25519_edwards(); + let p = curve25519_basepoint_edwards(); + let n = curve25519_group_order(); + let factors = curve25519_group_order_factors(); + let ell = curve25519_subgroup_order(); + + let ord = order_from_group_order(&p, &c, &n, &factors); + assert_eq!(ord, ell); + + let half = ord.wrapping_shr_vartime(1); + let maybe_killed = p.scalar_mul(half.as_words(), &c); + assert!( + !maybe_killed.is_identity(), + "[ℓ/2]P must not be O when |P| = ℓ is an odd prime", + ); +} + +// --------------------------------------------------------------------------- +// Case B: order_in_interval +// --------------------------------------------------------------------------- + +#[test] +fn case_b_matches_brute_force_for_points_in_hasse() { + // For every point whose order lies in the Hasse interval, BSGS must + // recover it exactly. Orders on this fixture inside [10, 26] are + // {18, 9} — that's 12 out of 18 points. + let c = curve(); + let (lo, hi) = hasse_interval(); + + let mut tested = 0; + for p in all_points() { + let true_order = brute_force_order(&p, &c); + if !(10u64..=26).contains(&true_order) { + continue; + } + tested += 1; + + let got = order_in_interval(&p, &c, &lo, &hi); + assert_eq!( + got, + Uint::<1>::from(true_order), + "order_in_interval: point {}: expected {}, got {:?}", + p, + true_order, + got, + ); + } + assert!( + tested >= 6, + "expected ≥6 points in Hasse, tested {}", + tested + ); +} + +#[test] +fn case_b_agrees_with_case_a_on_wide_bracket() { + // With a bracket wide enough to cover every possible order + // ([1, 18]), Case A and Case B must agree on every point. + let c = curve(); + let n = group_order(); + let factors = group_order_factors(); + let lo = Uint::<1>::from(1u64); + let hi = Uint::<1>::from(18u64); + + for p in all_points() { + let ord_a = order_from_group_order(&p, &c, &n, &factors); + let ord_b = order_in_interval(&p, &c, &lo, &hi); + assert_eq!( + ord_a, ord_b, + "Case A ≠ Case B for point {}: A={:?}, B={:?}", + p, ord_a, ord_b, + ); + } +} + +#[test] +fn case_b_tight_brackets_each_order() { + // For each achievable order `k` on this curve, check that Case B + // returns `k` when called with the tight bracket [k, k]. + // A bracket of width 0 gives w = 0, so this also exercises the + // degenerate (but legal) BSGS boundary. + let c = curve(); + for k in [1u64, 2, 3, 6, 9, 18] { + let lo = Uint::<1>::from(k); + let hi = Uint::<1>::from(k); + for p in all_points() { + if brute_force_order(&p, &c) != k { + continue; + } + let got = order_in_interval(&p, &c, &lo, &hi); + assert_eq!( + got, + Uint::<1>::from(k), + "tight bracket [{k},{k}] on order-{k} point {p}: got {got:?}", + ); + // One sample per order is enough. + break; + } + } +} + +// --------------------------------------------------------------------------- +// Post-conditions (property-style) +// --------------------------------------------------------------------------- + +#[test] +fn returned_order_annihilates_point() { + // Whatever order we return, [order] P must be O. + // Catches off-by-one bugs in the peeling loop. + let c = curve(); + let n = group_order(); + let factors = group_order_factors(); + + for p in all_points() { + let ord = order_from_group_order(&p, &c, &n, &factors); + let killed = p.scalar_mul(ord.as_words(), &c); + assert!( + killed.is_identity(), + "[order] P must be O, but wasn't for {p}, order={ord:?}", + ); + } +} + +#[test] +fn returned_order_is_minimal() { + // For every prime `ℓ | order(P)`, [order/ℓ] P must NOT be O. + // This distinguishes "exact order" from "a multiple of the order". + let c = curve(); + let n = group_order(); + let factors = group_order_factors(); + + for p in all_points() { + let ord = order_from_group_order(&p, &c, &n, &factors); + let ord_u64 = ord.as_words()[0]; + if ord_u64 <= 1 { + continue; + } + for (prime, _) in &factors { + let prime_u64 = prime.as_words()[0]; + if ord_u64 % prime_u64 != 0 { + continue; + } + let divisor = ord_u64 / prime_u64; + let maybe_killed = p.scalar_mul(&[divisor], &c); + assert!( + !maybe_killed.is_identity(), + "[{divisor}] P killed point {p}, so claimed order {ord_u64} is not minimal", + ); + } + } +} + +#[test] +fn case_b_returned_order_annihilates_point() { + // Same post-condition, but for Case B on the Hasse bracket. + let c = curve(); + let (lo, hi) = hasse_interval(); + + for p in all_points() { + if !(10u64..=26).contains(&brute_force_order(&p, &c)) { + continue; + } + let ord = order_in_interval(&p, &c, &lo, &hi); + let killed = p.scalar_mul(ord.as_words(), &c); + assert!( + killed.is_identity(), + "Case B [order] P must be O, but wasn't for {p}, order={ord:?}", + ); + } +} diff --git a/fp/src/f2_element.rs b/fp/src/f2_element.rs index 0c7cab4..d6245c4 100644 --- a/fp/src/f2_element.rs +++ b/fp/src/f2_element.rs @@ -8,7 +8,7 @@ use crypto_bigint::Uint; use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}; /// Element of the finite field $\mathbb{F}_2$ -#[derive(Debug, Eq, PartialEq, Copy, Clone)] +#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] pub struct F2Element { pub(crate) value: Uint<1>, } diff --git a/fp/src/fp_element.rs b/fp/src/fp_element.rs index 12cd28c..adf4d01 100644 --- a/fp/src/fp_element.rs +++ b/fp/src/fp_element.rs @@ -192,6 +192,20 @@ where } } +// Manual `Hash` via the canonical (non-Montgomery) representative, so that +// `a == b` ⇒ `hash(a) == hash(b)` independently of any internal Montgomery +// encoding. Matches the semantics of the derived `PartialEq` above, which +// ultimately compares `ConstMontyForm` values (themselves bijective with +// their canonical form). +impl core::hash::Hash for FpElement +where + MOD: ConstPrimeMontyParams, +{ + fn hash(&self, state: &mut H) { + self.as_uint().hash(state); + } +} + // --------------------------------------------------------------------------- // Operator overloads // --------------------------------------------------------------------------- From 981b189978e9e6b1693a90c7dddbb780a71d8c50 Mon Sep 17 00:00:00 2001 From: Gustavo Banegas Date: Fri, 24 Apr 2026 08:22:13 +0200 Subject: [PATCH 3/4] added some documentation. --- ec/src/lib.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ec/src/lib.rs b/ec/src/lib.rs index 303e04d..a7db001 100644 --- a/ec/src/lib.rs +++ b/ec/src/lib.rs @@ -20,15 +20,9 @@ pub mod curve_jacobi_intersection; pub mod point_jacobi_intersection; pub mod curve_hessian; pub mod point_hessian; -/// Point used in the Legendre form. pub mod point_legendre; -///! Elliptic curve definition in Legendre form. pub mod curve_legendre; -/// Twisted Hessian curves. pub mod curve_twisted_hessian; -/// Projective points on twisted Hessian curves. pub mod point_twisted_hessian; -/// Shared small-integer utilities (sieve, trial factorization). pub mod num_utils; -/// Order computations for points (Sutherland 2007). pub mod point_order_ops; From e7be231689781dc1437036fb7413f8737d471baf Mon Sep 17 00:00:00 2001 From: Gustavo Banegas Date: Fri, 24 Apr 2026 08:22:31 +0200 Subject: [PATCH 4/4] added some documentation. --- ec/src/point_order_ops.rs | 40 +++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/ec/src/point_order_ops.rs b/ec/src/point_order_ops.rs index eec679f..b4f0d52 100644 --- a/ec/src/point_order_ops.rs +++ b/ec/src/point_order_ops.rs @@ -6,7 +6,7 @@ //! to compute the **order** of $P$: the smallest integer $n \ge 1$ such that //! //! $$ -//! [n] P = O. +//! \left[n\right] P = O. //! $$ //! //! Following Sutherland (2007), we expose two regimes: @@ -14,20 +14,20 @@ //! | Regime | Input given | Cost | //! |-----------------------|--------------------------------------------------------------------------------------|------------------------------------------------------------------| //! | Known group order | $N = \#E(\mathbb{F}_q)$ and its factorization | $O(k \log N)$ scalar muls, where $k = \omega(N)$ | -//! | Generic (Hasse bound) | Interval $[\ell_{lo}, \ell_{hi}]$ with $\lvert P \rvert \in [\ell_{lo}, \ell_{hi}]$ | $\widetilde O\!\left(\sqrt{\ell_{hi} - \ell_{lo}}\right)$ | +//! | Generic (Hasse bound) | Interval $\left[\ell_{lo}, \ell_{hi}\right]$ with $\lvert P \rvert \in \left[\ell_{lo}, \ell_{hi}\right]$ | $\widetilde O\!\left(\sqrt{\ell_{hi} - \ell_{lo}}\right)$ | //! //! # When to use which //! //! - If you already know $N = \#E(\mathbb{F}_q)$ (e.g. via SEA in the isogeny -//! crate), use [`order_from_group_order`]. This is the usual cryptographic +//! crate), use [`crate::point_order_ops::order_from_group_order`]. This is the usual cryptographic //! case and runs in time polynomial in $\log q$. //! - Otherwise, bracket $\lvert P \rvert$ by the Hasse interval -//! $[q + 1 - 2\sqrt q,\; q + 1 + 2\sqrt q]$ and call -//! [`order_in_interval`]. +//! $\left[q + 1 - 2\sqrt q,\; q + 1 + 2\sqrt q\right]$ and call +//! [`crate::point_order_ops::order_in_interval`]. //! //! # On the complexity claim //! -//! The present [`order_in_interval`] gives the textbook $\widetilde O(\sqrt W)$ +//! The present [`crate::point_order_ops::order_in_interval`] gives the textbook $\widetilde O(\sqrt W)$ //! where $W = \ell_{hi} - \ell_{lo}$ is the Hasse width (so $W \in O(\sqrt q)$ //! and the total cost is $\widetilde O(q^{1/4})$). //! @@ -42,17 +42,17 @@ //! //! # Model coverage //! -//! The algorithms are written against [`PointOps`] and [`PointAdd`] so they +//! The algorithms are written against [`crate::point_ops::PointOps`] and [`crate::point_ops::PointAdd`] so they //! work for every curve model in this crate that supports full group //! addition: Weierstrass, Edwards, Hessian (and twisted Hessian), Jacobi //! quartic, Jacobi intersection, and Legendre. //! -//! [`order_in_interval`] additionally requires `P: Eq + Hash` for the +//! [`crate::point_order_ops::order_in_interval`] additionally requires `P: Eq + Hash` for the //! baby-step / giant-step table. Deriving `Hash` on the point structs is a //! one-line change wherever the underlying field element type is `Hash` //! (which is the case for `Uint`-backed `FpElement`). //! -//! Montgomery / Kummer points (x-only) do **not** implement [`PointAdd`] and +//! Montgomery / Kummer points (x-only) do **not** implement [`crate::point_ops::PointAdd`] and //! need a dedicated x-only variant using the pseudo-addition ladder; that //! specialisation is left for a follow-up. @@ -74,7 +74,7 @@ use std::hash::Hash; /// /// Since $\lvert P \rvert \mid N$ (Lagrange), start from $n \leftarrow N$ /// and, for each prime factor $p_i$ of $N$, strip as many copies of $p_i$ -/// from $n$ as possible while still satisfying $[n] P = O$: +/// from $n$ as possible while still satisfying $\left[n\right] P = O$: /// /// ```text /// n ← N @@ -136,14 +136,14 @@ where // Case B: generic order in a bracket (BSGS, O(√W)) // --------------------------------------------------------------------------- -/// Compute the order of `P` knowing only a bracket $[\ell_{lo}, \ell_{hi}]$ +/// Compute the order of `P` knowing only a bracket $\left[\ell_{lo}, \ell_{hi}\right]$ /// that contains it. /// /// For an elliptic curve over $\mathbb{F}_q$ the natural bracket is the /// **Hasse interval**: /// /// $$ -/// \lvert P \rvert \in [\,q + 1 - 2\sqrt q,\; q + 1 + 2\sqrt q\,]. +/// \lvert P \rvert \in \left[q + 1 - 2\sqrt q,\; q + 1 + 2\sqrt q\right]. /// $$ /// /// # Algorithm (baby-step / giant-step) @@ -152,17 +152,17 @@ where /// We look for a pair $(i, j)$ with $0 \le i, j \le w$ satisfying /// /// $$ -/// [\ell_{lo} + i \cdot w - j] P = O, +/// \left[\ell_{lo} + i \cdot w - j\right] P = O, /// $$ /// /// which is equivalent to the collision /// /// $$ -/// [\ell_{lo} + i \cdot w]\, P \;=\; [j]\, P +/// \left[\ell_{lo} + i \cdot w\right]\, P \;=\; \left[j\right]\, P /// $$ /// -/// in the group. The baby table $\{[j] P : 0 \le j \le w\}$ is built -/// once; giant steps walk from $[\ell_{lo}] P$ in strides of $[w] P$ +/// in the group. The baby table $\{\left[j\right] P : 0 \le j \le w\}$ is built +/// once; giant steps walk from $\left[\ell_{lo}\right] P$ in strides of $\left[w\right] P$ /// until a collision is found. /// /// ```text @@ -181,13 +181,13 @@ where /// /// # Cost /// -/// `O(\sqrtW)` point additions and a hash table of the same size for the +/// $O(\sqrt W)$ point additions and a hash table of the same size for the /// BSGS itself. Phase 3 additionally trial-factors the resulting -/// witness `c ≤ hi`, which costs `O(\sqrtc · M)` big-int operations — +/// witness `c ≤ hi`, which costs $O(\sqrt c \cdot M)$ big-int operations — /// practical only when `hi` fits in roughly 80 bits. /// /// In cryptographic settings ($q \ge 2^{128}$) you should **always** -/// prefer [`order_from_group_order`], feeding in a factorization you +/// prefer [`crate::point_order_ops::order_from_group_order`], feeding in a factorization you /// obtained separately (e.g. from SEA). `order_in_interval` is /// intended for testing, for small-field experimentation, and as the /// raw building block that the future Sutherland smoothness @@ -205,7 +205,7 @@ where /// /// # Panics /// -/// - If `[k] P ≠ O` for every `k ∈ [lo, hi]`, i.e. the bracket is wrong. +/// - If `\left[k\right] P ≠ O` for every `k ∈ [lo, hi]`, i.e. the bracket is wrong. /// - If the BSGS table size would exceed `u64::MAX` (unreachable in /// practice: for any Hasse interval of a curve over a field fitting in /// `Uint`, $\sqrt W$ fits comfortably in 64 bits).