@bitcoinerlab/descriptors 3.0.0 → 3.0.2
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/dist/descriptors.js +163 -29
- package/dist/ledger.js +33 -23
- package/dist/miniscript.js +10 -5
- package/dist/tapMiniscript.d.ts +29 -7
- package/dist/tapMiniscript.js +113 -26
- package/dist/tapTree.d.ts +16 -6
- package/dist/tapTree.js +20 -16
- package/dist/types.d.ts +14 -8
- package/package.json +2 -2
- package/dist/stackResourceLimits.d.ts +0 -17
- package/dist/stackResourceLimits.js +0 -35
package/dist/descriptors.js
CHANGED
|
@@ -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
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
|
|
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)({
|
|
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) {
|
|
@@ -1068,24 +1162,56 @@ expansion=${expansion}, isPKH=${isPKH}, isWPKH=${isWPKH}, isSH=${isSH}, isTR=${i
|
|
|
1068
1162
|
return (0, varuint_bitcoin_1.encodingLength)(length) + length;
|
|
1069
1163
|
};
|
|
1070
1164
|
const resolveMiniscriptSignatures = () => {
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1165
|
+
const signerPubKeys = __classPrivateFieldGet(this, _Output_instances, "m", _Output_resolveMiniscriptSignersPubKeys).call(this);
|
|
1166
|
+
if (signatures === 'DANGEROUSLY_USE_FAKE_SIGNATURES') {
|
|
1167
|
+
return signerPubKeys.map(pubkey => ({
|
|
1168
|
+
pubkey,
|
|
1169
|
+
// https://transactionfee.info/charts/bitcoin-script-ecdsa-length/
|
|
1170
|
+
signature: new Uint8Array(ECDSA_FAKE_SIGNATURE_SIZE)
|
|
1171
|
+
}));
|
|
1172
|
+
}
|
|
1173
|
+
const providedSignerPubKeysSet = new Set(signatures.map(sig => (0, uint8array_tools_1.toHex)(sig.pubkey)));
|
|
1174
|
+
const missingSignerPubKeys = signerPubKeys.filter(pubkey => !providedSignerPubKeysSet.has((0, uint8array_tools_1.toHex)(pubkey)));
|
|
1175
|
+
if (missingSignerPubKeys.length > 0)
|
|
1176
|
+
throw new Error(`Error: inputWeight expected signatures for all planned miniscript signers. Missing ${missingSignerPubKeys.length} signer(s)`);
|
|
1177
|
+
const signerPubKeysSet = new Set(signerPubKeys.map(pubkey => (0, uint8array_tools_1.toHex)(pubkey)));
|
|
1178
|
+
return signatures.filter(sig => signerPubKeysSet.has((0, uint8array_tools_1.toHex)(sig.pubkey)));
|
|
1078
1179
|
};
|
|
1079
1180
|
const taprootFakeSignatureSize = taprootSighash === 'SIGHASH_DEFAULT'
|
|
1080
1181
|
? TAPROOT_FAKE_SIGNATURE_SIZE
|
|
1081
1182
|
: TAPROOT_FAKE_SIGNATURE_SIZE + 1;
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
return __classPrivateFieldGet(this, _Output_instances, "m", _Output_resolveTapTreeSignersPubKeys).call(this).map(pubkey => ({
|
|
1183
|
+
const resolvePlannedTaprootRequiredPubKeys = () => {
|
|
1184
|
+
const tapTreeSignerPubKeys = __classPrivateFieldGet(this, _Output_instances, "m", _Output_resolveTapTreeSignersPubKeys).call(this).map(tapMiniscript_1.normalizeTaprootPubkey);
|
|
1185
|
+
const taggedFakeSignatures = tapTreeSignerPubKeys.map((pubkey, index) => ({
|
|
1086
1186
|
pubkey,
|
|
1087
|
-
signature: new Uint8Array(taprootFakeSignatureSize)
|
|
1187
|
+
signature: new Uint8Array(taprootFakeSignatureSize).fill(index + 1)
|
|
1088
1188
|
}));
|
|
1189
|
+
const plannedTaprootSatisfaction = this.getTapScriptSatisfaction(taggedFakeSignatures);
|
|
1190
|
+
const requiredPubkeys = taggedFakeSignatures
|
|
1191
|
+
.filter(fakeSignature => plannedTaprootSatisfaction.stackItems.some(stackItem => (0, uint8array_tools_1.compare)(stackItem, fakeSignature.signature) === 0))
|
|
1192
|
+
.map(fakeSignature => fakeSignature.pubkey);
|
|
1193
|
+
return Array.from(new Set(requiredPubkeys.map(pubkey => (0, uint8array_tools_1.toHex)(pubkey))))
|
|
1194
|
+
.map(hex => (0, uint8array_tools_1.fromHex)(hex))
|
|
1195
|
+
.map(tapMiniscript_1.normalizeTaprootPubkey);
|
|
1196
|
+
};
|
|
1197
|
+
const resolveTaprootSignatures = () => {
|
|
1198
|
+
if (signatures === 'DANGEROUSLY_USE_FAKE_SIGNATURES') {
|
|
1199
|
+
return __classPrivateFieldGet(this, _Output_instances, "m", _Output_resolveTapTreeSignersPubKeys).call(this).map(pubkey => ({
|
|
1200
|
+
pubkey,
|
|
1201
|
+
signature: new Uint8Array(taprootFakeSignatureSize)
|
|
1202
|
+
}));
|
|
1203
|
+
}
|
|
1204
|
+
const normalizedSignatures = signatures.map(sig => ({
|
|
1205
|
+
pubkey: (0, tapMiniscript_1.normalizeTaprootPubkey)(sig.pubkey),
|
|
1206
|
+
signature: sig.signature
|
|
1207
|
+
}));
|
|
1208
|
+
const plannedRequiredPubKeys = resolvePlannedTaprootRequiredPubKeys();
|
|
1209
|
+
const providedTapTreeSignerPubKeysSet = new Set(normalizedSignatures.map(sig => (0, uint8array_tools_1.toHex)(sig.pubkey)));
|
|
1210
|
+
const missingTapTreeSignerPubKeys = plannedRequiredPubKeys.filter(pubkey => !providedTapTreeSignerPubKeysSet.has((0, uint8array_tools_1.toHex)(pubkey)));
|
|
1211
|
+
if (missingTapTreeSignerPubKeys.length > 0)
|
|
1212
|
+
throw new Error(`Error: inputWeight expected signatures for the planned taproot script-path satisfaction. Missing ${missingTapTreeSignerPubKeys.length} signer(s)`);
|
|
1213
|
+
const plannedRequiredPubKeysSet = new Set(plannedRequiredPubKeys.map(pubkey => (0, uint8array_tools_1.toHex)(pubkey)));
|
|
1214
|
+
return normalizedSignatures.filter(sig => plannedRequiredPubKeysSet.has((0, uint8array_tools_1.toHex)(sig.pubkey)));
|
|
1089
1215
|
};
|
|
1090
1216
|
const resolveTaprootSignatureSize = () => {
|
|
1091
1217
|
let length;
|
|
@@ -1093,9 +1219,26 @@ expansion=${expansion}, isPKH=${isPKH}, isWPKH=${isWPKH}, isSH=${isSH}, isTR=${i
|
|
|
1093
1219
|
length = taprootFakeSignatureSize;
|
|
1094
1220
|
}
|
|
1095
1221
|
else {
|
|
1096
|
-
|
|
1222
|
+
const normalizedSignatures = signatures.map(sig => ({
|
|
1223
|
+
pubkey: (0, tapMiniscript_1.normalizeTaprootPubkey)(sig.pubkey),
|
|
1224
|
+
signature: sig.signature
|
|
1225
|
+
}));
|
|
1226
|
+
const internalPubkey = this.getPayment().internalPubkey;
|
|
1227
|
+
if (!internalPubkey) {
|
|
1228
|
+
//addr() of tr addresses may not have internalPubkey
|
|
1229
|
+
if (normalizedSignatures.length !== 1)
|
|
1230
|
+
throw new Error('Error: inputWeight for addr(TR_ADDRESS) requires exactly one signature. Internal taproot pubkey is unavailable in addr() descriptors; use tr(KEY) for strict key matching.');
|
|
1231
|
+
const singleSignature = normalizedSignatures[0];
|
|
1232
|
+
if (!singleSignature)
|
|
1233
|
+
throw new Error('Signatures not present');
|
|
1234
|
+
length = singleSignature.signature.length;
|
|
1235
|
+
return (0, varuint_bitcoin_1.encodingLength)(length) + length;
|
|
1236
|
+
}
|
|
1237
|
+
const normalizedInternalPubkey = (0, tapMiniscript_1.normalizeTaprootPubkey)(internalPubkey);
|
|
1238
|
+
const keyPathSignatures = normalizedSignatures.filter(sig => (0, uint8array_tools_1.compare)(sig.pubkey, normalizedInternalPubkey) === 0);
|
|
1239
|
+
if (keyPathSignatures.length !== 1)
|
|
1097
1240
|
throw new Error('More than one signture was not expected');
|
|
1098
|
-
const singleSignature =
|
|
1241
|
+
const singleSignature = keyPathSignatures[0];
|
|
1099
1242
|
if (!singleSignature)
|
|
1100
1243
|
throw new Error('Signatures not present');
|
|
1101
1244
|
length = singleSignature.signature.length;
|
|
@@ -1103,7 +1246,6 @@ expansion=${expansion}, isPKH=${isPKH}, isWPKH=${isWPKH}, isSH=${isSH}, isTR=${i
|
|
|
1103
1246
|
return (0, varuint_bitcoin_1.encodingLength)(length) + length;
|
|
1104
1247
|
};
|
|
1105
1248
|
const taprootSpendPath = __classPrivateFieldGet(this, _Output_taprootSpendPath, "f");
|
|
1106
|
-
const tapLeaf = __classPrivateFieldGet(this, _Output_tapLeaf, "f");
|
|
1107
1249
|
if (expansion ? expansion.startsWith('pkh(') : isPKH) {
|
|
1108
1250
|
return (
|
|
1109
1251
|
// Non-segwit: (txid:32) + (vout:4) + (sequence:4) + (script_len:1) + (sig:73) + (pubkey:34)
|
|
@@ -1201,15 +1343,7 @@ expansion=${expansion}, isPKH=${isPKH}, isWPKH=${isWPKH}, isSH=${isSH}, isTR=${i
|
|
|
1201
1343
|
throw new Error('Should be SegwitTx');
|
|
1202
1344
|
if (taprootSpendPath === 'key')
|
|
1203
1345
|
return 41 * 4 + (0, varuint_bitcoin_1.encodingLength)(1) + resolveTaprootSignatureSize();
|
|
1204
|
-
const
|
|
1205
|
-
if (!resolvedTapTreeInfo)
|
|
1206
|
-
throw new Error(`Error: taproot tree info not available`);
|
|
1207
|
-
const taprootSatisfaction = (0, tapMiniscript_1.satisfyTapTree)({
|
|
1208
|
-
tapTreeInfo: resolvedTapTreeInfo,
|
|
1209
|
-
preimages: __classPrivateFieldGet(this, _Output_preimages, "f"),
|
|
1210
|
-
signatures: resolveTaprootSignatures(),
|
|
1211
|
-
...(tapLeaf !== undefined ? { tapLeaf } : {})
|
|
1212
|
-
});
|
|
1346
|
+
const taprootSatisfaction = this.getTapScriptSatisfaction(resolveTaprootSignatures());
|
|
1213
1347
|
return 41 * 4 + taprootSatisfaction.totalWitnessSize;
|
|
1214
1348
|
}
|
|
1215
1349
|
else if (isTR && (!expansion || expansion === 'tr(@0)')) {
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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 ('
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|
package/dist/miniscript.js
CHANGED
|
@@ -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
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
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:
|
package/dist/tapMiniscript.d.ts
CHANGED
|
@@ -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
|
|
27
|
-
* and leaf version. This keeps the taproot script-path data
|
|
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
|
|
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
|
package/dist/tapMiniscript.js
CHANGED
|
@@ -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
|
|
34
|
-
* and leaf version. This keeps the taproot script-path data
|
|
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 ('
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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({
|
|
64
|
-
|
|
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 ('
|
|
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.
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
|
62
|
-
* expanded). Matching is whitespace-insensitive. If the
|
|
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
|
|
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 ('
|
|
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 ('
|
|
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
|
|
70
|
-
return
|
|
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
|
|
79
|
-
* expanded). Matching is whitespace-insensitive. If the
|
|
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
|
|
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 =
|
|
104
|
-
const matches = leaves.filter(entry =>
|
|
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:
|
|
106
|
+
throw new Error(`Error: taproot leaf expression not found in tapTreeInfo: ${tapLeaf}`);
|
|
107
107
|
if (matches.length > 1)
|
|
108
|
-
throw new Error(`Error:
|
|
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)` => {
|
|
129
|
-
* - `{pk(@0),pk(@1)}` => { left: {
|
|
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: {
|
|
133
|
-
* right: { left: {
|
|
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 {
|
|
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: {
|
|
141
|
+
* left: { expression: 'pk(02aa...)' },
|
|
142
142
|
* right: {
|
|
143
|
-
* left: {
|
|
144
|
-
* right: {
|
|
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
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
5
|
+
"version": "3.0.2",
|
|
6
6
|
"author": "Jose-Luis Landabaso",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"dependencies": {
|
|
70
70
|
"@bitcoinerlab/miniscript": "^2.0.0",
|
|
71
71
|
"@bitcoinerlab/secp256k1": "^1.2.0",
|
|
72
|
-
"bip32": "
|
|
72
|
+
"bip32": "github:bitcoinjs/bip32#9e4471ddad38c2d344c1167048072f1c569a115e",
|
|
73
73
|
"bitcoinjs-lib": "^7.0.1",
|
|
74
74
|
"ecpair": "^3.0.1",
|
|
75
75
|
"lodash.memoize": "^4.1.2",
|
|
@@ -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
|
-
}
|