Skip to content

Misc. improvements and cleanups #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Mar 25, 2022
59 changes: 39 additions & 20 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,21 +149,21 @@ 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(
[
buffer_1.Buffer.from([o.redeemVersion | outputKey.parity]),
a.internalPubkey,
].concat(path.reverse()),
].concat(path),
);
return [a.redeem.output, controlBock];
}
Expand Down Expand Up @@ -199,9 +207,17 @@ function p2tr(a, opts) {
if (!_ecc().isXOnlyPoint(pubkey))
throw new TypeError('Invalid pubkey for p2tr');
}
if (a.hash && a.scriptTree) {
const hash = (0, taprootutils_1.toHashTree)(a.scriptTree).hash;
if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch');
const hashTree = _hashTree();
if (a.hash && hashTree) {
if (!a.hash.equals(hashTree.hash)) throw new TypeError('Hash mismatch');
}
if (a.redeem && a.redeem.output && hashTree) {
const leafHash = (0, taprootutils_1.tapleafHash)({
output: a.redeem.output,
version: o.redeemVersion,
});
if (!(0, taprootutils_1.findScriptPath)(hashTree, leafHash))
throw new TypeError('Redeem script not in tree');
}
const witness = _witness();
// compare the provided redeem data with the one computed from witness
Expand Down Expand Up @@ -253,9 +269,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
41 changes: 23 additions & 18 deletions src/payments/taprootutils.d.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
/// <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;
}
/**
* Build the hash tree from the scripts binary tree.
* The binary tree can be balanced or not.
* @param scriptTree - is a list representing a binary tree where an element can be:
* - a taproot leaf [(output, version)], or
* - a pair of two taproot leafs [(output, version), (output, version)], or
* - one taproot leaf and a list of elements
* Binary tree representing leaf, branch, and root node hashes of a Taptree.
* Each node contains a hash, and potentially left and right branch hashes.
* This tree is used for 2 purposes: Providing the root hash for tweaking,
* and calculating merkle inclusion proofs when constructing a control block.
*/
export declare function toHashTree(scriptTree: Taptree): HashTree;
export declare type HashTree = HashLeaf | HashBranch;
/**
* Check if the tree is a binary tree with leafs of type Tapleaf
* Build a hash tree of merkle nodes from the scripts binary tree.
* @param scriptTree - the tree of scripts to pairwise hash.
*/
export declare function isTapTree(scriptTree: Taptree): boolean;
export declare function toHashTree(scriptTree: Taptree): HashTree;
/**
* Given a MAST tree, it finds the path of a particular hash.
* Given a HashTree, finds the path from a particular hash to the root.
* @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 - array of sibling hashes, from leaf (inclusive) to root
* (exclusive) needed to prove inclusion of the specified hash. 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 {};
106 changes: 34 additions & 72 deletions src/payments/taprootutils.js
Original file line number Diff line number Diff line change
@@ -1,52 +1,36 @@
'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.
* @param scriptTree - is a list representing a binary tree where an element can be:
* - a taproot leaf [(output, version)], or
* - a pair of two taproot leafs [(output, version), (output, version)], or
* - one taproot leaf and a list of elements
* Build a hash tree of merkle nodes from the scripts binary tree.
* @param scriptTree - the tree of scripts to pairwise hash.
*/
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,
Expand All @@ -55,67 +39,45 @@ function toHashTree(scriptTree) {
}
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.
* Given a HashTree, finds the path from a particular hash to the root.
* @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 - array of sibling hashes, from leaf (inclusive) to root
* (exclusive) needed to prove inclusion of the specified hash. 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] : [];
if (isHashBranch(node)) {
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] : [];
if (leftPath !== undefined) return [...leftPath, node.right.hash];
const rightPath = findScriptPath(node.right, hash);
if (rightPath.length)
return node.left ? [node.left.hash].concat(rightPath) : rightPath;
if (rightPath !== undefined) return [...rightPath, node.left.hash];
} else if (node.hash.equals(hash)) {
return [];
}
return [];
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
10 changes: 9 additions & 1 deletion src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@ 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;
/**
* Binary tree repsenting script path spends for a Taproot input.
* Each node is either a single Tapleaf, or a pair of Tapleaf | Taptree.
* The tree has no balancing requirements.
*/
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