Skip to content

Commit 9989088

Browse files
committed
feat(p2tr): PSBT-style tree coding
Deprecates weight-based tap tree coding Issue: BG-54756
1 parent fa8b104 commit 9989088

File tree

8 files changed

+188
-38
lines changed

8 files changed

+188
-38
lines changed

src/payments/index.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/// <reference types="node" />
22
import { Network } from '../networks';
3+
import { TapTree } from 'bip174/src/lib/interfaces';
34
import { TinySecp256k1Interface } from '../types';
45
import { p2data as embed } from './embed';
56
import { p2ms } from './p2ms';
@@ -29,7 +30,9 @@ export interface Payment {
2930
redeemIndex?: number;
3031
witness?: Buffer[];
3132
weight?: number;
33+
depth?: number;
3234
controlBlock?: Buffer;
35+
tapTree?: TapTree;
3336
annex?: Buffer;
3437
}
3538
export declare type PaymentCreator = (a: Payment, opts?: PaymentOpts) => Payment;

src/payments/p2tr.js

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ function p2tr(a, opts) {
5151
network: typef.maybe(typef.Object),
5252
output: typef.maybe(typef.Buffer),
5353
weight: typef.maybe(typef.Number),
54+
depth: typef.maybe(typef.Number),
5455
witness: typef.maybe(typef.arrayOf(typef.Buffer)),
5556
}),
5657
),
@@ -77,8 +78,13 @@ function p2tr(a, opts) {
7778
// extract the 32 byte taproot pubkey (aka witness program)
7879
return a.output && a.output.slice(2);
7980
});
80-
const _taptree = lazy.value(() => {
81+
const network = a.network || networks_1.bitcoin;
82+
const o = { network };
83+
const _taprootPaths = lazy.value(() => {
8184
if (!a.redeems) return;
85+
if (o.tapTree) {
86+
return taproot.getDepthFirstTaptree(o.tapTree);
87+
}
8288
const outputs = a.redeems.map(({ output }) => output);
8389
if (!outputs.every(output => output)) return;
8490
return taproot.getHuffmanTaptree(
@@ -146,11 +152,29 @@ function p2tr(a, opts) {
146152
tapscript,
147153
);
148154
}
149-
if (!taptreeRoot && _taptree()) taptreeRoot = _taptree().root;
155+
if (!taptreeRoot && _taprootPaths()) taptreeRoot = _taprootPaths().root;
150156
return taproot.tapTweakPubkey(ecc, _internalPubkey(), taptreeRoot);
151157
});
152-
const network = a.network || networks_1.bitcoin;
153-
const o = { network };
158+
lazy.prop(o, 'tapTree', () => {
159+
if (!a.redeems) return;
160+
if (a.redeems.find(({ depth }) => depth === undefined)) {
161+
console.warn(
162+
'Deprecation Warning: Weight-based tap tree construction will be removed in the future. ' +
163+
'Please use depth-first coding as specified in BIP-0371.',
164+
);
165+
return;
166+
}
167+
if (!a.redeems.every(({ output }) => output)) return;
168+
return {
169+
leaves: a.redeems.map(({ output, depth }) => {
170+
return {
171+
script: output,
172+
leafVersion: taproot.INITIAL_TAPSCRIPT_VERSION,
173+
depth,
174+
};
175+
}),
176+
};
177+
});
154178
lazy.prop(o, 'address', () => {
155179
const pubkey =
156180
_outputPubkey() || (_taprootPubkey() && _taprootPubkey().xOnlyPubkey);
@@ -164,12 +188,12 @@ function p2tr(a, opts) {
164188
if (parsedWitness && parsedWitness.spendType === 'Script')
165189
return parsedWitness.controlBlock;
166190
const taprootPubkey = _taprootPubkey();
167-
const taptree = _taptree();
168-
if (!taptree || !taprootPubkey || a.redeemIndex === undefined) return;
191+
const taprootPaths = _taprootPaths();
192+
if (!taprootPaths || !taprootPubkey || a.redeemIndex === undefined) return;
169193
return taproot.getControlBlock(
170194
taprootPubkey.parity,
171195
_internalPubkey(),
172-
taptree.paths[a.redeemIndex],
196+
taprootPaths.paths[a.redeemIndex],
173197
);
174198
});
175199
lazy.prop(o, 'signature', () => {

src/taproot.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/// <reference types="node" />
2+
import { TapTree as PsbtTapTree } from 'bip174/src/lib/interfaces';
23
import { TinySecp256k1Interface, XOnlyPointAddTweakResult } from './types';
34
/**
45
* The 0x02 prefix indicating an even Y coordinate which is implicitly assumed
@@ -55,6 +56,14 @@ export interface Taptree {
5556
root: Buffer;
5657
paths: Buffer[][];
5758
}
59+
/**
60+
* Gets the root hash and hash-paths of a taptree from the depth-first
61+
* construction used in BIP-0371 PSBTs
62+
* @param tree
63+
* @returns {Taptree} the tree, represented by its root hash, and the paths to
64+
* that root from each of the input scripts
65+
*/
66+
export declare function getDepthFirstTaptree(tree: PsbtTapTree): Taptree;
5867
/**
5968
* Gets the root hash of a taptree using a weighted Huffman construction from a
6069
* list of scripts and corresponding weights.

src/taproot.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki
44
// https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki
55
Object.defineProperty(exports, '__esModule', { value: true });
6-
exports.getTaptreeRoot = exports.getTapleafHash = exports.parseControlBlock = exports.parseTaprootWitness = exports.getControlBlock = exports.getHuffmanTaptree = exports.tapTweakPubkey = exports.tapTweakPrivkey = exports.hashTapBranch = exports.hashTapLeaf = exports.serializeScriptSize = exports.aggregateMuSigPubkeys = exports.INITIAL_TAPSCRIPT_VERSION = exports.EVEN_Y_COORD_PREFIX = void 0;
6+
exports.getTaptreeRoot = exports.getTapleafHash = exports.parseControlBlock = exports.parseTaprootWitness = exports.getControlBlock = exports.getHuffmanTaptree = exports.getDepthFirstTaptree = exports.tapTweakPubkey = exports.tapTweakPrivkey = exports.hashTapBranch = exports.hashTapLeaf = exports.serializeScriptSize = exports.aggregateMuSigPubkeys = exports.INITIAL_TAPSCRIPT_VERSION = exports.EVEN_Y_COORD_PREFIX = void 0;
77
const assert = require('assert');
88
const FastPriorityQueue = require('fastpriorityqueue');
99
const bcrypto = require('./crypto');
@@ -144,6 +144,39 @@ function tapTweakPubkey(ecc, pubkey, taptreeRoot) {
144144
return result;
145145
}
146146
exports.tapTweakPubkey = tapTweakPubkey;
147+
function recurseTaptree(leaves, targetDepth = 0) {
148+
const { value, done } = leaves.next();
149+
assert(!done, 'insufficient leaves to reconstruct tap tree');
150+
const [index, leaf] = value;
151+
const tree = {
152+
root: hashTapLeaf(leaf.script, leaf.leafVersion),
153+
paths: [],
154+
};
155+
tree.paths[index] = [];
156+
for (let depth = leaf.depth; depth > targetDepth; depth--) {
157+
const sibling = recurseTaptree(leaves, depth);
158+
tree.paths.forEach(path => path.push(sibling.root));
159+
sibling.paths.forEach(path => path.push(tree.root));
160+
tree.root = hashTapBranch(tree.root, sibling.root);
161+
// Merge disjoint sparse arrays of paths into tree.paths
162+
Object.assign(tree.paths, sibling.paths);
163+
}
164+
return tree;
165+
}
166+
/**
167+
* Gets the root hash and hash-paths of a taptree from the depth-first
168+
* construction used in BIP-0371 PSBTs
169+
* @param tree
170+
* @returns {Taptree} the tree, represented by its root hash, and the paths to
171+
* that root from each of the input scripts
172+
*/
173+
function getDepthFirstTaptree(tree) {
174+
const iter = tree.leaves.entries();
175+
const ret = recurseTaptree(iter);
176+
assert(iter.next().done, 'invalid tap tree, no path to some leaves');
177+
return ret;
178+
}
179+
exports.getDepthFirstTaptree = getDepthFirstTaptree;
147180
/**
148181
* Gets the root hash of a taptree using a weighted Huffman construction from a
149182
* list of scripts and corresponding weights.

test/fixtures/p2tr.json

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@
77
"redeems": [
88
{
99
"output": "8f5173bc367914e1574aceb3c7232a178a764fb6f14730b6b20bd36394c6c717 OP_CHECKSIG",
10-
"weight": 1
10+
"depth": 2
1111
},
1212
{
1313
"output": "07c7c32d159a27ba1824798b3b1d11e1b85f4dbc9e9fe63d95440a30737496de OP_CHECKSIG",
14-
"weight": 1
14+
"depth": 2
1515
},
1616
{
1717
"output": "4d4b27ab455a6e2b03af29a141ef47fc579c8435f563c065bf0dd12e6180ccd4 OP_CHECKSIG",
1818
"inputHex": "20deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
19-
"weight": 2
19+
"depth": 1
2020
}
2121
],
2222
"network": "regtest",
@@ -41,16 +41,16 @@
4141
"redeems": [
4242
{
4343
"output": "8f5173bc367914e1574aceb3c7232a178a764fb6f14730b6b20bd36394c6c717 OP_CHECKSIG",
44-
"weight": 1
44+
"depth": 2
4545
},
4646
{
4747
"output": "07c7c32d159a27ba1824798b3b1d11e1b85f4dbc9e9fe63d95440a30737496de OP_CHECKSIG",
48-
"weight": 1
48+
"depth": 2
4949
},
5050
{
5151
"output": "4d4b27ab455a6e2b03af29a141ef47fc579c8435f563c065bf0dd12e6180ccd4 OP_CHECKSIG",
5252
"inputHex": "20deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
53-
"weight": 2
53+
"depth": 1
5454
}
5555
],
5656
"network": "regtest"
@@ -69,16 +69,16 @@
6969
"redeems": [
7070
{
7171
"output": "8f5173bc367914e1574aceb3c7232a178a764fb6f14730b6b20bd36394c6c717 OP_CHECKSIG",
72-
"weight": 1
72+
"depth": 2
7373
},
7474
{
7575
"output": "07c7c32d159a27ba1824798b3b1d11e1b85f4dbc9e9fe63d95440a30737496de OP_CHECKSIG",
76-
"weight": 1
76+
"depth": 2
7777
},
7878
{
7979
"output": "4d4b27ab455a6e2b03af29a141ef47fc579c8435f563c065bf0dd12e6180ccd4 OP_CHECKSIG",
8080
"witness": [ "deadbeef" ],
81-
"weight": 2
81+
"depth": 1
8282
}
8383
],
8484
"network": "regtest",
@@ -106,16 +106,16 @@
106106
"redeems": [
107107
{
108108
"outputHex": "2020040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3ad20d806a63b6e2d83f11f22f9a11ba7a49ac451e8acf57591ec058e422eb997d55ead",
109-
"weight": 1
109+
"depth": 2
110110
},
111111
{
112112
"outputHex": "20d806a63b6e2d83f11f22f9a11ba7a49ac451e8acf57591ec058e422eb997d55ead200dcd7e6035f7ff5c860b78cfdd2bd80b4b160ca99a71654796afde11457e11e7ad",
113-
"weight": 1
113+
"depth": 2
114114
},
115115
{
116116
"outputHex": "200dcd7e6035f7ff5c860b78cfdd2bd80b4b160ca99a71654796afde11457e11e7ad2020040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3ad",
117117
"witness": [ "deadbeef", "deadbeef" ],
118-
"weight": 2
118+
"depth": 1
119119
}
120120
],
121121
"network": "regtest",
@@ -211,7 +211,8 @@
211211
"arguments": {
212212
"redeems": [{
213213
"witness": ["e6e81bea57db6bed922afbc5fab65c61f546b6f467b8c3570b5478ba21851e57b00bef791cd06d58afebb883770d956afaf2a9a796fb594a85d124b6837a4c7101"],
214-
"outputHex": "20dcd9a3fdad900e39ff367823f5d683135238d04425ac8dce19d0220e5791c700ac"
214+
"outputHex": "20dcd9a3fdad900e39ff367823f5d683135238d04425ac8dce19d0220e5791c700ac",
215+
"depth": 0
215216
}],
216217
"controlBlock": "c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0",
217218
"annex": "50abcd",
@@ -343,7 +344,8 @@
343344
"arguments": {
344345
"redeemIndex": -1,
345346
"redeems": [{
346-
"outputHex": "20dcd9a3fdad900e39ff367823f5d683135238d04425ac8dce19d0220e5791c700ac"
347+
"outputHex": "20dcd9a3fdad900e39ff367823f5d683135238d04425ac8dce19d0220e5791c700ac",
348+
"depth": 0
347349
}],
348350
"network": "testnet"
349351
},
@@ -355,7 +357,8 @@
355357
"arguments": {
356358
"redeemIndex": 2,
357359
"redeems": [{
358-
"outputHex": "20dcd9a3fdad900e39ff367823f5d683135238d04425ac8dce19d0220e5791c700ac"
360+
"outputHex": "20dcd9a3fdad900e39ff367823f5d683135238d04425ac8dce19d0220e5791c700ac",
361+
"depth": 0
359362
}],
360363
"network": "testnet"
361364
},
@@ -367,7 +370,8 @@
367370
"arguments": {
368371
"redeems": [{
369372
"outputHex": "20dcd9a3fdad900e39ff367823f5d683135238d04425ac8dce19d0220e5791c700ac",
370-
"network": "bitcoin"
373+
"network": "bitcoin",
374+
"depth": 0
371375
}],
372376
"network": "testnet"
373377
},
@@ -378,7 +382,8 @@
378382
"description": "p2tr from witness/redeem, mismatch",
379383
"arguments": {
380384
"redeems": [{
381-
"outputHex": "20dcd9a3fdad900e39ff367823f5d683135238d04425ac8dce19d0220e5791c700ac"
385+
"outputHex": "20dcd9a3fdad900e39ff367823f5d683135238d04425ac8dce19d0220e5791c700ac",
386+
"depth": 0
382387
}],
383388
"witness": [
384389
"e6e81bea57db6bed922afbc5fab65c61f546b6f467b8c3570b5478ba21851e57b00bef791cd06d58afebb883770d956afaf2a9a796fb594a85d124b6837a4c7101",
@@ -438,7 +443,8 @@
438443
"description": "p2tr from redeem/control block/output, script missing from tap tree",
439444
"arguments": {
440445
"redeems": [{
441-
"outputHex": "204d4b27ab455a6e2b03af29a141ef47fc579c8435f563c065bf0dd12e6180ccd4ac"
446+
"outputHex": "204d4b27ab455a6e2b03af29a141ef47fc579c8435f563c065bf0dd12e6180ccd4ac",
447+
"depth": 0
442448
}],
443449
"controlBlockHex": "c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0",
444450
"output": "OP_1 3d3e6d002d642acf8653bcf26b81dbe656df5bf88b6d54221b2606bc78f3d4e6",
@@ -451,7 +457,8 @@
451457
"description": "p2tr from redeem/control block/address, script missing from tap tree",
452458
"arguments": {
453459
"redeems": [{
454-
"outputHex": "204d4b27ab455a6e2b03af29a141ef47fc579c8435f563c065bf0dd12e6180ccd4ac"
460+
"outputHex": "204d4b27ab455a6e2b03af29a141ef47fc579c8435f563c065bf0dd12e6180ccd4ac",
461+
"depth": 0
455462
}],
456463
"controlBlockHex": "c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0",
457464
"address": "tb1p85lx6qpdvs4vlpjnhnexhqwmuetd7klc3dk4ggsmycrtc78n6nnqakvcc9",
@@ -474,7 +481,8 @@
474481
"description": "parity mismatch",
475482
"arguments": {
476483
"redeems": [{
477-
"outputHex": "20dcd9a3fdad900e39ff367823f5d683135238d04425ac8dce19d0220e5791c700ac"
484+
"outputHex": "20dcd9a3fdad900e39ff367823f5d683135238d04425ac8dce19d0220e5791c700ac",
485+
"depth": 0
478486
}],
479487
"controlBlock": "c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0",
480488
"network": "testnet"

ts_src/payments/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Network } from '../networks';
2+
import { TapTree } from 'bip174/src/lib/interfaces';
23
import { TinySecp256k1Interface } from '../types';
34
import { p2data as embed } from './embed';
45
import { p2ms } from './p2ms';
@@ -29,7 +30,9 @@ export interface Payment {
2930
redeemIndex?: number;
3031
witness?: Buffer[];
3132
weight?: number;
33+
depth?: number;
3234
controlBlock?: Buffer;
35+
tapTree?: TapTree;
3336
annex?: Buffer;
3437
}
3538

0 commit comments

Comments
 (0)