UNPKG

bitcoinjs-lib

Version:

Client-side Bitcoin JavaScript library

302 lines (301 loc) 10.4 kB
import { bitcoin as BITCOIN_NETWORK } from '../networks.js'; import * as bscript from '../script.js'; import { isTaptree, TAPLEAF_VERSION_MASK, stacksEqual, NBufferSchemaFactory, BufferSchema, } from '../types.js'; import { getEccLib } from '../ecc_lib.js'; import { toHashTree, rootHashFromPath, findScriptPath, tapleafHash, tweakKey, LEAF_VERSION_TAPSCRIPT, } from './bip341.js'; import * as lazy from './lazy.js'; import { bech32m } from 'bech32'; import { fromBech32 } from '../address.js'; import * as tools from 'uint8array-tools'; import * as v from 'valibot'; const OPS = bscript.OPS; const TAPROOT_WITNESS_VERSION = 0x01; const ANNEX_PREFIX = 0x50; /** * Creates a Pay-to-Taproot (P2TR) payment object. * * @param a - The payment object containing the necessary data for P2TR. * @param opts - Optional payment options. * @returns The P2TR payment object. * @throws {TypeError} If the provided data is invalid or insufficient. */ export 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 || {}); v.parse( v.partial( v.object({ address: v.string(), input: NBufferSchemaFactory(0), network: v.object({}), output: NBufferSchemaFactory(34), internalPubkey: NBufferSchemaFactory(32), hash: NBufferSchemaFactory(32), // merkle root hash, the tweak pubkey: NBufferSchemaFactory(32), // tweaked with `hash` from `internalPubkey` signature: v.union([ NBufferSchemaFactory(64), NBufferSchemaFactory(65), ]), witness: v.array(BufferSchema), scriptTree: v.custom(isTaptree, 'Taptree is not of type isTaptree'), redeem: v.partial( v.object({ output: BufferSchema, // tapleaf script redeemVersion: v.number(), // tapleaf version witness: v.array(BufferSchema), }), ), redeemVersion: v.number(), }), ), a, ); const _address = lazy.value(() => { return fromBech32(a.address); }); // remove annex if present, ignored by taproot const _witness = lazy.value(() => { if (!a.witness || !a.witness.length) return; if ( a.witness.length >= 2 && a.witness[a.witness.length - 1][0] === ANNEX_PREFIX ) { return a.witness.slice(0, -1); } return a.witness.slice(); }); const _hashTree = lazy.value(() => { if (a.scriptTree) return toHashTree(a.scriptTree); if (a.hash) return { hash: a.hash }; return; }); const network = a.network || BITCOIN_NETWORK; const o = { name: 'p2tr', network }; lazy.prop(o, 'address', () => { if (!o.pubkey) return; const words = bech32m.toWords(o.pubkey); words.unshift(TAPROOT_WITNESS_VERSION); return bech32m.encode(network.bech32, words); }); lazy.prop(o, 'hash', () => { const hashTree = _hashTree(); if (hashTree) return hashTree.hash; const w = _witness(); if (w && w.length > 1) { const controlBlock = w[w.length - 1]; const leafVersion = controlBlock[0] & TAPLEAF_VERSION_MASK; const script = w[w.length - 2]; const leafHash = tapleafHash({ output: script, version: leafVersion }); return rootHashFromPath(controlBlock, leafHash); } return null; }); lazy.prop(o, 'output', () => { if (!o.pubkey) return; return bscript.compile([OPS.OP_1, o.pubkey]); }); lazy.prop(o, 'redeemVersion', () => { if (a.redeemVersion) return a.redeemVersion; if ( a.redeem && a.redeem.redeemVersion !== undefined && a.redeem.redeemVersion !== null ) { return a.redeem.redeemVersion; } return LEAF_VERSION_TAPSCRIPT; }); lazy.prop(o, 'redeem', () => { const witness = _witness(); // witness without annex if (!witness || witness.length < 2) return; return { output: witness[witness.length - 2], witness: witness.slice(0, -2), redeemVersion: witness[witness.length - 1][0] & TAPLEAF_VERSION_MASK, }; }); lazy.prop(o, 'pubkey', () => { if (a.pubkey) return a.pubkey; if (a.output) return a.output.slice(2); if (a.address) return _address().data; if (o.internalPubkey) { const tweakedKey = tweakKey(o.internalPubkey, o.hash); if (tweakedKey) return tweakedKey.x; } }); lazy.prop(o, 'internalPubkey', () => { if (a.internalPubkey) return a.internalPubkey; const witness = _witness(); if (witness && witness.length > 1) return witness[witness.length - 1].slice(1, 33); }); lazy.prop(o, 'signature', () => { if (a.signature) return a.signature; 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 = tapleafHash({ output: a.redeem.output, version: o.redeemVersion, }); const path = findScriptPath(hashTree, leafHash); if (!path) return; const outputKey = tweakKey(a.internalPubkey, hashTree.hash); if (!outputKey) return; const controlBock = tools.concat( [ Uint8Array.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 = Uint8Array.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 && tools.compare(pubkey, a.pubkey) !== 0) 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 && tools.compare(pubkey, a.output.slice(2)) !== 0) 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 && tools.compare(pubkey, tweakedKey.x) !== 0) throw new TypeError('Pubkey mismatch'); else pubkey = tweakedKey.x; } if (pubkey && pubkey.length) { if (!getEccLib().isXOnlyPoint(pubkey)) throw new TypeError('Invalid pubkey for p2tr'); } const hashTree = _hashTree(); if (a.hash && hashTree) { if (tools.compare(a.hash, hashTree.hash) !== 0) throw new TypeError('Hash mismatch'); } if (a.redeem && a.redeem.output && hashTree) { const leafHash = tapleafHash({ output: a.redeem.output, version: o.redeemVersion, }); if (!findScriptPath(hashTree, leafHash)) throw new TypeError('Redeem script not in tree'); } const witness = _witness(); // 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 && tools.compare(a.redeem.output, o.redeem.output) !== 0 ) 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 && tools.compare(a.signature, witness[0]) !== 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 && tools.compare(a.internalPubkey, internalPubkey) !== 0 ) throw new TypeError('Internal pubkey mismatch'); if (!getEccLib().isXOnlyPoint(internalPubkey)) throw new TypeError('Invalid internalPubkey for p2tr witness'); const leafVersion = controlBlock[0] & TAPLEAF_VERSION_MASK; const script = witness[witness.length - 2]; const leafHash = tapleafHash({ output: script, version: 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 && tools.compare(pubkey, outputKey.x) !== 0) throw new TypeError('Pubkey mismatch for p2tr witness'); if (outputKey.parity !== (controlBlock[0] & 1)) throw new Error('Incorrect parity'); } } } return Object.assign(o, a); }