Skip to content

Misc. improvements and cleanups #4

New issue

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

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

Already on GitHub? # 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