import { schnorr, secp256k1 } from '@noble/curves/secp256k1.js'; import { bytesToNumberBE, concatBytes, hexToBytes, numberToBytesBE, randomBytes, } from '@noble/curves/utils.js'; import { describe, should } from '@paulmillr/jsbt/test.js'; import { deepStrictEqual, throws } from 'node:assert'; import * as musig2 from '../src/musig2.ts'; import { default as detSignVectors } from './vectors/bip327/det_sign_vectors.json' with { type: 'json' }; import { default as keyAggVectors } from './vectors/bip327/key_agg_vectors.json' with { type: 'json' }; import { default as keySortVectors } from './vectors/bip327/key_sort_vectors.json' with { type: 'json' }; import { default as nonceAggVectors } from './vectors/bip327/nonce_agg_vectors.json' with { type: 'json' }; import { default as nonceGenVectors } from './vectors/bip327/nonce_gen_vectors.json' with { type: 'json' }; import { default as sigAggVectors } from './vectors/bip327/sig_agg_vectors.json' with { type: 'json' }; import { default as signVerifyVectors } from './vectors/bip327/sign_verify_vectors.json' with { type: 'json' }; import { default as tweakVectors } from './vectors/bip327/tweak_vectors.json' with { type: 'json' }; const Point = secp256k1.Point; const assertError = (error, cb) => { try { cb(); } catch (e) { if (e instanceof ReferenceError) throw e; if (error.type === 'invalid_contribution' && error.signer !== null) deepStrictEqual(e, new musig2.InvalidContributionErr(error.signer, error.contrib)); return; } throw new Error('missing error'); }; describe('BIP327', () => { should('Example', () => { // MuSig2 Multi-signature for Alice, Bob, and Carol // 1. Key Generation (for each signer: Alice, Bob, Carol) // - Alice's key generation const aliceSecretKey = randomBytes(32); // Alice generates a random 32-byte secret key const alicePublicKey = musig2.IndividualPubkey(aliceSecretKey); // Alice derives her individual public key from her secret key // - Bob's key generation const bobSecretKey = randomBytes(32); // Bob generates a random 32-byte secret key const bobPublicKey = musig2.IndividualPubkey(bobSecretKey); // Bob derives his individual public key from his secret key // - Carol's key generation const carolSecretKey = randomBytes(32); // Carol generates a random 32-byte secret key const carolPublicKey = musig2.IndividualPubkey(carolSecretKey); // Carol derives her individual public key from her secret key // 2. Key Aggregation (All signers participate by sharing public keys) const individualPublicKeys = [alicePublicKey, bobPublicKey, carolPublicKey]; // Collect all individual public keys const sortedPublicKeys = musig2.sortKeys(individualPublicKeys); // Sort public keys lexicographically (as required by MuSig2) const aggregatePublicKey = musig2.keyAggExport(musig2.keyAggregate(sortedPublicKeys)); // Extract the X-only aggregate public key (32 bytes) // At this point, all signers have the 'aggregatePublicKey' and 'keyAggContext'. // 3. Nonce Generation - Round 1 (Each signer generates and broadcasts public nonce) const msg = new Uint8Array(32).fill(5); // Example message to be signed (32-byte message is recommended for BIP340) // Alice generates her nonce const aliceNonces = musig2.nonceGen(alicePublicKey, aliceSecretKey, aggregatePublicKey, msg); // Secret nonce: must be kept secret and used only once per signing session! // Public nonce: to be shared with Bob and Carol // Bob generates his nonce const bobNonces = musig2.nonceGen(bobPublicKey, bobSecretKey, aggregatePublicKey, msg); // Carol generates her nonce const carolNonces = musig2.nonceGen(carolPublicKey, carolSecretKey, aggregatePublicKey, msg); // Each signer creates own instance const session = new musig2.Session( // 4. Nonce Aggregation (All signers participate by sharing public nonces) musig2.nonceAggregate([aliceNonces.public, bobNonces.public, carolNonces.public]), sortedPublicKeys, msg ); // At this point, all signers have the 'aggregateNonce'. // 5. Partial Signature Generation - Round 2 (Each signer generates partial signature) // Alice generates her partial signature const alicePartialSignature = session.sign(aliceNonces.secret, aliceSecretKey); // Bob generates his partial signature const bobPartialSignature = session.sign(bobNonces.secret, bobSecretKey); // Carol generates her partial signature const carolPartialSignature = session.sign(carolNonces.secret, carolSecretKey); // 6. Partial Signature Aggregation (Anyone can aggregate partial signatures) const partialSignatures = [alicePartialSignature, bobPartialSignature, carolPartialSignature]; // Collect all partial signatures const finalSignature = session.partialSigAgg(partialSignatures); // Aggregate partial signatures to create the final signature // 7. Signature Verification (Anyone can verify the final signature) // Verify the final signature deepStrictEqual(schnorr.verify(finalSignature, msg, aggregatePublicKey), true); }); should('Example (deterministic)', () => { // 1. Key Generation (for each signer: Alice, Bob, Carol) - Same as before // - Alice's key generation const aliceSecretKey = randomBytes(32); const alicePublicKey = musig2.IndividualPubkey(aliceSecretKey); // - Bob's key generation const bobSecretKey = randomBytes(32); const bobPublicKey = musig2.IndividualPubkey(bobSecretKey); // - Carol's key generation const carolSecretKey = randomBytes(32); const carolPublicKey = musig2.IndividualPubkey(carolSecretKey); // 2. Key Aggregation (All signers participate) - Same as before const individualPublicKeys = [alicePublicKey, bobPublicKey, carolPublicKey]; const sortedPublicKeys = musig2.sortKeys(individualPublicKeys); const keyAggContext = musig2.keyAggregate(sortedPublicKeys); const aggregatePublicKey = musig2.keyAggExport(keyAggContext); // 3. Nonce Generation - Round 1 (Alice and Bob generate normal nonces, Carol will use deterministic) const msg = new Uint8Array(32).fill(5); // Alice generates her nonce (normal NonceGen) const aliceNonces = musig2.nonceGen(alicePublicKey, aliceSecretKey, aggregatePublicKey, msg); // Bob generates his nonce (normal NonceGen) const bobNonces = musig2.nonceGen(bobPublicKey, bobSecretKey, aggregatePublicKey, msg); // Carol will generate her nonce deterministically in Round 2, after receiving Alice and Bob's nonces. // Carol *does not* run nonceGen in this round yet for deterministic signing. // 4. Nonce Aggregation (Alice and Bob's nonces are aggregated for Carol) const otherPublicNoncesForCarol = [aliceNonces.public, bobNonces.public]; // Alice and Bob's public nonces for Carol // Aggregate nonces of *other* signers for Carol const aggregateOtherNonceForCarol = musig2.nonceAggregate(otherPublicNoncesForCarol); // Now Carol has 'aggregateOtherNonceForCarol' which is the aggregate nonce of all *other* signers. // 5. Deterministic Signing & Nonce Generation - Round 2 (Carol performs deterministic sign, including nonce gen) // Carol uses deterministicSign, providing the aggregate nonce of others const { publicNonce: carolPubNonce, partialSig: carolPartialSignature } = musig2.deterministicSign( carolSecretKey, // Carol's secret key aggregateOtherNonceForCarol, // Aggregate nonce of *other* signers (Alice & Bob) sortedPublicKeys, // All sorted public keys msg, // Message to sign [], // Tweaks (none in this example) [], // isXonly (none in this example) undefined // Optional randomness (can be undefined for fully deterministic, or provide extra randomness) ); // At this point, Carol has generated both her public nonce and partial signature deterministically. // Now collect *all* public nonces including Carol's deterministic one const allPublicNonces = [aliceNonces.public, bobNonces.public, carolPubNonce]; // 7. Partial Signature Generation - Round 2 (Alice & Bob generate partial signatures as before) const session = new musig2.Session( // 6. Complete Nonce Aggregation (Now include Carol's nonce) musig2.nonceAggregate(allPublicNonces), sortedPublicKeys, msg ); // Alice generates her partial signature (using the complete session context) const alicePartialSignature = session.sign(aliceNonces.secret, aliceSecretKey); // Bob generates his partial signature (using the complete session context) const bobPartialSignature = session.sign(bobNonces.secret, bobSecretKey); // Carol's partial signature is already generated in step 5 ('carolPartialSignature') // 8. Partial Signature Aggregation (Anyone can aggregate partial signatures) const partialSignatures = [alicePartialSignature, bobPartialSignature, carolPartialSignature]; // Collect all partial signatures const finalSignature = session.partialSigAgg(partialSignatures); // 9. Signature Verification (Anyone can verify the final signature) deepStrictEqual(schnorr.verify(finalSignature, msg, aggregatePublicKey), true); }); should('key sorting', () => { const t = keySortVectors; deepStrictEqual(musig2.sortKeys(t.pubkeys.map(hexToBytes)), t.sorted_pubkeys.map(hexToBytes)); }); should('validator constructors', () => { const secretKey = schnorr.utils.randomSecretKey(); const publicKey = musig2.IndividualPubkey(secretKey); throws(() => musig2.sortKeys('x' as any), TypeError); throws(() => musig2.keyAggregate([publicKey], [], [true]), RangeError); throws(() => musig2.nonceGen(publicKey, secretKey, new Uint8Array(31)), RangeError); throws( () => musig2.deterministicSign( secretKey, musig2.nonceGen(publicKey, secretKey).public, [publicKey], new Uint8Array([1, 2, 3]), [], [], new Uint8Array(31) ), RangeError ); }); should('sortKeys rejects empty key lists', () => { throws(() => musig2.sortKeys([]), RangeError); }); should('sortKeys returns a sorted copy without mutating caller input', () => { const a = new Uint8Array(33); a.fill(2); const b = new Uint8Array(33); b.fill(1); const input = [a, b]; const snapshot = input.map((i) => Uint8Array.from(i)); const out = musig2.sortKeys(input); deepStrictEqual(out, [snapshot[1], snapshot[0]]); deepStrictEqual(out === input, false); deepStrictEqual(input, snapshot); }); should('keyAggregate rejects empty key lists', () => { throws(() => musig2.keyAggregate([]), RangeError); }); should('nonceAggregate rejects empty nonce lists', () => { throws(() => musig2.nonceAggregate([]), RangeError); }); should('partialSigAgg rejects empty partial-signature lists', () => { const secretKey = schnorr.utils.randomSecretKey(); const publicKey = musig2.IndividualPubkey(secretKey); const msg = new Uint8Array([1, 2, 3]); const aggPublicKey = musig2.keyAggExport(musig2.keyAggregate([publicKey])); const nonce = musig2.nonceGen(publicKey, secretKey, aggPublicKey, msg); const session = new musig2.Session(musig2.nonceAggregate([nonce.public]), [publicKey], msg); throws(() => session.partialSigAgg([]), RangeError); }); should('keyAggregate rejects non-boolean isXonly entries', () => { const secretKey = schnorr.utils.randomSecretKey(); const publicKey = musig2.IndividualPubkey(secretKey); const tweak = new Uint8Array(32); tweak[31] = 1; throws(() => musig2.keyAggregate([publicKey], [tweak], [1 as any]), TypeError); }); should('deterministic sign accepts a zero plain tweak as a no-op', () => { const secretKey = schnorr.utils.randomSecretKey(); const publicKey = musig2.IndividualPubkey(secretKey); const aggOtherNonce = musig2.nonceGen(publicKey, secretKey).public; const msg = new Uint8Array([1, 2, 3]); deepStrictEqual( musig2.deterministicSign( secretKey, aggOtherNonce, [publicKey], msg, [new Uint8Array(32)], [false] ), musig2.deterministicSign(secretKey, aggOtherNonce, [publicKey], msg) ); }); should('partialSigVerify treats a zero partial signature as invalid instead of throwing', () => { const secretKey = schnorr.utils.randomSecretKey(); const publicKey = musig2.IndividualPubkey(secretKey); const msg = new Uint8Array([1, 2, 3]); const keyAgg = musig2.keyAggregate([publicKey]); const aggPublicKey = musig2.keyAggExport(keyAgg); const nonce = musig2.nonceGen(publicKey, secretKey, aggPublicKey, msg); const session = new musig2.Session(musig2.nonceAggregate([nonce.public]), [publicKey], msg); deepStrictEqual(session.partialSigVerify(new Uint8Array(32), [nonce.public], 0), false); }); should('partialSigVerify depends on the full signer nonce list, not just pubNonces[i]', () => { const empty = new Uint8Array(0); const msg = new Uint8Array([1, 2, 3]); const sk1 = new Uint8Array(32); const sk2 = new Uint8Array(32); const rand1 = new Uint8Array(32); const rand2 = new Uint8Array(32); const rand3 = new Uint8Array(32); sk1[31] = 1; sk2[31] = 2; rand1[31] = 1; rand2[31] = 2; rand3[31] = 3; const pk1 = musig2.IndividualPubkey(sk1); const pk2 = musig2.IndividualPubkey(sk2); const n1 = musig2.nonceGen(pk1, sk1, empty, msg, empty, rand1); const n2 = musig2.nonceGen(pk2, sk2, empty, msg, empty, rand2); const n2alt = musig2.nonceGen(pk2, sk2, empty, msg, empty, rand3); const pubNonces = [n1.public, n2.public]; const mismatched = [n1.public, n2alt.public]; const session = new musig2.Session(musig2.nonceAggregate(pubNonces), [pk1, pk2], msg); const recomputed = new musig2.Session(musig2.nonceAggregate(mismatched), [pk1, pk2], msg); const psig1 = session.sign(new Uint8Array(n1.secret), sk1, true); deepStrictEqual(session.partialSigVerify(psig1, pubNonces, 0), true); deepStrictEqual(recomputed.partialSigVerify(psig1, mismatched, 0), false); deepStrictEqual(session.partialSigVerify(psig1, mismatched, 0), false); }); should('Session signing is not affected by later publicKeys mutation', () => { const sk = new Uint8Array(32); sk[31] = 1; const pk = musig2.IndividualPubkey(sk); const rand = new Uint8Array(32); rand[31] = 9; const empty = new Uint8Array(0); const msg = new Uint8Array([1, 2, 3]); const nonce = musig2.nonceGen(pk, sk, empty, msg, empty, rand); const aggNonce = musig2.nonceAggregate([nonce.public]); const expected = new musig2.Session(aggNonce, [pk], msg).sign( musig2.nonceGen(pk, sk, empty, msg, empty, rand).secret, sk ); const publicKeys = [pk]; const session = new musig2.Session(aggNonce, publicKeys, msg); publicKeys.length = 0; deepStrictEqual( session.sign(musig2.nonceGen(pk, sk, empty, msg, empty, rand).secret, sk), expected ); }); should('Session signing is not affected by later Buffer-backed publicKey mutation', () => { const sk = new Uint8Array(32); sk[31] = 1; const pk = musig2.IndividualPubkey(sk); const rand = new Uint8Array(32); rand[31] = 9; const empty = new Uint8Array(0); const msg = new Uint8Array([1, 2, 3]); const nonce = musig2.nonceGen(pk, sk, empty, msg, empty, rand); const aggNonce = musig2.nonceAggregate([nonce.public]); const expected = new musig2.Session(aggNonce, [pk], msg).sign( musig2.nonceGen(pk, sk, empty, msg, empty, rand).secret, sk ); const pkBuf = Buffer.from(pk); const session = new musig2.Session(aggNonce, [pkBuf], msg); pkBuf[1] ^= 1; deepStrictEqual( session.sign(musig2.nonceGen(pk, sk, empty, msg, empty, rand).secret, sk), expected ); }); should( 'Session partialSigVerify is not affected by later Buffer-backed aggNonce mutation', () => { const sk = new Uint8Array(32); sk[31] = 1; const pk = musig2.IndividualPubkey(sk); const rand = new Uint8Array(32); rand[31] = 9; const empty = new Uint8Array(0); const msg = new Uint8Array([1, 2, 3]); const nonce = musig2.nonceGen(pk, sk, empty, msg, empty, rand); const aggNonce = musig2.nonceAggregate([nonce.public]); const partialSig = new musig2.Session(aggNonce, [pk], msg).sign( musig2.nonceGen(pk, sk, empty, msg, empty, rand).secret, sk ); const aggNonceBuf = Buffer.from(aggNonce); const session = new musig2.Session(aggNonceBuf, [pk], msg); aggNonceBuf[0] ^= 1; deepStrictEqual(session.partialSigVerify(partialSig, [nonce.public], 0), true); } ); should('key aggregation', () => { const pubkeys = keyAggVectors.pubkeys.map(hexToBytes); for (const t of keyAggVectors.valid_test_cases) { const pub = t.key_indices.map((i) => pubkeys[i]); deepStrictEqual(musig2.keyAggExport(musig2.keyAggregate(pub)), hexToBytes(t.expected)); } const tweaks = keyAggVectors.tweaks.map(hexToBytes); for (const t of keyAggVectors.error_test_cases) { assertError(t.error, () => { musig2.keyAggregate( t.key_indices.map((i) => pubkeys[i]), t.tweak_indices.map((i) => tweaks[i]), t.is_xonly ); }); } }); should('nonce geneneration', () => { for (const t of nonceGenVectors.test_cases) { const rand = hexToBytes(t.rand_); const pk = hexToBytes(t.pk); const sk = t.sk !== null ? hexToBytes(t.sk) : undefined; const aggpk = t.aggpk !== null ? hexToBytes(t.aggpk) : undefined; const msg = t.msg !== null ? hexToBytes(t.msg) : undefined; const extraIn = t.extra_in !== null ? hexToBytes(t.extra_in) : undefined; deepStrictEqual(musig2.nonceGen(pk, sk, aggpk, msg, extraIn, rand), { secret: hexToBytes(t.expected_secnonce), public: hexToBytes(t.expected_pubnonce), }); } }); should('nonce aggregate', () => { for (const t of nonceAggVectors.valid_test_cases) { const pubnonces = t.pnonce_indices.map((i) => hexToBytes(nonceAggVectors.pnonces[i])); deepStrictEqual(musig2.nonceAggregate(pubnonces), hexToBytes(t.expected)); } for (const t of nonceAggVectors.error_test_cases) { const pubnonces = t.pnonce_indices.map((i) => hexToBytes(nonceAggVectors.pnonces[i])); assertError(t.error, () => musig2.nonceAggregate(pubnonces)); } }); should('sign & verify', () => { const sk = hexToBytes(signVerifyVectors.sk); const X = signVerifyVectors.pubkeys.map(hexToBytes); deepStrictEqual(X[0], musig2.IndividualPubkey(sk)); const secnonces = signVerifyVectors.secnonces.map(hexToBytes); const pnonce = signVerifyVectors.pnonces.map(hexToBytes); // Public nonce correct for given secret nonce const k1 = bytesToNumberBE(secnonces[0].slice(0, 32)); const k2 = bytesToNumberBE(secnonces[0].slice(32, 64)); const R_s1 = Point.BASE.multiply(k1); const R_s2 = Point.BASE.multiply(k2); deepStrictEqual(pnonce[0], concatBytes(R_s1.toBytes(true), R_s2.toBytes(true))); for (const t of signVerifyVectors.valid_test_cases) { const pubkeys = t.key_indices.map((i) => X[i]); const pubnonces = t.nonce_indices.map((i) => hexToBytes(signVerifyVectors.pnonces[i])); const aggnonce = hexToBytes(signVerifyVectors.aggnonces[t.aggnonce_index]); deepStrictEqual(musig2.nonceAggregate(pubnonces), aggnonce); // aggnonce consistency const msg = hexToBytes(signVerifyVectors.msgs[t.msg_index]); const expected = hexToBytes(t.expected); const session = new musig2.Session(musig2.nonceAggregate(pubnonces), pubkeys, msg); const secnonceCopy = new Uint8Array(secnonces[0]); deepStrictEqual(session.sign(secnonceCopy, sk), expected); if (!session.partialSigVerify(expected, pubnonces, t.signer_index)) { throw new Error('partialSigVerify failed in valid test case'); } } for (const t of signVerifyVectors.sign_error_test_cases) { const publicKeys = t.key_indices.map((i) => X[i]); const aggNonce = hexToBytes(signVerifyVectors.aggnonces[t.aggnonce_index]); const msg = hexToBytes(signVerifyVectors.msgs[t.msg_index]); const secnonce = new Uint8Array(secnonces[t.secnonce_index]); assertError(t.error, () => { // TODO: uses already aggregated nonce here const session = new musig2.Session(aggNonce, publicKeys, msg); session.sign(secnonce, sk); }); } for (const t of signVerifyVectors.verify_fail_test_cases) { const sig = hexToBytes(t.sig); const pubkeys = t.key_indices.map((i) => X[i]); const pubnonces = t.nonce_indices.map((i) => pnonce[i]); const msg = hexToBytes(signVerifyVectors.msgs[t.msg_index]); const session = new musig2.Session(musig2.nonceAggregate(pubnonces), pubkeys, msg); if (session.partialSigVerify(sig, pubnonces, t.signer_index)) { throw new Error('partialSigVerify unexpectedly succeeded on a failing test case'); } } for (const t of signVerifyVectors.verify_error_test_cases) { const sig = hexToBytes(t.sig); const pubkeys = t.key_indices.map((i) => X[i]); const pubnonces = t.nonce_indices.map((i) => pnonce[i]); const msg = hexToBytes(signVerifyVectors.msgs[t.msg_index]); assertError(t.error, () => { const session = new musig2.Session(musig2.nonceAggregate(pubnonces), pubkeys, msg); return session.partialSigVerify(sig, pubnonces, t.signer_index); }); } }); should('tweak', () => { const sk = hexToBytes(tweakVectors.sk); const X = tweakVectors.pubkeys.map(hexToBytes); deepStrictEqual(X[0], musig2.IndividualPubkey(sk)); const secnonce = hexToBytes(tweakVectors.secnonce); const pnonce = tweakVectors.pnonces.map(hexToBytes); const aggnonceVec = musig2.nonceAggregate(pnonce.slice(0, 3)); deepStrictEqual(aggnonceVec, hexToBytes(tweakVectors.aggnonce)); const tweaks = tweakVectors.tweaks.map(hexToBytes); const msg = hexToBytes(tweakVectors.msg); for (const t of tweakVectors.valid_test_cases) { const pubkeys = t.key_indices.map((i) => X[i]); const pubnonces = t.nonce_indices.map((i) => pnonce[i]); const tweaksCase = t.tweak_indices.map((i) => tweaks[i]); const isXonly = t.is_xonly; const signerIndex = t.signer_index; const expected = hexToBytes(t.expected); const session = new musig2.Session( musig2.nonceAggregate(pnonce), pubkeys, msg, tweaksCase, isXonly ); const secnonceCopy = new Uint8Array(secnonce); deepStrictEqual(session.sign(secnonceCopy, sk), expected); if (!session.partialSigVerify(expected, pubnonces, signerIndex)) { throw new Error('partialSigVerify failed for tweak valid test case'); } } // Error test cases. for (const t of tweakVectors.error_test_cases) { assertError(t.error, () => { const session = new musig2.Session( hexToBytes(tweakVectors.aggnonce), t.key_indices.map((i) => X[i]), msg, t.tweak_indices.map((i) => tweaks[i]), t.is_xonly ); return session.sign(new Uint8Array(secnonce), sk); }); } }); should('deterministic sign', () => { const sk = hexToBytes(detSignVectors.sk); const X = detSignVectors.pubkeys.map(hexToBytes); deepStrictEqual(X[0], musig2.IndividualPubkey(sk)); const msgs = detSignVectors.msgs.map(hexToBytes); for (const t of detSignVectors.valid_test_cases) { const pubkeys = t.key_indices.map((i) => X[i]); const aggothernonce = hexToBytes(t.aggothernonce); const tweaks = t.tweaks.map(hexToBytes); const isXonly = t.is_xonly; const msg = msgs[t.msg_index]; const signerIndex = t.signer_index; const rand = t.rand !== null ? hexToBytes(t.rand) : undefined; const expected = t.expected.map(hexToBytes); const { publicNonce: pubnonce, partialSig: psig } = musig2.deterministicSign( sk, aggothernonce, pubkeys, msg, tweaks, isXonly, rand ); deepStrictEqual(pubnonce, expected[0]); deepStrictEqual(psig, expected[1]); const pubnonces = [pubnonce, aggothernonce]; const session = new musig2.Session( musig2.nonceAggregate(pubnonces), pubkeys, msg, tweaks, isXonly ); if (!session.partialSigVerifyInternal(psig, pubnonce, pubkeys[signerIndex])) throw new Error('partialSigVerify failed for deterministic signing'); } for (const t of detSignVectors.error_test_cases) { const pubkeys = t.key_indices.map((i) => X[i]); const aggothernonce = hexToBytes(t.aggothernonce); const tweaks = t.tweaks.map(hexToBytes); const isXonly = t.is_xonly; const msg = msgs[t.msg_index]; const rand = t.rand !== null ? hexToBytes(t.rand) : undefined; assertError(t.error, () => { musig2.deterministicSign(sk, aggothernonce, pubkeys, msg, tweaks, isXonly, rand); }); } }); should('signature aggregation', () => { const msg = hexToBytes(sigAggVectors.msg); for (const t of sigAggVectors.valid_test_cases) { const pubnonces = t.nonce_indices.map((i) => hexToBytes(sigAggVectors.pnonces[i])); const aggnonce = hexToBytes(t.aggnonce); deepStrictEqual(aggnonce, musig2.nonceAggregate(pubnonces)); const pubkeys = t.key_indices.map((i) => hexToBytes(sigAggVectors.pubkeys[i])); const tweaks = t.tweak_indices.map((i) => hexToBytes(sigAggVectors.tweaks[i])); const isXonly = t.is_xonly; const psigs = t.psig_indices.map((i) => hexToBytes(sigAggVectors.psigs[i])); const expected = hexToBytes(t.expected); const session = new musig2.Session( musig2.nonceAggregate(pubnonces), pubkeys, msg, tweaks, isXonly ); const aggSig = session.partialSigAgg(psigs); deepStrictEqual(aggSig, expected); const aggpk = musig2.keyAggExport(musig2.keyAggregate(pubkeys, tweaks, isXonly)); deepStrictEqual(schnorr.verify(aggSig, msg, aggpk), true); } for (const t of sigAggVectors.error_test_cases) { const pubnonces = t.nonce_indices.map((i) => hexToBytes(sigAggVectors.pnonces[i])); const aggnonce = musig2.nonceAggregate(pubnonces); deepStrictEqual(aggnonce, musig2.nonceAggregate(pubnonces)); const pubkeys = t.key_indices.map((i) => hexToBytes(sigAggVectors.pubkeys[i])); const tweaks = t.tweak_indices.map((i) => hexToBytes(sigAggVectors.tweaks[i])); const isXonly = t.is_xonly; const psigs = t.psig_indices.map((i) => hexToBytes(sigAggVectors.psigs[i])); const session = new musig2.Session( musig2.nonceAggregate(pubnonces), pubkeys, msg, tweaks, isXonly ); assertError(t.error, () => session.partialSigAgg(psigs)); } }); should('sign & verify (random)', () => { const rand = () => randomBytes(1)[0]; const rand_1_of_4 = () => rand() % 4 === 0; const rand_1_of_2 = () => rand() % 2 === 0; for (let i = 0; i < 6; i++) { const sk1 = randomBytes(32); const sk2 = randomBytes(32); const pk1 = musig2.IndividualPubkey(sk1); const pk2 = musig2.IndividualPubkey(sk2); const pubkeys = [pk1, pk2]; const msg = randomBytes(32); const v = rand_1_of_4(); // random xOnly const tweaks = []; const isXonly = []; for (let j = 0; j < v; j++) { tweaks.push(randomBytes(32)); isXonly.push(rand_1_of_2()); } const aggpk = musig2.keyAggExport(musig2.keyAggregate(pubkeys, tweaks, isXonly)); const extraIn = numberToBytesBE(i, 4); let { public: pubnonce1, secret: secnonce1 } = musig2.nonceGen(pk1, sk1, aggpk, msg, extraIn); let pubnonce2, secnonce2, psig2; if (i % 2 === 0) { ({ secret: secnonce2, public: pubnonce2 } = musig2.nonceGen( pk2, sk2, aggpk, msg, randomBytes(8) )); } else { // Use deterministicSign for signer 2. const aggothernonce = musig2.nonceAggregate([pubnonce1]); ({ publicNonce: pubnonce2, partialSig: psig2 } = musig2.deterministicSign( sk2, aggothernonce, pubkeys, msg, tweaks, isXonly )); } const pubnonces = [pubnonce1, pubnonce2]; const session = new musig2.Session( musig2.nonceAggregate(pubnonces), pubkeys, msg, tweaks, isXonly ); const psig1 = session.sign(secnonce1, sk1); if (!session.partialSigVerify(psig1, pubnonces, 0)) throw new Error('Random partial signature verification failed for signer 1'); throws(() => session.sign(secnonce1, sk1)); // Reusing the same secnonce should throw. if (i % 2 === 0 && secnonce2 && pubnonce2) { const psig2Computed = session.sign(secnonce2, sk2); if (!session.partialSigVerify(psig2Computed, pubnonces, 1)) throw new Error('Random partial signature verification failed for signer 2'); const fullSig = session.partialSigAgg([psig1, psig2Computed]); deepStrictEqual(schnorr.verify(fullSig, msg, aggpk), true); } } }); }); // Regression test. const privA = hexToBytes('02'.repeat(32)); const privB = hexToBytes('03'.repeat(32)); should('MuSig2 end-to-end 2-of-2 with tweaks', () => { // Independent check of the verification-path multiplyUnsafe refactor: run a full // signing round (random-nonce and deterministic) and verify the final signatures // with plain BIP340 schnorr.verify against the tweaked aggregate key. const [skA, skB] = [privA, privB]; const publicKeys = [musig2.IndividualPubkey(skA), musig2.IndividualPubkey(skB)]; const tweaks = [new Uint8Array(32).fill(7)]; const isXonly = [true]; const aggPk = musig2.keyAggExport(musig2.keyAggregate(publicKeys, tweaks, isXonly)); const msg = new Uint8Array(32).fill(0xab); const nA = musig2.nonceGen(publicKeys[0], skA, aggPk, msg, undefined, new Uint8Array(32).fill(1)); const nB = musig2.nonceGen(publicKeys[1], skB, aggPk, msg, undefined, new Uint8Array(32).fill(2)); const aggNonce = musig2.nonceAggregate([nA.public, nB.public]); const session = new musig2.Session(aggNonce, publicKeys, msg, tweaks, isXonly); const sigA = session.sign(Uint8Array.from(nA.secret), skA); // sign() zeroes its input copy const sigB = session.sign(Uint8Array.from(nB.secret), skB); deepStrictEqual(session.partialSigVerify(sigA, [nA.public, nB.public], 0), true); deepStrictEqual(session.partialSigVerify(sigB, [nA.public, nB.public], 1), true); // A nonce set that does not aggregate to the session's aggNonce must not verify // (pins the cached-session guard). const nC = musig2.nonceGen(publicKeys[1], skB, aggPk, msg, undefined, new Uint8Array(32).fill(3)); deepStrictEqual(session.partialSigVerify(sigA, [nA.public, nC.public], 0), false); const finalSig = session.partialSigAgg([sigA, sigB]); deepStrictEqual(schnorr.verify(finalSig, msg, aggPk), true); // Wrong-length aggregate nonce is rejected up front with a clear error. throws(() => new musig2.Session(aggNonce.slice(0, 65), publicKeys, msg)); // Deterministic flow: B derives nonce+psig from A's public nonce in one step. const nA2 = musig2.nonceGen( publicKeys[0], skA, aggPk, msg, undefined, new Uint8Array(32).fill(4) ); const det = musig2.deterministicSign(skB, nA2.public, publicKeys, msg, tweaks, isXonly); const session2 = new musig2.Session( musig2.nonceAggregate([nA2.public, det.publicNonce]), publicKeys, msg, tweaks, isXonly ); const sigA2 = session2.sign(Uint8Array.from(nA2.secret), skA); const finalSig2 = session2.partialSigAgg([sigA2, det.partialSig]); deepStrictEqual(schnorr.verify(finalSig2, msg, aggPk), true); }); should.runWhen(import.meta.url);