Skip to content

Commit 8fd07fc

Browse files
committed
refactor: extract taproot related logic to taproot.ts file
1 parent b8f8c91 commit 8fd07fc

File tree

11 files changed

+316
-342
lines changed

11 files changed

+316
-342
lines changed

src/merkle.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
/// <reference types="node" />
22
export declare function fastMerkleRoot(values: Buffer[], digestFn: (b: Buffer) => Buffer): Buffer;
3-
export declare function computeMastRoot(scripts: any): Buffer;

src/merkle.js

Lines changed: 1 addition & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
'use strict';
22
Object.defineProperty(exports, '__esModule', { value: true });
3-
exports.computeMastRoot = exports.fastMerkleRoot = void 0;
4-
const buffer_1 = require('buffer');
5-
const bcrypto = require('./crypto');
6-
// todo: use varuint-bitcoin??
7-
const varuint = require('bip174/src/lib/converter/varint');
8-
// todo: find better place for these consts
9-
const TAP_LEAF_TAG = buffer_1.Buffer.from('TapLeaf', 'utf8');
10-
const TAP_BRANCH_TAG = buffer_1.Buffer.from('TapBranch', 'utf8');
11-
const LEAF_VERSION_TAPSCRIPT = 0xc0;
3+
exports.fastMerkleRoot = void 0;
124
function fastMerkleRoot(values, digestFn) {
135
if (!Array.isArray(values)) throw TypeError('Expected values Array');
146
if (typeof digestFn !== 'function')
@@ -28,40 +20,3 @@ function fastMerkleRoot(values, digestFn) {
2820
return results[0];
2921
}
3022
exports.fastMerkleRoot = fastMerkleRoot;
31-
// todo: solve any[]
32-
function computeMastRoot(scripts) {
33-
if (scripts.length === 1) {
34-
const script = scripts[0];
35-
if (Array.isArray(script)) {
36-
return computeMastRoot(script);
37-
}
38-
script.version = script.version || LEAF_VERSION_TAPSCRIPT;
39-
if ((script.version & 1) !== 0) throw new Error('Invalid script version'); // todo typedef error
40-
// todo: if (script.output)scheck is bytes
41-
const scriptOutput = buffer_1.Buffer.from(script.output, 'hex');
42-
return bcrypto.taggedHash(
43-
TAP_LEAF_TAG,
44-
buffer_1.Buffer.concat([
45-
buffer_1.Buffer.from([script.version]),
46-
serializeScript(scriptOutput),
47-
]),
48-
);
49-
}
50-
// todo: this is a binary tree, use zero an one index
51-
const half = Math.trunc(scripts.length / 2);
52-
let leftHash = computeMastRoot(scripts.slice(0, half));
53-
let rightHash = computeMastRoot(scripts.slice(half));
54-
if (leftHash.compare(rightHash) === 1)
55-
[leftHash, rightHash] = [rightHash, leftHash];
56-
return bcrypto.taggedHash(
57-
TAP_BRANCH_TAG,
58-
buffer_1.Buffer.concat([leftHash, rightHash]),
59-
);
60-
}
61-
exports.computeMastRoot = computeMastRoot;
62-
function serializeScript(s) {
63-
const varintLen = varuint.encodingLength(s.length);
64-
const buffer = buffer_1.Buffer.allocUnsafe(varintLen); // better
65-
varuint.encode(s.length, buffer);
66-
return buffer_1.Buffer.concat([buffer, s]);
67-
}

src/payments/p2tr.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ exports.p2tr = void 0;
44
const networks_1 = require('../networks');
55
const bscript = require('../script');
66
const types_1 = require('../types');
7-
const merkle_1 = require('../merkle');
7+
const taproot_1 = require('../taproot');
88
const lazy = require('./lazy');
99
const bech32_1 = require('bech32');
1010
const OPS = bscript.OPS;
@@ -72,14 +72,14 @@ function p2tr(a, opts) {
7272
});
7373
lazy.prop(o, 'hash', () => {
7474
if (a.hash) return a.hash;
75-
if (a.scriptsTree) return (0, merkle_1.computeMastRoot)(a.scriptsTree);
75+
if (a.scriptsTree) return (0, taproot_1.computeMastRoot)(a.scriptsTree);
7676
const w = _witness();
7777
if (w && w.length > 1) {
7878
const controlBlock = w[w.length - 1];
7979
const leafVersion = controlBlock[0] & 0b11111110;
8080
const script = w[w.length - 2];
81-
const tapLeafHash = (0, types_1.leafHash)(script, leafVersion);
82-
return (0, types_1.rootHash)(controlBlock, tapLeafHash);
81+
const tapLeafHash = (0, taproot_1.leafHash)(script, leafVersion);
82+
return (0, taproot_1.rootHash)(controlBlock, tapLeafHash);
8383
}
8484
return null;
8585
});
@@ -92,7 +92,7 @@ function p2tr(a, opts) {
9292
if (a.output) return a.output.slice(2);
9393
if (a.address) return _address().data;
9494
if (o.internalPubkey) {
95-
const tweakedKey = (0, types_1.tweakKey)(o.internalPubkey, o.hash);
95+
const tweakedKey = (0, taproot_1.tweakKey)(o.internalPubkey, o.hash);
9696
if (tweakedKey) return tweakedKey.x;
9797
}
9898
});
@@ -143,19 +143,19 @@ function p2tr(a, opts) {
143143
else pubkey = a.output.slice(2);
144144
}
145145
if (a.internalPubkey) {
146-
const tweakedKey = (0, types_1.tweakKey)(a.internalPubkey, o.hash);
146+
const tweakedKey = (0, taproot_1.tweakKey)(a.internalPubkey, o.hash);
147147
if (tweakedKey === null)
148148
throw new TypeError('Invalid internalPubkey for p2tr');
149149
if (pubkey.length > 0 && !pubkey.equals(tweakedKey.x))
150150
throw new TypeError('Pubkey mismatch');
151151
else pubkey = tweakedKey.x;
152152
}
153153
if (pubkey?.length) {
154-
if ((0, types_1.liftX)(pubkey) === null)
154+
if ((0, taproot_1.liftX)(pubkey) === null)
155155
throw new TypeError('Invalid pubkey for p2tr');
156156
}
157157
if (a.hash && a.scriptsTree) {
158-
const hash = (0, merkle_1.computeMastRoot)(a.scriptsTree);
158+
const hash = (0, taproot_1.computeMastRoot)(a.scriptsTree);
159159
if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch');
160160
}
161161
// todo: review cache
@@ -189,14 +189,14 @@ function p2tr(a, opts) {
189189
const internalPubkey = controlBlock.slice(1, 33);
190190
if (a.internalPubkey && !a.internalPubkey.equals(internalPubkey))
191191
throw new TypeError('Internal pubkey mismatch');
192-
const internalPubkeyPoint = (0, types_1.liftX)(internalPubkey);
192+
const internalPubkeyPoint = (0, taproot_1.liftX)(internalPubkey);
193193
if (!internalPubkeyPoint)
194194
throw new TypeError('Invalid internalPubkey for p2tr witness');
195195
const leafVersion = controlBlock[0] & 0b11111110;
196196
const script = witness[witness.length - 2];
197-
const tapLeafHash = (0, types_1.leafHash)(script, leafVersion);
198-
const hash = (0, types_1.rootHash)(controlBlock, tapLeafHash);
199-
const outputKey = (0, types_1.tweakKey)(internalPubkey, hash);
197+
const tapLeafHash = (0, taproot_1.leafHash)(script, leafVersion);
198+
const hash = (0, taproot_1.rootHash)(controlBlock, tapLeafHash);
199+
const outputKey = (0, taproot_1.tweakKey)(internalPubkey, hash);
200200
if (!outputKey)
201201
// todo: needs test data
202202
throw new TypeError('Invalid outputKey for p2tr witness');

src/taproot.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/// <reference types="node" />
2+
import { TweakedPublicKey } from './types';
3+
export declare function liftX(buffer: Buffer): Buffer | null;
4+
export declare function tweakKey(pubKey: Buffer, h: Buffer | undefined): TweakedPublicKey | null;
5+
export declare function leafHash(script: Buffer, version: number): Buffer;
6+
export declare function rootHash(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer;
7+
export declare function computeMastRoot(scripts: any): Buffer;

src/taproot.js

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
'use strict';
2+
Object.defineProperty(exports, '__esModule', { value: true });
3+
exports.computeMastRoot = exports.rootHash = exports.leafHash = exports.tweakKey = exports.liftX = void 0;
4+
const buffer_1 = require('buffer');
5+
const BN = require('bn.js');
6+
const bcrypto = require('./crypto');
7+
// todo: use varuint-bitcoin??
8+
const varuint = require('bip174/src/lib/converter/varint');
9+
const types_1 = require('./types');
10+
// todo: !!!Temp, to be replaced. Only works because bip32 has it as dependecy. Linting will fail.
11+
const ecc = require('tiny-secp256k1');
12+
const LEAF_VERSION_TAPSCRIPT = 0xc0;
13+
const TAP_LEAF_TAG = buffer_1.Buffer.from('TapLeaf', 'utf8');
14+
const TAP_BRANCH_TAG = buffer_1.Buffer.from('TapBranch', 'utf8');
15+
const TAP_TWEAK_TAG = buffer_1.Buffer.from('TapTweak', 'utf8');
16+
// todo: compare buffers dirrectly
17+
const GROUP_ORDER = buffer_1.Buffer.from(
18+
'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141',
19+
'hex',
20+
);
21+
const GROUP_ORDER_BN = new BN(GROUP_ORDER);
22+
const EC_P_BN = new BN(types_1.EC_P);
23+
const EC_P_REDUCTION = BN.red(EC_P_BN);
24+
const EC_P_QUADRATIC_RESIDUE = EC_P_BN.addn(1).divn(4);
25+
const BN_2 = new BN(2);
26+
const BN_3 = new BN(3);
27+
const BN_7 = new BN(7);
28+
function liftX(buffer) {
29+
if (!buffer_1.Buffer.isBuffer(buffer)) return null;
30+
if (buffer.length !== 32) return null;
31+
if (buffer.compare(types_1.ZERO32) === 0) return null;
32+
if (buffer.compare(types_1.EC_P) >= 0) return null;
33+
const x = new BN(buffer);
34+
const x1 = x.toRed(EC_P_REDUCTION);
35+
const ySq = x1
36+
.redPow(BN_3)
37+
.add(BN_7)
38+
.mod(EC_P_BN);
39+
const y = ySq.redPow(EC_P_QUADRATIC_RESIDUE);
40+
if (!ySq.eq(y.redPow(BN_2))) {
41+
return null;
42+
}
43+
const y1 = y.isEven() ? y : EC_P_BN.sub(y);
44+
return buffer_1.Buffer.concat([
45+
buffer_1.Buffer.from([0x04]),
46+
buffer_1.Buffer.from(x1.toBuffer('be', 32)),
47+
buffer_1.Buffer.from(y1.toBuffer('be', 32)),
48+
]);
49+
}
50+
exports.liftX = liftX;
51+
function tweakKey(pubKey, h) {
52+
if (!buffer_1.Buffer.isBuffer(pubKey)) return null;
53+
if (pubKey.length !== 32) return null;
54+
if (h && h.length !== 32) return null;
55+
const tweakHash = bcrypto.taggedHash(
56+
TAP_TWEAK_TAG,
57+
buffer_1.Buffer.concat(h ? [pubKey, h] : [pubKey]),
58+
);
59+
const t = new BN(tweakHash);
60+
if (t.gte(GROUP_ORDER_BN)) {
61+
// todo: add test for this case
62+
throw new Error('Tweak value over the SECP256K1 Order');
63+
}
64+
const P = liftX(pubKey);
65+
if (P === null) return null;
66+
const Q = pointAddScalar(P, tweakHash);
67+
return {
68+
isOdd: Q[64] % 2 === 1,
69+
x: Q.slice(1, 33),
70+
};
71+
}
72+
exports.tweakKey = tweakKey;
73+
function leafHash(script, version) {
74+
return buffer_1.Buffer.concat([
75+
buffer_1.Buffer.from([version]),
76+
serializeScript(script),
77+
]);
78+
}
79+
exports.leafHash = leafHash;
80+
function rootHash(controlBlock, tapLeafMsg) {
81+
const k = [];
82+
const e = [];
83+
const m = (controlBlock.length - 33) / 32;
84+
k[0] = bcrypto.taggedHash(TAP_LEAF_TAG, tapLeafMsg);
85+
for (let j = 0; j < m; j++) {
86+
e[j] = controlBlock.slice(33 + 32 * j, 65 + 32 * j);
87+
if (k[j].compare(e[j]) < 0) {
88+
k[j + 1] = bcrypto.taggedHash(
89+
TAP_BRANCH_TAG,
90+
buffer_1.Buffer.concat([k[j], e[j]]),
91+
);
92+
} else {
93+
k[j + 1] = bcrypto.taggedHash(
94+
TAP_BRANCH_TAG,
95+
buffer_1.Buffer.concat([e[j], k[j]]),
96+
);
97+
}
98+
}
99+
return k[m];
100+
}
101+
exports.rootHash = rootHash;
102+
// todo: solve any[]
103+
function computeMastRoot(scripts) {
104+
if (scripts.length === 1) {
105+
const script = scripts[0];
106+
if (Array.isArray(script)) {
107+
return computeMastRoot(script);
108+
}
109+
script.version = script.version || LEAF_VERSION_TAPSCRIPT;
110+
if ((script.version & 1) !== 0) throw new Error('Invalid script version'); // todo typedef error
111+
// todo: if (script.output)scheck is bytes
112+
const scriptOutput = buffer_1.Buffer.from(script.output, 'hex');
113+
return bcrypto.taggedHash(
114+
TAP_LEAF_TAG,
115+
buffer_1.Buffer.concat([
116+
buffer_1.Buffer.from([script.version]),
117+
serializeScript(scriptOutput),
118+
]),
119+
);
120+
}
121+
// todo: this is a binary tree, use zero an one index
122+
const half = Math.trunc(scripts.length / 2);
123+
let leftHash = computeMastRoot(scripts.slice(0, half));
124+
let rightHash = computeMastRoot(scripts.slice(half));
125+
if (leftHash.compare(rightHash) === 1)
126+
[leftHash, rightHash] = [rightHash, leftHash];
127+
return bcrypto.taggedHash(
128+
TAP_BRANCH_TAG,
129+
buffer_1.Buffer.concat([leftHash, rightHash]),
130+
);
131+
}
132+
exports.computeMastRoot = computeMastRoot;
133+
function serializeScript(s) {
134+
const varintLen = varuint.encodingLength(s.length);
135+
const buffer = buffer_1.Buffer.allocUnsafe(varintLen); // better
136+
varuint.encode(s.length, buffer);
137+
return buffer_1.Buffer.concat([buffer, s]);
138+
}
139+
// todo: do not use ecc
140+
function pointAddScalar(P, h) {
141+
return ecc.pointAddScalar(P, h);
142+
}

src/types.d.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
/// <reference types="node" />
2+
import { Buffer as NBuffer } from 'buffer';
23
export declare const typeforce: any;
4+
export declare const ZERO32: NBuffer;
5+
export declare const EC_P: NBuffer;
36
export declare function isPoint(p: Buffer | number | undefined | null): boolean;
4-
export declare function liftX(buffer: Buffer): Buffer | null;
5-
export declare function tweakKey(pubKey: Buffer, h: Buffer | undefined): TweakedPublicKey | null;
6-
export declare function leafHash(script: Buffer, version: number): Buffer;
7-
export declare function rootHash(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer;
87
export declare function UInt31(value: number): boolean;
98
export declare function BIP32Path(value: string): boolean;
109
export declare namespace BIP32Path {

0 commit comments

Comments
 (0)