@scure/btc-signer 2.0.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/musig2.js CHANGED
@@ -2,12 +2,21 @@ import { schnorr, secp256k1 } from '@noble/curves/secp256k1.js';
2
2
  import { aInRange, concatBytes, equalBytes, numberToBytesBE } from '@noble/curves/utils.js';
3
3
  import { abytes, anumber, randomBytes } from '@noble/hashes/utils.js';
4
4
  import * as P from 'micro-packed';
5
- import { compareBytes, hasEven } from "./utils.js";
5
+ import { compareBytes, hasEven, validateObject } from "./utils.js";
6
6
  /**
7
7
  * Represents an error indicating an invalid contribution from a signer.
8
8
  * This allows pointing out which participant is malicious and what specifically is wrong.
9
+ * @param idx - signer index with the invalid contribution
10
+ * @param m - error message
11
+ * @example
12
+ * Create an error that points to the participant who sent invalid data.
13
+ * ```ts
14
+ * new InvalidContributionErr(0, 'pubkey');
15
+ * ```
9
16
  */
10
17
  export class InvalidContributionErr extends Error {
18
+ // BIP327 identifiable aborts blame exactly one signer by participant index in the
19
+ // caller's session ordering, so callers interpret idx using the same ordering they signed with.
11
20
  idx; // Indice of participant
12
21
  constructor(idx, m) {
13
22
  super(m);
@@ -15,65 +24,139 @@ export class InvalidContributionErr extends Error {
15
24
  }
16
25
  }
17
26
  // Utils
18
- const { taggedHash, pointToBytes } = schnorr.utils;
19
- const Point = secp256k1.Point;
20
- const Fn = Point.Fn;
21
- const PUBKEY_LEN = secp256k1.lengths.publicKey;
22
- const ZERO = new Uint8Array(PUBKEY_LEN); // Compressed zero point
27
+ // MuSig2 reuses BIP340 tagged hashing, i.e. SHA256(SHA256(tag) || SHA256(tag) || msg...),
28
+ // for all of the domain-separated hashes below (KeyAgg list, noncecoef, challenge, aux, ...).
29
+ const taggedHash = /* @__PURE__ */ (() => schnorr.utils.taggedHash)();
30
+ // BIP327 uses xbytes(P) = bytes(32, x(P)) for aggregate keys, nonce/challenge hashes,
31
+ // and final signatures, so this alias is intentionally x-only instead of 33-byte SEC1.
32
+ const pointToBytes = /* @__PURE__ */ (() => schnorr.utils.pointToBytes)();
33
+ // MuSig2 keeps aggregate keys/nonces as full secp256k1 points so it can represent infinity
34
+ // and inspect parity before exporting compressed or x-only encodings at the API boundaries.
35
+ const Point = /* @__PURE__ */ (() => secp256k1.Point)();
36
+ // MuSig2 scalars live in Z_n with fixed 32-byte encodings: strict inputs like secret keys
37
+ // use Fn.fromBytes(...), tweak bytes allow 0 per ApplyTweak, and hash-derived values are
38
+ // intentionally reduced mod n.
39
+ const Fn = /* @__PURE__ */ (() => Point.Fn)();
40
+ // BIP327 signer keys are plain compressed pubkeys (33 bytes); only aggregate exports switch
41
+ // to BIP340's 32-byte x-only format, so the internal input length stays on secp256k1.publicKey.
42
+ const PUBKEY_LEN = /* @__PURE__ */ (() => secp256k1.lengths.publicKey)();
43
+ // BIP327 uses bytes(33, 0) both as cbytes_ext(Point.ZERO) for infinity and as GetSecondKey's
44
+ // "no second distinct key" sentinel, so this all-zero compressed slot is intentionally out-of-band.
45
+ const ZERO = /* @__PURE__ */ new Uint8Array(PUBKEY_LEN); // Compressed zero point
46
+ // Be friendly to bad ECMAScript parsers by not using bigint literals.
47
+ // prettier-ignore
48
+ const _0n = /* @__PURE__ */ BigInt(0), _1n = /* @__PURE__ */ BigInt(1);
23
49
  // Encoding
24
50
  // TODO: re-use in PSBT?
25
- const compressed = P.apply(P.bytes(33), {
51
+ // This is BIP327's cbytes_ext/cpoint_ext adapter: normal points stay in compressed SEC1,
52
+ // while Point.ZERO maps to bytes(33, 0) as the out-of-band infinity sentinel for aggnonce.
53
+ const compressed = /* @__PURE__ */ (() => P.apply(P.bytes(33), {
26
54
  decode: (p) => (isZero(p) ? ZERO : p.toBytes(true)),
27
55
  encode: (b) => (equalBytes(b, ZERO) ? Point.ZERO : Point.fromBytes(b)),
28
- });
29
- const scalar = P.validate(P.U256BE, (n) => {
30
- aInRange('n', n, 1n, Fn.ORDER);
56
+ }))();
57
+ // This coder is only for stored secnonce limbs k1/k2, which BIP327 requires to be
58
+ // nonzero scalars in [1, n); tweak scalars use different validation because 0 is allowed there.
59
+ const scalar = /* @__PURE__ */ (() => P.validate(P.U256BE, (n) => {
60
+ aInRange('n', n, _1n, Fn.ORDER);
31
61
  return n;
32
- });
33
- const PubNonce = P.struct({ R1: compressed, R2: compressed });
34
- const SecretNonce = P.struct({ k1: scalar, k2: scalar, publicKey: P.bytes(PUBKEY_LEN) });
62
+ }))();
63
+ // Shared for both per-signer pubnonce bytes and aggregate aggnonce bytes. Because it accepts
64
+ // the BIP327 infinity sentinel, so individual pubnonce callers still need
65
+ // an explicit zero-point check.
66
+ const PubNonce = /* @__PURE__ */ (() => P.struct({ R1: compressed, R2: compressed }))();
67
+ // BIP327 stores secnonce as k1 || k2 || pk so Sign can reject reused or
68
+ // invalid nonce limbs and detect when the caller pairs the nonce with a
69
+ // different individual public key than NonceGen used.
70
+ const SecretNonce = /* @__PURE__ */ (() => P.struct({
71
+ k1: scalar,
72
+ k2: scalar,
73
+ publicKey: P.bytes(PUBKEY_LEN),
74
+ }))();
35
75
  function abytesOptional(b, ...lengths) {
76
+ // Optional-byte helper: exact-length checks only happen when callers pass them explicitly.
36
77
  if (b !== undefined)
37
78
  abytes(b, ...lengths);
38
79
  }
39
80
  function abytesArray(lst, ...lengths) {
81
+ // Element-shape helper only: callers still enforce list-size rules like BIP327's 0 < u < 2^32.
40
82
  if (!Array.isArray(lst))
41
- throw new Error('expected array');
83
+ throw new TypeError('expected array');
42
84
  lst.forEach((i) => abytes(i, ...lengths));
43
85
  }
44
86
  function aXonly(lst) {
87
+ // BIP327 tweak modes are strict booleans; callers should run this before
88
+ // branching on isXonly values because plain JS truthiness would accept invalid inputs.
45
89
  if (!Array.isArray(lst))
46
- throw new Error('expected array');
90
+ throw new TypeError('expected array');
47
91
  lst.forEach((i, j) => {
48
92
  if (typeof i !== 'boolean')
49
- throw new Error('expected boolean in xOnly array, got' + i + '(' + j + ')');
93
+ throw new TypeError('expected boolean in xOnly array, got' + i + '(' + j + ')');
50
94
  });
51
95
  }
96
+ // BIP327/BIP340 treat tagged-hash outputs as big-endian integers reduced mod n for
97
+ // coefficients, nonce scalars, and challenges; this helper is that int(hash_tag(...)) mod n step.
52
98
  const taggedInt = (tag, ...messages) => Fn.create(Fn.fromBytes(taggedHash(tag, ...messages), true));
99
+ // BIP327 repeatedly says "use x if the point has even Y, otherwise use n - x"; this
100
+ // helper is that parity-conditioned scalar negation for nonce limbs and Q-dependent signs.
53
101
  const evenScalar = (p, n) => (hasEven(p.y) ? n : Fn.neg(n));
54
102
  // Short utility for compat with reference implementation
103
+ /**
104
+ * Derives a compressed secp256k1 public key from a private key.
105
+ * @param seckey - signer private key
106
+ * @returns Compressed public key bytes.
107
+ * @example
108
+ * Turn a signer's secret key into the compressed key format MuSig2 expects.
109
+ * ```ts
110
+ * import { schnorr } from '@noble/curves/secp256k1.js';
111
+ * import { IndividualPubkey } from '@scure/btc-signer/musig2.js';
112
+ * IndividualPubkey(schnorr.utils.randomSecretKey());
113
+ * ```
114
+ */
115
+ // BIP327 IndividualPubkey is cbytes(d*G): the 33-byte compressed signer key.
116
+ // Aggregate exports use separate x-only encoding and should not call this helper.
55
117
  export function IndividualPubkey(seckey) {
56
118
  return secp256k1.getPublicKey(seckey, true);
57
119
  }
58
120
  // Same, but returns Point
121
+ // Base-point multiply helper for the BIP327 x*G steps below. Point.BASE.multiply rejects 0,
122
+ // so call sites that allow zero scalars need to handle that case explicitly instead of using this.
59
123
  function mulBase(n) {
60
124
  return Point.BASE.multiply(n);
61
125
  }
126
+ // Local alias for BIP327's is_infinite(P): used both for rejecting infinity where cpoint(...)
127
+ // must never yield it and for the "if R' is infinite, use G" nonce-aggregation fallback.
62
128
  function isZero(point) {
63
129
  return point.equals(Point.ZERO);
64
130
  }
65
131
  /**
66
132
  * Lexicographically sorts an array of public keys.
67
- * @param publicKeys An array of public keys (Uint8Array).
133
+ * @param publicKeys - array of public keys
68
134
  * @returns A new array containing the sorted public keys.
69
- * @throws {Error} If the input is not an array or if any element is not a Uint8Array of the correct length.
135
+ * @throws On wrong argument types. {@link TypeError}
136
+ * @throws On wrong argument ranges or values. {@link RangeError}
137
+ * @example
138
+ * Sort participant public keys before building the aggregate MuSig2 key.
139
+ * ```ts
140
+ * import { IndividualPubkey, sortKeys } from '@scure/btc-signer/musig2.js';
141
+ * import { randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
142
+ * sortKeys([
143
+ * IndividualPubkey(randomPrivateKeyBytes()),
144
+ * IndividualPubkey(randomPrivateKeyBytes()),
145
+ * ]);
146
+ * ```
70
147
  */
71
148
  export function sortKeys(publicKeys) {
149
+ // BIP327 KeySort is defined for non-empty signer-key lists, and this helper returns a sorted copy
150
+ // so callers do not lose their original participant ordering as a side effect of key aggregation.
72
151
  abytesArray(publicKeys, PUBKEY_LEN);
73
- return publicKeys.sort(compareBytes);
152
+ if (!publicKeys.length)
153
+ throw new RangeError('sortKeys: expected non-empty signer key list');
154
+ return Array.from(publicKeys).sort(compareBytes);
74
155
  }
75
156
  // Finds second distinct key (to make coefficient 1)
76
157
  function getSecondKey(publicKeys) {
158
+ // BIP327 GetSecondKey returns bytes(33, 0) when all signer keys are equal; that sentinel
159
+ // means no key hits the special pk' = pk2 coefficient-1 shortcut in the all-equal case.
77
160
  abytesArray(publicKeys, PUBKEY_LEN);
78
161
  for (let j = 1; j < publicKeys.length; j++)
79
162
  if (!equalBytes(publicKeys[j], publicKeys[0]))
@@ -81,30 +164,54 @@ function getSecondKey(publicKeys) {
81
164
  return ZERO;
82
165
  }
83
166
  function keyAggL(publicKeys) {
167
+ // BIP327 HashKeys hashes keys in the caller-provided order. Run KeySort first when
168
+ // the surrounding protocol requires the canonical lexicographic participant ordering.
84
169
  abytesArray(publicKeys, PUBKEY_LEN);
85
170
  return taggedHash('KeyAgg list', ...publicKeys);
86
171
  }
87
172
  function keyAggCoeffInternal(publicKey1, publicKey2, L) {
173
+ // BIP327 only short-circuits to coefficient 1 for pk' = pk2. When all keys are equal,
174
+ // pk2 is the all-zero sentinel from GetSecondKey, so every real signer key still hashes.
88
175
  abytes(publicKey1, PUBKEY_LEN);
89
176
  abytes(publicKey2, PUBKEY_LEN);
90
177
  if (equalBytes(publicKey1, publicKey2))
91
- return 1n;
178
+ return _1n;
92
179
  return taggedInt('KeyAgg coefficient', L, publicKey1);
93
180
  }
94
181
  /**
95
182
  * Aggregates multiple public keys using the MuSig2 key aggregation algorithm.
96
- * @param publicKeys An array of individual public keys (Uint8Array).
97
- * @param tweaks An optional array of tweaks (Uint8Array) to apply to the aggregate public key.
98
- * @param isXonly An optional array of booleans indicating whether each tweak is an X-only tweak.
99
- * @returns An object containing the aggregate public key, accumulated sign, and accumulated tweak.
100
- * @throws {Error} If the input is invalid, such as non array publicKeys, tweaks and isXonly array length not matching.
101
- * @throws {InvalidContributionErr} If any of the public keys are invalid and cannot be processed.
183
+ * @param publicKeys - individual participant public keys
184
+ * @param tweaks - optional tweaks applied to the aggregate key
185
+ * @param isXonly - whether each tweak uses x-only semantics
186
+ * @returns An object containing the aggregate public key, accumulated sign,
187
+ * and accumulated tweak.
188
+ * @throws If the input is invalid, such as non-array public keys or mismatched
189
+ * tweak metadata. {@link Error}
190
+ * @throws On wrong argument types. {@link TypeError}
191
+ * @throws On wrong argument ranges or values. {@link RangeError}
192
+ * @throws If any of the public keys are invalid and cannot be processed.
193
+ * {@link InvalidContributionErr}
194
+ * @example
195
+ * Combine all participant public keys into the shared MuSig2 context.
196
+ * ```ts
197
+ * import { IndividualPubkey, keyAggregate } from '@scure/btc-signer/musig2.js';
198
+ * import { randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
199
+ * keyAggregate([
200
+ * IndividualPubkey(randomPrivateKeyBytes()),
201
+ * IndividualPubkey(randomPrivateKeyBytes()),
202
+ * ]);
203
+ * ```
102
204
  */
103
205
  export function keyAggregate(publicKeys, tweaks = [], isXonly = []) {
206
+ // BIP327 KeyAgg inputs require `0 < u < 2^32`, and ApplyTweak consumes a one-for-one
207
+ // list of boolean tweak modes; callers should enforce that public contract here.
104
208
  abytesArray(publicKeys, PUBKEY_LEN);
209
+ if (publicKeys.length < 1)
210
+ throw new RangeError('keyAggregate: expected at least 1 public key');
105
211
  abytesArray(tweaks, 32);
212
+ aXonly(isXonly);
106
213
  if (tweaks.length !== isXonly.length)
107
- throw new Error('The tweaks and isXonly arrays must have the same length');
214
+ throw new RangeError('The tweaks and isXonly arrays must have the same length');
108
215
  // Aggregate
109
216
  const pk2 = getSecondKey(publicKeys);
110
217
  const L = keyAggL(publicKeys);
@@ -119,13 +226,20 @@ export function keyAggregate(publicKeys, tweaks = [], isXonly = []) {
119
226
  }
120
227
  aggPublicKey = aggPublicKey.add(Pi.multiply(keyAggCoeffInternal(publicKeys[i], pk2, L)));
121
228
  }
229
+ // BIP327 KeyAggInternal: "Fail if is_infinite(Q)". Computationally unreachable for
230
+ // hash-derived coefficients, but the spec mandates the explicit check before tweaking.
231
+ if (isZero(aggPublicKey))
232
+ throw new Error('keyAggregate: aggregate public key cannot be infinity');
122
233
  let gAcc = Fn.ONE;
123
234
  let tweakAcc = Fn.ZERO;
124
235
  // Apply tweaks
125
236
  for (let i = 0; i < tweaks.length; i++) {
126
237
  const g = isXonly[i] && !hasEven(aggPublicKey.y) ? Fn.neg(Fn.ONE) : Fn.ONE;
127
- const t = Fn.fromBytes(tweaks[i]);
128
- aggPublicKey = aggPublicKey.multiply(g).add(mulBase(t));
238
+ // BIP327 ApplyTweak: `Let t = int(tweak); fail if t >= n`, so 32-byte zero tweaks are valid.
239
+ const t = Fn.fromBytes(tweaks[i], true);
240
+ if (!Fn.isValid(t))
241
+ throw new RangeError('invalid scalar: out of range');
242
+ aggPublicKey = aggPublicKey.multiply(g).add(Fn.is0(t) ? Point.ZERO : mulBase(t));
129
243
  if (isZero(aggPublicKey))
130
244
  throw new Error('The result of tweaking cannot be infinity');
131
245
  gAcc = Fn.mul(g, gAcc);
@@ -135,13 +249,31 @@ export function keyAggregate(publicKeys, tweaks = [], isXonly = []) {
135
249
  }
136
250
  /**
137
251
  * Exports the aggregate public key to a byte array.
138
- * @param ctx The result of the keyAggregate function.
252
+ * @param ctx - result of {@link keyAggregate}
139
253
  * @returns The aggregate public key as a byte array.
254
+ * @example
255
+ * Serialize the aggregate key after building the MuSig2 context.
256
+ * ```ts
257
+ * import { IndividualPubkey, keyAggregate, keyAggExport } from '@scure/btc-signer/musig2.js';
258
+ * import { randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
259
+ * const ctx = keyAggregate([
260
+ * IndividualPubkey(randomPrivateKeyBytes()),
261
+ * IndividualPubkey(randomPrivateKeyBytes()),
262
+ * ]);
263
+ * keyAggExport(ctx);
264
+ * ```
140
265
  */
141
266
  export function keyAggExport(ctx) {
267
+ validateObject(ctx, {}, {}, 'ctx');
268
+ if (!(ctx.aggPublicKey instanceof Point))
269
+ throw new TypeError('"ctx.aggPublicKey" expected point, got type=' + typeof ctx.aggPublicKey);
270
+ // BIP327 GetXonlyPubkey returns xbytes(Q), so this is the 32-byte x-only aggregate key
271
+ // instead of the 33-byte compressed SEC1 form.
142
272
  return pointToBytes(ctx.aggPublicKey);
143
273
  }
144
274
  function aux(secret, rand) {
275
+ // BIP327 NonceGen and DeterministicSign blind the caller-provided 32-byte randomness with
276
+ // hash_MuSig/aux(rand) before hashing the session inputs into nonce scalars.
145
277
  const rand2 = taggedHash('MuSig/aux', rand);
146
278
  if (secret.length !== rand2.length)
147
279
  throw new Error('Cannot XOR arrays of different lengths');
@@ -150,29 +282,44 @@ function aux(secret, rand) {
150
282
  res[i] = secret[i] ^ rand2[i];
151
283
  return res;
152
284
  }
153
- const nonceHash = (rand, publicKey, aggPublicKey, i, msgPrefixed, extraIn) => taggedInt('MuSig/nonce', rand, new Uint8Array([publicKey.length]), publicKey, new Uint8Array([aggPublicKey.length]), aggPublicKey, msgPrefixed, numberToBytesBE(extraIn.length, 4), extraIn, new Uint8Array([i]));
285
+ const nonceHash = (rand, publicKey, aggPublicKey, i, msgPrefixed, extraIn) =>
286
+ // BIP327 NonceGen hashes rand || len(pk) || pk || len(aggpk) || aggpk || m_prefixed ||
287
+ // len(extra_in) || extra_in || bytes(1, i - 1), so callers pass i = 0/1 here.
288
+ taggedInt('MuSig/nonce', rand, new Uint8Array([publicKey.length]), publicKey, new Uint8Array([aggPublicKey.length]), aggPublicKey, msgPrefixed, numberToBytesBE(extraIn.length, 4), extraIn, new Uint8Array([i]));
154
289
  /**
155
290
  * Generates a nonce pair (public and secret) for MuSig2 signing.
156
- * @param publicKey The individual public key of the signer (Uint8Array).
157
- * @param secretKey The secret key of the signer (Uint8Array). Optional, included to xor randomness
158
- * @param aggPublicKey The aggregate public key of all signers (Uint8Array).
159
- * @param msg The message to be signed (Uint8Array).
160
- * @param extraIn Extra input for nonce generation (Uint8Array).
161
- * @param rand Random 32-bytes for generating the nonces (Uint8Array).
291
+ * @param publicKey - individual public key of the signer
292
+ * @param secretKey - optional secret key, mixed in to blind the randomness source
293
+ * @param aggPublicKey - aggregate public key of all signers
294
+ * @param msg - message to be signed
295
+ * @param extraIn - extra input mixed into nonce generation
296
+ * @param rand - random 32-byte seed for the nonce derivation
162
297
  * @returns An object containing the public and secret nonces.
163
- * @throws {Error} If the input is invalid, such as non array publicKey, secretKey, aggPublicKey.
298
+ * @throws If the input is invalid, such as non-array public keys or malformed
299
+ * nonce inputs. {@link Error}
300
+ * @throws On wrong argument ranges or values. {@link RangeError}
301
+ * @example
302
+ * Generate the signer-local nonce pair before sharing the public nonce with peers.
303
+ * ```ts
304
+ * import { IndividualPubkey, nonceGen } from '@scure/btc-signer/musig2.js';
305
+ * import { randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
306
+ * const secretKey = randomPrivateKeyBytes();
307
+ * nonceGen(IndividualPubkey(secretKey), secretKey);
308
+ * ```
164
309
  */
165
310
  export function nonceGen(publicKey, secretKey, aggPublicKey = new Uint8Array(0), msg, extraIn = new Uint8Array(0), rand = randomBytes(32)) {
166
311
  abytes(publicKey, PUBKEY_LEN);
167
312
  abytesOptional(secretKey, 32);
168
313
  abytes(aggPublicKey);
169
314
  if (![0, 32].includes(aggPublicKey.length))
170
- throw new Error('wrong aggPublicKey');
315
+ throw new RangeError('wrong aggPublicKey');
171
316
  abytesOptional(msg);
172
317
  abytes(extraIn);
173
318
  abytes(rand, 32);
174
319
  if (secretKey !== undefined)
175
320
  rand = aux(secretKey, rand);
321
+ // BIP327 distinguishes an omitted message from an explicitly empty one so the two cases
322
+ // derive different nonces even when every other session parameter matches.
176
323
  const msgPrefixed = msg !== undefined
177
324
  ? concatBytes(Uint8Array.of(1), numberToBytesBE(msg.length, 8), msg)
178
325
  : Uint8Array.of(0);
@@ -185,13 +332,33 @@ export function nonceGen(publicKey, secretKey, aggPublicKey = new Uint8Array(0),
185
332
  }
186
333
  /**
187
334
  * Aggregates public nonces from multiple signers into a single aggregate nonce.
188
- * @param pubNonces An array of public nonces from each signer (Uint8Array). Each pubnonce is assumed to be 66 bytes (two 33‐byte parts).
335
+ * @param pubNonces - public nonces from each signer
189
336
  * @returns The aggregate nonce (Uint8Array).
190
- * @throws {Error} If the input is not an array or if any element is not a Uint8Array of the correct length.
191
- * @throws {InvalidContributionErr} If any of the public nonces are invalid and cannot be processed.
337
+ * @throws If the nonce payloads are malformed or contain infinity points. {@link Error}
338
+ * @throws On wrong argument types. {@link TypeError}
339
+ * @throws On wrong argument ranges or values. {@link RangeError}
340
+ * @throws If any of the public nonces are invalid and cannot be processed.
341
+ * {@link InvalidContributionErr}
342
+ * @example
343
+ * Combine all participant public nonces before building the session.
344
+ * ```ts
345
+ * import { IndividualPubkey, nonceAggregate, nonceGen } from '@scure/btc-signer/musig2.js';
346
+ * import { randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
347
+ * const alice = randomPrivateKeyBytes();
348
+ * const bob = randomPrivateKeyBytes();
349
+ * nonceAggregate([
350
+ * nonceGen(IndividualPubkey(alice), alice).public,
351
+ * nonceGen(IndividualPubkey(bob), bob).public,
352
+ * ]);
353
+ * ```
192
354
  */
193
355
  export function nonceAggregate(pubNonces) {
194
356
  abytesArray(pubNonces, 66);
357
+ // BIP327 NonceAgg input: "The number u of pubnonces with 0 < u < 2^32".
358
+ // cbytes_ext uses bytes(33, 0) as the infinity sentinel for summed real contributions,
359
+ // not as an "empty aggregate" encoding.
360
+ if (pubNonces.length < 1)
361
+ throw new RangeError('nonceAggregate: expected at least 1 public nonce');
195
362
  let R1 = Point.ZERO;
196
363
  let R2 = Point.ZERO;
197
364
  for (let i = 0; i < pubNonces.length; i++) {
@@ -210,8 +377,41 @@ export function nonceAggregate(pubNonces) {
210
377
  return PubNonce.encode({ R1, R2 });
211
378
  }
212
379
  // Class allows us re-use pre-computed stuff
213
- // NOTE: it would be nice to aggregate nonce in construdctor, but there is test that passes already aggregated nonce here.
380
+ // NOTE: it would be nice to aggregate nonce in constructor, but there is a
381
+ // test that passes an already aggregated nonce here.
382
+ /**
383
+ * MuSig2 session context for partial signing and aggregation.
384
+ * @param aggNonce - aggregate nonce from all participants combined
385
+ * @param publicKeys - all participant public keys
386
+ * @param msg - message to be signed
387
+ * @param tweaks - optional tweaks applied to the aggregate public key
388
+ * @param isXonly - whether each tweak uses x-only semantics
389
+ * @example
390
+ * Build one session object and reuse it for all partial-signature steps.
391
+ * ```ts
392
+ * import {
393
+ * IndividualPubkey,
394
+ * Session,
395
+ * keyAggregate,
396
+ * keyAggExport,
397
+ * nonceAggregate,
398
+ * nonceGen,
399
+ * } from '@scure/btc-signer/musig2.js';
400
+ * import { randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
401
+ * const alice = randomPrivateKeyBytes();
402
+ * const bob = randomPrivateKeyBytes();
403
+ * const publicKeys = [IndividualPubkey(alice), IndividualPubkey(bob)];
404
+ * const agg = keyAggregate(publicKeys);
405
+ * const aggNonce = nonceAggregate([
406
+ * nonceGen(publicKeys[0], alice, keyAggExport(agg)).public,
407
+ * nonceGen(publicKeys[1], bob, keyAggExport(agg)).public,
408
+ * ]);
409
+ * const msg = new TextEncoder().encode('hello musig2');
410
+ * new Session(aggNonce, publicKeys, msg);
411
+ * ```
412
+ */
214
413
  export class Session {
414
+ aggNonce;
215
415
  publicKeys;
216
416
  Q;
217
417
  gAcc;
@@ -227,45 +427,56 @@ export class Session {
227
427
  * Constructor for the Session class.
228
428
  * It precomputes and stores values derived from the aggregate nonce, public keys,
229
429
  * message, and optional tweaks, optimizing the signing process.
230
- * @param aggNonce The aggregate nonce (Uint8Array) from all participants combined, must be 66 bytes.
231
- * @param publicKeys An array of public keys (Uint8Array) from each participant, must be 33 bytes.
232
- * @param msg The message (Uint8Array) to be signed.
233
- * @param tweaks Optional array of tweaks (Uint8Array) to be applied to the aggregate public key, each must be 32 bytes. Defaults to [].
234
- * @param isXonly Optional array of booleans indicating whether each tweak is an X-only tweak. Defaults to [].
235
- * @throws {Error} If the input is invalid, such as wrong array sizes or lengths.
430
+ * @param aggNonce - aggregate nonce from all participants combined
431
+ * @param publicKeys - participant public keys
432
+ * @param msg - message to be signed
433
+ * @param tweaks - tweaks applied to the aggregate public key
434
+ * @param isXonly - whether each tweak uses x-only semantics
435
+ * @throws If the input is invalid, such as wrong array sizes or lengths. {@link Error}
236
436
  */
237
437
  constructor(aggNonce, publicKeys, msg, tweaks = [], isXonly = []) {
438
+ abytes(aggNonce, 66);
238
439
  abytesArray(publicKeys, 33);
239
440
  abytesArray(tweaks, 32);
240
441
  aXonly(isXonly);
241
442
  abytes(msg);
242
443
  if (tweaks.length !== isXonly.length)
243
- throw new Error('The tweaks and isXonly arrays must have the same length');
444
+ throw new RangeError('The tweaks and isXonly arrays must have the same length');
244
445
  const { aggPublicKey, gAcc, tweakAcc } = keyAggregate(publicKeys, tweaks, isXonly);
245
446
  const { R1, R2 } = PubNonce.decode(aggNonce);
246
- this.publicKeys = publicKeys;
447
+ // BIP327 requires cached session-context state to be protected from third-party mutation,
448
+ // so Session must own detached copies of the caller-provided arrays and byte entries.
449
+ // Use Uint8Array.from(...) here instead of .slice() because Node Buffer overrides slice()
450
+ // to return a shared-memory view, which would let later caller mutation rewrite session state.
451
+ this.aggNonce = Uint8Array.from(aggNonce);
452
+ this.publicKeys = publicKeys.map((pk) => Uint8Array.from(pk));
247
453
  this.Q = aggPublicKey;
248
454
  this.gAcc = gAcc;
249
455
  this.tweakAcc = tweakAcc;
250
456
  this.b = taggedInt('MuSig/noncecoef', aggNonce, pointToBytes(aggPublicKey), msg);
251
- const R = R1.add(R2.multiply(this.b));
457
+ // b and the nonce points are public session values, so the faster variable-time
458
+ // multiplication is safe here; it also matches the reference point_mul, which
459
+ // maps a (negligible-probability) zero coefficient to infinity instead of failing.
460
+ const R = R1.add(R2.multiplyUnsafe(this.b));
252
461
  this.R = isZero(R) ? Point.BASE : R;
253
462
  this.e = taggedInt('BIP0340/challenge', pointToBytes(this.R), pointToBytes(aggPublicKey), msg);
254
- this.tweaks = tweaks;
255
- this.isXonly = isXonly;
256
- this.L = keyAggL(publicKeys);
257
- this.secondKey = getSecondKey(publicKeys);
463
+ this.tweaks = tweaks.map((t) => Uint8Array.from(t));
464
+ this.isXonly = isXonly.slice();
465
+ this.L = keyAggL(this.publicKeys);
466
+ this.secondKey = getSecondKey(this.publicKeys);
258
467
  }
259
468
  /**
260
469
  * Calculates the key aggregation coefficient for a given point.
261
470
  * @private
262
- * @param P The point to calculate the coefficient for.
471
+ * @param P - point to calculate the coefficient for
263
472
  * @returns The key aggregation coefficient as a bigint.
264
- * @throws {Error} If the provided public key is not included in the list of pubkeys.
473
+ * @throws If the provided public key is not included in the list of pubkeys. {@link Error}
265
474
  */
266
475
  getSessionKeyAggCoeff(P) {
267
476
  const { publicKeys } = this;
268
477
  const pk = P.toBytes(true);
478
+ // BIP327 GetSessionKeyAggCoeff fails if cbytes(P) is not one of the session pubkeys;
479
+ // once membership is confirmed, the cached L/secondKey state is enough to derive KeyAggCoeff.
269
480
  const found = publicKeys.some((p) => equalBytes(p, pk));
270
481
  if (!found)
271
482
  throw new Error("The signer's pubkey must be included in the list of pubkeys");
@@ -276,38 +487,47 @@ export class Session {
276
487
  const s = Fn.fromBytes(partialSig, true);
277
488
  if (!Fn.isValid(s))
278
489
  return false;
490
+ // BIP327 PartialSigVerifyInternal: `Let s = int(psig); fail if s >= n`, so s=0 must stay
491
+ // in the public verification equation and return false on mismatch instead of throwing.
279
492
  const { R1, R2 } = PubNonce.decode(publicNonce);
280
- const Re_s_ = R1.add(R2.multiply(b));
493
+ // Verification only handles public data (nonces, pubkeys, hash-derived scalars),
494
+ // so the faster variable-time multiplications are safe here; they also match the
495
+ // reference point_mul, which maps zero scalars to infinity instead of failing.
496
+ const Re_s_ = R1.add(R2.multiplyUnsafe(b));
281
497
  const Re_s = hasEven(R.y) ? Re_s_ : Re_s_.negate();
282
498
  const P = Point.fromBytes(publicKey);
283
499
  const a = this.getSessionKeyAggCoeff(P);
284
- const g = Fn.mul(evenScalar(Q, 1n), gAcc);
285
- const left = mulBase(s);
286
- const right = Re_s.add(P.multiply(Fn.mul(e, Fn.mul(a, g))));
500
+ const g = Fn.mul(evenScalar(Q, _1n), gAcc);
501
+ const left = Point.BASE.multiplyUnsafe(s);
502
+ const right = Re_s.add(P.multiplyUnsafe(Fn.mul(e, Fn.mul(a, g))));
287
503
  return left.equals(right);
288
504
  }
289
505
  /**
290
- * Generates a partial signature for a given message, secret nonce, secret key, and session context.
291
- * @param secretNonce The secret nonce for this signing session (Uint8Array). MUST be securely erased after use.
292
- * @param secret The secret key of the signer (Uint8Array).
293
- * @param sessionCtx The session context containing all necessary information for signing.
294
- * @param fastSign if set to true, the signature is created without checking validity.
506
+ * Generates a partial signature for a given message, secret nonce,
507
+ * secret key, and session context.
508
+ * @param secretNonce - secret nonce for this signing session; it is zeroed after use
509
+ * @param secret - secret key of the signer
510
+ * @param fastSign - if `true`, skip the self-verification pass
295
511
  * @returns The partial signature (Uint8Array).
296
- * @throws {Error} If the input is invalid, such as wrong array sizes, invalid nonce or secret key.
512
+ * @throws If the input is invalid, such as wrong array sizes,
513
+ * invalid nonce, or invalid secret key. {@link Error}
297
514
  */
298
515
  sign(secretNonce, secret, fastSign = false) {
299
516
  abytes(secret, 32);
300
517
  if (typeof fastSign !== 'boolean')
301
- throw new Error('expected boolean');
518
+ throw new TypeError('expected boolean');
302
519
  const { Q, gAcc, b, R, e } = this;
303
520
  const { k1: k1_, k2: k2_, publicKey: originalPk } = SecretNonce.decode(secretNonce);
304
521
  // zero-out the first 64 bytes of secretNonce so it cannot be reused
305
- // TODO: this was in reference implementation, but feels very broken. Modifying input arguments is pretty bad.
522
+ // BIP327 permits overwriting the first 64 secnonce bytes after reading k1/k2 so
523
+ // accidental reuse fails fast instead of reusing the same nonce scalars.
524
+ // TODO: this was in the reference implementation, but feels very broken.
525
+ // Modifying input arguments is pretty bad.
306
526
  secretNonce.fill(0, 0, 64);
307
527
  if (!Fn.isValid(k1_))
308
528
  throw new Error('wrong k1');
309
529
  if (!Fn.isValid(k2_))
310
- throw new Error('wrong k1');
530
+ throw new Error('wrong k2');
311
531
  const k1 = evenScalar(R, k1_);
312
532
  const k2 = evenScalar(R, k2_);
313
533
  const d_ = Fn.fromBytes(secret);
@@ -318,7 +538,7 @@ export class Session {
318
538
  if (!equalBytes(pk, originalPk))
319
539
  throw new Error('Public key does not match nonceGen argument');
320
540
  const a = this.getSessionKeyAggCoeff(P);
321
- const g = evenScalar(Q, 1n);
541
+ const g = evenScalar(Q, _1n);
322
542
  const d = Fn.mul(g, Fn.mul(gAcc, d_));
323
543
  /// k1 + (b*k2) + (e*a*d)
324
544
  const s = Fn.add(k1, Fn.add(Fn.mul(b, k2), Fn.mul(e, Fn.mul(a, d))));
@@ -336,15 +556,12 @@ export class Session {
336
556
  }
337
557
  /**
338
558
  * Verifies a partial signature against the aggregate public key and other session parameters.
339
- * @param partialSig The partial signature to verify (Uint8Array).
340
- * @param pubNonces An array of public nonces from each signer (Uint8Array).
341
- * @param pubKeys An array of public keys from each signer (Uint8Array).
342
- * @param tweaks An array of tweaks applied to the aggregate public key.
343
- * @param isXonly An array of booleans indicating whether each tweak is an X-only tweak.
344
- * @param msg The message that was signed (Uint8Array).
345
- * @param i The index of the signer whose partial signature is being verified.
346
- * @returns True if the partial signature is valid, false otherwise.
347
- * @throws {Error} If the input is invalid, such as non array partialSig, pubNonces, pubKeys, tweaks.
559
+ * @param partialSig - partial signature to verify
560
+ * @param pubNonces - public nonces from each signer
561
+ * @param i - index of the signer whose partial signature is being verified
562
+ * @returns `true` if the partial signature is valid.
563
+ * @throws If the input is invalid, such as non-array partial signatures
564
+ * or mismatched nonce counts. {@link Error}
348
565
  */
349
566
  partialSigVerify(partialSig, pubNonces, i) {
350
567
  const { publicKeys, tweaks, isXonly } = this;
@@ -355,46 +572,81 @@ export class Session {
355
572
  aXonly(isXonly);
356
573
  anumber(i);
357
574
  if (pubNonces.length !== publicKeys.length)
358
- throw new Error('The pubNonces and publicKeys arrays must have the same length');
575
+ throw new RangeError('The pubNonces and publicKeys arrays must have the same length');
359
576
  if (tweaks.length !== isXonly.length)
360
- throw new Error('The tweaks and isXonly arrays must have the same length');
577
+ throw new RangeError('The tweaks and isXonly arrays must have the same length');
578
+ // BIP327 PartialSigVerify rebuilds session_ctx from aggnonce = NonceAgg(pubnonce_1..u),
579
+ // and GetSessionValues derives b and R from that aggnonce. This Session caches b/R/e from
580
+ // the constructor aggNonce, so a stale Session would otherwise accept mismatched pubNonces
581
+ // as long as pubNonces[i] still matched the signer slot.
361
582
  if (i >= pubNonces.length)
362
- throw new Error('index outside of pubKeys/pubNonces');
583
+ throw new RangeError('index outside of pubKeys/pubNonces');
584
+ if (!equalBytes(this.aggNonce, nonceAggregate(pubNonces)))
585
+ return false;
363
586
  return this.partialSigVerifyInternal(partialSig, pubNonces[i], publicKeys[i]);
364
587
  }
365
588
  /**
366
589
  * Aggregates partial signatures from multiple signers into a single final signature.
367
- * @param partialSigs An array of partial signatures from each signer (Uint8Array).
368
- * @param sessionCtx The session context containing all necessary information for signing.
590
+ * @param partialSigs - partial signatures from each signer
369
591
  * @returns The final aggregate signature (Uint8Array).
370
- * @throws {Error} If the input is invalid, such as wrong array sizes, invalid signature.
592
+ * @throws If the input is invalid, such as wrong array sizes or malformed
593
+ * signatures. {@link Error}
371
594
  */
372
595
  partialSigAgg(partialSigs) {
373
596
  abytesArray(partialSigs, 32);
597
+ // BIP327 PartialSigAgg is defined for a non-empty psig_1..u list tied to this session_ctx;
598
+ // [] is not a valid aggregate-signature input even though the sum starts from zero.
599
+ if (partialSigs.length < 1)
600
+ throw new RangeError('partialSigs.length must be >= 1');
374
601
  const { Q, tweakAcc, R, e } = this;
375
- let s = 0n;
602
+ let s = _0n;
376
603
  for (let i = 0; i < partialSigs.length; i++) {
377
604
  const si = Fn.fromBytes(partialSigs[i], true);
378
605
  if (!Fn.isValid(si))
379
606
  throw new InvalidContributionErr(i, 'psig');
380
607
  s = Fn.add(s, si);
381
608
  }
382
- const g = evenScalar(Q, 1n);
609
+ const g = evenScalar(Q, _1n);
383
610
  s = Fn.add(s, Fn.mul(e, Fn.mul(g, tweakAcc))); // s + e * g * tweakAcc
384
611
  return concatBytes(pointToBytes(R), Fn.toBytes(s));
385
612
  }
386
613
  }
387
- const deterministicNonceHash = (secret, aggOtherNonce, aggPublicKey, msg, i) => taggedInt('MuSig/deterministic/nonce', secret, aggOtherNonce, aggPublicKey, numberToBytesBE(msg.length, 8), msg, new Uint8Array([i]));
614
+ const deterministicNonceHash = (secret, aggOtherNonce, aggPublicKey, msg, i) => taggedInt('MuSig/deterministic/nonce', secret, aggOtherNonce, aggPublicKey, numberToBytesBE(msg.length, 8), msg,
615
+ // BIP327 hashes bytes(1, i - 1) for i=1,2; callers pass 0/1 directly here.
616
+ new Uint8Array([i]));
388
617
  /**
389
618
  * Generates a nonce pair and partial signature deterministically for a single signer.
390
- * @param secret The secret key of the signer (Uint8Array).
391
- * @param aggOtherNonce The aggregate public nonce of all other signers (Uint8Array).
392
- * @param publicKeys An array of all signers' public keys (Uint8Array).
393
- * @param tweaks An array of tweaks to apply to the aggregate public key.
394
- * @param isXonly An array of booleans indicating whether each tweak is an X-only tweak.
395
- * @param msg The message to be signed (Uint8Array).
396
- * @param rand Optional extra randomness (Uint8Array).
619
+ * @param secret - secret key of the signer
620
+ * @param aggOtherNonce - aggregate public nonce of the other signers
621
+ * @param publicKeys - public keys of all signers
622
+ * @param msg - message to be signed
623
+ * @param tweaks - tweaks applied to the aggregate public key
624
+ * @param isXonly - whether each tweak uses x-only semantics
625
+ * @param rand - optional extra randomness
626
+ * @param fastSign - whether to skip partial-signature self-verification
397
627
  * @returns An object containing the public nonce and partial signature.
628
+ * @throws If MuSig2 session setup or signing fails. {@link Error}
629
+ * @throws On wrong argument types. {@link TypeError}
630
+ * @throws On wrong argument ranges or values. {@link RangeError}
631
+ * @throws If one of the aggregated public keys or nonces is invalid. {@link InvalidContributionErr}
632
+ * @example
633
+ * Generate one signer's deterministic nonce and partial signature in one step.
634
+ * ```ts
635
+ * import {
636
+ * IndividualPubkey,
637
+ * deterministicSign,
638
+ * keyAggregate,
639
+ * keyAggExport,
640
+ * nonceGen,
641
+ * } from '@scure/btc-signer/musig2.js';
642
+ * import { randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
643
+ * const alice = randomPrivateKeyBytes();
644
+ * const bob = randomPrivateKeyBytes();
645
+ * const publicKeys = [IndividualPubkey(alice), IndividualPubkey(bob)];
646
+ * const agg = keyAggregate(publicKeys);
647
+ * const otherNonce = nonceGen(publicKeys[1], bob, keyAggExport(agg)).public;
648
+ * deterministicSign(alice, otherNonce, publicKeys, new TextEncoder().encode('hello musig2'));
649
+ * ```
398
650
  */
399
651
  export function deterministicSign(secret, aggOtherNonce, publicKeys, msg, tweaks = [], isXonly = [], rand, fastSign = false) {
400
652
  abytes(secret, 32);
@@ -402,7 +654,9 @@ export function deterministicSign(secret, aggOtherNonce, publicKeys, msg, tweaks
402
654
  abytesArray(publicKeys, PUBKEY_LEN);
403
655
  abytesArray(tweaks, 32);
404
656
  abytes(msg);
405
- abytesOptional(rand);
657
+ abytesOptional(rand, 32);
658
+ // BIP327 DeterministicSign input bullet: `The auxiliary randomness rand: a 32-byte array`
659
+ // when present, so this optional argument still needs the exact-length check here.
406
660
  const sk = rand !== undefined ? aux(secret, rand) : secret;
407
661
  const aggPublicKey = keyAggExport(keyAggregate(publicKeys, tweaks, isXonly));
408
662
  const k1 = deterministicNonceHash(sk, aggOtherNonce, aggPublicKey, msg, 0);
@@ -416,4 +670,3 @@ export function deterministicSign(secret, aggOtherNonce, publicKeys, msg, tweaks
416
670
  const partialSig = session.sign(secretNonce, secret, fastSign);
417
671
  return { publicNonce, partialSig };
418
672
  }
419
- //# sourceMappingURL=musig2.js.map