Skip to content

Don't rely on ECC lib for curve/point check #34

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: p2tr-v1
Choose a base branch
from
5 changes: 2 additions & 3 deletions src/address.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/// <reference types="node" />
import { Network } from './networks';
import { TinySecp256k1Interface } from './types';
export interface Base58CheckResult {
hash: Buffer;
version: number;
Expand All @@ -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;
11 changes: 5 additions & 6 deletions src/address.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -102,15 +102,15 @@ 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);
} catch (e) {}
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;
Expand All @@ -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 &&
Expand Down
5 changes: 3 additions & 2 deletions src/payments/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="node" />
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';
Expand Down Expand Up @@ -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[];
Expand Down
124 changes: 91 additions & 33 deletions src/payments/p2tr.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand All @@ -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)(
{
Expand All @@ -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),
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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;
Expand All @@ -116,15 +122,16 @@ 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', () => {
if (a.pubkey) return a.pubkey;
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;
}
});
Expand All @@ -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];
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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');
Expand All @@ -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,
Expand All @@ -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');
}
});
}
41 changes: 23 additions & 18 deletions src/payments/taprootutils.d.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
/// <reference types="node" />
import { Taptree } from '../types';
import { Tapleaf, Taptree } from '../types';
export declare const LEAF_VERSION_TAPSCRIPT = 192;
export declare function rootHashFromPath(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer;
export interface HashTree {
export declare function rootHashFromPath(controlBlock: Buffer, leafHash: Buffer): Buffer;
interface HashLeaf {
hash: Buffer;
left?: HashTree;
right?: HashTree;
}
interface HashBranch {
hash: Buffer;
left: HashTree;
right: HashTree;
}
/**
* Build the hash tree from the scripts binary tree.
* The binary tree can be balanced or not.
* @param scriptTree - is a list representing a binary tree where an element can be:
* - a taproot leaf [(output, version)], or
* - a pair of two taproot leafs [(output, version), (output, version)], or
* - one taproot leaf and a list of elements
* Binary tree representing leaf, branch, and root node hashes of a Taptree.
* Each node contains a hash, and potentially left and right branch hashes.
* This tree is used for 2 purposes: Providing the root hash for tweaking,
* and calculating merkle inclusion proofs when constructing a control block.
*/
export declare function toHashTree(scriptTree: Taptree): HashTree;
export declare type HashTree = HashLeaf | HashBranch;
/**
* Check if the tree is a binary tree with leafs of type Tapleaf
* Build a hash tree of merkle nodes from the scripts binary tree.
* @param scriptTree - the tree of scripts to pairwise hash.
*/
export declare function isTapTree(scriptTree: Taptree): boolean;
export declare function toHashTree(scriptTree: Taptree): HashTree;
/**
* Given a MAST tree, it finds the path of a particular hash.
* Given a HashTree, finds the path from a particular hash to the root.
* @param node - the root of the tree
* @param hash - the hash to search for
* @returns - and array of hashes representing the path, or an empty array if no pat is found
* @returns - array of sibling hashes, from leaf (inclusive) to root
* (exclusive) needed to prove inclusion of the specified hash. undefined if no
* path is found
*/
export declare function findScriptPath(node: HashTree, hash: Buffer): Buffer[];
export declare function tapLeafHash(script: Buffer, version?: number): Buffer;
export declare function findScriptPath(node: HashTree, hash: Buffer): Buffer[] | undefined;
export declare function tapleafHash(leaf: Tapleaf): Buffer;
export declare function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer;
export {};
Loading