Skip to content

Add musig string and serde support #797

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 187 additions & 2 deletions src/musig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ use std;

use crate::ffi::{self, CPtr};
use crate::{
schnorr, Error, Keypair, Message, PublicKey, Scalar, Secp256k1, SecretKey, Signing,
from_hex, schnorr, Error, Keypair, Message, PublicKey, Scalar, Secp256k1, SecretKey, Signing,
Verification, XOnlyPublicKey,
};

/// Musig partial signature parsing errors
/// Musig parsing errors
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash)]
pub enum ParseError {
/// Parse Argument is malformed. This might occur if the point is on the secp order,
Expand Down Expand Up @@ -222,6 +222,62 @@ impl CPtr for PartialSignature {
fn as_mut_c_ptr(&mut self) -> *mut Self::Target { self.as_mut_ptr() }
}

impl fmt::LowerHex for PartialSignature {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for b in self.serialize() {
write!(f, "{:02x}", b)?;
}
Ok(())
}
}

impl fmt::Display for PartialSignature {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::LowerHex::fmt(self, f) }
}

impl core::str::FromStr for PartialSignature {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut res = [0u8; ffi::MUSIG_PART_SIG_SERIALIZED_LEN];
match from_hex(s, &mut res) {
Ok(ffi::MUSIG_PART_SIG_SERIALIZED_LEN) => PartialSignature::from_byte_array(&res),
_ => Err(ParseError::MalformedArg),
}
}
}

#[cfg(feature = "serde")]
impl serde::Serialize for PartialSignature {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
if s.is_human_readable() {
s.collect_str(self)
} else {
s.serialize_bytes(&self.serialize()[..])
}
}
}

#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for PartialSignature {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
if d.is_human_readable() {
d.deserialize_str(super::serde_util::FromStrVisitor::new(
"a hex string representing a MuSig2 partial signature",
))
} else {
d.deserialize_bytes(super::serde_util::BytesVisitor::new(
"a raw MuSig2 partial signature",
|slice| {
let bytes: &[u8; ffi::MUSIG_PART_SIG_SERIALIZED_LEN] =
slice.try_into().map_err(|_| ParseError::MalformedArg)?;

Self::from_byte_array(bytes)
},
))
}
}
}

impl PartialSignature {
/// Serialize a PartialSignature as a byte array.
pub fn serialize(&self) -> [u8; ffi::MUSIG_PART_SIG_SERIALIZED_LEN] {
Expand Down Expand Up @@ -635,6 +691,62 @@ impl CPtr for PublicNonce {
fn as_mut_c_ptr(&mut self) -> *mut Self::Target { self.as_mut_ptr() }
}

impl fmt::LowerHex for PublicNonce {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for b in self.serialize() {
write!(f, "{:02x}", b)?;
}
Ok(())
}
}

impl fmt::Display for PublicNonce {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::LowerHex::fmt(self, f) }
}

impl core::str::FromStr for PublicNonce {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut res = [0u8; ffi::MUSIG_PUBNONCE_SERIALIZED_LEN];
match from_hex(s, &mut res) {
Ok(ffi::MUSIG_PUBNONCE_SERIALIZED_LEN) => PublicNonce::from_byte_array(&res),
_ => Err(ParseError::MalformedArg),
}
}
}

#[cfg(feature = "serde")]
impl serde::Serialize for PublicNonce {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
if s.is_human_readable() {
s.collect_str(self)
} else {
s.serialize_bytes(&self.serialize()[..])
}
}
}

#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for PublicNonce {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
if d.is_human_readable() {
d.deserialize_str(super::serde_util::FromStrVisitor::new(
"a hex string representing a MuSig2 public nonce",
))
} else {
d.deserialize_bytes(super::serde_util::BytesVisitor::new(
"a raw MuSig2 public nonce",
|slice| {
let bytes: &[u8; ffi::MUSIG_PUBNONCE_SERIALIZED_LEN] =
slice.try_into().map_err(|_| ParseError::MalformedArg)?;

Self::from_byte_array(bytes)
},
))
}
}
}

impl PublicNonce {
/// Serialize a PublicNonce
pub fn serialize(&self) -> [u8; ffi::MUSIG_PUBNONCE_SERIALIZED_LEN] {
Expand Down Expand Up @@ -696,6 +808,62 @@ impl CPtr for AggregatedNonce {
fn as_mut_c_ptr(&mut self) -> *mut Self::Target { self.as_mut_ptr() }
}

impl fmt::LowerHex for AggregatedNonce {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for b in self.serialize() {
write!(f, "{:02x}", b)?;
}
Ok(())
}
}

impl fmt::Display for AggregatedNonce {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::LowerHex::fmt(self, f) }
}

impl core::str::FromStr for AggregatedNonce {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut res = [0u8; ffi::MUSIG_AGGNONCE_SERIALIZED_LEN];
match from_hex(s, &mut res) {
Ok(ffi::MUSIG_AGGNONCE_SERIALIZED_LEN) => AggregatedNonce::from_byte_array(&res),
_ => Err(ParseError::MalformedArg),
}
}
}

#[cfg(feature = "serde")]
impl serde::Serialize for AggregatedNonce {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
if s.is_human_readable() {
s.collect_str(self)
} else {
s.serialize_bytes(&self.serialize()[..])
}
}
}

#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for AggregatedNonce {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
if d.is_human_readable() {
d.deserialize_str(super::serde_util::FromStrVisitor::new(
"a hex string representing a MuSig2 aggregated nonce",
))
} else {
d.deserialize_bytes(super::serde_util::BytesVisitor::new(
"a raw MuSig2 aggregated nonce",
|slice| {
let bytes: &[u8; ffi::MUSIG_AGGNONCE_SERIALIZED_LEN] =
slice.try_into().map_err(|_| ParseError::MalformedArg)?;

Self::from_byte_array(bytes)
},
))
}
}
}

impl AggregatedNonce {
/// Combine received public nonces into a single aggregated nonce
///
Expand Down Expand Up @@ -1520,4 +1688,21 @@ mod tests {

let _agg_sig = session.partial_sig_agg(&[]);
}

#[test]
fn de_serialization() {
const MUSIG_PUBLIC_NONCE_HEX: &str = "03f4a361abd3d50535be08421dbc73b0a8f595654ae3238afcaf2599f94e25204c036ba174214433e21f5cd0fcb14b038eb40b05b7e7c820dd21aa568fdb0a9de4d7";
let pubnonce: PublicNonce = MUSIG_PUBLIC_NONCE_HEX.parse().unwrap();

assert_eq!(pubnonce.to_string(), MUSIG_PUBLIC_NONCE_HEX);

const MUSIG_AGGREGATED_NONCE_HEX: &str = "0218c30fe0f567a4a9c05eb4835e2735419cf30f834c9ce2fe3430f021ba4eacd503112e97bcf6a022d236d71a9357824a2b19515f980131b3970b087cadf94cc4a7";
let aggregated_nonce: AggregatedNonce = MUSIG_AGGREGATED_NONCE_HEX.parse().unwrap();
assert_eq!(aggregated_nonce.to_string(), MUSIG_AGGREGATED_NONCE_HEX);

const MUSIG_PARTIAL_SIGNATURE_HEX: &str =
"289eeb2f5efc314aa6d87bf58125043c96d15a007db4b6aaaac7d18086f49a99";
let partial_signature: PartialSignature = MUSIG_PARTIAL_SIGNATURE_HEX.parse().unwrap();
assert_eq!(partial_signature.to_string(), MUSIG_PARTIAL_SIGNATURE_HEX);
}
}
59 changes: 58 additions & 1 deletion tests/serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ extern crate bincode;
extern crate secp256k1;
extern crate serde_cbor;

use secp256k1::{musig, PublicKey, SecretKey, XOnlyPublicKey};
#[cfg(feature = "global-context")]
use secp256k1::{Keypair, Secp256k1};
use secp256k1::{PublicKey, SecretKey, XOnlyPublicKey};

// Arbitrary key data.

Expand Down Expand Up @@ -35,6 +35,43 @@ static XONLY_PK_BYTES: [u8; 32] = [
0x4a, 0xc8, 0x87, 0xfe, 0x91, 0xdd, 0xd1, 0x66,
];

#[rustfmt::skip]
static MUSIG_PUBLIC_NONCE_BYTES: [u8; 74] = [
0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x03, 0xf4, 0xa3, 0x61, 0xab, 0xd3, 0xd5, 0x05,
0x35, 0xbe, 0x08, 0x42, 0x1d, 0xbc, 0x73, 0xb0,
0xa8, 0xf5, 0x95, 0x65, 0x4a, 0xe3, 0x23, 0x8a,
0xfc, 0xaf, 0x25, 0x99, 0xf9, 0x4e, 0x25, 0x20,
0x4c, 0x03, 0x6b, 0xa1, 0x74, 0x21, 0x44, 0x33,
0xe2, 0x1f, 0x5c, 0xd0, 0xfc, 0xb1, 0x4b, 0x03,
0x8e, 0xb4, 0x0b, 0x05, 0xb7, 0xe7, 0xc8, 0x20,
0xdd, 0x21, 0xaa, 0x56, 0x8f, 0xdb, 0x0a, 0x9d,
0xe4, 0xd7,
];

#[rustfmt::skip]
static MUSIG_AGGREGATED_NONCE_BYTES: [u8; 74] = [
0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x18, 0xc3, 0x0f, 0xe0, 0xf5, 0x67, 0xa4,
0xa9, 0xc0, 0x5e, 0xb4, 0x83, 0x5e, 0x27, 0x35,
0x41, 0x9c, 0xf3, 0x0f, 0x83, 0x4c, 0x9c, 0xe2,
0xfe, 0x34, 0x30, 0xf0, 0x21, 0xba, 0x4e, 0xac,
0xd5, 0x03, 0x11, 0x2e, 0x97, 0xbc, 0xf6, 0xa0,
0x22, 0xd2, 0x36, 0xd7, 0x1a, 0x93, 0x57, 0x82,
0x4a, 0x2b, 0x19, 0x51, 0x5f, 0x98, 0x01, 0x31,
0xb3, 0x97, 0x0b, 0x08, 0x7c, 0xad, 0xf9, 0x4c,
0xc4, 0xa7,
];

#[rustfmt::skip]
static MUSIG_PARTIAL_SIG_BYTES: [u8; 40] = [
0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x28, 0x9e, 0xeb, 0x2f, 0x5e, 0xfc, 0x31, 0x4a,
0xa6, 0xd8, 0x7b, 0xf5, 0x81, 0x25, 0x04, 0x3c,
0x96, 0xd1, 0x5a, 0x00, 0x7d, 0xb4, 0xb6, 0xaa,
0xaa, 0xc7, 0xd1, 0x80, 0x86, 0xf4, 0x9a, 0x99,
];

fn secret_key() -> SecretKey {
SecretKey::from_slice(&SK_BYTES).expect("failed to create sk from slice")
}
Expand Down Expand Up @@ -85,3 +122,23 @@ fn cbor() {
// It also adds a 1-byte length prefix and a byte of metadata for the whole vector.
assert_eq!(e.len(), 54);
}

#[test]
fn musig() {
let public_nonce: musig::PublicNonce = bincode::deserialize(&MUSIG_PUBLIC_NONCE_BYTES).unwrap();
let ser = bincode::serialize(&public_nonce).unwrap();

assert_eq!(ser, MUSIG_PUBLIC_NONCE_BYTES);

let aggregated_nonce: musig::AggregatedNonce =
bincode::deserialize(&MUSIG_AGGREGATED_NONCE_BYTES).unwrap();
let ser = bincode::serialize(&aggregated_nonce).unwrap();

assert_eq!(ser, MUSIG_AGGREGATED_NONCE_BYTES);

let partial_sig: musig::PartialSignature =
bincode::deserialize(&MUSIG_PARTIAL_SIG_BYTES).unwrap();
let ser = bincode::serialize(&partial_sig).unwrap();

assert_eq!(ser, MUSIG_PARTIAL_SIG_BYTES);
}
Loading