Skip to content

Commit 6cbac53

Browse files
committed
feat: add to/from Psbt TapTree conversion
1 parent 3af7c11 commit 6cbac53

File tree

6 files changed

+188
-3
lines changed

6 files changed

+188
-3
lines changed

src/payments/taprootutils.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/// <reference types="node" />
22
import { Tapleaf, Taptree } from '../types';
33
export declare const LEAF_VERSION_TAPSCRIPT = 192;
4+
export declare const MAX_TAPTREE_DEPTH = 128;
45
interface HashLeaf {
56
hash: Buffer;
67
}

src/payments/taprootutils.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
'use strict';
22
Object.defineProperty(exports, '__esModule', { value: true });
3-
exports.tweakKey = exports.tapTweakHash = exports.tapleafHash = exports.findScriptPath = exports.toHashTree = exports.rootHashFromPath = exports.LEAF_VERSION_TAPSCRIPT = void 0;
3+
exports.tweakKey = exports.tapTweakHash = exports.tapleafHash = exports.findScriptPath = exports.toHashTree = exports.rootHashFromPath = exports.MAX_TAPTREE_DEPTH = exports.LEAF_VERSION_TAPSCRIPT = void 0;
44
const buffer_1 = require('buffer');
55
const ecc_lib_1 = require('../ecc_lib');
66
const bcrypto = require('../crypto');
77
const bufferutils_1 = require('../bufferutils');
88
const types_1 = require('../types');
99
exports.LEAF_VERSION_TAPSCRIPT = 0xc0;
10+
exports.MAX_TAPTREE_DEPTH = 128;
1011
const isHashBranch = ht => 'left' in ht && 'right' in ht;
1112
function rootHashFromPath(controlBlock, leafHash) {
1213
const m = (controlBlock.length - 33) / 32;

src/psbt/bip371.d.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/// <reference types="node" />
2-
import { PsbtInput } from 'bip174/src/lib/interfaces';
2+
import { Taptree } from '../types';
3+
import { PsbtInput, TapLeaf } from 'bip174/src/lib/interfaces';
34
export declare const toXOnly: (pubKey: Buffer) => Buffer;
45
/**
56
* Default tapscript finalizer. It searches for the `tapLeafHashToFinalize` if provided.
@@ -17,3 +18,21 @@ export declare function serializeTaprootSignature(sig: Buffer, sighashType?: num
1718
export declare function isTaprootInput(input: PsbtInput): boolean;
1819
export declare function checkTaprootInputFields(inputData: PsbtInput, newInputData: PsbtInput, action: string): void;
1920
export declare function tweakInternalPubKey(inputIndex: number, input: PsbtInput): Buffer;
21+
/**
22+
* Convert a binary tree to a BIP371 type list. Each element of the list is (according to BIP371):
23+
* One or more tuples representing the depth, leaf version, and script for a leaf in the Taproot tree,
24+
* allowing the entire tree to be reconstructed. The tuples must be in depth first search order so that
25+
* the tree is correctly reconstructed.
26+
* @param tree the binary tap tree
27+
* @returns a list of BIP 371 tapleaves
28+
*/
29+
export declare function tapTreeToList(tree: Taptree): TapLeaf[];
30+
/**
31+
* Convert a BIP371 TapLeaf list to a TapTree (binary).
32+
* @param leaves a list of tapleaves where each element of the list is (according to BIP371):
33+
* One or more tuples representing the depth, leaf version, and script for a leaf in the Taproot tree,
34+
* allowing the entire tree to be reconstructed. The tuples must be in depth first search order so that
35+
* the tree is correctly reconstructed.
36+
* @returns the corresponding taptree, or throws an exception if the tree cannot be reconstructed
37+
*/
38+
export declare function tapTreeFromList(leaves?: TapLeaf[]): Taptree;

src/psbt/bip371.js

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22
Object.defineProperty(exports, '__esModule', { value: true });
3-
exports.tweakInternalPubKey = exports.checkTaprootInputFields = exports.isTaprootInput = exports.serializeTaprootSignature = exports.tapScriptFinalizer = exports.toXOnly = void 0;
3+
exports.tapTreeFromList = exports.tapTreeToList = exports.tweakInternalPubKey = exports.checkTaprootInputFields = exports.isTaprootInput = exports.serializeTaprootSignature = exports.tapScriptFinalizer = exports.toXOnly = void 0;
4+
const types_1 = require('../types');
45
const psbtutils_1 = require('./psbtutils');
56
const taprootutils_1 = require('../payments/taprootutils');
67
const toXOnly = pubKey => (pubKey.length === 32 ? pubKey : pubKey.slice(1, 33));
@@ -69,6 +70,76 @@ function tweakInternalPubKey(inputIndex, input) {
6970
return outputKey.x;
7071
}
7172
exports.tweakInternalPubKey = tweakInternalPubKey;
73+
/**
74+
* Convert a binary tree to a BIP371 type list. Each element of the list is (according to BIP371):
75+
* One or more tuples representing the depth, leaf version, and script for a leaf in the Taproot tree,
76+
* allowing the entire tree to be reconstructed. The tuples must be in depth first search order so that
77+
* the tree is correctly reconstructed.
78+
* @param tree the binary tap tree
79+
* @returns a list of BIP 371 tapleaves
80+
*/
81+
function tapTreeToList(tree) {
82+
return _tapTreeToList(tree);
83+
}
84+
exports.tapTreeToList = tapTreeToList;
85+
/**
86+
* Convert a BIP371 TapLeaf list to a TapTree (binary).
87+
* @param leaves a list of tapleaves where each element of the list is (according to BIP371):
88+
* One or more tuples representing the depth, leaf version, and script for a leaf in the Taproot tree,
89+
* allowing the entire tree to be reconstructed. The tuples must be in depth first search order so that
90+
* the tree is correctly reconstructed.
91+
* @returns the corresponding taptree, or throws an exception if the tree cannot be reconstructed
92+
*/
93+
function tapTreeFromList(leaves = []) {
94+
if (leaves.length === 1 && leaves[0].depth === 0)
95+
return {
96+
output: leaves[0].script,
97+
version: leaves[0].leafVersion,
98+
};
99+
return instertLeavesInTree(leaves);
100+
}
101+
exports.tapTreeFromList = tapTreeFromList;
102+
function _tapTreeToList(tree, leaves = [], depth = 0) {
103+
if (depth > taprootutils_1.MAX_TAPTREE_DEPTH)
104+
throw new Error('Max taptree depth exceeded.');
105+
if (!tree) return [];
106+
if ((0, types_1.isTapleaf)(tree)) {
107+
leaves.push({
108+
depth,
109+
leafVersion: tree.version || taprootutils_1.LEAF_VERSION_TAPSCRIPT,
110+
script: tree.output,
111+
});
112+
return leaves;
113+
}
114+
if (tree[0]) _tapTreeToList(tree[0], leaves, depth + 1);
115+
if (tree[1]) _tapTreeToList(tree[1], leaves, depth + 1);
116+
return leaves;
117+
}
118+
function instertLeavesInTree(leaves) {
119+
let tree;
120+
for (const leaf of leaves) {
121+
tree = instertLeafInTree(leaf, tree);
122+
if (!tree) throw new Error(`No room left to insert tapleaf in tree`);
123+
}
124+
return tree;
125+
}
126+
function instertLeafInTree(leaf, tree, depth = 0) {
127+
if (depth > taprootutils_1.MAX_TAPTREE_DEPTH)
128+
throw new Error('Max taptree depth exceeded.');
129+
if (leaf.depth === depth) {
130+
if (!tree)
131+
return {
132+
output: leaf.script,
133+
version: leaf.leafVersion,
134+
};
135+
return;
136+
}
137+
if ((0, types_1.isTapleaf)(tree)) return;
138+
const leftSide = instertLeafInTree(leaf, tree && tree[0], depth + 1);
139+
if (leftSide) return [leftSide, tree && tree[1]];
140+
const rightSide = instertLeafInTree(leaf, tree && tree[1], depth + 1);
141+
if (rightSide) return [tree && tree[0], rightSide];
142+
}
72143
function checkMixedTaprootAndNonTaprootFields(inputData, newInputData, action) {
73144
const isBadTaprootUpdate =
74145
isTaprootInput(inputData) && hasNonTaprootInputFields(newInputData);

ts_src/payments/taprootutils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { varuint } from '../bufferutils';
66
import { Tapleaf, Taptree, isTapleaf } from '../types';
77

88
export const LEAF_VERSION_TAPSCRIPT = 0xc0;
9+
export const MAX_TAPTREE_DEPTH = 128;
910

1011
interface HashLeaf {
1112
hash: Buffer;

ts_src/psbt/bip371.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { Taptree, Tapleaf, isTapleaf } from '../types';
12
import {
23
PsbtInput,
34
TapLeafScript,
45
TapScriptSig,
6+
TapLeaf,
57
} from 'bip174/src/lib/interfaces';
68

79
import {
@@ -13,6 +15,8 @@ import {
1315
tweakKey,
1416
tapleafHash,
1517
rootHashFromPath,
18+
LEAF_VERSION_TAPSCRIPT,
19+
MAX_TAPTREE_DEPTH,
1620
} from '../payments/taprootutils';
1721

1822
export const toXOnly = (pubKey: Buffer) =>
@@ -98,6 +102,94 @@ export function tweakInternalPubKey(
98102
return outputKey.x;
99103
}
100104

105+
/**
106+
* Convert a binary tree to a BIP371 type list. Each element of the list is (according to BIP371):
107+
* One or more tuples representing the depth, leaf version, and script for a leaf in the Taproot tree,
108+
* allowing the entire tree to be reconstructed. The tuples must be in depth first search order so that
109+
* the tree is correctly reconstructed.
110+
* @param tree the binary tap tree
111+
* @returns a list of BIP 371 tapleaves
112+
*/
113+
export function tapTreeToList(tree: Taptree): TapLeaf[] {
114+
return _tapTreeToList(tree);
115+
}
116+
117+
/**
118+
* Convert a BIP371 TapLeaf list to a TapTree (binary).
119+
* @param leaves a list of tapleaves where each element of the list is (according to BIP371):
120+
* One or more tuples representing the depth, leaf version, and script for a leaf in the Taproot tree,
121+
* allowing the entire tree to be reconstructed. The tuples must be in depth first search order so that
122+
* the tree is correctly reconstructed.
123+
* @returns the corresponding taptree, or throws an exception if the tree cannot be reconstructed
124+
*/
125+
export function tapTreeFromList(leaves: TapLeaf[] = []): Taptree {
126+
if (leaves.length === 1 && leaves[0].depth === 0)
127+
return {
128+
output: leaves[0].script,
129+
version: leaves[0].leafVersion,
130+
};
131+
132+
return instertLeavesInTree(leaves);
133+
}
134+
135+
function _tapTreeToList(
136+
tree: Taptree,
137+
leaves: TapLeaf[] = [],
138+
depth = 0,
139+
): TapLeaf[] {
140+
if (depth > MAX_TAPTREE_DEPTH) throw new Error('Max taptree depth exceeded.');
141+
if (!tree) return [];
142+
if (isTapleaf(tree)) {
143+
leaves.push({
144+
depth,
145+
leafVersion: tree.version || LEAF_VERSION_TAPSCRIPT,
146+
script: tree.output,
147+
});
148+
return leaves;
149+
}
150+
if (tree[0]) _tapTreeToList(tree[0], leaves, depth + 1);
151+
if (tree[1]) _tapTreeToList(tree[1], leaves, depth + 1);
152+
return leaves;
153+
}
154+
155+
// Just like Taptree, but it accepts empty branches
156+
type PartialTaptree =
157+
| [PartialTaptree | Tapleaf, PartialTaptree | Tapleaf]
158+
| Tapleaf
159+
| undefined;
160+
function instertLeavesInTree(leaves: TapLeaf[]): Taptree {
161+
let tree: PartialTaptree;
162+
for (const leaf of leaves) {
163+
tree = instertLeafInTree(leaf, tree);
164+
if (!tree) throw new Error(`No room left to insert tapleaf in tree`);
165+
}
166+
167+
return tree as Taptree;
168+
}
169+
170+
function instertLeafInTree(
171+
leaf: TapLeaf,
172+
tree?: PartialTaptree,
173+
depth = 0,
174+
): PartialTaptree {
175+
if (depth > MAX_TAPTREE_DEPTH) throw new Error('Max taptree depth exceeded.');
176+
if (leaf.depth === depth) {
177+
if (!tree)
178+
return {
179+
output: leaf.script,
180+
version: leaf.leafVersion,
181+
};
182+
return;
183+
}
184+
185+
if (isTapleaf(tree)) return;
186+
const leftSide = instertLeafInTree(leaf, tree && tree[0], depth + 1);
187+
if (leftSide) return [leftSide, tree && tree[1]];
188+
189+
const rightSide = instertLeafInTree(leaf, tree && tree[1], depth + 1);
190+
if (rightSide) return [tree && tree[0], rightSide];
191+
}
192+
101193
function checkMixedTaprootAndNonTaprootFields(
102194
inputData: PsbtInput,
103195
newInputData: PsbtInput,

0 commit comments

Comments
 (0)