@scure/btc-signer 2.2.0 → 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/script.js CHANGED
@@ -1,11 +1,16 @@
1
1
  import * as P from 'micro-packed';
2
- import { isBytes, reverseObject } from "./utils.js";
2
+ import { aarray, abytes, isBytes, reverseObject, } from "./utils.js";
3
3
  /**
4
4
  * Maximum byte size allowed for a single pushed script element.
5
5
  * BIP 342 keeps this 520-byte stack-element limit even though tapscript removes
6
6
  * the old 10,000-byte overall script-size cap.
7
7
  */
8
8
  export const MAX_SCRIPT_BYTE_LENGTH = 520;
9
+ // Be friendly to bad ECMAScript parsers by not using bigint literals.
10
+ // prettier-ignore
11
+ const _0n = /* @__PURE__ */ BigInt(0), _1n = /* @__PURE__ */ BigInt(1), _2n = /* @__PURE__ */ BigInt(2), _8n = /* @__PURE__ */ BigInt(8);
12
+ const U8_MAX = /* @__PURE__ */ BigInt(0xff);
13
+ const COMPACT_DIRECT_MAX = /* @__PURE__ */ BigInt(0xfc);
9
14
  // prettier-ignore
10
15
  /**
11
16
  * Bitcoin Script opcode table.
@@ -75,13 +80,13 @@ export const OPNames = /* @__PURE__ */ (() => Object.freeze(reverseObject(OP)))(
75
80
  export function ScriptNum(bytesLimit = 6, forceMinimal = false) {
76
81
  return P.wrap({
77
82
  encodeStream: (w, value) => {
78
- if (value === 0n)
83
+ if (value === _0n)
79
84
  return;
80
85
  const neg = value < 0;
81
86
  const val = BigInt(value);
82
87
  const nums = [];
83
- for (let abs = neg ? -val : val; abs; abs >>= 8n)
84
- nums.push(Number(abs & 0xffn));
88
+ for (let abs = neg ? -val : val; abs; abs >>= _8n)
89
+ nums.push(Number(abs & U8_MAX));
85
90
  if (nums[nums.length - 1] >= 0x80)
86
91
  nums.push(neg ? 0x80 : 0);
87
92
  else if (neg)
@@ -93,24 +98,23 @@ export function ScriptNum(bytesLimit = 6, forceMinimal = false) {
93
98
  if (len > bytesLimit)
94
99
  throw new Error(`ScriptNum: number (${len}) bigger than limit=${bytesLimit}`);
95
100
  if (len === 0)
96
- return 0n;
101
+ return _0n;
102
+ // Read the payload once instead of peeking for the minimality check and
103
+ // then re-reading it byte-by-byte through the Reader.
104
+ const data = r.bytes(len);
97
105
  if (forceMinimal) {
98
- const data = r.bytes(len, true);
99
106
  // MSB is zero (without sign bit) -> not minimally encoded
100
- if ((data[data.length - 1] & 0x7f) === 0) {
107
+ if ((data[len - 1] & 0x7f) === 0) {
101
108
  // exception
102
- if (len <= 1 || (data[data.length - 2] & 0x80) === 0)
109
+ if (len <= 1 || (data[len - 2] & 0x80) === 0)
103
110
  throw new Error('Non-minimally encoded ScriptNum');
104
111
  }
105
112
  }
106
- let last = 0;
107
- let res = 0n;
108
- for (let i = 0; i < len; ++i) {
109
- last = r.byte();
110
- res |= BigInt(last) << (8n * BigInt(i));
111
- }
112
- if (last >= 0x80) {
113
- res &= (2n ** BigInt(len * 8) - 1n) >> 1n;
113
+ let res = _0n;
114
+ for (let i = 0; i < len; ++i)
115
+ res |= BigInt(data[i]) << (_8n * BigInt(i));
116
+ if (data[len - 1] >= 0x80) {
117
+ res &= (_2n ** BigInt(len * 8) - _1n) >> _1n;
114
118
  res = -res;
115
119
  }
116
120
  return res;
@@ -137,7 +141,9 @@ export function OpToNum(op, bytesLimit = 4, forceMinimal = true) {
137
141
  if (isBytes(op)) {
138
142
  try {
139
143
  const val = ScriptNum(bytesLimit, forceMinimal).decode(op);
140
- if (val > Number.MAX_SAFE_INTEGER)
144
+ // Symmetric safe-integer bound: large negative values would otherwise
145
+ // coerce through Number() with silent precision loss.
146
+ if (val > Number.MAX_SAFE_INTEGER || val < -Number.MAX_SAFE_INTEGER)
141
147
  return;
142
148
  return Number(val);
143
149
  }
@@ -192,11 +198,15 @@ export const scriptPushLen = (op, read) => {
192
198
  */
193
199
  export const Script = /* @__PURE__ */ (() => Object.freeze(P.wrap({
194
200
  encodeStream: (w, value) => {
201
+ aarray(value, 'value');
195
202
  for (let o of value) {
196
203
  if (typeof o === 'string') {
197
- if (OP[o] === undefined)
204
+ const op = OP[o];
205
+ // OP is a plain object, so inherited Object.prototype keys ('toString',
206
+ // 'constructor', ...) are not opcodes and must be rejected here too.
207
+ if (typeof op !== 'number')
198
208
  throw new Error(`Unknown opcode=${o}`);
199
- w.byte(OP[o]);
209
+ w.byte(op);
200
210
  continue;
201
211
  }
202
212
  else if (typeof o === 'number') {
@@ -218,8 +228,7 @@ export const Script = /* @__PURE__ */ (() => Object.freeze(P.wrap({
218
228
  // Encode big numbers
219
229
  if (typeof o === 'number')
220
230
  o = ScriptNum().encode(BigInt(o));
221
- if (!isBytes(o))
222
- throw new Error(`Wrong Script OP=${o} (${typeof o})`);
231
+ abytes(o, undefined, 'value');
223
232
  // Bytes
224
233
  const len = o.length;
225
234
  if (len < OP.PUSHDATA1)
@@ -269,13 +278,6 @@ export const Script = /* @__PURE__ */ (() => Object.freeze(P.wrap({
269
278
  return out;
270
279
  },
271
280
  })))();
272
- // BTC specific variable length integer encoding
273
- // https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer
274
- const CSLimits = {
275
- 0xfd: [0xfd, 2, 253n, 65535n],
276
- 0xfe: [0xfe, 4, 65536n, 4294967295n],
277
- 0xff: [0xff, 8, 4294967296n, 18446744073709551615n],
278
- };
279
281
  /**
280
282
  * Bitcoin CompactSize integer coder.
281
283
  * @example
@@ -284,37 +286,48 @@ const CSLimits = {
284
286
  * CompactSize.encode(1n);
285
287
  * ```
286
288
  */
287
- export const CompactSize = /* @__PURE__ */ (() => Object.freeze(P.wrap({
288
- encodeStream: (w, value) => {
289
- if (typeof value === 'number')
290
- value = BigInt(value);
291
- if (0n <= value && value <= 252n)
292
- return w.byte(Number(value));
293
- for (const [flag, bytes, start, stop] of Object.values(CSLimits)) {
294
- if (start > value || value > stop)
295
- continue;
296
- w.byte(flag);
289
+ export const CompactSize = /* @__PURE__ */ (() => {
290
+ // BTC specific variable length integer encoding
291
+ // https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer
292
+ const limits = {
293
+ 0xfd: [0xfd, 2, BigInt(0xfd), BigInt(0xffff)],
294
+ 0xfe: [0xfe, 4, BigInt(0x10000), BigInt(0xffffffff)],
295
+ 0xff: [0xff, 8, BigInt(0x100000000), BigInt('0xffffffffffffffff')],
296
+ };
297
+ // Hoisted: Object.values() would otherwise allocate a fresh array on every encode.
298
+ const limitsList = Object.values(limits);
299
+ return Object.freeze(P.wrap({
300
+ encodeStream: (w, value) => {
301
+ if (typeof value === 'number')
302
+ value = BigInt(value);
303
+ if (_0n <= value && value <= COMPACT_DIRECT_MAX)
304
+ return w.byte(Number(value));
305
+ for (const [flag, bytes, start, stop] of limitsList) {
306
+ if (start > value || value > stop)
307
+ continue;
308
+ w.byte(flag);
309
+ for (let i = 0; i < bytes; i++)
310
+ w.byte(Number((value >> (_8n * BigInt(i))) & U8_MAX));
311
+ return;
312
+ }
313
+ throw w.err(`VarInt too big: ${value}`);
314
+ },
315
+ decodeStream: (r) => {
316
+ const b0 = r.byte();
317
+ if (b0 <= 0xfc)
318
+ return BigInt(b0);
319
+ const [_, bytes, start] = limits[b0];
320
+ let num = _0n;
297
321
  for (let i = 0; i < bytes; i++)
298
- w.byte(Number((value >> (8n * BigInt(i))) & 0xffn));
299
- return;
300
- }
301
- throw w.err(`VarInt too big: ${value}`);
302
- },
303
- decodeStream: (r) => {
304
- const b0 = r.byte();
305
- if (b0 <= 0xfc)
306
- return BigInt(b0);
307
- const [_, bytes, start] = CSLimits[b0];
308
- let num = 0n;
309
- for (let i = 0; i < bytes; i++)
310
- num |= BigInt(r.byte()) << (8n * BigInt(i));
311
- // BIP 152 / BIP 174: CompactSize fields must use the shortest encoding,
312
- // so wider prefixes for smaller values are rejected here.
313
- if (num < start)
314
- throw r.err(`Wrong CompactSize(${8 * bytes})`);
315
- return num;
316
- },
317
- })))();
322
+ num |= BigInt(r.byte()) << (_8n * BigInt(i));
323
+ // BIP 152 / BIP 174: CompactSize fields must use the shortest encoding,
324
+ // so wider prefixes for smaller values are rejected here.
325
+ if (num < start)
326
+ throw r.err(`Wrong CompactSize(${8 * bytes})`);
327
+ return num;
328
+ },
329
+ }));
330
+ })();
318
331
  // Same thing, but in number instead of bigint. Checks for safe integer inside
319
332
  /**
320
333
  * CompactSize coder that decodes into JavaScript numbers.
@@ -470,4 +483,3 @@ export const RawOldTx = /* @__PURE__ */ (() => Object.freeze(P.struct({
470
483
  outputs: BTCArray(RawOutput),
471
484
  lockTime: P.U32LE,
472
485
  })))();
473
- //# sourceMappingURL=script.js.map
@@ -0,0 +1,69 @@
1
+ import { secp256k1 as secp } from '@noble/curves/secp256k1.js';
2
+ import { hex } from '@scure/base';
3
+ import * as btc from './index.ts';
4
+
5
+ const privKey1 = hex.decode('0101010101010101010101010101010101010101010101010101010101010101');
6
+ const P1 = secp.getPublicKey(privKey1, true);
7
+
8
+ const wpkh = btc.p2wpkh(P1);
9
+
10
+ const tx = new btc.Transaction();
11
+
12
+ // Basic input test
13
+ tx.addInput({
14
+ txid: hex.decode('0af50a00a22f74ece24c12cd667c290d3a35d48124a69f4082700589172a3aa2'),
15
+ index: 0,
16
+ ...wpkh,
17
+ finalScriptSig: Uint8Array.of(),
18
+ sequence: 1,
19
+ });
20
+
21
+ // Doesn't force any fields on input addition (only on sign)
22
+ tx.addInput({
23
+ sequence: 1,
24
+ });
25
+
26
+ tx.updateInput(0, {
27
+ sequence: 1,
28
+ });
29
+
30
+ const nonWitnessUtxo =
31
+ '0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000';
32
+ const nonWitnessUtxoB = hex.decode(nonWitnessUtxo);
33
+
34
+ tx.updateInput(0, { nonWitnessUtxo: nonWitnessUtxo });
35
+ tx.updateInput(0, { nonWitnessUtxo: nonWitnessUtxoB });
36
+ tx.addInput({
37
+ txid: hex.decode('0af50a00a22f74ece24c12cd667c290d3a35d48124a69f4082700589172a3aa2'),
38
+ index: 0,
39
+ nonWitnessUtxo: nonWitnessUtxo,
40
+ });
41
+
42
+ tx.addInput({
43
+ txid: hex.decode('0af50a00a22f74ece24c12cd667c290d3a35d48124a69f4082700589172a3aa2'),
44
+ index: 0,
45
+ nonWitnessUtxo: nonWitnessUtxoB,
46
+ });
47
+
48
+ // Should fail!
49
+ // tx.updateInput(0, {
50
+ // nonWitnessUtxo: 1,
51
+ // });
52
+ // Outputs
53
+ tx.addOutput({ amount: BigInt(123) });
54
+ // should fail
55
+ // tx.updateOutput(0, { amount: '1' });
56
+ // tx.updateOutput(0, { amount: 1 });
57
+ // should fail
58
+ // tx.addOutput({ amount: '123' });
59
+ // tx.addOutput({ amount: 123 });
60
+
61
+ for (let i = 0; i < tx.inputsLength; i++) {
62
+ // @ts-ignore
63
+ console.log('I', tx.getInput(i));
64
+ }
65
+
66
+ for (let i = 0; i < tx.outputsLength; i++) {
67
+ // @ts-ignore
68
+ console.log('O', tx.getOutput(i));
69
+ }
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.
@@ -33,6 +34,15 @@ export type DetNonce = {
33
34
  /** Partial signature produced after combining all participant data. */
34
35
  partialSig: Uint8Array;
35
36
  };
37
+ /** MuSig2 key aggregation context used by signing sessions. */
38
+ export type KeyAggregate = {
39
+ /** Aggregate public key before x-only export. */
40
+ aggPublicKey: WeierstrassPoint<bigint>;
41
+ /** Accumulated sign from x-only tweaks. */
42
+ gAcc: bigint;
43
+ /** Accumulated tweak scalar. */
44
+ tweakAcc: bigint;
45
+ };
36
46
  /**
37
47
  * Represents an error indicating an invalid contribution from a signer.
38
48
  * This allows pointing out which participant is malicious and what specifically is wrong.
@@ -75,6 +85,9 @@ const PUBKEY_LEN = /* @__PURE__ */ (() => secp256k1.lengths.publicKey!)();
75
85
  // BIP327 uses bytes(33, 0) both as cbytes_ext(Point.ZERO) for infinity and as GetSecondKey's
76
86
  // "no second distinct key" sentinel, so this all-zero compressed slot is intentionally out-of-band.
77
87
  const ZERO = /* @__PURE__ */ new Uint8Array(PUBKEY_LEN); // Compressed zero point
88
+ // Be friendly to bad ECMAScript parsers by not using bigint literals.
89
+ // prettier-ignore
90
+ const _0n = /* @__PURE__ */ BigInt(0), _1n = /* @__PURE__ */ BigInt(1);
78
91
 
79
92
  // Encoding
80
93
  // TODO: re-use in PSBT?
@@ -89,7 +102,7 @@ const compressed = /* @__PURE__ */ (() =>
89
102
  // nonzero scalars in [1, n); tweak scalars use different validation because 0 is allowed there.
90
103
  const scalar = /* @__PURE__ */ (() =>
91
104
  P.validate(P.U256BE, (n) => {
92
- aInRange('n', n, 1n, Fn.ORDER);
105
+ aInRange('n', n, _1n, Fn.ORDER);
93
106
  return n;
94
107
  }))();
95
108
  // Shared for both per-signer pubnonce bytes and aggregate aggnonce bytes. Because it accepts
@@ -216,7 +229,7 @@ function keyAggCoeffInternal(
216
229
  // pk2 is the all-zero sentinel from GetSecondKey, so every real signer key still hashes.
217
230
  abytes(publicKey1, PUBKEY_LEN);
218
231
  abytes(publicKey2, PUBKEY_LEN);
219
- if (equalBytes(publicKey1, publicKey2)) return 1n;
232
+ if (equalBytes(publicKey1, publicKey2)) return _1n;
220
233
  return taggedInt('KeyAgg coefficient', L, publicKey1);
221
234
  }
222
235
 
@@ -248,7 +261,7 @@ export function keyAggregate(
248
261
  publicKeys: TArg<Uint8Array[]>,
249
262
  tweaks: TArg<Uint8Array[]> = [],
250
263
  isXonly: boolean[] = []
251
- ) {
264
+ ): KeyAggregate {
252
265
  // BIP327 KeyAgg inputs require `0 < u < 2^32`, and ApplyTweak consumes a one-for-one
253
266
  // list of boolean tweak modes; callers should enforce that public contract here.
254
267
  abytesArray(publicKeys, PUBKEY_LEN);
@@ -270,6 +283,10 @@ export function keyAggregate(
270
283
  }
271
284
  aggPublicKey = aggPublicKey.add(Pi.multiply(keyAggCoeffInternal(publicKeys[i], pk2, L)));
272
285
  }
286
+ // BIP327 KeyAggInternal: "Fail if is_infinite(Q)". Computationally unreachable for
287
+ // hash-derived coefficients, but the spec mandates the explicit check before tweaking.
288
+ if (isZero(aggPublicKey))
289
+ throw new Error('keyAggregate: aggregate public key cannot be infinity');
273
290
  let gAcc = Fn.ONE;
274
291
  let tweakAcc = Fn.ZERO;
275
292
  // Apply tweaks
@@ -302,6 +319,9 @@ export function keyAggregate(
302
319
  * ```
303
320
  */
304
321
  export function keyAggExport(ctx: ReturnType<typeof keyAggregate>): TRet<Uint8Array> {
322
+ validateObject(ctx as Record<string, any>, {}, {}, 'ctx');
323
+ if (!(ctx.aggPublicKey instanceof Point))
324
+ throw new TypeError('"ctx.aggPublicKey" expected point, got type=' + typeof ctx.aggPublicKey);
305
325
  // BIP327 GetXonlyPubkey returns xbytes(Q), so this is the 32-byte x-only aggregate key
306
326
  // instead of the 33-byte compressed SEC1 form.
307
327
  return pointToBytes(ctx.aggPublicKey) as TRet<Uint8Array>;
@@ -502,6 +522,7 @@ export class Session {
502
522
  tweaks: Uint8Array[] = [],
503
523
  isXonly: boolean[] = []
504
524
  ) {
525
+ abytes(aggNonce, 66);
505
526
  abytesArray(publicKeys, 33);
506
527
  abytesArray(tweaks, 32);
507
528
  aXonly(isXonly);
@@ -520,7 +541,10 @@ export class Session {
520
541
  this.gAcc = gAcc;
521
542
  this.tweakAcc = tweakAcc;
522
543
  this.b = taggedInt('MuSig/noncecoef', aggNonce, pointToBytes(aggPublicKey), msg);
523
- const R = R1.add(R2.multiply(this.b));
544
+ // b and the nonce points are public session values, so the faster variable-time
545
+ // multiplication is safe here; it also matches the reference point_mul, which
546
+ // maps a (negligible-probability) zero coefficient to infinity instead of failing.
547
+ const R = R1.add(R2.multiplyUnsafe(this.b));
524
548
  this.R = isZero(R) ? Point.BASE : R;
525
549
  this.e = taggedInt('BIP0340/challenge', pointToBytes(this.R), pointToBytes(aggPublicKey), msg);
526
550
  this.tweaks = tweaks.map((t) => Uint8Array.from(t));
@@ -555,13 +579,16 @@ export class Session {
555
579
  // BIP327 PartialSigVerifyInternal: `Let s = int(psig); fail if s >= n`, so s=0 must stay
556
580
  // in the public verification equation and return false on mismatch instead of throwing.
557
581
  const { R1, R2 } = PubNonce.decode(publicNonce);
558
- const Re_s_ = R1.add(R2.multiply(b));
582
+ // Verification only handles public data (nonces, pubkeys, hash-derived scalars),
583
+ // so the faster variable-time multiplications are safe here; they also match the
584
+ // reference point_mul, which maps zero scalars to infinity instead of failing.
585
+ const Re_s_ = R1.add(R2.multiplyUnsafe(b));
559
586
  const Re_s = hasEven(R.y) ? Re_s_ : Re_s_.negate();
560
587
  const P = Point.fromBytes(publicKey);
561
588
  const a = this.getSessionKeyAggCoeff(P);
562
- const g = Fn.mul(evenScalar(Q, 1n), gAcc);
589
+ const g = Fn.mul(evenScalar(Q, _1n), gAcc);
563
590
  const left = Point.BASE.multiplyUnsafe(s);
564
- const right = Re_s.add(P.multiply(Fn.mul(e, Fn.mul(a, g))));
591
+ const right = Re_s.add(P.multiplyUnsafe(Fn.mul(e, Fn.mul(a, g))));
565
592
  return left.equals(right);
566
593
  }
567
594
 
@@ -587,7 +614,7 @@ export class Session {
587
614
  // Modifying input arguments is pretty bad.
588
615
  secretNonce.fill(0, 0, 64);
589
616
  if (!Fn.isValid(k1_)) throw new Error('wrong k1');
590
- if (!Fn.isValid(k2_)) throw new Error('wrong k1');
617
+ if (!Fn.isValid(k2_)) throw new Error('wrong k2');
591
618
  const k1 = evenScalar(R, k1_);
592
619
  const k2 = evenScalar(R, k2_);
593
620
  const d_ = Fn.fromBytes(secret);
@@ -596,7 +623,7 @@ export class Session {
596
623
  const pk = P.toBytes(true);
597
624
  if (!equalBytes(pk, originalPk)) throw new Error('Public key does not match nonceGen argument');
598
625
  const a = this.getSessionKeyAggCoeff(P);
599
- const g = evenScalar(Q, 1n);
626
+ const g = evenScalar(Q, _1n);
600
627
  const d = Fn.mul(g, Fn.mul(gAcc, d_));
601
628
  /// k1 + (b*k2) + (e*a*d)
602
629
  const s = Fn.add(k1, Fn.add(Fn.mul(b, k2), Fn.mul(e, Fn.mul(a, d))));
@@ -654,13 +681,13 @@ export class Session {
654
681
  // [] is not a valid aggregate-signature input even though the sum starts from zero.
655
682
  if (partialSigs.length < 1) throw new RangeError('partialSigs.length must be >= 1');
656
683
  const { Q, tweakAcc, R, e } = this;
657
- let s = 0n;
684
+ let s = _0n;
658
685
  for (let i = 0; i < partialSigs.length; i++) {
659
686
  const si = Fn.fromBytes(partialSigs[i], true);
660
687
  if (!Fn.isValid(si)) throw new InvalidContributionErr(i, 'psig');
661
688
  s = Fn.add(s, si);
662
689
  }
663
- const g = evenScalar(Q, 1n);
690
+ const g = evenScalar(Q, _1n);
664
691
  s = Fn.add(s, Fn.mul(e, Fn.mul(g, tweakAcc))); // s + e * g * tweakAcc
665
692
  return concatBytes(pointToBytes(R), Fn.toBytes(s)) as TRet<Uint8Array>;
666
693
  }