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);