Skip to content

Commit a3550c1

Browse files
committed
feat: build control-block as part of witness; update tests
1 parent f7d01b8 commit a3550c1

File tree

9 files changed

+426
-46
lines changed

9 files changed

+426
-46
lines changed

src/payments/p2tr.js

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22
Object.defineProperty(exports, '__esModule', { value: true });
33
exports.p2tr = void 0;
4+
const buffer_1 = require('buffer');
45
const networks_1 = require('../networks');
56
const bscript = require('../script');
67
const types_1 = require('../types');
@@ -52,7 +53,7 @@ function p2tr(a, opts) {
5253
return {
5354
version,
5455
prefix: result.prefix,
55-
data: Buffer.from(data),
56+
data: buffer_1.Buffer.from(data),
5657
};
5758
});
5859
const _witness = lazy.value(() => {
@@ -76,7 +77,7 @@ function p2tr(a, opts) {
7677
});
7778
lazy.prop(o, 'hash', () => {
7879
if (a.hash) return a.hash;
79-
if (a.scriptsTree) return (0, taproot_1.rootHashFromTree)(a.scriptsTree);
80+
if (a.scriptsTree) return (0, taproot_1.toHashTree)(a.scriptsTree).hash;
8081
const w = _witness();
8182
if (w && w.length > 1) {
8283
const controlBlock = w[w.length - 1];
@@ -118,12 +119,32 @@ function p2tr(a, opts) {
118119
});
119120
lazy.prop(o, 'witness', () => {
120121
if (a.witness) return a.witness;
121-
if (!a.signature) return;
122-
return [a.signature];
122+
if (a.scriptsTree && a.scriptLeaf && a.internalPubkey) {
123+
const hashTree = (0, taproot_1.toHashTree)(a.scriptsTree);
124+
const leafHash = (0, taproot_1.tapLeafHash)(
125+
a.scriptLeaf.output,
126+
a.scriptLeaf.version,
127+
);
128+
const path = (0, taproot_1.findScriptPath)(hashTree, leafHash);
129+
const outputKey = (0, taproot_1.tweakKey)(
130+
a.internalPubkey,
131+
hashTree.hash,
132+
);
133+
if (!outputKey) return;
134+
const version = a.scriptLeaf.version || 0xc0;
135+
const controlBock = buffer_1.Buffer.concat(
136+
[
137+
buffer_1.Buffer.from([version | outputKey.parity]),
138+
a.internalPubkey,
139+
].concat(path.reverse()),
140+
);
141+
return [a.scriptLeaf.output, controlBock];
142+
}
143+
if (a.signature) return [a.signature];
123144
});
124145
// extended validation
125146
if (opts.validate) {
126-
let pubkey = Buffer.from([]);
147+
let pubkey = buffer_1.Buffer.from([]);
127148
if (a.address) {
128149
if (network && network.bech32 !== _address().prefix)
129150
throw new TypeError('Invalid prefix or Network mismatch');
@@ -162,7 +183,7 @@ function p2tr(a, opts) {
162183
throw new TypeError('Invalid pubkey for p2tr');
163184
}
164185
if (a.hash && a.scriptsTree) {
165-
const hash = (0, taproot_1.rootHashFromTree)(a.scriptsTree);
186+
const hash = (0, taproot_1.toHashTree)(a.scriptsTree).hash;
166187
if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch');
167188
}
168189
const witness = _witness();
@@ -208,8 +229,7 @@ function p2tr(a, opts) {
208229
throw new TypeError('Invalid outputKey for p2tr witness');
209230
if (pubkey.length && !pubkey.equals(outputKey.x))
210231
throw new TypeError('Pubkey mismatch for p2tr witness');
211-
const controlBlockOddParity = (controlBlock[0] & 1) === 1;
212-
if (outputKey.isOdd !== controlBlockOddParity)
232+
if (outputKey.parity !== (controlBlock[0] & 1))
213233
throw new Error('Incorrect parity');
214234
}
215235
}

src/taproot.d.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,11 @@ import { TweakedPublicKey, TaprootLeaf } from './types';
33
export declare function liftX(buffer: Buffer): Buffer | null;
44
export declare function tweakKey(pubKey: Buffer, h: Buffer | undefined): TweakedPublicKey | null;
55
export declare function rootHashFromPath(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer;
6-
export declare function rootHashFromTree(scripts: TaprootLeaf[]): Buffer;
7-
export declare function tapLeafHash(script: Buffer, version: number): Buffer;
6+
export interface HashTree {
7+
hash: Buffer;
8+
left?: HashTree;
9+
right?: HashTree;
10+
}
11+
export declare function toHashTree(scripts: TaprootLeaf[]): HashTree;
12+
export declare function findScriptPath(node: HashTree, hash: Buffer): Buffer[];
13+
export declare function tapLeafHash(script: Buffer, version?: number): Buffer;

src/taproot.js

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict';
22
Object.defineProperty(exports, '__esModule', { value: true });
3-
exports.tapLeafHash = exports.rootHashFromTree = exports.rootHashFromPath = exports.tweakKey = exports.liftX = void 0;
3+
exports.tapLeafHash = exports.findScriptPath = exports.toHashTree = exports.rootHashFromPath = exports.tweakKey = exports.liftX = void 0;
44
const buffer_1 = require('buffer');
55
const BN = require('bn.js');
66
const bcrypto = require('./crypto');
@@ -58,7 +58,7 @@ function tweakKey(pubKey, h) {
5858
if (P === null) return null;
5959
const Q = pointAddScalar(P, tweakHash);
6060
return {
61-
isOdd: Q[64] % 2 === 1,
61+
parity: Q[64] % 2,
6262
x: Q.slice(1, 33),
6363
};
6464
}
@@ -78,28 +78,53 @@ function rootHashFromPath(controlBlock, tapLeafMsg) {
7878
return k[m];
7979
}
8080
exports.rootHashFromPath = rootHashFromPath;
81-
function rootHashFromTree(scripts) {
81+
function toHashTree(scripts) {
8282
if (scripts.length === 1) {
8383
const script = scripts[0];
8484
if (Array.isArray(script)) {
85-
return rootHashFromTree(script);
85+
return toHashTree(script);
8686
}
8787
script.version = script.version || LEAF_VERSION_TAPSCRIPT;
8888
if ((script.version & 1) !== 0)
8989
throw new TypeError('Invalid script version');
90-
return tapLeafHash(script.output, script.version);
90+
return {
91+
hash: tapLeafHash(script.output, script.version),
92+
};
9193
}
9294
// todo: this is a binary tree, use zero an one index
9395
const half = Math.trunc(scripts.length / 2);
94-
let leftHash = rootHashFromTree(scripts.slice(0, half));
95-
let rightHash = rootHashFromTree(scripts.slice(half));
96+
const left = toHashTree(scripts.slice(0, half));
97+
const right = toHashTree(scripts.slice(half));
98+
let leftHash = left.hash;
99+
let rightHash = right.hash;
96100
if (leftHash.compare(rightHash) === 1)
97101
[leftHash, rightHash] = [rightHash, leftHash];
98-
return tapBranchHash(leftHash, rightHash);
102+
return {
103+
hash: tapBranchHash(leftHash, rightHash),
104+
left,
105+
right,
106+
};
107+
}
108+
exports.toHashTree = toHashTree;
109+
function findScriptPath(node, hash) {
110+
if (node.left) {
111+
if (node.left.hash.equals(hash)) return node.right ? [node.right.hash] : [];
112+
const leftPath = findScriptPath(node.left, hash);
113+
if (leftPath.length)
114+
return node.right ? [node.right.hash].concat(leftPath) : leftPath;
115+
}
116+
if (node.right) {
117+
if (node.right.hash.equals(hash)) return node.left ? [node.left.hash] : [];
118+
const rightPath = findScriptPath(node.right, hash);
119+
if (rightPath.length) {
120+
}
121+
return node.left ? [node.left.hash].concat(rightPath) : rightPath;
122+
}
123+
return [];
99124
}
100-
exports.rootHashFromTree = rootHashFromTree;
101-
// todo: rename to tapLeafHash
125+
exports.findScriptPath = findScriptPath;
102126
function tapLeafHash(script, version) {
127+
version = version || LEAF_VERSION_TAPSCRIPT;
103128
return bcrypto.taggedHash(
104129
TAP_LEAF_TAG,
105130
buffer_1.Buffer.concat([

src/types.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export declare function Satoshi(value: number): boolean;
1515
export declare const ECPoint: any;
1616
export declare const Network: any;
1717
export interface TweakedPublicKey {
18-
isOdd: boolean;
18+
parity: number;
1919
x: Buffer;
2020
}
2121
export interface TaprootLeaf {

0 commit comments

Comments
 (0)