Skip to content

Commit b02a1b8

Browse files
authored
Merge pull request #42 from BitGo/p2tr_psbt
Make p2tr payments and taproot.ts more PSBT friendly
2 parents 68e4f8c + 5b1500e commit b02a1b8

File tree

8 files changed

+218
-58
lines changed

8 files changed

+218
-58
lines changed

src/payments/index.d.ts

Lines changed: 4 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';
@@ -21,6 +22,7 @@ export interface Payment {
2122
input?: Buffer;
2223
signatures?: Buffer[];
2324
pubkey?: Buffer;
25+
internalPubkey?: Buffer;
2426
signature?: Buffer;
2527
address?: string;
2628
hash?: Buffer;
@@ -29,7 +31,9 @@ export interface Payment {
2931
redeemIndex?: number;
3032
witness?: Buffer[];
3133
weight?: number;
34+
depth?: number;
3235
controlBlock?: Buffer;
36+
tapTree?: TapTree;
3337
annex?: Buffer;
3438
}
3539
export declare type PaymentCreator = (a: Payment, opts?: PaymentOpts) => Payment;

src/payments/p2tr.js

Lines changed: 36 additions & 12 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(
@@ -97,15 +103,15 @@ function p2tr(a, opts) {
97103
if (parsedWitness && parsedWitness.spendType === 'Script')
98104
return taproot.parseControlBlock(ecc, parsedWitness.controlBlock);
99105
});
100-
const _internalPubkey = lazy.value(() => {
106+
lazy.prop(o, 'internalPubkey', () => {
101107
if (a.pubkey) {
102108
// single pubkey
103109
return a.pubkey;
104110
} else if (a.pubkeys && a.pubkeys.length === 1) {
105111
return a.pubkeys[0];
106112
} else if (a.pubkeys && a.pubkeys.length > 1) {
107113
// multiple pubkeys
108-
return taproot.aggregateMuSigPubkeys(ecc, a.pubkeys);
114+
return Buffer.from(taproot.aggregateMuSigPubkeys(ecc, a.pubkeys));
109115
} else if (_parsedControlBlock()) {
110116
return _parsedControlBlock().internalPubkey;
111117
} else {
@@ -146,11 +152,29 @@ function p2tr(a, opts) {
146152
tapscript,
147153
);
148154
}
149-
if (!taptreeRoot && _taptree()) taptreeRoot = _taptree().root;
150-
return taproot.tapTweakPubkey(ecc, _internalPubkey(), taptreeRoot);
155+
if (!taptreeRoot && _taprootPaths()) taptreeRoot = _taprootPaths().root;
156+
return taproot.tapTweakPubkey(ecc, o.internalPubkey, taptreeRoot);
157+
});
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+
};
151177
});
152-
const network = a.network || networks_1.bitcoin;
153-
const o = { network };
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,
171-
_internalPubkey(),
172-
taptree.paths[a.redeemIndex],
195+
o.internalPubkey,
196+
taprootPaths.paths[a.redeemIndex],
173197
);
174198
});
175199
lazy.prop(o, 'signature', () => {
@@ -265,7 +289,7 @@ function p2tr(a, opts) {
265289
throw new TypeError('mismatch between address and taproot pubkey');
266290
const parsedControlBlock = _parsedControlBlock();
267291
if (parsedControlBlock) {
268-
if (!parsedControlBlock.internalPubkey.equals(_internalPubkey()))
292+
if (!parsedControlBlock.internalPubkey.equals(o.internalPubkey))
269293
throw new TypeError('Internal pubkey mismatch');
270294
if (taprootPubkey && parsedControlBlock.parity !== taprootPubkey.parity)
271295
throw new TypeError('Parity mismatch');

src/taproot.d.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
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
56
* on all 32 byte x-only pub keys as defined in BIP340.
67
*/
78
export declare const EVEN_Y_COORD_PREFIX: Buffer;
9+
export declare const INITIAL_TAPSCRIPT_VERSION = 192;
810
/**
911
* Aggregates a list of public keys into a single MuSig2* public key
1012
* according to the MuSig2 paper.
@@ -24,7 +26,7 @@ export declare function serializeScriptSize(script: Buffer): Buffer;
2426
* @param script
2527
* @returns
2628
*/
27-
export declare function hashTapLeaf(script: Buffer): Buffer;
29+
export declare function hashTapLeaf(script: Buffer, leafVersion?: number): Buffer;
2830
/**
2931
* Creates a lexicographically sorted tapbranch from two child taptree nodes
3032
* and returns its tagged hash.
@@ -54,6 +56,14 @@ export interface Taptree {
5456
root: Buffer;
5557
paths: Buffer[][];
5658
}
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;
5767
/**
5868
* Gets the root hash of a taptree using a weighted Huffman construction from a
5969
* list of scripts and corresponding weights.
@@ -62,7 +72,7 @@ export interface Taptree {
6272
* @returns {Taptree} the tree, represented by its root hash, and the paths to that root from each of the input scripts
6373
*/
6474
export declare function getHuffmanTaptree(scripts: Buffer[], weights: Array<number | undefined>): Taptree;
65-
export declare function getControlBlock(parity: 0 | 1, pubkey: Uint8Array, path: Buffer[]): Buffer;
75+
export declare function getControlBlock(parity: 0 | 1, pubkey: Uint8Array, path: Buffer[], leafVersion?: number): Buffer;
6676
export interface KeyPathWitness {
6777
spendType: 'Key';
6878
signature: Buffer;

src/taproot.js

Lines changed: 44 additions & 6 deletions
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.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');
@@ -14,7 +14,7 @@ const varuint = require('varuint-bitcoin');
1414
* on all 32 byte x-only pub keys as defined in BIP340.
1515
*/
1616
exports.EVEN_Y_COORD_PREFIX = Buffer.of(0x02);
17-
const INITIAL_TAPSCRIPT_VERSION = Buffer.of(0xc0);
17+
exports.INITIAL_TAPSCRIPT_VERSION = 0xc0;
1818
/**
1919
* Aggregates a list of public keys into a single MuSig2* public key
2020
* according to the MuSig2 paper.
@@ -82,11 +82,11 @@ exports.serializeScriptSize = serializeScriptSize;
8282
* @param script
8383
* @returns
8484
*/
85-
function hashTapLeaf(script) {
85+
function hashTapLeaf(script, leafVersion = exports.INITIAL_TAPSCRIPT_VERSION) {
8686
const size = serializeScriptSize(script);
8787
return bcrypto.taggedHash(
8888
'TapLeaf',
89-
Buffer.concat([INITIAL_TAPSCRIPT_VERSION, size, script]),
89+
Buffer.concat([Buffer.of(leafVersion), size, script]),
9090
);
9191
}
9292
exports.hashTapLeaf = hashTapLeaf;
@@ -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.
@@ -217,8 +250,13 @@ function getHuffmanTaptree(scripts, weights) {
217250
return { root: rootNode.taggedHash, paths };
218251
}
219252
exports.getHuffmanTaptree = getHuffmanTaptree;
220-
function getControlBlock(parity, pubkey, path) {
221-
const parityVersion = INITIAL_TAPSCRIPT_VERSION[0] + parity;
253+
function getControlBlock(
254+
parity,
255+
pubkey,
256+
path,
257+
leafVersion = exports.INITIAL_TAPSCRIPT_VERSION,
258+
) {
259+
const parityVersion = leafVersion + parity;
222260
return Buffer.concat([Buffer.of(parityVersion), pubkey, ...path]);
223261
}
224262
exports.getControlBlock = getControlBlock;

0 commit comments

Comments
 (0)