Skip to main content

chains_sdk/threshold/musig2/
signing.rs

1//! MuSig2 key aggregation and signing (BIP-327).
2
3use crate::crypto;
4use crate::error::SignerError;
5use core::fmt;
6use k256::elliptic_curve::group::GroupEncoding;
7use k256::elliptic_curve::ops::Reduce;
8use k256::elliptic_curve::sec1::ToEncodedPoint;
9use k256::{AffinePoint, ProjectivePoint, Scalar};
10use zeroize::Zeroizing;
11
12// ─── Tagged Hash Scalar ─────────────────────────────────────────────
13
14/// Hash to scalar using tagged hash.
15fn tagged_hash_scalar(tag: &[u8], data: &[u8]) -> Scalar {
16    let hash = crypto::tagged_hash(tag, data);
17    let wide = k256::U256::from_be_slice(&hash);
18    <Scalar as Reduce<k256::U256>>::reduce(wide)
19}
20
21// ─── Key Aggregation (BIP-327 KeyAgg) ───────────────────────────────
22
23/// Aggregated key context from `key_agg()`.
24#[derive(Clone)]
25pub struct KeyAggContext {
26    /// The aggregate public key `Q` (combined point).
27    pub aggregate_key: AffinePoint,
28    /// The x-only aggregate public key bytes (32 bytes).
29    pub x_only_pubkey: [u8; 32],
30    /// Per-key aggregation coefficients `a_i`.
31    pub(crate) coefficients: Vec<Scalar>,
32    /// The original (sorted) public keys.
33    pub(crate) pubkeys: Vec<[u8; 33]>,
34    /// Parity flag for the aggregate key (for x-only compatibility).
35    pub(crate) parity: bool,
36}
37
38impl core::fmt::Debug for KeyAggContext {
39    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
40        f.debug_struct("KeyAggContext")
41            .field("x_only_pubkey", &hex::encode(self.x_only_pubkey))
42            .field("coefficients", &"[REDACTED]")
43            .field("num_keys", &self.pubkeys.len())
44            .finish()
45    }
46}
47
48/// A public nonce pair (2 points, each 33 bytes SEC1 compressed).
49#[derive(Clone, Debug)]
50pub struct PubNonce {
51    /// First public nonce `R_1 = G * k_1`.
52    pub r1: AffinePoint,
53    /// Second public nonce `R_2 = G * k_2`.
54    pub r2: AffinePoint,
55}
56
57impl PubNonce {
58    /// Encode as 66 bytes: `R1 (33) || R2 (33)`.
59    pub fn to_bytes(&self) -> [u8; 66] {
60        let r1_enc = ProjectivePoint::from(self.r1)
61            .to_affine()
62            .to_encoded_point(true);
63        let r2_enc = ProjectivePoint::from(self.r2)
64            .to_affine()
65            .to_encoded_point(true);
66        let mut out = [0u8; 66];
67        out[..33].copy_from_slice(r1_enc.as_bytes());
68        out[33..].copy_from_slice(r2_enc.as_bytes());
69        out[33..].copy_from_slice(r2_enc.as_bytes());
70        out
71    }
72}
73
74/// Secret nonce pair (MUST be used exactly once).
75pub struct SecNonce {
76    /// First secret nonce scalar.
77    k1: Zeroizing<Scalar>,
78    /// Second secret nonce scalar.
79    k2: Zeroizing<Scalar>,
80    /// The associated public key (for safety checks).
81    pub(crate) pubkey: [u8; 33],
82}
83
84impl Drop for SecNonce {
85    fn drop(&mut self) {
86        // k1 and k2 are Zeroizing
87    }
88}
89
90/// Aggregated nonce (2 points).
91#[derive(Clone, Debug)]
92pub struct AggNonce {
93    /// First aggregated nonce `R_1 = Σ R_{1,i}`.
94    pub r1: AffinePoint,
95    /// Second aggregated nonce `R_2 = Σ R_{2,i}`.
96    pub r2: AffinePoint,
97}
98
99/// A partial signature scalar.
100#[derive(Clone)]
101pub struct PartialSignature {
102    /// The partial signature scalar.
103    pub s: Scalar,
104}
105
106impl fmt::Debug for PartialSignature {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        f.debug_struct("PartialSignature")
109            .field("s", &"[REDACTED]")
110            .finish()
111    }
112}
113
114/// A final MuSig2 Schnorr signature (64 bytes: x(R) || s).
115#[derive(Clone, Debug)]
116#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
117pub struct MuSig2Signature {
118    /// 32-byte x-coordinate of R.
119    pub r: [u8; 32],
120    /// 32-byte scalar s.
121    pub s: [u8; 32],
122}
123
124impl MuSig2Signature {
125    /// Encode as 64 bytes.
126    pub fn to_bytes(&self) -> [u8; 64] {
127        let mut out = [0u8; 64];
128        out[..32].copy_from_slice(&self.r);
129        out[32..].copy_from_slice(&self.s);
130        out
131    }
132}
133
134// ─── Public Key Utilities ───────────────────────────────────────────
135
136/// Compute the compressed (33-byte) public key from a 32-byte secret key.
137pub fn individual_pubkey(secret_key: &[u8; 32]) -> Result<[u8; 33], SignerError> {
138    let wide = k256::U256::from_be_slice(secret_key);
139    let scalar = <Scalar as Reduce<k256::U256>>::reduce(wide);
140    if scalar == Scalar::ZERO {
141        return Err(SignerError::InvalidPrivateKey("secret key is zero".into()));
142    }
143    let point = (ProjectivePoint::GENERATOR * scalar).to_affine();
144    let encoded = point.to_encoded_point(true);
145    let mut out = [0u8; 33];
146    out.copy_from_slice(encoded.as_bytes());
147    Ok(out)
148}
149
150/// Sort public keys lexicographically (BIP-327 KeySort).
151pub fn key_sort(pubkeys: &[[u8; 33]]) -> Vec<[u8; 33]> {
152    let mut sorted = pubkeys.to_vec();
153    sorted.sort();
154    sorted
155}
156
157/// Compute the hash of all public keys (used in key aggregation coefficient).
158pub(crate) fn hash_keys(pubkeys: &[[u8; 33]]) -> [u8; 32] {
159    let mut data = Vec::with_capacity(pubkeys.len() * 33);
160    for pk in pubkeys {
161        data.extend_from_slice(pk);
162    }
163    crypto::tagged_hash(b"KeyAgg list", &data)
164}
165
166/// Key aggregation: combine N public keys into a single aggregate key (BIP-327).
167///
168/// Each key gets a coefficient `a_i = H("KeyAgg coefficient", L || pk_i)`
169/// where `L = H("KeyAgg list", pk_1 || ... || pk_n)`.
170///
171/// Exception: the "second unique key" gets coefficient 1 for efficiency.
172pub fn key_agg(pubkeys: &[[u8; 33]]) -> Result<KeyAggContext, SignerError> {
173    if pubkeys.is_empty() {
174        return Err(SignerError::InvalidPrivateKey("empty pubkey list".into()));
175    }
176
177    // Validate all public keys
178    for pk in pubkeys {
179        parse_point(pk)?;
180    }
181
182    let pk_list_hash = hash_keys(pubkeys);
183
184    // Find the "second unique key" value (first key that differs from pubkeys[0])
185    let second_key: Option<&[u8; 33]> = pubkeys.iter().find(|pk| *pk != &pubkeys[0]);
186
187    // Compute coefficients
188    // Per BIP-327: a_i = 1 if pk_i == second_key, else H(L || pk_i)
189    let mut coefficients = Vec::with_capacity(pubkeys.len());
190    for pk in pubkeys {
191        let a_i = if second_key == Some(pk) {
192            // All keys equal to the second unique key get coefficient 1
193            Scalar::ONE
194        } else {
195            let mut data = Vec::with_capacity(32 + 33);
196            data.extend_from_slice(&pk_list_hash);
197            data.extend_from_slice(pk);
198            tagged_hash_scalar(b"KeyAgg coefficient", &data)
199        };
200        coefficients.push(a_i);
201    }
202
203    // Aggregate: Q = Σ a_i * P_i
204    let mut q = ProjectivePoint::IDENTITY;
205    for (i, pk) in pubkeys.iter().enumerate() {
206        let point = parse_point(pk)?;
207        q += point * coefficients[i];
208    }
209
210    let q_affine = q.to_affine();
211    let q_encoded = q_affine.to_encoded_point(true);
212    let q_bytes = q_encoded.as_bytes();
213
214    // x-only pubkey (32 bytes)
215    let mut x_only = [0u8; 32];
216    x_only.copy_from_slice(&q_bytes[1..33]);
217
218    // Parity: if the y-coordinate is odd, we negate
219    let parity = q_bytes[0] == 0x03;
220
221    Ok(KeyAggContext {
222        aggregate_key: q_affine,
223        x_only_pubkey: x_only,
224        coefficients,
225        pubkeys: pubkeys.to_vec(),
226        parity,
227    })
228}
229
230// ─── Nonce Generation ───────────────────────────────────────────────
231
232/// Generate a secret/public nonce pair for MuSig2 signing.
233///
234/// # Security
235/// The returned `SecNonce` MUST be used exactly once and then discarded.
236/// Nonce reuse across different messages leads to private key extraction.
237pub fn nonce_gen(
238    _secret_key: &[u8; 32],
239    pubkey: &[u8; 33],
240    key_agg_ctx: &KeyAggContext,
241    msg: &[u8],
242    extra_in: &[u8],
243) -> Result<(SecNonce, PubNonce), SignerError> {
244    // Generate random seed
245    let mut rand_bytes = [0u8; 32];
246    crate::security::secure_random(&mut rand_bytes)?;
247
248    // k_1 = H("MuSig/nonce" || rand || pk || agg_pk || msg_prefixed || extra)
249    let k1 = {
250        let mut data = Vec::new();
251        data.extend_from_slice(&rand_bytes);
252        data.extend_from_slice(pubkey);
253        data.extend_from_slice(&key_agg_ctx.x_only_pubkey);
254        data.push(0x01); // nonce index
255        data.extend_from_slice(msg);
256        data.extend_from_slice(extra_in);
257        let hash = crypto::tagged_hash(b"MuSig/nonce", &data);
258        let wide = k256::U256::from_be_slice(&hash);
259        let s = <Scalar as Reduce<k256::U256>>::reduce(wide);
260        if s == Scalar::ZERO {
261            return Err(SignerError::EntropyError);
262        }
263        s
264    };
265
266    // k_2 = H("MuSig/nonce" || rand || pk || agg_pk || msg_prefixed || extra) with different index
267    let k2 = {
268        let mut data = Vec::new();
269        data.extend_from_slice(&rand_bytes);
270        data.extend_from_slice(pubkey);
271        data.extend_from_slice(&key_agg_ctx.x_only_pubkey);
272        data.push(0x02); // nonce index
273        data.extend_from_slice(msg);
274        data.extend_from_slice(extra_in);
275        let hash = crypto::tagged_hash(b"MuSig/nonce", &data);
276        let wide = k256::U256::from_be_slice(&hash);
277        let s = <Scalar as Reduce<k256::U256>>::reduce(wide);
278        if s == Scalar::ZERO {
279            return Err(SignerError::EntropyError);
280        }
281        s
282    };
283
284    let r1 = (ProjectivePoint::GENERATOR * k1).to_affine();
285    let r2 = (ProjectivePoint::GENERATOR * k2).to_affine();
286
287    let sec_nonce = SecNonce {
288        k1: Zeroizing::new(k1),
289        k2: Zeroizing::new(k2),
290        pubkey: *pubkey,
291    };
292
293    let pub_nonce = PubNonce { r1, r2 };
294
295    Ok((sec_nonce, pub_nonce))
296}
297
298/// Aggregate public nonces from all signers.
299pub fn nonce_agg(pub_nonces: &[PubNonce]) -> Result<AggNonce, SignerError> {
300    if pub_nonces.is_empty() {
301        return Err(SignerError::InvalidPrivateKey("empty nonce list".into()));
302    }
303
304    let mut r1 = ProjectivePoint::IDENTITY;
305    let mut r2 = ProjectivePoint::IDENTITY;
306
307    for pn in pub_nonces {
308        r1 += ProjectivePoint::from(pn.r1);
309        r2 += ProjectivePoint::from(pn.r2);
310    }
311
312    Ok(AggNonce {
313        r1: r1.to_affine(),
314        r2: r2.to_affine(),
315    })
316}
317
318// ─── Partial Signing ────────────────────────────────────────────────
319
320/// Compute the nonce coefficient `b` from the session context.
321pub(crate) fn compute_nonce_coeff(
322    agg_nonce: &AggNonce,
323    x_only_pubkey: &[u8; 32],
324    msg: &[u8],
325) -> Scalar {
326    let r1_enc = ProjectivePoint::from(agg_nonce.r1)
327        .to_affine()
328        .to_encoded_point(true);
329    let r2_enc = ProjectivePoint::from(agg_nonce.r2)
330        .to_affine()
331        .to_encoded_point(true);
332
333    let mut data = Vec::new();
334    data.extend_from_slice(r1_enc.as_bytes());
335    data.extend_from_slice(r2_enc.as_bytes());
336    data.extend_from_slice(x_only_pubkey);
337    data.extend_from_slice(msg);
338
339    tagged_hash_scalar(b"MuSig/noncecoef", &data)
340}
341
342/// Produce a partial signature.
343///
344/// `s_i = k1_i + b * k2_i + e * a_i * sk_i`
345/// where `e` is the BIP-340 challenge and `a_i` is the key aggregation coefficient.
346pub fn sign(
347    sec_nonce: SecNonce,
348    secret_key: &[u8; 32],
349    key_agg_ctx: &KeyAggContext,
350    agg_nonce: &AggNonce,
351    msg: &[u8],
352) -> Result<PartialSignature, SignerError> {
353    let sk_wide = k256::U256::from_be_slice(secret_key);
354    let sk_scalar = <Scalar as Reduce<k256::U256>>::reduce(sk_wide);
355
356    // Compute nonce coefficient b
357    let b = compute_nonce_coeff(agg_nonce, &key_agg_ctx.x_only_pubkey, msg);
358
359    // Effective nonce: R = R1 + b * R2
360    let r = ProjectivePoint::from(agg_nonce.r1) + ProjectivePoint::from(agg_nonce.r2) * b;
361    let r_affine = r.to_affine();
362    let r_encoded = r_affine.to_encoded_point(true);
363    let r_bytes = r_encoded.as_bytes();
364
365    // x-only R for BIP-340 compatibility
366    let mut r_x = [0u8; 32];
367    r_x.copy_from_slice(&r_bytes[1..33]);
368
369    // Negate nonce if R has odd y
370    let nonce_negated = r_bytes[0] == 0x03;
371
372    // BIP-340 challenge: e = H("BIP0340/challenge", R_x || P_x || msg)
373    let mut challenge_data = Vec::new();
374    challenge_data.extend_from_slice(&r_x);
375    challenge_data.extend_from_slice(&key_agg_ctx.x_only_pubkey);
376    challenge_data.extend_from_slice(msg);
377    let e = tagged_hash_scalar(b"BIP0340/challenge", &challenge_data);
378
379    // Find my coefficient
380    let my_idx = key_agg_ctx
381        .pubkeys
382        .iter()
383        .position(|pk| pk == &sec_nonce.pubkey)
384        .ok_or_else(|| SignerError::SigningFailed("pubkey not in key_agg context".into()))?;
385    let a_i = key_agg_ctx.coefficients[my_idx];
386
387    // Effective secret key (negate if aggregate key has odd y)
388    let mut d = sk_scalar;
389    if key_agg_ctx.parity {
390        d = -d;
391    }
392
393    // Effective nonces (negate if R has odd y)
394    let mut k1 = *sec_nonce.k1;
395    let mut k2 = *sec_nonce.k2;
396    if nonce_negated {
397        k1 = -k1;
398        k2 = -k2;
399    }
400
401    // s_i = k1 + b*k2 + e * a_i * d
402    let s = k1 + b * k2 + e * a_i * d;
403
404    Ok(PartialSignature { s })
405}
406
407/// Aggregate partial signatures into a final MuSig2 Schnorr signature.
408///
409/// Returns a 64-byte BIP-340 compatible signature: `x(R) || s`.
410pub fn partial_sig_agg(
411    partial_sigs: &[PartialSignature],
412    agg_nonce: &AggNonce,
413    key_agg_ctx: &KeyAggContext,
414    msg: &[u8],
415) -> Result<MuSig2Signature, SignerError> {
416    if partial_sigs.is_empty() {
417        return Err(SignerError::SigningFailed(
418            "empty partial signatures".into(),
419        ));
420    }
421
422    // Compute effective R
423    let b = compute_nonce_coeff(agg_nonce, &key_agg_ctx.x_only_pubkey, msg);
424    let r = ProjectivePoint::from(agg_nonce.r1) + ProjectivePoint::from(agg_nonce.r2) * b;
425    let r_affine = r.to_affine();
426    let r_encoded = r_affine.to_encoded_point(true);
427    let r_bytes = r_encoded.as_bytes();
428
429    // x-only R
430    let mut r_x = [0u8; 32];
431    r_x.copy_from_slice(&r_bytes[1..33]);
432
433    // Sum partial signatures
434    let mut s = Scalar::ZERO;
435    for psig in partial_sigs {
436        s += psig.s;
437    }
438
439    Ok(MuSig2Signature {
440        r: r_x,
441        s: s.to_bytes().into(),
442    })
443}
444
445/// Verify a MuSig2 signature using standard BIP-340 Schnorr verification.
446///
447/// `s * G == R + e * P`
448pub fn verify(
449    sig: &MuSig2Signature,
450    x_only_pubkey: &[u8; 32],
451    msg: &[u8],
452) -> Result<bool, SignerError> {
453    // Parse R as a point with even y
454    let mut r_sec1 = [0u8; 33];
455    r_sec1[0] = 0x02; // even y
456    r_sec1[1..].copy_from_slice(&sig.r);
457    let r_point = parse_point(&r_sec1)?;
458
459    // Parse s
460    let s_wide = k256::U256::from_be_slice(&sig.s);
461    let s_scalar = <Scalar as Reduce<k256::U256>>::reduce(s_wide);
462
463    // Parse public key as point with even y
464    let mut pk_sec1 = [0u8; 33];
465    pk_sec1[0] = 0x02; // even y
466    pk_sec1[1..].copy_from_slice(x_only_pubkey);
467    let pk_point = parse_point(&pk_sec1)?;
468
469    // Challenge: e = H("BIP0340/challenge", R_x || P_x || msg)
470    let mut challenge_data = Vec::new();
471    challenge_data.extend_from_slice(&sig.r);
472    challenge_data.extend_from_slice(x_only_pubkey);
473    challenge_data.extend_from_slice(msg);
474    let e = tagged_hash_scalar(b"BIP0340/challenge", &challenge_data);
475
476    // Verify: s * G == R + e * P
477    let lhs = ProjectivePoint::GENERATOR * s_scalar;
478    let rhs = r_point + pk_point * e;
479
480    Ok(lhs == rhs)
481}
482
483// ─── Helpers ────────────────────────────────────────────────────────
484
485/// Parse a 33-byte SEC1 compressed point.
486fn parse_point(bytes: &[u8; 33]) -> Result<ProjectivePoint, SignerError> {
487    // Validate prefix byte first (0x02 = even y, 0x03 = odd y)
488    if bytes[0] != 0x02 && bytes[0] != 0x03 {
489        return Err(SignerError::InvalidPrivateKey(
490            "invalid compressed point prefix".into(),
491        ));
492    }
493    let ct = AffinePoint::from_bytes(bytes.into());
494    if !bool::from(ct.is_some()) {
495        return Err(SignerError::InvalidPrivateKey(
496            "invalid compressed point".into(),
497        ));
498    }
499    // Safe: is_some() verified above. CtOption::unwrap() is constant-time.
500    #[allow(clippy::unwrap_used)]
501    Ok(ProjectivePoint::from(ct.unwrap()))
502}
503
504#[cfg(test)]
505#[allow(clippy::unwrap_used, clippy::expect_used)]
506mod tests {
507    use super::*;
508
509    // ─── Individual Pubkey ──────────────────────────────────────
510
511    #[test]
512    fn test_individual_pubkey_from_known_key() {
513        // Secret key = 1 → generator point G (compressed)
514        let sk = {
515            let mut k = [0u8; 32];
516            k[31] = 1;
517            k
518        };
519        let pk = individual_pubkey(&sk).unwrap();
520        // Generator point compressed: 02 79BE667E...
521        assert_eq!(pk[0], 0x02);
522        assert_eq!(
523            hex::encode(&pk[1..]),
524            "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
525        );
526    }
527
528    #[test]
529    fn test_individual_pubkey_zero_key_rejected() {
530        let sk = [0u8; 32];
531        assert!(individual_pubkey(&sk).is_err());
532    }
533
534    #[test]
535    fn test_individual_pubkey_deterministic() {
536        let sk = [0x42u8; 32];
537        let pk1 = individual_pubkey(&sk).unwrap();
538        let pk2 = individual_pubkey(&sk).unwrap();
539        assert_eq!(pk1, pk2);
540    }
541
542    // ─── Key Aggregation (BIP-327 KeyAgg) ───────────────────────
543
544    #[test]
545    fn test_key_agg_two_keys() {
546        let sk1 = [0x01u8; 32];
547        let sk2 = [0x02u8; 32];
548        let pk1 = individual_pubkey(&sk1).unwrap();
549        let pk2 = individual_pubkey(&sk2).unwrap();
550
551        let ctx = key_agg(&[pk1, pk2]).unwrap();
552        assert_eq!(ctx.x_only_pubkey.len(), 32);
553        assert_eq!(ctx.pubkeys.len(), 2);
554        assert_eq!(ctx.coefficients.len(), 2);
555    }
556
557    #[test]
558    fn test_key_agg_deterministic() {
559        let sk1 = [0x01u8; 32];
560        let sk2 = [0x02u8; 32];
561        let pk1 = individual_pubkey(&sk1).unwrap();
562        let pk2 = individual_pubkey(&sk2).unwrap();
563
564        let ctx1 = key_agg(&[pk1, pk2]).unwrap();
565        let ctx2 = key_agg(&[pk1, pk2]).unwrap();
566        assert_eq!(ctx1.x_only_pubkey, ctx2.x_only_pubkey);
567    }
568
569    #[test]
570    fn test_key_agg_empty_rejected() {
571        assert!(key_agg(&[]).is_err());
572    }
573
574    #[test]
575    fn test_key_agg_order_matters() {
576        let sk1 = [0x01u8; 32];
577        let sk2 = [0x02u8; 32];
578        let pk1 = individual_pubkey(&sk1).unwrap();
579        let pk2 = individual_pubkey(&sk2).unwrap();
580
581        let ctx_12 = key_agg(&[pk1, pk2]).unwrap();
582        let ctx_21 = key_agg(&[pk2, pk1]).unwrap();
583        // Different order → different aggregate key (unless sorted first)
584        // This is expected BIP-327 behavior
585        assert_ne!(ctx_12.x_only_pubkey, ctx_21.x_only_pubkey);
586    }
587
588    #[test]
589    fn test_key_sort() {
590        let sk1 = [0x01u8; 32];
591        let sk2 = [0x02u8; 32];
592        let pk1 = individual_pubkey(&sk1).unwrap();
593        let pk2 = individual_pubkey(&sk2).unwrap();
594
595        let sorted = key_sort(&[pk2, pk1]);
596        let sorted2 = key_sort(&[pk1, pk2]);
597        assert_eq!(sorted, sorted2); // same order regardless of input
598    }
599
600    // ─── Full 2-of-2 Signing Round-Trip ─────────────────────────
601
602    #[test]
603    fn test_musig2_full_roundtrip() {
604        let sk1 = [0x11u8; 32];
605        let sk2 = [0x22u8; 32];
606        let pk1 = individual_pubkey(&sk1).unwrap();
607        let pk2 = individual_pubkey(&sk2).unwrap();
608
609        // Key aggregation
610        let key_agg_ctx = key_agg(&[pk1, pk2]).unwrap();
611
612        // Nonce generation
613        let msg = b"musig2 test message";
614        let (sec1, pub1) = nonce_gen(&sk1, &pk1, &key_agg_ctx, msg, &[]).unwrap();
615        let (sec2, pub2) = nonce_gen(&sk2, &pk2, &key_agg_ctx, msg, &[]).unwrap();
616
617        // Nonce aggregation
618        let agg_nonce = nonce_agg(&[pub1, pub2]).unwrap();
619
620        // Partial signing
621        let psig1 = sign(sec1, &sk1, &key_agg_ctx, &agg_nonce, msg).unwrap();
622        let psig2 = sign(sec2, &sk2, &key_agg_ctx, &agg_nonce, msg).unwrap();
623
624        // Aggregate
625        let sig = partial_sig_agg(&[psig1, psig2], &agg_nonce, &key_agg_ctx, msg).unwrap();
626        assert_eq!(sig.to_bytes().len(), 64);
627
628        // BIP-340 verification
629        let valid = verify(&sig, &key_agg_ctx.x_only_pubkey, msg).unwrap();
630        assert!(valid, "MuSig2 signature must verify");
631    }
632
633    #[test]
634    fn test_musig2_different_messages_different_sigs() {
635        let sk1 = [0x11u8; 32];
636        let sk2 = [0x22u8; 32];
637        let pk1 = individual_pubkey(&sk1).unwrap();
638        let pk2 = individual_pubkey(&sk2).unwrap();
639        let ctx = key_agg(&[pk1, pk2]).unwrap();
640
641        let msg1 = b"message one";
642        let msg2 = b"message two";
643
644        let (s1a, p1a) = nonce_gen(&sk1, &pk1, &ctx, msg1, &[]).unwrap();
645        let (s2a, p2a) = nonce_gen(&sk2, &pk2, &ctx, msg1, &[]).unwrap();
646        let an_a = nonce_agg(&[p1a, p2a]).unwrap();
647        let ps1a = sign(s1a, &sk1, &ctx, &an_a, msg1).unwrap();
648        let ps2a = sign(s2a, &sk2, &ctx, &an_a, msg1).unwrap();
649        let sig1 = partial_sig_agg(&[ps1a, ps2a], &an_a, &ctx, msg1).unwrap();
650
651        let (s1b, p1b) = nonce_gen(&sk1, &pk1, &ctx, msg2, &[]).unwrap();
652        let (s2b, p2b) = nonce_gen(&sk2, &pk2, &ctx, msg2, &[]).unwrap();
653        let an_b = nonce_agg(&[p1b, p2b]).unwrap();
654        let ps1b = sign(s1b, &sk1, &ctx, &an_b, msg2).unwrap();
655        let ps2b = sign(s2b, &sk2, &ctx, &an_b, msg2).unwrap();
656        let sig2 = partial_sig_agg(&[ps1b, ps2b], &an_b, &ctx, msg2).unwrap();
657
658        // Different messages → different signatures
659        assert_ne!(sig1.to_bytes(), sig2.to_bytes());
660    }
661
662    #[test]
663    fn test_musig2_wrong_message_fails_verification() {
664        let sk1 = [0x11u8; 32];
665        let sk2 = [0x22u8; 32];
666        let pk1 = individual_pubkey(&sk1).unwrap();
667        let pk2 = individual_pubkey(&sk2).unwrap();
668        let ctx = key_agg(&[pk1, pk2]).unwrap();
669
670        let msg = b"correct message";
671        let (s1, p1) = nonce_gen(&sk1, &pk1, &ctx, msg, &[]).unwrap();
672        let (s2, p2) = nonce_gen(&sk2, &pk2, &ctx, msg, &[]).unwrap();
673        let an = nonce_agg(&[p1, p2]).unwrap();
674        let ps1 = sign(s1, &sk1, &ctx, &an, msg).unwrap();
675        let ps2 = sign(s2, &sk2, &ctx, &an, msg).unwrap();
676        let sig = partial_sig_agg(&[ps1, ps2], &an, &ctx, msg).unwrap();
677
678        // Verify with wrong message
679        let valid = verify(&sig, &ctx.x_only_pubkey, b"wrong message").unwrap();
680        assert!(!valid, "signature must not verify for wrong message");
681    }
682
683    #[test]
684    fn test_nonce_agg_empty_rejected() {
685        assert!(nonce_agg(&[]).is_err());
686    }
687
688    #[test]
689    fn test_partial_sig_agg_empty_rejected() {
690        let sk1 = [0x11u8; 32];
691        let sk2 = [0x22u8; 32];
692        let pk1 = individual_pubkey(&sk1).unwrap();
693        let pk2 = individual_pubkey(&sk2).unwrap();
694        let ctx = key_agg(&[pk1, pk2]).unwrap();
695        let (_, p1) = nonce_gen(&sk1, &pk1, &ctx, b"x", &[]).unwrap();
696        let (_, p2) = nonce_gen(&sk2, &pk2, &ctx, b"x", &[]).unwrap();
697        let an = nonce_agg(&[p1, p2]).unwrap();
698        assert!(partial_sig_agg(&[], &an, &ctx, b"x").is_err());
699    }
700}