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
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