@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.
package/src/payment.ts CHANGED
@@ -29,11 +29,18 @@ export type P2Ret = {
29
29
  };
30
30
 
31
31
  // Pay to Anchor (P2A)
32
+ // BIP433 Pay-to-Anchor witness program bytes; the scriptPubKey is `OP_1 <0x4e73>`.
33
+ const P2A_PROGRAM = /* @__PURE__ */ Uint8Array.from([0x4e, 0x73]);
32
34
  type OutP2AType = { type: 'p2a'; script: Bytes };
33
35
  const OutP2A: Coder<OptScript, OutP2AType | undefined> = {
34
36
  encode(from: TArg<ScriptType>): TRet<OutP2AType | undefined> {
35
37
  // BIP433 defines P2A as the exact OP_1 <0x4e73> scriptPubKey.
36
- if (from.length !== 2 || from[0] !== 1 || !u.isBytes(from[1]) || hex.encode(from[1]) !== '4e73')
38
+ if (
39
+ from.length !== 2 ||
40
+ from[0] !== 1 ||
41
+ !u.isBytes(from[1]) ||
42
+ !u.equalBytes(from[1], P2A_PROGRAM)
43
+ )
37
44
  return;
38
45
  return { type: 'p2a', script: Script.encode(from) } as TRet<OutP2AType | undefined>;
39
46
  },
@@ -41,7 +48,7 @@ const OutP2A: Coder<OptScript, OutP2AType | undefined> = {
41
48
  if (to.type !== 'p2a') return;
42
49
  // The decoded object keeps `script` for caller convenience, but the `p2a`
43
50
  // tag always canonicalizes back to the fixed BIP433 script.
44
- return [1, hex.decode('4e73')] as TRet<OptScript>;
51
+ return [1, Uint8Array.from(P2A_PROGRAM)] as TRet<OptScript>;
45
52
  },
46
53
  };
47
54
 
@@ -87,8 +94,9 @@ const OutPKH: Coder<OptScript, OutPKHType | undefined> = {
87
94
  encode(from: TArg<ScriptType>): TRet<OutPKHType | undefined> {
88
95
  if (from.length !== 5 || from[0] !== 'DUP' || from[1] !== 'HASH160' || !u.isBytes(from[2]))
89
96
  return;
90
- // OutScript validates that the pushed HASH160 is exactly 20 bytes.
91
- // This child matcher only recognizes the canonical P2PKH opcode skeleton.
97
+ // Require the exact 20-byte HASH160 here so near-miss scripts fall through
98
+ // to OutUnknown instead of throwing in the OutScript validator on decode.
99
+ if (from[2].length !== 20) return;
92
100
  if (from[3] !== 'EQUALVERIFY' || from[4] !== 'CHECKSIG') return;
93
101
  return { type: 'pkh', hash: from[2] } as TRet<OutPKHType | undefined>;
94
102
  },
@@ -105,8 +113,9 @@ const OutSH: Coder<OptScript, OutSHType | undefined> = {
105
113
  encode(from: TArg<ScriptType>): TRet<OutSHType | undefined> {
106
114
  if (from.length !== 3 || from[0] !== 'HASH160' || !u.isBytes(from[1]) || from[2] !== 'EQUAL')
107
115
  return;
108
- // OutScript validates that the pushed HASH160 is exactly 20 bytes.
109
- // This child matcher only recognizes the canonical P2SH opcode skeleton.
116
+ // Require the exact 20-byte HASH160 here so near-miss scripts fall through
117
+ // to OutUnknown instead of throwing in the OutScript validator on decode.
118
+ if (from[1].length !== 20) return;
110
119
  return { type: 'sh', hash: from[1] } as TRet<OutSHType | undefined>;
111
120
  },
112
121
  // OutScript validates `sh.hash` before this branch emits the canonical
@@ -159,9 +168,12 @@ const OutMS: Coder<OptScript, OutMSType | undefined> = {
159
168
  if (typeof m !== 'number' || typeof n !== 'number') return;
160
169
  const pubkeys = from.slice(1, -2);
161
170
  if (n !== pubkeys.length) return;
162
- for (const pub of pubkeys) if (!u.isBytes(pub)) return;
163
- // OutScript validates pubkey encodings and `0 < m <= n <= 16`.
164
- // This child matcher only recognizes the canonical CHECKMULTISIG skeleton.
171
+ // Require valid ECDSA pubkeys and `0 < m <= n` here so near-miss
172
+ // CHECKMULTISIG scripts (garbage keys, degenerate 0-of-0) fall through to
173
+ // OutUnknown instead of throwing in the OutScript validator on decode.
174
+ // Script.decode only yields 0..16 for opcode numbers, so n <= 16 holds.
175
+ for (const pub of pubkeys) if (!u.isBytes(pub) || !isValidPubkey(pub, u.PubT.ecdsa)) return;
176
+ if (!Number.isSafeInteger(m) || m < 1 || m > n) return;
165
177
  // We don't need n here because it is the same as pubkeys.length.
166
178
  return { type: 'ms', m, pubkeys: pubkeys as Bytes[] } as TRet<OutMSType | undefined>;
167
179
  },
@@ -182,6 +194,10 @@ const OutTR: Coder<OptScript, OutTRType | undefined> = {
182
194
  // BIP341 assigns native taproot meaning only to version 1 with a 32-byte x-only program;
183
195
  // other OP_1 program lengths remain reserved future witness programs and should fall through.
184
196
  if (from.length !== 2 || from[0] !== 1 || !u.isBytes(from[1]) || from[1].length !== 32) return;
197
+ // A 32-byte v1 program with an off-curve x coordinate is a fundable but
198
+ // taproot-unspendable output; classify it as unknown instead of throwing
199
+ // in the OutScript validator on decode.
200
+ if (!isValidPubkey(from[1], u.PubT.schnorr)) return;
185
201
  return { type: 'tr', pubkey: from[1] } as TRet<OutTRType | undefined>;
186
202
  },
187
203
  // OutScript validates `tr.pubkey` before this branch emits the canonical
@@ -212,7 +228,7 @@ const OutTRNS: Coder<OptScript, OutTRNSType | undefined> = {
212
228
  // BIP342 "Using a k-of-k script for every combination" documents the shape
213
229
  // `<pubkey_1> CHECKSIGVERIFY ... <pubkey_n> CHECKSIG`; this matcher only
214
230
  // classifies that embedded-pubkey form, so bare CHECKSIG stays unknown.
215
- if (!pubkeys.length) return;
231
+ if (!pubkeys.length || pubkeys.length > 999) return;
216
232
  return { type: 'tr_ns', pubkeys } as TRet<OutTRNSType | undefined>;
217
233
  },
218
234
  decode: (to: TArg<OutTRNSType>): TRet<OptScript> => {
@@ -243,9 +259,13 @@ const OutTRMS: Coder<OptScript, OutTRMSType | undefined> = {
243
259
  if (elm !== (i === 1 ? 'CHECKSIG' : 'CHECKSIGADD')) return;
244
260
  continue;
245
261
  }
246
- if (!u.isBytes(elm)) return;
262
+ // Require actual Schnorr pubkeys here (same as tr_ns) so near-miss
263
+ // CHECKSIGADD scripts fall through to OutUnknown instead of throwing
264
+ // in the OutScript validator on decode.
265
+ if (!u.isBytes(elm) || !isValidPubkey(elm, u.PubT.schnorr)) return;
247
266
  pubkeys.push(elm);
248
267
  }
268
+ if (!Number.isSafeInteger(m) || m < 1 || m > pubkeys.length || pubkeys.length > 999) return;
249
269
  return { type: 'tr_ms', pubkeys, m } as TRet<OutTRMSType | undefined>;
250
270
  },
251
271
  decode: (to: TArg<OutTRMSType>): TRet<OptScript> => {
@@ -373,16 +393,18 @@ export const OutScript: TRet<
373
393
  if (i.m <= 0 || n > 16 || i.m > n) throw new Error('OutScript/multisig: invalid params');
374
394
  }
375
395
  if (i.type === 'tr_ns' || i.type === 'tr_ms') {
396
+ const n = i.pubkeys.length;
397
+ // BIP342 keeps the 1,000-element stack limit. Both supported tapscript multisig forms
398
+ // start with n signatures and then push a pubkey, so n must stay at or below 999.
399
+ if (n > 999) throw new Error(`OutScript/${i.type}: invalid params`);
376
400
  for (const p of i.pubkeys)
377
401
  if (!isValidPubkey(p, u.PubT.schnorr))
378
402
  throw new Error(`OutScript/${i.type}: wrong pubkey`);
379
403
  }
380
404
  if (i.type === 'tr_ms') {
381
405
  const n = i.pubkeys.length;
382
- // BIP 342 keeps the 1000-element stack limit. This CHECKSIG/CHECKSIGADD form
383
- // momentarily has n witness items plus one pushed pubkey on the stack, so n must stay <= 999.
384
406
  anumber(i.m, 'm');
385
- if (i.m <= 0 || n > 999 || i.m > n) throw new Error('OutScript/tr_ms: invalid params');
407
+ if (i.m <= 0 || i.m > n) throw new Error('OutScript/tr_ms: invalid params');
386
408
  }
387
409
  return i;
388
410
  })
@@ -406,9 +428,32 @@ export const OutScript: TRet<
406
428
  >;
407
429
  /** Type of the output-script coder. */
408
430
  export type OutScriptType = typeof OutScript;
431
+ // Internal raw-aware boundary for actual scriptPubKeys and P2SH-nested witness programs. Generic
432
+ // OutScript remains semantic because witnessScript and tapscript children may use the same opcodes
433
+ // without claiming BIP141 witness-program framing.
434
+ export const _WitnessOutScript: OutScriptType = /* @__PURE__ */ (() =>
435
+ Object.freeze(
436
+ P.wrap<P.UnwrapCoder<typeof _OutScript>>({
437
+ encodeStream: (w, value) => OutScript.encodeStream(w, value),
438
+ decodeStream: (r) => {
439
+ const raw = r.bytes(r.leftBytes);
440
+ const out = OutScript.decode(raw);
441
+ // Script.decode normalizes push opcodes, but BIP141 recognition requires the exact direct
442
+ // push. Reject only when this explicit outer-program boundary is selected by the caller.
443
+ if (
444
+ out &&
445
+ (out.type === 'p2a' || out.type === 'wpkh' || out.type === 'wsh' || out.type === 'tr') &&
446
+ !u.equalBytes(raw, OutScript.encode(out))
447
+ )
448
+ throw new Error('OutScript: non-canonical witness program');
449
+ return out;
450
+ },
451
+ })
452
+ ))() as OutScriptType;
409
453
  // TRet-wrapping OutScript changes decode() to the normalized descriptor surface, but the local
410
454
  // checkScript/Address caches still need an explicit alias that can carry the decode-side `undefined`.
411
- type OutScriptValue = ReturnType<OutScriptType['decode']> | undefined;
455
+ type AddressValue = NonNullable<ReturnType<OutScriptType['decode']>>;
456
+ type OutScriptValue = AddressValue | undefined;
412
457
 
413
458
  // Basic sanity check for scripts
414
459
  function checkWSH(s: TArg<OutWSHType>, witnessScript: TArg<Bytes>) {
@@ -449,7 +494,7 @@ export function checkScript(
449
494
  let hasWsh = false;
450
495
  let r: OutScriptValue = undefined;
451
496
  if (script) {
452
- const s = OutScript.decode(script);
497
+ const s = _WitnessOutScript.decode(script);
453
498
  // BIP174 Data Signers Check For bullets: provided redeemScript must match
454
499
  // the scriptPubKey, and provided witnessScript must match the scriptPubKey
455
500
  // or redeemScript instead of being silently ignored as stray metadata.
@@ -460,7 +505,7 @@ export function checkScript(
460
505
  if (s.type !== 'sh') throw new Error('checkScript: redeemScript without P2SH');
461
506
  if (!u.equalBytes(s.hash, u.hash160(redeemScript)))
462
507
  throw new Error('checkScript: sh wrong redeemScript hash');
463
- r = OutScript.decode(redeemScript) as OutScriptValue;
508
+ r = _WitnessOutScript.decode(redeemScript) as OutScriptValue;
464
509
  if (r?.type === 'tr' || r?.type === 'tr_ns' || r?.type === 'tr_ms')
465
510
  throw new Error(`checkScript: P2${r.type} cannot be wrapped in P2SH`);
466
511
  // Not sure if this unspendable, but we cannot represent this via PSBT
@@ -472,7 +517,7 @@ export function checkScript(
472
517
  }
473
518
  }
474
519
  if (redeemScript) {
475
- if (r === undefined) r = OutScript.decode(redeemScript) as OutScriptValue;
520
+ if (r === undefined) r = _WitnessOutScript.decode(redeemScript) as OutScriptValue;
476
521
  if (r?.type === 'wsh') {
477
522
  hasWsh = true;
478
523
  if (witnessScript) checkWSH(r as TArg<OutWSHType>, witnessScript);
@@ -481,12 +526,21 @@ export function checkScript(
481
526
  if (witnessScript && !hasWsh) throw new Error('checkScript: witnessScript without P2WSH');
482
527
  }
483
528
 
484
- function uniqPubkey(pubkeys: TArg<Bytes[]>) {
529
+ function uniqPubkey(pubkeys: TArg<Bytes[]>, type: u.PubT) {
485
530
  const map: Record<string, boolean> = {};
486
531
  for (const pub of pubkeys) {
487
- // Exact-byte duplicate filter only: BIP383 valid vectors still permit the
488
- // same point to appear in compressed and uncompressed SEC1 form in multi().
489
- const key = hex.encode(pub);
532
+ u.validatePubkey(pub, type);
533
+ let normalized = pub;
534
+ if (type === u.PubT.ecdsa && pub.length === 65) {
535
+ // Compressed and uncompressed SEC1 encodings can represent the same curve point. Normalize
536
+ // valid uncompressed keys before comparing so one signer cannot occupy multiple threshold
537
+ // slots merely by changing the serialization. `allowSamePubkeys` remains the explicit
538
+ // compatibility escape hatch for callers that intentionally construct duplicate-key scripts.
539
+ normalized = new Uint8Array(33);
540
+ normalized[0] = 2 | (pub[64] & 1);
541
+ normalized.set(pub.subarray(1, 33), 1);
542
+ }
543
+ const key = hex.encode(normalized);
490
544
  if (map[key]) throw new Error(`Multisig: non-uniq pubkey: ${pubkeys.map(hex.encode)}`);
491
545
  map[key] = true;
492
546
  }
@@ -588,10 +642,30 @@ export type P2SHWithoutWitness = Omit<P2SHBase, 'witnessScript'>;
588
642
  export type P2SHReturn<T extends P2Ret> = T extends { witnessScript: Bytes }
589
643
  ? P2SHWithWitness
590
644
  : P2SHWithoutWitness;
645
+
646
+ const checkCanonicalScript = (
647
+ script: TArg<Bytes>,
648
+ name: 'redeemScript' | 'witnessScript',
649
+ allowNonCanonicalScript: boolean
650
+ ): void => {
651
+ if (allowNonCanonicalScript) return;
652
+ const decoded = Script.decode(script);
653
+ const nonMinimalNumber = decoded.some(
654
+ (op) => u.isBytes(op) && op.length === 1 && ((1 <= op[0] && op[0] <= 16) || op[0] === 0x81)
655
+ );
656
+ // Core's default MINIMALDATA policy rejects these spends from its mempool, so address-producing
657
+ // helpers require an explicit opt-in even though the original bytes remain consensus-valid.
658
+ if (nonMinimalNumber || !u.equalBytes(script, Script.encode(decoded))) {
659
+ const wrapper = name === 'redeemScript' ? 'P2SH' : 'P2WSH';
660
+ throw new Error(`${wrapper}: non-canonical ${name}`);
661
+ }
662
+ };
663
+
591
664
  /**
592
665
  * Wraps a child script inside P2SH.
593
666
  * @param child - child payment descriptor to wrap
594
667
  * @param network - address network parameters
668
+ * @param allowNonCanonicalScript - whether to create an address for a non-minimal child script
595
669
  * @returns P2SH descriptor preserving witness metadata when present.
596
670
  * @throws If the wrapped script combination is invalid or unsupported. {@link Error}
597
671
  * @example
@@ -604,8 +678,10 @@ export type P2SHReturn<T extends P2Ret> = T extends { witnessScript: Bytes }
604
678
  */
605
679
  export const p2sh = <T extends P2Ret>(
606
680
  child: TArg<T>,
607
- network: BTC_NETWORK = NETWORK
681
+ network: BTC_NETWORK = NETWORK,
682
+ allowNonCanonicalScript = false
608
683
  ): TRet<Extends<P2SHReturn<T>, P2Ret>> => {
684
+ u.validateObject(child as Record<string, any>, {}, {}, 'child');
609
685
  // It is already tested inside noble-hashes and checkScript
610
686
  // BIP16 redeemScripts are pushed by scriptSig, so anything over the 520-byte pushed-element
611
687
  // limit would be fundable by HASH160 but unspendable once wrapped in P2SH.
@@ -616,6 +692,7 @@ export const p2sh = <T extends P2Ret>(
616
692
  throw new Error(
617
693
  `P2SH: redeemScript exceeds ${MAX_SCRIPT_BYTE_LENGTH}-byte push limit: len=${cs.length}`
618
694
  );
695
+ checkCanonicalScript(cs, 'redeemScript', allowNonCanonicalScript);
619
696
  const hash = u.hash160(cs);
620
697
  const out = { type: 'sh', hash } as const;
621
698
  const script = OutScript.encode(out);
@@ -658,6 +735,7 @@ export type P2WSH = {
658
735
  * Wraps a child script inside native SegWit P2WSH.
659
736
  * @param child - child payment descriptor to wrap
660
737
  * @param network - address network parameters
738
+ * @param allowNonCanonicalScript - whether to create an address for a non-minimal child script
661
739
  * @returns P2WSH descriptor.
662
740
  * @throws If the wrapped script combination is invalid or unsupported. {@link Error}
663
741
  * @example
@@ -670,13 +748,16 @@ export type P2WSH = {
670
748
  */
671
749
  export const p2wsh = (
672
750
  child: TArg<P2Ret>,
673
- network: BTC_NETWORK = NETWORK
751
+ network: BTC_NETWORK = NETWORK,
752
+ allowNonCanonicalScript = false
674
753
  ): TRet<Extends<P2WSH, P2Ret>> => {
754
+ u.validateObject(child as Record<string, any>, {}, {}, 'child');
675
755
  const cs = child.script;
676
756
  if (!u.isBytes(cs)) throw new Error(`Wrong script: ${typeof cs}, expected Uint8Array`);
677
757
  // BIP141 P2WSH says the witness "must consist of ... a serialized script (witnessScript)"
678
758
  // and that witnessScript is limited to 10,000 bytes, so larger wrapped scripts must reject.
679
759
  if (cs.length > 10000) throw new Error('P2WSH: witnessScript exceeds 10,000 bytes');
760
+ checkCanonicalScript(cs, 'witnessScript', allowNonCanonicalScript);
680
761
  const hash = u.sha256(cs);
681
762
  const script = OutScript.encode({ type: 'wsh', hash });
682
763
  checkScript(script, undefined, cs);
@@ -759,7 +840,7 @@ export const p2ms = (
759
840
  ): TRet<Extends<P2MS, P2Ret>> => {
760
841
  // BIP 11 only standardized bare multisig up to 3 keys; this helper still permits up to 16
761
842
  // because the same script shape is commonly wrapped by p2sh()/p2wsh() instead of used bare.
762
- if (!allowSamePubkeys) uniqPubkey(pubkeys);
843
+ if (!allowSamePubkeys) uniqPubkey(pubkeys, u.PubT.ecdsa);
763
844
  return {
764
845
  type: 'ms',
765
846
  script: OutScript.encode({ type: 'ms', pubkeys, m }),
@@ -782,9 +863,16 @@ function checkTaprootScript(
782
863
  // disable custom. All custom scripts for taproot should have prefix 'tr_'
783
864
  if (customScripts) {
784
865
  const cs = P.apply(Script, P.coders.match(customScripts));
785
- const c = cs.decode(script);
866
+ let c;
867
+ // match() throws when no custom coder matches; treat that as "not a custom
868
+ // script" so the allowUnknownOutputs escape below stays reachable.
869
+ try {
870
+ c = cs.decode(script);
871
+ } catch (e) {
872
+ c = undefined;
873
+ }
786
874
  if (c !== undefined) {
787
- if (typeof c.type !== 'string' || !c.type.startsWith('tr_'))
875
+ if (!u.astring(c.type, 'c.type').startsWith('tr_'))
788
876
  throw new Error(`P2TR: invalid custom type=${c.type}`);
789
877
  return;
790
878
  }
@@ -796,7 +884,7 @@ function checkTaprootScript(
796
884
  const outms = out as OutTRNSType | OutTRMSType;
797
885
  if (!allowUnknownOutputs && outms.pubkeys) {
798
886
  for (const p of outms.pubkeys) {
799
- if (u.equalBytes(p, u.TAPROOT_UNSPENDABLE_KEY))
887
+ if (u.equalBytes(p, u.taprootNumsKey()))
800
888
  throw new Error('Unspendable taproot key in leaf script');
801
889
  // It's likely a mistake at this point:
802
890
  // 1. p2tr(A, p2tr_ns(2, [A, B])) == p2tr(A, p2tr_pk(B)) (A or B key)
@@ -867,6 +955,13 @@ type _TaprootTreeInternal = {
867
955
  * ```
868
956
  */
869
957
  export function taprootListToTree(taprootList: TArg<TaprootScriptList>): TRet<TaprootScriptTree> {
958
+ u.aarray<TaprootScriptList[number]>(taprootList, 'taprootList', (leaf, title) => {
959
+ // p2tr reduces non-binary trees through this helper, so nested branch arrays are valid here.
960
+ if (Array.isArray(leaf)) return;
961
+ u.validateObject(leaf as Record<string, any>, {}, {}, title);
962
+ // This helper only arranges weighted tree nodes; p2tr validates leaf scripts while hashing.
963
+ if (leaf.weight !== undefined) anumber(leaf.weight, title + '.weight');
964
+ });
870
965
  // Empty flat lists cannot represent a taproot script tree; omit the tree entirely for
871
966
  // key-path-only outputs instead of passing [] here, otherwise this helper would return
872
967
  // undefined and downstream taproot tree walkers would fail much later on a non-tree value.
@@ -941,23 +1036,35 @@ function taprootWalkTree(tree: TArg<HashedTreeWithPath>): TRet<TaprootLeaf[]> {
941
1036
  return [...taprootWalkTree(tree.left), ...taprootWalkTree(tree.right)] as TRet<TaprootLeaf[]>;
942
1037
  }
943
1038
 
1039
+ // BIP 341 control blocks can encode at most 128 sibling hashes.
1040
+ const TAPROOT_MAX_DEPTH = 128;
944
1041
  function taprootHashTree(
945
1042
  tree: TArg<TaprootScriptTree>,
946
1043
  internalPubKey: TArg<Bytes>,
947
1044
  allowUnknownOutputs = false,
948
- customScripts?: TArg<CustomScript[]>
1045
+ customScripts?: TArg<CustomScript[]>,
1046
+ depth = 0
949
1047
  ): TRet<HashedTree> {
950
- if (!tree) throw new Error('taprootHashTree: empty tree');
1048
+ // Reject before inspecting or descending into the next node, which also bounds the
1049
+ // recursion used by the path annotation and tree-flattening passes below.
1050
+ if (depth > TAPROOT_MAX_DEPTH)
1051
+ throw new RangeError(`P2TR: tree depth exceeds ${TAPROOT_MAX_DEPTH}`);
1052
+ if (tree === undefined) throw new Error('taprootHashTree: empty tree');
1053
+ if (!Array.isArray(tree) && !P.utils.isPlainObject(tree))
1054
+ throw new TypeError('"tree" expected object or array, got type=' + typeof tree);
951
1055
  if (Array.isArray(tree) && tree.length === 1) tree = tree[0];
952
1056
  // Terminal node (leaf)
953
1057
  if (!Array.isArray(tree)) {
1058
+ u.validateObject(tree as Record<string, any>, {}, {}, 'tree');
954
1059
  const version = tree.leafVersion;
955
1060
  const { script: leafScript } = tree;
956
1061
  // Earliest tree walk where we can validate tapScripts
957
1062
  if (tree.tapLeafScript || (tree.tapMerkleRoot && !u.equalBytes(tree.tapMerkleRoot, P.EMPTY)))
958
1063
  throw new Error('P2TR: tapRoot leafScript cannot have tree');
959
- const script = typeof leafScript === 'string' ? hex.decode(leafScript) : leafScript;
960
- if (!u.isBytes(script)) throw new Error(`checkScript: wrong script type=${script}`);
1064
+ const script =
1065
+ typeof leafScript === 'string'
1066
+ ? hex.decode(leafScript)
1067
+ : abytes(leafScript, undefined, 'tree.script');
961
1068
  checkTaprootScript(script, internalPubKey, allowUnknownOutputs, customScripts);
962
1069
  return {
963
1070
  type: 'leaf',
@@ -971,8 +1078,20 @@ function taprootHashTree(
971
1078
  if (tree.length !== 2) throw new Error('hashTree: non binary tree!');
972
1079
  // branch
973
1080
  // Both nodes should exist
974
- const left = taprootHashTree(tree[0], internalPubKey, allowUnknownOutputs, customScripts);
975
- const right = taprootHashTree(tree[1], internalPubKey, allowUnknownOutputs, customScripts);
1081
+ const left = taprootHashTree(
1082
+ tree[0],
1083
+ internalPubKey,
1084
+ allowUnknownOutputs,
1085
+ customScripts,
1086
+ depth + 1
1087
+ );
1088
+ const right = taprootHashTree(
1089
+ tree[1],
1090
+ internalPubKey,
1091
+ allowUnknownOutputs,
1092
+ customScripts,
1093
+ depth + 1
1094
+ );
976
1095
  // BIP 341 sorts TapBranch child hashes lexicographically for hashing, but the original
977
1096
  // left/right structure still determines the control-block sibling paths for each leaf.
978
1097
  let [lH, rH] = [left.hash, right.hash];
@@ -1014,7 +1133,7 @@ export const tapLeafHash = (script: TArg<Bytes>, version: number = TAP_LEAF_VERS
1014
1133
 
1015
1134
  // Works as key OR tree.
1016
1135
  // If we only have tree, need to add unspendable key, otherwise
1017
- // complex multisig wallet can be spent by owner of key only. See TAPROOT_UNSPENDABLE_KEY
1136
+ // complex multisig wallet can be spent by owner of key only. See taprootNumsKey
1018
1137
  /** Conditional taproot return type for key-only or tree-backed outputs. */
1019
1138
  export type P2TRRet<T> = T extends TaprootScriptTree ? P2TR_TREE : P2TR;
1020
1139
  /**
@@ -1026,6 +1145,7 @@ export type P2TRRet<T> = T extends TaprootScriptTree ? P2TR_TREE : P2TR;
1026
1145
  * @param customScripts - optional custom script codecs for taproot leaves
1027
1146
  * @returns Taproot descriptor with optional script-path metadata.
1028
1147
  * @throws If the internal key or taproot script tree is invalid. {@link Error}
1148
+ * @throws If a numeric script value is outside its supported range. {@link RangeError}
1029
1149
  * @example
1030
1150
  * Combine script leaves into a final taproot output descriptor and address.
1031
1151
  * ```ts
@@ -1063,7 +1183,7 @@ export function p2tr(
1063
1183
  const pubKey =
1064
1184
  typeof internalPubKey === 'string'
1065
1185
  ? hex.decode(internalPubKey)
1066
- : internalPubKey || u.TAPROOT_UNSPENDABLE_KEY;
1186
+ : (internalPubKey ?? u.taprootNumsKey());
1067
1187
  if (!isValidPubkey(pubKey, u.PubT.schnorr)) throw new Error('p2tr: non-schnorr pubkey');
1068
1188
  if (tree) {
1069
1189
  let hashedTree = taprootAddPath(
@@ -1071,18 +1191,20 @@ export function p2tr(
1071
1191
  );
1072
1192
  const tapMerkleRoot = hashedTree.hash;
1073
1193
  const [tweakedPubkey, parity] = u.taprootTweakPubkey(pubKey, tapMerkleRoot);
1194
+ const tapLeafScript: NonNullable<TransactionInput['tapLeafScript']> = [];
1074
1195
  const leaves = taprootWalkTree(hashedTree).map((l) => {
1075
1196
  const version = tapLeafVersion(l.version);
1076
- return {
1077
- ...l,
1078
- // Leaf versions are stored as the base even byte; only the control block adds the
1079
- // output-key parity bit required by BIP 341 script-path spending.
1080
- controlBlock: TaprootControlBlock.encode({
1081
- version: version + parity,
1082
- internalKey: pubKey,
1083
- merklePath: l.path,
1084
- }),
1197
+ // Leaf versions are stored as the base even byte; only the control block adds the
1198
+ // output-key parity bit required by BIP 341 script-path spending.
1199
+ const controlBlock = {
1200
+ version: version + parity,
1201
+ internalKey: pubKey,
1202
+ merklePath: l.path,
1085
1203
  };
1204
+ // Skip an encode/decode copy for performance; callers must treat returned metadata as
1205
+ // immutable.
1206
+ tapLeafScript.push([controlBlock, u.concatBytes(l.script, new Uint8Array([version]))]);
1207
+ return { ...l, controlBlock: TaprootControlBlock.encode(controlBlock) };
1086
1208
  });
1087
1209
  return {
1088
1210
  type: 'tr',
@@ -1093,10 +1215,7 @@ export function p2tr(
1093
1215
  // PSBT stuff
1094
1216
  tapInternalKey: pubKey,
1095
1217
  leaves,
1096
- tapLeafScript: leaves.map((l) => [
1097
- TaprootControlBlock.decode(l.controlBlock),
1098
- u.concatBytes(l.script, new Uint8Array([tapLeafVersion(l.version)])),
1099
- ]),
1218
+ tapLeafScript,
1100
1219
  tapMerkleRoot,
1101
1220
  } as const as TRet<Extends<P2TR_TREE, P2Ret>>;
1102
1221
  } else {
@@ -1115,25 +1234,47 @@ export function p2tr(
1115
1234
  }
1116
1235
  }
1117
1236
 
1237
+ /** Maximum number of combinations materialized by one default helper call. */
1238
+ export const MAX_COMBINATIONS = 4096;
1239
+
1240
+ const combinationCount = (n: number, m: number): number => {
1241
+ const k = Math.min(m, n - m);
1242
+ let count = 1;
1243
+ for (let i = 1; i <= k; i++) {
1244
+ count = (count * (n - k + i)) / i;
1245
+ if (!Number.isSafeInteger(count)) return Number.POSITIVE_INFINITY;
1246
+ }
1247
+ return count;
1248
+ };
1249
+
1118
1250
  // Returns all combinations of size M from lst
1119
1251
  /**
1120
1252
  * Returns all size-`m` combinations from a list.
1121
1253
  * @param m - size of each combination
1122
1254
  * @param list - input items to combine
1255
+ * @param maxCombinations - maximum result rows to materialize
1123
1256
  * @returns Array of combinations.
1124
1257
  * @throws If the combination size or input list is invalid. {@link Error}
1258
+ * @throws If the requested result exceeds the materialization limit. {@link RangeError}
1125
1259
  * @example
1126
1260
  * Enumerate all size-two subsets of a short list.
1127
1261
  * ```ts
1128
1262
  * combinations(2, [1, 2, 3]);
1129
1263
  * ```
1130
1264
  */
1131
- export function combinations<T>(m: number, list: T[]): T[][] {
1265
+ export function combinations<T>(m: number, list: T[], maxCombinations = MAX_COMBINATIONS): T[][] {
1132
1266
  const res: T[][] = [];
1133
1267
  if (!Array.isArray(list)) throw new Error('combinations: lst arg should be array');
1134
1268
  const n = list.length;
1135
1269
  anumber(m, 'm');
1270
+ anumber(maxCombinations, 'maxCombinations');
1136
1271
  if (m < 1 || m > n) throw new Error('combinations: m must satisfy 1 <= m <= lst.length');
1272
+ if (maxCombinations < 1) throw new Error('combinations: maxCombinations must be >= 1');
1273
+ const count = combinationCount(n, m);
1274
+ if (count > maxCombinations)
1275
+ throw new RangeError(
1276
+ `combinations: C(${n}, ${m}) exceeds materialization limit=${maxCombinations}`
1277
+ );
1137
1278
  /*
1138
1279
  Basically works as M nested loops like:
1139
1280
  for (;idx[0]<lst.length;idx[0]++) for (idx[1]=idx[0]+1;idx[1]<lst.length;idx[1]++)
@@ -1163,8 +1304,8 @@ export function combinations<T>(m: number, list: T[]): T[][] {
1163
1304
 
1164
1305
  /**
1165
1306
  * M-of-N multi-leaf wallet via p2tr_ns. If m == n, single script is emitted.
1166
- * Takes O(n^2) if m != n. 99-of-100 is ok, 5-of-100 is not.
1167
- * It materializes C(n, m) leaves, so middle-of-the-range thresholds blow up combinatorially.
1307
+ * It materializes C(n, m) leaves up to {@link MAX_COMBINATIONS}; middle-of-the-range thresholds
1308
+ * above that bound are rejected before allocation.
1168
1309
  * `2-of-[A,B,C] => [A,B] | [A,C] | [B,C]`
1169
1310
  */
1170
1311
  export type P2TR_NS = {
@@ -1180,6 +1321,7 @@ export type P2TR_NS = {
1180
1321
  * @param allowSamePubkeys - whether duplicate keys are allowed
1181
1322
  * @returns Array of taproot leaf descriptors.
1182
1323
  * @throws If the taproot multisig parameters are invalid. {@link Error}
1324
+ * @throws If the requested leaf set exceeds the materialization limit. {@link RangeError}
1183
1325
  * @example
1184
1326
  * Build the leaf set for an M-of-N taproot `CHECKSIGVERIFY` policy.
1185
1327
  * ```ts
@@ -1193,14 +1335,24 @@ export const p2tr_ns = (
1193
1335
  pubkeys: TArg<Bytes[]>,
1194
1336
  allowSamePubkeys = false
1195
1337
  ): TRet<Extends<P2TR_NS, P2Ret>[]> => {
1196
- if (!allowSamePubkeys) uniqPubkey(pubkeys);
1197
- return combinations(m, pubkeys).map(
1198
- (i) =>
1199
- ({
1200
- type: 'tr_ns',
1201
- script: OutScript.encode({ type: 'tr_ns', pubkeys: i }),
1202
- }) as const as TRet<Extends<P2TR_NS, P2Ret>>
1203
- ) as TRet<Extends<P2TR_NS, P2Ret>[]>;
1338
+ anumber(m, 'm');
1339
+ if (m > 999) throw new Error('OutScript/tr_ns: invalid params');
1340
+ // Enforce the allocation bound before doing curve work on an attacker-controlled key list.
1341
+ const keySets = combinations(m, pubkeys);
1342
+ if (allowSamePubkeys) {
1343
+ for (const pubkey of pubkeys) u.validatePubkey(pubkey, u.PubT.schnorr);
1344
+ } else uniqPubkey(pubkeys, u.PubT.schnorr);
1345
+ return keySets.map((keys) => {
1346
+ // Keys were validated once above. Encoding through OutScript here would repeat lift_x for
1347
+ // every key in every combination, turning a bounded leaf set into avoidable curve-level work.
1348
+ const ops: ScriptType = [];
1349
+ for (let i = 0; i < keys.length - 1; i++) ops.push(keys[i], 'CHECKSIGVERIFY');
1350
+ ops.push(keys[keys.length - 1], 'CHECKSIG');
1351
+ return {
1352
+ type: 'tr_ns',
1353
+ script: Script.encode(ops),
1354
+ } as const as TRet<Extends<P2TR_NS, P2Ret>>;
1355
+ }) as TRet<Extends<P2TR_NS, P2Ret>[]>;
1204
1356
  };
1205
1357
  // Taproot public key (case of p2tr_ns)
1206
1358
  /** Single-key taproot leaf descriptor. */
@@ -1212,6 +1364,7 @@ export type P2TR_PK = P2TR_NS;
1212
1364
  * @param pubkey - Schnorr public key
1213
1365
  * @returns Taproot single-key leaf descriptor.
1214
1366
  * @throws If the taproot single-key leaf cannot be encoded. {@link Error}
1367
+ * @throws If the delegated leaf policy exceeds its supported range. {@link RangeError}
1215
1368
  * @example
1216
1369
  * Build a single-key tapscript leaf.
1217
1370
  * ```ts
@@ -1250,7 +1403,7 @@ export function p2tr_ms(
1250
1403
  pubkeys: TArg<Bytes[]>,
1251
1404
  allowSamePubkeys = false
1252
1405
  ): TRet<Extends<P2TR_MS, P2Ret>> {
1253
- if (!allowSamePubkeys) uniqPubkey(pubkeys);
1406
+ if (!allowSamePubkeys) uniqPubkey(pubkeys, u.PubT.schnorr);
1254
1407
  return {
1255
1408
  type: 'tr_ms',
1256
1409
  script: OutScript.encode({ type: 'tr_ms', pubkeys, m }),
@@ -1265,6 +1418,7 @@ export function p2tr_ms(
1265
1418
  * @param network - address network parameters
1266
1419
  * @returns Encoded Bitcoin address.
1267
1420
  * @throws If the requested address type is unknown. {@link Error}
1421
+ * @throws If a key-derived script value is outside its supported range. {@link RangeError}
1268
1422
  * @example
1269
1423
  * Pick the output type first, then derive the matching address from the private key.
1270
1424
  * ```ts
@@ -1278,6 +1432,7 @@ export function getAddress(
1278
1432
  privKey: TArg<Bytes>,
1279
1433
  network: BTC_NETWORK = NETWORK
1280
1434
  ): string {
1435
+ u.astring(type, 'type');
1281
1436
  if (type === 'tr') {
1282
1437
  return p2tr(u.pubSchnorr(privKey), undefined, network).address;
1283
1438
  }
@@ -1429,18 +1584,24 @@ export function WIF(network: BTC_NETWORK = NETWORK): TRet<Coder<Bytes, string>>
1429
1584
  * coder.encode(p2wpkh(pubECDSA(randomPrivateKeyBytes())));
1430
1585
  * ```
1431
1586
  */
1432
- export function Address(network: BTC_NETWORK = NETWORK) {
1587
+ export function Address(network: BTC_NETWORK = NETWORK): TRet<P.Coder<AddressValue, string>> {
1588
+ u.validateObject(network as Record<string, any>, {}, {}, 'network');
1433
1589
  return {
1434
- encode(from: Exclude<OutScriptValue, undefined>): string {
1590
+ encode(from: TArg<AddressValue>): string {
1591
+ u.validateObject(from as Record<string, any>, {}, {}, 'from');
1435
1592
  const { type } = from;
1593
+ u.astring(type, 'from.type');
1436
1594
  if (type === 'wpkh') return programToWitness(0, from.hash, network);
1437
1595
  else if (type === 'wsh') return programToWitness(0, from.hash, network);
1438
1596
  else if (type === 'tr') return programToWitness(1, from.pubkey, network);
1597
+ // BIP433 P2A is the fixed v1 witness program 0x4e73 ('bc1pfeessrawgf').
1598
+ else if (type === 'p2a') return programToWitness(1, P2A_PROGRAM, network);
1439
1599
  else if (type === 'pkh') return formatKey(from.hash, [network.pubKeyHash]);
1440
1600
  else if (type === 'sh') return formatKey(from.hash, [network.scriptHash]);
1441
1601
  throw new Error(`Unknown address type=${type}`);
1442
1602
  },
1443
- decode(address: string): OutScriptValue {
1603
+ decode(address: string): TRet<AddressValue> {
1604
+ u.astring(address, 'address');
1444
1605
  if (address.length < 14 || address.length > 74) throw new Error('Invalid address length');
1445
1606
  // Bech32
1446
1607
  if (network.bech32 && address.toLowerCase().startsWith(`${network.bech32}1`)) {
@@ -1458,25 +1619,27 @@ export function Address(network: BTC_NETWORK = NETWORK) {
1458
1619
  const data = bech32.fromWords(program);
1459
1620
  validateWitness(version, data);
1460
1621
  if (version === 0 && data.length === 32)
1461
- return { type: 'wsh', hash: data } as OutScriptValue;
1622
+ return { type: 'wsh', hash: data } as TRet<AddressValue>;
1462
1623
  else if (version === 0 && data.length === 20)
1463
- return { type: 'wpkh', hash: data } as OutScriptValue;
1624
+ return { type: 'wpkh', hash: data } as TRet<AddressValue>;
1464
1625
  else if (version === 1 && data.length === 32)
1465
- return { type: 'tr', pubkey: data } as OutScriptValue;
1626
+ return { type: 'tr', pubkey: data } as TRet<AddressValue>;
1627
+ else if (version === 1 && u.equalBytes(data, P2A_PROGRAM))
1628
+ return { type: 'p2a', script: Script.encode([1, data]) } as TRet<AddressValue>;
1466
1629
  // Future witness versions can still be valid addresses, but this helper
1467
- // only returns typed descriptors for recognized v0 and taproot templates.
1630
+ // only returns typed descriptors for recognized v0, taproot and P2A templates.
1468
1631
  else throw new Error('Unknown witness program');
1469
1632
  }
1470
1633
  const data = base58check.decode(address);
1471
1634
  if (data.length !== 21) throw new Error('Invalid base58 address');
1472
1635
  // Pay To Public Key Hash
1473
1636
  if (data[0] === network.pubKeyHash) {
1474
- return { type: 'pkh', hash: data.slice(1) } as OutScriptValue;
1637
+ return { type: 'pkh', hash: data.slice(1) } as TRet<AddressValue>;
1475
1638
  } else if (data[0] === network.scriptHash) {
1476
1639
  return {
1477
1640
  type: 'sh',
1478
1641
  hash: data.slice(1),
1479
- } as OutScriptValue;
1642
+ } as TRet<AddressValue>;
1480
1643
  }
1481
1644
  throw new Error(`Invalid address prefix=${data[0]}`);
1482
1645
  },