From ddef6c83d76902c3c4f5e21aad2ddc88f4406fcc Mon Sep 17 00:00:00 2001 From: Brandon Black Date: Fri, 18 Mar 2022 15:11:14 -0700 Subject: [PATCH 01/13] Declare tapscript version mask like the BIP --- src/payments/p2tr.js | 2 +- ts_src/payments/p2tr.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index 13f283ed8..bab9fcd4e 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -12,7 +12,7 @@ const verifyecc_1 = require('./verifyecc'); const OPS = bscript.OPS; const TAPROOT_WITNESS_VERSION = 0x01; const ANNEX_PREFIX = 0x50; -const LEAF_VERSION_MASK = 0b11111110; +const LEAF_VERSION_MASK = 0xfe; function p2tr(a, opts) { if ( !a.address && diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index 2c7d8557a..04d0f1d11 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -19,7 +19,7 @@ import { verifyEcc } from './verifyecc'; const OPS = bscript.OPS; const TAPROOT_WITNESS_VERSION = 0x01; const ANNEX_PREFIX = 0x50; -const LEAF_VERSION_MASK = 0b11111110; +const LEAF_VERSION_MASK = 0xfe; export function p2tr(a: Payment, opts?: PaymentOpts): Payment { if ( From 47f3158575a2727d2f23cde9b310dca51d0d0052 Mon Sep 17 00:00:00 2001 From: Brandon Black Date: Fri, 18 Mar 2022 12:54:52 -0700 Subject: [PATCH 02/13] Correct Taptree type * Move the (much simplified) type check function to types.ts * Use `Tapleaf` type a bit more (this might be a bad idea) * Be more consistent in the capitalization of `Taptree` --- src/payments/p2tr.js | 28 +++++++++++------- src/payments/taprootutils.d.ts | 8 ++--- src/payments/taprootutils.js | 49 +++++++------------------------ src/psbt.js | 2 +- src/types.d.ts | 5 +++- src/types.js | 17 ++++++++++- test/fixtures/p2tr.json | 42 +++++++++++---------------- test/integration/taproot.spec.ts | 15 +++++----- ts_src/payments/p2tr.ts | 26 ++++++++++------- ts_src/payments/taprootutils.ts | 50 +++++--------------------------- ts_src/psbt.ts | 2 +- ts_src/types.ts | 17 ++++++++++- 12 files changed, 115 insertions(+), 146 deletions(-) diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index bab9fcd4e..7f60e21ff 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -12,7 +12,6 @@ const verifyecc_1 = require('./verifyecc'); const OPS = bscript.OPS; const TAPROOT_WITNESS_VERSION = 0x01; const ANNEX_PREFIX = 0x50; -const LEAF_VERSION_MASK = 0xfe; function p2tr(a, opts) { if ( !a.address && @@ -41,7 +40,7 @@ function p2tr(a, opts) { witness: types_1.typeforce.maybe( types_1.typeforce.arrayOf(types_1.typeforce.Buffer), ), - scriptTree: types_1.typeforce.maybe(taprootutils_1.isTapTree), + scriptTree: types_1.typeforce.maybe(types_1.isTaptree), redeem: types_1.typeforce.maybe({ output: types_1.typeforce.maybe(types_1.typeforce.Buffer), redeemVersion: types_1.typeforce.maybe(types_1.typeforce.Number), @@ -88,9 +87,12 @@ function p2tr(a, opts) { 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 +118,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', () => { @@ -144,10 +147,10 @@ function p2tr(a, opts) { 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 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 (!outputKey) return; @@ -253,9 +256,12 @@ function p2tr(a, opts) { throw new TypeError('Internal pubkey mismatch'); if (!_ecc().isXOnlyPoint(internalPubkey)) throw new TypeError('Invalid internalPubkey for p2tr witness'); - const leafVersion = controlBlock[0] & LEAF_VERSION_MASK; + const leafVersion = controlBlock[0] & types_1.TAPLEAF_VERSION_MASK; const script = witness[witness.length - 2]; - const leafHash = (0, taprootutils_1.tapLeafHash)(script, leafVersion); + const leafHash = (0, taprootutils_1.tapLeafHash)({ + output: script, + version: leafVersion, + }); const hash = (0, taprootutils_1.rootHashFromPath)( controlBlock, leafHash, diff --git a/src/payments/taprootutils.d.ts b/src/payments/taprootutils.d.ts index 2bb998c84..c1181743c 100644 --- a/src/payments/taprootutils.d.ts +++ b/src/payments/taprootutils.d.ts @@ -1,5 +1,5 @@ /// -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 { @@ -16,10 +16,6 @@ export interface HashTree { * - one taproot leaf and a list of elements */ export declare function toHashTree(scriptTree: Taptree): HashTree; -/** - * Check if the tree is a binary tree with leafs of type Tapleaf - */ -export declare function isTapTree(scriptTree: Taptree): boolean; /** * Given a MAST tree, it finds the path of a particular hash. * @param node - the root of the tree @@ -27,5 +23,5 @@ export declare function isTapTree(scriptTree: Taptree): boolean; * @returns - and array of hashes representing the path, or an empty array if no pat is found */ export declare function findScriptPath(node: HashTree, hash: Buffer): Buffer[]; -export declare function tapLeafHash(script: Buffer, version?: number): Buffer; +export declare function tapLeafHash(leaf: Tapleaf): Buffer; export declare function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer; diff --git a/src/payments/taprootutils.js b/src/payments/taprootutils.js index d9221fc33..9cb88ace4 100644 --- a/src/payments/taprootutils.js +++ b/src/payments/taprootutils.js @@ -1,9 +1,10 @@ '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 types_1 = require('../types'); const TAP_LEAF_TAG = 'TapLeaf'; const TAP_BRANCH_TAG = 'TapBranch'; const TAP_TWEAK_TAG = 'TapTweak'; @@ -32,21 +33,11 @@ exports.rootHashFromPath = rootHashFromPath; * - one taproot leaf and a list of elements */ function toHashTree(scriptTree) { - if (scriptTree.length === 1) { - const script = scriptTree[0]; - if (Array.isArray(script)) { - return toHashTree(script); - } - script.version = script.version || exports.LEAF_VERSION_TAPSCRIPT; - if ((script.version & 1) !== 0) - throw new TypeError('Invalid script version'); - return { - hash: tapLeafHash(script.output, script.version), - }; - } - let left = toHashTree([scriptTree[0]]); - let right = toHashTree([scriptTree[1]]); - if (left.hash.compare(right.hash) === 1) [left, right] = [right, left]; + if ((0, types_1.isTapleaf)(scriptTree)) + return { hash: tapLeafHash(scriptTree) }; + const hashes = [toHashTree(scriptTree[0]), toHashTree(scriptTree[1])]; + hashes.sort((a, b) => a.hash.compare(b.hash)); + const [left, right] = hashes; return { hash: tapBranchHash(left.hash, right.hash), left, @@ -54,26 +45,6 @@ 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. * @param node - the root of the tree @@ -96,13 +67,13 @@ function findScriptPath(node, hash) { return []; } 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, buffer_1.Buffer.concat([ buffer_1.Buffer.from([version]), - serializeScript(script), + serializeScript(leaf.output), ]), ); } diff --git a/src/psbt.js b/src/psbt.js index 6747af981..8f46a2718 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -1079,7 +1079,7 @@ function getHashForSig( const signingScripts = prevOuts.map(o => o.script); const values = prevOuts.map(o => o.value); const leafHash = input.witnessScript - ? (0, taprootutils_1.tapLeafHash)(input.witnessScript) + ? (0, taprootutils_1.tapLeafHash)({ output: input.witnessScript }) : undefined; hash = unsignedTx.hashForWitnessV1( inputIndex, diff --git a/src/types.d.ts b/src/types.d.ts index 9b62b4933..c8048c29a 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -18,7 +18,10 @@ export interface Tapleaf { output: Buffer; version?: number; } -export declare type Taptree = Array<[Tapleaf, Tapleaf] | Tapleaf>; +export declare const TAPLEAF_VERSION_MASK = 254; +export declare function isTapleaf(o: any): o is Tapleaf; +export declare type Taptree = [Taptree | Tapleaf, Taptree | Tapleaf] | Tapleaf; +export declare function isTaptree(scriptTree: any): scriptTree is Taptree; export interface TinySecp256k1Interface { isXOnlyPoint(p: Uint8Array): boolean; xOnlyPointAddTweak(p: Uint8Array, tweak: Uint8Array): XOnlyPointAddTweakResult | null; diff --git a/src/types.js b/src/types.js index a6d1efa16..e1d0a528d 100644 --- a/src/types.js +++ b/src/types.js @@ -1,6 +1,6 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -exports.oneOf = exports.Null = exports.BufferN = exports.Function = exports.UInt32 = exports.UInt8 = exports.tuple = exports.maybe = exports.Hex = exports.Buffer = exports.String = exports.Boolean = exports.Array = exports.Number = exports.Hash256bit = exports.Hash160bit = exports.Buffer256bit = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.isPoint = exports.typeforce = void 0; +exports.oneOf = exports.Null = exports.BufferN = exports.Function = exports.UInt32 = exports.UInt8 = exports.tuple = exports.maybe = exports.Hex = exports.Buffer = exports.String = exports.Boolean = exports.Array = exports.Number = exports.Hash256bit = exports.Hash160bit = exports.Buffer256bit = exports.isTaptree = exports.isTapleaf = exports.TAPLEAF_VERSION_MASK = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.isPoint = exports.typeforce = void 0; const buffer_1 = require('buffer'); exports.typeforce = require('typeforce'); const ZERO32 = buffer_1.Buffer.alloc(32, 0); @@ -68,6 +68,21 @@ exports.Network = exports.typeforce.compile({ scriptHash: exports.typeforce.UInt8, wif: exports.typeforce.UInt8, }); +exports.TAPLEAF_VERSION_MASK = 0xfe; +function isTapleaf(o) { + if (!('output' in o)) return false; + if (!buffer_1.Buffer.isBuffer(o.output)) return false; + if (o.version !== undefined) + return (o.version & exports.TAPLEAF_VERSION_MASK) === o.version; + return true; +} +exports.isTapleaf = isTapleaf; +function isTaptree(scriptTree) { + if (!(0, exports.Array)(scriptTree)) return isTapleaf(scriptTree); + if (scriptTree.length !== 2) return false; + return scriptTree.every(t => isTaptree(t)); +} +exports.isTaptree = isTaptree; exports.Buffer256bit = exports.typeforce.BufferN(32); exports.Hash160bit = exports.typeforce.BufferN(20); exports.Hash256bit = exports.typeforce.BufferN(32); diff --git a/test/fixtures/p2tr.json b/test/fixtures/p2tr.json index 9bcf1f7c4..75e0456ab 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", @@ -393,12 +391,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 +423,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 +900,9 @@ "options": {}, "arguments": { "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7", - "scriptTree": [ - { - "output": "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c18 OP_CHECKSIG" - } - ], + "scriptTree": { + "output": "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c18 OP_CHECKSIG" + }, "hash": "b76077013c8e303085e300000000000000000000000000000000000000000000" } }, @@ -1037,7 +1029,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 +1058,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", @@ -1167,4 +1159,4 @@ "depends": {}, "details": [] } -} \ No newline at end of file +} diff --git a/test/integration/taproot.spec.ts b/test/integration/taproot.spec.ts index 90dacb63d..05d7d154d 100644 --- a/test/integration/taproot.spec.ts +++ b/test/integration/taproot.spec.ts @@ -4,6 +4,7 @@ import * as ecc from 'tiny-secp256k1'; import { describe, it } from 'mocha'; import { regtestUtils } from './_regtest'; import * as bitcoin from '../..'; +import { Taptree } from '../../src/types'; import { buildTapscriptFinalizer, toXOnly } from '../psbt.utils'; const rng = require('randombytes'); @@ -97,11 +98,9 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { )} OP_CHECKSIG`; const leafScript = bitcoin.script.fromASM(leafScriptAsm); - const scriptTree = [ - { - output: leafScript, - }, - ]; + const scriptTree = { + output: leafScript, + }; const { output, address, hash } = bitcoin.payments.p2tr( { @@ -157,7 +156,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { )} OP_CHECKSIG`; const leafScript = bitcoin.script.fromASM(leafScriptAsm); - const scriptTree: any[] = [ + const scriptTree: Taptree = [ [ { output: bitcoin.script.fromASM( @@ -262,7 +261,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { const leafScriptAsm = `OP_10 OP_CHECKSEQUENCEVERIFY OP_DROP ${leafPubkey} OP_CHECKSIG`; const leafScript = bitcoin.script.fromASM(leafScriptAsm); - const scriptTree: any[] = [ + const scriptTree: Taptree = [ { output: bitcoin.script.fromASM( '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG', @@ -361,7 +360,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { const leafScript = bitcoin.script.fromASM(leafScriptAsm); - const scriptTree: any[] = [ + const scriptTree: Taptree = [ { output: bitcoin.script.fromASM( '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG', diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index 04d0f1d11..b298e2ba2 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -1,14 +1,18 @@ import { Buffer as NBuffer } from 'buffer'; import { bitcoin as BITCOIN_NETWORK } from '../networks'; import * as bscript from '../script'; -import { typeforce as typef, TinySecp256k1Interface } from '../types'; +import { + typeforce as typef, + isTaptree, + TinySecp256k1Interface, + TAPLEAF_VERSION_MASK, +} from '../types'; import { toHashTree, rootHashFromPath, findScriptPath, tapLeafHash, tapTweakHash, - isTapTree, LEAF_VERSION_TAPSCRIPT, } from './taprootutils'; import { Payment, PaymentOpts } from './index'; @@ -19,7 +23,6 @@ import { verifyEcc } from './verifyecc'; const OPS = bscript.OPS; const TAPROOT_WITNESS_VERSION = 0x01; const ANNEX_PREFIX = 0x50; -const LEAF_VERSION_MASK = 0xfe; export function p2tr(a: Payment, opts?: PaymentOpts): Payment { if ( @@ -51,7 +54,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { pubkey: typef.maybe(typef.BufferN(32)), // tweaked with `hash` from `internalPubkey` signature: typef.maybe(typef.BufferN(64)), witness: typef.maybe(typef.arrayOf(typef.Buffer)), - scriptTree: typef.maybe(isTapTree), + scriptTree: typef.maybe(isTaptree), redeem: typef.maybe({ output: typef.maybe(typef.Buffer), // tapleaf script redeemVersion: typef.maybe(typef.Number), // tapleaf version @@ -102,9 +105,9 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { 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 +135,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', () => { @@ -161,7 +164,10 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { 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 leafHash = tapLeafHash({ + output: a.redeem.output, + version: o.redeemVersion, + }); const path = findScriptPath(hashTree, leafHash); const outputKey = tweakKey(a.internalPubkey, hashTree.hash, _ecc()); if (!outputKey) return; @@ -283,10 +289,10 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { if (!_ecc().isXOnlyPoint(internalPubkey)) throw new TypeError('Invalid internalPubkey for p2tr witness'); - const leafVersion = controlBlock[0] & LEAF_VERSION_MASK; + const leafVersion = controlBlock[0] & TAPLEAF_VERSION_MASK; const script = witness[witness.length - 2]; - const leafHash = tapLeafHash(script, leafVersion); + const leafHash = tapLeafHash({ output: script, version: leafVersion }); const hash = rootHashFromPath(controlBlock, leafHash); const outputKey = tweakKey(internalPubkey, hash, _ecc()); diff --git a/ts_src/payments/taprootutils.ts b/ts_src/payments/taprootutils.ts index cfa7a6dd2..48d08e617 100644 --- a/ts_src/payments/taprootutils.ts +++ b/ts_src/payments/taprootutils.ts @@ -2,7 +2,7 @@ import { Buffer as NBuffer } from 'buffer'; import * as bcrypto from '../crypto'; import { varuint } from '../bufferutils'; -import { Taptree } from '../types'; +import { Tapleaf, Taptree, isTapleaf } from '../types'; const TAP_LEAF_TAG = 'TapLeaf'; const TAP_BRANCH_TAG = 'TapBranch'; @@ -46,52 +46,18 @@ export interface HashTree { * - one taproot leaf and a list of elements */ 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'); - - return { - hash: tapLeafHash(script.output, script.version), - }; - } + 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. @@ -117,11 +83,11 @@ export function findScriptPath(node: HashTree, hash: Buffer): Buffer[] { return []; } -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)]), + NBuffer.concat([NBuffer.from([version]), serializeScript(leaf.output)]), ); } diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index f135173bd..7a64c3e02 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1406,7 +1406,7 @@ function getHashForSig( const signingScripts: any = prevOuts.map(o => o.script); const values: any = prevOuts.map(o => o.value); const leafHash = input.witnessScript - ? tapLeafHash(input.witnessScript) + ? tapLeafHash({ output: input.witnessScript }) : undefined; hash = unsignedTx.hashForWitnessV1( diff --git a/ts_src/types.ts b/ts_src/types.ts index 59e4e1929..1e49361b6 100644 --- a/ts_src/types.ts +++ b/ts_src/types.ts @@ -77,7 +77,22 @@ 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; +} + +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; From 37b0f1c2ceb71448fbb1a2e1f682579c45c9ca34 Mon Sep 17 00:00:00 2001 From: Brandon Black Date: Fri, 18 Mar 2022 13:59:18 -0700 Subject: [PATCH 03/13] Consistent capitalization of tapleaf --- src/payments/p2tr.js | 6 +++--- src/payments/taprootutils.d.ts | 4 ++-- src/payments/taprootutils.js | 12 ++++++------ src/psbt.js | 2 +- ts_src/payments/p2tr.ts | 8 ++++---- ts_src/payments/taprootutils.ts | 8 ++++---- ts_src/psbt.ts | 4 ++-- 7 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index 7f60e21ff..af31dc418 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -89,7 +89,7 @@ function p2tr(a, opts) { const controlBlock = w[w.length - 1]; const leafVersion = controlBlock[0] & types_1.TAPLEAF_VERSION_MASK; const script = w[w.length - 2]; - const leafHash = (0, taprootutils_1.tapLeafHash)({ + const leafHash = (0, taprootutils_1.tapleafHash)({ output: script, version: leafVersion, }); @@ -147,7 +147,7 @@ function p2tr(a, opts) { 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)({ + const leafHash = (0, taprootutils_1.tapleafHash)({ output: a.redeem.output, version: o.redeemVersion, }); @@ -258,7 +258,7 @@ function p2tr(a, opts) { throw new TypeError('Invalid internalPubkey for p2tr witness'); const leafVersion = controlBlock[0] & types_1.TAPLEAF_VERSION_MASK; const script = witness[witness.length - 2]; - const leafHash = (0, taprootutils_1.tapLeafHash)({ + const leafHash = (0, taprootutils_1.tapleafHash)({ output: script, version: leafVersion, }); diff --git a/src/payments/taprootutils.d.ts b/src/payments/taprootutils.d.ts index c1181743c..bc32e8523 100644 --- a/src/payments/taprootutils.d.ts +++ b/src/payments/taprootutils.d.ts @@ -1,7 +1,7 @@ /// import { Tapleaf, Taptree } from '../types'; export declare const LEAF_VERSION_TAPSCRIPT = 192; -export declare function rootHashFromPath(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer; +export declare function rootHashFromPath(controlBlock: Buffer, tapleafMsg: Buffer): Buffer; export interface HashTree { hash: Buffer; left?: HashTree; @@ -23,5 +23,5 @@ export declare function toHashTree(scriptTree: Taptree): HashTree; * @returns - and array of hashes representing the path, or an empty array if no pat is found */ export declare function findScriptPath(node: HashTree, hash: Buffer): Buffer[]; -export declare function tapLeafHash(leaf: Tapleaf): Buffer; +export declare function tapleafHash(leaf: Tapleaf): Buffer; export declare function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer; diff --git a/src/payments/taprootutils.js b/src/payments/taprootutils.js index 9cb88ace4..d946216cc 100644 --- a/src/payments/taprootutils.js +++ b/src/payments/taprootutils.js @@ -1,6 +1,6 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -exports.tapTweakHash = exports.tapLeafHash = exports.findScriptPath = 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'); @@ -9,8 +9,8 @@ const TAP_LEAF_TAG = 'TapLeaf'; const TAP_BRANCH_TAG = 'TapBranch'; const TAP_TWEAK_TAG = 'TapTweak'; exports.LEAF_VERSION_TAPSCRIPT = 0xc0; -function rootHashFromPath(controlBlock, tapLeafMsg) { - const k = [tapLeafMsg]; +function rootHashFromPath(controlBlock, tapleafMsg) { + const k = [tapleafMsg]; const e = []; const m = (controlBlock.length - 33) / 32; for (let j = 0; j < m; j++) { @@ -34,7 +34,7 @@ exports.rootHashFromPath = rootHashFromPath; */ function toHashTree(scriptTree) { if ((0, types_1.isTapleaf)(scriptTree)) - return { hash: tapLeafHash(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; @@ -67,7 +67,7 @@ function findScriptPath(node, hash) { return []; } exports.findScriptPath = findScriptPath; -function tapLeafHash(leaf) { +function tapleafHash(leaf) { const version = leaf.version || exports.LEAF_VERSION_TAPSCRIPT; return bcrypto.taggedHash( TAP_LEAF_TAG, @@ -77,7 +77,7 @@ function tapLeafHash(leaf) { ]), ); } -exports.tapLeafHash = tapLeafHash; +exports.tapleafHash = tapleafHash; function tapTweakHash(pubKey, h) { return bcrypto.taggedHash( TAP_TWEAK_TAG, diff --git a/src/psbt.js b/src/psbt.js index 8f46a2718..694a6cc1b 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -1079,7 +1079,7 @@ function getHashForSig( const signingScripts = prevOuts.map(o => o.script); const values = prevOuts.map(o => o.value); const leafHash = input.witnessScript - ? (0, taprootutils_1.tapLeafHash)({ output: input.witnessScript }) + ? (0, taprootutils_1.tapleafHash)({ output: input.witnessScript }) : undefined; hash = unsignedTx.hashForWitnessV1( inputIndex, diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index b298e2ba2..40fd49e03 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -11,7 +11,7 @@ import { toHashTree, rootHashFromPath, findScriptPath, - tapLeafHash, + tapleafHash, tapTweakHash, LEAF_VERSION_TAPSCRIPT, } from './taprootutils'; @@ -107,7 +107,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { const controlBlock = w[w.length - 1]; const leafVersion = controlBlock[0] & TAPLEAF_VERSION_MASK; const script = w[w.length - 2]; - const leafHash = tapLeafHash({ output: script, version: leafVersion }); + const leafHash = tapleafHash({ output: script, version: leafVersion }); return rootHashFromPath(controlBlock, leafHash); } return null; @@ -164,7 +164,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { if (a.scriptTree && a.redeem && a.redeem.output && a.internalPubkey) { // todo: optimize/cache const hashTree = toHashTree(a.scriptTree); - const leafHash = tapLeafHash({ + const leafHash = tapleafHash({ output: a.redeem.output, version: o.redeemVersion, }); @@ -292,7 +292,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { const leafVersion = controlBlock[0] & TAPLEAF_VERSION_MASK; const script = witness[witness.length - 2]; - const leafHash = tapLeafHash({ output: script, version: leafVersion }); + const leafHash = tapleafHash({ output: script, version: leafVersion }); const hash = rootHashFromPath(controlBlock, leafHash); const outputKey = tweakKey(internalPubkey, hash, _ecc()); diff --git a/ts_src/payments/taprootutils.ts b/ts_src/payments/taprootutils.ts index 48d08e617..12026ace3 100644 --- a/ts_src/payments/taprootutils.ts +++ b/ts_src/payments/taprootutils.ts @@ -12,9 +12,9 @@ export const LEAF_VERSION_TAPSCRIPT = 0xc0; export function rootHashFromPath( controlBlock: Buffer, - tapLeafMsg: Buffer, + tapleafMsg: Buffer, ): Buffer { - const k = [tapLeafMsg]; + const k = [tapleafMsg]; const e = []; const m = (controlBlock.length - 33) / 32; @@ -46,7 +46,7 @@ export interface HashTree { * - one taproot leaf and a list of elements */ export function toHashTree(scriptTree: Taptree): HashTree { - if (isTapleaf(scriptTree)) return { hash: tapLeafHash(scriptTree) }; + if (isTapleaf(scriptTree)) return { hash: tapleafHash(scriptTree) }; const hashes = [toHashTree(scriptTree[0]), toHashTree(scriptTree[1])]; hashes.sort((a, b) => a.hash.compare(b.hash)); @@ -83,7 +83,7 @@ export function findScriptPath(node: HashTree, hash: Buffer): Buffer[] { return []; } -export function tapLeafHash(leaf: Tapleaf): Buffer { +export function tapleafHash(leaf: Tapleaf): Buffer { const version = leaf.version || LEAF_VERSION_TAPSCRIPT; return bcrypto.taggedHash( TAP_LEAF_TAG, diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 7a64c3e02..479421636 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -21,7 +21,7 @@ import { bitcoin as btcNetwork, Network } from './networks'; import * as payments from './payments'; import * as bscript from './script'; import { Output, Transaction } from './transaction'; -import { tapLeafHash } from './payments/taprootutils'; +import { tapleafHash } from './payments/taprootutils'; import { TinySecp256k1Interface } from './types'; export interface TransactionInput { @@ -1406,7 +1406,7 @@ function getHashForSig( const signingScripts: any = prevOuts.map(o => o.script); const values: any = prevOuts.map(o => o.value); const leafHash = input.witnessScript - ? tapLeafHash({ output: input.witnessScript }) + ? tapleafHash({ output: input.witnessScript }) : undefined; hash = unsignedTx.hashForWitnessV1( From 89785be09d4d65449e6d9f6881ac557d0c7e3938 Mon Sep 17 00:00:00 2001 From: Brandon Black Date: Fri, 18 Mar 2022 14:00:26 -0700 Subject: [PATCH 04/13] Don't use constants for tag prefixes Because the taggedHash API is typed, these are compile-time checked and it's more clear w/o the constants. --- src/payments/taprootutils.js | 9 +++------ ts_src/payments/taprootutils.ts | 10 +++------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/payments/taprootutils.js b/src/payments/taprootutils.js index d946216cc..37cea1b20 100644 --- a/src/payments/taprootutils.js +++ b/src/payments/taprootutils.js @@ -5,9 +5,6 @@ const buffer_1 = require('buffer'); const bcrypto = require('../crypto'); const bufferutils_1 = require('../bufferutils'); const types_1 = require('../types'); -const TAP_LEAF_TAG = 'TapLeaf'; -const TAP_BRANCH_TAG = 'TapBranch'; -const TAP_TWEAK_TAG = 'TapTweak'; exports.LEAF_VERSION_TAPSCRIPT = 0xc0; function rootHashFromPath(controlBlock, tapleafMsg) { const k = [tapleafMsg]; @@ -70,7 +67,7 @@ exports.findScriptPath = findScriptPath; 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(leaf.output), @@ -80,13 +77,13 @@ function tapleafHash(leaf) { 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/ts_src/payments/taprootutils.ts b/ts_src/payments/taprootutils.ts index 12026ace3..090abadd5 100644 --- a/ts_src/payments/taprootutils.ts +++ b/ts_src/payments/taprootutils.ts @@ -4,10 +4,6 @@ import * as bcrypto from '../crypto'; import { varuint } from '../bufferutils'; import { Tapleaf, Taptree, isTapleaf } from '../types'; -const TAP_LEAF_TAG = 'TapLeaf'; -const TAP_BRANCH_TAG = 'TapBranch'; -const TAP_TWEAK_TAG = 'TapTweak'; - export const LEAF_VERSION_TAPSCRIPT = 0xc0; export function rootHashFromPath( @@ -86,20 +82,20 @@ export function findScriptPath(node: HashTree, hash: Buffer): Buffer[] { export function tapleafHash(leaf: Tapleaf): Buffer { const version = leaf.version || LEAF_VERSION_TAPSCRIPT; return bcrypto.taggedHash( - TAP_LEAF_TAG, + '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 { From 5b4e88c62c1ced4d89b0d2ef71fea6815fc9422c Mon Sep 17 00:00:00 2001 From: Brandon Black Date: Fri, 18 Mar 2022 14:49:26 -0700 Subject: [PATCH 05/13] Simplify HashTree processing, remove footgun * More clearly show the continuation and base cases in findScriptPath * Return undefined not empty path when no path is found * This would lead to generating an invalid witness * Tighten the type for HashTree to not allow 1-sided branch nodes --- src/payments/p2tr.js | 1 + src/payments/taprootutils.d.ts | 15 +++++++---- src/payments/taprootutils.js | 26 +++++++++---------- ts_src/payments/p2tr.ts | 1 + ts_src/payments/taprootutils.ts | 46 +++++++++++++++++++++------------ 5 files changed, 54 insertions(+), 35 deletions(-) diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index af31dc418..d9fa7ceca 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -152,6 +152,7 @@ function p2tr(a, opts) { version: o.redeemVersion, }); const path = (0, taprootutils_1.findScriptPath)(hashTree, leafHash); + if (!path) return; const outputKey = tweakKey(a.internalPubkey, hashTree.hash, _ecc()); if (!outputKey) return; const controlBock = buffer_1.Buffer.concat( diff --git a/src/payments/taprootutils.d.ts b/src/payments/taprootutils.d.ts index bc32e8523..3c4f800c6 100644 --- a/src/payments/taprootutils.d.ts +++ b/src/payments/taprootutils.d.ts @@ -2,11 +2,15 @@ import { Tapleaf, Taptree } from '../types'; export declare const LEAF_VERSION_TAPSCRIPT = 192; export declare function rootHashFromPath(controlBlock: Buffer, tapleafMsg: Buffer): Buffer; -export interface HashTree { +interface HashLeaf { hash: Buffer; - left?: HashTree; - right?: HashTree; } +interface HashBranch { + hash: Buffer; + left: HashTree; + right: HashTree; +} +export declare type HashTree = HashLeaf | HashBranch; /** * Build the hash tree from the scripts binary tree. * The binary tree can be balanced or not. @@ -20,8 +24,9 @@ export declare function toHashTree(scriptTree: Taptree): HashTree; * Given a MAST tree, it finds the path of a particular hash. * @param node - the root of the tree * @param hash - the hash to search for - * @returns - and array of hashes representing the path, or an empty array if no pat is found + * @returns - and array of hashes representing the path, undefined if no path is found */ -export declare function findScriptPath(node: HashTree, hash: Buffer): Buffer[]; +export declare function 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 37cea1b20..d9ebc7bcb 100644 --- a/src/payments/taprootutils.js +++ b/src/payments/taprootutils.js @@ -21,6 +21,7 @@ function rootHashFromPath(controlBlock, tapleafMsg) { return k[m]; } 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. @@ -46,22 +47,21 @@ exports.toHashTree = toHashTree; * Given a MAST tree, it finds the path of a particular hash. * @param node - the root of the tree * @param hash - the hash to search for - * @returns - and array of hashes representing the path, or an empty array if no pat is found + * @returns - and array of hashes representing the path, undefined if no path is found */ function findScriptPath(node, hash) { - if (node.left) { - if (node.left.hash.equals(hash)) return node.right ? [node.right.hash] : []; - const leftPath = findScriptPath(node.left, hash); - if (leftPath.length) - return node.right ? [node.right.hash].concat(leftPath) : leftPath; - } - if (node.right) { - if (node.right.hash.equals(hash)) return node.left ? [node.left.hash] : []; - const rightPath = findScriptPath(node.right, hash); - if (rightPath.length) - return node.left ? [node.left.hash].concat(rightPath) : rightPath; + if (!isHashBranch(node)) { + if (node.hash.equals(hash)) { + return []; + } else { + return undefined; + } } - return []; + const leftPath = findScriptPath(node.left, hash); + if (leftPath !== undefined) return [node.right.hash, ...leftPath]; + const rightPath = findScriptPath(node.right, hash); + if (rightPath !== undefined) return [node.left.hash, ...rightPath]; + return undefined; } exports.findScriptPath = findScriptPath; function tapleafHash(leaf) { diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index 40fd49e03..a254d6ab8 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -169,6 +169,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { version: o.redeemVersion, }); const path = findScriptPath(hashTree, leafHash); + if (!path) return; const outputKey = tweakKey(a.internalPubkey, hashTree.hash, _ecc()); if (!outputKey) return; const controlBock = NBuffer.concat( diff --git a/ts_src/payments/taprootutils.ts b/ts_src/payments/taprootutils.ts index 090abadd5..7c10e2306 100644 --- a/ts_src/payments/taprootutils.ts +++ b/ts_src/payments/taprootutils.ts @@ -27,12 +27,21 @@ export function rootHashFromPath( return k[m]; } -export interface HashTree { +interface HashLeaf { hash: Buffer; - left?: HashTree; - right?: HashTree; } +interface HashBranch { + hash: Buffer; + left: HashTree; + right: HashTree; +} + +const isHashBranch = (ht: HashTree): ht is HashBranch => + 'left' in ht && 'right' in ht; + +export type HashTree = HashLeaf | HashBranch; + /** * Build the hash tree from the scripts binary tree. * The binary tree can be balanced or not. @@ -59,24 +68,27 @@ export function toHashTree(scriptTree: Taptree): HashTree { * Given a MAST tree, it finds the path of a particular hash. * @param node - the root of the tree * @param hash - the hash to search for - * @returns - and array of hashes representing the path, or an empty array if no pat is found + * @returns - and array of hashes representing the path, undefined if no path is found */ -export function findScriptPath(node: HashTree, hash: Buffer): Buffer[] { - if (node.left) { - if (node.left.hash.equals(hash)) return node.right ? [node.right.hash] : []; - const leftPath = findScriptPath(node.left, hash); - if (leftPath.length) - return node.right ? [node.right.hash].concat(leftPath) : leftPath; +export function findScriptPath( + node: HashTree, + hash: Buffer, +): Buffer[] | undefined { + if (!isHashBranch(node)) { + if (node.hash.equals(hash)) { + return []; + } else { + return undefined; + } } - 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; - } + const leftPath = findScriptPath(node.left, hash); + if (leftPath !== undefined) return [node.right.hash, ...leftPath]; + + const rightPath = findScriptPath(node.right, hash); + if (rightPath !== undefined) return [node.left.hash, ...rightPath]; - return []; + return undefined; } export function tapleafHash(leaf: Tapleaf): Buffer { From 299308ae2f186ea76bd89c58a3670f0ce7cfc631 Mon Sep 17 00:00:00 2001 From: Brandon Black Date: Fri, 18 Mar 2022 15:46:47 -0700 Subject: [PATCH 06/13] Support p2tr with 1 script and no tree * Also added caching of `hashTree`, per todo. * Added a test for this functionality --- src/payments/p2tr.js | 16 ++++++++++------ test/fixtures/p2tr.json | 22 ++++++++++++++++++++++ ts_src/payments/p2tr.ts | 17 +++++++++++------ 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index d9fa7ceca..57087092f 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -73,6 +73,11 @@ function p2tr(a, opts) { } return a.witness.slice(); }); + const _hashTree = lazy.value(() => { + if (a.scriptTree) return (0, taprootutils_1.toHashTree)(a.scriptTree); + if (a.hash) return { hash: a.hash }; + return; + }); const network = a.network || networks_1.bitcoin; const o = { name: 'p2tr', network }; lazy.prop(o, 'address', () => { @@ -82,8 +87,8 @@ 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]; @@ -144,9 +149,8 @@ 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 hashTree = _hashTree(); + if (hashTree && a.redeem && a.redeem.output && a.internalPubkey) { const leafHash = (0, taprootutils_1.tapleafHash)({ output: a.redeem.output, version: o.redeemVersion, @@ -204,7 +208,7 @@ function p2tr(a, opts) { throw new TypeError('Invalid pubkey for p2tr'); } if (a.hash && a.scriptTree) { - const hash = (0, taprootutils_1.toHashTree)(a.scriptTree).hash; + const hash = _hashTree().hash; if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch'); } const witness = _witness(); diff --git a/test/fixtures/p2tr.json b/test/fixtures/p2tr.json index 75e0456ab..cf9482c36 100644 --- a/test/fixtures/p2tr.json +++ b/test/fixtures/p2tr.json @@ -312,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": { diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index a254d6ab8..0da70c11e 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -88,6 +88,12 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { return a.witness.slice(); }); + const _hashTree = lazy.value(() => { + if (a.scriptTree) return toHashTree(a.scriptTree); + if (a.hash) return { hash: a.hash }; + return; + }); + const network = a.network || BITCOIN_NETWORK; const o: Payment = { name: 'p2tr', network }; @@ -100,8 +106,8 @@ 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]; @@ -161,9 +167,8 @@ 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 hashTree = _hashTree(); + if (hashTree && a.redeem && a.redeem.output && a.internalPubkey) { const leafHash = tapleafHash({ output: a.redeem.output, version: o.redeemVersion, @@ -227,7 +232,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { } if (a.hash && a.scriptTree) { - const hash = toHashTree(a.scriptTree).hash; + const hash = _hashTree()!.hash; if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch'); } From 5f81cf4decbcdfd743bdad1625f3cf9d027ca940 Mon Sep 17 00:00:00 2001 From: Brandon Black Date: Fri, 18 Mar 2022 15:54:24 -0700 Subject: [PATCH 07/13] Remove unnecessary arrays of values The spec uses this notation because in a spec there's no such thing as reassigning a value. In real code it is appropriate to us accumulators or such. --- src/payments/taprootutils.js | 13 ++++++------- ts_src/payments/taprootutils.ts | 14 ++++++-------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/payments/taprootutils.js b/src/payments/taprootutils.js index d9ebc7bcb..445faf1e8 100644 --- a/src/payments/taprootutils.js +++ b/src/payments/taprootutils.js @@ -7,18 +7,17 @@ const bufferutils_1 = require('../bufferutils'); const types_1 = require('../types'); exports.LEAF_VERSION_TAPSCRIPT = 0xc0; function rootHashFromPath(controlBlock, tapleafMsg) { - const k = [tapleafMsg]; - const e = []; const m = (controlBlock.length - 33) / 32; + let kj = tapleafMsg; 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; diff --git a/ts_src/payments/taprootutils.ts b/ts_src/payments/taprootutils.ts index 7c10e2306..53fc2a6e6 100644 --- a/ts_src/payments/taprootutils.ts +++ b/ts_src/payments/taprootutils.ts @@ -10,21 +10,19 @@ export function rootHashFromPath( controlBlock: Buffer, tapleafMsg: Buffer, ): Buffer { - const k = [tapleafMsg]; - const e = []; - const m = (controlBlock.length - 33) / 32; + let kj = tapleafMsg; 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 { From ce27c13d90581eb85197e8f759f599e0ee78a083 Mon Sep 17 00:00:00 2001 From: Brandon Black Date: Fri, 18 Mar 2022 15:57:05 -0700 Subject: [PATCH 08/13] Improve tapleah hash parameter name --- src/payments/taprootutils.d.ts | 2 +- src/payments/taprootutils.js | 4 ++-- ts_src/payments/taprootutils.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/payments/taprootutils.d.ts b/src/payments/taprootutils.d.ts index 3c4f800c6..1635168c3 100644 --- a/src/payments/taprootutils.d.ts +++ b/src/payments/taprootutils.d.ts @@ -1,7 +1,7 @@ /// import { Tapleaf, Taptree } from '../types'; export declare const LEAF_VERSION_TAPSCRIPT = 192; -export declare function rootHashFromPath(controlBlock: Buffer, tapleafMsg: Buffer): Buffer; +export declare function rootHashFromPath(controlBlock: Buffer, leafHash: Buffer): Buffer; interface HashLeaf { hash: Buffer; } diff --git a/src/payments/taprootutils.js b/src/payments/taprootutils.js index 445faf1e8..0cf1b1657 100644 --- a/src/payments/taprootutils.js +++ b/src/payments/taprootutils.js @@ -6,9 +6,9 @@ const bcrypto = require('../crypto'); const bufferutils_1 = require('../bufferutils'); const types_1 = require('../types'); exports.LEAF_VERSION_TAPSCRIPT = 0xc0; -function rootHashFromPath(controlBlock, tapleafMsg) { +function rootHashFromPath(controlBlock, leafHash) { const m = (controlBlock.length - 33) / 32; - let kj = tapleafMsg; + let kj = leafHash; for (let j = 0; j < m; j++) { const ej = controlBlock.slice(33 + 32 * j, 65 + 32 * j); if (kj.compare(ej) < 0) { diff --git a/ts_src/payments/taprootutils.ts b/ts_src/payments/taprootutils.ts index 53fc2a6e6..fc043f696 100644 --- a/ts_src/payments/taprootutils.ts +++ b/ts_src/payments/taprootutils.ts @@ -8,11 +8,11 @@ export const LEAF_VERSION_TAPSCRIPT = 0xc0; export function rootHashFromPath( controlBlock: Buffer, - tapleafMsg: Buffer, + leafHash: Buffer, ): Buffer { const m = (controlBlock.length - 33) / 32; - let kj = tapleafMsg; + let kj = leafHash; for (let j = 0; j < m; j++) { const ej = controlBlock.slice(33 + 32 * j, 65 + 32 * j); if (kj.compare(ej) < 0) { From 4dd4a51c8eebfc96059471e673be567f3e48dba1 Mon Sep 17 00:00:00 2001 From: Brandon Black Date: Fri, 18 Mar 2022 16:05:56 -0700 Subject: [PATCH 09/13] Fix indentation --- test/fixtures/p2tr.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/fixtures/p2tr.json b/test/fixtures/p2tr.json index cf9482c36..3da6103fd 100644 --- a/test/fixtures/p2tr.json +++ b/test/fixtures/p2tr.json @@ -385,8 +385,8 @@ "input": null, "witness": [ "2050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac4ac", - "c0aba457d16a8d59151c387f24d1eb887efbe24644c1ee64b261282e7baebdb247dac795766bbda1eaeaa45e5bfa0a950fdd5f4c4aada5b1f3082edc9689b9fd0a315fb34a7a93dcaed5e26cf7468be5bd377dda7a4d29128f7dd98db6da9bf04325fff3aa86365bac7534dcb6495867109941ec444dd35294e0706e29e051066d73e0d427bd3249bb921fa78c04fb76511f583ff48c97210d17c2d9dcfbb95023" - ] + "c0aba457d16a8d59151c387f24d1eb887efbe24644c1ee64b261282e7baebdb247dac795766bbda1eaeaa45e5bfa0a950fdd5f4c4aada5b1f3082edc9689b9fd0a315fb34a7a93dcaed5e26cf7468be5bd377dda7a4d29128f7dd98db6da9bf04325fff3aa86365bac7534dcb6495867109941ec444dd35294e0706e29e051066d73e0d427bd3249bb921fa78c04fb76511f583ff48c97210d17c2d9dcfbb95023" + ] } }, { From 5610d4576e0620a67a32464f05c40cb9fe456897 Mon Sep 17 00:00:00 2001 From: Brandon Black Date: Mon, 21 Mar 2022 07:55:57 -0700 Subject: [PATCH 10/13] Add validation for redeem in scriptTree --- src/payments/p2tr.js | 14 +++++++++++--- test/fixtures/p2tr.json | 14 ++++++++++++++ ts_src/payments/p2tr.ts | 16 +++++++++++++--- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index 57087092f..ce30f4662 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -207,9 +207,17 @@ function p2tr(a, opts) { if (!_ecc().isXOnlyPoint(pubkey)) throw new TypeError('Invalid pubkey for p2tr'); } - if (a.hash && a.scriptTree) { - const hash = _hashTree().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 diff --git a/test/fixtures/p2tr.json b/test/fixtures/p2tr.json index 3da6103fd..aaa82fbb4 100644 --- a/test/fixtures/p2tr.json +++ b/test/fixtures/p2tr.json @@ -1175,6 +1175,20 @@ ] } } + }, + { + "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": { diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index 0da70c11e..71f7437a3 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -231,9 +231,19 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { throw new TypeError('Invalid pubkey for p2tr'); } - if (a.hash && a.scriptTree) { - const hash = _hashTree()!.hash; - if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch'); + const hashTree = _hashTree(); + + if (a.hash && hashTree) { + if (!a.hash.equals(hashTree.hash)) throw new TypeError('Hash mismatch'); + } + + if (a.redeem && a.redeem.output && hashTree) { + const leafHash = tapleafHash({ + output: a.redeem.output, + version: o.redeemVersion, + }); + if (!findScriptPath(hashTree, leafHash)) + throw new TypeError('Redeem script not in tree'); } const witness = _witness(); From ef5457efbe37718e44fb186c05d6dedf5cfb0ad7 Mon Sep 17 00:00:00 2001 From: Brandon Black Date: Mon, 21 Mar 2022 07:57:08 -0700 Subject: [PATCH 11/13] Improve comments and code clarity --- src/payments/p2tr.js | 2 +- src/payments/taprootutils.d.ts | 20 ++++++++++------- src/payments/taprootutils.js | 31 +++++++++++-------------- src/types.d.ts | 5 +++++ ts_src/payments/p2tr.ts | 2 +- ts_src/payments/taprootutils.ts | 40 ++++++++++++++++----------------- ts_src/types.ts | 5 +++++ 7 files changed, 57 insertions(+), 48 deletions(-) diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index ce30f4662..c24c16914 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -163,7 +163,7 @@ function p2tr(a, opts) { [ buffer_1.Buffer.from([o.redeemVersion | outputKey.parity]), a.internalPubkey, - ].concat(path.reverse()), + ].concat(path), ); return [a.redeem.output, controlBock]; } diff --git a/src/payments/taprootutils.d.ts b/src/payments/taprootutils.d.ts index 1635168c3..a5739c44f 100644 --- a/src/payments/taprootutils.d.ts +++ b/src/payments/taprootutils.d.ts @@ -10,21 +10,25 @@ interface HashBranch { left: HashTree; right: HashTree; } +/** + * 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 type HashTree = HashLeaf | HashBranch; /** - * 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. */ 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, undefined if no path 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[] | undefined; export declare function tapleafHash(leaf: Tapleaf): Buffer; diff --git a/src/payments/taprootutils.js b/src/payments/taprootutils.js index 0cf1b1657..85576960b 100644 --- a/src/payments/taprootutils.js +++ b/src/payments/taprootutils.js @@ -22,12 +22,8 @@ function rootHashFromPath(controlBlock, leafHash) { 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 ((0, types_1.isTapleaf)(scriptTree)) @@ -43,23 +39,22 @@ function toHashTree(scriptTree) { } exports.toHashTree = toHashTree; /** - * 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, undefined if no path 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 (!isHashBranch(node)) { - if (node.hash.equals(hash)) { - return []; - } else { - return undefined; - } + if (isHashBranch(node)) { + const leftPath = findScriptPath(node.left, hash); + if (leftPath !== undefined) return [...leftPath, node.right.hash]; + const rightPath = findScriptPath(node.right, hash); + if (rightPath !== undefined) return [...rightPath, node.left.hash]; + } else if (node.hash.equals(hash)) { + return []; } - const leftPath = findScriptPath(node.left, hash); - if (leftPath !== undefined) return [node.right.hash, ...leftPath]; - const rightPath = findScriptPath(node.right, hash); - if (rightPath !== undefined) return [node.left.hash, ...rightPath]; return undefined; } exports.findScriptPath = findScriptPath; diff --git a/src/types.d.ts b/src/types.d.ts index c8048c29a..b3d93589d 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -20,6 +20,11 @@ export interface 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 { diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index 71f7437a3..47a76a114 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -181,7 +181,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { [ NBuffer.from([o.redeemVersion! | outputKey.parity]), a.internalPubkey, - ].concat(path.reverse()), + ].concat(path), ); return [a.redeem.output, controlBock]; } diff --git a/ts_src/payments/taprootutils.ts b/ts_src/payments/taprootutils.ts index fc043f696..97cc1f6d8 100644 --- a/ts_src/payments/taprootutils.ts +++ b/ts_src/payments/taprootutils.ts @@ -38,15 +38,17 @@ interface HashBranch { const isHashBranch = (ht: HashTree): ht is HashBranch => 'left' in ht && 'right' in ht; +/** + * 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 type HashTree = HashLeaf | HashBranch; /** - * 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. */ export function toHashTree(scriptTree: Taptree): HashTree { if (isTapleaf(scriptTree)) return { hash: tapleafHash(scriptTree) }; @@ -63,29 +65,27 @@ export 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, undefined if no path 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[] | undefined { - if (!isHashBranch(node)) { - if (node.hash.equals(hash)) { - return []; - } else { - return undefined; - } + if (isHashBranch(node)) { + const leftPath = findScriptPath(node.left, hash); + if (leftPath !== undefined) return [...leftPath, node.right.hash]; + + const rightPath = findScriptPath(node.right, hash); + if (rightPath !== undefined) return [...rightPath, node.left.hash]; + } else if (node.hash.equals(hash)) { + return []; } - const leftPath = findScriptPath(node.left, hash); - if (leftPath !== undefined) return [node.right.hash, ...leftPath]; - - const rightPath = findScriptPath(node.right, hash); - if (rightPath !== undefined) return [node.left.hash, ...rightPath]; - return undefined; } diff --git a/ts_src/types.ts b/ts_src/types.ts index 1e49361b6..536646e86 100644 --- a/ts_src/types.ts +++ b/ts_src/types.ts @@ -86,6 +86,11 @@ export function isTapleaf(o: any): o is Tapleaf { 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 { From c5a3e1e12ca7aaa7e1ef83c4292cbff6764ea33e Mon Sep 17 00:00:00 2001 From: Brandon Black Date: Mon, 21 Mar 2022 13:40:08 -0700 Subject: [PATCH 12/13] Remove need for ECC lib from PSBT * Add basic isXOnlyPoint check to types. * Use basic isXOnlyPoint check in p2tr * Only use ECC lib for tweaking in p2tr * Remove ECC lib from PSBT * tweaking is done by provided `finalizeScriptsFunc` * Update test vectors to reflect these changes --- src/address.d.ts | 5 +-- src/address.js | 11 +++-- src/payments/index.d.ts | 5 ++- src/payments/p2tr.js | 65 ++++++++++++++++++++++------ src/payments/verifyecc.d.ts | 2 - src/payments/verifyecc.js | 72 ------------------------------- src/psbt.d.ts | 5 +-- src/psbt.js | 36 +++++----------- src/types.d.ts | 3 ++ src/types.js | 22 +++++----- test/address.spec.ts | 12 ++---- test/fixtures/p2tr.json | 8 ++-- test/integration/taproot.spec.ts | 27 ++++++------ test/payments.spec.ts | 13 +++--- test/psbt.spec.ts | 14 +++--- test/psbt.utils.ts | 5 +-- ts_src/address.ts | 28 +++--------- ts_src/payments/index.ts | 9 +++- ts_src/payments/p2tr.ts | 72 ++++++++++++++++++++++++------- ts_src/payments/verifyecc.ts | 74 -------------------------------- ts_src/psbt.ts | 48 ++++++--------------- ts_src/types.ts | 26 ++++++----- 22 files changed, 221 insertions(+), 341 deletions(-) delete mode 100644 src/payments/verifyecc.d.ts delete mode 100644 src/payments/verifyecc.js delete mode 100644 ts_src/payments/verifyecc.ts 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 c24c16914..10a21d453 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -8,7 +8,6 @@ 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; @@ -22,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)( { @@ -132,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; } }); @@ -157,7 +156,7 @@ function p2tr(a, opts) { }); const path = (0, taprootutils_1.findScriptPath)(hashTree, leafHash); if (!path) return; - const outputKey = tweakKey(a.internalPubkey, hashTree.hash, _ecc()); + const outputKey = tweakKey(a.internalPubkey, hashTree.hash, _tweakFn()); if (!outputKey) return; const controlBock = buffer_1.Buffer.concat( [ @@ -198,13 +197,13 @@ 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'); } const hashTree = _hashTree(); @@ -267,7 +266,7 @@ 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] & types_1.TAPLEAF_VERSION_MASK; const script = witness[witness.length - 2]; @@ -279,7 +278,7 @@ function p2tr(a, opts) { 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'); @@ -293,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, @@ -311,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/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 694a6cc1b..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,7 +1059,7 @@ 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), ); @@ -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 b3d93589d..c693844ae 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,5 +1,7 @@ /// export declare const typeforce: any; +declare function isFieldElement(c: Buffer | number | undefined | null): boolean; +export declare const isXOnlyPoint: typeof isFieldElement; export declare function isPoint(p: Buffer | number | undefined | null): boolean; export declare function UInt31(value: number): boolean; export declare function BIP32Path(value: string): boolean; @@ -50,3 +52,4 @@ export declare const Function: any; export declare const BufferN: any; export declare const Null: any; export declare const oneOf: any; +export {}; diff --git a/src/types.js b/src/types.js index e1d0a528d..4876172dc 100644 --- a/src/types.js +++ b/src/types.js @@ -1,6 +1,6 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -exports.oneOf = exports.Null = exports.BufferN = exports.Function = exports.UInt32 = exports.UInt8 = exports.tuple = exports.maybe = exports.Hex = exports.Buffer = exports.String = exports.Boolean = exports.Array = exports.Number = exports.Hash256bit = exports.Hash160bit = exports.Buffer256bit = exports.isTaptree = exports.isTapleaf = exports.TAPLEAF_VERSION_MASK = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.isPoint = exports.typeforce = void 0; +exports.oneOf = exports.Null = exports.BufferN = exports.Function = exports.UInt32 = exports.UInt8 = exports.tuple = exports.maybe = exports.Hex = exports.Buffer = exports.String = exports.Boolean = exports.Array = exports.Number = exports.Hash256bit = exports.Hash160bit = exports.Buffer256bit = exports.isTaptree = exports.isTapleaf = exports.TAPLEAF_VERSION_MASK = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.isPoint = exports.isXOnlyPoint = exports.typeforce = void 0; const buffer_1 = require('buffer'); exports.typeforce = require('typeforce'); const ZERO32 = buffer_1.Buffer.alloc(32, 0); @@ -8,19 +8,21 @@ const EC_P = buffer_1.Buffer.from( 'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f', 'hex', ); +function isFieldElement(c) { + if (!buffer_1.Buffer.isBuffer(c)) return false; + if (c.length !== 32) return false; + if (c.compare(ZERO32) === 0) return false; + if (c.compare(EC_P) >= 0) return false; + return true; +} +exports.isXOnlyPoint = isFieldElement; 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; - } - const y = p.slice(33); - if (y.compare(ZERO32) === 0) return false; - if (y.compare(EC_P) >= 0) return false; + if (!isFieldElement(p.slice(1, 33))) return false; + if ((t === 0x02 || t === 0x03) && p.length === 33) return true; + if (!isFieldElement(p.slice(33))) return false; if (t === 0x04 && p.length === 65) return true; return false; } 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 aaa82fbb4..3d809389b 100644 --- a/test/fixtures/p2tr.json +++ b/test/fixtures/p2tr.json @@ -863,21 +863,21 @@ "description": "Invalid x coordinate for pubkey in pubkey", "exception": "Invalid pubkey for p2tr", "arguments": { - "pubkey": "f136e956540197c21ff3c075d32a6e3c82f1ee1e646cc0f08f51b0b5edafa762" + "pubkey": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" } }, { "description": "Invalid x coordinate for pubkey in output", "exception": "Invalid pubkey for p2tr", "arguments": { - "output": "OP_1 f136e956540197c21ff3c075d32a6e3c82f1ee1e646cc0f08f51b0b5edafa762" + "output": "OP_1 ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" } }, { "description": "Invalid x coordinate for pubkey in address", "exception": "Invalid pubkey for p2tr", "arguments": { - "address": "bc1p7ymwj4j5qxtuy8lncp6ax2nw8jp0rms7v3kvpuy02xcttmd05a3qmwlnez" + "address": "bc1plllllllllllllllllllllllllllllllllllllllllllllllllllsr7rg6v" } }, { @@ -1007,7 +1007,7 @@ "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", - "c14444444444444444453d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c" + "c1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff68f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c" ] } }, diff --git a/test/integration/taproot.spec.ts b/test/integration/taproot.spec.ts index 05d7d154d..804b43f26 100644 --- a/test/integration/taproot.spec.ts +++ b/test/integration/taproot.spec.ts @@ -17,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 @@ -52,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 @@ -63,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, @@ -108,7 +111,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { scriptTree, network: regtest, }, - { eccLib: ecc }, + { tweakFn: ecc.xOnlyPointAddTweak }, ); // amount from faucet @@ -118,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, @@ -213,7 +216,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { redeem, network: regtest, }, - { eccLib: ecc }, + { tweakFn: ecc.xOnlyPointAddTweak }, ); // amount from faucet @@ -223,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, @@ -290,7 +293,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { redeem, network: regtest, }, - { eccLib: ecc }, + { tweakFn: ecc.xOnlyPointAddTweak }, ); // amount from faucet @@ -300,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, @@ -389,7 +392,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { redeem, network: regtest, }, - { eccLib: ecc }, + { tweakFn: ecc.xOnlyPointAddTweak }, ); // amount from faucet @@ -399,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 47a76a114..aed87db08 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -4,7 +4,7 @@ import * as bscript from '../script'; import { typeforce as typef, isTaptree, - TinySecp256k1Interface, + isXOnlyPoint, TAPLEAF_VERSION_MASK, } from '../types'; import { @@ -15,10 +15,9 @@ import { tapTweakHash, 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; @@ -36,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( @@ -149,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; } }); @@ -175,7 +174,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { }); const path = findScriptPath(hashTree, leafHash); if (!path) return; - const outputKey = tweakKey(a.internalPubkey, hashTree.hash, _ecc()); + const outputKey = tweakKey(a.internalPubkey, hashTree.hash, _tweakFn()); if (!outputKey) return; const controlBock = NBuffer.concat( [ @@ -220,15 +219,14 @@ 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(); @@ -302,7 +300,7 @@ 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] & TAPLEAF_VERSION_MASK; @@ -311,7 +309,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { 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'); @@ -336,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; @@ -344,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 { @@ -360,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/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 479421636..1517acffd 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -22,7 +22,6 @@ import * as payments from './payments'; import * as bscript from './script'; import { Output, Transaction } from './transaction'; import { tapleafHash } from './payments/taprootutils'; -import { TinySecp256k1Interface } from './types'; 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,7 +1381,7 @@ 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), ); @@ -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 536646e86..99faafd86 100644 --- a/ts_src/types.ts +++ b/ts_src/types.ts @@ -8,22 +8,28 @@ const EC_P = NBuffer.from( 'hex', ); +function isFieldElement(c: Buffer | number | undefined | null): boolean { + if (!NBuffer.isBuffer(c)) return false; + if (c.length !== 32) return false; + if (c.compare(ZERO32) === 0) return false; + if (c.compare(EC_P) >= 0) return false; + return true; +} + +export const isXOnlyPoint = isFieldElement; + 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; - } - - const y = p.slice(33); - if (y.compare(ZERO32) === 0) return false; - if (y.compare(EC_P) >= 0) return false; + + if (!isFieldElement(p.slice(1, 33))) return false; + if ((t === 0x02 || t === 0x03) && p.length === 33) return true; + + if (!isFieldElement(p.slice(33))) return false; if (t === 0x04 && p.length === 65) return true; + return false; } From 78b20db0e5dda03b4d77b00f9a6b4c0d6ccd086e Mon Sep 17 00:00:00 2001 From: Brandon Black Date: Tue, 22 Mar 2022 20:40:22 -0700 Subject: [PATCH 13/13] Add internal curve checking, using BigInt --- src/types.d.ts | 5 +-- src/types.js | 76 ++++++++++++++++++++++++++++++--------- test/fixtures/p2tr.json | 8 ++--- ts_src/types.ts | 79 +++++++++++++++++++++++++++++++---------- 4 files changed, 125 insertions(+), 43 deletions(-) diff --git a/src/types.d.ts b/src/types.d.ts index c693844ae..b905d44fa 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,8 +1,7 @@ /// export declare const typeforce: any; -declare function isFieldElement(c: Buffer | number | undefined | null): boolean; -export declare const isXOnlyPoint: typeof isFieldElement; 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 { @@ -30,7 +29,6 @@ export declare function isTapleaf(o: any): o is Tapleaf; export declare type Taptree = [Taptree | Tapleaf, Taptree | Tapleaf] | Tapleaf; export declare function isTaptree(scriptTree: any): scriptTree is Taptree; export interface TinySecp256k1Interface { - isXOnlyPoint(p: Uint8Array): boolean; xOnlyPointAddTweak(p: Uint8Array, tweak: Uint8Array): XOnlyPointAddTweakResult | null; privateAdd(d: Uint8Array, tweak: Uint8Array): Uint8Array | null; privateNegate(d: Uint8Array): Uint8Array; @@ -52,4 +50,3 @@ export declare const Function: any; export declare const BufferN: any; export declare const Null: any; export declare const oneOf: any; -export {}; diff --git a/src/types.js b/src/types.js index 4876172dc..eddb13b06 100644 --- a/src/types.js +++ b/src/types.js @@ -1,32 +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.isTaptree = exports.isTapleaf = exports.TAPLEAF_VERSION_MASK = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.isPoint = exports.isXOnlyPoint = 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`, ); -function isFieldElement(c) { - if (!buffer_1.Buffer.isBuffer(c)) return false; - if (c.length !== 32) return false; - if (c.compare(ZERO32) === 0) return false; - if (c.compare(EC_P) >= 0) return false; - return true; +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; } -exports.isXOnlyPoint = isFieldElement; function isPoint(p) { if (!buffer_1.Buffer.isBuffer(p)) return false; if (p.length < 33) return false; const t = p[0]; - if (!isFieldElement(p.slice(1, 33))) return false; - if ((t === 0x02 || t === 0x03) && p.length === 33) return true; - if (!isFieldElement(p.slice(33))) return false; - if (t === 0x04 && p.length === 65) return true; - return false; + if (p.length === 33) { + return (t === 0x02 || t === 0x03) && isXOnlyPoint(p.slice(1)); + } + 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; diff --git a/test/fixtures/p2tr.json b/test/fixtures/p2tr.json index 3d809389b..aaa82fbb4 100644 --- a/test/fixtures/p2tr.json +++ b/test/fixtures/p2tr.json @@ -863,21 +863,21 @@ "description": "Invalid x coordinate for pubkey in pubkey", "exception": "Invalid pubkey for p2tr", "arguments": { - "pubkey": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + "pubkey": "f136e956540197c21ff3c075d32a6e3c82f1ee1e646cc0f08f51b0b5edafa762" } }, { "description": "Invalid x coordinate for pubkey in output", "exception": "Invalid pubkey for p2tr", "arguments": { - "output": "OP_1 ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + "output": "OP_1 f136e956540197c21ff3c075d32a6e3c82f1ee1e646cc0f08f51b0b5edafa762" } }, { "description": "Invalid x coordinate for pubkey in address", "exception": "Invalid pubkey for p2tr", "arguments": { - "address": "bc1plllllllllllllllllllllllllllllllllllllllllllllllllllsr7rg6v" + "address": "bc1p7ymwj4j5qxtuy8lncp6ax2nw8jp0rms7v3kvpuy02xcttmd05a3qmwlnez" } }, { @@ -1007,7 +1007,7 @@ "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", - "c1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff68f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c" + "c14444444444444444453d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c" ] } }, diff --git a/ts_src/types.ts b/ts_src/types.ts index 99faafd86..6a0076734 100644 --- a/ts_src/types.ts +++ b/ts_src/types.ts @@ -2,35 +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`, ); - -function isFieldElement(c: Buffer | number | undefined | null): boolean { - if (!NBuffer.isBuffer(c)) return false; - if (c.length !== 32) return false; - if (c.compare(ZERO32) === 0) return false; - if (c.compare(EC_P) >= 0) return false; - return true; +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; } -export const isXOnlyPoint = isFieldElement; +// 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]; + if (p.length === 33) { + return (t === 0x02 || t === 0x03) && isXOnlyPoint(p.slice(1)); + } + + if (t !== 0x04 || p.length !== 65) return false; - if (!isFieldElement(p.slice(1, 33))) return false; - if ((t === 0x02 || t === 0x03) && p.length === 33) return true; + const x = BigInt(`0x${p.slice(1, 33).toString('hex')}`); + if (x === _0n) return false; + if (x >= EC_P) return false; - if (!isFieldElement(p.slice(33))) return false; - if (t === 0x04 && p.length === 65) return true; + const y = BigInt(`0x${p.slice(33).toString('hex')}`); + if (y === _0n) return false; + if (y >= EC_P) return false; - 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; @@ -106,7 +150,6 @@ export function isTaptree(scriptTree: any): scriptTree is Taptree { } export interface TinySecp256k1Interface { - isXOnlyPoint(p: Uint8Array): boolean; xOnlyPointAddTweak( p: Uint8Array, tweak: Uint8Array,