Skip to main content

binius_examples/circuits/
bitcoin_p2pkh.rs

1// Copyright 2025 Irreducible Inc.
2
3use std::array;
4
5use anyhow::Result;
6use binius_circuits::{
7	bignum::BigUint,
8	bitcoin::p2pkh_signature::{addr_bytes_to_le_words, build_p2pkh_circuit},
9};
10use binius_frontend::{CircuitBuilder, Wire, WitnessFiller};
11use bitcoin::{Network, PrivateKey, hashes::Hash, secp256k1::Secp256k1};
12use clap::Args;
13use rand::prelude::*;
14
15use crate::ExampleCircuit;
16
17/// Example circuit that proves knowledge of a Bitcoin private key corresponding to a P2PKH address.
18///
19/// This demonstrates a post-quantum secure Bitcoin signature scheme that maintains backwards
20/// compatibility with existing Bitcoin addresses. The circuit proves knowledge of a private key
21/// without revealing it, using the complete Bitcoin P2PKH address derivation:
22///
23/// Private Key → scalar_mul → Public Key → compress → Compressed PubKey
24/// → SHA256 → Digest → swap_bytes → LE Format → RIPEMD160 → Address
25pub struct BitcoinP2PKHExample {
26	private_key: BigUint,
27	expected_address: [Wire; 5],
28}
29
30#[derive(Args, Debug, Clone)]
31pub struct Params {
32	// No circuit parameters needed for this example
33	// The circuit is fixed-size for secp256k1 private keys
34}
35
36#[derive(Args, Debug, Clone)]
37pub struct Instance {
38	/// Private key as hex string (32 bytes = 64 hex chars, without 0x prefix).
39	/// If not provided, a random private key will be generated.
40	#[arg(long, value_parser = parse_hex_private_key)]
41	pub private_key: Option<[u8; 32]>,
42
43	/// Expected Bitcoin P2PKH address hash as hex string (20 bytes = 40 hex chars, without 0x
44	/// prefix). If not provided, the address will be computed from the private key.
45	#[arg(long, value_parser = parse_hex_address)]
46	pub expected_address: Option<[u8; 20]>,
47
48	/// Seed for deterministic random generation (for reproducible results)
49	#[arg(long, default_value_t = 42)]
50	pub seed: u64,
51}
52
53impl ExampleCircuit for BitcoinP2PKHExample {
54	type Params = Params;
55	type Instance = Instance;
56
57	fn build(_params: Params, builder: &mut CircuitBuilder) -> Result<Self> {
58		// Create witness for private key (4 limbs = 256 bits)
59		let private_key = BigUint::new_witness(builder, 4);
60
61		// Create witness wires for expected address (5 × 32-bit words = 160 bits = 20 bytes)
62		let expected_address: [Wire; 5] = array::from_fn(|_| builder.add_witness());
63
64		// Build the complete P2PKH circuit that proves knowledge of the private key
65		build_p2pkh_circuit(builder, &private_key, expected_address);
66
67		Ok(Self {
68			private_key,
69			expected_address,
70		})
71	}
72
73	fn populate_witness(&self, instance: Instance, w: &mut WitnessFiller<'_>) -> Result<()> {
74		// Generate or use provided private key.
75		// The generating arm is much the longer one, so `map_or_else` would bury the common case.
76		#[allow(clippy::option_if_let_else)]
77		let private_key_bytes = match instance.private_key {
78			Some(key) => {
79				tracing::info!("Using provided private key");
80				key
81			}
82			None => {
83				let mut rng = StdRng::seed_from_u64(instance.seed);
84				let mut key = [0u8; 32];
85
86				// Generate a valid secp256k1 private key (1 <= key < group_order)
87				loop {
88					rng.fill_bytes(&mut key);
89
90					// Ensure key is not zero and is less than secp256k1 group order
91					// Group order:
92					// 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
93					if !is_zero(&key) && is_valid_secp256k1_key(&key) {
94						break;
95					}
96				}
97
98				tracing::info!(
99					"Generated private key (seed={}): {}",
100					instance.seed,
101					hex::encode(key)
102				);
103				key
104			}
105		};
106
107		// Get expected Bitcoin address (hash160). If not provided, compute from the private key
108		let expected_address_bytes = match instance.expected_address {
109			Some(addr) => {
110				tracing::info!("Using provided expected address: {}", hex::encode(addr));
111				addr
112			}
113			None => {
114				// Use bitcoin crate to compute the P2PKH address hash from private key
115				let secp = Secp256k1::new();
116				let private_key = PrivateKey::from_slice(&private_key_bytes, Network::Bitcoin)
117					.map_err(|e| anyhow::anyhow!("Invalid private key: {}", e))?;
118				let public_key = private_key.public_key(&secp);
119				let addr = public_key.pubkey_hash().to_byte_array();
120				tracing::info!("Computed expected address from private key: {}", hex::encode(addr));
121				addr
122			}
123		};
124
125		// Convert private key bytes to little-endian 64-bit limbs
126		let private_key_limbs = bytes_to_le_limbs(&private_key_bytes);
127
128		// Convert address bytes to little-endian 32-bit words
129		let address_words = addr_bytes_to_le_words(&expected_address_bytes);
130
131		// Populate witness values
132		self.private_key.populate_limbs(w, &private_key_limbs);
133
134		for i in 0..5 {
135			w[self.expected_address[i]] =
136				binius_core::word::Word::from_u64(address_words[i] as u64);
137		}
138
139		tracing::info!("Successfully populated witness for Bitcoin P2PKH proof");
140		Ok(())
141	}
142}
143
144/// Parse hex string to 32-byte private key
145fn parse_hex_private_key(s: &str) -> Result<[u8; 32], String> {
146	let bytes = hex::decode(s).map_err(|e| format!("Invalid hex: {}", e))?;
147	if bytes.len() != 32 {
148		return Err(format!("Private key must be exactly 32 bytes, got {}", bytes.len()));
149	}
150	let mut key = [0u8; 32];
151	key.copy_from_slice(&bytes);
152
153	if is_zero(&key) || !is_valid_secp256k1_key(&key) {
154		return Err("Invalid secp256k1 private key".to_string());
155	}
156
157	Ok(key)
158}
159
160/// Parse hex string to 20-byte address
161fn parse_hex_address(s: &str) -> Result<[u8; 20], String> {
162	let bytes = hex::decode(s).map_err(|e| format!("Invalid hex: {}", e))?;
163	if bytes.len() != 20 {
164		return Err(format!("Address must be exactly 20 bytes, got {}", bytes.len()));
165	}
166	let mut addr = [0u8; 20];
167	addr.copy_from_slice(&bytes);
168	Ok(addr)
169}
170
171/// Check if byte array is all zeros
172fn is_zero(bytes: &[u8; 32]) -> bool {
173	bytes.iter().all(|&b| b == 0)
174}
175
176/// Simplified check for valid secp256k1 private key
177/// Real implementation would check against exact group order, but this is sufficient for examples
178fn is_valid_secp256k1_key(bytes: &[u8; 32]) -> bool {
179	// secp256k1 group order starts with 0xFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE
180	// So any key starting with 0xFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF is definitely too large
181	// Return false if the first 12 bytes are all 0xFF
182	for i in 0..12 {
183		if bytes[i] != 0xFF {
184			return true;
185		}
186	}
187	false
188}
189
190/// Convert a 32-byte big-endian private key into 4 little-endian 64-bit limbs
191fn bytes_to_le_limbs(bytes_be: &[u8; 32]) -> [u64; 4] {
192	// Reverse to little-endian byte order first, then chunk into u64 limbs
193	let mut le = [0u8; 32];
194	for i in 0..32 {
195		le[i] = bytes_be[31 - i];
196	}
197	[
198		u64::from_le_bytes(le[0..8].try_into().unwrap()),
199		u64::from_le_bytes(le[8..16].try_into().unwrap()),
200		u64::from_le_bytes(le[16..24].try_into().unwrap()),
201		u64::from_le_bytes(le[24..32].try_into().unwrap()),
202	]
203}