diff --git a/src/address.d.ts b/src/address.d.ts index 13922dab3..be0e00a61 100644 --- a/src/address.d.ts +++ b/src/address.d.ts @@ -1,6 +1,5 @@ /// import { Network } from './networks'; -import { TinySecp256k1Interface } from './types'; export interface Base58CheckResult { hash: Buffer; version: number; @@ -14,5 +13,5 @@ export declare function fromBase58Check(address: string): Base58CheckResult; export declare function fromBech32(address: string): Bech32Result; export declare function toBase58Check(hash: Buffer, version: number): string; export declare function toBech32(data: Buffer, version: number, prefix: string): string; -export declare function fromOutputScript(output: Buffer, network?: Network, eccLib?: TinySecp256k1Interface): string; -export declare function toOutputScript(address: string, network?: Network, eccLib?: TinySecp256k1Interface): Buffer; +export declare function fromOutputScript(output: Buffer, network?: Network): string; +export declare function toOutputScript(address: string, network?: Network): Buffer; diff --git a/src/address.js b/src/address.js index 2c7bc4857..de0154a3a 100644 --- a/src/address.js +++ b/src/address.js @@ -86,7 +86,7 @@ function toBech32(data, version, prefix) { : bech32_1.bech32m.encode(prefix, words); } exports.toBech32 = toBech32; -function fromOutputScript(output, network, eccLib) { +function fromOutputScript(output, network) { // TODO: Network network = network || networks.bitcoin; try { @@ -102,7 +102,7 @@ function fromOutputScript(output, network, eccLib) { return payments.p2wsh({ output, network }).address; } catch (e) {} try { - if (eccLib) return payments.p2tr({ output, network }, { eccLib }).address; + return payments.p2tr({ output, network }).address; } catch (e) {} try { return _toFutureSegwitAddress(output, network); @@ -110,7 +110,7 @@ function fromOutputScript(output, network, eccLib) { throw new Error(bscript.toASM(output) + ' has no matching Address'); } exports.fromOutputScript = fromOutputScript; -function toOutputScript(address, network, eccLib) { +function toOutputScript(address, network) { network = network || networks.bitcoin; let decodeBase58; let decodeBech32; @@ -135,9 +135,8 @@ function toOutputScript(address, network, eccLib) { if (decodeBech32.data.length === 32) return payments.p2wsh({ hash: decodeBech32.data }).output; } else if (decodeBech32.version === 1) { - if (decodeBech32.data.length === 32 && eccLib) - return payments.p2tr({ pubkey: decodeBech32.data }, { eccLib }) - .output; + if (decodeBech32.data.length === 32) + return payments.p2tr({ pubkey: decodeBech32.data }).output; } else if ( decodeBech32.version >= FUTURE_SEGWIT_MIN_VERSION && decodeBech32.version <= FUTURE_SEGWIT_MAX_VERSION && diff --git a/src/payments/index.d.ts b/src/payments/index.d.ts index 5a71f8cc1..5063f9869 100644 --- a/src/payments/index.d.ts +++ b/src/payments/index.d.ts @@ -1,6 +1,6 @@ /// import { Network } from '../networks'; -import { TinySecp256k1Interface, Taptree } from '../types'; +import { Taptree, XOnlyPointAddTweakResult } from '../types'; import { p2data as embed } from './embed'; import { p2ms } from './p2ms'; import { p2pk } from './p2pk'; @@ -31,10 +31,11 @@ export interface Payment { } export declare type PaymentCreator = (a: Payment, opts?: PaymentOpts) => Payment; export declare type PaymentFunction = () => Payment; +export declare type XOnlyTweakFunction = (p: Buffer, t: Buffer) => XOnlyPointAddTweakResult | null; export interface PaymentOpts { validate?: boolean; allowIncomplete?: boolean; - eccLib?: TinySecp256k1Interface; + tweakFn?: XOnlyTweakFunction; } export declare type StackElement = Buffer | number; export declare type Stack = StackElement[]; diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index 13f283ed8..10a21d453 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -8,11 +8,9 @@ const types_1 = require('../types'); const taprootutils_1 = require('./taprootutils'); const lazy = require('./lazy'); const bech32_1 = require('bech32'); -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 && @@ -23,10 +21,10 @@ function p2tr(a, opts) { ) throw new TypeError('Not enough data'); opts = Object.assign({ validate: true }, opts || {}); - const _ecc = lazy.value(() => { - if (!opts.eccLib) throw new Error('ECC Library is missing for p2tr.'); - (0, verifyecc_1.verifyEcc)(opts.eccLib); - return opts.eccLib; + const _tweakFn = lazy.value(() => { + if (!opts.tweakFn) throw new Error('Tweak function is missing for p2tr.'); + verifyTweakFn(opts.tweakFn); + return opts.tweakFn; }); (0, types_1.typeforce)( { @@ -41,7 +39,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 +72,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 +86,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 +122,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', () => { @@ -124,7 +131,7 @@ function p2tr(a, opts) { if (a.output) return a.output.slice(2); if (a.address) return _address().data; if (o.internalPubkey) { - const tweakedKey = tweakKey(o.internalPubkey, o.hash, _ecc()); + const tweakedKey = tweakKey(o.internalPubkey, o.hash, _tweakFn()); if (tweakedKey) return tweakedKey.x; } }); @@ -141,21 +148,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); - const outputKey = tweakKey(a.internalPubkey, hashTree.hash, _ecc()); + if (!path) return; + const outputKey = tweakKey(a.internalPubkey, hashTree.hash, _tweakFn()); 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]; } @@ -190,18 +197,26 @@ function p2tr(a, opts) { else pubkey = a.output.slice(2); } if (a.internalPubkey) { - const tweakedKey = tweakKey(a.internalPubkey, o.hash, _ecc()); + const tweakedKey = tweakKey(a.internalPubkey, o.hash, _tweakFn()); if (pubkey.length > 0 && !pubkey.equals(tweakedKey.x)) throw new TypeError('Pubkey mismatch'); else pubkey = tweakedKey.x; } if (pubkey && pubkey.length) { - if (!_ecc().isXOnlyPoint(pubkey)) + if (!(0, types_1.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 @@ -251,16 +266,19 @@ function p2tr(a, opts) { const internalPubkey = controlBlock.slice(1, 33); if (a.internalPubkey && !a.internalPubkey.equals(internalPubkey)) throw new TypeError('Internal pubkey mismatch'); - if (!_ecc().isXOnlyPoint(internalPubkey)) + if (!(0, types_1.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, ); - const outputKey = tweakKey(internalPubkey, hash, _ecc()); + const outputKey = tweakKey(internalPubkey, hash, _tweakFn()); if (!outputKey) // todo: needs test data throw new TypeError('Invalid outputKey for p2tr witness'); @@ -274,12 +292,12 @@ function p2tr(a, opts) { return Object.assign(o, a); } exports.p2tr = p2tr; -function tweakKey(pubKey, h, eccLib) { +function tweakKey(pubKey, h, tweakFn) { if (!buffer_1.Buffer.isBuffer(pubKey)) return null; if (pubKey.length !== 32) return null; if (h && h.length !== 32) return null; const tweakHash = (0, taprootutils_1.tapTweakHash)(pubKey, h); - const res = eccLib.xOnlyPointAddTweak(pubKey, tweakHash); + const res = tweakFn(pubKey, tweakHash); if (!res || res.xOnlyPubkey === null) return null; return { parity: res.parity, @@ -292,3 +310,43 @@ function stacksEqual(a, b) { return x.equals(b[i]); }); } +function verifyTweakFn(tweakFn) { + [ + { + pubkey: + '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', + tweak: 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140', + parity: -1, + result: null, + }, + { + pubkey: + '1617d38ed8d8657da4d4761e8057bc396ea9e4b9d29776d4be096016dbd2509b', + tweak: 'a8397a935f0dfceba6ba9618f6451ef4d80637abf4e6af2669fbc9de6a8fd2ac', + parity: 1, + result: + 'e478f99dab91052ab39a33ea35fd5e6e4933f4d28023cd597c9a1f6760346adf', + }, + { + pubkey: + '2c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991', + tweak: '823c3cd2142744b075a87eade7e1b8678ba308d566226a0056ca2b7a76f86b47', + parity: 0, + result: + '9534f8dc8c6deda2dc007655981c78b49c5d96c778fbf363462a11ec9dfd948c', + }, + ].forEach(t => { + const r = tweakFn( + Buffer.from(t.pubkey, 'hex'), + Buffer.from(t.tweak, 'hex'), + ); + if (t.result === null) { + if (r !== null) throw new Error('Expected failed tweak'); + } else { + if (r === null) throw new Error('Expected successful tweak'); + if (r.parity !== t.parity) throw new Error('Tweaked key parity mismatch'); + if (!Buffer.from(r.xOnlyPubkey).equals(Buffer.from(t.result, 'hex'))) + throw new Error('Tweaked key mismatch'); + } + }); +} 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/payments/verifyecc.d.ts b/src/payments/verifyecc.d.ts deleted file mode 100644 index 0f23affa7..000000000 --- a/src/payments/verifyecc.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { TinySecp256k1Interface } from '../types'; -export declare function verifyEcc(ecc: TinySecp256k1Interface): void; diff --git a/src/payments/verifyecc.js b/src/payments/verifyecc.js deleted file mode 100644 index 9a1eebd64..000000000 --- a/src/payments/verifyecc.js +++ /dev/null @@ -1,72 +0,0 @@ -'use strict'; -Object.defineProperty(exports, '__esModule', { value: true }); -exports.verifyEcc = void 0; -const h = hex => Buffer.from(hex, 'hex'); -function verifyEcc(ecc) { - assert(typeof ecc.isXOnlyPoint === 'function'); - assert( - ecc.isXOnlyPoint( - h('79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), - ), - ); - assert( - ecc.isXOnlyPoint( - h('fffffffffffffffffffffffffffffffffffffffffffffffffffffffeeffffc2e'), - ), - ); - assert( - ecc.isXOnlyPoint( - h('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9'), - ), - ); - assert( - ecc.isXOnlyPoint( - h('0000000000000000000000000000000000000000000000000000000000000001'), - ), - ); - assert( - !ecc.isXOnlyPoint( - h('0000000000000000000000000000000000000000000000000000000000000000'), - ), - ); - assert( - !ecc.isXOnlyPoint( - h('fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f'), - ), - ); - assert(typeof ecc.xOnlyPointAddTweak === 'function'); - tweakAddVectors.forEach(t => { - const r = ecc.xOnlyPointAddTweak(h(t.pubkey), h(t.tweak)); - if (t.result === null) { - assert(r === null); - } else { - assert(r !== null); - assert(r.parity === t.parity); - assert(Buffer.from(r.xOnlyPubkey).equals(h(t.result))); - } - }); -} -exports.verifyEcc = verifyEcc; -function assert(bool) { - if (!bool) throw new Error('ecc library invalid'); -} -const tweakAddVectors = [ - { - pubkey: '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', - tweak: 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140', - parity: -1, - result: null, - }, - { - pubkey: '1617d38ed8d8657da4d4761e8057bc396ea9e4b9d29776d4be096016dbd2509b', - tweak: 'a8397a935f0dfceba6ba9618f6451ef4d80637abf4e6af2669fbc9de6a8fd2ac', - parity: 1, - result: 'e478f99dab91052ab39a33ea35fd5e6e4933f4d28023cd597c9a1f6760346adf', - }, - { - pubkey: '2c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991', - tweak: '823c3cd2142744b075a87eade7e1b8678ba308d566226a0056ca2b7a76f86b47', - parity: 0, - result: '9534f8dc8c6deda2dc007655981c78b49c5d96c778fbf363462a11ec9dfd948c', - }, -]; diff --git a/src/psbt.d.ts b/src/psbt.d.ts index 8b21ce7bb..890f9e115 100644 --- a/src/psbt.d.ts +++ b/src/psbt.d.ts @@ -3,7 +3,6 @@ import { Psbt as PsbtBase } from 'bip174'; import { KeyValue, PsbtGlobalUpdate, PsbtInput, PsbtInputUpdate, PsbtOutput, PsbtOutputUpdate } from 'bip174/src/lib/interfaces'; import { Network } from './networks'; import { Transaction } from './transaction'; -import { TinySecp256k1Interface } from './types'; export interface TransactionInput { hash: string | Buffer; index: number; @@ -111,7 +110,6 @@ export declare class Psbt { interface PsbtOptsOptional { network?: Network; maximumFeeRate?: number; - eccLib?: TinySecp256k1Interface; } interface PsbtInputExtended extends PsbtInput, TransactionInput { } @@ -181,8 +179,7 @@ script: Buffer, // The "meaningful" locking script Buffer (redeemScript for P2SH isSegwit: boolean, // Is it segwit? isTapscript: boolean, // Is taproot script path? isP2SH: boolean, // Is it P2SH? -isP2WSH: boolean, // Is it P2WSH? -eccLib?: TinySecp256k1Interface) => { +isP2WSH: boolean) => { finalScriptSig: Buffer | undefined; finalScriptWitness: Buffer | Buffer[] | undefined; }; diff --git a/src/psbt.js b/src/psbt.js index 6747af981..c14086d0e 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -79,7 +79,6 @@ class Psbt { // We will disable exporting the Psbt when unsafe sign is active. // because it is not BIP174 compliant. __UNSAFE_SIGN_NONSEGWIT: false, - __EC_LIB: opts.eccLib, }; if (this.data.inputs.length === 0) this.setVersion(2); // Make data hidden when enumerating @@ -134,7 +133,6 @@ class Psbt { address = (0, address_1.fromOutputScript)( output.script, this.opts.network, - this.__CACHE.__EC_LIB, ); } catch (_) {} return { @@ -237,11 +235,7 @@ class Psbt { const { address } = outputData; if (typeof address === 'string') { const { network } = this.opts; - const script = (0, address_1.toOutputScript)( - address, - network, - this.__CACHE.__EC_LIB, - ); + const script = (0, address_1.toOutputScript)(address, network); outputData = Object.assign(outputData, { script }); } const c = this.__CACHE; @@ -297,7 +291,6 @@ class Psbt { isP2SH, isP2WSH, isTapscript, - this.__CACHE.__EC_LIB, ); if (finalScriptSig) this.data.updateInput(inputIndex, { finalScriptSig }); if (finalScriptWitness) { @@ -326,13 +319,9 @@ class Psbt { input.redeemScript || redeemFromFinalScriptSig(input.finalScriptSig), input.witnessScript || redeemFromFinalWitnessScript(input.finalScriptWitness), - this.__CACHE, ); const type = result.type === 'raw' ? '' : result.type + '-'; - const mainType = classifyScript( - result.meaningfulScript, - this.__CACHE.__EC_LIB, - ); + const mainType = classifyScript(result.meaningfulScript); return type + mainType; } inputHasPubkey(inputIndex, pubkey) { @@ -769,9 +758,9 @@ function isFinalized(input) { return !!input.finalScriptSig || !!input.finalScriptWitness; } function isPaymentFactory(payment) { - return (script, eccLib) => { + return script => { try { - payment({ output: script }, { eccLib }); + payment({ output: script }); return true; } catch (err) { return false; @@ -935,9 +924,8 @@ function getFinalScripts( isP2SH, isP2WSH, isTapscript = false, - eccLib, ) { - const scriptType = classifyScript(script, eccLib); + const scriptType = classifyScript(script); if (isTapscript || !canFinalize(input, script, scriptType)) throw new Error(`Can not finalize input #${inputIndex}`); return prepareFinalScripts( @@ -1053,7 +1041,6 @@ function getHashForSig( 'input', input.redeemScript, input.witnessScript, - cache, ); if (['p2sh-p2wsh', 'p2wsh'].indexOf(type) >= 0) { hash = unsignedTx.hashForWitnessV0( @@ -1072,14 +1059,14 @@ function getHashForSig( prevout.value, sighashType, ); - } else if (isP2TR(prevout.script, cache.__EC_LIB)) { + } else if (isP2TR(prevout.script)) { const prevOuts = inputs.map((i, index) => getScriptAndAmountFromUtxo(index, i, cache), ); 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, @@ -1204,7 +1191,7 @@ function getScriptFromInput(inputIndex, input, cache) { } else { res.script = utxoScript; } - const isTaproot = utxoScript && isP2TR(utxoScript, cache.__EC_LIB); + const isTaproot = utxoScript && isP2TR(utxoScript); // Segregated Witness versions 0 or 1 if (input.witnessScript || isP2WPKH(res.script) || isTaproot) { res.isSegwit = true; @@ -1410,7 +1397,6 @@ function pubkeyInInput(pubkey, input, inputIndex, cache) { 'input', input.redeemScript, input.witnessScript, - cache, ); return pubkeyInScript(pubkey, meaningfulScript); } @@ -1422,7 +1408,6 @@ function pubkeyInOutput(pubkey, output, outputIndex, cache) { 'output', output.redeemScript, output.witnessScript, - cache, ); return pubkeyInScript(pubkey, meaningfulScript); } @@ -1471,12 +1456,11 @@ function getMeaningfulScript( ioType, redeemScript, witnessScript, - cache, ) { const isP2SH = isP2SHScript(script); const isP2SHP2WSH = isP2SH && redeemScript && isP2WSHScript(redeemScript); const isP2WSH = isP2WSHScript(script); - const isP2TRScript = isP2TR(script, cache && cache.__EC_LIB); + const isP2TRScript = isP2TR(script); if (isP2SH && redeemScript === undefined) throw new Error('scriptPubkey is P2SH but redeemScript missing'); if ((isP2WSH || isP2SHP2WSH) && witnessScript === undefined) @@ -1539,12 +1523,12 @@ function isTaprootSpend(scriptType) { !!scriptType && (scriptType === 'taproot' || scriptType.startsWith('p2tr-')) ); } -function classifyScript(script, eccLib) { +function classifyScript(script) { if (isP2WPKH(script)) return 'witnesspubkeyhash'; if (isP2PKH(script)) return 'pubkeyhash'; if (isP2MS(script)) return 'multisig'; if (isP2PK(script)) return 'pubkey'; - if (isP2TR(script, eccLib)) return 'taproot'; + if (isP2TR(script)) return 'taproot'; return 'nonstandard'; } function range(n) { diff --git a/src/types.d.ts b/src/types.d.ts index 9b62b4933..b905d44fa 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,6 +1,7 @@ /// export declare const typeforce: any; export declare function isPoint(p: Buffer | number | undefined | null): boolean; +export declare function isXOnlyPoint(p: Buffer | number | undefined | null): boolean; export declare function UInt31(value: number): boolean; export declare function BIP32Path(value: string): boolean; export declare namespace BIP32Path { @@ -18,9 +19,16 @@ 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; privateAdd(d: Uint8Array, tweak: Uint8Array): Uint8Array | null; privateNegate(d: Uint8Array): Uint8Array; diff --git a/src/types.js b/src/types.js index a6d1efa16..eddb13b06 100644 --- a/src/types.js +++ b/src/types.js @@ -1,30 +1,74 @@ '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.isXOnlyPoint = exports.isPoint = exports.typeforce = void 0; const buffer_1 = require('buffer'); exports.typeforce = require('typeforce'); -const ZERO32 = buffer_1.Buffer.alloc(32, 0); -const EC_P = buffer_1.Buffer.from( - 'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f', - 'hex', +const EC_P = BigInt( + `0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f`, ); +const EC_B = BigInt(7); +// Idea from noble-secp256k1, to be nice to bad JS parsers +const _0n = BigInt(0); +const _1n = BigInt(1); +const _2n = BigInt(2); +const _3n = BigInt(3); +const _5n = BigInt(5); +const _7n = BigInt(7); +function weierstrass(x) { + const x2 = (x * x) % EC_P; + const x3 = (x2 * x) % EC_P; + return (x3 /* + a=0 a*x */ + EC_B) % EC_P; +} +// For prime P, the Jacobi symbol is 1 iff a is a quadratic residue mod P +function jacobiSymbol(a) { + if (a === _0n) return 0; + let p = EC_P; + let sign = 1; + for (;;) { + let and3; + // Handle runs of zeros efficiently w/o flipping sign each time + for (and3 = a & _3n; and3 === _0n; a >>= _2n, and3 = a & _3n); + // If there's one more zero, shift it off and flip the sign + if (and3 === _2n) { + a >>= _1n; + const pand7 = p & _7n; + if (pand7 === _3n || pand7 === _5n) sign = -sign; + } + if (a === _1n) break; + if ((_3n & a) === _3n && (_3n & p) === _3n) sign = -sign; + [a, p] = [p % a, a]; + } + return sign > 0 ? 1 : -1; +} function isPoint(p) { if (!buffer_1.Buffer.isBuffer(p)) return false; if (p.length < 33) return false; const t = p[0]; - const x = p.slice(1, 33); - if (x.compare(ZERO32) === 0) return false; - if (x.compare(EC_P) >= 0) return false; - if ((t === 0x02 || t === 0x03) && p.length === 33) { - return true; + if (p.length === 33) { + return (t === 0x02 || t === 0x03) && isXOnlyPoint(p.slice(1)); } - const y = p.slice(33); - if (y.compare(ZERO32) === 0) return false; - if (y.compare(EC_P) >= 0) return false; - if (t === 0x04 && p.length === 65) return true; - return false; + if (t !== 0x04 || p.length !== 65) return false; + const x = BigInt(`0x${p.slice(1, 33).toString('hex')}`); + if (x === _0n) return false; + if (x >= EC_P) return false; + const y = BigInt(`0x${p.slice(33).toString('hex')}`); + if (y === _0n) return false; + if (y >= EC_P) return false; + const left = (y * y) % EC_P; + const right = weierstrass(x); + return (left - right) % EC_P === _0n; } exports.isPoint = isPoint; +function isXOnlyPoint(p) { + if (!buffer_1.Buffer.isBuffer(p)) return false; + if (p.length !== 32) return false; + const x = BigInt(`0x${p.toString('hex')}`); + if (x === _0n) return false; + if (x >= EC_P) return false; + const y2 = weierstrass(x); + return jacobiSymbol(y2) === 1; +} +exports.isXOnlyPoint = isXOnlyPoint; const UINT31_MAX = Math.pow(2, 31) - 1; function UInt31(value) { return exports.typeforce.UInt32(value) && value <= UINT31_MAX; @@ -68,6 +112,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/address.spec.ts b/test/address.spec.ts index be08cf803..73e8f7848 100644 --- a/test/address.spec.ts +++ b/test/address.spec.ts @@ -1,6 +1,5 @@ import * as assert from 'assert'; import { describe, it } from 'mocha'; -import * as ecc from 'tiny-secp256k1'; import * as baddress from '../src/address'; import * as bscript from '../src/script'; import * as fixtures from './fixtures/address.json'; @@ -69,11 +68,7 @@ describe('address', () => { fixtures.standard.forEach(f => { it('encodes ' + f.script.slice(0, 30) + '... (' + f.network + ')', () => { const script = bscript.fromASM(f.script); - const address = baddress.fromOutputScript( - script, - NETWORKS[f.network], - ecc, - ); + const address = baddress.fromOutputScript(script, NETWORKS[f.network]); assert.strictEqual(address, f.base58check || f.bech32!.toLowerCase()); }); @@ -84,7 +79,7 @@ describe('address', () => { const script = bscript.fromASM(f.script); assert.throws(() => { - baddress.fromOutputScript(script, undefined, ecc); + baddress.fromOutputScript(script); }, new RegExp(f.exception)); }); }); @@ -136,7 +131,6 @@ describe('address', () => { const script = baddress.toOutputScript( (f.base58check || f.bech32)!, NETWORKS[f.network], - ecc, ); assert.strictEqual(bscript.toASM(script), f.script); @@ -147,7 +141,7 @@ describe('address', () => { it('throws when ' + (f.exception || f.paymentException), () => { const exception = f.paymentException || `${f.address} ${f.exception}`; assert.throws(() => { - baddress.toOutputScript(f.address, f.network as any, ecc); + baddress.toOutputScript(f.address, f.network as any); }, new RegExp(exception)); }); }); 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..804b43f26 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'); @@ -16,7 +17,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { const myKey = bip32.fromSeed(rng(64), regtest); const output = createKeySpendOutput(myKey.publicKey); - const address = bitcoin.address.fromOutputScript(output, regtest, ecc); + const address = bitcoin.address.fromOutputScript(output, regtest); // amount from faucet const amount = 42e4; // amount to send @@ -51,8 +52,11 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { const internalKey = bip32.fromSeed(rng(64), regtest); const { output, address } = bitcoin.payments.p2tr( - { internalPubkey: toXOnly(internalKey.publicKey), network: regtest }, - { eccLib: ecc }, + { + internalPubkey: toXOnly(internalKey.publicKey), + network: regtest, + }, + { tweakFn: ecc.xOnlyPointAddTweak }, ); // amount from faucet @@ -62,7 +66,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { // get faucet const unspent = await regtestUtils.faucetComplex(output!, amount); - const psbt = new bitcoin.Psbt({ eccLib: ecc, network: regtest }); + const psbt = new bitcoin.Psbt({ network: regtest }); psbt.addInput({ hash: unspent.txId, index: 0, @@ -97,11 +101,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( { @@ -109,7 +111,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { scriptTree, network: regtest, }, - { eccLib: ecc }, + { tweakFn: ecc.xOnlyPointAddTweak }, ); // amount from faucet @@ -119,7 +121,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { // get faucet const unspent = await regtestUtils.faucetComplex(output!, amount); - const psbt = new bitcoin.Psbt({ eccLib: ecc, network: regtest }); + const psbt = new bitcoin.Psbt({ network: regtest }); psbt.addInput({ hash: unspent.txId, index: 0, @@ -157,7 +159,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( @@ -214,7 +216,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { redeem, network: regtest, }, - { eccLib: ecc }, + { tweakFn: ecc.xOnlyPointAddTweak }, ); // amount from faucet @@ -224,7 +226,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { // get faucet const unspent = await regtestUtils.faucetComplex(output!, amount); - const psbt = new bitcoin.Psbt({ eccLib: ecc, network: regtest }); + const psbt = new bitcoin.Psbt({ network: regtest }); psbt.addInput({ hash: unspent.txId, index: 0, @@ -262,7 +264,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', @@ -291,7 +293,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { redeem, network: regtest, }, - { eccLib: ecc }, + { tweakFn: ecc.xOnlyPointAddTweak }, ); // amount from faucet @@ -301,7 +303,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { // get faucet const unspent = await regtestUtils.faucetComplex(output!, amount); - const psbt = new bitcoin.Psbt({ eccLib: ecc, network: regtest }); + const psbt = new bitcoin.Psbt({ network: regtest }); psbt.addInput({ hash: unspent.txId, index: 0, @@ -361,7 +363,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', @@ -390,7 +392,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { redeem, network: regtest, }, - { eccLib: ecc }, + { tweakFn: ecc.xOnlyPointAddTweak }, ); // amount from faucet @@ -400,7 +402,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { // get faucet const unspent = await regtestUtils.faucetComplex(output!, amount); - const psbt = new bitcoin.Psbt({ eccLib: ecc, network: regtest }); + const psbt = new bitcoin.Psbt({ network: regtest }); psbt.addInput({ hash: unspent.txId, index: 0, diff --git a/test/payments.spec.ts b/test/payments.spec.ts index e89834d3b..30ab632fa 100644 --- a/test/payments.spec.ts +++ b/test/payments.spec.ts @@ -1,16 +1,15 @@ import * as assert from 'assert'; -import * as ecc from 'tiny-secp256k1'; +import { xOnlyPointAddTweak } from 'tiny-secp256k1'; import { describe, it } from 'mocha'; -import { PaymentCreator } from '../src/payments'; +import { PaymentCreator, XOnlyTweakFunction } from '../src/payments'; import * as u from './payments.utils'; -import { TinySecp256k1Interface } from '../src/types'; ['embed', 'p2ms', 'p2pk', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'].forEach( p => { describe(p, () => { let fn: PaymentCreator; - const eccLib: TinySecp256k1Interface | undefined = - p === 'p2tr' ? ecc : undefined; + const tweakFn: XOnlyTweakFunction | undefined = + p === 'p2tr' ? xOnlyPointAddTweak : undefined; const payment = require('../src/payments/' + p); if (p === 'embed') { fn = payment.p2data; @@ -21,7 +20,7 @@ import { TinySecp256k1Interface } from '../src/types'; const fixtures = require('./fixtures/' + p); fixtures.valid.forEach((f: any) => { - const options = Object.assign({ eccLib }, f.options || {}); + const options = Object.assign({ tweakFn }, f.options || {}); it(f.description + ' as expected', () => { const args = u.preform(f.arguments); const actual = fn(args, options); @@ -43,7 +42,7 @@ import { TinySecp256k1Interface } from '../src/types'; }); fixtures.invalid.forEach((f: any) => { - const options = Object.assign({ eccLib }, f.options || {}); + const options = Object.assign({ tweakFn }, f.options || {}); it( 'throws ' + f.exception + diff --git a/test/psbt.spec.ts b/test/psbt.spec.ts index 871142194..f548bc84b 100644 --- a/test/psbt.spec.ts +++ b/test/psbt.spec.ts @@ -140,8 +140,7 @@ describe(`Psbt`, () => { fixtures.bip174.signer.forEach(f => { it('Signs PSBT to the expected result', () => { - const opts = f.isTaproot ? { eccLib: ecc } : {}; - const psbt = Psbt.fromBase64(f.psbt, opts); + const psbt = Psbt.fromBase64(f.psbt); f.keys.forEach(({ inputToSign, WIF }) => { const keyPair = ECPair.fromWIF(WIF, NETWORKS.testnet); @@ -168,8 +167,7 @@ describe(`Psbt`, () => { fixtures.bip174.finalizer.forEach(f => { it('Finalizes inputs and gives the expected PSBT', () => { - const opts = f.isTaproot ? { eccLib: ecc } : {}; - const psbt = Psbt.fromBase64(f.psbt, opts); + const psbt = Psbt.fromBase64(f.psbt); psbt.finalizeAllInputs(); @@ -964,7 +962,7 @@ describe(`Psbt`, () => { describe('validateSignaturesOfTaprootInput', () => { const f = fixtures.validateSignaturesOfTaprootInput; it('Correctly validates a signature', () => { - const psbt = Psbt.fromBase64(f.psbt, { eccLib: ecc }); + const psbt = Psbt.fromBase64(f.psbt); assert.strictEqual( psbt.validateSignaturesOfInput(f.index, schnorrValidator), true, @@ -972,7 +970,7 @@ describe(`Psbt`, () => { }); it('Correctly validates a signature against a pubkey', () => { - const psbt = Psbt.fromBase64(f.psbt, { eccLib: ecc }); + const psbt = Psbt.fromBase64(f.psbt); assert.strictEqual( psbt.validateSignaturesOfInput( f.index, @@ -994,7 +992,7 @@ describe(`Psbt`, () => { describe('finalizeTaprootInput', () => { it('Correctly finalizes a taproot script-path spend', () => { const f = fixtures.finalizeTaprootScriptPathSpendInput; - const psbt = Psbt.fromBase64(f.psbt, { eccLib: ecc }); + const psbt = Psbt.fromBase64(f.psbt); const tapscriptFinalizer = buildTapscriptFinalizer( f.internalPublicKey as any, f.scriptTree, @@ -1006,7 +1004,7 @@ describe(`Psbt`, () => { it('Failes to finalize a taproot script-path spend when a finalizer is not provided', () => { const f = fixtures.finalizeTaprootScriptPathSpendInput; - const psbt = Psbt.fromBase64(f.psbt, { eccLib: ecc }); + const psbt = Psbt.fromBase64(f.psbt); assert.throws(() => { psbt.finalizeInput(0); diff --git a/test/psbt.utils.ts b/test/psbt.utils.ts index 59cccb323..516b41d3c 100644 --- a/test/psbt.utils.ts +++ b/test/psbt.utils.ts @@ -1,6 +1,6 @@ import { PsbtInput } from 'bip174/src/lib/interfaces'; import * as bitcoin from './..'; -import { TinySecp256k1Interface } from '../src/types'; +import { xOnlyPointAddTweak } from 'tiny-secp256k1'; /** * Build finalizer function for Tapscript. @@ -20,7 +20,6 @@ const buildTapscriptFinalizer = ( _isP2SH: boolean, _isP2WSH: boolean, _isTapscript: boolean, - eccLib?: TinySecp256k1Interface, ): { finalScriptSig: Buffer | undefined; finalScriptWitness: Buffer | Buffer[] | undefined; @@ -36,7 +35,7 @@ const buildTapscriptFinalizer = ( redeem: { output: script }, network, }, - { eccLib }, + { tweakFn: xOnlyPointAddTweak }, ); const sigs = (input.partialSig || []).map(ps => ps.signature) as Buffer[]; const finalScriptWitness = sigs.concat( diff --git a/ts_src/address.ts b/ts_src/address.ts index 62bcf2ef7..8004b2668 100644 --- a/ts_src/address.ts +++ b/ts_src/address.ts @@ -2,13 +2,7 @@ import { Network } from './networks'; import * as networks from './networks'; import * as payments from './payments'; import * as bscript from './script'; -import { - typeforce, - tuple, - Hash160bit, - UInt8, - TinySecp256k1Interface, -} from './types'; +import { typeforce, tuple, Hash160bit, UInt8 } from './types'; import { bech32, bech32m } from 'bech32'; import * as bs58check from 'bs58check'; export interface Base58CheckResult { @@ -119,11 +113,7 @@ export function toBech32( : bech32m.encode(prefix, words); } -export function fromOutputScript( - output: Buffer, - network?: Network, - eccLib?: TinySecp256k1Interface, -): string { +export function fromOutputScript(output: Buffer, network?: Network): string { // TODO: Network network = network || networks.bitcoin; @@ -140,8 +130,7 @@ export function fromOutputScript( return payments.p2wsh({ output, network }).address as string; } catch (e) {} try { - if (eccLib) - return payments.p2tr({ output, network }, { eccLib }).address as string; + return payments.p2tr({ output, network }).address as string; } catch (e) {} try { return _toFutureSegwitAddress(output, network); @@ -150,11 +139,7 @@ export function fromOutputScript( throw new Error(bscript.toASM(output) + ' has no matching Address'); } -export function toOutputScript( - address: string, - network?: Network, - eccLib?: TinySecp256k1Interface, -): Buffer { +export function toOutputScript(address: string, network?: Network): Buffer { network = network || networks.bitcoin; let decodeBase58: Base58CheckResult | undefined; @@ -182,9 +167,8 @@ export function toOutputScript( if (decodeBech32.data.length === 32) return payments.p2wsh({ hash: decodeBech32.data }).output as Buffer; } else if (decodeBech32.version === 1) { - if (decodeBech32.data.length === 32 && eccLib) - return payments.p2tr({ pubkey: decodeBech32.data }, { eccLib }) - .output as Buffer; + if (decodeBech32.data.length === 32) + return payments.p2tr({ pubkey: decodeBech32.data }).output as Buffer; } else if ( decodeBech32.version >= FUTURE_SEGWIT_MIN_VERSION && decodeBech32.version <= FUTURE_SEGWIT_MAX_VERSION && diff --git a/ts_src/payments/index.ts b/ts_src/payments/index.ts index 70d7614b7..93ff7f7b6 100644 --- a/ts_src/payments/index.ts +++ b/ts_src/payments/index.ts @@ -1,5 +1,5 @@ import { Network } from '../networks'; -import { TinySecp256k1Interface, Taptree } from '../types'; +import { Taptree, XOnlyPointAddTweakResult } from '../types'; import { p2data as embed } from './embed'; import { p2ms } from './p2ms'; import { p2pk } from './p2pk'; @@ -34,10 +34,15 @@ export type PaymentCreator = (a: Payment, opts?: PaymentOpts) => Payment; export type PaymentFunction = () => Payment; +export type XOnlyTweakFunction = ( + p: Buffer, + t: Buffer, +) => XOnlyPointAddTweakResult | null; + export interface PaymentOpts { validate?: boolean; allowIncomplete?: boolean; - eccLib?: TinySecp256k1Interface; + tweakFn?: XOnlyTweakFunction; } export type StackElement = Buffer | number; diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index 2c7d8557a..aed87db08 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -1,25 +1,27 @@ 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, + isXOnlyPoint, + TAPLEAF_VERSION_MASK, +} from '../types'; import { toHashTree, rootHashFromPath, findScriptPath, - tapLeafHash, + tapleafHash, tapTweakHash, - isTapTree, LEAF_VERSION_TAPSCRIPT, } from './taprootutils'; -import { Payment, PaymentOpts } from './index'; +import { Payment, PaymentOpts, XOnlyTweakFunction } from './index'; import * as lazy from './lazy'; import { bech32m } from 'bech32'; -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 ( @@ -33,11 +35,11 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { opts = Object.assign({ validate: true }, opts || {}); - const _ecc = lazy.value(() => { - if (!opts!.eccLib) throw new Error('ECC Library is missing for p2tr.'); + const _tweakFn = lazy.value(() => { + if (!opts!.tweakFn) throw new Error('Tweak function is missing for p2tr.'); - verifyEcc(opts!.eccLib); - return opts!.eccLib; + verifyTweakFn(opts!.tweakFn); + return opts!.tweakFn; }); typef( @@ -51,7 +53,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 +87,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 +105,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 +140,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', () => { @@ -140,7 +148,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { if (a.output) return a.output.slice(2); if (a.address) return _address().data; if (o.internalPubkey) { - const tweakedKey = tweakKey(o.internalPubkey, o.hash, _ecc()); + const tweakedKey = tweakKey(o.internalPubkey, o.hash, _tweakFn()); if (tweakedKey) return tweakedKey.x; } }); @@ -158,18 +166,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); - const outputKey = tweakKey(a.internalPubkey, hashTree.hash, _ecc()); + if (!path) return; + const outputKey = tweakKey(a.internalPubkey, hashTree.hash, _tweakFn()); 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]; } @@ -208,20 +219,29 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { } if (a.internalPubkey) { - const tweakedKey = tweakKey(a.internalPubkey, o.hash, _ecc()); + const tweakedKey = tweakKey(a.internalPubkey, o.hash, _tweakFn()); if (pubkey.length > 0 && !pubkey.equals(tweakedKey!.x)) throw new TypeError('Pubkey mismatch'); else pubkey = tweakedKey!.x; } if (pubkey && pubkey.length) { - if (!_ecc().isXOnlyPoint(pubkey)) - throw new TypeError('Invalid pubkey for p2tr'); + if (!isXOnlyPoint(pubkey)) throw new TypeError('Invalid pubkey for p2tr'); + } + + const hashTree = _hashTree(); + + if (a.hash && hashTree) { + if (!a.hash.equals(hashTree.hash)) throw new TypeError('Hash mismatch'); } - if (a.hash && a.scriptTree) { - const hash = toHashTree(a.scriptTree).hash; - if (!a.hash.equals(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(); @@ -280,16 +300,16 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { if (a.internalPubkey && !a.internalPubkey.equals(internalPubkey)) throw new TypeError('Internal pubkey mismatch'); - if (!_ecc().isXOnlyPoint(internalPubkey)) + if (!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()); + const outputKey = tweakKey(internalPubkey, hash, _tweakFn()); if (!outputKey) // todo: needs test data throw new TypeError('Invalid outputKey for p2tr witness'); @@ -314,7 +334,7 @@ interface TweakedPublicKey { function tweakKey( pubKey: Buffer, h: Buffer | undefined, - eccLib: TinySecp256k1Interface, + tweakFn: XOnlyTweakFunction, ): TweakedPublicKey | null { if (!NBuffer.isBuffer(pubKey)) return null; if (pubKey.length !== 32) return null; @@ -322,7 +342,7 @@ function tweakKey( const tweakHash = tapTweakHash(pubKey, h); - const res = eccLib.xOnlyPointAddTweak(pubKey, tweakHash); + const res = tweakFn(pubKey, tweakHash); if (!res || res.xOnlyPubkey === null) return null; return { @@ -338,3 +358,45 @@ function stacksEqual(a: Buffer[], b: Buffer[]): boolean { return x.equals(b[i]); }); } + +function verifyTweakFn(tweakFn: XOnlyTweakFunction): void { + [ + { + pubkey: + '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', + tweak: 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140', + parity: -1, + result: null, + }, + { + pubkey: + '1617d38ed8d8657da4d4761e8057bc396ea9e4b9d29776d4be096016dbd2509b', + tweak: 'a8397a935f0dfceba6ba9618f6451ef4d80637abf4e6af2669fbc9de6a8fd2ac', + parity: 1, + result: + 'e478f99dab91052ab39a33ea35fd5e6e4933f4d28023cd597c9a1f6760346adf', + }, + { + pubkey: + '2c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991', + tweak: '823c3cd2142744b075a87eade7e1b8678ba308d566226a0056ca2b7a76f86b47', + parity: 0, + result: + '9534f8dc8c6deda2dc007655981c78b49c5d96c778fbf363462a11ec9dfd948c', + }, + ].forEach(t => { + const r = tweakFn( + Buffer.from(t.pubkey, 'hex'), + Buffer.from(t.tweak, 'hex'), + ); + if (t.result === null) { + if (r !== null) throw new Error('Expected failed tweak'); + } else { + if (r === null) throw new Error('Expected successful tweak'); + if (r!.parity !== t.parity) + throw new Error('Tweaked key parity mismatch'); + if (!Buffer.from(r!.xOnlyPubkey).equals(Buffer.from(t.result, 'hex'))) + throw new Error('Tweaked key mismatch'); + } + }); +} 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/payments/verifyecc.ts b/ts_src/payments/verifyecc.ts deleted file mode 100644 index 75c2c5062..000000000 --- a/ts_src/payments/verifyecc.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { TinySecp256k1Interface } from '../types'; - -const h = (hex: string): Buffer => Buffer.from(hex, 'hex'); - -export function verifyEcc(ecc: TinySecp256k1Interface): void { - assert(typeof ecc.isXOnlyPoint === 'function'); - assert( - ecc.isXOnlyPoint( - h('79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), - ), - ); - assert( - ecc.isXOnlyPoint( - h('fffffffffffffffffffffffffffffffffffffffffffffffffffffffeeffffc2e'), - ), - ); - assert( - ecc.isXOnlyPoint( - h('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9'), - ), - ); - assert( - ecc.isXOnlyPoint( - h('0000000000000000000000000000000000000000000000000000000000000001'), - ), - ); - assert( - !ecc.isXOnlyPoint( - h('0000000000000000000000000000000000000000000000000000000000000000'), - ), - ); - assert( - !ecc.isXOnlyPoint( - h('fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f'), - ), - ); - - assert(typeof ecc.xOnlyPointAddTweak === 'function'); - tweakAddVectors.forEach(t => { - const r = ecc.xOnlyPointAddTweak(h(t.pubkey), h(t.tweak)); - if (t.result === null) { - assert(r === null); - } else { - assert(r !== null); - assert(r!.parity === t.parity); - assert(Buffer.from(r!.xOnlyPubkey).equals(h(t.result))); - } - }); -} - -function assert(bool: boolean): void { - if (!bool) throw new Error('ecc library invalid'); -} - -const tweakAddVectors = [ - { - pubkey: '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', - tweak: 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140', - parity: -1, - result: null, - }, - { - pubkey: '1617d38ed8d8657da4d4761e8057bc396ea9e4b9d29776d4be096016dbd2509b', - tweak: 'a8397a935f0dfceba6ba9618f6451ef4d80637abf4e6af2669fbc9de6a8fd2ac', - parity: 1, - result: 'e478f99dab91052ab39a33ea35fd5e6e4933f4d28023cd597c9a1f6760346adf', - }, - { - pubkey: '2c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991', - tweak: '823c3cd2142744b075a87eade7e1b8678ba308d566226a0056ca2b7a76f86b47', - parity: 0, - result: '9534f8dc8c6deda2dc007655981c78b49c5d96c778fbf363462a11ec9dfd948c', - }, -]; diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index f135173bd..1517acffd 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -21,8 +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 { TinySecp256k1Interface } from './types'; +import { tapleafHash } from './payments/taprootutils'; export interface TransactionInput { hash: string | Buffer; @@ -140,7 +139,6 @@ export class Psbt { // We will disable exporting the Psbt when unsafe sign is active. // because it is not BIP174 compliant. __UNSAFE_SIGN_NONSEGWIT: false, - __EC_LIB: opts.eccLib, }; if (this.data.inputs.length === 0) this.setVersion(2); @@ -191,11 +189,7 @@ export class Psbt { return this.__CACHE.__TX.outs.map(output => { let address; try { - address = fromOutputScript( - output.script, - this.opts.network, - this.__CACHE.__EC_LIB, - ); + address = fromOutputScript(output.script, this.opts.network); } catch (_) {} return { script: cloneBuffer(output.script), @@ -309,7 +303,7 @@ export class Psbt { const { address } = outputData as any; if (typeof address === 'string') { const { network } = this.opts; - const script = toOutputScript(address, network, this.__CACHE.__EC_LIB); + const script = toOutputScript(address, network); outputData = Object.assign(outputData, { script }); } const c = this.__CACHE; @@ -375,7 +369,6 @@ export class Psbt { isP2SH, isP2WSH, isTapscript, - this.__CACHE.__EC_LIB, ); if (finalScriptSig) this.data.updateInput(inputIndex, { finalScriptSig }); @@ -407,13 +400,9 @@ export class Psbt { input.redeemScript || redeemFromFinalScriptSig(input.finalScriptSig), input.witnessScript || redeemFromFinalWitnessScript(input.finalScriptWitness), - this.__CACHE, ); const type = result.type === 'raw' ? '' : result.type + '-'; - const mainType = classifyScript( - result.meaningfulScript, - this.__CACHE.__EC_LIB, - ); + const mainType = classifyScript(result.meaningfulScript); return (type + mainType) as AllScriptType; } @@ -821,13 +810,11 @@ interface PsbtCache { __FEE?: number; __EXTRACTED_TX?: Transaction; __UNSAFE_SIGN_NONSEGWIT: boolean; - __EC_LIB?: TinySecp256k1Interface; } interface PsbtOptsOptional { network?: Network; maximumFeeRate?: number; - eccLib?: TinySecp256k1Interface; } interface PsbtOpts { @@ -1017,12 +1004,10 @@ function isFinalized(input: PsbtInput): boolean { return !!input.finalScriptSig || !!input.finalScriptWitness; } -function isPaymentFactory( - payment: any, -): (script: Buffer, eccLib?: any) => boolean { - return (script: Buffer, eccLib?: any): boolean => { +function isPaymentFactory(payment: any): (script: Buffer) => boolean { + return (script: Buffer): boolean => { try { - payment({ output: script }, { eccLib }); + payment({ output: script }); return true; } catch (err) { return false; @@ -1225,7 +1210,6 @@ type FinalScriptsFunc = ( isTapscript: boolean, // Is taproot script path? isP2SH: boolean, // Is it P2SH? isP2WSH: boolean, // Is it P2WSH? - eccLib?: TinySecp256k1Interface, // optional lib for checking taproot validity ) => { finalScriptSig: Buffer | undefined; finalScriptWitness: Buffer | Buffer[] | undefined; @@ -1239,12 +1223,11 @@ function getFinalScripts( isP2SH: boolean, isP2WSH: boolean, isTapscript: boolean = false, - eccLib?: TinySecp256k1Interface, ): { finalScriptSig: Buffer | undefined; finalScriptWitness: Buffer | undefined; } { - const scriptType = classifyScript(script, eccLib); + const scriptType = classifyScript(script); if (isTapscript || !canFinalize(input, script, scriptType)) throw new Error(`Can not finalize input #${inputIndex}`); return prepareFinalScripts( @@ -1379,7 +1362,6 @@ function getHashForSig( 'input', input.redeemScript, input.witnessScript, - cache, ); if (['p2sh-p2wsh', 'p2wsh'].indexOf(type) >= 0) { @@ -1399,14 +1381,14 @@ function getHashForSig( prevout.value, sighashType, ); - } else if (isP2TR(prevout.script, cache.__EC_LIB)) { + } else if (isP2TR(prevout.script)) { const prevOuts: Output[] = inputs.map((i, index) => getScriptAndAmountFromUtxo(index, i, cache), ); 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( @@ -1553,7 +1535,7 @@ function getScriptFromInput( res.script = utxoScript; } - const isTaproot = utxoScript && isP2TR(utxoScript, cache.__EC_LIB); + const isTaproot = utxoScript && isP2TR(utxoScript); // Segregated Witness versions 0 or 1 if (input.witnessScript || isP2WPKH(res.script!) || isTaproot) { @@ -1816,7 +1798,6 @@ function pubkeyInInput( 'input', input.redeemScript, input.witnessScript, - cache, ); return pubkeyInScript(pubkey, meaningfulScript); } @@ -1834,7 +1815,6 @@ function pubkeyInOutput( 'output', output.redeemScript, output.witnessScript, - cache, ); return pubkeyInScript(pubkey, meaningfulScript); } @@ -1893,7 +1873,6 @@ function getMeaningfulScript( ioType: 'input' | 'output', redeemScript?: Buffer, witnessScript?: Buffer, - cache?: PsbtCache, ): { meaningfulScript: Buffer; type: 'p2sh' | 'p2wsh' | 'p2sh-p2wsh' | 'p2tr' | 'raw'; @@ -1901,7 +1880,7 @@ function getMeaningfulScript( const isP2SH = isP2SHScript(script); const isP2SHP2WSH = isP2SH && redeemScript && isP2WSHScript(redeemScript); const isP2WSH = isP2WSHScript(script); - const isP2TRScript = isP2TR(script, cache && cache.__EC_LIB); + const isP2TRScript = isP2TR(script); if (isP2SH && redeemScript === undefined) throw new Error('scriptPubkey is P2SH but redeemScript missing'); @@ -2002,15 +1981,12 @@ type ScriptType = | 'pubkey' | 'taproot' | 'nonstandard'; -function classifyScript( - script: Buffer, - eccLib?: TinySecp256k1Interface, -): ScriptType { +function classifyScript(script: Buffer): ScriptType { if (isP2WPKH(script)) return 'witnesspubkeyhash'; if (isP2PKH(script)) return 'pubkeyhash'; if (isP2MS(script)) return 'multisig'; if (isP2PK(script)) return 'pubkey'; - if (isP2TR(script, eccLib)) return 'taproot'; + if (isP2TR(script)) return 'taproot'; return 'nonstandard'; } diff --git a/ts_src/types.ts b/ts_src/types.ts index 59e4e1929..6a0076734 100644 --- a/ts_src/types.ts +++ b/ts_src/types.ts @@ -2,29 +2,79 @@ import { Buffer as NBuffer } from 'buffer'; export const typeforce = require('typeforce'); -const ZERO32 = NBuffer.alloc(32, 0); -const EC_P = NBuffer.from( - 'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f', - 'hex', +const EC_P = BigInt( + `0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f`, ); +const EC_B = BigInt(7); +// Idea from noble-secp256k1, to be nice to bad JS parsers +const _0n = BigInt(0); +const _1n = BigInt(1); +const _2n = BigInt(2); +const _3n = BigInt(3); +const _5n = BigInt(5); +const _7n = BigInt(7); + +function weierstrass(x: bigint): bigint { + const x2 = (x * x) % EC_P; + const x3 = (x2 * x) % EC_P; + return (x3 /* + a=0 a*x */ + EC_B) % EC_P; +} + +// For prime P, the Jacobi symbol is 1 iff a is a quadratic residue mod P +function jacobiSymbol(a: bigint): -1 | 0 | 1 { + if (a === _0n) return 0; + + let p = EC_P; + let sign = 1; + for (;;) { + let and3; + // Handle runs of zeros efficiently w/o flipping sign each time + for (and3 = a & _3n; and3 === _0n; a >>= _2n, and3 = a & _3n); + // If there's one more zero, shift it off and flip the sign + if (and3 === _2n) { + a >>= _1n; + const pand7 = p & _7n; + if (pand7 === _3n || pand7 === _5n) sign = -sign; + } + if (a === _1n) break; + if ((_3n & a) === _3n && (_3n & p) === _3n) sign = -sign; + [a, p] = [p % a, a]; + } + return sign > 0 ? 1 : -1; +} export function isPoint(p: Buffer | number | undefined | null): boolean { if (!NBuffer.isBuffer(p)) return false; if (p.length < 33) return false; const t = p[0]; - const x = p.slice(1, 33); - if (x.compare(ZERO32) === 0) return false; - if (x.compare(EC_P) >= 0) return false; - if ((t === 0x02 || t === 0x03) && p.length === 33) { - return true; + if (p.length === 33) { + return (t === 0x02 || t === 0x03) && isXOnlyPoint(p.slice(1)); } - const y = p.slice(33); - if (y.compare(ZERO32) === 0) return false; - if (y.compare(EC_P) >= 0) return false; - if (t === 0x04 && p.length === 65) return true; - return false; + if (t !== 0x04 || p.length !== 65) return false; + + const x = BigInt(`0x${p.slice(1, 33).toString('hex')}`); + if (x === _0n) return false; + if (x >= EC_P) return false; + + const y = BigInt(`0x${p.slice(33).toString('hex')}`); + if (y === _0n) return false; + if (y >= EC_P) return false; + + const left = (y * y) % EC_P; + const right = weierstrass(x); + return (left - right) % EC_P === _0n; +} + +export function isXOnlyPoint(p: Buffer | number | undefined | null): boolean { + if (!NBuffer.isBuffer(p)) return false; + if (p.length !== 32) return false; + const x = BigInt(`0x${p.toString('hex')}`); + if (x === _0n) return false; + if (x >= EC_P) return false; + const y2 = weierstrass(x); + return jacobiSymbol(y2) === 1; } const UINT31_MAX: number = Math.pow(2, 31) - 1; @@ -77,10 +127,29 @@ 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; xOnlyPointAddTweak( p: Uint8Array, tweak: Uint8Array,