Skip to main content

bsv_rs/script/templates/
multisig.rs

1//! Multisig (M-of-N) script template.
2//!
3//! This module provides the [`Multisig`] template for creating and spending
4//! M-of-N multi-signature outputs using OP_CHECKMULTISIG.
5//!
6//! # Locking Script Pattern
7//!
8//! ```text
9//! OP_M <pubkey1> <pubkey2> ... <pubkeyN> OP_N OP_CHECKMULTISIG
10//! ```
11//!
12//! # Unlocking Script Pattern
13//!
14//! ```text
15//! OP_0 <sig1> <sig2> ... <sigM>
16//! ```
17//!
18//! The leading OP_0 is required due to a historical off-by-one bug in
19//! Bitcoin's OP_CHECKMULTISIG implementation.
20//!
21//! # Example
22//!
23//! ```rust,ignore
24//! use bsv_rs::script::templates::Multisig;
25//! use bsv_rs::script::template::SignOutputs;
26//! use bsv_rs::primitives::ec::PrivateKey;
27//!
28//! let key1 = PrivateKey::random();
29//! let key2 = PrivateKey::random();
30//! let key3 = PrivateKey::random();
31//!
32//! // Create 2-of-3 multisig locking script
33//! let template = Multisig::new(2);
34//! let pubkeys = vec![
35//!     key1.public_key(),
36//!     key2.public_key(),
37//!     key3.public_key(),
38//! ];
39//! let locking = template.lock_from_keys(&pubkeys)?;
40//!
41//! // Sign with keys 1 and 3 (in order matching the locking script)
42//! let signers = vec![key1.clone(), key3.clone()];
43//! let unlock = Multisig::unlock(&signers, SignOutputs::All, false);
44//! let unlocking = unlock.sign(&context)?;
45//! ```
46
47use 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
57/// Converts a small integer (1-16) to its opcode (OP_1 through OP_16).
58fn 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/// Multisig (M-of-N) script template.
70///
71/// Creates scripts that require M signatures from a set of N public keys.
72/// The threshold M is stored in the template; the public keys are provided
73/// when creating the locking script.
74///
75/// # Signature Order
76///
77/// Signatures in the unlocking script must appear in the same order as their
78/// corresponding public keys in the locking script. The OP_CHECKMULTISIG
79/// opcode walks through keys and signatures in order, matching each signature
80/// to the next available key.
81#[derive(Debug, Clone)]
82pub struct Multisig {
83    /// M — the number of required signatures.
84    pub threshold: u8,
85}
86
87impl Multisig {
88    /// Creates a new Multisig template with the given threshold.
89    ///
90    /// # Arguments
91    ///
92    /// * `threshold` - The number of signatures required (M). Must be 1-16.
93    pub fn new(threshold: u8) -> Self {
94        Self { threshold }
95    }
96
97    /// Creates a multisig locking script from public keys.
98    ///
99    /// This is the recommended API. Keys are encoded in compressed form (33 bytes).
100    ///
101    /// # Arguments
102    ///
103    /// * `pubkeys` - The N public keys. Must have 1-16 keys, and threshold <= N.
104    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        // OP_M
130        chunks.push(ScriptChunk::new_opcode(small_int_to_opcode(m)?));
131
132        // Push each compressed pubkey
133        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        // OP_N
142        chunks.push(ScriptChunk::new_opcode(small_int_to_opcode(n as u8)?));
143
144        // OP_CHECKMULTISIG
145        chunks.push(ScriptChunk::new_opcode(OP_CHECKMULTISIG));
146
147        Ok(LockingScript::from_chunks(chunks))
148    }
149
150    /// Creates an unlock template for spending a multisig output.
151    ///
152    /// # Arguments
153    ///
154    /// * `signers` - The M private keys to sign with. Must be in the same
155    ///   order as their corresponding public keys appear in the locking script.
156    /// * `sign_outputs` - Which outputs to sign.
157    /// * `anyone_can_pay` - Whether to allow other inputs to be added.
158    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                // OP_0 dummy element (required by OP_CHECKMULTISIG off-by-one bug)
173                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                // OP_0 (1 byte) + M signatures (1 push + 72 DER max + 1 sighash each)
185                1 + m * 74
186            },
187        )
188    }
189
190    /// Signs with a precomputed sighash.
191    ///
192    /// # Arguments
193    ///
194    /// * `signers` - The M private keys to sign with (in pubkey order).
195    /// * `sighash` - The precomputed sighash to sign.
196    /// * `sign_outputs` - Which outputs to sign (for the scope byte).
197    /// * `anyone_can_pay` - Whether to allow other inputs to be added.
198    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        // OP_0 dummy element
208        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    /// Creates a multisig locking script from concatenated 33-byte compressed public keys.
222    ///
223    /// The `params` slice must be a multiple of 33 bytes (each key is 33 bytes compressed).
224    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 = &params[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        // OP_2 + 3 pubkeys + OP_3 + OP_CHECKMULTISIG = 6 chunks
276        assert_eq!(chunks.len(), 6);
277
278        // First chunk: OP_2
279        assert_eq!(chunks[0].op, OP_2);
280
281        // Middle chunks: 33-byte pubkeys
282        for chunk in chunks.iter().take(3 + 1).skip(1) {
283            assert_eq!(chunk.data.as_ref().unwrap().len(), 33);
284        }
285
286        // OP_3
287        assert_eq!(chunks[4].op, OP_3);
288
289        // OP_CHECKMULTISIG
290        assert_eq!(chunks[5].op, OP_CHECKMULTISIG);
291
292        // Script detection
293        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        // OP_0 + 2 signatures = 3 chunks
338        assert_eq!(chunks.len(), 3);
339
340        // First chunk must be OP_0
341        assert_eq!(chunks[0].op, OP_0);
342        assert!(chunks[0].data.is_none());
343
344        // Second and third chunks are signatures
345        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        // OP_0 (1) + 2 * (1 push + 73 max sig) = 1 + 148 = 149
359        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(&params).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        // Empty
385        assert!(template.lock(&[]).is_err());
386
387        // Not a multiple of 33
388        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}