@scure/btc-signer 2.3.0 → 2.4.1

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/payment.js CHANGED
@@ -191,7 +191,7 @@ const OutTRNS = {
191
191
  // BIP342 "Using a k-of-k script for every combination" documents the shape
192
192
  // `<pubkey_1> CHECKSIGVERIFY ... <pubkey_n> CHECKSIG`; this matcher only
193
193
  // classifies that embedded-pubkey form, so bare CHECKSIG stays unknown.
194
- if (!pubkeys.length)
194
+ if (!pubkeys.length || pubkeys.length > 999)
195
195
  return;
196
196
  return { type: 'tr_ns', pubkeys };
197
197
  },
@@ -322,20 +322,40 @@ export const OutScript = /* @__PURE__ */ (() => Object.freeze(P.validate(_OutScr
322
322
  throw new Error('OutScript/multisig: invalid params');
323
323
  }
324
324
  if (i.type === 'tr_ns' || i.type === 'tr_ms') {
325
+ const n = i.pubkeys.length;
326
+ // BIP342 keeps the 1,000-element stack limit. Both supported tapscript multisig forms
327
+ // start with n signatures and then push a pubkey, so n must stay at or below 999.
328
+ if (n > 999)
329
+ throw new Error(`OutScript/${i.type}: invalid params`);
325
330
  for (const p of i.pubkeys)
326
331
  if (!isValidPubkey(p, u.PubT.schnorr))
327
332
  throw new Error(`OutScript/${i.type}: wrong pubkey`);
328
333
  }
329
334
  if (i.type === 'tr_ms') {
330
335
  const n = i.pubkeys.length;
331
- // BIP 342 keeps the 1000-element stack limit. This CHECKSIG/CHECKSIGADD form
332
- // momentarily has n witness items plus one pushed pubkey on the stack, so n must stay <= 999.
333
336
  anumber(i.m, 'm');
334
- if (i.m <= 0 || n > 999 || i.m > n)
337
+ if (i.m <= 0 || i.m > n)
335
338
  throw new Error('OutScript/tr_ms: invalid params');
336
339
  }
337
340
  return i;
338
341
  })))();
342
+ // Internal raw-aware boundary for actual scriptPubKeys and P2SH-nested witness programs. Generic
343
+ // OutScript remains semantic because witnessScript and tapscript children may use the same opcodes
344
+ // without claiming BIP141 witness-program framing.
345
+ export const _WitnessOutScript = /* @__PURE__ */ (() => Object.freeze(P.wrap({
346
+ encodeStream: (w, value) => OutScript.encodeStream(w, value),
347
+ decodeStream: (r) => {
348
+ const raw = r.bytes(r.leftBytes);
349
+ const out = OutScript.decode(raw);
350
+ // Script.decode normalizes push opcodes, but BIP141 recognition requires the exact direct
351
+ // push. Reject only when this explicit outer-program boundary is selected by the caller.
352
+ if (out &&
353
+ (out.type === 'p2a' || out.type === 'wpkh' || out.type === 'wsh' || out.type === 'tr') &&
354
+ !u.equalBytes(raw, OutScript.encode(out)))
355
+ throw new Error('OutScript: non-canonical witness program');
356
+ return out;
357
+ },
358
+ })))();
339
359
  // Basic sanity check for scripts
340
360
  function checkWSH(s, witnessScript) {
341
361
  if (!u.equalBytes(s.hash, u.sha256(witnessScript)))
@@ -370,7 +390,7 @@ export function checkScript(script, redeemScript, witnessScript) {
370
390
  let hasWsh = false;
371
391
  let r = undefined;
372
392
  if (script) {
373
- const s = OutScript.decode(script);
393
+ const s = _WitnessOutScript.decode(script);
374
394
  // BIP174 Data Signers Check For bullets: provided redeemScript must match
375
395
  // the scriptPubKey, and provided witnessScript must match the scriptPubKey
376
396
  // or redeemScript instead of being silently ignored as stray metadata.
@@ -382,7 +402,7 @@ export function checkScript(script, redeemScript, witnessScript) {
382
402
  throw new Error('checkScript: redeemScript without P2SH');
383
403
  if (!u.equalBytes(s.hash, u.hash160(redeemScript)))
384
404
  throw new Error('checkScript: sh wrong redeemScript hash');
385
- r = OutScript.decode(redeemScript);
405
+ r = _WitnessOutScript.decode(redeemScript);
386
406
  if (r?.type === 'tr' || r?.type === 'tr_ns' || r?.type === 'tr_ms')
387
407
  throw new Error(`checkScript: P2${r.type} cannot be wrapped in P2SH`);
388
408
  // Not sure if this unspendable, but we cannot represent this via PSBT
@@ -397,7 +417,7 @@ export function checkScript(script, redeemScript, witnessScript) {
397
417
  }
398
418
  if (redeemScript) {
399
419
  if (r === undefined)
400
- r = OutScript.decode(redeemScript);
420
+ r = _WitnessOutScript.decode(redeemScript);
401
421
  if (r?.type === 'wsh') {
402
422
  hasWsh = true;
403
423
  if (witnessScript)
@@ -407,12 +427,21 @@ export function checkScript(script, redeemScript, witnessScript) {
407
427
  if (witnessScript && !hasWsh)
408
428
  throw new Error('checkScript: witnessScript without P2WSH');
409
429
  }
410
- function uniqPubkey(pubkeys) {
430
+ function uniqPubkey(pubkeys, type) {
411
431
  const map = {};
412
432
  for (const pub of pubkeys) {
413
- // Exact-byte duplicate filter only: BIP383 valid vectors still permit the
414
- // same point to appear in compressed and uncompressed SEC1 form in multi().
415
- const key = hex.encode(pub);
433
+ u.validatePubkey(pub, type);
434
+ let normalized = pub;
435
+ if (type === u.PubT.ecdsa && pub.length === 65) {
436
+ // Compressed and uncompressed SEC1 encodings can represent the same curve point. Normalize
437
+ // valid uncompressed keys before comparing so one signer cannot occupy multiple threshold
438
+ // slots merely by changing the serialization. `allowSamePubkeys` remains the explicit
439
+ // compatibility escape hatch for callers that intentionally construct duplicate-key scripts.
440
+ normalized = new Uint8Array(33);
441
+ normalized[0] = 2 | (pub[64] & 1);
442
+ normalized.set(pub.subarray(1, 33), 1);
443
+ }
444
+ const key = hex.encode(normalized);
416
445
  if (map[key])
417
446
  throw new Error(`Multisig: non-uniq pubkey: ${pubkeys.map(hex.encode)}`);
418
447
  map[key] = true;
@@ -466,10 +495,23 @@ export const p2pkh = (publicKey, network = NETWORK) => {
466
495
  hash,
467
496
  };
468
497
  };
498
+ const checkCanonicalScript = (script, name, allowNonCanonicalScript) => {
499
+ if (allowNonCanonicalScript)
500
+ return;
501
+ const decoded = Script.decode(script);
502
+ const nonMinimalNumber = decoded.some((op) => u.isBytes(op) && op.length === 1 && ((1 <= op[0] && op[0] <= 16) || op[0] === 0x81));
503
+ // Core's default MINIMALDATA policy rejects these spends from its mempool, so address-producing
504
+ // helpers require an explicit opt-in even though the original bytes remain consensus-valid.
505
+ if (nonMinimalNumber || !u.equalBytes(script, Script.encode(decoded))) {
506
+ const wrapper = name === 'redeemScript' ? 'P2SH' : 'P2WSH';
507
+ throw new Error(`${wrapper}: non-canonical ${name}`);
508
+ }
509
+ };
469
510
  /**
470
511
  * Wraps a child script inside P2SH.
471
512
  * @param child - child payment descriptor to wrap
472
513
  * @param network - address network parameters
514
+ * @param allowNonCanonicalScript - whether to create an address for a non-minimal child script
473
515
  * @returns P2SH descriptor preserving witness metadata when present.
474
516
  * @throws If the wrapped script combination is invalid or unsupported. {@link Error}
475
517
  * @example
@@ -480,7 +522,7 @@ export const p2pkh = (publicKey, network = NETWORK) => {
480
522
  * p2sh(p2wsh(p2pk(hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'))));
481
523
  * ```
482
524
  */
483
- export const p2sh = (child, network = NETWORK) => {
525
+ export const p2sh = (child, network = NETWORK, allowNonCanonicalScript = false) => {
484
526
  u.validateObject(child, {}, {}, 'child');
485
527
  // It is already tested inside noble-hashes and checkScript
486
528
  // BIP16 redeemScripts are pushed by scriptSig, so anything over the 520-byte pushed-element
@@ -491,6 +533,7 @@ export const p2sh = (child, network = NETWORK) => {
491
533
  throw new Error(`Wrong script: ${typeof c.script}, expected Uint8Array`);
492
534
  if (cs.length > MAX_SCRIPT_BYTE_LENGTH)
493
535
  throw new Error(`P2SH: redeemScript exceeds ${MAX_SCRIPT_BYTE_LENGTH}-byte push limit: len=${cs.length}`);
536
+ checkCanonicalScript(cs, 'redeemScript', allowNonCanonicalScript);
494
537
  const hash = u.hash160(cs);
495
538
  const out = { type: 'sh', hash };
496
539
  const script = OutScript.encode(out);
@@ -520,6 +563,7 @@ export const p2sh = (child, network = NETWORK) => {
520
563
  * Wraps a child script inside native SegWit P2WSH.
521
564
  * @param child - child payment descriptor to wrap
522
565
  * @param network - address network parameters
566
+ * @param allowNonCanonicalScript - whether to create an address for a non-minimal child script
523
567
  * @returns P2WSH descriptor.
524
568
  * @throws If the wrapped script combination is invalid or unsupported. {@link Error}
525
569
  * @example
@@ -530,7 +574,7 @@ export const p2sh = (child, network = NETWORK) => {
530
574
  * p2wsh(p2pk(hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798')));
531
575
  * ```
532
576
  */
533
- export const p2wsh = (child, network = NETWORK) => {
577
+ export const p2wsh = (child, network = NETWORK, allowNonCanonicalScript = false) => {
534
578
  u.validateObject(child, {}, {}, 'child');
535
579
  const cs = child.script;
536
580
  if (!u.isBytes(cs))
@@ -539,6 +583,7 @@ export const p2wsh = (child, network = NETWORK) => {
539
583
  // and that witnessScript is limited to 10,000 bytes, so larger wrapped scripts must reject.
540
584
  if (cs.length > 10000)
541
585
  throw new Error('P2WSH: witnessScript exceeds 10,000 bytes');
586
+ checkCanonicalScript(cs, 'witnessScript', allowNonCanonicalScript);
542
587
  const hash = u.sha256(cs);
543
588
  const script = OutScript.encode({ type: 'wsh', hash });
544
589
  checkScript(script, undefined, cs);
@@ -597,7 +642,7 @@ export const p2ms = (m, pubkeys, allowSamePubkeys = false) => {
597
642
  // BIP 11 only standardized bare multisig up to 3 keys; this helper still permits up to 16
598
643
  // because the same script shape is commonly wrapped by p2sh()/p2wsh() instead of used bare.
599
644
  if (!allowSamePubkeys)
600
- uniqPubkey(pubkeys);
645
+ uniqPubkey(pubkeys, u.PubT.ecdsa);
601
646
  return {
602
647
  type: 'ms',
603
648
  script: OutScript.encode({ type: 'ms', pubkeys, m }),
@@ -633,7 +678,7 @@ function checkTaprootScript(script, internalPubKey, allowUnknownOutputs = false,
633
678
  const outms = out;
634
679
  if (!allowUnknownOutputs && outms.pubkeys) {
635
680
  for (const p of outms.pubkeys) {
636
- if (u.equalBytes(p, u.TAPROOT_UNSPENDABLE_KEY))
681
+ if (u.equalBytes(p, u.taprootNumsKey()))
637
682
  throw new Error('Unspendable taproot key in leaf script');
638
683
  // It's likely a mistake at this point:
639
684
  // 1. p2tr(A, p2tr_ns(2, [A, B])) == p2tr(A, p2tr_pk(B)) (A or B key)
@@ -726,7 +771,13 @@ function taprootWalkTree(tree) {
726
771
  // Keep a stable left-to-right DFS leaf order when flattening the annotated tree.
727
772
  return [...taprootWalkTree(tree.left), ...taprootWalkTree(tree.right)];
728
773
  }
729
- function taprootHashTree(tree, internalPubKey, allowUnknownOutputs = false, customScripts) {
774
+ // BIP 341 control blocks can encode at most 128 sibling hashes.
775
+ const TAPROOT_MAX_DEPTH = 128;
776
+ function taprootHashTree(tree, internalPubKey, allowUnknownOutputs = false, customScripts, depth = 0) {
777
+ // Reject before inspecting or descending into the next node, which also bounds the
778
+ // recursion used by the path annotation and tree-flattening passes below.
779
+ if (depth > TAPROOT_MAX_DEPTH)
780
+ throw new RangeError(`P2TR: tree depth exceeds ${TAPROOT_MAX_DEPTH}`);
730
781
  if (tree === undefined)
731
782
  throw new Error('taprootHashTree: empty tree');
732
783
  if (!Array.isArray(tree) && !P.utils.isPlainObject(tree))
@@ -759,8 +810,8 @@ function taprootHashTree(tree, internalPubKey, allowUnknownOutputs = false, cust
759
810
  throw new Error('hashTree: non binary tree!');
760
811
  // branch
761
812
  // Both nodes should exist
762
- const left = taprootHashTree(tree[0], internalPubKey, allowUnknownOutputs, customScripts);
763
- const right = taprootHashTree(tree[1], internalPubKey, allowUnknownOutputs, customScripts);
813
+ const left = taprootHashTree(tree[0], internalPubKey, allowUnknownOutputs, customScripts, depth + 1);
814
+ const right = taprootHashTree(tree[1], internalPubKey, allowUnknownOutputs, customScripts, depth + 1);
764
815
  // BIP 341 sorts TapBranch child hashes lexicographically for hashing, but the original
765
816
  // left/right structure still determines the control-block sibling paths for each leaf.
766
817
  let [lH, rH] = [left.hash, right.hash];
@@ -805,7 +856,7 @@ export function p2tr(internalPubKey, tree, network = NETWORK, allowUnknownOutput
805
856
  throw new Error('p2tr: should have pubKey or scriptTree (or both)');
806
857
  const pubKey = typeof internalPubKey === 'string'
807
858
  ? hex.decode(internalPubKey)
808
- : internalPubKey || u.TAPROOT_UNSPENDABLE_KEY;
859
+ : (internalPubKey ?? u.taprootNumsKey());
809
860
  if (!isValidPubkey(pubKey, u.PubT.schnorr))
810
861
  throw new Error('p2tr: non-schnorr pubkey');
811
862
  if (tree) {
@@ -855,27 +906,47 @@ export function p2tr(internalPubKey, tree, network = NETWORK, allowUnknownOutput
855
906
  };
856
907
  }
857
908
  }
909
+ /** Maximum number of combinations materialized by one default helper call. */
910
+ export const MAX_COMBINATIONS = 4096;
911
+ const combinationCount = (n, m) => {
912
+ const k = Math.min(m, n - m);
913
+ let count = 1;
914
+ for (let i = 1; i <= k; i++) {
915
+ count = (count * (n - k + i)) / i;
916
+ if (!Number.isSafeInteger(count))
917
+ return Number.POSITIVE_INFINITY;
918
+ }
919
+ return count;
920
+ };
858
921
  // Returns all combinations of size M from lst
859
922
  /**
860
923
  * Returns all size-`m` combinations from a list.
861
924
  * @param m - size of each combination
862
925
  * @param list - input items to combine
926
+ * @param maxCombinations - maximum result rows to materialize
863
927
  * @returns Array of combinations.
864
928
  * @throws If the combination size or input list is invalid. {@link Error}
929
+ * @throws If the requested result exceeds the materialization limit. {@link RangeError}
865
930
  * @example
866
931
  * Enumerate all size-two subsets of a short list.
867
932
  * ```ts
868
933
  * combinations(2, [1, 2, 3]);
869
934
  * ```
870
935
  */
871
- export function combinations(m, list) {
936
+ export function combinations(m, list, maxCombinations = MAX_COMBINATIONS) {
872
937
  const res = [];
873
938
  if (!Array.isArray(list))
874
939
  throw new Error('combinations: lst arg should be array');
875
940
  const n = list.length;
876
941
  anumber(m, 'm');
942
+ anumber(maxCombinations, 'maxCombinations');
877
943
  if (m < 1 || m > n)
878
944
  throw new Error('combinations: m must satisfy 1 <= m <= lst.length');
945
+ if (maxCombinations < 1)
946
+ throw new Error('combinations: maxCombinations must be >= 1');
947
+ const count = combinationCount(n, m);
948
+ if (count > maxCombinations)
949
+ throw new RangeError(`combinations: C(${n}, ${m}) exceeds materialization limit=${maxCombinations}`);
879
950
  /*
880
951
  Basically works as M nested loops like:
881
952
  for (;idx[0]<lst.length;idx[0]++) for (idx[1]=idx[0]+1;idx[1]<lst.length;idx[1]++)
@@ -911,6 +982,7 @@ export function combinations(m, list) {
911
982
  * @param allowSamePubkeys - whether duplicate keys are allowed
912
983
  * @returns Array of taproot leaf descriptors.
913
984
  * @throws If the taproot multisig parameters are invalid. {@link Error}
985
+ * @throws If the requested leaf set exceeds the materialization limit. {@link RangeError}
914
986
  * @example
915
987
  * Build the leaf set for an M-of-N taproot `CHECKSIGVERIFY` policy.
916
988
  * ```ts
@@ -920,12 +992,29 @@ export function combinations(m, list) {
920
992
  * ```
921
993
  */
922
994
  export const p2tr_ns = (m, pubkeys, allowSamePubkeys = false) => {
923
- if (!allowSamePubkeys)
924
- uniqPubkey(pubkeys);
925
- return combinations(m, pubkeys).map((i) => ({
926
- type: 'tr_ns',
927
- script: OutScript.encode({ type: 'tr_ns', pubkeys: i }),
928
- }));
995
+ anumber(m, 'm');
996
+ if (m > 999)
997
+ throw new Error('OutScript/tr_ns: invalid params');
998
+ // Enforce the allocation bound before doing curve work on an attacker-controlled key list.
999
+ const keySets = combinations(m, pubkeys);
1000
+ if (allowSamePubkeys) {
1001
+ for (const pubkey of pubkeys)
1002
+ u.validatePubkey(pubkey, u.PubT.schnorr);
1003
+ }
1004
+ else
1005
+ uniqPubkey(pubkeys, u.PubT.schnorr);
1006
+ return keySets.map((keys) => {
1007
+ // Keys were validated once above. Encoding through OutScript here would repeat lift_x for
1008
+ // every key in every combination, turning a bounded leaf set into avoidable curve-level work.
1009
+ const ops = [];
1010
+ for (let i = 0; i < keys.length - 1; i++)
1011
+ ops.push(keys[i], 'CHECKSIGVERIFY');
1012
+ ops.push(keys[keys.length - 1], 'CHECKSIG');
1013
+ return {
1014
+ type: 'tr_ns',
1015
+ script: Script.encode(ops),
1016
+ };
1017
+ });
929
1018
  };
930
1019
  /**
931
1020
  * Builds a single-key taproot leaf script.
@@ -934,6 +1023,7 @@ export const p2tr_ns = (m, pubkeys, allowSamePubkeys = false) => {
934
1023
  * @param pubkey - Schnorr public key
935
1024
  * @returns Taproot single-key leaf descriptor.
936
1025
  * @throws If the taproot single-key leaf cannot be encoded. {@link Error}
1026
+ * @throws If the delegated leaf policy exceeds its supported range. {@link RangeError}
937
1027
  * @example
938
1028
  * Build a single-key tapscript leaf.
939
1029
  * ```ts
@@ -960,7 +1050,7 @@ export const p2tr_pk = (pubkey) => p2tr_ns(1, [pubkey], undefined)[0];
960
1050
  */
961
1051
  export function p2tr_ms(m, pubkeys, allowSamePubkeys = false) {
962
1052
  if (!allowSamePubkeys)
963
- uniqPubkey(pubkeys);
1053
+ uniqPubkey(pubkeys, u.PubT.schnorr);
964
1054
  return {
965
1055
  type: 'tr_ms',
966
1056
  script: OutScript.encode({ type: 'tr_ms', pubkeys, m }),
@@ -974,6 +1064,7 @@ export function p2tr_ms(m, pubkeys, allowSamePubkeys = false) {
974
1064
  * @param network - address network parameters
975
1065
  * @returns Encoded Bitcoin address.
976
1066
  * @throws If the requested address type is unknown. {@link Error}
1067
+ * @throws If a key-derived script value is outside its supported range. {@link RangeError}
977
1068
  * @example
978
1069
  * Pick the output type first, then derive the matching address from the private key.
979
1070
  * ```ts