bsv_rs/script/templates/
multisig.rs1use crate::error::Error;
48use crate::primitives::bsv::TransactionSignature;
49use crate::primitives::ec::{PrivateKey, PublicKey};
50use crate::script::op::*;
51use crate::script::template::{
52 compute_sighash_scope, ScriptTemplate, ScriptTemplateUnlock, SignOutputs, SigningContext,
53};
54use crate::script::{LockingScript, Script, ScriptChunk, UnlockingScript};
55use crate::Result;
56
57fn small_int_to_opcode(n: u8) -> Result<u8> {
59 if (1..=16).contains(&n) {
60 Ok(OP_1 + n - 1)
61 } else {
62 Err(Error::CryptoError(format!(
63 "Value {} out of range for small int opcode (1-16)",
64 n
65 )))
66 }
67}
68
69#[derive(Debug, Clone)]
82pub struct Multisig {
83 pub threshold: u8,
85}
86
87impl Multisig {
88 pub fn new(threshold: u8) -> Self {
94 Self { threshold }
95 }
96
97 pub fn lock_from_keys(&self, pubkeys: &[PublicKey]) -> Result<LockingScript> {
105 let m = self.threshold;
106 let n = pubkeys.len();
107
108 if m == 0 || m > 16 {
109 return Err(Error::CryptoError(format!(
110 "Threshold must be 1-16, got {}",
111 m
112 )));
113 }
114 if n == 0 || n > 16 {
115 return Err(Error::CryptoError(format!(
116 "Number of keys must be 1-16, got {}",
117 n
118 )));
119 }
120 if (m as usize) > n {
121 return Err(Error::CryptoError(format!(
122 "Threshold {} exceeds number of keys {}",
123 m, n
124 )));
125 }
126
127 let mut chunks = Vec::with_capacity(n + 3);
128
129 chunks.push(ScriptChunk::new_opcode(small_int_to_opcode(m)?));
131
132 for pk in pubkeys {
134 let compressed = pk.to_compressed();
135 chunks.push(ScriptChunk::new(
136 compressed.len() as u8,
137 Some(compressed.to_vec()),
138 ));
139 }
140
141 chunks.push(ScriptChunk::new_opcode(small_int_to_opcode(n as u8)?));
143
144 chunks.push(ScriptChunk::new_opcode(OP_CHECKMULTISIG));
146
147 Ok(LockingScript::from_chunks(chunks))
148 }
149
150 pub fn unlock(
159 signers: &[PrivateKey],
160 sign_outputs: SignOutputs,
161 anyone_can_pay: bool,
162 ) -> ScriptTemplateUnlock {
163 let keys: Vec<PrivateKey> = signers.to_vec();
164 let scope = compute_sighash_scope(sign_outputs, anyone_can_pay);
165 let m = keys.len();
166
167 ScriptTemplateUnlock::new(
168 move |context: &SigningContext| {
169 let sighash = context.compute_sighash(scope)?;
170
171 let mut script = Script::new();
172 script.write_opcode(OP_0);
174
175 for key in &keys {
176 let signature = key.sign(&sighash)?;
177 let tx_sig = TransactionSignature::new(signature, scope);
178 script.write_bin(&tx_sig.to_checksig_format());
179 }
180
181 Ok(UnlockingScript::from_script(script))
182 },
183 move || {
184 1 + m * 74
186 },
187 )
188 }
189
190 pub fn sign_with_sighash(
199 signers: &[PrivateKey],
200 sighash: &[u8; 32],
201 sign_outputs: SignOutputs,
202 anyone_can_pay: bool,
203 ) -> Result<UnlockingScript> {
204 let scope = compute_sighash_scope(sign_outputs, anyone_can_pay);
205
206 let mut script = Script::new();
207 script.write_opcode(OP_0);
209
210 for key in signers {
211 let signature = key.sign(sighash)?;
212 let tx_sig = TransactionSignature::new(signature, scope);
213 script.write_bin(&tx_sig.to_checksig_format());
214 }
215
216 Ok(UnlockingScript::from_script(script))
217 }
218}
219
220impl ScriptTemplate for Multisig {
221 fn lock(&self, params: &[u8]) -> Result<LockingScript> {
225 if params.is_empty() || !params.len().is_multiple_of(33) {
226 return Err(Error::CryptoError(
227 "Params must be concatenated 33-byte compressed public keys".to_string(),
228 ));
229 }
230
231 let n = params.len() / 33;
232 if n > 16 {
233 return Err(Error::CryptoError(format!("Too many keys: {} (max 16)", n)));
234 }
235 if (self.threshold as usize) > n {
236 return Err(Error::CryptoError(format!(
237 "Threshold {} exceeds number of keys {}",
238 self.threshold, n
239 )));
240 }
241
242 let mut chunks = Vec::with_capacity(n + 3);
243 chunks.push(ScriptChunk::new_opcode(small_int_to_opcode(
244 self.threshold,
245 )?));
246
247 for i in 0..n {
248 let pk_bytes = ¶ms[i * 33..(i + 1) * 33];
249 chunks.push(ScriptChunk::new(33, Some(pk_bytes.to_vec())));
250 }
251
252 chunks.push(ScriptChunk::new_opcode(small_int_to_opcode(n as u8)?));
253 chunks.push(ScriptChunk::new_opcode(OP_CHECKMULTISIG));
254
255 Ok(LockingScript::from_chunks(chunks))
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn test_multisig_2_of_3_lock() {
265 let key1 = PrivateKey::random();
266 let key2 = PrivateKey::random();
267 let key3 = PrivateKey::random();
268
269 let template = Multisig::new(2);
270 let locking = template
271 .lock_from_keys(&[key1.public_key(), key2.public_key(), key3.public_key()])
272 .unwrap();
273
274 let chunks = locking.chunks();
275 assert_eq!(chunks.len(), 6);
277
278 assert_eq!(chunks[0].op, OP_2);
280
281 for chunk in chunks.iter().take(3 + 1).skip(1) {
283 assert_eq!(chunk.data.as_ref().unwrap().len(), 33);
284 }
285
286 assert_eq!(chunks[4].op, OP_3);
288
289 assert_eq!(chunks[5].op, OP_CHECKMULTISIG);
291
292 assert_eq!(locking.as_script().is_multisig(), Some((2, 3)));
294 }
295
296 #[test]
297 fn test_multisig_1_of_1_lock() {
298 let key = PrivateKey::random();
299
300 let template = Multisig::new(1);
301 let locking = template.lock_from_keys(&[key.public_key()]).unwrap();
302
303 assert_eq!(locking.as_script().is_multisig(), Some((1, 1)));
304 }
305
306 #[test]
307 fn test_multisig_invalid_threshold_exceeds_keys() {
308 let key = PrivateKey::random();
309 let template = Multisig::new(3);
310 assert!(template.lock_from_keys(&[key.public_key()]).is_err());
311 }
312
313 #[test]
314 fn test_multisig_invalid_zero_threshold() {
315 let key = PrivateKey::random();
316 let template = Multisig::new(0);
317 assert!(template.lock_from_keys(&[key.public_key()]).is_err());
318 }
319
320 #[test]
321 fn test_multisig_invalid_too_many_keys() {
322 let keys: Vec<PublicKey> = (0..17).map(|_| PrivateKey::random().public_key()).collect();
323 let template = Multisig::new(1);
324 assert!(template.lock_from_keys(&keys).is_err());
325 }
326
327 #[test]
328 fn test_multisig_unlock_has_dummy_op_0() {
329 let key1 = PrivateKey::random();
330 let key2 = PrivateKey::random();
331 let sighash = [1u8; 32];
332
333 let unlocking =
334 Multisig::sign_with_sighash(&[key1, key2], &sighash, SignOutputs::All, false).unwrap();
335
336 let chunks = unlocking.chunks();
337 assert_eq!(chunks.len(), 3);
339
340 assert_eq!(chunks[0].op, OP_0);
342 assert!(chunks[0].data.is_none());
343
344 for chunk in chunks.iter().take(2 + 1).skip(1) {
346 let sig = chunk.data.as_ref().unwrap();
347 assert!(sig.len() >= 70 && sig.len() <= 73);
348 assert_eq!(*sig.last().unwrap(), 0x41u8);
349 }
350 }
351
352 #[test]
353 fn test_multisig_estimate_length() {
354 let key1 = PrivateKey::random();
355 let key2 = PrivateKey::random();
356
357 let unlock = Multisig::unlock(&[key1, key2], SignOutputs::All, false);
358 assert_eq!(unlock.estimate_length(), 149);
360 }
361
362 #[test]
363 fn test_multisig_trait_lock_concatenated_keys() {
364 let key1 = PrivateKey::random();
365 let key2 = PrivateKey::random();
366
367 let pk1 = key1.public_key().to_compressed();
368 let pk2 = key2.public_key().to_compressed();
369
370 let mut params = Vec::with_capacity(66);
371 params.extend_from_slice(&pk1);
372 params.extend_from_slice(&pk2);
373
374 let template = Multisig::new(1);
375 let locking = template.lock(¶ms).unwrap();
376
377 assert_eq!(locking.as_script().is_multisig(), Some((1, 2)));
378 }
379
380 #[test]
381 fn test_multisig_trait_lock_invalid_params() {
382 let template = Multisig::new(1);
383
384 assert!(template.lock(&[]).is_err());
386
387 assert!(template.lock(&[0x02; 34]).is_err());
389 }
390
391 #[test]
392 fn test_multisig_asm() {
393 let key1 = PrivateKey::random();
394 let key2 = PrivateKey::random();
395
396 let template = Multisig::new(2);
397 let locking = template
398 .lock_from_keys(&[key1.public_key(), key2.public_key()])
399 .unwrap();
400
401 let asm = locking.to_asm();
402 assert!(asm.contains("OP_2"));
403 assert!(asm.contains("OP_CHECKMULTISIG"));
404 }
405
406 #[test]
407 fn test_small_int_to_opcode() {
408 assert_eq!(small_int_to_opcode(1).unwrap(), OP_1);
409 assert_eq!(small_int_to_opcode(2).unwrap(), OP_2);
410 assert_eq!(small_int_to_opcode(16).unwrap(), OP_16);
411 assert!(small_int_to_opcode(0).is_err());
412 assert!(small_int_to_opcode(17).is_err());
413 }
414}