use bsv_primitives::ec::{PrivateKey, PublicKey};
use bsv_primitives::hash::hash160;
use bsv_script::opcodes::OP_DATA_33;
use bsv_script::Script;
use crate::sighash::SIGHASH_ALL_FORKID;
use crate::template::UnlockingScriptTemplate;
use crate::transaction::Transaction;
use crate::TransactionError;
pub const MAX_MULTISIG_KEYS: usize = 5;
pub const MIN_MULTISIG_KEYS: usize = 1;
pub const P2MPKH_LOCKING_SCRIPT_LEN: usize = 70;
const P2MPKH_LOCKING_SUFFIX: [u8; 47] = [
0x88, 0x82, 0x01, 0x21, 0x87, 0x63, 0xac, 0x67, 0x51, 0x7f, 0x51, 0x7f, 0x73, 0x63, 0x7c, 0x7f,
0x68, 0x51, 0x7f, 0x73, 0x63, 0x7c, 0x7f, 0x68, 0x51, 0x7f, 0x73, 0x63, 0x7c, 0x7f, 0x68, 0x51,
0x7f, 0x73, 0x63, 0x7c, 0x7f, 0x68, 0x51, 0x7f, 0x73, 0x63, 0x7c, 0x7f, 0x68, 0xae, 0x68,
];
const P2MPKH_LOCKING_PREFIX: [u8; 3] = [0x76, 0xa9, 0x14];
#[derive(Clone, Debug)]
pub struct MultisigScript {
threshold: u8,
public_keys: Vec<PublicKey>,
}
impl MultisigScript {
pub fn new(threshold: u8, public_keys: Vec<PublicKey>) -> Result<Self, TransactionError> {
let n = public_keys.len();
if n < MIN_MULTISIG_KEYS {
return Err(TransactionError::InvalidTransaction(
"multisig requires at least 1 public key".to_string(),
));
}
if n > MAX_MULTISIG_KEYS {
return Err(TransactionError::InvalidTransaction(format!(
"multisig supports at most {} public keys, got {}",
MAX_MULTISIG_KEYS, n
)));
}
if threshold == 0 {
return Err(TransactionError::InvalidTransaction(
"multisig threshold must be at least 1".to_string(),
));
}
if threshold as usize > n {
return Err(TransactionError::InvalidTransaction(format!(
"threshold {} exceeds number of keys {}",
threshold, n
)));
}
Ok(MultisigScript {
threshold,
public_keys,
})
}
pub fn threshold(&self) -> u8 {
self.threshold
}
pub fn n(&self) -> usize {
self.public_keys.len()
}
pub fn public_keys(&self) -> &[PublicKey] {
&self.public_keys
}
pub fn to_serialized_bytes(&self) -> Vec<u8> {
let n = self.public_keys.len();
let mut bytes = Vec::with_capacity(2 + n * 34);
bytes.push(self.threshold);
for pk in &self.public_keys {
let compressed = pk.to_compressed();
bytes.push(OP_DATA_33);
bytes.extend_from_slice(&compressed);
}
bytes.push(n as u8);
bytes
}
pub fn to_bytes(&self) -> Vec<u8> {
self.to_serialized_bytes()
}
pub fn mpkh(&self) -> [u8; 20] {
hash160(&self.to_serialized_bytes())
}
pub fn p2mpkh_locking_script(&self) -> [u8; P2MPKH_LOCKING_SCRIPT_LEN] {
p2mpkh_locking_script(self.mpkh())
}
pub fn from_serialized_bytes(bytes: &[u8]) -> Result<Self, TransactionError> {
if bytes.len() < 2 + 34 {
return Err(TransactionError::InvalidTransaction(format!(
"redeem script too short: {} bytes",
bytes.len()
)));
}
let m = bytes[0];
let n = *bytes.last().unwrap();
if !(MIN_MULTISIG_KEYS as u8..=MAX_MULTISIG_KEYS as u8).contains(&m) {
return Err(TransactionError::InvalidTransaction(format!(
"invalid threshold byte: 0x{:02x}",
m
)));
}
if !(MIN_MULTISIG_KEYS as u8..=MAX_MULTISIG_KEYS as u8).contains(&n) {
return Err(TransactionError::InvalidTransaction(format!(
"invalid key-count byte: 0x{:02x}",
n
)));
}
if m > n {
return Err(TransactionError::InvalidTransaction(format!(
"threshold {} exceeds key count {}",
m, n
)));
}
let expected_len = 2 + (n as usize) * 34;
if bytes.len() != expected_len {
return Err(TransactionError::InvalidTransaction(format!(
"expected {} bytes for {}-of-{} redeem script, got {}",
expected_len,
m,
n,
bytes.len()
)));
}
let key_section = &bytes[1..bytes.len() - 1];
let mut public_keys = Vec::with_capacity(n as usize);
for i in 0..n as usize {
let offset = i * 34;
if key_section[offset] != OP_DATA_33 {
return Err(TransactionError::InvalidTransaction(format!(
"expected 0x21 push-prefix at key {}, got 0x{:02x}",
i, key_section[offset]
)));
}
let pk_bytes = &key_section[offset + 1..offset + 34];
let pk = PublicKey::from_bytes(pk_bytes).map_err(|e| {
TransactionError::InvalidTransaction(format!(
"invalid public key at index {}: {}",
i, e
))
})?;
public_keys.push(pk);
}
MultisigScript::new(m, public_keys)
}
pub fn from_script_bytes(bytes: &[u8]) -> Result<Self, TransactionError> {
Self::from_serialized_bytes(bytes)
}
}
pub fn p2mpkh_locking_script(mpkh: [u8; 20]) -> [u8; P2MPKH_LOCKING_SCRIPT_LEN] {
let mut out = [0u8; P2MPKH_LOCKING_SCRIPT_LEN];
out[..3].copy_from_slice(&P2MPKH_LOCKING_PREFIX);
out[3..23].copy_from_slice(&mpkh);
out[23..].copy_from_slice(&P2MPKH_LOCKING_SUFFIX);
out
}
pub fn lock(multisig: &MultisigScript) -> Result<Script, TransactionError> {
let body = multisig.p2mpkh_locking_script();
Ok(Script::from_bytes(&body))
}
pub fn unlock(
private_keys: Vec<PrivateKey>,
multisig: MultisigScript,
sighash_flag: Option<u32>,
) -> Result<P2MPKH, TransactionError> {
if private_keys.len() != multisig.threshold() as usize {
return Err(TransactionError::SigningError(format!(
"expected {} private keys for threshold, got {}",
multisig.threshold(),
private_keys.len()
)));
}
Ok(P2MPKH {
private_keys,
multisig,
sighash_flag: sighash_flag.unwrap_or(SIGHASH_ALL_FORKID),
})
}
#[derive(Debug)]
pub struct P2MPKH {
private_keys: Vec<PrivateKey>,
multisig: MultisigScript,
sighash_flag: u32,
}
impl P2MPKH {
pub fn multisig(&self) -> &MultisigScript {
&self.multisig
}
}
impl UnlockingScriptTemplate for P2MPKH {
fn sign(&self, tx: &Transaction, input_index: u32) -> Result<Script, TransactionError> {
let idx = input_index as usize;
if idx >= tx.inputs.len() {
return Err(TransactionError::SigningError(format!(
"input index {} out of range (tx has {} inputs)",
idx,
tx.inputs.len()
)));
}
let input = &tx.inputs[idx];
if input.source_tx_output().is_none() {
return Err(TransactionError::SigningError(
"missing source output on input (no previous tx info)".to_string(),
));
}
let sig_hash = tx.calc_input_signature_hash(idx, self.sighash_flag)?;
let mut script = Script::new();
script.append_push_data(&[])?;
for pk in &self.private_keys {
let signature = pk.sign(&sig_hash)?;
let der_sig = signature.to_der();
let mut sig_buf = Vec::with_capacity(der_sig.len() + 1);
sig_buf.extend_from_slice(&der_sig);
sig_buf.push(self.sighash_flag as u8);
script.append_push_data(&sig_buf)?;
}
script.append_push_data(&self.multisig.to_serialized_bytes())?;
Ok(script)
}
fn estimate_length(&self, _tx: &Transaction, _input_index: u32) -> u32 {
let m = self.multisig.threshold() as u32;
let n = self.multisig.n() as u32;
let redeem_len = 2 + n * 34;
1 + m * 73 + 2 + redeem_len
}
}
#[cfg(test)]
mod tests {
use super::*;
fn gen_keys(n: usize) -> (Vec<PrivateKey>, Vec<PublicKey>) {
let privs: Vec<PrivateKey> = (0..n).map(|_| PrivateKey::new()).collect();
let pubs: Vec<PublicKey> = privs.iter().map(|k| k.pub_key()).collect();
(privs, pubs)
}
fn det_pubkey(seed: u8) -> PublicKey {
let mut sk_bytes = [0u8; 32];
sk_bytes[31] = seed;
let sk = PrivateKey::from_bytes(&sk_bytes).expect("valid scalar");
sk.pub_key()
}
#[test]
fn multisig_script_2_of_3_roundtrip() {
let (_privs, pubs) = gen_keys(3);
let ms = MultisigScript::new(2, pubs).unwrap();
assert_eq!(ms.threshold(), 2);
assert_eq!(ms.n(), 3);
let bytes = ms.to_serialized_bytes();
assert_eq!(bytes.len(), 2 + 34 * 3);
assert_eq!(bytes[0], 0x02);
assert_eq!(*bytes.last().unwrap(), 0x03);
let ms2 = MultisigScript::from_serialized_bytes(&bytes).unwrap();
assert_eq!(ms2.threshold(), 2);
assert_eq!(ms2.n(), 3);
assert_eq!(ms.mpkh(), ms2.mpkh());
}
#[test]
fn multisig_script_1_of_1_length_is_36() {
let (_privs, pubs) = gen_keys(1);
let ms = MultisigScript::new(1, pubs).unwrap();
let bytes = ms.to_serialized_bytes();
assert_eq!(bytes.len(), 36);
assert_eq!(bytes[0], 0x01);
assert_eq!(bytes[1], 0x21);
assert_eq!(bytes[35], 0x01);
}
#[test]
fn multisig_script_max_keys_is_5() {
let (_privs, pubs) = gen_keys(MAX_MULTISIG_KEYS);
let ms = MultisigScript::new(1, pubs).unwrap();
assert_eq!(ms.n(), MAX_MULTISIG_KEYS);
let ms5_5 = MultisigScript::new(5, ms.public_keys().to_vec()).unwrap();
assert_eq!(ms5_5.to_serialized_bytes().len(), 172);
}
#[test]
fn multisig_script_rejects_zero_threshold() {
let (_privs, pubs) = gen_keys(3);
let err = MultisigScript::new(0, pubs).unwrap_err();
assert!(err.to_string().contains("threshold must be at least 1"));
}
#[test]
fn multisig_script_rejects_threshold_exceeding_keys() {
let (_privs, pubs) = gen_keys(2);
let err = MultisigScript::new(3, pubs).unwrap_err();
assert!(err.to_string().contains("threshold 3 exceeds"));
}
#[test]
fn multisig_script_rejects_too_many_keys() {
let (_privs, pubs) = gen_keys(MAX_MULTISIG_KEYS + 1);
let err = MultisigScript::new(1, pubs).unwrap_err();
assert!(err.to_string().contains("at most"));
}
#[test]
fn multisig_script_rejects_empty_keys() {
let err = MultisigScript::new(1, vec![]).unwrap_err();
assert!(err.to_string().contains("at least 1"));
}
#[test]
fn lock_produces_70_byte_locking_script() {
let (_privs, pubs) = gen_keys(3);
let ms = MultisigScript::new(2, pubs).unwrap();
let script = lock(&ms).unwrap();
let bytes = script.to_bytes();
assert_eq!(bytes.len(), P2MPKH_LOCKING_SCRIPT_LEN);
assert_eq!(&bytes[..3], &[0x76, 0xa9, 0x14]);
assert_eq!(&bytes[3..23], &ms.mpkh()[..]);
assert_eq!(&bytes[68..70], &[0xae, 0x68]);
}
#[test]
fn p2mpkh_locking_script_exact_bytes() {
let mpkh: [u8; 20] = [
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee,
0xff, 0x00, 0x01, 0x02, 0x03, 0x04,
];
let body = p2mpkh_locking_script(mpkh);
let expected_hex = format!(
"{}{}{}",
"76a914",
hex::encode(mpkh),
"8882012187 63ac6751 7f517f73 637c7f68 \
517f7363 7c7f6851 7f73637c 7f68517f \
73637c7f 68517f73 637c7f68 ae68"
.replace(' ', ""),
);
assert_eq!(hex::encode(body), expected_hex);
assert_eq!(body.len(), P2MPKH_LOCKING_SCRIPT_LEN);
}
#[test]
fn mpkh_is_20_bytes() {
let (_privs, pubs) = gen_keys(3);
let ms = MultisigScript::new(2, pubs).unwrap();
assert_eq!(ms.mpkh().len(), 20);
}
#[test]
fn mpkh_differs_for_different_key_sets() {
let (_privs1, pubs1) = gen_keys(3);
let (_privs2, pubs2) = gen_keys(3);
let ms1 = MultisigScript::new(2, pubs1).unwrap();
let ms2 = MultisigScript::new(2, pubs2).unwrap();
assert_ne!(ms1.mpkh(), ms2.mpkh());
}
#[test]
fn mpkh_differs_for_different_thresholds() {
let (_privs, pubs) = gen_keys(3);
let ms1 = MultisigScript::new(1, pubs.clone()).unwrap();
let ms2 = MultisigScript::new(2, pubs).unwrap();
assert_ne!(ms1.mpkh(), ms2.mpkh());
}
#[test]
fn mpkh_round_trip() {
let (_privs, pubs) = gen_keys(3);
let ms = MultisigScript::new(2, pubs).unwrap();
let bytes = ms.to_serialized_bytes();
let parsed = MultisigScript::from_serialized_bytes(&bytes).unwrap();
assert_eq!(parsed.mpkh(), ms.mpkh());
}
#[test]
fn deterministic_3_of_5_redeem_vector() {
let pubs: Vec<PublicKey> = (1u8..=5).map(det_pubkey).collect();
let ms = MultisigScript::new(3, pubs.clone()).unwrap();
let bytes = ms.to_serialized_bytes();
assert_eq!(bytes.len(), 172);
assert_eq!(bytes[0], 0x03);
assert_eq!(*bytes.last().unwrap(), 0x05);
for i in 0..5 {
let off = 1 + i * 34;
assert_eq!(bytes[off], 0x21, "key {} push prefix should be 0x21", i);
assert_eq!(
&bytes[off + 1..off + 34],
&pubs[i].to_compressed()[..],
"key {} body must match seed-derived public key",
i
);
}
let expected_hex = concat!(
"03",
"21",
"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
"21",
"02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5",
"21",
"02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9",
"21",
"02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13",
"21",
"022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4",
"05",
);
assert_eq!(hex::encode(&bytes), expected_hex);
assert_eq!(
hex::encode(ms.mpkh()),
"deb7bfb8b45c2bfe4579af5126b46c4d95e4e3a6"
);
}
#[test]
fn from_serialized_bytes_rejects_short() {
let err = MultisigScript::from_serialized_bytes(&[0x01, 0x02, 0x03]).unwrap_err();
assert!(err.to_string().contains("too short"));
}
#[test]
fn from_serialized_bytes_rejects_bad_threshold() {
let mut bytes = vec![0x06];
bytes.push(0x21);
bytes.extend_from_slice(&[0u8; 33]);
bytes.push(0x06);
let err = MultisigScript::from_serialized_bytes(&bytes).unwrap_err();
assert!(err.to_string().contains("invalid threshold byte"));
}
#[test]
fn from_serialized_bytes_rejects_bad_push_prefix() {
let mut bytes = vec![0x01];
bytes.push(0x4c); bytes.extend_from_slice(&[0u8; 33]);
bytes.push(0x01);
let err = MultisigScript::from_serialized_bytes(&bytes).unwrap_err();
assert!(err.to_string().contains("push-prefix"));
}
#[test]
fn from_serialized_bytes_rejects_threshold_exceeds_count() {
let mut bytes = vec![0x05];
bytes.push(0x21);
let pk = det_pubkey(1).to_compressed();
bytes.extend_from_slice(&pk);
bytes.push(0x01);
let err = MultisigScript::from_serialized_bytes(&bytes).unwrap_err();
assert!(
err.to_string().contains("threshold") || err.to_string().contains("exceeds"),
"unexpected error: {}",
err
);
}
#[test]
fn unlock_rejects_wrong_key_count() {
let (privs, pubs) = gen_keys(3);
let ms = MultisigScript::new(2, pubs).unwrap();
let err = unlock(privs, ms, None).unwrap_err();
assert!(err.to_string().contains("expected 2 private keys"));
}
#[test]
fn estimate_length_2_of_3() {
let (privs, pubs) = gen_keys(3);
let ms = MultisigScript::new(2, pubs).unwrap();
let unlocker = unlock(vec![privs[0].clone(), privs[1].clone()], ms, None).unwrap();
let tx = Transaction::default();
let est = unlocker.estimate_length(&tx, 0);
let expected = 1 + 2 * 73 + 2 + (2 + 3 * 34);
assert_eq!(est, expected);
}
use crate::input::TransactionInput;
use crate::output::TransactionOutput;
fn mock_tx_with_source(satoshis: u64) -> Transaction {
let locking_script = Script::from_asm(
"OP_DUP OP_HASH160 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa OP_EQUALVERIFY OP_CHECKSIG",
)
.unwrap();
let source_output = TransactionOutput {
satoshis,
locking_script: locking_script.clone(),
change: false,
};
let mut input = TransactionInput::new();
input.source_txid = [0u8; 32];
input.source_tx_out_index = 0;
input.set_source_output(Some(source_output));
let mut tx = Transaction::new();
tx.add_input(input);
tx.add_output(TransactionOutput {
satoshis: satoshis.saturating_sub(1000),
locking_script,
change: false,
});
tx
}
#[test]
fn p2mpkh_sign_2_of_3_script_structure() {
let (privs, pubs) = gen_keys(3);
let ms = MultisigScript::new(2, pubs).unwrap();
let ms_bytes = ms.to_serialized_bytes();
let unlocker = unlock(vec![privs[0].clone(), privs[1].clone()], ms, None).unwrap();
let tx = mock_tx_with_source(10_000);
let script = unlocker.sign(&tx, 0).unwrap();
let chunks = script.chunks().unwrap();
assert_eq!(chunks.len(), 4, "expected OP_0 + 2 sigs + redeem push");
let dummy = &chunks[0];
assert!(
dummy.data.is_none() || dummy.data.as_ref().is_some_and(|d| d.is_empty()),
"first chunk must be OP_0 (empty push)"
);
for i in 1..=2 {
let sig = chunks[i].data.as_ref().expect("signature push expected");
assert!(
sig.len() >= 71 && sig.len() <= 73,
"signature {} length {} out of range",
i - 1,
sig.len()
);
assert_eq!(*sig.last().unwrap(), 0x41);
}
let redeem = chunks[3].data.as_ref().expect("redeem push expected");
assert_eq!(redeem, &ms_bytes);
}
#[test]
fn p2mpkh_sign_1_of_1() {
let (privs, pubs) = gen_keys(1);
let ms = MultisigScript::new(1, pubs).unwrap();
let ms_bytes = ms.to_serialized_bytes();
let unlocker = unlock(vec![privs[0].clone()], ms, None).unwrap();
let tx = mock_tx_with_source(5_000);
let script = unlocker.sign(&tx, 0).unwrap();
let chunks = script.chunks().unwrap();
assert_eq!(chunks.len(), 3);
let sig = chunks[1].data.as_ref().unwrap();
assert!(sig.len() >= 71 && sig.len() <= 73);
assert_eq!(*sig.last().unwrap(), 0x41);
assert_eq!(chunks[2].data.as_ref().unwrap(), &ms_bytes);
}
#[test]
fn p2mpkh_sign_missing_source_output_returns_error() {
let (privs, pubs) = gen_keys(3);
let ms = MultisigScript::new(2, pubs).unwrap();
let unlocker = unlock(vec![privs[0].clone(), privs[1].clone()], ms, None).unwrap();
let mut tx = Transaction::new();
tx.add_input(TransactionInput::new());
tx.add_output(TransactionOutput::new());
let result = unlocker.sign(&tx, 0);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("missing source output"),
"error should mention missing source output"
);
}
#[test]
fn p2mpkh_sign_custom_sighash_flag() {
use crate::sighash::{SIGHASH_FORKID, SIGHASH_NONE};
let sighash_none_forkid = SIGHASH_NONE | SIGHASH_FORKID;
let (privs, pubs) = gen_keys(2);
let ms = MultisigScript::new(1, pubs).unwrap();
let unlocker = unlock(vec![privs[0].clone()], ms, Some(sighash_none_forkid)).unwrap();
let tx = mock_tx_with_source(8_000);
let script = unlocker.sign(&tx, 0).unwrap();
let chunks = script.chunks().unwrap();
assert_eq!(chunks.len(), 3);
let sig = chunks[1].data.as_ref().unwrap();
assert_eq!(*sig.last().unwrap(), sighash_none_forkid as u8);
}
#[test]
fn p2mpkh_estimate_length_1_of_1() {
let (privs, pubs) = gen_keys(1);
let ms = MultisigScript::new(1, pubs).unwrap();
let unlocker = unlock(vec![privs[0].clone()], ms, None).unwrap();
let tx = Transaction::default();
assert_eq!(unlocker.estimate_length(&tx, 0), 112);
}
#[test]
fn p2mpkh_estimate_length_3_of_5() {
let (privs, pubs) = gen_keys(5);
let ms = MultisigScript::new(3, pubs).unwrap();
let unlocker = unlock(
vec![privs[0].clone(), privs[1].clone(), privs[2].clone()],
ms,
None,
)
.unwrap();
let tx = Transaction::default();
assert_eq!(unlocker.estimate_length(&tx, 0), 394);
}
}