use crate::error::Error;
use crate::primitives::bsv::TransactionSignature;
use crate::primitives::ec::{PrivateKey, PublicKey};
use crate::script::op::*;
use crate::script::template::{
compute_sighash_scope, ScriptTemplate, ScriptTemplateUnlock, SignOutputs, SigningContext,
};
use crate::script::{LockingScript, Script, ScriptChunk, UnlockingScript};
use crate::Result;
fn small_int_to_opcode(n: u8) -> Result<u8> {
if (1..=16).contains(&n) {
Ok(OP_1 + n - 1)
} else {
Err(Error::CryptoError(format!(
"Value {} out of range for small int opcode (1-16)",
n
)))
}
}
#[derive(Debug, Clone)]
pub struct Multisig {
pub threshold: u8,
}
impl Multisig {
pub fn new(threshold: u8) -> Self {
Self { threshold }
}
pub fn lock_from_keys(&self, pubkeys: &[PublicKey]) -> Result<LockingScript> {
let m = self.threshold;
let n = pubkeys.len();
if m == 0 || m > 16 {
return Err(Error::CryptoError(format!(
"Threshold must be 1-16, got {}",
m
)));
}
if n == 0 || n > 16 {
return Err(Error::CryptoError(format!(
"Number of keys must be 1-16, got {}",
n
)));
}
if (m as usize) > n {
return Err(Error::CryptoError(format!(
"Threshold {} exceeds number of keys {}",
m, n
)));
}
let mut chunks = Vec::with_capacity(n + 3);
chunks.push(ScriptChunk::new_opcode(small_int_to_opcode(m)?));
for pk in pubkeys {
let compressed = pk.to_compressed();
chunks.push(ScriptChunk::new(
compressed.len() as u8,
Some(compressed.to_vec()),
));
}
chunks.push(ScriptChunk::new_opcode(small_int_to_opcode(n as u8)?));
chunks.push(ScriptChunk::new_opcode(OP_CHECKMULTISIG));
Ok(LockingScript::from_chunks(chunks))
}
pub fn unlock(
signers: &[PrivateKey],
sign_outputs: SignOutputs,
anyone_can_pay: bool,
) -> ScriptTemplateUnlock {
let keys: Vec<PrivateKey> = signers.to_vec();
let scope = compute_sighash_scope(sign_outputs, anyone_can_pay);
let m = keys.len();
ScriptTemplateUnlock::new(
move |context: &SigningContext| {
let sighash = context.compute_sighash(scope)?;
let mut script = Script::new();
script.write_opcode(OP_0);
for key in &keys {
let signature = key.sign(&sighash)?;
let tx_sig = TransactionSignature::new(signature, scope);
script.write_bin(&tx_sig.to_checksig_format());
}
Ok(UnlockingScript::from_script(script))
},
move || {
1 + m * 74
},
)
}
pub fn sign_with_sighash(
signers: &[PrivateKey],
sighash: &[u8; 32],
sign_outputs: SignOutputs,
anyone_can_pay: bool,
) -> Result<UnlockingScript> {
let scope = compute_sighash_scope(sign_outputs, anyone_can_pay);
let mut script = Script::new();
script.write_opcode(OP_0);
for key in signers {
let signature = key.sign(sighash)?;
let tx_sig = TransactionSignature::new(signature, scope);
script.write_bin(&tx_sig.to_checksig_format());
}
Ok(UnlockingScript::from_script(script))
}
}
impl ScriptTemplate for Multisig {
fn lock(&self, params: &[u8]) -> Result<LockingScript> {
if params.is_empty() || !params.len().is_multiple_of(33) {
return Err(Error::CryptoError(
"Params must be concatenated 33-byte compressed public keys".to_string(),
));
}
let n = params.len() / 33;
if n > 16 {
return Err(Error::CryptoError(format!("Too many keys: {} (max 16)", n)));
}
if (self.threshold as usize) > n {
return Err(Error::CryptoError(format!(
"Threshold {} exceeds number of keys {}",
self.threshold, n
)));
}
let mut chunks = Vec::with_capacity(n + 3);
chunks.push(ScriptChunk::new_opcode(small_int_to_opcode(
self.threshold,
)?));
for i in 0..n {
let pk_bytes = ¶ms[i * 33..(i + 1) * 33];
chunks.push(ScriptChunk::new(33, Some(pk_bytes.to_vec())));
}
chunks.push(ScriptChunk::new_opcode(small_int_to_opcode(n as u8)?));
chunks.push(ScriptChunk::new_opcode(OP_CHECKMULTISIG));
Ok(LockingScript::from_chunks(chunks))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_multisig_2_of_3_lock() {
let key1 = PrivateKey::random();
let key2 = PrivateKey::random();
let key3 = PrivateKey::random();
let template = Multisig::new(2);
let locking = template
.lock_from_keys(&[key1.public_key(), key2.public_key(), key3.public_key()])
.unwrap();
let chunks = locking.chunks();
assert_eq!(chunks.len(), 6);
assert_eq!(chunks[0].op, OP_2);
for chunk in chunks.iter().take(3 + 1).skip(1) {
assert_eq!(chunk.data.as_ref().unwrap().len(), 33);
}
assert_eq!(chunks[4].op, OP_3);
assert_eq!(chunks[5].op, OP_CHECKMULTISIG);
assert_eq!(locking.as_script().is_multisig(), Some((2, 3)));
}
#[test]
fn test_multisig_1_of_1_lock() {
let key = PrivateKey::random();
let template = Multisig::new(1);
let locking = template.lock_from_keys(&[key.public_key()]).unwrap();
assert_eq!(locking.as_script().is_multisig(), Some((1, 1)));
}
#[test]
fn test_multisig_invalid_threshold_exceeds_keys() {
let key = PrivateKey::random();
let template = Multisig::new(3);
assert!(template.lock_from_keys(&[key.public_key()]).is_err());
}
#[test]
fn test_multisig_invalid_zero_threshold() {
let key = PrivateKey::random();
let template = Multisig::new(0);
assert!(template.lock_from_keys(&[key.public_key()]).is_err());
}
#[test]
fn test_multisig_invalid_too_many_keys() {
let keys: Vec<PublicKey> = (0..17).map(|_| PrivateKey::random().public_key()).collect();
let template = Multisig::new(1);
assert!(template.lock_from_keys(&keys).is_err());
}
#[test]
fn test_multisig_unlock_has_dummy_op_0() {
let key1 = PrivateKey::random();
let key2 = PrivateKey::random();
let sighash = [1u8; 32];
let unlocking =
Multisig::sign_with_sighash(&[key1, key2], &sighash, SignOutputs::All, false).unwrap();
let chunks = unlocking.chunks();
assert_eq!(chunks.len(), 3);
assert_eq!(chunks[0].op, OP_0);
assert!(chunks[0].data.is_none());
for chunk in chunks.iter().take(2 + 1).skip(1) {
let sig = chunk.data.as_ref().unwrap();
assert!(sig.len() >= 70 && sig.len() <= 73);
assert_eq!(*sig.last().unwrap(), 0x41u8);
}
}
#[test]
fn test_multisig_estimate_length() {
let key1 = PrivateKey::random();
let key2 = PrivateKey::random();
let unlock = Multisig::unlock(&[key1, key2], SignOutputs::All, false);
assert_eq!(unlock.estimate_length(), 149);
}
#[test]
fn test_multisig_trait_lock_concatenated_keys() {
let key1 = PrivateKey::random();
let key2 = PrivateKey::random();
let pk1 = key1.public_key().to_compressed();
let pk2 = key2.public_key().to_compressed();
let mut params = Vec::with_capacity(66);
params.extend_from_slice(&pk1);
params.extend_from_slice(&pk2);
let template = Multisig::new(1);
let locking = template.lock(¶ms).unwrap();
assert_eq!(locking.as_script().is_multisig(), Some((1, 2)));
}
#[test]
fn test_multisig_trait_lock_invalid_params() {
let template = Multisig::new(1);
assert!(template.lock(&[]).is_err());
assert!(template.lock(&[0x02; 34]).is_err());
}
#[test]
fn test_multisig_asm() {
let key1 = PrivateKey::random();
let key2 = PrivateKey::random();
let template = Multisig::new(2);
let locking = template
.lock_from_keys(&[key1.public_key(), key2.public_key()])
.unwrap();
let asm = locking.to_asm();
assert!(asm.contains("OP_2"));
assert!(asm.contains("OP_CHECKMULTISIG"));
}
#[test]
fn test_small_int_to_opcode() {
assert_eq!(small_int_to_opcode(1).unwrap(), OP_1);
assert_eq!(small_int_to_opcode(2).unwrap(), OP_2);
assert_eq!(small_int_to_opcode(16).unwrap(), OP_16);
assert!(small_int_to_opcode(0).is_err());
assert!(small_int_to_opcode(17).is_err());
}
}