binius_circuits/secp256k1/
endosplit.rs1use binius_core::Word;
22use binius_frontend::{CircuitBuilder, Wire, hints::Hint};
23use hex_literal::hex;
24use num_bigint::BigUint;
25
26use crate::bignum::num_biguint_from_u64_limbs;
27
28pub struct Secp256k1EndosplitHint {
29 minus_b1: BigUint,
30 minus_b2: BigUint,
31 g1: BigUint,
32 g2: BigUint,
33 endomorphism_lambda: BigUint,
34 scalar_modulus: BigUint,
35 scalar_modulus_half: BigUint,
36 k1_tight_bound: BigUint,
37 k2_tight_bound: BigUint,
38}
39
40impl Secp256k1EndosplitHint {
41 pub fn new() -> Self {
42 let [
43 minus_b1,
44 minus_b2,
45 g1,
46 g2,
47 endomorphism_lambda,
48 scalar_modulus,
49 k1_tight_bound,
50 k2_tight_bound,
51 ] = [
52 hex!("e4437ed6010e88286f547fa90abfe4c3").as_slice(),
53 hex!("fffffffffffffffffffffffffffffffe8a280ac50774346dd765cda83db1562c").as_slice(),
54 hex!("3086d221a7d46bcde86c90e49284eb153daa8a1471e8ca7fe893209a45dbb031").as_slice(),
55 hex!("e4437ed6010e88286f547fa90abfe4c4221208ac9df506c61571b4ae8ac47f71").as_slice(),
56 hex!("5363ad4cc05c30e0a5261c028812645a122e22ea20816678df02967c1b23bd72").as_slice(),
57 hex!("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141").as_slice(),
58 hex!("a2a8918ca85bafe22016d0b917e4dd77").as_slice(),
59 hex!("8a65287bd47179fb2be08846cea267ed").as_slice(),
60 ]
61 .map(num_bigint::BigUint::from_bytes_be);
62
63 let scalar_modulus_half = &scalar_modulus >> 1;
64
65 Self {
66 minus_b1,
67 minus_b2,
68 g1,
69 g2,
70 endomorphism_lambda,
71 scalar_modulus,
72 scalar_modulus_half,
73 k1_tight_bound,
74 k2_tight_bound,
75 }
76 }
77
78 pub fn call(builder: &CircuitBuilder, k: &[Wire]) -> (Wire, Wire, [Wire; 2], [Wire; 2]) {
97 assert_eq!(k.len(), 4);
98 let out = builder.call_hint(Self::new(), &[], k);
99 let [k1_neg, k2_neg, k1_abs0, k1_abs1, k2_abs0, k2_abs1] = out.as_slice() else {
100 panic!("Secp256k1EndosplitHint must return 6 wires");
101 };
102 (*k1_neg, *k2_neg, [*k1_abs0, *k1_abs1], [*k2_abs0, *k2_abs1])
103 }
104
105 fn scalar_abs(&self, scalar: BigUint) -> (Word, BigUint) {
106 if scalar > self.scalar_modulus_half {
107 (Word::ALL_ONE, &self.scalar_modulus - scalar)
108 } else {
109 (Word::ZERO, scalar)
110 }
111 }
112}
113
114impl Default for Secp256k1EndosplitHint {
115 fn default() -> Self {
116 Self::new()
117 }
118}
119
120impl Hint for Secp256k1EndosplitHint {
121 const NAME: &'static str = "binius.secp256k1_endosplit";
122
123 fn shape(&self, dimensions: &[usize]) -> (usize, usize) {
124 assert!(dimensions.is_empty(), "Secp256k1EndosplitHint has constant shape");
125 (4, 6)
126 }
127
128 fn execute(&self, dimensions: &[usize], inputs: &[Word], outputs: &mut [Word]) {
129 assert!(dimensions.is_empty(), "Secp256k1EndosplitHint has constant shape");
130
131 assert_eq!(inputs.len(), 4);
132 assert_eq!(outputs.len(), 6);
133
134 let k =
135 num_biguint_from_u64_limbs(inputs.iter().map(|w| w.as_u64())) % &self.scalar_modulus;
136
137 let c1 = div_pow2_round(&k * &self.g1, 384) * &self.minus_b1;
139 let c2 = div_pow2_round(&k * &self.g2, 384) * &self.minus_b2;
140
141 let k2 = (c1 + c2) % &self.scalar_modulus;
142 let k2_lambda = (&k2 * &self.endomorphism_lambda) % &self.scalar_modulus;
143 let k1 = (&self.scalar_modulus - k2_lambda + k) % &self.scalar_modulus;
144
145 let (k1_neg, mut k1_abs) = self.scalar_abs(k1);
147 let (k2_neg, mut k2_abs) = self.scalar_abs(k2);
148
149 if k1_abs >= self.k1_tight_bound || k2_abs >= self.k2_tight_bound {
150 k1_abs = BigUint::ZERO;
151 k2_abs = BigUint::ZERO;
152 }
153
154 outputs.fill(Word::ZERO);
155
156 outputs[0] = k1_neg;
157 outputs[1] = k2_neg;
158
159 for (output, limb) in outputs[2..4].iter_mut().zip(k1_abs.iter_u64_digits()) {
160 *output = Word::from_u64(limb);
161 }
162
163 for (output, limb) in outputs[4..6].iter_mut().zip(k2_abs.iter_u64_digits()) {
164 *output = Word::from_u64(limb);
165 }
166 }
167}
168
169fn div_pow2_round(value: BigUint, shift: u64) -> BigUint {
170 let increment = if shift == 0 || !value.bit(shift - 1) {
171 BigUint::ZERO
172 } else {
173 BigUint::from(1usize)
174 };
175 (value >> shift) + increment
176}
177
178#[cfg(test)]
179mod tests {
180 use proptest::prelude::*;
181
182 use super::*;
183
184 proptest! {
185 #[test]
186 fn prop_secp256k1_endosplit(k in any::<[u64; 4]>()) {
187 let modulus = num_bigint::BigUint::from_bytes_be(
188 &hex_literal::hex!("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141")
189 );
190 let lambda = num_bigint::BigUint::from_bytes_be(
191 &hex_literal::hex!("5363ad4cc05c30e0a5261c028812645a122e22ea20816678df02967c1b23bd72")
192 );
193 let k_bignum = num_biguint_from_u64_limbs(k.iter());
194 prop_assume!(k_bignum < modulus);
195 prop_assume!(k_bignum > num_bigint::BigUint::ZERO);
196
197 let builder = CircuitBuilder::new();
198 let k = k.map(|limb| builder.add_constant_64(limb));
199 let (k1_neg, k2_neg, k1_abs, k2_abs) =
200 Secp256k1EndosplitHint::call(&builder, &k);
201
202 builder.mark_inout(k1_neg);
205 builder.mark_inout(k2_neg);
206 builder.mark_inout(k1_abs[0]);
207 builder.mark_inout(k1_abs[1]);
208 builder.mark_inout(k2_abs[0]);
209 builder.mark_inout(k2_abs[1]);
210
211 let circuit = builder.build();
212 let mut w = circuit.new_witness_filler();
213 circuit.populate_wire_witness(&mut w).unwrap();
214
215 let k1_abs_bignum = num_biguint_from_u64_limbs(k1_abs.iter().map(|&l| &w[l].0));
216 let k2_abs_bignum = num_biguint_from_u64_limbs(k2_abs.iter().map(|&l| &w[l].0));
217
218 assert!(k1_abs_bignum.bits() <= 128);
219 assert!(k2_abs_bignum.bits() <= 128);
220
221 let k1 = if w[k1_neg] != Word::ZERO {
222 &modulus - k1_abs_bignum
223 } else {
224 k1_abs_bignum
225 };
226
227 let k2 = if w[k2_neg] != Word::ZERO {
228 &modulus - k2_abs_bignum
229 } else {
230 k2_abs_bignum
231 };
232
233 assert_eq!((k1 + lambda * k2) % modulus, k_bignum);
234 }
235 }
236}