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