@scure/btc-signer 1.6.0 → 1.7.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/src/musig2.ts ADDED
@@ -0,0 +1,517 @@
1
+ import { mod } from '@noble/curves/abstract/modular';
2
+ import {
3
+ aInRange,
4
+ bytesToNumberBE,
5
+ concatBytes,
6
+ equalBytes,
7
+ numberToBytesBE,
8
+ } from '@noble/curves/abstract/utils';
9
+ import { schnorr, secp256k1 } from '@noble/curves/secp256k1';
10
+ import { abytes, anumber } from '@noble/hashes/_assert';
11
+ import { randomBytes } from '@noble/hashes/utils';
12
+ import * as P from 'micro-packed';
13
+ import { compareBytes } from './utils.js';
14
+
15
+ /*
16
+ MuSig2. This is not the full protocol: only an implementation of primitives from BIP-327.
17
+ The implementation can be used to create own protocol,
18
+ but you need to implement nonce/partial signatures exchange yourself.
19
+ Someday BIP-373 will be more "implementable" and we can use this from PSBT.
20
+
21
+ Links:
22
+ - https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki#user-content-Test_Vectors_and_Reference_Code
23
+ - https://github.com/bitcoin/bips/blob/master/bip-0373.mediawiki (PSBT MUSIG2): very raw, no vectors, not implemented for now.
24
+ - https://github.com/bitcoin/bips/blob/master/bip-0327/reference.py
25
+ */
26
+ // Types
27
+ /**
28
+ * Represents a pair of public and secret nonces used in MuSig2 signing.
29
+ */
30
+ export type Nonces = { public: Uint8Array; secret: Uint8Array };
31
+ /**
32
+ * Represents a deterministic nonce, including its public part and the resulting partial signature.
33
+ */
34
+ export type DetNonce = { publicNonce: Uint8Array; partialSig: Uint8Array };
35
+ /**
36
+ * Represents an error indicating an invalid contribution from a signer.
37
+ * This allows pointing out which participant is malicious and what specifically is wrong.
38
+ */
39
+ export class InvalidContributionErr extends Error {
40
+ readonly idx: number; // Indice of participant
41
+ constructor(idx: number, m: string) {
42
+ super(m);
43
+ this.idx = idx;
44
+ }
45
+ }
46
+
47
+ // Utils
48
+ const { taggedHash, pointToBytes } = schnorr.utils;
49
+ const Point = secp256k1.ProjectivePoint;
50
+ type Point = typeof Point.BASE;
51
+ const PUBKEY_LEN = 33;
52
+ const ZERO = new Uint8Array(PUBKEY_LEN); // Compressed zero point
53
+ const SECP_N = secp256k1.CURVE.n;
54
+
55
+ // Encoding
56
+ // TODO: re-use in PSBT?
57
+ const compressed = P.apply(P.bytes(33), {
58
+ decode: (p: Point) => (isZero(p) ? ZERO : p.toRawBytes(true)),
59
+ encode: (b: Uint8Array) => (equalBytes(b, ZERO) ? Point.ZERO : Point.fromHex(b)),
60
+ });
61
+ const scalar = P.validate(P.U256BE, (n) => {
62
+ aInRange('n', n, 1n, SECP_N);
63
+ return n;
64
+ });
65
+ const PubNonce = P.struct({ R1: compressed, R2: compressed });
66
+ const SecretNonce = P.struct({ k1: scalar, k2: scalar, publicKey: P.bytes(PUBKEY_LEN) });
67
+
68
+ function abytesOptional(b: Uint8Array | undefined, ...lengths: number[]) {
69
+ if (b !== undefined) abytes(b, ...lengths);
70
+ }
71
+
72
+ function abytesArray(lst: Uint8Array[], ...lengths: number[]) {
73
+ if (!Array.isArray(lst)) throw new Error('expected array');
74
+ lst.forEach((i) => abytes(i, ...lengths));
75
+ }
76
+
77
+ function aXonly(lst: boolean[]) {
78
+ if (!Array.isArray(lst)) throw new Error('expected array');
79
+ lst.forEach((i, j) => {
80
+ if (typeof i !== 'boolean')
81
+ throw new Error('expected boolean in xOnly array, got' + i + '(' + j + ')');
82
+ });
83
+ }
84
+
85
+ const modN = (x: bigint) => mod(x, SECP_N);
86
+ const taggedInt = (tag: string, ...messages: Uint8Array[]) =>
87
+ modN(bytesToNumberBE(taggedHash(tag, ...messages)));
88
+ const evenScalar = (p: Point, n: bigint) => (p.hasEvenY() ? n : modN(-n));
89
+
90
+ // Short utility for compat with reference implementation
91
+ export function IndividualPubkey(seckey: Uint8Array): Uint8Array {
92
+ return secp256k1.getPublicKey(seckey, true);
93
+ }
94
+ // Same, but returns Point
95
+ function mulBase(n: bigint): Point {
96
+ return Point.BASE.multiply(n);
97
+ }
98
+ function isZero(point: Point): boolean {
99
+ return point.equals(Point.ZERO);
100
+ }
101
+
102
+ /**
103
+ * Lexicographically sorts an array of public keys.
104
+ * @param publicKeys An array of public keys (Uint8Array).
105
+ * @returns A new array containing the sorted public keys.
106
+ * @throws {Error} If the input is not an array or if any element is not a Uint8Array of the correct length.
107
+ */
108
+ export function sortKeys(publicKeys: Uint8Array[]): Uint8Array[] {
109
+ abytesArray(publicKeys, PUBKEY_LEN);
110
+ return publicKeys.sort(compareBytes);
111
+ }
112
+
113
+ // Finds second distinct key (to make coefficient 1)
114
+ function getSecondKey(publicKeys: Uint8Array[]): Uint8Array {
115
+ abytesArray(publicKeys, PUBKEY_LEN);
116
+ for (let j = 1; j < publicKeys.length; j++)
117
+ if (!equalBytes(publicKeys[j], publicKeys[0])) return publicKeys[j];
118
+ return ZERO;
119
+ }
120
+
121
+ function keyAggL(publicKeys: Uint8Array[]) {
122
+ abytesArray(publicKeys, PUBKEY_LEN);
123
+ return taggedHash('KeyAgg list', ...publicKeys);
124
+ }
125
+
126
+ function keyAggCoeffInternal(
127
+ publicKey1: Uint8Array,
128
+ publicKey2: Uint8Array,
129
+ L: Uint8Array
130
+ ): bigint {
131
+ abytes(publicKey1, PUBKEY_LEN);
132
+ abytes(publicKey2, PUBKEY_LEN);
133
+ if (equalBytes(publicKey1, publicKey2)) return 1n;
134
+ return taggedInt('KeyAgg coefficient', L, publicKey1);
135
+ }
136
+
137
+ /**
138
+ * Aggregates multiple public keys using the MuSig2 key aggregation algorithm.
139
+ * @param publicKeys An array of individual public keys (Uint8Array).
140
+ * @param tweaks An optional array of tweaks (Uint8Array) to apply to the aggregate public key.
141
+ * @param isXonly An optional array of booleans indicating whether each tweak is an X-only tweak.
142
+ * @returns An object containing the aggregate public key, accumulated sign, and accumulated tweak.
143
+ * @throws {Error} If the input is invalid, such as non array publicKeys, tweaks and isXonly array length not matching.
144
+ * @throws {InvalidContributionErr} If any of the public keys are invalid and cannot be processed.
145
+ */
146
+ export function keyAggregate(
147
+ publicKeys: Uint8Array[],
148
+ tweaks: Uint8Array[] = [],
149
+ isXonly: boolean[] = []
150
+ ) {
151
+ abytesArray(publicKeys, PUBKEY_LEN);
152
+ abytesArray(tweaks, 32);
153
+ if (tweaks.length !== isXonly.length)
154
+ throw new Error('The tweaks and isXonly arrays must have the same length');
155
+ // Aggregate
156
+ const pk2 = getSecondKey(publicKeys);
157
+ const L = keyAggL(publicKeys);
158
+ let aggPublicKey = Point.ZERO;
159
+ for (let i = 0; i < publicKeys.length; i++) {
160
+ let Pi;
161
+ try {
162
+ Pi = Point.fromHex(publicKeys[i]);
163
+ } catch (error) {
164
+ throw new InvalidContributionErr(i, 'pubkey');
165
+ }
166
+ aggPublicKey = aggPublicKey.add(Pi.multiply(keyAggCoeffInternal(publicKeys[i], pk2, L)));
167
+ }
168
+ let gAcc = 1n;
169
+ let tweakAcc = 0n;
170
+ // Apply tweaks
171
+ for (let i = 0; i < tweaks.length; i++) {
172
+ const g = isXonly[i] && !aggPublicKey.hasEvenY() ? modN(-1n) : 1n;
173
+ const t = bytesToNumberBE(tweaks[i]);
174
+ aInRange('tweak', t, 0n, SECP_N);
175
+ aggPublicKey = aggPublicKey.multiply(g).add(mulBase(t));
176
+ if (isZero(aggPublicKey)) throw new Error('The result of tweaking cannot be infinity');
177
+ gAcc = modN(g * gAcc);
178
+ tweakAcc = modN(t + g * tweakAcc);
179
+ }
180
+ return { aggPublicKey, gAcc, tweakAcc };
181
+ }
182
+ /**
183
+ * Exports the aggregate public key to a byte array.
184
+ * @param ctx The result of the keyAggregate function.
185
+ * @returns The aggregate public key as a byte array.
186
+ */
187
+ export function keyAggExport(ctx: ReturnType<typeof keyAggregate>) {
188
+ return pointToBytes(ctx.aggPublicKey);
189
+ }
190
+
191
+ function aux(secret: Uint8Array, rand: Uint8Array): Uint8Array {
192
+ const rand2 = taggedHash('MuSig/aux', rand);
193
+ if (secret.length !== rand2.length) throw new Error('Cannot XOR arrays of different lengths');
194
+ const res = new Uint8Array(secret.length);
195
+ for (let i = 0; i < secret.length; i++) res[i] = secret[i] ^ rand2[i];
196
+ return res;
197
+ }
198
+
199
+ const nonceHash = (
200
+ rand: Uint8Array,
201
+ publicKey: Uint8Array,
202
+ aggPublicKey: Uint8Array,
203
+ i: number,
204
+ msgPrefixed: Uint8Array,
205
+ extraIn: Uint8Array
206
+ ): bigint =>
207
+ taggedInt(
208
+ 'MuSig/nonce',
209
+ rand,
210
+ new Uint8Array([publicKey.length]),
211
+ publicKey,
212
+ new Uint8Array([aggPublicKey.length]),
213
+ aggPublicKey,
214
+ msgPrefixed,
215
+ numberToBytesBE(extraIn.length, 4),
216
+ extraIn,
217
+ new Uint8Array([i])
218
+ );
219
+
220
+ /**
221
+ * Generates a nonce pair (public and secret) for MuSig2 signing.
222
+ * @param publicKey The individual public key of the signer (Uint8Array).
223
+ * @param secretKey The secret key of the signer (Uint8Array). Optional, included to xor randomness
224
+ * @param aggPublicKey The aggregate public key of all signers (Uint8Array).
225
+ * @param msg The message to be signed (Uint8Array).
226
+ * @param extraIn Extra input for nonce generation (Uint8Array).
227
+ * @param rand Random 32-bytes for generating the nonces (Uint8Array).
228
+ * @returns An object containing the public and secret nonces.
229
+ * @throws {Error} If the input is invalid, such as non array publicKey, secretKey, aggPublicKey.
230
+ */
231
+ export function nonceGen(
232
+ publicKey: Uint8Array,
233
+ secretKey?: Uint8Array,
234
+ aggPublicKey: Uint8Array = new Uint8Array(0),
235
+ msg?: Uint8Array,
236
+ extraIn: Uint8Array = new Uint8Array(0),
237
+ rand: Uint8Array = randomBytes(32)
238
+ ): Nonces {
239
+ abytes(publicKey, PUBKEY_LEN);
240
+ abytesOptional(secretKey, 32);
241
+ abytes(aggPublicKey, 0, 32);
242
+ abytesOptional(msg);
243
+ abytes(extraIn);
244
+ abytes(rand, 32);
245
+
246
+ if (secretKey !== undefined) rand = aux(secretKey, rand);
247
+ const msgPrefixed =
248
+ msg !== undefined
249
+ ? concatBytes(new Uint8Array([1]), numberToBytesBE(msg.length, 8), msg)
250
+ : new Uint8Array([0]);
251
+ const k1 = nonceHash(rand, publicKey, aggPublicKey, 0, msgPrefixed, extraIn);
252
+ const k2 = nonceHash(rand, publicKey, aggPublicKey, 1, msgPrefixed, extraIn);
253
+ return {
254
+ secret: SecretNonce.encode({ k1, k2, publicKey }),
255
+ public: PubNonce.encode({ R1: mulBase(k1), R2: mulBase(k2) }),
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Aggregates public nonces from multiple signers into a single aggregate nonce.
261
+ * @param pubNonces An array of public nonces from each signer (Uint8Array). Each pubnonce is assumed to be 66 bytes (two 33‐byte parts).
262
+ * @returns The aggregate nonce (Uint8Array).
263
+ * @throws {Error} If the input is not an array or if any element is not a Uint8Array of the correct length.
264
+ * @throws {InvalidContributionErr} If any of the public nonces are invalid and cannot be processed.
265
+ */
266
+ export function nonceAggregate(pubNonces: Uint8Array[]): Uint8Array {
267
+ abytesArray(pubNonces, 66);
268
+ let R1 = Point.ZERO;
269
+ let R2 = Point.ZERO;
270
+ for (let i = 0; i < pubNonces.length; i++) {
271
+ const pn = pubNonces[i];
272
+ try {
273
+ const { R1: R1n, R2: R2n } = PubNonce.decode(pn);
274
+ if (isZero(R1n) || isZero(R2n)) throw new Error('infinity point');
275
+ R1 = R1.add(R1n);
276
+ R2 = R2.add(R2n);
277
+ } catch (error) {
278
+ throw new InvalidContributionErr(i, 'pubnonce');
279
+ }
280
+ }
281
+ return PubNonce.encode({ R1, R2 });
282
+ }
283
+
284
+ // Class allows us re-use pre-computed stuff
285
+ // NOTE: it would be nice to aggregate nonce in construdctor, but there is test that passes already aggregated nonce here.
286
+ export class Session {
287
+ private publicKeys: Uint8Array[];
288
+ private Q: Point;
289
+ private gAcc: bigint;
290
+ private tweakAcc: bigint;
291
+ private b: bigint;
292
+ private R: Point;
293
+ private e: bigint;
294
+ private tweaks: Uint8Array[];
295
+ private isXonly: boolean[];
296
+ private L: Uint8Array;
297
+ private secondKey: Uint8Array;
298
+ /**
299
+ * Constructor for the Session class.
300
+ * It precomputes and stores values derived from the aggregate nonce, public keys,
301
+ * message, and optional tweaks, optimizing the signing process.
302
+ * @param aggNonce The aggregate nonce (Uint8Array) from all participants combined, must be 66 bytes.
303
+ * @param publicKeys An array of public keys (Uint8Array) from each participant, must be 33 bytes.
304
+ * @param msg The message (Uint8Array) to be signed.
305
+ * @param tweaks Optional array of tweaks (Uint8Array) to be applied to the aggregate public key, each must be 32 bytes. Defaults to [].
306
+ * @param isXonly Optional array of booleans indicating whether each tweak is an X-only tweak. Defaults to [].
307
+ * @throws {Error} If the input is invalid, such as wrong array sizes or lengths.
308
+ */
309
+ constructor(
310
+ aggNonce: Uint8Array,
311
+ publicKeys: Uint8Array[],
312
+ msg: Uint8Array,
313
+ tweaks: Uint8Array[] = [],
314
+ isXonly: boolean[] = []
315
+ ) {
316
+ abytesArray(publicKeys, 33);
317
+ abytesArray(tweaks, 32);
318
+ aXonly(isXonly);
319
+ abytes(msg);
320
+ if (tweaks.length !== isXonly.length)
321
+ throw new Error('The tweaks and isXonly arrays must have the same length');
322
+ const { aggPublicKey, gAcc, tweakAcc } = keyAggregate(publicKeys, tweaks, isXonly);
323
+ const { R1, R2 } = PubNonce.decode(aggNonce);
324
+ this.publicKeys = publicKeys;
325
+ this.Q = aggPublicKey;
326
+ this.gAcc = gAcc;
327
+ this.tweakAcc = tweakAcc;
328
+ this.b = taggedInt('MuSig/noncecoef', aggNonce, pointToBytes(aggPublicKey), msg);
329
+ const R = R1.add(R2.multiply(this.b));
330
+ this.R = isZero(R) ? Point.BASE : R;
331
+ this.e = taggedInt('BIP0340/challenge', pointToBytes(this.R), pointToBytes(aggPublicKey), msg);
332
+ this.tweaks = tweaks;
333
+ this.isXonly = isXonly;
334
+ this.L = keyAggL(publicKeys);
335
+ this.secondKey = getSecondKey(publicKeys);
336
+ }
337
+ /**
338
+ * Calculates the key aggregation coefficient for a given point.
339
+ * @private
340
+ * @param P The point to calculate the coefficient for.
341
+ * @returns The key aggregation coefficient as a bigint.
342
+ * @throws {Error} If the provided public key is not included in the list of pubkeys.
343
+ */
344
+ private getSessionKeyAggCoeff(P: Point): bigint {
345
+ const { publicKeys } = this;
346
+ const pk = P.toRawBytes(true);
347
+ const found = publicKeys.some((p) => equalBytes(p, pk));
348
+ if (!found) throw new Error("The signer's pubkey must be included in the list of pubkeys");
349
+ return keyAggCoeffInternal(pk, this.secondKey, this.L);
350
+ }
351
+ private partialSigVerifyInternal(
352
+ partialSig: Uint8Array,
353
+ publicNonce: Uint8Array,
354
+ publicKey: Uint8Array
355
+ ): boolean {
356
+ const { Q, gAcc, b, R, e } = this;
357
+ const s = bytesToNumberBE(partialSig);
358
+ if (s >= SECP_N) return false;
359
+ const { R1, R2 } = PubNonce.decode(publicNonce);
360
+ const Re_s_ = R1.add(R2.multiply(b));
361
+ const Re_s = R.hasEvenY() ? Re_s_ : Re_s_.negate();
362
+ const P = Point.fromHex(publicKey);
363
+ const a = this.getSessionKeyAggCoeff(P);
364
+ const g = modN(evenScalar(Q, 1n) * gAcc);
365
+ const left = mulBase(s);
366
+ const right = Re_s.add(P.multiply(modN(e * a * g)));
367
+ return left.equals(right);
368
+ }
369
+
370
+ /**
371
+ * Generates a partial signature for a given message, secret nonce, secret key, and session context.
372
+ * @param secretNonce The secret nonce for this signing session (Uint8Array). MUST be securely erased after use.
373
+ * @param secret The secret key of the signer (Uint8Array).
374
+ * @param sessionCtx The session context containing all necessary information for signing.
375
+ * @param fastSign if set to true, the signature is created without checking validity.
376
+ * @returns The partial signature (Uint8Array).
377
+ * @throws {Error} If the input is invalid, such as wrong array sizes, invalid nonce or secret key.
378
+ */
379
+ sign(secretNonce: Uint8Array, secret: Uint8Array, fastSign = false): Uint8Array {
380
+ abytes(secret, 32);
381
+ if (typeof fastSign !== 'boolean') throw new Error('expected boolean');
382
+ const { Q, gAcc, b, R, e } = this;
383
+ const { k1: k1_, k2: k2_, publicKey: originalPk } = SecretNonce.decode(secretNonce);
384
+ // zero-out the first 64 bytes of secretNonce so it cannot be reused
385
+ // TODO: this was in reference implementation, but feels very broken. Modifying input arguments is pretty bad.
386
+ secretNonce.fill(0, 0, 64);
387
+ aInRange('k1', k1_, 0n, SECP_N);
388
+ aInRange('k2', k2_, 0n, SECP_N);
389
+ const k1 = evenScalar(R, k1_);
390
+ const k2 = evenScalar(R, k2_);
391
+ const d_ = bytesToNumberBE(secret);
392
+ aInRange('d_', d_, 1n, SECP_N);
393
+ const P = mulBase(d_);
394
+ const pk = P.toRawBytes(true);
395
+ if (!equalBytes(pk, originalPk)) throw new Error('Public key does not match nonceGen argument');
396
+ const a = this.getSessionKeyAggCoeff(P);
397
+ const g = evenScalar(Q, 1n);
398
+ const d = modN(g * gAcc * d_);
399
+ const s = modN(k1 + b * k2 + e * a * d);
400
+ const partialSig = numberToBytesBE(s, 32);
401
+ // Skip validation in fast-sign mode
402
+ if (!fastSign) {
403
+ const publicNonce = PubNonce.encode({
404
+ R1: mulBase(k1_),
405
+ R2: mulBase(k2_),
406
+ });
407
+ if (!this.partialSigVerifyInternal(partialSig, publicNonce, pk))
408
+ throw new Error('Partial signature verification failed');
409
+ }
410
+ return partialSig;
411
+ }
412
+ /**
413
+ * Verifies a partial signature against the aggregate public key and other session parameters.
414
+ * @param partialSig The partial signature to verify (Uint8Array).
415
+ * @param pubNonces An array of public nonces from each signer (Uint8Array).
416
+ * @param pubKeys An array of public keys from each signer (Uint8Array).
417
+ * @param tweaks An array of tweaks applied to the aggregate public key.
418
+ * @param isXonly An array of booleans indicating whether each tweak is an X-only tweak.
419
+ * @param msg The message that was signed (Uint8Array).
420
+ * @param i The index of the signer whose partial signature is being verified.
421
+ * @returns True if the partial signature is valid, false otherwise.
422
+ * @throws {Error} If the input is invalid, such as non array partialSig, pubNonces, pubKeys, tweaks.
423
+ */
424
+ partialSigVerify(partialSig: Uint8Array, pubNonces: Uint8Array[], i: number): boolean {
425
+ const { publicKeys, tweaks, isXonly } = this;
426
+ abytes(partialSig, 32);
427
+ abytesArray(pubNonces, 66);
428
+ abytesArray(publicKeys, PUBKEY_LEN);
429
+ abytesArray(tweaks, 32);
430
+ aXonly(isXonly);
431
+ anumber(i);
432
+ if (pubNonces.length !== publicKeys.length)
433
+ throw new Error('The pubNonces and publicKeys arrays must have the same length');
434
+ if (tweaks.length !== isXonly.length)
435
+ throw new Error('The tweaks and isXonly arrays must have the same length');
436
+ if (i >= pubNonces.length) throw new Error('index outside of pubKeys/pubNonces');
437
+ return this.partialSigVerifyInternal(partialSig, pubNonces[i], publicKeys[i]);
438
+ }
439
+ /**
440
+ * Aggregates partial signatures from multiple signers into a single final signature.
441
+ * @param partialSigs An array of partial signatures from each signer (Uint8Array).
442
+ * @param sessionCtx The session context containing all necessary information for signing.
443
+ * @returns The final aggregate signature (Uint8Array).
444
+ * @throws {Error} If the input is invalid, such as wrong array sizes, invalid signature.
445
+ */
446
+ partialSigAgg(partialSigs: Uint8Array[]): Uint8Array {
447
+ abytesArray(partialSigs, 32);
448
+ const { Q, tweakAcc, R, e } = this;
449
+ let s = 0n;
450
+ for (let i = 0; i < partialSigs.length; i++) {
451
+ const si = bytesToNumberBE(partialSigs[i]);
452
+ if (si >= SECP_N) throw new InvalidContributionErr(i, 'psig');
453
+ s = modN(s + si);
454
+ }
455
+ const g = evenScalar(Q, 1n);
456
+ s = modN(s + e * g * tweakAcc);
457
+ return concatBytes(pointToBytes(R), numberToBytesBE(s, 32));
458
+ }
459
+ }
460
+
461
+ const deterministicNonceHash = (
462
+ secret: Uint8Array,
463
+ aggOtherNonce: Uint8Array,
464
+ aggPublicKey: Uint8Array,
465
+ msg: Uint8Array,
466
+ i: number
467
+ ): bigint =>
468
+ taggedInt(
469
+ 'MuSig/deterministic/nonce',
470
+ secret,
471
+ aggOtherNonce,
472
+ aggPublicKey,
473
+ numberToBytesBE(msg.length, 8),
474
+ msg,
475
+ new Uint8Array([i])
476
+ );
477
+
478
+ /**
479
+ * Generates a nonce pair and partial signature deterministically for a single signer.
480
+ * @param secret The secret key of the signer (Uint8Array).
481
+ * @param aggOtherNonce The aggregate public nonce of all other signers (Uint8Array).
482
+ * @param publicKeys An array of all signers' public keys (Uint8Array).
483
+ * @param tweaks An array of tweaks to apply to the aggregate public key.
484
+ * @param isXonly An array of booleans indicating whether each tweak is an X-only tweak.
485
+ * @param msg The message to be signed (Uint8Array).
486
+ * @param rand Optional extra randomness (Uint8Array).
487
+ * @returns An object containing the public nonce and partial signature.
488
+ */
489
+ export function deterministicSign(
490
+ secret: Uint8Array,
491
+ aggOtherNonce: Uint8Array,
492
+ publicKeys: Uint8Array[],
493
+ msg: Uint8Array,
494
+ tweaks: Uint8Array[] = [],
495
+ isXonly: boolean[] = [],
496
+ rand?: Uint8Array,
497
+ fastSign = false
498
+ ): DetNonce {
499
+ abytes(secret, 32);
500
+ abytes(aggOtherNonce, 66);
501
+ abytesArray(publicKeys, PUBKEY_LEN);
502
+ abytesArray(tweaks, 32);
503
+ abytes(msg);
504
+ abytesOptional(rand);
505
+ const sk = rand !== undefined ? aux(secret, rand) : secret;
506
+ const aggPublicKey = keyAggExport(keyAggregate(publicKeys, tweaks, isXonly));
507
+ const k1 = deterministicNonceHash(sk, aggOtherNonce, aggPublicKey, msg, 0);
508
+ const k2 = deterministicNonceHash(sk, aggOtherNonce, aggPublicKey, msg, 1);
509
+ const R1 = mulBase(k1);
510
+ const R2 = mulBase(k2);
511
+ const publicNonce = PubNonce.encode({ R1, R2 });
512
+ const secretNonce = SecretNonce.encode({ k1, k2, publicKey: IndividualPubkey(secret) });
513
+ const aggNonce = nonceAggregate([publicNonce, aggOtherNonce]);
514
+ const session = new Session(aggNonce, publicKeys, msg, tweaks, isXonly);
515
+ const partialSig = session.sign(secretNonce, secret, fastSign);
516
+ return { publicNonce, partialSig };
517
+ }
package/src/psbt.ts CHANGED
@@ -458,7 +458,8 @@ export function mergeKeyMap<T extends PSBTKeyMap>(
458
458
  psbtEnum: T,
459
459
  val: PSBTKeyMapKeys<T>,
460
460
  cur?: PSBTKeyMapKeys<T>,
461
- allowedFields?: (keyof PSBTKeyMapKeys<T>)[]
461
+ allowedFields?: (keyof PSBTKeyMapKeys<T>)[],
462
+ allowUnknown?: boolean
462
463
  ): PSBTKeyMapKeys<T> {
463
464
  const res: PSBTKeyMapKeys<T> = { ...cur, ...val };
464
465
  // All arguments can be provided as hex
@@ -517,8 +518,13 @@ export function mergeKeyMap<T extends PSBTKeyMap>(
517
518
  throw new Error(`Cannot change signed field=${k}`);
518
519
  }
519
520
  }
520
- // Remove unknown keys
521
- for (const k in res) if (!psbtEnum[k]) delete res[k];
521
+ // Remove unknown keys except the "unknown" array if allowUnknown is true
522
+ for (const k in res) {
523
+ if (!psbtEnum[k]) {
524
+ if (allowUnknown && k === 'unknown') continue;
525
+ delete res[k];
526
+ }
527
+ }
522
528
  return res;
523
529
  }
524
530
 
@@ -61,7 +61,7 @@ export function cloneDeep<T>(obj: T): T {
61
61
 
62
62
  // Mostly security features, hardened defaults;
63
63
  // but you still can parse other people tx with unspendable outputs and stuff if you want
64
- export type TxOpts = {
64
+ export interface TxOpts {
65
65
  version?: number;
66
66
  lockTime?: number;
67
67
  PSBTVersion?: number;
@@ -84,7 +84,9 @@ export type TxOpts = {
84
84
  allowLegacyWitnessUtxo?: boolean;
85
85
  lowR?: boolean; // Use lowR signatures
86
86
  customScripts?: CustomScript[]; // UNSAFE: Custom payment scripts
87
- };
87
+ // Allow to add additional unknown keys/values to the "unknown" array member
88
+ allowUnknown?: boolean;
89
+ }
88
90
 
89
91
  /**
90
92
  * Internal, exported only for backwards-compat. Use `SigHash` instead.
@@ -520,7 +522,8 @@ export class Transaction {
520
522
  input,
521
523
  this.inputs[idx],
522
524
  allowedFields,
523
- this.opts.disableScriptCheck
525
+ this.opts.disableScriptCheck,
526
+ this.opts.allowUnknown
524
527
  );
525
528
  }
526
529
  // Output stuff
@@ -556,7 +559,7 @@ export class Transaction {
556
559
  if (script === undefined) script = cur?.script;
557
560
  let res: psbt.PSBTKeyMapKeys<typeof psbt.PSBTOutput> = { ...cur, ...o, amount, script };
558
561
  if (res.amount === undefined) delete res.amount;
559
- res = psbt.mergeKeyMap(psbt.PSBTOutput, res, cur, allowedFields);
562
+ res = psbt.mergeKeyMap(psbt.PSBTOutput, res, cur, allowedFields, this.opts.allowUnknown);
560
563
  psbt.PSBTOutputCoder.encode(res);
561
564
  if (
562
565
  res.script &&
@@ -1070,7 +1073,13 @@ export class Transaction {
1070
1073
  : P.EMPTY;
1071
1074
  if (!equalBytes(thisUnsigned, otherUnsigned))
1072
1075
  throw new Error(`Transaction/combine: different unsigned tx`);
1073
- this.global = psbt.mergeKeyMap(psbt.PSBTGlobal, this.global, other.global);
1076
+ this.global = psbt.mergeKeyMap(
1077
+ psbt.PSBTGlobal,
1078
+ this.global,
1079
+ other.global,
1080
+ undefined,
1081
+ this.opts.allowUnknown
1082
+ );
1074
1083
  for (let i = 0; i < this.inputs.length; i++) this.updateInput(i, other.inputs[i], true);
1075
1084
  for (let i = 0; i < this.outputs.length; i++) this.updateOutput(i, other.outputs[i], true);
1076
1085
  return this;
package/src/utxo.ts CHANGED
@@ -37,7 +37,8 @@ export function normalizeInput(
37
37
  i: psbt.TransactionInputUpdate,
38
38
  cur?: psbt.TransactionInput,
39
39
  allowedFields?: (keyof psbt.TransactionInput)[],
40
- disableScriptCheck = false
40
+ disableScriptCheck = false,
41
+ allowUnknown = false
41
42
  ): psbt.TransactionInput {
42
43
  let { nonWitnessUtxo, txid } = i;
43
44
  // String support for common fields. We usually prefer Uint8Array to avoid errors
@@ -55,7 +56,7 @@ export function normalizeInput(
55
56
  if (!('nonWitnessUtxo' in i) && res.nonWitnessUtxo === undefined) delete res.nonWitnessUtxo;
56
57
  if (res.sequence === undefined) res.sequence = DEFAULT_SEQUENCE;
57
58
  if (res.tapMerkleRoot === null) delete res.tapMerkleRoot;
58
- res = psbt.mergeKeyMap(psbt.PSBTInput, res, cur, allowedFields);
59
+ res = psbt.mergeKeyMap(psbt.PSBTInput, res, cur, allowedFields, allowUnknown);
59
60
  psbt.PSBTInputCoder.encode(res); // Validates that everything is correct at this point
60
61
 
61
62
  let prevOut;
@@ -341,11 +342,11 @@ export class _Estimator {
341
342
  // - change address: can be smaller for segwit
342
343
  // - accumExact: ???
343
344
  private dust: bigint; // total dust limit (3||opts.dustRelayFeeRate * 182||opts.dust). Default: 546
344
- constructor(
345
- inputs: psbt.TransactionInputUpdate[],
346
- private outputs: Output[],
347
- private opts: EstimatorOpts
348
- ) {
345
+ private outputs: Output[];
346
+ private opts: EstimatorOpts;
347
+ constructor(inputs: psbt.TransactionInputUpdate[], outputs: Output[], opts: EstimatorOpts) {
348
+ this.outputs = outputs;
349
+ this.opts = opts;
349
350
  if (typeof opts.feePerByte !== 'bigint')
350
351
  throw new Error(
351
352
  `Estimator: wrong feePerByte=${
@@ -410,7 +411,13 @@ export class _Estimator {
410
411
  }
411
412
  const inputKeys = new Set();
412
413
  this.normalizedInputs = allInputs.map((i) => {
413
- const normalized = normalizeInput(i, undefined, undefined, opts.disableScriptCheck);
414
+ const normalized = normalizeInput(
415
+ i,
416
+ undefined,
417
+ undefined,
418
+ opts.disableScriptCheck,
419
+ opts.allowUnknown
420
+ );
414
421
  inputBeforeSign(normalized); // check fields
415
422
  const key = `${hex.encode(normalized.txid!)}:${normalized.index}`;
416
423
  if (!opts.allowSameUtxo && inputKeys.has(key))
@@ -487,10 +494,11 @@ export class _Estimator {
487
494
  let num = 0;
488
495
  let inputsAmount = 0n;
489
496
  const targetAmount = this.amount;
490
- const res = [];
497
+ const res: Set<number> = new Set();
491
498
  let fee;
492
499
  for (const idx of this.requiredIndices) {
493
500
  this.checkInputIdx(idx);
501
+ if (res.has(idx)) throw new Error('required input encountered multiple times'); // should not happen
494
502
  const { estimate, amount } = this.normalizedInputs[idx];
495
503
  let newWeight = weight + estimate.weight;
496
504
  if (!hasWitnesses && estimate.hasWitnesses) newWeight += 2; // enable witness if needed
@@ -500,13 +508,14 @@ export class _Estimator {
500
508
  if (estimate.hasWitnesses) hasWitnesses = true;
501
509
  num++;
502
510
  inputsAmount += amount;
503
- res.push(idx);
511
+ res.add(idx);
504
512
  // inputsAmount is enough to cover cost of tx
505
513
  if (!all && targetAmount + fee <= inputsAmount)
506
- return { indices: res, fee, weight: totalWeight, total: inputsAmount };
514
+ return { indices: Array.from(res), fee, weight: totalWeight, total: inputsAmount };
507
515
  }
508
516
  for (const idx of indices) {
509
517
  this.checkInputIdx(idx);
518
+ if (res.has(idx)) continue; // skip required inputs
510
519
  const { estimate, amount, value } = this.normalizedInputs[idx];
511
520
  let newWeight = weight + estimate.weight;
512
521
  if (!hasWitnesses && estimate.hasWitnesses) newWeight += 2; // enable witness if needed
@@ -522,14 +531,14 @@ export class _Estimator {
522
531
  if (estimate.hasWitnesses) hasWitnesses = true;
523
532
  num++;
524
533
  inputsAmount += amount;
525
- res.push(idx);
534
+ res.add(idx);
526
535
  // inputsAmount is enough to cover cost of tx
527
536
  if (!all && targetAmount + fee <= inputsAmount)
528
- return { indices: res, fee, weight: totalWeight, total: inputsAmount };
537
+ return { indices: Array.from(res), fee, weight: totalWeight, total: inputsAmount };
529
538
  }
530
539
  if (all) {
531
540
  const newWeight = weight + 4 * CompactSizeLen.encode(num).length;
532
- return { indices: res, fee, weight: newWeight, total: inputsAmount };
541
+ return { indices: Array.from(res), fee, weight: newWeight, total: inputsAmount };
533
542
  }
534
543
  return undefined;
535
544
  }