diff --git a/package-lock.json b/package-lock.json index 3e5bd51b5..956740594 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "bitcoinjs-lib", - "version": "6.0.1", + "version": "6.0.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -606,12 +606,6 @@ "integrity": "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow==", "dev": true }, - "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "dev": true - }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1972,9 +1966,9 @@ "dev": true }, "pbkdf2": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", - "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", "dev": true, "requires": { "create-hash": "^1.1.2", @@ -2374,9 +2368,9 @@ } }, "tiny-secp256k1": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.1.2.tgz", - "integrity": "sha512-8qPw7zDK6Hco2tVGYGQeOmOPp/hZnREwy2iIkcq0ygAuqc9WHo29vKN94lNymh1QbB3nthtAMF6KTIrdbsIotA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.0.tgz", + "integrity": "sha512-2hPuUGCroLrxh6xxwoe+1RgPpOOK1w2uTnhgiHBpvoutBR+krNuT4hOXQyOaaYnZgoXBB6hBYkuAJHxyeBOPzQ==", "dev": true, "requires": { "uint8array-tools": "0.0.6" diff --git a/package.json b/package.json index c5553d57c..8666bb478 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bitcoinjs-lib", - "version": "6.0.1", + "version": "6.0.2", "description": "Client-side Bitcoin JavaScript library", "main": "./src/index.js", "types": "./src/index.d.ts", @@ -70,7 +70,6 @@ "bip39": "^3.0.2", "bip65": "^1.0.1", "bip68": "^1.0.3", - "bn.js": "^4.11.8", "bs58": "^4.0.0", "dhttp": "^3.0.0", "ecpair": "^2.0.1", @@ -84,7 +83,7 @@ "randombytes": "^2.1.0", "regtest-client": "0.2.0", "rimraf": "^2.6.3", - "tiny-secp256k1": "^2.1.2", + "tiny-secp256k1": "^2.2.0", "ts-node": "^8.3.0", "tslint": "^6.1.3", "typescript": "^4.4.4" diff --git a/src/address.js b/src/address.js index 164bf7ef1..de0154a3a 100644 --- a/src/address.js +++ b/src/address.js @@ -4,14 +4,13 @@ exports.toOutputScript = exports.fromOutputScript = exports.toBech32 = exports.t const networks = require('./networks'); const payments = require('./payments'); const bscript = require('./script'); -const types = require('./types'); +const types_1 = require('./types'); const bech32_1 = require('bech32'); const bs58check = require('bs58check'); -const { typeforce } = types; const FUTURE_SEGWIT_MAX_SIZE = 40; const FUTURE_SEGWIT_MIN_SIZE = 2; const FUTURE_SEGWIT_MAX_VERSION = 16; -const FUTURE_SEGWIT_MIN_VERSION = 1; +const FUTURE_SEGWIT_MIN_VERSION = 2; const FUTURE_SEGWIT_VERSION_DIFF = 0x50; const FUTURE_SEGWIT_VERSION_WARNING = 'WARNING: Sending to a future segwit version address can lead to loss of funds. ' + @@ -69,7 +68,10 @@ function fromBech32(address) { } exports.fromBech32 = fromBech32; function toBase58Check(hash, version) { - typeforce(types.tuple(types.Hash160bit, types.UInt8), arguments); + (0, types_1.typeforce)( + (0, types_1.tuple)(types_1.Hash160bit, types_1.UInt8), + arguments, + ); const payload = Buffer.allocUnsafe(21); payload.writeUInt8(version, 0); hash.copy(payload, 1); @@ -99,6 +101,9 @@ function fromOutputScript(output, network) { try { return payments.p2wsh({ output, network }).address; } catch (e) {} + try { + return payments.p2tr({ output, network }).address; + } catch (e) {} try { return _toFutureSegwitAddress(output, network); } catch (e) {} @@ -129,6 +134,9 @@ function toOutputScript(address, network) { return payments.p2wpkh({ hash: decodeBech32.data }).output; if (decodeBech32.data.length === 32) return payments.p2wsh({ hash: decodeBech32.data }).output; + } else if (decodeBech32.version === 1) { + 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/ecc_lib.d.ts b/src/ecc_lib.d.ts new file mode 100644 index 000000000..201ebb5cf --- /dev/null +++ b/src/ecc_lib.d.ts @@ -0,0 +1,3 @@ +import { TinySecp256k1Interface } from './types'; +export declare function initEccLib(eccLib: TinySecp256k1Interface | undefined): void; +export declare function getEccLib(): TinySecp256k1Interface; diff --git a/src/ecc_lib.js b/src/ecc_lib.js new file mode 100644 index 000000000..eaa8a5327 --- /dev/null +++ b/src/ecc_lib.js @@ -0,0 +1,91 @@ +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); +exports.getEccLib = exports.initEccLib = void 0; +const _ECCLIB_CACHE = {}; +function initEccLib(eccLib) { + if (!eccLib) { + // allow clearing the library + _ECCLIB_CACHE.eccLib = eccLib; + } else if (eccLib !== _ECCLIB_CACHE.eccLib) { + // new instance, verify it + verifyEcc(eccLib); + _ECCLIB_CACHE.eccLib = eccLib; + } +} +exports.initEccLib = initEccLib; +function getEccLib() { + if (!_ECCLIB_CACHE.eccLib) + throw new Error( + 'No ECC Library provided. You must call initEccLib() with a valid TinySecp256k1Interface instance', + ); + return _ECCLIB_CACHE.eccLib; +} +exports.getEccLib = getEccLib; +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))); + } + }); +} +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/index.d.ts b/src/index.d.ts index b93c2aa40..420979ffe 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -12,3 +12,4 @@ export { Transaction } from './transaction'; export { Network } from './networks'; export { Payment, PaymentCreator, PaymentOpts, Stack, StackElement, } from './payments'; export { Input as TxInput, Output as TxOutput } from './transaction'; +export { initEccLib } from './ecc_lib'; diff --git a/src/index.js b/src/index.js index 983b0cc76..25d0b5a22 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -exports.Transaction = exports.opcodes = exports.Psbt = exports.Block = exports.script = exports.payments = exports.networks = exports.crypto = exports.address = void 0; +exports.initEccLib = exports.Transaction = exports.opcodes = exports.Psbt = exports.Block = exports.script = exports.payments = exports.networks = exports.crypto = exports.address = void 0; const address = require('./address'); exports.address = address; const crypto = require('./crypto'); @@ -39,3 +39,10 @@ Object.defineProperty(exports, 'Transaction', { return transaction_1.Transaction; }, }); +var ecc_lib_1 = require('./ecc_lib'); +Object.defineProperty(exports, 'initEccLib', { + enumerable: true, + get: function() { + return ecc_lib_1.initEccLib; + }, +}); diff --git a/src/ops.js b/src/ops.js index 9d629cd00..7853ad0f0 100644 --- a/src/ops.js +++ b/src/ops.js @@ -117,6 +117,7 @@ const OPS = { OP_NOP8: 183, OP_NOP9: 184, OP_NOP10: 185, + OP_CHECKSIGADD: 186, OP_PUBKEYHASH: 253, OP_PUBKEY: 254, OP_INVALIDOPCODE: 255, diff --git a/src/payments/index.d.ts b/src/payments/index.d.ts index 1edf07167..c53c7443d 100644 --- a/src/payments/index.d.ts +++ b/src/payments/index.d.ts @@ -1,5 +1,6 @@ /// import { Network } from '../networks'; +import { Taptree } from '../types'; import { p2data as embed } from './embed'; import { p2ms } from './p2ms'; import { p2pk } from './p2pk'; @@ -7,6 +8,8 @@ import { p2pkh } from './p2pkh'; import { p2sh } from './p2sh'; import { p2wpkh } from './p2wpkh'; import { p2wsh } from './p2wsh'; +import { p2tr } from './p2tr'; +import { p2tr_ns } from './p2tr_ns'; export interface Payment { name?: string; network?: Network; @@ -17,11 +20,14 @@ export interface Payment { pubkeys?: Buffer[]; input?: Buffer; signatures?: Buffer[]; + internalPubkey?: Buffer; pubkey?: Buffer; signature?: Buffer; address?: string; hash?: Buffer; redeem?: Payment; + redeemVersion?: number; + scriptTree?: Taptree; witness?: Buffer[]; } export declare type PaymentCreator = (a: Payment, opts?: PaymentOpts) => Payment; @@ -33,4 +39,4 @@ export interface PaymentOpts { export declare type StackElement = Buffer | number; export declare type Stack = StackElement[]; export declare type StackFunction = () => Stack; -export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh }; +export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh, p2tr, p2tr_ns }; diff --git a/src/payments/index.js b/src/payments/index.js index c23c529c6..5e0de02e2 100644 --- a/src/payments/index.js +++ b/src/payments/index.js @@ -1,6 +1,6 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -exports.p2wsh = exports.p2wpkh = exports.p2sh = exports.p2pkh = exports.p2pk = exports.p2ms = exports.embed = void 0; +exports.p2tr_ns = exports.p2tr = exports.p2wsh = exports.p2wpkh = exports.p2sh = exports.p2pkh = exports.p2pk = exports.p2ms = exports.embed = void 0; const embed_1 = require('./embed'); Object.defineProperty(exports, 'embed', { enumerable: true, @@ -50,5 +50,19 @@ Object.defineProperty(exports, 'p2wsh', { return p2wsh_1.p2wsh; }, }); +const p2tr_1 = require('./p2tr'); +Object.defineProperty(exports, 'p2tr', { + enumerable: true, + get: function() { + return p2tr_1.p2tr; + }, +}); +const p2tr_ns_1 = require('./p2tr_ns'); +Object.defineProperty(exports, 'p2tr_ns', { + enumerable: true, + get: function() { + return p2tr_ns_1.p2tr_ns; + }, +}); // TODO // witness commitment diff --git a/src/payments/p2tr.d.ts b/src/payments/p2tr.d.ts new file mode 100644 index 000000000..350ed0ffc --- /dev/null +++ b/src/payments/p2tr.d.ts @@ -0,0 +1,2 @@ +import { Payment, PaymentOpts } from './index'; +export declare function p2tr(a: Payment, opts?: PaymentOpts): Payment; diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js new file mode 100644 index 000000000..8b0819fab --- /dev/null +++ b/src/payments/p2tr.js @@ -0,0 +1,308 @@ +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); +exports.p2tr = void 0; +const buffer_1 = require('buffer'); +const networks_1 = require('../networks'); +const bscript = require('../script'); +const types_1 = require('../types'); +const ecc_lib_1 = require('../ecc_lib'); +const taprootutils_1 = require('./taprootutils'); +const lazy = require('./lazy'); +const bech32_1 = require('bech32'); +const OPS = bscript.OPS; +const TAPROOT_WITNESS_VERSION = 0x01; +const ANNEX_PREFIX = 0x50; +function p2tr(a, opts) { + if ( + !a.address && + !a.output && + !a.pubkey && + !a.internalPubkey && + !(a.witness && a.witness.length > 1) + ) + throw new TypeError('Not enough data'); + opts = Object.assign({ validate: true }, opts || {}); + (0, types_1.typeforce)( + { + address: types_1.typeforce.maybe(types_1.typeforce.String), + input: types_1.typeforce.maybe(types_1.typeforce.BufferN(0)), + network: types_1.typeforce.maybe(types_1.typeforce.Object), + output: types_1.typeforce.maybe(types_1.typeforce.BufferN(34)), + internalPubkey: types_1.typeforce.maybe(types_1.typeforce.BufferN(32)), + hash: types_1.typeforce.maybe(types_1.typeforce.BufferN(32)), + pubkey: types_1.typeforce.maybe(types_1.typeforce.BufferN(32)), + signature: types_1.typeforce.maybe(types_1.typeforce.BufferN(64)), + witness: types_1.typeforce.maybe( + types_1.typeforce.arrayOf(types_1.typeforce.Buffer), + ), + 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), + witness: types_1.typeforce.maybe( + types_1.typeforce.arrayOf(types_1.typeforce.Buffer), + ), + }), + redeemVersion: types_1.typeforce.maybe(types_1.typeforce.Number), + }, + a, + ); + const _address = lazy.value(() => { + const result = bech32_1.bech32m.decode(a.address); + const version = result.words.shift(); + const data = bech32_1.bech32m.fromWords(result.words); + return { + version, + prefix: result.prefix, + data: buffer_1.Buffer.from(data), + }; + }); + // remove annex if present, ignored by taproot + const _witness = lazy.value(() => { + if (!a.witness || !a.witness.length) return; + if ( + a.witness.length >= 2 && + a.witness[a.witness.length - 1][0] === ANNEX_PREFIX + ) { + return a.witness.slice(0, -1); + } + 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', () => { + if (!o.pubkey) return; + const words = bech32_1.bech32m.toWords(o.pubkey); + words.unshift(TAPROOT_WITNESS_VERSION); + return bech32_1.bech32m.encode(network.bech32, words); + }); + lazy.prop(o, 'hash', () => { + const hashTree = _hashTree(); + if (hashTree) return hashTree.hash; + const w = _witness(); + if (w && w.length > 1) { + const controlBlock = w[w.length - 1]; + const leafVersion = controlBlock[0] & types_1.TAPLEAF_VERSION_MASK; + const script = w[w.length - 2]; + const leafHash = (0, taprootutils_1.tapleafHash)({ + output: script, + redeemVersion: leafVersion, + }); + return (0, taprootutils_1.rootHashFromPath)(controlBlock, leafHash); + } + return null; + }); + lazy.prop(o, 'output', () => { + if (!o.pubkey) return; + return bscript.compile([OPS.OP_1, o.pubkey]); + }); + lazy.prop(o, 'redeemVersion', () => { + if (a.redeemVersion) return a.redeemVersion; + if ( + a.redeem && + a.redeem.redeemVersion !== undefined && + a.redeem.redeemVersion !== null + ) { + return a.redeem.redeemVersion; + } + return taprootutils_1.LEAF_VERSION_TAPSCRIPT; + }); + lazy.prop(o, 'redeem', () => { + const witness = _witness(); // witness without annex + if (!witness || witness.length < 2) return; + return { + output: witness[witness.length - 2], + witness: witness.slice(0, -2), + redeemVersion: + witness[witness.length - 1][0] & types_1.TAPLEAF_VERSION_MASK, + }; + }); + lazy.prop(o, 'pubkey', () => { + if (a.pubkey) return a.pubkey; + if (a.output) return a.output.slice(2); + if (a.address) return _address().data; + if (o.internalPubkey) { + const tweakedKey = tweakKey(o.internalPubkey, o.hash); + if (tweakedKey) return tweakedKey.x; + } + }); + lazy.prop(o, 'internalPubkey', () => { + if (a.internalPubkey) return a.internalPubkey; + const witness = _witness(); + if (witness && witness.length > 1) + return witness[witness.length - 1].slice(1, 33); + }); + lazy.prop(o, 'signature', () => { + if (a.signature) return a.signature; + if (!a.witness || a.witness.length !== 1) return; + return a.witness[0]; + }); + lazy.prop(o, 'witness', () => { + if (a.witness) return a.witness; + const hashTree = _hashTree(); + if (hashTree && a.redeem && a.redeem.output && a.internalPubkey) { + const leafHash = (0, taprootutils_1.tapleafHash)({ + output: a.redeem.output, + redeemVersion: o.redeemVersion, + }); + const path = (0, taprootutils_1.findScriptPath)(hashTree, leafHash); + if (!path) return; + const outputKey = tweakKey(a.internalPubkey, hashTree.hash); + if (!outputKey) return; + const controlBock = buffer_1.Buffer.concat( + [ + buffer_1.Buffer.from([o.redeemVersion | outputKey.parity]), + a.internalPubkey, + ].concat(path), + ); + return [a.redeem.output, controlBock]; + } + if (a.signature) return [a.signature]; + }); + // extended validation + if (opts.validate) { + let pubkey = buffer_1.Buffer.from([]); + if (a.address) { + if (network && network.bech32 !== _address().prefix) + throw new TypeError('Invalid prefix or Network mismatch'); + if (_address().version !== TAPROOT_WITNESS_VERSION) + throw new TypeError('Invalid address version'); + if (_address().data.length !== 32) + throw new TypeError('Invalid address data'); + pubkey = _address().data; + } + if (a.pubkey) { + if (pubkey.length > 0 && !pubkey.equals(a.pubkey)) + throw new TypeError('Pubkey mismatch'); + else pubkey = a.pubkey; + } + if (a.output) { + if ( + a.output.length !== 34 || + a.output[0] !== OPS.OP_1 || + a.output[1] !== 0x20 + ) + throw new TypeError('Output is invalid'); + if (pubkey.length > 0 && !pubkey.equals(a.output.slice(2))) + throw new TypeError('Pubkey mismatch'); + else pubkey = a.output.slice(2); + } + if (a.internalPubkey) { + const tweakedKey = tweakKey(a.internalPubkey, o.hash); + if (pubkey.length > 0 && !pubkey.equals(tweakedKey.x)) + throw new TypeError('Pubkey mismatch'); + else pubkey = tweakedKey.x; + } + if (pubkey && pubkey.length) { + if (!(0, ecc_lib_1.getEccLib)().isXOnlyPoint(pubkey)) + throw new TypeError('Invalid pubkey for p2tr'); + } + const hashTree = _hashTree(); + if (a.hash && hashTree) { + if (!a.hash.equals(hashTree.hash)) throw new TypeError('Hash mismatch'); + } + if (a.redeem && a.redeem.output && hashTree) { + const leafHash = (0, taprootutils_1.tapleafHash)({ + output: a.redeem.output, + redeemVersion: 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 + if (a.redeem && o.redeem) { + if (a.redeem.redeemVersion) { + if (a.redeem.redeemVersion !== o.redeem.redeemVersion) + throw new TypeError('Redeem.redeemVersion and witness mismatch'); + } + if (a.redeem.output) { + if (bscript.decompile(a.redeem.output).length === 0) + throw new TypeError('Redeem.output is invalid'); + // output redeem is constructed from the witness + if (o.redeem.output && !a.redeem.output.equals(o.redeem.output)) + throw new TypeError('Redeem.output and witness mismatch'); + } + if (a.redeem.witness) { + if ( + o.redeem.witness && + !stacksEqual(a.redeem.witness, o.redeem.witness) + ) + throw new TypeError('Redeem.witness and witness mismatch'); + } + } + if (witness && witness.length) { + if (witness.length === 1) { + // key spending + if (a.signature && !a.signature.equals(witness[0])) + throw new TypeError('Signature mismatch'); + } else { + // script path spending + const controlBlock = witness[witness.length - 1]; + if (controlBlock.length < 33) + throw new TypeError( + `The control-block length is too small. Got ${ + controlBlock.length + }, expected min 33.`, + ); + if ((controlBlock.length - 33) % 32 !== 0) + throw new TypeError( + `The control-block length of ${controlBlock.length} is incorrect!`, + ); + const m = (controlBlock.length - 33) / 32; + if (m > 128) + throw new TypeError( + `The script path is too long. Got ${m}, expected max 128.`, + ); + const internalPubkey = controlBlock.slice(1, 33); + if (a.internalPubkey && !a.internalPubkey.equals(internalPubkey)) + throw new TypeError('Internal pubkey mismatch'); + if (!(0, ecc_lib_1.getEccLib)().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]; + const leafHash = (0, taprootutils_1.tapleafHash)({ + output: script, + redeemVersion: leafVersion, + }); + const hash = (0, taprootutils_1.rootHashFromPath)( + controlBlock, + leafHash, + ); + const outputKey = tweakKey(internalPubkey, hash); + if (!outputKey) + // todo: needs test data + throw new TypeError('Invalid outputKey for p2tr witness'); + if (pubkey.length && !pubkey.equals(outputKey.x)) + throw new TypeError('Pubkey mismatch for p2tr witness'); + if (outputKey.parity !== (controlBlock[0] & 1)) + throw new Error('Incorrect parity'); + } + } + } + return Object.assign(o, a); +} +exports.p2tr = p2tr; +function tweakKey(pubKey, h) { + 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 = (0, ecc_lib_1.getEccLib)().xOnlyPointAddTweak(pubKey, tweakHash); + if (!res || res.xOnlyPubkey === null) return null; + return { + parity: res.parity, + x: buffer_1.Buffer.from(res.xOnlyPubkey), + }; +} +function stacksEqual(a, b) { + if (a.length !== b.length) return false; + return a.every((x, i) => { + return x.equals(b[i]); + }); +} diff --git a/src/payments/p2tr_ns.d.ts b/src/payments/p2tr_ns.d.ts new file mode 100644 index 000000000..1194d1197 --- /dev/null +++ b/src/payments/p2tr_ns.d.ts @@ -0,0 +1,2 @@ +import { Payment, PaymentOpts } from './index'; +export declare function p2tr_ns(a: Payment, opts?: PaymentOpts): Payment; diff --git a/src/payments/p2tr_ns.js b/src/payments/p2tr_ns.js new file mode 100644 index 000000000..db0176521 --- /dev/null +++ b/src/payments/p2tr_ns.js @@ -0,0 +1,134 @@ +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); +exports.p2tr_ns = void 0; +const ecc_lib_1 = require('../ecc_lib'); +const networks_1 = require('../networks'); +const bscript = require('../script'); +const lazy = require('./lazy'); +const OPS = bscript.OPS; +const typef = require('typeforce'); +function stacksEqual(a, b) { + if (a.length !== b.length) return false; + return a.every((x, i) => { + return x.equals(b[i]); + }); +} +// input: [signatures ...] +// output: [pubKeys[0:n-1] OP_CHECKSIGVERIFY] pubKeys[n-1] OP_CHECKSIG +function p2tr_ns(a, opts) { + if ( + !a.input && + !a.output && + !(a.pubkeys && a.pubkeys.length) && + !a.signatures + ) + throw new TypeError('Not enough data'); + opts = Object.assign({ validate: true }, opts || {}); + function isAcceptableSignature(x) { + if (Buffer.isBuffer(x)) + return ( + // empty signatures may be represented as empty buffers + (opts && opts.allowIncomplete && x.length === 0) || + bscript.isCanonicalSchnorrSignature(x) + ); + return !!(opts && opts.allowIncomplete && x === OPS.OP_0); + } + typef( + { + network: typef.maybe(typef.Object), + output: typef.maybe(typef.Buffer), + pubkeys: typef.maybe( + typef.arrayOf((0, ecc_lib_1.getEccLib)().isXOnlyPoint), + ), + signatures: typef.maybe(typef.arrayOf(isAcceptableSignature)), + input: typef.maybe(typef.Buffer), + }, + a, + ); + const network = a.network || networks_1.bitcoin; + const o = { network }; + const _chunks = lazy.value(() => { + if (!a.output) return; + return bscript.decompile(a.output); + }); + lazy.prop(o, 'output', () => { + if (!a.pubkeys) return; + return bscript.compile( + [].concat( + ...a.pubkeys.map((pk, i, pks) => [ + pk, + i === pks.length - 1 ? OPS.OP_CHECKSIG : OPS.OP_CHECKSIGVERIFY, + ]), + ), + ); + }); + lazy.prop(o, 'n', () => { + if (!o.pubkeys) return; + return o.pubkeys.length; + }); + lazy.prop(o, 'pubkeys', () => { + const chunks = _chunks(); + if (!chunks) return; + return chunks.filter((_, index) => index % 2 === 0); + }); + lazy.prop(o, 'signatures', () => { + if (!a.input) return; + return bscript.decompile(a.input).reverse(); + }); + lazy.prop(o, 'input', () => { + if (!a.signatures) return; + return bscript.compile([...a.signatures].reverse()); + }); + lazy.prop(o, 'witness', () => { + if (!o.input) return; + return []; + }); + lazy.prop(o, 'name', () => { + if (!o.n) return; + return `p2tr_ns(${o.n})`; + }); + // extended validation + if (opts.validate) { + const chunks = _chunks(); + if (chunks) { + if (chunks[chunks.length - 1] !== OPS.OP_CHECKSIG) + throw new TypeError('Output ends with unexpected opcode'); + if ( + chunks + .filter((_, index) => index % 2 === 1) + .slice(0, -1) + .some(op => op !== OPS.OP_CHECKSIGVERIFY) + ) + throw new TypeError('Output contains unexpected opcode'); + if (o.n > 16 || o.n !== chunks.length / 2) + throw new TypeError('Output contains too many pubkeys'); + if (o.pubkeys.some(x => !(0, ecc_lib_1.getEccLib)().isXOnlyPoint(x))) + throw new TypeError('Output contains invalid pubkey(s)'); + if (a.pubkeys && !stacksEqual(a.pubkeys, o.pubkeys)) + throw new TypeError('Pubkeys mismatch'); + } + if (a.pubkeys && a.pubkeys.length) { + o.n = a.pubkeys.length; + } + if (a.signatures) { + if (a.signatures.length < o.n) + throw new TypeError('Not enough signatures provided'); + if (a.signatures.length > o.n) + throw new TypeError('Too many signatures provided'); + } + if (a.input) { + if (!o.signatures.every(isAcceptableSignature)) + throw new TypeError('Input has invalid signature(s)'); + if (a.signatures && !stacksEqual(a.signatures, o.signatures)) + throw new TypeError('Signature mismatch'); + if (o.n !== o.signatures.length) + throw new TypeError( + `Signature count mismatch (n: ${o.n}, signatures.length: ${ + o.signatures.length + }`, + ); + } + } + return Object.assign(o, a); +} +exports.p2tr_ns = p2tr_ns; diff --git a/src/payments/taprootutils.d.ts b/src/payments/taprootutils.d.ts new file mode 100644 index 000000000..a5739c44f --- /dev/null +++ b/src/payments/taprootutils.d.ts @@ -0,0 +1,36 @@ +/// +import { Tapleaf, Taptree } from '../types'; +export declare const LEAF_VERSION_TAPSCRIPT = 192; +export declare function rootHashFromPath(controlBlock: Buffer, leafHash: Buffer): Buffer; +interface HashLeaf { + hash: Buffer; +} +interface HashBranch { + hash: Buffer; + 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 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 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 - 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; +export declare function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer; +export {}; diff --git a/src/payments/taprootutils.js b/src/payments/taprootutils.js new file mode 100644 index 000000000..ec67077d9 --- /dev/null +++ b/src/payments/taprootutils.js @@ -0,0 +1,87 @@ +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); +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'); +exports.LEAF_VERSION_TAPSCRIPT = 0xc0; +function rootHashFromPath(controlBlock, leafHash) { + const m = (controlBlock.length - 33) / 32; + 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) { + kj = tapBranchHash(kj, ej); + } else { + kj = tapBranchHash(ej, kj); + } + } + return kj; +} +exports.rootHashFromPath = rootHashFromPath; +const isHashBranch = ht => 'left' in ht && 'right' in ht; +/** + * 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)) + 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, + right, + }; +} +exports.toHashTree = toHashTree; +/** + * 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 - 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)) { + 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 []; + } + return undefined; +} +exports.findScriptPath = findScriptPath; +function tapleafHash(leaf) { + const version = leaf.redeemVersion || exports.LEAF_VERSION_TAPSCRIPT; + return bcrypto.taggedHash( + 'TapLeaf', + buffer_1.Buffer.concat([ + buffer_1.Buffer.from([version]), + serializeScript(leaf.output), + ]), + ); +} +exports.tapleafHash = tapleafHash; +function tapTweakHash(pubKey, h) { + return bcrypto.taggedHash( + 'TapTweak', + buffer_1.Buffer.concat(h ? [pubKey, h] : [pubKey]), + ); +} +exports.tapTweakHash = tapTweakHash; +function tapBranchHash(a, b) { + return bcrypto.taggedHash('TapBranch', buffer_1.Buffer.concat([a, b])); +} +function serializeScript(s) { + const varintLen = bufferutils_1.varuint.encodingLength(s.length); + const buffer = buffer_1.Buffer.allocUnsafe(varintLen); // better + bufferutils_1.varuint.encode(s.length, buffer); + return buffer_1.Buffer.concat([buffer, s]); +} diff --git a/src/psbt.d.ts b/src/psbt.d.ts index 8603a6955..890f9e115 100644 --- a/src/psbt.d.ts +++ b/src/psbt.d.ts @@ -143,6 +143,7 @@ export interface HDSigner extends HDSignerBase { * Return a 64 byte signature (32 byte r and 32 byte s in that order) */ sign(hash: Buffer): Buffer; + signSchnorr?(hash: Buffer): Buffer; } /** * Same as above but with async sign method @@ -150,17 +151,20 @@ export interface HDSigner extends HDSignerBase { export interface HDSignerAsync extends HDSignerBase { derivePath(path: string): HDSignerAsync; sign(hash: Buffer): Promise; + signSchnorr?(hash: Buffer): Promise; } export interface Signer { publicKey: Buffer; network?: any; sign(hash: Buffer, lowR?: boolean): Buffer; + signSchnorr?(hash: Buffer): Buffer; getPublicKey?(): Buffer; } export interface SignerAsync { publicKey: Buffer; network?: any; sign(hash: Buffer, lowR?: boolean): Promise; + signSchnorr?(hash: Buffer): Promise; getPublicKey?(): Buffer; } /** @@ -173,10 +177,11 @@ declare type FinalScriptsFunc = (inputIndex: number, // Which input is it? input: PsbtInput, // The PSBT input contents script: Buffer, // The "meaningful" locking script Buffer (redeemScript for P2SH etc.) isSegwit: boolean, // Is it segwit? +isTapscript: boolean, // Is taproot script path? isP2SH: boolean, // Is it P2SH? isP2WSH: boolean) => { finalScriptSig: Buffer | undefined; - finalScriptWitness: Buffer | undefined; + finalScriptWitness: Buffer | Buffer[] | undefined; }; -declare type AllScriptType = 'witnesspubkeyhash' | 'pubkeyhash' | 'multisig' | 'pubkey' | 'nonstandard' | 'p2sh-witnesspubkeyhash' | 'p2sh-pubkeyhash' | 'p2sh-multisig' | 'p2sh-pubkey' | 'p2sh-nonstandard' | 'p2wsh-pubkeyhash' | 'p2wsh-multisig' | 'p2wsh-pubkey' | 'p2wsh-nonstandard' | 'p2sh-p2wsh-pubkeyhash' | 'p2sh-p2wsh-multisig' | 'p2sh-p2wsh-pubkey' | 'p2sh-p2wsh-nonstandard'; +declare type AllScriptType = 'witnesspubkeyhash' | 'pubkeyhash' | 'multisig' | 'pubkey' | 'taproot' | 'nonstandard' | 'p2sh-witnesspubkeyhash' | 'p2sh-pubkeyhash' | 'p2sh-multisig' | 'p2sh-pubkey' | 'p2sh-nonstandard' | 'p2wsh-pubkeyhash' | 'p2wsh-multisig' | 'p2wsh-pubkey' | 'p2wsh-nonstandard' | 'p2sh-p2wsh-pubkeyhash' | 'p2sh-p2wsh-multisig' | 'p2sh-p2wsh-pubkey' | 'p2sh-p2wsh-nonstandard' | 'p2tr-pubkey' | 'p2tr-nonstandard'; export {}; diff --git a/src/psbt.js b/src/psbt.js index 616219580..c14086d0e 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -11,6 +11,7 @@ const networks_1 = require('./networks'); const payments = require('./payments'); const bscript = require('./script'); const transaction_1 = require('./transaction'); +const taprootutils_1 = require('./payments/taprootutils'); /** * These are the default arguments for a Psbt instance. */ @@ -273,11 +274,13 @@ class Psbt { } finalizeInput(inputIndex, finalScriptsFunc = getFinalScripts) { const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); - const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( - inputIndex, - input, - this.__CACHE, - ); + const { + script, + isP2SH, + isP2WSH, + isSegwit, + isTapscript, + } = getScriptFromInput(inputIndex, input, this.__CACHE); if (!script) throw new Error(`No script found for input #${inputIndex}`); checkPartialSigSighashes(input); const { finalScriptSig, finalScriptWitness } = finalScriptsFunc( @@ -287,10 +290,16 @@ class Psbt { isSegwit, isP2SH, isP2WSH, + isTapscript, ); if (finalScriptSig) this.data.updateInput(inputIndex, { finalScriptSig }); - if (finalScriptWitness) - this.data.updateInput(inputIndex, { finalScriptWitness }); + if (finalScriptWitness) { + // allow custom finalizers to build the witness as an array + const witness = Array.isArray(finalScriptWitness) + ? witnessStackToScriptWitness(finalScriptWitness) + : finalScriptWitness; + this.data.updateInput(inputIndex, { finalScriptWitness: witness }); + } if (!finalScriptSig && !finalScriptWitness) throw new Error(`Unknown error finalizing input #${inputIndex}`); this.data.clearFinalizedInput(inputIndex); @@ -298,7 +307,11 @@ class Psbt { } getInputType(inputIndex) { const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); - const script = getScriptFromUtxo(inputIndex, input, this.__CACHE); + const { script } = getScriptAndAmountFromUtxo( + inputIndex, + input, + this.__CACHE, + ); const result = getMeaningfulScript( script, inputIndex, @@ -355,13 +368,20 @@ class Psbt { let hashCache; let scriptCache; let sighashCache; + const scriptType = this.getInputType(inputIndex); for (const pSig of mySigs) { - const sig = bscript.signature.decode(pSig.signature); + const sig = isTaprootSpend(scriptType) + ? { + signature: pSig.signature, + hashType: transaction_1.Transaction.SIGHASH_DEFAULT, + } + : bscript.signature.decode(pSig.signature); const { hash, script } = sighashCache !== sig.hashType ? getHashForSig( inputIndex, Object.assign({}, input, { sighashType: sig.hashType }), + this.data.inputs, this.__CACHE, true, ) @@ -526,13 +546,29 @@ class Psbt { this.__CACHE, sighashTypes, ); - const partialSig = [ - { + const scriptType = this.getInputType(inputIndex); + if (isTaprootSpend(scriptType)) { + if (!keyPair.signSchnorr) { + throw new Error( + `Need Schnorr Signer to sign taproot input #${inputIndex}.`, + ); + } + const partialSig = this.data.inputs[inputIndex].partialSig || []; + partialSig.push({ pubkey: keyPair.publicKey, - signature: bscript.signature.encode(keyPair.sign(hash), sighashType), - }, - ]; - this.data.updateInput(inputIndex, { partialSig }); + signature: keyPair.signSchnorr(hash), + }); + // must be changed to use the `updateInput()` public API + this.data.inputs[inputIndex].partialSig = partialSig; + } else { + const partialSig = [ + { + pubkey: keyPair.publicKey, + signature: bscript.signature.encode(keyPair.sign(hash), sighashType), + }, + ]; + this.data.updateInput(inputIndex, { partialSig }); + } return this; } signInputAsync( @@ -550,6 +586,23 @@ class Psbt { this.__CACHE, sighashTypes, ); + const scriptType = this.getInputType(inputIndex); + if (isTaprootSpend(scriptType)) { + if (!keyPair.signSchnorr) { + throw new Error( + `Need Schnorr Signer to sign taproot input #${inputIndex}.`, + ); + } + return Promise.resolve(keyPair.signSchnorr(hash)).then(signature => { + const partialSig = this.data.inputs[inputIndex].partialSig || []; + partialSig.push({ + pubkey: keyPair.publicKey, + signature, + }); + // must be changed to use the `updateInput()` public API + this.data.inputs[inputIndex].partialSig = partialSig; + }); + } return Promise.resolve(keyPair.sign(hash)).then(signature => { const partialSig = [ { @@ -671,6 +724,7 @@ function canFinalize(input, script, scriptType) { case 'pubkey': case 'pubkeyhash': case 'witnesspubkeyhash': + case 'taproot': return hasSigs(1, input.partialSig); case 'multisig': const p2ms = payments.p2ms({ output: script }); @@ -719,6 +773,7 @@ const isP2PKH = isPaymentFactory(payments.p2pkh); const isP2WPKH = isPaymentFactory(payments.p2wpkh); const isP2WSHScript = isPaymentFactory(payments.p2wsh); const isP2SHScript = isPaymentFactory(payments.p2sh); +const isP2TR = isPaymentFactory(payments.p2tr); function bip32DerivationIsMine(root) { return d => { if (!d.masterFingerprint.equals(root.fingerprint)) return false; @@ -861,9 +916,17 @@ function getTxCacheValue(key, name, inputs, c) { if (key === '__FEE_RATE') return c.__FEE_RATE; else if (key === '__FEE') return c.__FEE; } -function getFinalScripts(inputIndex, input, script, isSegwit, isP2SH, isP2WSH) { +function getFinalScripts( + inputIndex, + input, + script, + isSegwit, + isP2SH, + isP2WSH, + isTapscript = false, +) { const scriptType = classifyScript(script); - if (!canFinalize(input, script, scriptType)) + if (isTapscript || !canFinalize(input, script, scriptType)) throw new Error(`Can not finalize input #${inputIndex}`); return prepareFinalScripts( script, @@ -920,6 +983,7 @@ function getHashAndSighashType( const { hash, sighashType, script } = getHashForSig( inputIndex, input, + inputs, cache, false, sighashTypes, @@ -930,7 +994,14 @@ function getHashAndSighashType( sighashType, }; } -function getHashForSig(inputIndex, input, cache, forValidate, sighashTypes) { +function getHashForSig( + inputIndex, + input, + inputs, + cache, + forValidate, + sighashTypes, +) { const unsignedTx = cache.__TX; const sighashType = input.sighashType || transaction_1.Transaction.SIGHASH_ALL; @@ -988,6 +1059,22 @@ function getHashForSig(inputIndex, input, cache, forValidate, sighashTypes) { prevout.value, sighashType, ); + } else if (isP2TR(prevout.script)) { + const prevOuts = inputs.map((i, index) => + getScriptAndAmountFromUtxo(index, i, cache), + ); + const signingScripts = prevOuts.map(o => o.script); + const values = prevOuts.map(o => o.value); + const leafHash = input.witnessScript + ? (0, taprootutils_1.tapleafHash)({ output: input.witnessScript }) + : undefined; + hash = unsignedTx.hashForWitnessV1( + inputIndex, + signingScripts, + values, + transaction_1.Transaction.SIGHASH_DEFAULT, + leafHash, + ); } else { // non-segwit if ( @@ -1050,6 +1137,15 @@ function getPayment(script, scriptType, partialSig) { signature: partialSig[0].signature, }); break; + case 'taproot': + payment = payments.p2tr( + { + output: script, + signature: partialSig[0].signature, + }, + { validate: false }, + ); + break; } return payment; } @@ -1072,31 +1168,39 @@ function getScriptFromInput(inputIndex, input, cache) { const res = { script: null, isSegwit: false, + isTapscript: false, isP2SH: false, isP2WSH: false, }; - res.isP2SH = !!input.redeemScript; - res.isP2WSH = !!input.witnessScript; + let utxoScript = null; + if (input.nonWitnessUtxo) { + const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( + cache, + input, + inputIndex, + ); + const prevoutIndex = unsignedTx.ins[inputIndex].index; + utxoScript = nonWitnessUtxoTx.outs[prevoutIndex].script; + } else if (input.witnessUtxo) { + utxoScript = input.witnessUtxo.script; + } if (input.witnessScript) { res.script = input.witnessScript; } else if (input.redeemScript) { res.script = input.redeemScript; } else { - if (input.nonWitnessUtxo) { - const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( - cache, - input, - inputIndex, - ); - const prevoutIndex = unsignedTx.ins[inputIndex].index; - res.script = nonWitnessUtxoTx.outs[prevoutIndex].script; - } else if (input.witnessUtxo) { - res.script = input.witnessUtxo.script; - } + res.script = utxoScript; } - if (input.witnessScript || isP2WPKH(res.script)) { + const isTaproot = utxoScript && isP2TR(utxoScript); + // Segregated Witness versions 0 or 1 + if (input.witnessScript || isP2WPKH(res.script) || isTaproot) { res.isSegwit = true; } + if (isTaproot && input.witnessScript) { + res.isTapscript = true; + } + res.isP2SH = !!input.redeemScript; + res.isP2WSH = !!input.witnessScript && !res.isTapscript; return res; } function getSignersFromHD(inputIndex, inputs, hdKeyPair) { @@ -1267,22 +1371,26 @@ function nonWitnessUtxoTxFromCache(cache, input, inputIndex) { } return c[inputIndex]; } -function getScriptFromUtxo(inputIndex, input, cache) { +function getScriptAndAmountFromUtxo(inputIndex, input, cache) { if (input.witnessUtxo !== undefined) { - return input.witnessUtxo.script; + return { + script: input.witnessUtxo.script, + value: input.witnessUtxo.value, + }; } else if (input.nonWitnessUtxo !== undefined) { const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( cache, input, inputIndex, ); - return nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index].script; + const o = nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index]; + return { script: o.script, value: o.value }; } else { throw new Error("Can't find pubkey in input without Utxo data"); } } function pubkeyInInput(pubkey, input, inputIndex, cache) { - const script = getScriptFromUtxo(inputIndex, input, cache); + const { script } = getScriptAndAmountFromUtxo(inputIndex, input, cache); const { meaningfulScript } = getMeaningfulScript( script, inputIndex, @@ -1352,6 +1460,7 @@ function getMeaningfulScript( const isP2SH = isP2SHScript(script); const isP2SHP2WSH = isP2SH && redeemScript && isP2WSHScript(redeemScript); const isP2WSH = isP2WSHScript(script); + const isP2TRScript = isP2TR(script); if (isP2SH && redeemScript === undefined) throw new Error('scriptPubkey is P2SH but redeemScript missing'); if ((isP2WSH || isP2SHP2WSH) && witnessScript === undefined) @@ -1371,6 +1480,9 @@ function getMeaningfulScript( } else if (isP2SH) { meaningfulScript = redeemScript; checkRedeemScript(index, script, redeemScript, ioType); + } else if (isP2TRScript && !!witnessScript) { + meaningfulScript = witnessScript; + // TODO: check here something? } else { meaningfulScript = script; } @@ -1382,6 +1494,8 @@ function getMeaningfulScript( ? 'p2sh' : isP2WSH ? 'p2wsh' + : isP2TRScript + ? 'p2tr' : 'raw', }; } @@ -1392,18 +1506,29 @@ function checkInvalidP2WSH(script) { } function pubkeyInScript(pubkey, script) { const pubkeyHash = (0, crypto_1.hash160)(pubkey); + const pubkeyXOnly = pubkey.slice(1, 33); const decompiled = bscript.decompile(script); if (decompiled === null) throw new Error('Unknown script error'); return decompiled.some(element => { if (typeof element === 'number') return false; - return element.equals(pubkey) || element.equals(pubkeyHash); + return ( + element.equals(pubkey) || + element.equals(pubkeyHash) || + element.equals(pubkeyXOnly) + ); }); } +function isTaprootSpend(scriptType) { + return ( + !!scriptType && (scriptType === 'taproot' || scriptType.startsWith('p2tr-')) + ); +} 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)) return 'taproot'; return 'nonstandard'; } function range(n) { diff --git a/src/script.d.ts b/src/script.d.ts index 261ecf4af..c01a67b4f 100644 --- a/src/script.d.ts +++ b/src/script.d.ts @@ -13,5 +13,6 @@ export declare function toStack(chunks: Buffer | Array): Buffer export declare function isCanonicalPubKey(buffer: Buffer): boolean; export declare function isDefinedHashType(hashType: number): boolean; export declare function isCanonicalScriptSignature(buffer: Buffer): boolean; +export declare function isCanonicalSchnorrSignature(buffer: Buffer): boolean; export declare const number: typeof scriptNumber; export declare const signature: typeof scriptSignature; diff --git a/src/script.js b/src/script.js index 5e5e17ba1..c502bc407 100644 --- a/src/script.js +++ b/src/script.js @@ -1,6 +1,6 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -exports.signature = exports.number = exports.isCanonicalScriptSignature = exports.isDefinedHashType = exports.isCanonicalPubKey = exports.toStack = exports.fromASM = exports.toASM = exports.decompile = exports.compile = exports.isPushOnly = exports.OPS = void 0; +exports.signature = exports.number = exports.isCanonicalSchnorrSignature = exports.isCanonicalScriptSignature = exports.isDefinedHashType = exports.isCanonicalPubKey = exports.toStack = exports.fromASM = exports.toASM = exports.decompile = exports.compile = exports.isPushOnly = exports.OPS = void 0; const bip66 = require('./bip66'); const ops_1 = require('./ops'); Object.defineProperty(exports, 'OPS', { @@ -177,6 +177,13 @@ function isCanonicalScriptSignature(buffer) { return bip66.check(buffer.slice(0, -1)); } exports.isCanonicalScriptSignature = isCanonicalScriptSignature; +function isCanonicalSchnorrSignature(buffer) { + if (!Buffer.isBuffer(buffer)) return false; + if (buffer.length === 64) return true; // implied SIGHASH_DEFAULT + if (buffer.length === 65 && isDefinedHashType(buffer[64])) return true; // explicit SIGHASH trailing byte + return false; +} +exports.isCanonicalSchnorrSignature = isCanonicalSchnorrSignature; // tslint:disable-next-line variable-name exports.number = scriptNumber; exports.signature = scriptSignature; diff --git a/src/types.d.ts b/src/types.d.ts index 5a8505d34..2c07fbc30 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -10,6 +10,29 @@ export declare function Signer(obj: any): boolean; export declare function Satoshi(value: number): boolean; export declare const ECPoint: any; export declare const Network: any; +export interface XOnlyPointAddTweakResult { + parity: 1 | 0; + xOnlyPubkey: Uint8Array; +} +export interface Tapleaf { + output: Buffer; + redeemVersion?: number; +} +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, Taptree] | 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; +} export declare const Buffer256bit: any; export declare const Hash160bit: any; export declare const Hash256bit: any; diff --git a/src/types.js b/src/types.js index a6d1efa16..61f2f46ad 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.redeemVersion !== undefined) + return (o.redeemVersion & exports.TAPLEAF_VERSION_MASK) === o.redeemVersion; + return true; +} +exports.isTapleaf = isTapleaf; +function isTaptree(scriptTree) { + if (!(0, exports.Array)(scriptTree)) return isTapleaf(scriptTree); + if (scriptTree.length !== 2) return false; + return scriptTree.every(t => isTaptree(t)); +} +exports.isTaptree = isTaptree; exports.Buffer256bit = exports.typeforce.BufferN(32); exports.Hash160bit = exports.typeforce.BufferN(20); exports.Hash256bit = exports.typeforce.BufferN(32); diff --git a/test/address.spec.ts b/test/address.spec.ts index b1304c323..23c18b9f6 100644 --- a/test/address.spec.ts +++ b/test/address.spec.ts @@ -1,9 +1,12 @@ 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'; +import { initEccLib } from '../src'; + const NETWORKS = Object.assign( { litecoin: { @@ -65,6 +68,7 @@ describe('address', () => { }); describe('fromOutputScript', () => { + initEccLib(ecc); fixtures.standard.forEach(f => { it('encodes ' + f.script.slice(0, 30) + '... (' + f.network + ')', () => { const script = bscript.fromASM(f.script); @@ -79,7 +83,7 @@ describe('address', () => { const script = bscript.fromASM(f.script); assert.throws(() => { - baddress.fromOutputScript(script); + baddress.fromOutputScript(script, undefined); }, new RegExp(f.exception)); }); }); @@ -138,10 +142,11 @@ describe('address', () => { }); fixtures.invalid.toOutputScript.forEach(f => { - it('throws when ' + f.exception, () => { + 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); - }, new RegExp(f.address + ' ' + f.exception)); + }, new RegExp(exception)); }); }); }); diff --git a/test/fixtures/address.json b/test/fixtures/address.json index 765ea8aca..0430b7887 100644 --- a/test/fixtures/address.json +++ b/test/fixtures/address.json @@ -80,10 +80,10 @@ }, { "network": "bitcoin", - "bech32": "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y", + "bech32": "bc1p3efq8ujsj0qr5xvms7mv89p8cz0crqdtuxe9ms6grqgxc9sgsntslthf6w", "version": 1, - "data": "751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6", - "script": "OP_1 751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6" + "data": "8e5203f25093c03a199b87b6c39427c09f8181abe1b25dc34818106c160884d7", + "script": "OP_1 8e5203f25093c03a199b87b6c39427c09f8181abe1b25dc34818106c160884d7" }, { "network": "bitcoin", @@ -116,10 +116,16 @@ ], "bech32": [ { - "address": "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y", + "address": "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", + "version": 0, + "prefix": "tb", + "data": "000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433" + }, + { + "address": "bc1p3efq8ujsj0qr5xvms7mv89p8cz0crqdtuxe9ms6grqgxc9sgsntslthf6w", "version": 1, "prefix": "bc", - "data": "751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6" + "data": "8e5203f25093c03a199b87b6c39427c09f8181abe1b25dc34818106c160884d7" }, { "address": "bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs", @@ -195,6 +201,19 @@ { "exception": "has no matching Address", "script": "OP_0 751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd675" + }, + { + "exception": "has no matching Address", + "script": "OP_1 75" + }, + { + "exception": "has no matching Address", + "script": "OP_1 751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd675" + }, + { + "description": "pubkey is not valid x coordinate", + "exception": "has no matching Address", + "script": "OP_1 fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f" } ], "toOutputScript": [ @@ -297,6 +316,14 @@ { "address": "bc1gmk9yu", "exception": "has no matching Script" + }, + { + "address": "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw55h884v", + "exception": "has no matching Script" + }, + { + "address": "bc1pllllllllllllllllllllllllllllllllllllllllllllallllshsdfvw2y", + "paymentException": "TypeError: Invalid pubkey for p2tr" } ] } diff --git a/test/fixtures/p2tr.json b/test/fixtures/p2tr.json new file mode 100644 index 000000000..deba31c2c --- /dev/null +++ b/test/fixtures/p2tr.json @@ -0,0 +1,1198 @@ +{ + "valid": [ + { + "description": "output and pubkey from address", + "arguments": { + "address": "bc1p4dss6gkgq8003g0qyd5drwfqrztsadf2w2v3juz73gdz7cx82r6sj7lcqx" + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "signature": null, + "input": null, + "witness": null + } + }, + { + "description": "address and pubkey from output", + "arguments": { + "output": "OP_1 ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5" + }, + "expected": { + "name": "p2tr", + "address": "bc1p4dss6gkgq8003g0qyd5drwfqrztsadf2w2v3juz73gdz7cx82r6sj7lcqx", + "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "signature": null, + "input": null, + "witness": null + } + }, + { + "description": "address and output from pubkey", + "arguments": { + "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5" + }, + "expected": { + "name": "p2tr", + "address": "bc1p4dss6gkgq8003g0qyd5drwfqrztsadf2w2v3juz73gdz7cx82r6sj7lcqx", + "output": "OP_1 ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "signature": null, + "input": null, + "witness": null + } + }, + { + "description": "address, output and witness from pubkey and signature", + "arguments": { + "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "signature": "a251221c339a7129dd0b769698aca40d8d9da9570ab796a1820b91ab7dbf5acbea21c88ba8f1e9308a21729baf080734beaf97023882d972f75e380d480fd704" + }, + "expected": { + "name": "p2tr", + "address": "bc1p4dss6gkgq8003g0qyd5drwfqrztsadf2w2v3juz73gdz7cx82r6sj7lcqx", + "output": "OP_1 ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "input": null, + "witness": [ + "a251221c339a7129dd0b769698aca40d8d9da9570ab796a1820b91ab7dbf5acbea21c88ba8f1e9308a21729baf080734beaf97023882d972f75e380d480fd704" + ] + } + }, + { + "description": "address, output and signature from pubkey and witness", + "arguments": { + "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "witness": [ + "300602010002010001" + ] + }, + "expected": { + "name": "p2tr", + "address": "bc1p4dss6gkgq8003g0qyd5drwfqrztsadf2w2v3juz73gdz7cx82r6sj7lcqx", + "output": "OP_1 ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "input": null, + "signature": "300602010002010001", + "witness": [ + "300602010002010001" + ] + } + }, + { + "description": "address, pubkey and output from internalPubkey", + "arguments": { + "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7" + }, + "expected": { + "name": "p2tr", + "address": "bc1prs7pxymu7jhsptzjlwlqnk8jyg5qmq4sdlc3rwcy7pd3ydz92xjq5ap2sg", + "pubkey": "1c3c13137cf4af00ac52fbbe09d8f222280d82b06ff111bb04f05b12344551a4", + "output": "OP_1 1c3c13137cf4af00ac52fbbe09d8f222280d82b06ff111bb04f05b12344551a4", + "signature": null, + "input": null, + "witness": null + } + }, + { + "description": "address, pubkey, internalPubkey, redeeem and output from witness", + "arguments": { + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c1a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c" + ] + }, + "expected": { + "name": "p2tr", + "internalPubkey": "a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a36", + "pubkey": "1ebe8b90363bd097aa9f352c8b21914e1886bc09fe9e70c09f33ef2d2abdf4bc", + "hash": "c5c62d7fc595ba5fbe61602eb1a29e2e4763408fe1e2b161beb7cb3c71ebcad9", + "address": "bc1pr6lghypk80gf025lx5kgkgv3fcvgd0qfl608psylx0hj624a7j7qay80rv", + "output": "OP_1 1ebe8b90363bd097aa9f352c8b21914e1886bc09fe9e70c09f33ef2d2abdf4bc", + "signature": null, + "input": null, + "redeem" : { + "output": "OP_DROP c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0 OP_CHECKSIG", + "redeemVersion": 192, + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba" + ] + }, + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c1a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c" + ] + } + }, + { + "description": "address, pubkey, internalPubkey and output from witness with annex", + "arguments": { + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c1a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c", + "5000000000000000001111111111111111" + ] + }, + "expected": { + "name": "p2tr", + "internalPubkey": "a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a36", + "pubkey": "1ebe8b90363bd097aa9f352c8b21914e1886bc09fe9e70c09f33ef2d2abdf4bc", + "hash": "c5c62d7fc595ba5fbe61602eb1a29e2e4763408fe1e2b161beb7cb3c71ebcad9", + "address": "bc1pr6lghypk80gf025lx5kgkgv3fcvgd0qfl608psylx0hj624a7j7qay80rv", + "output": "OP_1 1ebe8b90363bd097aa9f352c8b21914e1886bc09fe9e70c09f33ef2d2abdf4bc", + "signature": null, + "input": null, + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c1a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c", + "5000000000000000001111111111111111" + ] + } + }, + { + "description": "address, pubkey, output and hash from internalPubkey and a script tree with one leaf", + "arguments": { + "internalPubkey": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + "scriptTree": { + "output": "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c18 OP_CHECKSIG" + } + }, + "expected": { + "name": "p2tr", + "address": "bc1pjegs09vkeder9m4sw3ycjf2rnpa8nljdqmuleunk9eshu8cq3xysvhgp2u", + "pubkey": "9651079596cb7232eeb07449892543987a79fe4d06f9fcf2762e617e1f008989", + "output": "OP_1 9651079596cb7232eeb07449892543987a79fe4d06f9fcf2762e617e1f008989", + "hash": "16e3f3b8b9c1e453c56b547785cdd25259d65823a2064f30783acc58ef012633", + "signature": null, + "input": null, + "witness": null + } + }, + { + "description": "address, pubkey, output and hash from internalPubkey and a script tree with two leafs", + "arguments": { + "internalPubkey": "2258b1c3160be0864a541854eec9164a572f094f7562628281a8073bb89173a7", + "scriptTree": [ + { + "output": "d826a0a53abb6ffc60df25b9c152870578faef4b2eb5a09bdd672bbe32cdd79b OP_CHECKSIG" + }, + { + "output": "d826a0a53abb6ffc60df25b9c152870578faef4b2eb5a09bdd672bbe32cdd79b OP_CHECKSIG" + } + ] + }, + "expected": { + "name": "p2tr", + "address": "bc1ptj0v8rwcj6s36p4r26ws6htx0fct43n0mxdvdeh9043whlxlq3kq9965ke", + "pubkey": "5c9ec38dd896a11d06a3569d0d5d667a70bac66fd99ac6e6e57d62ebfcdf046c", + "output": "OP_1 5c9ec38dd896a11d06a3569d0d5d667a70bac66fd99ac6e6e57d62ebfcdf046c", + "hash": "ce00198cd4667abae1f94aa5862d089e2967af5aec20715c692db74e3d66bb73", + "signature": null, + "input": null, + "witness": null + } + }, + { + "description": "address, pubkey, output and hash from internalPubkey and a script tree with three leafs", + "arguments": { + "internalPubkey": "7631cacec3343052d87ef4d0065f61dde82d7d2db0c1cc02ef61ef3c982ea763", + "scriptTree": [ + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG" + }, + [ + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG" + }, + { + "output": "9b4d495b74887815a1ff623c055c6eac6b6b2e07d2a016d6526ebac71dd99744 OP_CHECKSIG" + } + ] + ] + }, + "expected": { + "name": "p2tr", + "address": "bc1pkq0t8nkmqswn3qjg9uy6ux2hsyyz4as25v8unfjc9s8q2e4c00sqku9lxh", + "pubkey": "b01eb3cedb041d3882482f09ae195781082af60aa30fc9a6582c0e0566b87be0", + "output": "OP_1 b01eb3cedb041d3882482f09ae195781082af60aa30fc9a6582c0e0566b87be0", + "hash": "7ae0cc2057b1a7bf0e09c787e1d7b6b2355ac112a7b80380a5c1e942155b0c0f", + "signature": null, + "input": null, + "witness": null + } + }, + { + "description": "address, pubkey, output and hash from internalPubkey and a script tree with four leafs", + "arguments": { + "internalPubkey": "d0c19def28bb1b39451c1a814737615983967780d223b79969ba692182c6006b", + "scriptTree": [ + [ + { + "output": "9b4d495b74887815a1ff623c055c6eac6b6b2e07d2a016d6526ebac71dd99744 OP_CHECKSIG" + }, + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG" + } + ], + [ + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG" + }, + { + "output": "9b4d495b74887815a1ff623c055c6eac6b6b2e07d2a016d6526ebac71dd99744 OP_CHECKSIG" + } + ] + ] + }, + "expected": { + "name": "p2tr", + "address": "bc1pstdzevc40j059s0473rghhv9e05l9f5xv7l6dtlavvq22rzfna3syjvjut", + "pubkey": "82da2cb3157c9f42c1f5f4468bdd85cbe9f2a68667bfa6affd6300a50c499f63", + "output": "OP_1 82da2cb3157c9f42c1f5f4468bdd85cbe9f2a68667bfa6affd6300a50c499f63", + "hash": "d673e784eac9b70289130a0bd359023a0fbdde51dc069b9efb4157c2cdce3ea5", + "signature": null, + "input": null, + "witness": null + } + }, + { + "description": "address, pubkey, output and hash from internalPubkey and a script tree with seven leafs", + "arguments": { + "internalPubkey": "f95886b02a84928c5c15bdca32784993105f73de27fa6ad8c1a60389b999267c", + "scriptTree": [ + [ + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG" + }, + [ + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG" + }, + { + "output": "2258b1c3160be0864a541854eec9164a572f094f7562628281a8073bb89173a7 OP_CHECKSIG" + } + ] + ], + [ + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG" + }, + [ + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG" + }, + [ + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG" + }, + { + "output": "03a669ea926f381582ec4a000b9472ba8a17347f5fb159eddd4a07036a6718eb OP_CHECKSIG" + } + ] + ] + ] + ] + }, + "expected": { + "name": "p2tr", + "address": "bc1pfas4r5s5208puwzj20hvwg2dw2kanc06yxczzdd66729z63pk43q7zwlu6", + "pubkey": "4f6151d21453ce1e385253eec7214d72add9e1fa21b02135bad794516a21b562", + "output": "OP_1 4f6151d21453ce1e385253eec7214d72add9e1fa21b02135bad794516a21b562", + "hash": "16fb2e99bdf86f67ee6980d0418658f15df7e19476053b58f45a89df2e219b1b", + "signature": null, + "input": null, + "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": { + "internalPubkey": "aba457d16a8d59151c387f24d1eb887efbe24644c1ee64b261282e7baebdb247", + "redeem": { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac4 OP_CHECKSIG" + }, + "scriptTree": [ + { + "output": "00a9da96087a72258f83b338ef7f0ea8cbbe05da5f18f091eb397d1ecbf7c3d3 OP_CHECKSIG" + }, + [ + [ + { + "output": "00a9da96087a72258f83b338ef7f0ea8cbbe05da5f18f091eb397d1ecbf7c3d4 OP_CHECKSIG" + }, + [ + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac1 OP_CHECKSIG" + }, + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac2 OP_CHECKSIG" + } + ] + ], + [ + [ + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac3 OP_CHECKSIG" + }, + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac4 OP_CHECKSIG" + } + ], + { + "output": "00a9da96087a72258f83b338ef7f0ea8cbbe05da5f18f091eb397d1ecbf7c3d5 OP_CHECKSIG" + } + ] + ] + ] + }, + "expected": { + "name": "p2tr", + "address": "bc1pd2llmtym6c5hyecf5zqsyjz9q0jlxaaksw9j0atx8lc8a0e0vrmsw9ewly", + "pubkey": "6abffdac9bd629726709a08102484503e5f377b6838b27f5663ff07ebf2f60f7", + "output": "OP_1 6abffdac9bd629726709a08102484503e5f377b6838b27f5663ff07ebf2f60f7", + "hash": "88b7e3b495a84aa2bc12780b1773f130ce5eb747b0c28dc4840b7c9280f7326d", + "signature": null, + "input": null, + "witness": [ + "2050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac4ac", + "c0aba457d16a8d59151c387f24d1eb887efbe24644c1ee64b261282e7baebdb247dac795766bbda1eaeaa45e5bfa0a950fdd5f4c4aada5b1f3082edc9689b9fd0a315fb34a7a93dcaed5e26cf7468be5bd377dda7a4d29128f7dd98db6da9bf04325fff3aa86365bac7534dcb6495867109941ec444dd35294e0706e29e051066d73e0d427bd3249bb921fa78c04fb76511f583ff48c97210d17c2d9dcfbb95023" + ] + } + }, + { + "description": "BIP341 Test case 1", + "arguments": { + "internalPubkey": "d6889cb081036e0faefa3a35157ad71086b123b2b144b649798b494c300a961d" + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 53a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343", + "pubkey": "53a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343", + "address": "bc1p2wsldez5mud2yam29q22wgfh9439spgduvct83k3pm50fcxa5dps59h4z5", + "signature": null, + "input": null, + "witness": null + } + }, + { + "description": "BIP341 Test case 2", + "arguments": { + "internalPubkey": "187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27", + "redeem": { + "output": "d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8 OP_CHECKSIG", + "redeemVersion": 192 + }, + "scriptTree": { + "output": "d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8 OP_CHECKSIG", + "redeemVersion": 192 + } + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3", + "pubkey": "147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3", + "address": "bc1pz37fc4cn9ah8anwm4xqqhvxygjf9rjf2resrw8h8w4tmvcs0863sa2e586", + "hash": "5b75adecf53548f3ec6ad7d78383bf84cc57b55a3127c72b9a2481752dd88b21", + "witness": [ + "20d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8ac", + "c1187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27" + ], + "redeem": { + "output": "d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8 OP_CHECKSIG", + "redeemVersion": 192 + }, + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 3", + "arguments": { + "internalPubkey": "93478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820", + "redeem": { + "output": "b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007 OP_CHECKSIG", + "redeemVersion": 192 + }, + "scriptTree": { + "output": "b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007 OP_CHECKSIG", + "redeemVersion": 192 + } + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e", + "pubkey": "e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e", + "address": "bc1punvppl2stp38f7kwv2u2spltjuvuaayuqsthe34hd2dyy5w4g58qqfuag5", + "hash": "c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b", + "witness": [ + "20b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007ac", + "c093478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820" + ], + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 4 - spend leaf 0", + "arguments": { + "internalPubkey": "ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf3786592", + "redeem": { + "output": "387671353e273264c495656e27e39ba899ea8fee3bb69fb2a680e22093447d48 OP_CHECKSIG", + "redeemVersion": 192 + }, + "scriptTree": [ + { + "output": "387671353e273264c495656e27e39ba899ea8fee3bb69fb2a680e22093447d48 OP_CHECKSIG", + "redeemVersion": 192 + }, + { + "output": "424950333431", + "redeemVersion": 152 + } + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 0f63ca2c7639b9bb4be0465cc0aa3ee78a0761ba5f5f7d6ff8eab340f09da561", + "pubkey": "0f63ca2c7639b9bb4be0465cc0aa3ee78a0761ba5f5f7d6ff8eab340f09da561", + "address": "bc1ppa3u5trk8xumkjlqgewvp237u79qwcd6ta0h6mlca2e5puya54ssw9zq0y", + "hash": "f3004d6c183e038105d436db1424f321613366cbb7b05939bf05d763a9ebb962", + "witness": [ + "20387671353e273264c495656e27e39ba899ea8fee3bb69fb2a680e22093447d48ac", + "c0ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf37865927b2c2af8aa3e8b7bfe2f62a155f91427489c5c3b32be47e0b3fac755fc780e0e" + ], + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 4 - spend leaf 1", + "arguments": { + "internalPubkey": "ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf3786592", + "redeem": { + "output": "424950333431", + "redeemVersion": 152 + }, + "scriptTree": [ + { + "output": "387671353e273264c495656e27e39ba899ea8fee3bb69fb2a680e22093447d48 OP_CHECKSIG", + "redeemVersion": 192 + }, + { + "output": "424950333431", + "redeemVersion": 152 + } + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 0f63ca2c7639b9bb4be0465cc0aa3ee78a0761ba5f5f7d6ff8eab340f09da561", + "pubkey": "0f63ca2c7639b9bb4be0465cc0aa3ee78a0761ba5f5f7d6ff8eab340f09da561", + "address": "bc1ppa3u5trk8xumkjlqgewvp237u79qwcd6ta0h6mlca2e5puya54ssw9zq0y", + "hash": "f3004d6c183e038105d436db1424f321613366cbb7b05939bf05d763a9ebb962", + "witness": [ + "06424950333431", + "98ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf37865928ad69ec7cf41c2a4001fd1f738bf1e505ce2277acdcaa63fe4765192497f47a7" + ], + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 5 - spend leaf 0", + "arguments": { + "internalPubkey": "f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd8", + "redeem": { + "output": "44b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fd OP_CHECKSIG", + "redeemVersion": 192 + }, + "scriptTree": [ + { + "output": "44b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fd OP_CHECKSIG", + "redeemVersion": 192 + }, + { + "output": "546170726f6f74", + "redeemVersion": 82 + } + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 053690babeabbb7850c32eead0acf8df990ced79f7a31e358fabf2658b4bc587", + "pubkey": "053690babeabbb7850c32eead0acf8df990ced79f7a31e358fabf2658b4bc587", + "address": "bc1pq5mfpw474wahs5xr9m4dpt8cm7vsemte7733udv040extz6tckrs29g04c", + "hash": "d9c2c32808b41c0301d876d49c0af72e1d98e84b99ca9b4bb67fea1a7424b755", + "witness": [ + "2044b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fdac", + "c1f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd8e44d5f8fa5892c8b6d4d09a08d36edd0b08636e30311302e2448ad8172fb3433" + ], + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 5 - spend leaf 1", + "arguments": { + "internalPubkey": "f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd8", + "redeem": { + "output": "546170726f6f74", + "redeemVersion": 82 + }, + "scriptTree": [ + { + "output": "44b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fd OP_CHECKSIG", + "redeemVersion": 192 + }, + { + "output": "546170726f6f74", + "redeemVersion": 82 + } + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 053690babeabbb7850c32eead0acf8df990ced79f7a31e358fabf2658b4bc587", + "pubkey": "053690babeabbb7850c32eead0acf8df990ced79f7a31e358fabf2658b4bc587", + "address": "bc1pq5mfpw474wahs5xr9m4dpt8cm7vsemte7733udv040extz6tckrs29g04c", + "hash": "d9c2c32808b41c0301d876d49c0af72e1d98e84b99ca9b4bb67fea1a7424b755", + "witness": [ + "07546170726f6f74", + "53f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd864512fecdb5afa04f98839b50e6f0cb7b1e539bf6f205f67934083cdcc3c8d89" + ], + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 6 - spend leaf 0", + "arguments": { + "internalPubkey": "e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f", + "redeem": { + "output": "72ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69 OP_CHECKSIG", + "redeemVersion": 192 + }, + "scriptTree": [ + { + "output": "72ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69 OP_CHECKSIG", + "redeemVersion": 192 + }, + [ + { + "output": "2352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8 OP_CHECKSIG", + "redeemVersion": 192 + }, + { + "output": "7337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186a OP_CHECKSIG", + "redeemVersion": 192 + } + ] + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 91b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605", + "pubkey": "91b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605", + "address": "bc1pjxmy65eywgafs5tsunw95ruycpqcqnev6ynxp7jaasylcgtcxczs6n332e", + "hash": "ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2", + "witness": [ + "2072ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69ac", + "c0e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6fffe578e9ea769027e4f5a3de40732f75a88a6353a09d767ddeb66accef85e553" + ], + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 6 - spend leaf 1", + "arguments": { + "internalPubkey": "e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f", + "redeem": { + "output": "2352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8 OP_CHECKSIG", + "redeemVersion": 192 + }, + "scriptTree": [ + { + "output": "72ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69 OP_CHECKSIG", + "redeemVersion": 192 + }, + [ + { + "output": "2352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8 OP_CHECKSIG", + "redeemVersion": 192 + }, + { + "output": "7337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186a OP_CHECKSIG", + "redeemVersion": 192 + } + ] + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 91b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605", + "pubkey": "91b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605", + "address": "bc1pjxmy65eywgafs5tsunw95ruycpqcqnev6ynxp7jaasylcgtcxczs6n332e", + "hash": "ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2", + "witness": [ + "202352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8ac", + "c0e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f9e31407bffa15fefbf5090b149d53959ecdf3f62b1246780238c24501d5ceaf62645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817" + ], + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 6 - spend leaf 2", + "arguments": { + "internalPubkey": "e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f", + "redeem": { + "output": "7337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186a OP_CHECKSIG", + "redeemVersion": 192 + }, + "scriptTree": [ + { + "output": "72ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69 OP_CHECKSIG", + "redeemVersion": 192 + }, + [ + { + "output": "2352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8 OP_CHECKSIG", + "redeemVersion": 192 + }, + { + "output": "7337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186a OP_CHECKSIG", + "redeemVersion": 192 + } + ] + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 91b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605", + "pubkey": "91b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605", + "address": "bc1pjxmy65eywgafs5tsunw95ruycpqcqnev6ynxp7jaasylcgtcxczs6n332e", + "hash": "ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2", + "witness": [ + "207337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186aac", + "c0e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6fba982a91d4fc552163cb1c0da03676102d5b7a014304c01f0c77b2b8e888de1c2645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817" + ], + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 7 - spend leaf 0", + "arguments": { + "internalPubkey": "55adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d", + "redeem": { + "output": "71981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2 OP_CHECKSIG" + }, + "scriptTree": [ + { + "output": "71981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2 OP_CHECKSIG", + "redeemVersion": 192 + }, + [ + { + "output": "d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748 OP_CHECKSIG", + "redeemVersion": 192 + }, + { + "output": "c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4c OP_CHECKSIG", + "redeemVersion": 192 + } + ] + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 75169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + "pubkey": "75169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + "address": "bc1pw5tf7sqp4f50zka7629jrr036znzew70zxyvvej3zrpf8jg8hqcssyuewe", + "hash": "2f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def", + "witness": [ + "2071981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2ac", + "c155adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d3cd369a528b326bc9d2133cbd2ac21451acb31681a410434672c8e34fe757e91" + ], + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 7 - spend leaf 1", + "arguments": { + "internalPubkey": "55adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d", + "redeem": { + "output": "d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748 OP_CHECKSIG", + "redeemVersion": 192 + }, + "scriptTree": [ + { + "output": "71981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2 OP_CHECKSIG", + "redeemVersion": 192 + }, + [ + { + "output": "d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748 OP_CHECKSIG", + "redeemVersion": 192 + }, + { + "output": "c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4c OP_CHECKSIG", + "redeemVersion": 192 + } + ] + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 75169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + "pubkey": "75169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + "address": "bc1pw5tf7sqp4f50zka7629jrr036znzew70zxyvvej3zrpf8jg8hqcssyuewe", + "hash": "2f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def", + "witness": [ + "20d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748ac", + "c155adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312dd7485025fceb78b9ed667db36ed8b8dc7b1f0b307ac167fa516fe4352b9f4ef7f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d" + ], + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 7 - spend leaf 2", + "arguments": { + "internalPubkey": "55adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d", + "redeem": { + "output": "c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4c OP_CHECKSIG", + "redeemVersion": 192 + }, + "scriptTree": [ + { + "output": "71981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2 OP_CHECKSIG", + "redeemVersion": 192 + }, + [ + { + "output": "d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748 OP_CHECKSIG", + "redeemVersion": 192 + }, + { + "output": "c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4c OP_CHECKSIG", + "redeemVersion": 192 + } + ] + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 75169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + "pubkey": "75169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + "address": "bc1pw5tf7sqp4f50zka7629jrr036znzew70zxyvvej3zrpf8jg8hqcssyuewe", + "hash": "2f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def", + "witness": [ + "20c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4cac", + "c155adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d" + ], + "signature": null, + "input": null + } + } + ], + "invalid": [ + { + "exception": "Not enough data", + "arguments": {} + }, + { + "exception": "Not enough data", + "arguments": { + "signature": "300602010002010001" + } + }, + { + "description": "Incorrect Witness Version", + "exception": "Output is invalid", + "arguments": { + "output": "OP_0 ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5" + } + }, + { + "description": "Invalid x coordinate for pubkey in pubkey", + "exception": "Invalid pubkey for p2tr", + "arguments": { + "pubkey": "f136e956540197c21ff3c075d32a6e3c82f1ee1e646cc0f08f51b0b5edafa762" + } + }, + { + "description": "Invalid x coordinate for pubkey in output", + "exception": "Invalid pubkey for p2tr", + "arguments": { + "output": "OP_1 f136e956540197c21ff3c075d32a6e3c82f1ee1e646cc0f08f51b0b5edafa762" + } + }, + { + "description": "Invalid x coordinate for pubkey in address", + "exception": "Invalid pubkey for p2tr", + "arguments": { + "address": "bc1p7ymwj4j5qxtuy8lncp6ax2nw8jp0rms7v3kvpuy02xcttmd05a3qmwlnez" + } + }, + { + "description": "Pubkey mismatch between pubkey and output", + "exception": "Pubkey mismatch", + "options": {}, + "arguments": { + "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "output": "OP_1 12d7dac98d69a086a50b30959a3537950f356ffc6f50a263ab75c8a3ec9d44c1" + } + }, + { + "description": "Pubkey mismatch between pubkey and address", + "exception": "Pubkey mismatch", + "options": {}, + "arguments": { + "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "address": "bc1pztta4jvddxsgdfgtxz2e5dfhj58n2mludag2ycatwhy28myagnqsnl7mv7" + } + }, + { + "description": "Pubkey mismatch between output and address", + "exception": "Pubkey mismatch", + "options": {}, + "arguments": { + "output": "OP_1 ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "address": "bc1pztta4jvddxsgdfgtxz2e5dfhj58n2mludag2ycatwhy28myagnqsnl7mv7" + } + }, + { + "description": "Pubkey mismatch between internalPubkey and pubkey", + "exception": "Pubkey mismatch", + "options": {}, + "arguments": { + "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7", + "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5" + } + }, + { + "description": "Hash mismatch between scriptTree and hash", + "exception": "Hash mismatch", + "options": {}, + "arguments": { + "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7", + "scriptTree": { + "output": "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c18 OP_CHECKSIG" + }, + "hash": "b76077013c8e303085e300000000000000000000000000000000000000000000" + } + }, + { + "exception": "Expected Point", + "options": {}, + "arguments": { + "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f8" + } + }, + { + "exception": "Signature mismatch", + "arguments": { + "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "signature": "a251221c339a7129dd0b769698aca40d8d9da9570ab796a1820b91ab7dbf5acbea21c88ba8f1e9308a21729baf080734beaf97023882d972f75e380d480fd704", + "witness": [ + "607b8b5b5c8614757736e3d5811790636d2a8e2ea14418f8cff66b2e898b3b7536a49b7c4bc8b3227953194bf5d0548e13e3526fdb36beeefadda1ec834a0db2" + ] + } + }, + { + "exception": "Invalid prefix or Network mismatch", + "arguments": { + "address": "bcrt1prhepe49mpmhclwcqmkzpaz43revunykc7fc0f9az6pq08sn4qe7sxtrd8y" + } + }, + { + "exception": "Invalid address version", + "arguments": { + "address": "bc1z4dss6gkgq8003g0qyd5drwfqrztsadf2w2v3juz73gdz7cx82r6s6rxhwd" + } + }, + { + "exception": "Invalid address data", + "arguments": { + "address": "bc1p4dss6gkgq8003g0qyd5drwfqrztsadf2w2v3juz73gdz7cx82qh3d2w3" + } + }, + { + "description": "Control block length too small", + "exception": "The control-block length is too small. Got 16, expected min 33.", + "arguments": { + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c1a7957acbaaf7b444c53d9e0c9436e8" + ] + } + }, + { + "description": "Control block must have a length of 33 + 32m (0 <= m <= 128)", + "exception": "The control-block length of 40 is incorrect!", + "arguments": { + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c1a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf77" + ] + } + }, + { + "description": "Control block length too large", + "exception": "The script path is too long. Got 129, expected max 128.", + "arguments": { + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c1a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c50cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c" + ] + } + }, + { + "description": "Invalid internalPubkey in control block", + "exception": "Invalid internalPubkey for p2tr witness", + "arguments": { + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c14444444444444444453d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c" + ] + } + }, + { + "description": "internalPubkey mismatch between control block and internalKey", + "exception": "Internal pubkey mismatch", + "arguments": { + "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7", + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c1a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c" + ] + } + }, + { + "description": "pubkey mismatch between outputKey and pubkey", + "exception": "Pubkey mismatch for p2tr witness", + "arguments": { + "pubkey": "df0e070ca2fca05ecd191bdba047841d62414b2bdcb6249c258fd64c0dd251ff", + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c1a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c" + ] + } + }, + { + "description": "parity", + "exception": "Incorrect parity", + "arguments": { + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c0a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c" + ] + } + }, + { + "description": "Script Tree is not a binary tree (has tree leafs)", + "exception": "property \"scriptTree\" of type \\?isTaptree, got Array", + "options": {}, + "arguments": { + "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7", + "scriptTree": [ + { + "output": "71981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2 OP_CHECKSIG", + "version": 192 + }, + [ + { + "output": "d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748 OP_CHECKSIG", + "version": 192 + }, + { + "output": "c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4c OP_CHECKSIG", + "version": 192 + }, + { + "output": "c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4c OP_CHECKSIG", + "version": 192 + } + ] + ], + "hash": "b76077013c8e303085e300000000000000000000000000000000000000000000" + } + }, + { + "description": "Script Tree is not a TapTree tree (leaf has no script)", + "exception": "property \"scriptTree\" of type \\?isTaptree, got Array", + "options": {}, + "arguments": { + "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7", + "scriptTree": [ + { + "output": "71981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2 OP_CHECKSIG", + "version": 192 + }, + [ + [ + [ + [ + { + "output": "d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748 OP_CHECKSIG", + "version": 192 + }, + { + "version": 192 + } + ] + ] + ] + ] + ], + "hash": "b76077013c8e303085e300000000000000000000000000000000000000000000" + } + }, + { + "description": "Incorrect redeem version", + "exception": "Redeem.redeemVersion and witness mismatch", + "arguments": { + "witness": [ + "20d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8ac", + "c1187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27" + ], + "redeem": { + "output": "d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8 OP_CHECKSIG", + "redeemVersion": 111 + } + } + }, + { + "description": "Incorrect redeem output", + "exception": "Redeem.output and witness mismatch", + "arguments": { + "witness": [ + "20d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8ac", + "c1187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27" + ], + "redeem": { + "output": "d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e0000000000 OP_CHECKSIG", + "redeemVersion": 192 + } + } + }, + { + "description": "Incorrect redeem witness", + "exception": "Redeem.witness and witness mismatch", + "arguments": { + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c1a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c" + ], + "redeem" : { + "output": "OP_DROP c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0 OP_CHECKSIG", + "redeemVersion": 192, + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e0100000000000000000000", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba" + ] + } + } + }, + { + "description": "Incorrect redeem output ASM", + "exception": "Redeem.output is invalid", + "arguments": { + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c1a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c" + ], + "redeem" : { + "output": "", + "redeemVersion": 192, + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e0100000000000000000000", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba" + ] + } + } + }, + { + "description": "Redeem script not in tree", + "exception": "Redeem script not in tree", + "options": {}, + "arguments": { + "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7", + "scriptTree": { + "output": "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c18 OP_CHECKSIG" + }, + "redeem": { + "output": "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c19 OP_CHECKSIG" + } + } + } + ], + "dynamic": { + "depends": {}, + "details": [] + } +} diff --git a/test/fixtures/p2tr_ns.json b/test/fixtures/p2tr_ns.json new file mode 100644 index 000000000..952b80637 --- /dev/null +++ b/test/fixtures/p2tr_ns.json @@ -0,0 +1,218 @@ +{ + "valid": [ + { + "description": "p2tr_ns(1), pubkey, in (from out, sigs)", + "arguments": { + "output": "8f5173bc367914e1574aceb3c7232a178a764fb6f14730b6b20bd36394c6c717 OP_CHECKSIG", + "signatures": [ + "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + ], + "network": "regtest" + }, + "options": {}, + "expected": { + "name": "p2tr_ns(1)", + "n": 1, + "pubkeys": ["8f5173bc367914e1574aceb3c7232a178a764fb6f14730b6b20bd36394c6c717"], + "input": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + } + }, + { + "description": "p2tr_ns(1), pubkey, in (from out, sigs) incomplete", + "arguments": { + "output": "8f5173bc367914e1574aceb3c7232a178a764fb6f14730b6b20bd36394c6c717 OP_CHECKSIG", + "signatures": [ + 0 + ], + "network": "regtest" + }, + "options": { "allowIncomplete": true }, + "expected": { + "name": "p2tr_ns(1)", + "n": 1, + "pubkeys": ["8f5173bc367914e1574aceb3c7232a178a764fb6f14730b6b20bd36394c6c717"], + "input": "OP_0" + } + }, + { + "description": "p2tr_ns(1), out, sigs (from key, in)", + "arguments": { + "pubkeys": ["af455f4989d122e9185f8c351dbaecd13adca3eef8a9d38ef8ffed6867e342e3"], + "input": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "network": "regtest" + }, + "options": {}, + "expected": { + "name": "p2tr_ns(1)", + "n": 1, + "output": "af455f4989d122e9185f8c351dbaecd13adca3eef8a9d38ef8ffed6867e342e3 OP_CHECKSIG", + "signatures": [ + "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + ] + } + }, + { + "description": "p2tr_ns(2), out, in (from keys, out, sigs)", + "arguments": { + "pubkeys": [ + "20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3", + "d806a63b6e2d83f11f22f9a11ba7a49ac451e8acf57591ec058e422eb997d55e" + ], + "output": "20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3 OP_CHECKSIGVERIFY d806a63b6e2d83f11f22f9a11ba7a49ac451e8acf57591ec058e422eb997d55e OP_CHECKSIG", + "signatures": [ + "beefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdead", + "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + ], + "network": "regtest" + }, + "options": {}, + "expected": { + "name": "p2tr_ns(2)", + "n": 2, + "outputHex": "2020040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3ad20d806a63b6e2d83f11f22f9a11ba7a49ac451e8acf57591ec058e422eb997d55ead", + "input": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef beefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdead" + } + }, + { + "description": "p2tr_ns(2), out, in (from keys, sigs)", + "arguments": { + "pubkeys": [ + "20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3", + "d806a63b6e2d83f11f22f9a11ba7a49ac451e8acf57591ec058e422eb997d55e" + ], + "signatures": [ + "beefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdead", + "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + ], + "network": "regtest" + }, + "options": {}, + "expected": { + "name": "p2tr_ns(2)", + "n": 2, + "outputHex": "2020040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3ad20d806a63b6e2d83f11f22f9a11ba7a49ac451e8acf57591ec058e422eb997d55ead", + "input": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef beefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdead" + } + } + ], + "invalid": [ + { + "description": "p2tr_ns(2), mismatched keys/sigs", + "arguments": { + "pubkeys": [ + "d806a63b6e2d83f11f22f9a11ba7a49ac451e8acf57591ec058e422eb997d55e" + ], + "signatures": [ + "beefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdead", + "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + ], + "network": "regtest" + }, + "options": {} + }, + { + "description": "p2tr_ns(2), mismatched keys/sigs", + "arguments": { + "pubkeys": [ + "20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3", + "d806a63b6e2d83f11f22f9a11ba7a49ac451e8acf57591ec058e422eb997d55e" + ], + "signatures": [ + "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + ], + "network": "regtest" + }, + "options": {} + }, + { + "description": "p2tr_ns(2), mismatched sigs/input", + "arguments": { + "pubkeys": [ + "20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3", + "d806a63b6e2d83f11f22f9a11ba7a49ac451e8acf57591ec058e422eb997d55e" + ], + "input": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "signatures": [ + "beefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdead", + "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + ], + "network": "regtest" + }, + "options": {} + }, + { + "description": "p2tr_ns(2), mismatched keys/input", + "arguments": { + "pubkeys": [ + "20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3", + "d806a63b6e2d83f11f22f9a11ba7a49ac451e8acf57591ec058e422eb997d55e" + ], + "input": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "network": "regtest" + }, + "options": {} + }, + { + "description": "p2tr_ns(1) no data", + "arguments": { + "network": "regtest" + }, + "options": {} + }, + { + "description": "p2tr_ns(1), bad sig", + "arguments": { + "pubkeys": [ + "20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3" + ], + "input": "deadbeef", + "network": "regtest" + }, + "options": {} + }, + { + "description": "p2tr_ns(2) swapped opcodes", + "arguments": { + "output": "20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3 OP_CHECKSIG 8f5173bc367914e1574aceb3c7232a178a764fb6f14730b6b20bd36394c6c717 OP_CHECKSIGVERIFY", + "network": "regtest" + }, + "options": {} + }, + { + "description": "p2tr_ns(2) wrong internal opcode", + "arguments": { + "output": "20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3 OP_CHECKSIG 8f5173bc367914e1574aceb3c7232a178a764fb6f14730b6b20bd36394c6c717 OP_CHECKSIG", + "network": "regtest" + }, + "options": {} + }, + { + "description": "p2tr_ns(2) too many pubkeys", + "arguments": { + "output": "20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3 OP_CHECKSIGVERIFY 20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3 OP_CHECKSIGVERIFY 20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3 OP_CHECKSIGVERIFY 20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3 OP_CHECKSIGVERIFY 20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3 OP_CHECKSIGVERIFY 20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3 OP_CHECKSIGVERIFY 20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3 OP_CHECKSIGVERIFY 20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3 OP_CHECKSIGVERIFY 20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3 OP_CHECKSIGVERIFY 20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3 OP_CHECKSIGVERIFY 20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3 OP_CHECKSIGVERIFY 20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3 OP_CHECKSIGVERIFY 20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3 OP_CHECKSIGVERIFY 20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3 OP_CHECKSIGVERIFY 20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3 OP_CHECKSIGVERIFY 20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3 OP_CHECKSIGVERIFY 8f5173bc367914e1574aceb3c7232a178a764fb6f14730b6b20bd36394c6c717 OP_CHECKSIG", + "network": "regtest" + }, + "options": {} + }, + { + "description": "p2tr_ns(2) pubkey mismatch", + "arguments": { + "output": "20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3 OP_CHECKSIGVERIFY 8f5173bc367914e1574aceb3c7232a178a764fb6f14730b6b20bd36394c6c717 OP_CHECKSIG", + "pubkeys": [ + "8f5173bc367914e1574aceb3c7232a178a764fb6f14730b6b20bd36394c6c717", + "20040c8338b34cb9c06c6b1bd38095eafa8f9b72398a1084fdb67473d82dfda3" + ], + "network": "regtest" + }, + "options": {} + }, + { + "description": "p2tr_ns(2) bad pubkey", + "arguments": { + "output": "ff20040c8338b34cb9c06c6b1d38095eafa8f9b72398a1084fdb67473d8dfda3 OP_CHECKSIGVERIFY 8f5173bc367914e1574aceb3c7232a178a764fb6f14730b6b20bd36394c6c717 OP_CHECKSIG", + "network": "regtest" + }, + "options": {} + } + ] +} diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 0e51d57cf..5a78e3527 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -116,6 +116,10 @@ { "description": "PSBT with unknown types in the inputs.", "psbt": "cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAACg8BAgMEBQYHCAkPAQIDBAUGBwgJCgsMDQ4PAAA=" + }, + { + "description": "PSBT with one P2TR input and one P2TR output.", + "psbt": "cHNidP8BAF4CAAAAAWbQAKi9hNXynJhqPu8bqkvp0kHihVShkWGh3yLy15+LAAAAAAD/////Aej9AAAAAAAAIlEgRvZJfLLxnVDD6emCqVDcyGIUsB/M5DekIGHHvbEjDTMAAAAAAAEBK7gFAQAAAAAAIlEglCHnNLD50sRn6n3Rl8Yay0RnzcvJ9MsMVx+LY6XEDK4AAA==" } ], "failSignChecks": [ @@ -273,6 +277,46 @@ } ], "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMBBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSriIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=" + }, + { + "description": "Sign PSBT with 3 inputs [P2PKH, P2TR, P2WPKH] and two outputs [P2TR, P2WPKH]", + "psbt": "cHNidP8BAM8CAAAAAwPzd9k+uLSN1rgF01xY1TIA/8N+YytNZ4VP9gKFP4MyAAAAAAD/////ZtAAqL2E1fKcmGo+7xuqS+nSQeKFVKGRYaHfIvLXn4sAAAAAAP////9+h+SlCwIx1MUDT7Bek0NrWXS7xnSPi5LbYbDc9sxYIgAAAAAA/////wIgKRsAAAAAACJRIEb2SXyy8Z1Qw+npgqlQ3MhiFLAfzOQ3pCBhx72xIw0zuAUBAAAAAAAWABTJijE0v48z5ZmmfEAADXdCBcG0FAAAAAAAAQDiAgAAAAABAUfY2D1t0dyMeEH39C1yOdIxigpqm7XJNqHVT3Lc+FkiAAAAAAD+////AhIsGwAAAAAAGXapFJ5+8XZ3ZP80oFldvEwrcNsBftBmiKyYdK6xAAAAABepFLDBn59UffGbX7u/olyFDG0eG1UJhwJHMEQCIDAd3s05C61flXVFqOtov0NoHRGr8KFcOpH6R/81F46EAiBt+j9hHyvT2hYEyf8fdYsM9IgbnybtPV+kRTHDa6Rj0AEhAmmZfwmoHsmCkEOn9AfRTh+863mURelmE8hSqL4MG1EydJwgAAABASu4BQEAAAAAACJRIJQh5zSw+dLEZ+p90ZfGGstEZ83LyfTLDFcfi2OlxAyuAAEBHxAnAAAAAAAAFgAUT6KsoSi2+d7lMJxPcAUeScZf1zIAAAA=", + "isTaproot": true, + "keys": [ + { + "inputToSign": 0, + "WIF": "cRyKzLXVgTReWe7wgfEiXktTa9tf4e5DK1STha274d7BBbnucTaR" + }, + { + "inputToSign": 1, + "WIF": "cNPzVNoVCAfNEadTExqN2HzfC4dX42RtduE39D2i7cxuVEKY3DM3" + }, + { + "inputToSign": 2, + "WIF": "cPPRdCmAMZMjPdHfRmTCmzYVruZHJ8GbM1FqN2W6DnmEPWDg29aL" + } + ], + "result": "cHNidP8BAM8CAAAAAwPzd9k+uLSN1rgF01xY1TIA/8N+YytNZ4VP9gKFP4MyAAAAAAD/////ZtAAqL2E1fKcmGo+7xuqS+nSQeKFVKGRYaHfIvLXn4sAAAAAAP////9+h+SlCwIx1MUDT7Bek0NrWXS7xnSPi5LbYbDc9sxYIgAAAAAA/////wIgKRsAAAAAACJRIEb2SXyy8Z1Qw+npgqlQ3MhiFLAfzOQ3pCBhx72xIw0zuAUBAAAAAAAWABTJijE0v48z5ZmmfEAADXdCBcG0FAAAAAAAAQDiAgAAAAABAUfY2D1t0dyMeEH39C1yOdIxigpqm7XJNqHVT3Lc+FkiAAAAAAD+////AhIsGwAAAAAAGXapFJ5+8XZ3ZP80oFldvEwrcNsBftBmiKyYdK6xAAAAABepFLDBn59UffGbX7u/olyFDG0eG1UJhwJHMEQCIDAd3s05C61flXVFqOtov0NoHRGr8KFcOpH6R/81F46EAiBt+j9hHyvT2hYEyf8fdYsM9IgbnybtPV+kRTHDa6Rj0AEhAmmZfwmoHsmCkEOn9AfRTh+863mURelmE8hSqL4MG1EydJwgACICAi5ovBH1xLoGxPqtFh48wUEpnM+St1SbPzRwO7kBNKOQRzBEAiBpWClBybtHveXkhAgTiE8QSczMJs8MGuH4LOSNRA6s/AIgWlbB3xJOtJIsszj1qZ/whA5jK9wnTzeZzDlVs/ivq2cBAAEBK7gFAQAAAAAAIlEglCHnNLD50sRn6n3Rl8Yay0RnzcvJ9MsMVx+LY6XEDK4iAgKUIec0sPnSxGfqfdGXxhrLRGfNy8n0ywxXH4tjpcQMrkADaUubfpFFrzbU+vL8qCzZE/FO+9unzylfpIgQZ4HTy2qPUtLvbyH59GApdz0SiUZGl8K6Crvt9YIfI/5FxbOLAAEBHxAnAAAAAAAAFgAUT6KsoSi2+d7lMJxPcAUeScZf1zIiAgOlTqRAWzyTP8WLKjtnrrbWBaYHnPb3MYIMk8qJJSuutEgwRQIhAKAiJLYIS+eYrjAJpM8GCc2/ofqpjXsGV8QMf9Ojm8SEAiBCwrAc/8HdsD5ZyW9uzpbsTJEz5wshwNgvksR4l/xbzwEAAAA=" + }, + { + "description": "Sign PSBT with 1 input [P2TR] (script-path, 3-of-3) and one output [P2TR]", + "psbt": "cHNidP8BAF4CAAAAAcPe80j90ChJ8zbpzcG0ZVC7LEvKwW5HRFLFTyc7y4bHAAAAAAD/////AZBBBgAAAAAAIlEgMqacf47eQ4lDRqNGRo3DRZ07bl/zz75tVIEa7xr7H0IAAAAAAAEBK6BoBgAAAAAAIlEgMqacf47eQ4lDRqNGRo3DRZ07bl/zz75tVIEa7xr7H0IBBWggj4kary4texRheMVKh+Ku3dnR1oZpIleSjmfPCBdP83WsIDlfgSndY7SlwvEhJOqgW3p+0w9w5R+5MwXe7MVC5/nruiCoqze8FgnYNMOROzU42tHITX+baoNf/BdXd5FaN641crpTnAAA", + "isTaproot": true, + "keys": [ + { + "inputToSign": 0, + "WIF": "cRXSy63fXDve59e8cvqozVFfqXJB6YL6cPzoRewmEsux81SgPrfj" + }, + { + "inputToSign": 0, + "WIF": "cQQXUJocNBS6oZCCtyhCsdN5ammK6WoJWpx44ANKxZSN2A3WDDEN" + }, + { + "inputToSign": 0, + "WIF": "cTrPNrN2EQo4ppBHcFNxyLBFq2WLjZoNKY5nQbPwAGdhQqqsRKSu" + } + ], + "result": "cHNidP8BAF4CAAAAAcPe80j90ChJ8zbpzcG0ZVC7LEvKwW5HRFLFTyc7y4bHAAAAAAD/////AZBBBgAAAAAAIlEgMqacf47eQ4lDRqNGRo3DRZ07bl/zz75tVIEa7xr7H0IAAAAAAAEBK6BoBgAAAAAAIlEgMqacf47eQ4lDRqNGRo3DRZ07bl/zz75tVIEa7xr7H0IiAgI5X4Ep3WO0pcLxISTqoFt6ftMPcOUfuTMF3uzFQuf560BOGsYvM/Ot27p7l86wu+8NyTgqenZPSYN3g88W0NWF1C6O2P7k8vMntZ7qXQJwahuxKQPjlHAhb+wCOp+sHi1cIgIDj4kary4texRheMVKh+Ku3dnR1oZpIleSjmfPCBdP83VAygBYJBfi/tFJV/2WNjeuh+rBnU17ZtihdMICzGGN7OLURnBRB5oARqqvcGwQF4ta2sarDwZd+mg6DMaHUqOQ4CICA6irN7wWCdg0w5E7NTja0chNf5tqg1/8F1d3kVo3rjVyQGNFuQCECKRcz9CR7vuYOQ5p9dwxty0rMxt33MPpUh+RoihShEgazosa20pguB1lg/TF1RTY25OYfjl9CUYTQdgBBWggj4kary4texRheMVKh+Ku3dnR1oZpIleSjmfPCBdP83WsIDlfgSndY7SlwvEhJOqgW3p+0w9w5R+5MwXe7MVC5/nruiCoqze8FgnYNMOROzU42tHITX+baoNf/BdXd5FaN641crpTnAAA" } ], "combiner": [ @@ -295,6 +339,11 @@ { "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgf0cwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMASICAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAQEDBAEAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHIgIDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtxHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwEiAgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc0cwRAIgZfRbpZmLWaJ//hp77QFq8fH5DVSzqo90UKpfVqJRA70CIH9yRwOtHtuWaAsoS1bU/8uI9/t1nqu+CKow8puFE4PSAQEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA", "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQjaBABHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwFHMEQCIGX0W6WZi1mif/4ae+0BavHx+Q1Us6qPdFCqX1aiUQO9AiB/ckcDrR7blmgLKEtW1P/LiPf7dZ6rvgiqMPKbhROD0gFHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4AIgIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1Ptnuylh3EQ2QxqTwAAAIAAAACABAAAgAAiAgJ/Y5l1fS7/VaE2rQLGhLGDi2VW5fG2s0KCqUtrUAUQlhDZDGpPAAAAgAAAAIAFAACAAA==" + }, + { + "psbt": "cHNidP8BAF4CAAAAAWbQAKi9hNXynJhqPu8bqkvp0kHihVShkWGh3yLy15+LAAAAAAD/////AbgFAQAAAAAAIlEgRvZJfLLxnVDD6emCqVDcyGIUsB/M5DekIGHHvbEjDTMAAAAAAAEBK7gFAQAAAAAAIlEglCHnNLD50sRn6n3Rl8Yay0RnzcvJ9MsMVx+LY6XEDK4iAgKUIec0sPnSxGfqfdGXxhrLRGfNy8n0ywxXH4tjpcQMrkDYKEk9EBnhyC92Y/sV2r8U7uushyGLzWj/UcNmsym5qFYJUq2Fjhh2mUeMRu1yad248jY5EC8LQ7bb4vNqFVuMAAA=", + "result": "cHNidP8BAF4CAAAAAWbQAKi9hNXynJhqPu8bqkvp0kHihVShkWGh3yLy15+LAAAAAAD/////AbgFAQAAAAAAIlEgRvZJfLLxnVDD6emCqVDcyGIUsB/M5DekIGHHvbEjDTMAAAAAAAEBK7gFAQAAAAAAIlEglCHnNLD50sRn6n3Rl8Yay0RnzcvJ9MsMVx+LY6XEDK4BCEIBQNgoST0QGeHIL3Zj+xXavxTu66yHIYvNaP9Rw2azKbmoVglSrYWOGHaZR4xG7XJp3bjyNjkQLwtDttvi82oVW4wAAA==", + "isTaproot": true } ], "extractor": [ @@ -551,6 +600,26 @@ "incorrectPubkey": "Buffer.from('029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e02a', 'hex')", "nonExistantIndex": 42 }, + "validateSignaturesOfTaprootInput": { + "psbt": "cHNidP8BAM8CAAAAAwPzd9k+uLSN1rgF01xY1TIA/8N+YytNZ4VP9gKFP4MyAAAAAAD/////ZtAAqL2E1fKcmGo+7xuqS+nSQeKFVKGRYaHfIvLXn4sAAAAAAP////9+h+SlCwIx1MUDT7Bek0NrWXS7xnSPi5LbYbDc9sxYIgAAAAAA/////wIgKRsAAAAAACJRIEb2SXyy8Z1Qw+npgqlQ3MhiFLAfzOQ3pCBhx72xIw0zuAUBAAAAAAAWABTJijE0v48z5ZmmfEAADXdCBcG0FAAAAAAAAQDiAgAAAAABAUfY2D1t0dyMeEH39C1yOdIxigpqm7XJNqHVT3Lc+FkiAAAAAAD+////AhIsGwAAAAAAGXapFJ5+8XZ3ZP80oFldvEwrcNsBftBmiKyYdK6xAAAAABepFLDBn59UffGbX7u/olyFDG0eG1UJhwJHMEQCIDAd3s05C61flXVFqOtov0NoHRGr8KFcOpH6R/81F46EAiBt+j9hHyvT2hYEyf8fdYsM9IgbnybtPV+kRTHDa6Rj0AEhAmmZfwmoHsmCkEOn9AfRTh+863mURelmE8hSqL4MG1EydJwgAAABASu4BQEAAAAAACJRIJQh5zSw+dLEZ+p90ZfGGstEZ83LyfTLDFcfi2OlxAyuIgIClCHnNLD50sRn6n3Rl8Yay0RnzcvJ9MsMVx+LY6XEDK5AA2lLm36RRa821Pry/Kgs2RPxTvvbp88pX6SIEGeB08tqj1LS728h+fRgKXc9EolGRpfCugq77fWCHyP+RcWziwABAR8QJwAAAAAAABYAFE+irKEotvne5TCcT3AFHknGX9cyAAAA", + "index": 1, + "pubkey": "Buffer.from('029421e734b0f9d2c467ea7dd197c61acb4467cdcbc9f4cb0c571f8b63a5c40cae', 'hex')", + "incorrectPubkey": "Buffer.from('029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e02a', 'hex')", + "nonExistantIndex": 42 + }, + "finalizeTaprootScriptPathSpendInput": { + "psbt": "cHNidP8BAF4CAAAAAWbQAKi9hNXynJhqPu8bqkvp0kHihVShkWGh3yLy15+LAAAAAAD/////AbgFAQAAAAAAIlEgRvZJfLLxnVDD6emCqVDcyGIUsB/M5DekIGHHvbEjDTMAAAAAAAEBK7gFAQAAAAAAIlEglCHnNLD50sRn6n3Rl8Yay0RnzcvJ9MsMVx+LY6XEDK4iAgIuaLwR9cS6BsT6rRYePMFBKZzPkrdUmz80cDu5ATSjkEC5NO2PsYVquVp/60QIc7eTYcr16eABzDpFibWxBgfXoEDrH0oCzDH5HQ8lu7S9VWJwKvJ7GJIMGLDCX/n13qSsAQUiIC5ovBH1xLoGxPqtFh48wUEpnM+St1SbPzRwO7kBNKOQrAAA", + "internalPublicKey": "Buffer.from('02982a2876765bb37b53a12418b9e72b8afa8d54e344a1bd585299a211fbe625f3', 'hex')", + "scriptTree": [ + { + "output": "Buffer.from('2050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0ac', 'hex')" + }, + { + "output": "Buffer.from('202e68bc11f5c4ba06c4faad161e3cc141299ccf92b7549b3f34703bb90134a390ac', 'hex')" + } + ], + "result": "cHNidP8BAF4CAAAAAWbQAKi9hNXynJhqPu8bqkvp0kHihVShkWGh3yLy15+LAAAAAAD/////AbgFAQAAAAAAIlEgRvZJfLLxnVDD6emCqVDcyGIUsB/M5DekIGHHvbEjDTMAAAAAAAEBK7gFAQAAAAAAIlEglCHnNLD50sRn6n3Rl8Yay0RnzcvJ9MsMVx+LY6XEDK4BCKcDQLk07Y+xhWq5Wn/rRAhzt5NhyvXp4AHMOkWJtbEGB9egQOsfSgLMMfkdDyW7tL1VYnAq8nsYkgwYsMJf+fXepKwiIC5ovBH1xLoGxPqtFh48wUEpnM+St1SbPzRwO7kBNKOQrEHAmCoodnZbs3tToSQYuecrivqNVONEob1YUpmiEfvmJfMaUpyfs81+d21htiJbbGEOiQb7j6psWaxcPpW1+C0p1gAA" + }, "getFeeRate": { "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgf0cwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMASICAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAQEDBAEAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHIgIDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtxHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwEiAgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc0cwRAIgZfRbpZmLWaJ//hp77QFq8fH5DVSzqo90UKpfVqJRA70CIH9yRwOtHtuWaAsoS1bU/8uI9/t1nqu+CKow8puFE4PSAQEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA", "fee": 21 diff --git a/test/integration/taproot.md b/test/integration/taproot.md index 401034061..7627450e2 100644 --- a/test/integration/taproot.md +++ b/test/integration/taproot.md @@ -9,8 +9,9 @@ A simple keyspend example that is possible with the current API is below. ## TODO -- [ ] p2tr payment API to make script spends easier +- [x] p2tr payment API to make script spends easier - [ ] Support within the Psbt class + - partial support added ## Example diff --git a/test/integration/taproot.spec.ts b/test/integration/taproot.spec.ts index f7b3733fa..01c785e80 100644 --- a/test/integration/taproot.spec.ts +++ b/test/integration/taproot.spec.ts @@ -1,11 +1,17 @@ import BIP32Factory from 'bip32'; +import ECPairFactory from 'ecpair'; import * as ecc from 'tiny-secp256k1'; import { describe, it } from 'mocha'; -import * as bitcoin from '../..'; import { regtestUtils } from './_regtest'; +import * as bitcoin from '../..'; +import { Taptree, isTaptree } from '../../src/types'; +import { buildTapscriptFinalizer, toXOnly } from '../psbt.utils'; + const rng = require('randombytes'); const regtest = regtestUtils.network; +bitcoin.initEccLib(ecc); const bip32 = BIP32Factory(ecc); +const ECPair = ECPairFactory(ecc); describe('bitcoinjs-lib (transaction with taproot)', () => { it('can create (and broadcast via 3PBP) a taproot keyspend Transaction', async () => { @@ -42,6 +48,376 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { value: sendAmount, }); }); + + it('can create (and broadcast via 3PBP) a taproot key-path spend Transaction', async () => { + const internalKey = bip32.fromSeed(rng(64), regtest); + + const { output, address } = bitcoin.payments.p2tr({ + internalPubkey: toXOnly(internalKey.publicKey), + network: regtest, + }); + + // amount from faucet + const amount = 42e4; + // amount to send + const sendAmount = amount - 1e4; + // get faucet + const unspent = await regtestUtils.faucetComplex(output!, amount); + + const psbt = new bitcoin.Psbt({ network: regtest }); + psbt.addInput({ + hash: unspent.txId, + index: 0, + witnessUtxo: { value: amount, script: output! }, + }); + psbt.addOutput({ value: sendAmount, address: address! }); + + const tweakedSigher = tweakSigner(internalKey!, { network: regtest }); + psbt.signInput(0, tweakedSigher); + + psbt.finalizeAllInputs(); + const tx = psbt.extractTransaction(); + const rawTx = tx.toBuffer(); + + const hex = rawTx.toString('hex'); + + await regtestUtils.broadcast(hex); + await regtestUtils.verify({ + txId: tx.getId(), + address: address!, + vout: 0, + value: sendAmount, + }); + }); + + it('can create (and broadcast via 3PBP) a taproot key-path spend Transaction (with unused scriptTree)', async () => { + const internalKey = bip32.fromSeed(rng(64), regtest); + const leafKey = bip32.fromSeed(rng(64), regtest); + + const leafScriptAsm = `${toXOnly(leafKey.publicKey).toString( + 'hex', + )} OP_CHECKSIG`; + const leafScript = bitcoin.script.fromASM(leafScriptAsm); + + const scriptTree = { + output: leafScript, + }; + + const { output, address, hash } = bitcoin.payments.p2tr({ + internalPubkey: toXOnly(internalKey.publicKey), + scriptTree, + network: regtest, + }); + + // amount from faucet + const amount = 42e4; + // amount to send + const sendAmount = amount - 1e4; + // get faucet + const unspent = await regtestUtils.faucetComplex(output!, amount); + + const psbt = new bitcoin.Psbt({ network: regtest }); + psbt.addInput({ + hash: unspent.txId, + index: 0, + witnessUtxo: { value: amount, script: output! }, + }); + psbt.addOutput({ value: sendAmount, address: address! }); + + const tweakedSigher = tweakSigner(internalKey!, { + tweakHash: hash, + network: regtest, + }); + psbt.signInput(0, tweakedSigher); + + psbt.finalizeAllInputs(); + const tx = psbt.extractTransaction(); + const rawTx = tx.toBuffer(); + + const hex = rawTx.toString('hex'); + + await regtestUtils.broadcast(hex); + await regtestUtils.verify({ + txId: tx.getId(), + address: address!, + vout: 0, + value: sendAmount, + }); + }); + + it('can create (and broadcast via 3PBP) a taproot script-path spend Transaction - OP_CHECKSIG(VERIFY)', async () => { + const internalKey = bip32.fromSeed(rng(64), regtest); + const leafKeys = [ + bip32.fromSeed(rng(64), regtest), + bip32.fromSeed(rng(64), regtest), + ]; + const leafXOnlyKeys = leafKeys.map(leafKey => toXOnly(leafKey.publicKey)); + + const redeem = bitcoin.payments.p2tr_ns({ pubkeys: leafXOnlyKeys }); + + const scriptTree = [ + [ + { + output: bitcoin.script.fromASM( + '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG', + ), + }, + [ + { + output: bitcoin.script.fromASM( + '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac1 OP_CHECKSIG', + ), + }, + { + output: bitcoin.script.fromASM( + '2258b1c3160be0864a541854eec9164a572f094f7562628281a8073bb89173a7 OP_CHECKSIG', + ), + }, + ], + ], + [ + { + output: bitcoin.script.fromASM( + '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac2 OP_CHECKSIG', + ), + }, + [ + { + output: bitcoin.script.fromASM( + '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac3 OP_CHECKSIG', + ), + }, + [ + { + output: bitcoin.script.fromASM( + '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac4 OP_CHECKSIG', + ), + }, + redeem, + ], + ], + ], + ]; + + if (!isTaptree(scriptTree)) throw new Error('Invalid taptree'); + + const { output, address } = bitcoin.payments.p2tr({ + internalPubkey: toXOnly(internalKey.publicKey), + scriptTree, + redeem, + network: regtest, + }); + + // amount from faucet + const amount = 42e4; + // amount to send + const sendAmount = amount - 1e4; + // get faucet + const unspent = await regtestUtils.faucetComplex(output!, amount); + + const psbt = new bitcoin.Psbt({ network: regtest }); + psbt.addInput({ + hash: unspent.txId, + index: 0, + witnessUtxo: { value: amount, script: output! }, + witnessScript: redeem.output, + }); + psbt.addOutput({ value: sendAmount, address: address! }); + + psbt.signInput(0, leafKeys[0]); + psbt.signInput(0, leafKeys[1]); + + const tapscriptFinalizer = buildTapscriptFinalizer( + internalKey.publicKey, + scriptTree, + regtest, + ); + psbt.finalizeInput(0, tapscriptFinalizer); + const tx = psbt.extractTransaction(); + const rawTx = tx.toBuffer(); + const hex = rawTx.toString('hex'); + + await regtestUtils.broadcast(hex); + await regtestUtils.verify({ + txId: tx.getId(), + address: address!, + vout: 0, + value: sendAmount, + }); + }); + + it('can create (and broadcast via 3PBP) a taproot script-path spend Transaction - OP_CHECKSEQUENCEVERIFY', async () => { + const internalKey = bip32.fromSeed(rng(64), regtest); + const leafKey = bip32.fromSeed(rng(64), regtest); + const leafPubkey = toXOnly(leafKey.publicKey).toString('hex'); + + const leafScriptAsm = `OP_10 OP_CHECKSEQUENCEVERIFY OP_DROP ${leafPubkey} OP_CHECKSIG`; + const leafScript = bitcoin.script.fromASM(leafScriptAsm); + + const scriptTree: Taptree = [ + { + output: bitcoin.script.fromASM( + '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG', + ), + }, + [ + { + output: bitcoin.script.fromASM( + '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG', + ), + }, + { + output: leafScript, + }, + ], + ]; + const redeem = { + output: leafScript, + redeemVersion: 192, + }; + + const { output, address } = bitcoin.payments.p2tr({ + internalPubkey: toXOnly(internalKey.publicKey), + scriptTree, + redeem, + network: regtest, + }); + + // amount from faucet + const amount = 42e4; + // amount to send + const sendAmount = amount - 1e4; + // get faucet + const unspent = await regtestUtils.faucetComplex(output!, amount); + + const psbt = new bitcoin.Psbt({ network: regtest }); + psbt.addInput({ + hash: unspent.txId, + index: 0, + sequence: 10, + witnessUtxo: { value: amount, script: output! }, + witnessScript: redeem.output, + }); + psbt.addOutput({ value: sendAmount, address: address! }); + + psbt.signInput(0, leafKey); + + const tapscriptFinalizer = buildTapscriptFinalizer( + internalKey.publicKey, + scriptTree, + regtest, + ); + psbt.finalizeInput(0, tapscriptFinalizer); + const tx = psbt.extractTransaction(); + const rawTx = tx.toBuffer(); + const hex = rawTx.toString('hex'); + + try { + // broadcast before the confirmation period has expired + await regtestUtils.broadcast(hex); + throw new Error('Broadcast should fail.'); + } catch (err) { + if ((err as any).message !== 'non-BIP68-final') + throw new Error( + 'Expected OP_CHECKSEQUENCEVERIFY validation to fail. But it faild with: ' + + err, + ); + } + await regtestUtils.mine(10); + await regtestUtils.broadcast(hex); + await regtestUtils.verify({ + txId: tx.getId(), + address: address!, + vout: 0, + value: sendAmount, + }); + }); + + it('can create (and broadcast via 3PBP) a taproot script-path spend Transaction - OP_CHECKSIGADD (3-of-3)', async () => { + const internalKey = bip32.fromSeed(rng(64), regtest); + + const leafKeys = []; + const leafPubkeys = []; + for (let i = 0; i < 3; i++) { + const leafKey = bip32.fromSeed(rng(64), regtest); + leafKeys.push(leafKey); + leafPubkeys.push(toXOnly(leafKey.publicKey).toString('hex')); + } + + const leafScriptAsm = `${leafPubkeys[0]} OP_CHECKSIG ${ + leafPubkeys[1] + } OP_CHECKSIGADD ${leafPubkeys[2]} OP_CHECKSIGADD OP_3 OP_NUMEQUAL`; + + const leafScript = bitcoin.script.fromASM(leafScriptAsm); + + const scriptTree: Taptree = [ + { + output: bitcoin.script.fromASM( + '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG', + ), + }, + [ + { + output: bitcoin.script.fromASM( + '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG', + ), + }, + { + output: leafScript, + }, + ], + ]; + + const redeem = { + output: leafScript, + redeemVersion: 192, + }; + + const { output, address } = bitcoin.payments.p2tr({ + internalPubkey: toXOnly(internalKey.publicKey), + scriptTree, + redeem, + network: regtest, + }); + + // amount from faucet + const amount = 42e4; + // amount to send + const sendAmount = amount - 1e4; + // get faucet + const unspent = await regtestUtils.faucetComplex(output!, amount); + + const psbt = new bitcoin.Psbt({ network: regtest }); + psbt.addInput({ + hash: unspent.txId, + index: 0, + witnessUtxo: { value: amount, script: output! }, + witnessScript: redeem.output, + }); + psbt.addOutput({ value: sendAmount, address: address! }); + + psbt.signInput(0, leafKeys[0]); + psbt.signInput(0, leafKeys[1]); + psbt.signInput(0, leafKeys[2]); + + const tapscriptFinalizer = buildTapscriptFinalizer( + internalKey.publicKey, + scriptTree, + regtest, + ); + psbt.finalizeInput(0, tapscriptFinalizer); + const tx = psbt.extractTransaction(); + const rawTx = tx.toBuffer(); + const hex = rawTx.toString('hex'); + + await regtestUtils.broadcast(hex); + await regtestUtils.verify({ + txId: tx.getId(), + address: address!, + vout: 0, + value: sendAmount, + }); + }); }); // Order of the curve (N) - 1 @@ -59,7 +435,7 @@ const ONE = Buffer.from( // (This is recommended by BIP341) function createKeySpendOutput(publicKey: Buffer): Buffer { // x-only pubkey (remove 1 byte y parity) - const myXOnlyPubkey = publicKey.slice(1, 33); + const myXOnlyPubkey = toXOnly(publicKey); const commitHash = bitcoin.crypto.taggedHash('TapTweak', myXOnlyPubkey); const tweakResult = ecc.xOnlyPointAddTweak(myXOnlyPubkey, commitHash); if (tweakResult === null) throw new Error('Invalid Tweak'); @@ -86,7 +462,7 @@ function signTweaked(messageHash: Buffer, key: KeyPair): Uint8Array { : ecc.privateAdd(ecc.privateSub(N_LESS_1, key.privateKey!)!, ONE)!; const tweakHash = bitcoin.crypto.taggedHash( 'TapTweak', - key.publicKey.slice(1, 33), + toXOnly(key.publicKey), ); const newPrivateKey = ecc.privateAdd(privateKey!, tweakHash); if (newPrivateKey === null) throw new Error('Invalid Tweak'); @@ -120,3 +496,34 @@ function createSigned( tx.ins[0].witness = [signature]; return tx; } + +// This logic will be extracted to ecpair +function tweakSigner(signer: bitcoin.Signer, opts: any = {}): bitcoin.Signer { + // @ts-ignore + let privateKey: Uint8Array | undefined = signer.privateKey!; + if (!privateKey) { + throw new Error('Private key is required for tweaking signer!'); + } + if (signer.publicKey[0] === 3) { + privateKey = ecc.privateNegate(privateKey); + } + + const tweakedPrivateKey = ecc.privateAdd( + privateKey, + tapTweakHash(toXOnly(signer.publicKey), opts.tweakHash), + ); + if (!tweakedPrivateKey) { + throw new Error('Invalid tweaked private key!'); + } + + return ECPair.fromPrivateKey(Buffer.from(tweakedPrivateKey), { + network: opts.network, + }); +} + +function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer { + return bitcoin.crypto.taggedHash( + 'TapTweak', + Buffer.concat(h ? [pubKey, h] : [pubKey]), + ); +} diff --git a/test/payments.spec.ts b/test/payments.spec.ts index bc123cba3..f223b765c 100644 --- a/test/payments.spec.ts +++ b/test/payments.spec.ts @@ -1,9 +1,25 @@ import * as assert from 'assert'; +import * as ecc from 'tiny-secp256k1'; import { describe, it } from 'mocha'; import { PaymentCreator } from '../src/payments'; import * as u from './payments.utils'; -['embed', 'p2ms', 'p2pk', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh'].forEach(p => { +import { initEccLib } from '../src'; + +[ + 'embed', + 'p2ms', + 'p2pk', + 'p2pkh', + 'p2sh', + 'p2wpkh', + 'p2wsh', + 'p2tr', + 'p2tr_ns', +].forEach(p => { describe(p, () => { + beforeEach(() => { + initEccLib(p.startsWith('p2tr') ? ecc : undefined); + }); let fn: PaymentCreator; const payment = require('../src/payments/' + p); if (p === 'embed') { @@ -11,6 +27,7 @@ import * as u from './payments.utils'; } else { fn = payment[p]; } + const fixtures = require('./fixtures/' + p); fixtures.valid.forEach((f: any) => { diff --git a/test/payments.utils.ts b/test/payments.utils.ts index c0635f3cf..12fc20712 100644 --- a/test/payments.utils.ts +++ b/test/payments.utils.ts @@ -52,6 +52,12 @@ function equateBase(a: any, b: any, context: string): void { tryHex(b.witness), `Inequal ${context}witness`, ); + if ('redeemVersion' in b) + t.strictEqual( + a.redeemVersion, + b.redeemVersion, + `Inequal ${context}redeemVersion`, + ); } export function equate(a: any, b: any, args?: any): void { @@ -62,10 +68,12 @@ export function equate(a: any, b: any, args?: any): void { if (b.input === null) b.input = undefined; if (b.output === null) b.output = undefined; if (b.witness === null) b.witness = undefined; + if (b.redeemVersion === null) b.redeemVersion = undefined; if (b.redeem) { if (b.redeem.input === null) b.redeem.input = undefined; if (b.redeem.output === null) b.redeem.output = undefined; if (b.redeem.witness === null) b.redeem.witness = undefined; + if (b.redeem.redeemVersion === null) b.redeem.redeemVersion = undefined; } equateBase(a, b, ''); @@ -86,6 +94,12 @@ export function equate(a: any, b: any, args?: any): void { t.strictEqual(tryHex(a.hash), tryHex(b.hash), 'Inequal *.hash'); if ('pubkey' in b) t.strictEqual(tryHex(a.pubkey), tryHex(b.pubkey), 'Inequal *.pubkey'); + if ('internalPubkey' in b) + t.strictEqual( + tryHex(a.internalPubkey), + tryHex(b.internalPubkey), + 'Inequal *.internalPubkey', + ); if ('signature' in b) t.strictEqual( tryHex(a.signature), @@ -129,6 +143,7 @@ export function preform(x: any): any { if (x.data) x.data = x.data.map(fromHex); if (x.hash) x.hash = Buffer.from(x.hash, 'hex'); if (x.pubkey) x.pubkey = Buffer.from(x.pubkey, 'hex'); + if (x.internalPubkey) x.internalPubkey = Buffer.from(x.internalPubkey, 'hex'); if (x.signature) x.signature = Buffer.from(x.signature, 'hex'); if (x.pubkeys) x.pubkeys = x.pubkeys.map(fromHex); if (x.signatures) @@ -147,6 +162,7 @@ export function preform(x: any): any { x.redeem.network = (BNETWORKS as any)[x.redeem.network]; } + if (x.scriptTree) x.scriptTree = convertScriptTree(x.scriptTree); return x; } @@ -169,3 +185,12 @@ export function from(path: string, object: any, result?: any): any { return result; } + +function convertScriptTree(scriptTree: any): any { + if (Array.isArray(scriptTree)) return scriptTree.map(convertScriptTree); + + const script = Object.assign({}, scriptTree); + if (typeof script.output === 'string') + script.output = asmToBuffer(scriptTree.output); + return script; +} diff --git a/test/psbt.spec.ts b/test/psbt.spec.ts index f583e8068..76ab4da49 100644 --- a/test/psbt.spec.ts +++ b/test/psbt.spec.ts @@ -5,10 +5,13 @@ import * as crypto from 'crypto'; import ECPairFactory from 'ecpair'; import { describe, it } from 'mocha'; +import { initEccLib } from '../src'; + const bip32 = BIP32Factory(ecc); const ECPair = ECPairFactory(ecc); import { networks as NETWORKS, payments, Psbt, Signer, SignerAsync } from '..'; +import { buildTapscriptFinalizer } from './psbt.utils'; import * as preFixtures from './fixtures/psbt.json'; @@ -18,6 +21,12 @@ const validator = ( signature: Buffer, ): boolean => ECPair.fromPublicKey(pubkey).verify(msghash, signature); +const schnorrValidator = ( + pubkey: Buffer, + msghash: Buffer, + signature: Buffer, +): boolean => ECPair.fromPublicKey(pubkey).verifySchnorr(msghash, signature); + const initBuffers = (object: any): typeof preFixtures => JSON.parse(JSON.stringify(object), (_, value) => { const regex = new RegExp(/^Buffer.from\(['"](.*)['"], ['"](.*)['"]\)$/); @@ -72,6 +81,10 @@ const failedAsyncSigner = (publicKey: Buffer): SignerAsync => { // const b = (hex: string) => Buffer.from(hex, 'hex'); describe(`Psbt`, () => { + beforeEach(() => { + // provide the ECC lib only when required + initEccLib(undefined); + }); describe('BIP174 Test Vectors', () => { fixtures.bip174.invalid.forEach(f => { it(`Invalid: ${f.description}`, () => { @@ -133,6 +146,7 @@ describe(`Psbt`, () => { fixtures.bip174.signer.forEach(f => { it('Signs PSBT to the expected result', () => { + if (f.isTaproot) initEccLib(ecc); const psbt = Psbt.fromBase64(f.psbt); f.keys.forEach(({ inputToSign, WIF }) => { @@ -160,6 +174,7 @@ describe(`Psbt`, () => { fixtures.bip174.finalizer.forEach(f => { it('Finalizes inputs and gives the expected PSBT', () => { + if (f.isTaproot) initEccLib(ecc); const psbt = Psbt.fromBase64(f.psbt); psbt.finalizeAllInputs(); @@ -952,6 +967,63 @@ describe(`Psbt`, () => { }); }); + describe('validateSignaturesOfTaprootInput', () => { + const f = fixtures.validateSignaturesOfTaprootInput; + it('Correctly validates a signature', () => { + initEccLib(ecc); + const psbt = Psbt.fromBase64(f.psbt); + assert.strictEqual( + psbt.validateSignaturesOfInput(f.index, schnorrValidator), + true, + ); + }); + + it('Correctly validates a signature against a pubkey', () => { + initEccLib(ecc); + const psbt = Psbt.fromBase64(f.psbt); + assert.strictEqual( + psbt.validateSignaturesOfInput( + f.index, + schnorrValidator, + f.pubkey as any, + ), + true, + ); + assert.throws(() => { + psbt.validateSignaturesOfInput( + f.index, + schnorrValidator, + f.incorrectPubkey as any, + ); + }, new RegExp('No signatures for this pubkey')); + }); + }); + + describe('finalizeTaprootInput', () => { + it('Correctly finalizes a taproot script-path spend', () => { + initEccLib(ecc); + const f = fixtures.finalizeTaprootScriptPathSpendInput; + const psbt = Psbt.fromBase64(f.psbt); + const tapscriptFinalizer = buildTapscriptFinalizer( + f.internalPublicKey as any, + f.scriptTree, + NETWORKS.testnet, + ); + psbt.finalizeInput(0, tapscriptFinalizer); + assert.strictEqual(psbt.toBase64(), f.result); + }); + + it('Failes to finalize a taproot script-path spend when a finalizer is not provided', () => { + initEccLib(ecc); + const f = fixtures.finalizeTaprootScriptPathSpendInput; + const psbt = Psbt.fromBase64(f.psbt); + + assert.throws(() => { + psbt.finalizeInput(0); + }, new RegExp('Can not finalize input #0')); + }); + }); + describe('getFeeRate', () => { it('Throws error if called before inputs are finalized', () => { const f = fixtures.getFeeRate; diff --git a/test/psbt.utils.ts b/test/psbt.utils.ts new file mode 100644 index 000000000..b1aabb330 --- /dev/null +++ b/test/psbt.utils.ts @@ -0,0 +1,62 @@ +import { PsbtInput } from 'bip174/src/lib/interfaces'; +import * as bitcoin from './..'; + +/** + * Build finalizer function for Tapscript. + * Usees the default Tapscript version (0xc0). + * @returns finalizer function + */ +const buildTapscriptFinalizer = ( + internalPubkey: Buffer, + scriptTree: any, + network: bitcoin.networks.Network, +) => { + return ( + inputIndex: number, + input: PsbtInput, + script: Buffer, + _isSegwit: boolean, + _isP2SH: boolean, + _isP2WSH: boolean, + _isTapscript: boolean, + ): { + finalScriptSig: Buffer | undefined; + finalScriptWitness: Buffer | Buffer[] | undefined; + } => { + if (!internalPubkey || !scriptTree || !script) + throw new Error(`Can not finalize taproot input #${inputIndex}`); + + try { + const tapscriptSpend = bitcoin.payments.p2tr({ + internalPubkey: toXOnly(internalPubkey), + scriptTree, + redeem: { output: script }, + network, + }); + const stack = bitcoin.script.decompile(script); + if (!stack) throw new Error('Invalid script'); + const pushes = stack.filter(chunk => Buffer.isBuffer(chunk)) as Buffer[]; + const pkIdx = (pk: Buffer) => pushes.findIndex(chunk => pk.equals(chunk)); + let sigs: Buffer[] = []; + if (input.partialSig) { + const pkIdxSigs = input.partialSig.map(ps => ({ + idx: pkIdx(ps.pubkey), + sig: ps.signature, + })); + // Sigs need to be reverse script order + pkIdxSigs.sort((a, b) => a.idx - b.idx); + sigs = pkIdxSigs.map(({ sig }) => sig); + } + const finalScriptWitness = sigs.concat( + tapscriptSpend.witness as Buffer[], + ); + return { finalScriptWitness, finalScriptSig: undefined }; + } catch (err) { + throw new Error(`Can not finalize taproot input #${inputIndex}: ${err}`); + } + }; +}; + +const toXOnly = (pubKey: Buffer) => pubKey.slice(1, 33); + +export { buildTapscriptFinalizer, toXOnly }; diff --git a/ts_src/address.ts b/ts_src/address.ts index 753589d46..8004b2668 100644 --- a/ts_src/address.ts +++ b/ts_src/address.ts @@ -2,11 +2,9 @@ import { Network } from './networks'; import * as networks from './networks'; import * as payments from './payments'; import * as bscript from './script'; -import * as types from './types'; +import { typeforce, tuple, Hash160bit, UInt8 } from './types'; import { bech32, bech32m } from 'bech32'; import * as bs58check from 'bs58check'; -const { typeforce } = types; - export interface Base58CheckResult { hash: Buffer; version: number; @@ -21,7 +19,7 @@ export interface Bech32Result { const FUTURE_SEGWIT_MAX_SIZE: number = 40; const FUTURE_SEGWIT_MIN_SIZE: number = 2; const FUTURE_SEGWIT_MAX_VERSION: number = 16; -const FUTURE_SEGWIT_MIN_VERSION: number = 1; +const FUTURE_SEGWIT_MIN_VERSION: number = 2; const FUTURE_SEGWIT_VERSION_DIFF: number = 0x50; const FUTURE_SEGWIT_VERSION_WARNING: string = 'WARNING: Sending to a future segwit version address can lead to loss of funds. ' + @@ -93,7 +91,7 @@ export function fromBech32(address: string): Bech32Result { } export function toBase58Check(hash: Buffer, version: number): string { - typeforce(types.tuple(types.Hash160bit, types.UInt8), arguments); + typeforce(tuple(Hash160bit, UInt8), arguments); const payload = Buffer.allocUnsafe(21); payload.writeUInt8(version, 0); @@ -131,6 +129,9 @@ export function fromOutputScript(output: Buffer, network?: Network): string { try { return payments.p2wsh({ output, network }).address as string; } catch (e) {} + try { + return payments.p2tr({ output, network }).address as string; + } catch (e) {} try { return _toFutureSegwitAddress(output, network); } catch (e) {} @@ -165,6 +166,9 @@ export function toOutputScript(address: string, network?: Network): Buffer { return payments.p2wpkh({ hash: decodeBech32.data }).output as Buffer; if (decodeBech32.data.length === 32) return payments.p2wsh({ hash: decodeBech32.data }).output as Buffer; + } else if (decodeBech32.version === 1) { + 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/ecc_lib.ts b/ts_src/ecc_lib.ts new file mode 100644 index 000000000..eb4c59eeb --- /dev/null +++ b/ts_src/ecc_lib.ts @@ -0,0 +1,95 @@ +import { TinySecp256k1Interface } from './types'; + +const _ECCLIB_CACHE: { eccLib?: TinySecp256k1Interface } = {}; + +export function initEccLib(eccLib: TinySecp256k1Interface | undefined): void { + if (!eccLib) { + // allow clearing the library + _ECCLIB_CACHE.eccLib = eccLib; + } else if (eccLib !== _ECCLIB_CACHE.eccLib) { + // new instance, verify it + verifyEcc(eccLib!); + _ECCLIB_CACHE.eccLib = eccLib; + } +} + +export function getEccLib(): TinySecp256k1Interface { + if (!_ECCLIB_CACHE.eccLib) + throw new Error( + 'No ECC Library provided. You must call initEccLib() with a valid TinySecp256k1Interface instance', + ); + return _ECCLIB_CACHE.eccLib; +} + +const h = (hex: string): Buffer => Buffer.from(hex, 'hex'); + +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/index.ts b/ts_src/index.ts index d8b8619d1..64c4294c2 100644 --- a/ts_src/index.ts +++ b/ts_src/index.ts @@ -29,3 +29,4 @@ export { StackElement, } from './payments'; export { Input as TxInput, Output as TxOutput } from './transaction'; +export { initEccLib } from './ecc_lib'; diff --git a/ts_src/ops.ts b/ts_src/ops.ts index 8e2c41c11..dd8b1e6da 100644 --- a/ts_src/ops.ts +++ b/ts_src/ops.ts @@ -127,6 +127,8 @@ const OPS: { [key: string]: number } = { OP_NOP9: 184, OP_NOP10: 185, + OP_CHECKSIGADD: 186, + OP_PUBKEYHASH: 253, OP_PUBKEY: 254, OP_INVALIDOPCODE: 255, diff --git a/ts_src/payments/index.ts b/ts_src/payments/index.ts index 4b7f1117e..a50912128 100644 --- a/ts_src/payments/index.ts +++ b/ts_src/payments/index.ts @@ -1,4 +1,5 @@ import { Network } from '../networks'; +import { Taptree } from '../types'; import { p2data as embed } from './embed'; import { p2ms } from './p2ms'; import { p2pk } from './p2pk'; @@ -6,6 +7,8 @@ import { p2pkh } from './p2pkh'; import { p2sh } from './p2sh'; import { p2wpkh } from './p2wpkh'; import { p2wsh } from './p2wsh'; +import { p2tr } from './p2tr'; +import { p2tr_ns } from './p2tr_ns'; export interface Payment { name?: string; @@ -17,11 +20,14 @@ export interface Payment { pubkeys?: Buffer[]; input?: Buffer; signatures?: Buffer[]; + internalPubkey?: Buffer; pubkey?: Buffer; signature?: Buffer; address?: string; hash?: Buffer; redeem?: Payment; + redeemVersion?: number; + scriptTree?: Taptree; witness?: Buffer[]; } @@ -38,7 +44,7 @@ export type StackElement = Buffer | number; export type Stack = StackElement[]; export type StackFunction = () => Stack; -export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh }; +export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh, p2tr, p2tr_ns }; // TODO // witness commitment diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts new file mode 100644 index 000000000..646097c7d --- /dev/null +++ b/ts_src/payments/p2tr.ts @@ -0,0 +1,355 @@ +import { Buffer as NBuffer } from 'buffer'; +import { bitcoin as BITCOIN_NETWORK } from '../networks'; +import * as bscript from '../script'; +import { typeforce as typef, isTaptree, TAPLEAF_VERSION_MASK } from '../types'; +import { getEccLib } from '../ecc_lib'; +import { + toHashTree, + rootHashFromPath, + findScriptPath, + tapleafHash, + tapTweakHash, + LEAF_VERSION_TAPSCRIPT, +} from './taprootutils'; +import { Payment, PaymentOpts } from './index'; +import * as lazy from './lazy'; +import { bech32m } from 'bech32'; + +const OPS = bscript.OPS; +const TAPROOT_WITNESS_VERSION = 0x01; +const ANNEX_PREFIX = 0x50; + +export function p2tr(a: Payment, opts?: PaymentOpts): Payment { + if ( + !a.address && + !a.output && + !a.pubkey && + !a.internalPubkey && + !(a.witness && a.witness.length > 1) + ) + throw new TypeError('Not enough data'); + + opts = Object.assign({ validate: true }, opts || {}); + + typef( + { + address: typef.maybe(typef.String), + input: typef.maybe(typef.BufferN(0)), + network: typef.maybe(typef.Object), + output: typef.maybe(typef.BufferN(34)), + internalPubkey: typef.maybe(typef.BufferN(32)), + hash: typef.maybe(typef.BufferN(32)), // merkle root hash, the tweak + 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), + redeem: typef.maybe({ + output: typef.maybe(typef.Buffer), // tapleaf script + redeemVersion: typef.maybe(typef.Number), // tapleaf version + witness: typef.maybe(typef.arrayOf(typef.Buffer)), + }), + redeemVersion: typef.maybe(typef.Number), + }, + a, + ); + + const _address = lazy.value(() => { + const result = bech32m.decode(a.address!); + const version = result.words.shift(); + const data = bech32m.fromWords(result.words); + return { + version, + prefix: result.prefix, + data: NBuffer.from(data), + }; + }); + + // remove annex if present, ignored by taproot + const _witness = lazy.value(() => { + if (!a.witness || !a.witness.length) return; + if ( + a.witness.length >= 2 && + a.witness[a.witness.length - 1][0] === ANNEX_PREFIX + ) { + return a.witness.slice(0, -1); + } + 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 }; + + lazy.prop(o, 'address', () => { + if (!o.pubkey) return; + + const words = bech32m.toWords(o.pubkey); + words.unshift(TAPROOT_WITNESS_VERSION); + return bech32m.encode(network.bech32, words); + }); + + lazy.prop(o, 'hash', () => { + const hashTree = _hashTree(); + if (hashTree) return hashTree.hash; + const w = _witness(); + if (w && w.length > 1) { + const controlBlock = w[w.length - 1]; + const leafVersion = controlBlock[0] & TAPLEAF_VERSION_MASK; + const script = w[w.length - 2]; + const leafHash = tapleafHash({ + output: script, + redeemVersion: leafVersion, + }); + return rootHashFromPath(controlBlock, leafHash); + } + return null; + }); + lazy.prop(o, 'output', () => { + if (!o.pubkey) return; + return bscript.compile([OPS.OP_1, o.pubkey]); + }); + lazy.prop(o, 'redeemVersion', () => { + if (a.redeemVersion) return a.redeemVersion; + if ( + a.redeem && + a.redeem.redeemVersion !== undefined && + a.redeem.redeemVersion !== null + ) { + return a.redeem.redeemVersion; + } + + return LEAF_VERSION_TAPSCRIPT; + }); + lazy.prop(o, 'redeem', () => { + const witness = _witness(); // witness without annex + if (!witness || witness.length < 2) return; + + return { + output: witness[witness.length - 2], + witness: witness.slice(0, -2), + redeemVersion: witness[witness.length - 1][0] & TAPLEAF_VERSION_MASK, + }; + }); + lazy.prop(o, 'pubkey', () => { + if (a.pubkey) return a.pubkey; + if (a.output) return a.output.slice(2); + if (a.address) return _address().data; + if (o.internalPubkey) { + const tweakedKey = tweakKey(o.internalPubkey, o.hash); + if (tweakedKey) return tweakedKey.x; + } + }); + lazy.prop(o, 'internalPubkey', () => { + if (a.internalPubkey) return a.internalPubkey; + const witness = _witness(); + if (witness && witness.length > 1) + return witness[witness.length - 1].slice(1, 33); + }); + lazy.prop(o, 'signature', () => { + if (a.signature) return a.signature; + if (!a.witness || a.witness.length !== 1) return; + return a.witness[0]; + }); + + lazy.prop(o, 'witness', () => { + if (a.witness) return a.witness; + const hashTree = _hashTree(); + if (hashTree && a.redeem && a.redeem.output && a.internalPubkey) { + const leafHash = tapleafHash({ + output: a.redeem.output, + redeemVersion: o.redeemVersion, + }); + const path = findScriptPath(hashTree, leafHash); + if (!path) return; + const outputKey = tweakKey(a.internalPubkey, hashTree.hash); + if (!outputKey) return; + const controlBock = NBuffer.concat( + [ + NBuffer.from([o.redeemVersion! | outputKey.parity]), + a.internalPubkey, + ].concat(path), + ); + return [a.redeem.output, controlBock]; + } + if (a.signature) return [a.signature]; + }); + + // extended validation + if (opts.validate) { + let pubkey: Buffer = NBuffer.from([]); + if (a.address) { + if (network && network.bech32 !== _address().prefix) + throw new TypeError('Invalid prefix or Network mismatch'); + if (_address().version !== TAPROOT_WITNESS_VERSION) + throw new TypeError('Invalid address version'); + if (_address().data.length !== 32) + throw new TypeError('Invalid address data'); + pubkey = _address().data; + } + + if (a.pubkey) { + if (pubkey.length > 0 && !pubkey.equals(a.pubkey)) + throw new TypeError('Pubkey mismatch'); + else pubkey = a.pubkey; + } + + if (a.output) { + if ( + a.output.length !== 34 || + a.output[0] !== OPS.OP_1 || + a.output[1] !== 0x20 + ) + throw new TypeError('Output is invalid'); + if (pubkey.length > 0 && !pubkey.equals(a.output.slice(2))) + throw new TypeError('Pubkey mismatch'); + else pubkey = a.output.slice(2); + } + + if (a.internalPubkey) { + const tweakedKey = tweakKey(a.internalPubkey, o.hash); + if (pubkey.length > 0 && !pubkey.equals(tweakedKey!.x)) + throw new TypeError('Pubkey mismatch'); + else pubkey = tweakedKey!.x; + } + + if (pubkey && pubkey.length) { + if (!getEccLib().isXOnlyPoint(pubkey)) + throw new TypeError('Invalid pubkey for p2tr'); + } + + const hashTree = _hashTree(); + + if (a.hash && hashTree) { + if (!a.hash.equals(hashTree.hash)) throw new TypeError('Hash mismatch'); + } + + if (a.redeem && a.redeem.output && hashTree) { + const leafHash = tapleafHash({ + output: a.redeem.output, + redeemVersion: o.redeemVersion, + }); + if (!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 + if (a.redeem && o.redeem) { + if (a.redeem.redeemVersion) { + if (a.redeem.redeemVersion !== o.redeem.redeemVersion) + throw new TypeError('Redeem.redeemVersion and witness mismatch'); + } + + if (a.redeem.output) { + if (bscript.decompile(a.redeem.output)!.length === 0) + throw new TypeError('Redeem.output is invalid'); + + // output redeem is constructed from the witness + if (o.redeem.output && !a.redeem.output.equals(o.redeem.output)) + throw new TypeError('Redeem.output and witness mismatch'); + } + if (a.redeem.witness) { + if ( + o.redeem.witness && + !stacksEqual(a.redeem.witness, o.redeem.witness) + ) + throw new TypeError('Redeem.witness and witness mismatch'); + } + } + + if (witness && witness.length) { + if (witness.length === 1) { + // key spending + if (a.signature && !a.signature.equals(witness[0])) + throw new TypeError('Signature mismatch'); + } else { + // script path spending + const controlBlock = witness[witness.length - 1]; + if (controlBlock.length < 33) + throw new TypeError( + `The control-block length is too small. Got ${ + controlBlock.length + }, expected min 33.`, + ); + + if ((controlBlock.length - 33) % 32 !== 0) + throw new TypeError( + `The control-block length of ${controlBlock.length} is incorrect!`, + ); + + const m = (controlBlock.length - 33) / 32; + if (m > 128) + throw new TypeError( + `The script path is too long. Got ${m}, expected max 128.`, + ); + + const internalPubkey = controlBlock.slice(1, 33); + if (a.internalPubkey && !a.internalPubkey.equals(internalPubkey)) + throw new TypeError('Internal pubkey mismatch'); + + if (!getEccLib().isXOnlyPoint(internalPubkey)) + throw new TypeError('Invalid internalPubkey for p2tr witness'); + + const leafVersion = controlBlock[0] & TAPLEAF_VERSION_MASK; + const script = witness[witness.length - 2]; + + const leafHash = tapleafHash({ + output: script, + redeemVersion: leafVersion, + }); + const hash = rootHashFromPath(controlBlock, leafHash); + + const outputKey = tweakKey(internalPubkey, hash); + if (!outputKey) + // todo: needs test data + throw new TypeError('Invalid outputKey for p2tr witness'); + + if (pubkey.length && !pubkey.equals(outputKey.x)) + throw new TypeError('Pubkey mismatch for p2tr witness'); + + if (outputKey.parity !== (controlBlock[0] & 1)) + throw new Error('Incorrect parity'); + } + } + } + + return Object.assign(o, a); +} + +interface TweakedPublicKey { + parity: number; + x: Buffer; +} + +function tweakKey( + pubKey: Buffer, + h: Buffer | undefined, +): TweakedPublicKey | null { + if (!NBuffer.isBuffer(pubKey)) return null; + if (pubKey.length !== 32) return null; + if (h && h.length !== 32) return null; + + const tweakHash = tapTweakHash(pubKey, h); + + const res = getEccLib().xOnlyPointAddTweak(pubKey, tweakHash); + if (!res || res.xOnlyPubkey === null) return null; + + return { + parity: res.parity, + x: NBuffer.from(res.xOnlyPubkey), + }; +} + +function stacksEqual(a: Buffer[], b: Buffer[]): boolean { + if (a.length !== b.length) return false; + + return a.every((x, i) => { + return x.equals(b[i]); + }); +} diff --git a/ts_src/payments/p2tr_ns.ts b/ts_src/payments/p2tr_ns.ts new file mode 100644 index 000000000..d2f6221ad --- /dev/null +++ b/ts_src/payments/p2tr_ns.ts @@ -0,0 +1,145 @@ +import { getEccLib } from '../ecc_lib'; +import { bitcoin as BITCOIN_NETWORK } from '../networks'; +import * as bscript from '../script'; +import { Payment, PaymentOpts, Stack } from './index'; +import * as lazy from './lazy'; +const OPS = bscript.OPS; +const typef = require('typeforce'); + +function stacksEqual(a: Buffer[], b: Buffer[]): boolean { + if (a.length !== b.length) return false; + + return a.every((x, i) => { + return x.equals(b[i]); + }); +} + +// input: [signatures ...] +// output: [pubKeys[0:n-1] OP_CHECKSIGVERIFY] pubKeys[n-1] OP_CHECKSIG +export function p2tr_ns(a: Payment, opts?: PaymentOpts): Payment { + if ( + !a.input && + !a.output && + !(a.pubkeys && a.pubkeys.length) && + !a.signatures + ) + throw new TypeError('Not enough data'); + opts = Object.assign({ validate: true }, opts || {}); + + function isAcceptableSignature(x: Buffer | number): boolean { + if (Buffer.isBuffer(x)) + return ( + // empty signatures may be represented as empty buffers + (opts && opts.allowIncomplete && x.length === 0) || + bscript.isCanonicalSchnorrSignature(x) + ); + return !!(opts && opts.allowIncomplete && x === OPS.OP_0); + } + + typef( + { + network: typef.maybe(typef.Object), + output: typef.maybe(typef.Buffer), + pubkeys: typef.maybe(typef.arrayOf(getEccLib().isXOnlyPoint)), + + signatures: typef.maybe(typef.arrayOf(isAcceptableSignature)), + input: typef.maybe(typef.Buffer), + }, + a, + ); + + const network = a.network || BITCOIN_NETWORK; + const o: Payment = { network }; + + const _chunks = lazy.value(() => { + if (!a.output) return; + return bscript.decompile(a.output) as Stack; + }); + + lazy.prop(o, 'output', () => { + if (!a.pubkeys) return; + return bscript.compile( + ([] as Stack).concat( + ...a.pubkeys.map((pk, i, pks) => [ + pk, + i === pks.length - 1 ? OPS.OP_CHECKSIG : OPS.OP_CHECKSIGVERIFY, + ]), + ), + ); + }); + lazy.prop(o, 'n', () => { + if (!o.pubkeys) return; + return o.pubkeys.length; + }); + lazy.prop(o, 'pubkeys', () => { + const chunks = _chunks(); + if (!chunks) return; + return chunks.filter((_, index) => index % 2 === 0) as Buffer[]; + }); + lazy.prop(o, 'signatures', () => { + if (!a.input) return; + return bscript.decompile(a.input)!.reverse(); + }); + lazy.prop(o, 'input', () => { + if (!a.signatures) return; + return bscript.compile([...a.signatures].reverse()); + }); + lazy.prop(o, 'witness', () => { + if (!o.input) return; + return []; + }); + lazy.prop(o, 'name', () => { + if (!o.n) return; + return `p2tr_ns(${o.n})`; + }); + + // extended validation + if (opts.validate) { + const chunks = _chunks(); + if (chunks) { + if (chunks[chunks.length - 1] !== OPS.OP_CHECKSIG) + throw new TypeError('Output ends with unexpected opcode'); + if ( + chunks + .filter((_, index) => index % 2 === 1) + .slice(0, -1) + .some(op => op !== OPS.OP_CHECKSIGVERIFY) + ) + throw new TypeError('Output contains unexpected opcode'); + if (o.n! > 16 || o.n !== chunks.length / 2) + throw new TypeError('Output contains too many pubkeys'); + if (o.pubkeys!.some(x => !getEccLib().isXOnlyPoint(x))) + throw new TypeError('Output contains invalid pubkey(s)'); + + if (a.pubkeys && !stacksEqual(a.pubkeys, o.pubkeys!)) + throw new TypeError('Pubkeys mismatch'); + } + + if (a.pubkeys && a.pubkeys.length) { + o.n = a.pubkeys.length; + } + + if (a.signatures) { + if (a.signatures.length < o.n!) + throw new TypeError('Not enough signatures provided'); + if (a.signatures.length > o.n!) + throw new TypeError('Too many signatures provided'); + } + + if (a.input) { + if (!o.signatures!.every(isAcceptableSignature)) + throw new TypeError('Input has invalid signature(s)'); + + if (a.signatures && !stacksEqual(a.signatures, o.signatures!)) + throw new TypeError('Signature mismatch'); + if (o.n !== o.signatures!.length) + throw new TypeError( + `Signature count mismatch (n: ${o.n}, signatures.length: ${ + o.signatures!.length + }`, + ); + } + } + + return Object.assign(o, a); +} diff --git a/ts_src/payments/taprootutils.ts b/ts_src/payments/taprootutils.ts new file mode 100644 index 000000000..eb08d5839 --- /dev/null +++ b/ts_src/payments/taprootutils.ts @@ -0,0 +1,116 @@ +import { Buffer as NBuffer } from 'buffer'; +import * as bcrypto from '../crypto'; + +import { varuint } from '../bufferutils'; +import { Tapleaf, Taptree, isTapleaf } from '../types'; + +export const LEAF_VERSION_TAPSCRIPT = 0xc0; + +export function rootHashFromPath( + controlBlock: Buffer, + leafHash: Buffer, +): Buffer { + const m = (controlBlock.length - 33) / 32; + + 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) { + kj = tapBranchHash(kj, ej); + } else { + kj = tapBranchHash(ej, kj); + } + } + + return kj; +} + +interface HashLeaf { + hash: Buffer; +} + +interface HashBranch { + hash: Buffer; + left: HashTree; + right: HashTree; +} + +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 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) }; + + 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, + right, + }; +} + +/** + * 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 - 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)) { + 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 []; + } + + return undefined; +} + +export function tapleafHash(leaf: Tapleaf): Buffer { + const version = leaf.redeemVersion || LEAF_VERSION_TAPSCRIPT; + return bcrypto.taggedHash( + 'TapLeaf', + NBuffer.concat([NBuffer.from([version]), serializeScript(leaf.output)]), + ); +} + +export function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer { + return bcrypto.taggedHash( + 'TapTweak', + NBuffer.concat(h ? [pubKey, h] : [pubKey]), + ); +} + +function tapBranchHash(a: Buffer, b: Buffer): Buffer { + return bcrypto.taggedHash('TapBranch', NBuffer.concat([a, b])); +} + +function serializeScript(s: Buffer): Buffer { + const varintLen = varuint.encodingLength(s.length); + const buffer = NBuffer.allocUnsafe(varintLen); // better + varuint.encode(s.length, buffer); + return NBuffer.concat([buffer, s]); +} diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index b9af10fcf..1517acffd 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -13,6 +13,7 @@ import { TransactionFromBuffer, } from 'bip174/src/lib/interfaces'; import { checkForInput, checkForOutput } from 'bip174/src/lib/utils'; + import { fromOutputScript, toOutputScript } from './address'; import { cloneBuffer, reverseBuffer } from './bufferutils'; import { hash160 } from './crypto'; @@ -20,6 +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'; export interface TransactionInput { hash: string | Buffer; @@ -348,11 +350,13 @@ export class Psbt { finalScriptsFunc: FinalScriptsFunc = getFinalScripts, ): this { const input = checkForInput(this.data.inputs, inputIndex); - const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( - inputIndex, - input, - this.__CACHE, - ); + const { + script, + isP2SH, + isP2WSH, + isSegwit, + isTapscript, + } = getScriptFromInput(inputIndex, input, this.__CACHE); if (!script) throw new Error(`No script found for input #${inputIndex}`); checkPartialSigSighashes(input); @@ -364,11 +368,17 @@ export class Psbt { isSegwit, isP2SH, isP2WSH, + isTapscript, ); if (finalScriptSig) this.data.updateInput(inputIndex, { finalScriptSig }); - if (finalScriptWitness) - this.data.updateInput(inputIndex, { finalScriptWitness }); + if (finalScriptWitness) { + // allow custom finalizers to build the witness as an array + const witness = Array.isArray(finalScriptWitness) + ? witnessStackToScriptWitness(finalScriptWitness) + : finalScriptWitness; + this.data.updateInput(inputIndex, { finalScriptWitness: witness }); + } if (!finalScriptSig && !finalScriptWitness) throw new Error(`Unknown error finalizing input #${inputIndex}`); @@ -378,7 +388,11 @@ export class Psbt { getInputType(inputIndex: number): AllScriptType { const input = checkForInput(this.data.inputs, inputIndex); - const script = getScriptFromUtxo(inputIndex, input, this.__CACHE); + const { script } = getScriptAndAmountFromUtxo( + inputIndex, + input, + this.__CACHE, + ); const result = getMeaningfulScript( script, inputIndex, @@ -445,13 +459,22 @@ export class Psbt { let hashCache: Buffer; let scriptCache: Buffer; let sighashCache: number; + const scriptType = this.getInputType(inputIndex); + for (const pSig of mySigs) { - const sig = bscript.signature.decode(pSig.signature); + const sig = isTaprootSpend(scriptType) + ? { + signature: pSig.signature, + hashType: Transaction.SIGHASH_DEFAULT, + } + : bscript.signature.decode(pSig.signature); + const { hash, script } = sighashCache! !== sig.hashType ? getHashForSig( inputIndex, Object.assign({}, input, { sighashType: sig.hashType }), + this.data.inputs, this.__CACHE, true, ) @@ -642,14 +665,31 @@ export class Psbt { sighashTypes, ); - const partialSig = [ - { + const scriptType = this.getInputType(inputIndex); + + if (isTaprootSpend(scriptType)) { + if (!keyPair.signSchnorr) { + throw new Error( + `Need Schnorr Signer to sign taproot input #${inputIndex}.`, + ); + } + const partialSig = this.data.inputs[inputIndex].partialSig || []; + partialSig.push({ pubkey: keyPair.publicKey, - signature: bscript.signature.encode(keyPair.sign(hash), sighashType), - }, - ]; + signature: keyPair.signSchnorr!(hash), + }); + // must be changed to use the `updateInput()` public API + this.data.inputs[inputIndex].partialSig = partialSig; + } else { + const partialSig = [ + { + pubkey: keyPair.publicKey, + signature: bscript.signature.encode(keyPair.sign(hash), sighashType), + }, + ]; + this.data.updateInput(inputIndex, { partialSig }); + } - this.data.updateInput(inputIndex, { partialSig }); return this; } @@ -669,6 +709,26 @@ export class Psbt { sighashTypes, ); + const scriptType = this.getInputType(inputIndex); + + if (isTaprootSpend(scriptType)) { + if (!keyPair.signSchnorr) { + throw new Error( + `Need Schnorr Signer to sign taproot input #${inputIndex}.`, + ); + } + return Promise.resolve(keyPair.signSchnorr(hash)).then(signature => { + const partialSig = this.data.inputs[inputIndex].partialSig || []; + partialSig.push({ + pubkey: keyPair.publicKey, + signature, + }); + + // must be changed to use the `updateInput()` public API + this.data.inputs[inputIndex].partialSig = partialSig; + }); + } + return Promise.resolve(keyPair.sign(hash)).then(signature => { const partialSig = [ { @@ -798,6 +858,7 @@ export interface HDSigner extends HDSignerBase { * Return a 64 byte signature (32 byte r and 32 byte s in that order) */ sign(hash: Buffer): Buffer; + signSchnorr?(hash: Buffer): Buffer; } /** @@ -806,12 +867,14 @@ export interface HDSigner extends HDSignerBase { export interface HDSignerAsync extends HDSignerBase { derivePath(path: string): HDSignerAsync; sign(hash: Buffer): Promise; + signSchnorr?(hash: Buffer): Promise; } export interface Signer { publicKey: Buffer; network?: any; sign(hash: Buffer, lowR?: boolean): Buffer; + signSchnorr?(hash: Buffer): Buffer; getPublicKey?(): Buffer; } @@ -819,6 +882,7 @@ export interface SignerAsync { publicKey: Buffer; network?: any; sign(hash: Buffer, lowR?: boolean): Promise; + signSchnorr?(hash: Buffer): Promise; getPublicKey?(): Buffer; } @@ -899,6 +963,7 @@ function canFinalize( case 'pubkey': case 'pubkeyhash': case 'witnesspubkeyhash': + case 'taproot': return hasSigs(1, input.partialSig); case 'multisig': const p2ms = payments.p2ms({ output: script }); @@ -955,6 +1020,7 @@ const isP2PKH = isPaymentFactory(payments.p2pkh); const isP2WPKH = isPaymentFactory(payments.p2wpkh); const isP2WSHScript = isPaymentFactory(payments.p2wsh); const isP2SHScript = isPaymentFactory(payments.p2sh); +const isP2TR = isPaymentFactory(payments.p2tr); function bip32DerivationIsMine( root: HDSigner, @@ -1141,11 +1207,12 @@ type FinalScriptsFunc = ( input: PsbtInput, // The PSBT input contents script: Buffer, // The "meaningful" locking script Buffer (redeemScript for P2SH etc.) isSegwit: boolean, // Is it segwit? + isTapscript: boolean, // Is taproot script path? isP2SH: boolean, // Is it P2SH? isP2WSH: boolean, // Is it P2WSH? ) => { finalScriptSig: Buffer | undefined; - finalScriptWitness: Buffer | undefined; + finalScriptWitness: Buffer | Buffer[] | undefined; }; function getFinalScripts( @@ -1155,12 +1222,13 @@ function getFinalScripts( isSegwit: boolean, isP2SH: boolean, isP2WSH: boolean, + isTapscript: boolean = false, ): { finalScriptSig: Buffer | undefined; finalScriptWitness: Buffer | undefined; } { const scriptType = classifyScript(script); - if (!canFinalize(input, script, scriptType)) + if (isTapscript || !canFinalize(input, script, scriptType)) throw new Error(`Can not finalize input #${inputIndex}`); return prepareFinalScripts( script, @@ -1227,6 +1295,7 @@ function getHashAndSighashType( const { hash, sighashType, script } = getHashForSig( inputIndex, input, + inputs, cache, false, sighashTypes, @@ -1241,6 +1310,7 @@ function getHashAndSighashType( function getHashForSig( inputIndex: number, input: PsbtInput, + inputs: PsbtInput[], cache: PsbtCache, forValidate: boolean, sighashTypes?: number[], @@ -1311,6 +1381,23 @@ function getHashForSig( prevout.value, sighashType, ); + } else if (isP2TR(prevout.script)) { + const prevOuts: Output[] = inputs.map((i, index) => + getScriptAndAmountFromUtxo(index, i, cache), + ); + const signingScripts: any = prevOuts.map(o => o.script); + const values: any = prevOuts.map(o => o.value); + const leafHash = input.witnessScript + ? tapleafHash({ output: input.witnessScript }) + : undefined; + + hash = unsignedTx.hashForWitnessV1( + inputIndex, + signingScripts, + values, + Transaction.SIGHASH_DEFAULT, + leafHash, + ); } else { // non-segwit if ( @@ -1379,6 +1466,15 @@ function getPayment( signature: partialSig[0].signature, }); break; + case 'taproot': + payment = payments.p2tr( + { + output: script, + signature: partialSig[0].signature, + }, + { validate: false }, // skip validation + ); + break; } return payment!; } @@ -1401,6 +1497,7 @@ function getPsigsFromInputFinalScripts(input: PsbtInput): PartialSig[] { interface GetScriptReturn { script: Buffer | null; isSegwit: boolean; + isTapscript: boolean; isP2SH: boolean; isP2WSH: boolean; } @@ -1413,31 +1510,45 @@ function getScriptFromInput( const res: GetScriptReturn = { script: null, isSegwit: false, + isTapscript: false, isP2SH: false, isP2WSH: false, }; - res.isP2SH = !!input.redeemScript; - res.isP2WSH = !!input.witnessScript; + let utxoScript = null; + if (input.nonWitnessUtxo) { + const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( + cache, + input, + inputIndex, + ); + const prevoutIndex = unsignedTx.ins[inputIndex].index; + utxoScript = nonWitnessUtxoTx.outs[prevoutIndex].script; + } else if (input.witnessUtxo) { + utxoScript = input.witnessUtxo.script; + } + if (input.witnessScript) { res.script = input.witnessScript; } else if (input.redeemScript) { res.script = input.redeemScript; } else { - if (input.nonWitnessUtxo) { - const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( - cache, - input, - inputIndex, - ); - const prevoutIndex = unsignedTx.ins[inputIndex].index; - res.script = nonWitnessUtxoTx.outs[prevoutIndex].script; - } else if (input.witnessUtxo) { - res.script = input.witnessUtxo.script; - } + res.script = utxoScript; } - if (input.witnessScript || isP2WPKH(res.script!)) { + + const isTaproot = utxoScript && isP2TR(utxoScript); + + // Segregated Witness versions 0 or 1 + if (input.witnessScript || isP2WPKH(res.script!) || isTaproot) { res.isSegwit = true; } + + if (isTaproot && input.witnessScript) { + res.isTapscript = true; + } + + res.isP2SH = !!input.redeemScript; + res.isP2WSH = !!input.witnessScript && !res.isTapscript; + return res; } @@ -1651,20 +1762,24 @@ function nonWitnessUtxoTxFromCache( return c[inputIndex]; } -function getScriptFromUtxo( +function getScriptAndAmountFromUtxo( inputIndex: number, input: PsbtInput, cache: PsbtCache, -): Buffer { +): { script: Buffer; value: number } { if (input.witnessUtxo !== undefined) { - return input.witnessUtxo.script; + return { + script: input.witnessUtxo.script, + value: input.witnessUtxo.value, + }; } else if (input.nonWitnessUtxo !== undefined) { const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( cache, input, inputIndex, ); - return nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index].script; + const o = nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index]; + return { script: o.script, value: o.value }; } else { throw new Error("Can't find pubkey in input without Utxo data"); } @@ -1676,7 +1791,7 @@ function pubkeyInInput( inputIndex: number, cache: PsbtCache, ): boolean { - const script = getScriptFromUtxo(inputIndex, input, cache); + const { script } = getScriptAndAmountFromUtxo(inputIndex, input, cache); const { meaningfulScript } = getMeaningfulScript( script, inputIndex, @@ -1760,11 +1875,12 @@ function getMeaningfulScript( witnessScript?: Buffer, ): { meaningfulScript: Buffer; - type: 'p2sh' | 'p2wsh' | 'p2sh-p2wsh' | 'raw'; + type: 'p2sh' | 'p2wsh' | 'p2sh-p2wsh' | 'p2tr' | 'raw'; } { const isP2SH = isP2SHScript(script); const isP2SHP2WSH = isP2SH && redeemScript && isP2WSHScript(redeemScript); const isP2WSH = isP2WSHScript(script); + const isP2TRScript = isP2TR(script); if (isP2SH && redeemScript === undefined) throw new Error('scriptPubkey is P2SH but redeemScript missing'); @@ -1787,6 +1903,9 @@ function getMeaningfulScript( } else if (isP2SH) { meaningfulScript = redeemScript!; checkRedeemScript(index, script, redeemScript!, ioType); + } else if (isP2TRScript && !!witnessScript) { + meaningfulScript = witnessScript; + // TODO: check here something? } else { meaningfulScript = script; } @@ -1798,6 +1917,8 @@ function getMeaningfulScript( ? 'p2sh' : isP2WSH ? 'p2wsh' + : isP2TRScript + ? 'p2tr' : 'raw', }; } @@ -1810,21 +1931,33 @@ function checkInvalidP2WSH(script: Buffer): void { function pubkeyInScript(pubkey: Buffer, script: Buffer): boolean { const pubkeyHash = hash160(pubkey); + const pubkeyXOnly = pubkey.slice(1, 33); const decompiled = bscript.decompile(script); if (decompiled === null) throw new Error('Unknown script error'); return decompiled.some(element => { if (typeof element === 'number') return false; - return element.equals(pubkey) || element.equals(pubkeyHash); + return ( + element.equals(pubkey) || + element.equals(pubkeyHash) || + element.equals(pubkeyXOnly) + ); }); } +function isTaprootSpend(scriptType: string): boolean { + return ( + !!scriptType && (scriptType === 'taproot' || scriptType.startsWith('p2tr-')) + ); +} + type AllScriptType = | 'witnesspubkeyhash' | 'pubkeyhash' | 'multisig' | 'pubkey' + | 'taproot' | 'nonstandard' | 'p2sh-witnesspubkeyhash' | 'p2sh-pubkeyhash' @@ -1838,18 +1971,22 @@ type AllScriptType = | 'p2sh-p2wsh-pubkeyhash' | 'p2sh-p2wsh-multisig' | 'p2sh-p2wsh-pubkey' - | 'p2sh-p2wsh-nonstandard'; + | 'p2sh-p2wsh-nonstandard' + | 'p2tr-pubkey' + | 'p2tr-nonstandard'; type ScriptType = | 'witnesspubkeyhash' | 'pubkeyhash' | 'multisig' | 'pubkey' + | 'taproot' | 'nonstandard'; 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)) return 'taproot'; return 'nonstandard'; } diff --git a/ts_src/script.ts b/ts_src/script.ts index 5d20ebc01..a10c629af 100644 --- a/ts_src/script.ts +++ b/ts_src/script.ts @@ -208,6 +208,13 @@ export function isCanonicalScriptSignature(buffer: Buffer): boolean { return bip66.check(buffer.slice(0, -1)); } +export function isCanonicalSchnorrSignature(buffer: Buffer): boolean { + if (!Buffer.isBuffer(buffer)) return false; + if (buffer.length === 64) return true; // implied SIGHASH_DEFAULT + if (buffer.length === 65 && isDefinedHashType(buffer[64])) return true; // explicit SIGHASH trailing byte + return false; +} + // tslint:disable-next-line variable-name export const number = scriptNumber; export const signature = scriptSignature; diff --git a/ts_src/types.ts b/ts_src/types.ts index c035b4008..073395b17 100644 --- a/ts_src/types.ts +++ b/ts_src/types.ts @@ -1,4 +1,5 @@ import { Buffer as NBuffer } from 'buffer'; + export const typeforce = require('typeforce'); const ZERO32 = NBuffer.alloc(32, 0); @@ -6,6 +7,7 @@ const EC_P = NBuffer.from( 'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f', 'hex', ); + export function isPoint(p: Buffer | number | undefined | null): boolean { if (!NBuffer.isBuffer(p)) return false; if (p.length < 33) return false; @@ -65,6 +67,48 @@ export const Network = typeforce.compile({ wif: typeforce.UInt8, }); +export interface XOnlyPointAddTweakResult { + parity: 1 | 0; + xOnlyPubkey: Uint8Array; +} + +export interface Tapleaf { + output: Buffer; + redeemVersion?: number; +} + +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.redeemVersion !== undefined) + return (o.redeemVersion & TAPLEAF_VERSION_MASK) === o.redeemVersion; + 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, Taptree] | Tapleaf; + +export function isTaptree(scriptTree: any): scriptTree is Taptree { + if (!Array(scriptTree)) return isTapleaf(scriptTree); + if (scriptTree.length !== 2) return false; + return scriptTree.every((t: any) => isTaptree(t)); +} + +export interface TinySecp256k1Interface { + isXOnlyPoint(p: Uint8Array): boolean; + xOnlyPointAddTweak( + p: Uint8Array, + tweak: Uint8Array, + ): XOnlyPointAddTweakResult | null; + privateAdd(d: Uint8Array, tweak: Uint8Array): Uint8Array | null; + privateNegate(d: Uint8Array): Uint8Array; +} + export const Buffer256bit = typeforce.BufferN(32); export const Hash160bit = typeforce.BufferN(20); export const Hash256bit = typeforce.BufferN(32);