1use binius_core::word::Word;
5use binius_frontend::{CircuitBuilder, Wire};
6
7use crate::{
8 bignum::BigUint,
9 bytes::swap_bytes_32,
10 ecdsa::scalar_mul::scalar_mul,
11 ripemd::ripemd160_fixed,
12 secp256k1::{Secp256k1, Secp256k1Affine},
13 sha256::sha256_fixed,
14 util::clear_high_bits,
15};
16
17pub const fn addr_bytes_to_le_words(addr: &[u8; 20]) -> [u32; 5] {
19 [
20 u32::from_le_bytes([addr[0], addr[1], addr[2], addr[3]]),
21 u32::from_le_bytes([addr[4], addr[5], addr[6], addr[7]]),
22 u32::from_le_bytes([addr[8], addr[9], addr[10], addr[11]]),
23 u32::from_le_bytes([addr[12], addr[13], addr[14], addr[15]]),
24 u32::from_le_bytes([addr[16], addr[17], addr[18], addr[19]]),
25 ]
26}
27
28pub fn build_p2pkh_circuit(
51 builder: &CircuitBuilder,
52 private_key: &BigUint,
53 expected_address: [Wire; 5],
54) {
55 assert_eq!(private_key.limbs.len(), 4, "private_key must be exactly 4 limbs (256 bits)");
56
57 let curve = Secp256k1::new(builder);
59 let generator = Secp256k1Affine::generator(builder);
60
61 let public_key_point = scalar_mul(builder, &curve, private_key, generator);
62
63 let compressed_pubkey = compress_pubkey(builder, &public_key_point.x, &public_key_point.y);
65
66 let sha256_digest = sha256_fixed(builder, &compressed_pubkey, 33);
68
69 let mut swapped_digest = Vec::with_capacity(8);
73 for &word in &sha256_digest {
74 swapped_digest.push(swap_bytes_32(builder, word));
75 }
76
77 let computed_address = ripemd160_fixed(builder, &swapped_digest, 32);
79
80 for i in 0..5 {
82 builder.assert_eq(
83 format!("p2pkh_address[{}]", i),
84 computed_address[i],
85 expected_address[i],
86 );
87 }
88}
89
90pub fn compress_pubkey(builder: &CircuitBuilder, x: &BigUint, y: &BigUint) -> Vec<Wire> {
112 assert_eq!(x.limbs.len(), 4, "x-coordinate must be exactly 4 limbs (256 bits)");
113 assert_eq!(y.limbs.len(), 4, "y-coordinate must be exactly 4 limbs (256 bits)");
114
115 let y_is_odd = builder.shl(y.limbs[0], 63);
116
117 let prefix_even = builder.add_constant(Word::from_u64(0x02 << 24));
119 let prefix_odd = builder.add_constant(Word::from_u64(0x03 << 24));
120 let prefix_byte = builder.select(y_is_odd, prefix_odd, prefix_even);
121
122 let lower_32_bits = |limb| clear_high_bits(builder, limb, 32);
125 vec![
129 builder.bxor(builder.shr(x.limbs[3], 40), prefix_byte),
130 lower_32_bits(builder.shr(x.limbs[3], 8)),
131 lower_32_bits(builder.bxor(builder.shl(x.limbs[3], 24), builder.shr(x.limbs[2], 40))),
132 lower_32_bits(builder.shr(x.limbs[2], 8)),
133 lower_32_bits(builder.bxor(builder.shl(x.limbs[2], 24), builder.shr(x.limbs[1], 40))),
134 lower_32_bits(builder.shr(x.limbs[1], 8)),
135 lower_32_bits(builder.bxor(builder.shl(x.limbs[1], 24), builder.shr(x.limbs[0], 40))),
136 lower_32_bits(builder.shr(x.limbs[0], 8)),
137 lower_32_bits(builder.shl(x.limbs[0], 24)),
138 ]
139}
140
141#[cfg(test)]
142mod tests {
143 use binius_core::word::Word;
144 use bitcoin::{PublicKey, hashes::Hash};
145 use rand::prelude::*;
146
147 use super::*;
148
149 pub fn compressed_pubkey_from_be_sk(sk_be: [u8; 32]) -> [u8; 33] {
153 use k256::{ProjectivePoint, Scalar, elliptic_curve::sec1::ToSec1Point};
154
155 let mut s = Scalar::from(0u64);
157 let base = Scalar::from(256u64);
158 for &b in &sk_be {
159 s = s * base + Scalar::from(b as u64);
160 }
161 assert!(!bool::from(s.is_zero()), "private key must be non-zero");
162
163 let affine = ProjectivePoint::mul_by_generator(&s).to_affine();
164 let ep = affine.to_sec1_point(true);
165 let mut out = [0u8; 33];
166 out.copy_from_slice(ep.as_bytes());
167 out
168 }
169
170 pub fn gen_priv_pub_pair_from_rng(mut rng: impl Rng) -> ([u8; 32], [u8; 33]) {
172 let mut sk_be = [0u8; 32];
173 rng.fill_bytes(&mut sk_be);
174 let pk_comp = compressed_pubkey_from_be_sk(sk_be);
175 (sk_be, pk_comp)
176 }
177
178 fn validate_priv_pub_pair(private_key_be: [u8; 32], compressed_pubkey: [u8; 33]) {
180 assert_circuit_matches_priv_pub_pair(private_key_be, compressed_pubkey);
182
183 let builder = CircuitBuilder::new();
185 let private_key = BigUint::new_witness(&builder, 4);
186 let expected_address: [Wire; 5] = std::array::from_fn(|_| builder.add_witness());
187
188 build_p2pkh_circuit(&builder, &private_key, expected_address);
189
190 let circuit = builder.build();
191 let mut w = circuit.new_witness_filler();
192
193 let mut sk_le = [0u8; 32];
195 for i in 0..32 {
196 sk_le[i] = private_key_be[31 - i];
197 }
198 let limbs: [u64; 4] = [
199 u64::from_le_bytes(sk_le[0..8].try_into().unwrap()),
200 u64::from_le_bytes(sk_le[8..16].try_into().unwrap()),
201 u64::from_le_bytes(sk_le[16..24].try_into().unwrap()),
202 u64::from_le_bytes(sk_le[24..32].try_into().unwrap()),
203 ];
204 private_key.populate_limbs(&mut w, &limbs);
205
206 let public_key =
208 PublicKey::from_slice(&compressed_pubkey).expect("Invalid compressed public key");
209 let pubkey_hash = public_key.pubkey_hash();
210 let addr_bytes: [u8; 20] = pubkey_hash.to_byte_array();
211 for i in 0..5 {
212 let start = i * 4;
213 let v = u32::from_le_bytes([
214 addr_bytes[start],
215 addr_bytes[start + 1],
216 addr_bytes[start + 2],
217 addr_bytes[start + 3],
218 ]);
219 w[expected_address[i]] = Word::from_u64(v as u64);
220 }
221
222 circuit.populate_wire_witness(&mut w).unwrap();
223 circuit
224 .constraint_system()
225 .verify(&w.into_value_vec())
226 .unwrap();
227 }
228
229 fn assert_circuit_matches_priv_pub_pair(
233 private_key_be: [u8; 32],
234 expected_compressed_sec1: [u8; 33],
235 ) {
236 let builder = CircuitBuilder::new();
237
238 let private_key = BigUint::new_witness(&builder, 4);
240 let expected_words: Vec<Wire> = (0..9).map(|_| builder.add_witness()).collect();
241
242 let curve = Secp256k1::new(&builder);
244 let generator = Secp256k1Affine::generator(&builder);
245 let pub_point = scalar_mul(&builder, &curve, &private_key, generator);
246 let compressed = compress_pubkey(&builder, &pub_point.x, &pub_point.y);
247
248 for i in 0..9 {
249 builder.assert_eq(format!("compressed[{i}]"), compressed[i], expected_words[i]);
250 }
251
252 let circuit = builder.build();
253 let mut w = circuit.new_witness_filler();
254
255 let mut sk_le = [0u8; 32];
257 for i in 0..32 {
258 sk_le[i] = private_key_be[31 - i];
259 }
260 let limbs: [u64; 4] = [
261 u64::from_le_bytes(sk_le[0..8].try_into().unwrap()),
262 u64::from_le_bytes(sk_le[8..16].try_into().unwrap()),
263 u64::from_le_bytes(sk_le[16..24].try_into().unwrap()),
264 u64::from_le_bytes(sk_le[24..32].try_into().unwrap()),
265 ];
266 private_key.populate_limbs(&mut w, &limbs);
267
268 let mut expected_word_values = [0u32; 9];
270 for i in 0..9 {
271 let start = i * 4;
272 let mut word = 0u32;
273 for j in 0..4 {
274 if start + j < 33 {
275 word |= (expected_compressed_sec1[start + j] as u32) << (24 - j * 8);
276 }
277 }
278 expected_word_values[i] = word;
279 }
280 for i in 0..9 {
281 w[expected_words[i]] = Word::from_u64(expected_word_values[i] as u64);
282 }
283
284 circuit.populate_wire_witness(&mut w).unwrap();
285 circuit
286 .constraint_system()
287 .verify(&w.into_value_vec())
288 .unwrap();
289 }
290
291 fn test_compress_helper(x_bytes: [u8; 32], y_bytes: [u8; 32], prefix: u8) {
292 let builder = CircuitBuilder::new();
293
294 let x = BigUint::new_witness(&builder, 4);
296 let y = BigUint::new_witness(&builder, 4);
297
298 let expected_words: Vec<Wire> = (0..9).map(|_| builder.add_witness()).collect();
300
301 let compressed = compress_pubkey(&builder, &x, &y);
303
304 for i in 0..9 {
306 builder.assert_eq(format!("compressed[{}]", i), compressed[i], expected_words[i]);
307 }
308
309 let circuit = builder.build();
310 let mut w = circuit.new_witness_filler();
311
312 let x_limbs: [u64; 4] = [
314 u64::from_le_bytes([
315 x_bytes[0], x_bytes[1], x_bytes[2], x_bytes[3], x_bytes[4], x_bytes[5], x_bytes[6],
316 x_bytes[7],
317 ]),
318 u64::from_le_bytes([
319 x_bytes[8],
320 x_bytes[9],
321 x_bytes[10],
322 x_bytes[11],
323 x_bytes[12],
324 x_bytes[13],
325 x_bytes[14],
326 x_bytes[15],
327 ]),
328 u64::from_le_bytes([
329 x_bytes[16],
330 x_bytes[17],
331 x_bytes[18],
332 x_bytes[19],
333 x_bytes[20],
334 x_bytes[21],
335 x_bytes[22],
336 x_bytes[23],
337 ]),
338 u64::from_le_bytes([
339 x_bytes[24],
340 x_bytes[25],
341 x_bytes[26],
342 x_bytes[27],
343 x_bytes[28],
344 x_bytes[29],
345 x_bytes[30],
346 x_bytes[31],
347 ]),
348 ];
349
350 let y_limbs: [u64; 4] = [
351 u64::from_le_bytes([
352 y_bytes[0], y_bytes[1], y_bytes[2], y_bytes[3], y_bytes[4], y_bytes[5], y_bytes[6],
353 y_bytes[7],
354 ]),
355 u64::from_le_bytes([
356 y_bytes[8],
357 y_bytes[9],
358 y_bytes[10],
359 y_bytes[11],
360 y_bytes[12],
361 y_bytes[13],
362 y_bytes[14],
363 y_bytes[15],
364 ]),
365 u64::from_le_bytes([
366 y_bytes[16],
367 y_bytes[17],
368 y_bytes[18],
369 y_bytes[19],
370 y_bytes[20],
371 y_bytes[21],
372 y_bytes[22],
373 y_bytes[23],
374 ]),
375 u64::from_le_bytes([
376 y_bytes[24],
377 y_bytes[25],
378 y_bytes[26],
379 y_bytes[27],
380 y_bytes[28],
381 y_bytes[29],
382 y_bytes[30],
383 y_bytes[31],
384 ]),
385 ];
386
387 x.populate_limbs(&mut w, &x_limbs);
388 y.populate_limbs(&mut w, &y_limbs);
389
390 let mut expected_compressed = [0u8; 33];
392 expected_compressed[0] = prefix;
393 for i in 0..32 {
394 expected_compressed[1 + i] = x_bytes[31 - i];
395 }
396
397 let mut expected_word_values = [0u32; 9];
399 for i in 0..9 {
400 let word_start = i * 4;
401 if word_start < 33 {
402 let bytes_in_word = std::cmp::min(4, 33 - word_start);
403 let mut word = 0u32;
404 for j in 0..bytes_in_word {
405 word |= (expected_compressed[word_start + j] as u32) << (24 - j * 8);
406 }
407 expected_word_values[i] = word;
408 }
409 }
410
411 for i in 0..9 {
412 w[expected_words[i]] = Word::from_u64(expected_word_values[i] as u64);
413 }
414
415 circuit.populate_wire_witness(&mut w).unwrap();
416 circuit
417 .constraint_system()
418 .verify(&w.into_value_vec())
419 .unwrap();
420 }
421
422 #[test]
423 fn test_p2pkh_circuit_privkey_external() {
424 let (sk_be, pk_comp) = gen_priv_pub_pair_from_rng(StdRng::seed_from_u64(0));
425 validate_priv_pub_pair(sk_be, pk_comp);
426 }
427
428 #[test]
429 fn test_compress_simple() {
430 let x_bytes = [
432 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
433 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C,
434 0x1D, 0x1E, 0x1F, 0x20,
435 ];
436
437 let y_bytes = [
438 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
439 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
441 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
442 ];
443
444 test_compress_helper(x_bytes, y_bytes, 0x02);
446 }
447
448 #[test]
449 fn test_compress_odd_y() {
450 let x_bytes = [
452 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
453 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
454 0x11, 0x11, 0x11, 0x11,
455 ];
456
457 let y_bytes = [
458 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
460 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
461 ];
462
463 test_compress_helper(x_bytes, y_bytes, 0x03);
465 }
466
467 #[test]
468 fn test_compress_even_y() {
469 let x_bytes = [
471 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
472 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
473 0xAA, 0xAA, 0xAA, 0xAA,
474 ];
475
476 let y_bytes = [
477 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
479 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
480 ];
481
482 test_compress_helper(x_bytes, y_bytes, 0x02);
484 }
485}