Skip to content
45 changes: 28 additions & 17 deletions src/payments/p2tr.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ const verifyecc_1 = require('./verifyecc');
const OPS = bscript.OPS;
const TAPROOT_WITNESS_VERSION = 0x01;
const ANNEX_PREFIX = 0x50;
const LEAF_VERSION_MASK = 0b11111110;
function p2tr(a, opts) {
if (
!a.address &&
Expand Down Expand Up @@ -41,7 +40,7 @@ function p2tr(a, opts) {
witness: types_1.typeforce.maybe(
types_1.typeforce.arrayOf(types_1.typeforce.Buffer),
),
scriptTree: types_1.typeforce.maybe(taprootutils_1.isTapTree),
scriptTree: types_1.typeforce.maybe(types_1.isTaptree),
redeem: types_1.typeforce.maybe({
output: types_1.typeforce.maybe(types_1.typeforce.Buffer),
redeemVersion: types_1.typeforce.maybe(types_1.typeforce.Number),
Expand Down Expand Up @@ -74,6 +73,11 @@ function p2tr(a, opts) {
}
return a.witness.slice();
});
const _hashTree = lazy.value(() => {
if (a.scriptTree) return (0, taprootutils_1.toHashTree)(a.scriptTree);
if (a.hash) return { hash: a.hash };
return;
});
const network = a.network || networks_1.bitcoin;
const o = { name: 'p2tr', network };
lazy.prop(o, 'address', () => {
Expand All @@ -83,14 +87,17 @@ function p2tr(a, opts) {
return bech32_1.bech32m.encode(network.bech32, words);
});
lazy.prop(o, 'hash', () => {
if (a.hash) return a.hash;
if (a.scriptTree) return (0, taprootutils_1.toHashTree)(a.scriptTree).hash;
const hashTree = _hashTree();
if (hashTree) return hashTree.hash;
const w = _witness();
if (w && w.length > 1) {
const controlBlock = w[w.length - 1];
const leafVersion = controlBlock[0] & LEAF_VERSION_MASK;
const leafVersion = controlBlock[0] & types_1.TAPLEAF_VERSION_MASK;
const script = w[w.length - 2];
const leafHash = (0, taprootutils_1.tapLeafHash)(script, leafVersion);
const leafHash = (0, taprootutils_1.tapleafHash)({
output: script,
version: leafVersion,
});
return (0, taprootutils_1.rootHashFromPath)(controlBlock, leafHash);
}
return null;
Expand All @@ -116,7 +123,8 @@ function p2tr(a, opts) {
return {
output: witness[witness.length - 2],
witness: witness.slice(0, -2),
redeemVersion: witness[witness.length - 1][0] & LEAF_VERSION_MASK,
redeemVersion:
witness[witness.length - 1][0] & types_1.TAPLEAF_VERSION_MASK,
};
});
lazy.prop(o, 'pubkey', () => {
Expand All @@ -141,14 +149,14 @@ function p2tr(a, opts) {
});
lazy.prop(o, 'witness', () => {
if (a.witness) return a.witness;
if (a.scriptTree && a.redeem && a.redeem.output && a.internalPubkey) {
// todo: optimize/cache
const hashTree = (0, taprootutils_1.toHashTree)(a.scriptTree);
const leafHash = (0, taprootutils_1.tapLeafHash)(
a.redeem.output,
o.redeemVersion,
);
const hashTree = _hashTree();
if (hashTree && a.redeem && a.redeem.output && a.internalPubkey) {
const leafHash = (0, taprootutils_1.tapleafHash)({
output: a.redeem.output,
version: o.redeemVersion,
});
const path = (0, taprootutils_1.findScriptPath)(hashTree, leafHash);
if (!path) return;
const outputKey = tweakKey(a.internalPubkey, hashTree.hash, _ecc());
if (!outputKey) return;
const controlBock = buffer_1.Buffer.concat(
Expand Down Expand Up @@ -200,7 +208,7 @@ function p2tr(a, opts) {
throw new TypeError('Invalid pubkey for p2tr');
}
if (a.hash && a.scriptTree) {
const hash = (0, taprootutils_1.toHashTree)(a.scriptTree).hash;
const hash = _hashTree().hash;
if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch');
}
const witness = _witness();
Expand Down Expand Up @@ -253,9 +261,12 @@ function p2tr(a, opts) {
throw new TypeError('Internal pubkey mismatch');
if (!_ecc().isXOnlyPoint(internalPubkey))
throw new TypeError('Invalid internalPubkey for p2tr witness');
const leafVersion = controlBlock[0] & LEAF_VERSION_MASK;
const leafVersion = controlBlock[0] & types_1.TAPLEAF_VERSION_MASK;
const script = witness[witness.length - 2];
const leafHash = (0, taprootutils_1.tapLeafHash)(script, leafVersion);
const leafHash = (0, taprootutils_1.tapleafHash)({
output: script,
version: leafVersion,
});
const hash = (0, taprootutils_1.rootHashFromPath)(
controlBlock,
leafHash,
Expand Down
25 changes: 13 additions & 12 deletions src/payments/taprootutils.d.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
/// <reference types="node" />
import { Taptree } from '../types';
import { Tapleaf, Taptree } from '../types';
export declare const LEAF_VERSION_TAPSCRIPT = 192;
export declare function rootHashFromPath(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer;
export interface HashTree {
export declare function rootHashFromPath(controlBlock: Buffer, leafHash: Buffer): Buffer;
interface HashLeaf {
hash: Buffer;
left?: HashTree;
right?: HashTree;
}
interface HashBranch {
hash: Buffer;
left: HashTree;
right: HashTree;
}
export declare type HashTree = HashLeaf | HashBranch;
/**
* Build the hash tree from the scripts binary tree.
* The binary tree can be balanced or not.
Expand All @@ -16,16 +20,13 @@ export interface HashTree {
* - one taproot leaf and a list of elements
*/
export declare function toHashTree(scriptTree: Taptree): HashTree;
/**
* Check if the tree is a binary tree with leafs of type Tapleaf
*/
export declare function isTapTree(scriptTree: Taptree): boolean;
/**
* Given a MAST tree, it finds the path of a particular hash.
* @param node - the root of the tree
* @param hash - the hash to search for
* @returns - and array of hashes representing the path, or an empty array if no pat is found
* @returns - and array of hashes representing the path, undefined if no path is found
*/
export declare function findScriptPath(node: HashTree, hash: Buffer): Buffer[];
export declare function tapLeafHash(script: Buffer, version?: number): Buffer;
export declare function findScriptPath(node: HashTree, hash: Buffer): Buffer[] | undefined;
export declare function tapleafHash(leaf: Tapleaf): Buffer;
export declare function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer;
export {};
101 changes: 34 additions & 67 deletions src/payments/taprootutils.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
exports.tapTweakHash = exports.tapLeafHash = exports.findScriptPath = exports.isTapTree = exports.toHashTree = exports.rootHashFromPath = exports.LEAF_VERSION_TAPSCRIPT = void 0;
exports.tapTweakHash = exports.tapleafHash = exports.findScriptPath = exports.toHashTree = exports.rootHashFromPath = exports.LEAF_VERSION_TAPSCRIPT = void 0;
const buffer_1 = require('buffer');
const bcrypto = require('../crypto');
const bufferutils_1 = require('../bufferutils');
const TAP_LEAF_TAG = 'TapLeaf';
const TAP_BRANCH_TAG = 'TapBranch';
const TAP_TWEAK_TAG = 'TapTweak';
const types_1 = require('../types');
exports.LEAF_VERSION_TAPSCRIPT = 0xc0;
function rootHashFromPath(controlBlock, tapLeafMsg) {
const k = [tapLeafMsg];
const e = [];
function rootHashFromPath(controlBlock, leafHash) {
const m = (controlBlock.length - 33) / 32;
let kj = leafHash;
for (let j = 0; j < m; j++) {
e[j] = controlBlock.slice(33 + 32 * j, 65 + 32 * j);
if (k[j].compare(e[j]) < 0) {
k[j + 1] = tapBranchHash(k[j], e[j]);
const ej = controlBlock.slice(33 + 32 * j, 65 + 32 * j);
if (kj.compare(ej) < 0) {
kj = tapBranchHash(kj, ej);
} else {
k[j + 1] = tapBranchHash(e[j], k[j]);
kj = tapBranchHash(ej, kj);
}
}
return k[m];
return kj;
}
exports.rootHashFromPath = rootHashFromPath;
const isHashBranch = ht => 'left' in ht && 'right' in ht;
/**
* Build the hash tree from the scripts binary tree.
* The binary tree can be balanced or not.
Expand All @@ -32,90 +30,59 @@ exports.rootHashFromPath = rootHashFromPath;
* - one taproot leaf and a list of elements
*/
function toHashTree(scriptTree) {
if (scriptTree.length === 1) {
const script = scriptTree[0];
if (Array.isArray(script)) {
return toHashTree(script);
}
script.version = script.version || exports.LEAF_VERSION_TAPSCRIPT;
if ((script.version & 1) !== 0)
throw new TypeError('Invalid script version');
return {
hash: tapLeafHash(script.output, script.version),
};
}
let left = toHashTree([scriptTree[0]]);
let right = toHashTree([scriptTree[1]]);
if (left.hash.compare(right.hash) === 1) [left, right] = [right, left];
if ((0, types_1.isTapleaf)(scriptTree))
return { hash: tapleafHash(scriptTree) };
const hashes = [toHashTree(scriptTree[0]), toHashTree(scriptTree[1])];
hashes.sort((a, b) => a.hash.compare(b.hash));
const [left, right] = hashes;
return {
hash: tapBranchHash(left.hash, right.hash),
left,
right,
};
}
exports.toHashTree = toHashTree;
/**
* Check if the tree is a binary tree with leafs of type Tapleaf
*/
function isTapTree(scriptTree) {
if (scriptTree.length > 2) return false;
if (scriptTree.length === 1) {
const script = scriptTree[0];
if (Array.isArray(script)) {
return isTapTree(script);
}
if (!script.output) return false;
script.version = script.version || exports.LEAF_VERSION_TAPSCRIPT;
if ((script.version & 1) !== 0) return false;
return true;
}
if (!isTapTree([scriptTree[0]])) return false;
if (!isTapTree([scriptTree[1]])) return false;
return true;
}
exports.isTapTree = isTapTree;
/**
* Given a MAST tree, it finds the path of a particular hash.
* @param node - the root of the tree
* @param hash - the hash to search for
* @returns - and array of hashes representing the path, or an empty array if no pat is found
* @returns - and array of hashes representing the path, undefined if no path is found
*/
function findScriptPath(node, hash) {
if (node.left) {
if (node.left.hash.equals(hash)) return node.right ? [node.right.hash] : [];
const leftPath = findScriptPath(node.left, hash);
if (leftPath.length)
return node.right ? [node.right.hash].concat(leftPath) : leftPath;
}
if (node.right) {
if (node.right.hash.equals(hash)) return node.left ? [node.left.hash] : [];
const rightPath = findScriptPath(node.right, hash);
if (rightPath.length)
return node.left ? [node.left.hash].concat(rightPath) : rightPath;
if (!isHashBranch(node)) {
if (node.hash.equals(hash)) {
return [];
} else {
return undefined;
}
}
return [];
const leftPath = findScriptPath(node.left, hash);
if (leftPath !== undefined) return [node.right.hash, ...leftPath];
const rightPath = findScriptPath(node.right, hash);
if (rightPath !== undefined) return [node.left.hash, ...rightPath];
return undefined;
}
exports.findScriptPath = findScriptPath;
function tapLeafHash(script, version) {
version = version || exports.LEAF_VERSION_TAPSCRIPT;
function tapleafHash(leaf) {
const version = leaf.version || exports.LEAF_VERSION_TAPSCRIPT;
return bcrypto.taggedHash(
TAP_LEAF_TAG,
'TapLeaf',
buffer_1.Buffer.concat([
buffer_1.Buffer.from([version]),
serializeScript(script),
serializeScript(leaf.output),
]),
);
}
exports.tapLeafHash = tapLeafHash;
exports.tapleafHash = tapleafHash;
function tapTweakHash(pubKey, h) {
return bcrypto.taggedHash(
TAP_TWEAK_TAG,
'TapTweak',
buffer_1.Buffer.concat(h ? [pubKey, h] : [pubKey]),
);
}
exports.tapTweakHash = tapTweakHash;
function tapBranchHash(a, b) {
return bcrypto.taggedHash(TAP_BRANCH_TAG, buffer_1.Buffer.concat([a, b]));
return bcrypto.taggedHash('TapBranch', buffer_1.Buffer.concat([a, b]));
}
function serializeScript(s) {
const varintLen = bufferutils_1.varuint.encodingLength(s.length);
Expand Down
2 changes: 1 addition & 1 deletion src/psbt.js
Original file line number Diff line number Diff line change
Expand Up @@ -1079,7 +1079,7 @@ function getHashForSig(
const signingScripts = prevOuts.map(o => o.script);
const values = prevOuts.map(o => o.value);
const leafHash = input.witnessScript
? (0, taprootutils_1.tapLeafHash)(input.witnessScript)
? (0, taprootutils_1.tapleafHash)({ output: input.witnessScript })
: undefined;
hash = unsignedTx.hashForWitnessV1(
inputIndex,
Expand Down
5 changes: 4 additions & 1 deletion src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ export interface Tapleaf {
output: Buffer;
version?: number;
}
export declare type Taptree = Array<[Tapleaf, Tapleaf] | Tapleaf>;
export declare const TAPLEAF_VERSION_MASK = 254;
export declare function isTapleaf(o: any): o is Tapleaf;
export declare type Taptree = [Taptree | Tapleaf, Taptree | Tapleaf] | Tapleaf;
export declare function isTaptree(scriptTree: any): scriptTree is Taptree;
export interface TinySecp256k1Interface {
isXOnlyPoint(p: Uint8Array): boolean;
xOnlyPointAddTweak(p: Uint8Array, tweak: Uint8Array): XOnlyPointAddTweakResult | null;
Expand Down
17 changes: 16 additions & 1 deletion src/types.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
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;
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;
const buffer_1 = require('buffer');
exports.typeforce = require('typeforce');
const ZERO32 = buffer_1.Buffer.alloc(32, 0);
Expand Down Expand Up @@ -68,6 +68,21 @@ exports.Network = exports.typeforce.compile({
scriptHash: exports.typeforce.UInt8,
wif: exports.typeforce.UInt8,
});
exports.TAPLEAF_VERSION_MASK = 0xfe;
function isTapleaf(o) {
if (!('output' in o)) return false;
if (!buffer_1.Buffer.isBuffer(o.output)) return false;
if (o.version !== undefined)
return (o.version & exports.TAPLEAF_VERSION_MASK) === o.version;
return true;
}
exports.isTapleaf = isTapleaf;
function isTaptree(scriptTree) {
if (!(0, exports.Array)(scriptTree)) return isTapleaf(scriptTree);
if (scriptTree.length !== 2) return false;
return scriptTree.every(t => isTaptree(t));
}
exports.isTaptree = isTaptree;
exports.Buffer256bit = exports.typeforce.BufferN(32);
exports.Hash160bit = exports.typeforce.BufferN(20);
exports.Hash256bit = exports.typeforce.BufferN(32);
Expand Down
Loading