From 3e8e6d9aa14f6eeffb77961326bd6b4c055b82ee Mon Sep 17 00:00:00 2001 From: Daniel Roberts Date: Thu, 1 May 2025 10:52:50 -0500 Subject: [PATCH 1/2] Add BIP-373 PSBT_{IN,OUT}_MUSIG2_PARTICIPANT_PUBKEYS serialization and deserialization --- bitcoin/src/psbt/map/input.rs | 15 +++++++++++++++ bitcoin/src/psbt/map/output.rs | 14 ++++++++++++++ bitcoin/src/psbt/serialize.rs | 22 ++++++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/bitcoin/src/psbt/map/input.rs b/bitcoin/src/psbt/map/input.rs index 287c7f362..2422c57d2 100644 --- a/bitcoin/src/psbt/map/input.rs +++ b/bitcoin/src/psbt/map/input.rs @@ -59,6 +59,8 @@ const PSBT_IN_TAP_BIP32_DERIVATION: u64 = 0x16; const PSBT_IN_TAP_INTERNAL_KEY: u64 = 0x17; /// Type: Taproot Merkle Root PSBT_IN_TAP_MERKLE_ROOT = 0x18 const PSBT_IN_TAP_MERKLE_ROOT: u64 = 0x18; +/// Type: MuSig2 Public Keys Participating in Aggregate Input PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS = 0x1a +const PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS: u64 = 0x1a; /// Type: Proprietary Use Type PSBT_IN_PROPRIETARY = 0xFC const PSBT_IN_PROPRIETARY: u64 = 0xFC; @@ -113,6 +115,8 @@ pub struct Input { pub tap_internal_key: Option, /// Taproot Merkle root. pub tap_merkle_root: Option, + /// Mapping from MuSig2 aggregate keys to the participant keys from which they were aggregated. + pub musig2_participant_pubkeys: BTreeMap>, /// Proprietary key-value pairs for this input. pub proprietary: BTreeMap>, /// Unknown key-value pairs for this input. @@ -352,6 +356,11 @@ impl Input { self.tap_merkle_root <= |< raw_value: TapNodeHash> } } + PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS => { + impl_psbt_insert_pair! { + self.musig2_participant_pubkeys <= |< raw_value: Vec > + } + } PSBT_IN_PROPRIETARY => { let key = raw::ProprietaryKey::try_from(raw_key.clone())?; match self.proprietary.entry(key) { @@ -390,6 +399,7 @@ impl Input { self.tap_script_sigs.extend(other.tap_script_sigs); self.tap_scripts.extend(other.tap_scripts); self.tap_key_origins.extend(other.tap_key_origins); + self.musig2_participant_pubkeys.extend(other.musig2_participant_pubkeys); self.proprietary.extend(other.proprietary); self.unknown.extend(other.unknown); @@ -482,6 +492,11 @@ impl Map for Input { impl_psbt_get_pair! { rv.push(self.tap_merkle_root, PSBT_IN_TAP_MERKLE_ROOT) } + + impl_psbt_get_pair! { + rv.push_map(self.musig2_participant_pubkeys, PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS) + } + for (key, value) in self.proprietary.iter() { rv.push(raw::Pair { key: key.to_key(), value: value.clone() }); } diff --git a/bitcoin/src/psbt/map/output.rs b/bitcoin/src/psbt/map/output.rs index 7f86bdb65..37e853f81 100644 --- a/bitcoin/src/psbt/map/output.rs +++ b/bitcoin/src/psbt/map/output.rs @@ -20,6 +20,8 @@ const PSBT_OUT_TAP_INTERNAL_KEY: u64 = 0x05; const PSBT_OUT_TAP_TREE: u64 = 0x06; /// Type: Taproot Key BIP 32 Derivation Path PSBT_OUT_TAP_BIP32_DERIVATION = 0x07 const PSBT_OUT_TAP_BIP32_DERIVATION: u64 = 0x07; +/// Type: MuSig2 Public Keys Participating in Aggregate Output PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS = 0x08 +const PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS: u64 = 0x08; /// Type: Proprietary Use Type PSBT_IN_PROPRIETARY = 0xFC const PSBT_OUT_PROPRIETARY: u64 = 0xFC; @@ -40,6 +42,8 @@ pub struct Output { pub tap_tree: Option, /// Map of tap root x only keys to origin info and leaf hashes contained in it. pub tap_key_origins: BTreeMap, KeySource)>, + /// Mapping from MuSig2 aggregate keys to the participant keys from which they were aggregated. + pub musig2_participant_pubkeys: BTreeMap>, /// Proprietary key-value pairs for this output. pub proprietary: BTreeMap>, /// Unknown key-value pairs for this output. @@ -90,6 +94,11 @@ impl Output { self.tap_key_origins <= |< raw_value: (Vec, KeySource)> } } + PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS => { + impl_psbt_insert_pair! { + self.musig2_participant_pubkeys <= |< raw_value: Vec > + } + } _ => match self.unknown.entry(raw_key) { btree_map::Entry::Vacant(empty_key) => { empty_key.insert(raw_value); @@ -107,6 +116,7 @@ impl Output { self.proprietary.extend(other.proprietary); self.unknown.extend(other.unknown); self.tap_key_origins.extend(other.tap_key_origins); + self.musig2_participant_pubkeys.extend(other.musig2_participant_pubkeys); combine!(redeem_script, self, other); combine!(witness_script, self, other); @@ -143,6 +153,10 @@ impl Map for Output { rv.push_map(self.tap_key_origins, PSBT_OUT_TAP_BIP32_DERIVATION) } + impl_psbt_get_pair! { + rv.push_map(self.musig2_participant_pubkeys, PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS) + } + for (key, value) in self.proprietary.iter() { rv.push(raw::Pair { key: key.to_key(), value: value.clone() }); } diff --git a/bitcoin/src/psbt/serialize.rs b/bitcoin/src/psbt/serialize.rs index 8f6193fd2..270f8a75e 100644 --- a/bitcoin/src/psbt/serialize.rs +++ b/bitcoin/src/psbt/serialize.rs @@ -172,6 +172,28 @@ impl Deserialize for secp256k1::PublicKey { } } +impl Serialize for Vec { + fn serialize(&self) -> Vec { + let mut result: Vec = Vec::with_capacity(secp256k1::constants::PUBLIC_KEY_SIZE * self.len()); + + for pubkey in self.iter() { + result.extend(Serialize::serialize(pubkey)); + } + + result + } +} + +impl Deserialize for Vec { + fn deserialize(bytes: &[u8]) -> Result { + bytes.chunks(secp256k1::constants::PUBLIC_KEY_SIZE) + .map(|pubkey_bytes| { + secp256k1::PublicKey::deserialize(pubkey_bytes) + }) + .collect() + } +} + impl Serialize for ecdsa::Signature { fn serialize(&self) -> Vec { self.to_vec() } } From 2481695b456bcccbb25c247c1fd39bbda24dbb30 Mon Sep 17 00:00:00 2001 From: Daniel Roberts Date: Thu, 1 May 2025 11:47:59 -0500 Subject: [PATCH 2/2] Add tests for BIP-373 PSBT_{IN,OUT}_MUSIG2_PARTICIPANT_PUBKEYS serialization and deserialization --- bitcoin/src/psbt/mod.rs | 45 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/bitcoin/src/psbt/mod.rs b/bitcoin/src/psbt/mod.rs index 72f053c2f..1c3e7368b 100644 --- a/bitcoin/src/psbt/mod.rs +++ b/bitcoin/src/psbt/mod.rs @@ -1341,6 +1341,8 @@ mod tests { secp256k1::{All, SecretKey}, }; + use std::str::FromStr; + use super::*; use crate::address::script_pubkey::ScriptExt as _; use crate::bip32::ChildNumber; @@ -2237,6 +2239,49 @@ mod tests { assert!(!rtt.proprietary.is_empty()); } + // Deserialize MuSig2 PSBT participant keys according to BIP-373 + #[test] + fn serialize_and_deserialize_musig2_participants() { + // XXX: Does not cover PSBT_IN_MUSIG2_PUB_NONCE, PSBT_IN_MUSIG2_PARTIAL_SIG (yet) + + let expected_in_agg_pk = secp256k1::PublicKey::from_str("021401301810a46a4e3f39e4603ec228ed301d9f2079767fda758dee7224b32e00").unwrap(); + let expected_in_pubkeys = vec![ + secp256k1::PublicKey::from_str("02bebd7a1cef20283444b96e9ce78137e951ce48705390933896311a9abc75736a").unwrap(), + secp256k1::PublicKey::from_str("0355212dff7b3d7e8126687a62fd0435a3fb4de56d9af9ae23a1c9ca05b349c8e2").unwrap(), + ]; + + let expected_out_agg_pk = secp256k1::PublicKey::from_str("0364934a64831bd917a2667b886671650846f021e1c025e4b2bb65e49ab3e7cba5").unwrap(); + + let expected_out_pubkeys = vec![ + secp256k1::PublicKey::from_str("02841d69a8b80ae23a8090e6f3765540ea5efd8c287b1307c983a6e2a3a171b525").unwrap(), + secp256k1::PublicKey::from_str("02bad833849a98cdfb0a0749609ddccab16ad54485ecc67f828df4bdc4f2b90d4c").unwrap(), + ]; + + const PSBT_HEX: &str = "70736274ff01005e02000000017b42be5ea467afe0d0571dc4a91bef97ff9605a590c0b8d5892323946414d1810000000000ffffffff01f0b9f50500000000225120bc7e18f55e2c7a28d78cadac1bc72c248372375d269bafe6b315bc40505d07e5000000000001012b00e1f50500000000225120de564ebf8ff7bd9bb41bd88264c04b1713ebb9dc8df36319091d2eabb16cda6221161401301810a46a4e3f39e4603ec228ed301d9f2079767fda758dee7224b32e000500eb4cbe62211655212dff7b3d7e8126687a62fd0435a3fb4de56d9af9ae23a1c9ca05b349c8e20500755abbf92116bebd7a1cef20283444b96e9ce78137e951ce48705390933896311a9abc75736a05002a33dfd90117201401301810a46a4e3f39e4603ec228ed301d9f2079767fda758dee7224b32e00221a021401301810a46a4e3f39e4603ec228ed301d9f2079767fda758dee7224b32e004202bebd7a1cef20283444b96e9ce78137e951ce48705390933896311a9abc75736a0355212dff7b3d7e8126687a62fd0435a3fb4de56d9af9ae23a1c9ca05b349c8e20001052064934a64831bd917a2667b886671650846f021e1c025e4b2bb65e49ab3e7cba5210764934a64831bd917a2667b886671650846f021e1c025e4b2bb65e49ab3e7cba50500fa4c6afa22080364934a64831bd917a2667b886671650846f021e1c025e4b2bb65e49ab3e7cba54202841d69a8b80ae23a8090e6f3765540ea5efd8c287b1307c983a6e2a3a171b52502bad833849a98cdfb0a0749609ddccab16ad54485ecc67f828df4bdc4f2b90d4c00"; + + let psbt = hex_psbt(PSBT_HEX).unwrap(); + + assert_eq!(psbt.inputs[0].musig2_participant_pubkeys.len(), 1); + assert_eq!( + psbt.inputs[0].musig2_participant_pubkeys.iter().next().unwrap(), + (&expected_in_agg_pk, &expected_in_pubkeys) + ); + + assert_eq!(psbt.outputs[0].musig2_participant_pubkeys.len(), 1); + assert_eq!( + psbt.outputs[0].musig2_participant_pubkeys.iter().next().unwrap(), + (&expected_out_agg_pk, &expected_out_pubkeys) + ); + + // Check round trip de/serialization + assert_eq!(psbt.serialize_hex(), PSBT_HEX); + + const PSBT_TRUNCATED_MUSIG_PARTICIPANTS_HEX: &str = "70736274ff01005e0200000001f034711ce319b1db76ce73440f2cb64a7e3a02e75c936b8d8a4958a024ea8d870000000000ffffffff01f0b9f50500000000225120bc7e18f55e2c7a28d78cadac1bc72c248372375d269bafe6b315bc40505d07e5000000000001012b00e1f50500000000225120de564ebf8ff7bd9bb41bd88264c04b1713ebb9dc8df36319091d2eabb16cda6221161401301810a46a4e3f39e4603ec228ed301d9f2079767fda758dee7224b32e000500eb4cbe62211655212dff7b3d7e8126687a62fd0435a3fb4de56d9af9ae23a1c9ca05b349c8e20500755abbf92116bebd7a1cef20283444b96e9ce78137e951ce48705390933896311a9abc75736a05002a33dfd90117201401301810a46a4e3f39e4603ec228ed301d9f2079767fda758dee7224b32e00221a021401301810a46a4e3f39e4603ec228ed301d9f2079767fda758dee7224b32e002a02bebd7a1cef20283444b96e9ce78137e951ce48705390933896311a9abc75736a0355212dff7b3d7e810001052064934a64831bd917a2667b886671650846f021e1c025e4b2bb65e49ab3e7cba5210764934a64831bd917a2667b886671650846f021e1c025e4b2bb65e49ab3e7cba50500fa4c6afa22080364934a64831bd917a2667b886671650846f021e1c025e4b2bb65e49ab3e7cba52a02841d69a8b80ae23a8090e6f3765540ea5efd8c287b1307c983a6e2a3a171b52502bad833849a98cdfb00"; + + hex_psbt(PSBT_TRUNCATED_MUSIG_PARTICIPANTS_HEX) + .expect_err("Deserializing PSBT with truncated musig participants should error"); + } + // PSBTs taken from BIP 174 test vectors. #[test] fn combine_psbts() {