Skip to main content

bsv/primitives/
point.rs

1//! Affine point representation on the secp256k1 curve.
2//!
3//! Point provides the public-facing API for elliptic curve point operations
4//! including addition, scalar multiplication, compression/decompression, and
5//! DER encoding. Internally delegates heavy arithmetic to JacobianPoint for
6//! efficiency.
7
8use crate::primitives::big_number::{BigNumber, Endian};
9use crate::primitives::curve::Curve;
10use crate::primitives::error::PrimitivesError;
11use crate::primitives::jacobian_point::JacobianPoint;
12
13/// A point on the secp256k1 curve in affine coordinates (x, y).
14///
15/// The point at infinity is represented by `inf == true` (x and y are zero).
16#[derive(Clone, Debug)]
17pub struct Point {
18    /// The x-coordinate.
19    pub x: BigNumber,
20    /// The y-coordinate.
21    pub y: BigNumber,
22    /// Whether this is the point at infinity.
23    pub inf: bool,
24}
25
26impl Point {
27    /// Create a new point from x, y coordinates.
28    pub fn new(x: BigNumber, y: BigNumber) -> Self {
29        Point { x, y, inf: false }
30    }
31
32    /// Create the point at infinity (identity element).
33    pub fn infinity() -> Self {
34        Point {
35            x: BigNumber::zero(),
36            y: BigNumber::zero(),
37            inf: true,
38        }
39    }
40
41    /// Check if this is the point at infinity.
42    pub fn is_infinity(&self) -> bool {
43        self.inf
44    }
45
46    /// Validate that this point lies on the secp256k1 curve.
47    /// Returns true if y^2 = x^3 + 7 (mod p).
48    pub fn validate(&self) -> bool {
49        if self.inf {
50            return false;
51        }
52
53        let curve = Curve::secp256k1();
54        let red = &curve.red;
55
56        let x_red = self.x.to_red(red.clone());
57        let y_red = self.y.to_red(red.clone());
58
59        // lhs = y^2 mod p
60        let y2 = red.sqr(&y_red);
61
62        // rhs = x^3 + 7 mod p
63        let x2 = red.sqr(&x_red);
64        let x3 = red.mul(&x_red, &x2);
65        let seven = BigNumber::from_number(7).to_red(red.clone());
66        let rhs = red.add(&x3, &seven);
67
68        y2.from_red().cmp(&rhs.from_red()) == 0
69    }
70
71    /// Recover a point from its x coordinate and y-parity.
72    /// `odd` = true means y should be odd.
73    pub fn from_x(x: &BigNumber, odd: bool) -> Result<Self, PrimitivesError> {
74        let curve = Curve::secp256k1();
75        let red = &curve.red;
76
77        let x_red = x.to_red(red.clone());
78
79        // y^2 = x^3 + 7 mod p
80        let x2 = red.sqr(&x_red);
81        let x3 = red.mul(&x_red, &x2);
82        let seven = BigNumber::from_number(7).to_red(red.clone());
83        let y2 = red.add(&x3, &seven);
84
85        // sqrt(y^2) mod p
86        // For secp256k1, p % 4 == 3, so sqrt(a) = a^((p+1)/4)
87        let y_red = red.sqrt(&y2);
88
89        // Verify the square root is valid
90        let y_check = red.sqr(&y_red);
91        if y_check.from_red().cmp(&y2.from_red()) != 0 {
92            return Err(PrimitivesError::PointNotOnCurve);
93        }
94
95        let mut y_val = y_red.from_red();
96
97        // Adjust parity
98        if y_val.is_odd() != odd {
99            y_val = curve.p.sub(&y_val);
100        }
101
102        let point = Point::new(x.clone(), y_val);
103        if !point.validate() {
104            return Err(PrimitivesError::PointNotOnCurve);
105        }
106        Ok(point)
107    }
108
109    /// Parse a point from DER-encoded bytes (compressed or uncompressed).
110    ///
111    /// Compressed format: 0x02/0x03 || x (33 bytes total)
112    /// Uncompressed format: 0x04 || x || y (65 bytes total)
113    pub fn from_der(bytes: &[u8]) -> Result<Self, PrimitivesError> {
114        if bytes.is_empty() {
115            return Err(PrimitivesError::InvalidDer("empty input".to_string()));
116        }
117
118        let prefix = bytes[0];
119
120        match prefix {
121            0x04 | 0x06 | 0x07 => {
122                // Uncompressed or hybrid format
123                if bytes.len() != 65 {
124                    return Err(PrimitivesError::InvalidDer(format!(
125                        "uncompressed point must be 65 bytes, got {}",
126                        bytes.len()
127                    )));
128                }
129
130                // Validate hybrid format parity
131                if prefix == 0x06 {
132                    if bytes[64] & 1 != 0 {
133                        return Err(PrimitivesError::InvalidDer(
134                            "hybrid point parity mismatch (expected even y)".to_string(),
135                        ));
136                    }
137                } else if prefix == 0x07 && bytes[64] & 1 == 0 {
138                    return Err(PrimitivesError::InvalidDer(
139                        "hybrid point parity mismatch (expected odd y)".to_string(),
140                    ));
141                }
142
143                let x = BigNumber::from_bytes(&bytes[1..33], Endian::Big);
144                let y = BigNumber::from_bytes(&bytes[33..65], Endian::Big);
145
146                let point = Point::new(x, y);
147                if !point.validate() {
148                    return Err(PrimitivesError::PointNotOnCurve);
149                }
150                Ok(point)
151            }
152            0x02 | 0x03 => {
153                // Compressed format
154                if bytes.len() != 33 {
155                    return Err(PrimitivesError::InvalidDer(format!(
156                        "compressed point must be 33 bytes, got {}",
157                        bytes.len()
158                    )));
159                }
160
161                let x = BigNumber::from_bytes(&bytes[1..33], Endian::Big);
162                let odd = prefix == 0x03;
163                Point::from_x(&x, odd)
164            }
165            _ => Err(PrimitivesError::InvalidDer(format!(
166                "unknown point format prefix: 0x{prefix:02x}"
167            ))),
168        }
169    }
170
171    /// Parse a point from a hex string (DER encoded).
172    pub fn from_string(hex: &str) -> Result<Self, PrimitivesError> {
173        let bytes = hex_to_bytes(hex)?;
174        Self::from_der(&bytes)
175    }
176
177    /// Encode this point to DER format.
178    ///
179    /// Compressed (33 bytes): 0x02/0x03 || x
180    /// Uncompressed (65 bytes): 0x04 || x || y
181    pub fn to_der(&self, compressed: bool) -> Vec<u8> {
182        if self.inf {
183            return vec![0x00];
184        }
185
186        let x_bytes = self.x.to_array(Endian::Big, Some(32));
187
188        if compressed {
189            let prefix = if self.y.is_even() { 0x02 } else { 0x03 };
190            let mut result = Vec::with_capacity(33);
191            result.push(prefix);
192            result.extend_from_slice(&x_bytes);
193            result
194        } else {
195            let y_bytes = self.y.to_array(Endian::Big, Some(32));
196            let mut result = Vec::with_capacity(65);
197            result.push(0x04);
198            result.extend_from_slice(&x_bytes);
199            result.extend_from_slice(&y_bytes);
200            result
201        }
202    }
203
204    /// Encode to hex string (compressed DER).
205    pub fn to_hex(&self) -> String {
206        bytes_to_hex(&self.to_der(true))
207    }
208
209    /// Add two points.
210    pub fn add(&self, other: &Point) -> Point {
211        if self.inf {
212            return other.clone();
213        }
214        if other.inf {
215            return self.clone();
216        }
217
218        // Use Jacobian arithmetic for efficiency
219        let jp1 = JacobianPoint::from_affine(&self.x, &self.y);
220        let jp2 = JacobianPoint::from_affine(&other.x, &other.y);
221        let result = jp1.add(&jp2);
222
223        if result.is_infinity() {
224            return Point::infinity();
225        }
226
227        let (x, y) = result.to_affine();
228        Point::new(x, y)
229    }
230
231    /// Scalar multiplication: self * k.
232    pub fn mul(&self, k: &BigNumber) -> Point {
233        if k.is_zero() || self.inf {
234            return Point::infinity();
235        }
236
237        let is_neg = k.is_neg();
238        let k_abs = if is_neg { k.neg() } else { k.clone() };
239
240        // Reduce k mod n
241        let curve = Curve::secp256k1();
242        let k_mod = k_abs.umod(&curve.n).unwrap_or(k_abs);
243
244        if k_mod.is_zero() {
245            return Point::infinity();
246        }
247
248        let jp = JacobianPoint::from_affine(&self.x, &self.y);
249        let result = jp.mul_wnaf(&k_mod);
250
251        if result.is_infinity() {
252            return Point::infinity();
253        }
254
255        let (x, y) = result.to_affine();
256        let point = Point::new(x, y);
257
258        if is_neg {
259            point.negate()
260        } else {
261            point
262        }
263    }
264
265    /// Negate a point (same x, y = p - y).
266    pub fn negate(&self) -> Point {
267        if self.inf {
268            return self.clone();
269        }
270        let curve = Curve::secp256k1();
271        let neg_y = curve.p.sub(&self.y);
272        Point::new(self.x.clone(), neg_y)
273    }
274
275    /// Check equality of two points.
276    #[allow(clippy::should_implement_trait)]
277    pub fn eq(&self, other: &Point) -> bool {
278        if self.inf && other.inf {
279            return true;
280        }
281        if self.inf != other.inf {
282            return false;
283        }
284        self.x.cmp(&other.x) == 0 && self.y.cmp(&other.y) == 0
285    }
286
287    /// Double this point (P + P = 2P).
288    pub fn dbl(&self) -> Point {
289        if self.inf {
290            return self.clone();
291        }
292        let jp = JacobianPoint::from_affine(&self.x, &self.y);
293        let result = jp.dbl();
294        if result.is_infinity() {
295            return Point::infinity();
296        }
297        let (x, y) = result.to_affine();
298        Point::new(x, y)
299    }
300
301    /// Get x coordinate (clone).
302    pub fn get_x(&self) -> BigNumber {
303        self.x.clone()
304    }
305
306    /// Get y coordinate (clone).
307    pub fn get_y(&self) -> BigNumber {
308        self.y.clone()
309    }
310}
311
312// ---------------------------------------------------------------------------
313// Hex helpers
314// ---------------------------------------------------------------------------
315
316fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, PrimitivesError> {
317    if hex.len() & 1 != 0 {
318        return Err(PrimitivesError::InvalidHex(
319            "odd-length hex string".to_string(),
320        ));
321    }
322    let mut bytes = Vec::with_capacity(hex.len() / 2);
323    for i in (0..hex.len()).step_by(2) {
324        let byte = u8::from_str_radix(&hex[i..i + 2], 16)
325            .map_err(|e| PrimitivesError::InvalidHex(e.to_string()))?;
326        bytes.push(byte);
327    }
328    Ok(bytes)
329}
330
331fn bytes_to_hex(bytes: &[u8]) -> String {
332    bytes.iter().map(|b| format!("{b:02x}")).collect()
333}
334
335// ---------------------------------------------------------------------------
336// Tests
337// ---------------------------------------------------------------------------
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    fn g() -> Point {
344        let curve = Curve::secp256k1();
345        curve.generator()
346    }
347
348    #[test]
349    fn test_point_infinity() {
350        let inf = Point::infinity();
351        assert!(inf.is_infinity());
352    }
353
354    #[test]
355    fn test_point_g_on_curve() {
356        let g = g();
357        assert!(g.validate());
358    }
359
360    #[test]
361    fn test_point_infinity_not_on_curve() {
362        let inf = Point::infinity();
363        assert!(!inf.validate());
364    }
365
366    #[test]
367    fn test_point_add_g_plus_g() {
368        let g = g();
369        let two_g = g.add(&g);
370        assert_eq!(
371            two_g.x.to_hex(),
372            "c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5"
373        );
374        assert_eq!(
375            two_g.y.to_hex(),
376            "1ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a"
377        );
378    }
379
380    #[test]
381    fn test_point_add_identity() {
382        let g = g();
383        let inf = Point::infinity();
384
385        let r1 = g.add(&inf);
386        assert!(r1.eq(&g));
387
388        let r2 = inf.add(&g);
389        assert!(r2.eq(&g));
390    }
391
392    #[test]
393    fn test_point_mul_1() {
394        let g = g();
395        let k = BigNumber::one();
396        let result = g.mul(&k);
397        assert!(result.eq(&g));
398    }
399
400    #[test]
401    fn test_point_mul_2_equals_add() {
402        let g = g();
403        let k = BigNumber::from_number(2);
404        let mul_result = g.mul(&k);
405        let add_result = g.add(&g);
406        assert!(mul_result.eq(&add_result));
407    }
408
409    #[test]
410    fn test_point_mul_n_is_infinity() {
411        let g = g();
412        let curve = Curve::secp256k1();
413        let result = g.mul(&curve.n);
414        assert!(result.is_infinity());
415    }
416
417    #[test]
418    fn test_point_mul_n_minus_1() {
419        let g = g();
420        let curve = Curve::secp256k1();
421        let n_minus_1 = curve.n.subn(1);
422        let result = g.mul(&n_minus_1);
423        // (n-1)*G should have same x as G but negated y (= p - G.y)
424        assert_eq!(result.x.cmp(&g.x), 0);
425        let neg_y = curve.p.sub(&g.y);
426        assert_eq!(result.y.cmp(&neg_y), 0);
427    }
428
429    #[test]
430    fn test_point_negate() {
431        let g = g();
432        let neg_g = g.negate();
433        assert_eq!(neg_g.x.cmp(&g.x), 0);
434        let curve = Curve::secp256k1();
435        let expected_y = curve.p.sub(&g.y);
436        assert_eq!(neg_g.y.cmp(&expected_y), 0);
437    }
438
439    #[test]
440    fn test_point_negate_add_is_infinity() {
441        let g = g();
442        let neg_g = g.negate();
443        let result = g.add(&neg_g);
444        assert!(result.is_infinity());
445    }
446
447    #[test]
448    fn test_point_compressed_even_y() {
449        let g = g();
450        let der = g.to_der(true);
451        assert_eq!(der.len(), 33);
452        // G has even y, so prefix should be 0x02
453        assert_eq!(der[0], 0x02);
454    }
455
456    #[test]
457    fn test_point_uncompressed() {
458        let g = g();
459        let der = g.to_der(false);
460        assert_eq!(der.len(), 65);
461        assert_eq!(der[0], 0x04);
462    }
463
464    #[test]
465    fn test_point_from_der_compressed() {
466        let g = g();
467        let der = g.to_der(true);
468        let recovered = Point::from_der(&der).unwrap();
469        assert!(recovered.eq(&g));
470    }
471
472    #[test]
473    fn test_point_from_der_uncompressed() {
474        let g = g();
475        let der = g.to_der(false);
476        let recovered = Point::from_der(&der).unwrap();
477        assert!(recovered.eq(&g));
478    }
479
480    #[test]
481    fn test_point_from_der_round_trip_compressed() {
482        let g = g();
483        for k in 1..=10 {
484            let p = g.mul(&BigNumber::from_number(k));
485            if p.is_infinity() {
486                continue;
487            }
488            let der = p.to_der(true);
489            let recovered = Point::from_der(&der).unwrap();
490            assert!(recovered.eq(&p), "round-trip failed for k={k}");
491        }
492    }
493
494    #[test]
495    fn test_point_from_der_round_trip_uncompressed() {
496        let g = g();
497        for k in 1..=10 {
498            let p = g.mul(&BigNumber::from_number(k));
499            if p.is_infinity() {
500                continue;
501            }
502            let der = p.to_der(false);
503            let recovered = Point::from_der(&der).unwrap();
504            assert!(recovered.eq(&p), "round-trip failed for k={k}");
505        }
506    }
507
508    #[test]
509    fn test_point_invalid_not_on_curve() {
510        // Random bytes that are not on the curve
511        let mut bytes = vec![0x04];
512        bytes.extend_from_slice(&[0x01; 32]); // x = 1
513        bytes.extend_from_slice(&[0x01; 32]); // y = 1
514        let result = Point::from_der(&bytes);
515        assert!(result.is_err());
516    }
517
518    #[test]
519    fn test_point_from_string() {
520        let g = g();
521        let hex = g.to_hex();
522        let recovered = Point::from_string(&hex).unwrap();
523        assert!(recovered.eq(&g));
524    }
525
526    #[test]
527    fn test_point_mul_known_multiples() {
528        let g = g();
529        let expected = vec![
530            (
531                2,
532                "c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5",
533                "1ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a",
534            ),
535            (
536                3,
537                "f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9",
538                "388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e672",
539            ),
540            (
541                5,
542                "2f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4",
543                "d8ac222636e5e3d6d4dba9dda6c9c426f788271bab0d6840dca87d3aa6ac62d6",
544            ),
545            (
546                10,
547                "a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7",
548                "893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7",
549            ),
550        ];
551
552        for (k, ex, ey) in expected {
553            let result = g.mul(&BigNumber::from_number(k));
554            assert_eq!(result.x.to_hex(), ex, "x mismatch for k={k}");
555            assert_eq!(result.y.to_hex(), ey, "y mismatch for k={k}");
556        }
557    }
558
559    #[test]
560    fn test_point_dbl() {
561        let g = g();
562        let dbl = g.dbl();
563        let add = g.add(&g);
564        assert!(dbl.eq(&add));
565    }
566
567    #[test]
568    fn test_point_from_x() {
569        let curve = Curve::secp256k1();
570        // Recover G from its x coordinate
571        let p = Point::from_x(&curve.g_x, false).unwrap();
572        assert_eq!(p.x.cmp(&curve.g_x), 0);
573        assert_eq!(p.y.cmp(&curve.g_y), 0);
574    }
575
576    #[test]
577    fn test_point_from_x_odd() {
578        let curve = Curve::secp256k1();
579        // G.y is even, so asking for odd should give p - G.y
580        let p = Point::from_x(&curve.g_x, true).unwrap();
581        let neg_y = curve.p.sub(&curve.g_y);
582        assert_eq!(p.y.cmp(&neg_y), 0);
583    }
584}