From 574a287d309a312017f620c3c814e386cb220557 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 28 Oct 2021 17:53:15 +0300 Subject: [PATCH 01/53] chore: add bn.js to dependencies (previously it was present in devDependencies) --- package-lock.json | 21 ++++++++++++++++----- package.json | 2 +- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3e5bd51b5..9a60ab056 100644 --- a/package-lock.json +++ b/package-lock.json @@ -607,10 +607,9 @@ "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 + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==" }, "brace-expansion": { "version": "1.1.11", @@ -2379,7 +2378,19 @@ "integrity": "sha512-8qPw7zDK6Hco2tVGYGQeOmOPp/hZnREwy2iIkcq0ygAuqc9WHo29vKN94lNymh1QbB3nthtAMF6KTIrdbsIotA==", "dev": true, "requires": { - "uint8array-tools": "0.0.6" + "bindings": "^1.3.0", + "bn.js": "^4.11.8", + "create-hmac": "^1.1.7", + "elliptic": "^6.4.0", + "nan": "^2.13.2" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + } } }, "to-fast-properties": { diff --git a/package.json b/package.json index c5553d57c..172aa18b8 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "dependencies": { "bech32": "^2.0.0", "bip174": "^2.0.1", + "bn.js": "^5.2.0", "bs58check": "^2.1.2", "create-hash": "^1.1.0", "typeforce": "^1.11.3", @@ -70,7 +71,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", From 7ae53689a2be0801070cd968c35e3a393be44b99 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 28 Oct 2021 17:55:41 +0300 Subject: [PATCH 02/53] feat: add liftX() function (first version) --- ts_src/types.ts | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/ts_src/types.ts b/ts_src/types.ts index c035b4008..d4b37cc66 100644 --- a/ts_src/types.ts +++ b/ts_src/types.ts @@ -1,4 +1,7 @@ import { Buffer as NBuffer } from 'buffer'; +// todo, use import? +const BN = require('bn.js'); + export const typeforce = require('typeforce'); const ZERO32 = NBuffer.alloc(32, 0); @@ -6,6 +9,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; @@ -25,6 +29,43 @@ export function isPoint(p: Buffer | number | undefined | null): boolean { return false; } +// todo review. Do not add dependcy to BN? +const EC_P_BN = new BN(EC_P) +const EC_P_REDUCTION = BN.red(EC_P_BN); +const EC_P_QUADRATIC_RESIDUE = EC_P_BN.addn(1).divn(4); +const BN_2 = new BN(2); +const BN_3 = new BN(3); +const BN_7 = new BN(7); + +export function liftX(buffer: Buffer): Buffer | null { + if (!NBuffer.isBuffer(buffer)) return null; + if (buffer.length !== 32) return null; + + if (buffer.compare(ZERO32) === 0) return null; + if (buffer.compare(EC_P) >= 0) return null; + + const x = new BN(buffer); + + const x1 = x.toRed(EC_P_REDUCTION); + const ySq = x1 + .redPow(BN_3) + .add(BN_7) + .mod(EC_P_BN); + + const y = ySq.redPow(EC_P_QUADRATIC_RESIDUE); + + if (!ySq.eq(y.redPow(BN_2))) { + return null; + } + const y1 = y.isEven() ? y : EC_P_BN.sub(y); + + return NBuffer.concat([ + NBuffer.from([0x04]), + NBuffer.from(x1.toBuffer('be', 32)), + NBuffer.from(y1.toBuffer('be', 32)), + ]); +} + const UINT31_MAX: number = Math.pow(2, 31) - 1; export function UInt31(value: number): boolean { return typeforce.UInt32(value) && value <= UINT31_MAX; From 8fb2f0a9bbb88e45ecf3d52579088a3143655718 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 28 Oct 2021 17:57:14 +0300 Subject: [PATCH 03/53] feat: update the Payment interface with taproot specific fields - add `internalPubkey` and `redeems` fields - add some temp comments --- ts_src/payments/index.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/ts_src/payments/index.ts b/ts_src/payments/index.ts index 4b7f1117e..d42649b3f 100644 --- a/ts_src/payments/index.ts +++ b/ts_src/payments/index.ts @@ -10,18 +10,20 @@ import { p2wsh } from './p2wsh'; export interface Payment { name?: string; network?: Network; - output?: Buffer; + output?: Buffer; // the full scriptPubKey data?: Buffer[]; m?: number; n?: number; pubkeys?: Buffer[]; input?: Buffer; signatures?: Buffer[]; - pubkey?: Buffer; + internalPubkey?: Buffer; // taproot: output key + pubkey?: Buffer; // taproot: output key signature?: Buffer; - address?: string; - hash?: Buffer; - redeem?: Payment; + address?: string; // taproot: betch32m + hash?: Buffer; // taproot: MAST root + redeem?: Payment; // taproot: when script path spending is used spending + redeems?: Payment; // taproot can have more than one redeem script witness?: Buffer[]; } From 47eab68b044ff87d3016570cff9ee9289c9bac79 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 28 Oct 2021 18:02:59 +0300 Subject: [PATCH 04/53] feat: add first version of p2tr; basic logic for key path construct/spend --- ts_src/payments/p2tr.ts | 134 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 ts_src/payments/p2tr.ts diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts new file mode 100644 index 000000000..439834247 --- /dev/null +++ b/ts_src/payments/p2tr.ts @@ -0,0 +1,134 @@ +// import * as bcrypto from '../../crypto'; +import { bitcoin as BITCOIN_NETWORK } from '../networks'; +import * as bscript from '../script'; +import { liftX, typeforce as typef } from '../types'; +import { Payment, PaymentOpts } from './index'; +import * as lazy from './lazy'; +import { bech32m } from 'bech32'; +const OPS = bscript.OPS; + +const TAPROOT_VERSION = 0x01; + +// witness: {signature} +// input: <> +// output: OP_1 {pubKey} +export function p2tr(a: Payment, opts?: PaymentOpts): Payment { + if (!a.address && !a.output && !a.pubkey && !a.output) + throw new TypeError('Not enough data'); + opts = Object.assign({ validate: true }, opts || {}); + + typef( + { + // todo: revisit + address: typef.maybe(typef.String), + hash: typef.maybe(typef.BufferN(20)), + input: typef.maybe(typef.BufferN(0)), + network: typef.maybe(typef.Object), + output: typef.maybe(typef.BufferN(34)), + pubkey: typef.maybe(typef.BufferN(32)), + signature: typef.maybe(bscript.isCanonicalScriptSignature), + witness: typef.maybe(typef.arrayOf(typef.Buffer)), + }, + 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: Buffer.from(data), + }; + }); + + // todo: clean-up withness (annex), etc + + 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_VERSION); + return bech32m.encode(network.bech32, words); + }); + + + lazy.prop(o, 'hash', () => { + // compute from MAST + }); + lazy.prop(o, 'output', () => { + if (!o.pubkey) return; + return bscript.compile([OPS.OP_1, o.pubkey]); + }); + lazy.prop(o, 'pubkey', () => { + if (a.pubkey) return a.pubkey; + if (a.output) return a.output.slice(2) + if (!a.address) return; + return _address().data; + }); + lazy.prop(o, 'signature', () => { + if (a.witness?.length !== 1) return; + return a.witness[0]; + }); + lazy.prop(o, 'input', () => { + // todo: not sure + }); + lazy.prop(o, 'witness', () => { + if (!a.signature) return; + return [a.signature]; + }); + + // extended validation + if (opts.validate) { + let pubkey: Buffer = Buffer.from([]); + if (a.address) { + if (network && network.bech32 !== _address().prefix) + throw new TypeError('Invalid prefix or Network mismatch'); + if (_address().version !== TAPROOT_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 (pubkey) { + if (liftX(pubkey) === null) + throw new TypeError('Invalid pubkey for p2tr'); + } + + if (a.witness) { + if (a.witness.length !== 1) throw new TypeError('Witness is invalid'); + + // todo: recheck + // if (!bscript.isCanonicalScriptSignature(a.witness[0])) + // throw new TypeError('Witness has invalid signature'); + + if (a.signature && !a.signature.equals(a.witness[0])) + throw new TypeError('Signature mismatch'); + } + } + + return Object.assign(o, a); +} From 38fe8546501524afda62ce31a39d5a8643c622d5 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 28 Oct 2021 18:04:54 +0300 Subject: [PATCH 05/53] test: add first tests for p2tr --- test/fixtures/p2tr.json | 169 ++++++++++++++++++++++++++++++++++++++++ test/payments.spec.ts | 2 +- 2 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/p2tr.json diff --git a/test/fixtures/p2tr.json b/test/fixtures/p2tr.json new file mode 100644 index 000000000..02cb6abfc --- /dev/null +++ b/test/fixtures/p2tr.json @@ -0,0 +1,169 @@ +{ + "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": "300602010002010001" + }, + "expected": { + "name": "p2tr", + "address": "bc1p4dss6gkgq8003g0qyd5drwfqrztsadf2w2v3juz73gdz7cx82r6sj7lcqx", + "output": "OP_1 ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "input": null, + "witness": [ + "300602010002010001" + ] + } + } + ], + "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" + } + }, + { + "exception": "Signature mismatch", + "arguments": { + "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", + "signature": "300602010002010002", + "witness": [ + "300602010002010001" + ] + } + }, + { + "exception": "Invalid prefix or Network mismatch", + "arguments": { + "address": "bcrt1prhepe49mpmhclwcqmkzpaz43revunykc7fc0f9az6pq08sn4qe7sxtrd8y" + } + }, + { + "exception": "Invalid address version", + "arguments": { + "address": "bc1z4dss6gkgq8003g0qyd5drwfqrztsadf2w2v3juz73gdz7cx82r6s6rxhwd" + } + }, + { + "exception": "Invalid address data", + "arguments": { + "address": "bc1p4dss6gkgq8003g0qyd5drwfqrztsadf2w2v3juz73gdz7cx82qh3d2w3" + } + }, + { + "exception": "Witness is invalid", + "arguments": { + "address": "bc1p4dss6gkgq8003g0qyd5drwfqrztsadf2w2v3juz73gdz7cx82r6sj7lcqx", + "witness": [] + } + } + ], + "dynamic": { + "depends": {}, + "details": [] + } +} \ No newline at end of file diff --git a/test/payments.spec.ts b/test/payments.spec.ts index bc123cba3..9e28501ae 100644 --- a/test/payments.spec.ts +++ b/test/payments.spec.ts @@ -2,7 +2,7 @@ import * as assert from 'assert'; 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 => { +['embed', 'p2ms', 'p2pk', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'].forEach(p => { describe(p, () => { let fn: PaymentCreator; const payment = require('../src/payments/' + p); From 1bf104e981b2ddb58ce82bbce39d6d0eca60668b Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 28 Oct 2021 18:08:20 +0300 Subject: [PATCH 06/53] feat: add generated files --- src/payments/index.d.ts | 2 + src/payments/p2tr.d.ts | 2 + src/payments/p2tr.js | 121 ++++++++++++++++++++++++++++++++++++++++ src/types.d.ts | 1 + src/types.js | 34 ++++++++++- 5 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 src/payments/p2tr.d.ts create mode 100644 src/payments/p2tr.js diff --git a/src/payments/index.d.ts b/src/payments/index.d.ts index 1edf07167..e569aa3cf 100644 --- a/src/payments/index.d.ts +++ b/src/payments/index.d.ts @@ -17,11 +17,13 @@ export interface Payment { pubkeys?: Buffer[]; input?: Buffer; signatures?: Buffer[]; + internalPubkey?: Buffer; pubkey?: Buffer; signature?: Buffer; address?: string; hash?: Buffer; redeem?: Payment; + redeems?: Payment; witness?: Buffer[]; } export declare type PaymentCreator = (a: Payment, opts?: PaymentOpts) => Payment; 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..2c3a0c71b --- /dev/null +++ b/src/payments/p2tr.js @@ -0,0 +1,121 @@ +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); +exports.p2tr = void 0; +// import * as bcrypto from '../../crypto'; +const networks_1 = require('../networks'); +const bscript = require('../script'); +const types_1 = require('../types'); +const lazy = require('./lazy'); +const bech32_1 = require('bech32'); +const OPS = bscript.OPS; +const TAPROOT_VERSION = 0x01; +// witness: {signature} +// input: <> +// output: OP_1 {pubKey} +function p2tr(a, opts) { + if (!a.address && !a.output && !a.pubkey && !a.output) + throw new TypeError('Not enough data'); + opts = Object.assign({ validate: true }, opts || {}); + (0, types_1.typeforce)( + { + // todo: revisit + address: types_1.typeforce.maybe(types_1.typeforce.String), + hash: types_1.typeforce.maybe(types_1.typeforce.BufferN(20)), + 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)), + pubkey: types_1.typeforce.maybe(types_1.typeforce.BufferN(32)), + signature: types_1.typeforce.maybe(bscript.isCanonicalScriptSignature), + witness: types_1.typeforce.maybe( + types_1.typeforce.arrayOf(types_1.typeforce.Buffer), + ), + }, + 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.from(data), + }; + }); + // todo: clean-up withness (annex), etc + 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_VERSION); + return bech32_1.bech32m.encode(network.bech32, words); + }); + lazy.prop(o, 'hash', () => { + // compute from MAST + }); + lazy.prop(o, 'output', () => { + if (!o.pubkey) return; + return bscript.compile([OPS.OP_1, o.pubkey]); + }); + lazy.prop(o, 'pubkey', () => { + if (a.pubkey) return a.pubkey; + if (a.output) return a.output.slice(2); + if (!a.address) return; + return _address().data; + }); + lazy.prop(o, 'signature', () => { + if (a.witness?.length !== 1) return; + return a.witness[0]; + }); + lazy.prop(o, 'input', () => { + // todo: not sure + }); + lazy.prop(o, 'witness', () => { + if (!a.signature) return; + return [a.signature]; + }); + // extended validation + if (opts.validate) { + let pubkey = Buffer.from([]); + if (a.address) { + if (network && network.bech32 !== _address().prefix) + throw new TypeError('Invalid prefix or Network mismatch'); + if (_address().version !== TAPROOT_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 (pubkey) { + if ((0, types_1.liftX)(pubkey) === null) + throw new TypeError('Invalid pubkey for p2tr'); + } + if (a.witness) { + if (a.witness.length !== 1) throw new TypeError('Witness is invalid'); + // todo: recheck + // if (!bscript.isCanonicalScriptSignature(a.witness[0])) + // throw new TypeError('Witness has invalid signature'); + if (a.signature && !a.signature.equals(a.witness[0])) + throw new TypeError('Signature mismatch'); + } + } + return Object.assign(o, a); +} +exports.p2tr = p2tr; diff --git a/src/types.d.ts b/src/types.d.ts index 5a8505d34..0fe6419f8 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,6 +1,7 @@ /// export declare const typeforce: any; export declare function isPoint(p: Buffer | number | undefined | null): boolean; +export declare function liftX(buffer: Buffer): Buffer | null; export declare function UInt31(value: number): boolean; export declare function BIP32Path(value: string): boolean; export declare namespace BIP32Path { diff --git a/src/types.js b/src/types.js index a6d1efa16..a7a89b0d2 100644 --- a/src/types.js +++ b/src/types.js @@ -1,7 +1,9 @@ '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.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.liftX = exports.isPoint = exports.typeforce = void 0; const buffer_1 = require('buffer'); +// todo, use import? +const BN = require('bn.js'); exports.typeforce = require('typeforce'); const ZERO32 = buffer_1.Buffer.alloc(32, 0); const EC_P = buffer_1.Buffer.from( @@ -25,6 +27,36 @@ function isPoint(p) { return false; } exports.isPoint = isPoint; +// todo review. Do not add dependcy to BN? +const EC_P_BN = new BN(EC_P); +const EC_P_REDUCTION = BN.red(EC_P_BN); +const EC_P_QUADRATIC_RESIDUE = EC_P_BN.addn(1).divn(4); +const BN_2 = new BN(2); +const BN_3 = new BN(3); +const BN_7 = new BN(7); +function liftX(buffer) { + if (!buffer_1.Buffer.isBuffer(buffer)) return null; + if (buffer.length !== 32) return null; + if (buffer.compare(ZERO32) === 0) return null; + if (buffer.compare(EC_P) >= 0) return null; + const x = new BN(buffer); + const x1 = x.toRed(EC_P_REDUCTION); + const ySq = x1 + .redPow(BN_3) + .add(BN_7) + .mod(EC_P_BN); + const y = ySq.redPow(EC_P_QUADRATIC_RESIDUE); + if (!ySq.eq(y.redPow(BN_2))) { + return null; + } + const y1 = y.isEven() ? y : EC_P_BN.sub(y); + return buffer_1.Buffer.concat([ + buffer_1.Buffer.from([0x04]), + buffer_1.Buffer.from(x1.toBuffer('be', 32)), + buffer_1.Buffer.from(y1.toBuffer('be', 32)), + ]); +} +exports.liftX = liftX; const UINT31_MAX = Math.pow(2, 31) - 1; function UInt31(value) { return exports.typeforce.UInt32(value) && value <= UINT31_MAX; From a598134b17a29da9465e42b2d1ddd48e1d618b74 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 29 Oct 2021 14:21:58 +0300 Subject: [PATCH 07/53] feat: compute "taproot output key" when "taoroot internal key" is known and script path is not used --- src/payments/p2tr.js | 30 +++++++++++++++++------- src/types.d.ts | 5 ++++ src/types.js | 38 +++++++++++++++++++++++++++++- test/fixtures/p2tr.json | 24 +++++++++++++++++++ test/payments.utils.ts | 1 + ts_src/payments/p2tr.ts | 32 ++++++++++++++++++-------- ts_src/types.ts | 51 ++++++++++++++++++++++++++++++++++++++++- 7 files changed, 162 insertions(+), 19 deletions(-) diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index 2c3a0c71b..cde6d705a 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -1,7 +1,6 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); exports.p2tr = void 0; -// import * as bcrypto from '../../crypto'; const networks_1 = require('../networks'); const bscript = require('../script'); const types_1 = require('../types'); @@ -13,17 +12,17 @@ const TAPROOT_VERSION = 0x01; // input: <> // output: OP_1 {pubKey} function p2tr(a, opts) { - if (!a.address && !a.output && !a.pubkey && !a.output) + if (!a.address && !a.output && !a.pubkey && !a.output && !a.internalPubkey) throw new TypeError('Not enough data'); opts = Object.assign({ validate: true }, opts || {}); (0, types_1.typeforce)( { - // todo: revisit address: types_1.typeforce.maybe(types_1.typeforce.String), - hash: types_1.typeforce.maybe(types_1.typeforce.BufferN(20)), 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(bscript.isCanonicalScriptSignature), witness: types_1.typeforce.maybe( @@ -52,7 +51,9 @@ function p2tr(a, opts) { return bech32_1.bech32m.encode(network.bech32, words); }); lazy.prop(o, 'hash', () => { - // compute from MAST + if (a.hash) return a.hash; + // todo: if (a.redeems?.length) compute from MAST root from redeems + return null; }); lazy.prop(o, 'output', () => { if (!o.pubkey) return; @@ -61,8 +62,12 @@ function p2tr(a, opts) { lazy.prop(o, 'pubkey', () => { if (a.pubkey) return a.pubkey; if (a.output) return a.output.slice(2); - if (!a.address) return; - return _address().data; + if (a.address) return _address().data; + if (a.internalPubkey) { + const tweakedKey = (0, types_1.tweakPublicKey)(a.internalPubkey, o.hash); + if (tweakedKey) return tweakedKey.x; + } + return null; }); lazy.prop(o, 'signature', () => { if (a.witness?.length !== 1) return; @@ -103,7 +108,16 @@ function p2tr(a, opts) { throw new TypeError('Pubkey mismatch'); else pubkey = a.output.slice(2); } - if (pubkey) { + // todo: optimze o.hash? + if (a.internalPubkey) { + const tweakedKey = (0, types_1.tweakPublicKey)(a.internalPubkey, o.hash); + if (tweakedKey === null) + throw new TypeError('Invalid internalPubkey for p2tr'); + if (pubkey.length > 0 && !pubkey.equals(tweakedKey.x)) + throw new TypeError('Pubkey mismatch'); + else pubkey = tweakedKey.x; + } + if (pubkey?.length) { if ((0, types_1.liftX)(pubkey) === null) throw new TypeError('Invalid pubkey for p2tr'); } diff --git a/src/types.d.ts b/src/types.d.ts index 0fe6419f8..cb24d6a48 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -2,6 +2,7 @@ export declare const typeforce: any; export declare function isPoint(p: Buffer | number | undefined | null): boolean; export declare function liftX(buffer: Buffer): Buffer | null; +export declare function tweakPublicKey(pubKey: Buffer, h: Buffer | undefined): TweakedPublicKey | null; export declare function UInt31(value: number): boolean; export declare function BIP32Path(value: string): boolean; export declare namespace BIP32Path { @@ -11,6 +12,10 @@ 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 TweakedPublicKey { + isOdd: boolean; + x: Buffer; +} 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 a7a89b0d2..1113ee89a 100644 --- a/src/types.js +++ b/src/types.js @@ -1,7 +1,11 @@ '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.liftX = 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.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.tweakPublicKey = exports.liftX = exports.isPoint = exports.typeforce = void 0; const buffer_1 = require('buffer'); +const bcrypto = require('./crypto'); +// Temp, to be replaced +// Only works because bip32 has it as dependecy. Linting will fail. +const ecc = require('tiny-secp256k1'); // todo, use import? const BN = require('bn.js'); exports.typeforce = require('typeforce'); @@ -57,6 +61,38 @@ function liftX(buffer) { ]); } exports.liftX = liftX; +const TAP_TWEAK_TAG = buffer_1.Buffer.from('TapTweak', 'utf8'); +const GROUP_ORDER = new BN( + buffer_1.Buffer.from( + 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', + 'hex', + ), +); +function tweakPublicKey(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 = bcrypto.taggedHash( + TAP_TWEAK_TAG, + buffer_1.Buffer.concat(h ? [pubKey, h] : [pubKey]), + ); + const t = new BN(tweakHash); + if (t.gte(GROUP_ORDER)) { + throw new Error('Tweak value over the SECP256K1 Order'); + } + const P = liftX(pubKey); + if (P === null) return null; + const Q = pointAddScalar(P, tweakHash); + return { + isOdd: Q[64] % 2 === 1, + x: Q.slice(1, 33), + }; +} +exports.tweakPublicKey = tweakPublicKey; +// todo: do not use ecc +function pointAddScalar(P, h) { + return ecc.pointAddScalar(P, h); +} const UINT31_MAX = Math.pow(2, 31) - 1; function UInt31(value) { return exports.typeforce.UInt32(value) && value <= UINT31_MAX; diff --git a/test/fixtures/p2tr.json b/test/fixtures/p2tr.json index 02cb6abfc..8916e0fad 100644 --- a/test/fixtures/p2tr.json +++ b/test/fixtures/p2tr.json @@ -58,6 +58,21 @@ "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 + } } ], "invalid": [ @@ -126,6 +141,15 @@ "address": "bc1pztta4jvddxsgdfgtxz2e5dfhj58n2mludag2ycatwhy28myagnqsnl7mv7" } }, + { + "description": "Pubkey mismatch between internalPubkey and pubkey", + "exception": "Pubkey mismatch", + "options": {}, + "arguments": { + "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7", + "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5" + } + }, { "exception": "Signature mismatch", "arguments": { diff --git a/test/payments.utils.ts b/test/payments.utils.ts index c0635f3cf..fbd6e58a6 100644 --- a/test/payments.utils.ts +++ b/test/payments.utils.ts @@ -129,6 +129,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) diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index 439834247..ff83ff43f 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -1,7 +1,6 @@ -// import * as bcrypto from '../../crypto'; import { bitcoin as BITCOIN_NETWORK } from '../networks'; import * as bscript from '../script'; -import { liftX, typeforce as typef } from '../types'; +import { liftX, tweakPublicKey, typeforce as typef } from '../types'; import { Payment, PaymentOpts } from './index'; import * as lazy from './lazy'; import { bech32m } from 'bech32'; @@ -13,18 +12,18 @@ const TAPROOT_VERSION = 0x01; // input: <> // output: OP_1 {pubKey} export function p2tr(a: Payment, opts?: PaymentOpts): Payment { - if (!a.address && !a.output && !a.pubkey && !a.output) + if (!a.address && !a.output && !a.pubkey && !a.output && !a.internalPubkey) throw new TypeError('Not enough data'); opts = Object.assign({ validate: true }, opts || {}); typef( { - // todo: revisit address: typef.maybe(typef.String), - hash: typef.maybe(typef.BufferN(20)), 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)), pubkey: typef.maybe(typef.BufferN(32)), signature: typef.maybe(bscript.isCanonicalScriptSignature), witness: typef.maybe(typef.arrayOf(typef.Buffer)), @@ -58,7 +57,9 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { lazy.prop(o, 'hash', () => { - // compute from MAST + if (a.hash) return a.hash; + // todo: if (a.redeems?.length) compute from MAST root from redeems + return null }); lazy.prop(o, 'output', () => { if (!o.pubkey) return; @@ -67,8 +68,12 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { lazy.prop(o, 'pubkey', () => { if (a.pubkey) return a.pubkey; if (a.output) return a.output.slice(2) - if (!a.address) return; - return _address().data; + if (a.address) return _address().data; + if (a.internalPubkey) { + const tweakedKey = tweakPublicKey(a.internalPubkey, o.hash) + if (tweakedKey) return tweakedKey.x + } + return null }); lazy.prop(o, 'signature', () => { if (a.witness?.length !== 1) return; @@ -113,7 +118,16 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { else pubkey = a.output.slice(2); } - if (pubkey) { + // todo: optimze o.hash? + if (a.internalPubkey) { + const tweakedKey = tweakPublicKey(a.internalPubkey, o.hash) + if (tweakedKey === null) throw new TypeError('Invalid internalPubkey for p2tr'); + if (pubkey.length > 0 && !pubkey.equals(tweakedKey.x)) + throw new TypeError('Pubkey mismatch'); + else pubkey = tweakedKey.x; + } + + if (pubkey?.length) { if (liftX(pubkey) === null) throw new TypeError('Invalid pubkey for p2tr'); } diff --git a/ts_src/types.ts b/ts_src/types.ts index d4b37cc66..8a8a4e4f9 100644 --- a/ts_src/types.ts +++ b/ts_src/types.ts @@ -1,4 +1,9 @@ import { Buffer as NBuffer } from 'buffer'; +import * as bcrypto from './crypto'; + +// Temp, to be replaced +// Only works because bip32 has it as dependecy. Linting will fail. +const ecc = require('tiny-secp256k1'); // todo, use import? const BN = require('bn.js'); @@ -30,7 +35,7 @@ export function isPoint(p: Buffer | number | undefined | null): boolean { } // todo review. Do not add dependcy to BN? -const EC_P_BN = new BN(EC_P) +const EC_P_BN = new BN(EC_P); const EC_P_REDUCTION = BN.red(EC_P_BN); const EC_P_QUADRATIC_RESIDUE = EC_P_BN.addn(1).divn(4); const BN_2 = new BN(2); @@ -66,6 +71,46 @@ export function liftX(buffer: Buffer): Buffer | null { ]); } +const TAP_TWEAK_TAG = NBuffer.from('TapTweak', 'utf8'); +const GROUP_ORDER = new BN( + NBuffer.from( + 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', + 'hex', + ), +); + +export function tweakPublicKey( + 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 = bcrypto.taggedHash( + TAP_TWEAK_TAG, + NBuffer.concat(h ? [pubKey, h] : [pubKey]), + ); + const t = new BN(tweakHash); + if (t.gte(GROUP_ORDER)) { + throw new Error('Tweak value over the SECP256K1 Order'); + } + + const P = liftX(pubKey); + if (P === null) return null; + + const Q = pointAddScalar(P, tweakHash); + return { + isOdd: Q[64] % 2 === 1, + x: Q.slice(1, 33), + }; +} + +// todo: do not use ecc +function pointAddScalar(P: Buffer, h: Buffer): Buffer { + return ecc.pointAddScalar(P, h); +} + const UINT31_MAX: number = Math.pow(2, 31) - 1; export function UInt31(value: number): boolean { return typeforce.UInt32(value) && value <= UINT31_MAX; @@ -106,6 +151,10 @@ export const Network = typeforce.compile({ wif: typeforce.UInt8, }); +export interface TweakedPublicKey { + isOdd: boolean; + x: Buffer; +} export const Buffer256bit = typeforce.BufferN(32); export const Hash160bit = typeforce.BufferN(20); export const Hash256bit = typeforce.BufferN(32); From 33c2bd94c2e5e43335fc9ff80390b47798038db4 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 29 Oct 2021 14:43:10 +0300 Subject: [PATCH 08/53] tests: improve test coverage --- src/payments/p2tr.js | 1 - test/fixtures/p2tr.json | 26 ++++++++++++++++++++++++++ ts_src/payments/p2tr.ts | 1 - 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index cde6d705a..c84579aea 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -67,7 +67,6 @@ function p2tr(a, opts) { const tweakedKey = (0, types_1.tweakPublicKey)(a.internalPubkey, o.hash); if (tweakedKey) return tweakedKey.x; } - return null; }); lazy.prop(o, 'signature', () => { if (a.witness?.length !== 1) return; diff --git a/test/fixtures/p2tr.json b/test/fixtures/p2tr.json index 8916e0fad..e4bc86af6 100644 --- a/test/fixtures/p2tr.json +++ b/test/fixtures/p2tr.json @@ -59,6 +59,25 @@ ] } }, + { + "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": { @@ -150,6 +169,13 @@ "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5" } }, + { + "exception": "Invalid internalPubkey for p2t", + "options": {}, + "arguments": { + "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f8" + } + }, { "exception": "Signature mismatch", "arguments": { diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index ff83ff43f..926301f85 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -73,7 +73,6 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { const tweakedKey = tweakPublicKey(a.internalPubkey, o.hash) if (tweakedKey) return tweakedKey.x } - return null }); lazy.prop(o, 'signature', () => { if (a.witness?.length !== 1) return; From f04d8b2598fb43f4f79239e5ae231ddcb8e93a88 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Mon, 1 Nov 2021 16:11:59 +0200 Subject: [PATCH 09/53] feat: add function `computeMastRoot()` --- src/merkle.d.ts | 1 + src/merkle.js | 46 +++++++++++++++++++++++++++++++++++++++++++++- ts_src/merkle.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/src/merkle.d.ts b/src/merkle.d.ts index d602201b9..d4b43f3ba 100644 --- a/src/merkle.d.ts +++ b/src/merkle.d.ts @@ -1,2 +1,3 @@ /// export declare function fastMerkleRoot(values: Buffer[], digestFn: (b: Buffer) => Buffer): Buffer; +export declare function computeMastRoot(scripts: any): Buffer; diff --git a/src/merkle.js b/src/merkle.js index e93f9cab6..ba00ec4d3 100644 --- a/src/merkle.js +++ b/src/merkle.js @@ -1,6 +1,13 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -exports.fastMerkleRoot = void 0; +exports.computeMastRoot = exports.fastMerkleRoot = void 0; +const buffer_1 = require('buffer'); +const bcrypto = require('./crypto'); +// todo: use varuint-bitcoin?? +const varuint = require('bip174/src/lib/converter/varint'); +const TAP_LEAF_TAG = buffer_1.Buffer.from('TapLeaf', 'utf8'); +const TAP_BRANCH_TAG = buffer_1.Buffer.from('TapBranch', 'utf8'); +const LEAF_VERSION_TAPSCRIPT = 0xc0; function fastMerkleRoot(values, digestFn) { if (!Array.isArray(values)) throw TypeError('Expected values Array'); if (typeof digestFn !== 'function') @@ -20,3 +27,40 @@ function fastMerkleRoot(values, digestFn) { return results[0]; } exports.fastMerkleRoot = fastMerkleRoot; +// todo: solve any[] +function computeMastRoot(scripts) { + if (scripts.length === 1) { + const script = scripts[0]; + if (Array.isArray(script)) { + return computeMastRoot(script); + } + script.version = script.version || LEAF_VERSION_TAPSCRIPT; + if ((script.version & 1) !== 0) throw new Error('Invalid script version'); // todo typedef error + // todo: if (script.output)scheck is bytes + const scriptOutput = buffer_1.Buffer.from(script.output, 'hex'); + return bcrypto.taggedHash( + TAP_LEAF_TAG, + buffer_1.Buffer.concat([ + buffer_1.Buffer.from([script.version]), + serializeScript(scriptOutput), + ]), + ); + } + // todo: this is a binary tree, use zero an one index + const half = Math.trunc(scripts.length / 2); + let leftHash = computeMastRoot(scripts.slice(0, half)); + let rightHash = computeMastRoot(scripts.slice(half)); + if (leftHash.compare(rightHash) === 1) + [leftHash, rightHash] = [rightHash, leftHash]; + return bcrypto.taggedHash( + TAP_BRANCH_TAG, + buffer_1.Buffer.concat([leftHash, rightHash]), + ); +} +exports.computeMastRoot = computeMastRoot; +function serializeScript(s) { + const varintLen = varuint.encodingLength(s.length); + const buffer = buffer_1.Buffer.allocUnsafe(varintLen); // better + varuint.encode(s.length, buffer); + return buffer_1.Buffer.concat([buffer, s]); +} diff --git a/ts_src/merkle.ts b/ts_src/merkle.ts index 8ff8c3f8c..d328acea9 100644 --- a/ts_src/merkle.ts +++ b/ts_src/merkle.ts @@ -1,3 +1,14 @@ +import { Buffer as NBuffer } from 'buffer'; +import * as bcrypto from './crypto'; +// todo: use varuint-bitcoin?? +import * as varuint from 'bip174/src/lib/converter/varint'; + + +const TAP_LEAF_TAG = NBuffer.from('TapLeaf', 'utf8'); +const TAP_BRANCH_TAG = NBuffer.from('TapBranch', 'utf8'); +const LEAF_VERSION_TAPSCRIPT = 0xc0 + + export function fastMerkleRoot( values: Buffer[], digestFn: (b: Buffer) => Buffer, @@ -25,3 +36,32 @@ export function fastMerkleRoot( return results[0]; } + +// todo: solve any[] +export function computeMastRoot(scripts: any): Buffer { + if (scripts.length === 1) { + const script = scripts[0] + if (Array.isArray(script)) { + return computeMastRoot(script) + } + script.version = script.version || LEAF_VERSION_TAPSCRIPT + if ((script.version & 1) !== 0) throw new Error("Invalid script version") // todo typedef error + // todo: if (script.output)scheck is bytes + const scriptOutput = NBuffer.from(script.output, 'hex') + return bcrypto.taggedHash(TAP_LEAF_TAG, NBuffer.concat([NBuffer.from([script.version]), serializeScript(scriptOutput)])) + } + // todo: this is a binary tree, use zero an one index + const half = Math.trunc(scripts.length / 2) + let leftHash = computeMastRoot(scripts.slice(0, half)) + let rightHash = computeMastRoot(scripts.slice(half)) + + if (leftHash.compare(rightHash) === 1) [leftHash, rightHash] = [rightHash, leftHash] + return bcrypto.taggedHash(TAP_BRANCH_TAG, NBuffer.concat([leftHash, rightHash])) +} + +function serializeScript(s: Buffer) { + const varintLen = varuint.encodingLength(s.length); + const buffer = NBuffer.allocUnsafe(varintLen); // better + varuint.encode(s.length, buffer); + return NBuffer.concat([buffer, s]) +} \ No newline at end of file From 583174d3485231acfd44515cbd48947e55c1fc8a Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Mon, 1 Nov 2021 16:12:55 +0200 Subject: [PATCH 10/53] feat: compute p2tr hash based on the script tree --- src/payments/p2tr.js | 4 +++- ts_src/payments/p2tr.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index c84579aea..6572ac080 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -4,6 +4,7 @@ exports.p2tr = void 0; const networks_1 = require('../networks'); const bscript = require('../script'); const types_1 = require('../types'); +const merkle_1 = require('../merkle'); const lazy = require('./lazy'); const bech32_1 = require('bech32'); const OPS = bscript.OPS; @@ -28,6 +29,7 @@ function p2tr(a, opts) { witness: types_1.typeforce.maybe( types_1.typeforce.arrayOf(types_1.typeforce.Buffer), ), + // scriptsTree: typef.maybe(typef.TaprootNode), // use merkel.isMast ? }, a, ); @@ -52,7 +54,7 @@ function p2tr(a, opts) { }); lazy.prop(o, 'hash', () => { if (a.hash) return a.hash; - // todo: if (a.redeems?.length) compute from MAST root from redeems + if (a.scriptsTree) return (0, merkle_1.computeMastRoot)(a.scriptsTree); return null; }); lazy.prop(o, 'output', () => { diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index 926301f85..c0365cfca 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -1,6 +1,7 @@ import { bitcoin as BITCOIN_NETWORK } from '../networks'; import * as bscript from '../script'; import { liftX, tweakPublicKey, typeforce as typef } from '../types'; +import { computeMastRoot } from '../merkle'; import { Payment, PaymentOpts } from './index'; import * as lazy from './lazy'; import { bech32m } from 'bech32'; @@ -27,6 +28,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { pubkey: typef.maybe(typef.BufferN(32)), signature: typef.maybe(bscript.isCanonicalScriptSignature), witness: typef.maybe(typef.arrayOf(typef.Buffer)), + // scriptsTree: typef.maybe(typef.TaprootNode), // use merkel.isMast ? }, a, ); @@ -58,7 +60,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { lazy.prop(o, 'hash', () => { if (a.hash) return a.hash; - // todo: if (a.redeems?.length) compute from MAST root from redeems + if (a.scriptsTree) return computeMastRoot(a.scriptsTree) return null }); lazy.prop(o, 'output', () => { From 1b96e6dbbe5bcb3a6ee1ae068d05acb0c77fea7d Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Mon, 1 Nov 2021 16:13:57 +0200 Subject: [PATCH 11/53] feat: add scriptsTree field to Payment interface; export p2tr --- src/payments/index.d.ts | 5 +++-- src/payments/index.js | 9 ++++++++- ts_src/payments/index.ts | 5 +++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/payments/index.d.ts b/src/payments/index.d.ts index e569aa3cf..dc1978ab0 100644 --- a/src/payments/index.d.ts +++ b/src/payments/index.d.ts @@ -7,6 +7,7 @@ import { p2pkh } from './p2pkh'; import { p2sh } from './p2sh'; import { p2wpkh } from './p2wpkh'; import { p2wsh } from './p2wsh'; +import { p2tr } from './p2tr'; export interface Payment { name?: string; network?: Network; @@ -23,7 +24,7 @@ export interface Payment { address?: string; hash?: Buffer; redeem?: Payment; - redeems?: Payment; + scriptsTree?: any; witness?: Buffer[]; } export declare type PaymentCreator = (a: Payment, opts?: PaymentOpts) => Payment; @@ -35,4 +36,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 }; diff --git a/src/payments/index.js b/src/payments/index.js index c23c529c6..9ce55f859 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 = 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,12 @@ 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; + }, +}); // TODO // witness commitment diff --git a/ts_src/payments/index.ts b/ts_src/payments/index.ts index d42649b3f..5243902d4 100644 --- a/ts_src/payments/index.ts +++ b/ts_src/payments/index.ts @@ -6,6 +6,7 @@ import { p2pkh } from './p2pkh'; import { p2sh } from './p2sh'; import { p2wpkh } from './p2wpkh'; import { p2wsh } from './p2wsh'; +import { p2tr } from './p2tr'; export interface Payment { name?: string; @@ -23,7 +24,7 @@ export interface Payment { address?: string; // taproot: betch32m hash?: Buffer; // taproot: MAST root redeem?: Payment; // taproot: when script path spending is used spending - redeems?: Payment; // taproot can have more than one redeem script + scriptsTree?: any // todo: solve witness?: Buffer[]; } @@ -40,7 +41,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 }; // TODO // witness commitment From fc1f1b2d0cde88b4e1026147ad7e052987904e5f Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Mon, 1 Nov 2021 16:15:45 +0200 Subject: [PATCH 12/53] feat: convert `scriptsTree` output to Buffer --- test/payments.utils.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test/payments.utils.ts b/test/payments.utils.ts index fbd6e58a6..71863a602 100644 --- a/test/payments.utils.ts +++ b/test/payments.utils.ts @@ -147,7 +147,8 @@ export function preform(x: any): any { if (x.redeem.network) x.redeem.network = (BNETWORKS as any)[x.redeem.network]; } - + if (x.scriptsTree) + x.scriptsTree = convertScriptsTree(x.scriptsTree) return x; } @@ -170,3 +171,15 @@ export function from(path: string, object: any, result?: any): any { return result; } + +// todo: solve any type +function convertScriptsTree(scriptsTree: any): any { + if (Array.isArray(scriptsTree)) + return scriptsTree.map(convertScriptsTree) + + + const script = Object.assign({}, scriptsTree); + if ((typeof script.output === 'string')) + script.output = asmToBuffer(scriptsTree.output) + return script +} From 91ccef3f14ab250413b04a20bec36b956922fed1 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Mon, 1 Nov 2021 16:16:17 +0200 Subject: [PATCH 13/53] tests: add tests for script tree --- test/fixtures/p2tr.json | 206 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) diff --git a/test/fixtures/p2tr.json b/test/fixtures/p2tr.json index e4bc86af6..3f088bb60 100644 --- a/test/fixtures/p2tr.json +++ b/test/fixtures/p2tr.json @@ -92,6 +92,212 @@ "input": null, "witness": null } + }, + { + "description": "address, pubkey, output and hash from internalPubkey and a script tree with one leaf", + "arguments": { + "internalPubkey": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + "scriptsTree": [ + { + "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", + "scriptsTree": [ + { + "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", + "scriptsTree": [ + { + "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", + "scriptsTree": [ + [ + { + "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", + "scriptsTree": [ + [ + { + "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, output and hash from internalPubkey and a script tree with seven leafs (2)", + "arguments": { + "internalPubkey": "aba457d16a8d59151c387f24d1eb887efbe24644c1ee64b261282e7baebdb247", + "scriptsTree": [ + { + "output": "00a9da96087a72258f83b338ef7f0ea8cbbe05da5f18f091eb397d1ecbf7c3d3 OP_CHECKSIG" + }, + [ + [ + { + "output": "00a9da96087a72258f83b338ef7f0ea8cbbe05da5f18f091eb397d1ecbf7c3d3 OP_CHECKSIG" + }, + [ + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG" + }, + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG" + } + ] + ], + [ + [ + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG" + }, + { + "output": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG" + } + ], + { + "output": "00a9da96087a72258f83b338ef7f0ea8cbbe05da5f18f091eb397d1ecbf7c3d3 OP_CHECKSIG" + } + ] + ] + ] + }, + "expected": { + "name": "p2tr", + "address": "bc1pmu8qwr9zljs9anger0d6q3uyr43yzjetmjmzf8p93ltycrwj28lsee3e0n", + "pubkey": "df0e070ca2fca05ecd191bdba047841d62414b2bdcb6249c258fd64c0dd251ff", + "output": "OP_1 df0e070ca2fca05ecd191bdba047841d62414b2bdcb6249c258fd64c0dd251ff", + "hash": "027391d0aac8d94725e4fcec4b07214d7c8a14bcdca2b1c08e4bc786308bdae5", + "signature": null, + "input": null, + "witness": null + } } ], "invalid": [ From d840ab618ec78d11dc8e39d533ac9b5ec4326abd Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Mon, 1 Nov 2021 16:17:13 +0200 Subject: [PATCH 14/53] feat: add simple type for TaprootLeaf and TaprootNode --- src/types.d.ts | 2 ++ src/types.js | 13 ++++++++++++- ts_src/types.ts | 10 ++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/types.d.ts b/src/types.d.ts index cb24d6a48..45bfcd1c6 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -16,6 +16,8 @@ export interface TweakedPublicKey { isOdd: boolean; x: Buffer; } +export declare const TaprootLeaf: any; +export declare const TaprootNode: any; 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 1113ee89a..668af87bd 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.tweakPublicKey = exports.liftX = 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.TaprootNode = exports.TaprootLeaf = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.tweakPublicKey = exports.liftX = exports.isPoint = exports.typeforce = void 0; const buffer_1 = require('buffer'); const bcrypto = require('./crypto'); // Temp, to be replaced @@ -136,6 +136,17 @@ exports.Network = exports.typeforce.compile({ scriptHash: exports.typeforce.UInt8, wif: exports.typeforce.UInt8, }); +exports.TaprootLeaf = exports.typeforce.compile({ + output: exports.typeforce.BufferN(34), + version: exports.typeforce.maybe(exports.typeforce.UInt8), // todo: recheck +}); +// / todo: revisit +exports.TaprootNode = exports.typeforce.arrayOf( + exports.typeforce.oneOf( + exports.TaprootLeaf, + exports.typeforce.arrayOf(exports.TaprootLeaf), + ), +); exports.Buffer256bit = exports.typeforce.BufferN(32); exports.Hash160bit = exports.typeforce.BufferN(20); exports.Hash256bit = exports.typeforce.BufferN(32); diff --git a/ts_src/types.ts b/ts_src/types.ts index 8a8a4e4f9..875df29f7 100644 --- a/ts_src/types.ts +++ b/ts_src/types.ts @@ -155,6 +155,16 @@ export interface TweakedPublicKey { isOdd: boolean; x: Buffer; } + +export const TaprootLeaf = typeforce.compile({ + output: typeforce.BufferN(34), + version: typeforce.maybe(typeforce.UInt8) // todo: recheck +}) + +// / todo: revisit +export const TaprootNode = typeforce.arrayOf(typeforce.oneOf(TaprootLeaf, typeforce.arrayOf(TaprootLeaf))) + + export const Buffer256bit = typeforce.BufferN(32); export const Hash160bit = typeforce.BufferN(20); export const Hash256bit = typeforce.BufferN(32); From fb7df4acf0fd132ff3a446c59ed3eced4d96372b Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Mon, 1 Nov 2021 16:36:41 +0200 Subject: [PATCH 15/53] feat: check for hash mismatch between the input hash and the computed hash from the taproot tree --- src/payments/p2tr.js | 4 ++++ test/fixtures/p2tr.json | 14 ++++++++++++++ ts_src/payments/p2tr.ts | 6 ++++++ 3 files changed, 24 insertions(+) diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index 6572ac080..be912a4f3 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -122,6 +122,10 @@ function p2tr(a, opts) { if ((0, types_1.liftX)(pubkey) === null) throw new TypeError('Invalid pubkey for p2tr'); } + if (a.hash && a.scriptsTree) { + const hash = (0, merkle_1.computeMastRoot)(a.scriptsTree); + if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch'); + } if (a.witness) { if (a.witness.length !== 1) throw new TypeError('Witness is invalid'); // todo: recheck diff --git a/test/fixtures/p2tr.json b/test/fixtures/p2tr.json index 3f088bb60..25fec29ba 100644 --- a/test/fixtures/p2tr.json +++ b/test/fixtures/p2tr.json @@ -375,6 +375,20 @@ "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5" } }, + { + "description": "Hash mismatch between scriptsTree and hash", + "exception": "Hash mismatch", + "options": {}, + "arguments": { + "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f7", + "scriptsTree": [ + { + "output": "83d8ee77a0f3a32a5cea96fd1624d623b836c1e5d1ac2dcde46814b619320c18 OP_CHECKSIG" + } + ], + "hash": "b76077013c8e303085e300000000000000000000000000000000000000000000" + } + }, { "exception": "Invalid internalPubkey for p2t", "options": {}, diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index c0365cfca..6bbbfa101 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -133,6 +133,12 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { throw new TypeError('Invalid pubkey for p2tr'); } + if (a.hash && a.scriptsTree) { + const hash = computeMastRoot(a.scriptsTree) + if (!a.hash.equals(hash)) + throw new TypeError('Hash mismatch'); + } + if (a.witness) { if (a.witness.length !== 1) throw new TypeError('Witness is invalid'); From af639f9eb580275e347d679f5376575bc1c8694f Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 2 Nov 2021 15:34:37 +0200 Subject: [PATCH 16/53] feat: validate witness data (partial) --- src/merkle.js | 1 + src/payments/p2tr.js | 95 +++++++++++++++++++++++---- src/types.d.ts | 2 + src/types.js | 63 ++++++++++++++++-- test/fixtures/p2tr.json | 139 +++++++++++++++++++++++++++++++++++++++- test/payments.utils.ts | 2 + ts_src/merkle.ts | 1 + ts_src/payments/p2tr.ts | 87 ++++++++++++++++++++----- ts_src/types.ts | 50 +++++++++++++-- 9 files changed, 396 insertions(+), 44 deletions(-) diff --git a/src/merkle.js b/src/merkle.js index ba00ec4d3..009260ac4 100644 --- a/src/merkle.js +++ b/src/merkle.js @@ -5,6 +5,7 @@ const buffer_1 = require('buffer'); const bcrypto = require('./crypto'); // todo: use varuint-bitcoin?? const varuint = require('bip174/src/lib/converter/varint'); +// todo: find better place for these consts const TAP_LEAF_TAG = buffer_1.Buffer.from('TapLeaf', 'utf8'); const TAP_BRANCH_TAG = buffer_1.Buffer.from('TapBranch', 'utf8'); const LEAF_VERSION_TAPSCRIPT = 0xc0; diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index be912a4f3..7d7e93a43 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -9,11 +9,19 @@ const lazy = require('./lazy'); const bech32_1 = require('bech32'); const OPS = bscript.OPS; const TAPROOT_VERSION = 0x01; +const ANNEX_PREFIX = 0x50; // witness: {signature} // input: <> // output: OP_1 {pubKey} function p2tr(a, opts) { - if (!a.address && !a.output && !a.pubkey && !a.output && !a.internalPubkey) + if ( + !a.address && + !a.output && + !a.pubkey && + !a.output && + !a.internalPubkey && + !(a.witness && a.witness.length > 1) + ) throw new TypeError('Not enough data'); opts = Object.assign({ validate: true }, opts || {}); (0, types_1.typeforce)( @@ -43,7 +51,17 @@ function p2tr(a, opts) { data: Buffer.from(data), }; }); - // todo: clean-up withness (annex), etc + 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 + ) { + // remove annex, ignored by taproot + return a.witness.slice(0, -1); + } + return a.witness.slice(); + }); const network = a.network || networks_1.bitcoin; const o = { name: 'p2tr', network }; lazy.prop(o, 'address', () => { @@ -55,6 +73,7 @@ function p2tr(a, opts) { lazy.prop(o, 'hash', () => { if (a.hash) return a.hash; if (a.scriptsTree) return (0, merkle_1.computeMastRoot)(a.scriptsTree); + // todo: compute from witness return null; }); lazy.prop(o, 'output', () => { @@ -65,11 +84,17 @@ function p2tr(a, opts) { if (a.pubkey) return a.pubkey; if (a.output) return a.output.slice(2); if (a.address) return _address().data; - if (a.internalPubkey) { - const tweakedKey = (0, types_1.tweakPublicKey)(a.internalPubkey, o.hash); + if (o.internalPubkey) { + const tweakedKey = (0, types_1.tweakPublicKey)(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.witness?.length !== 1) return; return a.witness[0]; @@ -78,6 +103,7 @@ function p2tr(a, opts) { // todo: not sure }); lazy.prop(o, 'witness', () => { + if (a.witness) return a.witness; if (!a.signature) return; return [a.signature]; }); @@ -109,7 +135,6 @@ function p2tr(a, opts) { throw new TypeError('Pubkey mismatch'); else pubkey = a.output.slice(2); } - // todo: optimze o.hash? if (a.internalPubkey) { const tweakedKey = (0, types_1.tweakPublicKey)(a.internalPubkey, o.hash); if (tweakedKey === null) @@ -126,13 +151,59 @@ function p2tr(a, opts) { const hash = (0, merkle_1.computeMastRoot)(a.scriptsTree); if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch'); } - if (a.witness) { - if (a.witness.length !== 1) throw new TypeError('Witness is invalid'); - // todo: recheck - // if (!bscript.isCanonicalScriptSignature(a.witness[0])) - // throw new TypeError('Witness has invalid signature'); - if (a.signature && !a.signature.equals(a.witness[0])) - throw new TypeError('Signature mismatch'); + // todo: review cache + const witness = _witness(); + if (witness && witness.length) { + if (witness.length === 1) { + // key spending + if (a.signature && !a.signature.equals(witness[0])) + throw new TypeError('Signature mismatch'); + // todo: recheck + // if (!bscript.isSchnorSignature(a.pubkey, a.witness[0])) + // throw new TypeError('Witness has invalid signature'); + } 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'); + const internalPubkeyPoint = (0, types_1.liftX)(internalPubkey); + if (!internalPubkeyPoint) + throw new TypeError('Invalid internalPubkey for p2tr witness'); + const leafVersion = controlBlock[0] & 0b11111110; + const script = witness[witness.length - 2]; + const tweak = (0, types_1.computeTweakFromScriptPath)( + controlBlock, + script, + internalPubkey, + m, + leafVersion, + ); + const outputKey = (0, types_1.tweakPublicKey)(internalPubkey, tweak); + 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'); + const controlBlockOddParity = (controlBlock[0] & 1) === 1; + if (outputKey.isOdd !== controlBlockOddParity) + throw new Error('Incorrect parity'); + } } } return Object.assign(o, a); diff --git a/src/types.d.ts b/src/types.d.ts index 45bfcd1c6..a4813ee4f 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,8 +1,10 @@ /// +import { Buffer as NBuffer } from 'buffer'; export declare const typeforce: any; export declare function isPoint(p: Buffer | number | undefined | null): boolean; export declare function liftX(buffer: Buffer): Buffer | null; export declare function tweakPublicKey(pubKey: Buffer, h: Buffer | undefined): TweakedPublicKey | null; +export declare function computeTweakFromScriptPath(controlBlock: Buffer, script: Buffer, internalPubkey: Buffer, m: number, v: number): NBuffer; export declare function UInt31(value: number): boolean; export declare function BIP32Path(value: string): boolean; export declare namespace BIP32Path { diff --git a/src/types.js b/src/types.js index 668af87bd..07756ecc4 100644 --- a/src/types.js +++ b/src/types.js @@ -1,8 +1,9 @@ '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.TaprootNode = exports.TaprootLeaf = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.tweakPublicKey = exports.liftX = 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.TaprootNode = exports.TaprootLeaf = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.computeTweakFromScriptPath = exports.tweakPublicKey = exports.liftX = exports.isPoint = exports.typeforce = void 0; const buffer_1 = require('buffer'); const bcrypto = require('./crypto'); +const varuint = require('bip174/src/lib/converter/varint'); // Temp, to be replaced // Only works because bip32 has it as dependecy. Linting will fail. const ecc = require('tiny-secp256k1'); @@ -62,12 +63,12 @@ function liftX(buffer) { } exports.liftX = liftX; const TAP_TWEAK_TAG = buffer_1.Buffer.from('TapTweak', 'utf8'); -const GROUP_ORDER = new BN( - buffer_1.Buffer.from( - 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', - 'hex', - ), +const GROUP_ORDER = buffer_1.Buffer.from( + 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', + 'hex', ); +// todo: compare buffers dirrectly +const GROUP_ORDER_BN = new BN(GROUP_ORDER); function tweakPublicKey(pubKey, h) { if (!buffer_1.Buffer.isBuffer(pubKey)) return null; if (pubKey.length !== 32) return null; @@ -77,7 +78,8 @@ function tweakPublicKey(pubKey, h) { buffer_1.Buffer.concat(h ? [pubKey, h] : [pubKey]), ); const t = new BN(tweakHash); - if (t.gte(GROUP_ORDER)) { + if (t.gte(GROUP_ORDER_BN)) { + // todo: add test for this case throw new Error('Tweak value over the SECP256K1 Order'); } const P = liftX(pubKey); @@ -89,6 +91,53 @@ function tweakPublicKey(pubKey, h) { }; } exports.tweakPublicKey = tweakPublicKey; +const TAP_LEAF_TAG = buffer_1.Buffer.from('TapLeaf', 'utf8'); +const TAP_BRANCH_TAG = buffer_1.Buffer.from('TapBranch', 'utf8'); +function computeTweakFromScriptPath( + controlBlock, + script, + internalPubkey, + m, + v, +) { + const k = []; + const e = []; + const tapLeafMsg = buffer_1.Buffer.concat([ + buffer_1.Buffer.from([v]), + serializeScript(script), + ]); + k[0] = bcrypto.taggedHash(TAP_LEAF_TAG, tapLeafMsg); + for (let j = 0; j < m; j++) { + e[j] = controlBlock.slice(33 + 32 * j, 65 + 32 * j); + if (k[j].compare(e[j]) < 0) { + k[j + 1] = bcrypto.taggedHash( + TAP_BRANCH_TAG, + buffer_1.Buffer.concat([k[j], e[j]]), + ); + } else { + k[j + 1] = bcrypto.taggedHash( + TAP_BRANCH_TAG, + buffer_1.Buffer.concat([e[j], k[j]]), + ); + } + } + const t = bcrypto.taggedHash( + TAP_TWEAK_TAG, + buffer_1.Buffer.concat([internalPubkey, k[m]]), + ); + if (t.compare(GROUP_ORDER) >= 0) { + throw new Error('Over the order of secp256k1'); + } + return t; +} +exports.computeTweakFromScriptPath = computeTweakFromScriptPath; +// todo: move out +function serializeScript(s) { + const varintLen = varuint.encodingLength(s.length); + const buffer = buffer_1.Buffer.allocUnsafe(varintLen); // better + varuint.encode(s.length, buffer); + return buffer_1.Buffer.concat([buffer, s]); +} // todo: do not use ecc function pointAddScalar(P, h) { return ecc.pointAddScalar(P, h); diff --git a/test/fixtures/p2tr.json b/test/fixtures/p2tr.json index 25fec29ba..532fe2297 100644 --- a/test/fixtures/p2tr.json +++ b/test/fixtures/p2tr.json @@ -93,6 +93,60 @@ "witness": null } }, + { + "description": "address, pubkey, internalPubkey and output from witness", + "arguments": { + "witness": [ + "9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602", + "5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba", + "7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac", + "c1a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c" + ] + }, + "expected": { + "name": "p2tr", + "internalPubkey": "a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a36", + "pubkey": "a44784d6b2d0159ca57f52e3e6023a4f1960d22c089d019f3656d9e23926dcc9", + "address": "bc1p53rcf44j6q2eeftl2t37vq36fuvkp53vpzwsr8ek2mv7ywfxmnysdul78t", + "output": "OP_1 a44784d6b2d0159ca57f52e3e6023a4f1960d22c089d019f3656d9e23926dcc9", + "signature": null, + "input": null, + "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": "a44784d6b2d0159ca57f52e3e6023a4f1960d22c089d019f3656d9e23926dcc9", + "address": "bc1p53rcf44j6q2eeftl2t37vq36fuvkp53vpzwsr8ek2mv7ywfxmnysdul78t", + "output": "OP_1 a44784d6b2d0159ca57f52e3e6023a4f1960d22c089d019f3656d9e23926dcc9", + "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": { @@ -425,10 +479,89 @@ } }, { - "exception": "Witness is invalid", + "description": "Control block length too small", + "exception": "The control-block length is too small. Got 16, expected min 33.", "arguments": { - "address": "bc1p4dss6gkgq8003g0qyd5drwfqrztsadf2w2v3juz73gdz7cx82r6sj7lcqx", - "witness": [] + "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" + ] } } ], diff --git a/test/payments.utils.ts b/test/payments.utils.ts index 71863a602..d1b40aa5e 100644 --- a/test/payments.utils.ts +++ b/test/payments.utils.ts @@ -86,6 +86,8 @@ 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), diff --git a/ts_src/merkle.ts b/ts_src/merkle.ts index d328acea9..125769215 100644 --- a/ts_src/merkle.ts +++ b/ts_src/merkle.ts @@ -4,6 +4,7 @@ import * as bcrypto from './crypto'; import * as varuint from 'bip174/src/lib/converter/varint'; +// todo: find better place for these consts const TAP_LEAF_TAG = NBuffer.from('TapLeaf', 'utf8'); const TAP_BRANCH_TAG = NBuffer.from('TapBranch', 'utf8'); const LEAF_VERSION_TAPSCRIPT = 0xc0 diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index 6bbbfa101..2abc6a72e 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -1,6 +1,6 @@ import { bitcoin as BITCOIN_NETWORK } from '../networks'; import * as bscript from '../script'; -import { liftX, tweakPublicKey, typeforce as typef } from '../types'; +import { liftX, tweakPublicKey, computeTweakFromScriptPath, typeforce as typef } from '../types'; import { computeMastRoot } from '../merkle'; import { Payment, PaymentOpts } from './index'; import * as lazy from './lazy'; @@ -8,12 +8,13 @@ import { bech32m } from 'bech32'; const OPS = bscript.OPS; const TAPROOT_VERSION = 0x01; +const ANNEX_PREFIX = 0x50; // witness: {signature} // input: <> // output: OP_1 {pubKey} export function p2tr(a: Payment, opts?: PaymentOpts): Payment { - if (!a.address && !a.output && !a.pubkey && !a.output && !a.internalPubkey) + if (!a.address && !a.output && !a.pubkey && !a.output && !a.internalPubkey && !(a.witness && a.witness.length > 1)) throw new TypeError('Not enough data'); opts = Object.assign({ validate: true }, opts || {}); @@ -44,7 +45,14 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { }; }); - // todo: clean-up withness (annex), etc + 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)) { + // remove annex, ignored by taproot + return a.witness.slice(0, -1); + } + return a.witness.slice() + }) const network = a.network || BITCOIN_NETWORK; const o: Payment = { name: 'p2tr', network }; @@ -61,6 +69,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { lazy.prop(o, 'hash', () => { if (a.hash) return a.hash; if (a.scriptsTree) return computeMastRoot(a.scriptsTree) + // todo: compute from witness return null }); lazy.prop(o, 'output', () => { @@ -71,11 +80,17 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { if (a.pubkey) return a.pubkey; if (a.output) return a.output.slice(2) if (a.address) return _address().data; - if (a.internalPubkey) { - const tweakedKey = tweakPublicKey(a.internalPubkey, o.hash) + if (o.internalPubkey) { + const tweakedKey = tweakPublicKey(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.witness?.length !== 1) return; return a.witness[0]; @@ -84,6 +99,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { // todo: not sure }); lazy.prop(o, 'witness', () => { + if (a.witness) return a.witness if (!a.signature) return; return [a.signature]; }); @@ -98,7 +114,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { throw new TypeError('Invalid address version'); if (_address().data.length !== 32) throw new TypeError('Invalid address data'); - pubkey = _address().data; + pubkey = _address().data; } if (a.pubkey) { @@ -110,7 +126,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { if (a.output) { if ( a.output.length !== 34 || - a.output[0] !== OPS.OP_1 || + a.output[0] !== OPS.OP_1 || a.output[1] !== 0x20 ) throw new TypeError('Output is invalid'); @@ -119,10 +135,9 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { else pubkey = a.output.slice(2); } - // todo: optimze o.hash? if (a.internalPubkey) { const tweakedKey = tweakPublicKey(a.internalPubkey, o.hash) - if (tweakedKey === null) throw new TypeError('Invalid internalPubkey for p2tr'); + if (tweakedKey === null) throw new TypeError('Invalid internalPubkey for p2tr'); if (pubkey.length > 0 && !pubkey.equals(tweakedKey.x)) throw new TypeError('Pubkey mismatch'); else pubkey = tweakedKey.x; @@ -139,15 +154,57 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { throw new TypeError('Hash mismatch'); } - if (a.witness) { - if (a.witness.length !== 1) throw new TypeError('Witness is invalid'); + // todo: review cache + const witness = _witness() - // todo: recheck - // if (!bscript.isCanonicalScriptSignature(a.witness[0])) + if (witness && witness.length) { + if (witness.length === 1) { + // key spending + if (a.signature && !a.signature.equals(witness[0])) + throw new TypeError('Signature mismatch'); + // todo: recheck + // if (!bscript.isSchnorSignature(a.pubkey, a.witness[0])) // throw new TypeError('Witness has invalid signature'); + } 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'); + + const internalPubkeyPoint = liftX(internalPubkey); + if (!internalPubkeyPoint) + throw new TypeError('Invalid internalPubkey for p2tr witness'); + + + const leafVersion = controlBlock[0] & 0b11111110; + const script = witness[witness.length - 2]; + const tweak = computeTweakFromScriptPath(controlBlock, script, internalPubkey, m, leafVersion) + + const outputKey = tweakPublicKey(internalPubkey, tweak) + 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'); + + const controlBlockOddParity = (controlBlock[0] & 1) === 1 + if (outputKey.isOdd !== controlBlockOddParity) + throw new Error('Incorrect parity') + + } - if (a.signature && !a.signature.equals(a.witness[0])) - throw new TypeError('Signature mismatch'); } } diff --git a/ts_src/types.ts b/ts_src/types.ts index 875df29f7..6fe711450 100644 --- a/ts_src/types.ts +++ b/ts_src/types.ts @@ -1,5 +1,6 @@ import { Buffer as NBuffer } from 'buffer'; import * as bcrypto from './crypto'; +import * as varuint from 'bip174/src/lib/converter/varint'; // Temp, to be replaced // Only works because bip32 has it as dependecy. Linting will fail. @@ -72,12 +73,10 @@ export function liftX(buffer: Buffer): Buffer | null { } const TAP_TWEAK_TAG = NBuffer.from('TapTweak', 'utf8'); -const GROUP_ORDER = new BN( - NBuffer.from( - 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', - 'hex', - ), -); + +const GROUP_ORDER = NBuffer.from('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', 'hex'); +// todo: compare buffers dirrectly +const GROUP_ORDER_BN = new BN(GROUP_ORDER); export function tweakPublicKey( pubKey: Buffer, @@ -92,7 +91,8 @@ export function tweakPublicKey( NBuffer.concat(h ? [pubKey, h] : [pubKey]), ); const t = new BN(tweakHash); - if (t.gte(GROUP_ORDER)) { + if (t.gte(GROUP_ORDER_BN)) { + // todo: add test for this case throw new Error('Tweak value over the SECP256K1 Order'); } @@ -106,6 +106,42 @@ export function tweakPublicKey( }; } +const TAP_LEAF_TAG = NBuffer.from('TapLeaf', 'utf8'); +const TAP_BRANCH_TAG = NBuffer.from('TapBranch', 'utf8'); + +export function computeTweakFromScriptPath(controlBlock: Buffer, script: Buffer, internalPubkey: Buffer, m: number, v: number) { + const k = []; + const e = []; + + const tapLeafMsg = NBuffer.concat([NBuffer.from([v]), serializeScript(script)]); + k[0] = bcrypto.taggedHash(TAP_LEAF_TAG, tapLeafMsg); + + + for (let j = 0; j < m; j++) { + e[j] = controlBlock.slice(33 + 32 * j, 65 + 32 * j); + if (k[j].compare(e[j]) < 0) { + k[j + 1] = bcrypto.taggedHash(TAP_BRANCH_TAG, NBuffer.concat([k[j], e[j]])); + } else { + k[j + 1] = bcrypto.taggedHash(TAP_BRANCH_TAG, NBuffer.concat([e[j], k[j]])); + } + } + + const t = bcrypto.taggedHash(TAP_TWEAK_TAG, NBuffer.concat([internalPubkey, k[m]])); + if (t.compare(GROUP_ORDER) >= 0) { + throw new Error('Over the order of secp256k1') + } + + return t +} + +// todo: move out +function serializeScript(s: Buffer) { + const varintLen = varuint.encodingLength(s.length); + const buffer = NBuffer.allocUnsafe(varintLen); // better + varuint.encode(s.length, buffer); + return NBuffer.concat([buffer, s]) +} + // todo: do not use ecc function pointAddScalar(P: Buffer, h: Buffer): Buffer { return ecc.pointAddScalar(P, h); From 9947571aaee1676271beab50461defb046f796d3 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 2 Nov 2021 16:06:01 +0200 Subject: [PATCH 17/53] refactor: split `computeTweakFromScriptPath()` into `rootHash()` and `leafHash()`` --- src/payments/p2tr.js | 15 +++++---------- src/types.d.ts | 6 +++--- src/types.js | 37 ++++++++++++++----------------------- ts_src/payments/p2tr.ts | 12 +++++++----- ts_src/types.ts | 19 +++++++++---------- 5 files changed, 38 insertions(+), 51 deletions(-) diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index 7d7e93a43..b9db56038 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -85,7 +85,7 @@ function p2tr(a, opts) { if (a.output) return a.output.slice(2); if (a.address) return _address().data; if (o.internalPubkey) { - const tweakedKey = (0, types_1.tweakPublicKey)(o.internalPubkey, o.hash); + const tweakedKey = (0, types_1.tweakKey)(o.internalPubkey, o.hash); if (tweakedKey) return tweakedKey.x; } }); @@ -136,7 +136,7 @@ function p2tr(a, opts) { else pubkey = a.output.slice(2); } if (a.internalPubkey) { - const tweakedKey = (0, types_1.tweakPublicKey)(a.internalPubkey, o.hash); + const tweakedKey = (0, types_1.tweakKey)(a.internalPubkey, o.hash); if (tweakedKey === null) throw new TypeError('Invalid internalPubkey for p2tr'); if (pubkey.length > 0 && !pubkey.equals(tweakedKey.x)) @@ -187,14 +187,9 @@ function p2tr(a, opts) { throw new TypeError('Invalid internalPubkey for p2tr witness'); const leafVersion = controlBlock[0] & 0b11111110; const script = witness[witness.length - 2]; - const tweak = (0, types_1.computeTweakFromScriptPath)( - controlBlock, - script, - internalPubkey, - m, - leafVersion, - ); - const outputKey = (0, types_1.tweakPublicKey)(internalPubkey, tweak); + const tapLeafHash = (0, types_1.leafHash)(script, leafVersion); + const hash = (0, types_1.rootHash)(controlBlock, tapLeafHash); + const outputKey = (0, types_1.tweakKey)(internalPubkey, hash); if (!outputKey) // todo: needs test data throw new TypeError('Invalid outputKey for p2tr witness'); diff --git a/src/types.d.ts b/src/types.d.ts index a4813ee4f..b63d2c26b 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,10 +1,10 @@ /// -import { Buffer as NBuffer } from 'buffer'; export declare const typeforce: any; export declare function isPoint(p: Buffer | number | undefined | null): boolean; export declare function liftX(buffer: Buffer): Buffer | null; -export declare function tweakPublicKey(pubKey: Buffer, h: Buffer | undefined): TweakedPublicKey | null; -export declare function computeTweakFromScriptPath(controlBlock: Buffer, script: Buffer, internalPubkey: Buffer, m: number, v: number): NBuffer; +export declare function tweakKey(pubKey: Buffer, h: Buffer | undefined): TweakedPublicKey | null; +export declare function leafHash(script: Buffer, version: number): Buffer; +export declare function rootHash(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer; export declare function UInt31(value: number): boolean; export declare function BIP32Path(value: string): boolean; export declare namespace BIP32Path { diff --git a/src/types.js b/src/types.js index 07756ecc4..d7fd587fe 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.TaprootNode = exports.TaprootLeaf = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.computeTweakFromScriptPath = exports.tweakPublicKey = exports.liftX = 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.TaprootNode = exports.TaprootLeaf = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.rootHash = exports.leafHash = exports.tweakKey = exports.liftX = exports.isPoint = exports.typeforce = void 0; const buffer_1 = require('buffer'); const bcrypto = require('./crypto'); const varuint = require('bip174/src/lib/converter/varint'); @@ -69,7 +69,7 @@ const GROUP_ORDER = buffer_1.Buffer.from( ); // todo: compare buffers dirrectly const GROUP_ORDER_BN = new BN(GROUP_ORDER); -function tweakPublicKey(pubKey, h) { +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; @@ -90,22 +90,20 @@ function tweakPublicKey(pubKey, h) { x: Q.slice(1, 33), }; } -exports.tweakPublicKey = tweakPublicKey; +exports.tweakKey = tweakKey; const TAP_LEAF_TAG = buffer_1.Buffer.from('TapLeaf', 'utf8'); const TAP_BRANCH_TAG = buffer_1.Buffer.from('TapBranch', 'utf8'); -function computeTweakFromScriptPath( - controlBlock, - script, - internalPubkey, - m, - v, -) { - const k = []; - const e = []; - const tapLeafMsg = buffer_1.Buffer.concat([ - buffer_1.Buffer.from([v]), +function leafHash(script, version) { + return buffer_1.Buffer.concat([ + buffer_1.Buffer.from([version]), serializeScript(script), ]); +} +exports.leafHash = leafHash; +function rootHash(controlBlock, tapLeafMsg) { + const k = []; + const e = []; + const m = (controlBlock.length - 33) / 32; k[0] = bcrypto.taggedHash(TAP_LEAF_TAG, tapLeafMsg); for (let j = 0; j < m; j++) { e[j] = controlBlock.slice(33 + 32 * j, 65 + 32 * j); @@ -121,16 +119,9 @@ function computeTweakFromScriptPath( ); } } - const t = bcrypto.taggedHash( - TAP_TWEAK_TAG, - buffer_1.Buffer.concat([internalPubkey, k[m]]), - ); - if (t.compare(GROUP_ORDER) >= 0) { - throw new Error('Over the order of secp256k1'); - } - return t; + return k[m]; } -exports.computeTweakFromScriptPath = computeTweakFromScriptPath; +exports.rootHash = rootHash; // todo: move out function serializeScript(s) { const varintLen = varuint.encodingLength(s.length); diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index 2abc6a72e..8992a9ca8 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -1,6 +1,6 @@ import { bitcoin as BITCOIN_NETWORK } from '../networks'; import * as bscript from '../script'; -import { liftX, tweakPublicKey, computeTweakFromScriptPath, typeforce as typef } from '../types'; +import { liftX, leafHash, rootHash, tweakKey, typeforce as typef } from '../types'; import { computeMastRoot } from '../merkle'; import { Payment, PaymentOpts } from './index'; import * as lazy from './lazy'; @@ -81,7 +81,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { if (a.output) return a.output.slice(2) if (a.address) return _address().data; if (o.internalPubkey) { - const tweakedKey = tweakPublicKey(o.internalPubkey, o.hash) + const tweakedKey = tweakKey(o.internalPubkey, o.hash) if (tweakedKey) return tweakedKey.x } }); @@ -136,7 +136,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { } if (a.internalPubkey) { - const tweakedKey = tweakPublicKey(a.internalPubkey, o.hash) + const tweakedKey = tweakKey(a.internalPubkey, o.hash) if (tweakedKey === null) throw new TypeError('Invalid internalPubkey for p2tr'); if (pubkey.length > 0 && !pubkey.equals(tweakedKey.x)) throw new TypeError('Pubkey mismatch'); @@ -189,9 +189,11 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { const leafVersion = controlBlock[0] & 0b11111110; const script = witness[witness.length - 2]; - const tweak = computeTweakFromScriptPath(controlBlock, script, internalPubkey, m, leafVersion) + + const tapLeafHash = leafHash(script, leafVersion) + const hash = rootHash(controlBlock, tapLeafHash) - const outputKey = tweakPublicKey(internalPubkey, tweak) + const outputKey = tweakKey(internalPubkey, hash) if (!outputKey) // todo: needs test data throw new TypeError('Invalid outputKey for p2tr witness'); diff --git a/ts_src/types.ts b/ts_src/types.ts index 6fe711450..54f3b816d 100644 --- a/ts_src/types.ts +++ b/ts_src/types.ts @@ -78,7 +78,7 @@ const GROUP_ORDER = NBuffer.from('fffffffffffffffffffffffffffffffebaaedce6af48a0 // todo: compare buffers dirrectly const GROUP_ORDER_BN = new BN(GROUP_ORDER); -export function tweakPublicKey( +export function tweakKey( pubKey: Buffer, h: Buffer | undefined, ): TweakedPublicKey | null { @@ -109,14 +109,18 @@ export function tweakPublicKey( const TAP_LEAF_TAG = NBuffer.from('TapLeaf', 'utf8'); const TAP_BRANCH_TAG = NBuffer.from('TapBranch', 'utf8'); -export function computeTweakFromScriptPath(controlBlock: Buffer, script: Buffer, internalPubkey: Buffer, m: number, v: number) { + +export function leafHash(script: Buffer, version: number): Buffer { + return NBuffer.concat([NBuffer.from([version]), serializeScript(script)]); +} + +export function rootHash(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer { const k = []; const e = []; - const tapLeafMsg = NBuffer.concat([NBuffer.from([v]), serializeScript(script)]); + const m = (controlBlock.length - 33) / 32; k[0] = bcrypto.taggedHash(TAP_LEAF_TAG, tapLeafMsg); - for (let j = 0; j < m; j++) { e[j] = controlBlock.slice(33 + 32 * j, 65 + 32 * j); if (k[j].compare(e[j]) < 0) { @@ -126,12 +130,7 @@ export function computeTweakFromScriptPath(controlBlock: Buffer, script: Buffer, } } - const t = bcrypto.taggedHash(TAP_TWEAK_TAG, NBuffer.concat([internalPubkey, k[m]])); - if (t.compare(GROUP_ORDER) >= 0) { - throw new Error('Over the order of secp256k1') - } - - return t + return k[m] } // todo: move out From b8f8c91d68c2432b66a0dc00a6ef8d4646160e7b Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 2 Nov 2021 16:57:31 +0200 Subject: [PATCH 18/53] feat: compute hash from witness control-block --- src/payments/p2tr.js | 9 ++++++++- test/fixtures/p2tr.json | 14 ++++++++------ ts_src/payments/p2tr.ts | 12 +++++++++--- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index b9db56038..a88332b22 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -73,7 +73,14 @@ function p2tr(a, opts) { lazy.prop(o, 'hash', () => { if (a.hash) return a.hash; if (a.scriptsTree) return (0, merkle_1.computeMastRoot)(a.scriptsTree); - // todo: compute from witness + const w = _witness(); + if (w && w.length > 1) { + const controlBlock = w[w.length - 1]; + const leafVersion = controlBlock[0] & 0b11111110; + const script = w[w.length - 2]; + const tapLeafHash = (0, types_1.leafHash)(script, leafVersion); + return (0, types_1.rootHash)(controlBlock, tapLeafHash); + } return null; }); lazy.prop(o, 'output', () => { diff --git a/test/fixtures/p2tr.json b/test/fixtures/p2tr.json index 532fe2297..24cb6472f 100644 --- a/test/fixtures/p2tr.json +++ b/test/fixtures/p2tr.json @@ -106,9 +106,10 @@ "expected": { "name": "p2tr", "internalPubkey": "a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a36", - "pubkey": "a44784d6b2d0159ca57f52e3e6023a4f1960d22c089d019f3656d9e23926dcc9", - "address": "bc1p53rcf44j6q2eeftl2t37vq36fuvkp53vpzwsr8ek2mv7ywfxmnysdul78t", - "output": "OP_1 a44784d6b2d0159ca57f52e3e6023a4f1960d22c089d019f3656d9e23926dcc9", + "pubkey": "1ebe8b90363bd097aa9f352c8b21914e1886bc09fe9e70c09f33ef2d2abdf4bc", + "hash": "c5c62d7fc595ba5fbe61602eb1a29e2e4763408fe1e2b161beb7cb3c71ebcad9", + "address": "bc1pr6lghypk80gf025lx5kgkgv3fcvgd0qfl608psylx0hj624a7j7qay80rv", + "output": "OP_1 1ebe8b90363bd097aa9f352c8b21914e1886bc09fe9e70c09f33ef2d2abdf4bc", "signature": null, "input": null, "witness": [ @@ -133,9 +134,10 @@ "expected": { "name": "p2tr", "internalPubkey": "a7957acbaaf7b444c53d9e0c9436e8a8a3247fd515095d66ddf6201918b40a36", - "pubkey": "a44784d6b2d0159ca57f52e3e6023a4f1960d22c089d019f3656d9e23926dcc9", - "address": "bc1p53rcf44j6q2eeftl2t37vq36fuvkp53vpzwsr8ek2mv7ywfxmnysdul78t", - "output": "OP_1 a44784d6b2d0159ca57f52e3e6023a4f1960d22c089d019f3656d9e23926dcc9", + "pubkey": "1ebe8b90363bd097aa9f352c8b21914e1886bc09fe9e70c09f33ef2d2abdf4bc", + "hash": "c5c62d7fc595ba5fbe61602eb1a29e2e4763408fe1e2b161beb7cb3c71ebcad9", + "address": "bc1pr6lghypk80gf025lx5kgkgv3fcvgd0qfl608psylx0hj624a7j7qay80rv", + "output": "OP_1 1ebe8b90363bd097aa9f352c8b21914e1886bc09fe9e70c09f33ef2d2abdf4bc", "signature": null, "input": null, "witness": [ diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index 8992a9ca8..b1ed834e6 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -69,7 +69,14 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { lazy.prop(o, 'hash', () => { if (a.hash) return a.hash; if (a.scriptsTree) return computeMastRoot(a.scriptsTree) - // todo: compute from witness + const w = _witness() + if (w && w.length > 1) { + const controlBlock = w[w.length - 1]; + const leafVersion = controlBlock[0] & 0b11111110; + const script = w[w.length - 2]; + const tapLeafHash = leafHash(script, leafVersion) + return rootHash(controlBlock, tapLeafHash) + } return null }); lazy.prop(o, 'output', () => { @@ -186,10 +193,9 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { if (!internalPubkeyPoint) throw new TypeError('Invalid internalPubkey for p2tr witness'); - const leafVersion = controlBlock[0] & 0b11111110; const script = witness[witness.length - 2]; - + const tapLeafHash = leafHash(script, leafVersion) const hash = rootHash(controlBlock, tapLeafHash) From 8fd07fc7b073ae686d85226580707fc746154b69 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 2 Nov 2021 17:26:04 +0200 Subject: [PATCH 19/53] refactor: extract taproot related logic to taproot.ts file --- src/merkle.d.ts | 1 - src/merkle.js | 47 +------------ src/payments/p2tr.js | 24 +++---- src/taproot.d.ts | 7 ++ src/taproot.js | 142 ++++++++++++++++++++++++++++++++++++++++ src/types.d.ts | 7 +- src/types.js | 122 ++-------------------------------- ts_src/merkle.ts | 41 ------------ ts_src/payments/p2tr.ts | 4 +- ts_src/taproot.ts | 140 +++++++++++++++++++++++++++++++++++++++ ts_src/types.ts | 123 +--------------------------------- 11 files changed, 316 insertions(+), 342 deletions(-) create mode 100644 src/taproot.d.ts create mode 100644 src/taproot.js create mode 100644 ts_src/taproot.ts diff --git a/src/merkle.d.ts b/src/merkle.d.ts index d4b43f3ba..d602201b9 100644 --- a/src/merkle.d.ts +++ b/src/merkle.d.ts @@ -1,3 +1,2 @@ /// export declare function fastMerkleRoot(values: Buffer[], digestFn: (b: Buffer) => Buffer): Buffer; -export declare function computeMastRoot(scripts: any): Buffer; diff --git a/src/merkle.js b/src/merkle.js index 009260ac4..e93f9cab6 100644 --- a/src/merkle.js +++ b/src/merkle.js @@ -1,14 +1,6 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -exports.computeMastRoot = exports.fastMerkleRoot = void 0; -const buffer_1 = require('buffer'); -const bcrypto = require('./crypto'); -// todo: use varuint-bitcoin?? -const varuint = require('bip174/src/lib/converter/varint'); -// todo: find better place for these consts -const TAP_LEAF_TAG = buffer_1.Buffer.from('TapLeaf', 'utf8'); -const TAP_BRANCH_TAG = buffer_1.Buffer.from('TapBranch', 'utf8'); -const LEAF_VERSION_TAPSCRIPT = 0xc0; +exports.fastMerkleRoot = void 0; function fastMerkleRoot(values, digestFn) { if (!Array.isArray(values)) throw TypeError('Expected values Array'); if (typeof digestFn !== 'function') @@ -28,40 +20,3 @@ function fastMerkleRoot(values, digestFn) { return results[0]; } exports.fastMerkleRoot = fastMerkleRoot; -// todo: solve any[] -function computeMastRoot(scripts) { - if (scripts.length === 1) { - const script = scripts[0]; - if (Array.isArray(script)) { - return computeMastRoot(script); - } - script.version = script.version || LEAF_VERSION_TAPSCRIPT; - if ((script.version & 1) !== 0) throw new Error('Invalid script version'); // todo typedef error - // todo: if (script.output)scheck is bytes - const scriptOutput = buffer_1.Buffer.from(script.output, 'hex'); - return bcrypto.taggedHash( - TAP_LEAF_TAG, - buffer_1.Buffer.concat([ - buffer_1.Buffer.from([script.version]), - serializeScript(scriptOutput), - ]), - ); - } - // todo: this is a binary tree, use zero an one index - const half = Math.trunc(scripts.length / 2); - let leftHash = computeMastRoot(scripts.slice(0, half)); - let rightHash = computeMastRoot(scripts.slice(half)); - if (leftHash.compare(rightHash) === 1) - [leftHash, rightHash] = [rightHash, leftHash]; - return bcrypto.taggedHash( - TAP_BRANCH_TAG, - buffer_1.Buffer.concat([leftHash, rightHash]), - ); -} -exports.computeMastRoot = computeMastRoot; -function serializeScript(s) { - const varintLen = varuint.encodingLength(s.length); - const buffer = buffer_1.Buffer.allocUnsafe(varintLen); // better - varuint.encode(s.length, buffer); - return buffer_1.Buffer.concat([buffer, s]); -} diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index a88332b22..53767318a 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -4,7 +4,7 @@ exports.p2tr = void 0; const networks_1 = require('../networks'); const bscript = require('../script'); const types_1 = require('../types'); -const merkle_1 = require('../merkle'); +const taproot_1 = require('../taproot'); const lazy = require('./lazy'); const bech32_1 = require('bech32'); const OPS = bscript.OPS; @@ -72,14 +72,14 @@ function p2tr(a, opts) { }); lazy.prop(o, 'hash', () => { if (a.hash) return a.hash; - if (a.scriptsTree) return (0, merkle_1.computeMastRoot)(a.scriptsTree); + if (a.scriptsTree) return (0, taproot_1.computeMastRoot)(a.scriptsTree); const w = _witness(); if (w && w.length > 1) { const controlBlock = w[w.length - 1]; const leafVersion = controlBlock[0] & 0b11111110; const script = w[w.length - 2]; - const tapLeafHash = (0, types_1.leafHash)(script, leafVersion); - return (0, types_1.rootHash)(controlBlock, tapLeafHash); + const tapLeafHash = (0, taproot_1.leafHash)(script, leafVersion); + return (0, taproot_1.rootHash)(controlBlock, tapLeafHash); } return null; }); @@ -92,7 +92,7 @@ function p2tr(a, opts) { if (a.output) return a.output.slice(2); if (a.address) return _address().data; if (o.internalPubkey) { - const tweakedKey = (0, types_1.tweakKey)(o.internalPubkey, o.hash); + const tweakedKey = (0, taproot_1.tweakKey)(o.internalPubkey, o.hash); if (tweakedKey) return tweakedKey.x; } }); @@ -143,7 +143,7 @@ function p2tr(a, opts) { else pubkey = a.output.slice(2); } if (a.internalPubkey) { - const tweakedKey = (0, types_1.tweakKey)(a.internalPubkey, o.hash); + const tweakedKey = (0, taproot_1.tweakKey)(a.internalPubkey, o.hash); if (tweakedKey === null) throw new TypeError('Invalid internalPubkey for p2tr'); if (pubkey.length > 0 && !pubkey.equals(tweakedKey.x)) @@ -151,11 +151,11 @@ function p2tr(a, opts) { else pubkey = tweakedKey.x; } if (pubkey?.length) { - if ((0, types_1.liftX)(pubkey) === null) + if ((0, taproot_1.liftX)(pubkey) === null) throw new TypeError('Invalid pubkey for p2tr'); } if (a.hash && a.scriptsTree) { - const hash = (0, merkle_1.computeMastRoot)(a.scriptsTree); + const hash = (0, taproot_1.computeMastRoot)(a.scriptsTree); if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch'); } // todo: review cache @@ -189,14 +189,14 @@ function p2tr(a, opts) { const internalPubkey = controlBlock.slice(1, 33); if (a.internalPubkey && !a.internalPubkey.equals(internalPubkey)) throw new TypeError('Internal pubkey mismatch'); - const internalPubkeyPoint = (0, types_1.liftX)(internalPubkey); + const internalPubkeyPoint = (0, taproot_1.liftX)(internalPubkey); if (!internalPubkeyPoint) throw new TypeError('Invalid internalPubkey for p2tr witness'); const leafVersion = controlBlock[0] & 0b11111110; const script = witness[witness.length - 2]; - const tapLeafHash = (0, types_1.leafHash)(script, leafVersion); - const hash = (0, types_1.rootHash)(controlBlock, tapLeafHash); - const outputKey = (0, types_1.tweakKey)(internalPubkey, hash); + const tapLeafHash = (0, taproot_1.leafHash)(script, leafVersion); + const hash = (0, taproot_1.rootHash)(controlBlock, tapLeafHash); + const outputKey = (0, taproot_1.tweakKey)(internalPubkey, hash); if (!outputKey) // todo: needs test data throw new TypeError('Invalid outputKey for p2tr witness'); diff --git a/src/taproot.d.ts b/src/taproot.d.ts new file mode 100644 index 000000000..0391f6bf1 --- /dev/null +++ b/src/taproot.d.ts @@ -0,0 +1,7 @@ +/// +import { TweakedPublicKey } from './types'; +export declare function liftX(buffer: Buffer): Buffer | null; +export declare function tweakKey(pubKey: Buffer, h: Buffer | undefined): TweakedPublicKey | null; +export declare function leafHash(script: Buffer, version: number): Buffer; +export declare function rootHash(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer; +export declare function computeMastRoot(scripts: any): Buffer; diff --git a/src/taproot.js b/src/taproot.js new file mode 100644 index 000000000..4930f4331 --- /dev/null +++ b/src/taproot.js @@ -0,0 +1,142 @@ +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); +exports.computeMastRoot = exports.rootHash = exports.leafHash = exports.tweakKey = exports.liftX = void 0; +const buffer_1 = require('buffer'); +const BN = require('bn.js'); +const bcrypto = require('./crypto'); +// todo: use varuint-bitcoin?? +const varuint = require('bip174/src/lib/converter/varint'); +const types_1 = require('./types'); +// todo: !!!Temp, to be replaced. Only works because bip32 has it as dependecy. Linting will fail. +const ecc = require('tiny-secp256k1'); +const LEAF_VERSION_TAPSCRIPT = 0xc0; +const TAP_LEAF_TAG = buffer_1.Buffer.from('TapLeaf', 'utf8'); +const TAP_BRANCH_TAG = buffer_1.Buffer.from('TapBranch', 'utf8'); +const TAP_TWEAK_TAG = buffer_1.Buffer.from('TapTweak', 'utf8'); +// todo: compare buffers dirrectly +const GROUP_ORDER = buffer_1.Buffer.from( + 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', + 'hex', +); +const GROUP_ORDER_BN = new BN(GROUP_ORDER); +const EC_P_BN = new BN(types_1.EC_P); +const EC_P_REDUCTION = BN.red(EC_P_BN); +const EC_P_QUADRATIC_RESIDUE = EC_P_BN.addn(1).divn(4); +const BN_2 = new BN(2); +const BN_3 = new BN(3); +const BN_7 = new BN(7); +function liftX(buffer) { + if (!buffer_1.Buffer.isBuffer(buffer)) return null; + if (buffer.length !== 32) return null; + if (buffer.compare(types_1.ZERO32) === 0) return null; + if (buffer.compare(types_1.EC_P) >= 0) return null; + const x = new BN(buffer); + const x1 = x.toRed(EC_P_REDUCTION); + const ySq = x1 + .redPow(BN_3) + .add(BN_7) + .mod(EC_P_BN); + const y = ySq.redPow(EC_P_QUADRATIC_RESIDUE); + if (!ySq.eq(y.redPow(BN_2))) { + return null; + } + const y1 = y.isEven() ? y : EC_P_BN.sub(y); + return buffer_1.Buffer.concat([ + buffer_1.Buffer.from([0x04]), + buffer_1.Buffer.from(x1.toBuffer('be', 32)), + buffer_1.Buffer.from(y1.toBuffer('be', 32)), + ]); +} +exports.liftX = liftX; +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 = bcrypto.taggedHash( + TAP_TWEAK_TAG, + buffer_1.Buffer.concat(h ? [pubKey, h] : [pubKey]), + ); + const t = new BN(tweakHash); + if (t.gte(GROUP_ORDER_BN)) { + // todo: add test for this case + throw new Error('Tweak value over the SECP256K1 Order'); + } + const P = liftX(pubKey); + if (P === null) return null; + const Q = pointAddScalar(P, tweakHash); + return { + isOdd: Q[64] % 2 === 1, + x: Q.slice(1, 33), + }; +} +exports.tweakKey = tweakKey; +function leafHash(script, version) { + return buffer_1.Buffer.concat([ + buffer_1.Buffer.from([version]), + serializeScript(script), + ]); +} +exports.leafHash = leafHash; +function rootHash(controlBlock, tapLeafMsg) { + const k = []; + const e = []; + const m = (controlBlock.length - 33) / 32; + k[0] = bcrypto.taggedHash(TAP_LEAF_TAG, tapLeafMsg); + for (let j = 0; j < m; j++) { + e[j] = controlBlock.slice(33 + 32 * j, 65 + 32 * j); + if (k[j].compare(e[j]) < 0) { + k[j + 1] = bcrypto.taggedHash( + TAP_BRANCH_TAG, + buffer_1.Buffer.concat([k[j], e[j]]), + ); + } else { + k[j + 1] = bcrypto.taggedHash( + TAP_BRANCH_TAG, + buffer_1.Buffer.concat([e[j], k[j]]), + ); + } + } + return k[m]; +} +exports.rootHash = rootHash; +// todo: solve any[] +function computeMastRoot(scripts) { + if (scripts.length === 1) { + const script = scripts[0]; + if (Array.isArray(script)) { + return computeMastRoot(script); + } + script.version = script.version || LEAF_VERSION_TAPSCRIPT; + if ((script.version & 1) !== 0) throw new Error('Invalid script version'); // todo typedef error + // todo: if (script.output)scheck is bytes + const scriptOutput = buffer_1.Buffer.from(script.output, 'hex'); + return bcrypto.taggedHash( + TAP_LEAF_TAG, + buffer_1.Buffer.concat([ + buffer_1.Buffer.from([script.version]), + serializeScript(scriptOutput), + ]), + ); + } + // todo: this is a binary tree, use zero an one index + const half = Math.trunc(scripts.length / 2); + let leftHash = computeMastRoot(scripts.slice(0, half)); + let rightHash = computeMastRoot(scripts.slice(half)); + if (leftHash.compare(rightHash) === 1) + [leftHash, rightHash] = [rightHash, leftHash]; + return bcrypto.taggedHash( + TAP_BRANCH_TAG, + buffer_1.Buffer.concat([leftHash, rightHash]), + ); +} +exports.computeMastRoot = computeMastRoot; +function serializeScript(s) { + const varintLen = varuint.encodingLength(s.length); + const buffer = buffer_1.Buffer.allocUnsafe(varintLen); // better + varuint.encode(s.length, buffer); + return buffer_1.Buffer.concat([buffer, s]); +} +// todo: do not use ecc +function pointAddScalar(P, h) { + return ecc.pointAddScalar(P, h); +} diff --git a/src/types.d.ts b/src/types.d.ts index b63d2c26b..6e72a6d47 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,10 +1,9 @@ /// +import { Buffer as NBuffer } from 'buffer'; export declare const typeforce: any; +export declare const ZERO32: NBuffer; +export declare const EC_P: NBuffer; export declare function isPoint(p: Buffer | number | undefined | null): boolean; -export declare function liftX(buffer: Buffer): Buffer | null; -export declare function tweakKey(pubKey: Buffer, h: Buffer | undefined): TweakedPublicKey | null; -export declare function leafHash(script: Buffer, version: number): Buffer; -export declare function rootHash(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer; export declare function UInt31(value: number): boolean; export declare function BIP32Path(value: string): boolean; export declare namespace BIP32Path { diff --git a/src/types.js b/src/types.js index d7fd587fe..ba62bd813 100644 --- a/src/types.js +++ b/src/types.js @@ -1,17 +1,10 @@ '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.TaprootNode = exports.TaprootLeaf = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.rootHash = exports.leafHash = exports.tweakKey = exports.liftX = 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.TaprootNode = exports.TaprootLeaf = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.isPoint = exports.EC_P = exports.ZERO32 = exports.typeforce = void 0; const buffer_1 = require('buffer'); -const bcrypto = require('./crypto'); -const varuint = require('bip174/src/lib/converter/varint'); -// Temp, to be replaced -// Only works because bip32 has it as dependecy. Linting will fail. -const ecc = require('tiny-secp256k1'); -// todo, use import? -const BN = require('bn.js'); exports.typeforce = require('typeforce'); -const ZERO32 = buffer_1.Buffer.alloc(32, 0); -const EC_P = buffer_1.Buffer.from( +exports.ZERO32 = buffer_1.Buffer.alloc(32, 0); +exports.EC_P = buffer_1.Buffer.from( 'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f', 'hex', ); @@ -20,119 +13,18 @@ function isPoint(p) { if (p.length < 33) return false; const t = p[0]; const x = p.slice(1, 33); - if (x.compare(ZERO32) === 0) return false; - if (x.compare(EC_P) >= 0) return false; + if (x.compare(exports.ZERO32) === 0) return false; + if (x.compare(exports.EC_P) >= 0) return false; if ((t === 0x02 || t === 0x03) && p.length === 33) { return true; } const y = p.slice(33); - if (y.compare(ZERO32) === 0) return false; - if (y.compare(EC_P) >= 0) return false; + if (y.compare(exports.ZERO32) === 0) return false; + if (y.compare(exports.EC_P) >= 0) return false; if (t === 0x04 && p.length === 65) return true; return false; } exports.isPoint = isPoint; -// todo review. Do not add dependcy to BN? -const EC_P_BN = new BN(EC_P); -const EC_P_REDUCTION = BN.red(EC_P_BN); -const EC_P_QUADRATIC_RESIDUE = EC_P_BN.addn(1).divn(4); -const BN_2 = new BN(2); -const BN_3 = new BN(3); -const BN_7 = new BN(7); -function liftX(buffer) { - if (!buffer_1.Buffer.isBuffer(buffer)) return null; - if (buffer.length !== 32) return null; - if (buffer.compare(ZERO32) === 0) return null; - if (buffer.compare(EC_P) >= 0) return null; - const x = new BN(buffer); - const x1 = x.toRed(EC_P_REDUCTION); - const ySq = x1 - .redPow(BN_3) - .add(BN_7) - .mod(EC_P_BN); - const y = ySq.redPow(EC_P_QUADRATIC_RESIDUE); - if (!ySq.eq(y.redPow(BN_2))) { - return null; - } - const y1 = y.isEven() ? y : EC_P_BN.sub(y); - return buffer_1.Buffer.concat([ - buffer_1.Buffer.from([0x04]), - buffer_1.Buffer.from(x1.toBuffer('be', 32)), - buffer_1.Buffer.from(y1.toBuffer('be', 32)), - ]); -} -exports.liftX = liftX; -const TAP_TWEAK_TAG = buffer_1.Buffer.from('TapTweak', 'utf8'); -const GROUP_ORDER = buffer_1.Buffer.from( - 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', - 'hex', -); -// todo: compare buffers dirrectly -const GROUP_ORDER_BN = new BN(GROUP_ORDER); -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 = bcrypto.taggedHash( - TAP_TWEAK_TAG, - buffer_1.Buffer.concat(h ? [pubKey, h] : [pubKey]), - ); - const t = new BN(tweakHash); - if (t.gte(GROUP_ORDER_BN)) { - // todo: add test for this case - throw new Error('Tweak value over the SECP256K1 Order'); - } - const P = liftX(pubKey); - if (P === null) return null; - const Q = pointAddScalar(P, tweakHash); - return { - isOdd: Q[64] % 2 === 1, - x: Q.slice(1, 33), - }; -} -exports.tweakKey = tweakKey; -const TAP_LEAF_TAG = buffer_1.Buffer.from('TapLeaf', 'utf8'); -const TAP_BRANCH_TAG = buffer_1.Buffer.from('TapBranch', 'utf8'); -function leafHash(script, version) { - return buffer_1.Buffer.concat([ - buffer_1.Buffer.from([version]), - serializeScript(script), - ]); -} -exports.leafHash = leafHash; -function rootHash(controlBlock, tapLeafMsg) { - const k = []; - const e = []; - const m = (controlBlock.length - 33) / 32; - k[0] = bcrypto.taggedHash(TAP_LEAF_TAG, tapLeafMsg); - for (let j = 0; j < m; j++) { - e[j] = controlBlock.slice(33 + 32 * j, 65 + 32 * j); - if (k[j].compare(e[j]) < 0) { - k[j + 1] = bcrypto.taggedHash( - TAP_BRANCH_TAG, - buffer_1.Buffer.concat([k[j], e[j]]), - ); - } else { - k[j + 1] = bcrypto.taggedHash( - TAP_BRANCH_TAG, - buffer_1.Buffer.concat([e[j], k[j]]), - ); - } - } - return k[m]; -} -exports.rootHash = rootHash; -// todo: move out -function serializeScript(s) { - const varintLen = varuint.encodingLength(s.length); - const buffer = buffer_1.Buffer.allocUnsafe(varintLen); // better - varuint.encode(s.length, buffer); - return buffer_1.Buffer.concat([buffer, s]); -} -// todo: do not use ecc -function pointAddScalar(P, h) { - return ecc.pointAddScalar(P, h); -} const UINT31_MAX = Math.pow(2, 31) - 1; function UInt31(value) { return exports.typeforce.UInt32(value) && value <= UINT31_MAX; diff --git a/ts_src/merkle.ts b/ts_src/merkle.ts index 125769215..814819c79 100644 --- a/ts_src/merkle.ts +++ b/ts_src/merkle.ts @@ -1,15 +1,3 @@ -import { Buffer as NBuffer } from 'buffer'; -import * as bcrypto from './crypto'; -// todo: use varuint-bitcoin?? -import * as varuint from 'bip174/src/lib/converter/varint'; - - -// todo: find better place for these consts -const TAP_LEAF_TAG = NBuffer.from('TapLeaf', 'utf8'); -const TAP_BRANCH_TAG = NBuffer.from('TapBranch', 'utf8'); -const LEAF_VERSION_TAPSCRIPT = 0xc0 - - export function fastMerkleRoot( values: Buffer[], digestFn: (b: Buffer) => Buffer, @@ -36,33 +24,4 @@ export function fastMerkleRoot( } return results[0]; -} - -// todo: solve any[] -export function computeMastRoot(scripts: any): Buffer { - if (scripts.length === 1) { - const script = scripts[0] - if (Array.isArray(script)) { - return computeMastRoot(script) - } - script.version = script.version || LEAF_VERSION_TAPSCRIPT - if ((script.version & 1) !== 0) throw new Error("Invalid script version") // todo typedef error - // todo: if (script.output)scheck is bytes - const scriptOutput = NBuffer.from(script.output, 'hex') - return bcrypto.taggedHash(TAP_LEAF_TAG, NBuffer.concat([NBuffer.from([script.version]), serializeScript(scriptOutput)])) - } - // todo: this is a binary tree, use zero an one index - const half = Math.trunc(scripts.length / 2) - let leftHash = computeMastRoot(scripts.slice(0, half)) - let rightHash = computeMastRoot(scripts.slice(half)) - - if (leftHash.compare(rightHash) === 1) [leftHash, rightHash] = [rightHash, leftHash] - return bcrypto.taggedHash(TAP_BRANCH_TAG, NBuffer.concat([leftHash, rightHash])) -} - -function serializeScript(s: Buffer) { - const varintLen = varuint.encodingLength(s.length); - const buffer = NBuffer.allocUnsafe(varintLen); // better - varuint.encode(s.length, buffer); - return NBuffer.concat([buffer, s]) } \ No newline at end of file diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index b1ed834e6..aef500c85 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -1,7 +1,7 @@ import { bitcoin as BITCOIN_NETWORK } from '../networks'; import * as bscript from '../script'; -import { liftX, leafHash, rootHash, tweakKey, typeforce as typef } from '../types'; -import { computeMastRoot } from '../merkle'; +import { typeforce as typef } from '../types'; +import { computeMastRoot, leafHash, rootHash, tweakKey, liftX } from '../taproot'; import { Payment, PaymentOpts } from './index'; import * as lazy from './lazy'; import { bech32m } from 'bech32'; diff --git a/ts_src/taproot.ts b/ts_src/taproot.ts new file mode 100644 index 000000000..5370e326f --- /dev/null +++ b/ts_src/taproot.ts @@ -0,0 +1,140 @@ +import { Buffer as NBuffer } from 'buffer'; +const BN = require('bn.js'); + +import * as bcrypto from './crypto'; +// todo: use varuint-bitcoin?? +import * as varuint from 'bip174/src/lib/converter/varint'; +import { TweakedPublicKey, ZERO32, EC_P } from './types' + +// todo: !!!Temp, to be replaced. Only works because bip32 has it as dependecy. Linting will fail. +const ecc = require('tiny-secp256k1'); + +const LEAF_VERSION_TAPSCRIPT = 0xc0 +const TAP_LEAF_TAG = NBuffer.from('TapLeaf', 'utf8'); +const TAP_BRANCH_TAG = NBuffer.from('TapBranch', 'utf8'); +const TAP_TWEAK_TAG = NBuffer.from('TapTweak', 'utf8'); + +// todo: compare buffers dirrectly +const GROUP_ORDER = NBuffer.from('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', 'hex'); +const GROUP_ORDER_BN = new BN(GROUP_ORDER); + +const EC_P_BN = new BN(EC_P); +const EC_P_REDUCTION = BN.red(EC_P_BN); +const EC_P_QUADRATIC_RESIDUE = EC_P_BN.addn(1).divn(4); +const BN_2 = new BN(2); +const BN_3 = new BN(3); +const BN_7 = new BN(7); + +export function liftX(buffer: Buffer): Buffer | null { + if (!NBuffer.isBuffer(buffer)) return null; + if (buffer.length !== 32) return null; + + if (buffer.compare(ZERO32) === 0) return null; + if (buffer.compare(EC_P) >= 0) return null; + + const x = new BN(buffer); + + const x1 = x.toRed(EC_P_REDUCTION); + const ySq = x1 + .redPow(BN_3) + .add(BN_7) + .mod(EC_P_BN); + + const y = ySq.redPow(EC_P_QUADRATIC_RESIDUE); + + if (!ySq.eq(y.redPow(BN_2))) { + return null; + } + const y1 = y.isEven() ? y : EC_P_BN.sub(y); + + return NBuffer.concat([ + NBuffer.from([0x04]), + NBuffer.from(x1.toBuffer('be', 32)), + NBuffer.from(y1.toBuffer('be', 32)), + ]); +} + +export 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 = bcrypto.taggedHash( + TAP_TWEAK_TAG, + NBuffer.concat(h ? [pubKey, h] : [pubKey]), + ); + const t = new BN(tweakHash); + if (t.gte(GROUP_ORDER_BN)) { + // todo: add test for this case + throw new Error('Tweak value over the SECP256K1 Order'); + } + + const P = liftX(pubKey); + if (P === null) return null; + + const Q = pointAddScalar(P, tweakHash); + return { + isOdd: Q[64] % 2 === 1, + x: Q.slice(1, 33), + }; +} + +export function leafHash(script: Buffer, version: number): Buffer { + return NBuffer.concat([NBuffer.from([version]), serializeScript(script)]); +} + +export function rootHash(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer { + const k = []; + const e = []; + + const m = (controlBlock.length - 33) / 32; + k[0] = bcrypto.taggedHash(TAP_LEAF_TAG, tapLeafMsg); + + for (let j = 0; j < m; j++) { + e[j] = controlBlock.slice(33 + 32 * j, 65 + 32 * j); + if (k[j].compare(e[j]) < 0) { + k[j + 1] = bcrypto.taggedHash(TAP_BRANCH_TAG, NBuffer.concat([k[j], e[j]])); + } else { + k[j + 1] = bcrypto.taggedHash(TAP_BRANCH_TAG, NBuffer.concat([e[j], k[j]])); + } + } + + return k[m] +} + +// todo: solve any[] +export function computeMastRoot(scripts: any): Buffer { + if (scripts.length === 1) { + const script = scripts[0] + if (Array.isArray(script)) { + return computeMastRoot(script) + } + script.version = script.version || LEAF_VERSION_TAPSCRIPT + if ((script.version & 1) !== 0) throw new Error("Invalid script version") // todo typedef error + // todo: if (script.output)scheck is bytes + const scriptOutput = NBuffer.from(script.output, 'hex') + return bcrypto.taggedHash(TAP_LEAF_TAG, NBuffer.concat([NBuffer.from([script.version]), serializeScript(scriptOutput)])) + } + // todo: this is a binary tree, use zero an one index + const half = Math.trunc(scripts.length / 2) + let leftHash = computeMastRoot(scripts.slice(0, half)) + let rightHash = computeMastRoot(scripts.slice(half)) + + if (leftHash.compare(rightHash) === 1) [leftHash, rightHash] = [rightHash, leftHash] + return bcrypto.taggedHash(TAP_BRANCH_TAG, NBuffer.concat([leftHash, rightHash])) +} + +function serializeScript(s: Buffer) { + const varintLen = varuint.encodingLength(s.length); + const buffer = NBuffer.allocUnsafe(varintLen); // better + varuint.encode(s.length, buffer); + return NBuffer.concat([buffer, s]) +} + +// todo: do not use ecc +function pointAddScalar(P: Buffer, h: Buffer): Buffer { + return ecc.pointAddScalar(P, h); +} diff --git a/ts_src/types.ts b/ts_src/types.ts index 54f3b816d..014bfaafc 100644 --- a/ts_src/types.ts +++ b/ts_src/types.ts @@ -1,17 +1,9 @@ import { Buffer as NBuffer } from 'buffer'; -import * as bcrypto from './crypto'; -import * as varuint from 'bip174/src/lib/converter/varint'; - -// Temp, to be replaced -// Only works because bip32 has it as dependecy. Linting will fail. -const ecc = require('tiny-secp256k1'); -// todo, use import? -const BN = require('bn.js'); export const typeforce = require('typeforce'); -const ZERO32 = NBuffer.alloc(32, 0); -const EC_P = NBuffer.from( +export const ZERO32 = NBuffer.alloc(32, 0); +export const EC_P = NBuffer.from( 'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f', 'hex', ); @@ -35,117 +27,6 @@ export function isPoint(p: Buffer | number | undefined | null): boolean { return false; } -// todo review. Do not add dependcy to BN? -const EC_P_BN = new BN(EC_P); -const EC_P_REDUCTION = BN.red(EC_P_BN); -const EC_P_QUADRATIC_RESIDUE = EC_P_BN.addn(1).divn(4); -const BN_2 = new BN(2); -const BN_3 = new BN(3); -const BN_7 = new BN(7); - -export function liftX(buffer: Buffer): Buffer | null { - if (!NBuffer.isBuffer(buffer)) return null; - if (buffer.length !== 32) return null; - - if (buffer.compare(ZERO32) === 0) return null; - if (buffer.compare(EC_P) >= 0) return null; - - const x = new BN(buffer); - - const x1 = x.toRed(EC_P_REDUCTION); - const ySq = x1 - .redPow(BN_3) - .add(BN_7) - .mod(EC_P_BN); - - const y = ySq.redPow(EC_P_QUADRATIC_RESIDUE); - - if (!ySq.eq(y.redPow(BN_2))) { - return null; - } - const y1 = y.isEven() ? y : EC_P_BN.sub(y); - - return NBuffer.concat([ - NBuffer.from([0x04]), - NBuffer.from(x1.toBuffer('be', 32)), - NBuffer.from(y1.toBuffer('be', 32)), - ]); -} - -const TAP_TWEAK_TAG = NBuffer.from('TapTweak', 'utf8'); - -const GROUP_ORDER = NBuffer.from('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', 'hex'); -// todo: compare buffers dirrectly -const GROUP_ORDER_BN = new BN(GROUP_ORDER); - -export 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 = bcrypto.taggedHash( - TAP_TWEAK_TAG, - NBuffer.concat(h ? [pubKey, h] : [pubKey]), - ); - const t = new BN(tweakHash); - if (t.gte(GROUP_ORDER_BN)) { - // todo: add test for this case - throw new Error('Tweak value over the SECP256K1 Order'); - } - - const P = liftX(pubKey); - if (P === null) return null; - - const Q = pointAddScalar(P, tweakHash); - return { - isOdd: Q[64] % 2 === 1, - x: Q.slice(1, 33), - }; -} - -const TAP_LEAF_TAG = NBuffer.from('TapLeaf', 'utf8'); -const TAP_BRANCH_TAG = NBuffer.from('TapBranch', 'utf8'); - - -export function leafHash(script: Buffer, version: number): Buffer { - return NBuffer.concat([NBuffer.from([version]), serializeScript(script)]); -} - -export function rootHash(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer { - const k = []; - const e = []; - - const m = (controlBlock.length - 33) / 32; - k[0] = bcrypto.taggedHash(TAP_LEAF_TAG, tapLeafMsg); - - for (let j = 0; j < m; j++) { - e[j] = controlBlock.slice(33 + 32 * j, 65 + 32 * j); - if (k[j].compare(e[j]) < 0) { - k[j + 1] = bcrypto.taggedHash(TAP_BRANCH_TAG, NBuffer.concat([k[j], e[j]])); - } else { - k[j + 1] = bcrypto.taggedHash(TAP_BRANCH_TAG, NBuffer.concat([e[j], k[j]])); - } - } - - return k[m] -} - -// todo: move out -function serializeScript(s: Buffer) { - const varintLen = varuint.encodingLength(s.length); - const buffer = NBuffer.allocUnsafe(varintLen); // better - varuint.encode(s.length, buffer); - return NBuffer.concat([buffer, s]) -} - -// todo: do not use ecc -function pointAddScalar(P: Buffer, h: Buffer): Buffer { - return ecc.pointAddScalar(P, h); -} - const UINT31_MAX: number = Math.pow(2, 31) - 1; export function UInt31(value: number): boolean { return typeforce.UInt32(value) && value <= UINT31_MAX; From c987b0c03668061a7d519222f17b012e5ab14bd5 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 2 Nov 2021 17:40:25 +0200 Subject: [PATCH 20/53] chore: code format and lint --- src/payments/p2tr.js | 4 +- test/payments.utils.ts | 19 ++-- ts_src/merkle.ts | 2 +- ts_src/payments/index.ts | 2 +- ts_src/payments/p2tr.ts | 91 +++++++++++-------- ts_src/taproot.ts | 189 +++++++++++++++++++++------------------ ts_src/types.ts | 9 +- 7 files changed, 179 insertions(+), 137 deletions(-) diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index 53767318a..22c03b088 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -103,7 +103,7 @@ function p2tr(a, opts) { return witness[witness.length - 1].slice(1, 33); }); lazy.prop(o, 'signature', () => { - if (a.witness?.length !== 1) return; + if (!a.witness || a.witness.length !== 1) return; return a.witness[0]; }); lazy.prop(o, 'input', () => { @@ -150,7 +150,7 @@ function p2tr(a, opts) { throw new TypeError('Pubkey mismatch'); else pubkey = tweakedKey.x; } - if (pubkey?.length) { + if (pubkey && pubkey.length) { if ((0, taproot_1.liftX)(pubkey) === null) throw new TypeError('Invalid pubkey for p2tr'); } diff --git a/test/payments.utils.ts b/test/payments.utils.ts index d1b40aa5e..1aa401830 100644 --- a/test/payments.utils.ts +++ b/test/payments.utils.ts @@ -87,7 +87,11 @@ export function equate(a: any, b: any, args?: any): void { 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'); + t.strictEqual( + tryHex(a.internalPubkey), + tryHex(b.internalPubkey), + 'Inequal *.internalPubkey', + ); if ('signature' in b) t.strictEqual( tryHex(a.signature), @@ -149,8 +153,7 @@ export function preform(x: any): any { if (x.redeem.network) x.redeem.network = (BNETWORKS as any)[x.redeem.network]; } - if (x.scriptsTree) - x.scriptsTree = convertScriptsTree(x.scriptsTree) + if (x.scriptsTree) x.scriptsTree = convertScriptsTree(x.scriptsTree); return x; } @@ -176,12 +179,10 @@ export function from(path: string, object: any, result?: any): any { // todo: solve any type function convertScriptsTree(scriptsTree: any): any { - if (Array.isArray(scriptsTree)) - return scriptsTree.map(convertScriptsTree) - + if (Array.isArray(scriptsTree)) return scriptsTree.map(convertScriptsTree); const script = Object.assign({}, scriptsTree); - if ((typeof script.output === 'string')) - script.output = asmToBuffer(scriptsTree.output) - return script + if (typeof script.output === 'string') + script.output = asmToBuffer(scriptsTree.output); + return script; } diff --git a/ts_src/merkle.ts b/ts_src/merkle.ts index 814819c79..8ff8c3f8c 100644 --- a/ts_src/merkle.ts +++ b/ts_src/merkle.ts @@ -24,4 +24,4 @@ export function fastMerkleRoot( } return results[0]; -} \ No newline at end of file +} diff --git a/ts_src/payments/index.ts b/ts_src/payments/index.ts index 5243902d4..f6223a3d0 100644 --- a/ts_src/payments/index.ts +++ b/ts_src/payments/index.ts @@ -24,7 +24,7 @@ export interface Payment { address?: string; // taproot: betch32m hash?: Buffer; // taproot: MAST root redeem?: Payment; // taproot: when script path spending is used spending - scriptsTree?: any // todo: solve + scriptsTree?: any; // todo: solve witness?: Buffer[]; } diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index aef500c85..25e52696c 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -1,7 +1,13 @@ import { bitcoin as BITCOIN_NETWORK } from '../networks'; import * as bscript from '../script'; import { typeforce as typef } from '../types'; -import { computeMastRoot, leafHash, rootHash, tweakKey, liftX } from '../taproot'; +import { + computeMastRoot, + leafHash, + rootHash, + tweakKey, + liftX, +} from '../taproot'; import { Payment, PaymentOpts } from './index'; import * as lazy from './lazy'; import { bech32m } from 'bech32'; @@ -14,7 +20,14 @@ const ANNEX_PREFIX = 0x50; // input: <> // output: OP_1 {pubKey} export function p2tr(a: Payment, opts?: PaymentOpts): Payment { - if (!a.address && !a.output && !a.pubkey && !a.output && !a.internalPubkey && !(a.witness && a.witness.length > 1)) + if ( + !a.address && + !a.output && + !a.pubkey && + !a.output && + !a.internalPubkey && + !(a.witness && a.witness.length > 1) + ) throw new TypeError('Not enough data'); opts = Object.assign({ validate: true }, opts || {}); @@ -46,13 +59,16 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { }); 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)) { + if (!a.witness || !a.witness.length) return; + if ( + a.witness.length >= 2 && + a.witness[a.witness.length - 1][0] === ANNEX_PREFIX + ) { // remove annex, ignored by taproot return a.witness.slice(0, -1); } - return a.witness.slice() - }) + return a.witness.slice(); + }); const network = a.network || BITCOIN_NETWORK; const o: Payment = { name: 'p2tr', network }; @@ -65,19 +81,18 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { return bech32m.encode(network.bech32, words); }); - lazy.prop(o, 'hash', () => { if (a.hash) return a.hash; - if (a.scriptsTree) return computeMastRoot(a.scriptsTree) - const w = _witness() + if (a.scriptsTree) return computeMastRoot(a.scriptsTree); + const w = _witness(); if (w && w.length > 1) { const controlBlock = w[w.length - 1]; const leafVersion = controlBlock[0] & 0b11111110; const script = w[w.length - 2]; - const tapLeafHash = leafHash(script, leafVersion) - return rootHash(controlBlock, tapLeafHash) + const tapLeafHash = leafHash(script, leafVersion); + return rootHash(controlBlock, tapLeafHash); } - return null + return null; }); lazy.prop(o, 'output', () => { if (!o.pubkey) return; @@ -85,28 +100,28 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { }); lazy.prop(o, 'pubkey', () => { if (a.pubkey) return a.pubkey; - if (a.output) return a.output.slice(2) + 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 + 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() + const witness = _witness(); if (witness && witness.length > 1) return witness[witness.length - 1].slice(1, 33); }); lazy.prop(o, 'signature', () => { - if (a.witness?.length !== 1) return; + if (!a.witness || a.witness.length !== 1) return; return a.witness[0]; }); lazy.prop(o, 'input', () => { // todo: not sure }); lazy.prop(o, 'witness', () => { - if (a.witness) return a.witness + if (a.witness) return a.witness; if (!a.signature) return; return [a.signature]; }); @@ -143,26 +158,26 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { } if (a.internalPubkey) { - const tweakedKey = tweakKey(a.internalPubkey, o.hash) - if (tweakedKey === null) throw new TypeError('Invalid internalPubkey for p2tr'); + const tweakedKey = tweakKey(a.internalPubkey, o.hash); + if (tweakedKey === null) + throw new TypeError('Invalid internalPubkey for p2tr'); if (pubkey.length > 0 && !pubkey.equals(tweakedKey.x)) throw new TypeError('Pubkey mismatch'); else pubkey = tweakedKey.x; } - if (pubkey?.length) { + if (pubkey && pubkey.length) { if (liftX(pubkey) === null) throw new TypeError('Invalid pubkey for p2tr'); } if (a.hash && a.scriptsTree) { - const hash = computeMastRoot(a.scriptsTree) - if (!a.hash.equals(hash)) - throw new TypeError('Hash mismatch'); + const hash = computeMastRoot(a.scriptsTree); + if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch'); } // todo: review cache - const witness = _witness() + const witness = _witness(); if (witness && witness.length) { if (witness.length === 1) { @@ -176,14 +191,22 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { // 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.`); + 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!`); + 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.`); + 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)) @@ -196,10 +219,10 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { const leafVersion = controlBlock[0] & 0b11111110; const script = witness[witness.length - 2]; - const tapLeafHash = leafHash(script, leafVersion) - const hash = rootHash(controlBlock, tapLeafHash) + const tapLeafHash = leafHash(script, leafVersion); + const hash = rootHash(controlBlock, tapLeafHash); - const outputKey = tweakKey(internalPubkey, hash) + const outputKey = tweakKey(internalPubkey, hash); if (!outputKey) // todo: needs test data throw new TypeError('Invalid outputKey for p2tr witness'); @@ -207,12 +230,10 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { if (pubkey.length && !pubkey.equals(outputKey.x)) throw new TypeError('Pubkey mismatch for p2tr witness'); - const controlBlockOddParity = (controlBlock[0] & 1) === 1 + const controlBlockOddParity = (controlBlock[0] & 1) === 1; if (outputKey.isOdd !== controlBlockOddParity) - throw new Error('Incorrect parity') - + throw new Error('Incorrect parity'); } - } } diff --git a/ts_src/taproot.ts b/ts_src/taproot.ts index 5370e326f..de4083b0e 100644 --- a/ts_src/taproot.ts +++ b/ts_src/taproot.ts @@ -4,18 +4,21 @@ const BN = require('bn.js'); import * as bcrypto from './crypto'; // todo: use varuint-bitcoin?? import * as varuint from 'bip174/src/lib/converter/varint'; -import { TweakedPublicKey, ZERO32, EC_P } from './types' +import { TweakedPublicKey, ZERO32, EC_P } from './types'; // todo: !!!Temp, to be replaced. Only works because bip32 has it as dependecy. Linting will fail. const ecc = require('tiny-secp256k1'); -const LEAF_VERSION_TAPSCRIPT = 0xc0 +const LEAF_VERSION_TAPSCRIPT = 0xc0; const TAP_LEAF_TAG = NBuffer.from('TapLeaf', 'utf8'); const TAP_BRANCH_TAG = NBuffer.from('TapBranch', 'utf8'); const TAP_TWEAK_TAG = NBuffer.from('TapTweak', 'utf8'); // todo: compare buffers dirrectly -const GROUP_ORDER = NBuffer.from('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', 'hex'); +const GROUP_ORDER = NBuffer.from( + 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', + 'hex', +); const GROUP_ORDER_BN = new BN(GROUP_ORDER); const EC_P_BN = new BN(EC_P); @@ -26,115 +29,131 @@ const BN_3 = new BN(3); const BN_7 = new BN(7); export function liftX(buffer: Buffer): Buffer | null { - if (!NBuffer.isBuffer(buffer)) return null; - if (buffer.length !== 32) return null; + if (!NBuffer.isBuffer(buffer)) return null; + if (buffer.length !== 32) return null; - if (buffer.compare(ZERO32) === 0) return null; - if (buffer.compare(EC_P) >= 0) return null; + if (buffer.compare(ZERO32) === 0) return null; + if (buffer.compare(EC_P) >= 0) return null; - const x = new BN(buffer); + const x = new BN(buffer); - const x1 = x.toRed(EC_P_REDUCTION); - const ySq = x1 - .redPow(BN_3) - .add(BN_7) - .mod(EC_P_BN); + const x1 = x.toRed(EC_P_REDUCTION); + const ySq = x1 + .redPow(BN_3) + .add(BN_7) + .mod(EC_P_BN); - const y = ySq.redPow(EC_P_QUADRATIC_RESIDUE); + const y = ySq.redPow(EC_P_QUADRATIC_RESIDUE); - if (!ySq.eq(y.redPow(BN_2))) { - return null; - } - const y1 = y.isEven() ? y : EC_P_BN.sub(y); + if (!ySq.eq(y.redPow(BN_2))) { + return null; + } + const y1 = y.isEven() ? y : EC_P_BN.sub(y); - return NBuffer.concat([ - NBuffer.from([0x04]), - NBuffer.from(x1.toBuffer('be', 32)), - NBuffer.from(y1.toBuffer('be', 32)), - ]); + return NBuffer.concat([ + NBuffer.from([0x04]), + NBuffer.from(x1.toBuffer('be', 32)), + NBuffer.from(y1.toBuffer('be', 32)), + ]); } export function tweakKey( - pubKey: Buffer, - h: Buffer | undefined, + 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 = bcrypto.taggedHash( - TAP_TWEAK_TAG, - NBuffer.concat(h ? [pubKey, h] : [pubKey]), - ); - const t = new BN(tweakHash); - if (t.gte(GROUP_ORDER_BN)) { - // todo: add test for this case - throw new Error('Tweak value over the SECP256K1 Order'); - } - - const P = liftX(pubKey); - if (P === null) return null; - - const Q = pointAddScalar(P, tweakHash); - return { - isOdd: Q[64] % 2 === 1, - x: Q.slice(1, 33), - }; + if (!NBuffer.isBuffer(pubKey)) return null; + if (pubKey.length !== 32) return null; + if (h && h.length !== 32) return null; + + const tweakHash = bcrypto.taggedHash( + TAP_TWEAK_TAG, + NBuffer.concat(h ? [pubKey, h] : [pubKey]), + ); + const t = new BN(tweakHash); + if (t.gte(GROUP_ORDER_BN)) { + // todo: add test for this case + throw new Error('Tweak value over the SECP256K1 Order'); + } + + const P = liftX(pubKey); + if (P === null) return null; + + const Q = pointAddScalar(P, tweakHash); + return { + isOdd: Q[64] % 2 === 1, + x: Q.slice(1, 33), + }; } export function leafHash(script: Buffer, version: number): Buffer { - return NBuffer.concat([NBuffer.from([version]), serializeScript(script)]); + return NBuffer.concat([NBuffer.from([version]), serializeScript(script)]); } export function rootHash(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer { - const k = []; - const e = []; - - const m = (controlBlock.length - 33) / 32; - k[0] = bcrypto.taggedHash(TAP_LEAF_TAG, tapLeafMsg); - - for (let j = 0; j < m; j++) { - e[j] = controlBlock.slice(33 + 32 * j, 65 + 32 * j); - if (k[j].compare(e[j]) < 0) { - k[j + 1] = bcrypto.taggedHash(TAP_BRANCH_TAG, NBuffer.concat([k[j], e[j]])); - } else { - k[j + 1] = bcrypto.taggedHash(TAP_BRANCH_TAG, NBuffer.concat([e[j], k[j]])); - } + const k = []; + const e = []; + + const m = (controlBlock.length - 33) / 32; + k[0] = bcrypto.taggedHash(TAP_LEAF_TAG, tapLeafMsg); + + for (let j = 0; j < m; j++) { + e[j] = controlBlock.slice(33 + 32 * j, 65 + 32 * j); + if (k[j].compare(e[j]) < 0) { + k[j + 1] = bcrypto.taggedHash( + TAP_BRANCH_TAG, + NBuffer.concat([k[j], e[j]]), + ); + } else { + k[j + 1] = bcrypto.taggedHash( + TAP_BRANCH_TAG, + NBuffer.concat([e[j], k[j]]), + ); } + } - return k[m] + return k[m]; } // todo: solve any[] export function computeMastRoot(scripts: any): Buffer { - if (scripts.length === 1) { - const script = scripts[0] - if (Array.isArray(script)) { - return computeMastRoot(script) - } - script.version = script.version || LEAF_VERSION_TAPSCRIPT - if ((script.version & 1) !== 0) throw new Error("Invalid script version") // todo typedef error - // todo: if (script.output)scheck is bytes - const scriptOutput = NBuffer.from(script.output, 'hex') - return bcrypto.taggedHash(TAP_LEAF_TAG, NBuffer.concat([NBuffer.from([script.version]), serializeScript(scriptOutput)])) + if (scripts.length === 1) { + const script = scripts[0]; + if (Array.isArray(script)) { + return computeMastRoot(script); } - // todo: this is a binary tree, use zero an one index - const half = Math.trunc(scripts.length / 2) - let leftHash = computeMastRoot(scripts.slice(0, half)) - let rightHash = computeMastRoot(scripts.slice(half)) - - if (leftHash.compare(rightHash) === 1) [leftHash, rightHash] = [rightHash, leftHash] - return bcrypto.taggedHash(TAP_BRANCH_TAG, NBuffer.concat([leftHash, rightHash])) + script.version = script.version || LEAF_VERSION_TAPSCRIPT; + if ((script.version & 1) !== 0) throw new Error('Invalid script version'); // todo typedef error + // todo: if (script.output)scheck is bytes + const scriptOutput = NBuffer.from(script.output, 'hex'); + return bcrypto.taggedHash( + TAP_LEAF_TAG, + NBuffer.concat([ + NBuffer.from([script.version]), + serializeScript(scriptOutput), + ]), + ); + } + // todo: this is a binary tree, use zero an one index + const half = Math.trunc(scripts.length / 2); + let leftHash = computeMastRoot(scripts.slice(0, half)); + let rightHash = computeMastRoot(scripts.slice(half)); + + if (leftHash.compare(rightHash) === 1) + [leftHash, rightHash] = [rightHash, leftHash]; + return bcrypto.taggedHash( + TAP_BRANCH_TAG, + NBuffer.concat([leftHash, rightHash]), + ); } -function serializeScript(s: Buffer) { - const varintLen = varuint.encodingLength(s.length); - const buffer = NBuffer.allocUnsafe(varintLen); // better - varuint.encode(s.length, buffer); - return NBuffer.concat([buffer, s]) +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]); } // todo: do not use ecc function pointAddScalar(P: Buffer, h: Buffer): Buffer { - return ecc.pointAddScalar(P, h); + return ecc.pointAddScalar(P, h); } diff --git a/ts_src/types.ts b/ts_src/types.ts index 014bfaafc..fd0893df5 100644 --- a/ts_src/types.ts +++ b/ts_src/types.ts @@ -74,12 +74,13 @@ export interface TweakedPublicKey { export const TaprootLeaf = typeforce.compile({ output: typeforce.BufferN(34), - version: typeforce.maybe(typeforce.UInt8) // todo: recheck -}) + version: typeforce.maybe(typeforce.UInt8), // todo: recheck +}); // / todo: revisit -export const TaprootNode = typeforce.arrayOf(typeforce.oneOf(TaprootLeaf, typeforce.arrayOf(TaprootLeaf))) - +export const TaprootNode = typeforce.arrayOf( + typeforce.oneOf(TaprootLeaf, typeforce.arrayOf(TaprootLeaf)), +); export const Buffer256bit = typeforce.BufferN(32); export const Hash160bit = typeforce.BufferN(20); From 2feff5dd58014a76ba90022cfb2171e6789d3f70 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 2 Nov 2021 17:49:59 +0200 Subject: [PATCH 21/53] refactor: compare GROUP_ORDER as buffer (instead of using BN.js) --- src/taproot.js | 9 +-------- src/types.d.ts | 1 + src/types.js | 6 +++++- ts_src/taproot.ts | 13 +++---------- ts_src/types.ts | 4 ++++ 5 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/taproot.js b/src/taproot.js index 4930f4331..f0cade074 100644 --- a/src/taproot.js +++ b/src/taproot.js @@ -13,12 +13,6 @@ const LEAF_VERSION_TAPSCRIPT = 0xc0; const TAP_LEAF_TAG = buffer_1.Buffer.from('TapLeaf', 'utf8'); const TAP_BRANCH_TAG = buffer_1.Buffer.from('TapBranch', 'utf8'); const TAP_TWEAK_TAG = buffer_1.Buffer.from('TapTweak', 'utf8'); -// todo: compare buffers dirrectly -const GROUP_ORDER = buffer_1.Buffer.from( - 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', - 'hex', -); -const GROUP_ORDER_BN = new BN(GROUP_ORDER); const EC_P_BN = new BN(types_1.EC_P); const EC_P_REDUCTION = BN.red(EC_P_BN); const EC_P_QUADRATIC_RESIDUE = EC_P_BN.addn(1).divn(4); @@ -56,8 +50,7 @@ function tweakKey(pubKey, h) { TAP_TWEAK_TAG, buffer_1.Buffer.concat(h ? [pubKey, h] : [pubKey]), ); - const t = new BN(tweakHash); - if (t.gte(GROUP_ORDER_BN)) { + if (tweakHash.compare(types_1.GROUP_ORDER) >= 0) { // todo: add test for this case throw new Error('Tweak value over the SECP256K1 Order'); } diff --git a/src/types.d.ts b/src/types.d.ts index 6e72a6d47..7f58a0a87 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -3,6 +3,7 @@ import { Buffer as NBuffer } from 'buffer'; export declare const typeforce: any; export declare const ZERO32: NBuffer; export declare const EC_P: NBuffer; +export declare const GROUP_ORDER: NBuffer; export declare function isPoint(p: Buffer | number | undefined | null): boolean; export declare function UInt31(value: number): boolean; export declare function BIP32Path(value: string): boolean; diff --git a/src/types.js b/src/types.js index ba62bd813..ff1786256 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.TaprootNode = exports.TaprootLeaf = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.isPoint = exports.EC_P = exports.ZERO32 = 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.TaprootNode = exports.TaprootLeaf = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.isPoint = exports.GROUP_ORDER = exports.EC_P = exports.ZERO32 = exports.typeforce = void 0; const buffer_1 = require('buffer'); exports.typeforce = require('typeforce'); exports.ZERO32 = buffer_1.Buffer.alloc(32, 0); @@ -8,6 +8,10 @@ exports.EC_P = buffer_1.Buffer.from( 'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f', 'hex', ); +exports.GROUP_ORDER = buffer_1.Buffer.from( + 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', + 'hex', +); function isPoint(p) { if (!buffer_1.Buffer.isBuffer(p)) return false; if (p.length < 33) return false; diff --git a/ts_src/taproot.ts b/ts_src/taproot.ts index de4083b0e..b09e17508 100644 --- a/ts_src/taproot.ts +++ b/ts_src/taproot.ts @@ -4,7 +4,7 @@ const BN = require('bn.js'); import * as bcrypto from './crypto'; // todo: use varuint-bitcoin?? import * as varuint from 'bip174/src/lib/converter/varint'; -import { TweakedPublicKey, ZERO32, EC_P } from './types'; +import { TweakedPublicKey, ZERO32, EC_P, GROUP_ORDER } from './types'; // todo: !!!Temp, to be replaced. Only works because bip32 has it as dependecy. Linting will fail. const ecc = require('tiny-secp256k1'); @@ -14,13 +14,6 @@ const TAP_LEAF_TAG = NBuffer.from('TapLeaf', 'utf8'); const TAP_BRANCH_TAG = NBuffer.from('TapBranch', 'utf8'); const TAP_TWEAK_TAG = NBuffer.from('TapTweak', 'utf8'); -// todo: compare buffers dirrectly -const GROUP_ORDER = NBuffer.from( - 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', - 'hex', -); -const GROUP_ORDER_BN = new BN(GROUP_ORDER); - const EC_P_BN = new BN(EC_P); const EC_P_REDUCTION = BN.red(EC_P_BN); const EC_P_QUADRATIC_RESIDUE = EC_P_BN.addn(1).divn(4); @@ -69,8 +62,8 @@ export function tweakKey( TAP_TWEAK_TAG, NBuffer.concat(h ? [pubKey, h] : [pubKey]), ); - const t = new BN(tweakHash); - if (t.gte(GROUP_ORDER_BN)) { + + if (tweakHash.compare(GROUP_ORDER) >= 0) { // todo: add test for this case throw new Error('Tweak value over the SECP256K1 Order'); } diff --git a/ts_src/types.ts b/ts_src/types.ts index fd0893df5..e04a05589 100644 --- a/ts_src/types.ts +++ b/ts_src/types.ts @@ -7,6 +7,10 @@ export const EC_P = NBuffer.from( 'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f', 'hex', ); +export const GROUP_ORDER = NBuffer.from( + 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', + 'hex', +); export function isPoint(p: Buffer | number | undefined | null): boolean { if (!NBuffer.isBuffer(p)) return false; From 2f55aad419f63d289a3319c7eb9116a5b966700f Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 3 Nov 2021 09:43:38 +0200 Subject: [PATCH 22/53] refactor: rename `rootHash` to `rootHashFromPath` and `computeMastRoot` to `rootHashFromTree` --- src/payments/p2tr.js | 8 ++++---- src/taproot.d.ts | 4 ++-- src/taproot.js | 16 ++++++++-------- ts_src/payments/p2tr.ts | 13 ++++++------- ts_src/taproot.ts | 10 +++++----- 5 files changed, 25 insertions(+), 26 deletions(-) diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index 22c03b088..50e99536d 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -72,14 +72,14 @@ function p2tr(a, opts) { }); lazy.prop(o, 'hash', () => { if (a.hash) return a.hash; - if (a.scriptsTree) return (0, taproot_1.computeMastRoot)(a.scriptsTree); + if (a.scriptsTree) return (0, taproot_1.rootHashFromTree)(a.scriptsTree); const w = _witness(); if (w && w.length > 1) { const controlBlock = w[w.length - 1]; const leafVersion = controlBlock[0] & 0b11111110; const script = w[w.length - 2]; const tapLeafHash = (0, taproot_1.leafHash)(script, leafVersion); - return (0, taproot_1.rootHash)(controlBlock, tapLeafHash); + return (0, taproot_1.rootHashFromPath)(controlBlock, tapLeafHash); } return null; }); @@ -155,7 +155,7 @@ function p2tr(a, opts) { throw new TypeError('Invalid pubkey for p2tr'); } if (a.hash && a.scriptsTree) { - const hash = (0, taproot_1.computeMastRoot)(a.scriptsTree); + const hash = (0, taproot_1.rootHashFromTree)(a.scriptsTree); if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch'); } // todo: review cache @@ -195,7 +195,7 @@ function p2tr(a, opts) { const leafVersion = controlBlock[0] & 0b11111110; const script = witness[witness.length - 2]; const tapLeafHash = (0, taproot_1.leafHash)(script, leafVersion); - const hash = (0, taproot_1.rootHash)(controlBlock, tapLeafHash); + const hash = (0, taproot_1.rootHashFromPath)(controlBlock, tapLeafHash); const outputKey = (0, taproot_1.tweakKey)(internalPubkey, hash); if (!outputKey) // todo: needs test data diff --git a/src/taproot.d.ts b/src/taproot.d.ts index 0391f6bf1..581c09f13 100644 --- a/src/taproot.d.ts +++ b/src/taproot.d.ts @@ -3,5 +3,5 @@ import { TweakedPublicKey } from './types'; export declare function liftX(buffer: Buffer): Buffer | null; export declare function tweakKey(pubKey: Buffer, h: Buffer | undefined): TweakedPublicKey | null; export declare function leafHash(script: Buffer, version: number): Buffer; -export declare function rootHash(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer; -export declare function computeMastRoot(scripts: any): Buffer; +export declare function rootHashFromPath(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer; +export declare function rootHashFromTree(scripts: any): Buffer; diff --git a/src/taproot.js b/src/taproot.js index f0cade074..10452a88e 100644 --- a/src/taproot.js +++ b/src/taproot.js @@ -1,6 +1,6 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -exports.computeMastRoot = exports.rootHash = exports.leafHash = exports.tweakKey = exports.liftX = void 0; +exports.rootHashFromTree = exports.rootHashFromPath = exports.leafHash = exports.tweakKey = exports.liftX = void 0; const buffer_1 = require('buffer'); const BN = require('bn.js'); const bcrypto = require('./crypto'); @@ -70,7 +70,7 @@ function leafHash(script, version) { ]); } exports.leafHash = leafHash; -function rootHash(controlBlock, tapLeafMsg) { +function rootHashFromPath(controlBlock, tapLeafMsg) { const k = []; const e = []; const m = (controlBlock.length - 33) / 32; @@ -91,13 +91,13 @@ function rootHash(controlBlock, tapLeafMsg) { } return k[m]; } -exports.rootHash = rootHash; +exports.rootHashFromPath = rootHashFromPath; // todo: solve any[] -function computeMastRoot(scripts) { +function rootHashFromTree(scripts) { if (scripts.length === 1) { const script = scripts[0]; if (Array.isArray(script)) { - return computeMastRoot(script); + return rootHashFromTree(script); } script.version = script.version || LEAF_VERSION_TAPSCRIPT; if ((script.version & 1) !== 0) throw new Error('Invalid script version'); // todo typedef error @@ -113,8 +113,8 @@ function computeMastRoot(scripts) { } // todo: this is a binary tree, use zero an one index const half = Math.trunc(scripts.length / 2); - let leftHash = computeMastRoot(scripts.slice(0, half)); - let rightHash = computeMastRoot(scripts.slice(half)); + let leftHash = rootHashFromTree(scripts.slice(0, half)); + let rightHash = rootHashFromTree(scripts.slice(half)); if (leftHash.compare(rightHash) === 1) [leftHash, rightHash] = [rightHash, leftHash]; return bcrypto.taggedHash( @@ -122,7 +122,7 @@ function computeMastRoot(scripts) { buffer_1.Buffer.concat([leftHash, rightHash]), ); } -exports.computeMastRoot = computeMastRoot; +exports.rootHashFromTree = rootHashFromTree; function serializeScript(s) { const varintLen = varuint.encodingLength(s.length); const buffer = buffer_1.Buffer.allocUnsafe(varintLen); // better diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index 25e52696c..ccbf4790d 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -2,9 +2,9 @@ import { bitcoin as BITCOIN_NETWORK } from '../networks'; import * as bscript from '../script'; import { typeforce as typef } from '../types'; import { - computeMastRoot, + rootHashFromTree, + rootHashFromPath, leafHash, - rootHash, tweakKey, liftX, } from '../taproot'; @@ -83,14 +83,14 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { lazy.prop(o, 'hash', () => { if (a.hash) return a.hash; - if (a.scriptsTree) return computeMastRoot(a.scriptsTree); + if (a.scriptsTree) return rootHashFromTree(a.scriptsTree); const w = _witness(); if (w && w.length > 1) { const controlBlock = w[w.length - 1]; const leafVersion = controlBlock[0] & 0b11111110; const script = w[w.length - 2]; const tapLeafHash = leafHash(script, leafVersion); - return rootHash(controlBlock, tapLeafHash); + return rootHashFromPath(controlBlock, tapLeafHash); } return null; }); @@ -172,11 +172,10 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { } if (a.hash && a.scriptsTree) { - const hash = computeMastRoot(a.scriptsTree); + const hash = rootHashFromTree(a.scriptsTree); if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch'); } - // todo: review cache const witness = _witness(); if (witness && witness.length) { @@ -220,7 +219,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { const script = witness[witness.length - 2]; const tapLeafHash = leafHash(script, leafVersion); - const hash = rootHash(controlBlock, tapLeafHash); + const hash = rootHashFromPath(controlBlock, tapLeafHash); const outputKey = tweakKey(internalPubkey, hash); if (!outputKey) diff --git a/ts_src/taproot.ts b/ts_src/taproot.ts index b09e17508..36213b757 100644 --- a/ts_src/taproot.ts +++ b/ts_src/taproot.ts @@ -82,7 +82,7 @@ export function leafHash(script: Buffer, version: number): Buffer { return NBuffer.concat([NBuffer.from([version]), serializeScript(script)]); } -export function rootHash(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer { +export function rootHashFromPath(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer { const k = []; const e = []; @@ -108,11 +108,11 @@ export function rootHash(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer { } // todo: solve any[] -export function computeMastRoot(scripts: any): Buffer { +export function rootHashFromTree(scripts: any): Buffer { if (scripts.length === 1) { const script = scripts[0]; if (Array.isArray(script)) { - return computeMastRoot(script); + return rootHashFromTree(script); } script.version = script.version || LEAF_VERSION_TAPSCRIPT; if ((script.version & 1) !== 0) throw new Error('Invalid script version'); // todo typedef error @@ -128,8 +128,8 @@ export function computeMastRoot(scripts: any): Buffer { } // todo: this is a binary tree, use zero an one index const half = Math.trunc(scripts.length / 2); - let leftHash = computeMastRoot(scripts.slice(0, half)); - let rightHash = computeMastRoot(scripts.slice(half)); + let leftHash = rootHashFromTree(scripts.slice(0, half)); + let rightHash = rootHashFromTree(scripts.slice(half)); if (leftHash.compare(rightHash) === 1) [leftHash, rightHash] = [rightHash, leftHash]; From 2a4e64b691bf1c36895c74945fbb04b2f5ebbfe3 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 3 Nov 2021 15:47:49 +0200 Subject: [PATCH 23/53] tests: add bib341 tests by @sipa; plus refactoring --- src/payments/index.d.ts | 2 + src/payments/p2tr.js | 10 ++- src/taproot.d.ts | 8 +- src/taproot.js | 30 +++---- src/types.d.ts | 6 +- src/types.js | 13 +-- test/fixtures/p2tr.json | 176 +++++++++++++++++++++++++++++++++++++++ ts_src/payments/index.ts | 4 +- ts_src/payments/p2tr.ts | 16 +++- ts_src/taproot.ts | 25 ++---- ts_src/types.ts | 13 +-- 11 files changed, 236 insertions(+), 67 deletions(-) diff --git a/src/payments/index.d.ts b/src/payments/index.d.ts index dc1978ab0..0170dd093 100644 --- a/src/payments/index.d.ts +++ b/src/payments/index.d.ts @@ -1,5 +1,6 @@ /// import { Network } from '../networks'; +import { TaprootLeaf } from '../types'; import { p2data as embed } from './embed'; import { p2ms } from './p2ms'; import { p2pk } from './p2pk'; @@ -25,6 +26,7 @@ export interface Payment { hash?: Buffer; redeem?: Payment; scriptsTree?: any; + scriptLeaf?: TaprootLeaf; witness?: Buffer[]; } export declare type PaymentCreator = (a: Payment, opts?: PaymentOpts) => Payment; diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index 50e99536d..1d66fb5f7 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -38,6 +38,10 @@ function p2tr(a, opts) { types_1.typeforce.arrayOf(types_1.typeforce.Buffer), ), // scriptsTree: typef.maybe(typef.TaprootNode), // use merkel.isMast ? + scriptLeaf: types_1.typeforce.maybe({ + version: types_1.typeforce.maybe(types_1.typeforce.Number), + output: types_1.typeforce.maybe(types_1.typeforce.Buffer), + }), }, a, ); @@ -87,6 +91,9 @@ function p2tr(a, opts) { if (!o.pubkey) return; return bscript.compile([OPS.OP_1, o.pubkey]); }); + lazy.prop(o, 'scriptLeaf', () => { + if (!a.scriptLeaf) return a.scriptLeaf; + }); lazy.prop(o, 'pubkey', () => { if (a.pubkey) return a.pubkey; if (a.output) return a.output.slice(2); @@ -107,7 +114,7 @@ function p2tr(a, opts) { return a.witness[0]; }); lazy.prop(o, 'input', () => { - // todo: not sure + // todo }); lazy.prop(o, 'witness', () => { if (a.witness) return a.witness; @@ -158,7 +165,6 @@ function p2tr(a, opts) { const hash = (0, taproot_1.rootHashFromTree)(a.scriptsTree); if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch'); } - // todo: review cache const witness = _witness(); if (witness && witness.length) { if (witness.length === 1) { diff --git a/src/taproot.d.ts b/src/taproot.d.ts index 581c09f13..39f333958 100644 --- a/src/taproot.d.ts +++ b/src/taproot.d.ts @@ -1,7 +1,11 @@ /// -import { TweakedPublicKey } from './types'; +import { TweakedPublicKey, TaprootLeaf } from './types'; export declare function liftX(buffer: Buffer): Buffer | null; export declare function tweakKey(pubKey: Buffer, h: Buffer | undefined): TweakedPublicKey | null; export declare function leafHash(script: Buffer, version: number): Buffer; export declare function rootHashFromPath(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer; -export declare function rootHashFromTree(scripts: any): Buffer; +export interface HashTree { + rootHash: Buffer; + scritptPath?: Buffer; +} +export declare function rootHashFromTree(scripts: TaprootLeaf[]): Buffer; diff --git a/src/taproot.js b/src/taproot.js index 10452a88e..e749d237e 100644 --- a/src/taproot.js +++ b/src/taproot.js @@ -52,7 +52,7 @@ function tweakKey(pubKey, h) { ); if (tweakHash.compare(types_1.GROUP_ORDER) >= 0) { // todo: add test for this case - throw new Error('Tweak value over the SECP256K1 Order'); + throw new TypeError('Tweak value over the SECP256K1 Order'); } const P = liftX(pubKey); if (P === null) return null; @@ -64,17 +64,19 @@ function tweakKey(pubKey, h) { } exports.tweakKey = tweakKey; function leafHash(script, version) { - return buffer_1.Buffer.concat([ - buffer_1.Buffer.from([version]), - serializeScript(script), - ]); + return bcrypto.taggedHash( + TAP_LEAF_TAG, + buffer_1.Buffer.concat([ + buffer_1.Buffer.from([version]), + serializeScript(script), + ]), + ); } exports.leafHash = leafHash; function rootHashFromPath(controlBlock, tapLeafMsg) { - const k = []; + const k = [tapLeafMsg]; const e = []; const m = (controlBlock.length - 33) / 32; - k[0] = bcrypto.taggedHash(TAP_LEAF_TAG, tapLeafMsg); for (let j = 0; j < m; j++) { e[j] = controlBlock.slice(33 + 32 * j, 65 + 32 * j); if (k[j].compare(e[j]) < 0) { @@ -92,7 +94,6 @@ function rootHashFromPath(controlBlock, tapLeafMsg) { return k[m]; } exports.rootHashFromPath = rootHashFromPath; -// todo: solve any[] function rootHashFromTree(scripts) { if (scripts.length === 1) { const script = scripts[0]; @@ -100,16 +101,9 @@ function rootHashFromTree(scripts) { return rootHashFromTree(script); } script.version = script.version || LEAF_VERSION_TAPSCRIPT; - if ((script.version & 1) !== 0) throw new Error('Invalid script version'); // todo typedef error - // todo: if (script.output)scheck is bytes - const scriptOutput = buffer_1.Buffer.from(script.output, 'hex'); - return bcrypto.taggedHash( - TAP_LEAF_TAG, - buffer_1.Buffer.concat([ - buffer_1.Buffer.from([script.version]), - serializeScript(scriptOutput), - ]), - ); + if ((script.version & 1) !== 0) + throw new TypeError('Invalid script version'); + return leafHash(script.output, script.version); } // todo: this is a binary tree, use zero an one index const half = Math.trunc(scripts.length / 2); diff --git a/src/types.d.ts b/src/types.d.ts index 7f58a0a87..b33872800 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -18,8 +18,10 @@ export interface TweakedPublicKey { isOdd: boolean; x: Buffer; } -export declare const TaprootLeaf: any; -export declare const TaprootNode: any; +export interface TaprootLeaf { + output: Buffer; + version?: number; +} 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 ff1786256..a8acbef86 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.TaprootNode = exports.TaprootLeaf = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.isPoint = exports.GROUP_ORDER = exports.EC_P = exports.ZERO32 = 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.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.isPoint = exports.GROUP_ORDER = exports.EC_P = exports.ZERO32 = exports.typeforce = void 0; const buffer_1 = require('buffer'); exports.typeforce = require('typeforce'); exports.ZERO32 = buffer_1.Buffer.alloc(32, 0); @@ -72,17 +72,6 @@ exports.Network = exports.typeforce.compile({ scriptHash: exports.typeforce.UInt8, wif: exports.typeforce.UInt8, }); -exports.TaprootLeaf = exports.typeforce.compile({ - output: exports.typeforce.BufferN(34), - version: exports.typeforce.maybe(exports.typeforce.UInt8), // todo: recheck -}); -// / todo: revisit -exports.TaprootNode = exports.typeforce.arrayOf( - exports.typeforce.oneOf( - exports.TaprootLeaf, - exports.typeforce.arrayOf(exports.TaprootLeaf), - ), -); exports.Buffer256bit = exports.typeforce.BufferN(32); exports.Hash160bit = exports.typeforce.BufferN(20); exports.Hash256bit = exports.typeforce.BufferN(32); diff --git a/test/fixtures/p2tr.json b/test/fixtures/p2tr.json index 24cb6472f..cff1fe28f 100644 --- a/test/fixtures/p2tr.json +++ b/test/fixtures/p2tr.json @@ -354,6 +354,182 @@ "input": null, "witness": null } + }, + { + "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", + "scriptsTree": [ + { + "output": "d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8 OP_CHECKSIG", + "version": 192 + } + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3", + "pubkey": "147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3", + "address": "bc1pz37fc4cn9ah8anwm4xqqhvxygjf9rjf2resrw8h8w4tmvcs0863sa2e586", + "hash": "5b75adecf53548f3ec6ad7d78383bf84cc57b55a3127c72b9a2481752dd88b21", + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 3", + "arguments": { + "internalPubkey": "93478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820", + "scriptsTree": [ + { + "output": "b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007 OP_CHECKSIG", + "version": 192 + } + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e", + "pubkey": "e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e", + "address": "bc1punvppl2stp38f7kwv2u2spltjuvuaayuqsthe34hd2dyy5w4g58qqfuag5", + "hash": "c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b", + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 4", + "arguments": { + "internalPubkey": "ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf3786592", + "scriptsTree": [ + { + "output": "387671353e273264c495656e27e39ba899ea8fee3bb69fb2a680e22093447d48 OP_CHECKSIG", + "version": 192 + }, + { + "output": "424950333431", + "version": 152 + } + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 0f63ca2c7639b9bb4be0465cc0aa3ee78a0761ba5f5f7d6ff8eab340f09da561", + "pubkey": "0f63ca2c7639b9bb4be0465cc0aa3ee78a0761ba5f5f7d6ff8eab340f09da561", + "address": "bc1ppa3u5trk8xumkjlqgewvp237u79qwcd6ta0h6mlca2e5puya54ssw9zq0y", + "hash": "f3004d6c183e038105d436db1424f321613366cbb7b05939bf05d763a9ebb962", + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 5", + "arguments": { + "internalPubkey": "f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd8", + "scriptsTree": [ + { + "output": "44b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fd OP_CHECKSIG", + "version": 192 + }, + { + "output": "546170726f6f74", + "version": 82 + } + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 053690babeabbb7850c32eead0acf8df990ced79f7a31e358fabf2658b4bc587", + "pubkey": "053690babeabbb7850c32eead0acf8df990ced79f7a31e358fabf2658b4bc587", + "address": "bc1pq5mfpw474wahs5xr9m4dpt8cm7vsemte7733udv040extz6tckrs29g04c", + "hash": "d9c2c32808b41c0301d876d49c0af72e1d98e84b99ca9b4bb67fea1a7424b755", + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 6", + "arguments": { + "internalPubkey": "e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f", + "scriptsTree": [ + { + "output": "72ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69 OP_CHECKSIG", + "version": 192 + }, + [ + { + "output": "2352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8 OP_CHECKSIG", + "version": 192 + }, + { + "output": "7337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186a OP_CHECKSIG", + "version": 192 + } + ] + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 91b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605", + "pubkey": "91b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605", + "address": "bc1pjxmy65eywgafs5tsunw95ruycpqcqnev6ynxp7jaasylcgtcxczs6n332e", + "hash": "ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2", + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 7", + "arguments": { + "internalPubkey": "55adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d", + "scriptsTree": [ + { + "output": "71981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2 OP_CHECKSIG", + "version": 192 + }, + [ + { + "output": "d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748 OP_CHECKSIG", + "version": 192 + }, + { + "output": "c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4c OP_CHECKSIG", + "version": 192 + } + ] + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 75169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + "pubkey": "75169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + "address": "bc1pw5tf7sqp4f50zka7629jrr036znzew70zxyvvej3zrpf8jg8hqcssyuewe", + "hash": "2f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def", + "signature": null, + "input": null + } } ], "invalid": [ diff --git a/ts_src/payments/index.ts b/ts_src/payments/index.ts index f6223a3d0..bcf09e2a6 100644 --- a/ts_src/payments/index.ts +++ b/ts_src/payments/index.ts @@ -1,4 +1,5 @@ import { Network } from '../networks'; +import { TaprootLeaf } from '../types'; import { p2data as embed } from './embed'; import { p2ms } from './p2ms'; import { p2pk } from './p2pk'; @@ -23,8 +24,9 @@ export interface Payment { signature?: Buffer; address?: string; // taproot: betch32m hash?: Buffer; // taproot: MAST root - redeem?: Payment; // taproot: when script path spending is used spending + redeem?: Payment; scriptsTree?: any; // todo: solve + scriptLeaf?: TaprootLeaf; witness?: Buffer[]; } diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index ccbf4790d..eb6d7a443 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -2,7 +2,7 @@ import { bitcoin as BITCOIN_NETWORK } from '../networks'; import * as bscript from '../script'; import { typeforce as typef } from '../types'; import { - rootHashFromTree, + rootHashFromTree, rootHashFromPath, leafHash, tweakKey, @@ -42,7 +42,12 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { pubkey: typef.maybe(typef.BufferN(32)), signature: typef.maybe(bscript.isCanonicalScriptSignature), witness: typef.maybe(typef.arrayOf(typef.Buffer)), + // scriptsTree: typef.maybe(typef.TaprootNode), // use merkel.isMast ? + scriptLeaf: typef.maybe({ + version: typef.maybe(typef.Number), + output: typef.maybe(typef.Buffer), + }), }, a, ); @@ -98,6 +103,10 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { if (!o.pubkey) return; return bscript.compile([OPS.OP_1, o.pubkey]); }); + lazy.prop(o, 'scriptLeaf', () => { + if (!a.scriptLeaf) return a.scriptLeaf; + + }); lazy.prop(o, 'pubkey', () => { if (a.pubkey) return a.pubkey; if (a.output) return a.output.slice(2); @@ -118,7 +127,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { return a.witness[0]; }); lazy.prop(o, 'input', () => { - // todo: not sure + // todo }); lazy.prop(o, 'witness', () => { if (a.witness) return a.witness; @@ -191,8 +200,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { const controlBlock = witness[witness.length - 1]; if (controlBlock.length < 33) throw new TypeError( - `The control-block length is too small. Got ${ - controlBlock.length + `The control-block length is too small. Got ${controlBlock.length }, expected min 33.`, ); diff --git a/ts_src/taproot.ts b/ts_src/taproot.ts index 36213b757..0df09e833 100644 --- a/ts_src/taproot.ts +++ b/ts_src/taproot.ts @@ -4,7 +4,7 @@ const BN = require('bn.js'); import * as bcrypto from './crypto'; // todo: use varuint-bitcoin?? import * as varuint from 'bip174/src/lib/converter/varint'; -import { TweakedPublicKey, ZERO32, EC_P, GROUP_ORDER } from './types'; +import { TweakedPublicKey, TaprootLeaf, ZERO32, EC_P, GROUP_ORDER } from './types'; // todo: !!!Temp, to be replaced. Only works because bip32 has it as dependecy. Linting will fail. const ecc = require('tiny-secp256k1'); @@ -65,7 +65,7 @@ export function tweakKey( if (tweakHash.compare(GROUP_ORDER) >= 0) { // todo: add test for this case - throw new Error('Tweak value over the SECP256K1 Order'); + throw new TypeError('Tweak value over the SECP256K1 Order'); } const P = liftX(pubKey); @@ -79,15 +79,14 @@ export function tweakKey( } export function leafHash(script: Buffer, version: number): Buffer { - return NBuffer.concat([NBuffer.from([version]), serializeScript(script)]); + return bcrypto.taggedHash(TAP_LEAF_TAG, NBuffer.concat([NBuffer.from([version]), serializeScript(script)])); } export function rootHashFromPath(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer { - const k = []; + const k = [tapLeafMsg]; const e = []; const m = (controlBlock.length - 33) / 32; - k[0] = bcrypto.taggedHash(TAP_LEAF_TAG, tapLeafMsg); for (let j = 0; j < m; j++) { e[j] = controlBlock.slice(33 + 32 * j, 65 + 32 * j); @@ -107,24 +106,16 @@ export function rootHashFromPath(controlBlock: Buffer, tapLeafMsg: Buffer): Buff return k[m]; } -// todo: solve any[] -export function rootHashFromTree(scripts: any): Buffer { +export function rootHashFromTree(scripts: TaprootLeaf[]): Buffer { if (scripts.length === 1) { const script = scripts[0]; if (Array.isArray(script)) { return rootHashFromTree(script); } script.version = script.version || LEAF_VERSION_TAPSCRIPT; - if ((script.version & 1) !== 0) throw new Error('Invalid script version'); // todo typedef error - // todo: if (script.output)scheck is bytes - const scriptOutput = NBuffer.from(script.output, 'hex'); - return bcrypto.taggedHash( - TAP_LEAF_TAG, - NBuffer.concat([ - NBuffer.from([script.version]), - serializeScript(scriptOutput), - ]), - ); + if ((script.version & 1) !== 0) throw new TypeError('Invalid script version'); + + return leafHash(script.output, script.version); } // todo: this is a binary tree, use zero an one index const half = Math.trunc(scripts.length / 2); diff --git a/ts_src/types.ts b/ts_src/types.ts index e04a05589..b22ab7261 100644 --- a/ts_src/types.ts +++ b/ts_src/types.ts @@ -76,15 +76,10 @@ export interface TweakedPublicKey { x: Buffer; } -export const TaprootLeaf = typeforce.compile({ - output: typeforce.BufferN(34), - version: typeforce.maybe(typeforce.UInt8), // todo: recheck -}); - -// / todo: revisit -export const TaprootNode = typeforce.arrayOf( - typeforce.oneOf(TaprootLeaf, typeforce.arrayOf(TaprootLeaf)), -); +export interface TaprootLeaf { + output: Buffer; + version?: number; +} export const Buffer256bit = typeforce.BufferN(32); export const Hash160bit = typeforce.BufferN(20); From f7d01b8c54e0aba55351bfbc5e780e5f471a6446 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 3 Nov 2021 16:10:00 +0200 Subject: [PATCH 24/53] refactor: extract `tapBranchHash()` rename `leafHash()` to `tapBranchHash()` --- src/payments/p2tr.js | 8 ++++---- src/taproot.d.ts | 6 +----- src/taproot.js | 41 ++++++++++++++++++----------------------- ts_src/payments/p2tr.ts | 10 +++++----- ts_src/taproot.ts | 30 +++++++++++++----------------- 5 files changed, 41 insertions(+), 54 deletions(-) diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index 1d66fb5f7..17b8b404a 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -82,8 +82,8 @@ function p2tr(a, opts) { const controlBlock = w[w.length - 1]; const leafVersion = controlBlock[0] & 0b11111110; const script = w[w.length - 2]; - const tapLeafHash = (0, taproot_1.leafHash)(script, leafVersion); - return (0, taproot_1.rootHashFromPath)(controlBlock, tapLeafHash); + const leafHash = (0, taproot_1.tapLeafHash)(script, leafVersion); + return (0, taproot_1.rootHashFromPath)(controlBlock, leafHash); } return null; }); @@ -200,8 +200,8 @@ function p2tr(a, opts) { throw new TypeError('Invalid internalPubkey for p2tr witness'); const leafVersion = controlBlock[0] & 0b11111110; const script = witness[witness.length - 2]; - const tapLeafHash = (0, taproot_1.leafHash)(script, leafVersion); - const hash = (0, taproot_1.rootHashFromPath)(controlBlock, tapLeafHash); + const leafHash = (0, taproot_1.tapLeafHash)(script, leafVersion); + const hash = (0, taproot_1.rootHashFromPath)(controlBlock, leafHash); const outputKey = (0, taproot_1.tweakKey)(internalPubkey, hash); if (!outputKey) // todo: needs test data diff --git a/src/taproot.d.ts b/src/taproot.d.ts index 39f333958..a552b95b0 100644 --- a/src/taproot.d.ts +++ b/src/taproot.d.ts @@ -2,10 +2,6 @@ import { TweakedPublicKey, TaprootLeaf } from './types'; export declare function liftX(buffer: Buffer): Buffer | null; export declare function tweakKey(pubKey: Buffer, h: Buffer | undefined): TweakedPublicKey | null; -export declare function leafHash(script: Buffer, version: number): Buffer; export declare function rootHashFromPath(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer; -export interface HashTree { - rootHash: Buffer; - scritptPath?: Buffer; -} export declare function rootHashFromTree(scripts: TaprootLeaf[]): Buffer; +export declare function tapLeafHash(script: Buffer, version: number): Buffer; diff --git a/src/taproot.js b/src/taproot.js index e749d237e..7cc6ab850 100644 --- a/src/taproot.js +++ b/src/taproot.js @@ -1,6 +1,6 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -exports.rootHashFromTree = exports.rootHashFromPath = exports.leafHash = exports.tweakKey = exports.liftX = void 0; +exports.tapLeafHash = exports.rootHashFromTree = exports.rootHashFromPath = exports.tweakKey = exports.liftX = void 0; const buffer_1 = require('buffer'); const BN = require('bn.js'); const bcrypto = require('./crypto'); @@ -63,16 +63,6 @@ function tweakKey(pubKey, h) { }; } exports.tweakKey = tweakKey; -function leafHash(script, version) { - return bcrypto.taggedHash( - TAP_LEAF_TAG, - buffer_1.Buffer.concat([ - buffer_1.Buffer.from([version]), - serializeScript(script), - ]), - ); -} -exports.leafHash = leafHash; function rootHashFromPath(controlBlock, tapLeafMsg) { const k = [tapLeafMsg]; const e = []; @@ -80,15 +70,9 @@ function rootHashFromPath(controlBlock, tapLeafMsg) { for (let j = 0; j < m; j++) { e[j] = controlBlock.slice(33 + 32 * j, 65 + 32 * j); if (k[j].compare(e[j]) < 0) { - k[j + 1] = bcrypto.taggedHash( - TAP_BRANCH_TAG, - buffer_1.Buffer.concat([k[j], e[j]]), - ); + k[j + 1] = tapBranchHash(k[j], e[j]); } else { - k[j + 1] = bcrypto.taggedHash( - TAP_BRANCH_TAG, - buffer_1.Buffer.concat([e[j], k[j]]), - ); + k[j + 1] = tapBranchHash(e[j], k[j]); } } return k[m]; @@ -103,7 +87,7 @@ function rootHashFromTree(scripts) { script.version = script.version || LEAF_VERSION_TAPSCRIPT; if ((script.version & 1) !== 0) throw new TypeError('Invalid script version'); - return leafHash(script.output, script.version); + return tapLeafHash(script.output, script.version); } // todo: this is a binary tree, use zero an one index const half = Math.trunc(scripts.length / 2); @@ -111,12 +95,23 @@ function rootHashFromTree(scripts) { let rightHash = rootHashFromTree(scripts.slice(half)); if (leftHash.compare(rightHash) === 1) [leftHash, rightHash] = [rightHash, leftHash]; + return tapBranchHash(leftHash, rightHash); +} +exports.rootHashFromTree = rootHashFromTree; +// todo: rename to tapLeafHash +function tapLeafHash(script, version) { return bcrypto.taggedHash( - TAP_BRANCH_TAG, - buffer_1.Buffer.concat([leftHash, rightHash]), + TAP_LEAF_TAG, + buffer_1.Buffer.concat([ + buffer_1.Buffer.from([version]), + serializeScript(script), + ]), ); } -exports.rootHashFromTree = rootHashFromTree; +exports.tapLeafHash = tapLeafHash; +function tapBranchHash(a, b) { + return bcrypto.taggedHash(TAP_BRANCH_TAG, buffer_1.Buffer.concat([a, b])); +} function serializeScript(s) { const varintLen = varuint.encodingLength(s.length); const buffer = buffer_1.Buffer.allocUnsafe(varintLen); // better diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index eb6d7a443..8a79c10f7 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -4,7 +4,7 @@ import { typeforce as typef } from '../types'; import { rootHashFromTree, rootHashFromPath, - leafHash, + tapLeafHash, tweakKey, liftX, } from '../taproot'; @@ -94,8 +94,8 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { const controlBlock = w[w.length - 1]; const leafVersion = controlBlock[0] & 0b11111110; const script = w[w.length - 2]; - const tapLeafHash = leafHash(script, leafVersion); - return rootHashFromPath(controlBlock, tapLeafHash); + const leafHash = tapLeafHash(script, leafVersion); + return rootHashFromPath(controlBlock, leafHash); } return null; }); @@ -226,8 +226,8 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { const leafVersion = controlBlock[0] & 0b11111110; const script = witness[witness.length - 2]; - const tapLeafHash = leafHash(script, leafVersion); - const hash = rootHashFromPath(controlBlock, tapLeafHash); + const leafHash = tapLeafHash(script, leafVersion); + const hash = rootHashFromPath(controlBlock, leafHash); const outputKey = tweakKey(internalPubkey, hash); if (!outputKey) diff --git a/ts_src/taproot.ts b/ts_src/taproot.ts index 0df09e833..6c2477895 100644 --- a/ts_src/taproot.ts +++ b/ts_src/taproot.ts @@ -78,10 +78,6 @@ export function tweakKey( }; } -export function leafHash(script: Buffer, version: number): Buffer { - return bcrypto.taggedHash(TAP_LEAF_TAG, NBuffer.concat([NBuffer.from([version]), serializeScript(script)])); -} - export function rootHashFromPath(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer { const k = [tapLeafMsg]; const e = []; @@ -91,15 +87,9 @@ export function rootHashFromPath(controlBlock: Buffer, tapLeafMsg: Buffer): Buff for (let j = 0; j < m; j++) { e[j] = controlBlock.slice(33 + 32 * j, 65 + 32 * j); if (k[j].compare(e[j]) < 0) { - k[j + 1] = bcrypto.taggedHash( - TAP_BRANCH_TAG, - NBuffer.concat([k[j], e[j]]), - ); + k[j + 1] = tapBranchHash(k[j], e[j]); } else { - k[j + 1] = bcrypto.taggedHash( - TAP_BRANCH_TAG, - NBuffer.concat([e[j], k[j]]), - ); + k[j + 1] = tapBranchHash(e[j], k[j]); } } @@ -115,7 +105,7 @@ export function rootHashFromTree(scripts: TaprootLeaf[]): Buffer { script.version = script.version || LEAF_VERSION_TAPSCRIPT; if ((script.version & 1) !== 0) throw new TypeError('Invalid script version'); - return leafHash(script.output, script.version); + return tapLeafHash(script.output, script.version); } // todo: this is a binary tree, use zero an one index const half = Math.trunc(scripts.length / 2); @@ -124,10 +114,16 @@ export function rootHashFromTree(scripts: TaprootLeaf[]): Buffer { if (leftHash.compare(rightHash) === 1) [leftHash, rightHash] = [rightHash, leftHash]; - return bcrypto.taggedHash( - TAP_BRANCH_TAG, - NBuffer.concat([leftHash, rightHash]), - ); + return tapBranchHash(leftHash, rightHash); +} + +// todo: rename to tapLeafHash +export function tapLeafHash(script: Buffer, version: number): Buffer { + return bcrypto.taggedHash(TAP_LEAF_TAG, NBuffer.concat([NBuffer.from([version]), serializeScript(script)])); +} + +function tapBranchHash(a: Buffer, b: Buffer): Buffer { + return bcrypto.taggedHash(TAP_BRANCH_TAG, NBuffer.concat([a, b]), ); } function serializeScript(s: Buffer): Buffer { From a3550c1e6551d5441b42e389f810429492ace362 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 3 Nov 2021 19:15:08 +0200 Subject: [PATCH 25/53] feat: build control-block as part of witness; update tests --- src/payments/p2tr.js | 36 +++-- src/taproot.d.ts | 10 +- src/taproot.js | 45 +++++-- src/types.d.ts | 2 +- test/fixtures/p2tr.json | 284 +++++++++++++++++++++++++++++++++++++++- test/payments.utils.ts | 5 + ts_src/payments/p2tr.ts | 30 +++-- ts_src/taproot.ts | 58 ++++++-- ts_src/types.ts | 2 +- 9 files changed, 426 insertions(+), 46 deletions(-) diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index 17b8b404a..8e48aea87 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -1,6 +1,7 @@ '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'); @@ -52,7 +53,7 @@ function p2tr(a, opts) { return { version, prefix: result.prefix, - data: Buffer.from(data), + data: buffer_1.Buffer.from(data), }; }); const _witness = lazy.value(() => { @@ -76,7 +77,7 @@ function p2tr(a, opts) { }); lazy.prop(o, 'hash', () => { if (a.hash) return a.hash; - if (a.scriptsTree) return (0, taproot_1.rootHashFromTree)(a.scriptsTree); + if (a.scriptsTree) return (0, taproot_1.toHashTree)(a.scriptsTree).hash; const w = _witness(); if (w && w.length > 1) { const controlBlock = w[w.length - 1]; @@ -118,12 +119,32 @@ function p2tr(a, opts) { }); lazy.prop(o, 'witness', () => { if (a.witness) return a.witness; - if (!a.signature) return; - return [a.signature]; + if (a.scriptsTree && a.scriptLeaf && a.internalPubkey) { + const hashTree = (0, taproot_1.toHashTree)(a.scriptsTree); + const leafHash = (0, taproot_1.tapLeafHash)( + a.scriptLeaf.output, + a.scriptLeaf.version, + ); + const path = (0, taproot_1.findScriptPath)(hashTree, leafHash); + const outputKey = (0, taproot_1.tweakKey)( + a.internalPubkey, + hashTree.hash, + ); + if (!outputKey) return; + const version = a.scriptLeaf.version || 0xc0; + const controlBock = buffer_1.Buffer.concat( + [ + buffer_1.Buffer.from([version | outputKey.parity]), + a.internalPubkey, + ].concat(path.reverse()), + ); + return [a.scriptLeaf.output, controlBock]; + } + if (a.signature) return [a.signature]; }); // extended validation if (opts.validate) { - let pubkey = Buffer.from([]); + let pubkey = buffer_1.Buffer.from([]); if (a.address) { if (network && network.bech32 !== _address().prefix) throw new TypeError('Invalid prefix or Network mismatch'); @@ -162,7 +183,7 @@ function p2tr(a, opts) { throw new TypeError('Invalid pubkey for p2tr'); } if (a.hash && a.scriptsTree) { - const hash = (0, taproot_1.rootHashFromTree)(a.scriptsTree); + const hash = (0, taproot_1.toHashTree)(a.scriptsTree).hash; if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch'); } const witness = _witness(); @@ -208,8 +229,7 @@ function p2tr(a, opts) { throw new TypeError('Invalid outputKey for p2tr witness'); if (pubkey.length && !pubkey.equals(outputKey.x)) throw new TypeError('Pubkey mismatch for p2tr witness'); - const controlBlockOddParity = (controlBlock[0] & 1) === 1; - if (outputKey.isOdd !== controlBlockOddParity) + if (outputKey.parity !== (controlBlock[0] & 1)) throw new Error('Incorrect parity'); } } diff --git a/src/taproot.d.ts b/src/taproot.d.ts index a552b95b0..c55f10d72 100644 --- a/src/taproot.d.ts +++ b/src/taproot.d.ts @@ -3,5 +3,11 @@ import { TweakedPublicKey, TaprootLeaf } from './types'; export declare function liftX(buffer: Buffer): Buffer | null; export declare function tweakKey(pubKey: Buffer, h: Buffer | undefined): TweakedPublicKey | null; export declare function rootHashFromPath(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer; -export declare function rootHashFromTree(scripts: TaprootLeaf[]): Buffer; -export declare function tapLeafHash(script: Buffer, version: number): Buffer; +export interface HashTree { + hash: Buffer; + left?: HashTree; + right?: HashTree; +} +export declare function toHashTree(scripts: TaprootLeaf[]): HashTree; +export declare function findScriptPath(node: HashTree, hash: Buffer): Buffer[]; +export declare function tapLeafHash(script: Buffer, version?: number): Buffer; diff --git a/src/taproot.js b/src/taproot.js index 7cc6ab850..d11e77799 100644 --- a/src/taproot.js +++ b/src/taproot.js @@ -1,6 +1,6 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -exports.tapLeafHash = exports.rootHashFromTree = exports.rootHashFromPath = exports.tweakKey = exports.liftX = void 0; +exports.tapLeafHash = exports.findScriptPath = exports.toHashTree = exports.rootHashFromPath = exports.tweakKey = exports.liftX = void 0; const buffer_1 = require('buffer'); const BN = require('bn.js'); const bcrypto = require('./crypto'); @@ -58,7 +58,7 @@ function tweakKey(pubKey, h) { if (P === null) return null; const Q = pointAddScalar(P, tweakHash); return { - isOdd: Q[64] % 2 === 1, + parity: Q[64] % 2, x: Q.slice(1, 33), }; } @@ -78,28 +78,53 @@ function rootHashFromPath(controlBlock, tapLeafMsg) { return k[m]; } exports.rootHashFromPath = rootHashFromPath; -function rootHashFromTree(scripts) { +function toHashTree(scripts) { if (scripts.length === 1) { const script = scripts[0]; if (Array.isArray(script)) { - return rootHashFromTree(script); + return toHashTree(script); } script.version = script.version || LEAF_VERSION_TAPSCRIPT; if ((script.version & 1) !== 0) throw new TypeError('Invalid script version'); - return tapLeafHash(script.output, script.version); + return { + hash: tapLeafHash(script.output, script.version), + }; } // todo: this is a binary tree, use zero an one index const half = Math.trunc(scripts.length / 2); - let leftHash = rootHashFromTree(scripts.slice(0, half)); - let rightHash = rootHashFromTree(scripts.slice(half)); + const left = toHashTree(scripts.slice(0, half)); + const right = toHashTree(scripts.slice(half)); + let leftHash = left.hash; + let rightHash = right.hash; if (leftHash.compare(rightHash) === 1) [leftHash, rightHash] = [rightHash, leftHash]; - return tapBranchHash(leftHash, rightHash); + return { + hash: tapBranchHash(leftHash, rightHash), + left, + right, + }; +} +exports.toHashTree = toHashTree; +function findScriptPath(node, hash) { + if (node.left) { + if (node.left.hash.equals(hash)) return node.right ? [node.right.hash] : []; + const leftPath = findScriptPath(node.left, hash); + if (leftPath.length) + return node.right ? [node.right.hash].concat(leftPath) : leftPath; + } + if (node.right) { + if (node.right.hash.equals(hash)) return node.left ? [node.left.hash] : []; + const rightPath = findScriptPath(node.right, hash); + if (rightPath.length) { + } + return node.left ? [node.left.hash].concat(rightPath) : rightPath; + } + return []; } -exports.rootHashFromTree = rootHashFromTree; -// todo: rename to tapLeafHash +exports.findScriptPath = findScriptPath; function tapLeafHash(script, version) { + version = version || LEAF_VERSION_TAPSCRIPT; return bcrypto.taggedHash( TAP_LEAF_TAG, buffer_1.Buffer.concat([ diff --git a/src/types.d.ts b/src/types.d.ts index b33872800..1bcb12426 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -15,7 +15,7 @@ export declare function Satoshi(value: number): boolean; export declare const ECPoint: any; export declare const Network: any; export interface TweakedPublicKey { - isOdd: boolean; + parity: number; x: Buffer; } export interface TaprootLeaf { diff --git a/test/fixtures/p2tr.json b/test/fixtures/p2tr.json index cff1fe28f..1ef6ed62d 100644 --- a/test/fixtures/p2tr.json +++ b/test/fixtures/p2tr.json @@ -375,6 +375,10 @@ "description": "BIP341 Test case 2", "arguments": { "internalPubkey": "187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27", + "scriptLeaf": { + "output": "d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8 OP_CHECKSIG", + "version": 192 + }, "scriptsTree": [ { "output": "d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8 OP_CHECKSIG", @@ -389,6 +393,10 @@ "pubkey": "147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3", "address": "bc1pz37fc4cn9ah8anwm4xqqhvxygjf9rjf2resrw8h8w4tmvcs0863sa2e586", "hash": "5b75adecf53548f3ec6ad7d78383bf84cc57b55a3127c72b9a2481752dd88b21", + "witness": [ + "20d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8ac", + "c1187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27" + ], "signature": null, "input": null } @@ -397,6 +405,10 @@ "description": "BIP341 Test case 3", "arguments": { "internalPubkey": "93478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820", + "scriptLeaf": { + "output": "b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007 OP_CHECKSIG", + "version": 192 + }, "scriptsTree": [ { "output": "b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007 OP_CHECKSIG", @@ -411,14 +423,56 @@ "pubkey": "e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e", "address": "bc1punvppl2stp38f7kwv2u2spltjuvuaayuqsthe34hd2dyy5w4g58qqfuag5", "hash": "c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b", + "witness": [ + "20b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007ac", + "c093478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820" + ], + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 4 - spend leaf 0", + "arguments": { + "internalPubkey": "ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf3786592", + "scriptLeaf": { + "output": "387671353e273264c495656e27e39ba899ea8fee3bb69fb2a680e22093447d48 OP_CHECKSIG", + "version": 192 + }, + "scriptsTree": [ + { + "output": "387671353e273264c495656e27e39ba899ea8fee3bb69fb2a680e22093447d48 OP_CHECKSIG", + "version": 192 + }, + { + "output": "424950333431", + "version": 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", + "description": "BIP341 Test case 4 - spend leaf 1", "arguments": { "internalPubkey": "ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf3786592", + "scriptLeaf": { + "output": "424950333431 OP_CHECKSIG", + "version": 152 + }, "scriptsTree": [ { "output": "387671353e273264c495656e27e39ba899ea8fee3bb69fb2a680e22093447d48 OP_CHECKSIG", @@ -437,14 +491,22 @@ "pubkey": "0f63ca2c7639b9bb4be0465cc0aa3ee78a0761ba5f5f7d6ff8eab340f09da561", "address": "bc1ppa3u5trk8xumkjlqgewvp237u79qwcd6ta0h6mlca2e5puya54ssw9zq0y", "hash": "f3004d6c183e038105d436db1424f321613366cbb7b05939bf05d763a9ebb962", + "witness": [ + "06424950333431ac", + "98ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf37865928ad69ec7cf41c2a4001fd1f738bf1e505ce2277acdcaa63fe4765192497f47a7" + ], "signature": null, "input": null } }, { - "description": "BIP341 Test case 5", + "description": "BIP341 Test case 5 - spend leaf 0", "arguments": { "internalPubkey": "f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd8", + "scriptLeaf": { + "output": "44b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fd OP_CHECKSIG", + "version": 192 + }, "scriptsTree": [ { "output": "44b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fd OP_CHECKSIG", @@ -463,14 +525,136 @@ "pubkey": "053690babeabbb7850c32eead0acf8df990ced79f7a31e358fabf2658b4bc587", "address": "bc1pq5mfpw474wahs5xr9m4dpt8cm7vsemte7733udv040extz6tckrs29g04c", "hash": "d9c2c32808b41c0301d876d49c0af72e1d98e84b99ca9b4bb67fea1a7424b755", + "witness": [ + "2044b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fdac", + "c1f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd8e44d5f8fa5892c8b6d4d09a08d36edd0b08636e30311302e2448ad8172fb3433" + ], + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 5 - spend leaf 1", + "arguments": { + "internalPubkey": "f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd8", + "scriptLeaf": { + "output": "546170726f6f74", + "version": 82 + }, + "scriptsTree": [ + { + "output": "44b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fd OP_CHECKSIG", + "version": 192 + }, + { + "output": "546170726f6f74", + "version": 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", + "scriptLeaf": { + "output": "72ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69 OP_CHECKSIG", + "version": 192 + }, + "scriptsTree": [ + { + "output": "72ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69 OP_CHECKSIG", + "version": 192 + }, + [ + { + "output": "2352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8 OP_CHECKSIG", + "version": 192 + }, + { + "output": "7337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186a OP_CHECKSIG", + "version": 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", + "scriptLeaf": { + "output": "2352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8 OP_CHECKSIG", + "version": 192 + }, + "scriptsTree": [ + { + "output": "72ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69 OP_CHECKSIG", + "version": 192 + }, + [ + { + "output": "2352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8 OP_CHECKSIG", + "version": 192 + }, + { + "output": "7337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186a OP_CHECKSIG", + "version": 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", + "description": "BIP341 Test case 6 - spend leaf 2", "arguments": { "internalPubkey": "e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f", + "scriptLeaf": { + "output": "7337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186a OP_CHECKSIG", + "version": 192 + }, "scriptsTree": [ { "output": "72ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69 OP_CHECKSIG", @@ -495,14 +679,22 @@ "pubkey": "91b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605", "address": "bc1pjxmy65eywgafs5tsunw95ruycpqcqnev6ynxp7jaasylcgtcxczs6n332e", "hash": "ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2", + "witness": [ + "207337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186aac", + "c0e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6fba982a91d4fc552163cb1c0da03676102d5b7a014304c01f0c77b2b8e888de1c2645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817" + ], "signature": null, "input": null } }, { - "description": "BIP341 Test case 7", + "description": "BIP341 Test case 7 - spend leaf 0", "arguments": { "internalPubkey": "55adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d", + "scriptLeaf": { + "output": "71981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2 OP_CHECKSIG", + "version": 192 + }, "scriptsTree": [ { "output": "71981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2 OP_CHECKSIG", @@ -527,6 +719,90 @@ "pubkey": "75169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", "address": "bc1pw5tf7sqp4f50zka7629jrr036znzew70zxyvvej3zrpf8jg8hqcssyuewe", "hash": "2f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def", + "witness": [ + "2071981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2ac", + "c155adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d3cd369a528b326bc9d2133cbd2ac21451acb31681a410434672c8e34fe757e91" + ], + "signature": null, + "input": null + } + }, + { + "description": "BIP341 Test case 7 - spend leaf 1", + "arguments": { + "internalPubkey": "55adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d", + "scriptLeaf": { + "output": "d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748 OP_CHECKSIG", + "version": 192 + }, + "scriptsTree": [ + { + "output": "71981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2 OP_CHECKSIG", + "version": 192 + }, + [ + { + "output": "d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748 OP_CHECKSIG", + "version": 192 + }, + { + "output": "c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4c OP_CHECKSIG", + "version": 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", + "scriptLeaf": { + "output": "c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4c OP_CHECKSIG", + "version": 192 + }, + "scriptsTree": [ + { + "output": "71981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2 OP_CHECKSIG", + "version": 192 + }, + [ + { + "output": "d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748 OP_CHECKSIG", + "version": 192 + }, + { + "output": "c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4c OP_CHECKSIG", + "version": 192 + } + ] + ] + }, + "options": {}, + "expected": { + "name": "p2tr", + "output": "OP_1 75169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + "pubkey": "75169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + "address": "bc1pw5tf7sqp4f50zka7629jrr036znzew70zxyvvej3zrpf8jg8hqcssyuewe", + "hash": "2f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def", + "witness": [ + "20c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4cac", + "c155adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d" + ], "signature": null, "input": null } diff --git a/test/payments.utils.ts b/test/payments.utils.ts index 1aa401830..d4aee8374 100644 --- a/test/payments.utils.ts +++ b/test/payments.utils.ts @@ -153,6 +153,11 @@ export function preform(x: any): any { if (x.redeem.network) x.redeem.network = (BNETWORKS as any)[x.redeem.network]; } + if (x.scriptLeaf) { + x.scriptLeaf = Object.assign({}, x.scriptLeaf); + if (typeof x.scriptLeaf.output === 'string') + x.scriptLeaf.output = asmToBuffer(x.scriptLeaf.output); + } if (x.scriptsTree) x.scriptsTree = convertScriptsTree(x.scriptsTree); return x; } diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index 8a79c10f7..476ffadbc 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -1,9 +1,11 @@ +import { Buffer as NBuffer } from 'buffer'; import { bitcoin as BITCOIN_NETWORK } from '../networks'; import * as bscript from '../script'; import { typeforce as typef } from '../types'; import { - rootHashFromTree, + toHashTree, rootHashFromPath, + findScriptPath, tapLeafHash, tweakKey, liftX, @@ -59,7 +61,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { return { version, prefix: result.prefix, - data: Buffer.from(data), + data: NBuffer.from(data), }; }); @@ -88,7 +90,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { lazy.prop(o, 'hash', () => { if (a.hash) return a.hash; - if (a.scriptsTree) return rootHashFromTree(a.scriptsTree); + if (a.scriptsTree) return toHashTree(a.scriptsTree).hash; const w = _witness(); if (w && w.length > 1) { const controlBlock = w[w.length - 1]; @@ -105,7 +107,6 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { }); lazy.prop(o, 'scriptLeaf', () => { if (!a.scriptLeaf) return a.scriptLeaf; - }); lazy.prop(o, 'pubkey', () => { if (a.pubkey) return a.pubkey; @@ -131,13 +132,23 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { }); lazy.prop(o, 'witness', () => { if (a.witness) return a.witness; - if (!a.signature) return; - return [a.signature]; + if (a.scriptsTree && a.scriptLeaf && a.internalPubkey) { + // todo: optimize/cache + const hashTree = toHashTree(a.scriptsTree) + const leafHash = tapLeafHash(a.scriptLeaf.output, a.scriptLeaf.version) + const path = findScriptPath(hashTree, leafHash) + const outputKey = tweakKey(a.internalPubkey, hashTree.hash); + if (!outputKey) return + const version = a.scriptLeaf.version || 0xc0 + const controlBock = NBuffer.concat([NBuffer.from([version | outputKey.parity]), a.internalPubkey].concat(path.reverse())) + return [a.scriptLeaf.output, controlBock] + } + if (a.signature) return [a.signature]; }); // extended validation if (opts.validate) { - let pubkey: Buffer = Buffer.from([]); + let pubkey: Buffer = NBuffer.from([]); if (a.address) { if (network && network.bech32 !== _address().prefix) throw new TypeError('Invalid prefix or Network mismatch'); @@ -181,7 +192,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { } if (a.hash && a.scriptsTree) { - const hash = rootHashFromTree(a.scriptsTree); + const hash = toHashTree(a.scriptsTree).hash; if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch'); } @@ -237,8 +248,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { if (pubkey.length && !pubkey.equals(outputKey.x)) throw new TypeError('Pubkey mismatch for p2tr witness'); - const controlBlockOddParity = (controlBlock[0] & 1) === 1; - if (outputKey.isOdd !== controlBlockOddParity) + if (outputKey.parity !== (controlBlock[0] & 1)) throw new Error('Incorrect parity'); } } diff --git a/ts_src/taproot.ts b/ts_src/taproot.ts index 6c2477895..e1bb718db 100644 --- a/ts_src/taproot.ts +++ b/ts_src/taproot.ts @@ -73,7 +73,7 @@ export function tweakKey( const Q = pointAddScalar(P, tweakHash); return { - isOdd: Q[64] % 2 === 1, + parity: Q[64] % 2, x: Q.slice(1, 33), }; } @@ -96,34 +96,72 @@ export function rootHashFromPath(controlBlock: Buffer, tapLeafMsg: Buffer): Buff return k[m]; } -export function rootHashFromTree(scripts: TaprootLeaf[]): Buffer { +export interface HashTree { + hash: Buffer + left?: HashTree + right?: HashTree +} + + +export function toHashTree(scripts: TaprootLeaf[]): HashTree { if (scripts.length === 1) { const script = scripts[0]; if (Array.isArray(script)) { - return rootHashFromTree(script); + return toHashTree(script); } script.version = script.version || LEAF_VERSION_TAPSCRIPT; if ((script.version & 1) !== 0) throw new TypeError('Invalid script version'); - return tapLeafHash(script.output, script.version); + return { + hash: tapLeafHash(script.output, script.version) + } + } // todo: this is a binary tree, use zero an one index const half = Math.trunc(scripts.length / 2); - let leftHash = rootHashFromTree(scripts.slice(0, half)); - let rightHash = rootHashFromTree(scripts.slice(half)); + const left = toHashTree(scripts.slice(0, half)); + const right = toHashTree(scripts.slice(half)); + + let leftHash = left.hash; + let rightHash = right.hash; if (leftHash.compare(rightHash) === 1) [leftHash, rightHash] = [rightHash, leftHash]; - return tapBranchHash(leftHash, rightHash); + return { + hash: tapBranchHash(leftHash, rightHash), + left, + right + } +} + +export function findScriptPath(node: HashTree, hash: Buffer): Buffer[] { + if (node.left) { + if (node.left.hash.equals(hash)) + return node.right ? [node.right.hash] : [] + const leftPath = findScriptPath(node.left, hash) + if (leftPath.length) + return node.right ? [node.right.hash].concat(leftPath) : leftPath + } + + if (node.right) { + if (node.right.hash.equals(hash)) + return node.left ? [node.left.hash] : [] + const rightPath = findScriptPath(node.right, hash) + if (rightPath.length) {} + return node.left ? [node.left.hash].concat(rightPath) : rightPath + } + + return [] + } -// todo: rename to tapLeafHash -export function tapLeafHash(script: Buffer, version: number): Buffer { +export function tapLeafHash(script: Buffer, version?: number): Buffer { + version = version || LEAF_VERSION_TAPSCRIPT return bcrypto.taggedHash(TAP_LEAF_TAG, NBuffer.concat([NBuffer.from([version]), serializeScript(script)])); } function tapBranchHash(a: Buffer, b: Buffer): Buffer { - return bcrypto.taggedHash(TAP_BRANCH_TAG, NBuffer.concat([a, b]), ); + return bcrypto.taggedHash(TAP_BRANCH_TAG, NBuffer.concat([a, b]),); } function serializeScript(s: Buffer): Buffer { diff --git a/ts_src/types.ts b/ts_src/types.ts index b22ab7261..7fd2452f1 100644 --- a/ts_src/types.ts +++ b/ts_src/types.ts @@ -72,7 +72,7 @@ export const Network = typeforce.compile({ }); export interface TweakedPublicKey { - isOdd: boolean; + parity: number; x: Buffer; } From b1fca660bebc76f708621600e240fd3b9596d04b Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 3 Nov 2021 19:28:22 +0200 Subject: [PATCH 26/53] chore: lint & format; fix: discovered bug in findScriptPath() after lint --- src/payments/p2tr.js | 1 + src/taproot.js | 5 ++-- test/fixtures/p2tr.json | 4 +-- ts_src/payments/p2tr.ts | 22 ++++++++------- ts_src/taproot.ts | 60 +++++++++++++++++++++++------------------ 5 files changed, 52 insertions(+), 40 deletions(-) diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index 8e48aea87..2de193f7c 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -120,6 +120,7 @@ function p2tr(a, opts) { lazy.prop(o, 'witness', () => { if (a.witness) return a.witness; if (a.scriptsTree && a.scriptLeaf && a.internalPubkey) { + // todo: optimize/cache const hashTree = (0, taproot_1.toHashTree)(a.scriptsTree); const leafHash = (0, taproot_1.tapLeafHash)( a.scriptLeaf.output, diff --git a/src/taproot.js b/src/taproot.js index d11e77799..220274d61 100644 --- a/src/taproot.js +++ b/src/taproot.js @@ -116,9 +116,8 @@ function findScriptPath(node, hash) { if (node.right) { if (node.right.hash.equals(hash)) return node.left ? [node.left.hash] : []; const rightPath = findScriptPath(node.right, hash); - if (rightPath.length) { - } - return node.left ? [node.left.hash].concat(rightPath) : rightPath; + if (rightPath.length) + return node.left ? [node.left.hash].concat(rightPath) : rightPath; } return []; } diff --git a/test/fixtures/p2tr.json b/test/fixtures/p2tr.json index 1ef6ed62d..da89f26f6 100644 --- a/test/fixtures/p2tr.json +++ b/test/fixtures/p2tr.json @@ -470,7 +470,7 @@ "arguments": { "internalPubkey": "ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf3786592", "scriptLeaf": { - "output": "424950333431 OP_CHECKSIG", + "output": "424950333431", "version": 152 }, "scriptsTree": [ @@ -492,7 +492,7 @@ "address": "bc1ppa3u5trk8xumkjlqgewvp237u79qwcd6ta0h6mlca2e5puya54ssw9zq0y", "hash": "f3004d6c183e038105d436db1424f321613366cbb7b05939bf05d763a9ebb962", "witness": [ - "06424950333431ac", + "06424950333431", "98ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf37865928ad69ec7cf41c2a4001fd1f738bf1e505ce2277acdcaa63fe4765192497f47a7" ], "signature": null, diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index 476ffadbc..0dff7713b 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -44,7 +44,6 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { pubkey: typef.maybe(typef.BufferN(32)), signature: typef.maybe(bscript.isCanonicalScriptSignature), witness: typef.maybe(typef.arrayOf(typef.Buffer)), - // scriptsTree: typef.maybe(typef.TaprootNode), // use merkel.isMast ? scriptLeaf: typef.maybe({ version: typef.maybe(typef.Number), @@ -134,14 +133,18 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { if (a.witness) return a.witness; if (a.scriptsTree && a.scriptLeaf && a.internalPubkey) { // todo: optimize/cache - const hashTree = toHashTree(a.scriptsTree) - const leafHash = tapLeafHash(a.scriptLeaf.output, a.scriptLeaf.version) - const path = findScriptPath(hashTree, leafHash) + const hashTree = toHashTree(a.scriptsTree); + const leafHash = tapLeafHash(a.scriptLeaf.output, a.scriptLeaf.version); + const path = findScriptPath(hashTree, leafHash); const outputKey = tweakKey(a.internalPubkey, hashTree.hash); - if (!outputKey) return - const version = a.scriptLeaf.version || 0xc0 - const controlBock = NBuffer.concat([NBuffer.from([version | outputKey.parity]), a.internalPubkey].concat(path.reverse())) - return [a.scriptLeaf.output, controlBock] + if (!outputKey) return; + const version = a.scriptLeaf.version || 0xc0; + const controlBock = NBuffer.concat( + [NBuffer.from([version | outputKey.parity]), a.internalPubkey].concat( + path.reverse(), + ), + ); + return [a.scriptLeaf.output, controlBock]; } if (a.signature) return [a.signature]; }); @@ -211,7 +214,8 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { const controlBlock = witness[witness.length - 1]; if (controlBlock.length < 33) throw new TypeError( - `The control-block length is too small. Got ${controlBlock.length + `The control-block length is too small. Got ${ + controlBlock.length }, expected min 33.`, ); diff --git a/ts_src/taproot.ts b/ts_src/taproot.ts index e1bb718db..d364670a9 100644 --- a/ts_src/taproot.ts +++ b/ts_src/taproot.ts @@ -4,7 +4,13 @@ const BN = require('bn.js'); import * as bcrypto from './crypto'; // todo: use varuint-bitcoin?? import * as varuint from 'bip174/src/lib/converter/varint'; -import { TweakedPublicKey, TaprootLeaf, ZERO32, EC_P, GROUP_ORDER } from './types'; +import { + TweakedPublicKey, + TaprootLeaf, + ZERO32, + EC_P, + GROUP_ORDER, +} from './types'; // todo: !!!Temp, to be replaced. Only works because bip32 has it as dependecy. Linting will fail. const ecc = require('tiny-secp256k1'); @@ -78,7 +84,10 @@ export function tweakKey( }; } -export function rootHashFromPath(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer { +export function rootHashFromPath( + controlBlock: Buffer, + tapLeafMsg: Buffer, +): Buffer { const k = [tapLeafMsg]; const e = []; @@ -97,12 +106,11 @@ export function rootHashFromPath(controlBlock: Buffer, tapLeafMsg: Buffer): Buff } export interface HashTree { - hash: Buffer - left?: HashTree - right?: HashTree + hash: Buffer; + left?: HashTree; + right?: HashTree; } - export function toHashTree(scripts: TaprootLeaf[]): HashTree { if (scripts.length === 1) { const script = scripts[0]; @@ -110,12 +118,12 @@ export function toHashTree(scripts: TaprootLeaf[]): HashTree { return toHashTree(script); } script.version = script.version || LEAF_VERSION_TAPSCRIPT; - if ((script.version & 1) !== 0) throw new TypeError('Invalid script version'); + if ((script.version & 1) !== 0) + throw new TypeError('Invalid script version'); return { - hash: tapLeafHash(script.output, script.version) - } - + hash: tapLeafHash(script.output, script.version), + }; } // todo: this is a binary tree, use zero an one index const half = Math.trunc(scripts.length / 2); @@ -130,38 +138,38 @@ export function toHashTree(scripts: TaprootLeaf[]): HashTree { return { hash: tapBranchHash(leftHash, rightHash), left, - right - } + right, + }; } export function findScriptPath(node: HashTree, hash: Buffer): Buffer[] { if (node.left) { - if (node.left.hash.equals(hash)) - return node.right ? [node.right.hash] : [] - const leftPath = findScriptPath(node.left, hash) + if (node.left.hash.equals(hash)) return node.right ? [node.right.hash] : []; + const leftPath = findScriptPath(node.left, hash); if (leftPath.length) - return node.right ? [node.right.hash].concat(leftPath) : leftPath + return node.right ? [node.right.hash].concat(leftPath) : leftPath; } if (node.right) { - if (node.right.hash.equals(hash)) - return node.left ? [node.left.hash] : [] - const rightPath = findScriptPath(node.right, hash) - if (rightPath.length) {} - return node.left ? [node.left.hash].concat(rightPath) : rightPath + if (node.right.hash.equals(hash)) return node.left ? [node.left.hash] : []; + const rightPath = findScriptPath(node.right, hash); + if (rightPath.length) + return node.left ? [node.left.hash].concat(rightPath) : rightPath; } - return [] - + return []; } export function tapLeafHash(script: Buffer, version?: number): Buffer { - version = version || LEAF_VERSION_TAPSCRIPT - return bcrypto.taggedHash(TAP_LEAF_TAG, NBuffer.concat([NBuffer.from([version]), serializeScript(script)])); + version = version || LEAF_VERSION_TAPSCRIPT; + return bcrypto.taggedHash( + TAP_LEAF_TAG, + NBuffer.concat([NBuffer.from([version]), serializeScript(script)]), + ); } function tapBranchHash(a: Buffer, b: Buffer): Buffer { - return bcrypto.taggedHash(TAP_BRANCH_TAG, NBuffer.concat([a, b]),); + return bcrypto.taggedHash(TAP_BRANCH_TAG, NBuffer.concat([a, b])); } function serializeScript(s: Buffer): Buffer { From ad2aec1906653d379aa6f2a83951afff93da4186 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 3 Nov 2021 20:00:42 +0200 Subject: [PATCH 27/53] chore: code clean-up; fix o.scriptLeaf (needs tests) --- src/payments/p2tr.js | 5 +---- ts_src/payments/index.ts | 10 +++++----- ts_src/payments/p2tr.ts | 5 +---- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index 2de193f7c..435d2f101 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -11,9 +11,6 @@ const bech32_1 = require('bech32'); const OPS = bscript.OPS; const TAPROOT_VERSION = 0x01; const ANNEX_PREFIX = 0x50; -// witness: {signature} -// input: <> -// output: OP_1 {pubKey} function p2tr(a, opts) { if ( !a.address && @@ -93,7 +90,7 @@ function p2tr(a, opts) { return bscript.compile([OPS.OP_1, o.pubkey]); }); lazy.prop(o, 'scriptLeaf', () => { - if (!a.scriptLeaf) return a.scriptLeaf; + if (a.scriptLeaf) return a.scriptLeaf; }); lazy.prop(o, 'pubkey', () => { if (a.pubkey) return a.pubkey; diff --git a/ts_src/payments/index.ts b/ts_src/payments/index.ts index bcf09e2a6..996f292e9 100644 --- a/ts_src/payments/index.ts +++ b/ts_src/payments/index.ts @@ -12,18 +12,18 @@ import { p2tr } from './p2tr'; export interface Payment { name?: string; network?: Network; - output?: Buffer; // the full scriptPubKey + output?: Buffer; data?: Buffer[]; m?: number; n?: number; pubkeys?: Buffer[]; input?: Buffer; signatures?: Buffer[]; - internalPubkey?: Buffer; // taproot: output key - pubkey?: Buffer; // taproot: output key + internalPubkey?: Buffer; + pubkey?: Buffer; signature?: Buffer; - address?: string; // taproot: betch32m - hash?: Buffer; // taproot: MAST root + address?: string; + hash?: Buffer; redeem?: Payment; scriptsTree?: any; // todo: solve scriptLeaf?: TaprootLeaf; diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index 0dff7713b..98ca8eb56 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -18,9 +18,6 @@ const OPS = bscript.OPS; const TAPROOT_VERSION = 0x01; const ANNEX_PREFIX = 0x50; -// witness: {signature} -// input: <> -// output: OP_1 {pubKey} export function p2tr(a: Payment, opts?: PaymentOpts): Payment { if ( !a.address && @@ -105,7 +102,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { return bscript.compile([OPS.OP_1, o.pubkey]); }); lazy.prop(o, 'scriptLeaf', () => { - if (!a.scriptLeaf) return a.scriptLeaf; + if (a.scriptLeaf) return a.scriptLeaf; }); lazy.prop(o, 'pubkey', () => { if (a.pubkey) return a.pubkey; From dcffed31ebb135e718aa8e0d36cc629007bf6347 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 12 Nov 2021 12:13:26 +0200 Subject: [PATCH 28/53] chore: update taggedHash() prefix after rebase --- src/taproot.js | 6 +++--- ts_src/taproot.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/taproot.js b/src/taproot.js index 220274d61..0bb77079d 100644 --- a/src/taproot.js +++ b/src/taproot.js @@ -10,9 +10,9 @@ const types_1 = require('./types'); // todo: !!!Temp, to be replaced. Only works because bip32 has it as dependecy. Linting will fail. const ecc = require('tiny-secp256k1'); const LEAF_VERSION_TAPSCRIPT = 0xc0; -const TAP_LEAF_TAG = buffer_1.Buffer.from('TapLeaf', 'utf8'); -const TAP_BRANCH_TAG = buffer_1.Buffer.from('TapBranch', 'utf8'); -const TAP_TWEAK_TAG = buffer_1.Buffer.from('TapTweak', 'utf8'); +const TAP_LEAF_TAG = 'TapLeaf'; +const TAP_BRANCH_TAG = 'TapBranch'; +const TAP_TWEAK_TAG = 'TapTweak'; const EC_P_BN = new BN(types_1.EC_P); const EC_P_REDUCTION = BN.red(EC_P_BN); const EC_P_QUADRATIC_RESIDUE = EC_P_BN.addn(1).divn(4); diff --git a/ts_src/taproot.ts b/ts_src/taproot.ts index d364670a9..7f1a90bd4 100644 --- a/ts_src/taproot.ts +++ b/ts_src/taproot.ts @@ -16,9 +16,9 @@ import { const ecc = require('tiny-secp256k1'); const LEAF_VERSION_TAPSCRIPT = 0xc0; -const TAP_LEAF_TAG = NBuffer.from('TapLeaf', 'utf8'); -const TAP_BRANCH_TAG = NBuffer.from('TapBranch', 'utf8'); -const TAP_TWEAK_TAG = NBuffer.from('TapTweak', 'utf8'); +const TAP_LEAF_TAG ='TapLeaf'; +const TAP_BRANCH_TAG ='TapBranch'; +const TAP_TWEAK_TAG ='TapTweak'; const EC_P_BN = new BN(EC_P); const EC_P_REDUCTION = BN.red(EC_P_BN); From d10716146dc60b570b327c86fb5c53326dcb0908 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 12 Jan 2022 15:17:13 +0200 Subject: [PATCH 29/53] fix: rebase issues --- package-lock.json | 14 +------------- src/taproot.js | 2 +- ts_src/taproot.ts | 2 +- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9a60ab056..15caf4e12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2378,19 +2378,7 @@ "integrity": "sha512-8qPw7zDK6Hco2tVGYGQeOmOPp/hZnREwy2iIkcq0ygAuqc9WHo29vKN94lNymh1QbB3nthtAMF6KTIrdbsIotA==", "dev": true, "requires": { - "bindings": "^1.3.0", - "bn.js": "^4.11.8", - "create-hmac": "^1.1.7", - "elliptic": "^6.4.0", - "nan": "^2.13.2" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } + "uint8array-tools": "0.0.6" } }, "to-fast-properties": { diff --git a/src/taproot.js b/src/taproot.js index 0bb77079d..f85b942a6 100644 --- a/src/taproot.js +++ b/src/taproot.js @@ -144,5 +144,5 @@ function serializeScript(s) { } // todo: do not use ecc function pointAddScalar(P, h) { - return ecc.pointAddScalar(P, h); + return buffer_1.Buffer.from(ecc.pointAddScalar(P, h)); } diff --git a/ts_src/taproot.ts b/ts_src/taproot.ts index 7f1a90bd4..ea996760b 100644 --- a/ts_src/taproot.ts +++ b/ts_src/taproot.ts @@ -181,5 +181,5 @@ function serializeScript(s: Buffer): Buffer { // todo: do not use ecc function pointAddScalar(P: Buffer, h: Buffer): Buffer { - return ecc.pointAddScalar(P, h); + return NBuffer.from(ecc.pointAddScalar(P, h)); } From 4d2af065c1bfba780a26e80c9a65a01106abd266 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 13 Jan 2022 16:54:23 +0200 Subject: [PATCH 30/53] chore: remove the bn.js dependency --- package-lock.json | 5 ----- package.json | 1 - 2 files changed, 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 15caf4e12..f576a482c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -606,11 +606,6 @@ "integrity": "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow==", "dev": true }, - "bn.js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", - "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==" - }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", diff --git a/package.json b/package.json index 172aa18b8..13e0e68fb 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ "dependencies": { "bech32": "^2.0.0", "bip174": "^2.0.1", - "bn.js": "^5.2.0", "bs58check": "^2.1.2", "create-hash": "^1.1.0", "typeforce": "^1.11.3", From 6cfbf659d24bc32568585dc9c64e6b5bfa4d2795 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 13 Jan 2022 16:55:49 +0200 Subject: [PATCH 31/53] refactor: use injectable ecc lib --- src/payments/index.d.ts | 16 +- src/payments/index.js | 24 +- src/payments/p2tr.d.ts | 5 +- src/payments/p2tr.js | 440 +++++++++++++++++----------------- src/payments/testecc.d.ts | 2 + src/payments/testecc.js | 172 ++++++++++++++ src/taproot.d.ts | 5 +- src/taproot.js | 67 +----- src/types.d.ts | 10 +- test/fixtures/p2tr.json | 2 +- test/payments.spec.ts | 15 +- ts_src/payments/index.ts | 24 +- ts_src/payments/p2tr.ts | 474 +++++++++++++++++++------------------ ts_src/payments/testecc.ts | 174 ++++++++++++++ ts_src/taproot.ts | 95 +------- ts_src/types.ts | 13 +- 16 files changed, 926 insertions(+), 612 deletions(-) create mode 100644 src/payments/testecc.d.ts create mode 100644 src/payments/testecc.js create mode 100644 ts_src/payments/testecc.ts diff --git a/src/payments/index.d.ts b/src/payments/index.d.ts index 0170dd093..72db5de25 100644 --- a/src/payments/index.d.ts +++ b/src/payments/index.d.ts @@ -1,6 +1,6 @@ /// import { Network } from '../networks'; -import { TaprootLeaf } from '../types'; +import { TaprootLeaf, TinySecp256k1Interface } from '../types'; import { p2data as embed } from './embed'; import { p2ms } from './p2ms'; import { p2pk } from './p2pk'; @@ -8,7 +8,6 @@ import { p2pkh } from './p2pkh'; import { p2sh } from './p2sh'; import { p2wpkh } from './p2wpkh'; import { p2wsh } from './p2wsh'; -import { p2tr } from './p2tr'; export interface Payment { name?: string; network?: Network; @@ -35,7 +34,18 @@ export interface PaymentOpts { validate?: boolean; allowIncomplete?: boolean; } +export interface PaymentAPI { + embed: PaymentCreator; + p2ms: PaymentCreator; + p2pk: PaymentCreator; + p2pkh: PaymentCreator; + p2sh: PaymentCreator; + p2wpkh: PaymentCreator; + p2wsh: PaymentCreator; + p2tr: PaymentCreator; +} export declare type StackElement = Buffer | number; export declare type Stack = StackElement[]; export declare type StackFunction = () => Stack; -export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh, p2tr }; +export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh, PaymentFactory }; +export default function PaymentFactory(ecc: TinySecp256k1Interface): PaymentAPI; diff --git a/src/payments/index.js b/src/payments/index.js index 9ce55f859..779df03fa 100644 --- a/src/payments/index.js +++ b/src/payments/index.js @@ -1,6 +1,6 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -exports.p2tr = exports.p2wsh = exports.p2wpkh = exports.p2sh = exports.p2pkh = exports.p2pk = exports.p2ms = exports.embed = void 0; +exports.PaymentFactory = 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, @@ -51,11 +51,21 @@ Object.defineProperty(exports, 'p2wsh', { }, }); const p2tr_1 = require('./p2tr'); -Object.defineProperty(exports, 'p2tr', { - enumerable: true, - get: function() { - return p2tr_1.p2tr; - }, -}); +const testecc_1 = require('./testecc'); +function PaymentFactory(ecc) { + (0, testecc_1.testEcc)(ecc); + return { + embed: embed_1.p2data, + p2ms: p2ms_1.p2ms, + p2pk: p2pk_1.p2pk, + p2pkh: p2pkh_1.p2pkh, + p2sh: p2sh_1.p2sh, + p2wpkh: p2wpkh_1.p2wpkh, + p2wsh: p2wsh_1.p2wsh, + p2tr: (0, p2tr_1.p2tr)(ecc), + }; +} +exports.default = PaymentFactory; +exports.PaymentFactory = PaymentFactory; // TODO // witness commitment diff --git a/src/payments/p2tr.d.ts b/src/payments/p2tr.d.ts index 350ed0ffc..e4d4c8a9b 100644 --- a/src/payments/p2tr.d.ts +++ b/src/payments/p2tr.d.ts @@ -1,2 +1,3 @@ -import { Payment, PaymentOpts } from './index'; -export declare function p2tr(a: Payment, opts?: PaymentOpts): Payment; +import { TinySecp256k1Interface } from '../types'; +import { PaymentCreator } from './index'; +export declare function p2tr(ecc: TinySecp256k1Interface): PaymentCreator; diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index 435d2f101..78ef95311 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -11,227 +11,237 @@ const bech32_1 = require('bech32'); const OPS = bscript.OPS; const TAPROOT_VERSION = 0x01; const ANNEX_PREFIX = 0x50; -function p2tr(a, opts) { - if ( - !a.address && - !a.output && - !a.pubkey && - !a.output && - !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(bscript.isCanonicalScriptSignature), - witness: types_1.typeforce.maybe( - types_1.typeforce.arrayOf(types_1.typeforce.Buffer), - ), - // scriptsTree: typef.maybe(typef.TaprootNode), // use merkel.isMast ? - scriptLeaf: types_1.typeforce.maybe({ - version: types_1.typeforce.maybe(types_1.typeforce.Number), - output: types_1.typeforce.maybe(types_1.typeforce.Buffer), - }), - }, - 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), - }; - }); - const _witness = lazy.value(() => { - if (!a.witness || !a.witness.length) return; +function p2tr(ecc) { + return (a, opts) => { if ( - a.witness.length >= 2 && - a.witness[a.witness.length - 1][0] === ANNEX_PREFIX - ) { - // remove annex, ignored by taproot - return a.witness.slice(0, -1); - } - return a.witness.slice(); - }); - 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_VERSION); - return bech32_1.bech32m.encode(network.bech32, words); - }); - lazy.prop(o, 'hash', () => { - if (a.hash) return a.hash; - if (a.scriptsTree) return (0, taproot_1.toHashTree)(a.scriptsTree).hash; - const w = _witness(); - if (w && w.length > 1) { - const controlBlock = w[w.length - 1]; - const leafVersion = controlBlock[0] & 0b11111110; - const script = w[w.length - 2]; - const leafHash = (0, taproot_1.tapLeafHash)(script, leafVersion); - return (0, taproot_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, 'scriptLeaf', () => { - if (a.scriptLeaf) return a.scriptLeaf; - }); - 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 = (0, taproot_1.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.witness || a.witness.length !== 1) return; - return a.witness[0]; - }); - lazy.prop(o, 'input', () => { - // todo - }); - lazy.prop(o, 'witness', () => { - if (a.witness) return a.witness; - if (a.scriptsTree && a.scriptLeaf && a.internalPubkey) { - // todo: optimize/cache - const hashTree = (0, taproot_1.toHashTree)(a.scriptsTree); - const leafHash = (0, taproot_1.tapLeafHash)( - a.scriptLeaf.output, - a.scriptLeaf.version, - ); - const path = (0, taproot_1.findScriptPath)(hashTree, leafHash); - const outputKey = (0, taproot_1.tweakKey)( - a.internalPubkey, - hashTree.hash, - ); - if (!outputKey) return; - const version = a.scriptLeaf.version || 0xc0; - const controlBock = buffer_1.Buffer.concat( - [ - buffer_1.Buffer.from([version | outputKey.parity]), - a.internalPubkey, - ].concat(path.reverse()), - ); - return [a.scriptLeaf.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_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) { + !a.address && + !a.output && + !a.pubkey && + !a.output && + !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(bscript.isCanonicalScriptSignature), + witness: types_1.typeforce.maybe( + types_1.typeforce.arrayOf(types_1.typeforce.Buffer), + ), + // scriptsTree: typef.maybe(typef.TaprootNode), // use merkel.isMast ? + scriptLeaf: types_1.typeforce.maybe({ + version: types_1.typeforce.maybe(types_1.typeforce.Number), + output: types_1.typeforce.maybe(types_1.typeforce.Buffer), + }), + }, + 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), + }; + }); + const _witness = lazy.value(() => { + if (!a.witness || !a.witness.length) return; 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 = (0, taproot_1.tweakKey)(a.internalPubkey, o.hash); - if (tweakedKey === null) - throw new TypeError('Invalid internalPubkey for p2tr'); - if (pubkey.length > 0 && !pubkey.equals(tweakedKey.x)) - throw new TypeError('Pubkey mismatch'); - else pubkey = tweakedKey.x; - } - if (pubkey && pubkey.length) { - if ((0, taproot_1.liftX)(pubkey) === null) - throw new TypeError('Invalid pubkey for p2tr'); - } - if (a.hash && a.scriptsTree) { - const hash = (0, taproot_1.toHashTree)(a.scriptsTree).hash; - if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch'); - } - const witness = _witness(); - if (witness && witness.length) { - if (witness.length === 1) { - // key spending - if (a.signature && !a.signature.equals(witness[0])) - throw new TypeError('Signature mismatch'); - // todo: recheck - // if (!bscript.isSchnorSignature(a.pubkey, a.witness[0])) - // throw new TypeError('Witness has invalid signature'); - } 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'); - const internalPubkeyPoint = (0, taproot_1.liftX)(internalPubkey); - if (!internalPubkeyPoint) - throw new TypeError('Invalid internalPubkey for p2tr witness'); + a.witness.length >= 2 && + a.witness[a.witness.length - 1][0] === ANNEX_PREFIX + ) { + // remove annex, ignored by taproot + return a.witness.slice(0, -1); + } + return a.witness.slice(); + }); + 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_VERSION); + return bech32_1.bech32m.encode(network.bech32, words); + }); + lazy.prop(o, 'hash', () => { + if (a.hash) return a.hash; + if (a.scriptsTree) return (0, taproot_1.toHashTree)(a.scriptsTree).hash; + const w = _witness(); + if (w && w.length > 1) { + const controlBlock = w[w.length - 1]; const leafVersion = controlBlock[0] & 0b11111110; - const script = witness[witness.length - 2]; + const script = w[w.length - 2]; const leafHash = (0, taproot_1.tapLeafHash)(script, leafVersion); - const hash = (0, taproot_1.rootHashFromPath)(controlBlock, leafHash); - const outputKey = (0, taproot_1.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 (0, taproot_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, 'scriptLeaf', () => { + if (a.scriptLeaf) return a.scriptLeaf; + }); + 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.witness || a.witness.length !== 1) return; + return a.witness[0]; + }); + lazy.prop(o, 'input', () => { + // todo + }); + lazy.prop(o, 'witness', () => { + if (a.witness) return a.witness; + if (a.scriptsTree && a.scriptLeaf && a.internalPubkey) { + // todo: optimize/cache + const hashTree = (0, taproot_1.toHashTree)(a.scriptsTree); + const leafHash = (0, taproot_1.tapLeafHash)( + a.scriptLeaf.output, + a.scriptLeaf.version, + ); + const path = (0, taproot_1.findScriptPath)(hashTree, leafHash); + const outputKey = tweakKey(a.internalPubkey, hashTree.hash); + if (!outputKey) return; + const version = a.scriptLeaf.version || 0xc0; + const controlBock = buffer_1.Buffer.concat( + [ + buffer_1.Buffer.from([version | outputKey.parity]), + a.internalPubkey, + ].concat(path.reverse()), + ); + return [a.scriptLeaf.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_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 (!ecc.isXOnlyPoint(pubkey)) + throw new TypeError('Invalid pubkey for p2tr'); + } + if (a.hash && a.scriptsTree) { + const hash = (0, taproot_1.toHashTree)(a.scriptsTree).hash; + if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch'); + } + const witness = _witness(); + if (witness && witness.length) { + if (witness.length === 1) { + // key spending + if (a.signature && !a.signature.equals(witness[0])) + throw new TypeError('Signature mismatch'); + // todo: recheck + // if (!bscript.isSchnorSignature(a.pubkey, a.witness[0])) + // throw new TypeError('Witness has invalid signature'); + } 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 (!ecc.isXOnlyPoint(internalPubkey)) + throw new TypeError('Invalid internalPubkey for p2tr witness'); + const leafVersion = controlBlock[0] & 0b11111110; + const script = witness[witness.length - 2]; + const leafHash = (0, taproot_1.tapLeafHash)(script, leafVersion); + const hash = (0, taproot_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); + }; + 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, taproot_1.tapTweakHash)(pubKey, h); + const res = ecc.xOnlyPointAddTweak(pubKey, tweakHash); + if (!res || res.xOnlyPubkey === null) return null; + return { + parity: res.parity, + x: buffer_1.Buffer.from(res.xOnlyPubkey), + }; } - return Object.assign(o, a); } exports.p2tr = p2tr; diff --git a/src/payments/testecc.d.ts b/src/payments/testecc.d.ts new file mode 100644 index 000000000..59d0de2b2 --- /dev/null +++ b/src/payments/testecc.d.ts @@ -0,0 +1,2 @@ +import { TinySecp256k1Interface } from '../types'; +export declare function testEcc(ecc: TinySecp256k1Interface): void; diff --git a/src/payments/testecc.js b/src/payments/testecc.js new file mode 100644 index 000000000..9bdc62031 --- /dev/null +++ b/src/payments/testecc.js @@ -0,0 +1,172 @@ +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); +exports.testEcc = void 0; +const h = hex => Buffer.from(hex, 'hex'); +function testEcc(ecc) { + 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'), + ), + ); + tweakAddVectors.forEach(t => { + const r = ecc.xOnlyPointAddTweak(h(t.pubkey), h(t.tweak)); + if (t.result === null) { + assert(r === null); + } else { + assert(r !== null); + assert(r.parity === t.parity); + assert(Buffer.from(r.xOnlyPubkey).equals(h(t.result))); + } + }); +} +exports.testEcc = testEcc; +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: '2f1b310f4c065331bc0d79ba4661bb9822d67d7c4a1b0a1892e1fd0cd23aa68d', + tweak: '40ab636041bb695843ac7f220344565bed3b5dca919e256b7e31e19d69b36fad', + parity: 1, + result: '5786150f0ac36a2aaeb8d11aaeb7aa2d03ad63c528cc307a7cd5648c84041f34', + }, + { + pubkey: 'e7e9acacbdb43fc9fb71a8db1536c0f866caa78def49f666fa121a6f7954bb01', + tweak: '1ad613216778a70490c8a681a4901d4ca880d76916deb69979b5ac52d2760e09', + parity: 1, + result: 'ae10fa880c85848cc1faf056f56a64b7d45c68838cfb308d03ca2be9b485c130', + }, + { + pubkey: '2c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991', + tweak: '823c3cd2142744b075a87eade7e1b8678ba308d566226a0056ca2b7a76f86b47', + parity: 0, + result: '9534f8dc8c6deda2dc007655981c78b49c5d96c778fbf363462a11ec9dfd948c', + }, + { + pubkey: '8f19ee7677713806c662078a9f2b2c7b930376d22f2d617bce50b5e444839a7c', + tweak: '7df1f4b66058f8be34b6b7d17be9bcf35ba5c98edf8d4e763b95964bad655fe4', + parity: 1, + result: '74619a5990750928d0728817b02bb0d398062dad0e568f46ea5348d35bef914f', + }, + { + pubkey: '2bda68b3aa0239d382f185ca2d8c31ce604cc26220cef3eb65223f47a0088d87', + tweak: 'e5a018b3a2e155316109d9cdc5eab739759c0e07e0c00bf9fccb8237fe4d7f02', + parity: 0, + result: '84479dc6bf70762721b10b89c8798e00d525507edc3fabbfc89ad915b6509379', + }, + { + pubkey: '9611ba340bce00a594f1ffb1294974af80e1301e49597378732fd77bbdedf454', + tweak: 'bbb8ec1f063522953a4a9f90ff4e849560e0f0597458529ea13b8868f255c7c7', + parity: 0, + result: '30bebfdad18b87b646f60e51d3c45c6658fbb4364c94b1b33d925a4515b66757', + }, + { + pubkey: 'd9f5792078a845303c1f1ea88aec79ed0fd9f0c49e9e7bff2765877e79b4dd52', + tweak: '531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337', + parity: 0, + result: 'fe5dd39f0491af71711454eee5b6a9c99779c422dd97f5e7f75d7ce7be7b32f0', + }, + { + pubkey: '40ab636041bb695843ac7f220344565bed3b5dca919e256b7e31e19d69b36fad', + tweak: '048968943184ce8a0239ab2141641e8eaead35a6dc8e6b55ad33ac1eca975a47', + parity: 1, + result: '6c8245a62201887c5e3aeb022fff06e6c110f3e58ad6d37cc20e877082b72c58', + }, + { + pubkey: '6aa3da9b5c1d61956076cb3014ffdaa0996bacdae29ba4b89e39b4088f86ec78', + tweak: 'ff8adab52623bcb2717fc71d7edc6f55e98396e6c234dff01f307a12b2af1c99', + parity: 1, + result: 'd6080b5df61525fe8be31a823f3943e5fc9354d5a091b2dea195985c7c395787', + }, + { + pubkey: '24653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c', + tweak: '9e5f7dbe6d62ade5aab476b40559852ea1b5fc7bb99a61a42eab550f69ffafb4', + parity: 0, + result: '58289ee230fcf6a78cb9878cae5102cc9104490abab9d03f3eccc2f0cd07de5f', + }, + { + pubkey: 'e5a018b3a2e155316109d9cdc5eab739759c0e07e0c00bf9fccb8237fe4d7f02', + tweak: 'bde750d93efe821813df9c15ee676f2e9c63386336c164f5a15cf240ac653c06', + parity: 0, + result: '9a1ae919c5c78da635d94a92b3053e46b2261b81ec70db82a382f5bff474bec4', + }, + { + pubkey: 'bc14bc97e2d818ee360a9ba7782bd6a6dfc2c1e335fffc584a095fdac5fea641', + tweak: '4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa', + parity: 1, + result: '19a7416f4f95f36c5e48dc7630ffea8b292e1721cecfa9cc5f794c83973e41d6', + }, + { + pubkey: '35e9d7c48e3a5254d5881b60abf004cf6eedc6ab842393caa2fdd20d6d0ad170', + tweak: '18bb586dc109adf49ffb42e0ac293d2a2965e49a0a4900c2be776b426b7cbfde', + parity: 0, + result: 'fa7cca72580bb686fbbae09ded801c7d109fa378f52e8a5f43a1922e442e44c1', + }, + { + pubkey: '67bff656551f25009ac8ed88664736c08074a15dbd2268292f5de7ca7e718338', + tweak: 'b96359049e97f49d871e856f37e54d0978bae2cc936b4484d96df984cd20daa1', + parity: 0, + result: 'dd081d737da17fb4f6686f8497cac56b16ea06e1dc05859633f735fb304e7e5a', + }, + { + pubkey: '5b0da52533a1620fe947cb658c35e1772f39ef1253753493b7dc4b8d8f31f40e', + tweak: '3d481f46056f2da27870a5d00c0c7bf484036780a83bbcc2e2da2f03bc33bff0', + parity: 1, + result: '164e13b54edc89673f94563120d87db4a47b12e49c40c195ac51ea7bc50f22e1', + }, + { + pubkey: '0612c5e8c98a9677a2ddd13770e26f5f1e771a088c88ce519a1e1b65872423f9', + tweak: 'dbcfa1c73674cba4aa1b6992ebdc6a77008d38f6c6ec068c3c862b9ff6d287f2', + parity: 0, + result: '82fc6954352b7189a156e4678d0c315c122431fa9551961b8e3c811b55a42c8b', + }, + { + pubkey: '9ac20335eb38768d2052be1dbbc3c8f6178407458e51e6b4ad22f1d91758895b', + tweak: '6aa3da9b5c1d61956076cb3014ffdaa0996bacdae29ba4b89e39b4088f86ec78', + parity: 1, + result: 'cf9065a7e2c9f909becc1c95f9884ed9fbe19c4a8954ed17880f02d94ae96a63', + }, + { + pubkey: 'c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5', + tweak: 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413f', + parity: -1, + result: null, + }, +]; diff --git a/src/taproot.d.ts b/src/taproot.d.ts index c55f10d72..37c118430 100644 --- a/src/taproot.d.ts +++ b/src/taproot.d.ts @@ -1,7 +1,5 @@ /// -import { TweakedPublicKey, TaprootLeaf } from './types'; -export declare function liftX(buffer: Buffer): Buffer | null; -export declare function tweakKey(pubKey: Buffer, h: Buffer | undefined): TweakedPublicKey | null; +import { TaprootLeaf } from './types'; export declare function rootHashFromPath(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer; export interface HashTree { hash: Buffer; @@ -11,3 +9,4 @@ export interface HashTree { export declare function toHashTree(scripts: TaprootLeaf[]): HashTree; export declare function findScriptPath(node: HashTree, hash: Buffer): Buffer[]; export declare function tapLeafHash(script: Buffer, version?: number): Buffer; +export declare function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer; diff --git a/src/taproot.js b/src/taproot.js index f85b942a6..98a7db377 100644 --- a/src/taproot.js +++ b/src/taproot.js @@ -1,68 +1,16 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -exports.tapLeafHash = exports.findScriptPath = exports.toHashTree = exports.rootHashFromPath = exports.tweakKey = exports.liftX = void 0; +exports.tapTweakHash = exports.tapLeafHash = exports.findScriptPath = exports.toHashTree = exports.rootHashFromPath = void 0; const buffer_1 = require('buffer'); -const BN = require('bn.js'); const bcrypto = require('./crypto'); // todo: use varuint-bitcoin?? const varuint = require('bip174/src/lib/converter/varint'); -const types_1 = require('./types'); // todo: !!!Temp, to be replaced. Only works because bip32 has it as dependecy. Linting will fail. -const ecc = require('tiny-secp256k1'); +// const ecc = require('tiny-secp256k1'); const LEAF_VERSION_TAPSCRIPT = 0xc0; const TAP_LEAF_TAG = 'TapLeaf'; const TAP_BRANCH_TAG = 'TapBranch'; const TAP_TWEAK_TAG = 'TapTweak'; -const EC_P_BN = new BN(types_1.EC_P); -const EC_P_REDUCTION = BN.red(EC_P_BN); -const EC_P_QUADRATIC_RESIDUE = EC_P_BN.addn(1).divn(4); -const BN_2 = new BN(2); -const BN_3 = new BN(3); -const BN_7 = new BN(7); -function liftX(buffer) { - if (!buffer_1.Buffer.isBuffer(buffer)) return null; - if (buffer.length !== 32) return null; - if (buffer.compare(types_1.ZERO32) === 0) return null; - if (buffer.compare(types_1.EC_P) >= 0) return null; - const x = new BN(buffer); - const x1 = x.toRed(EC_P_REDUCTION); - const ySq = x1 - .redPow(BN_3) - .add(BN_7) - .mod(EC_P_BN); - const y = ySq.redPow(EC_P_QUADRATIC_RESIDUE); - if (!ySq.eq(y.redPow(BN_2))) { - return null; - } - const y1 = y.isEven() ? y : EC_P_BN.sub(y); - return buffer_1.Buffer.concat([ - buffer_1.Buffer.from([0x04]), - buffer_1.Buffer.from(x1.toBuffer('be', 32)), - buffer_1.Buffer.from(y1.toBuffer('be', 32)), - ]); -} -exports.liftX = liftX; -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 = bcrypto.taggedHash( - TAP_TWEAK_TAG, - buffer_1.Buffer.concat(h ? [pubKey, h] : [pubKey]), - ); - if (tweakHash.compare(types_1.GROUP_ORDER) >= 0) { - // todo: add test for this case - throw new TypeError('Tweak value over the SECP256K1 Order'); - } - const P = liftX(pubKey); - if (P === null) return null; - const Q = pointAddScalar(P, tweakHash); - return { - parity: Q[64] % 2, - x: Q.slice(1, 33), - }; -} -exports.tweakKey = tweakKey; function rootHashFromPath(controlBlock, tapLeafMsg) { const k = [tapLeafMsg]; const e = []; @@ -136,13 +84,16 @@ exports.tapLeafHash = tapLeafHash; function tapBranchHash(a, b) { return bcrypto.taggedHash(TAP_BRANCH_TAG, buffer_1.Buffer.concat([a, b])); } +function tapTweakHash(pubKey, h) { + return bcrypto.taggedHash( + TAP_TWEAK_TAG, + buffer_1.Buffer.concat(h ? [pubKey, h] : [pubKey]), + ); +} +exports.tapTweakHash = tapTweakHash; function serializeScript(s) { const varintLen = varuint.encodingLength(s.length); const buffer = buffer_1.Buffer.allocUnsafe(varintLen); // better varuint.encode(s.length, buffer); return buffer_1.Buffer.concat([buffer, s]); } -// todo: do not use ecc -function pointAddScalar(P, h) { - return buffer_1.Buffer.from(ecc.pointAddScalar(P, h)); -} diff --git a/src/types.d.ts b/src/types.d.ts index 1bcb12426..014c2b910 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -14,14 +14,18 @@ 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 TweakedPublicKey { - parity: number; - x: Buffer; +export interface XOnlyPointAddTweakResult { + parity: 1 | 0; + xOnlyPubkey: Uint8Array; } export interface TaprootLeaf { output: Buffer; version?: number; } +export interface TinySecp256k1Interface { + isXOnlyPoint(p: Uint8Array): boolean; + xOnlyPointAddTweak(p: Uint8Array, tweak: Uint8Array): XOnlyPointAddTweakResult | null; +} export declare const Buffer256bit: any; export declare const Hash160bit: any; export declare const Hash256bit: any; diff --git a/test/fixtures/p2tr.json b/test/fixtures/p2tr.json index da89f26f6..02b1cb3c0 100644 --- a/test/fixtures/p2tr.json +++ b/test/fixtures/p2tr.json @@ -898,7 +898,7 @@ } }, { - "exception": "Invalid internalPubkey for p2t", + "exception": "Expected Point", "options": {}, "arguments": { "internalPubkey": "9fa5ffb68821cf559001caa0577eeea4978b29416def328a707b15e91701a2f8" diff --git a/test/payments.spec.ts b/test/payments.spec.ts index 9e28501ae..957396199 100644 --- a/test/payments.spec.ts +++ b/test/payments.spec.ts @@ -1,16 +1,15 @@ import * as assert from 'assert'; import { describe, it } from 'mocha'; -import { PaymentCreator } from '../src/payments'; +import * as ecc from 'tiny-secp256k1'; +import { PaymentCreator, PaymentFactory } from '../src/payments'; import * as u from './payments.utils'; + +const payments = PaymentFactory(ecc); ['embed', 'p2ms', 'p2pk', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'].forEach(p => { describe(p, () => { - let fn: PaymentCreator; - const payment = require('../src/payments/' + p); - if (p === 'embed') { - fn = payment.p2data; - } else { - fn = payment[p]; - } + //@ts-ignore + const fn: PaymentCreator = payments[p]; + const fixtures = require('./fixtures/' + p); fixtures.valid.forEach((f: any) => { diff --git a/ts_src/payments/index.ts b/ts_src/payments/index.ts index 996f292e9..f7c5dbadd 100644 --- a/ts_src/payments/index.ts +++ b/ts_src/payments/index.ts @@ -1,5 +1,5 @@ import { Network } from '../networks'; -import { TaprootLeaf } from '../types'; +import { TaprootLeaf, TinySecp256k1Interface } from '../types'; import { p2data as embed } from './embed'; import { p2ms } from './p2ms'; import { p2pk } from './p2pk'; @@ -8,6 +8,7 @@ import { p2sh } from './p2sh'; import { p2wpkh } from './p2wpkh'; import { p2wsh } from './p2wsh'; import { p2tr } from './p2tr'; +import { testEcc } from './testecc'; export interface Payment { name?: string; @@ -39,11 +40,30 @@ export interface PaymentOpts { allowIncomplete?: boolean; } +export interface PaymentAPI { + embed: PaymentCreator; + p2ms: PaymentCreator; + p2pk: PaymentCreator; + p2pkh: PaymentCreator; + p2sh: PaymentCreator; + p2wpkh: PaymentCreator; + p2wsh: PaymentCreator; + p2tr: PaymentCreator; +} + export type StackElement = Buffer | number; export type Stack = StackElement[]; export type StackFunction = () => Stack; -export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh, p2tr }; +export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh, PaymentFactory }; + +export default function PaymentFactory( + ecc: TinySecp256k1Interface, +): PaymentAPI { + testEcc(ecc); + + return { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh, p2tr: p2tr(ecc) }; +} // TODO // witness commitment diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index 98ca8eb56..0bb68465d 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -1,16 +1,15 @@ import { Buffer as NBuffer } from 'buffer'; import { bitcoin as BITCOIN_NETWORK } from '../networks'; import * as bscript from '../script'; -import { typeforce as typef } from '../types'; +import { typeforce as typef, TinySecp256k1Interface } from '../types'; import { toHashTree, rootHashFromPath, findScriptPath, tapLeafHash, - tweakKey, - liftX, + tapTweakHash, } from '../taproot'; -import { Payment, PaymentOpts } from './index'; +import { Payment, PaymentOpts, PaymentCreator } from './index'; import * as lazy from './lazy'; import { bech32m } from 'bech32'; const OPS = bscript.OPS; @@ -18,242 +17,267 @@ const OPS = bscript.OPS; const TAPROOT_VERSION = 0x01; const ANNEX_PREFIX = 0x50; -export function p2tr(a: Payment, opts?: PaymentOpts): Payment { - if ( - !a.address && - !a.output && - !a.pubkey && - !a.output && - !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)), - pubkey: typef.maybe(typef.BufferN(32)), - signature: typef.maybe(bscript.isCanonicalScriptSignature), - witness: typef.maybe(typef.arrayOf(typef.Buffer)), - // scriptsTree: typef.maybe(typef.TaprootNode), // use merkel.isMast ? - scriptLeaf: typef.maybe({ - version: typef.maybe(typef.Number), - output: typef.maybe(typef.Buffer), - }), - }, - 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), - }; - }); - - const _witness = lazy.value(() => { - if (!a.witness || !a.witness.length) return; +export function p2tr(ecc: TinySecp256k1Interface): PaymentCreator { + return (a: Payment, opts?: PaymentOpts): Payment => { if ( - a.witness.length >= 2 && - a.witness[a.witness.length - 1][0] === ANNEX_PREFIX - ) { - // remove annex, ignored by taproot - return a.witness.slice(0, -1); - } - return a.witness.slice(); - }); - - 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_VERSION); - return bech32m.encode(network.bech32, words); - }); - - lazy.prop(o, 'hash', () => { - if (a.hash) return a.hash; - if (a.scriptsTree) return toHashTree(a.scriptsTree).hash; - const w = _witness(); - if (w && w.length > 1) { - const controlBlock = w[w.length - 1]; - const leafVersion = controlBlock[0] & 0b11111110; - const script = w[w.length - 2]; - const leafHash = tapLeafHash(script, 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, 'scriptLeaf', () => { - if (a.scriptLeaf) return a.scriptLeaf; - }); - 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.witness || a.witness.length !== 1) return; - return a.witness[0]; - }); - lazy.prop(o, 'input', () => { - // todo - }); - lazy.prop(o, 'witness', () => { - if (a.witness) return a.witness; - if (a.scriptsTree && a.scriptLeaf && a.internalPubkey) { - // todo: optimize/cache - const hashTree = toHashTree(a.scriptsTree); - const leafHash = tapLeafHash(a.scriptLeaf.output, a.scriptLeaf.version); - const path = findScriptPath(hashTree, leafHash); - const outputKey = tweakKey(a.internalPubkey, hashTree.hash); - if (!outputKey) return; - const version = a.scriptLeaf.version || 0xc0; - const controlBock = NBuffer.concat( - [NBuffer.from([version | outputKey.parity]), a.internalPubkey].concat( - path.reverse(), - ), - ); - return [a.scriptLeaf.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_VERSION) - throw new TypeError('Invalid address version'); - if (_address().data.length !== 32) - throw new TypeError('Invalid address data'); - pubkey = _address().data; - } + !a.address && + !a.output && + !a.pubkey && + !a.output && + !a.internalPubkey && + !(a.witness && a.witness.length > 1) + ) + throw new TypeError('Not enough data'); + opts = Object.assign({ validate: true }, opts || {}); - if (a.pubkey) { - if (pubkey.length > 0 && !pubkey.equals(a.pubkey)) - throw new TypeError('Pubkey mismatch'); - else pubkey = a.pubkey; - } + 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)), + pubkey: typef.maybe(typef.BufferN(32)), + signature: typef.maybe(bscript.isCanonicalScriptSignature), + witness: typef.maybe(typef.arrayOf(typef.Buffer)), + // scriptsTree: typef.maybe(typef.TaprootNode), // use merkel.isMast ? + scriptLeaf: typef.maybe({ + version: typef.maybe(typef.Number), + output: typef.maybe(typef.Buffer), + }), + }, + a, + ); - 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); - } + 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), + }; + }); - if (a.internalPubkey) { - const tweakedKey = tweakKey(a.internalPubkey, o.hash); - if (tweakedKey === null) - throw new TypeError('Invalid internalPubkey for p2tr'); - if (pubkey.length > 0 && !pubkey.equals(tweakedKey.x)) - throw new TypeError('Pubkey mismatch'); - else pubkey = tweakedKey.x; - } + 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 + ) { + // remove annex, ignored by taproot + return a.witness.slice(0, -1); + } + return a.witness.slice(); + }); - if (pubkey && pubkey.length) { - if (liftX(pubkey) === null) - throw new TypeError('Invalid pubkey for p2tr'); - } + const network = a.network || BITCOIN_NETWORK; + const o: Payment = { name: 'p2tr', network }; - if (a.hash && a.scriptsTree) { - const hash = toHashTree(a.scriptsTree).hash; - if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch'); - } + lazy.prop(o, 'address', () => { + if (!o.pubkey) return; - const witness = _witness(); - - if (witness && witness.length) { - if (witness.length === 1) { - // key spending - if (a.signature && !a.signature.equals(witness[0])) - throw new TypeError('Signature mismatch'); - // todo: recheck - // if (!bscript.isSchnorSignature(a.pubkey, a.witness[0])) - // throw new TypeError('Witness has invalid signature'); - } 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'); - - const internalPubkeyPoint = liftX(internalPubkey); - if (!internalPubkeyPoint) - throw new TypeError('Invalid internalPubkey for p2tr witness'); + const words = bech32m.toWords(o.pubkey); + words.unshift(TAPROOT_VERSION); + return bech32m.encode(network.bech32, words); + }); + lazy.prop(o, 'hash', () => { + if (a.hash) return a.hash; + if (a.scriptsTree) return toHashTree(a.scriptsTree).hash; + const w = _witness(); + if (w && w.length > 1) { + const controlBlock = w[w.length - 1]; const leafVersion = controlBlock[0] & 0b11111110; - const script = witness[witness.length - 2]; - + const script = w[w.length - 2]; const leafHash = tapLeafHash(script, leafVersion); - const hash = rootHashFromPath(controlBlock, leafHash); + 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, 'scriptLeaf', () => { + if (a.scriptLeaf) return a.scriptLeaf; + }); + 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.witness || a.witness.length !== 1) return; + return a.witness[0]; + }); + lazy.prop(o, 'input', () => { + // todo + }); + lazy.prop(o, 'witness', () => { + if (a.witness) return a.witness; + if (a.scriptsTree && a.scriptLeaf && a.internalPubkey) { + // todo: optimize/cache + const hashTree = toHashTree(a.scriptsTree); + const leafHash = tapLeafHash(a.scriptLeaf.output, a.scriptLeaf.version); + const path = findScriptPath(hashTree, leafHash); + const outputKey = tweakKey(a.internalPubkey, hashTree.hash); + if (!outputKey) return; + const version = a.scriptLeaf.version || 0xc0; + const controlBock = NBuffer.concat( + [NBuffer.from([version | outputKey.parity]), a.internalPubkey].concat( + path.reverse(), + ), + ); + return [a.scriptLeaf.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_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; + } - const outputKey = tweakKey(internalPubkey, hash); - if (!outputKey) - // todo: needs test data - throw new TypeError('Invalid outputKey for p2tr witness'); + 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 (!ecc.isXOnlyPoint(pubkey)) + throw new TypeError('Invalid pubkey for p2tr'); + } + + if (a.hash && a.scriptsTree) { + const hash = toHashTree(a.scriptsTree).hash; + if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch'); + } + + const witness = _witness(); + + if (witness && witness.length) { + if (witness.length === 1) { + // key spending + if (a.signature && !a.signature.equals(witness[0])) + throw new TypeError('Signature mismatch'); + // todo: recheck + // if (!bscript.isSchnorSignature(a.pubkey, a.witness[0])) + // throw new TypeError('Witness has invalid signature'); + } 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 (pubkey.length && !pubkey.equals(outputKey.x)) - throw new TypeError('Pubkey mismatch for p2tr witness'); + if (!ecc.isXOnlyPoint(internalPubkey)) + throw new TypeError('Invalid internalPubkey for p2tr witness'); - if (outputKey.parity !== (controlBlock[0] & 1)) - throw new Error('Incorrect parity'); + const leafVersion = controlBlock[0] & 0b11111110; + const script = witness[witness.length - 2]; + + const leafHash = tapLeafHash(script, 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); + }; + + 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 = ecc.xOnlyPointAddTweak(pubKey, tweakHash); + if (!res || res.xOnlyPubkey === null) return null; + + return { + parity: res.parity, + x: NBuffer.from(res.xOnlyPubkey), + }; } +} - return Object.assign(o, a); +interface TweakedPublicKey { + parity: number; + x: Buffer; } diff --git a/ts_src/payments/testecc.ts b/ts_src/payments/testecc.ts new file mode 100644 index 000000000..773fa3eb8 --- /dev/null +++ b/ts_src/payments/testecc.ts @@ -0,0 +1,174 @@ +import { TinySecp256k1Interface } from '../types'; + +const h = (hex: string): Buffer => Buffer.from(hex, 'hex'); + +export function testEcc(ecc: TinySecp256k1Interface): void { + 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'), + ), + ); + + 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: '2f1b310f4c065331bc0d79ba4661bb9822d67d7c4a1b0a1892e1fd0cd23aa68d', + tweak: '40ab636041bb695843ac7f220344565bed3b5dca919e256b7e31e19d69b36fad', + parity: 1, + result: '5786150f0ac36a2aaeb8d11aaeb7aa2d03ad63c528cc307a7cd5648c84041f34', + }, + { + pubkey: 'e7e9acacbdb43fc9fb71a8db1536c0f866caa78def49f666fa121a6f7954bb01', + tweak: '1ad613216778a70490c8a681a4901d4ca880d76916deb69979b5ac52d2760e09', + parity: 1, + result: 'ae10fa880c85848cc1faf056f56a64b7d45c68838cfb308d03ca2be9b485c130', + }, + { + pubkey: '2c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991', + tweak: '823c3cd2142744b075a87eade7e1b8678ba308d566226a0056ca2b7a76f86b47', + parity: 0, + result: '9534f8dc8c6deda2dc007655981c78b49c5d96c778fbf363462a11ec9dfd948c', + }, + { + pubkey: '8f19ee7677713806c662078a9f2b2c7b930376d22f2d617bce50b5e444839a7c', + tweak: '7df1f4b66058f8be34b6b7d17be9bcf35ba5c98edf8d4e763b95964bad655fe4', + parity: 1, + result: '74619a5990750928d0728817b02bb0d398062dad0e568f46ea5348d35bef914f', + }, + { + pubkey: '2bda68b3aa0239d382f185ca2d8c31ce604cc26220cef3eb65223f47a0088d87', + tweak: 'e5a018b3a2e155316109d9cdc5eab739759c0e07e0c00bf9fccb8237fe4d7f02', + parity: 0, + result: '84479dc6bf70762721b10b89c8798e00d525507edc3fabbfc89ad915b6509379', + }, + { + pubkey: '9611ba340bce00a594f1ffb1294974af80e1301e49597378732fd77bbdedf454', + tweak: 'bbb8ec1f063522953a4a9f90ff4e849560e0f0597458529ea13b8868f255c7c7', + parity: 0, + result: '30bebfdad18b87b646f60e51d3c45c6658fbb4364c94b1b33d925a4515b66757', + }, + { + pubkey: 'd9f5792078a845303c1f1ea88aec79ed0fd9f0c49e9e7bff2765877e79b4dd52', + tweak: '531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337', + parity: 0, + result: 'fe5dd39f0491af71711454eee5b6a9c99779c422dd97f5e7f75d7ce7be7b32f0', + }, + { + pubkey: '40ab636041bb695843ac7f220344565bed3b5dca919e256b7e31e19d69b36fad', + tweak: '048968943184ce8a0239ab2141641e8eaead35a6dc8e6b55ad33ac1eca975a47', + parity: 1, + result: '6c8245a62201887c5e3aeb022fff06e6c110f3e58ad6d37cc20e877082b72c58', + }, + { + pubkey: '6aa3da9b5c1d61956076cb3014ffdaa0996bacdae29ba4b89e39b4088f86ec78', + tweak: 'ff8adab52623bcb2717fc71d7edc6f55e98396e6c234dff01f307a12b2af1c99', + parity: 1, + result: 'd6080b5df61525fe8be31a823f3943e5fc9354d5a091b2dea195985c7c395787', + }, + { + pubkey: '24653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c', + tweak: '9e5f7dbe6d62ade5aab476b40559852ea1b5fc7bb99a61a42eab550f69ffafb4', + parity: 0, + result: '58289ee230fcf6a78cb9878cae5102cc9104490abab9d03f3eccc2f0cd07de5f', + }, + { + pubkey: 'e5a018b3a2e155316109d9cdc5eab739759c0e07e0c00bf9fccb8237fe4d7f02', + tweak: 'bde750d93efe821813df9c15ee676f2e9c63386336c164f5a15cf240ac653c06', + parity: 0, + result: '9a1ae919c5c78da635d94a92b3053e46b2261b81ec70db82a382f5bff474bec4', + }, + { + pubkey: 'bc14bc97e2d818ee360a9ba7782bd6a6dfc2c1e335fffc584a095fdac5fea641', + tweak: '4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa', + parity: 1, + result: '19a7416f4f95f36c5e48dc7630ffea8b292e1721cecfa9cc5f794c83973e41d6', + }, + { + pubkey: '35e9d7c48e3a5254d5881b60abf004cf6eedc6ab842393caa2fdd20d6d0ad170', + tweak: '18bb586dc109adf49ffb42e0ac293d2a2965e49a0a4900c2be776b426b7cbfde', + parity: 0, + result: 'fa7cca72580bb686fbbae09ded801c7d109fa378f52e8a5f43a1922e442e44c1', + }, + { + pubkey: '67bff656551f25009ac8ed88664736c08074a15dbd2268292f5de7ca7e718338', + tweak: 'b96359049e97f49d871e856f37e54d0978bae2cc936b4484d96df984cd20daa1', + parity: 0, + result: 'dd081d737da17fb4f6686f8497cac56b16ea06e1dc05859633f735fb304e7e5a', + }, + { + pubkey: '5b0da52533a1620fe947cb658c35e1772f39ef1253753493b7dc4b8d8f31f40e', + tweak: '3d481f46056f2da27870a5d00c0c7bf484036780a83bbcc2e2da2f03bc33bff0', + parity: 1, + result: '164e13b54edc89673f94563120d87db4a47b12e49c40c195ac51ea7bc50f22e1', + }, + { + pubkey: '0612c5e8c98a9677a2ddd13770e26f5f1e771a088c88ce519a1e1b65872423f9', + tweak: 'dbcfa1c73674cba4aa1b6992ebdc6a77008d38f6c6ec068c3c862b9ff6d287f2', + parity: 0, + result: '82fc6954352b7189a156e4678d0c315c122431fa9551961b8e3c811b55a42c8b', + }, + { + pubkey: '9ac20335eb38768d2052be1dbbc3c8f6178407458e51e6b4ad22f1d91758895b', + tweak: '6aa3da9b5c1d61956076cb3014ffdaa0996bacdae29ba4b89e39b4088f86ec78', + parity: 1, + result: 'cf9065a7e2c9f909becc1c95f9884ed9fbe19c4a8954ed17880f02d94ae96a63', + }, + { + pubkey: 'c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5', + tweak: 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413f', + parity: -1, + result: null, + }, +]; diff --git a/ts_src/taproot.ts b/ts_src/taproot.ts index ea996760b..ca8fe9fd5 100644 --- a/ts_src/taproot.ts +++ b/ts_src/taproot.ts @@ -1,88 +1,17 @@ import { Buffer as NBuffer } from 'buffer'; -const BN = require('bn.js'); - import * as bcrypto from './crypto'; + // todo: use varuint-bitcoin?? import * as varuint from 'bip174/src/lib/converter/varint'; -import { - TweakedPublicKey, - TaprootLeaf, - ZERO32, - EC_P, - GROUP_ORDER, -} from './types'; +import { TaprootLeaf } from './types'; // todo: !!!Temp, to be replaced. Only works because bip32 has it as dependecy. Linting will fail. -const ecc = require('tiny-secp256k1'); +// const ecc = require('tiny-secp256k1'); const LEAF_VERSION_TAPSCRIPT = 0xc0; -const TAP_LEAF_TAG ='TapLeaf'; -const TAP_BRANCH_TAG ='TapBranch'; -const TAP_TWEAK_TAG ='TapTweak'; - -const EC_P_BN = new BN(EC_P); -const EC_P_REDUCTION = BN.red(EC_P_BN); -const EC_P_QUADRATIC_RESIDUE = EC_P_BN.addn(1).divn(4); -const BN_2 = new BN(2); -const BN_3 = new BN(3); -const BN_7 = new BN(7); - -export function liftX(buffer: Buffer): Buffer | null { - if (!NBuffer.isBuffer(buffer)) return null; - if (buffer.length !== 32) return null; - - if (buffer.compare(ZERO32) === 0) return null; - if (buffer.compare(EC_P) >= 0) return null; - - const x = new BN(buffer); - - const x1 = x.toRed(EC_P_REDUCTION); - const ySq = x1 - .redPow(BN_3) - .add(BN_7) - .mod(EC_P_BN); - - const y = ySq.redPow(EC_P_QUADRATIC_RESIDUE); - - if (!ySq.eq(y.redPow(BN_2))) { - return null; - } - const y1 = y.isEven() ? y : EC_P_BN.sub(y); - - return NBuffer.concat([ - NBuffer.from([0x04]), - NBuffer.from(x1.toBuffer('be', 32)), - NBuffer.from(y1.toBuffer('be', 32)), - ]); -} - -export 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 = bcrypto.taggedHash( - TAP_TWEAK_TAG, - NBuffer.concat(h ? [pubKey, h] : [pubKey]), - ); - - if (tweakHash.compare(GROUP_ORDER) >= 0) { - // todo: add test for this case - throw new TypeError('Tweak value over the SECP256K1 Order'); - } - - const P = liftX(pubKey); - if (P === null) return null; - - const Q = pointAddScalar(P, tweakHash); - return { - parity: Q[64] % 2, - x: Q.slice(1, 33), - }; -} +const TAP_LEAF_TAG = 'TapLeaf'; +const TAP_BRANCH_TAG = 'TapBranch'; +const TAP_TWEAK_TAG = 'TapTweak'; export function rootHashFromPath( controlBlock: Buffer, @@ -172,14 +101,16 @@ function tapBranchHash(a: Buffer, b: Buffer): Buffer { return bcrypto.taggedHash(TAP_BRANCH_TAG, NBuffer.concat([a, b])); } +export function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer { + return bcrypto.taggedHash( + TAP_TWEAK_TAG, + NBuffer.concat(h ? [pubKey, h] : [pubKey]), + ); +} + 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]); } - -// todo: do not use ecc -function pointAddScalar(P: Buffer, h: Buffer): Buffer { - return NBuffer.from(ecc.pointAddScalar(P, h)); -} diff --git a/ts_src/types.ts b/ts_src/types.ts index 7fd2452f1..93446e920 100644 --- a/ts_src/types.ts +++ b/ts_src/types.ts @@ -71,15 +71,22 @@ export const Network = typeforce.compile({ wif: typeforce.UInt8, }); -export interface TweakedPublicKey { - parity: number; - x: Buffer; +export interface XOnlyPointAddTweakResult { + parity: 1 | 0; + xOnlyPubkey: Uint8Array; } export interface TaprootLeaf { output: Buffer; version?: number; } +export interface TinySecp256k1Interface { + isXOnlyPoint(p: Uint8Array): boolean; + xOnlyPointAddTweak( + p: Uint8Array, + tweak: Uint8Array, + ): XOnlyPointAddTweakResult | null; +} export const Buffer256bit = typeforce.BufferN(32); export const Hash160bit = typeforce.BufferN(20); From e64c2d87b21414b496f3d2d7dbac2aab011d9094 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 13 Jan 2022 16:56:14 +0200 Subject: [PATCH 32/53] chore: code format --- test/payments.spec.ts | 178 +++++++++++++++++++++--------------------- 1 file changed, 91 insertions(+), 87 deletions(-) diff --git a/test/payments.spec.ts b/test/payments.spec.ts index 957396199..b151a4d05 100644 --- a/test/payments.spec.ts +++ b/test/payments.spec.ts @@ -5,108 +5,112 @@ import { PaymentCreator, PaymentFactory } from '../src/payments'; import * as u from './payments.utils'; const payments = PaymentFactory(ecc); -['embed', 'p2ms', 'p2pk', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'].forEach(p => { - describe(p, () => { - //@ts-ignore - const fn: PaymentCreator = payments[p]; +['embed', 'p2ms', 'p2pk', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'].forEach( + p => { + describe(p, () => { + //@ts-ignore + const fn: PaymentCreator = payments[p]; - const fixtures = require('./fixtures/' + p); + const fixtures = require('./fixtures/' + p); - fixtures.valid.forEach((f: any) => { - it(f.description + ' as expected', () => { - const args = u.preform(f.arguments); - const actual = fn(args, f.options); + fixtures.valid.forEach((f: any) => { + it(f.description + ' as expected', () => { + const args = u.preform(f.arguments); + const actual = fn(args, f.options); - u.equate(actual, f.expected, f.arguments); - }); + u.equate(actual, f.expected, f.arguments); + }); - it(f.description + ' as expected (no validation)', () => { - const args = u.preform(f.arguments); - const actual = fn( - args, - Object.assign({}, f.options, { - validate: false, - }), - ); + it(f.description + ' as expected (no validation)', () => { + const args = u.preform(f.arguments); + const actual = fn( + args, + Object.assign({}, f.options, { + validate: false, + }), + ); - u.equate(actual, f.expected, f.arguments); + u.equate(actual, f.expected, f.arguments); + }); }); - }); - fixtures.invalid.forEach((f: any) => { - it( - 'throws ' + f.exception + (f.description ? 'for ' + f.description : ''), - () => { - const args = u.preform(f.arguments); + fixtures.invalid.forEach((f: any) => { + it( + 'throws ' + + f.exception + + (f.description ? 'for ' + f.description : ''), + () => { + const args = u.preform(f.arguments); - assert.throws(() => { - fn(args, f.options); - }, new RegExp(f.exception)); - }, - ); - }); + assert.throws(() => { + fn(args, f.options); + }, new RegExp(f.exception)); + }, + ); + }); - if (p === 'p2sh') { - const p2wsh = require('../src/payments/p2wsh').p2wsh; - const p2pk = require('../src/payments/p2pk').p2pk; - it('properly assembles nested p2wsh with names', () => { - const actual = fn({ - redeem: p2wsh({ - redeem: p2pk({ - pubkey: Buffer.from( - '03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058', - 'hex', - ), + if (p === 'p2sh') { + const p2wsh = require('../src/payments/p2wsh').p2wsh; + const p2pk = require('../src/payments/p2pk').p2pk; + it('properly assembles nested p2wsh with names', () => { + const actual = fn({ + redeem: p2wsh({ + redeem: p2pk({ + pubkey: Buffer.from( + '03e15819590382a9dd878f01e2f0cbce541564eb415e43b440472d883ecd283058', + 'hex', + ), + }), }), - }), + }); + assert.strictEqual( + actual.address, + '3MGbrbye4ttNUXM8WAvBFRKry4fkS9fjuw', + ); + assert.strictEqual(actual.name, 'p2sh-p2wsh-p2pk'); + assert.strictEqual(actual.redeem!.name, 'p2wsh-p2pk'); + assert.strictEqual(actual.redeem!.redeem!.name, 'p2pk'); }); - assert.strictEqual( - actual.address, - '3MGbrbye4ttNUXM8WAvBFRKry4fkS9fjuw', - ); - assert.strictEqual(actual.name, 'p2sh-p2wsh-p2pk'); - assert.strictEqual(actual.redeem!.name, 'p2wsh-p2pk'); - assert.strictEqual(actual.redeem!.redeem!.name, 'p2pk'); - }); - } + } - // cross-verify dynamically too - if (!fixtures.dynamic) return; - const { depends, details } = fixtures.dynamic; + // cross-verify dynamically too + if (!fixtures.dynamic) return; + const { depends, details } = fixtures.dynamic; - details.forEach((f: any) => { - const detail = u.preform(f); - const disabled: any = {}; - if (f.disabled) - f.disabled.forEach((k: string) => { - disabled[k] = true; - }); + details.forEach((f: any) => { + const detail = u.preform(f); + const disabled: any = {}; + if (f.disabled) + f.disabled.forEach((k: string) => { + disabled[k] = true; + }); - for (const key in depends) { - if (key in disabled) continue; - const dependencies = depends[key]; + for (const key in depends) { + if (key in disabled) continue; + const dependencies = depends[key]; - dependencies.forEach((dependency: any) => { - if (!Array.isArray(dependency)) dependency = [dependency]; + dependencies.forEach((dependency: any) => { + if (!Array.isArray(dependency)) dependency = [dependency]; - const args = {}; - dependency.forEach((d: any) => { - u.from(d, detail, args); - }); - const expected = u.from(key, detail); + const args = {}; + dependency.forEach((d: any) => { + u.from(d, detail, args); + }); + const expected = u.from(key, detail); - it( - f.description + - ', ' + - key + - ' derives from ' + - JSON.stringify(dependency), - () => { - u.equate(fn(args), expected); - }, - ); - }); - } + it( + f.description + + ', ' + + key + + ' derives from ' + + JSON.stringify(dependency), + () => { + u.equate(fn(args), expected); + }, + ); + }); + } + }); }); - }); -}); + }, +); From d987d8d48c5de939dfe62eebfed49d5a5019321f Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 13 Jan 2022 17:03:08 +0200 Subject: [PATCH 33/53] refactor: move taproot utils file --- src/payments/p2tr.js | 26 +++++++++++-------- .../taprootutils.d.ts} | 2 +- src/{taproot.js => payments/taprootutils.js} | 11 +++----- ts_src/payments/p2tr.ts | 4 +-- .../{taproot.ts => payments/taprootutils.ts} | 10 +++---- 5 files changed, 25 insertions(+), 28 deletions(-) rename src/{taproot.d.ts => payments/taprootutils.d.ts} (92%) rename src/{taproot.js => payments/taprootutils.js} (89%) rename ts_src/{taproot.ts => payments/taprootutils.ts} (90%) diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index 78ef95311..4c79a9b1d 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -5,7 +5,7 @@ const buffer_1 = require('buffer'); const networks_1 = require('../networks'); const bscript = require('../script'); const types_1 = require('../types'); -const taproot_1 = require('../taproot'); +const taprootutils_1 = require('./taprootutils'); const lazy = require('./lazy'); const bech32_1 = require('bech32'); const OPS = bscript.OPS; @@ -75,14 +75,15 @@ function p2tr(ecc) { }); lazy.prop(o, 'hash', () => { if (a.hash) return a.hash; - if (a.scriptsTree) return (0, taproot_1.toHashTree)(a.scriptsTree).hash; + if (a.scriptsTree) + return (0, taprootutils_1.toHashTree)(a.scriptsTree).hash; const w = _witness(); if (w && w.length > 1) { const controlBlock = w[w.length - 1]; const leafVersion = controlBlock[0] & 0b11111110; const script = w[w.length - 2]; - const leafHash = (0, taproot_1.tapLeafHash)(script, leafVersion); - return (0, taproot_1.rootHashFromPath)(controlBlock, leafHash); + const leafHash = (0, taprootutils_1.tapLeafHash)(script, leafVersion); + return (0, taprootutils_1.rootHashFromPath)(controlBlock, leafHash); } return null; }); @@ -119,12 +120,12 @@ function p2tr(ecc) { if (a.witness) return a.witness; if (a.scriptsTree && a.scriptLeaf && a.internalPubkey) { // todo: optimize/cache - const hashTree = (0, taproot_1.toHashTree)(a.scriptsTree); - const leafHash = (0, taproot_1.tapLeafHash)( + const hashTree = (0, taprootutils_1.toHashTree)(a.scriptsTree); + const leafHash = (0, taprootutils_1.tapLeafHash)( a.scriptLeaf.output, a.scriptLeaf.version, ); - const path = (0, taproot_1.findScriptPath)(hashTree, leafHash); + const path = (0, taprootutils_1.findScriptPath)(hashTree, leafHash); const outputKey = tweakKey(a.internalPubkey, hashTree.hash); if (!outputKey) return; const version = a.scriptLeaf.version || 0xc0; @@ -177,7 +178,7 @@ function p2tr(ecc) { throw new TypeError('Invalid pubkey for p2tr'); } if (a.hash && a.scriptsTree) { - const hash = (0, taproot_1.toHashTree)(a.scriptsTree).hash; + const hash = (0, taprootutils_1.toHashTree)(a.scriptsTree).hash; if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch'); } const witness = _witness(); @@ -216,8 +217,11 @@ function p2tr(ecc) { throw new TypeError('Invalid internalPubkey for p2tr witness'); const leafVersion = controlBlock[0] & 0b11111110; const script = witness[witness.length - 2]; - const leafHash = (0, taproot_1.tapLeafHash)(script, leafVersion); - const hash = (0, taproot_1.rootHashFromPath)(controlBlock, leafHash); + const leafHash = (0, taprootutils_1.tapLeafHash)(script, leafVersion); + const hash = (0, taprootutils_1.rootHashFromPath)( + controlBlock, + leafHash, + ); const outputKey = tweakKey(internalPubkey, hash); if (!outputKey) // todo: needs test data @@ -235,7 +239,7 @@ function p2tr(ecc) { if (!buffer_1.Buffer.isBuffer(pubKey)) return null; if (pubKey.length !== 32) return null; if (h && h.length !== 32) return null; - const tweakHash = (0, taproot_1.tapTweakHash)(pubKey, h); + const tweakHash = (0, taprootutils_1.tapTweakHash)(pubKey, h); const res = ecc.xOnlyPointAddTweak(pubKey, tweakHash); if (!res || res.xOnlyPubkey === null) return null; return { diff --git a/src/taproot.d.ts b/src/payments/taprootutils.d.ts similarity index 92% rename from src/taproot.d.ts rename to src/payments/taprootutils.d.ts index 37c118430..185ee79de 100644 --- a/src/taproot.d.ts +++ b/src/payments/taprootutils.d.ts @@ -1,5 +1,5 @@ /// -import { TaprootLeaf } from './types'; +import { TaprootLeaf } from '../types'; export declare function rootHashFromPath(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer; export interface HashTree { hash: Buffer; diff --git a/src/taproot.js b/src/payments/taprootutils.js similarity index 89% rename from src/taproot.js rename to src/payments/taprootutils.js index 98a7db377..d97169b1b 100644 --- a/src/taproot.js +++ b/src/payments/taprootutils.js @@ -2,11 +2,8 @@ Object.defineProperty(exports, '__esModule', { value: true }); exports.tapTweakHash = exports.tapLeafHash = exports.findScriptPath = exports.toHashTree = exports.rootHashFromPath = void 0; const buffer_1 = require('buffer'); -const bcrypto = require('./crypto'); -// todo: use varuint-bitcoin?? -const varuint = require('bip174/src/lib/converter/varint'); -// todo: !!!Temp, to be replaced. Only works because bip32 has it as dependecy. Linting will fail. -// const ecc = require('tiny-secp256k1'); +const bcrypto = require('../crypto'); +const bufferutils_1 = require('../bufferutils'); const LEAF_VERSION_TAPSCRIPT = 0xc0; const TAP_LEAF_TAG = 'TapLeaf'; const TAP_BRANCH_TAG = 'TapBranch'; @@ -92,8 +89,8 @@ function tapTweakHash(pubKey, h) { } exports.tapTweakHash = tapTweakHash; function serializeScript(s) { - const varintLen = varuint.encodingLength(s.length); + const varintLen = bufferutils_1.varuint.encodingLength(s.length); const buffer = buffer_1.Buffer.allocUnsafe(varintLen); // better - varuint.encode(s.length, buffer); + bufferutils_1.varuint.encode(s.length, buffer); return buffer_1.Buffer.concat([buffer, s]); } diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index 0bb68465d..4520e93b5 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -8,12 +8,12 @@ import { findScriptPath, tapLeafHash, tapTweakHash, -} from '../taproot'; +} from './taprootutils'; import { Payment, PaymentOpts, PaymentCreator } from './index'; import * as lazy from './lazy'; import { bech32m } from 'bech32'; -const OPS = bscript.OPS; +const OPS = bscript.OPS; const TAPROOT_VERSION = 0x01; const ANNEX_PREFIX = 0x50; diff --git a/ts_src/taproot.ts b/ts_src/payments/taprootutils.ts similarity index 90% rename from ts_src/taproot.ts rename to ts_src/payments/taprootutils.ts index ca8fe9fd5..94567b37c 100644 --- a/ts_src/taproot.ts +++ b/ts_src/payments/taprootutils.ts @@ -1,12 +1,8 @@ import { Buffer as NBuffer } from 'buffer'; -import * as bcrypto from './crypto'; +import * as bcrypto from '../crypto'; -// todo: use varuint-bitcoin?? -import * as varuint from 'bip174/src/lib/converter/varint'; -import { TaprootLeaf } from './types'; - -// todo: !!!Temp, to be replaced. Only works because bip32 has it as dependecy. Linting will fail. -// const ecc = require('tiny-secp256k1'); +import { varuint } from '../bufferutils'; +import { TaprootLeaf } from '../types'; const LEAF_VERSION_TAPSCRIPT = 0xc0; const TAP_LEAF_TAG = 'TapLeaf'; From 9f51a1a540512009c2f8134cc71ad539a40343ee Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 13 Jan 2022 17:04:06 +0200 Subject: [PATCH 34/53] refactor: move non-exported function to the bottom --- ts_src/payments/taprootutils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ts_src/payments/taprootutils.ts b/ts_src/payments/taprootutils.ts index 94567b37c..cbfc3bfac 100644 --- a/ts_src/payments/taprootutils.ts +++ b/ts_src/payments/taprootutils.ts @@ -93,10 +93,6 @@ export function tapLeafHash(script: Buffer, version?: number): Buffer { ); } -function tapBranchHash(a: Buffer, b: Buffer): Buffer { - return bcrypto.taggedHash(TAP_BRANCH_TAG, NBuffer.concat([a, b])); -} - export function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer { return bcrypto.taggedHash( TAP_TWEAK_TAG, @@ -104,6 +100,10 @@ export function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer { ); } +function tapBranchHash(a: Buffer, b: Buffer): Buffer { + return bcrypto.taggedHash(TAP_BRANCH_TAG, NBuffer.concat([a, b])); +} + function serializeScript(s: Buffer): Buffer { const varintLen = varuint.encodingLength(s.length); const buffer = NBuffer.allocUnsafe(varintLen); // better From bc6358f1e9cb60ca49c171edf78520aca40a90c4 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 13 Jan 2022 17:14:33 +0200 Subject: [PATCH 35/53] fix: lint & gitdiff issues --- src/payments/taprootutils.js | 6 +++--- test/payments.spec.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/payments/taprootutils.js b/src/payments/taprootutils.js index d97169b1b..fbcf90a7a 100644 --- a/src/payments/taprootutils.js +++ b/src/payments/taprootutils.js @@ -78,9 +78,6 @@ function tapLeafHash(script, version) { ); } exports.tapLeafHash = tapLeafHash; -function tapBranchHash(a, b) { - return bcrypto.taggedHash(TAP_BRANCH_TAG, buffer_1.Buffer.concat([a, b])); -} function tapTweakHash(pubKey, h) { return bcrypto.taggedHash( TAP_TWEAK_TAG, @@ -88,6 +85,9 @@ function tapTweakHash(pubKey, h) { ); } exports.tapTweakHash = tapTweakHash; +function tapBranchHash(a, b) { + return bcrypto.taggedHash(TAP_BRANCH_TAG, 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 diff --git a/test/payments.spec.ts b/test/payments.spec.ts index b151a4d05..139594af2 100644 --- a/test/payments.spec.ts +++ b/test/payments.spec.ts @@ -8,7 +8,7 @@ const payments = PaymentFactory(ecc); ['embed', 'p2ms', 'p2pk', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'].forEach( p => { describe(p, () => { - //@ts-ignore + // @ts-ignore const fn: PaymentCreator = payments[p]; const fixtures = require('./fixtures/' + p); From f61371c7d573f0c0b659a4fb9e502f42a7583ef7 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 13 Jan 2022 18:39:06 +0200 Subject: [PATCH 36/53] chore: removed un-used exports --- src/types.d.ts | 4 ---- src/types.js | 18 +++++++----------- ts_src/types.ts | 8 ++------ 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/src/types.d.ts b/src/types.d.ts index 014c2b910..aefc6bed7 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,9 +1,5 @@ /// -import { Buffer as NBuffer } from 'buffer'; export declare const typeforce: any; -export declare const ZERO32: NBuffer; -export declare const EC_P: NBuffer; -export declare const GROUP_ORDER: NBuffer; export declare function isPoint(p: Buffer | number | undefined | null): boolean; export declare function UInt31(value: number): boolean; export declare function BIP32Path(value: string): boolean; diff --git a/src/types.js b/src/types.js index a8acbef86..a6d1efa16 100644 --- a/src/types.js +++ b/src/types.js @@ -1,30 +1,26 @@ '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.GROUP_ORDER = exports.EC_P = exports.ZERO32 = 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.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'); -exports.ZERO32 = buffer_1.Buffer.alloc(32, 0); -exports.EC_P = buffer_1.Buffer.from( +const ZERO32 = buffer_1.Buffer.alloc(32, 0); +const EC_P = buffer_1.Buffer.from( 'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f', 'hex', ); -exports.GROUP_ORDER = buffer_1.Buffer.from( - 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', - 'hex', -); function isPoint(p) { if (!buffer_1.Buffer.isBuffer(p)) return false; if (p.length < 33) return false; const t = p[0]; const x = p.slice(1, 33); - if (x.compare(exports.ZERO32) === 0) return false; - if (x.compare(exports.EC_P) >= 0) return false; + if (x.compare(ZERO32) === 0) return false; + if (x.compare(EC_P) >= 0) return false; if ((t === 0x02 || t === 0x03) && p.length === 33) { return true; } const y = p.slice(33); - if (y.compare(exports.ZERO32) === 0) return false; - if (y.compare(exports.EC_P) >= 0) return false; + if (y.compare(ZERO32) === 0) return false; + if (y.compare(EC_P) >= 0) return false; if (t === 0x04 && p.length === 65) return true; return false; } diff --git a/ts_src/types.ts b/ts_src/types.ts index 93446e920..840ab9be2 100644 --- a/ts_src/types.ts +++ b/ts_src/types.ts @@ -2,15 +2,11 @@ import { Buffer as NBuffer } from 'buffer'; export const typeforce = require('typeforce'); -export const ZERO32 = NBuffer.alloc(32, 0); -export const EC_P = NBuffer.from( +const ZERO32 = NBuffer.alloc(32, 0); +const EC_P = NBuffer.from( 'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f', 'hex', ); -export const GROUP_ORDER = NBuffer.from( - 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', - 'hex', -); export function isPoint(p: Buffer | number | undefined | null): boolean { if (!NBuffer.isBuffer(p)) return false; From 4573e6c0ec82a845fd2cfc48653ea8b62c6d47e2 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 13 Jan 2022 18:40:55 +0200 Subject: [PATCH 37/53] chore: add docs, simplify code --- src/payments/taprootutils.d.ts | 16 +++++++++++++++- src/payments/taprootutils.js | 26 +++++++++++++++++++------- ts_src/payments/taprootutils.ts | 27 ++++++++++++++++++++------- 3 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/payments/taprootutils.d.ts b/src/payments/taprootutils.d.ts index 185ee79de..f68a37495 100644 --- a/src/payments/taprootutils.d.ts +++ b/src/payments/taprootutils.d.ts @@ -6,7 +6,21 @@ export interface HashTree { left?: HashTree; right?: HashTree; } -export declare function toHashTree(scripts: TaprootLeaf[]): HashTree; +/** + * Build the hash tree from the scripts binary tree. + * The binary tree can be balanced or not. + * @param scriptsTree - is a list representing a binary tree where an element can be: + * - a taproot leaf [(output, version)], or + * - a pair of two taproot leafs [(output, version), (output, version)], or + * - one taproot leaf and a list of elements + */ +export declare function toHashTree(scriptsTree: TaprootLeaf[]): HashTree; +/** + * Given a MAST tree, it finds the path of a particular hash. + * @param node - the root of the tree + * @param hash - the hash to search for + * @returns - and array of hashes representing the path, or an empty array if no pat is found + */ export declare function findScriptPath(node: HashTree, hash: Buffer): Buffer[]; export declare function tapLeafHash(script: Buffer, version?: number): Buffer; export declare function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer; diff --git a/src/payments/taprootutils.js b/src/payments/taprootutils.js index fbcf90a7a..2da6a4b7d 100644 --- a/src/payments/taprootutils.js +++ b/src/payments/taprootutils.js @@ -23,9 +23,17 @@ function rootHashFromPath(controlBlock, tapLeafMsg) { return k[m]; } exports.rootHashFromPath = rootHashFromPath; -function toHashTree(scripts) { - if (scripts.length === 1) { - const script = scripts[0]; +/** + * Build the hash tree from the scripts binary tree. + * The binary tree can be balanced or not. + * @param scriptsTree - is a list representing a binary tree where an element can be: + * - a taproot leaf [(output, version)], or + * - a pair of two taproot leafs [(output, version), (output, version)], or + * - one taproot leaf and a list of elements + */ +function toHashTree(scriptsTree) { + if (scriptsTree.length === 1) { + const script = scriptsTree[0]; if (Array.isArray(script)) { return toHashTree(script); } @@ -36,10 +44,8 @@ function toHashTree(scripts) { hash: tapLeafHash(script.output, script.version), }; } - // todo: this is a binary tree, use zero an one index - const half = Math.trunc(scripts.length / 2); - const left = toHashTree(scripts.slice(0, half)); - const right = toHashTree(scripts.slice(half)); + const left = toHashTree([scriptsTree[0]]); + const right = toHashTree([scriptsTree[1]]); let leftHash = left.hash; let rightHash = right.hash; if (leftHash.compare(rightHash) === 1) @@ -51,6 +57,12 @@ function toHashTree(scripts) { }; } exports.toHashTree = toHashTree; +/** + * Given a MAST tree, it finds the path of a particular hash. + * @param node - the root of the tree + * @param hash - the hash to search for + * @returns - and array of hashes representing the path, or an empty array if no pat is found + */ function findScriptPath(node, hash) { if (node.left) { if (node.left.hash.equals(hash)) return node.right ? [node.right.hash] : []; diff --git a/ts_src/payments/taprootutils.ts b/ts_src/payments/taprootutils.ts index cbfc3bfac..83ed9b14c 100644 --- a/ts_src/payments/taprootutils.ts +++ b/ts_src/payments/taprootutils.ts @@ -36,9 +36,17 @@ export interface HashTree { right?: HashTree; } -export function toHashTree(scripts: TaprootLeaf[]): HashTree { - if (scripts.length === 1) { - const script = scripts[0]; +/** + * Build the hash tree from the scripts binary tree. + * The binary tree can be balanced or not. + * @param scriptsTree - is a list representing a binary tree where an element can be: + * - a taproot leaf [(output, version)], or + * - a pair of two taproot leafs [(output, version), (output, version)], or + * - one taproot leaf and a list of elements + */ +export function toHashTree(scriptsTree: TaprootLeaf[]): HashTree { + if (scriptsTree.length === 1) { + const script = scriptsTree[0]; if (Array.isArray(script)) { return toHashTree(script); } @@ -50,10 +58,9 @@ export function toHashTree(scripts: TaprootLeaf[]): HashTree { hash: tapLeafHash(script.output, script.version), }; } - // todo: this is a binary tree, use zero an one index - const half = Math.trunc(scripts.length / 2); - const left = toHashTree(scripts.slice(0, half)); - const right = toHashTree(scripts.slice(half)); + + const left = toHashTree([scriptsTree[0]]); + const right = toHashTree([scriptsTree[1]]); let leftHash = left.hash; let rightHash = right.hash; @@ -67,6 +74,12 @@ export function toHashTree(scripts: TaprootLeaf[]): HashTree { }; } +/** + * Given a MAST tree, it finds the path of a particular hash. + * @param node - the root of the tree + * @param hash - the hash to search for + * @returns - and array of hashes representing the path, or an empty array if no pat is found + */ export function findScriptPath(node: HashTree, hash: Buffer): Buffer[] { if (node.left) { if (node.left.hash.equals(hash)) return node.right ? [node.right.hash] : []; From ddc1e8dc7bf33d41089336f2a2f6f665912f5125 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 14 Jan 2022 16:18:13 +0200 Subject: [PATCH 38/53] feat: pass the ECC library as an optional parameter to p2tr --- src/payments/index.d.ts | 16 +- src/payments/index.js | 24 +- src/payments/p2tr.d.ts | 4 +- src/payments/p2tr.js | 458 ++++++++++++++++----------------- src/payments/testecc.js | 2 + test/payments.spec.ts | 22 +- ts_src/payments/index.ts | 28 +-- ts_src/payments/p2tr.ts | 502 +++++++++++++++++++------------------ ts_src/payments/testecc.ts | 2 + 9 files changed, 523 insertions(+), 535 deletions(-) diff --git a/src/payments/index.d.ts b/src/payments/index.d.ts index 72db5de25..a72a8ea41 100644 --- a/src/payments/index.d.ts +++ b/src/payments/index.d.ts @@ -8,6 +8,7 @@ import { p2pkh } from './p2pkh'; import { p2sh } from './p2sh'; import { p2wpkh } from './p2wpkh'; import { p2wsh } from './p2wsh'; +import { p2tr } from './p2tr'; export interface Payment { name?: string; network?: Network; @@ -28,24 +29,13 @@ export interface Payment { scriptLeaf?: TaprootLeaf; witness?: Buffer[]; } -export declare type PaymentCreator = (a: Payment, opts?: PaymentOpts) => Payment; +export declare type PaymentCreator = (a: Payment, opts?: PaymentOpts, eccLib?: TinySecp256k1Interface) => Payment; export declare type PaymentFunction = () => Payment; export interface PaymentOpts { validate?: boolean; allowIncomplete?: boolean; } -export interface PaymentAPI { - embed: PaymentCreator; - p2ms: PaymentCreator; - p2pk: PaymentCreator; - p2pkh: PaymentCreator; - p2sh: PaymentCreator; - p2wpkh: PaymentCreator; - p2wsh: PaymentCreator; - p2tr: PaymentCreator; -} export declare type StackElement = Buffer | number; export declare type Stack = StackElement[]; export declare type StackFunction = () => Stack; -export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh, PaymentFactory }; -export default function PaymentFactory(ecc: TinySecp256k1Interface): PaymentAPI; +export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh, p2tr }; diff --git a/src/payments/index.js b/src/payments/index.js index 779df03fa..9ce55f859 100644 --- a/src/payments/index.js +++ b/src/payments/index.js @@ -1,6 +1,6 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -exports.PaymentFactory = exports.p2wsh = exports.p2wpkh = exports.p2sh = exports.p2pkh = exports.p2pk = exports.p2ms = exports.embed = void 0; +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, @@ -51,21 +51,11 @@ Object.defineProperty(exports, 'p2wsh', { }, }); const p2tr_1 = require('./p2tr'); -const testecc_1 = require('./testecc'); -function PaymentFactory(ecc) { - (0, testecc_1.testEcc)(ecc); - return { - embed: embed_1.p2data, - p2ms: p2ms_1.p2ms, - p2pk: p2pk_1.p2pk, - p2pkh: p2pkh_1.p2pkh, - p2sh: p2sh_1.p2sh, - p2wpkh: p2wpkh_1.p2wpkh, - p2wsh: p2wsh_1.p2wsh, - p2tr: (0, p2tr_1.p2tr)(ecc), - }; -} -exports.default = PaymentFactory; -exports.PaymentFactory = PaymentFactory; +Object.defineProperty(exports, 'p2tr', { + enumerable: true, + get: function() { + return p2tr_1.p2tr; + }, +}); // TODO // witness commitment diff --git a/src/payments/p2tr.d.ts b/src/payments/p2tr.d.ts index e4d4c8a9b..3268bf450 100644 --- a/src/payments/p2tr.d.ts +++ b/src/payments/p2tr.d.ts @@ -1,3 +1,3 @@ import { TinySecp256k1Interface } from '../types'; -import { PaymentCreator } from './index'; -export declare function p2tr(ecc: TinySecp256k1Interface): PaymentCreator; +import { Payment, PaymentOpts } from './index'; +export declare function p2tr(a: Payment, opts?: PaymentOpts, eccLib?: TinySecp256k1Interface): Payment; diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index 4c79a9b1d..1517ef0c6 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -8,244 +8,246 @@ const types_1 = require('../types'); const taprootutils_1 = require('./taprootutils'); const lazy = require('./lazy'); const bech32_1 = require('bech32'); +const testecc_1 = require('./testecc'); const OPS = bscript.OPS; const TAPROOT_VERSION = 0x01; const ANNEX_PREFIX = 0x50; -function p2tr(ecc) { - return (a, opts) => { +function p2tr(a, opts, eccLib) { + if ( + !a.address && + !a.output && + !a.pubkey && + !a.output && + !a.internalPubkey && + !(a.witness && a.witness.length > 1) + ) + throw new TypeError('Not enough data'); + opts = Object.assign({ validate: true }, opts || {}); + const _ecc = lazy.value(() => { + if (!eccLib) throw new Error('ECC Library is missing for p2tr.'); + (0, testecc_1.testEcc)(eccLib); + return eccLib; + }); + (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(bscript.isCanonicalScriptSignature), + witness: types_1.typeforce.maybe( + types_1.typeforce.arrayOf(types_1.typeforce.Buffer), + ), + // scriptsTree: typef.maybe(typef.TaprootNode), // use merkel.isMast ? + scriptLeaf: types_1.typeforce.maybe({ + version: types_1.typeforce.maybe(types_1.typeforce.Number), + output: types_1.typeforce.maybe(types_1.typeforce.Buffer), + }), + }, + 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), + }; + }); + const _witness = lazy.value(() => { + if (!a.witness || !a.witness.length) return; if ( - !a.address && - !a.output && - !a.pubkey && - !a.output && - !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(bscript.isCanonicalScriptSignature), - witness: types_1.typeforce.maybe( - types_1.typeforce.arrayOf(types_1.typeforce.Buffer), - ), - // scriptsTree: typef.maybe(typef.TaprootNode), // use merkel.isMast ? - scriptLeaf: types_1.typeforce.maybe({ - version: types_1.typeforce.maybe(types_1.typeforce.Number), - output: types_1.typeforce.maybe(types_1.typeforce.Buffer), - }), - }, - 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), - }; - }); - const _witness = lazy.value(() => { - if (!a.witness || !a.witness.length) return; + a.witness.length >= 2 && + a.witness[a.witness.length - 1][0] === ANNEX_PREFIX + ) { + // remove annex, ignored by taproot + return a.witness.slice(0, -1); + } + return a.witness.slice(); + }); + 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_VERSION); + return bech32_1.bech32m.encode(network.bech32, words); + }); + lazy.prop(o, 'hash', () => { + if (a.hash) return a.hash; + if (a.scriptsTree) + return (0, taprootutils_1.toHashTree)(a.scriptsTree).hash; + const w = _witness(); + if (w && w.length > 1) { + const controlBlock = w[w.length - 1]; + const leafVersion = controlBlock[0] & 0b11111110; + const script = w[w.length - 2]; + const leafHash = (0, taprootutils_1.tapLeafHash)(script, 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, 'scriptLeaf', () => { + if (a.scriptLeaf) return a.scriptLeaf; + }); + lazy.prop(o, 'pubkey', () => { + if (a.pubkey) return a.pubkey; + if (a.output) return a.output.slice(2); + if (a.address) return _address().data; + if (o.internalPubkey) { + const tweakedKey = tweakKey(o.internalPubkey, o.hash, _ecc()); + 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.witness || a.witness.length !== 1) return; + return a.witness[0]; + }); + lazy.prop(o, 'input', () => { + // todo + }); + lazy.prop(o, 'witness', () => { + if (a.witness) return a.witness; + if (a.scriptsTree && a.scriptLeaf && a.internalPubkey) { + // todo: optimize/cache + const hashTree = (0, taprootutils_1.toHashTree)(a.scriptsTree); + const leafHash = (0, taprootutils_1.tapLeafHash)( + a.scriptLeaf.output, + a.scriptLeaf.version, + ); + const path = (0, taprootutils_1.findScriptPath)(hashTree, leafHash); + const outputKey = tweakKey(a.internalPubkey, hashTree.hash, _ecc()); + if (!outputKey) return; + const version = a.scriptLeaf.version || 0xc0; + const controlBock = buffer_1.Buffer.concat( + [ + buffer_1.Buffer.from([version | outputKey.parity]), + a.internalPubkey, + ].concat(path.reverse()), + ); + return [a.scriptLeaf.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_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.witness.length >= 2 && - a.witness[a.witness.length - 1][0] === ANNEX_PREFIX - ) { - // remove annex, ignored by taproot - return a.witness.slice(0, -1); - } - return a.witness.slice(); - }); - 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_VERSION); - return bech32_1.bech32m.encode(network.bech32, words); - }); - lazy.prop(o, 'hash', () => { - if (a.hash) return a.hash; - if (a.scriptsTree) - return (0, taprootutils_1.toHashTree)(a.scriptsTree).hash; - const w = _witness(); - if (w && w.length > 1) { - const controlBlock = w[w.length - 1]; + 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, _ecc()); + if (pubkey.length > 0 && !pubkey.equals(tweakedKey.x)) + throw new TypeError('Pubkey mismatch'); + else pubkey = tweakedKey.x; + } + if (pubkey && pubkey.length) { + if (!_ecc().isXOnlyPoint(pubkey)) + throw new TypeError('Invalid pubkey for p2tr'); + } + if (a.hash && a.scriptsTree) { + const hash = (0, taprootutils_1.toHashTree)(a.scriptsTree).hash; + if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch'); + } + const witness = _witness(); + if (witness && witness.length) { + if (witness.length === 1) { + // key spending + if (a.signature && !a.signature.equals(witness[0])) + throw new TypeError('Signature mismatch'); + // todo: recheck + // if (!bscript.isSchnorSignature(a.pubkey, a.witness[0])) + // throw new TypeError('Witness has invalid signature'); + } 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 (!_ecc().isXOnlyPoint(internalPubkey)) + throw new TypeError('Invalid internalPubkey for p2tr witness'); const leafVersion = controlBlock[0] & 0b11111110; - const script = w[w.length - 2]; + const script = witness[witness.length - 2]; const leafHash = (0, taprootutils_1.tapLeafHash)(script, 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, 'scriptLeaf', () => { - if (a.scriptLeaf) return a.scriptLeaf; - }); - 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.witness || a.witness.length !== 1) return; - return a.witness[0]; - }); - lazy.prop(o, 'input', () => { - // todo - }); - lazy.prop(o, 'witness', () => { - if (a.witness) return a.witness; - if (a.scriptsTree && a.scriptLeaf && a.internalPubkey) { - // todo: optimize/cache - const hashTree = (0, taprootutils_1.toHashTree)(a.scriptsTree); - const leafHash = (0, taprootutils_1.tapLeafHash)( - a.scriptLeaf.output, - a.scriptLeaf.version, + const hash = (0, taprootutils_1.rootHashFromPath)( + controlBlock, + leafHash, ); - const path = (0, taprootutils_1.findScriptPath)(hashTree, leafHash); - const outputKey = tweakKey(a.internalPubkey, hashTree.hash); - if (!outputKey) return; - const version = a.scriptLeaf.version || 0xc0; - const controlBock = buffer_1.Buffer.concat( - [ - buffer_1.Buffer.from([version | outputKey.parity]), - a.internalPubkey, - ].concat(path.reverse()), - ); - return [a.scriptLeaf.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_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 (!ecc.isXOnlyPoint(pubkey)) - throw new TypeError('Invalid pubkey for p2tr'); - } - if (a.hash && a.scriptsTree) { - const hash = (0, taprootutils_1.toHashTree)(a.scriptsTree).hash; - if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch'); - } - const witness = _witness(); - if (witness && witness.length) { - if (witness.length === 1) { - // key spending - if (a.signature && !a.signature.equals(witness[0])) - throw new TypeError('Signature mismatch'); - // todo: recheck - // if (!bscript.isSchnorSignature(a.pubkey, a.witness[0])) - // throw new TypeError('Witness has invalid signature'); - } 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 (!ecc.isXOnlyPoint(internalPubkey)) - throw new TypeError('Invalid internalPubkey for p2tr witness'); - const leafVersion = controlBlock[0] & 0b11111110; - const script = witness[witness.length - 2]; - const leafHash = (0, taprootutils_1.tapLeafHash)(script, 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'); - } + const outputKey = tweakKey(internalPubkey, hash, _ecc()); + 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); - }; - 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 = ecc.xOnlyPointAddTweak(pubKey, tweakHash); - if (!res || res.xOnlyPubkey === null) return null; - return { - parity: res.parity, - x: buffer_1.Buffer.from(res.xOnlyPubkey), - }; } + return Object.assign(o, a); } exports.p2tr = p2tr; +function tweakKey(pubKey, h, eccLib) { + if (!buffer_1.Buffer.isBuffer(pubKey)) return null; + if (pubKey.length !== 32) return null; + if (h && h.length !== 32) return null; + const tweakHash = (0, taprootutils_1.tapTweakHash)(pubKey, h); + const res = eccLib.xOnlyPointAddTweak(pubKey, tweakHash); + if (!res || res.xOnlyPubkey === null) return null; + return { + parity: res.parity, + x: buffer_1.Buffer.from(res.xOnlyPubkey), + }; +} diff --git a/src/payments/testecc.js b/src/payments/testecc.js index 9bdc62031..77e2b3e15 100644 --- a/src/payments/testecc.js +++ b/src/payments/testecc.js @@ -3,6 +3,7 @@ Object.defineProperty(exports, '__esModule', { value: true }); exports.testEcc = void 0; const h = hex => Buffer.from(hex, 'hex'); function testEcc(ecc) { + assert(typeof ecc.isXOnlyPoint === 'function'); assert( ecc.isXOnlyPoint( h('79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), @@ -33,6 +34,7 @@ function testEcc(ecc) { h('fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f'), ), ); + assert(typeof ecc.xOnlyPointAddTweak === 'function'); tweakAddVectors.forEach(t => { const r = ecc.xOnlyPointAddTweak(h(t.pubkey), h(t.tweak)); if (t.result === null) { diff --git a/test/payments.spec.ts b/test/payments.spec.ts index 139594af2..e5227124e 100644 --- a/test/payments.spec.ts +++ b/test/payments.spec.ts @@ -1,22 +1,29 @@ import * as assert from 'assert'; -import { describe, it } from 'mocha'; import * as ecc from 'tiny-secp256k1'; -import { PaymentCreator, PaymentFactory } from '../src/payments'; +import { describe, it } from 'mocha'; +import { PaymentCreator } from '../src/payments'; import * as u from './payments.utils'; +import { TinySecp256k1Interface } from '../src/types'; -const payments = PaymentFactory(ecc); ['embed', 'p2ms', 'p2pk', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'].forEach( p => { describe(p, () => { - // @ts-ignore - const fn: PaymentCreator = payments[p]; + let fn: PaymentCreator; + const eccLib: TinySecp256k1Interface | undefined = + p === 'p2tr' ? ecc : undefined; + const payment = require('../src/payments/' + p); + if (p === 'embed') { + fn = payment.p2data; + } else { + fn = payment[p]; + } const fixtures = require('./fixtures/' + p); fixtures.valid.forEach((f: any) => { it(f.description + ' as expected', () => { const args = u.preform(f.arguments); - const actual = fn(args, f.options); + const actual = fn(args, f.options, eccLib); u.equate(actual, f.expected, f.arguments); }); @@ -28,6 +35,7 @@ const payments = PaymentFactory(ecc); Object.assign({}, f.options, { validate: false, }), + eccLib, ); u.equate(actual, f.expected, f.arguments); @@ -43,7 +51,7 @@ const payments = PaymentFactory(ecc); const args = u.preform(f.arguments); assert.throws(() => { - fn(args, f.options); + fn(args, f.options, eccLib); }, new RegExp(f.exception)); }, ); diff --git a/ts_src/payments/index.ts b/ts_src/payments/index.ts index f7c5dbadd..f2dcfb939 100644 --- a/ts_src/payments/index.ts +++ b/ts_src/payments/index.ts @@ -8,7 +8,6 @@ import { p2sh } from './p2sh'; import { p2wpkh } from './p2wpkh'; import { p2wsh } from './p2wsh'; import { p2tr } from './p2tr'; -import { testEcc } from './testecc'; export interface Payment { name?: string; @@ -31,7 +30,11 @@ export interface Payment { witness?: Buffer[]; } -export type PaymentCreator = (a: Payment, opts?: PaymentOpts) => Payment; +export type PaymentCreator = ( + a: Payment, + opts?: PaymentOpts, + eccLib?: TinySecp256k1Interface, +) => Payment; export type PaymentFunction = () => Payment; @@ -40,30 +43,11 @@ export interface PaymentOpts { allowIncomplete?: boolean; } -export interface PaymentAPI { - embed: PaymentCreator; - p2ms: PaymentCreator; - p2pk: PaymentCreator; - p2pkh: PaymentCreator; - p2sh: PaymentCreator; - p2wpkh: PaymentCreator; - p2wsh: PaymentCreator; - p2tr: PaymentCreator; -} - export type StackElement = Buffer | number; export type Stack = StackElement[]; export type StackFunction = () => Stack; -export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh, PaymentFactory }; - -export default function PaymentFactory( - ecc: TinySecp256k1Interface, -): PaymentAPI { - testEcc(ecc); - - return { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh, p2tr: p2tr(ecc) }; -} +export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh, p2tr }; // TODO // witness commitment diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index 4520e93b5..1ecce1432 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -9,275 +9,285 @@ import { tapLeafHash, tapTweakHash, } from './taprootutils'; -import { Payment, PaymentOpts, PaymentCreator } from './index'; +import { Payment, PaymentOpts } from './index'; import * as lazy from './lazy'; import { bech32m } from 'bech32'; +import { testEcc } from './testecc'; const OPS = bscript.OPS; const TAPROOT_VERSION = 0x01; const ANNEX_PREFIX = 0x50; -export function p2tr(ecc: TinySecp256k1Interface): PaymentCreator { - return (a: Payment, opts?: PaymentOpts): Payment => { - if ( - !a.address && - !a.output && - !a.pubkey && - !a.output && - !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)), - pubkey: typef.maybe(typef.BufferN(32)), - signature: typef.maybe(bscript.isCanonicalScriptSignature), - witness: typef.maybe(typef.arrayOf(typef.Buffer)), - // scriptsTree: typef.maybe(typef.TaprootNode), // use merkel.isMast ? - scriptLeaf: typef.maybe({ - version: typef.maybe(typef.Number), - output: typef.maybe(typef.Buffer), - }), - }, - 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), - }; - }); - - 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 - ) { - // remove annex, ignored by taproot - return a.witness.slice(0, -1); - } - return a.witness.slice(); - }); - - 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_VERSION); - return bech32m.encode(network.bech32, words); - }); - - lazy.prop(o, 'hash', () => { - if (a.hash) return a.hash; - if (a.scriptsTree) return toHashTree(a.scriptsTree).hash; - const w = _witness(); - if (w && w.length > 1) { - const controlBlock = w[w.length - 1]; - const leafVersion = controlBlock[0] & 0b11111110; - const script = w[w.length - 2]; - const leafHash = tapLeafHash(script, 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, 'scriptLeaf', () => { - if (a.scriptLeaf) return a.scriptLeaf; - }); - 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.witness || a.witness.length !== 1) return; - return a.witness[0]; - }); - lazy.prop(o, 'input', () => { - // todo - }); - lazy.prop(o, 'witness', () => { - if (a.witness) return a.witness; - if (a.scriptsTree && a.scriptLeaf && a.internalPubkey) { - // todo: optimize/cache - const hashTree = toHashTree(a.scriptsTree); - const leafHash = tapLeafHash(a.scriptLeaf.output, a.scriptLeaf.version); - const path = findScriptPath(hashTree, leafHash); - const outputKey = tweakKey(a.internalPubkey, hashTree.hash); - if (!outputKey) return; - const version = a.scriptLeaf.version || 0xc0; - const controlBock = NBuffer.concat( - [NBuffer.from([version | outputKey.parity]), a.internalPubkey].concat( - path.reverse(), - ), - ); - return [a.scriptLeaf.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_VERSION) - throw new TypeError('Invalid address version'); - if (_address().data.length !== 32) - throw new TypeError('Invalid address data'); - pubkey = _address().data; - } +export function p2tr( + a: Payment, + opts?: PaymentOpts, + eccLib?: TinySecp256k1Interface, +): Payment { + if ( + !a.address && + !a.output && + !a.pubkey && + !a.output && + !a.internalPubkey && + !(a.witness && a.witness.length > 1) + ) + throw new TypeError('Not enough data'); + + opts = Object.assign({ validate: true }, opts || {}); + + const _ecc = lazy.value(() => { + if (!eccLib) throw new Error('ECC Library is missing for p2tr.'); + + testEcc(eccLib); + return eccLib; + }); + + 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)), + pubkey: typef.maybe(typef.BufferN(32)), + signature: typef.maybe(bscript.isCanonicalScriptSignature), + witness: typef.maybe(typef.arrayOf(typef.Buffer)), + // scriptsTree: typef.maybe(typef.TaprootNode), // use merkel.isMast ? + scriptLeaf: typef.maybe({ + version: typef.maybe(typef.Number), + output: typef.maybe(typef.Buffer), + }), + }, + 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), + }; + }); - if (a.pubkey) { - if (pubkey.length > 0 && !pubkey.equals(a.pubkey)) - throw new TypeError('Pubkey mismatch'); - else pubkey = a.pubkey; - } + 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 + ) { + // remove annex, ignored by taproot + return a.witness.slice(0, -1); + } + return a.witness.slice(); + }); + + 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_VERSION); + return bech32m.encode(network.bech32, words); + }); + + lazy.prop(o, 'hash', () => { + if (a.hash) return a.hash; + if (a.scriptsTree) return toHashTree(a.scriptsTree).hash; + const w = _witness(); + if (w && w.length > 1) { + const controlBlock = w[w.length - 1]; + const leafVersion = controlBlock[0] & 0b11111110; + const script = w[w.length - 2]; + const leafHash = tapLeafHash(script, 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, 'scriptLeaf', () => { + if (a.scriptLeaf) return a.scriptLeaf; + }); + lazy.prop(o, 'pubkey', () => { + if (a.pubkey) return a.pubkey; + if (a.output) return a.output.slice(2); + if (a.address) return _address().data; + if (o.internalPubkey) { + const tweakedKey = tweakKey(o.internalPubkey, o.hash, _ecc()); + 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.witness || a.witness.length !== 1) return; + return a.witness[0]; + }); + lazy.prop(o, 'input', () => { + // todo + }); + lazy.prop(o, 'witness', () => { + if (a.witness) return a.witness; + if (a.scriptsTree && a.scriptLeaf && a.internalPubkey) { + // todo: optimize/cache + const hashTree = toHashTree(a.scriptsTree); + const leafHash = tapLeafHash(a.scriptLeaf.output, a.scriptLeaf.version); + const path = findScriptPath(hashTree, leafHash); + const outputKey = tweakKey(a.internalPubkey, hashTree.hash, _ecc()); + if (!outputKey) return; + const version = a.scriptLeaf.version || 0xc0; + const controlBock = NBuffer.concat( + [NBuffer.from([version | outputKey.parity]), a.internalPubkey].concat( + path.reverse(), + ), + ); + return [a.scriptLeaf.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_VERSION) + throw new TypeError('Invalid address version'); + if (_address().data.length !== 32) + throw new TypeError('Invalid address data'); + pubkey = _address().data; + } - 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.pubkey) { + if (pubkey.length > 0 && !pubkey.equals(a.pubkey)) + throw new TypeError('Pubkey mismatch'); + else pubkey = a.pubkey; + } - 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 (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 (pubkey && pubkey.length) { - if (!ecc.isXOnlyPoint(pubkey)) - throw new TypeError('Invalid pubkey for p2tr'); - } + if (a.internalPubkey) { + const tweakedKey = tweakKey(a.internalPubkey, o.hash, _ecc()); + if (pubkey.length > 0 && !pubkey.equals(tweakedKey!.x)) + throw new TypeError('Pubkey mismatch'); + else pubkey = tweakedKey!.x; + } - if (a.hash && a.scriptsTree) { - const hash = toHashTree(a.scriptsTree).hash; - if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch'); - } + if (pubkey && pubkey.length) { + if (!_ecc().isXOnlyPoint(pubkey)) + throw new TypeError('Invalid pubkey for p2tr'); + } - const witness = _witness(); - - if (witness && witness.length) { - if (witness.length === 1) { - // key spending - if (a.signature && !a.signature.equals(witness[0])) - throw new TypeError('Signature mismatch'); - // todo: recheck - // if (!bscript.isSchnorSignature(a.pubkey, a.witness[0])) - // throw new TypeError('Witness has invalid signature'); - } 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 (!ecc.isXOnlyPoint(internalPubkey)) - throw new TypeError('Invalid internalPubkey for p2tr witness'); - - const leafVersion = controlBlock[0] & 0b11111110; - const script = witness[witness.length - 2]; - - const leafHash = tapLeafHash(script, 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'); - } - } + if (a.hash && a.scriptsTree) { + const hash = toHashTree(a.scriptsTree).hash; + if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch'); } - return Object.assign(o, a); - }; + const witness = _witness(); + + if (witness && witness.length) { + if (witness.length === 1) { + // key spending + if (a.signature && !a.signature.equals(witness[0])) + throw new TypeError('Signature mismatch'); + // todo: recheck + // if (!bscript.isSchnorSignature(a.pubkey, a.witness[0])) + // throw new TypeError('Witness has invalid signature'); + } 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 (!_ecc().isXOnlyPoint(internalPubkey)) + throw new TypeError('Invalid internalPubkey for p2tr witness'); - 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 leafVersion = controlBlock[0] & 0b11111110; + const script = witness[witness.length - 2]; - const tweakHash = tapTweakHash(pubKey, h); + const leafHash = tapLeafHash(script, leafVersion); + const hash = rootHashFromPath(controlBlock, leafHash); - const res = ecc.xOnlyPointAddTweak(pubKey, tweakHash); - if (!res || res.xOnlyPubkey === null) return null; + const outputKey = tweakKey(internalPubkey, hash, _ecc()); + if (!outputKey) + // todo: needs test data + throw new TypeError('Invalid outputKey for p2tr witness'); - return { - parity: res.parity, - x: NBuffer.from(res.xOnlyPubkey), - }; + 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, + eccLib: TinySecp256k1Interface, +): 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 = eccLib.xOnlyPointAddTweak(pubKey, tweakHash); + if (!res || res.xOnlyPubkey === null) return null; + + return { + parity: res.parity, + x: NBuffer.from(res.xOnlyPubkey), + }; +} diff --git a/ts_src/payments/testecc.ts b/ts_src/payments/testecc.ts index 773fa3eb8..5e8643d67 100644 --- a/ts_src/payments/testecc.ts +++ b/ts_src/payments/testecc.ts @@ -3,6 +3,7 @@ import { TinySecp256k1Interface } from '../types'; const h = (hex: string): Buffer => Buffer.from(hex, 'hex'); export function testEcc(ecc: TinySecp256k1Interface): void { + assert(typeof ecc.isXOnlyPoint === 'function'); assert( ecc.isXOnlyPoint( h('79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), @@ -34,6 +35,7 @@ export function testEcc(ecc: TinySecp256k1Interface): void { ), ); + assert(typeof ecc.xOnlyPointAddTweak === 'function'); tweakAddVectors.forEach(t => { const r = ecc.xOnlyPointAddTweak(h(t.pubkey), h(t.tweak)); if (t.result === null) { From 4cad59ce2d32c01ff33d2bd69f2903f6967ecfd5 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 18 Jan 2022 09:10:37 +0200 Subject: [PATCH 39/53] refactor: move eccLib to PaymentOptions --- src/payments/index.d.ts | 1 + src/payments/p2tr.d.ts | 3 +-- src/payments/p2tr.js | 8 ++++---- test/payments.spec.ts | 8 +++++--- ts_src/payments/index.ts | 1 + ts_src/payments/p2tr.ts | 12 ++++-------- 6 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/payments/index.d.ts b/src/payments/index.d.ts index a72a8ea41..6c3e09c1a 100644 --- a/src/payments/index.d.ts +++ b/src/payments/index.d.ts @@ -34,6 +34,7 @@ export declare type PaymentFunction = () => Payment; export interface PaymentOpts { validate?: boolean; allowIncomplete?: boolean; + eccLib?: TinySecp256k1Interface; } export declare type StackElement = Buffer | number; export declare type Stack = StackElement[]; diff --git a/src/payments/p2tr.d.ts b/src/payments/p2tr.d.ts index 3268bf450..350ed0ffc 100644 --- a/src/payments/p2tr.d.ts +++ b/src/payments/p2tr.d.ts @@ -1,3 +1,2 @@ -import { TinySecp256k1Interface } from '../types'; import { Payment, PaymentOpts } from './index'; -export declare function p2tr(a: Payment, opts?: PaymentOpts, eccLib?: TinySecp256k1Interface): Payment; +export declare function p2tr(a: Payment, opts?: PaymentOpts): Payment; diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index 1517ef0c6..7e944e68b 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -12,7 +12,7 @@ const testecc_1 = require('./testecc'); const OPS = bscript.OPS; const TAPROOT_VERSION = 0x01; const ANNEX_PREFIX = 0x50; -function p2tr(a, opts, eccLib) { +function p2tr(a, opts) { if ( !a.address && !a.output && @@ -24,9 +24,9 @@ function p2tr(a, opts, eccLib) { throw new TypeError('Not enough data'); opts = Object.assign({ validate: true }, opts || {}); const _ecc = lazy.value(() => { - if (!eccLib) throw new Error('ECC Library is missing for p2tr.'); - (0, testecc_1.testEcc)(eccLib); - return eccLib; + if (!opts.eccLib) throw new Error('ECC Library is missing for p2tr.'); + (0, testecc_1.testEcc)(opts.eccLib); + return opts.eccLib; }); (0, types_1.typeforce)( { diff --git a/test/payments.spec.ts b/test/payments.spec.ts index e5227124e..6726e96d8 100644 --- a/test/payments.spec.ts +++ b/test/payments.spec.ts @@ -21,9 +21,10 @@ import { TinySecp256k1Interface } from '../src/types'; const fixtures = require('./fixtures/' + p); fixtures.valid.forEach((f: any) => { + const options = Object.assign({ eccLib }, f.options || {}); it(f.description + ' as expected', () => { const args = u.preform(f.arguments); - const actual = fn(args, f.options, eccLib); + const actual = fn(args, options, eccLib); u.equate(actual, f.expected, f.arguments); }); @@ -32,7 +33,7 @@ import { TinySecp256k1Interface } from '../src/types'; const args = u.preform(f.arguments); const actual = fn( args, - Object.assign({}, f.options, { + Object.assign({}, options, { validate: false, }), eccLib, @@ -43,6 +44,7 @@ import { TinySecp256k1Interface } from '../src/types'; }); fixtures.invalid.forEach((f: any) => { + const options = Object.assign({ eccLib }, f.options || {}); it( 'throws ' + f.exception + @@ -51,7 +53,7 @@ import { TinySecp256k1Interface } from '../src/types'; const args = u.preform(f.arguments); assert.throws(() => { - fn(args, f.options, eccLib); + fn(args, options, eccLib); }, new RegExp(f.exception)); }, ); diff --git a/ts_src/payments/index.ts b/ts_src/payments/index.ts index f2dcfb939..577bdcb22 100644 --- a/ts_src/payments/index.ts +++ b/ts_src/payments/index.ts @@ -41,6 +41,7 @@ export type PaymentFunction = () => Payment; export interface PaymentOpts { validate?: boolean; allowIncomplete?: boolean; + eccLib?: TinySecp256k1Interface; } export type StackElement = Buffer | number; diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index 1ecce1432..66fa4946d 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -18,11 +18,7 @@ const OPS = bscript.OPS; const TAPROOT_VERSION = 0x01; const ANNEX_PREFIX = 0x50; -export function p2tr( - a: Payment, - opts?: PaymentOpts, - eccLib?: TinySecp256k1Interface, -): Payment { +export function p2tr(a: Payment, opts?: PaymentOpts): Payment { if ( !a.address && !a.output && @@ -36,10 +32,10 @@ export function p2tr( opts = Object.assign({ validate: true }, opts || {}); const _ecc = lazy.value(() => { - if (!eccLib) throw new Error('ECC Library is missing for p2tr.'); + if (!opts!.eccLib) throw new Error('ECC Library is missing for p2tr.'); - testEcc(eccLib); - return eccLib; + testEcc(opts!.eccLib); + return opts!.eccLib; }); typef( From c6706989324a7f2a92558515b3b99002c352da2a Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 20 Jan 2022 08:51:44 +0200 Subject: [PATCH 40/53] fix: remove `TinySecp256k1Interface` from `PaymentCreator` --- src/payments/index.d.ts | 2 +- test/payments.spec.ts | 5 ++--- ts_src/payments/index.ts | 6 +----- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/payments/index.d.ts b/src/payments/index.d.ts index 6c3e09c1a..6b186c4d1 100644 --- a/src/payments/index.d.ts +++ b/src/payments/index.d.ts @@ -29,7 +29,7 @@ export interface Payment { scriptLeaf?: TaprootLeaf; witness?: Buffer[]; } -export declare type PaymentCreator = (a: Payment, opts?: PaymentOpts, eccLib?: TinySecp256k1Interface) => Payment; +export declare type PaymentCreator = (a: Payment, opts?: PaymentOpts) => Payment; export declare type PaymentFunction = () => Payment; export interface PaymentOpts { validate?: boolean; diff --git a/test/payments.spec.ts b/test/payments.spec.ts index 6726e96d8..e89834d3b 100644 --- a/test/payments.spec.ts +++ b/test/payments.spec.ts @@ -24,7 +24,7 @@ import { TinySecp256k1Interface } from '../src/types'; const options = Object.assign({ eccLib }, f.options || {}); it(f.description + ' as expected', () => { const args = u.preform(f.arguments); - const actual = fn(args, options, eccLib); + const actual = fn(args, options); u.equate(actual, f.expected, f.arguments); }); @@ -36,7 +36,6 @@ import { TinySecp256k1Interface } from '../src/types'; Object.assign({}, options, { validate: false, }), - eccLib, ); u.equate(actual, f.expected, f.arguments); @@ -53,7 +52,7 @@ import { TinySecp256k1Interface } from '../src/types'; const args = u.preform(f.arguments); assert.throws(() => { - fn(args, options, eccLib); + fn(args, options); }, new RegExp(f.exception)); }, ); diff --git a/ts_src/payments/index.ts b/ts_src/payments/index.ts index 577bdcb22..7bb77b6ac 100644 --- a/ts_src/payments/index.ts +++ b/ts_src/payments/index.ts @@ -30,11 +30,7 @@ export interface Payment { witness?: Buffer[]; } -export type PaymentCreator = ( - a: Payment, - opts?: PaymentOpts, - eccLib?: TinySecp256k1Interface, -) => Payment; +export type PaymentCreator = (a: Payment, opts?: PaymentOpts) => Payment; export type PaymentFunction = () => Payment; From b566dd76bd3e917eb4eea03e1f58425fed7bbf0d Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 20 Jan 2022 08:52:43 +0200 Subject: [PATCH 41/53] chore: keep ecc test vectors to a minimum --- src/payments/testecc.js | 102 ------------------------------------- ts_src/payments/testecc.ts | 102 ------------------------------------- 2 files changed, 204 deletions(-) diff --git a/src/payments/testecc.js b/src/payments/testecc.js index 77e2b3e15..44e19c887 100644 --- a/src/payments/testecc.js +++ b/src/payments/testecc.js @@ -63,112 +63,10 @@ const tweakAddVectors = [ parity: 1, result: 'e478f99dab91052ab39a33ea35fd5e6e4933f4d28023cd597c9a1f6760346adf', }, - { - pubkey: '2f1b310f4c065331bc0d79ba4661bb9822d67d7c4a1b0a1892e1fd0cd23aa68d', - tweak: '40ab636041bb695843ac7f220344565bed3b5dca919e256b7e31e19d69b36fad', - parity: 1, - result: '5786150f0ac36a2aaeb8d11aaeb7aa2d03ad63c528cc307a7cd5648c84041f34', - }, - { - pubkey: 'e7e9acacbdb43fc9fb71a8db1536c0f866caa78def49f666fa121a6f7954bb01', - tweak: '1ad613216778a70490c8a681a4901d4ca880d76916deb69979b5ac52d2760e09', - parity: 1, - result: 'ae10fa880c85848cc1faf056f56a64b7d45c68838cfb308d03ca2be9b485c130', - }, { pubkey: '2c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991', tweak: '823c3cd2142744b075a87eade7e1b8678ba308d566226a0056ca2b7a76f86b47', parity: 0, result: '9534f8dc8c6deda2dc007655981c78b49c5d96c778fbf363462a11ec9dfd948c', }, - { - pubkey: '8f19ee7677713806c662078a9f2b2c7b930376d22f2d617bce50b5e444839a7c', - tweak: '7df1f4b66058f8be34b6b7d17be9bcf35ba5c98edf8d4e763b95964bad655fe4', - parity: 1, - result: '74619a5990750928d0728817b02bb0d398062dad0e568f46ea5348d35bef914f', - }, - { - pubkey: '2bda68b3aa0239d382f185ca2d8c31ce604cc26220cef3eb65223f47a0088d87', - tweak: 'e5a018b3a2e155316109d9cdc5eab739759c0e07e0c00bf9fccb8237fe4d7f02', - parity: 0, - result: '84479dc6bf70762721b10b89c8798e00d525507edc3fabbfc89ad915b6509379', - }, - { - pubkey: '9611ba340bce00a594f1ffb1294974af80e1301e49597378732fd77bbdedf454', - tweak: 'bbb8ec1f063522953a4a9f90ff4e849560e0f0597458529ea13b8868f255c7c7', - parity: 0, - result: '30bebfdad18b87b646f60e51d3c45c6658fbb4364c94b1b33d925a4515b66757', - }, - { - pubkey: 'd9f5792078a845303c1f1ea88aec79ed0fd9f0c49e9e7bff2765877e79b4dd52', - tweak: '531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337', - parity: 0, - result: 'fe5dd39f0491af71711454eee5b6a9c99779c422dd97f5e7f75d7ce7be7b32f0', - }, - { - pubkey: '40ab636041bb695843ac7f220344565bed3b5dca919e256b7e31e19d69b36fad', - tweak: '048968943184ce8a0239ab2141641e8eaead35a6dc8e6b55ad33ac1eca975a47', - parity: 1, - result: '6c8245a62201887c5e3aeb022fff06e6c110f3e58ad6d37cc20e877082b72c58', - }, - { - pubkey: '6aa3da9b5c1d61956076cb3014ffdaa0996bacdae29ba4b89e39b4088f86ec78', - tweak: 'ff8adab52623bcb2717fc71d7edc6f55e98396e6c234dff01f307a12b2af1c99', - parity: 1, - result: 'd6080b5df61525fe8be31a823f3943e5fc9354d5a091b2dea195985c7c395787', - }, - { - pubkey: '24653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c', - tweak: '9e5f7dbe6d62ade5aab476b40559852ea1b5fc7bb99a61a42eab550f69ffafb4', - parity: 0, - result: '58289ee230fcf6a78cb9878cae5102cc9104490abab9d03f3eccc2f0cd07de5f', - }, - { - pubkey: 'e5a018b3a2e155316109d9cdc5eab739759c0e07e0c00bf9fccb8237fe4d7f02', - tweak: 'bde750d93efe821813df9c15ee676f2e9c63386336c164f5a15cf240ac653c06', - parity: 0, - result: '9a1ae919c5c78da635d94a92b3053e46b2261b81ec70db82a382f5bff474bec4', - }, - { - pubkey: 'bc14bc97e2d818ee360a9ba7782bd6a6dfc2c1e335fffc584a095fdac5fea641', - tweak: '4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa', - parity: 1, - result: '19a7416f4f95f36c5e48dc7630ffea8b292e1721cecfa9cc5f794c83973e41d6', - }, - { - pubkey: '35e9d7c48e3a5254d5881b60abf004cf6eedc6ab842393caa2fdd20d6d0ad170', - tweak: '18bb586dc109adf49ffb42e0ac293d2a2965e49a0a4900c2be776b426b7cbfde', - parity: 0, - result: 'fa7cca72580bb686fbbae09ded801c7d109fa378f52e8a5f43a1922e442e44c1', - }, - { - pubkey: '67bff656551f25009ac8ed88664736c08074a15dbd2268292f5de7ca7e718338', - tweak: 'b96359049e97f49d871e856f37e54d0978bae2cc936b4484d96df984cd20daa1', - parity: 0, - result: 'dd081d737da17fb4f6686f8497cac56b16ea06e1dc05859633f735fb304e7e5a', - }, - { - pubkey: '5b0da52533a1620fe947cb658c35e1772f39ef1253753493b7dc4b8d8f31f40e', - tweak: '3d481f46056f2da27870a5d00c0c7bf484036780a83bbcc2e2da2f03bc33bff0', - parity: 1, - result: '164e13b54edc89673f94563120d87db4a47b12e49c40c195ac51ea7bc50f22e1', - }, - { - pubkey: '0612c5e8c98a9677a2ddd13770e26f5f1e771a088c88ce519a1e1b65872423f9', - tweak: 'dbcfa1c73674cba4aa1b6992ebdc6a77008d38f6c6ec068c3c862b9ff6d287f2', - parity: 0, - result: '82fc6954352b7189a156e4678d0c315c122431fa9551961b8e3c811b55a42c8b', - }, - { - pubkey: '9ac20335eb38768d2052be1dbbc3c8f6178407458e51e6b4ad22f1d91758895b', - tweak: '6aa3da9b5c1d61956076cb3014ffdaa0996bacdae29ba4b89e39b4088f86ec78', - parity: 1, - result: 'cf9065a7e2c9f909becc1c95f9884ed9fbe19c4a8954ed17880f02d94ae96a63', - }, - { - pubkey: 'c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5', - tweak: 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413f', - parity: -1, - result: null, - }, ]; diff --git a/ts_src/payments/testecc.ts b/ts_src/payments/testecc.ts index 5e8643d67..382d6149a 100644 --- a/ts_src/payments/testecc.ts +++ b/ts_src/payments/testecc.ts @@ -65,112 +65,10 @@ const tweakAddVectors = [ parity: 1, result: 'e478f99dab91052ab39a33ea35fd5e6e4933f4d28023cd597c9a1f6760346adf', }, - { - pubkey: '2f1b310f4c065331bc0d79ba4661bb9822d67d7c4a1b0a1892e1fd0cd23aa68d', - tweak: '40ab636041bb695843ac7f220344565bed3b5dca919e256b7e31e19d69b36fad', - parity: 1, - result: '5786150f0ac36a2aaeb8d11aaeb7aa2d03ad63c528cc307a7cd5648c84041f34', - }, - { - pubkey: 'e7e9acacbdb43fc9fb71a8db1536c0f866caa78def49f666fa121a6f7954bb01', - tweak: '1ad613216778a70490c8a681a4901d4ca880d76916deb69979b5ac52d2760e09', - parity: 1, - result: 'ae10fa880c85848cc1faf056f56a64b7d45c68838cfb308d03ca2be9b485c130', - }, { pubkey: '2c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991', tweak: '823c3cd2142744b075a87eade7e1b8678ba308d566226a0056ca2b7a76f86b47', parity: 0, result: '9534f8dc8c6deda2dc007655981c78b49c5d96c778fbf363462a11ec9dfd948c', }, - { - pubkey: '8f19ee7677713806c662078a9f2b2c7b930376d22f2d617bce50b5e444839a7c', - tweak: '7df1f4b66058f8be34b6b7d17be9bcf35ba5c98edf8d4e763b95964bad655fe4', - parity: 1, - result: '74619a5990750928d0728817b02bb0d398062dad0e568f46ea5348d35bef914f', - }, - { - pubkey: '2bda68b3aa0239d382f185ca2d8c31ce604cc26220cef3eb65223f47a0088d87', - tweak: 'e5a018b3a2e155316109d9cdc5eab739759c0e07e0c00bf9fccb8237fe4d7f02', - parity: 0, - result: '84479dc6bf70762721b10b89c8798e00d525507edc3fabbfc89ad915b6509379', - }, - { - pubkey: '9611ba340bce00a594f1ffb1294974af80e1301e49597378732fd77bbdedf454', - tweak: 'bbb8ec1f063522953a4a9f90ff4e849560e0f0597458529ea13b8868f255c7c7', - parity: 0, - result: '30bebfdad18b87b646f60e51d3c45c6658fbb4364c94b1b33d925a4515b66757', - }, - { - pubkey: 'd9f5792078a845303c1f1ea88aec79ed0fd9f0c49e9e7bff2765877e79b4dd52', - tweak: '531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337', - parity: 0, - result: 'fe5dd39f0491af71711454eee5b6a9c99779c422dd97f5e7f75d7ce7be7b32f0', - }, - { - pubkey: '40ab636041bb695843ac7f220344565bed3b5dca919e256b7e31e19d69b36fad', - tweak: '048968943184ce8a0239ab2141641e8eaead35a6dc8e6b55ad33ac1eca975a47', - parity: 1, - result: '6c8245a62201887c5e3aeb022fff06e6c110f3e58ad6d37cc20e877082b72c58', - }, - { - pubkey: '6aa3da9b5c1d61956076cb3014ffdaa0996bacdae29ba4b89e39b4088f86ec78', - tweak: 'ff8adab52623bcb2717fc71d7edc6f55e98396e6c234dff01f307a12b2af1c99', - parity: 1, - result: 'd6080b5df61525fe8be31a823f3943e5fc9354d5a091b2dea195985c7c395787', - }, - { - pubkey: '24653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c', - tweak: '9e5f7dbe6d62ade5aab476b40559852ea1b5fc7bb99a61a42eab550f69ffafb4', - parity: 0, - result: '58289ee230fcf6a78cb9878cae5102cc9104490abab9d03f3eccc2f0cd07de5f', - }, - { - pubkey: 'e5a018b3a2e155316109d9cdc5eab739759c0e07e0c00bf9fccb8237fe4d7f02', - tweak: 'bde750d93efe821813df9c15ee676f2e9c63386336c164f5a15cf240ac653c06', - parity: 0, - result: '9a1ae919c5c78da635d94a92b3053e46b2261b81ec70db82a382f5bff474bec4', - }, - { - pubkey: 'bc14bc97e2d818ee360a9ba7782bd6a6dfc2c1e335fffc584a095fdac5fea641', - tweak: '4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa', - parity: 1, - result: '19a7416f4f95f36c5e48dc7630ffea8b292e1721cecfa9cc5f794c83973e41d6', - }, - { - pubkey: '35e9d7c48e3a5254d5881b60abf004cf6eedc6ab842393caa2fdd20d6d0ad170', - tweak: '18bb586dc109adf49ffb42e0ac293d2a2965e49a0a4900c2be776b426b7cbfde', - parity: 0, - result: 'fa7cca72580bb686fbbae09ded801c7d109fa378f52e8a5f43a1922e442e44c1', - }, - { - pubkey: '67bff656551f25009ac8ed88664736c08074a15dbd2268292f5de7ca7e718338', - tweak: 'b96359049e97f49d871e856f37e54d0978bae2cc936b4484d96df984cd20daa1', - parity: 0, - result: 'dd081d737da17fb4f6686f8497cac56b16ea06e1dc05859633f735fb304e7e5a', - }, - { - pubkey: '5b0da52533a1620fe947cb658c35e1772f39ef1253753493b7dc4b8d8f31f40e', - tweak: '3d481f46056f2da27870a5d00c0c7bf484036780a83bbcc2e2da2f03bc33bff0', - parity: 1, - result: '164e13b54edc89673f94563120d87db4a47b12e49c40c195ac51ea7bc50f22e1', - }, - { - pubkey: '0612c5e8c98a9677a2ddd13770e26f5f1e771a088c88ce519a1e1b65872423f9', - tweak: 'dbcfa1c73674cba4aa1b6992ebdc6a77008d38f6c6ec068c3c862b9ff6d287f2', - parity: 0, - result: '82fc6954352b7189a156e4678d0c315c122431fa9551961b8e3c811b55a42c8b', - }, - { - pubkey: '9ac20335eb38768d2052be1dbbc3c8f6178407458e51e6b4ad22f1d91758895b', - tweak: '6aa3da9b5c1d61956076cb3014ffdaa0996bacdae29ba4b89e39b4088f86ec78', - parity: 1, - result: 'cf9065a7e2c9f909becc1c95f9884ed9fbe19c4a8954ed17880f02d94ae96a63', - }, - { - pubkey: 'c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5', - tweak: 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413f', - parity: -1, - result: null, - }, ]; From ef751d1051bb37e9e28b3f03a278442af4117a06 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 25 Jan 2022 16:04:00 +0200 Subject: [PATCH 42/53] chore: just bla bla --- play.mjs | 66 +++++++++++++++++ play2.mjs | 110 ++++++++++++++++++++++++++++ play3.mjs | 11 +++ src/payments/p2tr.js | 2 +- src/psbt.d.ts | 6 +- src/psbt.js | 64 ++++++++++++++--- test/integration/taproot.spec.ts | 2 +- ts_src/payments/p2tr.ts | 2 +- ts_src/psbt.ts | 118 +++++++++++++++++++++---------- 9 files changed, 332 insertions(+), 49 deletions(-) create mode 100644 play.mjs create mode 100644 play2.mjs create mode 100644 play3.mjs diff --git a/play.mjs b/play.mjs new file mode 100644 index 000000000..03e5e7561 --- /dev/null +++ b/play.mjs @@ -0,0 +1,66 @@ +import * as ecc from 'tiny-secp256k1'; +import { ECPairFactory } from 'ecpair' + +import { Psbt } from './src/psbt.js' +import { p2pkh, p2wpkh, p2tr } from './src/payments/index.js' +import { testnet as network } from './src/networks.js' +import { tapTweakHash } from './src/payments/taprootutils.js' + +console.log(''.padEnd(100, '#')) +const ECPair = ECPairFactory(ecc); +const hex = (s) => Buffer.from(s, 'hex') + +const inP2pkhKey = ECPair.fromPrivateKey(hex('82fd530c9eb33570c7e05ca5e80b740bcf1118e8f4c73d44a801fa9dd60f6449')) +const inP2wpkhKey = ECPair.fromPrivateKey(hex('35dfb4dc373860005d6f74d37064e328bc772343c354c95e31b654d0c5e22f58')) +const inP2trKey = ECPair.fromPrivateKey(hex('accaf12e04e11b08fc28f5fe75b47ea663843b698981e31f1cafa2224d6e28c0')) +const outP2trKey = ECPair.fromPrivateKey(hex('900afde76badc8914c9940379c74857d70b4d7da590097285572df6b88ad2975')) + + + +const inP2tr = p2tr({ internalPubkey: inP2trKey.publicKey.slice(1), network }, { eccLib: ecc }) +const outP2tr = p2tr({ internalPubkey: outP2trKey.publicKey.slice(1), network }, { eccLib: ecc }) +const inTweakedP2trKey = ECPair.fromPrivateKey(Buffer.from(ecc.privateAdd(inP2trKey.privateKey, tapTweakHash(inP2trKey.publicKey.slice(1, 33))))) + +// console.log('### inP2tr.hash ', inP2tr.hash.toString('hex')) +console.log('### inP2trKey.privateKey ', inP2trKey.privateKey.toString('hex')) +console.log('### inP2trKey.publicKey ', inP2trKey.publicKey.toString('hex')) +console.log('### inTweakedP2trKey.privateKey ', inTweakedP2trKey.privateKey.toString('hex')) +console.log('### inTweakedP2trKey.publicKey ', inTweakedP2trKey.publicKey.toString('hex')) +console.log(''.padEnd(100, '#')) + + +console.log('### inP2tr.script ', inP2tr.output.toString('hex')) +console.log('### inP2tr.address ', inP2tr.address) +console.log('### inP2tr.pubkey ', inP2tr.pubkey.toString('hex')) +console.log(''.padEnd(100, '#')) + +console.log('### p2pkh.address ', p2pkh({ pubkey: inP2pkhKey.publicKey, network }).address) +console.log('### p2wpkh.address ', p2wpkh({ pubkey: inP2wpkhKey.publicKey, network }).address) +console.log('### inP2tr.address ', inP2tr.address) +console.log('### outP2tr.address ', outP2tr.address) +console.log(''.padEnd(100, '#')) + + +const psbt = new Psbt({ network }) +// spend p2tr +psbt.addInput({ + hash: hex('1b9e7e80288a059adb9da6fd7c30e7383a5a924f78d3a0b6b047e07345990f48').reverse(), + index: 0, +}).updateInput(0, { witnessUtxo: { script: inP2tr.output, value: 70000 } }) +// test with full utxo also + +psbt.addOutput({ + address: outP2tr.address, + value: 69000 +}) + + +psbt.signInput(0, inTweakedP2trKey) + +// Serialize tx +psbt.finalizeAllInputs() +const tx = psbt.extractTransaction() +const rawTx = tx.toBuffer() + +console.log('### rawTx', rawTx.toString('hex')) +/// 1b9e7e80288a059adb9da6fd7c30e7383a5a924f78d3a0b6b047e07345990f48 \ No newline at end of file diff --git a/play2.mjs b/play2.mjs new file mode 100644 index 000000000..10a432d59 --- /dev/null +++ b/play2.mjs @@ -0,0 +1,110 @@ +import { BIP32Factory } from 'bip32'; +import { ECPairFactory } from 'ecpair' +import * as ecc from 'tiny-secp256k1'; + +import * as bitcoin from './src/index.js'; +import { testnet as network } from './src/networks.js' + +const ECPair = ECPairFactory(ecc); +const bip32 = BIP32Factory(ecc); +const hex = (s) => Buffer.from(s, 'hex') + +// Order of the curve (N) - 1 +const N_LESS_1 = Buffer.from( + 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140', + 'hex', +); +// 1 represented as 32 bytes BE +const ONE = Buffer.from( + '0000000000000000000000000000000000000000000000000000000000000001', + 'hex', +); + + +const myKey = ECPair.fromPrivateKey(hex('accaf12e04e11b08fc28f5fe75b47ea663843b698981e31f1cafa2224d6e28c0')) +const output = createKeySpendOutput(myKey.publicKey); +const address = bitcoin.address.fromOutputScript(output, network); +const amount = 100000; +const sendAmount = 75000 +console.log('### output', output.toString('hex')) +console.log('### address', address) + +const unspent = { txId: '8162b0310f37182049429e147b0ba7ac17eb4dd5c6d3016927e4c993b63edc2b', vout: 0} + +const tx = createSigned( + myKey, + unspent.txId, + unspent.vout, + sendAmount, + [output], // public key + [amount], +); + +const txHex = tx.toHex(); +console.log('### txHex', txHex) +// txId: a618fdb361a6cfbc345d2b030ab7533dc88b3c097af58df6979b4d9aa4472379 + + +function createKeySpendOutput(publicKey) { + // x-only pubkey (remove 1 byte y parity) + const myXOnlyPubkey = publicKey.slice(1, 33); + const commitHash = bitcoin.crypto.taggedHash('TapTweak', myXOnlyPubkey); + console.log('### commitHash ', commitHash.toString('hex')) + const tweakResult = ecc.xOnlyPointAddTweak(myXOnlyPubkey, commitHash); + console.log('### tweakResult xOnlyPubkey', Buffer.from(tweakResult.xOnlyPubkey).toString('hex')) + if (tweakResult === null) throw new Error('Invalid Tweak'); + const { xOnlyPubkey: tweaked } = tweakResult; + // scriptPubkey + return Buffer.concat([ + // witness v1, PUSH_DATA 32 bytes + Buffer.from([0x51, 0x20]), + // x-only tweaked pubkey + tweaked, + ]); +} + +function signTweaked(messageHash, key) { + const privateKey = + key.publicKey[0] === 2 + ? key.privateKey + : ecc.privateAdd(ecc.privateSub(N_LESS_1, key.privateKey), ONE); + const tweakHash = bitcoin.crypto.taggedHash( + 'TapTweak', + key.publicKey.slice(1, 33), + ); + const newPrivateKey = ecc.privateAdd(privateKey, tweakHash); + if (newPrivateKey === null) throw new Error('Invalid Tweak'); + return ecc.signSchnorr(messageHash, newPrivateKey, Buffer.alloc(32)); +} + +// Function for creating signed tx +function createSigned( + key, + txid, + vout, + amountToSend, + scriptPubkeys, + values, + ) { + console.log('### scriptPubkeys', scriptPubkeys.map(s => s.toString('hex'))) + console.log('### values', values) + const tx = new bitcoin.Transaction(); + tx.version = 2; + // Add input + tx.addInput(Buffer.from(txid, 'hex').reverse(), vout); + // Add output + tx.addOutput(scriptPubkeys[0], amountToSend); + const sighash = tx.hashForWitnessV1( + 0, // which input + scriptPubkeys, // All previous outputs of all inputs + values, // All previous values of all inputs + bitcoin.Transaction.SIGHASH_DEFAULT, // sighash flag, DEFAULT is schnorr-only (DEFAULT == ALL) + ); + console.log('### sighash', sighash.toString('hex')) + console.log('### unsignedTx', tx.toHex()) + const signature = Buffer.from(signTweaked(sighash, key)); + // witness stack for keypath spend is just the signature. + // If sighash is not SIGHASH_DEFAULT (ALL) then you must add 1 byte with sighash value + tx.ins[0].witness = [signature]; + return tx; + } \ No newline at end of file diff --git a/play3.mjs b/play3.mjs new file mode 100644 index 000000000..fdff92091 --- /dev/null +++ b/play3.mjs @@ -0,0 +1,11 @@ +import * as ecc from 'tiny-secp256k1'; +import { ECPairFactory } from 'ecpair' + +import { p2tr } from './src/payments/index.js' +import { testnet as network } from './src/networks.js' + +console.log(''.padEnd(100, '#')) + +const inP2tr = p2tr({ output: Buffer.from('51209421e734b0f9d2c467ea7dd197c61acb4467cdcbc9f4cb0c571f8b63a5c40cae', 'hex'), network }, { eccLib: ecc }) + +console.log('### inP2tr',inP2tr.address) \ No newline at end of file diff --git a/src/payments/p2tr.js b/src/payments/p2tr.js index 7e944e68b..d03d7980d 100644 --- a/src/payments/p2tr.js +++ b/src/payments/p2tr.js @@ -37,7 +37,7 @@ function p2tr(a, opts) { 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(bscript.isCanonicalScriptSignature), + signature: types_1.typeforce.maybe(types_1.typeforce.BufferN(64)), witness: types_1.typeforce.maybe( types_1.typeforce.arrayOf(types_1.typeforce.Buffer), ), diff --git a/src/psbt.d.ts b/src/psbt.d.ts index 8603a6955..241c66a26 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; } /** @@ -178,5 +182,5 @@ isP2WSH: boolean) => { finalScriptSig: Buffer | undefined; finalScriptWitness: 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'; export {}; diff --git a/src/psbt.js b/src/psbt.js index 616219580..f10ab97d3 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -1,6 +1,7 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); exports.Psbt = void 0; +const ecc = require('tiny-secp256k1'); // TODO: extract const bip174_1 = require('bip174'); const varuint = require('bip174/src/lib/converter/varint'); const utils_1 = require('bip174/src/lib/utils'); @@ -280,6 +281,7 @@ class Psbt { ); if (!script) throw new Error(`No script found for input #${inputIndex}`); checkPartialSigSighashes(input); + console.log('### isSegwit', isSegwit); const { finalScriptSig, finalScriptWitness } = finalScriptsFunc( inputIndex, input, @@ -515,24 +517,29 @@ class Psbt { signInput( inputIndex, keyPair, - sighashTypes = [transaction_1.Transaction.SIGHASH_ALL], + sighashTypes = [ + transaction_1.Transaction.SIGHASH_ALL, + transaction_1.Transaction.SIGHASH_DEFAULT, + ], ) { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); - const { hash, sighashType } = getHashAndSighashType( + const { hash } = getHashAndSighashType( this.data.inputs, inputIndex, keyPair.publicKey, this.__CACHE, sighashTypes, ); + console.log('### hash', hash.toString('hex')); const partialSig = [ { pubkey: keyPair.publicKey, - signature: bscript.signature.encode(keyPair.sign(hash), sighashType), + signature: keyPair.signSchnorr(hash), // change }, ]; - this.data.updateInput(inputIndex, { partialSig }); + // this.data.updateInput(inputIndex, { partialSig }); + this.data.inputs[inputIndex].partialSig = partialSig; return this; } signInputAsync( @@ -667,11 +674,14 @@ class PsbtTransaction { } } function canFinalize(input, script, scriptType) { + console.log('### scriptType', scriptType); switch (scriptType) { case 'pubkey': case 'pubkeyhash': case 'witnesspubkeyhash': return hasSigs(1, input.partialSig); + case 'taproot': + return true; // finalScriptWitness case 'multisig': const p2ms = payments.p2ms({ output: script }); return hasSigs(p2ms.m, input.partialSig, p2ms.pubkeys); @@ -704,11 +714,12 @@ function isFinalized(input) { return !!input.finalScriptSig || !!input.finalScriptWitness; } function isPaymentFactory(payment) { - return script => { + return (script, eccLib) => { try { - payment({ output: script }); + payment({ output: script }, { eccLib }); return true; } catch (err) { + // console.log('###', err) return false; } }; @@ -719,6 +730,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; @@ -943,6 +955,7 @@ function getHashForSig(inputIndex, input, cache, forValidate, sighashTypes) { } let hash; let prevout; + const prevOuts = []; if (input.nonWitnessUtxo) { const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( cache, @@ -959,8 +972,10 @@ function getHashForSig(inputIndex, input, cache, forValidate, sighashTypes) { } const prevoutIndex = unsignedTx.ins[inputIndex].index; prevout = nonWitnessUtxoTx.outs[prevoutIndex]; + prevOuts.push(prevout); } else if (input.witnessUtxo) { prevout = input.witnessUtxo; + prevOuts.push(prevout); } else { throw new Error('Need a Utxo input item for signing'); } @@ -988,6 +1003,21 @@ function getHashForSig(inputIndex, input, cache, forValidate, sighashTypes) { prevout.value, sighashType, ); + } else if (isP2TR(meaningfulScript, ecc)) { + // const signingScripts: any = unsignedTx.outs.map(o => o.script); + // const values: any = unsignedTx.outs.map(o => o.value); + const signingScripts = prevOuts.map(o => o.script); + const values = prevOuts.map(o => o.value); + console.log('### scriptPubkeys', signingScripts[0].toString('hex')); + console.log('### values, inputIndex', values, inputIndex); + console.log('### unsignedTx', unsignedTx); + hash = unsignedTx.hashForWitnessV1( + inputIndex, + signingScripts, + values, + transaction_1.Transaction.SIGHASH_DEFAULT, + ); + console.log('### isP2TR hash: ', hash.toString('hex')); } else { // non-segwit if ( @@ -1050,6 +1080,15 @@ function getPayment(script, scriptType, partialSig) { signature: partialSig[0].signature, }); break; + case 'taproot': + payment = payments.p2tr( + { + output: script, + signature: partialSig[0].signature, + }, + { eccLib: ecc }, + ); + break; } return payment; } @@ -1094,7 +1133,9 @@ function getScriptFromInput(inputIndex, input, cache) { res.script = input.witnessUtxo.script; } } - if (input.witnessScript || isP2WPKH(res.script)) { + console.log('### res.scrip', res.script && res.script.toString('hex')); + console.log('### res.scrip isP2TR', isP2TR(res.script, ecc)); + if (input.witnessScript || isP2WPKH(res.script) || isP2TR(res.script, ecc)) { res.isSegwit = true; } return res; @@ -1392,18 +1433,25 @@ 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 classifyScript(script) { + // console.log('### script', script.toString('hex')) if (isP2WPKH(script)) return 'witnesspubkeyhash'; if (isP2PKH(script)) return 'pubkeyhash'; if (isP2MS(script)) return 'multisig'; if (isP2PK(script)) return 'pubkey'; + if (isP2TR(script, ecc)) return 'taproot'; return 'nonstandard'; } function range(n) { diff --git a/test/integration/taproot.spec.ts b/test/integration/taproot.spec.ts index f7b3733fa..358620f76 100644 --- a/test/integration/taproot.spec.ts +++ b/test/integration/taproot.spec.ts @@ -25,7 +25,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { unspent.txId, unspent.vout, sendAmount, - [output], + [output], // public key [amount], ); diff --git a/ts_src/payments/p2tr.ts b/ts_src/payments/p2tr.ts index 66fa4946d..fa6598dc3 100644 --- a/ts_src/payments/p2tr.ts +++ b/ts_src/payments/p2tr.ts @@ -47,7 +47,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment { internalPubkey: typef.maybe(typef.BufferN(32)), hash: typef.maybe(typef.BufferN(32)), pubkey: typef.maybe(typef.BufferN(32)), - signature: typef.maybe(bscript.isCanonicalScriptSignature), + signature: typef.maybe(typef.BufferN(64)), witness: typef.maybe(typef.arrayOf(typef.Buffer)), // scriptsTree: typef.maybe(typef.TaprootNode), // use merkel.isMast ? scriptLeaf: typef.maybe({ diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index b9af10fcf..08f301452 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1,3 +1,4 @@ +import * as ecc from 'tiny-secp256k1'; // TODO: extract import { Psbt as PsbtBase } from 'bip174'; import * as varuint from 'bip174/src/lib/converter/varint'; import { @@ -188,7 +189,7 @@ export class Psbt { let address; try { address = fromOutputScript(output.script, this.opts.network); - } catch (_) {} + } catch (_) { } return { script: cloneBuffer(output.script), value: output.value, @@ -258,7 +259,7 @@ export class Psbt { ) { throw new Error( `Invalid arguments for Psbt.addInput. ` + - `Requires single object with at least [hash] and [index]`, + `Requires single object with at least [hash] and [index]`, ); } checkInputsForPartialSig(this.data.inputs, 'addInput'); @@ -294,7 +295,7 @@ export class Psbt { ) { throw new Error( `Invalid arguments for Psbt.addOutput. ` + - `Requires single object with at least [script or address] and [value]`, + `Requires single object with at least [script or address] and [value]`, ); } checkInputsForPartialSig(this.data.inputs, 'addOutput'); @@ -357,6 +358,7 @@ export class Psbt { checkPartialSigSighashes(input); + console.log('### isSegwit', isSegwit) const { finalScriptSig, finalScriptWitness } = finalScriptsFunc( inputIndex, input, @@ -385,7 +387,7 @@ export class Psbt { 'input', input.redeemScript || redeemFromFinalScriptSig(input.finalScriptSig), input.witnessScript || - redeemFromFinalWitnessScript(input.finalScriptWitness), + redeemFromFinalWitnessScript(input.finalScriptWitness), ); const type = result.type === 'raw' ? '' : result.type + '-'; const mainType = classifyScript(result.meaningfulScript); @@ -450,11 +452,11 @@ export class Psbt { const { hash, script } = sighashCache! !== sig.hashType ? getHashForSig( - inputIndex, - Object.assign({}, input, { sighashType: sig.hashType }), - this.__CACHE, - true, - ) + inputIndex, + Object.assign({}, input, { sighashType: sig.hashType }), + this.__CACHE, + true, + ) : { hash: hashCache!, script: scriptCache! }; sighashCache = sig.hashType; hashCache = hash; @@ -630,11 +632,11 @@ export class Psbt { signInput( inputIndex: number, keyPair: Signer, - sighashTypes: number[] = [Transaction.SIGHASH_ALL], + sighashTypes: number[] = [Transaction.SIGHASH_ALL, Transaction.SIGHASH_DEFAULT], ): this { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); - const { hash, sighashType } = getHashAndSighashType( + const { hash } = getHashAndSighashType( this.data.inputs, inputIndex, keyPair.publicKey, @@ -642,14 +644,16 @@ export class Psbt { sighashTypes, ); + console.log('### hash', hash.toString('hex')) const partialSig = [ { pubkey: keyPair.publicKey, - signature: bscript.signature.encode(keyPair.sign(hash), sighashType), + signature: keyPair.signSchnorr(hash), // change }, ]; - this.data.updateInput(inputIndex, { partialSig }); + // this.data.updateInput(inputIndex, { partialSig }); + this.data.inputs[inputIndex].partialSig = partialSig; return this; } @@ -762,7 +766,7 @@ interface PsbtOpts { maximumFeeRate: number; } -interface PsbtInputExtended extends PsbtInput, TransactionInput {} +interface PsbtInputExtended extends PsbtInput, TransactionInput { } type PsbtOutputExtended = PsbtOutputExtendedAddress | PsbtOutputExtendedScript; @@ -798,6 +802,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; // todo: remove } /** @@ -806,12 +811,14 @@ export interface HDSigner extends HDSignerBase { export interface HDSignerAsync extends HDSignerBase { derivePath(path: string): HDSignerAsync; sign(hash: Buffer): Promise; + signSchnorr(hash: Buffer): Promise; // todo: remove } export interface Signer { publicKey: Buffer; network?: any; sign(hash: Buffer, lowR?: boolean): Buffer; + signSchnorr(hash: Buffer): Buffer; // todo: remove getPublicKey?(): Buffer; } @@ -819,6 +826,7 @@ export interface SignerAsync { publicKey: Buffer; network?: any; sign(hash: Buffer, lowR?: boolean): Promise; + signSchnorr(hash: Buffer): Promise; // todo: remove getPublicKey?(): Buffer; } @@ -895,11 +903,14 @@ function canFinalize( script: Buffer, scriptType: string, ): boolean { + console.log('### scriptType', scriptType) switch (scriptType) { case 'pubkey': case 'pubkeyhash': case 'witnesspubkeyhash': return hasSigs(1, input.partialSig); + case 'taproot': + return true; // finalScriptWitness case 'multisig': const p2ms = payments.p2ms({ output: script }); return hasSigs(p2ms.m!, input.partialSig, p2ms.pubkeys); @@ -939,12 +950,15 @@ function isFinalized(input: PsbtInput): boolean { return !!input.finalScriptSig || !!input.finalScriptWitness; } -function isPaymentFactory(payment: any): (script: Buffer) => boolean { - return (script: Buffer): boolean => { +function isPaymentFactory( + payment: any, +): (script: Buffer, eccLib?: any) => boolean { + return (script: Buffer, eccLib?: any): boolean => { try { - payment({ output: script }); + payment({ output: script }, { eccLib }); return true; } catch (err) { + // console.log('###', err) return false; } }; @@ -955,6 +969,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, @@ -984,10 +999,10 @@ function checkFees(psbt: Psbt, cache: PsbtCache, opts: PsbtOpts): void { if (feeRate >= opts.maximumFeeRate) { throw new Error( `Warning: You are paying around ${(satoshis / 1e8).toFixed(8)} in ` + - `fees, which is ${feeRate} satoshi per byte for a transaction ` + - `with a VSize of ${vsize} bytes (segwit counted as 0.25 byte per ` + - `byte). Use setMaximumFeeRate method to raise your threshold, or ` + - `pass true to the first arg of extractTransaction.`, + `fees, which is ${feeRate} satoshi per byte for a transaction ` + + `with a VSize of ${vsize} bytes (segwit counted as 0.25 byte per ` + + `byte). Use setMaximumFeeRate method to raise your threshold, or ` + + `pass true to the first arg of extractTransaction.`, ); } } @@ -1255,12 +1270,12 @@ function getHashForSig( const str = sighashTypeToString(sighashType); throw new Error( `Sighash type is not allowed. Retry the sign method passing the ` + - `sighashTypes array of whitelisted types. Sighash type: ${str}`, + `sighashTypes array of whitelisted types. Sighash type: ${str}`, ); } let hash: Buffer; let prevout: Output; - + const prevOuts: Output[] = [] if (input.nonWitnessUtxo) { const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( cache, @@ -1280,8 +1295,10 @@ function getHashForSig( const prevoutIndex = unsignedTx.ins[inputIndex].index; prevout = nonWitnessUtxoTx.outs[prevoutIndex] as Output; + prevOuts.push(prevout) } else if (input.witnessUtxo) { prevout = input.witnessUtxo; + prevOuts.push(prevout) } else { throw new Error('Need a Utxo input item for signing'); } @@ -1293,7 +1310,6 @@ function getHashForSig( input.redeemScript, input.witnessScript, ); - if (['p2sh-p2wsh', 'p2wsh'].indexOf(type) >= 0) { hash = unsignedTx.hashForWitnessV0( inputIndex, @@ -1311,6 +1327,20 @@ function getHashForSig( prevout.value, sighashType, ); + } else if (isP2TR(meaningfulScript, ecc)) { + // const signingScripts: any = unsignedTx.outs.map(o => o.script); + // const values: any = unsignedTx.outs.map(o => o.value); + const signingScripts: any = prevOuts.map(o => o.script); + const values: any = prevOuts.map(o => o.value); + console.log('### scriptPubkeys', signingScripts[0].toString('hex')) + console.log('### values, inputIndex', values, inputIndex) + hash = unsignedTx.hashForWitnessV1( + inputIndex, + signingScripts, + values, + Transaction.SIGHASH_DEFAULT, + ); + console.log('### isP2TR hash: ', hash.toString('hex')) } else { // non-segwit if ( @@ -1319,17 +1349,17 @@ function getHashForSig( ) throw new Error( `Input #${inputIndex} has witnessUtxo but non-segwit script: ` + - `${meaningfulScript.toString('hex')}`, + `${meaningfulScript.toString('hex')}`, ); if (!forValidate && cache.__UNSAFE_SIGN_NONSEGWIT !== false) console.warn( 'Warning: Signing non-segwit inputs without the full parent transaction ' + - 'means there is a chance that a miner could feed you incorrect information ' + - "to trick you into paying large fees. This behavior is the same as Psbt's predecesor " + - '(TransactionBuilder - now removed) when signing non-segwit scripts. You are not ' + - 'able to export this Psbt with toBuffer|toBase64|toHex since it is not ' + - 'BIP174 compliant.\n*********************\nPROCEED WITH CAUTION!\n' + - '*********************', + 'means there is a chance that a miner could feed you incorrect information ' + + "to trick you into paying large fees. This behavior is the same as Psbt's predecesor " + + '(TransactionBuilder - now removed) when signing non-segwit scripts. You are not ' + + 'able to export this Psbt with toBuffer|toBase64|toHex since it is not ' + + 'BIP174 compliant.\n*********************\nPROCEED WITH CAUTION!\n' + + '*********************', ); hash = unsignedTx.hashForSignature( inputIndex, @@ -1379,6 +1409,12 @@ function getPayment( signature: partialSig[0].signature, }); break; + case 'taproot': + payment = payments.p2tr({ + output: script, + signature: partialSig[0].signature, + }, { eccLib: ecc }); + break; } return payment!; } @@ -1435,7 +1471,10 @@ function getScriptFromInput( res.script = input.witnessUtxo.script; } } - if (input.witnessScript || isP2WPKH(res.script!)) { + + console.log('### res.scrip', res.script && res.script!.toString('hex')) + console.log('### res.scrip isP2TR', isP2TR(res.script!, ecc)) + if (input.witnessScript || isP2WPKH(res.script!) || isP2TR(res.script!, ecc)) { res.isSegwit = true; } return res; @@ -1795,10 +1834,10 @@ function getMeaningfulScript( type: isP2SHP2WSH ? 'p2sh-p2wsh' : isP2SH - ? 'p2sh' - : isP2WSH - ? 'p2wsh' - : 'raw', + ? 'p2sh' + : isP2WSH + ? 'p2wsh' + : 'raw', }; } @@ -1810,13 +1849,14 @@ 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);; }); } @@ -1825,6 +1865,7 @@ type AllScriptType = | 'pubkeyhash' | 'multisig' | 'pubkey' + | 'taproot' | 'nonstandard' | 'p2sh-witnesspubkeyhash' | 'p2sh-pubkeyhash' @@ -1844,12 +1885,15 @@ type ScriptType = | 'pubkeyhash' | 'multisig' | 'pubkey' + | 'taproot' | 'nonstandard'; function classifyScript(script: Buffer): ScriptType { + // console.log('### script', script.toString('hex')) if (isP2WPKH(script)) return 'witnesspubkeyhash'; if (isP2PKH(script)) return 'pubkeyhash'; if (isP2MS(script)) return 'multisig'; if (isP2PK(script)) return 'pubkey'; + if (isP2TR(script, ecc)) return 'taproot'; return 'nonstandard'; } From 56d30644824d0e6c0bdbd71e509868037a3210e5 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 25 Jan 2022 16:44:26 +0200 Subject: [PATCH 43/53] chore: some clean-up --- package-lock.json | 3403 ++++++++++++++++++++++++++++++++++++++++++++- package.json | 2 +- play.mjs | 19 +- src/psbt.js | 21 +- ts_src/psbt.ts | 98 +- 5 files changed, 3460 insertions(+), 83 deletions(-) diff --git a/package-lock.json b/package-lock.json index f576a482c..7528202bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,8 +1,3399 @@ { "name": "bitcoinjs-lib", "version": "6.0.1", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "bech32": "^2.0.0", + "bip174": "^2.0.1", + "bs58check": "^2.1.2", + "create-hash": "^1.1.0", + "tiny-secp256k1": "^2.2.0", + "typeforce": "^1.11.3", + "varuint-bitcoin": "^1.1.2", + "wif": "^2.0.1" + }, + "devDependencies": { + "@types/bs58": "^4.0.0", + "@types/bs58check": "^2.1.0", + "@types/create-hash": "^1.2.2", + "@types/mocha": "^5.2.7", + "@types/node": "^16.11.7", + "@types/proxyquire": "^1.3.28", + "@types/randombytes": "^2.0.0", + "@types/wif": "^2.0.2", + "bip32": "^3.0.1", + "bip39": "^3.0.2", + "bip65": "^1.0.1", + "bip68": "^1.0.3", + "bs58": "^4.0.0", + "dhttp": "^3.0.0", + "ecpair": "^2.0.1", + "hoodwink": "^2.0.0", + "minimaldata": "^1.0.2", + "mocha": "^7.1.1", + "npm-audit-whitelister": "^1.0.2", + "nyc": "^15.1.0", + "prettier": "1.16.4", + "proxyquire": "^2.0.1", + "randombytes": "^2.1.0", + "regtest-client": "0.2.0", + "rimraf": "^2.6.3", + "ts-node": "^8.3.0", + "tslint": "^6.1.3", + "typescript": "^4.4.4" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.13.tgz", + "integrity": "sha512-BQKE9kXkPlXHPeqissfxo0lySWJcYdEP0hdtJOH/iJfDdhOCcgtNCjftCJg3qqauB4h+lz2N6ixM++b9DN1Tcw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.12.13", + "@babel/helper-module-transforms": "^7.12.13", + "@babel/helpers": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.12.13", + "@babel/types": "^7.12.13", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.19", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.12.13" + } + }, + "node_modules/@babel/core/node_modules/@babel/highlight": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.12.13.tgz", + "integrity": "sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.12.11", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/generator": { + "version": "7.12.15", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.15.tgz", + "integrity": "sha512-6F2xHxBiFXWNSGb7vyCUTBF8RCLY66rS0zEPcP8t/nQyXjha5EuK4z7H5o7fWG8B4M7y6mqVWq1J+1PuwRhecQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.13", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", + "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "dev": true, + "dependencies": { + "@babel/helper-get-function-arity": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "node_modules/@babel/helper-get-function-arity": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", + "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.13" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.13.tgz", + "integrity": "sha512-B+7nN0gIL8FZ8SvMcF+EPyB21KnCcZHQZFczCxbiNGV/O0rsrSBlWGLzmtBJ3GMjSVMIm4lpFhR+VdVBuIsUcQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.13" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.13.tgz", + "integrity": "sha512-NGmfvRp9Rqxy0uHSSVP+SRIW1q31a7Ji10cLBcqSDUngGentY4FRiHOFZFE1CLU5eiL0oE8reH7Tg1y99TDM/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.13" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.13.tgz", + "integrity": "sha512-acKF7EjqOR67ASIlDTupwkKM1eUisNAjaSduo5Cz+793ikfnpe7p4Q7B7EWU2PCoSTPWsQkR7hRUWEIZPiVLGA==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-replace-supers": "^7.12.13", + "@babel/helper-simple-access": "^7.12.13", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/helper-validator-identifier": "^7.12.11", + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.12.13", + "@babel/types": "^7.12.13", + "lodash": "^4.17.19" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz", + "integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.13" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.13.tgz", + "integrity": "sha512-pctAOIAMVStI2TMLhozPKbf5yTEXc0OJa0eENheb4w09SrgOWEs+P4nTOZYJQCqs8JlErGLDPDJTiGIp3ygbLg==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.12.13", + "@babel/helper-optimise-call-expression": "^7.12.13", + "@babel/traverse": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.13.tgz", + "integrity": "sha512-0ski5dyYIHEfwpWGx5GPWhH35j342JaflmCeQmsPWcrOQDtCN6C1zKAVRFVbK53lPW2c9TsuLLSUDf0tIGJ5hA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.13" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", + "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.13" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "node_modules/@babel/helpers": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.13.tgz", + "integrity": "sha512-oohVzLRZ3GQEk4Cjhfs9YkJA4TdIDTObdBEZGrd6F/T0GPSnuV6l22eMcxlvcvzVIPH3VTtxbseudM1zIE+rPQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "node_modules/@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.12.15", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.15.tgz", + "integrity": "sha512-AQBOU2Z9kWwSZMd6lNjCX0GUgFonL1wAM1db8L8PMk9UDaGsRCArBkU4Sc+UCM3AE4hjbXx+h58Lb3QT4oRmrA==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", + "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "node_modules/@babel/template/node_modules/@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.12.13" + } + }, + "node_modules/@babel/template/node_modules/@babel/highlight": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.12.13.tgz", + "integrity": "sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.12.11", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.13.tgz", + "integrity": "sha512-3Zb4w7eE/OslI0fTp8c7b286/cQps3+vdLW3UcwC8VSJC6GbKn55aeVVu2QJNuCDoeKyptLOFrPq8WqZZBodyA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.12.13", + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.12.13" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/highlight": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.12.13.tgz", + "integrity": "sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.12.11", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/types": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.13.tgz", + "integrity": "sha512-oKrdZTld2im1z8bDwTOQvUbxKwE+854zc16qWZQlcTqMN00pWxHQ4ZeOq0yDMnisOpRykH2/5Qqcrk/OlbAjiQ==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@types/base-x": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/base-x/-/base-x-3.0.0.tgz", + "integrity": "sha512-vnqSlpsv9uFX5/z8GyKWAfWHhLGJDBkrgRRsnxlsX23DHOlNyqP/eHQiv4TwnYcZULzQIxaWA/xRWU9Dyy4qzw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/bs58": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/bs58/-/bs58-4.0.0.tgz", + "integrity": "sha512-gYX+MHD4G/R+YGYwdhG5gbJj4LsEQGr3Vg6gVDAbe7xC5Bn8dNNG2Lpo6uDX/rT5dE7VBj0rGEFuV8L0AEx4Rg==", + "dev": true, + "dependencies": { + "@types/base-x": "*" + } + }, + "node_modules/@types/bs58check": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/bs58check/-/bs58check-2.1.0.tgz", + "integrity": "sha512-OxsysnJQh82vy9DRbOcw9m2j/WiyqZLn0YBhKxdQ+aCwoHj+tWzyCgpwAkr79IfDXZKxc6h7k89T9pwS78CqTQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/create-hash": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/create-hash/-/create-hash-1.2.2.tgz", + "integrity": "sha512-Fg8/kfMJObbETFU/Tn+Y0jieYewryLrbKwLCEIwPyklZZVY2qB+64KFjhplGSw+cseZosfFXctXO+PyIYD8iZQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mocha": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "16.11.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.7.tgz", + "integrity": "sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==", + "dev": true + }, + "node_modules/@types/proxyquire": { + "version": "1.3.28", + "resolved": "https://registry.npmjs.org/@types/proxyquire/-/proxyquire-1.3.28.tgz", + "integrity": "sha512-SQaNzWQ2YZSr7FqAyPPiA3FYpux2Lqh3HWMZQk47x3xbMCqgC/w0dY3dw9rGqlweDDkrySQBcaScXWeR+Yb11Q==", + "dev": true + }, + "node_modules/@types/randombytes": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/randombytes/-/randombytes-2.0.0.tgz", + "integrity": "sha512-bz8PhAVlwN72vqefzxa14DKNT8jK/mV66CSjwdVQM/k3Th3EPKfUtdMniwZgMedQTFuywAsfjnZsg+pEnltaMA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/wif": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/wif/-/wif-2.0.2.tgz", + "integrity": "sha512-IiIuBeJzlh4LWJ7kVTrX0nwB60OG0vvGTaWC/SgSbVFw7uYUTF6gEuvDZ1goWkeirekJDD58Y8g7NljQh2fNkA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "node_modules/arg": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.1.tgz", + "integrity": "sha512-SlmP3fEA88MBv0PypnXZ8ZfJhwmDeIE3SP71j37AiXQBXYosPV0x6uISAaHYSlSVhmHOVkomen0tbGk6Anlebw==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "node_modules/base-x": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.5.tgz", + "integrity": "sha512-C3picSgzPSLE+jW3tcBzJoGwitOtazb5B+5YmAxZm2ybmTi9LNgAtDO/jjVEBZwHoXmDBZ9m/IELj3elJVRBcA==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" + }, + "node_modules/binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bip174": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-2.0.1.tgz", + "integrity": "sha512-i3X26uKJOkDTAalYAp0Er+qGMDhrbbh2o93/xiPyAN2s25KrClSpe3VXo/7mNJoqA5qfko8rLS2l3RWZgYmjKQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/bip32": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bip32/-/bip32-3.0.1.tgz", + "integrity": "sha512-Uhpp9aEx3iyiO7CpbNGFxv9WcMIVdGoHG04doQ5Ln0u60uwDah7jUSc3QMV/fSZGm/Oo01/OeAmYevXV+Gz5jQ==", + "dev": true, + "dependencies": { + "@types/node": "10.12.18", + "bs58check": "^2.1.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "typeforce": "^1.11.5", + "wif": "^2.0.6" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bip32/node_modules/@types/node": { + "version": "10.12.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz", + "integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==", + "dev": true + }, + "node_modules/bip39": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.2.tgz", + "integrity": "sha512-J4E1r2N0tUylTKt07ibXvhpT2c5pyAFgvuA5q1H9uDy6dEGpjV8jmymh3MTYJDLCNbIVClSB9FbND49I6N24MQ==", + "dev": true, + "dependencies": { + "@types/node": "11.11.6", + "create-hash": "^1.1.0", + "pbkdf2": "^3.0.9", + "randombytes": "^2.0.1" + } + }, + "node_modules/bip39/node_modules/@types/node": { + "version": "11.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", + "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==", + "dev": true + }, + "node_modules/bip65": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bip65/-/bip65-1.0.3.tgz", + "integrity": "sha512-RQ1nc7xtnLa5XltnCqkoR2zmhuz498RjMJwrLKQzOE049D1HUqnYfon7cVSbwS5UGm0/EQlC2CH+NY3MyITA4Q==", + "dev": true, + "engines": { + "node": ">=4.5.0" + } + }, + "node_modules/bip68": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/bip68/-/bip68-1.0.4.tgz", + "integrity": "sha512-O1htyufFTYy3EO0JkHg2CLykdXEtV2ssqw47Gq9A0WByp662xpJnMEB9m43LZjsSDjIAOozWRExlFQk2hlV1XQ==", + "dev": true, + "engines": { + "node": ">=4.5.0" + } + }, + "node_modules/bitcoin-ops": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz", + "integrity": "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/bs58check": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", + "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", + "dependencies": { + "bs58": "^4.0.0", + "create-hash": "^1.1.0", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "node_modules/builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", + "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.2.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.1.1" + } + }, + "node_modules/cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "dependencies": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-require-extensions": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", + "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", + "dev": true, + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "dependencies": { + "object-keys": "^1.0.12" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/dhttp": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dhttp/-/dhttp-3.0.3.tgz", + "integrity": "sha512-map1b8iyvxSv0uEw3DUDDK5XvH3aYA7QU9DcXy8e3FBIXSwHPHTZWVrOot7Iu9mieWq5XcrZemEJlob6IdCBmg==", + "deprecated": "Not maintained, don't use this", + "dev": true, + "dependencies": { + "statuses": "^1.5.0" + } + }, + "node_modules/diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/ecpair": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ecpair/-/ecpair-2.0.1.tgz", + "integrity": "sha512-iT3wztQMeE/nDTlfnAg8dAFUfBS7Tq2BXzq3ae6L+pWgFU0fQ3l0woTzdTBrJV3OxBjxbzjq8EQhAbEmJNWFSw==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0", + "typeforce": "^1.18.0", + "wif": "^2.0.6" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "node_modules/es-abstract": { + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", + "dev": true, + "dependencies": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fill-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", + "integrity": "sha1-mo+jb06K1jTjv2tPPIiCVRRS6yA=", + "dev": true, + "dependencies": { + "is-object": "~1.0.1", + "merge-descriptors": "~1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "deprecated": "Fixed a prototype pollution security issue in 4.1.0, please upgrade to ^4.1.1 or ^5.0.1.", + "dev": true, + "dependencies": { + "is-buffer": "~2.0.3" + }, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", + "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", + "deprecated": "\"Please update to latest v2.3 or v2.2\"", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "node_modules/growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true, + "engines": { + "node": ">=4.x" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/hoodwink": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hoodwink/-/hoodwink-2.0.0.tgz", + "integrity": "sha512-j1jog3tDfhpWlqbVbh29qc7FG7w+NT4ed+QQFGqvww83+50AzzretB7wykZGOe28mBdvCYH3GdHaVWJQ2lJ/4w==", + "dev": true + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", + "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", + "dev": true + }, + "node_modules/is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", + "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", + "dev": true, + "dependencies": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.0", + "istanbul-lib-coverage": "^3.0.0-alpha.1", + "make-dir": "^3.0.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^3.3.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, + "node_modules/log-symbols": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", + "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "dev": true, + "dependencies": { + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", + "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", + "dev": true + }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "node_modules/minimaldata": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minimaldata/-/minimaldata-1.0.2.tgz", + "integrity": "sha1-AfOywB2LJzmEP9hT1AY2xaLrdms=", + "dev": true, + "dependencies": { + "bitcoin-ops": "^1.3.0", + "pushdata-bitcoin": "^1.0.1" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "node_modules/mkdirp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", + "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mocha": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.1.tgz", + "integrity": "sha512-3qQsu3ijNS3GkWcccT5Zw0hf/rWvu1fTN9sPvEd81hlwsr30GX2GcDSSoBxo24IR8FelmrAydGC6/1J5QQP4WA==", + "dev": true, + "dependencies": { + "ansi-colors": "3.2.3", + "browser-stdout": "1.3.1", + "chokidar": "3.3.0", + "debug": "3.2.6", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "find-up": "3.0.0", + "glob": "7.1.3", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "3.0.0", + "minimatch": "3.0.4", + "mkdirp": "0.5.3", + "ms": "2.1.1", + "node-environment-flags": "1.0.6", + "object.assign": "4.1.0", + "strip-json-comments": "2.0.1", + "supports-color": "6.0.0", + "which": "1.3.1", + "wide-align": "1.1.3", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", + "yargs-unparser": "1.6.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/module-not-found-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", + "integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "node_modules/node-environment-flags": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", + "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", + "dev": true, + "dependencies": { + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" + } + }, + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-audit-whitelister": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/npm-audit-whitelister/-/npm-audit-whitelister-1.0.2.tgz", + "integrity": "sha512-MNaYMUPI4P1cGcnLNvMv0XW4F5NkVEJv2aAfLqXXKY4cgo5lXCHl1h9eUIQnWLKM3WHVOqKzUipMzfunzQZXUg==", + "dev": true, + "bin": { + "npm-audit-whitelister": "bin/npm-audit-whitelister.js" + } + }, + "node_modules/nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/nyc/node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/nyc/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/nyc/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/nyc/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/nyc/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/nyc/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nyc/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nyc/node_modules/string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.getownpropertydescriptors": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", + "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", + "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/pbkdf2": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", + "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "dev": true, + "dependencies": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/picomatch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.1.tgz", + "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/prettier": { + "version": "1.16.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.16.4.tgz", + "integrity": "sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/proxyquire": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.0.tgz", + "integrity": "sha512-kptdFArCfGRtQFv3Qwjr10lwbEV0TBJYvfqzhwucyfEXqVgmnAkyEw/S3FYzR5HI9i5QOq4rcqQjZ6AlknlCDQ==", + "dev": true, + "dependencies": { + "fill-keys": "^1.0.2", + "module-not-found-error": "^1.0.0", + "resolve": "~1.8.1" + } + }, + "node_modules/pushdata-bitcoin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pushdata-bitcoin/-/pushdata-bitcoin-1.0.1.tgz", + "integrity": "sha1-FZMdPNlnreUiBvUjqnMxrvfUOvc=", + "dev": true, + "dependencies": { + "bitcoin-ops": "^1.3.0" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", + "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", + "dev": true, + "dependencies": { + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/regtest-client": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regtest-client/-/regtest-client-0.2.0.tgz", + "integrity": "sha512-eIcC8Kle/wjS47pRlw7nJpstrJDWp0bkvVPl2KJpJcK3JDNW0fMxJgE/CGpMEUSjhhFXW1rtJMN6kyKw5NIzqg==", + "dev": true, + "dependencies": { + "bs58check": "^2.1.2", + "dhttp": "^3.0.3", + "randombytes": "^2.1.0" + } + }, + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", + "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", + "dev": true, + "dependencies": { + "path-parse": "^1.0.5" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "dependencies": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/spawn-wrap/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/spawn-wrap/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/string.prototype.trimleft": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", + "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimright": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", + "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tiny-secp256k1": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.0.tgz", + "integrity": "sha512-2hPuUGCroLrxh6xxwoe+1RgPpOOK1w2uTnhgiHBpvoutBR+krNuT4hOXQyOaaYnZgoXBB6hBYkuAJHxyeBOPzQ==", + "dependencies": { + "uint8array-tools": "0.0.6" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-node": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.3.0.tgz", + "integrity": "sha512-dyNS/RqyVTDcmNM4NIBAeDMpsAdaQ+ojdf0GOLqE6nwJOgzEkdRNzJywhDfwnuvB10oa6NLVG1rUJQCpRN7qoQ==", + "dev": true, + "dependencies": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.6", + "yn": "^3.0.0" + }, + "bin": { + "ts-node": "dist/bin.js" + }, + "engines": { + "node": ">=4.2.0" + }, + "peerDependencies": { + "typescript": ">=2.0" + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", + "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tslint": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz", + "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==", + "deprecated": "TSLint has been deprecated in favor of ESLint. Please see https://github.com/palantir/tslint/issues/4534 for more information.", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.3", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.13.0", + "tsutils": "^2.29.0" + }, + "bin": { + "tslint": "bin/tslint" + }, + "engines": { + "node": ">=4.8.0" + }, + "peerDependencies": { + "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev || >= 4.0.0-dev" + } + }, + "node_modules/tslint/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "peerDependencies": { + "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev" + } + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typeforce": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", + "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" + }, + "node_modules/typescript": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", + "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uint8array-tools": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.6.tgz", + "integrity": "sha512-aIvEHNRX1AlOYAr6qSUjQBn4mCduxx6MtC967aRDTb2UUBPj0Ix2ZFQDcmXUUO/UxRPHcw1f5a5nVbCSKDSOqA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/varuint-bitcoin": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz", + "integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==", + "dependencies": { + "safe-buffer": "^5.1.1" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "node_modules/wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "dependencies": { + "string-width": "^1.0.2 || 2" + } + }, + "node_modules/wif": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", + "integrity": "sha1-CNP1IFbGZnkplyb63g1DKudLRwQ=", + "dependencies": { + "bs58check": "<3.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "node_modules/yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "dependencies": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "node_modules/yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "node_modules/yargs-unparser": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", + "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", + "dev": true, + "dependencies": { + "flat": "^4.1.0", + "lodash": "^4.17.15", + "yargs": "^13.3.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + }, "dependencies": { "@babel/code-frame": { "version": "7.15.8", @@ -2368,10 +5759,9 @@ } }, "tiny-secp256k1": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.1.2.tgz", - "integrity": "sha512-8qPw7zDK6Hco2tVGYGQeOmOPp/hZnREwy2iIkcq0ygAuqc9WHo29vKN94lNymh1QbB3nthtAMF6KTIrdbsIotA==", - "dev": true, + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.0.tgz", + "integrity": "sha512-2hPuUGCroLrxh6xxwoe+1RgPpOOK1w2uTnhgiHBpvoutBR+krNuT4hOXQyOaaYnZgoXBB6hBYkuAJHxyeBOPzQ==", "requires": { "uint8array-tools": "0.0.6" } @@ -2485,8 +5875,7 @@ "uint8array-tools": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.6.tgz", - "integrity": "sha512-aIvEHNRX1AlOYAr6qSUjQBn4mCduxx6MtC967aRDTb2UUBPj0Ix2ZFQDcmXUUO/UxRPHcw1f5a5nVbCSKDSOqA==", - "dev": true + "integrity": "sha512-aIvEHNRX1AlOYAr6qSUjQBn4mCduxx6MtC967aRDTb2UUBPj0Ix2ZFQDcmXUUO/UxRPHcw1f5a5nVbCSKDSOqA==" }, "uuid": { "version": "3.4.0", diff --git a/package.json b/package.json index 13e0e68fb..57c1b4d6e 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "bip174": "^2.0.1", "bs58check": "^2.1.2", "create-hash": "^1.1.0", + "tiny-secp256k1": "^2.2.0", "typeforce": "^1.11.3", "varuint-bitcoin": "^1.1.2", "wif": "^2.0.1" @@ -83,7 +84,6 @@ "randombytes": "^2.1.0", "regtest-client": "0.2.0", "rimraf": "^2.6.3", - "tiny-secp256k1": "^2.1.2", "ts-node": "^8.3.0", "tslint": "^6.1.3", "typescript": "^4.4.4" diff --git a/play.mjs b/play.mjs index 03e5e7561..53e37f52c 100644 --- a/play.mjs +++ b/play.mjs @@ -19,7 +19,11 @@ const outP2trKey = ECPair.fromPrivateKey(hex('900afde76badc8914c9940379c74857d70 const inP2tr = p2tr({ internalPubkey: inP2trKey.publicKey.slice(1), network }, { eccLib: ecc }) const outP2tr = p2tr({ internalPubkey: outP2trKey.publicKey.slice(1), network }, { eccLib: ecc }) -const inTweakedP2trKey = ECPair.fromPrivateKey(Buffer.from(ecc.privateAdd(inP2trKey.privateKey, tapTweakHash(inP2trKey.publicKey.slice(1, 33))))) +const signPrivateKey = outP2trKey.publicKey[0] === 2 + ? outP2trKey.privateKey + : ecc.privateNegate(outP2trKey.privateKey) + +const inTweakedP2trKey = ECPair.fromPrivateKey(Buffer.from(ecc.privateAdd(signPrivateKey, tapTweakHash(outP2trKey.publicKey.slice(1, 33))))) // console.log('### inP2tr.hash ', inP2tr.hash.toString('hex')) console.log('### inP2trKey.privateKey ', inP2trKey.privateKey.toString('hex')) @@ -44,14 +48,14 @@ console.log(''.padEnd(100, '#')) const psbt = new Psbt({ network }) // spend p2tr psbt.addInput({ - hash: hex('1b9e7e80288a059adb9da6fd7c30e7383a5a924f78d3a0b6b047e07345990f48').reverse(), + hash: hex('e7a79cc65d17bebfc4cf03d26d6d879b754afe9cdc56ce98d2f1fac29cc7f1e2').reverse(), index: 0, -}).updateInput(0, { witnessUtxo: { script: inP2tr.output, value: 70000 } }) +}).updateInput(0, { witnessUtxo: { script: outP2tr.output, value: 68000 } }) // test with full utxo also psbt.addOutput({ - address: outP2tr.address, - value: 69000 + address: inP2tr.address, + value: 67000 }) @@ -63,4 +67,7 @@ const tx = psbt.extractTransaction() const rawTx = tx.toBuffer() console.log('### rawTx', rawTx.toString('hex')) -/// 1b9e7e80288a059adb9da6fd7c30e7383a5a924f78d3a0b6b047e07345990f48 \ No newline at end of file +/// 1b9e7e80288a059adb9da6fd7c30e7383a5a924f78d3a0b6b047e07345990f48 +/// b2138ee2639c569fe7f2666635399caaa7a435b3763c079f069e194116840892 +/// e7a79cc65d17bebfc4cf03d26d6d879b754afe9cdc56ce98d2f1fac29cc7f1e2 +/// 8b9fd7f222dfa16191a15485e241d2e94baa1bef3e6a989cf2d584bda800d066 \ No newline at end of file diff --git a/src/psbt.js b/src/psbt.js index f10ab97d3..ddf41b9ca 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -281,7 +281,6 @@ class Psbt { ); if (!script) throw new Error(`No script found for input #${inputIndex}`); checkPartialSigSighashes(input); - console.log('### isSegwit', isSegwit); const { finalScriptSig, finalScriptWitness } = finalScriptsFunc( inputIndex, input, @@ -517,10 +516,7 @@ class Psbt { signInput( inputIndex, keyPair, - sighashTypes = [ - transaction_1.Transaction.SIGHASH_ALL, - transaction_1.Transaction.SIGHASH_DEFAULT, - ], + sighashTypes = [transaction_1.Transaction.SIGHASH_ALL], ) { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); @@ -531,7 +527,6 @@ class Psbt { this.__CACHE, sighashTypes, ); - console.log('### hash', hash.toString('hex')); const partialSig = [ { pubkey: keyPair.publicKey, @@ -674,14 +669,12 @@ class PsbtTransaction { } } function canFinalize(input, script, scriptType) { - console.log('### scriptType', scriptType); switch (scriptType) { case 'pubkey': case 'pubkeyhash': case 'witnesspubkeyhash': - return hasSigs(1, input.partialSig); case 'taproot': - return true; // finalScriptWitness + return hasSigs(1, input.partialSig); case 'multisig': const p2ms = payments.p2ms({ output: script }); return hasSigs(p2ms.m, input.partialSig, p2ms.pubkeys); @@ -719,7 +712,6 @@ function isPaymentFactory(payment) { payment({ output: script }, { eccLib }); return true; } catch (err) { - // console.log('###', err) return false; } }; @@ -1004,20 +996,14 @@ function getHashForSig(inputIndex, input, cache, forValidate, sighashTypes) { sighashType, ); } else if (isP2TR(meaningfulScript, ecc)) { - // const signingScripts: any = unsignedTx.outs.map(o => o.script); - // const values: any = unsignedTx.outs.map(o => o.value); const signingScripts = prevOuts.map(o => o.script); const values = prevOuts.map(o => o.value); - console.log('### scriptPubkeys', signingScripts[0].toString('hex')); - console.log('### values, inputIndex', values, inputIndex); - console.log('### unsignedTx', unsignedTx); hash = unsignedTx.hashForWitnessV1( inputIndex, signingScripts, values, transaction_1.Transaction.SIGHASH_DEFAULT, ); - console.log('### isP2TR hash: ', hash.toString('hex')); } else { // non-segwit if ( @@ -1133,8 +1119,6 @@ function getScriptFromInput(inputIndex, input, cache) { res.script = input.witnessUtxo.script; } } - console.log('### res.scrip', res.script && res.script.toString('hex')); - console.log('### res.scrip isP2TR', isP2TR(res.script, ecc)); if (input.witnessScript || isP2WPKH(res.script) || isP2TR(res.script, ecc)) { res.isSegwit = true; } @@ -1446,7 +1430,6 @@ function pubkeyInScript(pubkey, script) { }); } function classifyScript(script) { - // console.log('### script', script.toString('hex')) if (isP2WPKH(script)) return 'witnesspubkeyhash'; if (isP2PKH(script)) return 'pubkeyhash'; if (isP2MS(script)) return 'multisig'; diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 08f301452..e5370e5c4 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -189,7 +189,7 @@ export class Psbt { let address; try { address = fromOutputScript(output.script, this.opts.network); - } catch (_) { } + } catch (_) {} return { script: cloneBuffer(output.script), value: output.value, @@ -259,7 +259,7 @@ export class Psbt { ) { throw new Error( `Invalid arguments for Psbt.addInput. ` + - `Requires single object with at least [hash] and [index]`, + `Requires single object with at least [hash] and [index]`, ); } checkInputsForPartialSig(this.data.inputs, 'addInput'); @@ -295,7 +295,7 @@ export class Psbt { ) { throw new Error( `Invalid arguments for Psbt.addOutput. ` + - `Requires single object with at least [script or address] and [value]`, + `Requires single object with at least [script or address] and [value]`, ); } checkInputsForPartialSig(this.data.inputs, 'addOutput'); @@ -358,7 +358,6 @@ export class Psbt { checkPartialSigSighashes(input); - console.log('### isSegwit', isSegwit) const { finalScriptSig, finalScriptWitness } = finalScriptsFunc( inputIndex, input, @@ -387,7 +386,7 @@ export class Psbt { 'input', input.redeemScript || redeemFromFinalScriptSig(input.finalScriptSig), input.witnessScript || - redeemFromFinalWitnessScript(input.finalScriptWitness), + redeemFromFinalWitnessScript(input.finalScriptWitness), ); const type = result.type === 'raw' ? '' : result.type + '-'; const mainType = classifyScript(result.meaningfulScript); @@ -452,11 +451,11 @@ export class Psbt { const { hash, script } = sighashCache! !== sig.hashType ? getHashForSig( - inputIndex, - Object.assign({}, input, { sighashType: sig.hashType }), - this.__CACHE, - true, - ) + inputIndex, + Object.assign({}, input, { sighashType: sig.hashType }), + this.__CACHE, + true, + ) : { hash: hashCache!, script: scriptCache! }; sighashCache = sig.hashType; hashCache = hash; @@ -632,7 +631,7 @@ export class Psbt { signInput( inputIndex: number, keyPair: Signer, - sighashTypes: number[] = [Transaction.SIGHASH_ALL, Transaction.SIGHASH_DEFAULT], + sighashTypes: number[] = [Transaction.SIGHASH_ALL], ): this { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); @@ -644,7 +643,6 @@ export class Psbt { sighashTypes, ); - console.log('### hash', hash.toString('hex')) const partialSig = [ { pubkey: keyPair.publicKey, @@ -766,7 +764,7 @@ interface PsbtOpts { maximumFeeRate: number; } -interface PsbtInputExtended extends PsbtInput, TransactionInput { } +interface PsbtInputExtended extends PsbtInput, TransactionInput {} type PsbtOutputExtended = PsbtOutputExtendedAddress | PsbtOutputExtendedScript; @@ -903,14 +901,12 @@ function canFinalize( script: Buffer, scriptType: string, ): boolean { - console.log('### scriptType', scriptType) switch (scriptType) { case 'pubkey': case 'pubkeyhash': case 'witnesspubkeyhash': - return hasSigs(1, input.partialSig); case 'taproot': - return true; // finalScriptWitness + return hasSigs(1, input.partialSig); case 'multisig': const p2ms = payments.p2ms({ output: script }); return hasSigs(p2ms.m!, input.partialSig, p2ms.pubkeys); @@ -958,7 +954,6 @@ function isPaymentFactory( payment({ output: script }, { eccLib }); return true; } catch (err) { - // console.log('###', err) return false; } }; @@ -999,10 +994,10 @@ function checkFees(psbt: Psbt, cache: PsbtCache, opts: PsbtOpts): void { if (feeRate >= opts.maximumFeeRate) { throw new Error( `Warning: You are paying around ${(satoshis / 1e8).toFixed(8)} in ` + - `fees, which is ${feeRate} satoshi per byte for a transaction ` + - `with a VSize of ${vsize} bytes (segwit counted as 0.25 byte per ` + - `byte). Use setMaximumFeeRate method to raise your threshold, or ` + - `pass true to the first arg of extractTransaction.`, + `fees, which is ${feeRate} satoshi per byte for a transaction ` + + `with a VSize of ${vsize} bytes (segwit counted as 0.25 byte per ` + + `byte). Use setMaximumFeeRate method to raise your threshold, or ` + + `pass true to the first arg of extractTransaction.`, ); } } @@ -1270,12 +1265,12 @@ function getHashForSig( const str = sighashTypeToString(sighashType); throw new Error( `Sighash type is not allowed. Retry the sign method passing the ` + - `sighashTypes array of whitelisted types. Sighash type: ${str}`, + `sighashTypes array of whitelisted types. Sighash type: ${str}`, ); } let hash: Buffer; let prevout: Output; - const prevOuts: Output[] = [] + const prevOuts: Output[] = []; if (input.nonWitnessUtxo) { const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( cache, @@ -1295,10 +1290,10 @@ function getHashForSig( const prevoutIndex = unsignedTx.ins[inputIndex].index; prevout = nonWitnessUtxoTx.outs[prevoutIndex] as Output; - prevOuts.push(prevout) + prevOuts.push(prevout); } else if (input.witnessUtxo) { prevout = input.witnessUtxo; - prevOuts.push(prevout) + prevOuts.push(prevout); } else { throw new Error('Need a Utxo input item for signing'); } @@ -1328,19 +1323,14 @@ function getHashForSig( sighashType, ); } else if (isP2TR(meaningfulScript, ecc)) { - // const signingScripts: any = unsignedTx.outs.map(o => o.script); - // const values: any = unsignedTx.outs.map(o => o.value); const signingScripts: any = prevOuts.map(o => o.script); const values: any = prevOuts.map(o => o.value); - console.log('### scriptPubkeys', signingScripts[0].toString('hex')) - console.log('### values, inputIndex', values, inputIndex) hash = unsignedTx.hashForWitnessV1( inputIndex, signingScripts, values, Transaction.SIGHASH_DEFAULT, ); - console.log('### isP2TR hash: ', hash.toString('hex')) } else { // non-segwit if ( @@ -1349,17 +1339,17 @@ function getHashForSig( ) throw new Error( `Input #${inputIndex} has witnessUtxo but non-segwit script: ` + - `${meaningfulScript.toString('hex')}`, + `${meaningfulScript.toString('hex')}`, ); if (!forValidate && cache.__UNSAFE_SIGN_NONSEGWIT !== false) console.warn( 'Warning: Signing non-segwit inputs without the full parent transaction ' + - 'means there is a chance that a miner could feed you incorrect information ' + - "to trick you into paying large fees. This behavior is the same as Psbt's predecesor " + - '(TransactionBuilder - now removed) when signing non-segwit scripts. You are not ' + - 'able to export this Psbt with toBuffer|toBase64|toHex since it is not ' + - 'BIP174 compliant.\n*********************\nPROCEED WITH CAUTION!\n' + - '*********************', + 'means there is a chance that a miner could feed you incorrect information ' + + "to trick you into paying large fees. This behavior is the same as Psbt's predecesor " + + '(TransactionBuilder - now removed) when signing non-segwit scripts. You are not ' + + 'able to export this Psbt with toBuffer|toBase64|toHex since it is not ' + + 'BIP174 compliant.\n*********************\nPROCEED WITH CAUTION!\n' + + '*********************', ); hash = unsignedTx.hashForSignature( inputIndex, @@ -1410,10 +1400,13 @@ function getPayment( }); break; case 'taproot': - payment = payments.p2tr({ - output: script, - signature: partialSig[0].signature, - }, { eccLib: ecc }); + payment = payments.p2tr( + { + output: script, + signature: partialSig[0].signature, + }, + { eccLib: ecc }, + ); break; } return payment!; @@ -1472,9 +1465,11 @@ function getScriptFromInput( } } - console.log('### res.scrip', res.script && res.script!.toString('hex')) - console.log('### res.scrip isP2TR', isP2TR(res.script!, ecc)) - if (input.witnessScript || isP2WPKH(res.script!) || isP2TR(res.script!, ecc)) { + if ( + input.witnessScript || + isP2WPKH(res.script!) || + isP2TR(res.script!, ecc) + ) { res.isSegwit = true; } return res; @@ -1834,10 +1829,10 @@ function getMeaningfulScript( type: isP2SHP2WSH ? 'p2sh-p2wsh' : isP2SH - ? 'p2sh' - : isP2WSH - ? 'p2wsh' - : 'raw', + ? 'p2sh' + : isP2WSH + ? 'p2wsh' + : 'raw', }; } @@ -1856,7 +1851,11 @@ function pubkeyInScript(pubkey: Buffer, script: Buffer): boolean { return decompiled.some(element => { if (typeof element === 'number') return false; - return element.equals(pubkey) || element.equals(pubkeyHash) || element.equals(pubkeyXOnly);; + return ( + element.equals(pubkey) || + element.equals(pubkeyHash) || + element.equals(pubkeyXOnly) + ); }); } @@ -1888,7 +1887,6 @@ type ScriptType = | 'taproot' | 'nonstandard'; function classifyScript(script: Buffer): ScriptType { - // console.log('### script', script.toString('hex')) if (isP2WPKH(script)) return 'witnesspubkeyhash'; if (isP2PKH(script)) return 'pubkeyhash'; if (isP2MS(script)) return 'multisig'; From 9a3c13efe4a30b3cb3f18fd61fe36a5650499bbc Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 26 Jan 2022 11:18:13 +0200 Subject: [PATCH 44/53] test: fix unit test for signature validation --- test/fixtures/p2tr.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/fixtures/p2tr.json b/test/fixtures/p2tr.json index 02b1cb3c0..4c7174fc0 100644 --- a/test/fixtures/p2tr.json +++ b/test/fixtures/p2tr.json @@ -47,7 +47,7 @@ "description": "address, output and witness from pubkey and signature", "arguments": { "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", - "signature": "300602010002010001" + "signature": "a251221c339a7129dd0b769698aca40d8d9da9570ab796a1820b91ab7dbf5acbea21c88ba8f1e9308a21729baf080734beaf97023882d972f75e380d480fd704" }, "expected": { "name": "p2tr", @@ -55,7 +55,7 @@ "output": "OP_1 ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", "input": null, "witness": [ - "300602010002010001" + "a251221c339a7129dd0b769698aca40d8d9da9570ab796a1820b91ab7dbf5acbea21c88ba8f1e9308a21729baf080734beaf97023882d972f75e380d480fd704" ] } }, @@ -908,9 +908,9 @@ "exception": "Signature mismatch", "arguments": { "pubkey": "ab610d22c801def8a1e02368d1b92018970eb52a729919705e8a1a2f60c750f5", - "signature": "300602010002010002", + "signature": "a251221c339a7129dd0b769698aca40d8d9da9570ab796a1820b91ab7dbf5acbea21c88ba8f1e9308a21729baf080734beaf97023882d972f75e380d480fd704", "witness": [ - "300602010002010001" + "607b8b5b5c8614757736e3d5811790636d2a8e2ea14418f8cff66b2e898b3b7536a49b7c4bc8b3227953194bf5d0548e13e3526fdb36beeefadda1ec834a0db2" ] } }, From 6c9b2ea92994aaa305bfae9e355710c0a3235499 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 26 Jan 2022 12:08:03 +0200 Subject: [PATCH 45/53] chore: more testing --- src/psbt.d.ts | 8 +++--- src/psbt.js | 75 ++++++++++++++++++++++++++++++++++++------------ ts_src/psbt.ts | 77 +++++++++++++++++++++++++++++++++++--------------- 3 files changed, 116 insertions(+), 44 deletions(-) diff --git a/src/psbt.d.ts b/src/psbt.d.ts index 241c66a26..e1e080c19 100644 --- a/src/psbt.d.ts +++ b/src/psbt.d.ts @@ -143,7 +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; + signSchnorr?(hash: Buffer): Buffer; } /** * Same as above but with async sign method @@ -151,20 +151,20 @@ export interface HDSigner extends HDSignerBase { export interface HDSignerAsync extends HDSignerBase { derivePath(path: string): HDSignerAsync; sign(hash: Buffer): Promise; - signSchnorr(hash: Buffer): Promise; + signSchnorr?(hash: Buffer): Promise; } export interface Signer { publicKey: Buffer; network?: any; sign(hash: Buffer, lowR?: boolean): Buffer; - signSchnorr(hash: Buffer): Buffer; + signSchnorr?(hash: Buffer): Buffer; getPublicKey?(): Buffer; } export interface SignerAsync { publicKey: Buffer; network?: any; sign(hash: Buffer, lowR?: boolean): Promise; - signSchnorr(hash: Buffer): Promise; + signSchnorr?(hash: Buffer): Promise; getPublicKey?(): Buffer; } /** diff --git a/src/psbt.js b/src/psbt.js index ddf41b9ca..8a1213c4b 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -299,7 +299,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, @@ -363,6 +367,7 @@ class Psbt { ? getHashForSig( inputIndex, Object.assign({}, input, { sighashType: sig.hashType }), + this.data.inputs, this.__CACHE, true, ) @@ -520,21 +525,37 @@ class Psbt { ) { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); - const { hash } = getHashAndSighashType( + const { hash, sighashType } = getHashAndSighashType( this.data.inputs, inputIndex, keyPair.publicKey, this.__CACHE, sighashTypes, ); - const partialSig = [ - { - pubkey: keyPair.publicKey, - signature: keyPair.signSchnorr(hash), // change - }, - ]; - // this.data.updateInput(inputIndex, { partialSig }); - this.data.inputs[inputIndex].partialSig = partialSig; + const scriptType = this.getInputType(inputIndex); + if (scriptType === 'taproot') { + if (!keyPair.signSchnorr) { + throw new Error( + `Need Schnorr Signer to sign taproot input #${inputIndex}.`, + ); + } + const partialSig = [ + { + pubkey: keyPair.publicKey, + 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( @@ -924,6 +945,7 @@ function getHashAndSighashType( const { hash, sighashType, script } = getHashForSig( inputIndex, input, + inputs, cache, false, sighashTypes, @@ -934,7 +956,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; @@ -947,7 +976,6 @@ function getHashForSig(inputIndex, input, cache, forValidate, sighashTypes) { } let hash; let prevout; - const prevOuts = []; if (input.nonWitnessUtxo) { const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( cache, @@ -964,10 +992,8 @@ function getHashForSig(inputIndex, input, cache, forValidate, sighashTypes) { } const prevoutIndex = unsignedTx.ins[inputIndex].index; prevout = nonWitnessUtxoTx.outs[prevoutIndex]; - prevOuts.push(prevout); } else if (input.witnessUtxo) { prevout = input.witnessUtxo; - prevOuts.push(prevout); } else { throw new Error('Need a Utxo input item for signing'); } @@ -996,8 +1022,16 @@ function getHashForSig(inputIndex, input, cache, forValidate, sighashTypes) { sighashType, ); } else if (isP2TR(meaningfulScript, ecc)) { + const prevOuts = inputs.map((input, index) => + getScriptAndAmountFromUtxo(index, input, cache), + ); const signingScripts = prevOuts.map(o => o.script); const values = prevOuts.map(o => o.value); + console.log( + '### signingScripts', + signingScripts.map(s => s.toString('hex')), + ); + console.log('### values', values); hash = unsignedTx.hashForWitnessV1( inputIndex, signingScripts, @@ -1292,22 +1326,27 @@ 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 { + console.trace('###'); 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, diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index e5370e5c4..a549a731e 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -379,7 +379,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, @@ -453,6 +457,7 @@ export class Psbt { ? getHashForSig( inputIndex, Object.assign({}, input, { sighashType: sig.hashType }), + this.data.inputs, this.__CACHE, true, ) @@ -635,7 +640,8 @@ export class Psbt { ): this { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); - const { hash } = getHashAndSighashType( + + const { hash, sighashType } = getHashAndSighashType( this.data.inputs, inputIndex, keyPair.publicKey, @@ -643,15 +649,32 @@ export class Psbt { sighashTypes, ); - const partialSig = [ - { - pubkey: keyPair.publicKey, - signature: keyPair.signSchnorr(hash), // change - }, - ]; + const scriptType = this.getInputType(inputIndex); + + if (scriptType === 'taproot') { + if (!keyPair.signSchnorr) { + throw new Error( + `Need Schnorr Signer to sign taproot input #${inputIndex}.`, + ); + } + const partialSig = [ + { + pubkey: keyPair.publicKey, + 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 }); - this.data.inputs[inputIndex].partialSig = partialSig; return this; } @@ -800,7 +823,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; // todo: remove + signSchnorr?(hash: Buffer): Buffer; // todo: remove } /** @@ -809,14 +832,14 @@ export interface HDSigner extends HDSignerBase { export interface HDSignerAsync extends HDSignerBase { derivePath(path: string): HDSignerAsync; sign(hash: Buffer): Promise; - signSchnorr(hash: Buffer): Promise; // todo: remove + signSchnorr?(hash: Buffer): Promise; // todo: remove } export interface Signer { publicKey: Buffer; network?: any; sign(hash: Buffer, lowR?: boolean): Buffer; - signSchnorr(hash: Buffer): Buffer; // todo: remove + signSchnorr?(hash: Buffer): Buffer; // todo: remove getPublicKey?(): Buffer; } @@ -824,7 +847,7 @@ export interface SignerAsync { publicKey: Buffer; network?: any; sign(hash: Buffer, lowR?: boolean): Promise; - signSchnorr(hash: Buffer): Promise; // todo: remove + signSchnorr?(hash: Buffer): Promise; // todo: remove getPublicKey?(): Buffer; } @@ -1237,6 +1260,7 @@ function getHashAndSighashType( const { hash, sighashType, script } = getHashForSig( inputIndex, input, + inputs, cache, false, sighashTypes, @@ -1251,6 +1275,7 @@ function getHashAndSighashType( function getHashForSig( inputIndex: number, input: PsbtInput, + inputs: PsbtInput[], cache: PsbtCache, forValidate: boolean, sighashTypes?: number[], @@ -1270,7 +1295,6 @@ function getHashForSig( } let hash: Buffer; let prevout: Output; - const prevOuts: Output[] = []; if (input.nonWitnessUtxo) { const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( cache, @@ -1290,10 +1314,8 @@ function getHashForSig( const prevoutIndex = unsignedTx.ins[inputIndex].index; prevout = nonWitnessUtxoTx.outs[prevoutIndex] as Output; - prevOuts.push(prevout); } else if (input.witnessUtxo) { prevout = input.witnessUtxo; - prevOuts.push(prevout); } else { throw new Error('Need a Utxo input item for signing'); } @@ -1323,8 +1345,14 @@ function getHashForSig( sighashType, ); } else if (isP2TR(meaningfulScript, ecc)) { + const prevOuts: Output[] = inputs.map((input, index) => + getScriptAndAmountFromUtxo(index, input, cache), + ); const signingScripts: any = prevOuts.map(o => o.script); const values: any = prevOuts.map(o => o.value); + + console.log('### signingScripts', signingScripts.map((s: any) => s.toString('hex'))); + console.log('### values', values); hash = unsignedTx.hashForWitnessV1( inputIndex, signingScripts, @@ -1685,21 +1713,26 @@ 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 { + console.trace('###'); throw new Error("Can't find pubkey in input without Utxo data"); } } @@ -1710,7 +1743,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, From ea3872f8731243f617a73b490f0e979dbbc74853 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 26 Jan 2022 13:00:33 +0200 Subject: [PATCH 46/53] chore code clean-up --- play.mjs | 49 ++++++++++++++++++++++++++++++++++++------------- src/psbt.js | 7 +------ ts_src/psbt.ts | 15 +++++++-------- 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/play.mjs b/play.mjs index 53e37f52c..fd7fa64ed 100644 --- a/play.mjs +++ b/play.mjs @@ -13,19 +13,25 @@ const hex = (s) => Buffer.from(s, 'hex') const inP2pkhKey = ECPair.fromPrivateKey(hex('82fd530c9eb33570c7e05ca5e80b740bcf1118e8f4c73d44a801fa9dd60f6449')) const inP2wpkhKey = ECPair.fromPrivateKey(hex('35dfb4dc373860005d6f74d37064e328bc772343c354c95e31b654d0c5e22f58')) const inP2trKey = ECPair.fromPrivateKey(hex('accaf12e04e11b08fc28f5fe75b47ea663843b698981e31f1cafa2224d6e28c0')) + const outP2trKey = ECPair.fromPrivateKey(hex('900afde76badc8914c9940379c74857d70b4d7da590097285572df6b88ad2975')) +const outP2wpkhKey = ECPair.fromPrivateKey(hex('65ba77c6052f41325d13df8c740b5e33a26d6612e1923bf3afd67ad8081227ee')) +const inP2wpkh = p2wpkh({ pubkey: inP2wpkhKey.publicKey, network }) const inP2tr = p2tr({ internalPubkey: inP2trKey.publicKey.slice(1), network }, { eccLib: ecc }) const outP2tr = p2tr({ internalPubkey: outP2trKey.publicKey.slice(1), network }, { eccLib: ecc }) -const signPrivateKey = outP2trKey.publicKey[0] === 2 - ? outP2trKey.privateKey - : ecc.privateNegate(outP2trKey.privateKey) +const outP2wpkh = p2wpkh({ pubkey: outP2wpkhKey.publicKey, network }) + + +const signPrivateKey = inP2trKey.publicKey[0] === 2 + ? inP2trKey.privateKey + : ecc.privateNegate(inP2trKey.privateKey) -const inTweakedP2trKey = ECPair.fromPrivateKey(Buffer.from(ecc.privateAdd(signPrivateKey, tapTweakHash(outP2trKey.publicKey.slice(1, 33))))) +const inTweakedP2trKey = ECPair.fromPrivateKey(Buffer.from(ecc.privateAdd(signPrivateKey, tapTweakHash(inP2trKey.publicKey.slice(1, 33))))) + -// console.log('### inP2tr.hash ', inP2tr.hash.toString('hex')) console.log('### inP2trKey.privateKey ', inP2trKey.privateKey.toString('hex')) console.log('### inP2trKey.publicKey ', inP2trKey.publicKey.toString('hex')) console.log('### inTweakedP2trKey.privateKey ', inTweakedP2trKey.privateKey.toString('hex')) @@ -39,27 +45,43 @@ console.log('### inP2tr.pubkey ', inP2tr.pubkey.toString('hex')) console.log(''.padEnd(100, '#')) console.log('### p2pkh.address ', p2pkh({ pubkey: inP2pkhKey.publicKey, network }).address) -console.log('### p2wpkh.address ', p2wpkh({ pubkey: inP2wpkhKey.publicKey, network }).address) +console.log('### p2wpkh.address ', inP2wpkh.address) console.log('### inP2tr.address ', inP2tr.address) console.log('### outP2tr.address ', outP2tr.address) console.log(''.padEnd(100, '#')) const psbt = new Psbt({ network }) +// spend p2pkh +psbt.addInput({ + hash: hex('32833f8502f64f85674d2b637ec3ff0032d5585cd305b8d68db4b83ed977f303').reverse(), + index: 0, +}).updateInput(0, { nonWitnessUtxo: hex('0200000000010147d8d83d6dd1dc8c7841f7f42d7239d2318a0a6a9bb5c936a1d54f72dcf859220000000000feffffff02122c1b00000000001976a9149e7ef1767764ff34a0595dbc4c2b70db017ed06688ac9874aeb10000000017a914b0c19f9f547df19b5fbbbfa25c850c6d1e1b550987024730440220301ddecd390bad5f957545a8eb68bf43681d11abf0a15c3a91fa47ff35178e8402206dfa3f611f2bd3da1604c9ff1f758b0cf4881b9f26ed3d5fa44531c36ba463d001210269997f09a81ec9829043a7f407d14e1fbceb799445e96613c852a8be0c1b5132749c2000') }) + // spend p2tr psbt.addInput({ - hash: hex('e7a79cc65d17bebfc4cf03d26d6d879b754afe9cdc56ce98d2f1fac29cc7f1e2').reverse(), + hash: hex('8b9fd7f222dfa16191a15485e241d2e94baa1bef3e6a989cf2d584bda800d066').reverse(), index: 0, -}).updateInput(0, { witnessUtxo: { script: outP2tr.output, value: 68000 } }) -// test with full utxo also +}).updateInput(1, { witnessUtxo: { script: inP2tr.output, value: 67000 } }) + +// spend pwpkh +psbt.addInput({ + hash: hex('2258ccf6dcb061db928b8f74c6bb74596b43935eb04f03c5d431020ba5e4877e').reverse(), + index: 0, +}).updateInput(2, { witnessUtxo: { script: inP2wpkh.output, value: 10000 } }) psbt.addOutput({ - address: inP2tr.address, + address: outP2tr.address, + value: 1780000 +}) +psbt.addOutput({ + address: outP2wpkh.address, value: 67000 }) - -psbt.signInput(0, inTweakedP2trKey) +psbt.signInput(0, inP2pkhKey) +psbt.signInput(1, inTweakedP2trKey) +psbt.signInput(2, inP2wpkhKey) // Serialize tx psbt.finalizeAllInputs() @@ -70,4 +92,5 @@ console.log('### rawTx', rawTx.toString('hex')) /// 1b9e7e80288a059adb9da6fd7c30e7383a5a924f78d3a0b6b047e07345990f48 /// b2138ee2639c569fe7f2666635399caaa7a435b3763c079f069e194116840892 /// e7a79cc65d17bebfc4cf03d26d6d879b754afe9cdc56ce98d2f1fac29cc7f1e2 -/// 8b9fd7f222dfa16191a15485e241d2e94baa1bef3e6a989cf2d584bda800d066 \ No newline at end of file +/// 8b9fd7f222dfa16191a15485e241d2e94baa1bef3e6a989cf2d584bda800d066 +/// 735321667cf43ded23dc935f355c4b1b1e77d9a47d11c77d384af5094e1ad171 -> 3 inputs, 2 outputs \ No newline at end of file diff --git a/src/psbt.js b/src/psbt.js index 8a1213c4b..191bf7ceb 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -1027,17 +1027,13 @@ function getHashForSig( ); const signingScripts = prevOuts.map(o => o.script); const values = prevOuts.map(o => o.value); - console.log( - '### signingScripts', - signingScripts.map(s => s.toString('hex')), - ); - console.log('### values', values); hash = unsignedTx.hashForWitnessV1( inputIndex, signingScripts, values, transaction_1.Transaction.SIGHASH_DEFAULT, ); + console.log('### hash', hash.toString('hex')); } else { // non-segwit if ( @@ -1341,7 +1337,6 @@ function getScriptAndAmountFromUtxo(inputIndex, input, cache) { const o = nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index]; return { script: o.script, value: o.value }; } else { - console.trace('###'); throw new Error("Can't find pubkey in input without Utxo data"); } } diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index a549a731e..a2bbcc24f 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -578,7 +578,6 @@ export class Psbt { ): this { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); - // TODO: Add a pubkey/pubkeyhash cache to each input // as input information is added, then eventually // optimize this method. @@ -823,7 +822,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; // todo: remove + signSchnorr?(hash: Buffer): Buffer; } /** @@ -832,14 +831,14 @@ export interface HDSigner extends HDSignerBase { export interface HDSignerAsync extends HDSignerBase { derivePath(path: string): HDSignerAsync; sign(hash: Buffer): Promise; - signSchnorr?(hash: Buffer): Promise; // todo: remove + signSchnorr?(hash: Buffer): Promise; } export interface Signer { publicKey: Buffer; network?: any; sign(hash: Buffer, lowR?: boolean): Buffer; - signSchnorr?(hash: Buffer): Buffer; // todo: remove + signSchnorr?(hash: Buffer): Buffer; getPublicKey?(): Buffer; } @@ -847,7 +846,7 @@ export interface SignerAsync { publicKey: Buffer; network?: any; sign(hash: Buffer, lowR?: boolean): Promise; - signSchnorr?(hash: Buffer): Promise; // todo: remove + signSchnorr?(hash: Buffer): Promise; getPublicKey?(): Buffer; } @@ -1295,6 +1294,7 @@ function getHashForSig( } let hash: Buffer; let prevout: Output; + if (input.nonWitnessUtxo) { const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( cache, @@ -1327,6 +1327,7 @@ function getHashForSig( input.redeemScript, input.witnessScript, ); + if (['p2sh-p2wsh', 'p2wsh'].indexOf(type) >= 0) { hash = unsignedTx.hashForWitnessV0( inputIndex, @@ -1351,14 +1352,13 @@ function getHashForSig( const signingScripts: any = prevOuts.map(o => o.script); const values: any = prevOuts.map(o => o.value); - console.log('### signingScripts', signingScripts.map((s: any) => s.toString('hex'))); - console.log('### values', values); hash = unsignedTx.hashForWitnessV1( inputIndex, signingScripts, values, Transaction.SIGHASH_DEFAULT, ); + console.log('### hash', hash.toString('hex')); } else { // non-segwit if ( @@ -1732,7 +1732,6 @@ function getScriptAndAmountFromUtxo( const o = nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index]; return { script: o.script, value: o.value }; } else { - console.trace('###'); throw new Error("Can't find pubkey in input without Utxo data"); } } From 6c0a6e7693a1e91f2ed615f18aa34bcd38e9ce03 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 26 Jan 2022 14:28:11 +0200 Subject: [PATCH 47/53] test: add more tests --- test/fixtures/psbt.json | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 0e51d57cf..4d5350662 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 onne P2TR output.", + "psbt": "cHNidP8BAF4CAAAAAWbQAKi9hNXynJhqPu8bqkvp0kHihVShkWGh3yLy15+LAAAAAAD/////Aej9AAAAAAAAIlEgRvZJfLLxnVDD6emCqVDcyGIUsB/M5DekIGHHvbEjDTMAAAAAAAEBK7gFAQAAAAAAIlEglCHnNLD50sRn6n3Rl8Yay0RnzcvJ9MsMVx+LY6XEDK4AAA==" } ], "failSignChecks": [ @@ -273,6 +277,25 @@ } ], "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=", + "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=" } ], "combiner": [ From 4ce7e27f17b645b05b86146b461e9862a024c88f Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 27 Jan 2022 13:08:52 +0200 Subject: [PATCH 48/53] feat: add tweakSigner() to PSBT --- src/psbt.d.ts | 22 ++++++++++++++++++++++ src/psbt.js | 36 +++++++++++++++++++++++++++++++++++- ts_src/psbt.ts | 50 +++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 106 insertions(+), 2 deletions(-) diff --git a/src/psbt.d.ts b/src/psbt.d.ts index e1e080c19..43ce706f9 100644 --- a/src/psbt.d.ts +++ b/src/psbt.d.ts @@ -56,6 +56,17 @@ export declare class Psbt { static fromBase64(data: string, opts?: PsbtOptsOptional): Psbt; static fromHex(data: string, opts?: PsbtOptsOptional): Psbt; static fromBuffer(buffer: Buffer, opts?: PsbtOptsOptional): Psbt; + /** + * This is a helper method for converting a normal Signer into a Taproot Signer. + * Note that this helper method required for the Private Key of the Signer to be present. + * Steps: + * - if the Y coordinate of the Signer Public Key is odd then negate the Private Key + * - tweak the private key with the provided hash (should be empty for key-path spending) + * @param signer a taproot signer object, the Private Key must be present + * @param opts + * @returns a Signer having the Private and Public keys tweaked + */ + static tweakSigner(signer: Signer, opts?: TaprootSignerOpts): Signer; private __CACHE; private opts; constructor(opts?: PsbtOptsOptional, data?: PsbtBase); @@ -155,11 +166,22 @@ export interface HDSignerAsync extends HDSignerBase { } export interface Signer { publicKey: Buffer; + /** + * Private Key is optional, it is required only if the signer must be tweaked. + * See the `tweakSigner()` method. + */ + privateKey?: Buffer; network?: any; sign(hash: Buffer, lowR?: boolean): Buffer; signSchnorr?(hash: Buffer): Buffer; getPublicKey?(): Buffer; } +export interface TaprootSignerOpts { + network?: Network; + eccLib?: any; + /** The hash used to tweak the Signer */ + tweakHash?: Buffer; +} export interface SignerAsync { publicKey: Buffer; network?: any; diff --git a/src/psbt.js b/src/psbt.js index 191bf7ceb..c236c015f 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -2,6 +2,7 @@ Object.defineProperty(exports, '__esModule', { value: true }); exports.Psbt = void 0; const ecc = require('tiny-secp256k1'); // TODO: extract +const ecpair_1 = require('ecpair'); const bip174_1 = require('bip174'); const varuint = require('bip174/src/lib/converter/varint'); const utils_1 = require('bip174/src/lib/utils'); @@ -12,6 +13,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. */ @@ -104,6 +106,39 @@ class Psbt { checkTxForDupeIns(psbt.__CACHE.__TX, psbt.__CACHE); return psbt; } + /** + * This is a helper method for converting a normal Signer into a Taproot Signer. + * Note that this helper method required for the Private Key of the Signer to be present. + * Steps: + * - if the Y coordinate of the Signer Public Key is odd then negate the Private Key + * - tweak the private key with the provided hash (should be empty for key-path spending) + * @param signer a taproot signer object, the Private Key must be present + * @param opts + * @returns a Signer having the Private and Public keys tweaked + */ + static tweakSigner(signer, opts = {}) { + let privateKey = 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, + (0, taprootutils_1.tapTweakHash)( + signer.publicKey.slice(1, 33), + opts.tweakHash, + ), + ); + if (!tweakedPrivateKey) { + throw new Error('Invalid tweaked private key!'); + } + const ECPair = (0, ecpair_1.ECPairFactory)(ecc); + return ECPair.fromPrivateKey(Buffer.from(tweakedPrivateKey), { + network: opts.network, + }); + } get inputCount() { return this.data.inputs.length; } @@ -1033,7 +1068,6 @@ function getHashForSig( values, transaction_1.Transaction.SIGHASH_DEFAULT, ); - console.log('### hash', hash.toString('hex')); } else { // non-segwit if ( diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index a2bbcc24f..4a376540e 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1,4 +1,6 @@ import * as ecc from 'tiny-secp256k1'; // TODO: extract +import { ECPairFactory } from 'ecpair'; + import { Psbt as PsbtBase } from 'bip174'; import * as varuint from 'bip174/src/lib/converter/varint'; import { @@ -21,6 +23,7 @@ import { bitcoin as btcNetwork, Network } from './networks'; import * as payments from './payments'; import * as bscript from './script'; import { Output, Transaction } from './transaction'; +import { tapTweakHash } from './payments/taprootutils'; export interface TransactionInput { hash: string | Buffer; @@ -115,6 +118,39 @@ export class Psbt { return psbt; } + /** + * This is a helper method for converting a normal Signer into a Taproot Signer. + * Note that this helper method required for the Private Key of the Signer to be present. + * Steps: + * - if the Y coordinate of the Signer Public Key is odd then negate the Private Key + * - tweak the private key with the provided hash (should be empty for key-path spending) + * @param signer a taproot signer object, the Private Key must be present + * @param opts + * @returns a Signer having the Private and Public keys tweaked + */ + static tweakSigner(signer: Signer, opts: TaprootSignerOpts = {}): Signer { + 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(signer.publicKey.slice(1, 33), opts.tweakHash), + ); + if (!tweakedPrivateKey) { + throw new Error('Invalid tweaked private key!'); + } + + const ECPair = ECPairFactory(ecc); + return ECPair.fromPrivateKey(Buffer.from(tweakedPrivateKey), { + network: opts.network, + }); + } + private __CACHE: PsbtCache; private opts: PsbtOpts; @@ -836,12 +872,25 @@ export interface HDSignerAsync extends HDSignerBase { export interface Signer { publicKey: Buffer; + /** + * Private Key is optional, it is required only if the signer must be tweaked. + * See the `tweakSigner()` method. + */ + privateKey?: Buffer; network?: any; sign(hash: Buffer, lowR?: boolean): Buffer; signSchnorr?(hash: Buffer): Buffer; getPublicKey?(): Buffer; } +export interface TaprootSignerOpts { + network?: Network; + // TODO: revisit. + eccLib?: any; + /** The hash used to tweak the Signer */ + tweakHash?: Buffer; +} + export interface SignerAsync { publicKey: Buffer; network?: any; @@ -1358,7 +1407,6 @@ function getHashForSig( values, Transaction.SIGHASH_DEFAULT, ); - console.log('### hash', hash.toString('hex')); } else { // non-segwit if ( From be42121ae88f85e0ddbf605b19d2ac0aaeb2dd15 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 27 Jan 2022 13:27:56 +0200 Subject: [PATCH 49/53] test: add tweakSigner test --- test/psbt.spec.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/psbt.spec.ts b/test/psbt.spec.ts index f583e8068..d94cbd0ff 100644 --- a/test/psbt.spec.ts +++ b/test/psbt.spec.ts @@ -969,6 +969,34 @@ describe(`Psbt`, () => { }); }); + describe('tweakSigner', () => { + it('Throws error if signer is missing private key', () => { + const keyPair = Object.assign({}, ECPair.makeRandom(), { + privateKey: null, + }); + assert.throws(() => { + Psbt.tweakSigner(keyPair); + }, new RegExp('Private key is required for tweaking signer!')); + }); + it('Correctly creates tweaked signer', () => { + const keyPair = ECPair.fromPrivateKey( + Buffer.from( + 'accaf12e04e11b08fc28f5fe75b47ea663843b698981e31f1cafa2224d6e28c0', + 'hex', + ), + ); + const tweakedSigner: Signer = Psbt.tweakSigner(keyPair); + assert.strictEqual( + '029421e734b0f9d2c467ea7dd197c61acb4467cdcbc9f4cb0c571f8b63a5c40cae', + tweakedSigner.publicKey.toString('hex'), + ); + assert.strictEqual( + '1853f5034982ec659e015873a0a958a73eac785850f425fd3444b12430d58692', + tweakedSigner.privateKey!.toString('hex'), + ); + }); + }); + describe('create 1-to-1 transaction', () => { const alice = ECPair.fromWIF( 'L2uPYXe17xSTqbCjZvL2DsyXPCbXspvcu5mHLDYUgzdUbZGSKrSr', From db7fb7dd1633299e191832be2a5389687f543ae6 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 27 Jan 2022 13:37:15 +0200 Subject: [PATCH 50/53] chore: code clean-up --- test/fixtures/psbt.json | 2 +- test/integration/taproot.spec.ts | 2 +- test/psbt.spec.ts | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 4d5350662..5c8e93018 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -118,7 +118,7 @@ "psbt": "cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAACg8BAgMEBQYHCAkPAQIDBAUGBwgJCgsMDQ4PAAA=" }, { - "description": "PSBT with one P2TR input and onne P2TR output.", + "description": "PSBT with one P2TR input and one P2TR output.", "psbt": "cHNidP8BAF4CAAAAAWbQAKi9hNXynJhqPu8bqkvp0kHihVShkWGh3yLy15+LAAAAAAD/////Aej9AAAAAAAAIlEgRvZJfLLxnVDD6emCqVDcyGIUsB/M5DekIGHHvbEjDTMAAAAAAAEBK7gFAQAAAAAAIlEglCHnNLD50sRn6n3Rl8Yay0RnzcvJ9MsMVx+LY6XEDK4AAA==" } ], diff --git a/test/integration/taproot.spec.ts b/test/integration/taproot.spec.ts index 358620f76..f7b3733fa 100644 --- a/test/integration/taproot.spec.ts +++ b/test/integration/taproot.spec.ts @@ -25,7 +25,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { unspent.txId, unspent.vout, sendAmount, - [output], // public key + [output], [amount], ); diff --git a/test/psbt.spec.ts b/test/psbt.spec.ts index d94cbd0ff..eb40ad37e 100644 --- a/test/psbt.spec.ts +++ b/test/psbt.spec.ts @@ -978,6 +978,7 @@ describe(`Psbt`, () => { Psbt.tweakSigner(keyPair); }, new RegExp('Private key is required for tweaking signer!')); }); + it('Correctly creates tweaked signer', () => { const keyPair = ECPair.fromPrivateKey( Buffer.from( From 80b5fb7227f961f67cb9887a84731366dd8b5231 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 27 Jan 2022 13:40:22 +0200 Subject: [PATCH 51/53] chore: fix lint --- src/psbt.js | 4 ++-- ts_src/psbt.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index c236c015f..186e21bd9 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -1057,8 +1057,8 @@ function getHashForSig( sighashType, ); } else if (isP2TR(meaningfulScript, ecc)) { - const prevOuts = inputs.map((input, index) => - getScriptAndAmountFromUtxo(index, input, cache), + const prevOuts = inputs.map((i, index) => + getScriptAndAmountFromUtxo(index, i, cache), ); const signingScripts = prevOuts.map(o => o.script); const values = prevOuts.map(o => o.value); diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 4a376540e..9b338d503 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1395,8 +1395,8 @@ function getHashForSig( sighashType, ); } else if (isP2TR(meaningfulScript, ecc)) { - const prevOuts: Output[] = inputs.map((input, index) => - getScriptAndAmountFromUtxo(index, input, cache), + 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); From 04969fd9bb5695a2884b3c4a984fc5338c9d261c Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 27 Jan 2022 14:28:44 +0200 Subject: [PATCH 52/53] feat: add logic for validate taproot input --- src/psbt.js | 9 ++++++++- test/fixtures/psbt.json | 7 +++++++ test/psbt.spec.ts | 36 ++++++++++++++++++++++++++++++++++++ ts_src/psbt.ts | 11 ++++++++++- 4 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 186e21bd9..4dce4643a 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -395,8 +395,15 @@ 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 = + scriptType === 'taproot' + ? { + signature: pSig.signature, + hashType: transaction_1.Transaction.SIGHASH_DEFAULT, + } + : bscript.signature.decode(pSig.signature); const { hash, script } = sighashCache !== sig.hashType ? getHashForSig( diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 5c8e93018..b113d75b6 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -574,6 +574,13 @@ "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 + }, "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/psbt.spec.ts b/test/psbt.spec.ts index eb40ad37e..3f4cf93b8 100644 --- a/test/psbt.spec.ts +++ b/test/psbt.spec.ts @@ -18,6 +18,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\(['"](.*)['"], ['"](.*)['"]\)$/); @@ -952,6 +958,36 @@ describe(`Psbt`, () => { }); }); + describe('validateSignaturesOfTaprootInput', () => { + const f = fixtures.validateSignaturesOfTaprootInput; + it('Correctly validates a signature', () => { + const psbt = Psbt.fromBase64(f.psbt); + assert.strictEqual( + psbt.validateSignaturesOfInput(f.index, schnorrValidator), + true, + ); + }); + + it('Correctly validates a signature against a pubkey', () => { + 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('getFeeRate', () => { it('Throws error if called before inputs are finalized', () => { const f = fixtures.getFeeRate; diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 9b338d503..467d8fc58 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -486,8 +486,17 @@ 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 = + scriptType === 'taproot' + ? { + signature: pSig.signature, + hashType: Transaction.SIGHASH_DEFAULT, + } + : bscript.signature.decode(pSig.signature); + const { hash, script } = sighashCache! !== sig.hashType ? getHashForSig( From a38d65057b68eb7a024cc3acdbecc860a35b736e Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 27 Jan 2022 14:32:12 +0200 Subject: [PATCH 53/53] chore: testing leftovers --- play.mjs | 29 ++++++++----- play4.mjs | 11 +++++ play5.mjs | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++ published tx | 4 ++ r1.txt | 5 +++ 5 files changed, 151 insertions(+), 10 deletions(-) create mode 100644 play4.mjs create mode 100644 play5.mjs create mode 100644 published tx create mode 100644 r1.txt diff --git a/play.mjs b/play.mjs index fd7fa64ed..75840951f 100644 --- a/play.mjs +++ b/play.mjs @@ -4,7 +4,6 @@ import { ECPairFactory } from 'ecpair' import { Psbt } from './src/psbt.js' import { p2pkh, p2wpkh, p2tr } from './src/payments/index.js' import { testnet as network } from './src/networks.js' -import { tapTweakHash } from './src/payments/taprootutils.js' console.log(''.padEnd(100, '#')) const ECPair = ECPairFactory(ecc); @@ -24,16 +23,11 @@ const inP2tr = p2tr({ internalPubkey: inP2trKey.publicKey.slice(1), network }, { const outP2tr = p2tr({ internalPubkey: outP2trKey.publicKey.slice(1), network }, { eccLib: ecc }) const outP2wpkh = p2wpkh({ pubkey: outP2wpkhKey.publicKey, network }) - -const signPrivateKey = inP2trKey.publicKey[0] === 2 - ? inP2trKey.privateKey - : ecc.privateNegate(inP2trKey.privateKey) - -const inTweakedP2trKey = ECPair.fromPrivateKey(Buffer.from(ecc.privateAdd(signPrivateKey, tapTweakHash(inP2trKey.publicKey.slice(1, 33))))) - +const inTweakedP2trKey = Psbt.tweakSigner(inP2trKey, { network }) console.log('### inP2trKey.privateKey ', inP2trKey.privateKey.toString('hex')) console.log('### inP2trKey.publicKey ', inP2trKey.publicKey.toString('hex')) +console.log('### inP2trKey.toWIF() ', inP2trKey.toWIF()) console.log('### inTweakedP2trKey.privateKey ', inTweakedP2trKey.privateKey.toString('hex')) console.log('### inTweakedP2trKey.publicKey ', inTweakedP2trKey.publicKey.toString('hex')) console.log(''.padEnd(100, '#')) @@ -79,9 +73,24 @@ psbt.addOutput({ value: 67000 }) -psbt.signInput(0, inP2pkhKey) +console.log('### psbt 1', psbt.toBase64()) +// psbt.signInput(0, inP2pkhKey) psbt.signInput(1, inTweakedP2trKey) -psbt.signInput(2, inP2wpkhKey) +// psbt.signInput(2, inP2wpkhKey) +console.log('### psbt 2', psbt.toBase64()) + +const validator = ( + pubkey, + msghash, + signature, +) => { + console.log('### verifySchnorr', pubkey.toString('hex')) + // msghash[0]=0 + return ECPair.fromPublicKey(pubkey).verifySchnorr(msghash, signature) +}; + +const isValid = psbt.validateSignaturesOfInput(1, validator) +console.log('### isValid', isValid) // Serialize tx psbt.finalizeAllInputs() diff --git a/play4.mjs b/play4.mjs new file mode 100644 index 000000000..1b61431c6 --- /dev/null +++ b/play4.mjs @@ -0,0 +1,11 @@ +import * as ecc from 'tiny-secp256k1'; +import { ECPairFactory } from 'ecpair' +import NETWORKS from './src/networks.js' + +const ECPair = ECPairFactory(ecc); +const hex = (s) => Buffer.from(s, 'hex') + +const key = ECPair.fromPrivateKey(hex('82fd530c9eb33570c7e05ca5e80b740bcf1118e8f4c73d44a801fa9dd60f6449'), { network: NETWORKS.testnet }) +console.log('### key.toWIF()', key.toWIF()) + +ECPair.fromWIF('cRyKzLXVgTReWe7wgfEiXktTa9tf4e5DK1STha274d7BBbnucTaR', NETWORKS.testnet); \ No newline at end of file diff --git a/play5.mjs b/play5.mjs new file mode 100644 index 000000000..bae7a7d3e --- /dev/null +++ b/play5.mjs @@ -0,0 +1,112 @@ +import * as ecc from 'tiny-secp256k1'; +import { ECPairFactory } from 'ecpair' + +import { Psbt } from './src/psbt.js' +import { p2pkh, p2wpkh, p2tr } from './src/payments/index.js' +import { testnet as network } from './src/networks.js' +console.log(''.padEnd(100, '#')) +const ECPair = ECPairFactory(ecc); +const hex = (s) => Buffer.from(s, 'hex') + +const inP2pkhKey = ECPair.fromPrivateKey(hex('82fd530c9eb33570c7e05ca5e80b740bcf1118e8f4c73d44a801fa9dd60f6449'), { network }) +const inP2wpkhKey = ECPair.fromPrivateKey(hex('35dfb4dc373860005d6f74d37064e328bc772343c354c95e31b654d0c5e22f58'), { network }) +const inP2trKey = ECPair.fromPrivateKey(hex('accaf12e04e11b08fc28f5fe75b47ea663843b698981e31f1cafa2224d6e28c0'), { network }) + +const outP2trKey = ECPair.fromPrivateKey(hex('900afde76badc8914c9940379c74857d70b4d7da590097285572df6b88ad2975'), { network }) +const outP2wpkhKey = ECPair.fromPrivateKey(hex('65ba77c6052f41325d13df8c740b5e33a26d6612e1923bf3afd67ad8081227ee'), { network }) + +console.log('### inP2pkhKey.toWIF()', inP2pkhKey.toWIF()) +console.log('### inP2trKey.toWIF()', inP2trKey.toWIF()) +console.log('### inP2wpkhKey.toWIF()', inP2wpkhKey.toWIF()) +console.log('### outP2trKey.toWIF()', outP2trKey.toWIF()) +// 02982a2876765bb37b53a12418b9e72b8afa8d54e344a1bd585299a211fbe625f3 + + + +const inP2wpkh = p2wpkh({ pubkey: inP2wpkhKey.publicKey, network }) +const inP2tr = p2tr({ internalPubkey: inP2trKey.publicKey.slice(1), network }, { eccLib: ecc }) +const outP2tr = p2tr({ internalPubkey: outP2trKey.publicKey.slice(1), network }, { eccLib: ecc }) +const outP2wpkh = p2wpkh({ pubkey: outP2wpkhKey.publicKey, network }) + + +// const inTweakedP2trKey = inP2trKeyPsbt.prepareTaprootSigner(outP2trKey, { network }) +// console.log('### inTweakedP2trKey.toWIF()', inTweakedP2trKey.toWIF()) + + +console.log('### inP2trKey.privateKey ', inP2trKey.privateKey.toString('hex')) +console.log('### inP2trKey.publicKey ', inP2trKey.publicKey.toString('hex')) +console.log('### outP2trKey.publicKey ', outP2trKey.publicKey.toString('hex')) +console.log('### inP2pkhKey.publicKey ', inP2pkhKey.publicKey.toString('hex')) +console.log('### inP2wpkhKey.publicKey ', inP2wpkhKey.publicKey.toString('hex')) +// console.log('### inTweakedP2trKey.privateKey ', inTweakedP2trKey.privateKey.toString('hex')) +// console.log('### inTweakedP2trKey.publicKey ', inTweakedP2trKey.publicKey.toString('hex')) +console.log(''.padEnd(100, '#')) + + +console.log('### inP2tr.script ', inP2tr.output.toString('hex')) +console.log('### inP2tr.address ', inP2tr.address) +console.log('### inP2tr.pubkey ', inP2tr.pubkey.toString('hex')) +console.log(''.padEnd(100, '#')) + +console.log('### p2pkh.address ', p2pkh({ pubkey: inP2pkhKey.publicKey, network }).address) +console.log('### p2wpkh.address ', inP2wpkh.address) +console.log('### inP2tr.address ', inP2tr.address) +console.log('### outP2tr.address ', outP2tr.address) + +console.log(''.padEnd(100, '#')) + + +const psbt = new Psbt({ network }) +// spend p2pkh +psbt.addInput({ + hash: hex('32833f8502f64f85674d2b637ec3ff0032d5585cd305b8d68db4b83ed977f303').reverse(), + index: 0, +}).updateInput(0, { nonWitnessUtxo: hex('0200000000010147d8d83d6dd1dc8c7841f7f42d7239d2318a0a6a9bb5c936a1d54f72dcf859220000000000feffffff02122c1b00000000001976a9149e7ef1767764ff34a0595dbc4c2b70db017ed06688ac9874aeb10000000017a914b0c19f9f547df19b5fbbbfa25c850c6d1e1b550987024730440220301ddecd390bad5f957545a8eb68bf43681d11abf0a15c3a91fa47ff35178e8402206dfa3f611f2bd3da1604c9ff1f758b0cf4881b9f26ed3d5fa44531c36ba463d001210269997f09a81ec9829043a7f407d14e1fbceb799445e96613c852a8be0c1b5132749c2000') }) + +// spend p2tr +psbt.addInput({ + hash: hex('8b9fd7f222dfa16191a15485e241d2e94baa1bef3e6a989cf2d584bda800d066').reverse(), + index: 0, +}).updateInput(1, { witnessUtxo: { script: outP2tr.output, value: 67000 } }) + +// spend pwpkh +psbt.addInput({ + hash: hex('2258ccf6dcb061db928b8f74c6bb74596b43935eb04f03c5d431020ba5e4877e').reverse(), + index: 0, +}).updateInput(2, { witnessUtxo: { script: inP2wpkh.output, value: 10000 } }) + +psbt.addOutput({ + address: inP2tr.address, + value: 1780000 +}) +psbt.addOutput({ + address: outP2wpkh.address, + value: 67000 +}) + +console.log('### psbt 1', psbt.toBase64()) + +psbt.signInput(0, inP2pkhKey) +psbt.signInput(1, outP2trKey) +psbt.signInput(2, inP2wpkhKey) + +console.log('### psbt 2', psbt.toBase64()) + +// const validator = ( +// pubkey, +// msghash, +// signature, +// ) => ECPair.fromPublicKey(pubkey).verify(msghash, signature); +// psbt.validateSignaturesOfAllInputs(validator) + +// Serialize tx +psbt.finalizeAllInputs() +const tx = psbt.extractTransaction() +const rawTx = tx.toBuffer() + +console.log('### rawTx', rawTx.toString('hex')) +/// 1b9e7e80288a059adb9da6fd7c30e7383a5a924f78d3a0b6b047e07345990f48 +/// b2138ee2639c569fe7f2666635399caaa7a435b3763c079f069e194116840892 +/// e7a79cc65d17bebfc4cf03d26d6d879b754afe9cdc56ce98d2f1fac29cc7f1e2 +/// 8b9fd7f222dfa16191a15485e241d2e94baa1bef3e6a989cf2d584bda800d066 +/// 735321667cf43ded23dc935f355c4b1b1e77d9a47d11c77d384af5094e1ad171 -> 3 inputs, 2 outputs \ No newline at end of file diff --git a/published tx b/published tx new file mode 100644 index 000000000..958922a53 --- /dev/null +++ b/published tx @@ -0,0 +1,4 @@ +published tx +### hash 990576cefd2f30f46709a14cf0eff8fe94c9d004229b9e0ff7fca7a526e9cfea +### rawTx 0200000000010303f377d93eb8b48dd6b805d35c58d53200ffc37e632b4d67854ff602853f8332000000006a473044022069582941c9bb47bde5e4840813884f1049cccc26cf0c1ae1f82ce48d440eacfc02205a56c1df124eb4922cb338f5a99ff0840e632bdc274f3799cc3955b3f8afab670121022e68bc11f5c4ba06c4faad161e3cc141299ccf92b7549b3f34703bb90134a390ffffffff66d000a8bd84d5f29c986a3eef1baa4be9d241e28554a19161a1df22f2d79f8b0000000000ffffffff7e87e4a50b0231d4c5034fb05e93436b5974bbc6748f8b92db61b0dcf6cc58220000000000ffffffff0220291b000000000022512046f6497cb2f19d50c3e9e982a950dcc86214b01fcce437a42061c7bdb1230d33b805010000000000160014c98a3134bf8f33e599a67c40000d774205c1b41400014003694b9b7e9145af36d4faf2fca82cd913f14efbdba7cf295fa488106781d3cb6a8f52d2ef6f21f9f46029773d1289464697c2ba0abbedf5821f23fe45c5b38b02483045022100a02224b6084be798ae3009a4cf0609cdbfa1faa98d7b0657c40c7fd3a39bc484022042c2b01cffc1ddb03e59c96f6ece96ec4c9133e70b21c0d82f92c47897fc5bcf012103a54ea4405b3c933fc58b2a3b67aeb6d605a6079cf6f731820c93ca89252baeb400000000 + diff --git a/r1.txt b/r1.txt new file mode 100644 index 000000000..36beb16c4 --- /dev/null +++ b/r1.txt @@ -0,0 +1,5 @@ +### rawTx 0200000000010303f377d93eb8b48dd6b805d35c58d53200ffc37e632b4d67854ff602853f8332000000006a473044022069582941c9bb47bde5e4840813884f1049cccc26cf0c1ae1f82ce48d440eacfc02205a56c1df124eb4922cb338f5a99ff0840e632bdc274f3799cc3955b3f8afab670121022e68bc11f5c4ba06c4faad161e3cc141299ccf92b7549b3f34703bb90134a390ffffffff66d000a8bd84d5f29c986a3eef1baa4be9d241e28554a19161a1df22f2d79f8b0000000000ffffffff7e87e4a50b0231d4c5034fb05e93436b5974bbc6748f8b92db61b0dcf6cc58220000000000ffffffff0220291b000000000022512046f6497cb2f19d50c3e9e982a950dcc86214b01fcce437a42061c7bdb1230d33b805010000000000160014c98a3134bf8f33e599a67c40000d774205c1b41400014003694b9b7e9145af36d4faf2fca82cd913f14efbdba7cf295fa488106781d3cb6a8f52d2ef6f21f9f46029773d1289464697c2ba0abbedf5821f23fe45c5b38b02483045022100a02224b6084be798ae3009a4cf0609cdbfa1faa98d7b0657c40c7fd3a39bc484022042c2b01cffc1ddb03e59c96f6ece96ec4c9133e70b21c0d82f92c47897fc5bcf012103a54ea4405b3c933fc58b2a3b67aeb6d605a6079cf6f731820c93ca89252baeb400000000 + +### rawTx 0200000000010303f377d93eb8b48dd6b805d35c58d53200ffc37e632b4d67854ff602853f8332000000006a473044022069582941c9bb47bde5e4840813884f1049cccc26cf0c1ae1f82ce48d440eacfc02205a56c1df124eb4922cb338f5a99ff0840e632bdc274f3799cc3955b3f8afab670121022e68bc11f5c4ba06c4faad161e3cc141299ccf92b7549b3f34703bb90134a390ffffffff66d000a8bd84d5f29c986a3eef1baa4be9d241e28554a19161a1df22f2d79f8b0000000000ffffffff7e87e4a50b0231d4c5034fb05e93436b5974bbc6748f8b92db61b0dcf6cc58220000000000ffffffff0220291b000000000022512046f6497cb2f19d50c3e9e982a950dcc86214b01fcce437a42061c7bdb1230d33b805010000000000160014c98a3134bf8f33e599a67c40000d774205c1b41400014003694b9b7e9145af36d4faf2fca82cd913f14efbdba7cf295fa488106781d3cb6a8f52d2ef6f21f9f46029773d1289464697c2ba0abbedf5821f23fe45c5b38b02483045022100a02224b6084be798ae3009a4cf0609cdbfa1faa98d7b0657c40c7fd3a39bc484022042c2b01cffc1ddb03e59c96f6ece96ec4c9133e70b21c0d82f92c47897fc5bcf012103a54ea4405b3c933fc58b2a3b67aeb6d605a6079cf6f731820c93ca89252baeb400000000 + +mempool: 0200000000010303f377d93eb8b48dd6b805d35c58d53200ffc37e632b4d67854ff602853f8332000000006a473044022069582941c9bb47bde5e4840813884f1049cccc26cf0c1ae1f82ce48d440eacfc02205a56c1df124eb4922cb338f5a99ff0840e632bdc274f3799cc3955b3f8afab670121022e68bc11f5c4ba06c4faad161e3cc141299ccf92b7549b3f34703bb90134a390ffffffff66d000a8bd84d5f29c986a3eef1baa4be9d241e28554a19161a1df22f2d79f8b0000000000ffffffff7e87e4a50b0231d4c5034fb05e93436b5974bbc6748f8b92db61b0dcf6cc58220000000000ffffffff0220291b000000000022512046f6497cb2f19d50c3e9e982a950dcc86214b01fcce437a42061c7bdb1230d33b805010000000000160014c98a3134bf8f33e599a67c40000d774205c1b41400014003694b9b7e9145af36d4faf2fca82cd913f14efbdba7cf295fa488106781d3cb6a8f52d2ef6f21f9f46029773d1289464697c2ba0abbedf5821f23fe45c5b38b02483045022100a02224b6084be798ae3009a4cf0609cdbfa1faa98d7b0657c40c7fd3a39bc484022042c2b01cffc1ddb03e59c96f6ece96ec4c9133e70b21c0d82f92c47897fc5bcf012103a54ea4405b3c933fc58b2a3b67aeb6d605a6079cf6f731820c93ca89252baeb400000000 \ No newline at end of file