Skip to main content

binius_circuits/secp256k1/
curve.rs

1// Copyright 2026 The Binius Developers
2// Copyright 2025 Irreducible Inc.
3use binius_core::word::Word;
4use binius_frontend::{CircuitBuilder, Wire};
5
6use super::{
7	common::{coord_b, coord_beta, coord_field, coord_zero, pow_sqrt, scalar_field},
8	point::Secp256k1Affine,
9};
10use crate::bignum::{
11	BigUint, BigUintModPowHint, PseudoMersennePrimeField, assert_eq, biguint_eq, select,
12};
13
14/// Secp256k1 - a short Weierstrass elliptic curve of the form `y^2 = x^3 + 7` over
15/// the prime field of modulus `2^256 - 2^32 - 977`.
16pub struct Secp256k1 {
17	f_p: PseudoMersennePrimeField,
18	f_scalar: PseudoMersennePrimeField,
19	b: BigUint,
20}
21
22impl Secp256k1 {
23	/// Creates new curve struct with constants related to curve and its coordinate and scalar
24	/// fields.
25	pub fn new(b: &CircuitBuilder) -> Self {
26		let f_p = coord_field(b);
27		let f_scalar = scalar_field(b);
28		let b = coord_b(b);
29		Self { f_p, f_scalar, b }
30	}
31
32	/// Coordinate field.
33	pub const fn f_p(&self) -> &PseudoMersennePrimeField {
34		&self.f_p
35	}
36
37	/// Scalar field.
38	pub const fn f_scalar(&self) -> &PseudoMersennePrimeField {
39		&self.f_scalar
40	}
41
42	/// Assert that given affine point actually resides on curve.
43	///
44	/// Considers point-at-infinity not on curve.
45	pub fn assert_on_curve(&self, b: &CircuitBuilder, p: &Secp256k1Affine) {
46		let f_p = &self.f_p;
47
48		let x_pow2 = f_p.square(b, &p.x);
49		let x_pow3 = f_p.mul(b, &x_pow2, &p.x);
50
51		let y_pow2 = f_p.square(b, &p.y);
52		assert_eq(b, "secp256k1_on_curve", &y_pow2, &f_p.add(b, &x_pow3, &self.b));
53
54		b.assert_false("not_point_at_infinity", p.is_point_at_infinity);
55	}
56
57	/// Recover the full affine point `(r, y)` by its x coordinate and y parity.
58	/// Returns point-at-infinity in case recovery isn't possible.
59	///
60	/// Note: we don't handle the case where r does not fit into the scalar field, thus
61	/// recid is boolean, and not a 0-3 bitmask signifying both parity and scalar field overflow.
62	pub fn recover(&self, b: &CircuitBuilder, r: &BigUint, recid_odd: Wire) -> Secp256k1Affine {
63		let f_p = self.f_p();
64
65		// y^2 = x^3 + 7, x = r
66		let y_squared = f_p.add(b, &f_p.mul(b, &f_p.square(b, r), r), &self.b);
67
68		// p = 2^256 - 2^32 - 977 = 3 (mod 4), compute the quadratic residue by raising to (p+1)/4
69		let res_1_limbs =
70			BigUintModPowHint::call(b, &y_squared.limbs, &pow_sqrt(b).limbs, &f_p.modulus().limbs);
71		let res_1 = BigUint { limbs: res_1_limbs };
72		let res_2 = f_p.sub(b, &coord_zero(b), &res_1);
73
74		// both residues differ in parity
75		let res_1_low_limb = *res_1.limbs.first().expect("N_LIMBS > 0");
76		let odd_1 = b.shl(res_1_low_limb, (Word::BITS - 1) as u32);
77		let is_res_2 = b.bxor(odd_1, recid_odd);
78
79		let y = select(b, is_res_2, &res_2, &res_1);
80		let is_valid_point = biguint_eq(b, &f_p.square(b, &y), &y_squared);
81
82		Secp256k1Affine {
83			x: r.clone(),
84			y,
85			is_point_at_infinity: b.bnot(is_valid_point),
86		}
87	}
88
89	/// Negate the curve point `p` if MSB-bool `cond` is true.
90	pub fn negate_if(
91		&self,
92		b: &CircuitBuilder,
93		cond: Wire,
94		p: &Secp256k1Affine,
95	) -> Secp256k1Affine {
96		let neg_y = self.f_p.sub(b, &coord_zero(b), &p.y);
97		let y = select(b, cond, &neg_y, &p.y);
98		Secp256k1Affine {
99			x: p.x.clone(),
100			y,
101			is_point_at_infinity: p.is_point_at_infinity,
102		}
103	}
104
105	/// Compute the endomorphism `λ (x, y) = (βx, y)`.
106	pub fn endomorphism(&self, b: &CircuitBuilder, p: &Secp256k1Affine) -> Secp256k1Affine {
107		let x = self.f_p.mul(b, &coord_beta(b), &p.x);
108		Secp256k1Affine {
109			x,
110			y: p.y.clone(),
111			is_point_at_infinity: p.is_point_at_infinity,
112		}
113	}
114
115	/// Add two curve points.
116	///
117	/// Requires both `p1` and `p2` to be either valid curve points or points at infinities.
118	///
119	/// This implementation is complete - it handles the cases of either `p1` or `p2` being
120	/// point-at-infinities, as well as being equal (which falls back to doubling).
121	pub fn add(
122		&self,
123		b: &CircuitBuilder,
124		p1: &Secp256k1Affine,
125		p2: &Secp256k1Affine,
126	) -> Secp256k1Affine {
127		let (addition_slope, x_diff_zero, y_diff_zero) = self.addition_slope(b, p1, p2);
128		let doubling_slope = self.doubling_slope(b, p1);
129
130		let pai_1 = p1.is_point_at_infinity;
131		let pai_2 = p2.is_point_at_infinity;
132
133		let slope = select(b, x_diff_zero, &doubling_slope, &addition_slope);
134		let (add_x, add_y) = self.sloped_add(b, &slope, p1, p2);
135
136		let x = select(b, pai_1, &p2.x, &select(b, pai_2, &p1.x, &add_x));
137		let y = select(b, pai_1, &p2.y, &select(b, pai_2, &p1.y, &add_y));
138
139		let pai_sum = b.band(x_diff_zero, b.bnot(y_diff_zero)); // adding negation
140		let is_point_at_infinity = b.select(pai_1, pai_2, b.select(pai_2, pai_1, pai_sum));
141
142		Secp256k1Affine {
143			x,
144			y,
145			is_point_at_infinity,
146		}
147	}
148
149	/// Add two curve points, incomplete.
150	///
151	/// Requires both `p1` and `p2` to be either valid curve points or points at infinities.
152	///
153	/// Unlike [`Secp256k1::add`] this implementation does not handle doubling, asserting false in
154	/// that case.
155	pub fn add_incomplete(
156		&self,
157		b: &CircuitBuilder,
158		p1: &Secp256k1Affine,
159		p2: &Secp256k1Affine,
160	) -> Secp256k1Affine {
161		let (slope, x_diff_zero, y_diff_zero) = self.addition_slope(b, p1, p2);
162
163		let pai_1 = p1.is_point_at_infinity;
164		let pai_2 = p2.is_point_at_infinity;
165		let any_pai = b.bor(pai_1, pai_2);
166
167		let (add_x, add_y) = self.sloped_add(b, &slope, p1, p2);
168		let x = select(b, pai_1, &p2.x, &select(b, pai_2, &p1.x, &add_x));
169		let y = select(b, pai_1, &p2.y, &select(b, pai_2, &p1.y, &add_y));
170
171		let pai_sum = b.band(x_diff_zero, b.bnot(y_diff_zero)); // adding negation
172		let is_point_at_infinity = b.select(pai_1, pai_2, b.select(pai_2, pai_1, pai_sum));
173
174		b.assert_false("not_doubling", b.band(b.bnot(any_pai), b.band(x_diff_zero, y_diff_zero)));
175
176		Secp256k1Affine {
177			x,
178			y,
179			is_point_at_infinity,
180		}
181	}
182
183	/// Double a curve point.
184	///
185	/// Requires both `p` to be either a valid curve point or point at infinity.
186	pub fn double(&self, b: &CircuitBuilder, p: &Secp256k1Affine) -> Secp256k1Affine {
187		let slope = self.doubling_slope(b, p);
188		let (x, y) = self.sloped_add(b, &slope, p, p);
189		// x ≠ 0 ∧ y = 0 is not possible on secp256k1 because -7 is not a cubic residue in Fp;
190		// we can only get PAI result when p is PAI.
191		let is_point_at_infinity = p.is_point_at_infinity;
192		Secp256k1Affine {
193			x,
194			y,
195			is_point_at_infinity,
196		}
197	}
198
199	fn addition_slope(
200		&self,
201		b: &CircuitBuilder,
202		p1: &Secp256k1Affine,
203		p2: &Secp256k1Affine,
204	) -> (BigUint, Wire, Wire) {
205		let f_p = &self.f_p;
206		// y₂−y₁/x₂−x₁
207		let y_diff = f_p.sub(b, &p2.y, &p1.y);
208		let x_diff = f_p.sub(b, &p2.x, &p1.x);
209		let y_diff_zero = y_diff.is_zero(b);
210		let x_diff_zero = x_diff.is_zero(b);
211		// slope = y_diff / x_diff; y_diff is reduced (output of `sub`) as `div` requires.
212		let slope = f_p.div(b, &y_diff, &x_diff, b.bnot(x_diff_zero));
213		(slope, x_diff_zero, y_diff_zero)
214	}
215
216	fn doubling_slope(&self, b: &CircuitBuilder, p: &Secp256k1Affine) -> BigUint {
217		let f_p = &self.f_p;
218		// λ=3x²/2y
219		let x_sqr = f_p.square(b, &p.x);
220		let x_sqr_by_3 = f_p.add(b, &f_p.add(b, &x_sqr, &x_sqr), &x_sqr);
221		// secp256k1 does not allow y=0, but we still have to check to avoid overconstraining.
222		let y_zero = p.y.is_zero(b);
223		let y_by_2 = f_p.add(b, &p.y, &p.y);
224		// λ = 3x² / 2y; x_sqr_by_3 is reduced (output of `add`) as `div` requires.
225		f_p.div(b, &x_sqr_by_3, &y_by_2, b.bnot(y_zero))
226	}
227
228	fn sloped_add(
229		&self,
230		b: &CircuitBuilder,
231		slope: &BigUint,
232		p1: &Secp256k1Affine,
233		p2: &Secp256k1Affine,
234	) -> (BigUint, BigUint) {
235		let f_p = &self.f_p;
236		// x₃ = λ² − x₁ − x₂
237		// x₃ = λ (x₁ − x₃) − y₁
238		let slope_sqr = f_p.square(b, slope);
239		let x = f_p.sub(b, &f_p.sub(b, &slope_sqr, &p1.x), &p2.x);
240		let y = f_p.sub(b, &f_p.mul(b, slope, &f_p.sub(b, &p1.x, &x)), &p1.y);
241		(x, y)
242	}
243}