Skip to content

Commit 719e4a4

Browse files
authored
add integration tests for taproot (#3)
* test: add PSBT example for taproot * refactor: variable renaming * refactor: use constant for tapscript leaf version * feat: reuse redeem field for taproot spend * test: make sure it defaults to tapscript version 192 * chore: remove `scriptLeaf` logic * feat: add tapscript sign() a finalize() logic * test: spend taproot script-path * test: add tapscript for OP_CHECKSEQUENCEVERIFY * feat: add multisig integration test * refactor: rename scriptsTree to scriptTree (as per BP341) * feat: check that the scriptTree is a binary tree * feat: compute the redeem from witness; add unit tests * test: add test for invalid redeem script * feat: check the redeemVersion on the input data first * test: add tests for taproot script-path sign, and key-path finalize
1 parent 203974d commit 719e4a4

File tree

20 files changed

+1136
-238
lines changed

20 files changed

+1136
-238
lines changed

src/ops.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ const OPS = {
117117
OP_NOP8: 183,
118118
OP_NOP9: 184,
119119
OP_NOP10: 185,
120+
OP_CHECKSIGADD: 186,
120121
OP_PUBKEYHASH: 253,
121122
OP_PUBKEY: 254,
122123
OP_INVALIDOPCODE: 255,

src/payments/index.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference types="node" />
22
import { Network } from '../networks';
3-
import { TaprootLeaf, TinySecp256k1Interface } from '../types';
3+
import { TinySecp256k1Interface, TaprootLeaf } from '../types';
44
import { p2data as embed } from './embed';
55
import { p2ms } from './p2ms';
66
import { p2pk } from './p2pk';
@@ -25,8 +25,8 @@ export interface Payment {
2525
address?: string;
2626
hash?: Buffer;
2727
redeem?: Payment;
28-
scriptsTree?: any;
29-
scriptLeaf?: TaprootLeaf;
28+
redeemVersion?: number;
29+
scriptTree?: TaprootLeaf[];
3030
witness?: Buffer[];
3131
}
3232
export declare type PaymentCreator = (a: Payment, opts?: PaymentOpts) => Payment;

src/payments/p2tr.js

Lines changed: 69 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ const lazy = require('./lazy');
1010
const bech32_1 = require('bech32');
1111
const testecc_1 = require('./testecc');
1212
const OPS = bscript.OPS;
13-
const TAPROOT_VERSION = 0x01;
13+
const TAPROOT_WITNESS_VERSION = 0x01;
1414
const ANNEX_PREFIX = 0x50;
15+
const LEAF_VERSION_MASK = 0b11111110;
1516
function p2tr(a, opts) {
1617
if (
1718
!a.address &&
@@ -40,11 +41,15 @@ function p2tr(a, opts) {
4041
witness: types_1.typeforce.maybe(
4142
types_1.typeforce.arrayOf(types_1.typeforce.Buffer),
4243
),
43-
// scriptsTree: typef.maybe(typef.TaprootNode), // use merkel.isMast ?
44-
scriptLeaf: types_1.typeforce.maybe({
45-
version: types_1.typeforce.maybe(types_1.typeforce.Number),
44+
scriptTree: types_1.typeforce.maybe(taprootutils_1.isTapTree),
45+
redeem: types_1.typeforce.maybe({
4646
output: types_1.typeforce.maybe(types_1.typeforce.Buffer),
47+
redeemVersion: types_1.typeforce.maybe(types_1.typeforce.Number),
48+
witness: types_1.typeforce.maybe(
49+
types_1.typeforce.arrayOf(types_1.typeforce.Buffer),
50+
),
4751
}),
52+
redeemVersion: types_1.typeforce.maybe(types_1.typeforce.Number),
4853
},
4954
a,
5055
);
@@ -58,13 +63,13 @@ function p2tr(a, opts) {
5863
data: buffer_1.Buffer.from(data),
5964
};
6065
});
66+
// remove annex if present, ignored by taproot
6167
const _witness = lazy.value(() => {
6268
if (!a.witness || !a.witness.length) return;
6369
if (
6470
a.witness.length >= 2 &&
6571
a.witness[a.witness.length - 1][0] === ANNEX_PREFIX
6672
) {
67-
// remove annex, ignored by taproot
6873
return a.witness.slice(0, -1);
6974
}
7075
return a.witness.slice();
@@ -74,17 +79,16 @@ function p2tr(a, opts) {
7479
lazy.prop(o, 'address', () => {
7580
if (!o.pubkey) return;
7681
const words = bech32_1.bech32m.toWords(o.pubkey);
77-
words.unshift(TAPROOT_VERSION);
82+
words.unshift(TAPROOT_WITNESS_VERSION);
7883
return bech32_1.bech32m.encode(network.bech32, words);
7984
});
8085
lazy.prop(o, 'hash', () => {
8186
if (a.hash) return a.hash;
82-
if (a.scriptsTree)
83-
return (0, taprootutils_1.toHashTree)(a.scriptsTree).hash;
87+
if (a.scriptTree) return (0, taprootutils_1.toHashTree)(a.scriptTree).hash;
8488
const w = _witness();
8589
if (w && w.length > 1) {
8690
const controlBlock = w[w.length - 1];
87-
const leafVersion = controlBlock[0] & 0b11111110;
91+
const leafVersion = controlBlock[0] & LEAF_VERSION_MASK;
8892
const script = w[w.length - 2];
8993
const leafHash = (0, taprootutils_1.tapLeafHash)(script, leafVersion);
9094
return (0, taprootutils_1.rootHashFromPath)(controlBlock, leafHash);
@@ -95,8 +99,25 @@ function p2tr(a, opts) {
9599
if (!o.pubkey) return;
96100
return bscript.compile([OPS.OP_1, o.pubkey]);
97101
});
98-
lazy.prop(o, 'scriptLeaf', () => {
99-
if (a.scriptLeaf) return a.scriptLeaf;
102+
lazy.prop(o, 'redeemVersion', () => {
103+
if (a.redeemVersion) return a.redeemVersion;
104+
if (
105+
a.redeem &&
106+
a.redeem.redeemVersion !== undefined &&
107+
a.redeem.redeemVersion !== null
108+
) {
109+
return a.redeem.redeemVersion;
110+
}
111+
return taprootutils_1.LEAF_VERSION_TAPSCRIPT;
112+
});
113+
lazy.prop(o, 'redeem', () => {
114+
const witness = _witness(); // witness without annex
115+
if (!witness || witness.length < 2) return;
116+
return {
117+
output: witness[witness.length - 2],
118+
witness: witness.slice(0, -2),
119+
redeemVersion: witness[witness.length - 1][0] & LEAF_VERSION_MASK,
120+
};
100121
});
101122
lazy.prop(o, 'pubkey', () => {
102123
if (a.pubkey) return a.pubkey;
@@ -118,29 +139,25 @@ function p2tr(a, opts) {
118139
if (!a.witness || a.witness.length !== 1) return;
119140
return a.witness[0];
120141
});
121-
lazy.prop(o, 'input', () => {
122-
// todo
123-
});
124142
lazy.prop(o, 'witness', () => {
125143
if (a.witness) return a.witness;
126-
if (a.scriptsTree && a.scriptLeaf && a.internalPubkey) {
144+
if (a.scriptTree && a.redeem && a.redeem.output && a.internalPubkey) {
127145
// todo: optimize/cache
128-
const hashTree = (0, taprootutils_1.toHashTree)(a.scriptsTree);
146+
const hashTree = (0, taprootutils_1.toHashTree)(a.scriptTree);
129147
const leafHash = (0, taprootutils_1.tapLeafHash)(
130-
a.scriptLeaf.output,
131-
a.scriptLeaf.version,
148+
a.redeem.output,
149+
o.redeemVersion,
132150
);
133151
const path = (0, taprootutils_1.findScriptPath)(hashTree, leafHash);
134152
const outputKey = tweakKey(a.internalPubkey, hashTree.hash, _ecc());
135153
if (!outputKey) return;
136-
const version = a.scriptLeaf.version || 0xc0;
137154
const controlBock = buffer_1.Buffer.concat(
138155
[
139-
buffer_1.Buffer.from([version | outputKey.parity]),
156+
buffer_1.Buffer.from([o.redeemVersion | outputKey.parity]),
140157
a.internalPubkey,
141158
].concat(path.reverse()),
142159
);
143-
return [a.scriptLeaf.output, controlBock];
160+
return [a.redeem.output, controlBock];
144161
}
145162
if (a.signature) return [a.signature];
146163
});
@@ -150,7 +167,7 @@ function p2tr(a, opts) {
150167
if (a.address) {
151168
if (network && network.bech32 !== _address().prefix)
152169
throw new TypeError('Invalid prefix or Network mismatch');
153-
if (_address().version !== TAPROOT_VERSION)
170+
if (_address().version !== TAPROOT_WITNESS_VERSION)
154171
throw new TypeError('Invalid address version');
155172
if (_address().data.length !== 32)
156173
throw new TypeError('Invalid address data');
@@ -182,11 +199,32 @@ function p2tr(a, opts) {
182199
if (!_ecc().isXOnlyPoint(pubkey))
183200
throw new TypeError('Invalid pubkey for p2tr');
184201
}
185-
if (a.hash && a.scriptsTree) {
186-
const hash = (0, taprootutils_1.toHashTree)(a.scriptsTree).hash;
202+
if (a.hash && a.scriptTree) {
203+
const hash = (0, taprootutils_1.toHashTree)(a.scriptTree).hash;
187204
if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch');
188205
}
189206
const witness = _witness();
207+
// compare the provided redeem data with the one computed from witness
208+
if (a.redeem && o.redeem) {
209+
if (a.redeem.redeemVersion) {
210+
if (a.redeem.redeemVersion !== o.redeem.redeemVersion)
211+
throw new TypeError('Redeem.redeemVersion and witness mismatch');
212+
}
213+
if (a.redeem.output) {
214+
if (bscript.decompile(a.redeem.output).length === 0)
215+
throw new TypeError('Redeem.output is invalid');
216+
// output redeem is constructed from the witness
217+
if (o.redeem.output && !a.redeem.output.equals(o.redeem.output))
218+
throw new TypeError('Redeem.output and witness mismatch');
219+
}
220+
if (a.redeem.witness) {
221+
if (
222+
o.redeem.witness &&
223+
!stacksEqual(a.redeem.witness, o.redeem.witness)
224+
)
225+
throw new TypeError('Redeem.witness and witness mismatch');
226+
}
227+
}
190228
if (witness && witness.length) {
191229
if (witness.length === 1) {
192230
// key spending
@@ -215,7 +253,7 @@ function p2tr(a, opts) {
215253
throw new TypeError('Internal pubkey mismatch');
216254
if (!_ecc().isXOnlyPoint(internalPubkey))
217255
throw new TypeError('Invalid internalPubkey for p2tr witness');
218-
const leafVersion = controlBlock[0] & 0b11111110;
256+
const leafVersion = controlBlock[0] & LEAF_VERSION_MASK;
219257
const script = witness[witness.length - 2];
220258
const leafHash = (0, taprootutils_1.tapLeafHash)(script, leafVersion);
221259
const hash = (0, taprootutils_1.rootHashFromPath)(
@@ -248,3 +286,9 @@ function tweakKey(pubKey, h, eccLib) {
248286
x: buffer_1.Buffer.from(res.xOnlyPubkey),
249287
};
250288
}
289+
function stacksEqual(a, b) {
290+
if (a.length !== b.length) return false;
291+
return a.every((x, i) => {
292+
return x.equals(b[i]);
293+
});
294+
}

src/payments/taprootutils.d.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/// <reference types="node" />
22
import { TaprootLeaf } from '../types';
3+
export declare const LEAF_VERSION_TAPSCRIPT = 192;
34
export declare function rootHashFromPath(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer;
45
export interface HashTree {
56
hash: Buffer;
@@ -9,12 +10,16 @@ export interface HashTree {
910
/**
1011
* Build the hash tree from the scripts binary tree.
1112
* The binary tree can be balanced or not.
12-
* @param scriptsTree - is a list representing a binary tree where an element can be:
13+
* @param scriptTree - is a list representing a binary tree where an element can be:
1314
* - a taproot leaf [(output, version)], or
1415
* - a pair of two taproot leafs [(output, version), (output, version)], or
1516
* - one taproot leaf and a list of elements
1617
*/
17-
export declare function toHashTree(scriptsTree: TaprootLeaf[]): HashTree;
18+
export declare function toHashTree(scriptTree: TaprootLeaf[]): HashTree;
19+
/**
20+
* Check if the tree is a binary tree with leafs of type TaprootLeaf
21+
*/
22+
export declare function isTapTree(scriptTree: TaprootLeaf[]): boolean;
1823
/**
1924
* Given a MAST tree, it finds the path of a particular hash.
2025
* @param node - the root of the tree

src/payments/taprootutils.js

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
'use strict';
22
Object.defineProperty(exports, '__esModule', { value: true });
3-
exports.tapTweakHash = exports.tapLeafHash = exports.findScriptPath = exports.toHashTree = exports.rootHashFromPath = void 0;
3+
exports.tapTweakHash = exports.tapLeafHash = exports.findScriptPath = exports.isTapTree = 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 LEAF_VERSION_TAPSCRIPT = 0xc0;
87
const TAP_LEAF_TAG = 'TapLeaf';
98
const TAP_BRANCH_TAG = 'TapBranch';
109
const TAP_TWEAK_TAG = 'TapTweak';
10+
exports.LEAF_VERSION_TAPSCRIPT = 0xc0;
1111
function rootHashFromPath(controlBlock, tapLeafMsg) {
1212
const k = [tapLeafMsg];
1313
const e = [];
@@ -26,26 +26,26 @@ exports.rootHashFromPath = rootHashFromPath;
2626
/**
2727
* Build the hash tree from the scripts binary tree.
2828
* The binary tree can be balanced or not.
29-
* @param scriptsTree - is a list representing a binary tree where an element can be:
29+
* @param scriptTree - is a list representing a binary tree where an element can be:
3030
* - a taproot leaf [(output, version)], or
3131
* - a pair of two taproot leafs [(output, version), (output, version)], or
3232
* - one taproot leaf and a list of elements
3333
*/
34-
function toHashTree(scriptsTree) {
35-
if (scriptsTree.length === 1) {
36-
const script = scriptsTree[0];
34+
function toHashTree(scriptTree) {
35+
if (scriptTree.length === 1) {
36+
const script = scriptTree[0];
3737
if (Array.isArray(script)) {
3838
return toHashTree(script);
3939
}
40-
script.version = script.version || LEAF_VERSION_TAPSCRIPT;
40+
script.version = script.version || exports.LEAF_VERSION_TAPSCRIPT;
4141
if ((script.version & 1) !== 0)
4242
throw new TypeError('Invalid script version');
4343
return {
4444
hash: tapLeafHash(script.output, script.version),
4545
};
4646
}
47-
const left = toHashTree([scriptsTree[0]]);
48-
const right = toHashTree([scriptsTree[1]]);
47+
const left = toHashTree([scriptTree[0]]);
48+
const right = toHashTree([scriptTree[1]]);
4949
let leftHash = left.hash;
5050
let rightHash = right.hash;
5151
if (leftHash.compare(rightHash) === 1)
@@ -57,6 +57,26 @@ function toHashTree(scriptsTree) {
5757
};
5858
}
5959
exports.toHashTree = toHashTree;
60+
/**
61+
* Check if the tree is a binary tree with leafs of type TaprootLeaf
62+
*/
63+
function isTapTree(scriptTree) {
64+
if (scriptTree.length > 2) return false;
65+
if (scriptTree.length === 1) {
66+
const script = scriptTree[0];
67+
if (Array.isArray(script)) {
68+
return isTapTree(script);
69+
}
70+
if (!script.output) return false;
71+
script.version = script.version || exports.LEAF_VERSION_TAPSCRIPT;
72+
if ((script.version & 1) !== 0) return false;
73+
return true;
74+
}
75+
if (!isTapTree([scriptTree[0]])) return false;
76+
if (!isTapTree([scriptTree[1]])) return false;
77+
return true;
78+
}
79+
exports.isTapTree = isTapTree;
6080
/**
6181
* Given a MAST tree, it finds the path of a particular hash.
6282
* @param node - the root of the tree
@@ -80,7 +100,7 @@ function findScriptPath(node, hash) {
80100
}
81101
exports.findScriptPath = findScriptPath;
82102
function tapLeafHash(script, version) {
83-
version = version || LEAF_VERSION_TAPSCRIPT;
103+
version = version || exports.LEAF_VERSION_TAPSCRIPT;
84104
return bcrypto.taggedHash(
85105
TAP_LEAF_TAG,
86106
buffer_1.Buffer.concat([

src/psbt.d.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,10 @@ input: PsbtInput, // The PSBT input contents
180180
script: Buffer, // The "meaningful" locking script Buffer (redeemScript for P2SH etc.)
181181
isSegwit: boolean, // Is it segwit?
182182
isP2SH: boolean, // Is it P2SH?
183-
isP2WSH: boolean) => {
183+
isP2WSH: boolean, // Is it P2WSH?
184+
eccLib?: TinySecp256k1Interface) => {
184185
finalScriptSig: Buffer | undefined;
185-
finalScriptWitness: Buffer | undefined;
186+
finalScriptWitness: Buffer | Buffer[] | undefined;
186187
};
187-
declare type AllScriptType = 'witnesspubkeyhash' | 'pubkeyhash' | 'multisig' | 'pubkey' | 'taproot' | 'nonstandard' | 'p2sh-witnesspubkeyhash' | 'p2sh-pubkeyhash' | 'p2sh-multisig' | 'p2sh-pubkey' | 'p2sh-nonstandard' | 'p2wsh-pubkeyhash' | 'p2wsh-multisig' | 'p2wsh-pubkey' | 'p2wsh-nonstandard' | 'p2sh-p2wsh-pubkeyhash' | 'p2sh-p2wsh-multisig' | 'p2sh-p2wsh-pubkey' | 'p2sh-p2wsh-nonstandard';
188+
declare type AllScriptType = 'witnesspubkeyhash' | 'pubkeyhash' | 'multisig' | 'pubkey' | 'taproot' | 'nonstandard' | 'p2sh-witnesspubkeyhash' | 'p2sh-pubkeyhash' | 'p2sh-multisig' | 'p2sh-pubkey' | 'p2sh-nonstandard' | 'p2wsh-pubkeyhash' | 'p2wsh-multisig' | 'p2wsh-pubkey' | 'p2wsh-nonstandard' | 'p2sh-p2wsh-pubkeyhash' | 'p2sh-p2wsh-multisig' | 'p2sh-p2wsh-pubkey' | 'p2sh-p2wsh-nonstandard' | 'p2tr-pubkey' | 'p2tr-nonstandard';
188189
export {};

0 commit comments

Comments
 (0)