UNPKG

bitcoinjs-lib

Version:

Client-side Bitcoin JavaScript library

298 lines (297 loc) 10.9 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); exports.p2tr = void 0; const buffer_1 = require('buffer'); const networks_1 = require('../networks'); const bscript = require('../script'); const types_1 = require('../types'); const ecc_lib_1 = require('../ecc_lib'); const bip341_1 = require('./bip341'); const lazy = require('./lazy'); const bech32_1 = require('bech32'); const OPS = bscript.OPS; const TAPROOT_WITNESS_VERSION = 0x01; const ANNEX_PREFIX = 0x50; function p2tr(a, opts) { if ( !a.address && !a.output && !a.pubkey && !a.internalPubkey && !(a.witness && a.witness.length > 1) ) throw new TypeError('Not enough data'); opts = Object.assign({ validate: true }, opts || {}); (0, types_1.typeforce)( { address: types_1.typeforce.maybe(types_1.typeforce.String), input: types_1.typeforce.maybe(types_1.typeforce.BufferN(0)), network: types_1.typeforce.maybe(types_1.typeforce.Object), output: types_1.typeforce.maybe(types_1.typeforce.BufferN(34)), internalPubkey: types_1.typeforce.maybe(types_1.typeforce.BufferN(32)), hash: types_1.typeforce.maybe(types_1.typeforce.BufferN(32)), pubkey: types_1.typeforce.maybe(types_1.typeforce.BufferN(32)), signature: types_1.typeforce.maybe( types_1.typeforce.anyOf( types_1.typeforce.BufferN(64), types_1.typeforce.BufferN(65), ), ), witness: types_1.typeforce.maybe( types_1.typeforce.arrayOf(types_1.typeforce.Buffer), ), scriptTree: types_1.typeforce.maybe(types_1.isTaptree), redeem: types_1.typeforce.maybe({ output: types_1.typeforce.maybe(types_1.typeforce.Buffer), redeemVersion: types_1.typeforce.maybe(types_1.typeforce.Number), witness: types_1.typeforce.maybe( types_1.typeforce.arrayOf(types_1.typeforce.Buffer), ), }), redeemVersion: types_1.typeforce.maybe(types_1.typeforce.Number), }, a, ); const _address = lazy.value(() => { const result = bech32_1.bech32m.decode(a.address); const version = result.words.shift(); const data = bech32_1.bech32m.fromWords(result.words); return { version, prefix: result.prefix, data: buffer_1.Buffer.from(data), }; }); // remove annex if present, ignored by taproot const _witness = lazy.value(() => { if (!a.witness || !a.witness.length) return; if ( a.witness.length >= 2 && a.witness[a.witness.length - 1][0] === ANNEX_PREFIX ) { return a.witness.slice(0, -1); } return a.witness.slice(); }); const _hashTree = lazy.value(() => { if (a.scriptTree) return (0, bip341_1.toHashTree)(a.scriptTree); if (a.hash) return { hash: a.hash }; return; }); const network = a.network || networks_1.bitcoin; const o = { name: 'p2tr', network }; lazy.prop(o, 'address', () => { if (!o.pubkey) return; const words = bech32_1.bech32m.toWords(o.pubkey); words.unshift(TAPROOT_WITNESS_VERSION); return bech32_1.bech32m.encode(network.bech32, words); }); lazy.prop(o, 'hash', () => { const hashTree = _hashTree(); if (hashTree) return hashTree.hash; const w = _witness(); if (w && w.length > 1) { const controlBlock = w[w.length - 1]; const leafVersion = controlBlock[0] & types_1.TAPLEAF_VERSION_MASK; const script = w[w.length - 2]; const leafHash = (0, bip341_1.tapleafHash)({ output: script, version: leafVersion, }); return (0, bip341_1.rootHashFromPath)(controlBlock, leafHash); } return null; }); lazy.prop(o, 'output', () => { if (!o.pubkey) return; return bscript.compile([OPS.OP_1, o.pubkey]); }); lazy.prop(o, 'redeemVersion', () => { if (a.redeemVersion) return a.redeemVersion; if ( a.redeem && a.redeem.redeemVersion !== undefined && a.redeem.redeemVersion !== null ) { return a.redeem.redeemVersion; } return bip341_1.LEAF_VERSION_TAPSCRIPT; }); lazy.prop(o, 'redeem', () => { const witness = _witness(); // witness without annex if (!witness || witness.length < 2) return; return { output: witness[witness.length - 2], witness: witness.slice(0, -2), redeemVersion: witness[witness.length - 1][0] & types_1.TAPLEAF_VERSION_MASK, }; }); lazy.prop(o, 'pubkey', () => { if (a.pubkey) return a.pubkey; if (a.output) return a.output.slice(2); if (a.address) return _address().data; if (o.internalPubkey) { const tweakedKey = (0, bip341_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.signature) return a.signature; const witness = _witness(); // witness without annex if (!witness || witness.length !== 1) return; return witness[0]; }); lazy.prop(o, 'witness', () => { if (a.witness) return a.witness; const hashTree = _hashTree(); if (hashTree && a.redeem && a.redeem.output && a.internalPubkey) { const leafHash = (0, bip341_1.tapleafHash)({ output: a.redeem.output, version: o.redeemVersion, }); const path = (0, bip341_1.findScriptPath)(hashTree, leafHash); if (!path) return; const outputKey = (0, bip341_1.tweakKey)(a.internalPubkey, hashTree.hash); if (!outputKey) return; const controlBock = buffer_1.Buffer.concat( [ buffer_1.Buffer.from([o.redeemVersion | outputKey.parity]), a.internalPubkey, ].concat(path), ); return [a.redeem.output, controlBock]; } if (a.signature) return [a.signature]; }); // extended validation if (opts.validate) { let pubkey = buffer_1.Buffer.from([]); if (a.address) { if (network && network.bech32 !== _address().prefix) throw new TypeError('Invalid prefix or Network mismatch'); if (_address().version !== TAPROOT_WITNESS_VERSION) throw new TypeError('Invalid address version'); if (_address().data.length !== 32) throw new TypeError('Invalid address data'); pubkey = _address().data; } if (a.pubkey) { if (pubkey.length > 0 && !pubkey.equals(a.pubkey)) throw new TypeError('Pubkey mismatch'); else pubkey = a.pubkey; } if (a.output) { if ( a.output.length !== 34 || a.output[0] !== OPS.OP_1 || a.output[1] !== 0x20 ) throw new TypeError('Output is invalid'); if (pubkey.length > 0 && !pubkey.equals(a.output.slice(2))) throw new TypeError('Pubkey mismatch'); else pubkey = a.output.slice(2); } if (a.internalPubkey) { const tweakedKey = (0, bip341_1.tweakKey)(a.internalPubkey, o.hash); if (pubkey.length > 0 && !pubkey.equals(tweakedKey.x)) throw new TypeError('Pubkey mismatch'); else pubkey = tweakedKey.x; } if (pubkey && pubkey.length) { if (!(0, ecc_lib_1.getEccLib)().isXOnlyPoint(pubkey)) throw new TypeError('Invalid pubkey for p2tr'); } const hashTree = _hashTree(); if (a.hash && hashTree) { if (!a.hash.equals(hashTree.hash)) throw new TypeError('Hash mismatch'); } if (a.redeem && a.redeem.output && hashTree) { const leafHash = (0, bip341_1.tapleafHash)({ output: a.redeem.output, version: o.redeemVersion, }); if (!(0, bip341_1.findScriptPath)(hashTree, leafHash)) throw new TypeError('Redeem script not in tree'); } const witness = _witness(); // compare the provided redeem data with the one computed from witness if (a.redeem && o.redeem) { if (a.redeem.redeemVersion) { if (a.redeem.redeemVersion !== o.redeem.redeemVersion) throw new TypeError('Redeem.redeemVersion and witness mismatch'); } if (a.redeem.output) { if (bscript.decompile(a.redeem.output).length === 0) throw new TypeError('Redeem.output is invalid'); // output redeem is constructed from the witness if (o.redeem.output && !a.redeem.output.equals(o.redeem.output)) throw new TypeError('Redeem.output and witness mismatch'); } if (a.redeem.witness) { if ( o.redeem.witness && !stacksEqual(a.redeem.witness, o.redeem.witness) ) throw new TypeError('Redeem.witness and witness mismatch'); } } if (witness && witness.length) { if (witness.length === 1) { // key spending if (a.signature && !a.signature.equals(witness[0])) throw new TypeError('Signature mismatch'); } else { // script path spending const controlBlock = witness[witness.length - 1]; if (controlBlock.length < 33) throw new TypeError( `The control-block length is too small. Got ${controlBlock.length}, expected min 33.`, ); if ((controlBlock.length - 33) % 32 !== 0) throw new TypeError( `The control-block length of ${controlBlock.length} is incorrect!`, ); const m = (controlBlock.length - 33) / 32; if (m > 128) throw new TypeError( `The script path is too long. Got ${m}, expected max 128.`, ); const internalPubkey = controlBlock.slice(1, 33); if (a.internalPubkey && !a.internalPubkey.equals(internalPubkey)) throw new TypeError('Internal pubkey mismatch'); if (!(0, ecc_lib_1.getEccLib)().isXOnlyPoint(internalPubkey)) throw new TypeError('Invalid internalPubkey for p2tr witness'); const leafVersion = controlBlock[0] & types_1.TAPLEAF_VERSION_MASK; const script = witness[witness.length - 2]; const leafHash = (0, bip341_1.tapleafHash)({ output: script, version: leafVersion, }); const hash = (0, bip341_1.rootHashFromPath)(controlBlock, leafHash); const outputKey = (0, bip341_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 Object.assign(o, a); } exports.p2tr = p2tr; function stacksEqual(a, b) { if (a.length !== b.length) return false; return a.every((x, i) => { return x.equals(b[i]); }); }