@bitcoinerlab/descriptors 2.3.6 → 3.0.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/README.md +173 -77
- package/dist/applyPR2137.js +36 -17
- package/dist/bitcoinjs-lib-internals.d.ts +10 -0
- package/dist/bitcoinjs-lib-internals.js +18 -0
- package/dist/descriptors.d.ts +161 -392
- package/dist/descriptors.js +512 -281
- package/dist/index.d.ts +2 -29
- package/dist/index.js +0 -14
- package/dist/keyExpressions.d.ts +4 -13
- package/dist/keyExpressions.js +15 -18
- package/dist/ledger.d.ts +14 -37
- package/dist/ledger.js +118 -100
- package/dist/miniscript.d.ts +20 -6
- package/dist/miniscript.js +59 -17
- package/dist/multipath.d.ts +13 -0
- package/dist/multipath.js +76 -0
- package/dist/networkUtils.d.ts +3 -0
- package/dist/networkUtils.js +16 -0
- package/dist/parseUtils.d.ts +7 -0
- package/dist/parseUtils.js +46 -0
- package/dist/psbt.d.ts +17 -13
- package/dist/psbt.js +34 -50
- package/dist/resourceLimits.d.ts +25 -0
- package/dist/resourceLimits.js +89 -0
- package/dist/scriptExpressions.d.ts +29 -77
- package/dist/scriptExpressions.js +19 -16
- package/dist/signers.d.ts +1 -21
- package/dist/signers.js +85 -129
- package/dist/stackResourceLimits.d.ts +17 -0
- package/dist/stackResourceLimits.js +35 -0
- package/dist/tapMiniscript.d.ts +193 -0
- package/dist/tapMiniscript.js +428 -0
- package/dist/tapTree.d.ts +76 -0
- package/dist/tapTree.js +163 -0
- package/dist/types.d.ts +46 -6
- package/package.json +13 -13
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Distributed under the MIT software license
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.buildTapTreeInfo = buildTapTreeInfo;
|
|
5
|
+
exports.tapTreeInfoToScriptTree = tapTreeInfoToScriptTree;
|
|
6
|
+
exports.buildTaprootLeafPsbtMetadata = buildTaprootLeafPsbtMetadata;
|
|
7
|
+
exports.buildTaprootBip32Derivations = buildTaprootBip32Derivations;
|
|
8
|
+
exports.normalizeTaprootPubkey = normalizeTaprootPubkey;
|
|
9
|
+
exports.collectTaprootLeafSatisfactions = collectTaprootLeafSatisfactions;
|
|
10
|
+
exports.selectBestTaprootLeafSatisfaction = selectBestTaprootLeafSatisfaction;
|
|
11
|
+
exports.collectTapTreePubkeys = collectTapTreePubkeys;
|
|
12
|
+
exports.satisfyTapTree = satisfyTapTree;
|
|
13
|
+
const bitcoinjs_lib_1 = require("bitcoinjs-lib");
|
|
14
|
+
const bitcoinjs_lib_internals_1 = require("./bitcoinjs-lib-internals");
|
|
15
|
+
const varuint_bitcoin_1 = require("varuint-bitcoin");
|
|
16
|
+
const uint8array_tools_1 = require("uint8array-tools");
|
|
17
|
+
const miniscript_1 = require("./miniscript");
|
|
18
|
+
const tapTree_1 = require("./tapTree");
|
|
19
|
+
const resourceLimits_1 = require("./resourceLimits");
|
|
20
|
+
const TAPROOT_LEAF_VERSION_TAPSCRIPT = 0xc0;
|
|
21
|
+
function expandTaprootMiniscript({ miniscript, network = bitcoinjs_lib_1.networks.bitcoin, BIP32, ECPair }) {
|
|
22
|
+
return (0, miniscript_1.expandMiniscript)({
|
|
23
|
+
miniscript,
|
|
24
|
+
isSegwit: true,
|
|
25
|
+
isTaproot: true,
|
|
26
|
+
network,
|
|
27
|
+
BIP32,
|
|
28
|
+
ECPair
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 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.
|
|
36
|
+
*/
|
|
37
|
+
function buildTapTreeInfo({ tapTree, network = bitcoinjs_lib_1.networks.bitcoin, BIP32, ECPair }) {
|
|
38
|
+
// Defensive: parseTapTreeExpression() already enforces this for descriptor
|
|
39
|
+
// strings, but buildTapTreeInfo is exported and can be called directly.
|
|
40
|
+
(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
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
miniscript,
|
|
56
|
+
expandedMiniscript,
|
|
57
|
+
expansionMap,
|
|
58
|
+
tapScript,
|
|
59
|
+
version: TAPROOT_LEAF_VERSION_TAPSCRIPT
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
left: buildTapTreeInfo({ tapTree: tapTree.left, network, BIP32, ECPair }),
|
|
64
|
+
right: buildTapTreeInfo({ tapTree: tapTree.right, network, BIP32, ECPair })
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function tapTreeInfoToScriptTree(tapTreeInfo) {
|
|
68
|
+
if ('miniscript' in tapTreeInfo) {
|
|
69
|
+
return {
|
|
70
|
+
output: tapTreeInfo.tapScript,
|
|
71
|
+
version: tapTreeInfo.version
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return [
|
|
75
|
+
tapTreeInfoToScriptTree(tapTreeInfo.left),
|
|
76
|
+
tapTreeInfoToScriptTree(tapTreeInfo.right)
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Builds taproot PSBT leaf metadata for every leaf in a `tapTreeInfo`.
|
|
81
|
+
*
|
|
82
|
+
* For each leaf, this function computes:
|
|
83
|
+
* - `tapLeafHash`: BIP341 leaf hash of tapscript + leaf version
|
|
84
|
+
* - `depth`: leaf depth in the tree (root children have depth 1)
|
|
85
|
+
* - `controlBlock`: script-path proof used in PSBT `tapLeafScript`
|
|
86
|
+
*
|
|
87
|
+
* The control block layout is:
|
|
88
|
+
*
|
|
89
|
+
* ```text
|
|
90
|
+
* [1-byte (leafVersion | parity)] [32-byte internal key]
|
|
91
|
+
* [32-byte sibling hash #1] ... [32-byte sibling hash #N]
|
|
92
|
+
* ```
|
|
93
|
+
*
|
|
94
|
+
* where:
|
|
95
|
+
* - `parity` is derived from tweaking the internal key with the tree root
|
|
96
|
+
* - sibling hashes are the merkle path from that leaf to the root
|
|
97
|
+
*
|
|
98
|
+
* Example tree:
|
|
99
|
+
*
|
|
100
|
+
* ```text
|
|
101
|
+
* root
|
|
102
|
+
* / \
|
|
103
|
+
* L1 L2
|
|
104
|
+
* / \
|
|
105
|
+
* L3 L4
|
|
106
|
+
* ```
|
|
107
|
+
*
|
|
108
|
+
* Depths:
|
|
109
|
+
* - L1 depth = 1
|
|
110
|
+
* - L3 depth = 2
|
|
111
|
+
* - L4 depth = 2
|
|
112
|
+
*
|
|
113
|
+
* Conceptual output:
|
|
114
|
+
*
|
|
115
|
+
* ```text
|
|
116
|
+
* [
|
|
117
|
+
* L1 -> { depth: 1, tapLeafHash: h1, controlBlock: [v|p, ik, hash(L2)] }
|
|
118
|
+
* L3 -> { depth: 2, tapLeafHash: h3, controlBlock: [v|p, ik, hash(L4), hash(L1)] }
|
|
119
|
+
* L4 -> { depth: 2, tapLeafHash: h4, controlBlock: [v|p, ik, hash(L3), hash(L1)] }
|
|
120
|
+
* ]
|
|
121
|
+
* ```
|
|
122
|
+
*
|
|
123
|
+
* Legend:
|
|
124
|
+
* - `ik`: the 32-byte internal key placed in the control block.
|
|
125
|
+
* - `hash(X)`: the merkle sibling hash at each level when proving leaf `X`.
|
|
126
|
+
*
|
|
127
|
+
* Note: in this diagram, `L2` is a branch node (right subtree), not a leaf,
|
|
128
|
+
* so `hash(L2) = TapBranch(hash(L3), hash(L4))`.
|
|
129
|
+
*
|
|
130
|
+
* Notes:
|
|
131
|
+
* - Leaves are returned in deterministic left-first order.
|
|
132
|
+
* - One metadata entry is returned per leaf.
|
|
133
|
+
* - `controlBlock.length === 33 + 32 * depth`.
|
|
134
|
+
* - Throws if internal key is invalid or merkle path cannot be found.
|
|
135
|
+
*
|
|
136
|
+
* Typical usage:
|
|
137
|
+
* - Convert this metadata into PSBT `tapLeafScript[]` entries
|
|
138
|
+
* for all leaves.
|
|
139
|
+
*/
|
|
140
|
+
function buildTaprootLeafPsbtMetadata({ tapTreeInfo, internalPubkey }) {
|
|
141
|
+
const normalizedInternalPubkey = normalizeTaprootPubkey(internalPubkey);
|
|
142
|
+
const scriptTree = tapTreeInfoToScriptTree(tapTreeInfo);
|
|
143
|
+
const hashTree = (0, bitcoinjs_lib_internals_1.toHashTree)(scriptTree);
|
|
144
|
+
const tweaked = (0, bitcoinjs_lib_internals_1.tweakKey)(normalizedInternalPubkey, hashTree.hash);
|
|
145
|
+
if (!tweaked)
|
|
146
|
+
throw new Error(`Error: invalid taproot internal pubkey`);
|
|
147
|
+
return (0, tapTree_1.collectTapTreeLeaves)(tapTreeInfo).map(({ leaf, depth }) => {
|
|
148
|
+
if (depth > tapTree_1.MAX_TAPTREE_DEPTH)
|
|
149
|
+
throw new Error(`Error: taproot tree depth is too large, ${depth} is larger than ${tapTree_1.MAX_TAPTREE_DEPTH}`);
|
|
150
|
+
const tapLeafHash = (0, bitcoinjs_lib_internals_1.tapleafHash)({
|
|
151
|
+
output: leaf.tapScript,
|
|
152
|
+
version: leaf.version
|
|
153
|
+
});
|
|
154
|
+
const merklePath = (0, bitcoinjs_lib_internals_1.findScriptPath)(hashTree, tapLeafHash);
|
|
155
|
+
if (!merklePath)
|
|
156
|
+
throw new Error(`Error: could not build controlBlock for taproot leaf ${leaf.miniscript}`);
|
|
157
|
+
// controlBlock[0] packs:
|
|
158
|
+
// - leaf version (high bits), and
|
|
159
|
+
// - parity of the tweaked output key Q = P + t*G (low bit).
|
|
160
|
+
// `normalizedInternalPubkey` is x-only P, so parity is not encoded there.
|
|
161
|
+
// BIP341 requires carrying Q parity in controlBlock[0].
|
|
162
|
+
const controlBlock = (0, uint8array_tools_1.concat)([
|
|
163
|
+
Uint8Array.from([leaf.version | tweaked.parity]),
|
|
164
|
+
normalizedInternalPubkey,
|
|
165
|
+
...merklePath
|
|
166
|
+
]);
|
|
167
|
+
return { leaf, depth, tapLeafHash, controlBlock };
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Builds PSBT `tapBip32Derivation` entries for taproot script-path spends.
|
|
172
|
+
*
|
|
173
|
+
* Leaf keys include the list of tapleaf hashes where they appear.
|
|
174
|
+
* If `internalKeyInfo` has derivation data, it is included with empty
|
|
175
|
+
* `leafHashes`.
|
|
176
|
+
*
|
|
177
|
+
* Example tree:
|
|
178
|
+
*
|
|
179
|
+
* ```text
|
|
180
|
+
* root
|
|
181
|
+
* / \
|
|
182
|
+
* L1 L2
|
|
183
|
+
*
|
|
184
|
+
* L1 uses key A
|
|
185
|
+
* L2 uses key A and key B
|
|
186
|
+
*
|
|
187
|
+
* h1 = tapleafHash(L1)
|
|
188
|
+
* h2 = tapleafHash(L2)
|
|
189
|
+
* ```
|
|
190
|
+
*
|
|
191
|
+
* Then output is conceptually:
|
|
192
|
+
*
|
|
193
|
+
* ```text
|
|
194
|
+
* [
|
|
195
|
+
* key A -> leafHashes [h1, h2]
|
|
196
|
+
* key B -> leafHashes [h2]
|
|
197
|
+
* internal key -> leafHashes []
|
|
198
|
+
* ]
|
|
199
|
+
* ```
|
|
200
|
+
*
|
|
201
|
+
* Notes:
|
|
202
|
+
* - Keys missing `masterFingerprint` or `path` are skipped.
|
|
203
|
+
* - Duplicate pubkeys are merged.
|
|
204
|
+
* - If the same pubkey appears with conflicting derivation metadata,
|
|
205
|
+
* this function throws.
|
|
206
|
+
* - Output and `leafHashes` are sorted deterministically.
|
|
207
|
+
*/
|
|
208
|
+
function buildTaprootBip32Derivations({ tapTreeInfo, internalKeyInfo }) {
|
|
209
|
+
const entries = new Map();
|
|
210
|
+
const updateAndInsert = ({ pubkey, masterFingerprint, path, leafHash }) => {
|
|
211
|
+
const normalizedPubkey = normalizeTaprootPubkey(pubkey);
|
|
212
|
+
const pubkeyHex = (0, uint8array_tools_1.toHex)(normalizedPubkey);
|
|
213
|
+
const current = entries.get(pubkeyHex);
|
|
214
|
+
if (!current) {
|
|
215
|
+
const next = {
|
|
216
|
+
masterFingerprint,
|
|
217
|
+
pubkey: normalizedPubkey,
|
|
218
|
+
path,
|
|
219
|
+
leafHashes: new Map()
|
|
220
|
+
};
|
|
221
|
+
if (leafHash)
|
|
222
|
+
next.leafHashes.set((0, uint8array_tools_1.toHex)(leafHash), leafHash);
|
|
223
|
+
entries.set(pubkeyHex, next);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if ((0, uint8array_tools_1.compare)(current.masterFingerprint, masterFingerprint) !== 0 ||
|
|
227
|
+
current.path !== path) {
|
|
228
|
+
throw new Error(`Error: inconsistent taproot key derivation metadata for pubkey ${pubkeyHex}`);
|
|
229
|
+
}
|
|
230
|
+
if (leafHash)
|
|
231
|
+
current.leafHashes.set((0, uint8array_tools_1.toHex)(leafHash), leafHash);
|
|
232
|
+
};
|
|
233
|
+
const leaves = (0, tapTree_1.collectTapTreeLeaves)(tapTreeInfo);
|
|
234
|
+
for (const { leaf } of leaves) {
|
|
235
|
+
const leafHash = (0, bitcoinjs_lib_internals_1.tapleafHash)({
|
|
236
|
+
output: leaf.tapScript,
|
|
237
|
+
version: leaf.version
|
|
238
|
+
});
|
|
239
|
+
for (const keyInfo of Object.values(leaf.expansionMap)) {
|
|
240
|
+
if (!keyInfo.pubkey || !keyInfo.masterFingerprint || !keyInfo.path)
|
|
241
|
+
continue;
|
|
242
|
+
updateAndInsert({
|
|
243
|
+
pubkey: keyInfo.pubkey,
|
|
244
|
+
masterFingerprint: keyInfo.masterFingerprint,
|
|
245
|
+
path: keyInfo.path,
|
|
246
|
+
leafHash
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (internalKeyInfo?.pubkey &&
|
|
251
|
+
internalKeyInfo.masterFingerprint &&
|
|
252
|
+
internalKeyInfo.path) {
|
|
253
|
+
updateAndInsert({
|
|
254
|
+
pubkey: internalKeyInfo.pubkey,
|
|
255
|
+
masterFingerprint: internalKeyInfo.masterFingerprint,
|
|
256
|
+
path: internalKeyInfo.path
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return [...entries.entries()]
|
|
260
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
261
|
+
.map(([, entry]) => ({
|
|
262
|
+
masterFingerprint: entry.masterFingerprint,
|
|
263
|
+
pubkey: entry.pubkey,
|
|
264
|
+
path: entry.path,
|
|
265
|
+
leafHashes: [...entry.leafHashes.entries()]
|
|
266
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
267
|
+
.map(([, leafHash]) => leafHash)
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
270
|
+
function varSliceSize(someScript) {
|
|
271
|
+
const length = someScript.length;
|
|
272
|
+
return (0, varuint_bitcoin_1.encodingLength)(length) + length;
|
|
273
|
+
}
|
|
274
|
+
function vectorSize(someVector) {
|
|
275
|
+
const length = someVector.length;
|
|
276
|
+
return ((0, varuint_bitcoin_1.encodingLength)(length) +
|
|
277
|
+
someVector.reduce((sum, witness) => sum + varSliceSize(witness), 0));
|
|
278
|
+
}
|
|
279
|
+
function witnessStackSize(witness) {
|
|
280
|
+
return vectorSize(witness);
|
|
281
|
+
}
|
|
282
|
+
function estimateTaprootWitnessSize({ stackItems, tapScript, depth }) {
|
|
283
|
+
if (depth > tapTree_1.MAX_TAPTREE_DEPTH)
|
|
284
|
+
throw new Error(`Error: taproot tree depth is too large, ${depth} is larger than ${tapTree_1.MAX_TAPTREE_DEPTH}`);
|
|
285
|
+
const controlBlock = new Uint8Array(33 + 32 * depth);
|
|
286
|
+
return witnessStackSize([...stackItems, tapScript, controlBlock]);
|
|
287
|
+
}
|
|
288
|
+
function normalizeTaprootPubkey(pubkey) {
|
|
289
|
+
if (pubkey.length === 32)
|
|
290
|
+
return pubkey;
|
|
291
|
+
if (pubkey.length === 33)
|
|
292
|
+
return pubkey.slice(1, 33);
|
|
293
|
+
throw new Error(`Error: invalid taproot pubkey length`);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Computes satisfactions for taproot script-path leaves.
|
|
297
|
+
*
|
|
298
|
+
* If `tapLeaf` is undefined, all satisfiable leaves are returned. If `tapLeaf`
|
|
299
|
+
* is provided, only that leaf is considered.
|
|
300
|
+
*
|
|
301
|
+
* Callers are expected to pass real signatures, or fake signatures generated
|
|
302
|
+
* during planning. See satisfyMiniscript() for how timeConstraints keep the
|
|
303
|
+
* chosen leaf consistent between planning and signing.
|
|
304
|
+
*/
|
|
305
|
+
function collectTaprootLeafSatisfactions({ tapTreeInfo, preimages, signatures, timeConstraints, tapLeaf }) {
|
|
306
|
+
const candidates = (0, tapTree_1.selectTapLeafCandidates)({
|
|
307
|
+
tapTreeInfo,
|
|
308
|
+
...(tapLeaf !== undefined ? { tapLeaf } : {})
|
|
309
|
+
});
|
|
310
|
+
const getLeafPubkeys = (leaf) => {
|
|
311
|
+
return Object.values(leaf.expansionMap).map(keyInfo => {
|
|
312
|
+
if (!keyInfo.pubkey)
|
|
313
|
+
throw new Error(`Error: taproot leaf key missing pubkey`);
|
|
314
|
+
return normalizeTaprootPubkey(keyInfo.pubkey);
|
|
315
|
+
});
|
|
316
|
+
};
|
|
317
|
+
const resolveLeafSignatures = (leaf) => {
|
|
318
|
+
const leafPubkeys = getLeafPubkeys(leaf);
|
|
319
|
+
const leafPubkeySet = new Set(leafPubkeys.map(pubkey => (0, uint8array_tools_1.toHex)(pubkey)));
|
|
320
|
+
return signatures
|
|
321
|
+
.map((sig) => ({
|
|
322
|
+
pubkey: normalizeTaprootPubkey(sig.pubkey),
|
|
323
|
+
signature: sig.signature
|
|
324
|
+
}))
|
|
325
|
+
.filter((sig) => leafPubkeySet.has((0, uint8array_tools_1.toHex)(sig.pubkey)));
|
|
326
|
+
};
|
|
327
|
+
const satisfactions = [];
|
|
328
|
+
for (const candidate of candidates) {
|
|
329
|
+
const { leaf } = candidate;
|
|
330
|
+
const leafSignatures = resolveLeafSignatures(leaf);
|
|
331
|
+
try {
|
|
332
|
+
const { scriptSatisfaction, nLockTime, nSequence } = (0, miniscript_1.satisfyMiniscript)({
|
|
333
|
+
expandedMiniscript: leaf.expandedMiniscript,
|
|
334
|
+
expansionMap: leaf.expansionMap,
|
|
335
|
+
signatures: leafSignatures,
|
|
336
|
+
preimages,
|
|
337
|
+
...(timeConstraints !== undefined ? { timeConstraints } : {}),
|
|
338
|
+
tapscript: true
|
|
339
|
+
});
|
|
340
|
+
const satisfactionStackItems = bitcoinjs_lib_1.script.toStack(scriptSatisfaction);
|
|
341
|
+
(0, resourceLimits_1.assertTaprootScriptPathSatisfactionResourceLimits)({
|
|
342
|
+
stackItems: satisfactionStackItems
|
|
343
|
+
});
|
|
344
|
+
const totalWitnessSize = estimateTaprootWitnessSize({
|
|
345
|
+
stackItems: satisfactionStackItems,
|
|
346
|
+
tapScript: leaf.tapScript,
|
|
347
|
+
depth: candidate.depth
|
|
348
|
+
});
|
|
349
|
+
satisfactions.push({
|
|
350
|
+
leaf,
|
|
351
|
+
depth: candidate.depth,
|
|
352
|
+
tapLeafHash: candidate.tapLeafHash,
|
|
353
|
+
scriptSatisfaction,
|
|
354
|
+
stackItems: satisfactionStackItems,
|
|
355
|
+
nLockTime,
|
|
356
|
+
nSequence,
|
|
357
|
+
totalWitnessSize
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
if (tapLeaf !== undefined)
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (satisfactions.length === 0)
|
|
366
|
+
throw new Error(`Error: no satisfiable taproot leaves found`);
|
|
367
|
+
return satisfactions;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Selects the taproot leaf satisfaction with the smallest total witness size.
|
|
371
|
+
* Assumes the input list is in left-first tree order for deterministic ties.
|
|
372
|
+
*/
|
|
373
|
+
function selectBestTaprootLeafSatisfaction(satisfactions) {
|
|
374
|
+
return satisfactions.reduce((best, current) => {
|
|
375
|
+
if (!best)
|
|
376
|
+
return current;
|
|
377
|
+
if (current.totalWitnessSize < best.totalWitnessSize)
|
|
378
|
+
return current;
|
|
379
|
+
return best;
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Collects a unique set of taproot leaf pubkeys (x-only) across the tree.
|
|
384
|
+
* This is useful for building fake signatures when no signer subset is given.
|
|
385
|
+
*/
|
|
386
|
+
function collectTapTreePubkeys(tapTreeInfo) {
|
|
387
|
+
const pubkeySet = new Set();
|
|
388
|
+
const pubkeys = [];
|
|
389
|
+
const leaves = (0, tapTree_1.collectTapTreeLeaves)(tapTreeInfo);
|
|
390
|
+
for (const entry of leaves) {
|
|
391
|
+
for (const keyInfo of Object.values(entry.leaf.expansionMap)) {
|
|
392
|
+
if (!keyInfo.pubkey)
|
|
393
|
+
throw new Error(`Error: taproot leaf key missing pubkey`);
|
|
394
|
+
const normalized = normalizeTaprootPubkey(keyInfo.pubkey);
|
|
395
|
+
const hex = (0, uint8array_tools_1.toHex)(normalized);
|
|
396
|
+
if (pubkeySet.has(hex))
|
|
397
|
+
continue;
|
|
398
|
+
pubkeySet.add(hex);
|
|
399
|
+
pubkeys.push(normalized);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return pubkeys;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Returns the best satisfaction for a taproot tree, by witness size.
|
|
406
|
+
*
|
|
407
|
+
* If `tapLeaf` is provided, only that leaf is considered. If `tapLeaf` is a
|
|
408
|
+
* 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).
|
|
411
|
+
*
|
|
412
|
+
* This function is typically called twice:
|
|
413
|
+
* 1) Planning pass: call it with fake signatures (built by the caller) to
|
|
414
|
+
* choose the best leaf without requiring user signatures.
|
|
415
|
+
* 2) Signing pass: call it again with real signatures and the timeConstraints
|
|
416
|
+
* returned from the first pass (see satisfyMiniscript() for why this keeps
|
|
417
|
+
* the chosen leaf consistent between planning and signing).
|
|
418
|
+
*/
|
|
419
|
+
function satisfyTapTree({ tapTreeInfo, signatures, preimages, tapLeaf, timeConstraints }) {
|
|
420
|
+
const satisfactions = collectTaprootLeafSatisfactions({
|
|
421
|
+
tapTreeInfo,
|
|
422
|
+
preimages,
|
|
423
|
+
signatures,
|
|
424
|
+
...(tapLeaf !== undefined ? { tapLeaf } : {}),
|
|
425
|
+
...(timeConstraints !== undefined ? { timeConstraints } : {})
|
|
426
|
+
});
|
|
427
|
+
return selectBestTaprootLeafSatisfaction(satisfactions);
|
|
428
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ExpansionMap } from './types';
|
|
2
|
+
export type TreeNode<TLeaf> = TLeaf | {
|
|
3
|
+
left: TreeNode<TLeaf>;
|
|
4
|
+
right: TreeNode<TLeaf>;
|
|
5
|
+
};
|
|
6
|
+
export type TapLeaf = {
|
|
7
|
+
miniscript: string;
|
|
8
|
+
};
|
|
9
|
+
export type TapTreeNode = TreeNode<TapLeaf>;
|
|
10
|
+
export type TapLeafInfo = {
|
|
11
|
+
miniscript: string;
|
|
12
|
+
expandedMiniscript: string;
|
|
13
|
+
expansionMap: ExpansionMap;
|
|
14
|
+
tapScript: Uint8Array;
|
|
15
|
+
version: number;
|
|
16
|
+
};
|
|
17
|
+
export type TapTreeInfoNode = TreeNode<TapLeafInfo>;
|
|
18
|
+
export type TapLeafSelection = {
|
|
19
|
+
leaf: TapLeafInfo;
|
|
20
|
+
depth: number;
|
|
21
|
+
tapLeafHash: Uint8Array;
|
|
22
|
+
};
|
|
23
|
+
export declare const MAX_TAPTREE_DEPTH = 128;
|
|
24
|
+
export declare function assertTapTreeDepth(tapTree: TapTreeNode): void;
|
|
25
|
+
/**
|
|
26
|
+
* Collects taproot leaf metadata with depth from a tree.
|
|
27
|
+
* Traversal is left-first, following the order of `{left,right}` in the
|
|
28
|
+
* expression so tie-breaks are deterministic.
|
|
29
|
+
*
|
|
30
|
+
* Example tree:
|
|
31
|
+
* ```
|
|
32
|
+
* {pk(A),{pk(B),pk(C)}}
|
|
33
|
+
* ```
|
|
34
|
+
* Visual shape:
|
|
35
|
+
* ```
|
|
36
|
+
* root
|
|
37
|
+
* / \
|
|
38
|
+
* pk(A) branch
|
|
39
|
+
* / \
|
|
40
|
+
* pk(B) pk(C)
|
|
41
|
+
* ```
|
|
42
|
+
* Collected leaves with depth:
|
|
43
|
+
* ```
|
|
44
|
+
* [
|
|
45
|
+
* { leaf: pk(A), depth: 1 },
|
|
46
|
+
* { leaf: pk(B), depth: 2 },
|
|
47
|
+
* { leaf: pk(C), depth: 2 }
|
|
48
|
+
* ]
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare function collectTapTreeLeaves(tapTreeInfo: TapTreeInfoNode): Array<{
|
|
52
|
+
leaf: TapLeafInfo;
|
|
53
|
+
depth: number;
|
|
54
|
+
}>;
|
|
55
|
+
/**
|
|
56
|
+
* Resolves taproot leaf candidates based on an optional selector.
|
|
57
|
+
*
|
|
58
|
+
* If `tapLeaf` is undefined, all leaves are returned for auto-selection.
|
|
59
|
+
* If `tapLeaf` is bytes, it is treated as a tapleaf hash and must match
|
|
60
|
+
* 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
|
|
63
|
+
* more than once, this function throws an error.
|
|
64
|
+
*
|
|
65
|
+
* Example:
|
|
66
|
+
* ```
|
|
67
|
+
* const candidates = selectTapLeafCandidates({ tapTreeInfo, tapLeaf });
|
|
68
|
+
* // tapLeaf can be undefined, bytes (tapleaf hash) or a miniscript string:
|
|
69
|
+
* // f.ex.: 'pk(03bb...)'
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export declare function selectTapLeafCandidates({ tapTreeInfo, tapLeaf }: {
|
|
73
|
+
tapTreeInfo: TapTreeInfoNode;
|
|
74
|
+
tapLeaf?: Uint8Array | string;
|
|
75
|
+
}): TapLeafSelection[];
|
|
76
|
+
export declare function parseTapTreeExpression(expression: string): TapTreeNode;
|
package/dist/tapTree.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_TAPTREE_DEPTH = void 0;
|
|
4
|
+
exports.assertTapTreeDepth = assertTapTreeDepth;
|
|
5
|
+
exports.collectTapTreeLeaves = collectTapTreeLeaves;
|
|
6
|
+
exports.selectTapLeafCandidates = selectTapLeafCandidates;
|
|
7
|
+
exports.parseTapTreeExpression = parseTapTreeExpression;
|
|
8
|
+
// NOTE: This uses an internal bitcoinjs-lib module. Consider adding a local wrapper.
|
|
9
|
+
const bitcoinjs_lib_internals_1 = require("./bitcoinjs-lib-internals");
|
|
10
|
+
const uint8array_tools_1 = require("uint8array-tools");
|
|
11
|
+
const parseUtils_1 = require("./parseUtils");
|
|
12
|
+
// See BIP341 control block limits and Sipa's Miniscript "Resource limitations":
|
|
13
|
+
// https://bitcoin.sipa.be/miniscript/
|
|
14
|
+
// Taproot script path depth is encoded in the control block as 32-byte hashes,
|
|
15
|
+
// with consensus max depth 128.
|
|
16
|
+
exports.MAX_TAPTREE_DEPTH = 128;
|
|
17
|
+
function tapTreeMaxDepth(tapTree, depth = 0) {
|
|
18
|
+
if ('miniscript' in tapTree)
|
|
19
|
+
return depth;
|
|
20
|
+
return Math.max(tapTreeMaxDepth(tapTree.left, depth + 1), tapTreeMaxDepth(tapTree.right, depth + 1));
|
|
21
|
+
}
|
|
22
|
+
function assertTapTreeDepth(tapTree) {
|
|
23
|
+
const maxDepth = tapTreeMaxDepth(tapTree);
|
|
24
|
+
if (maxDepth > exports.MAX_TAPTREE_DEPTH)
|
|
25
|
+
throw new Error(`Error: taproot tree depth is too large, ${maxDepth} is larger than ${exports.MAX_TAPTREE_DEPTH}`);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Collects taproot leaf metadata with depth from a tree.
|
|
29
|
+
* Traversal is left-first, following the order of `{left,right}` in the
|
|
30
|
+
* expression so tie-breaks are deterministic.
|
|
31
|
+
*
|
|
32
|
+
* Example tree:
|
|
33
|
+
* ```
|
|
34
|
+
* {pk(A),{pk(B),pk(C)}}
|
|
35
|
+
* ```
|
|
36
|
+
* Visual shape:
|
|
37
|
+
* ```
|
|
38
|
+
* root
|
|
39
|
+
* / \
|
|
40
|
+
* pk(A) branch
|
|
41
|
+
* / \
|
|
42
|
+
* pk(B) pk(C)
|
|
43
|
+
* ```
|
|
44
|
+
* Collected leaves with depth:
|
|
45
|
+
* ```
|
|
46
|
+
* [
|
|
47
|
+
* { leaf: pk(A), depth: 1 },
|
|
48
|
+
* { leaf: pk(B), depth: 2 },
|
|
49
|
+
* { leaf: pk(C), depth: 2 }
|
|
50
|
+
* ]
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
function collectTapTreeLeaves(tapTreeInfo) {
|
|
54
|
+
const leaves = [];
|
|
55
|
+
const walk = (node, depth) => {
|
|
56
|
+
if ('miniscript' in node) {
|
|
57
|
+
leaves.push({ leaf: node, depth });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
walk(node.left, depth + 1);
|
|
61
|
+
walk(node.right, depth + 1);
|
|
62
|
+
};
|
|
63
|
+
walk(tapTreeInfo, 0);
|
|
64
|
+
return leaves;
|
|
65
|
+
}
|
|
66
|
+
function computeTapLeafHash(leaf) {
|
|
67
|
+
return (0, bitcoinjs_lib_internals_1.tapleafHash)({ output: leaf.tapScript, version: leaf.version });
|
|
68
|
+
}
|
|
69
|
+
function normalizeMiniscriptForMatch(miniscript) {
|
|
70
|
+
return miniscript.replace(/\s+/g, '');
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Resolves taproot leaf candidates based on an optional selector.
|
|
74
|
+
*
|
|
75
|
+
* If `tapLeaf` is undefined, all leaves are returned for auto-selection.
|
|
76
|
+
* If `tapLeaf` is bytes, it is treated as a tapleaf hash and must match
|
|
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
|
|
80
|
+
* more than once, this function throws an error.
|
|
81
|
+
*
|
|
82
|
+
* Example:
|
|
83
|
+
* ```
|
|
84
|
+
* const candidates = selectTapLeafCandidates({ tapTreeInfo, tapLeaf });
|
|
85
|
+
* // tapLeaf can be undefined, bytes (tapleaf hash) or a miniscript string:
|
|
86
|
+
* // f.ex.: 'pk(03bb...)'
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
function selectTapLeafCandidates({ tapTreeInfo, tapLeaf }) {
|
|
90
|
+
const leaves = collectTapTreeLeaves(tapTreeInfo).map(({ leaf, depth }) => ({
|
|
91
|
+
leaf,
|
|
92
|
+
depth,
|
|
93
|
+
tapLeafHash: computeTapLeafHash(leaf)
|
|
94
|
+
}));
|
|
95
|
+
if (tapLeaf === undefined)
|
|
96
|
+
return leaves;
|
|
97
|
+
if (tapLeaf instanceof Uint8Array) {
|
|
98
|
+
const match = leaves.find(entry => (0, uint8array_tools_1.compare)(entry.tapLeafHash, tapLeaf) === 0);
|
|
99
|
+
if (!match)
|
|
100
|
+
throw new Error(`Error: tapleaf hash not found in tapTreeInfo`);
|
|
101
|
+
return [match];
|
|
102
|
+
}
|
|
103
|
+
const normalizedSelector = normalizeMiniscriptForMatch(tapLeaf);
|
|
104
|
+
const matches = leaves.filter(entry => normalizeMiniscriptForMatch(entry.leaf.miniscript) === normalizedSelector);
|
|
105
|
+
if (matches.length === 0)
|
|
106
|
+
throw new Error(`Error: miniscript leaf not found in tapTreeInfo: ${tapLeaf}`);
|
|
107
|
+
if (matches.length > 1)
|
|
108
|
+
throw new Error(`Error: miniscript leaf is ambiguous in tapTreeInfo: ${tapLeaf}`);
|
|
109
|
+
return matches;
|
|
110
|
+
}
|
|
111
|
+
function tapTreeError(expression) {
|
|
112
|
+
return new Error(`Error: invalid taproot tree expression: ${expression}`);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Splits the inner tree expression of a branch into left/right parts.
|
|
116
|
+
* The input must be the contents inside `{}` (no outer braces).
|
|
117
|
+
* Example: `pk(@0),{pk(@1),pk(@2)}` => left: `pk(@0)`, right: `{pk(@1),pk(@2)}`.
|
|
118
|
+
*/
|
|
119
|
+
function splitTapTreeExpression(expression) {
|
|
120
|
+
const result = (0, parseUtils_1.splitTopLevelComma)({ expression, onError: tapTreeError });
|
|
121
|
+
if (!result)
|
|
122
|
+
throw tapTreeError(expression);
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Parses a single taproot tree node expression.
|
|
127
|
+
* Examples:
|
|
128
|
+
* - `pk(@0)` => { miniscript: 'pk(@0)' }
|
|
129
|
+
* - `{pk(@0),pk(@1)}` => { left: { miniscript: 'pk(@0)' }, right: { miniscript: 'pk(@1)' } }
|
|
130
|
+
* - `{pk(@0),{pk(@1),pk(@2)}}` =>
|
|
131
|
+
* {
|
|
132
|
+
* left: { miniscript: 'pk(@0)' },
|
|
133
|
+
* right: { left: { miniscript: 'pk(@1)' }, right: { miniscript: 'pk(@2)' } }
|
|
134
|
+
* }
|
|
135
|
+
*/
|
|
136
|
+
function parseTapTreeNode(expression) {
|
|
137
|
+
const trimmedExpression = expression.trim();
|
|
138
|
+
if (!trimmedExpression)
|
|
139
|
+
throw tapTreeError(expression);
|
|
140
|
+
if (trimmedExpression.startsWith('{')) {
|
|
141
|
+
if (!trimmedExpression.endsWith('}'))
|
|
142
|
+
throw tapTreeError(expression);
|
|
143
|
+
const inner = trimmedExpression.slice(1, -1).trim();
|
|
144
|
+
if (!inner)
|
|
145
|
+
throw tapTreeError(expression);
|
|
146
|
+
const { left, right } = splitTapTreeExpression(inner);
|
|
147
|
+
return {
|
|
148
|
+
left: parseTapTreeNode(left),
|
|
149
|
+
right: parseTapTreeNode(right)
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (trimmedExpression.includes('{') || trimmedExpression.includes('}'))
|
|
153
|
+
throw tapTreeError(expression);
|
|
154
|
+
return { miniscript: trimmedExpression };
|
|
155
|
+
}
|
|
156
|
+
function parseTapTreeExpression(expression) {
|
|
157
|
+
const trimmed = expression.trim();
|
|
158
|
+
if (!trimmed)
|
|
159
|
+
throw tapTreeError(expression);
|
|
160
|
+
const tapTree = parseTapTreeNode(trimmed);
|
|
161
|
+
assertTapTreeDepth(tapTree);
|
|
162
|
+
return tapTree;
|
|
163
|
+
}
|