ec/point_montgomery.rs
1//! X-only arithmetic on a Montgomery curve via the Kummer line.
2//!
3//!
4//! # Kummer-line representation
5//!
6//! A Montgomery point is represented only by its x-coordinate, in projective
7//! form `(X : Z)`, corresponding to the affine value `x = X / Z` when `Z ≠ 0`.
8//!
9//! This is not a full point on the elliptic curve: it is a point on the
10//! quotient `E / {±1}`. In other words, `P` and `-P` are identified.
11//!
12//! # Why projective x/z coordinates?
13//!
14//! The Montgomery ladder is naturally expressed using x-only formulas in
15//! projective coordinates. This avoids field inversions during the ladder and
16//! yields a uniform constant-time scalar multiplication pattern.
17//!
18//! # Important limitation
19//!
20//! Since the sign of `y` is forgotten, x-only arithmetic does **not** provide a
21//! full group law on points. In particular, ordinary addition `P + Q` is not
22//! available from x-coordinates alone.
23//!
24//! What *is* available is:
25//!
26//! - doubling,
27//! - differential addition: given `x(P)`, `x(Q)`, and `x(P - Q)`, compute
28//! `x(P + Q)`,
29//! - scalar multiplication via the Montgomery ladder.
30
31use core::fmt;
32use subtle::{Choice, CtOption, ConditionallySelectable, ConstantTimeEq};
33
34use crate::curve_montgomery::MontgomeryCurve;
35use crate::point_ops::PointOps;
36use fp::field_ops::{FieldOps};
37
38/// A point on the Kummer line of a Montgomery curve, represented by `(X : Z)`.
39///
40/// The affine x-coordinate is `x = X / Z` when `Z ≠ 0`.
41/// A conventional choice is:
42/// - `(1 : 0)` for the identity image,
43/// - `(X : Z)` with `Z ≠ 0` for finite x-coordinates.
44#[derive(Debug, Clone, Copy)]
45pub struct KummerPoint<F: FieldOps + Copy> {
46 /// Projective x coordinate
47 pub x: F,
48 /// Projective z coordinate
49 pub z: F,
50}
51
52// ---------------------------------------------------------------------------
53// Manual trait impls
54// ---------------------------------------------------------------------------
55
56impl<F> fmt::Display for KummerPoint<F>
57where
58 F: FieldOps + Copy + fmt::Display,
59{
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 if self.is_identity() {
62 if f.alternate() {
63 write!(f, "KummerPoint {{ O = (1:0) }}")
64 } else {
65 write!(f, "O")
66 }
67 } else if f.alternate() {
68 match self.to_x().into_option() {
69 Some(x_aff) => write!(
70 f,
71 "KummerPoint {{ X:Z = ({}:{}), x = {} }}",
72 self.x, self.z, x_aff
73 ),
74 None => write!(
75 f,
76 "KummerPoint {{ X:Z = ({}:{}) }}",
77 self.x, self.z
78 ),
79 }
80 } else {
81 write!(f, "({}:{})", self.x, self.z)
82 }
83 }
84}
85
86impl<F: FieldOps> PartialEq for KummerPoint<F>
87where
88 F: FieldOps + ConstantTimeEq,
89{
90 /// Equality of projective x-line points.
91 ///
92 /// A standard criterion is cross-multiplication:
93 /// ```text
94 /// X1 Z2 = X2 Z1.
95 /// ```
96 fn eq(&self, other: &Self) -> bool {
97 self.x * other.z == other.x * self.z
98 }
99}
100
101impl<F: FieldOps> Eq for KummerPoint<F> where F: FieldOps + ConstantTimeEq {}
102
103// ---------------------------------------------------------------------------
104// Constructors
105// ---------------------------------------------------------------------------
106
107impl<F: FieldOps> KummerPoint<F> {
108 /// Construct a projective x-line point `(X : Z)` without validation.
109 pub fn new(x: F, z: F) -> Self {
110 assert!(!bool::from((x * z).is_zero()));
111 Self { x, z }
112 }
113
114 /// Construct the finite x-line point corresponding to the affine
115 /// x-coordinate `x`, i.e. `(x : 1)`.
116 pub fn from_x(x: F) -> Self {
117 Self { x, z: F::one() }
118 }
119
120 /// The image of the identity point on the Kummer line.
121 pub fn identity() -> Self {
122 Self {
123 x: F::one(),
124 z: F::zero(),
125 }
126 }
127
128 /// Return `true` if this point is the image of identity.
129 pub fn is_identity(&self) -> bool {
130 bool::from(self.z.is_zero())
131 }
132
133 /// Attempt to recover the affine x-coordinate (succeeds when Z != 0).
134 pub fn to_x(&self) -> CtOption<F> {
135 let z_inv = self.z.invert();
136 z_inv.map(|zinv| self.x * zinv)
137 }
138}
139
140// ---------------------------------------------------------------------------
141// Constant-time functionalities
142// ---------------------------------------------------------------------------
143
144impl<F> ConditionallySelectable for KummerPoint<F>
145where
146 F: FieldOps + Copy,
147{
148 fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self {
149 Self {
150 x: F::conditional_select(&a.x, &b.x, choice),
151 z: F::conditional_select(&a.z, &b.z, choice),
152 }
153 }
154
155 fn conditional_assign(&mut self, other: &Self, choice: Choice) {
156 F::conditional_assign(&mut self.x, &other.x, choice);
157 F::conditional_assign(&mut self.z, &other.z, choice);
158 }
159
160 fn conditional_swap(a: &mut Self, b: &mut Self, choice: Choice) {
161 F::conditional_swap(&mut a.x, &mut b.x, choice);
162 F::conditional_swap(&mut a.z, &mut b.z, choice);
163 }
164}
165
166impl<F> ConstantTimeEq for KummerPoint<F>
167where
168 F: FieldOps + Copy + ConstantTimeEq,
169{
170 /// Constant-time projective equality test on the Kummer line.
171 /// ```text
172 /// X1 Z2 ?= X2 Z1
173 /// ```
174 fn ct_eq(&self, other: &Self) -> Choice {
175 let x1z2 = self.x * other.z;
176 let x2z1 = other.x * self.z;
177 x1z2.ct_eq(&x2z1)
178 }
179
180 fn ct_ne(&self, other: &Self) -> Choice {
181 !self.ct_eq(other)
182 }
183}
184
185// ---------------------------------------------------------------------------
186// X-only arithmetic
187// ---------------------------------------------------------------------------
188
189impl<F: FieldOps + Copy> KummerPoint<F> {
190 /// Point doubling on the Kummer line.
191 ///
192 /// Given `x(P)` in projective form `(X:Z)`, compute `x([2]P)`.
193 pub fn xdouble(&self, curve: &MontgomeryCurve<F>) -> Self {
194 if F::characteristic()[0] != 2 {
195 let sumsq = <F as FieldOps>::square(&(self.x + self.z));
196 let diffsq = <F as FieldOps>::square(&(self.x - self.z));
197 let fourxz = sumsq - diffsq;
198 let a24 = curve.a24();
199
200 let new_z = fourxz * (diffsq + a24 * fourxz);
201
202 Self {
203 x: sumsq * diffsq,
204 z: new_z,
205 }
206 } else {
207 let temp_x = self.x + curve.b * self.z;
208 let new_x = <F as FieldOps>::square(&<F as FieldOps>::square(&temp_x));
209 let xz = self.x * self.z;
210 let new_z = <F as FieldOps>::square(&xz);
211 Self { x: new_x, z: new_z }
212 }
213 }
214
215 /// Differential addition. Given (in projective form `(X:Z)`):
216 ///
217 /// - `self = x(P)`,
218 /// - `other = x(Q)`,
219 /// - `diff = x(P - Q)`,
220 ///
221 /// compute `x(P + Q)`.
222 pub fn xadd(&self, other: &Self, diff: &Self) -> Self {
223 if F::characteristic()[0] != 2 {
224 let u = (self.x - self.z) * (other.x + other.z);
225 let v = (self.x + self.z) * (other.x - other.z);
226
227 let new_x = diff.z * <F as FieldOps>::square(&(u + v));
228 let new_z = diff.x * <F as FieldOps>::square(&(u - v));
229
230 Self { x: new_x, z: new_z }
231 } else {
232 let x1z2 = self.x * other.z;
233 let x2z1 = other.x * self.z;
234 let sum_squared = <F as FieldOps>::square(&(x1z2 + x2z1));
235
236 let new_x = diff.x * sum_squared + diff.z * x1z2 * x2z1;
237 let new_z = diff.z * sum_squared;
238
239 Self { x: new_x, z: new_z }
240 }
241 }
242
243 /// Montgomery ladder for scalar multiplication.
244 ///
245 /// Given an x-line point `x(P)` and a scalar `k`, compute `x([k]P)`.
246 /// The scalar `k` is given as a slice of `u64` limbs in **little-endian**
247 /// order (same convention as `FieldOps::pow`).
248 pub fn scalar_mul(&self, k: &[u64], curve: &MontgomeryCurve<F>) -> Self {
249 if self.is_identity() || k.is_empty() {
250 return Self::identity();
251 }
252
253 // Ladder state:
254 // r0 = x([m]P)
255 // r1 = x([m+1]P)
256 let mut r0 = Self::identity();
257 let mut r1 = *self;
258
259 let mut swap = Choice::from(0u8);
260
261 for &limb in k.iter().rev() {
262 for bit in (0..64).rev() {
263 let ki = Choice::from(((limb >> bit) & 1) as u8);
264
265 // Swap exactly when the current bit differs from the previous one.
266 let do_swap = swap ^ ki;
267 Self::conditional_swap(&mut r0, &mut r1, do_swap);
268 swap = ki;
269
270 // After the swap, the invariant is arranged so that:
271 // r0 <- [2]r0
272 // r1 <- r0 + r1
273 // and x(r1 - r0) remains x(P).
274 let r0_dbl = r0.xdouble(curve);
275 let r0_plus_r1 = r0.xadd(&r1, self);
276
277 r0 = r0_dbl;
278 r1 = r0_plus_r1;
279 }
280 }
281
282 // Final swap to undo the last deferred permutation.
283 Self::conditional_swap(&mut r0, &mut r1, swap);
284
285 r0
286 }
287}
288
289// ---------------------------------------------------------------------------
290// PointOps bridge
291// ---------------------------------------------------------------------------
292
293impl<F> PointOps for KummerPoint<F>
294where
295 F: FieldOps + Copy,
296{
297 type BaseField = F;
298 type Curve = MontgomeryCurve<F>;
299
300 /// Return the identity image on the Kummer line.
301 fn identity(_curve: &Self::Curve) -> Self {
302 KummerPoint::<F>::identity()
303 }
304
305 /// Return `true` if this is the identity image.
306 fn is_identity(&self) -> bool {
307 KummerPoint::<F>::is_identity(self)
308 }
309
310 /// Negation is trivial on the Kummer line because `P` and `-P` have the
311 /// same image.
312 fn negate(&self, _curve: &Self::Curve) -> Self {
313 *self
314 }
315
316 /// Scalar multiplication is naturally implemented by the Montgomery ladder.
317 fn scalar_mul(&self, k: &[u64], curve: &Self::Curve) -> Self {
318 KummerPoint::<F>::scalar_mul(self, k, curve)
319 }
320}