diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index 13f283ed8..c24c16914 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -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 && @@ -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), @@ -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', () => { @@ -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; @@ -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', () => { @@ -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]; } @@ -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 @@ -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, diff --git a/src/payments/taprootutils.d.ts b/src/payments/taprootutils.d.ts index 2bb998c84..a5739c44f 100644 --- a/src/payments/taprootutils.d.ts +++ b/src/payments/taprootutils.d.ts @@ -1,31 +1,36 @@ /// -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 {}; diff --git a/src/payments/taprootutils.js b/src/payments/taprootutils.js index d9221fc33..85576960b 100644 --- a/src/payments/taprootutils.js +++ b/src/payments/taprootutils.js @@ -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, @@ -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); diff --git a/src/psbt.js b/src/psbt.js index 6747af981..694a6cc1b 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -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, diff --git a/src/types.d.ts b/src/types.d.ts index 9b62b4933..b3d93589d 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -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; diff --git a/src/types.js b/src/types.js index a6d1efa16..e1d0a528d 100644 --- a/src/types.js +++ b/src/types.js @@ -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); @@ -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); diff --git a/test/fixtures/p2tr.json b/test/fixtures/p2tr.json index 9bcf1f7c4..aaa82fbb4 100644 --- a/test/fixtures/p2tr.json +++ b/test/fixtures/p2tr.json @@ -161,11 +161,9 @@ "description": "address, pubkey, output and hash from internalPubkey and a script tree with one leaf", "arguments": { "internalPubkey": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", - "scriptTree": [ - { - "output": "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c18 OP_CHECKSIG" - } - ] + "scriptTree": { + "output": "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c18 OP_CHECKSIG" + } }, "expected": { "name": "p2tr", @@ -314,6 +312,28 @@ "witness": null } }, + { + "description": "address, pubkey, and output from internalPubkey redeem, and hash (one leaf, no tree)", + "arguments": { + "internalPubkey": "aba457d16a8d59151c387f24d1eb887efbe24644c1ee64b261282e7baebdb247", + "redeem": { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac4 OP_CHECKSIG" + }, + "hash": "b424dea09f840b932a00373cdcdbd25650b8c3acfe54a9f4a641a286721b8d26" + }, + "expected": { + "name": "p2tr", + "address": "bc1pnxyp0ahcg53jzgrzj57hnlgdtqtzn7qqhmgjgczk8hzhcltq974qazepzf", + "pubkey": "998817f6f84523212062953d79fd0d581629f800bed12460563dc57c7d602faa", + "output": "OP_1 998817f6f84523212062953d79fd0d581629f800bed12460563dc57c7d602faa", + "signature": null, + "input": null, + "witness": [ + "2050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac4ac", + "c0aba457d16a8d59151c387f24d1eb887efbe24644c1ee64b261282e7baebdb247" + ] + } + }, { "description": "address, pubkey, output and hash from internalPubkey and a script tree with seven leafs (2)", "arguments": { @@ -365,8 +385,8 @@ "input": null, "witness": [ "2050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac4ac", - "c0aba457d16a8d59151c387f24d1eb887efbe24644c1ee64b261282e7baebdb247dac795766bbda1eaeaa45e5bfa0a950fdd5f4c4aada5b1f3082edc9689b9fd0a315fb34a7a93dcaed5e26cf7468be5bd377dda7a4d29128f7dd98db6da9bf04325fff3aa86365bac7534dcb6495867109941ec444dd35294e0706e29e051066d73e0d427bd3249bb921fa78c04fb76511f583ff48c97210d17c2d9dcfbb95023" - ] + "c0aba457d16a8d59151c387f24d1eb887efbe24644c1ee64b261282e7baebdb247dac795766bbda1eaeaa45e5bfa0a950fdd5f4c4aada5b1f3082edc9689b9fd0a315fb34a7a93dcaed5e26cf7468be5bd377dda7a4d29128f7dd98db6da9bf04325fff3aa86365bac7534dcb6495867109941ec444dd35294e0706e29e051066d73e0d427bd3249bb921fa78c04fb76511f583ff48c97210d17c2d9dcfbb95023" + ] } }, { @@ -393,12 +413,10 @@ "output": "d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8 OP_CHECKSIG", "redeemVersion": 192 }, - "scriptTree": [ - { - "output": "d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8 OP_CHECKSIG", - "version": 192 - } - ] + "scriptTree": { + "output": "d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8 OP_CHECKSIG", + "version": 192 + } }, "options": {}, "expected": { @@ -427,12 +445,10 @@ "output": "b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007 OP_CHECKSIG", "redeemVersion": 192 }, - "scriptTree": [ - { - "output": "b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007 OP_CHECKSIG", - "version": 192 - } - ] + "scriptTree": { + "output": "b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007 OP_CHECKSIG", + "version": 192 + } }, "options": {}, "expected": { @@ -906,11 +922,9 @@ "options": {}, "arguments": { "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7", - "scriptTree": [ - { - "output": "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c18 OP_CHECKSIG" - } - ], + "scriptTree": { + "output": "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c18 OP_CHECKSIG" + }, "hash": "b76077013c8e303085e300000000000000000000000000000000000000000000" } }, @@ -1037,7 +1051,7 @@ }, { "description": "Script Tree is not a binary tree (has tree leafs)", - "exception": "property \"scriptTree\" of type \\?isTapTree, got Array", + "exception": "property \"scriptTree\" of type \\?isTaptree, got Array", "options": {}, "arguments": { "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7", @@ -1066,7 +1080,7 @@ }, { "description": "Script Tree is not a TapTree tree (leaf has no script)", - "exception": "property \"scriptTree\" of type \\?isTapTree, got Array", + "exception": "property \"scriptTree\" of type \\?isTaptree, got Array", "options": {}, "arguments": { "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7", @@ -1161,10 +1175,24 @@ ] } } + }, + { + "description": "Redeem script not in tree", + "exception": "Redeem script not in tree", + "options": {}, + "arguments": { + "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7", + "scriptTree": { + "output": "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c18 OP_CHECKSIG" + }, + "redeem": { + "output": "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c19 OP_CHECKSIG" + } + } } ], "dynamic": { "depends": {}, "details": [] } -} \ No newline at end of file +} diff --git a/test/integration/taproot.spec.ts b/test/integration/taproot.spec.ts index 90dacb63d..05d7d154d 100644 --- a/test/integration/taproot.spec.ts +++ b/test/integration/taproot.spec.ts @@ -4,6 +4,7 @@ import * as ecc from 'tiny-secp256k1'; import { describe, it } from 'mocha'; import { regtestUtils } from './_regtest'; import * as bitcoin from '../..'; +import { Taptree } from '../../src/types'; import { buildTapscriptFinalizer, toXOnly } from '../psbt.utils'; const rng = require('randombytes'); @@ -97,11 +98,9 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { )} OP_CHECKSIG`; const leafScript = bitcoin.script.fromASM(leafScriptAsm); - const scriptTree = [ - { - output: leafScript, - }, - ]; + const scriptTree = { + output: leafScript, + }; const { output, address, hash } = bitcoin.payments.p2tr( { @@ -157,7 +156,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { )} OP_CHECKSIG`; const leafScript = bitcoin.script.fromASM(leafScriptAsm); - const scriptTree: any[] = [ + const scriptTree: Taptree = [ [ { output: bitcoin.script.fromASM( @@ -262,7 +261,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { const leafScriptAsm = `OP_10 OP_CHECKSEQUENCEVERIFY OP_DROP ${leafPubkey} OP_CHECKSIG`; const leafScript = bitcoin.script.fromASM(leafScriptAsm); - const scriptTree: any[] = [ + const scriptTree: Taptree = [ { output: bitcoin.script.fromASM( '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG', @@ -361,7 +360,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { const leafScript = bitcoin.script.fromASM(leafScriptAsm); - const scriptTree: any[] = [ + const scriptTree: Taptree = [ { output: bitcoin.script.fromASM( '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG', diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index 2c7d8557a..47a76a114 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -1,14 +1,18 @@ import { Buffer as NBuffer } from 'buffer'; import { bitcoin as BITCOIN_NETWORK } from '../networks'; import * as bscript from '../script'; -import { typeforce as typef, TinySecp256k1Interface } from '../types'; +import { + typeforce as typef, + isTaptree, + TinySecp256k1Interface, + TAPLEAF_VERSION_MASK, +} from '../types'; import { toHashTree, rootHashFromPath, findScriptPath, - tapLeafHash, + tapleafHash, tapTweakHash, - isTapTree, LEAF_VERSION_TAPSCRIPT, } from './taprootutils'; import { Payment, PaymentOpts } from './index'; @@ -19,7 +23,6 @@ import { verifyEcc } from './verifyecc'; const OPS = bscript.OPS; const TAPROOT_WITNESS_VERSION = 0x01; const ANNEX_PREFIX = 0x50; -const LEAF_VERSION_MASK = 0b11111110; export function p2tr(a: Payment, opts?: PaymentOpts): Payment { if ( @@ -51,7 +54,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { pubkey: typef.maybe(typef.BufferN(32)), // tweaked with `hash` from `internalPubkey` signature: typef.maybe(typef.BufferN(64)), witness: typef.maybe(typef.arrayOf(typef.Buffer)), - scriptTree: typef.maybe(isTapTree), + scriptTree: typef.maybe(isTaptree), redeem: typef.maybe({ output: typef.maybe(typef.Buffer), // tapleaf script redeemVersion: typef.maybe(typef.Number), // tapleaf version @@ -85,6 +88,12 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { return a.witness.slice(); }); + const _hashTree = lazy.value(() => { + if (a.scriptTree) return toHashTree(a.scriptTree); + if (a.hash) return { hash: a.hash }; + return; + }); + const network = a.network || BITCOIN_NETWORK; const o: Payment = { name: 'p2tr', network }; @@ -97,14 +106,14 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { }); lazy.prop(o, 'hash', () => { - if (a.hash) return a.hash; - if (a.scriptTree) return 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] & TAPLEAF_VERSION_MASK; const script = w[w.length - 2]; - const leafHash = tapLeafHash(script, leafVersion); + const leafHash = tapleafHash({ output: script, version: leafVersion }); return rootHashFromPath(controlBlock, leafHash); } return null; @@ -132,7 +141,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { 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] & TAPLEAF_VERSION_MASK, }; }); lazy.prop(o, 'pubkey', () => { @@ -158,18 +167,21 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { 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 = toHashTree(a.scriptTree); - const leafHash = tapLeafHash(a.redeem.output, o.redeemVersion); + const hashTree = _hashTree(); + if (hashTree && a.redeem && a.redeem.output && a.internalPubkey) { + const leafHash = tapleafHash({ + output: a.redeem.output, + version: o.redeemVersion, + }); const path = findScriptPath(hashTree, leafHash); + if (!path) return; const outputKey = tweakKey(a.internalPubkey, hashTree.hash, _ecc()); if (!outputKey) return; const controlBock = NBuffer.concat( [ NBuffer.from([o.redeemVersion! | outputKey.parity]), a.internalPubkey, - ].concat(path.reverse()), + ].concat(path), ); return [a.redeem.output, controlBock]; } @@ -219,9 +231,19 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { throw new TypeError('Invalid pubkey for p2tr'); } - if (a.hash && a.scriptTree) { - const hash = 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 = tapleafHash({ + output: a.redeem.output, + version: o.redeemVersion, + }); + if (!findScriptPath(hashTree, leafHash)) + throw new TypeError('Redeem script not in tree'); } const witness = _witness(); @@ -283,10 +305,10 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { if (!_ecc().isXOnlyPoint(internalPubkey)) throw new TypeError('Invalid internalPubkey for p2tr witness'); - const leafVersion = controlBlock[0] & LEAF_VERSION_MASK; + const leafVersion = controlBlock[0] & TAPLEAF_VERSION_MASK; const script = witness[witness.length - 2]; - const leafHash = tapLeafHash(script, leafVersion); + const leafHash = tapleafHash({ output: script, version: leafVersion }); const hash = rootHashFromPath(controlBlock, leafHash); const outputKey = tweakKey(internalPubkey, hash, _ecc()); diff --git a/ts_src/payments/taprootutils.ts b/ts_src/payments/taprootutils.ts index cfa7a6dd2..97cc1f6d8 100644 --- a/ts_src/payments/taprootutils.ts +++ b/ts_src/payments/taprootutils.ts @@ -2,138 +2,110 @@ import { Buffer as NBuffer } from 'buffer'; import * as bcrypto from '../crypto'; import { varuint } from '../bufferutils'; -import { Taptree } from '../types'; - -const TAP_LEAF_TAG = 'TapLeaf'; -const TAP_BRANCH_TAG = 'TapBranch'; -const TAP_TWEAK_TAG = 'TapTweak'; +import { Tapleaf, Taptree, isTapleaf } from '../types'; export const LEAF_VERSION_TAPSCRIPT = 0xc0; export function rootHashFromPath( controlBlock: Buffer, - tapLeafMsg: Buffer, + leafHash: Buffer, ): Buffer { - const k = [tapLeafMsg]; - const e = []; - 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; +} + +interface HashLeaf { + hash: Buffer; } -export interface HashTree { +interface HashBranch { hash: Buffer; - left?: HashTree; - right?: HashTree; + left: HashTree; + right: HashTree; } +const isHashBranch = (ht: HashTree): ht is HashBranch => + '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 + * 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 function toHashTree(scriptTree: Taptree): HashTree { - if (scriptTree.length === 1) { - const script = scriptTree[0]; - if (Array.isArray(script)) { - return toHashTree(script); - } - script.version = script.version || LEAF_VERSION_TAPSCRIPT; - if ((script.version & 1) !== 0) - throw new TypeError('Invalid script version'); +export type HashTree = HashLeaf | HashBranch; - return { - hash: tapLeafHash(script.output, script.version), - }; - } +/** + * Build a hash tree of merkle nodes from the scripts binary tree. + * @param scriptTree - the tree of scripts to pairwise hash. + */ +export function toHashTree(scriptTree: Taptree): HashTree { + if (isTapleaf(scriptTree)) return { hash: tapleafHash(scriptTree) }; - let left = toHashTree([scriptTree[0]]); - let right = toHashTree([scriptTree[1]]); + const hashes = [toHashTree(scriptTree[0]), toHashTree(scriptTree[1])]; + hashes.sort((a, b) => a.hash.compare(b.hash)); + const [left, right] = hashes; - if (left.hash.compare(right.hash) === 1) [left, right] = [right, left]; return { hash: tapBranchHash(left.hash, right.hash), left, right, }; } -/** - * Check if the tree is a binary tree with leafs of type Tapleaf - */ -export function isTapTree(scriptTree: Taptree): boolean { - 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 || 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; -} /** - * 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 function findScriptPath(node: HashTree, hash: Buffer): Buffer[] { - if (node.left) { - if (node.left.hash.equals(hash)) return node.right ? [node.right.hash] : []; +export function findScriptPath( + node: HashTree, + hash: Buffer, +): Buffer[] | undefined { + if (isHashBranch(node)) { const leftPath = findScriptPath(node.left, hash); - if (leftPath.length) - return node.right ? [node.right.hash].concat(leftPath) : leftPath; - } + if (leftPath !== undefined) return [...leftPath, node.right.hash]; - 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 (rightPath !== undefined) return [...rightPath, node.left.hash]; + } else if (node.hash.equals(hash)) { + return []; } - return []; + return undefined; } -export function tapLeafHash(script: Buffer, version?: number): Buffer { - version = version || LEAF_VERSION_TAPSCRIPT; +export function tapleafHash(leaf: Tapleaf): Buffer { + const version = leaf.version || LEAF_VERSION_TAPSCRIPT; return bcrypto.taggedHash( - TAP_LEAF_TAG, - NBuffer.concat([NBuffer.from([version]), serializeScript(script)]), + 'TapLeaf', + NBuffer.concat([NBuffer.from([version]), serializeScript(leaf.output)]), ); } export function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer { return bcrypto.taggedHash( - TAP_TWEAK_TAG, + 'TapTweak', NBuffer.concat(h ? [pubKey, h] : [pubKey]), ); } function tapBranchHash(a: Buffer, b: Buffer): Buffer { - return bcrypto.taggedHash(TAP_BRANCH_TAG, NBuffer.concat([a, b])); + return bcrypto.taggedHash('TapBranch', NBuffer.concat([a, b])); } function serializeScript(s: Buffer): Buffer { diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index f135173bd..479421636 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -21,7 +21,7 @@ import { bitcoin as btcNetwork, Network } from './networks'; import * as payments from './payments'; import * as bscript from './script'; import { Output, Transaction } from './transaction'; -import { tapLeafHash } from './payments/taprootutils'; +import { tapleafHash } from './payments/taprootutils'; import { TinySecp256k1Interface } from './types'; export interface TransactionInput { @@ -1406,7 +1406,7 @@ function getHashForSig( const signingScripts: any = prevOuts.map(o => o.script); const values: any = prevOuts.map(o => o.value); const leafHash = input.witnessScript - ? tapLeafHash(input.witnessScript) + ? tapleafHash({ output: input.witnessScript }) : undefined; hash = unsignedTx.hashForWitnessV1( diff --git a/ts_src/types.ts b/ts_src/types.ts index 59e4e1929..536646e86 100644 --- a/ts_src/types.ts +++ b/ts_src/types.ts @@ -77,7 +77,27 @@ export interface Tapleaf { version?: number; } -export type Taptree = Array<[Tapleaf, Tapleaf] | Tapleaf>; +export const TAPLEAF_VERSION_MASK = 0xfe; +export function isTapleaf(o: any): o is Tapleaf { + if (!('output' in o)) return false; + if (!NBuffer.isBuffer(o.output)) return false; + if (o.version !== undefined) + return (o.version & TAPLEAF_VERSION_MASK) === o.version; + return true; +} + +/** + * 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 type Taptree = [Taptree | Tapleaf, Taptree | Tapleaf] | Tapleaf; + +export function isTaptree(scriptTree: any): scriptTree is Taptree { + if (!Array(scriptTree)) return isTapleaf(scriptTree); + if (scriptTree.length !== 2) return false; + return scriptTree.every((t: any) => isTaptree(t)); +} export interface TinySecp256k1Interface { isXOnlyPoint(p: Uint8Array): boolean;