@bitcoinerlab/descriptors 3.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -139,6 +139,22 @@ function parseSortedMulti(inner) {
139
139
  throw new Error(`sortedmulti(): descriptors support up to 20 keys (per BIP 380/383).`);
140
140
  return { m, keyExpressions };
141
141
  }
142
+ const MAX_PUBKEYS_PER_MULTI_A = 999;
143
+ // Helper: parse sortedmulti_a(M, k1, k2,...)
144
+ function parseSortedMultiA(inner) {
145
+ const parts = inner.split(',').map(p => p.trim());
146
+ if (parts.length < 2)
147
+ throw new Error(`sortedmulti_a(): must contain M and at least one key: ${inner}`);
148
+ const m = Number(parts[0]);
149
+ if (!Number.isInteger(m) || m < 1)
150
+ throw new Error(`sortedmulti_a(): invalid M=${parts[0]}`);
151
+ const keyExpressions = parts.slice(1);
152
+ if (keyExpressions.length < m)
153
+ throw new Error(`sortedmulti_a(): M cannot exceed number of keys: ${inner}`);
154
+ if (keyExpressions.length > MAX_PUBKEYS_PER_MULTI_A)
155
+ throw new Error(`sortedmulti_a(): descriptors support up to ${MAX_PUBKEYS_PER_MULTI_A} keys.`);
156
+ return { m, keyExpressions };
157
+ }
142
158
  function parseTrExpression(expression) {
143
159
  if (!expression.startsWith('tr(') || !expression.endsWith(')'))
144
160
  throw new Error(`Error: invalid descriptor ${expression}`);
@@ -205,6 +221,69 @@ function DescriptorsFactory(ecc) {
205
221
  BIP32
206
222
  });
207
223
  };
224
+ /**
225
+ * Builds a taproot leaf expansion override for descriptor-level
226
+ * `sortedmulti_a(...)`.
227
+ *
228
+ * Why this exists:
229
+ * - `sortedmulti_a` is a descriptor script expression (not a Miniscript
230
+ * fragment).
231
+ *
232
+ * What this does:
233
+ * - Resolves each key expression to a concrete pubkey and builds a leaf-local
234
+ * placeholder map (`@0`, `@1`, ... in input order).
235
+ * - Derives the internal compilation form by sorting placeholders by pubkey
236
+ * bytes and lowering to `multi_a(...)`.
237
+ * - Compiles tapscript from that lowered form and returns it as override
238
+ * data.
239
+ *
240
+ * Returns `undefined` for non-`sortedmulti_a` leaves so normal taproot miniscript
241
+ * expansion/compilation is used.
242
+ */
243
+ function buildTapLeafSortedMultiAOverride({ expression, network = bitcoinjs_lib_1.networks.bitcoin }) {
244
+ if (!/\bsortedmulti_a\(/.test(expression))
245
+ return undefined;
246
+ const trimmed = expression.trim();
247
+ const match = trimmed.match(/^sortedmulti_a\((.*)\)$/);
248
+ if (!match)
249
+ throw new Error(`Error: sortedmulti_a() must be a standalone taproot leaf expression`);
250
+ const inner = match[1];
251
+ if (!inner)
252
+ throw new Error(`Error: invalid sortedmulti_a() expression: ${expression}`);
253
+ const { m, keyExpressions } = parseSortedMultiA(inner);
254
+ const keyInfos = keyExpressions.map(keyExpression => {
255
+ const keyInfo = parseKeyExpression({
256
+ keyExpression,
257
+ isSegwit: true,
258
+ isTaproot: true,
259
+ network
260
+ });
261
+ if (!keyInfo.pubkey)
262
+ throw new Error(`Error: sortedmulti_a() key must resolve to a concrete pubkey: ${keyExpression}`);
263
+ return keyInfo;
264
+ });
265
+ const expansionMap = {};
266
+ keyInfos.forEach((keyInfo, index) => {
267
+ expansionMap[`@${index}`] = keyInfo;
268
+ });
269
+ // sortedmulti_a is descriptor-level sugar. We preserve it in
270
+ // expandedExpression, but compile tapscript from its internal multi_a
271
+ // lowering with sorted placeholders.
272
+ const expandedExpression = `sortedmulti_a(${[
273
+ m,
274
+ ...Object.keys(expansionMap)
275
+ ].join(',')})`;
276
+ const compileExpandedMiniscript = (0, tapMiniscript_1.compileSortedMultiAExpandedExpression)({
277
+ expandedExpression,
278
+ expansionMap
279
+ });
280
+ const tapScript = (0, miniscript_1.miniscript2Script)({
281
+ expandedMiniscript: compileExpandedMiniscript,
282
+ expansionMap,
283
+ tapscript: true
284
+ });
285
+ return { expandedExpression, expansionMap, tapScript };
286
+ }
208
287
  function expand({ descriptor, index, change, checksumRequired = false, network = bitcoinjs_lib_1.networks.bitcoin, allowMiniscriptInP2SH = false }) {
209
288
  if (!descriptor)
210
289
  throw new Error(`descriptor not provided`);
@@ -503,10 +582,14 @@ function DescriptorsFactory(ecc) {
503
582
  if (!miniscript)
504
583
  throw new Error(`Error: could not get miniscript in ${descriptor}`);
505
584
  if (allowMiniscriptInP2SH === false &&
506
- //These top-level expressions within sh are allowed within sh.
507
- //They can be parsed with miniscript2Script, but first we must make sure
508
- //that other expressions are not accepted (unless forced with allowMiniscriptInP2SH).
509
- miniscript.search(/^(pk\(|pkh\(|wpkh\(|combo\(|multi\(|sortedmulti\(|multi_a\(|sortedmulti_a\()/) !== 0) {
585
+ // These top-level script expressions are allowed inside sh(...).
586
+ // The sorted script expressions (`sortedmulti`, `sortedmulti_a`) are
587
+ // handled in dedicated descriptor/taproot branches and are intentionally
588
+ // not part of this miniscript gate.
589
+ // Here we only keep legacy top-level forms to avoid accepting arbitrary
590
+ // miniscript in P2SH unless explicitly enabled.
591
+ miniscript.search(/^(pk\(|pkh\(|wpkh\(|combo\(|multi\(|multi_a\()/) !==
592
+ 0) {
510
593
  throw new Error(`Error: Miniscript expressions can only be used in wsh`);
511
594
  }
512
595
  ({ expandedMiniscript, expansionMap } = expandMiniscript({
@@ -567,7 +650,18 @@ function DescriptorsFactory(ecc) {
567
650
  tapTreeExpression = treeExpression;
568
651
  tapTree = (0, tapTree_1.parseTapTreeExpression)(treeExpression);
569
652
  if (!isCanonicalRanged) {
570
- tapTreeInfo = (0, tapMiniscript_1.buildTapTreeInfo)({ tapTree, network, BIP32, ECPair });
653
+ tapTreeInfo = (0, tapMiniscript_1.buildTapTreeInfo)({
654
+ tapTree,
655
+ network,
656
+ BIP32,
657
+ ECPair,
658
+ // `leafExpansionOverride` runs per leaf expression.
659
+ // For non-matching leaves it returns undefined and
660
+ // normal miniscript expansion is used;
661
+ // for sortedmulti_a leaves it returns descriptor-level
662
+ // metadata plus precompiled tapscript bytes.
663
+ leafExpansionOverride: (expression) => buildTapLeafSortedMultiAOverride({ expression, network })
664
+ });
571
665
  }
572
666
  }
573
667
  if (!isCanonicalRanged) {
package/dist/ledger.js CHANGED
@@ -268,18 +268,16 @@ async function ledgerPolicyFromPsbtInput({ ledgerManager, psbt, index }) {
268
268
  // Replace change (making sure the value in the change level for the
269
269
  // template of the policy meets the change in bip32Derivation):
270
270
  descriptor = descriptor.replace(/\/\*\*/g, `/<0;1>/*`);
271
- const regExpMN = new RegExp(`/<(\\d+);(\\d+)>`, 'g');
272
- let matchMN;
273
- while (descriptor && (matchMN = regExpMN.exec(descriptor)) !== null) {
274
- const [M, N] = [
275
- parseInt(matchMN[1], 10),
276
- parseInt(matchMN[2], 10)
277
- ];
271
+ let tupleMismatch = false;
272
+ descriptor = descriptor.replace(/\/<(\d+);(\d+)>/g, (token, strM, strN) => {
273
+ const [M, N] = [parseInt(strM, 10), parseInt(strN, 10)];
278
274
  if (M === change || N === change)
279
- descriptor = descriptor.replace(`/<${M};${N}>`, `/${change}`);
280
- else
281
- descriptor = undefined;
282
- }
275
+ return `/${change}`;
276
+ tupleMismatch = true;
277
+ return token;
278
+ });
279
+ if (tupleMismatch)
280
+ descriptor = undefined;
283
281
  if (descriptor) {
284
282
  // Replace index:
285
283
  descriptor = descriptor.replace(/\/\*/g, `/${index}`);
@@ -382,13 +380,20 @@ async function ledgerPolicyFromOutput({ output, ledgerManager }) {
382
380
  return placeholder;
383
381
  };
384
382
  const remapTapTree = (node) => {
385
- if ('miniscript' in node) {
386
- let remappedMiniscript = node.expandedMiniscript;
387
- const localEntries = Object.entries(node.expansionMap).sort(([placeholderA], [placeholderB]) => placeholderB.length - placeholderA.length);
383
+ if ('expression' in node) {
384
+ // Prefer descriptor-level expanded expression for policy templates so
385
+ // script expressions (e.g. sortedmulti_a) are preserved. Fall back to
386
+ // expandedMiniscript for older metadata.
387
+ let remappedMiniscript = node.expandedExpression ?? node.expandedMiniscript;
388
+ if (!remappedMiniscript)
389
+ throw new Error(`Error: taproot leaf expansion not available`);
390
+ const localEntries = Object.entries(node.expansionMap);
391
+ const localToGlobalPlaceholder = new Map();
388
392
  for (const [localPlaceholder, keyInfo] of localEntries) {
389
393
  const globalPlaceholder = globalPlaceholderFor(keyInfo);
390
- remappedMiniscript = remappedMiniscript.replaceAll(localPlaceholder, globalPlaceholder);
394
+ localToGlobalPlaceholder.set(localPlaceholder, globalPlaceholder);
391
395
  }
396
+ remappedMiniscript = remappedMiniscript.replace(/@\d+/g, placeholder => localToGlobalPlaceholder.get(placeholder) ?? placeholder);
392
397
  return remappedMiniscript;
393
398
  }
394
399
  return `{${remapTapTree(node.left)},${remapTapTree(node.right)}}`;
@@ -400,11 +405,15 @@ async function ledgerPolicyFromOutput({ output, ledgerManager }) {
400
405
  const ledgerMasterFingerprint = await getLedgerMasterFingerPrint({
401
406
  ledgerManager
402
407
  });
403
- //It's important to have keys sorted in ascii order. keys
404
- //are of this type: @0, @1, @2, .... and they also appear in the expandedExpression
405
- //in ascending ascii order. Note that Object.keys(expansionMap ) does not ensure
406
- //that the order is respected and so we force it.
407
- const allKeys = Object.keys(expansionMap).sort();
408
+ // Keep placeholders in numeric order (@0, @1, @2, ...). This avoids
409
+ // lexicographic pitfalls like @10 being ordered before @2.
410
+ const allKeys = Object.keys(expansionMap).sort((a, b) => {
411
+ const aIndex = Number(a.slice(1));
412
+ const bIndex = Number(b.slice(1));
413
+ if (Number.isNaN(aIndex) || Number.isNaN(bIndex))
414
+ return a.localeCompare(b);
415
+ return aIndex - bIndex;
416
+ });
408
417
  const ledgerKeys = allKeys.filter(key => {
409
418
  const masterFingerprint = expansionMap[key]?.masterFingerprint;
410
419
  return (masterFingerprint &&
@@ -425,8 +434,8 @@ async function ledgerPolicyFromOutput({ output, ledgerManager }) {
425
434
  if (!/^\/[01]\/\d+$/.test(keyPath))
426
435
  throw new Error(`Error: key paths must be /<1;0>/index, where change is 1 or 0 and index >= 0`);
427
436
  const keyRoots = [];
428
- let ledgerTemplate = expandedExpression;
429
- allKeys.forEach(key => {
437
+ const placeholderToLedgerPlaceholder = new Map();
438
+ allKeys.forEach((key, index) => {
430
439
  if (key !== ledgerKey) {
431
440
  //This block here only does data integrity assertions:
432
441
  const otherKeyInfo = expansionMap[key];
@@ -442,13 +451,14 @@ async function ledgerPolicyFromOutput({ output, ledgerManager }) {
442
451
  throw new Error(`Error: all keyPaths must be the same for Ledger being able to sign: ${otherKeyInfo.keyPath} !== ${keyPath}`);
443
452
  }
444
453
  }
445
- ledgerTemplate = ledgerTemplate.replaceAll(key, `@${keyRoots.length}/**`);
454
+ placeholderToLedgerPlaceholder.set(key, `@${index}/**`);
446
455
  const keyInfo = expansionMap[key];
447
456
  if (keyInfo.masterFingerprint && keyInfo.originPath)
448
457
  keyRoots.push(`[${(0, uint8array_tools_1.toHex)(keyInfo.masterFingerprint)}${keyInfo.originPath}]${keyInfo?.bip32?.neutered().toBase58()}`);
449
458
  else
450
459
  keyRoots.push(`${keyInfo?.bip32?.neutered().toBase58()}`);
451
460
  });
461
+ const ledgerTemplate = expandedExpression.replace(/@\d+/g, placeholder => placeholderToLedgerPlaceholder.get(placeholder) ?? placeholder);
452
462
  return { ledgerTemplate, keyRoots };
453
463
  }
454
464
  /**
@@ -78,11 +78,16 @@ function expandMiniscript({ miniscript, isSegwit, isTaproot, network = bitcoinjs
78
78
  });
79
79
  return key;
80
80
  };
81
- //These are the fragments where keys are allowed. Note that we only look
82
- //inside these fragments to avoid problems in pregimages like sha256 which can
83
- //contain hex values which could be confussed with a key
84
- const keyFragmentRegex = /\b(pk|pkh|sortedmulti_a|sortedmulti|multi_a|multi)\(([^()]*)\)/g;
85
- const expandedMiniscript = miniscript.replace(keyFragmentRegex, (_, name, inner) => {
81
+ // These are the Miniscript fragments where key arguments are expected.
82
+ // We only replace key expressions inside these fragments to avoid confusing
83
+ // hash preimages (e.g. sha256(03...)) with keys.
84
+ //
85
+ // Note: sorted script expressions (`sortedmulti`, `sortedmulti_a`) are
86
+ // intentionally excluded here. They are descriptor-level expressions (not
87
+ // Miniscript fragments) and are handled in descriptor/tap-tree code paths
88
+ // before Miniscript compilation.
89
+ const keyBearingFragmentRegex = /\b(pk|pkh|multi_a|multi)\(([^()]*)\)/g;
90
+ const expandedMiniscript = miniscript.replace(keyBearingFragmentRegex, (_, name, inner) => {
86
91
  if (name === 'pk' || name === 'pkh')
87
92
  return `${name}(${replaceKeyExpression(inner)})`;
88
93
  //now do *multi* which has arguments:
@@ -3,8 +3,13 @@ import type { BIP32API } from 'bip32';
3
3
  import type { ECPairAPI } from 'ecpair';
4
4
  import type { PartialSig, TapBip32Derivation } from 'bip174';
5
5
  import type { Taptree } from 'bitcoinjs-lib/src/cjs/types';
6
- import type { KeyInfo, Preimage, TimeConstraints } from './types';
6
+ import type { ExpansionMap, KeyInfo, Preimage, TimeConstraints } from './types';
7
7
  import type { TapLeafInfo, TapTreeInfoNode, TapTreeNode } from './tapTree';
8
+ export type TapLeafExpansionOverride = {
9
+ expandedExpression: string;
10
+ expansionMap: ExpansionMap;
11
+ tapScript: Uint8Array;
12
+ };
8
13
  export type TaprootLeafSatisfaction = {
9
14
  leaf: TapLeafInfo;
10
15
  depth: number;
@@ -23,15 +28,23 @@ export type TaprootPsbtLeafMetadata = {
23
28
  };
24
29
  /**
25
30
  * Compiles a taproot miniscript tree into per-leaf metadata.
26
- * Each leaf contains its expanded miniscript, expansion map, compiled tapscript
27
- * and leaf version. This keeps the taproot script-path data ready for
28
- * satisfactions and witness building.
31
+ * Each leaf contains expanded expression metadata, key expansion map,
32
+ * compiled tapscript and leaf version. This keeps the taproot script-path data
33
+ * ready for satisfactions and witness building.
34
+ *
35
+ * `leafExpansionOverride` allows descriptor-level script expressions to provide:
36
+ * - user-facing expanded expression metadata (for selector/template use), and
37
+ * - custom expansion map and tapscript bytes for compilation.
38
+ *
39
+ * Example: sortedmulti_a can expose `expandedExpression=sortedmulti_a(...)`
40
+ * while providing a tapscript already compiled.
29
41
  */
30
- export declare function buildTapTreeInfo({ tapTree, network, BIP32, ECPair }: {
42
+ export declare function buildTapTreeInfo({ tapTree, network, BIP32, ECPair, leafExpansionOverride }: {
31
43
  tapTree: TapTreeNode;
32
44
  network?: Network;
33
45
  BIP32: BIP32API;
34
46
  ECPair: ECPairAPI;
47
+ leafExpansionOverride: (expression: string) => TapLeafExpansionOverride | undefined;
35
48
  }): TapTreeInfoNode;
36
49
  export declare function tapTreeInfoToScriptTree(tapTreeInfo: TapTreeInfoNode): Taptree;
37
50
  /**
@@ -142,6 +155,15 @@ export declare function buildTaprootBip32Derivations({ tapTreeInfo, internalKeyI
142
155
  internalKeyInfo?: KeyInfo;
143
156
  }): TapBip32Derivation[];
144
157
  export declare function normalizeTaprootPubkey(pubkey: Uint8Array): Uint8Array;
158
+ /**
159
+ * Compiles an expanded `sortedmulti_a(...)` leaf expression to its internal
160
+ * `multi_a(...)` form by sorting placeholders using the resolved pubkeys from
161
+ * `expansionMap`.
162
+ */
163
+ export declare function compileSortedMultiAExpandedExpression({ expandedExpression, expansionMap }: {
164
+ expandedExpression: string;
165
+ expansionMap: ExpansionMap;
166
+ }): string;
145
167
  /**
146
168
  * Computes satisfactions for taproot script-path leaves.
147
169
  *
@@ -174,8 +196,8 @@ export declare function collectTapTreePubkeys(tapTreeInfo: TapTreeInfoNode): Uin
174
196
  *
175
197
  * If `tapLeaf` is provided, only that leaf is considered. If `tapLeaf` is a
176
198
  * bytes, it is treated as a tapLeafHash and must match exactly one leaf. If
177
- * `tapLeaf` is a string, it is treated as a miniscript leaf and must match
178
- * exactly one leaf (whitespace-insensitive).
199
+ * `tapLeaf` is a string, it is treated as a raw leaf expression and must
200
+ * match exactly one leaf (whitespace-insensitive).
179
201
  *
180
202
  * This function is typically called twice:
181
203
  * 1) Planning pass: call it with fake signatures (built by the caller) to
@@ -6,6 +6,7 @@ exports.tapTreeInfoToScriptTree = tapTreeInfoToScriptTree;
6
6
  exports.buildTaprootLeafPsbtMetadata = buildTaprootLeafPsbtMetadata;
7
7
  exports.buildTaprootBip32Derivations = buildTaprootBip32Derivations;
8
8
  exports.normalizeTaprootPubkey = normalizeTaprootPubkey;
9
+ exports.compileSortedMultiAExpandedExpression = compileSortedMultiAExpandedExpression;
9
10
  exports.collectTaprootLeafSatisfactions = collectTaprootLeafSatisfactions;
10
11
  exports.selectBestTaprootLeafSatisfaction = selectBestTaprootLeafSatisfaction;
11
12
  exports.collectTapTreePubkeys = collectTapTreePubkeys;
@@ -30,42 +31,83 @@ function expandTaprootMiniscript({ miniscript, network = bitcoinjs_lib_1.network
30
31
  }
31
32
  /**
32
33
  * Compiles a taproot miniscript tree into per-leaf metadata.
33
- * Each leaf contains its expanded miniscript, expansion map, compiled tapscript
34
- * and leaf version. This keeps the taproot script-path data ready for
35
- * satisfactions and witness building.
34
+ * Each leaf contains expanded expression metadata, key expansion map,
35
+ * compiled tapscript and leaf version. This keeps the taproot script-path data
36
+ * ready for satisfactions and witness building.
37
+ *
38
+ * `leafExpansionOverride` allows descriptor-level script expressions to provide:
39
+ * - user-facing expanded expression metadata (for selector/template use), and
40
+ * - custom expansion map and tapscript bytes for compilation.
41
+ *
42
+ * Example: sortedmulti_a can expose `expandedExpression=sortedmulti_a(...)`
43
+ * while providing a tapscript already compiled.
36
44
  */
37
- function buildTapTreeInfo({ tapTree, network = bitcoinjs_lib_1.networks.bitcoin, BIP32, ECPair }) {
45
+ function buildTapTreeInfo({ tapTree, network = bitcoinjs_lib_1.networks.bitcoin, BIP32, ECPair, leafExpansionOverride }) {
38
46
  // Defensive: parseTapTreeExpression() already enforces this for descriptor
39
47
  // strings, but buildTapTreeInfo is exported and can be called directly.
40
48
  (0, tapTree_1.assertTapTreeDepth)(tapTree);
41
- if ('miniscript' in tapTree) {
42
- const miniscript = tapTree.miniscript;
43
- const { expandedMiniscript, expansionMap } = expandTaprootMiniscript({
44
- miniscript,
45
- network,
46
- BIP32,
47
- ECPair
48
- });
49
- const tapScript = (0, miniscript_1.miniscript2Script)({
50
- expandedMiniscript,
51
- expansionMap,
52
- tapscript: true
53
- });
49
+ if ('expression' in tapTree) {
50
+ // Tap-tree leaves store generic descriptor expressions. Most leaves are
51
+ // miniscript fragments, but descriptor-level script expressions (such as
52
+ // sortedmulti_a) can also appear and be normalized through
53
+ // `leafExpansionOverride`.
54
+ const expression = tapTree.expression;
55
+ const override = leafExpansionOverride(expression);
56
+ let expandedExpression;
57
+ let expandedMiniscript;
58
+ let expansionMap;
59
+ let tapScript;
60
+ if (override) {
61
+ // Descriptor-level expression overrides (e.g. sortedmulti_a) preserve a
62
+ // user-facing expanded expression while allowing custom compilation.
63
+ expandedExpression = override.expandedExpression;
64
+ expansionMap = override.expansionMap;
65
+ tapScript = override.tapScript;
66
+ }
67
+ else {
68
+ const expanded = expandTaprootMiniscript({
69
+ miniscript: expression,
70
+ network,
71
+ BIP32,
72
+ ECPair
73
+ });
74
+ expandedExpression = expanded.expandedMiniscript;
75
+ expandedMiniscript = expanded.expandedMiniscript;
76
+ expansionMap = expanded.expansionMap;
77
+ tapScript = (0, miniscript_1.miniscript2Script)({
78
+ expandedMiniscript: expanded.expandedMiniscript,
79
+ expansionMap,
80
+ tapscript: true
81
+ });
82
+ }
54
83
  return {
55
- miniscript,
56
- expandedMiniscript,
84
+ expression,
85
+ expandedExpression,
86
+ ...(expandedMiniscript !== undefined ? { expandedMiniscript } : {}),
57
87
  expansionMap,
58
88
  tapScript,
59
89
  version: TAPROOT_LEAF_VERSION_TAPSCRIPT
60
90
  };
61
91
  }
62
92
  return {
63
- left: buildTapTreeInfo({ tapTree: tapTree.left, network, BIP32, ECPair }),
64
- right: buildTapTreeInfo({ tapTree: tapTree.right, network, BIP32, ECPair })
93
+ left: buildTapTreeInfo({
94
+ tapTree: tapTree.left,
95
+ network,
96
+ BIP32,
97
+ ECPair,
98
+ leafExpansionOverride
99
+ }),
100
+ right: buildTapTreeInfo({
101
+ tapTree: tapTree.right,
102
+ network,
103
+ BIP32,
104
+ ECPair,
105
+ leafExpansionOverride
106
+ })
65
107
  };
66
108
  }
67
109
  function tapTreeInfoToScriptTree(tapTreeInfo) {
68
- if ('miniscript' in tapTreeInfo) {
110
+ if ('expression' in tapTreeInfo) {
69
111
  return {
70
112
  output: tapTreeInfo.tapScript,
71
113
  version: tapTreeInfo.version
@@ -153,7 +195,7 @@ function buildTaprootLeafPsbtMetadata({ tapTreeInfo, internalPubkey }) {
153
195
  });
154
196
  const merklePath = (0, bitcoinjs_lib_internals_1.findScriptPath)(hashTree, tapLeafHash);
155
197
  if (!merklePath)
156
- throw new Error(`Error: could not build controlBlock for taproot leaf ${leaf.miniscript}`);
198
+ throw new Error(`Error: could not build controlBlock for taproot leaf ${leaf.expression}`);
157
199
  // controlBlock[0] packs:
158
200
  // - leaf version (high bits), and
159
201
  // - parity of the tweaked output key Q = P + t*G (low bit).
@@ -292,6 +334,37 @@ function normalizeTaprootPubkey(pubkey) {
292
334
  return pubkey.slice(1, 33);
293
335
  throw new Error(`Error: invalid taproot pubkey length`);
294
336
  }
337
+ /**
338
+ * Compiles an expanded `sortedmulti_a(...)` leaf expression to its internal
339
+ * `multi_a(...)` form by sorting placeholders using the resolved pubkeys from
340
+ * `expansionMap`.
341
+ */
342
+ function compileSortedMultiAExpandedExpression({ expandedExpression, expansionMap }) {
343
+ const trimmed = expandedExpression.trim();
344
+ const match = trimmed.match(/^sortedmulti_a\((.*)\)$/);
345
+ if (!match)
346
+ throw new Error(`Error: invalid sortedmulti_a() expression: ${trimmed}`);
347
+ const inner = match[1];
348
+ if (!inner)
349
+ throw new Error(`Error: invalid sortedmulti_a() expression: ${trimmed}`);
350
+ const parts = inner.split(',').map(part => part.trim());
351
+ if (parts.length < 2)
352
+ throw new Error(`Error: invalid sortedmulti_a() expression: ${trimmed}`);
353
+ const threshold = parts[0];
354
+ if (!threshold)
355
+ throw new Error(`Error: invalid sortedmulti_a() threshold: ${trimmed}`);
356
+ const placeholders = parts.slice(1);
357
+ const sortedPlaceholders = placeholders
358
+ .map(placeholder => {
359
+ const keyInfo = expansionMap[placeholder];
360
+ if (!keyInfo?.pubkey)
361
+ throw new Error(`Error: sortedmulti_a() placeholder ${placeholder} not found in expansionMap`);
362
+ return { placeholder, pubkey: keyInfo.pubkey };
363
+ })
364
+ .sort((a, b) => (0, uint8array_tools_1.compare)(a.pubkey, b.pubkey))
365
+ .map(({ placeholder }) => placeholder);
366
+ return `multi_a(${[threshold, ...sortedPlaceholders].join(',')})`;
367
+ }
295
368
  /**
296
369
  * Computes satisfactions for taproot script-path leaves.
297
370
  *
@@ -329,8 +402,22 @@ function collectTaprootLeafSatisfactions({ tapTreeInfo, preimages, signatures, t
329
402
  const { leaf } = candidate;
330
403
  const leafSignatures = resolveLeafSignatures(leaf);
331
404
  try {
405
+ let satisfierMiniscript = leaf.expandedMiniscript;
406
+ // Most taproot leaves are Miniscript fragments, so `expandedMiniscript`
407
+ // is available (and currently matches `expandedExpression`).
408
+ // Descriptor-level leaf expressions (currently `sortedmulti_a`)
409
+ // expose only `expandedExpression`, so we derive an internal
410
+ // Miniscript-equivalent form here before calling `satisfyMiniscript(...)`
411
+ if (!satisfierMiniscript) {
412
+ if (!/^sortedmulti_a\(/.test(leaf.expandedExpression.trim()))
413
+ throw new Error(`Error: taproot leaf does not provide a satisfier miniscript`);
414
+ satisfierMiniscript = compileSortedMultiAExpandedExpression({
415
+ expandedExpression: leaf.expandedExpression,
416
+ expansionMap: leaf.expansionMap
417
+ });
418
+ }
332
419
  const { scriptSatisfaction, nLockTime, nSequence } = (0, miniscript_1.satisfyMiniscript)({
333
- expandedMiniscript: leaf.expandedMiniscript,
420
+ expandedMiniscript: satisfierMiniscript,
334
421
  expansionMap: leaf.expansionMap,
335
422
  signatures: leafSignatures,
336
423
  preimages,
@@ -406,8 +493,8 @@ function collectTapTreePubkeys(tapTreeInfo) {
406
493
  *
407
494
  * If `tapLeaf` is provided, only that leaf is considered. If `tapLeaf` is a
408
495
  * bytes, it is treated as a tapLeafHash and must match exactly one leaf. If
409
- * `tapLeaf` is a string, it is treated as a miniscript leaf and must match
410
- * exactly one leaf (whitespace-insensitive).
496
+ * `tapLeaf` is a string, it is treated as a raw leaf expression and must
497
+ * match exactly one leaf (whitespace-insensitive).
411
498
  *
412
499
  * This function is typically called twice:
413
500
  * 1) Planning pass: call it with fake signatures (built by the caller) to
package/dist/tapTree.d.ts CHANGED
@@ -4,12 +4,22 @@ export type TreeNode<TLeaf> = TLeaf | {
4
4
  right: TreeNode<TLeaf>;
5
5
  };
6
6
  export type TapLeaf = {
7
- miniscript: string;
7
+ /** Raw leaf expression as written in tr(KEY,TREE). */
8
+ expression: string;
8
9
  };
9
10
  export type TapTreeNode = TreeNode<TapLeaf>;
10
11
  export type TapLeafInfo = {
11
- miniscript: string;
12
- expandedMiniscript: string;
12
+ /** Raw leaf expression as written in tr(KEY,TREE). */
13
+ expression: string;
14
+ /** Expanded descriptor-level expression for this leaf. */
15
+ expandedExpression: string;
16
+ /**
17
+ * Expanded miniscript form when the leaf expression is a miniscript fragment.
18
+ *
19
+ * For descriptor-level script expressions (e.g. sortedmulti_a), this can be
20
+ * undefined even though `tapScript` is available.
21
+ */
22
+ expandedMiniscript?: string;
13
23
  expansionMap: ExpansionMap;
14
24
  tapScript: Uint8Array;
15
25
  version: number;
@@ -58,14 +68,14 @@ export declare function collectTapTreeLeaves(tapTreeInfo: TapTreeInfoNode): Arra
58
68
  * If `tapLeaf` is undefined, all leaves are returned for auto-selection.
59
69
  * If `tapLeaf` is bytes, it is treated as a tapleaf hash and must match
60
70
  * exactly one leaf.
61
- * If `tapLeaf` is a string, it is treated as a miniscript leaf (raw, not
62
- * expanded). Matching is whitespace-insensitive. If the miniscript appears
71
+ * If `tapLeaf` is a string, it is treated as a raw taproot leaf expression
72
+ * (not expanded). Matching is whitespace-insensitive. If the expression appears
63
73
  * more than once, this function throws an error.
64
74
  *
65
75
  * Example:
66
76
  * ```
67
77
  * const candidates = selectTapLeafCandidates({ tapTreeInfo, tapLeaf });
68
- * // tapLeaf can be undefined, bytes (tapleaf hash) or a miniscript string:
78
+ * // tapLeaf can be undefined, bytes (tapleaf hash) or a leaf expression:
69
79
  * // f.ex.: 'pk(03bb...)'
70
80
  * ```
71
81
  */
package/dist/tapTree.js CHANGED
@@ -15,7 +15,7 @@ const parseUtils_1 = require("./parseUtils");
15
15
  // with consensus max depth 128.
16
16
  exports.MAX_TAPTREE_DEPTH = 128;
17
17
  function tapTreeMaxDepth(tapTree, depth = 0) {
18
- if ('miniscript' in tapTree)
18
+ if ('expression' in tapTree)
19
19
  return depth;
20
20
  return Math.max(tapTreeMaxDepth(tapTree.left, depth + 1), tapTreeMaxDepth(tapTree.right, depth + 1));
21
21
  }
@@ -53,7 +53,7 @@ function assertTapTreeDepth(tapTree) {
53
53
  function collectTapTreeLeaves(tapTreeInfo) {
54
54
  const leaves = [];
55
55
  const walk = (node, depth) => {
56
- if ('miniscript' in node) {
56
+ if ('expression' in node) {
57
57
  leaves.push({ leaf: node, depth });
58
58
  return;
59
59
  }
@@ -66,8 +66,8 @@ function collectTapTreeLeaves(tapTreeInfo) {
66
66
  function computeTapLeafHash(leaf) {
67
67
  return (0, bitcoinjs_lib_internals_1.tapleafHash)({ output: leaf.tapScript, version: leaf.version });
68
68
  }
69
- function normalizeMiniscriptForMatch(miniscript) {
70
- return miniscript.replace(/\s+/g, '');
69
+ function normalizeExpressionForMatch(expression) {
70
+ return expression.replace(/\s+/g, '');
71
71
  }
72
72
  /**
73
73
  * Resolves taproot leaf candidates based on an optional selector.
@@ -75,14 +75,14 @@ function normalizeMiniscriptForMatch(miniscript) {
75
75
  * If `tapLeaf` is undefined, all leaves are returned for auto-selection.
76
76
  * If `tapLeaf` is bytes, it is treated as a tapleaf hash and must match
77
77
  * exactly one leaf.
78
- * If `tapLeaf` is a string, it is treated as a miniscript leaf (raw, not
79
- * expanded). Matching is whitespace-insensitive. If the miniscript appears
78
+ * If `tapLeaf` is a string, it is treated as a raw taproot leaf expression
79
+ * (not expanded). Matching is whitespace-insensitive. If the expression appears
80
80
  * more than once, this function throws an error.
81
81
  *
82
82
  * Example:
83
83
  * ```
84
84
  * const candidates = selectTapLeafCandidates({ tapTreeInfo, tapLeaf });
85
- * // tapLeaf can be undefined, bytes (tapleaf hash) or a miniscript string:
85
+ * // tapLeaf can be undefined, bytes (tapleaf hash) or a leaf expression:
86
86
  * // f.ex.: 'pk(03bb...)'
87
87
  * ```
88
88
  */
@@ -100,12 +100,12 @@ function selectTapLeafCandidates({ tapTreeInfo, tapLeaf }) {
100
100
  throw new Error(`Error: tapleaf hash not found in tapTreeInfo`);
101
101
  return [match];
102
102
  }
103
- const normalizedSelector = normalizeMiniscriptForMatch(tapLeaf);
104
- const matches = leaves.filter(entry => normalizeMiniscriptForMatch(entry.leaf.miniscript) === normalizedSelector);
103
+ const normalizedSelector = normalizeExpressionForMatch(tapLeaf);
104
+ const matches = leaves.filter(entry => normalizeExpressionForMatch(entry.leaf.expression) === normalizedSelector);
105
105
  if (matches.length === 0)
106
- throw new Error(`Error: miniscript leaf not found in tapTreeInfo: ${tapLeaf}`);
106
+ throw new Error(`Error: taproot leaf expression not found in tapTreeInfo: ${tapLeaf}`);
107
107
  if (matches.length > 1)
108
- throw new Error(`Error: miniscript leaf is ambiguous in tapTreeInfo: ${tapLeaf}`);
108
+ throw new Error(`Error: taproot leaf expression is ambiguous in tapTreeInfo: ${tapLeaf}`);
109
109
  return matches;
110
110
  }
111
111
  function tapTreeError(expression) {
@@ -124,13 +124,17 @@ function splitTapTreeExpression(expression) {
124
124
  }
125
125
  /**
126
126
  * Parses a single taproot tree node expression.
127
+ *
128
+ * Note: the field name is intentionally generic (`expression`) because taproot
129
+ * leaves can contain either miniscript fragments (e.g. `pk(...)`) or
130
+ * descriptor-level script expressions (e.g. `sortedmulti_a(...)`).
127
131
  * Examples:
128
- * - `pk(@0)` => { miniscript: 'pk(@0)' }
129
- * - `{pk(@0),pk(@1)}` => { left: { miniscript: 'pk(@0)' }, right: { miniscript: 'pk(@1)' } }
132
+ * - `pk(@0)` => { expression: 'pk(@0)' }
133
+ * - `{pk(@0),pk(@1)}` => { left: { expression: 'pk(@0)' }, right: { expression: 'pk(@1)' } }
130
134
  * - `{pk(@0),{pk(@1),pk(@2)}}` =>
131
135
  * {
132
- * left: { miniscript: 'pk(@0)' },
133
- * right: { left: { miniscript: 'pk(@1)' }, right: { miniscript: 'pk(@2)' } }
136
+ * left: { expression: 'pk(@0)' },
137
+ * right: { left: { expression: 'pk(@1)' }, right: { expression: 'pk(@2)' } }
134
138
  * }
135
139
  */
136
140
  function parseTapTreeNode(expression) {
@@ -151,7 +155,7 @@ function parseTapTreeNode(expression) {
151
155
  }
152
156
  if (trimmedExpression.includes('{') || trimmedExpression.includes('}'))
153
157
  throw tapTreeError(expression);
154
- return { miniscript: trimmedExpression };
158
+ return { expression: trimmedExpression };
155
159
  }
156
160
  function parseTapTreeExpression(expression) {
157
161
  const trimmed = expression.trim();
package/dist/types.d.ts CHANGED
@@ -138,10 +138,10 @@ export type Expansion = {
138
138
  * Example:
139
139
  * ```
140
140
  * {
141
- * left: { miniscript: 'pk(02aa...)' },
141
+ * left: { expression: 'pk(02aa...)' },
142
142
  * right: {
143
- * left: { miniscript: 'pk(03bb...)' },
144
- * right: { miniscript: 'pk(02cc...)' }
143
+ * left: { expression: 'pk(03bb...)' },
144
+ * right: { expression: 'pk(02cc...)' }
145
145
  * }
146
146
  * }
147
147
  * ```
@@ -149,16 +149,22 @@ export type Expansion = {
149
149
  tapTree?: TapTreeNode;
150
150
  /**
151
151
  * The compiled taproot tree metadata, if any. Only defined for `tr(KEY, TREE)`.
152
- * Same as tapTree, but each leaf includes the expanded miniscript, key
153
- * expansion map, compiled tapscript (`tapScript`), and leaf version.
154
- * Note: `@i` placeholders in `expandedMiniscript` are scoped per leaf, since
155
- * each leaf is expanded and satisfied independently.
152
+ * Same as tapTree, but each leaf includes:
153
+ * - `expandedExpression`: descriptor-level expanded leaf expression
154
+ * - optional `expandedMiniscript`: miniscript-expanded leaf (when applicable)
155
+ * - key expansion map
156
+ * - compiled tapscript (`tapScript`)
157
+ * - leaf version.
158
+ *
159
+ * Note: `@i` placeholders are scoped per leaf, since each leaf is expanded
160
+ * and satisfied independently.
156
161
  *
157
162
  * Example:
158
163
  * ```
159
164
  * {
160
165
  * left: {
161
- * miniscript: 'pk(02aa...)',
166
+ * expression: 'pk(02aa...)',
167
+ * expandedExpression: 'pk(@0)',
162
168
  * expandedMiniscript: 'pk(@0)',
163
169
  * expansionMap: ExpansionMap;
164
170
  * tapScript: Uint8Array;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@bitcoinerlab/descriptors",
3
3
  "description": "This library parses and creates Bitcoin Miniscript Descriptors and generates Partially Signed Bitcoin Transactions (PSBTs). It provides PSBT finalizers and signers for single-signature, BIP32 and Hardware Wallets.",
4
4
  "homepage": "https://github.com/bitcoinerlab/descriptors",
5
- "version": "3.0.0",
5
+ "version": "3.0.1",
6
6
  "author": "Jose-Luis Landabaso",
7
7
  "license": "MIT",
8
8
  "repository": {
@@ -1,17 +0,0 @@
1
- export declare const MAX_STACK_SIZE = 1000;
2
- export declare const MAX_SCRIPT_ELEMENT_SIZE = 520;
3
- export declare function assertConsensusStackResourceLimits({ stackItems, stackLabel, stackItemLabel }: {
4
- stackItems: Uint8Array[];
5
- stackLabel?: string;
6
- stackItemLabel?: string;
7
- }): void;
8
- export declare function assertStandardPolicyStackItemCountLimit({ stackItems, maxStackItems, stackLabel }: {
9
- stackItems: Uint8Array[];
10
- maxStackItems: number;
11
- stackLabel: string;
12
- }): void;
13
- export declare function assertStandardPolicyStackItemSizeLimit({ stackItems, maxStackItemSize, stackItemLabel }: {
14
- stackItems: Uint8Array[];
15
- maxStackItemSize: number;
16
- stackItemLabel: string;
17
- }): void;
@@ -1,35 +0,0 @@
1
- "use strict";
2
- // Copyright (c) 2026 Jose-Luis Landabaso
3
- // Distributed under the MIT software license
4
- Object.defineProperty(exports, "__esModule", { value: true });
5
- exports.MAX_SCRIPT_ELEMENT_SIZE = exports.MAX_STACK_SIZE = void 0;
6
- exports.assertConsensusStackResourceLimits = assertConsensusStackResourceLimits;
7
- exports.assertStandardPolicyStackItemCountLimit = assertStandardPolicyStackItemCountLimit;
8
- exports.assertStandardPolicyStackItemSizeLimit = assertStandardPolicyStackItemSizeLimit;
9
- // See Sipa's Miniscript "Resource limitations":
10
- // https://bitcoin.sipa.be/miniscript/
11
- // and Bitcoin Core policy/consensus constants.
12
- // Consensus: max number of elements in initial stack (and stack+altstack after
13
- // each opcode execution).
14
- exports.MAX_STACK_SIZE = 1000;
15
- // Consensus: max size for any pushed stack element. This is an element limit,
16
- // not a full script-size limit.
17
- exports.MAX_SCRIPT_ELEMENT_SIZE = 520;
18
- function assertConsensusStackResourceLimits({ stackItems, stackLabel = 'stack', stackItemLabel = 'stack item' }) {
19
- if (stackItems.length > exports.MAX_STACK_SIZE)
20
- throw new Error(`Error: ${stackLabel} has too many items, ${stackItems.length} is larger than ${exports.MAX_STACK_SIZE}`);
21
- for (const stackItem of stackItems) {
22
- if (stackItem.length > exports.MAX_SCRIPT_ELEMENT_SIZE)
23
- throw new Error(`Error: ${stackItemLabel} is too large, ${stackItem.length} bytes is larger than ${exports.MAX_SCRIPT_ELEMENT_SIZE} bytes`);
24
- }
25
- }
26
- function assertStandardPolicyStackItemCountLimit({ stackItems, maxStackItems, stackLabel }) {
27
- if (stackItems.length > maxStackItems)
28
- throw new Error(`Error: ${stackLabel} has too many items, ${stackItems.length} is larger than ${maxStackItems}`);
29
- }
30
- function assertStandardPolicyStackItemSizeLimit({ stackItems, maxStackItemSize, stackItemLabel }) {
31
- for (const stackItem of stackItems) {
32
- if (stackItem.length > maxStackItemSize)
33
- throw new Error(`Error: ${stackItemLabel} exceeds standard policy, ${stackItem.length} bytes is larger than ${maxStackItemSize} bytes`);
34
- }
35
- }