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,