@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/payment.js CHANGED
@@ -6,10 +6,16 @@ import { TaprootControlBlock } from "./psbt.js";
6
6
  import { MAX_SCRIPT_BYTE_LENGTH, OpToNum, Script, VarBytes } from "./script.js";
7
7
  import * as u from "./utils.js";
8
8
  import { NETWORK } from "./utils.js";
9
+ // Pay to Anchor (P2A)
10
+ // BIP433 Pay-to-Anchor witness program bytes; the scriptPubKey is `OP_1 <0x4e73>`.
11
+ const P2A_PROGRAM = /* @__PURE__ */ Uint8Array.from([0x4e, 0x73]);
9
12
  const OutP2A = {
10
13
  encode(from) {
11
14
  // BIP433 defines P2A as the exact OP_1 <0x4e73> scriptPubKey.
12
- if (from.length !== 2 || from[0] !== 1 || !u.isBytes(from[1]) || hex.encode(from[1]) !== '4e73')
15
+ if (from.length !== 2 ||
16
+ from[0] !== 1 ||
17
+ !u.isBytes(from[1]) ||
18
+ !u.equalBytes(from[1], P2A_PROGRAM))
13
19
  return;
14
20
  return { type: 'p2a', script: Script.encode(from) };
15
21
  },
@@ -18,7 +24,7 @@ const OutP2A = {
18
24
  return;
19
25
  // The decoded object keeps `script` for caller convenience, but the `p2a`
20
26
  // tag always canonicalizes back to the fixed BIP433 script.
21
- return [1, hex.decode('4e73')];
27
+ return [1, Uint8Array.from(P2A_PROGRAM)];
22
28
  },
23
29
  };
24
30
  function isValidPubkey(pub, type) {
@@ -54,8 +60,10 @@ const OutPKH = {
54
60
  encode(from) {
55
61
  if (from.length !== 5 || from[0] !== 'DUP' || from[1] !== 'HASH160' || !u.isBytes(from[2]))
56
62
  return;
57
- // OutScript validates that the pushed HASH160 is exactly 20 bytes.
58
- // This child matcher only recognizes the canonical P2PKH opcode skeleton.
63
+ // Require the exact 20-byte HASH160 here so near-miss scripts fall through
64
+ // to OutUnknown instead of throwing in the OutScript validator on decode.
65
+ if (from[2].length !== 20)
66
+ return;
59
67
  if (from[3] !== 'EQUALVERIFY' || from[4] !== 'CHECKSIG')
60
68
  return;
61
69
  return { type: 'pkh', hash: from[2] };
@@ -70,8 +78,10 @@ const OutSH = {
70
78
  encode(from) {
71
79
  if (from.length !== 3 || from[0] !== 'HASH160' || !u.isBytes(from[1]) || from[2] !== 'EQUAL')
72
80
  return;
73
- // OutScript validates that the pushed HASH160 is exactly 20 bytes.
74
- // This child matcher only recognizes the canonical P2SH opcode skeleton.
81
+ // Require the exact 20-byte HASH160 here so near-miss scripts fall through
82
+ // to OutUnknown instead of throwing in the OutScript validator on decode.
83
+ if (from[1].length !== 20)
84
+ return;
75
85
  return { type: 'sh', hash: from[1] };
76
86
  },
77
87
  // OutScript validates `sh.hash` before this branch emits the canonical
@@ -119,11 +129,15 @@ const OutMS = {
119
129
  const pubkeys = from.slice(1, -2);
120
130
  if (n !== pubkeys.length)
121
131
  return;
132
+ // Require valid ECDSA pubkeys and `0 < m <= n` here so near-miss
133
+ // CHECKMULTISIG scripts (garbage keys, degenerate 0-of-0) fall through to
134
+ // OutUnknown instead of throwing in the OutScript validator on decode.
135
+ // Script.decode only yields 0..16 for opcode numbers, so n <= 16 holds.
122
136
  for (const pub of pubkeys)
123
- if (!u.isBytes(pub))
137
+ if (!u.isBytes(pub) || !isValidPubkey(pub, u.PubT.ecdsa))
124
138
  return;
125
- // OutScript validates pubkey encodings and `0 < m <= n <= 16`.
126
- // This child matcher only recognizes the canonical CHECKMULTISIG skeleton.
139
+ if (!Number.isSafeInteger(m) || m < 1 || m > n)
140
+ return;
127
141
  // We don't need n here because it is the same as pubkeys.length.
128
142
  return { type: 'ms', m, pubkeys: pubkeys };
129
143
  },
@@ -143,6 +157,11 @@ const OutTR = {
143
157
  // other OP_1 program lengths remain reserved future witness programs and should fall through.
144
158
  if (from.length !== 2 || from[0] !== 1 || !u.isBytes(from[1]) || from[1].length !== 32)
145
159
  return;
160
+ // A 32-byte v1 program with an off-curve x coordinate is a fundable but
161
+ // taproot-unspendable output; classify it as unknown instead of throwing
162
+ // in the OutScript validator on decode.
163
+ if (!isValidPubkey(from[1], u.PubT.schnorr))
164
+ return;
146
165
  return { type: 'tr', pubkey: from[1] };
147
166
  },
148
167
  // OutScript validates `tr.pubkey` before this branch emits the canonical
@@ -172,7 +191,7 @@ const OutTRNS = {
172
191
  // BIP342 "Using a k-of-k script for every combination" documents the shape
173
192
  // `<pubkey_1> CHECKSIGVERIFY ... <pubkey_n> CHECKSIG`; this matcher only
174
193
  // classifies that embedded-pubkey form, so bare CHECKSIG stays unknown.
175
- if (!pubkeys.length)
194
+ if (!pubkeys.length || pubkeys.length > 999)
176
195
  return;
177
196
  return { type: 'tr_ns', pubkeys };
178
197
  },
@@ -206,10 +225,15 @@ const OutTRMS = {
206
225
  return;
207
226
  continue;
208
227
  }
209
- if (!u.isBytes(elm))
228
+ // Require actual Schnorr pubkeys here (same as tr_ns) so near-miss
229
+ // CHECKSIGADD scripts fall through to OutUnknown instead of throwing
230
+ // in the OutScript validator on decode.
231
+ if (!u.isBytes(elm) || !isValidPubkey(elm, u.PubT.schnorr))
210
232
  return;
211
233
  pubkeys.push(elm);
212
234
  }
235
+ if (!Number.isSafeInteger(m) || m < 1 || m > pubkeys.length || pubkeys.length > 999)
236
+ return;
213
237
  return { type: 'tr_ms', pubkeys, m };
214
238
  },
215
239
  decode: (to) => {
@@ -298,20 +322,40 @@ export const OutScript = /* @__PURE__ */ (() => Object.freeze(P.validate(_OutScr
298
322
  throw new Error('OutScript/multisig: invalid params');
299
323
  }
300
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`);
301
330
  for (const p of i.pubkeys)
302
331
  if (!isValidPubkey(p, u.PubT.schnorr))
303
332
  throw new Error(`OutScript/${i.type}: wrong pubkey`);
304
333
  }
305
334
  if (i.type === 'tr_ms') {
306
335
  const n = i.pubkeys.length;
307
- // BIP 342 keeps the 1000-element stack limit. This CHECKSIG/CHECKSIGADD form
308
- // momentarily has n witness items plus one pushed pubkey on the stack, so n must stay <= 999.
309
336
  anumber(i.m, 'm');
310
- if (i.m <= 0 || n > 999 || i.m > n)
337
+ if (i.m <= 0 || i.m > n)
311
338
  throw new Error('OutScript/tr_ms: invalid params');
312
339
  }
313
340
  return i;
314
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
+ })))();
315
359
  // Basic sanity check for scripts
316
360
  function checkWSH(s, witnessScript) {
317
361
  if (!u.equalBytes(s.hash, u.sha256(witnessScript)))
@@ -346,7 +390,7 @@ export function checkScript(script, redeemScript, witnessScript) {
346
390
  let hasWsh = false;
347
391
  let r = undefined;
348
392
  if (script) {
349
- const s = OutScript.decode(script);
393
+ const s = _WitnessOutScript.decode(script);
350
394
  // BIP174 Data Signers Check For bullets: provided redeemScript must match
351
395
  // the scriptPubKey, and provided witnessScript must match the scriptPubKey
352
396
  // or redeemScript instead of being silently ignored as stray metadata.
@@ -358,7 +402,7 @@ export function checkScript(script, redeemScript, witnessScript) {
358
402
  throw new Error('checkScript: redeemScript without P2SH');
359
403
  if (!u.equalBytes(s.hash, u.hash160(redeemScript)))
360
404
  throw new Error('checkScript: sh wrong redeemScript hash');
361
- r = OutScript.decode(redeemScript);
405
+ r = _WitnessOutScript.decode(redeemScript);
362
406
  if (r?.type === 'tr' || r?.type === 'tr_ns' || r?.type === 'tr_ms')
363
407
  throw new Error(`checkScript: P2${r.type} cannot be wrapped in P2SH`);
364
408
  // Not sure if this unspendable, but we cannot represent this via PSBT
@@ -373,7 +417,7 @@ export function checkScript(script, redeemScript, witnessScript) {
373
417
  }
374
418
  if (redeemScript) {
375
419
  if (r === undefined)
376
- r = OutScript.decode(redeemScript);
420
+ r = _WitnessOutScript.decode(redeemScript);
377
421
  if (r?.type === 'wsh') {
378
422
  hasWsh = true;
379
423
  if (witnessScript)
@@ -383,12 +427,21 @@ export function checkScript(script, redeemScript, witnessScript) {
383
427
  if (witnessScript && !hasWsh)
384
428
  throw new Error('checkScript: witnessScript without P2WSH');
385
429
  }
386
- function uniqPubkey(pubkeys) {
430
+ function uniqPubkey(pubkeys, type) {
387
431
  const map = {};
388
432
  for (const pub of pubkeys) {
389
- // Exact-byte duplicate filter only: BIP383 valid vectors still permit the
390
- // same point to appear in compressed and uncompressed SEC1 form in multi().
391
- 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);
392
445
  if (map[key])
393
446
  throw new Error(`Multisig: non-uniq pubkey: ${pubkeys.map(hex.encode)}`);
394
447
  map[key] = true;
@@ -442,10 +495,23 @@ export const p2pkh = (publicKey, network = NETWORK) => {
442
495
  hash,
443
496
  };
444
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
+ };
445
510
  /**
446
511
  * Wraps a child script inside P2SH.
447
512
  * @param child - child payment descriptor to wrap
448
513
  * @param network - address network parameters
514
+ * @param allowNonCanonicalScript - whether to create an address for a non-minimal child script
449
515
  * @returns P2SH descriptor preserving witness metadata when present.
450
516
  * @throws If the wrapped script combination is invalid or unsupported. {@link Error}
451
517
  * @example
@@ -456,7 +522,8 @@ export const p2pkh = (publicKey, network = NETWORK) => {
456
522
  * p2sh(p2wsh(p2pk(hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'))));
457
523
  * ```
458
524
  */
459
- export const p2sh = (child, network = NETWORK) => {
525
+ export const p2sh = (child, network = NETWORK, allowNonCanonicalScript = false) => {
526
+ u.validateObject(child, {}, {}, 'child');
460
527
  // It is already tested inside noble-hashes and checkScript
461
528
  // BIP16 redeemScripts are pushed by scriptSig, so anything over the 520-byte pushed-element
462
529
  // limit would be fundable by HASH160 but unspendable once wrapped in P2SH.
@@ -466,6 +533,7 @@ export const p2sh = (child, network = NETWORK) => {
466
533
  throw new Error(`Wrong script: ${typeof c.script}, expected Uint8Array`);
467
534
  if (cs.length > MAX_SCRIPT_BYTE_LENGTH)
468
535
  throw new Error(`P2SH: redeemScript exceeds ${MAX_SCRIPT_BYTE_LENGTH}-byte push limit: len=${cs.length}`);
536
+ checkCanonicalScript(cs, 'redeemScript', allowNonCanonicalScript);
469
537
  const hash = u.hash160(cs);
470
538
  const out = { type: 'sh', hash };
471
539
  const script = OutScript.encode(out);
@@ -495,6 +563,7 @@ export const p2sh = (child, network = NETWORK) => {
495
563
  * Wraps a child script inside native SegWit P2WSH.
496
564
  * @param child - child payment descriptor to wrap
497
565
  * @param network - address network parameters
566
+ * @param allowNonCanonicalScript - whether to create an address for a non-minimal child script
498
567
  * @returns P2WSH descriptor.
499
568
  * @throws If the wrapped script combination is invalid or unsupported. {@link Error}
500
569
  * @example
@@ -505,7 +574,8 @@ export const p2sh = (child, network = NETWORK) => {
505
574
  * p2wsh(p2pk(hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798')));
506
575
  * ```
507
576
  */
508
- export const p2wsh = (child, network = NETWORK) => {
577
+ export const p2wsh = (child, network = NETWORK, allowNonCanonicalScript = false) => {
578
+ u.validateObject(child, {}, {}, 'child');
509
579
  const cs = child.script;
510
580
  if (!u.isBytes(cs))
511
581
  throw new Error(`Wrong script: ${typeof cs}, expected Uint8Array`);
@@ -513,6 +583,7 @@ export const p2wsh = (child, network = NETWORK) => {
513
583
  // and that witnessScript is limited to 10,000 bytes, so larger wrapped scripts must reject.
514
584
  if (cs.length > 10000)
515
585
  throw new Error('P2WSH: witnessScript exceeds 10,000 bytes');
586
+ checkCanonicalScript(cs, 'witnessScript', allowNonCanonicalScript);
516
587
  const hash = u.sha256(cs);
517
588
  const script = OutScript.encode({ type: 'wsh', hash });
518
589
  checkScript(script, undefined, cs);
@@ -571,7 +642,7 @@ export const p2ms = (m, pubkeys, allowSamePubkeys = false) => {
571
642
  // BIP 11 only standardized bare multisig up to 3 keys; this helper still permits up to 16
572
643
  // because the same script shape is commonly wrapped by p2sh()/p2wsh() instead of used bare.
573
644
  if (!allowSamePubkeys)
574
- uniqPubkey(pubkeys);
645
+ uniqPubkey(pubkeys, u.PubT.ecdsa);
575
646
  return {
576
647
  type: 'ms',
577
648
  script: OutScript.encode({ type: 'ms', pubkeys, m }),
@@ -584,9 +655,17 @@ function checkTaprootScript(script, internalPubKey, allowUnknownOutputs = false,
584
655
  // disable custom. All custom scripts for taproot should have prefix 'tr_'
585
656
  if (customScripts) {
586
657
  const cs = P.apply(Script, P.coders.match(customScripts));
587
- const c = cs.decode(script);
658
+ let c;
659
+ // match() throws when no custom coder matches; treat that as "not a custom
660
+ // script" so the allowUnknownOutputs escape below stays reachable.
661
+ try {
662
+ c = cs.decode(script);
663
+ }
664
+ catch (e) {
665
+ c = undefined;
666
+ }
588
667
  if (c !== undefined) {
589
- if (typeof c.type !== 'string' || !c.type.startsWith('tr_'))
668
+ if (!u.astring(c.type, 'c.type').startsWith('tr_'))
590
669
  throw new Error(`P2TR: invalid custom type=${c.type}`);
591
670
  return;
592
671
  }
@@ -599,7 +678,7 @@ function checkTaprootScript(script, internalPubKey, allowUnknownOutputs = false,
599
678
  const outms = out;
600
679
  if (!allowUnknownOutputs && outms.pubkeys) {
601
680
  for (const p of outms.pubkeys) {
602
- if (u.equalBytes(p, u.TAPROOT_UNSPENDABLE_KEY))
681
+ if (u.equalBytes(p, u.taprootNumsKey()))
603
682
  throw new Error('Unspendable taproot key in leaf script');
604
683
  // It's likely a mistake at this point:
605
684
  // 1. p2tr(A, p2tr_ns(2, [A, B])) == p2tr(A, p2tr_pk(B)) (A or B key)
@@ -632,6 +711,15 @@ function checkTaprootScript(script, internalPubKey, allowUnknownOutputs = false,
632
711
  * ```
633
712
  */
634
713
  export function taprootListToTree(taprootList) {
714
+ u.aarray(taprootList, 'taprootList', (leaf, title) => {
715
+ // p2tr reduces non-binary trees through this helper, so nested branch arrays are valid here.
716
+ if (Array.isArray(leaf))
717
+ return;
718
+ u.validateObject(leaf, {}, {}, title);
719
+ // This helper only arranges weighted tree nodes; p2tr validates leaf scripts while hashing.
720
+ if (leaf.weight !== undefined)
721
+ anumber(leaf.weight, title + '.weight');
722
+ });
635
723
  // Empty flat lists cannot represent a taproot script tree; omit the tree entirely for
636
724
  // key-path-only outputs instead of passing [] here, otherwise this helper would return
637
725
  // undefined and downstream taproot tree walkers would fail much later on a non-tree value.
@@ -683,21 +771,30 @@ function taprootWalkTree(tree) {
683
771
  // Keep a stable left-to-right DFS leaf order when flattening the annotated tree.
684
772
  return [...taprootWalkTree(tree.left), ...taprootWalkTree(tree.right)];
685
773
  }
686
- function taprootHashTree(tree, internalPubKey, allowUnknownOutputs = false, customScripts) {
687
- if (!tree)
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}`);
781
+ if (tree === undefined)
688
782
  throw new Error('taprootHashTree: empty tree');
783
+ if (!Array.isArray(tree) && !P.utils.isPlainObject(tree))
784
+ throw new TypeError('"tree" expected object or array, got type=' + typeof tree);
689
785
  if (Array.isArray(tree) && tree.length === 1)
690
786
  tree = tree[0];
691
787
  // Terminal node (leaf)
692
788
  if (!Array.isArray(tree)) {
789
+ u.validateObject(tree, {}, {}, 'tree');
693
790
  const version = tree.leafVersion;
694
791
  const { script: leafScript } = tree;
695
792
  // Earliest tree walk where we can validate tapScripts
696
793
  if (tree.tapLeafScript || (tree.tapMerkleRoot && !u.equalBytes(tree.tapMerkleRoot, P.EMPTY)))
697
794
  throw new Error('P2TR: tapRoot leafScript cannot have tree');
698
- const script = typeof leafScript === 'string' ? hex.decode(leafScript) : leafScript;
699
- if (!u.isBytes(script))
700
- throw new Error(`checkScript: wrong script type=${script}`);
795
+ const script = typeof leafScript === 'string'
796
+ ? hex.decode(leafScript)
797
+ : abytes(leafScript, undefined, 'tree.script');
701
798
  checkTaprootScript(script, internalPubKey, allowUnknownOutputs, customScripts);
702
799
  return {
703
800
  type: 'leaf',
@@ -713,8 +810,8 @@ function taprootHashTree(tree, internalPubKey, allowUnknownOutputs = false, cust
713
810
  throw new Error('hashTree: non binary tree!');
714
811
  // branch
715
812
  // Both nodes should exist
716
- const left = taprootHashTree(tree[0], internalPubKey, allowUnknownOutputs, customScripts);
717
- 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);
718
815
  // BIP 341 sorts TapBranch child hashes lexicographically for hashing, but the original
719
816
  // left/right structure still determines the control-block sibling paths for each leaf.
720
817
  let [lH, rH] = [left.hash, right.hash];
@@ -759,25 +856,27 @@ export function p2tr(internalPubKey, tree, network = NETWORK, allowUnknownOutput
759
856
  throw new Error('p2tr: should have pubKey or scriptTree (or both)');
760
857
  const pubKey = typeof internalPubKey === 'string'
761
858
  ? hex.decode(internalPubKey)
762
- : internalPubKey || u.TAPROOT_UNSPENDABLE_KEY;
859
+ : (internalPubKey ?? u.taprootNumsKey());
763
860
  if (!isValidPubkey(pubKey, u.PubT.schnorr))
764
861
  throw new Error('p2tr: non-schnorr pubkey');
765
862
  if (tree) {
766
863
  let hashedTree = taprootAddPath(taprootHashTree(tree, pubKey, allowUnknownOutputs, customScripts));
767
864
  const tapMerkleRoot = hashedTree.hash;
768
865
  const [tweakedPubkey, parity] = u.taprootTweakPubkey(pubKey, tapMerkleRoot);
866
+ const tapLeafScript = [];
769
867
  const leaves = taprootWalkTree(hashedTree).map((l) => {
770
868
  const version = tapLeafVersion(l.version);
771
- return {
772
- ...l,
773
- // Leaf versions are stored as the base even byte; only the control block adds the
774
- // output-key parity bit required by BIP 341 script-path spending.
775
- controlBlock: TaprootControlBlock.encode({
776
- version: version + parity,
777
- internalKey: pubKey,
778
- merklePath: l.path,
779
- }),
869
+ // Leaf versions are stored as the base even byte; only the control block adds the
870
+ // output-key parity bit required by BIP 341 script-path spending.
871
+ const controlBlock = {
872
+ version: version + parity,
873
+ internalKey: pubKey,
874
+ merklePath: l.path,
780
875
  };
876
+ // Skip an encode/decode copy for performance; callers must treat returned metadata as
877
+ // immutable.
878
+ tapLeafScript.push([controlBlock, u.concatBytes(l.script, new Uint8Array([version]))]);
879
+ return { ...l, controlBlock: TaprootControlBlock.encode(controlBlock) };
781
880
  });
782
881
  return {
783
882
  type: 'tr',
@@ -788,10 +887,7 @@ export function p2tr(internalPubKey, tree, network = NETWORK, allowUnknownOutput
788
887
  // PSBT stuff
789
888
  tapInternalKey: pubKey,
790
889
  leaves,
791
- tapLeafScript: leaves.map((l) => [
792
- TaprootControlBlock.decode(l.controlBlock),
793
- u.concatBytes(l.script, new Uint8Array([tapLeafVersion(l.version)])),
794
- ]),
890
+ tapLeafScript,
795
891
  tapMerkleRoot,
796
892
  };
797
893
  }
@@ -810,27 +906,47 @@ export function p2tr(internalPubKey, tree, network = NETWORK, allowUnknownOutput
810
906
  };
811
907
  }
812
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
+ };
813
921
  // Returns all combinations of size M from lst
814
922
  /**
815
923
  * Returns all size-`m` combinations from a list.
816
924
  * @param m - size of each combination
817
925
  * @param list - input items to combine
926
+ * @param maxCombinations - maximum result rows to materialize
818
927
  * @returns Array of combinations.
819
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}
820
930
  * @example
821
931
  * Enumerate all size-two subsets of a short list.
822
932
  * ```ts
823
933
  * combinations(2, [1, 2, 3]);
824
934
  * ```
825
935
  */
826
- export function combinations(m, list) {
936
+ export function combinations(m, list, maxCombinations = MAX_COMBINATIONS) {
827
937
  const res = [];
828
938
  if (!Array.isArray(list))
829
939
  throw new Error('combinations: lst arg should be array');
830
940
  const n = list.length;
831
941
  anumber(m, 'm');
942
+ anumber(maxCombinations, 'maxCombinations');
832
943
  if (m < 1 || m > n)
833
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}`);
834
950
  /*
835
951
  Basically works as M nested loops like:
836
952
  for (;idx[0]<lst.length;idx[0]++) for (idx[1]=idx[0]+1;idx[1]<lst.length;idx[1]++)
@@ -866,6 +982,7 @@ export function combinations(m, list) {
866
982
  * @param allowSamePubkeys - whether duplicate keys are allowed
867
983
  * @returns Array of taproot leaf descriptors.
868
984
  * @throws If the taproot multisig parameters are invalid. {@link Error}
985
+ * @throws If the requested leaf set exceeds the materialization limit. {@link RangeError}
869
986
  * @example
870
987
  * Build the leaf set for an M-of-N taproot `CHECKSIGVERIFY` policy.
871
988
  * ```ts
@@ -875,12 +992,29 @@ export function combinations(m, list) {
875
992
  * ```
876
993
  */
877
994
  export const p2tr_ns = (m, pubkeys, allowSamePubkeys = false) => {
878
- if (!allowSamePubkeys)
879
- uniqPubkey(pubkeys);
880
- return combinations(m, pubkeys).map((i) => ({
881
- type: 'tr_ns',
882
- script: OutScript.encode({ type: 'tr_ns', pubkeys: i }),
883
- }));
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
+ });
884
1018
  };
885
1019
  /**
886
1020
  * Builds a single-key taproot leaf script.
@@ -889,6 +1023,7 @@ export const p2tr_ns = (m, pubkeys, allowSamePubkeys = false) => {
889
1023
  * @param pubkey - Schnorr public key
890
1024
  * @returns Taproot single-key leaf descriptor.
891
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}
892
1027
  * @example
893
1028
  * Build a single-key tapscript leaf.
894
1029
  * ```ts
@@ -915,7 +1050,7 @@ export const p2tr_pk = (pubkey) => p2tr_ns(1, [pubkey], undefined)[0];
915
1050
  */
916
1051
  export function p2tr_ms(m, pubkeys, allowSamePubkeys = false) {
917
1052
  if (!allowSamePubkeys)
918
- uniqPubkey(pubkeys);
1053
+ uniqPubkey(pubkeys, u.PubT.schnorr);
919
1054
  return {
920
1055
  type: 'tr_ms',
921
1056
  script: OutScript.encode({ type: 'tr_ms', pubkeys, m }),
@@ -929,6 +1064,7 @@ export function p2tr_ms(m, pubkeys, allowSamePubkeys = false) {
929
1064
  * @param network - address network parameters
930
1065
  * @returns Encoded Bitcoin address.
931
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}
932
1068
  * @example
933
1069
  * Pick the output type first, then derive the matching address from the private key.
934
1070
  * ```ts
@@ -938,6 +1074,7 @@ export function p2tr_ms(m, pubkeys, allowSamePubkeys = false) {
938
1074
  * ```
939
1075
  */
940
1076
  export function getAddress(type, privKey, network = NETWORK) {
1077
+ u.astring(type, 'type');
941
1078
  if (type === 'tr') {
942
1079
  return p2tr(u.pubSchnorr(privKey), undefined, network).address;
943
1080
  }
@@ -1076,15 +1213,21 @@ export function WIF(network = NETWORK) {
1076
1213
  * ```
1077
1214
  */
1078
1215
  export function Address(network = NETWORK) {
1216
+ u.validateObject(network, {}, {}, 'network');
1079
1217
  return {
1080
1218
  encode(from) {
1219
+ u.validateObject(from, {}, {}, 'from');
1081
1220
  const { type } = from;
1221
+ u.astring(type, 'from.type');
1082
1222
  if (type === 'wpkh')
1083
1223
  return programToWitness(0, from.hash, network);
1084
1224
  else if (type === 'wsh')
1085
1225
  return programToWitness(0, from.hash, network);
1086
1226
  else if (type === 'tr')
1087
1227
  return programToWitness(1, from.pubkey, network);
1228
+ // BIP433 P2A is the fixed v1 witness program 0x4e73 ('bc1pfeessrawgf').
1229
+ else if (type === 'p2a')
1230
+ return programToWitness(1, P2A_PROGRAM, network);
1088
1231
  else if (type === 'pkh')
1089
1232
  return formatKey(from.hash, [network.pubKeyHash]);
1090
1233
  else if (type === 'sh')
@@ -1092,6 +1235,7 @@ export function Address(network = NETWORK) {
1092
1235
  throw new Error(`Unknown address type=${type}`);
1093
1236
  },
1094
1237
  decode(address) {
1238
+ u.astring(address, 'address');
1095
1239
  if (address.length < 14 || address.length > 74)
1096
1240
  throw new Error('Invalid address length');
1097
1241
  // Bech32
@@ -1119,8 +1263,10 @@ export function Address(network = NETWORK) {
1119
1263
  return { type: 'wpkh', hash: data };
1120
1264
  else if (version === 1 && data.length === 32)
1121
1265
  return { type: 'tr', pubkey: data };
1266
+ else if (version === 1 && u.equalBytes(data, P2A_PROGRAM))
1267
+ return { type: 'p2a', script: Script.encode([1, data]) };
1122
1268
  // Future witness versions can still be valid addresses, but this helper
1123
- // only returns typed descriptors for recognized v0 and taproot templates.
1269
+ // only returns typed descriptors for recognized v0, taproot and P2A templates.
1124
1270
  else
1125
1271
  throw new Error('Unknown witness program');
1126
1272
  }
@@ -1141,4 +1287,3 @@ export function Address(network = NETWORK) {
1141
1287
  },
1142
1288
  };
1143
1289
  }
1144
- //# sourceMappingURL=payment.js.map