use bitcoin::{
consensus::encode::{deserialize_partial, VarInt},
hashes::Hash,
taproot::TapLeafHash,
PublicKey,
};
use crate::psbt::{PartialSignature, PartialSignatureError};
pub const CCMD_YIELD_MUSIG_PUBNONCE_TAG: u64 = 0xFFFFFFFF;
pub const CCMD_YIELD_MUSIG_PARTIALSIGNATURE_TAG: u64 = 0xFFFFFFFE;
#[derive(Debug, Clone)]
pub struct MusigPubNonce {
pub participant_pubkey: PublicKey,
pub aggregate_pubkey: PublicKey,
pub tapleaf_hash: Option<TapLeafHash>,
pub pubnonce: [u8; 66],
}
#[derive(Debug, Clone)]
pub struct MusigPartialSignature {
pub participant_pubkey: PublicKey,
pub aggregate_pubkey: PublicKey,
pub tapleaf_hash: Option<TapLeafHash>,
pub partial_signature: [u8; 32],
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum SignPsbtYieldedObject {
Partial(PartialSignature),
MusigPubNonce(MusigPubNonce),
MusigPartialSignature(MusigPartialSignature),
Unknown(Vec<u8>),
}
pub fn parse_sign_psbt_yielded(
data: &[u8],
) -> Result<(usize, SignPsbtYieldedObject), PartialSignatureError> {
let (tag, i): (VarInt, usize) =
deserialize_partial(data).map_err(|_| PartialSignatureError::InvalidLength)?;
match tag.0 {
CCMD_YIELD_MUSIG_PUBNONCE_TAG => {
let (input_index, j): (VarInt, usize) = deserialize_partial(&data[i..])
.map_err(|_| PartialSignatureError::InvalidLength)?;
let rest = &data[i + j..];
if rest.len() != 132 && rest.len() != 164 {
return Err(PartialSignatureError::InvalidLength);
}
let mut pubnonce = [0u8; 66];
pubnonce.copy_from_slice(&rest[0..66]);
let participant_pubkey =
PublicKey::from_slice(&rest[66..99]).map_err(PartialSignatureError::PubKey)?;
let aggregate_pubkey =
PublicKey::from_slice(&rest[99..132]).map_err(PartialSignatureError::PubKey)?;
let tapleaf_hash = if rest.len() == 164 {
Some(
TapLeafHash::from_slice(&rest[132..164])
.map_err(PartialSignatureError::TapLeaf)?,
)
} else {
None
};
Ok((
input_index.0 as usize,
SignPsbtYieldedObject::MusigPubNonce(MusigPubNonce {
participant_pubkey,
aggregate_pubkey,
tapleaf_hash,
pubnonce,
}),
))
}
CCMD_YIELD_MUSIG_PARTIALSIGNATURE_TAG => {
let (input_index, j): (VarInt, usize) = deserialize_partial(&data[i..])
.map_err(|_| PartialSignatureError::InvalidLength)?;
let rest = &data[i + j..];
if rest.len() != 98 && rest.len() != 130 {
return Err(PartialSignatureError::InvalidLength);
}
let mut partial_signature = [0u8; 32];
partial_signature.copy_from_slice(&rest[0..32]);
let participant_pubkey =
PublicKey::from_slice(&rest[32..65]).map_err(PartialSignatureError::PubKey)?;
let aggregate_pubkey =
PublicKey::from_slice(&rest[65..98]).map_err(PartialSignatureError::PubKey)?;
let tapleaf_hash = if rest.len() == 130 {
Some(
TapLeafHash::from_slice(&rest[98..130])
.map_err(PartialSignatureError::TapLeaf)?,
)
} else {
None
};
Ok((
input_index.0 as usize,
SignPsbtYieldedObject::MusigPartialSignature(MusigPartialSignature {
participant_pubkey,
aggregate_pubkey,
tapleaf_hash,
partial_signature,
}),
))
}
tag_value if tag_value >= 0x80000000 => {
let (input_index, j): (VarInt, usize) = deserialize_partial(&data[i..])
.map_err(|_| PartialSignatureError::InvalidLength)?;
let rest = &data[i + j..];
Ok((
input_index.0 as usize,
SignPsbtYieldedObject::Unknown(rest.to_vec()),
))
}
_ => {
let input_index = tag.0 as usize;
let ps = PartialSignature::from_slice(&data[i..])?;
Ok((input_index, SignPsbtYieldedObject::Partial(ps)))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use bitcoin::consensus::encode::serialize;
use hex_literal::hex;
const PUBKEY: [u8; 33] =
hex!("034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa");
const XONLY: [u8; 32] =
hex!("4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa");
const TAPLEAF: [u8; 32] =
hex!("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20");
fn parse_ok(payload: &[u8]) -> (usize, SignPsbtYieldedObject) {
match parse_sign_psbt_yielded(payload) {
Ok(v) => v,
Err(_) => panic!("parse_sign_psbt_yielded returned an unexpected error"),
}
}
#[test]
fn parse_legacy_partial_taproot_no_tapleaf() {
let mut payload = serialize(&VarInt(3));
payload.push(32);
payload.extend(XONLY);
payload.extend([0xAAu8; 64]);
let (idx, obj) = parse_ok(&payload);
assert_eq!(idx, 3);
match obj {
SignPsbtYieldedObject::Partial(PartialSignature::TapScriptSig(_, tlh, _)) => {
assert!(tlh.is_none());
}
other => panic!("expected TapScriptSig without tapleaf, got {:?}", other),
}
}
fn build_musig_pubnonce(input_index: u64, with_tapleaf: bool) -> Vec<u8> {
let mut payload = serialize(&VarInt(CCMD_YIELD_MUSIG_PUBNONCE_TAG));
payload.extend(serialize(&VarInt(input_index)));
payload.extend([0xAAu8; 66]); payload.extend(PUBKEY); payload.extend(PUBKEY); if with_tapleaf {
payload.extend(TAPLEAF);
}
payload
}
#[test]
fn parse_musig_pubnonce_without_tapleaf() {
let payload = build_musig_pubnonce(7, false);
let (idx, obj) = parse_ok(&payload);
assert_eq!(idx, 7);
match obj {
SignPsbtYieldedObject::MusigPubNonce(n) => {
assert!(n.tapleaf_hash.is_none());
assert_eq!(n.pubnonce, [0xAAu8; 66]);
assert_eq!(n.participant_pubkey.to_bytes(), PUBKEY);
assert_eq!(n.aggregate_pubkey.to_bytes(), PUBKEY);
}
other => panic!("expected MusigPubNonce, got {:?}", other),
}
}
#[test]
fn parse_musig_pubnonce_with_tapleaf() {
let payload = build_musig_pubnonce(0, true);
let (idx, obj) = parse_ok(&payload);
assert_eq!(idx, 0);
match obj {
SignPsbtYieldedObject::MusigPubNonce(n) => {
let tlh = n.tapleaf_hash.expect("tapleaf hash should be present");
assert_eq!(tlh.to_byte_array(), TAPLEAF);
}
other => panic!("expected MusigPubNonce, got {:?}", other),
}
}
fn build_musig_partial_sig(input_index: u64, with_tapleaf: bool) -> Vec<u8> {
let mut payload = serialize(&VarInt(CCMD_YIELD_MUSIG_PARTIALSIGNATURE_TAG));
payload.extend(serialize(&VarInt(input_index)));
payload.extend([0xBBu8; 32]); payload.extend(PUBKEY); payload.extend(PUBKEY); if with_tapleaf {
payload.extend(TAPLEAF);
}
payload
}
#[test]
fn parse_musig_partial_signature_without_tapleaf() {
let payload = build_musig_partial_sig(2, false);
let (idx, obj) = parse_ok(&payload);
assert_eq!(idx, 2);
match obj {
SignPsbtYieldedObject::MusigPartialSignature(s) => {
assert!(s.tapleaf_hash.is_none());
assert_eq!(s.partial_signature, [0xBBu8; 32]);
assert_eq!(s.participant_pubkey.to_bytes(), PUBKEY);
assert_eq!(s.aggregate_pubkey.to_bytes(), PUBKEY);
}
other => panic!("expected MusigPartialSignature, got {:?}", other),
}
}
#[test]
fn parse_musig_partial_signature_with_tapleaf() {
let payload = build_musig_partial_sig(42, true);
let (idx, obj) = parse_ok(&payload);
assert_eq!(idx, 42);
match obj {
SignPsbtYieldedObject::MusigPartialSignature(s) => {
let tlh = s.tapleaf_hash.expect("tapleaf hash should be present");
assert_eq!(tlh.to_byte_array(), TAPLEAF);
}
other => panic!("expected MusigPartialSignature, got {:?}", other),
}
}
#[test]
fn parse_unknown_reserved_tag() {
const UNKNOWN_TAG: u64 = 0x89AB_CDEF;
let trailer = hex!("deadbeef");
let mut payload = serialize(&VarInt(UNKNOWN_TAG));
payload.extend(serialize(&VarInt(11)));
payload.extend(&trailer);
let (idx, obj) = parse_ok(&payload);
assert_eq!(idx, 11);
match obj {
SignPsbtYieldedObject::Unknown(bytes) => assert_eq!(bytes, trailer),
other => panic!("expected Unknown, got {:?}", other),
}
}
#[test]
fn parse_musig_pubnonce_wrong_trailer_length_is_error() {
let mut payload = build_musig_pubnonce(0, false);
payload.push(0x00);
assert!(matches!(
parse_sign_psbt_yielded(&payload),
Err(PartialSignatureError::InvalidLength)
));
}
#[test]
fn parse_musig_partial_signature_wrong_trailer_length_is_error() {
let mut payload = build_musig_partial_sig(0, false);
payload.push(0x00);
assert!(matches!(
parse_sign_psbt_yielded(&payload),
Err(PartialSignatureError::InvalidLength)
));
}
#[test]
fn parse_musig_pubnonce_truncated_input_is_error() {
let full = build_musig_pubnonce(0, false);
let truncated = &full[..72];
assert!(matches!(
parse_sign_psbt_yielded(truncated),
Err(PartialSignatureError::InvalidLength)
));
}
#[test]
fn parse_musig_pubnonce_invalid_participant_pubkey_is_error() {
let mut payload = build_musig_pubnonce(0, false);
let pubkey_offset = 72;
for b in &mut payload[pubkey_offset..pubkey_offset + 33] {
*b = 0x00;
}
assert!(matches!(
parse_sign_psbt_yielded(&payload),
Err(PartialSignatureError::PubKey(_))
));
}
}