@scure/btc-signer 2.2.0 → 2.4.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.
@@ -0,0 +1,83 @@
1
+ import { secp256k1 as secp } from '@noble/curves/secp256k1.js';
2
+ import { hex } from '@scure/base';
3
+ import * as btc from './index.ts';
4
+ import type { TxOpts, Unknowns } from './index.ts';
5
+ import type { TArg } from './utils.ts';
6
+
7
+ const unknownPolicy: Unknowns = 'strip';
8
+ const extensionOpts: TxOpts = { unknown: unknownPolicy, proprietary: 'strict' };
9
+ new btc.Transaction(extensionOpts);
10
+
11
+ declare const combineOpts: TArg<TxOpts>;
12
+ btc.PSBTCombine([], combineOpts);
13
+ declare const first: btc.Transaction;
14
+ declare const second: btc.Transaction;
15
+ // Direct combination retains main's receiver-options API; byte-only PSBTCombine owns policy opts.
16
+ // @ts-expect-error Transaction.combine does not accept operation-specific options.
17
+ first.combine(second, combineOpts);
18
+
19
+ const privKey1 = hex.decode('0101010101010101010101010101010101010101010101010101010101010101');
20
+ const P1 = secp.getPublicKey(privKey1, true);
21
+
22
+ const wpkh = btc.p2wpkh(P1);
23
+
24
+ const tx = new btc.Transaction();
25
+
26
+ // Basic input test
27
+ tx.addInput({
28
+ txid: hex.decode('0af50a00a22f74ece24c12cd667c290d3a35d48124a69f4082700589172a3aa2'),
29
+ index: 0,
30
+ ...wpkh,
31
+ finalScriptSig: Uint8Array.of(),
32
+ sequence: 1,
33
+ });
34
+
35
+ // Doesn't force any fields on input addition (only on sign)
36
+ tx.addInput({
37
+ sequence: 1,
38
+ });
39
+
40
+ tx.updateInput(0, {
41
+ sequence: 1,
42
+ });
43
+
44
+ const nonWitnessUtxo =
45
+ '0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000';
46
+ const nonWitnessUtxoB = hex.decode(nonWitnessUtxo);
47
+
48
+ tx.updateInput(0, { nonWitnessUtxo: nonWitnessUtxo });
49
+ tx.updateInput(0, { nonWitnessUtxo: nonWitnessUtxoB });
50
+ tx.addInput({
51
+ txid: hex.decode('0af50a00a22f74ece24c12cd667c290d3a35d48124a69f4082700589172a3aa2'),
52
+ index: 0,
53
+ nonWitnessUtxo: nonWitnessUtxo,
54
+ });
55
+
56
+ tx.addInput({
57
+ txid: hex.decode('0af50a00a22f74ece24c12cd667c290d3a35d48124a69f4082700589172a3aa2'),
58
+ index: 0,
59
+ nonWitnessUtxo: nonWitnessUtxoB,
60
+ });
61
+
62
+ // Should fail!
63
+ // tx.updateInput(0, {
64
+ // nonWitnessUtxo: 1,
65
+ // });
66
+ // Outputs
67
+ tx.addOutput({ amount: BigInt(123) });
68
+ // should fail
69
+ // tx.updateOutput(0, { amount: '1' });
70
+ // tx.updateOutput(0, { amount: 1 });
71
+ // should fail
72
+ // tx.addOutput({ amount: '123' });
73
+ // tx.addOutput({ amount: 123 });
74
+
75
+ for (let i = 0; i < tx.inputsLength; i++) {
76
+ // @ts-ignore
77
+ console.log('I', tx.getInput(i));
78
+ }
79
+
80
+ for (let i = 0; i < tx.outputsLength; i++) {
81
+ // @ts-ignore
82
+ console.log('O', tx.getOutput(i));
83
+ }
package/src/index.ts CHANGED
@@ -25,9 +25,10 @@ export {
25
25
  } from './script.ts';
26
26
  export type { ScriptType } from './script.ts';
27
27
  export { getInputType, Transaction } from './transaction.ts';
28
- export { NETWORK, TAPROOT_UNSPENDABLE_KEY, TEST_NETWORK } from './utils.ts';
28
+ export type { TxOpts, Unknowns } from './transaction.ts';
29
+ export { NETWORK, TAPROOT_UNSPENDABLE_KEY, TEST_NETWORK, taprootNumsKey } from './utils.ts';
29
30
  export type { TArg, TRet } from './utils.ts';
30
- export { selectUTXO } from './utxo.ts';
31
+ export { filterTaproot, selectUTXO } from './utxo.ts';
31
32
 
32
33
  /**
33
34
  * Small collection of commonly used utility exports.
@@ -62,6 +63,7 @@ export {
62
63
  Address,
63
64
  combinations,
64
65
  getAddress,
66
+ MAX_COMBINATIONS,
65
67
  OutScript,
66
68
  sortedMultisig,
67
69
  taprootListToTree,
package/src/musig2.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { schnorr, secp256k1 } from '@noble/curves/secp256k1.js';
2
+ import type { WeierstrassPoint } from '@noble/curves/abstract/weierstrass.js';
2
3
  import { aInRange, concatBytes, equalBytes, numberToBytesBE } from '@noble/curves/utils.js';
3
4
  import { abytes, anumber, randomBytes } from '@noble/hashes/utils.js';
4
5
  import * as P from 'micro-packed';
5
- import { compareBytes, hasEven, type TArg, type TRet } from './utils.ts';
6
+ import { compareBytes, hasEven, type TArg, type TRet, validateObject } from './utils.ts';
6
7
 
7
8
  /*
8
9
  MuSig2. This is not the full protocol: only an implementation of primitives from BIP-327.
@@ -10,9 +11,16 @@ The implementation can be used to create own protocol,
10
11
  but you need to implement nonce/partial signatures exchange yourself.
11
12
  Someday BIP-373 will be more "implementable" and we can use this from PSBT.
12
13
 
14
+ SECURITY: A secret nonce MUST be used for exactly one partial signature. Session.sign() zeroes
15
+ only the Uint8Array instance passed to it; copies, serialized values, database records, and process
16
+ snapshots are not erased. Reusing the same nonce scalars in distinct sessions can reveal the
17
+ signer's secret key. Stateful integrations must keep one authoritative nonce record and atomically
18
+ consume it before releasing a partial signature.
19
+
13
20
  Links:
14
21
  - https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki#user-content-Test_Vectors_and_Reference_Code
15
- - https://github.com/bitcoin/bips/blob/master/bip-0373.mediawiki (PSBT MUSIG2): very raw, no vectors, not implemented for now.
22
+ - https://github.com/bitcoin/bips/blob/master/bip-0373.mediawiki (PSBT MuSig2): psbt.ts supports
23
+ its transport fields and vectors, but this module does not orchestrate the signing protocol.
16
24
  - https://github.com/bitcoin/bips/blob/master/bip-0327/reference.py
17
25
  */
18
26
  // Types
@@ -20,7 +28,10 @@ Links:
20
28
  export type Nonces = {
21
29
  /** Public nonce that gets shared with the other participants. */
22
30
  public: Uint8Array;
23
- /** Secret nonce that stays local until partial signing finishes. */
31
+ /**
32
+ * Secret nonce that stays local until partial signing finishes. It MUST be consumed exactly once;
33
+ * never retain a copy that could be loaded for another signing session.
34
+ */
24
35
  secret: Uint8Array;
25
36
  };
26
37
  /**
@@ -33,6 +44,15 @@ export type DetNonce = {
33
44
  /** Partial signature produced after combining all participant data. */
34
45
  partialSig: Uint8Array;
35
46
  };
47
+ /** MuSig2 key aggregation context used by signing sessions. */
48
+ export type KeyAggregate = {
49
+ /** Aggregate public key before x-only export. */
50
+ aggPublicKey: WeierstrassPoint<bigint>;
51
+ /** Accumulated sign from x-only tweaks. */
52
+ gAcc: bigint;
53
+ /** Accumulated tweak scalar. */
54
+ tweakAcc: bigint;
55
+ };
36
56
  /**
37
57
  * Represents an error indicating an invalid contribution from a signer.
38
58
  * This allows pointing out which participant is malicious and what specifically is wrong.
@@ -75,6 +95,9 @@ const PUBKEY_LEN = /* @__PURE__ */ (() => secp256k1.lengths.publicKey!)();
75
95
  // BIP327 uses bytes(33, 0) both as cbytes_ext(Point.ZERO) for infinity and as GetSecondKey's
76
96
  // "no second distinct key" sentinel, so this all-zero compressed slot is intentionally out-of-band.
77
97
  const ZERO = /* @__PURE__ */ new Uint8Array(PUBKEY_LEN); // Compressed zero point
98
+ // Be friendly to bad ECMAScript parsers by not using bigint literals.
99
+ // prettier-ignore
100
+ const _0n = /* @__PURE__ */ BigInt(0), _1n = /* @__PURE__ */ BigInt(1);
78
101
 
79
102
  // Encoding
80
103
  // TODO: re-use in PSBT?
@@ -89,7 +112,7 @@ const compressed = /* @__PURE__ */ (() =>
89
112
  // nonzero scalars in [1, n); tweak scalars use different validation because 0 is allowed there.
90
113
  const scalar = /* @__PURE__ */ (() =>
91
114
  P.validate(P.U256BE, (n) => {
92
- aInRange('n', n, 1n, Fn.ORDER);
115
+ aInRange('n', n, _1n, Fn.ORDER);
93
116
  return n;
94
117
  }))();
95
118
  // Shared for both per-signer pubnonce bytes and aggregate aggnonce bytes. Because it accepts
@@ -216,7 +239,7 @@ function keyAggCoeffInternal(
216
239
  // pk2 is the all-zero sentinel from GetSecondKey, so every real signer key still hashes.
217
240
  abytes(publicKey1, PUBKEY_LEN);
218
241
  abytes(publicKey2, PUBKEY_LEN);
219
- if (equalBytes(publicKey1, publicKey2)) return 1n;
242
+ if (equalBytes(publicKey1, publicKey2)) return _1n;
220
243
  return taggedInt('KeyAgg coefficient', L, publicKey1);
221
244
  }
222
245
 
@@ -248,7 +271,7 @@ export function keyAggregate(
248
271
  publicKeys: TArg<Uint8Array[]>,
249
272
  tweaks: TArg<Uint8Array[]> = [],
250
273
  isXonly: boolean[] = []
251
- ) {
274
+ ): KeyAggregate {
252
275
  // BIP327 KeyAgg inputs require `0 < u < 2^32`, and ApplyTweak consumes a one-for-one
253
276
  // list of boolean tweak modes; callers should enforce that public contract here.
254
277
  abytesArray(publicKeys, PUBKEY_LEN);
@@ -270,6 +293,10 @@ export function keyAggregate(
270
293
  }
271
294
  aggPublicKey = aggPublicKey.add(Pi.multiply(keyAggCoeffInternal(publicKeys[i], pk2, L)));
272
295
  }
296
+ // BIP327 KeyAggInternal: "Fail if is_infinite(Q)". Computationally unreachable for
297
+ // hash-derived coefficients, but the spec mandates the explicit check before tweaking.
298
+ if (isZero(aggPublicKey))
299
+ throw new Error('keyAggregate: aggregate public key cannot be infinity');
273
300
  let gAcc = Fn.ONE;
274
301
  let tweakAcc = Fn.ZERO;
275
302
  // Apply tweaks
@@ -302,6 +329,9 @@ export function keyAggregate(
302
329
  * ```
303
330
  */
304
331
  export function keyAggExport(ctx: ReturnType<typeof keyAggregate>): TRet<Uint8Array> {
332
+ validateObject(ctx as Record<string, any>, {}, {}, 'ctx');
333
+ if (!(ctx.aggPublicKey instanceof Point))
334
+ throw new TypeError('"ctx.aggPublicKey" expected point, got type=' + typeof ctx.aggPublicKey);
305
335
  // BIP327 GetXonlyPubkey returns xbytes(Q), so this is the 32-byte x-only aggregate key
306
336
  // instead of the 33-byte compressed SEC1 form.
307
337
  return pointToBytes(ctx.aggPublicKey) as TRet<Uint8Array>;
@@ -342,6 +372,12 @@ const nonceHash = (
342
372
 
343
373
  /**
344
374
  * Generates a nonce pair (public and secret) for MuSig2 signing.
375
+ *
376
+ * SECURITY: The returned secret nonce MUST be used for exactly one partial signature. Keep one
377
+ * authoritative copy and atomically consume it when calling {@link Session.sign}. That method
378
+ * zeroes only the exact `Uint8Array` passed to it; clones, serialized values, database records, and
379
+ * snapshots remain live. Reusing a secret nonce in distinct sessions can reveal the secret key.
380
+ *
345
381
  * @param publicKey - individual public key of the signer
346
382
  * @param secretKey - optional secret key, mixed in to blind the randomness source
347
383
  * @param aggPublicKey - aggregate public key of all signers
@@ -502,6 +538,7 @@ export class Session {
502
538
  tweaks: Uint8Array[] = [],
503
539
  isXonly: boolean[] = []
504
540
  ) {
541
+ abytes(aggNonce, 66);
505
542
  abytesArray(publicKeys, 33);
506
543
  abytesArray(tweaks, 32);
507
544
  aXonly(isXonly);
@@ -520,7 +557,10 @@ export class Session {
520
557
  this.gAcc = gAcc;
521
558
  this.tweakAcc = tweakAcc;
522
559
  this.b = taggedInt('MuSig/noncecoef', aggNonce, pointToBytes(aggPublicKey), msg);
523
- const R = R1.add(R2.multiply(this.b));
560
+ // b and the nonce points are public session values, so the faster variable-time
561
+ // multiplication is safe here; it also matches the reference point_mul, which
562
+ // maps a (negligible-probability) zero coefficient to infinity instead of failing.
563
+ const R = R1.add(R2.multiplyUnsafe(this.b));
524
564
  this.R = isZero(R) ? Point.BASE : R;
525
565
  this.e = taggedInt('BIP0340/challenge', pointToBytes(this.R), pointToBytes(aggPublicKey), msg);
526
566
  this.tweaks = tweaks.map((t) => Uint8Array.from(t));
@@ -555,20 +595,28 @@ export class Session {
555
595
  // BIP327 PartialSigVerifyInternal: `Let s = int(psig); fail if s >= n`, so s=0 must stay
556
596
  // in the public verification equation and return false on mismatch instead of throwing.
557
597
  const { R1, R2 } = PubNonce.decode(publicNonce);
558
- const Re_s_ = R1.add(R2.multiply(b));
598
+ // Verification only handles public data (nonces, pubkeys, hash-derived scalars),
599
+ // so the faster variable-time multiplications are safe here; they also match the
600
+ // reference point_mul, which maps zero scalars to infinity instead of failing.
601
+ const Re_s_ = R1.add(R2.multiplyUnsafe(b));
559
602
  const Re_s = hasEven(R.y) ? Re_s_ : Re_s_.negate();
560
603
  const P = Point.fromBytes(publicKey);
561
604
  const a = this.getSessionKeyAggCoeff(P);
562
- const g = Fn.mul(evenScalar(Q, 1n), gAcc);
605
+ const g = Fn.mul(evenScalar(Q, _1n), gAcc);
563
606
  const left = Point.BASE.multiplyUnsafe(s);
564
- const right = Re_s.add(P.multiply(Fn.mul(e, Fn.mul(a, g))));
607
+ const right = Re_s.add(P.multiplyUnsafe(Fn.mul(e, Fn.mul(a, g))));
565
608
  return left.equals(right);
566
609
  }
567
610
 
568
611
  /**
569
612
  * Generates a partial signature for a given message, secret nonce,
570
613
  * secret key, and session context.
571
- * @param secretNonce - secret nonce for this signing session; it is zeroed after use
614
+ *
615
+ * SECURITY: `secretNonce` MUST be used exactly once. This method zeroes the first 64 bytes of the
616
+ * supplied array, including when later validation fails, but cannot erase copies or persisted
617
+ * representations. Reusing those nonce scalars in a distinct session can reveal the secret key.
618
+ *
619
+ * @param secretNonce - sole authoritative secret-nonce buffer for this signing session
572
620
  * @param secret - secret key of the signer
573
621
  * @param fastSign - if `true`, skip the self-verification pass
574
622
  * @returns The partial signature (Uint8Array).
@@ -587,7 +635,7 @@ export class Session {
587
635
  // Modifying input arguments is pretty bad.
588
636
  secretNonce.fill(0, 0, 64);
589
637
  if (!Fn.isValid(k1_)) throw new Error('wrong k1');
590
- if (!Fn.isValid(k2_)) throw new Error('wrong k1');
638
+ if (!Fn.isValid(k2_)) throw new Error('wrong k2');
591
639
  const k1 = evenScalar(R, k1_);
592
640
  const k2 = evenScalar(R, k2_);
593
641
  const d_ = Fn.fromBytes(secret);
@@ -596,7 +644,7 @@ export class Session {
596
644
  const pk = P.toBytes(true);
597
645
  if (!equalBytes(pk, originalPk)) throw new Error('Public key does not match nonceGen argument');
598
646
  const a = this.getSessionKeyAggCoeff(P);
599
- const g = evenScalar(Q, 1n);
647
+ const g = evenScalar(Q, _1n);
600
648
  const d = Fn.mul(g, Fn.mul(gAcc, d_));
601
649
  /// k1 + (b*k2) + (e*a*d)
602
650
  const s = Fn.add(k1, Fn.add(Fn.mul(b, k2), Fn.mul(e, Fn.mul(a, d))));
@@ -643,24 +691,28 @@ export class Session {
643
691
  }
644
692
  /**
645
693
  * Aggregates partial signatures from multiple signers into a single final signature.
646
- * @param partialSigs - partial signatures from each signer
694
+ * @param partialSigs - exactly one positional partial signature per session participant
647
695
  * @returns The final aggregate signature (Uint8Array).
648
696
  * @throws If the input is invalid, such as wrong array sizes or malformed
649
697
  * signatures. {@link Error}
650
698
  */
651
699
  partialSigAgg(partialSigs: TArg<Uint8Array[]>): TRet<Uint8Array> {
652
700
  abytesArray(partialSigs, 32);
653
- // BIP327 PartialSigAgg is defined for a non-empty psig_1..u list tied to this session_ctx;
654
- // [] is not a valid aggregate-signature input even though the sum starts from zero.
655
- if (partialSigs.length < 1) throw new RangeError('partialSigs.length must be >= 1');
701
+ // BIP327 PartialSigAgg consumes psig_1..u for the same u signers in session_ctx. Accepting
702
+ // fewer or more scalars would return a signature-shaped value for a different equation.
703
+ if (partialSigs.length !== this.publicKeys.length)
704
+ throw new RangeError(
705
+ `partialSigs.length=${partialSigs.length} must equal ` +
706
+ `participant count=${this.publicKeys.length}`
707
+ );
656
708
  const { Q, tweakAcc, R, e } = this;
657
- let s = 0n;
709
+ let s = _0n;
658
710
  for (let i = 0; i < partialSigs.length; i++) {
659
711
  const si = Fn.fromBytes(partialSigs[i], true);
660
712
  if (!Fn.isValid(si)) throw new InvalidContributionErr(i, 'psig');
661
713
  s = Fn.add(s, si);
662
714
  }
663
- const g = evenScalar(Q, 1n);
715
+ const g = evenScalar(Q, _1n);
664
716
  s = Fn.add(s, Fn.mul(e, Fn.mul(g, tweakAcc))); // s + e * g * tweakAcc
665
717
  return concatBytes(pointToBytes(R), Fn.toBytes(s)) as TRet<Uint8Array>;
666
718
  }