Skip to main content

sp1_hypercube/
septic_curve.rs

1//! Elliptic Curve `y^2 = x^3 + 45x + 41z^3` over the `F_{p^7} = F_p[z]/(z^7 - 3z - 5)` extension
2//! field.
3use crate::{inner_perm, septic_extension::SepticExtension};
4use deepsize2::DeepSizeOf;
5use serde::{Deserialize, Serialize};
6use slop_algebra::{AbstractExtensionField, AbstractField, Field, PrimeField32};
7use slop_symmetric::Permutation;
8use sp1_primitives::SP1Field;
9use std::ops::Add;
10
11/// A septic elliptic curve point on y^2 = x^3 + 45x + 41z^3 over field
12/// `F_{p^7} = F_p[z]/(z^7 - 3z - 5)`.
13#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Hash, DeepSizeOf)]
14#[repr(C)]
15pub struct SepticCurve<F> {
16    /// The x-coordinate of an elliptic curve point.
17    pub x: SepticExtension<F>,
18    /// The y-coordinate of an elliptic curve point.
19    pub y: SepticExtension<F>,
20}
21
22/// The x-coordinate for a curve point used as a witness for padding interactions, derived from `e`.
23pub const CURVE_WITNESS_DUMMY_POINT_X: [u32; 7] =
24    [0x2718281 + (1 << 24), 0x8284590, 0x4523536, 0x0287471, 0x3526624, 0x9775724, 0x7093699];
25
26/// The y-coordinate for a curve point used as a witness for padding interactions, derived from `e`.
27pub const CURVE_WITNESS_DUMMY_POINT_Y: [u32; 7] =
28    [1250555984, 1592495468, 656721246, 420301347, 2125819749, 819876460, 17687681];
29
30impl<F: Field> SepticCurve<F> {
31    /// Returns the dummy point.
32    #[must_use]
33    pub fn dummy() -> Self {
34        Self {
35            x: SepticExtension::from_base_fn(|i| {
36                F::from_canonical_u32(CURVE_WITNESS_DUMMY_POINT_X[i])
37            }),
38            y: SepticExtension::from_base_fn(|i| {
39                F::from_canonical_u32(CURVE_WITNESS_DUMMY_POINT_Y[i])
40            }),
41        }
42    }
43
44    /// Check if a `SepticCurve` struct is on the elliptic curve.
45    pub fn check_on_point(&self) -> bool {
46        self.y.square() == Self::curve_formula(self.x)
47    }
48
49    /// Negates a `SepticCurve` point.
50    #[must_use]
51    pub fn neg(&self) -> Self {
52        SepticCurve { x: self.x, y: -self.y }
53    }
54
55    #[must_use]
56    /// Adds two elliptic curve points, assuming that the addition doesn't lead to the exception
57    /// cases of weierstrass addition.
58    pub fn add_incomplete(&self, other: SepticCurve<F>) -> Self {
59        let slope = (other.y - self.y) / (other.x - self.x);
60        let result_x = slope.square() - self.x - other.x;
61        let result_y = slope * (self.x - result_x) - self.y;
62        Self { x: result_x, y: result_y }
63    }
64
65    /// Add assigns an elliptic curve point, assuming that the addition doesn't lead to the
66    /// exception cases of weierstrass addition.
67    pub fn add_assign(&mut self, other: SepticCurve<F>) {
68        let result = self.add_incomplete(other);
69        self.x = result.x;
70        self.y = result.y;
71    }
72
73    #[must_use]
74    /// Double the elliptic curve point.
75    pub fn double(&self) -> Self {
76        let slope = (self.x * self.x * F::from_canonical_u8(3u8) + F::from_canonical_u16(45))
77            / (self.y * F::two());
78        let result_x = slope.square() - self.x * F::two();
79        let result_y = slope * (self.x - result_x) - self.y;
80        Self { x: result_x, y: result_y }
81    }
82
83    /// Subtracts two elliptic curve points, assuming that the subtraction doesn't lead to the
84    /// exception cases of weierstrass addition.
85    #[must_use]
86    pub fn sub_incomplete(&self, other: SepticCurve<F>) -> Self {
87        self.add_incomplete(other.neg())
88    }
89
90    /// Subtract assigns an elliptic curve point, assuming that the subtraction doesn't lead to the
91    /// exception cases of weierstrass addition.
92    pub fn sub_assign(&mut self, other: SepticCurve<F>) {
93        let result = self.add_incomplete(other.neg());
94        self.x = result.x;
95        self.y = result.y;
96    }
97}
98
99impl<F: AbstractField> SepticCurve<F> {
100    /// Evaluates the curve formula x^3 + 45x + 41z^3
101    pub fn curve_formula(x: SepticExtension<F>) -> SepticExtension<F> {
102        x.cube()
103            + x * F::from_canonical_u16(45)
104            + SepticExtension::from_base_slice(&[
105                F::zero(),
106                F::zero(),
107                F::zero(),
108                F::from_canonical_u32(41),
109                F::zero(),
110                F::zero(),
111                F::zero(),
112            ])
113    }
114}
115
116impl<F: PrimeField32> SepticCurve<F> {
117    /// Lift an x coordinate into an elliptic curve.
118    /// As an x-coordinate may not be a valid one, we allow an additional value in `[0, 256)` to the
119    /// hash input. Also, we always return the curve point with y-coordinate within `[1,
120    /// (p-1)/2]`, where p is the characteristic. The returned values are the curve point, the
121    /// offset used, and the hash input and output.
122    ///
123    /// TODO: this function should maybe take a Perm argument or express the dependency in some way.
124    pub fn lift_x(m: [F; 8]) -> (Self, u8, [F; 16], [F; 16]) {
125        let perm = inner_perm();
126        for offset in 0..=255 {
127            let m_trial = [
128                m[0],
129                m[1],
130                m[2],
131                m[3],
132                m[4],
133                m[5],
134                m[6],
135                m[7] + F::from_canonical_u32(1 << 16) * F::from_canonical_u8(offset),
136                F::zero(),
137                F::zero(),
138                F::zero(),
139                F::zero(),
140                F::zero(),
141                F::zero(),
142                F::zero(),
143                F::zero(),
144            ];
145
146            let m_hash = perm
147                .permute(m_trial.map(|x| SP1Field::from_canonical_u32(x.as_canonical_u32())))
148                .map(|x| F::from_canonical_u32(x.as_canonical_u32()));
149            let result = m_hash[..7].try_into().unwrap();
150            let x_trial = SepticExtension(result);
151
152            let y_sq = Self::curve_formula(x_trial);
153            if let Some(y) = y_sq.sqrt() {
154                if y.is_exception() {
155                    continue;
156                }
157                if y.is_send() {
158                    return (Self { x: x_trial, y: -y }, offset, m_trial, m_hash);
159                }
160                return (Self { x: x_trial, y }, offset, m_trial, m_hash);
161            }
162        }
163        panic!("curve point couldn't be found after 256 attempts");
164    }
165}
166
167impl<F: AbstractField> SepticCurve<F> {
168    /// Given three points p1, p2, p3, the function is zero if and only if p3.x == (p1 + p2).x
169    /// assuming that no weierstrass edge cases occur.
170    pub fn sum_checker_x(
171        p1: SepticCurve<F>,
172        p2: SepticCurve<F>,
173        p3: SepticCurve<F>,
174    ) -> SepticExtension<F> {
175        (p1.x.clone() + p2.x.clone() + p3.x) * (p2.x.clone() - p1.x.clone()).square()
176            - (p2.y - p1.y).square()
177    }
178
179    /// Given three points p1, p2, p3, the function is zero if and only if p3.y == (p1 + p2).y
180    /// assuming that no weierstrass edge cases occur.
181    pub fn sum_checker_y(
182        p1: SepticCurve<F>,
183        p2: SepticCurve<F>,
184        p3: SepticCurve<F>,
185    ) -> SepticExtension<F> {
186        (p1.y.clone() + p3.y.clone()) * (p2.x.clone() - p1.x.clone())
187            - (p2.y - p1.y.clone()) * (p1.x - p3.x)
188    }
189}
190
191impl<T> SepticCurve<T> {
192    /// Convert a `SepticCurve<S>` into `SepticCurve<T>`, with a map that implements `FnMut(S) ->
193    /// T`.
194    pub fn convert<S: Copy, G: FnMut(S) -> T>(point: SepticCurve<S>, mut f: G) -> Self {
195        SepticCurve {
196            x: SepticExtension(point.x.0.map(&mut f)),
197            y: SepticExtension(point.y.0.map(&mut f)),
198        }
199    }
200}
201
202/// A septic elliptic curve point on y^2 = x^3 + 45x + 41z^3 over field
203/// `F_{p^7} = F_p[z]/(z^7 - 3z - 5)`, including the point at infinity.
204#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
205pub enum SepticCurveComplete<T> {
206    /// The point at infinity.
207    Infinity,
208    /// The affine point which can be represented with a `SepticCurve<T>` structure.
209    Affine(SepticCurve<T>),
210}
211
212impl<F: Field> Add for SepticCurveComplete<F> {
213    type Output = Self;
214    fn add(self, rhs: Self) -> Self::Output {
215        if self.is_infinity() {
216            return rhs;
217        }
218        if rhs.is_infinity() {
219            return self;
220        }
221        let point1 = self.point();
222        let point2 = rhs.point();
223        if point1.x != point2.x {
224            return Self::Affine(point1.add_incomplete(point2));
225        }
226        if point1.y == point2.y {
227            return Self::Affine(point1.double());
228        }
229        Self::Infinity
230    }
231}
232
233impl<F: Field> SepticCurveComplete<F> {
234    /// Returns whether or not the point is a point at infinity.
235    pub fn is_infinity(&self) -> bool {
236        match self {
237            Self::Infinity => true,
238            Self::Affine(_) => false,
239        }
240    }
241
242    /// Asserts that the point is not a point at infinity, and returns the `SepticCurve` value.
243    pub fn point(&self) -> SepticCurve<F> {
244        match self {
245            Self::Infinity => panic!("point() called for point at infinity"),
246            Self::Affine(point) => *point,
247        }
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use rayon::prelude::*;
254    use rayon_scan::ScanParallelIterator;
255    use sp1_primitives::SP1Field;
256    use std::time::Instant;
257
258    use super::*;
259
260    #[test]
261    fn test_lift_x() {
262        let x = [
263            SP1Field::from_canonical_u32(0x2013),
264            SP1Field::from_canonical_u32(0x2015),
265            SP1Field::from_canonical_u32(0x2016),
266            SP1Field::from_canonical_u32(0x2023),
267            SP1Field::from_canonical_u32(0x2024),
268            SP1Field::from_canonical_u32(0x2016),
269            SP1Field::from_canonical_u32(0x2017),
270            SP1Field::from_canonical_u32(0),
271        ];
272        let (curve_point, _, _, _) = SepticCurve::<SP1Field>::lift_x(x);
273        assert!(curve_point.check_on_point());
274        assert!(curve_point.y.is_receive());
275    }
276
277    #[test]
278    fn test_double() {
279        let x = [
280            SP1Field::from_canonical_u32(0x2013),
281            SP1Field::from_canonical_u32(0x2015),
282            SP1Field::from_canonical_u32(0x2016),
283            SP1Field::from_canonical_u32(0x2023),
284            SP1Field::from_canonical_u32(0x2024),
285            SP1Field::from_canonical_u32(0x2016),
286            SP1Field::from_canonical_u32(0x2017),
287            SP1Field::from_canonical_u32(0x2018),
288        ];
289        let (curve_point, _, _, _) = SepticCurve::<SP1Field>::lift_x(x);
290        let double_point = curve_point.double();
291        assert!(double_point.check_on_point());
292    }
293
294    #[test]
295    #[ignore = "benchmarking purposes only"]
296    #[allow(clippy::print_stdout)]
297    fn test_simple_bench() {
298        const D: u32 = 1 << 16;
299        let mut vec = Vec::with_capacity(D as usize);
300        let mut sum = Vec::with_capacity(D as usize);
301        let start = Instant::now();
302        for i in 0..D {
303            let x = [
304                SP1Field::from_canonical_u32(i + 25),
305                SP1Field::from_canonical_u32(2 * i + 376),
306                SP1Field::from_canonical_u32(4 * i + 23),
307                SP1Field::from_canonical_u32(8 * i + 531),
308                SP1Field::from_canonical_u32(16 * i + 542),
309                SP1Field::from_canonical_u32(32 * i + 196),
310                SP1Field::from_canonical_u32(64 * i + 667),
311                SP1Field::from_canonical_u32(128 * i + 123),
312            ];
313            let (curve_point, _, _, _) = SepticCurve::<SP1Field>::lift_x(x);
314            vec.push(curve_point);
315        }
316        println!("Time elapsed: {:?}", start.elapsed());
317        let start = Instant::now();
318        for i in 0..D {
319            sum.push(vec[i as usize].add_incomplete(vec[((i + 1) % D) as usize]));
320        }
321        println!("Time elapsed: {:?}", start.elapsed());
322        let start = Instant::now();
323        for i in 0..(D as usize) {
324            assert!(
325                SepticCurve::<SP1Field>::sum_checker_x(vec[i], vec[(i + 1) % D as usize], sum[i])
326                    == SepticExtension::<SP1Field>::zero()
327            );
328            assert!(
329                SepticCurve::<SP1Field>::sum_checker_y(vec[i], vec[(i + 1) % D as usize], sum[i])
330                    == SepticExtension::<SP1Field>::zero()
331            );
332        }
333        println!("Time elapsed: {:?}", start.elapsed());
334    }
335
336    #[test]
337    #[ignore = "benchmarking purposes only"]
338    #[allow(clippy::print_stdout)]
339    fn test_parallel_bench() {
340        const D: u32 = 1 << 20;
341        let mut vec = Vec::with_capacity(D as usize);
342        let start = Instant::now();
343        for i in 0..D {
344            let x = [
345                SP1Field::from_canonical_u32(i + 25),
346                SP1Field::from_canonical_u32(2 * i + 376),
347                SP1Field::from_canonical_u32(4 * i + 23),
348                SP1Field::from_canonical_u32(8 * i + 531),
349                SP1Field::from_canonical_u32(16 * i + 542),
350                SP1Field::from_canonical_u32(32 * i + 196),
351                SP1Field::from_canonical_u32(64 * i + 667),
352                SP1Field::from_canonical_u32(128 * i + 123),
353            ];
354            let (curve_point, _, _, _) = SepticCurve::<SP1Field>::lift_x(x);
355            vec.push(SepticCurveComplete::Affine(curve_point));
356        }
357        println!("Time elapsed: {:?}", start.elapsed());
358
359        let mut cum_sum = SepticCurveComplete::Infinity;
360        let start = Instant::now();
361        for point in &vec {
362            cum_sum = cum_sum + *point;
363        }
364        println!("Time elapsed: {:?}", start.elapsed());
365        let start = Instant::now();
366        let par_sum = vec
367            .into_par_iter()
368            .with_min_len(1 << 16)
369            .scan(|a, b| *a + *b, SepticCurveComplete::Infinity)
370            .collect::<Vec<SepticCurveComplete<SP1Field>>>();
371        println!("Time elapsed: {:?}", start.elapsed());
372        assert_eq!(cum_sum, *par_sum.last().unwrap());
373    }
374}