@scure/btc-signer 2.3.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/utils.d.ts CHANGED
@@ -264,8 +264,22 @@ export declare function taprootTweakPrivKey(privKey: TArg<Bytes>, merkleRoot?: T
264
264
  * ```
265
265
  */
266
266
  export declare function taprootTweakPubkey(pubKey: TArg<Bytes>, h: TArg<Bytes>): TRet<[Bytes, number]>;
267
- /** Standard unspendable internal key used for script-only Taproot outputs. */
267
+ /**
268
+ * Standard unspendable internal key used for script-only Taproot outputs.
269
+ * @deprecated Use {@link taprootNumsKey} to receive an owned copy.
270
+ */
268
271
  export declare const TAPROOT_UNSPENDABLE_KEY: TRet<Bytes>;
272
+ /**
273
+ * Returns an owned copy of the library's stable Taproot NUMS key.
274
+ * @returns A new 32-byte NUMS key copy.
275
+ * @example
276
+ * Obtain an internal key without sharing mutable exported storage.
277
+ * ```ts
278
+ * import { taprootNumsKey } from '@scure/btc-signer/utils.js';
279
+ * const internalKey = taprootNumsKey();
280
+ * ```
281
+ */
282
+ export declare function taprootNumsKey(): TRet<Bytes>;
269
283
  /** Bitcoin network parameters. */
270
284
  export type BTC_NETWORK = {
271
285
  /** Human-readable prefix used by Bech32 and Bech32m addresses. */
package/utils.js CHANGED
@@ -373,8 +373,27 @@ export function taprootTweakPubkey(pubKey, h) {
373
373
  // This is the fixed BIP 341 H example, not the privacy-preserving H + rG variant.
374
374
  // Downstream helpers use exact-byte equality with it to recognize
375
375
  // library-generated script-only outputs.
376
- /** Standard unspendable internal key used for script-only Taproot outputs. */
377
- export const TAPROOT_UNSPENDABLE_KEY = /* @__PURE__ */ (() => sha256(Point.BASE.toBytes(false)))();
376
+ // Keep the value used by library internals private: exported Uint8Arrays are mutable, so the
377
+ // public compatibility export below cannot safely be a source of cryptographic key material.
378
+ const INTERNAL_TAPROOT_NUMS = /* @__PURE__ */ (() => sha256(Point.BASE.toBytes(false)))();
379
+ /**
380
+ * Standard unspendable internal key used for script-only Taproot outputs.
381
+ * @deprecated Use {@link taprootNumsKey} to receive an owned copy.
382
+ */
383
+ export const TAPROOT_UNSPENDABLE_KEY = /* @__PURE__ */ (() => Uint8Array.from(INTERNAL_TAPROOT_NUMS))();
384
+ /**
385
+ * Returns an owned copy of the library's stable Taproot NUMS key.
386
+ * @returns A new 32-byte NUMS key copy.
387
+ * @example
388
+ * Obtain an internal key without sharing mutable exported storage.
389
+ * ```ts
390
+ * import { taprootNumsKey } from '@scure/btc-signer/utils.js';
391
+ * const internalKey = taprootNumsKey();
392
+ * ```
393
+ */
394
+ export function taprootNumsKey() {
395
+ return Uint8Array.from(INTERNAL_TAPROOT_NUMS);
396
+ }
378
397
  /** Bitcoin mainnet network parameters. */
379
398
  export const NETWORK = /* @__PURE__ */ Object.freeze({
380
399
  bech32: 'bc',
package/utxo.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as P from 'micro-packed';
2
2
  import * as psbt from './psbt.ts';
3
3
  import { Transaction, type TxOpts } from './transaction.ts';
4
- import { type Bytes, NETWORK, type TArg } from './utils.ts';
4
+ import { type Bytes, NETWORK, type TArg, type TRet } from './utils.ts';
5
5
  /** Minimal output target accepted by the UTXO selector. */
6
6
  export type Output = {
7
7
  address: string;
@@ -17,6 +17,21 @@ export type Accumulated = {
17
17
  weight: number;
18
18
  total: bigint;
19
19
  } | undefined;
20
+ /**
21
+ * Removes Taproot spend paths that cannot be satisfied by the supplied Schnorr public keys.
22
+ * Non-Taproot inputs are retained, and caller-owned input metadata is never mutated.
23
+ * @param inputs - candidate PSBT input records to filter
24
+ * @param pubkeys - available x-only Schnorr public keys
25
+ * @returns Copies of inputs that retain at least one available path
26
+ * @example
27
+ * Filter wallet UTXOs before selecting coins.
28
+ * ```ts
29
+ * import { filterTaproot } from '@scure/btc-signer/utxo.js';
30
+ * import { pubSchnorr, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
31
+ * const spendable = filterTaproot([], [pubSchnorr(randomPrivateKeyBytes())]);
32
+ * ```
33
+ */
34
+ export declare function filterTaproot(inputs: TArg<psbt.TransactionInputUpdate[]>, pubkeys: TArg<Bytes[]>): TRet<psbt.TransactionInputUpdate[]>;
20
35
  export declare const _cmpBig: (a: bigint, b: bigint) => 0 | 1 | -1;
21
36
  /** Options for fee estimation and UTXO selection. */
22
37
  export type EstimatorOpts = TxOpts & {
@@ -30,6 +45,8 @@ export type EstimatorOpts = TxOpts & {
30
45
  createTx?: boolean;
31
46
  requiredInputs?: psbt.TransactionInputUpdate[];
32
47
  allowSameUtxo?: boolean;
48
+ /** Filter Taproot candidate paths to those satisfiable by these Schnorr public keys. */
49
+ filterTaproot?: Bytes[];
33
50
  };
34
51
  type SortStrategy = 'Newest' | 'Oldest' | 'Smallest' | 'Biggest';
35
52
  type ExactStrategy = `exact${SortStrategy}`;
@@ -118,6 +135,25 @@ export declare class _Estimator {
118
135
  }>][] | undefined;
119
136
  tapInternalKey?: Bytes | undefined;
120
137
  tapMerkleRoot?: Bytes | undefined;
138
+ p2cKeyTweak?: [Bytes, Bytes][] | undefined;
139
+ musig2ParticipantPubkeys?: [Bytes, Bytes[]][] | undefined;
140
+ musig2PubNonce?: [TArg<{
141
+ participantPubkey: Bytes;
142
+ aggregatePubkey: Bytes;
143
+ leafHash?: Bytes;
144
+ }>, Bytes][] | undefined;
145
+ musig2PartialSig?: [TArg<{
146
+ participantPubkey: Bytes;
147
+ aggregatePubkey: Bytes;
148
+ leafHash?: Bytes;
149
+ }>, Bytes][] | undefined;
150
+ spEcdhShare?: [Bytes, Bytes][] | undefined;
151
+ spDleq?: [Bytes, Bytes][] | undefined;
152
+ spSpendBip32Derivation?: [Bytes, P.StructInput<{
153
+ fingerprint: number;
154
+ path: number[];
155
+ }>][] | undefined;
156
+ spTweak?: Bytes | undefined;
121
157
  proprietary?: [Bytes, Bytes][] | undefined;
122
158
  } & {
123
159
  unknown?: [P.UnwrapCoder<P.CoderType<P.StructInput<{
@@ -270,6 +306,74 @@ export declare class _Estimator {
270
306
  }])[]) | undefined;
271
307
  tapInternalKey?: (Bytes & Uint8Array<ArrayBuffer>) | undefined;
272
308
  tapMerkleRoot?: (Bytes & Uint8Array<ArrayBuffer>) | undefined;
309
+ p2cKeyTweak?: ([Bytes, Bytes][] & ([Bytes, Bytes] & [Bytes & Uint8Array<ArrayBuffer>, Bytes & Uint8Array<ArrayBuffer>])[]) | undefined;
310
+ musig2ParticipantPubkeys?: ([Bytes, Bytes[]][] & ([Bytes, Bytes[]] & [Bytes & Uint8Array<ArrayBuffer>, Bytes[] & (Bytes & Uint8Array<ArrayBuffer>)[]])[]) | undefined;
311
+ musig2PubNonce?: ([TArg<{
312
+ participantPubkey: Bytes;
313
+ aggregatePubkey: Bytes;
314
+ leafHash?: Bytes;
315
+ }>, Bytes][] & ([TArg<{
316
+ participantPubkey: Bytes;
317
+ aggregatePubkey: Bytes;
318
+ leafHash?: Bytes;
319
+ }>, Bytes] & [({
320
+ participantPubkey: Bytes;
321
+ aggregatePubkey: Bytes;
322
+ leafHash?: Bytes;
323
+ } & {
324
+ participantPubkey: Bytes & Uint8Array<ArrayBuffer>;
325
+ aggregatePubkey: Bytes & Uint8Array<ArrayBuffer>;
326
+ leafHash?: (Bytes & Uint8Array<ArrayBuffer>) | undefined;
327
+ }) | ({
328
+ participantPubkey: TArg<Bytes>;
329
+ aggregatePubkey: TArg<Bytes>;
330
+ leafHash?: TArg<Bytes | undefined>;
331
+ } & {
332
+ participantPubkey: (Bytes & Uint8Array<ArrayBuffer>) | (Uint8Array<ArrayBufferLike> & Uint8Array<ArrayBuffer>);
333
+ aggregatePubkey: (Bytes & Uint8Array<ArrayBuffer>) | (Uint8Array<ArrayBufferLike> & Uint8Array<ArrayBuffer>);
334
+ leafHash?: (Bytes & Uint8Array<ArrayBuffer>) | (Uint8Array<ArrayBufferLike> & Uint8Array<ArrayBuffer>) | undefined;
335
+ }), Bytes & Uint8Array<ArrayBuffer>])[]) | undefined;
336
+ musig2PartialSig?: ([TArg<{
337
+ participantPubkey: Bytes;
338
+ aggregatePubkey: Bytes;
339
+ leafHash?: Bytes;
340
+ }>, Bytes][] & ([TArg<{
341
+ participantPubkey: Bytes;
342
+ aggregatePubkey: Bytes;
343
+ leafHash?: Bytes;
344
+ }>, Bytes] & [({
345
+ participantPubkey: Bytes;
346
+ aggregatePubkey: Bytes;
347
+ leafHash?: Bytes;
348
+ } & {
349
+ participantPubkey: Bytes & Uint8Array<ArrayBuffer>;
350
+ aggregatePubkey: Bytes & Uint8Array<ArrayBuffer>;
351
+ leafHash?: (Bytes & Uint8Array<ArrayBuffer>) | undefined;
352
+ }) | ({
353
+ participantPubkey: TArg<Bytes>;
354
+ aggregatePubkey: TArg<Bytes>;
355
+ leafHash?: TArg<Bytes | undefined>;
356
+ } & {
357
+ participantPubkey: (Bytes & Uint8Array<ArrayBuffer>) | (Uint8Array<ArrayBufferLike> & Uint8Array<ArrayBuffer>);
358
+ aggregatePubkey: (Bytes & Uint8Array<ArrayBuffer>) | (Uint8Array<ArrayBufferLike> & Uint8Array<ArrayBuffer>);
359
+ leafHash?: (Bytes & Uint8Array<ArrayBuffer>) | (Uint8Array<ArrayBufferLike> & Uint8Array<ArrayBuffer>) | undefined;
360
+ }), Bytes & Uint8Array<ArrayBuffer>])[]) | undefined;
361
+ spEcdhShare?: ([Bytes, Bytes][] & ([Bytes, Bytes] & [Bytes & Uint8Array<ArrayBuffer>, Bytes & Uint8Array<ArrayBuffer>])[]) | undefined;
362
+ spDleq?: ([Bytes, Bytes][] & ([Bytes, Bytes] & [Bytes & Uint8Array<ArrayBuffer>, Bytes & Uint8Array<ArrayBuffer>])[]) | undefined;
363
+ spSpendBip32Derivation?: ([Bytes, P.StructInput<{
364
+ fingerprint: number;
365
+ path: number[];
366
+ }>][] & ([Bytes, P.StructInput<{
367
+ fingerprint: number;
368
+ path: number[];
369
+ }>] & [Bytes & Uint8Array<ArrayBuffer>, {
370
+ path: number[];
371
+ fingerprint: number;
372
+ } & {} & {
373
+ path: number[] & number[];
374
+ fingerprint: number;
375
+ }])[]) | undefined;
376
+ spTweak?: (Bytes & Uint8Array<ArrayBuffer>) | undefined;
273
377
  proprietary?: ([Bytes, Bytes][] & ([Bytes, Bytes] & [Bytes & Uint8Array<ArrayBuffer>, Bytes & Uint8Array<ArrayBuffer>])[]) | undefined;
274
378
  unknown?: ([P.StructInput<{
275
379
  type: number;
@@ -383,6 +487,25 @@ export declare function selectUTXO(inputs: TArg<psbt.TransactionInputUpdate[]>,
383
487
  }>][] | undefined;
384
488
  tapInternalKey?: Bytes | undefined;
385
489
  tapMerkleRoot?: Bytes | undefined;
490
+ p2cKeyTweak?: [Bytes, Bytes][] | undefined;
491
+ musig2ParticipantPubkeys?: [Bytes, Bytes[]][] | undefined;
492
+ musig2PubNonce?: [TArg<{
493
+ participantPubkey: Bytes;
494
+ aggregatePubkey: Bytes;
495
+ leafHash?: Bytes;
496
+ }>, Bytes][] | undefined;
497
+ musig2PartialSig?: [TArg<{
498
+ participantPubkey: Bytes;
499
+ aggregatePubkey: Bytes;
500
+ leafHash?: Bytes;
501
+ }>, Bytes][] | undefined;
502
+ spEcdhShare?: [Bytes, Bytes][] | undefined;
503
+ spDleq?: [Bytes, Bytes][] | undefined;
504
+ spSpendBip32Derivation?: [Bytes, P.StructInput<{
505
+ fingerprint: number;
506
+ path: number[];
507
+ }>][] | undefined;
508
+ spTweak?: Bytes | undefined;
386
509
  proprietary?: [Bytes, Bytes][] | undefined;
387
510
  } & {
388
511
  unknown?: [P.UnwrapCoder<P.CoderType<P.StructInput<{
@@ -535,6 +658,74 @@ export declare function selectUTXO(inputs: TArg<psbt.TransactionInputUpdate[]>,
535
658
  }])[]) | undefined;
536
659
  tapInternalKey?: (Bytes & Uint8Array<ArrayBuffer>) | undefined;
537
660
  tapMerkleRoot?: (Bytes & Uint8Array<ArrayBuffer>) | undefined;
661
+ p2cKeyTweak?: ([Bytes, Bytes][] & ([Bytes, Bytes] & [Bytes & Uint8Array<ArrayBuffer>, Bytes & Uint8Array<ArrayBuffer>])[]) | undefined;
662
+ musig2ParticipantPubkeys?: ([Bytes, Bytes[]][] & ([Bytes, Bytes[]] & [Bytes & Uint8Array<ArrayBuffer>, Bytes[] & (Bytes & Uint8Array<ArrayBuffer>)[]])[]) | undefined;
663
+ musig2PubNonce?: ([TArg<{
664
+ participantPubkey: Bytes;
665
+ aggregatePubkey: Bytes;
666
+ leafHash?: Bytes;
667
+ }>, Bytes][] & ([TArg<{
668
+ participantPubkey: Bytes;
669
+ aggregatePubkey: Bytes;
670
+ leafHash?: Bytes;
671
+ }>, Bytes] & [({
672
+ participantPubkey: Bytes;
673
+ aggregatePubkey: Bytes;
674
+ leafHash?: Bytes;
675
+ } & {
676
+ participantPubkey: Bytes & Uint8Array<ArrayBuffer>;
677
+ aggregatePubkey: Bytes & Uint8Array<ArrayBuffer>;
678
+ leafHash?: (Bytes & Uint8Array<ArrayBuffer>) | undefined;
679
+ }) | ({
680
+ participantPubkey: TArg<Bytes>;
681
+ aggregatePubkey: TArg<Bytes>;
682
+ leafHash?: TArg<Bytes | undefined>;
683
+ } & {
684
+ participantPubkey: (Bytes & Uint8Array<ArrayBuffer>) | (Uint8Array<ArrayBufferLike> & Uint8Array<ArrayBuffer>);
685
+ aggregatePubkey: (Bytes & Uint8Array<ArrayBuffer>) | (Uint8Array<ArrayBufferLike> & Uint8Array<ArrayBuffer>);
686
+ leafHash?: (Bytes & Uint8Array<ArrayBuffer>) | (Uint8Array<ArrayBufferLike> & Uint8Array<ArrayBuffer>) | undefined;
687
+ }), Bytes & Uint8Array<ArrayBuffer>])[]) | undefined;
688
+ musig2PartialSig?: ([TArg<{
689
+ participantPubkey: Bytes;
690
+ aggregatePubkey: Bytes;
691
+ leafHash?: Bytes;
692
+ }>, Bytes][] & ([TArg<{
693
+ participantPubkey: Bytes;
694
+ aggregatePubkey: Bytes;
695
+ leafHash?: Bytes;
696
+ }>, Bytes] & [({
697
+ participantPubkey: Bytes;
698
+ aggregatePubkey: Bytes;
699
+ leafHash?: Bytes;
700
+ } & {
701
+ participantPubkey: Bytes & Uint8Array<ArrayBuffer>;
702
+ aggregatePubkey: Bytes & Uint8Array<ArrayBuffer>;
703
+ leafHash?: (Bytes & Uint8Array<ArrayBuffer>) | undefined;
704
+ }) | ({
705
+ participantPubkey: TArg<Bytes>;
706
+ aggregatePubkey: TArg<Bytes>;
707
+ leafHash?: TArg<Bytes | undefined>;
708
+ } & {
709
+ participantPubkey: (Bytes & Uint8Array<ArrayBuffer>) | (Uint8Array<ArrayBufferLike> & Uint8Array<ArrayBuffer>);
710
+ aggregatePubkey: (Bytes & Uint8Array<ArrayBuffer>) | (Uint8Array<ArrayBufferLike> & Uint8Array<ArrayBuffer>);
711
+ leafHash?: (Bytes & Uint8Array<ArrayBuffer>) | (Uint8Array<ArrayBufferLike> & Uint8Array<ArrayBuffer>) | undefined;
712
+ }), Bytes & Uint8Array<ArrayBuffer>])[]) | undefined;
713
+ spEcdhShare?: ([Bytes, Bytes][] & ([Bytes, Bytes] & [Bytes & Uint8Array<ArrayBuffer>, Bytes & Uint8Array<ArrayBuffer>])[]) | undefined;
714
+ spDleq?: ([Bytes, Bytes][] & ([Bytes, Bytes] & [Bytes & Uint8Array<ArrayBuffer>, Bytes & Uint8Array<ArrayBuffer>])[]) | undefined;
715
+ spSpendBip32Derivation?: ([Bytes, P.StructInput<{
716
+ fingerprint: number;
717
+ path: number[];
718
+ }>][] & ([Bytes, P.StructInput<{
719
+ fingerprint: number;
720
+ path: number[];
721
+ }>] & [Bytes & Uint8Array<ArrayBuffer>, {
722
+ path: number[];
723
+ fingerprint: number;
724
+ } & {} & {
725
+ path: number[] & number[];
726
+ fingerprint: number;
727
+ }])[]) | undefined;
728
+ spTweak?: (Bytes & Uint8Array<ArrayBuffer>) | undefined;
538
729
  proprietary?: ([Bytes, Bytes][] & ([Bytes, Bytes] & [Bytes & Uint8Array<ArrayBuffer>, Bytes & Uint8Array<ArrayBuffer>])[]) | undefined;
539
730
  unknown?: ([P.StructInput<{
540
731
  type: number;
package/utxo.js CHANGED
@@ -1,81 +1,169 @@
1
1
  import { hex } from '@scure/base';
2
2
  import * as P from 'micro-packed';
3
- import { Address, OutScript, checkScript, tapLeafHash } from "./payment.js";
3
+ import { Address, OutScript, _WitnessOutScript, checkScript, tapLeafHash, } from "./payment.js";
4
4
  import * as psbt from "./psbt.js";
5
- import { CompactSizeLen, Script } from "./script.js";
5
+ import { CompactSizeLen, RawWitness, Script } from "./script.js";
6
6
  import { SignatureHash, Transaction, getInputType, getPrevOut, inputBeforeSign, normalizeInput, toVsize, } from "./transaction.js";
7
- import { abigint, aarray, astring, NETWORK, PubT, TAPROOT_UNSPENDABLE_KEY, compareBytes, equalBytes, isBytes, sha256, validatePubkey, validateObject, } from "./utils.js";
7
+ import { abigint, aarray, astring, NETWORK, PubT, compareBytes, equalBytes, isBytes, sha256, taprootNumsKey, taprootTweakPubkey, validatePubkey, validateObject, } from "./utils.js";
8
8
  const encodeTapBlock = (item) => psbt.TaprootControlBlock.encode(item);
9
9
  // Be friendly to bad ECMAScript parsers by not using bigint literals.
10
10
  // prettier-ignore
11
11
  const _0n = /* @__PURE__ */ BigInt(0), _3n = /* @__PURE__ */ BigInt(3);
12
12
  // Serialized length of VarBytes(data) without allocating the encoded copy
13
13
  const varLen = (dataLen) => CompactSizeLen.encode(dataLen).length + dataLen;
14
+ const tapLeafWitness = (leaf, sigSize, customScripts, pubkeys, unknownError = 'Finalize: Unknown tapLeafScript') => {
15
+ const [cb, _script] = leaf;
16
+ const _customScripts = customScripts;
17
+ // Last byte is version
18
+ const script = _script.slice(0, -1);
19
+ const ver = _script[_script.length - 1];
20
+ const outs = OutScript.decode(script);
21
+ const available = (pubkey) => !pubkeys || pubkeys.has(hex.encode(pubkey));
22
+ const empty = () => new Uint8Array(sigSize);
23
+ let signatures = [];
24
+ if (outs.type === 'tr_ms') {
25
+ let added = 0;
26
+ for (const pubkey of outs.pubkeys) {
27
+ if (added === outs.m || !available(pubkey))
28
+ signatures.push(P.EMPTY);
29
+ else {
30
+ signatures.push(empty());
31
+ added++;
32
+ }
33
+ }
34
+ if (added !== outs.m)
35
+ return;
36
+ }
37
+ else if (outs.type === 'tr_ns') {
38
+ for (const pubkey of outs.pubkeys) {
39
+ if (!available(pubkey))
40
+ return;
41
+ signatures.push(empty());
42
+ }
43
+ }
44
+ else {
45
+ if (!_customScripts)
46
+ throw new Error(unknownError);
47
+ const leafHash = tapLeafHash(script, ver);
48
+ const scriptDecoded = Script.decode(script);
49
+ const scriptPubkeys = scriptDecoded.filter((i) => {
50
+ if (!isBytes(i))
51
+ return false;
52
+ try {
53
+ validatePubkey(i, PubT.schnorr);
54
+ return true;
55
+ }
56
+ catch (e) {
57
+ return false;
58
+ }
59
+ });
60
+ const availablePubkeys = scriptPubkeys.filter(available);
61
+ // A custom finalizer may treat an unexpected empty signature list as malformed input. If the
62
+ // script embeds keys but none are owned, the path is unavailable without consulting the hook.
63
+ if (pubkeys && scriptPubkeys.length && !availablePubkeys.length)
64
+ return;
65
+ let recognized = false;
66
+ for (const c of _customScripts) {
67
+ if (!c.finalizeTaproot)
68
+ continue;
69
+ const csEncoded = c.encode(scriptDecoded);
70
+ if (csEncoded === undefined)
71
+ continue;
72
+ recognized = true;
73
+ const finalized = c.finalizeTaproot(script, csEncoded, availablePubkeys.map((pubKey) => [{ pubKey, leafHash }, empty()]));
74
+ if (finalized)
75
+ return finalized.concat(encodeTapBlock(cb));
76
+ }
77
+ if (recognized && pubkeys)
78
+ return;
79
+ throw new Error(unknownError);
80
+ }
81
+ // Witness is stack, so last element will be used first
82
+ return signatures.reverse().concat([script, encodeTapBlock(cb)]);
83
+ };
14
84
  function iterLeafs(tapLeafScript, sigSize, customScripts) {
15
85
  const _tapLeafScript = tapLeafScript;
16
86
  const _customScripts = customScripts;
17
87
  if (!_tapLeafScript || !_tapLeafScript.length)
18
88
  throw new Error('no leafs');
19
- // Dummy non-empty Schnorr signature bytes for weight estimation.
20
- // Unsigned tr_ms slots use P.EMPTY below.
21
- const empty = () => new Uint8Array(sigSize);
22
- // If user want to select specific leaf, which can signed,
23
- // it is possible to remove all other leafs manually.
24
- // Sort leafs by control block length.
25
- const leafs = _tapLeafScript.sort((a, b) => encodeTapBlock(a[0]).length - encodeTapBlock(b[0]).length);
26
- for (const [cb, _script] of leafs) {
27
- // Last byte is version
28
- const script = _script.slice(0, -1);
29
- const ver = _script[_script.length - 1];
30
- const outs = OutScript.decode(script);
31
- let signatures = [];
32
- if (outs.type === 'tr_ms') {
33
- const m = outs.m;
34
- const n = outs.pubkeys.length - m;
35
- for (let i = 0; i < m; i++)
36
- signatures.push(empty());
37
- for (let i = 0; i < n; i++)
38
- signatures.push(P.EMPTY);
89
+ // Start with the old shallowest-path order for stable equal-weight ties. Full witness size can
90
+ // reverse that order when a shallow leaf needs a larger script or more signatures.
91
+ const leafs = _tapLeafScript
92
+ .slice()
93
+ .sort((a, b) => encodeTapBlock(a[0]).length - encodeTapBlock(b[0]).length);
94
+ let smallest;
95
+ let smallestSize = Number.POSITIVE_INFINITY;
96
+ for (const leaf of leafs) {
97
+ const witness = tapLeafWitness(leaf, sigSize, _customScripts);
98
+ if (!witness)
99
+ continue;
100
+ const size = RawWitness.encode(witness).length;
101
+ if (size >= smallestSize)
102
+ continue;
103
+ smallest = witness;
104
+ smallestSize = size;
105
+ }
106
+ if (!smallest)
107
+ throw new Error('there was no witness');
108
+ return smallest;
109
+ }
110
+ /**
111
+ * Removes Taproot spend paths that cannot be satisfied by the supplied Schnorr public keys.
112
+ * Non-Taproot inputs are retained, and caller-owned input metadata is never mutated.
113
+ * @param inputs - candidate PSBT input records to filter
114
+ * @param pubkeys - available x-only Schnorr public keys
115
+ * @returns Copies of inputs that retain at least one available path
116
+ * @example
117
+ * Filter wallet UTXOs before selecting coins.
118
+ * ```ts
119
+ * import { filterTaproot } from '@scure/btc-signer/utxo.js';
120
+ * import { pubSchnorr, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
121
+ * const spendable = filterTaproot([], [pubSchnorr(randomPrivateKeyBytes())]);
122
+ * ```
123
+ */
124
+ export function filterTaproot(inputs, pubkeys) {
125
+ aarray(inputs, 'inputs');
126
+ aarray(pubkeys, 'pubkeys');
127
+ const _inputs = inputs;
128
+ const _pubkeys = pubkeys;
129
+ const keys = new Set();
130
+ for (const pubkey of _pubkeys)
131
+ keys.add(hex.encode(validatePubkey(pubkey, PubT.schnorr)));
132
+ const res = [];
133
+ for (const input of _inputs) {
134
+ const filtered = { ...input };
135
+ // BIP371 path fields are optional and empty keyed lists encode as absence, so the committed
136
+ // previous-output script is the authoritative input type.
137
+ const prevScript = _WitnessOutScript.decode(getPrevOut(filtered).script);
138
+ if (prevScript.type !== 'tr') {
139
+ res.push(filtered);
140
+ continue;
39
141
  }
40
- else if (outs.type === 'tr_ns') {
41
- for (const _pub of outs.pubkeys)
42
- signatures.push(empty());
142
+ if (filtered.tapInternalKey) {
143
+ // A control block can reconstruct the root, but BIP371 does not require that inference.
144
+ // Omission may be broken data or an earlier filter deliberately disabling the key path.
145
+ // Recognize root omission only when the prevout proves this is an empty-tree commitment.
146
+ const hasRoot = filtered.tapMerkleRoot !== undefined ||
147
+ equalBytes(taprootTweakPubkey(filtered.tapInternalKey, P.EMPTY)[0], prevScript.pubkey);
148
+ if (!hasRoot ||
149
+ equalBytes(filtered.tapInternalKey, taprootNumsKey()) ||
150
+ (!keys.has(hex.encode(filtered.tapInternalKey)) && !keys.has(hex.encode(prevScript.pubkey))))
151
+ delete filtered.tapInternalKey;
43
152
  }
44
- else {
45
- if (!_customScripts)
46
- throw new Error('Finalize: Unknown tapLeafScript');
47
- const leafHash = tapLeafHash(script, ver);
48
- for (const c of _customScripts) {
49
- if (!c.finalizeTaproot)
50
- continue;
51
- const scriptDecoded = Script.decode(script);
52
- const csEncoded = c.encode(scriptDecoded);
53
- if (csEncoded === undefined)
54
- continue;
55
- const pubKeys = scriptDecoded.filter((i) => {
56
- if (!isBytes(i))
57
- return false;
58
- try {
59
- validatePubkey(i, PubT.schnorr);
60
- return true;
61
- }
62
- catch (e) {
63
- return false;
64
- }
65
- });
66
- const finalized = c.finalizeTaproot(script, csEncoded, pubKeys.map((pubKey) => [{ pubKey, leafHash }, empty()]));
67
- if (!finalized)
68
- continue;
69
- return finalized.concat(encodeTapBlock(cb));
70
- }
71
- // UTXO selection may run without the real signer/finalizer process. When no matching local
72
- // finalizeTaproot hook exists, keep a minimal script-path witness lower bound here instead of
73
- // failing selection; callers that need exact fee estimates must provide the matching hook.
153
+ if (filtered.tapLeafScript) {
154
+ const sigSize = filtered.sighashType !== undefined && filtered.sighashType !== SignatureHash.DEFAULT
155
+ ? 65
156
+ : 64;
157
+ const leafs = filtered.tapLeafScript.filter((leaf) => tapLeafWitness(leaf, sigSize, undefined, keys, 'filterTaproot: unknown Taproot leaf'));
158
+ if (leafs.length)
159
+ filtered.tapLeafScript = leafs;
160
+ else
161
+ delete filtered.tapLeafScript;
74
162
  }
75
- // Witness is stack, so last element will be used first
76
- return signatures.reverse().concat([script, encodeTapBlock(cb)]);
163
+ if (filtered.tapInternalKey || filtered.tapLeafScript)
164
+ res.push(filtered);
77
165
  }
78
- throw new Error('there was no witness');
166
+ return res;
79
167
  }
80
168
  function estimateInput(inputType, input, opts) {
81
169
  const _input = input;
@@ -85,13 +173,9 @@ function estimateInput(inputType, input, opts) {
85
173
  // schnorr sig is always 64 bytes. except for cases when sighash is not default!
86
174
  if (inputType.txType === 'taproot') {
87
175
  const SCHNORR_SIG_SIZE = inputType.sighash !== SignatureHash.DEFAULT ? 65 : 64;
88
- // BIP371 `PSBT_IN_TAP_INTERNAL_KEY` is signer metadata, but UTXO selection
89
- // runs before signer availability is known. We intentionally treat a
90
- // present internal key as a key-path hint here to avoid overestimating fees
91
- // on the online side. Callers that know only script-path signing is
92
- // possible should omit `tapInternalKey` or pre-filter `tapLeafScript`
93
- // before estimation.
94
- if (_input.tapInternalKey && !equalBytes(_input.tapInternalKey, TAPROOT_UNSPENDABLE_KEY)) {
176
+ // A real internal key and every retained leaf declare paths available to this caller. Use
177
+ // filterTaproot before estimation when the supplied input contains unavailable paths.
178
+ if (_input.tapInternalKey && !equalBytes(_input.tapInternalKey, taprootNumsKey())) {
95
179
  witness = [new Uint8Array(SCHNORR_SIG_SIZE)];
96
180
  }
97
181
  else if (_input.tapLeafScript) {
@@ -195,7 +279,7 @@ function getScript(o, opts = {}, network = NETWORK) {
195
279
  // Keep selector-only `createTx: false` flows aligned with the transaction/PSBT output boundary:
196
280
  // satoshi-denominated outputs are not allowed to go negative.
197
281
  abigint(_o.amount, 'output.amount');
198
- if (script && !_opts.allowUnknownOutputs && OutScript.decode(script).type === 'unknown') {
282
+ if (script && !_opts.allowUnknownOutputs && _WitnessOutScript.decode(script).type === 'unknown') {
199
283
  throw new Error('Estimator: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownOutputs=true, if you sure');
200
284
  }
201
285
  if (!_opts.disableScriptCheck)
@@ -217,6 +301,9 @@ export class _Estimator {
217
301
  outputs;
218
302
  opts;
219
303
  constructor(inputs, outputs, opts) {
304
+ // EstimatorOpts extends TxOpts, so resolve the complete transaction policy once even when
305
+ // createTx=false. This keeps selection normalization and a returned Transaction identical.
306
+ opts = new Transaction(opts).opts;
220
307
  this.outputs = outputs;
221
308
  this.opts = opts;
222
309
  // Zero-fee estimation is useful on regtest/in tests, but negative fee rates would make
@@ -273,7 +360,7 @@ export class _Estimator {
273
360
  }
274
361
  const inputKeys = new Set();
275
362
  this.normalizedInputs = allInputs.map((i) => {
276
- const normalized = normalizeInput(i, undefined, undefined, opts.disableScriptCheck, opts.allowUnknown);
363
+ const normalized = normalizeInput(i, undefined, undefined, opts.disableScriptCheck, opts.unknown, opts.proprietary);
277
364
  inputBeforeSign(normalized); // check fields
278
365
  const key = `${hex.encode(normalized.txid)}:${normalized.index}`;
279
366
  if (!opts.allowSameUtxo && inputKeys.has(key))
@@ -557,6 +644,16 @@ export function selectUTXO(inputs, outputs, strategy, opts) {
557
644
  astring(strategy, 'strategy');
558
645
  // Public wrapper defaults to BIP69 ordering and tx construction unless callers override them.
559
646
  const _opts = { createTx: true, bip69: true, ...opts };
560
- const est = new _Estimator(inputs, outputs, _opts);
647
+ let candidates = inputs;
648
+ if (_opts.filterTaproot !== undefined) {
649
+ candidates = filterTaproot(candidates, _opts.filterTaproot);
650
+ if (_opts.requiredInputs) {
651
+ const requiredInputs = filterTaproot(_opts.requiredInputs, _opts.filterTaproot);
652
+ if (requiredInputs.length !== _opts.requiredInputs.length)
653
+ throw new Error('filterTaproot: required input has no available Taproot path');
654
+ _opts.requiredInputs = requiredInputs;
655
+ }
656
+ }
657
+ const est = new _Estimator(candidates, outputs, _opts);
561
658
  return est.result(strategy);
562
659
  }