Skip to content

Commit 47f3158

Browse files
committed
Correct Taptree type
* Move the (much simplified) type check function to types.ts * Use `Tapleaf` type a bit more (this might be a bad idea) * Be more consistent in the capitalization of `Taptree`
1 parent ddef6c8 commit 47f3158

File tree

12 files changed

+115
-146
lines changed

12 files changed

+115
-146
lines changed

src/payments/p2tr.js

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ const verifyecc_1 = require('./verifyecc');
1212
const OPS = bscript.OPS;
1313
const TAPROOT_WITNESS_VERSION = 0x01;
1414
const ANNEX_PREFIX = 0x50;
15-
const LEAF_VERSION_MASK = 0xfe;
1615
function p2tr(a, opts) {
1716
if (
1817
!a.address &&
@@ -41,7 +40,7 @@ function p2tr(a, opts) {
4140
witness: types_1.typeforce.maybe(
4241
types_1.typeforce.arrayOf(types_1.typeforce.Buffer),
4342
),
44-
scriptTree: types_1.typeforce.maybe(taprootutils_1.isTapTree),
43+
scriptTree: types_1.typeforce.maybe(types_1.isTaptree),
4544
redeem: types_1.typeforce.maybe({
4645
output: types_1.typeforce.maybe(types_1.typeforce.Buffer),
4746
redeemVersion: types_1.typeforce.maybe(types_1.typeforce.Number),
@@ -88,9 +87,12 @@ function p2tr(a, opts) {
8887
const w = _witness();
8988
if (w && w.length > 1) {
9089
const controlBlock = w[w.length - 1];
91-
const leafVersion = controlBlock[0] & LEAF_VERSION_MASK;
90+
const leafVersion = controlBlock[0] & types_1.TAPLEAF_VERSION_MASK;
9291
const script = w[w.length - 2];
93-
const leafHash = (0, taprootutils_1.tapLeafHash)(script, leafVersion);
92+
const leafHash = (0, taprootutils_1.tapLeafHash)({
93+
output: script,
94+
version: leafVersion,
95+
});
9496
return (0, taprootutils_1.rootHashFromPath)(controlBlock, leafHash);
9597
}
9698
return null;
@@ -116,7 +118,8 @@ function p2tr(a, opts) {
116118
return {
117119
output: witness[witness.length - 2],
118120
witness: witness.slice(0, -2),
119-
redeemVersion: witness[witness.length - 1][0] & LEAF_VERSION_MASK,
121+
redeemVersion:
122+
witness[witness.length - 1][0] & types_1.TAPLEAF_VERSION_MASK,
120123
};
121124
});
122125
lazy.prop(o, 'pubkey', () => {
@@ -144,10 +147,10 @@ function p2tr(a, opts) {
144147
if (a.scriptTree && a.redeem && a.redeem.output && a.internalPubkey) {
145148
// todo: optimize/cache
146149
const hashTree = (0, taprootutils_1.toHashTree)(a.scriptTree);
147-
const leafHash = (0, taprootutils_1.tapLeafHash)(
148-
a.redeem.output,
149-
o.redeemVersion,
150-
);
150+
const leafHash = (0, taprootutils_1.tapLeafHash)({
151+
output: a.redeem.output,
152+
version: o.redeemVersion,
153+
});
151154
const path = (0, taprootutils_1.findScriptPath)(hashTree, leafHash);
152155
const outputKey = tweakKey(a.internalPubkey, hashTree.hash, _ecc());
153156
if (!outputKey) return;
@@ -253,9 +256,12 @@ function p2tr(a, opts) {
253256
throw new TypeError('Internal pubkey mismatch');
254257
if (!_ecc().isXOnlyPoint(internalPubkey))
255258
throw new TypeError('Invalid internalPubkey for p2tr witness');
256-
const leafVersion = controlBlock[0] & LEAF_VERSION_MASK;
259+
const leafVersion = controlBlock[0] & types_1.TAPLEAF_VERSION_MASK;
257260
const script = witness[witness.length - 2];
258-
const leafHash = (0, taprootutils_1.tapLeafHash)(script, leafVersion);
261+
const leafHash = (0, taprootutils_1.tapLeafHash)({
262+
output: script,
263+
version: leafVersion,
264+
});
259265
const hash = (0, taprootutils_1.rootHashFromPath)(
260266
controlBlock,
261267
leafHash,

src/payments/taprootutils.d.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/// <reference types="node" />
2-
import { Taptree } from '../types';
2+
import { Tapleaf, Taptree } from '../types';
33
export declare const LEAF_VERSION_TAPSCRIPT = 192;
44
export declare function rootHashFromPath(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer;
55
export interface HashTree {
@@ -16,16 +16,12 @@ export interface HashTree {
1616
* - one taproot leaf and a list of elements
1717
*/
1818
export declare function toHashTree(scriptTree: Taptree): HashTree;
19-
/**
20-
* Check if the tree is a binary tree with leafs of type Tapleaf
21-
*/
22-
export declare function isTapTree(scriptTree: Taptree): boolean;
2319
/**
2420
* Given a MAST tree, it finds the path of a particular hash.
2521
* @param node - the root of the tree
2622
* @param hash - the hash to search for
2723
* @returns - and array of hashes representing the path, or an empty array if no pat is found
2824
*/
2925
export declare function findScriptPath(node: HashTree, hash: Buffer): Buffer[];
30-
export declare function tapLeafHash(script: Buffer, version?: number): Buffer;
26+
export declare function tapLeafHash(leaf: Tapleaf): Buffer;
3127
export declare function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer;

src/payments/taprootutils.js

Lines changed: 10 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
'use strict';
22
Object.defineProperty(exports, '__esModule', { value: true });
3-
exports.tapTweakHash = exports.tapLeafHash = exports.findScriptPath = exports.isTapTree = exports.toHashTree = exports.rootHashFromPath = exports.LEAF_VERSION_TAPSCRIPT = void 0;
3+
exports.tapTweakHash = exports.tapLeafHash = exports.findScriptPath = exports.toHashTree = exports.rootHashFromPath = exports.LEAF_VERSION_TAPSCRIPT = void 0;
44
const buffer_1 = require('buffer');
55
const bcrypto = require('../crypto');
66
const bufferutils_1 = require('../bufferutils');
7+
const types_1 = require('../types');
78
const TAP_LEAF_TAG = 'TapLeaf';
89
const TAP_BRANCH_TAG = 'TapBranch';
910
const TAP_TWEAK_TAG = 'TapTweak';
@@ -32,48 +33,18 @@ exports.rootHashFromPath = rootHashFromPath;
3233
* - one taproot leaf and a list of elements
3334
*/
3435
function toHashTree(scriptTree) {
35-
if (scriptTree.length === 1) {
36-
const script = scriptTree[0];
37-
if (Array.isArray(script)) {
38-
return toHashTree(script);
39-
}
40-
script.version = script.version || exports.LEAF_VERSION_TAPSCRIPT;
41-
if ((script.version & 1) !== 0)
42-
throw new TypeError('Invalid script version');
43-
return {
44-
hash: tapLeafHash(script.output, script.version),
45-
};
46-
}
47-
let left = toHashTree([scriptTree[0]]);
48-
let right = toHashTree([scriptTree[1]]);
49-
if (left.hash.compare(right.hash) === 1) [left, right] = [right, left];
36+
if ((0, types_1.isTapleaf)(scriptTree))
37+
return { hash: tapLeafHash(scriptTree) };
38+
const hashes = [toHashTree(scriptTree[0]), toHashTree(scriptTree[1])];
39+
hashes.sort((a, b) => a.hash.compare(b.hash));
40+
const [left, right] = hashes;
5041
return {
5142
hash: tapBranchHash(left.hash, right.hash),
5243
left,
5344
right,
5445
};
5546
}
5647
exports.toHashTree = toHashTree;
57-
/**
58-
* Check if the tree is a binary tree with leafs of type Tapleaf
59-
*/
60-
function isTapTree(scriptTree) {
61-
if (scriptTree.length > 2) return false;
62-
if (scriptTree.length === 1) {
63-
const script = scriptTree[0];
64-
if (Array.isArray(script)) {
65-
return isTapTree(script);
66-
}
67-
if (!script.output) return false;
68-
script.version = script.version || exports.LEAF_VERSION_TAPSCRIPT;
69-
if ((script.version & 1) !== 0) return false;
70-
return true;
71-
}
72-
if (!isTapTree([scriptTree[0]])) return false;
73-
if (!isTapTree([scriptTree[1]])) return false;
74-
return true;
75-
}
76-
exports.isTapTree = isTapTree;
7748
/**
7849
* Given a MAST tree, it finds the path of a particular hash.
7950
* @param node - the root of the tree
@@ -96,13 +67,13 @@ function findScriptPath(node, hash) {
9667
return [];
9768
}
9869
exports.findScriptPath = findScriptPath;
99-
function tapLeafHash(script, version) {
100-
version = version || exports.LEAF_VERSION_TAPSCRIPT;
70+
function tapLeafHash(leaf) {
71+
const version = leaf.version || exports.LEAF_VERSION_TAPSCRIPT;
10172
return bcrypto.taggedHash(
10273
TAP_LEAF_TAG,
10374
buffer_1.Buffer.concat([
10475
buffer_1.Buffer.from([version]),
105-
serializeScript(script),
76+
serializeScript(leaf.output),
10677
]),
10778
);
10879
}

src/psbt.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1079,7 +1079,7 @@ function getHashForSig(
10791079
const signingScripts = prevOuts.map(o => o.script);
10801080
const values = prevOuts.map(o => o.value);
10811081
const leafHash = input.witnessScript
1082-
? (0, taprootutils_1.tapLeafHash)(input.witnessScript)
1082+
? (0, taprootutils_1.tapLeafHash)({ output: input.witnessScript })
10831083
: undefined;
10841084
hash = unsignedTx.hashForWitnessV1(
10851085
inputIndex,

src/types.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ export interface Tapleaf {
1818
output: Buffer;
1919
version?: number;
2020
}
21-
export declare type Taptree = Array<[Tapleaf, Tapleaf] | Tapleaf>;
21+
export declare const TAPLEAF_VERSION_MASK = 254;
22+
export declare function isTapleaf(o: any): o is Tapleaf;
23+
export declare type Taptree = [Taptree | Tapleaf, Taptree | Tapleaf] | Tapleaf;
24+
export declare function isTaptree(scriptTree: any): scriptTree is Taptree;
2225
export interface TinySecp256k1Interface {
2326
isXOnlyPoint(p: Uint8Array): boolean;
2427
xOnlyPointAddTweak(p: Uint8Array, tweak: Uint8Array): XOnlyPointAddTweakResult | null;

src/types.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict';
22
Object.defineProperty(exports, '__esModule', { value: true });
3-
exports.oneOf = exports.Null = exports.BufferN = exports.Function = exports.UInt32 = exports.UInt8 = exports.tuple = exports.maybe = exports.Hex = exports.Buffer = exports.String = exports.Boolean = exports.Array = exports.Number = exports.Hash256bit = exports.Hash160bit = exports.Buffer256bit = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.isPoint = exports.typeforce = void 0;
3+
exports.oneOf = exports.Null = exports.BufferN = exports.Function = exports.UInt32 = exports.UInt8 = exports.tuple = exports.maybe = exports.Hex = exports.Buffer = exports.String = exports.Boolean = exports.Array = exports.Number = exports.Hash256bit = exports.Hash160bit = exports.Buffer256bit = exports.isTaptree = exports.isTapleaf = exports.TAPLEAF_VERSION_MASK = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.isPoint = exports.typeforce = void 0;
44
const buffer_1 = require('buffer');
55
exports.typeforce = require('typeforce');
66
const ZERO32 = buffer_1.Buffer.alloc(32, 0);
@@ -68,6 +68,21 @@ exports.Network = exports.typeforce.compile({
6868
scriptHash: exports.typeforce.UInt8,
6969
wif: exports.typeforce.UInt8,
7070
});
71+
exports.TAPLEAF_VERSION_MASK = 0xfe;
72+
function isTapleaf(o) {
73+
if (!('output' in o)) return false;
74+
if (!buffer_1.Buffer.isBuffer(o.output)) return false;
75+
if (o.version !== undefined)
76+
return (o.version & exports.TAPLEAF_VERSION_MASK) === o.version;
77+
return true;
78+
}
79+
exports.isTapleaf = isTapleaf;
80+
function isTaptree(scriptTree) {
81+
if (!(0, exports.Array)(scriptTree)) return isTapleaf(scriptTree);
82+
if (scriptTree.length !== 2) return false;
83+
return scriptTree.every(t => isTaptree(t));
84+
}
85+
exports.isTaptree = isTaptree;
7186
exports.Buffer256bit = exports.typeforce.BufferN(32);
7287
exports.Hash160bit = exports.typeforce.BufferN(20);
7388
exports.Hash256bit = exports.typeforce.BufferN(32);

test/fixtures/p2tr.json

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -161,11 +161,9 @@
161161
"description": "address, pubkey, output and hash from internalPubkey and a script tree with one leaf",
162162
"arguments": {
163163
"internalPubkey": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0",
164-
"scriptTree": [
165-
{
166-
"output": "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c18 OP_CHECKSIG"
167-
}
168-
]
164+
"scriptTree": {
165+
"output": "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c18 OP_CHECKSIG"
166+
}
169167
},
170168
"expected": {
171169
"name": "p2tr",
@@ -393,12 +391,10 @@
393391
"output": "d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8 OP_CHECKSIG",
394392
"redeemVersion": 192
395393
},
396-
"scriptTree": [
397-
{
398-
"output": "d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8 OP_CHECKSIG",
399-
"version": 192
400-
}
401-
]
394+
"scriptTree": {
395+
"output": "d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8 OP_CHECKSIG",
396+
"version": 192
397+
}
402398
},
403399
"options": {},
404400
"expected": {
@@ -427,12 +423,10 @@
427423
"output": "b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007 OP_CHECKSIG",
428424
"redeemVersion": 192
429425
},
430-
"scriptTree": [
431-
{
432-
"output": "b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007 OP_CHECKSIG",
433-
"version": 192
434-
}
435-
]
426+
"scriptTree": {
427+
"output": "b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007 OP_CHECKSIG",
428+
"version": 192
429+
}
436430
},
437431
"options": {},
438432
"expected": {
@@ -906,11 +900,9 @@
906900
"options": {},
907901
"arguments": {
908902
"internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7",
909-
"scriptTree": [
910-
{
911-
"output": "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c18 OP_CHECKSIG"
912-
}
913-
],
903+
"scriptTree": {
904+
"output": "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c18 OP_CHECKSIG"
905+
},
914906
"hash": "b76077013c8e303085e300000000000000000000000000000000000000000000"
915907
}
916908
},
@@ -1037,7 +1029,7 @@
10371029
},
10381030
{
10391031
"description": "Script Tree is not a binary tree (has tree leafs)",
1040-
"exception": "property \"scriptTree\" of type \\?isTapTree, got Array",
1032+
"exception": "property \"scriptTree\" of type \\?isTaptree, got Array",
10411033
"options": {},
10421034
"arguments": {
10431035
"internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7",
@@ -1066,7 +1058,7 @@
10661058
},
10671059
{
10681060
"description": "Script Tree is not a TapTree tree (leaf has no script)",
1069-
"exception": "property \"scriptTree\" of type \\?isTapTree, got Array",
1061+
"exception": "property \"scriptTree\" of type \\?isTaptree, got Array",
10701062
"options": {},
10711063
"arguments": {
10721064
"internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7",
@@ -1167,4 +1159,4 @@
11671159
"depends": {},
11681160
"details": []
11691161
}
1170-
}
1162+
}

test/integration/taproot.spec.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as ecc from 'tiny-secp256k1';
44
import { describe, it } from 'mocha';
55
import { regtestUtils } from './_regtest';
66
import * as bitcoin from '../..';
7+
import { Taptree } from '../../src/types';
78
import { buildTapscriptFinalizer, toXOnly } from '../psbt.utils';
89

910
const rng = require('randombytes');
@@ -97,11 +98,9 @@ describe('bitcoinjs-lib (transaction with taproot)', () => {
9798
)} OP_CHECKSIG`;
9899
const leafScript = bitcoin.script.fromASM(leafScriptAsm);
99100

100-
const scriptTree = [
101-
{
102-
output: leafScript,
103-
},
104-
];
101+
const scriptTree = {
102+
output: leafScript,
103+
};
105104

106105
const { output, address, hash } = bitcoin.payments.p2tr(
107106
{
@@ -157,7 +156,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => {
157156
)} OP_CHECKSIG`;
158157
const leafScript = bitcoin.script.fromASM(leafScriptAsm);
159158

160-
const scriptTree: any[] = [
159+
const scriptTree: Taptree = [
161160
[
162161
{
163162
output: bitcoin.script.fromASM(
@@ -262,7 +261,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => {
262261
const leafScriptAsm = `OP_10 OP_CHECKSEQUENCEVERIFY OP_DROP ${leafPubkey} OP_CHECKSIG`;
263262
const leafScript = bitcoin.script.fromASM(leafScriptAsm);
264263

265-
const scriptTree: any[] = [
264+
const scriptTree: Taptree = [
266265
{
267266
output: bitcoin.script.fromASM(
268267
'50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG',
@@ -361,7 +360,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => {
361360

362361
const leafScript = bitcoin.script.fromASM(leafScriptAsm);
363362

364-
const scriptTree: any[] = [
363+
const scriptTree: Taptree = [
365364
{
366365
output: bitcoin.script.fromASM(
367366
'50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG',

0 commit comments

Comments
 (0)