diff --git a/src/cjs/psbt.cjs b/src/cjs/psbt.cjs index c6ccbf07d..c272c1da2 100644 --- a/src/cjs/psbt.cjs +++ b/src/cjs/psbt.cjs @@ -46,7 +46,6 @@ var __importStar = Object.defineProperty(exports, '__esModule', { value: true }); exports.Psbt = exports.toXOnly = void 0; const bip174_1 = require('bip174'); -const varuint = __importStar(require('varuint-bitcoin')); const bip174_2 = require('bip174'); const address_js_1 = require('./address.cjs'); const bufferutils_js_1 = require('./bufferutils.cjs'); @@ -63,6 +62,9 @@ Object.defineProperty(exports, 'toXOnly', { }, }); const psbtutils_js_1 = require('./psbt/psbtutils.cjs'); +const witness_js_1 = require('./psbt/internal/witness.cjs'); +const finalize_js_1 = require('./psbt/internal/finalize.cjs'); +const scriptType_js_1 = require('./psbt/internal/scriptType.cjs'); const tools = __importStar(require('uint8array-tools')); /** * These are the default arguments for a Psbt instance. @@ -261,7 +263,8 @@ class Psbt { } (0, bip371_js_1.checkTaprootInputFields)(inputData, inputData, 'addInput'); checkInputsForPartialSig(this.data.inputs, 'addInput'); - if (inputData.witnessScript) checkInvalidP2WSH(inputData.witnessScript); + if (inputData.witnessScript) + checkInvalidP2WSHScript(inputData.witnessScript); const c = this.__CACHE; this.data.addInput(inputData); const txIn = c.__TX.ins[c.__TX.ins.length - 1]; @@ -364,7 +367,11 @@ class Psbt { ); throw new Error(`Cannot finalize input #${inputIndex}. Not Taproot.`); } - _finalizeInput(inputIndex, input, finalScriptsFunc = getFinalScripts) { + _finalizeInput( + inputIndex, + input, + finalScriptsFunc = finalize_js_1.getFinalScripts, + ) { const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( inputIndex, input, @@ -421,16 +428,20 @@ class Psbt { getInputType(inputIndex) { const input = (0, bip174_2.checkForInput)(this.data.inputs, inputIndex); const script = getScriptFromUtxo(inputIndex, input, this.__CACHE); - const result = getMeaningfulScript( + const result = (0, scriptType_js_1.getMeaningfulScript)( script, inputIndex, 'input', input.redeemScript || redeemFromFinalScriptSig(input.finalScriptSig), input.witnessScript || redeemFromFinalWitnessScript(input.finalScriptWitness), + SCRIPT_TYPE_DEPS, ); const type = result.type === 'raw' ? '' : result.type + '-'; - const mainType = classifyScript(result.meaningfulScript); + const mainType = (0, scriptType_js_1.classifyScript)( + result.meaningfulScript, + SCRIPT_TYPE_DEPS, + ); return type + mainType; } inputHasPubkey(inputIndex, pubkey) { @@ -932,7 +943,8 @@ class Psbt { return this; } updateInput(inputIndex, updateData) { - if (updateData.witnessScript) checkInvalidP2WSH(updateData.witnessScript); + if (updateData.witnessScript) + checkInvalidP2WSHScript(updateData.witnessScript); (0, bip371_js_1.checkTaprootInputFields)( this.data.inputs[inputIndex], updateData, @@ -1032,42 +1044,11 @@ class PsbtTransaction { return this.tx.toBuffer(); } } -function canFinalize(input, script, scriptType) { - switch (scriptType) { - case 'pubkey': - case 'pubkeyhash': - case 'witnesspubkeyhash': - return hasSigs(1, input.partialSig); - case 'multisig': - const p2ms = payments.p2ms({ output: script }); - return hasSigs(p2ms.m, input.partialSig, p2ms.pubkeys); - default: - return false; - } -} function checkCache(cache) { if (cache.__UNSAFE_SIGN_NONSEGWIT !== false) { throw new Error('Not BIP174 compliant, can not export'); } } -function hasSigs(neededSigs, partialSig, pubkeys) { - if (!partialSig) return false; - let sigs; - if (pubkeys) { - sigs = pubkeys - .map(pkey => { - const pubkey = compressPubkey(pkey); - return partialSig.find( - pSig => tools.compare(pSig.pubkey, pubkey) === 0, - ); - }) - .filter(v => !!v); - } else { - sigs = partialSig; - } - if (sigs.length > neededSigs) throw new Error('Too many signatures'); - return sigs.length === neededSigs; -} function isFinalized(input) { return !!input.finalScriptSig || !!input.finalScriptWitness; } @@ -1173,6 +1154,22 @@ const checkWitnessScript = scriptCheckerFactory( payments.p2wsh, 'Witness script', ); +const SCRIPT_TYPE_DEPS = { + isP2WPKH: psbtutils_js_1.isP2WPKH, + isP2PKH: psbtutils_js_1.isP2PKH, + isP2MS: psbtutils_js_1.isP2MS, + isP2PK: psbtutils_js_1.isP2PK, + isP2SHScript: psbtutils_js_1.isP2SHScript, + isP2WSHScript: psbtutils_js_1.isP2WSHScript, + checkRedeemScript, + checkWitnessScript, +}; +const checkInvalidP2WSHScript = script => + (0, scriptType_js_1.checkInvalidP2WSH)( + script, + psbtutils_js_1.isP2WPKH, + psbtutils_js_1.isP2SHScript, + ); function getTxCacheValue(key, name, inputs, c) { if (!inputs.every(isFinalized)) throw new Error(`PSBT must be finalized to calculate ${name}`); @@ -1190,58 +1187,6 @@ function getTxCacheValue(key, name, inputs, c) { if (key === '__FEE_RATE') return c.__FEE_RATE; else if (key === '__FEE') return c.__FEE; } -function getFinalScripts(inputIndex, input, script, isSegwit, isP2SH, isP2WSH) { - const scriptType = classifyScript(script); - if (!canFinalize(input, script, scriptType)) - throw new Error(`Can not finalize input #${inputIndex}`); - return prepareFinalScripts( - script, - scriptType, - input.partialSig, - isSegwit, - isP2SH, - isP2WSH, - ); -} -function prepareFinalScripts( - script, - scriptType, - partialSig, - isSegwit, - isP2SH, - isP2WSH, -) { - let finalScriptSig; - let finalScriptWitness; - // Wow, the payments API is very handy - const payment = getPayment(script, scriptType, partialSig); - const p2wsh = !isP2WSH ? null : payments.p2wsh({ redeem: payment }); - const p2sh = !isP2SH ? null : payments.p2sh({ redeem: p2wsh || payment }); - if (isSegwit) { - if (p2wsh) { - finalScriptWitness = (0, psbtutils_js_1.witnessStackToScriptWitness)( - p2wsh.witness, - ); - } else { - finalScriptWitness = (0, psbtutils_js_1.witnessStackToScriptWitness)( - payment.witness, - ); - } - if (p2sh) { - finalScriptSig = p2sh.input; - } - } else { - if (p2sh) { - finalScriptSig = p2sh.input; - } else { - finalScriptSig = payment.input; - } - } - return { - finalScriptSig, - finalScriptWitness, - }; -} function getHashAndSighashType( inputs, inputIndex, @@ -1291,12 +1236,13 @@ function getHashForSig(inputIndex, input, cache, forValidate, sighashTypes) { } else { throw new Error('Need a Utxo input item for signing'); } - const { meaningfulScript, type } = getMeaningfulScript( + const { meaningfulScript, type } = (0, scriptType_js_1.getMeaningfulScript)( prevout.script, inputIndex, 'input', input.redeemScript, input.witnessScript, + SCRIPT_TYPE_DEPS, ); if (['p2sh-p2wsh', 'p2wsh'].indexOf(type) >= 0) { hash = unsignedTx.hashForWitnessV0( @@ -1445,39 +1391,6 @@ function checkSighashTypeAllowed(sighashType, sighashTypes) { ); } } -function getPayment(script, scriptType, partialSig) { - let payment; - switch (scriptType) { - case 'multisig': - const sigs = getSortedSigs(script, partialSig); - payment = payments.p2ms({ - output: script, - signatures: sigs, - }); - break; - case 'pubkey': - payment = payments.p2pk({ - output: script, - signature: partialSig[0].signature, - }); - break; - case 'pubkeyhash': - payment = payments.p2pkh({ - output: script, - pubkey: partialSig[0].pubkey, - signature: partialSig[0].signature, - }); - break; - case 'witnesspubkeyhash': - payment = payments.p2wpkh({ - output: script, - pubkey: partialSig[0].pubkey, - signature: partialSig[0].signature, - }); - break; - } - return payment; -} function getScriptFromInput(inputIndex, input, cache) { const unsignedTx = cache.__TX; const res = { @@ -1538,44 +1451,6 @@ function getSignersFromHD(inputIndex, inputs, hdKeyPair) { }); return signers; } -function getSortedSigs(script, partialSig) { - const p2ms = payments.p2ms({ output: script }); - // for each pubkey in order of p2ms script - return p2ms.pubkeys - .map(pk => { - // filter partialSig array by pubkey being equal - return ( - partialSig.filter(ps => { - return tools.compare(ps.pubkey, pk) === 0; - })[0] || {} - ).signature; - // Any pubkey without a match will return undefined - // this last filter removes all the undefined items in the array. - }) - .filter(v => !!v); -} -function scriptWitnessToWitnessStack(buffer) { - let offset = 0; - function readSlice(n) { - offset += n; - return buffer.slice(offset - n, offset); - } - function readVarInt() { - const vi = varuint.decode(buffer, offset); - offset += varuint.encodingLength(vi.bigintValue); - return vi.numberValue; - } - function readVarSlice() { - return readSlice(readVarInt()); - } - function readVector() { - const count = readVarInt(); - const vector = []; - for (let i = 0; i < count; i++) vector.push(readVarSlice()); - return vector; - } - return readVector(); -} function sighashTypeToString(sighashType) { let text = sighashType & transaction_js_1.Transaction.SIGHASH_ANYONECANPAY @@ -1626,7 +1501,7 @@ function inputFinalizeGetAmts(inputs, tx, cache, mustFinalize) { if (mustFinalize && input.finalScriptSig) tx.ins[idx].script = input.finalScriptSig; if (mustFinalize && input.finalScriptWitness) { - tx.ins[idx].witness = scriptWitnessToWitnessStack( + tx.ins[idx].witness = (0, witness_js_1.scriptWitnessToWitnessStack)( input.finalScriptWitness, ); } @@ -1680,23 +1555,25 @@ function getScriptAndAmountFromUtxo(inputIndex, input, cache) { } function pubkeyInInput(pubkey, input, inputIndex, cache) { const script = getScriptFromUtxo(inputIndex, input, cache); - const { meaningfulScript } = getMeaningfulScript( + const { meaningfulScript } = (0, scriptType_js_1.getMeaningfulScript)( script, inputIndex, 'input', input.redeemScript, input.witnessScript, + SCRIPT_TYPE_DEPS, ); return (0, psbtutils_js_1.pubkeyInScript)(pubkey, meaningfulScript); } function pubkeyInOutput(pubkey, output, outputIndex, cache) { const script = cache.__TX.outs[outputIndex].script; - const { meaningfulScript } = getMeaningfulScript( + const { meaningfulScript } = (0, scriptType_js_1.getMeaningfulScript)( script, outputIndex, 'output', output.redeemScript, output.witnessScript, + SCRIPT_TYPE_DEPS, ); return (0, psbtutils_js_1.pubkeyInScript)(pubkey, meaningfulScript); } @@ -1717,87 +1594,19 @@ function redeemFromFinalScriptSig(finalScript) { } function redeemFromFinalWitnessScript(finalScript) { if (!finalScript) return; - const decomp = scriptWitnessToWitnessStack(finalScript); + const decomp = (0, witness_js_1.scriptWitnessToWitnessStack)(finalScript); const lastItem = decomp[decomp.length - 1]; if (isPubkeyLike(lastItem)) return; const sDecomp = bscript.decompile(lastItem); if (!sDecomp) return; return lastItem; } -function compressPubkey(pubkey) { - if (pubkey.length === 65) { - const parity = pubkey[64] & 1; - const newKey = pubkey.slice(0, 33); - newKey[0] = 2 | parity; - return newKey; - } - return pubkey.slice(); -} function isPubkeyLike(buf) { return buf.length === 33 && bscript.isCanonicalPubKey(buf); } function isSigLike(buf) { return bscript.isCanonicalScriptSignature(buf); } -function getMeaningfulScript( - script, - index, - ioType, - redeemScript, - witnessScript, -) { - const isP2SH = (0, psbtutils_js_1.isP2SHScript)(script); - const isP2SHP2WSH = - isP2SH && redeemScript && (0, psbtutils_js_1.isP2WSHScript)(redeemScript); - const isP2WSH = (0, psbtutils_js_1.isP2WSHScript)(script); - if (isP2SH && redeemScript === undefined) - throw new Error('scriptPubkey is P2SH but redeemScript missing'); - if ((isP2WSH || isP2SHP2WSH) && witnessScript === undefined) - throw new Error( - 'scriptPubkey or redeemScript is P2WSH but witnessScript missing', - ); - let meaningfulScript; - if (isP2SHP2WSH) { - meaningfulScript = witnessScript; - checkRedeemScript(index, script, redeemScript, ioType); - checkWitnessScript(index, redeemScript, witnessScript, ioType); - checkInvalidP2WSH(meaningfulScript); - } else if (isP2WSH) { - meaningfulScript = witnessScript; - checkWitnessScript(index, script, witnessScript, ioType); - checkInvalidP2WSH(meaningfulScript); - } else if (isP2SH) { - meaningfulScript = redeemScript; - checkRedeemScript(index, script, redeemScript, ioType); - } else { - meaningfulScript = script; - } - return { - meaningfulScript, - type: isP2SHP2WSH - ? 'p2sh-p2wsh' - : isP2SH - ? 'p2sh' - : isP2WSH - ? 'p2wsh' - : 'raw', - }; -} -function checkInvalidP2WSH(script) { - if ( - (0, psbtutils_js_1.isP2WPKH)(script) || - (0, psbtutils_js_1.isP2SHScript)(script) - ) { - throw new Error('P2WPKH or P2SH can not be contained within P2WSH'); - } -} -function classifyScript(script) { - if ((0, psbtutils_js_1.isP2WPKH)(script)) return 'witnesspubkeyhash'; - if ((0, psbtutils_js_1.isP2PKH)(script)) return 'pubkeyhash'; - if ((0, psbtutils_js_1.isP2MS)(script)) return 'multisig'; - if ((0, psbtutils_js_1.isP2PK)(script)) return 'pubkey'; - return 'nonstandard'; -} function range(n) { return [...Array(n).keys()]; } diff --git a/src/cjs/psbt/internal/finalize.cjs b/src/cjs/psbt/internal/finalize.cjs new file mode 100644 index 000000000..d23c7c08b --- /dev/null +++ b/src/cjs/psbt/internal/finalize.cjs @@ -0,0 +1,193 @@ +'use strict'; +var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if ( + !desc || + ('get' in desc ? !m.__esModule : desc.writable || desc.configurable) + ) { + desc = { + enumerable: true, + get: function () { + return m[k]; + }, + }; + } + Object.defineProperty(o, k2, desc); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); +var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', { enumerable: true, value: v }); + } + : function (o, v) { + o['default'] = v; + }); +var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== 'default' && Object.prototype.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; +Object.defineProperty(exports, '__esModule', { value: true }); +exports.getFinalScripts = getFinalScripts; +const payments = __importStar(require('../../payments/index.cjs')); +const psbtutils_js_1 = require('../psbtutils.cjs'); +const psbtutils_js_2 = require('../psbtutils.cjs'); +const scriptType_js_1 = require('./scriptType.cjs'); +const tools = __importStar(require('uint8array-tools')); +function getFinalScripts(inputIndex, input, script, isSegwit, isP2SH, isP2WSH) { + const scriptType = (0, scriptType_js_1.classifyScript)(script, { + isP2WPKH: psbtutils_js_2.isP2WPKH, + isP2PKH: psbtutils_js_2.isP2PKH, + isP2MS: psbtutils_js_2.isP2MS, + isP2PK: psbtutils_js_2.isP2PK, + }); + if (!canFinalize(input, script, scriptType)) + throw new Error(`Can not finalize input #${inputIndex}`); + return prepareFinalScripts( + script, + scriptType, + input.partialSig, + isSegwit, + isP2SH, + isP2WSH, + ); +} +function prepareFinalScripts( + script, + scriptType, + partialSig, + isSegwit, + isP2SH, + isP2WSH, +) { + let finalScriptSig; + let finalScriptWitness; + const payment = getPayment(script, scriptType, partialSig); + const p2wsh = !isP2WSH ? null : payments.p2wsh({ redeem: payment }); + const p2sh = !isP2SH ? null : payments.p2sh({ redeem: p2wsh || payment }); + if (isSegwit) { + if (p2wsh) { + finalScriptWitness = (0, psbtutils_js_1.witnessStackToScriptWitness)( + p2wsh.witness, + ); + } else { + finalScriptWitness = (0, psbtutils_js_1.witnessStackToScriptWitness)( + payment.witness, + ); + } + if (p2sh) { + finalScriptSig = p2sh.input; + } + } else { + if (p2sh) { + finalScriptSig = p2sh.input; + } else { + finalScriptSig = payment.input; + } + } + return { + finalScriptSig, + finalScriptWitness, + }; +} +function canFinalize(input, script, scriptType) { + switch (scriptType) { + case 'pubkey': + case 'pubkeyhash': + case 'witnesspubkeyhash': + return hasSigs(1, input.partialSig); + case 'multisig': + const p2ms = payments.p2ms({ output: script }); + return hasSigs(p2ms.m, input.partialSig, p2ms.pubkeys); + default: + return false; + } +} +function hasSigs(neededSigs, partialSig, pubkeys) { + if (!partialSig) return false; + let sigs; + if (pubkeys) { + sigs = pubkeys + .map(pkey => { + const pubkey = compressPubkey(pkey); + return partialSig.find( + pSig => tools.compare(pSig.pubkey, pubkey) === 0, + ); + }) + .filter(v => !!v); + } else { + sigs = partialSig; + } + if (sigs.length > neededSigs) throw new Error('Too many signatures'); + return sigs.length === neededSigs; +} +function getSortedSigs(script, partialSig) { + const p2ms = payments.p2ms({ output: script }); + return p2ms.pubkeys + .map(pk => { + return ( + partialSig.filter(ps => { + return tools.compare(ps.pubkey, pk) === 0; + })[0] || {} + ).signature; + }) + .filter(v => !!v); +} +function getPayment(script, scriptType, partialSig) { + let payment; + switch (scriptType) { + case 'multisig': + const sigs = getSortedSigs(script, partialSig); + payment = payments.p2ms({ + output: script, + signatures: sigs, + }); + break; + case 'pubkey': + payment = payments.p2pk({ + output: script, + signature: partialSig[0].signature, + }); + break; + case 'pubkeyhash': + payment = payments.p2pkh({ + output: script, + pubkey: partialSig[0].pubkey, + signature: partialSig[0].signature, + }); + break; + case 'witnesspubkeyhash': + payment = payments.p2wpkh({ + output: script, + pubkey: partialSig[0].pubkey, + signature: partialSig[0].signature, + }); + break; + } + return payment; +} +function compressPubkey(pubkey) { + if (pubkey.length === 65) { + const parity = pubkey[64] & 1; + const newKey = pubkey.slice(0, 33); + newKey[0] = 2 | parity; + return newKey; + } + return pubkey.slice(); +} diff --git a/src/cjs/psbt/internal/finalize.d.ts b/src/cjs/psbt/internal/finalize.d.ts new file mode 100644 index 000000000..9013dba45 --- /dev/null +++ b/src/cjs/psbt/internal/finalize.d.ts @@ -0,0 +1,5 @@ +import { PsbtInput } from 'bip174'; +export declare function getFinalScripts(inputIndex: number, input: PsbtInput, script: Uint8Array, isSegwit: boolean, isP2SH: boolean, isP2WSH: boolean): { + finalScriptSig: Uint8Array | undefined; + finalScriptWitness: Uint8Array | undefined; +}; diff --git a/src/cjs/psbt/internal/scriptType.cjs b/src/cjs/psbt/internal/scriptType.cjs new file mode 100644 index 000000000..db94345cd --- /dev/null +++ b/src/cjs/psbt/internal/scriptType.cjs @@ -0,0 +1,62 @@ +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); +exports.classifyScript = classifyScript; +exports.getMeaningfulScript = getMeaningfulScript; +exports.checkInvalidP2WSH = checkInvalidP2WSH; +function classifyScript(script, deps) { + if (deps.isP2WPKH(script)) return 'witnesspubkeyhash'; + if (deps.isP2PKH(script)) return 'pubkeyhash'; + if (deps.isP2MS(script)) return 'multisig'; + if (deps.isP2PK(script)) return 'pubkey'; + return 'nonstandard'; +} +function getMeaningfulScript( + script, + index, + ioType, + redeemScript, + witnessScript, + deps, +) { + const isP2SH = deps.isP2SHScript(script); + const isP2SHP2WSH = + isP2SH && redeemScript && deps.isP2WSHScript(redeemScript); + const isP2WSH = deps.isP2WSHScript(script); + if (isP2SH && redeemScript === undefined) + throw new Error('scriptPubkey is P2SH but redeemScript missing'); + if ((isP2WSH || isP2SHP2WSH) && witnessScript === undefined) + throw new Error( + 'scriptPubkey or redeemScript is P2WSH but witnessScript missing', + ); + let meaningfulScript; + if (isP2SHP2WSH) { + meaningfulScript = witnessScript; + deps.checkRedeemScript(index, script, redeemScript, ioType); + deps.checkWitnessScript(index, redeemScript, witnessScript, ioType); + checkInvalidP2WSH(meaningfulScript, deps.isP2WPKH, deps.isP2SHScript); + } else if (isP2WSH) { + meaningfulScript = witnessScript; + deps.checkWitnessScript(index, script, witnessScript, ioType); + checkInvalidP2WSH(meaningfulScript, deps.isP2WPKH, deps.isP2SHScript); + } else if (isP2SH) { + meaningfulScript = redeemScript; + deps.checkRedeemScript(index, script, redeemScript, ioType); + } else { + meaningfulScript = script; + } + return { + meaningfulScript, + type: isP2SHP2WSH + ? 'p2sh-p2wsh' + : isP2SH + ? 'p2sh' + : isP2WSH + ? 'p2wsh' + : 'raw', + }; +} +function checkInvalidP2WSH(script, isP2WPKH, isP2SHScript) { + if (isP2WPKH(script) || isP2SHScript(script)) { + throw new Error('P2WPKH or P2SH can not be contained within P2WSH'); + } +} diff --git a/src/cjs/psbt/internal/scriptType.d.ts b/src/cjs/psbt/internal/scriptType.d.ts new file mode 100644 index 000000000..fe5b843c8 --- /dev/null +++ b/src/cjs/psbt/internal/scriptType.d.ts @@ -0,0 +1,17 @@ +export type ScriptType = 'witnesspubkeyhash' | 'pubkeyhash' | 'multisig' | 'pubkey' | 'nonstandard'; +export interface ScriptTypeDeps { + isP2WPKH(script: Uint8Array): boolean; + isP2PKH(script: Uint8Array): boolean; + isP2MS(script: Uint8Array): boolean; + isP2PK(script: Uint8Array): boolean; + isP2SHScript(script: Uint8Array): boolean; + isP2WSHScript(script: Uint8Array): boolean; + checkRedeemScript(index: number, script: Uint8Array, redeemScript: Uint8Array, ioType: 'input' | 'output'): void; + checkWitnessScript(index: number, script: Uint8Array, witnessScript: Uint8Array, ioType: 'input' | 'output'): void; +} +export declare function classifyScript(script: Uint8Array, deps: Pick): ScriptType; +export declare function getMeaningfulScript(script: Uint8Array, index: number, ioType: 'input' | 'output', redeemScript: Uint8Array | undefined, witnessScript: Uint8Array | undefined, deps: Pick): { + meaningfulScript: Uint8Array; + type: 'p2sh' | 'p2wsh' | 'p2sh-p2wsh' | 'raw'; +}; +export declare function checkInvalidP2WSH(script: Uint8Array, isP2WPKH: (script: Uint8Array) => boolean, isP2SHScript: (script: Uint8Array) => boolean): void; diff --git a/src/cjs/psbt/internal/witness.cjs b/src/cjs/psbt/internal/witness.cjs new file mode 100644 index 000000000..b2fc6ce3e --- /dev/null +++ b/src/cjs/psbt/internal/witness.cjs @@ -0,0 +1,70 @@ +'use strict'; +var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if ( + !desc || + ('get' in desc ? !m.__esModule : desc.writable || desc.configurable) + ) { + desc = { + enumerable: true, + get: function () { + return m[k]; + }, + }; + } + Object.defineProperty(o, k2, desc); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); +var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', { enumerable: true, value: v }); + } + : function (o, v) { + o['default'] = v; + }); +var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== 'default' && Object.prototype.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; +Object.defineProperty(exports, '__esModule', { value: true }); +exports.scriptWitnessToWitnessStack = scriptWitnessToWitnessStack; +const varuint = __importStar(require('varuint-bitcoin')); +function scriptWitnessToWitnessStack(buffer) { + let offset = 0; + function readSlice(n) { + offset += n; + return buffer.slice(offset - n, offset); + } + function readVarInt() { + const vi = varuint.decode(buffer, offset); + offset += varuint.encodingLength(vi.bigintValue); + return vi.numberValue; + } + function readVarSlice() { + return readSlice(readVarInt()); + } + function readVector() { + const count = readVarInt(); + const vector = []; + for (let i = 0; i < count; i++) vector.push(readVarSlice()); + return vector; + } + return readVector(); +} diff --git a/src/cjs/psbt/internal/witness.d.ts b/src/cjs/psbt/internal/witness.d.ts new file mode 100644 index 000000000..8debfc5a1 --- /dev/null +++ b/src/cjs/psbt/internal/witness.d.ts @@ -0,0 +1 @@ +export declare function scriptWitnessToWitnessStack(buffer: Uint8Array): Uint8Array[]; diff --git a/src/cjs/types.d.ts b/src/cjs/types.d.ts index 7abcd67bb..6123a3bf4 100644 --- a/src/cjs/types.d.ts +++ b/src/cjs/types.d.ts @@ -1,5 +1,5 @@ import * as v from 'valibot'; -export declare const NBufferSchemaFactory: (size: number) => v.SchemaWithPipe<[v.InstanceSchema, v.LengthAction]>; +export declare const NBufferSchemaFactory: (size: number) => v.SchemaWithPipe, v.LengthAction]>; /** * Checks if two arrays of Buffers are equal. * @param a - The first array of Buffers. @@ -34,12 +34,12 @@ export interface TinySecp256k1Interface { isXOnlyPoint(p: Uint8Array): boolean; xOnlyPointAddTweak(p: Uint8Array, tweak: Uint8Array): XOnlyPointAddTweakResult | null; } -export declare const Buffer256bitSchema: v.SchemaWithPipe<[v.InstanceSchema, v.LengthAction]>; -export declare const Hash160bitSchema: v.SchemaWithPipe<[v.InstanceSchema, v.LengthAction]>; -export declare const Hash256bitSchema: v.SchemaWithPipe<[v.InstanceSchema, v.LengthAction]>; +export declare const Buffer256bitSchema: v.SchemaWithPipe, v.LengthAction]>; +export declare const Hash160bitSchema: v.SchemaWithPipe, v.LengthAction]>; +export declare const Hash256bitSchema: v.SchemaWithPipe, v.LengthAction]>; export declare const BufferSchema: v.InstanceSchema; -export declare const HexSchema: v.SchemaWithPipe<[v.StringSchema, v.RegexAction]>; -export declare const UInt8Schema: v.SchemaWithPipe<[v.NumberSchema, v.IntegerAction, v.MinValueAction, v.MaxValueAction]>; -export declare const UInt32Schema: v.SchemaWithPipe<[v.NumberSchema, v.IntegerAction, v.MinValueAction, v.MaxValueAction]>; -export declare const SatoshiSchema: v.SchemaWithPipe<[v.BigintSchema, v.MinValueAction, v.MaxValueAction]>; +export declare const HexSchema: v.SchemaWithPipe, v.RegexAction]>; +export declare const UInt8Schema: v.SchemaWithPipe, v.IntegerAction, v.MinValueAction, v.MaxValueAction]>; +export declare const UInt32Schema: v.SchemaWithPipe, v.IntegerAction, v.MinValueAction, v.MaxValueAction]>; +export declare const SatoshiSchema: v.SchemaWithPipe, v.MinValueAction, v.MaxValueAction]>; export declare const NullablePartial: (a: Record) => v.ObjectSchema<{}, undefined>; diff --git a/src/esm/psbt.js b/src/esm/psbt.js index 5e2e39b93..6bfc00150 100644 --- a/src/esm/psbt.js +++ b/src/esm/psbt.js @@ -1,5 +1,4 @@ import { Psbt as PsbtBase } from 'bip174'; -import * as varuint from 'varuint-bitcoin'; import { checkForInput, checkForOutput } from 'bip174'; import { fromOutputScript, toOutputScript } from './address.js'; import { cloneBuffer, reverseBuffer } from './bufferutils.js'; @@ -29,6 +28,13 @@ import { isP2SHScript, isP2TR, } from './psbt/psbtutils.js'; +import { scriptWitnessToWitnessStack } from './psbt/internal/witness.js'; +import { getFinalScripts } from './psbt/internal/finalize.js'; +import { + classifyScript, + getMeaningfulScript, + checkInvalidP2WSH, +} from './psbt/internal/scriptType.js'; import * as tools from 'uint8array-tools'; export { toXOnly }; /** @@ -225,7 +231,8 @@ export class Psbt { } checkTaprootInputFields(inputData, inputData, 'addInput'); checkInputsForPartialSig(this.data.inputs, 'addInput'); - if (inputData.witnessScript) checkInvalidP2WSH(inputData.witnessScript); + if (inputData.witnessScript) + checkInvalidP2WSHScript(inputData.witnessScript); const c = this.__CACHE; this.data.addInput(inputData); const txIn = c.__TX.ins[c.__TX.ins.length - 1]; @@ -387,9 +394,10 @@ export class Psbt { input.redeemScript || redeemFromFinalScriptSig(input.finalScriptSig), input.witnessScript || redeemFromFinalWitnessScript(input.finalScriptWitness), + SCRIPT_TYPE_DEPS, ); const type = result.type === 'raw' ? '' : result.type + '-'; - const mainType = classifyScript(result.meaningfulScript); + const mainType = classifyScript(result.meaningfulScript, SCRIPT_TYPE_DEPS); return type + mainType; } inputHasPubkey(inputIndex, pubkey) { @@ -872,7 +880,8 @@ export class Psbt { return this; } updateInput(inputIndex, updateData) { - if (updateData.witnessScript) checkInvalidP2WSH(updateData.witnessScript); + if (updateData.witnessScript) + checkInvalidP2WSHScript(updateData.witnessScript); checkTaprootInputFields( this.data.inputs[inputIndex], updateData, @@ -967,42 +976,11 @@ class PsbtTransaction { return this.tx.toBuffer(); } } -function canFinalize(input, script, scriptType) { - switch (scriptType) { - case 'pubkey': - case 'pubkeyhash': - case 'witnesspubkeyhash': - return hasSigs(1, input.partialSig); - case 'multisig': - const p2ms = payments.p2ms({ output: script }); - return hasSigs(p2ms.m, input.partialSig, p2ms.pubkeys); - default: - return false; - } -} function checkCache(cache) { if (cache.__UNSAFE_SIGN_NONSEGWIT !== false) { throw new Error('Not BIP174 compliant, can not export'); } } -function hasSigs(neededSigs, partialSig, pubkeys) { - if (!partialSig) return false; - let sigs; - if (pubkeys) { - sigs = pubkeys - .map(pkey => { - const pubkey = compressPubkey(pkey); - return partialSig.find( - pSig => tools.compare(pSig.pubkey, pubkey) === 0, - ); - }) - .filter(v => !!v); - } else { - sigs = partialSig; - } - if (sigs.length > neededSigs) throw new Error('Too many signatures'); - return sigs.length === neededSigs; -} function isFinalized(input) { return !!input.finalScriptSig || !!input.finalScriptWitness; } @@ -1104,6 +1082,18 @@ const checkWitnessScript = scriptCheckerFactory( payments.p2wsh, 'Witness script', ); +const SCRIPT_TYPE_DEPS = { + isP2WPKH, + isP2PKH, + isP2MS, + isP2PK, + isP2SHScript, + isP2WSHScript, + checkRedeemScript, + checkWitnessScript, +}; +const checkInvalidP2WSHScript = script => + checkInvalidP2WSH(script, isP2WPKH, isP2SHScript); function getTxCacheValue(key, name, inputs, c) { if (!inputs.every(isFinalized)) throw new Error(`PSBT must be finalized to calculate ${name}`); @@ -1121,54 +1111,6 @@ function getTxCacheValue(key, name, inputs, c) { if (key === '__FEE_RATE') return c.__FEE_RATE; else if (key === '__FEE') return c.__FEE; } -function getFinalScripts(inputIndex, input, script, isSegwit, isP2SH, isP2WSH) { - const scriptType = classifyScript(script); - if (!canFinalize(input, script, scriptType)) - throw new Error(`Can not finalize input #${inputIndex}`); - return prepareFinalScripts( - script, - scriptType, - input.partialSig, - isSegwit, - isP2SH, - isP2WSH, - ); -} -function prepareFinalScripts( - script, - scriptType, - partialSig, - isSegwit, - isP2SH, - isP2WSH, -) { - let finalScriptSig; - let finalScriptWitness; - // Wow, the payments API is very handy - const payment = getPayment(script, scriptType, partialSig); - const p2wsh = !isP2WSH ? null : payments.p2wsh({ redeem: payment }); - const p2sh = !isP2SH ? null : payments.p2sh({ redeem: p2wsh || payment }); - if (isSegwit) { - if (p2wsh) { - finalScriptWitness = witnessStackToScriptWitness(p2wsh.witness); - } else { - finalScriptWitness = witnessStackToScriptWitness(payment.witness); - } - if (p2sh) { - finalScriptSig = p2sh.input; - } - } else { - if (p2sh) { - finalScriptSig = p2sh.input; - } else { - finalScriptSig = payment.input; - } - } - return { - finalScriptSig, - finalScriptWitness, - }; -} function getHashAndSighashType( inputs, inputIndex, @@ -1223,6 +1165,7 @@ function getHashForSig(inputIndex, input, cache, forValidate, sighashTypes) { 'input', input.redeemScript, input.witnessScript, + SCRIPT_TYPE_DEPS, ); if (['p2sh-p2wsh', 'p2wsh'].indexOf(type) >= 0) { hash = unsignedTx.hashForWitnessV0( @@ -1368,39 +1311,6 @@ function checkSighashTypeAllowed(sighashType, sighashTypes) { ); } } -function getPayment(script, scriptType, partialSig) { - let payment; - switch (scriptType) { - case 'multisig': - const sigs = getSortedSigs(script, partialSig); - payment = payments.p2ms({ - output: script, - signatures: sigs, - }); - break; - case 'pubkey': - payment = payments.p2pk({ - output: script, - signature: partialSig[0].signature, - }); - break; - case 'pubkeyhash': - payment = payments.p2pkh({ - output: script, - pubkey: partialSig[0].pubkey, - signature: partialSig[0].signature, - }); - break; - case 'witnesspubkeyhash': - payment = payments.p2wpkh({ - output: script, - pubkey: partialSig[0].pubkey, - signature: partialSig[0].signature, - }); - break; - } - return payment; -} function getScriptFromInput(inputIndex, input, cache) { const unsignedTx = cache.__TX; const res = { @@ -1461,44 +1371,6 @@ function getSignersFromHD(inputIndex, inputs, hdKeyPair) { }); return signers; } -function getSortedSigs(script, partialSig) { - const p2ms = payments.p2ms({ output: script }); - // for each pubkey in order of p2ms script - return p2ms.pubkeys - .map(pk => { - // filter partialSig array by pubkey being equal - return ( - partialSig.filter(ps => { - return tools.compare(ps.pubkey, pk) === 0; - })[0] || {} - ).signature; - // Any pubkey without a match will return undefined - // this last filter removes all the undefined items in the array. - }) - .filter(v => !!v); -} -function scriptWitnessToWitnessStack(buffer) { - let offset = 0; - function readSlice(n) { - offset += n; - return buffer.slice(offset - n, offset); - } - function readVarInt() { - const vi = varuint.decode(buffer, offset); - offset += varuint.encodingLength(vi.bigintValue); - return vi.numberValue; - } - function readVarSlice() { - return readSlice(readVarInt()); - } - function readVector() { - const count = readVarInt(); - const vector = []; - for (let i = 0; i < count; i++) vector.push(readVarSlice()); - return vector; - } - return readVector(); -} function sighashTypeToString(sighashType) { let text = sighashType & Transaction.SIGHASH_ANYONECANPAY @@ -1609,6 +1481,7 @@ function pubkeyInInput(pubkey, input, inputIndex, cache) { 'input', input.redeemScript, input.witnessScript, + SCRIPT_TYPE_DEPS, ); return pubkeyInScript(pubkey, meaningfulScript); } @@ -1620,6 +1493,7 @@ function pubkeyInOutput(pubkey, output, outputIndex, cache) { 'output', output.redeemScript, output.witnessScript, + SCRIPT_TYPE_DEPS, ); return pubkeyInScript(pubkey, meaningfulScript); } @@ -1647,76 +1521,12 @@ function redeemFromFinalWitnessScript(finalScript) { if (!sDecomp) return; return lastItem; } -function compressPubkey(pubkey) { - if (pubkey.length === 65) { - const parity = pubkey[64] & 1; - const newKey = pubkey.slice(0, 33); - newKey[0] = 2 | parity; - return newKey; - } - return pubkey.slice(); -} function isPubkeyLike(buf) { return buf.length === 33 && bscript.isCanonicalPubKey(buf); } function isSigLike(buf) { return bscript.isCanonicalScriptSignature(buf); } -function getMeaningfulScript( - script, - index, - ioType, - redeemScript, - witnessScript, -) { - const isP2SH = isP2SHScript(script); - const isP2SHP2WSH = isP2SH && redeemScript && isP2WSHScript(redeemScript); - const isP2WSH = isP2WSHScript(script); - if (isP2SH && redeemScript === undefined) - throw new Error('scriptPubkey is P2SH but redeemScript missing'); - if ((isP2WSH || isP2SHP2WSH) && witnessScript === undefined) - throw new Error( - 'scriptPubkey or redeemScript is P2WSH but witnessScript missing', - ); - let meaningfulScript; - if (isP2SHP2WSH) { - meaningfulScript = witnessScript; - checkRedeemScript(index, script, redeemScript, ioType); - checkWitnessScript(index, redeemScript, witnessScript, ioType); - checkInvalidP2WSH(meaningfulScript); - } else if (isP2WSH) { - meaningfulScript = witnessScript; - checkWitnessScript(index, script, witnessScript, ioType); - checkInvalidP2WSH(meaningfulScript); - } else if (isP2SH) { - meaningfulScript = redeemScript; - checkRedeemScript(index, script, redeemScript, ioType); - } else { - meaningfulScript = script; - } - return { - meaningfulScript, - type: isP2SHP2WSH - ? 'p2sh-p2wsh' - : isP2SH - ? 'p2sh' - : isP2WSH - ? 'p2wsh' - : 'raw', - }; -} -function checkInvalidP2WSH(script) { - if (isP2WPKH(script) || isP2SHScript(script)) { - throw new Error('P2WPKH or P2SH can not be contained within P2WSH'); - } -} -function classifyScript(script) { - if (isP2WPKH(script)) return 'witnesspubkeyhash'; - if (isP2PKH(script)) return 'pubkeyhash'; - if (isP2MS(script)) return 'multisig'; - if (isP2PK(script)) return 'pubkey'; - return 'nonstandard'; -} function range(n) { return [...Array(n).keys()]; } diff --git a/src/esm/psbt/internal/finalize.js b/src/esm/psbt/internal/finalize.js new file mode 100644 index 000000000..7a7003ac7 --- /dev/null +++ b/src/esm/psbt/internal/finalize.js @@ -0,0 +1,149 @@ +import * as payments from '../../payments/index.js'; +import { witnessStackToScriptWitness } from '../psbtutils.js'; +import { isP2MS, isP2PK, isP2PKH, isP2WPKH } from '../psbtutils.js'; +import { classifyScript } from './scriptType.js'; +import * as tools from 'uint8array-tools'; +export function getFinalScripts( + inputIndex, + input, + script, + isSegwit, + isP2SH, + isP2WSH, +) { + const scriptType = classifyScript(script, { + isP2WPKH, + isP2PKH, + isP2MS, + isP2PK, + }); + if (!canFinalize(input, script, scriptType)) + throw new Error(`Can not finalize input #${inputIndex}`); + return prepareFinalScripts( + script, + scriptType, + input.partialSig, + isSegwit, + isP2SH, + isP2WSH, + ); +} +function prepareFinalScripts( + script, + scriptType, + partialSig, + isSegwit, + isP2SH, + isP2WSH, +) { + let finalScriptSig; + let finalScriptWitness; + const payment = getPayment(script, scriptType, partialSig); + const p2wsh = !isP2WSH ? null : payments.p2wsh({ redeem: payment }); + const p2sh = !isP2SH ? null : payments.p2sh({ redeem: p2wsh || payment }); + if (isSegwit) { + if (p2wsh) { + finalScriptWitness = witnessStackToScriptWitness(p2wsh.witness); + } else { + finalScriptWitness = witnessStackToScriptWitness(payment.witness); + } + if (p2sh) { + finalScriptSig = p2sh.input; + } + } else { + if (p2sh) { + finalScriptSig = p2sh.input; + } else { + finalScriptSig = payment.input; + } + } + return { + finalScriptSig, + finalScriptWitness, + }; +} +function canFinalize(input, script, scriptType) { + switch (scriptType) { + case 'pubkey': + case 'pubkeyhash': + case 'witnesspubkeyhash': + return hasSigs(1, input.partialSig); + case 'multisig': + const p2ms = payments.p2ms({ output: script }); + return hasSigs(p2ms.m, input.partialSig, p2ms.pubkeys); + default: + return false; + } +} +function hasSigs(neededSigs, partialSig, pubkeys) { + if (!partialSig) return false; + let sigs; + if (pubkeys) { + sigs = pubkeys + .map(pkey => { + const pubkey = compressPubkey(pkey); + return partialSig.find( + pSig => tools.compare(pSig.pubkey, pubkey) === 0, + ); + }) + .filter(v => !!v); + } else { + sigs = partialSig; + } + if (sigs.length > neededSigs) throw new Error('Too many signatures'); + return sigs.length === neededSigs; +} +function getSortedSigs(script, partialSig) { + const p2ms = payments.p2ms({ output: script }); + return p2ms.pubkeys + .map(pk => { + return ( + partialSig.filter(ps => { + return tools.compare(ps.pubkey, pk) === 0; + })[0] || {} + ).signature; + }) + .filter(v => !!v); +} +function getPayment(script, scriptType, partialSig) { + let payment; + switch (scriptType) { + case 'multisig': + const sigs = getSortedSigs(script, partialSig); + payment = payments.p2ms({ + output: script, + signatures: sigs, + }); + break; + case 'pubkey': + payment = payments.p2pk({ + output: script, + signature: partialSig[0].signature, + }); + break; + case 'pubkeyhash': + payment = payments.p2pkh({ + output: script, + pubkey: partialSig[0].pubkey, + signature: partialSig[0].signature, + }); + break; + case 'witnesspubkeyhash': + payment = payments.p2wpkh({ + output: script, + pubkey: partialSig[0].pubkey, + signature: partialSig[0].signature, + }); + break; + } + return payment; +} +function compressPubkey(pubkey) { + if (pubkey.length === 65) { + const parity = pubkey[64] & 1; + const newKey = pubkey.slice(0, 33); + newKey[0] = 2 | parity; + return newKey; + } + return pubkey.slice(); +} diff --git a/src/esm/psbt/internal/scriptType.js b/src/esm/psbt/internal/scriptType.js new file mode 100644 index 000000000..5f03373f0 --- /dev/null +++ b/src/esm/psbt/internal/scriptType.js @@ -0,0 +1,57 @@ +export function classifyScript(script, deps) { + if (deps.isP2WPKH(script)) return 'witnesspubkeyhash'; + if (deps.isP2PKH(script)) return 'pubkeyhash'; + if (deps.isP2MS(script)) return 'multisig'; + if (deps.isP2PK(script)) return 'pubkey'; + return 'nonstandard'; +} +export function getMeaningfulScript( + script, + index, + ioType, + redeemScript, + witnessScript, + deps, +) { + const isP2SH = deps.isP2SHScript(script); + const isP2SHP2WSH = + isP2SH && redeemScript && deps.isP2WSHScript(redeemScript); + const isP2WSH = deps.isP2WSHScript(script); + if (isP2SH && redeemScript === undefined) + throw new Error('scriptPubkey is P2SH but redeemScript missing'); + if ((isP2WSH || isP2SHP2WSH) && witnessScript === undefined) + throw new Error( + 'scriptPubkey or redeemScript is P2WSH but witnessScript missing', + ); + let meaningfulScript; + if (isP2SHP2WSH) { + meaningfulScript = witnessScript; + deps.checkRedeemScript(index, script, redeemScript, ioType); + deps.checkWitnessScript(index, redeemScript, witnessScript, ioType); + checkInvalidP2WSH(meaningfulScript, deps.isP2WPKH, deps.isP2SHScript); + } else if (isP2WSH) { + meaningfulScript = witnessScript; + deps.checkWitnessScript(index, script, witnessScript, ioType); + checkInvalidP2WSH(meaningfulScript, deps.isP2WPKH, deps.isP2SHScript); + } else if (isP2SH) { + meaningfulScript = redeemScript; + deps.checkRedeemScript(index, script, redeemScript, ioType); + } else { + meaningfulScript = script; + } + return { + meaningfulScript, + type: isP2SHP2WSH + ? 'p2sh-p2wsh' + : isP2SH + ? 'p2sh' + : isP2WSH + ? 'p2wsh' + : 'raw', + }; +} +export function checkInvalidP2WSH(script, isP2WPKH, isP2SHScript) { + if (isP2WPKH(script) || isP2SHScript(script)) { + throw new Error('P2WPKH or P2SH can not be contained within P2WSH'); + } +} diff --git a/src/esm/psbt/internal/witness.js b/src/esm/psbt/internal/witness.js new file mode 100644 index 000000000..c6c3a5fb0 --- /dev/null +++ b/src/esm/psbt/internal/witness.js @@ -0,0 +1,23 @@ +import * as varuint from 'varuint-bitcoin'; +export function scriptWitnessToWitnessStack(buffer) { + let offset = 0; + function readSlice(n) { + offset += n; + return buffer.slice(offset - n, offset); + } + function readVarInt() { + const vi = varuint.decode(buffer, offset); + offset += varuint.encodingLength(vi.bigintValue); + return vi.numberValue; + } + function readVarSlice() { + return readSlice(readVarInt()); + } + function readVector() { + const count = readVarInt(); + const vector = []; + for (let i = 0; i < count; i++) vector.push(readVarSlice()); + return vector; + } + return readVector(); +} diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 357a059cd..fb9ec19ac 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1,5 +1,4 @@ import { Psbt as PsbtBase } from 'bip174'; -import * as varuint from 'varuint-bitcoin'; import { Bip32Derivation, KeyValue, @@ -44,6 +43,13 @@ import { isP2SHScript, isP2TR, } from './psbt/psbtutils.js'; +import { scriptWitnessToWitnessStack } from './psbt/internal/witness.js'; +import { getFinalScripts } from './psbt/internal/finalize.js'; +import { + classifyScript, + getMeaningfulScript, + checkInvalidP2WSH, +} from './psbt/internal/scriptType.js'; import * as tools from 'uint8array-tools'; export { toXOnly }; @@ -295,7 +301,8 @@ export class Psbt { } checkTaprootInputFields(inputData, inputData, 'addInput'); checkInputsForPartialSig(this.data.inputs, 'addInput'); - if (inputData.witnessScript) checkInvalidP2WSH(inputData.witnessScript); + if (inputData.witnessScript) + checkInvalidP2WSHScript(inputData.witnessScript); const c = this.__CACHE; this.data.addInput(inputData); const txIn = c.__TX.ins[c.__TX.ins.length - 1]; @@ -494,9 +501,10 @@ export class Psbt { input.redeemScript || redeemFromFinalScriptSig(input.finalScriptSig), input.witnessScript || redeemFromFinalWitnessScript(input.finalScriptWitness), + SCRIPT_TYPE_DEPS, ); const type = result.type === 'raw' ? '' : result.type + '-'; - const mainType = classifyScript(result.meaningfulScript); + const mainType = classifyScript(result.meaningfulScript, SCRIPT_TYPE_DEPS); return (type + mainType) as AllScriptType; } @@ -1097,7 +1105,8 @@ export class Psbt { } updateInput(inputIndex: number, updateData: PsbtInputUpdate): this { - if (updateData.witnessScript) checkInvalidP2WSH(updateData.witnessScript); + if (updateData.witnessScript) + checkInvalidP2WSHScript(updateData.witnessScript); checkTaprootInputFields( this.data.inputs[inputIndex], updateData, @@ -1296,53 +1305,12 @@ class PsbtTransaction implements ITransaction { } } -function canFinalize( - input: PsbtInput, - script: Uint8Array, - scriptType: string, -): boolean { - switch (scriptType) { - case 'pubkey': - case 'pubkeyhash': - case 'witnesspubkeyhash': - return hasSigs(1, input.partialSig); - case 'multisig': - const p2ms = payments.p2ms({ output: script }); - return hasSigs(p2ms.m!, input.partialSig, p2ms.pubkeys); - default: - return false; - } -} - function checkCache(cache: PsbtCache): void { if (cache.__UNSAFE_SIGN_NONSEGWIT !== false) { throw new Error('Not BIP174 compliant, can not export'); } } -function hasSigs( - neededSigs: number, - partialSig?: any[], - pubkeys?: Uint8Array[], -): boolean { - if (!partialSig) return false; - let sigs: any; - if (pubkeys) { - sigs = pubkeys - .map(pkey => { - const pubkey = compressPubkey(pkey); - return partialSig.find( - pSig => tools.compare(pSig.pubkey, pubkey) === 0, - ); - }) - .filter(v => !!v); - } else { - sigs = partialSig; - } - if (sigs.length > neededSigs) throw new Error('Too many signatures'); - return sigs.length === neededSigs; -} - function isFinalized(input: PsbtInput): boolean { return !!input.finalScriptSig || !!input.finalScriptWitness; } @@ -1478,6 +1446,20 @@ const checkWitnessScript = scriptCheckerFactory( 'Witness script', ); +const SCRIPT_TYPE_DEPS = { + isP2WPKH, + isP2PKH, + isP2MS, + isP2PK, + isP2SHScript, + isP2WSHScript, + checkRedeemScript, + checkWitnessScript, +}; + +const checkInvalidP2WSHScript = (script: Uint8Array): void => + checkInvalidP2WSH(script, isP2WPKH, isP2SHScript); + type TxCacheNumberKey = '__FEE_RATE' | '__FEE'; function getTxCacheValue( key: T, @@ -1527,71 +1509,6 @@ type FinalTaprootScriptsFunc = ( finalScriptWitness: Uint8Array | undefined; }; -function getFinalScripts( - inputIndex: number, - input: PsbtInput, - script: Uint8Array, - isSegwit: boolean, - isP2SH: boolean, - isP2WSH: boolean, -): { - finalScriptSig: Uint8Array | undefined; - finalScriptWitness: Uint8Array | undefined; -} { - const scriptType = classifyScript(script); - if (!canFinalize(input, script, scriptType)) - throw new Error(`Can not finalize input #${inputIndex}`); - return prepareFinalScripts( - script, - scriptType, - input.partialSig!, - isSegwit, - isP2SH, - isP2WSH, - ); -} - -function prepareFinalScripts( - script: Uint8Array, - scriptType: string, - partialSig: PartialSig[], - isSegwit: boolean, - isP2SH: boolean, - isP2WSH: boolean, -): { - finalScriptSig: Uint8Array | undefined; - finalScriptWitness: Uint8Array | undefined; -} { - let finalScriptSig: Uint8Array | undefined; - let finalScriptWitness: Uint8Array | undefined; - - // Wow, the payments API is very handy - const payment: payments.Payment = getPayment(script, scriptType, partialSig); - const p2wsh = !isP2WSH ? null : payments.p2wsh({ redeem: payment }); - const p2sh = !isP2SH ? null : payments.p2sh({ redeem: p2wsh || payment }); - - if (isSegwit) { - if (p2wsh) { - finalScriptWitness = witnessStackToScriptWitness(p2wsh.witness!); - } else { - finalScriptWitness = witnessStackToScriptWitness(payment.witness!); - } - if (p2sh) { - finalScriptSig = p2sh.input; - } - } else { - if (p2sh) { - finalScriptSig = p2sh.input; - } else { - finalScriptSig = payment.input; - } - } - return { - finalScriptSig, - finalScriptWitness, - }; -} - function getHashAndSighashType( inputs: PsbtInput[], inputIndex: number, @@ -1666,6 +1583,7 @@ function getHashForSig( 'input', input.redeemScript, input.witnessScript, + SCRIPT_TYPE_DEPS, ); if (['p2sh-p2wsh', 'p2wsh'].indexOf(type) >= 0) { @@ -1842,44 +1760,6 @@ function checkSighashTypeAllowed( } } -function getPayment( - script: Uint8Array, - scriptType: string, - partialSig: PartialSig[], -): payments.Payment { - let payment: payments.Payment; - switch (scriptType) { - case 'multisig': - const sigs = getSortedSigs(script, partialSig); - payment = payments.p2ms({ - output: script, - signatures: sigs, - }); - break; - case 'pubkey': - payment = payments.p2pk({ - output: script, - signature: partialSig[0].signature, - }); - break; - case 'pubkeyhash': - payment = payments.p2pkh({ - output: script, - pubkey: partialSig[0].pubkey, - signature: partialSig[0].signature, - }); - break; - case 'witnesspubkeyhash': - payment = payments.p2wpkh({ - output: script, - pubkey: partialSig[0].pubkey, - signature: partialSig[0].signature, - }); - break; - } - return payment!; -} - interface GetScriptReturn { script: Uint8Array | null; isSegwit: boolean; @@ -1956,54 +1836,6 @@ function getSignersFromHD( return signers; } -function getSortedSigs( - script: Uint8Array, - partialSig: PartialSig[], -): Uint8Array[] { - const p2ms = payments.p2ms({ output: script }); - // for each pubkey in order of p2ms script - return p2ms - .pubkeys!.map(pk => { - // filter partialSig array by pubkey being equal - return ( - partialSig.filter(ps => { - return tools.compare(ps.pubkey, pk) === 0; - })[0] || {} - ).signature; - // Any pubkey without a match will return undefined - // this last filter removes all the undefined items in the array. - }) - .filter(v => !!v); -} - -function scriptWitnessToWitnessStack(buffer: Uint8Array): Uint8Array[] { - let offset = 0; - - function readSlice(n: number): Uint8Array { - offset += n; - return buffer.slice(offset - n, offset); - } - - function readVarInt(): number { - const vi = varuint.decode(buffer, offset); - offset += varuint.encodingLength(vi.bigintValue); - return vi.numberValue!; - } - - function readVarSlice(): Uint8Array { - return readSlice(readVarInt()); - } - - function readVector(): Uint8Array[] { - const count = readVarInt(); - const vector: Uint8Array[] = []; - for (let i = 0; i < count; i++) vector.push(readVarSlice()); - return vector; - } - - return readVector(); -} - function sighashTypeToString(sighashType: number): string { let text = sighashType & Transaction.SIGHASH_ANYONECANPAY @@ -2151,6 +1983,7 @@ function pubkeyInInput( 'input', input.redeemScript, input.witnessScript, + SCRIPT_TYPE_DEPS, ); return pubkeyInScript(pubkey, meaningfulScript); } @@ -2168,6 +2001,7 @@ function pubkeyInOutput( 'output', output.redeemScript, output.witnessScript, + SCRIPT_TYPE_DEPS, ); return pubkeyInScript(pubkey, meaningfulScript); } @@ -2202,16 +2036,6 @@ function redeemFromFinalWitnessScript( return lastItem; } -function compressPubkey(pubkey: Uint8Array): Uint8Array { - if (pubkey.length === 65) { - const parity = pubkey[64] & 1; - const newKey = pubkey.slice(0, 33); - newKey[0] = 2 | parity; - return newKey; - } - return pubkey.slice(); -} - function isPubkeyLike(buf: Uint8Array): boolean { return buf.length === 33 && bscript.isCanonicalPubKey(buf); } @@ -2220,62 +2044,6 @@ function isSigLike(buf: Uint8Array): boolean { return bscript.isCanonicalScriptSignature(buf); } -function getMeaningfulScript( - script: Uint8Array, - index: number, - ioType: 'input' | 'output', - redeemScript?: Uint8Array, - witnessScript?: Uint8Array, -): { - meaningfulScript: Uint8Array; - type: 'p2sh' | 'p2wsh' | 'p2sh-p2wsh' | 'raw'; -} { - const isP2SH = isP2SHScript(script); - const isP2SHP2WSH = isP2SH && redeemScript && isP2WSHScript(redeemScript); - const isP2WSH = isP2WSHScript(script); - - if (isP2SH && redeemScript === undefined) - throw new Error('scriptPubkey is P2SH but redeemScript missing'); - if ((isP2WSH || isP2SHP2WSH) && witnessScript === undefined) - throw new Error( - 'scriptPubkey or redeemScript is P2WSH but witnessScript missing', - ); - - let meaningfulScript: Uint8Array; - - if (isP2SHP2WSH) { - meaningfulScript = witnessScript!; - checkRedeemScript(index, script, redeemScript!, ioType); - checkWitnessScript(index, redeemScript!, witnessScript!, ioType); - checkInvalidP2WSH(meaningfulScript); - } else if (isP2WSH) { - meaningfulScript = witnessScript!; - checkWitnessScript(index, script, witnessScript!, ioType); - checkInvalidP2WSH(meaningfulScript); - } else if (isP2SH) { - meaningfulScript = redeemScript!; - checkRedeemScript(index, script, redeemScript!, ioType); - } else { - meaningfulScript = script; - } - return { - meaningfulScript, - type: isP2SHP2WSH - ? 'p2sh-p2wsh' - : isP2SH - ? 'p2sh' - : isP2WSH - ? 'p2wsh' - : 'raw', - }; -} - -function checkInvalidP2WSH(script: Uint8Array): void { - if (isP2WPKH(script) || isP2SHScript(script)) { - throw new Error('P2WPKH or P2SH can not be contained within P2WSH'); - } -} - type AllScriptType = | 'witnesspubkeyhash' | 'pubkeyhash' @@ -2295,20 +2063,6 @@ type AllScriptType = | 'p2sh-p2wsh-multisig' | 'p2sh-p2wsh-pubkey' | 'p2sh-p2wsh-nonstandard'; -type ScriptType = - | 'witnesspubkeyhash' - | 'pubkeyhash' - | 'multisig' - | 'pubkey' - | 'nonstandard'; -function classifyScript(script: Uint8Array): ScriptType { - if (isP2WPKH(script)) return 'witnesspubkeyhash'; - if (isP2PKH(script)) return 'pubkeyhash'; - if (isP2MS(script)) return 'multisig'; - if (isP2PK(script)) return 'pubkey'; - return 'nonstandard'; -} - function range(n: number): number[] { return [...Array(n).keys()]; } diff --git a/ts_src/psbt/internal/finalize.ts b/ts_src/psbt/internal/finalize.ts new file mode 100644 index 000000000..361ba9df7 --- /dev/null +++ b/ts_src/psbt/internal/finalize.ts @@ -0,0 +1,180 @@ +import { PartialSig, PsbtInput } from 'bip174'; +import * as payments from '../../payments/index.js'; +import { witnessStackToScriptWitness } from '../psbtutils.js'; +import { isP2MS, isP2PK, isP2PKH, isP2WPKH } from '../psbtutils.js'; +import { classifyScript } from './scriptType.js'; +import * as tools from 'uint8array-tools'; + +export function getFinalScripts( + inputIndex: number, + input: PsbtInput, + script: Uint8Array, + isSegwit: boolean, + isP2SH: boolean, + isP2WSH: boolean, +): { + finalScriptSig: Uint8Array | undefined; + finalScriptWitness: Uint8Array | undefined; +} { + const scriptType = classifyScript(script, { + isP2WPKH, + isP2PKH, + isP2MS, + isP2PK, + }); + if (!canFinalize(input, script, scriptType)) + throw new Error(`Can not finalize input #${inputIndex}`); + return prepareFinalScripts( + script, + scriptType, + input.partialSig!, + isSegwit, + isP2SH, + isP2WSH, + ); +} + +function prepareFinalScripts( + script: Uint8Array, + scriptType: string, + partialSig: PartialSig[], + isSegwit: boolean, + isP2SH: boolean, + isP2WSH: boolean, +): { + finalScriptSig: Uint8Array | undefined; + finalScriptWitness: Uint8Array | undefined; +} { + let finalScriptSig: Uint8Array | undefined; + let finalScriptWitness: Uint8Array | undefined; + + const payment: payments.Payment = getPayment(script, scriptType, partialSig); + const p2wsh = !isP2WSH ? null : payments.p2wsh({ redeem: payment }); + const p2sh = !isP2SH ? null : payments.p2sh({ redeem: p2wsh || payment }); + + if (isSegwit) { + if (p2wsh) { + finalScriptWitness = witnessStackToScriptWitness(p2wsh.witness!); + } else { + finalScriptWitness = witnessStackToScriptWitness(payment.witness!); + } + if (p2sh) { + finalScriptSig = p2sh.input; + } + } else { + if (p2sh) { + finalScriptSig = p2sh.input; + } else { + finalScriptSig = payment.input; + } + } + return { + finalScriptSig, + finalScriptWitness, + }; +} + +function canFinalize( + input: PsbtInput, + script: Uint8Array, + scriptType: string, +): boolean { + switch (scriptType) { + case 'pubkey': + case 'pubkeyhash': + case 'witnesspubkeyhash': + return hasSigs(1, input.partialSig); + case 'multisig': + const p2ms = payments.p2ms({ output: script }); + return hasSigs(p2ms.m!, input.partialSig, p2ms.pubkeys); + default: + return false; + } +} + +function hasSigs( + neededSigs: number, + partialSig?: any[], + pubkeys?: Uint8Array[], +): boolean { + if (!partialSig) return false; + let sigs: any; + if (pubkeys) { + sigs = pubkeys + .map(pkey => { + const pubkey = compressPubkey(pkey); + return partialSig.find( + pSig => tools.compare(pSig.pubkey, pubkey) === 0, + ); + }) + .filter(v => !!v); + } else { + sigs = partialSig; + } + if (sigs.length > neededSigs) throw new Error('Too many signatures'); + return sigs.length === neededSigs; +} + +function getSortedSigs( + script: Uint8Array, + partialSig: PartialSig[], +): Uint8Array[] { + const p2ms = payments.p2ms({ output: script }); + return p2ms + .pubkeys!.map(pk => { + return ( + partialSig.filter(ps => { + return tools.compare(ps.pubkey, pk) === 0; + })[0] || {} + ).signature; + }) + .filter(v => !!v); +} + +function getPayment( + script: Uint8Array, + scriptType: string, + partialSig: PartialSig[], +): payments.Payment { + let payment: payments.Payment; + switch (scriptType) { + case 'multisig': + const sigs = getSortedSigs(script, partialSig); + payment = payments.p2ms({ + output: script, + signatures: sigs, + }); + break; + case 'pubkey': + payment = payments.p2pk({ + output: script, + signature: partialSig[0].signature, + }); + break; + case 'pubkeyhash': + payment = payments.p2pkh({ + output: script, + pubkey: partialSig[0].pubkey, + signature: partialSig[0].signature, + }); + break; + case 'witnesspubkeyhash': + payment = payments.p2wpkh({ + output: script, + pubkey: partialSig[0].pubkey, + signature: partialSig[0].signature, + }); + break; + } + return payment!; +} + +function compressPubkey(pubkey: Uint8Array): Uint8Array { + if (pubkey.length === 65) { + const parity = pubkey[64] & 1; + const newKey = pubkey.slice(0, 33); + newKey[0] = 2 | parity; + return newKey; + } + return pubkey.slice(); +} diff --git a/ts_src/psbt/internal/scriptType.ts b/ts_src/psbt/internal/scriptType.ts new file mode 100644 index 000000000..33d0e1dae --- /dev/null +++ b/ts_src/psbt/internal/scriptType.ts @@ -0,0 +1,107 @@ +export type ScriptType = + | 'witnesspubkeyhash' + | 'pubkeyhash' + | 'multisig' + | 'pubkey' + | 'nonstandard'; + +export interface ScriptTypeDeps { + isP2WPKH(script: Uint8Array): boolean; + isP2PKH(script: Uint8Array): boolean; + isP2MS(script: Uint8Array): boolean; + isP2PK(script: Uint8Array): boolean; + isP2SHScript(script: Uint8Array): boolean; + isP2WSHScript(script: Uint8Array): boolean; + checkRedeemScript( + index: number, + script: Uint8Array, + redeemScript: Uint8Array, + ioType: 'input' | 'output', + ): void; + checkWitnessScript( + index: number, + script: Uint8Array, + witnessScript: Uint8Array, + ioType: 'input' | 'output', + ): void; +} + +export function classifyScript( + script: Uint8Array, + deps: Pick, +): ScriptType { + if (deps.isP2WPKH(script)) return 'witnesspubkeyhash'; + if (deps.isP2PKH(script)) return 'pubkeyhash'; + if (deps.isP2MS(script)) return 'multisig'; + if (deps.isP2PK(script)) return 'pubkey'; + return 'nonstandard'; +} + +export function getMeaningfulScript( + script: Uint8Array, + index: number, + ioType: 'input' | 'output', + redeemScript: Uint8Array | undefined, + witnessScript: Uint8Array | undefined, + deps: Pick< + ScriptTypeDeps, + | 'isP2SHScript' + | 'isP2WSHScript' + | 'isP2WPKH' + | 'checkRedeemScript' + | 'checkWitnessScript' + >, +): { + meaningfulScript: Uint8Array; + type: 'p2sh' | 'p2wsh' | 'p2sh-p2wsh' | 'raw'; +} { + const isP2SH = deps.isP2SHScript(script); + const isP2SHP2WSH = + isP2SH && redeemScript && deps.isP2WSHScript(redeemScript); + const isP2WSH = deps.isP2WSHScript(script); + + if (isP2SH && redeemScript === undefined) + throw new Error('scriptPubkey is P2SH but redeemScript missing'); + if ((isP2WSH || isP2SHP2WSH) && witnessScript === undefined) + throw new Error( + 'scriptPubkey or redeemScript is P2WSH but witnessScript missing', + ); + + let meaningfulScript: Uint8Array; + + if (isP2SHP2WSH) { + meaningfulScript = witnessScript!; + deps.checkRedeemScript(index, script, redeemScript!, ioType); + deps.checkWitnessScript(index, redeemScript!, witnessScript!, ioType); + checkInvalidP2WSH(meaningfulScript, deps.isP2WPKH, deps.isP2SHScript); + } else if (isP2WSH) { + meaningfulScript = witnessScript!; + deps.checkWitnessScript(index, script, witnessScript!, ioType); + checkInvalidP2WSH(meaningfulScript, deps.isP2WPKH, deps.isP2SHScript); + } else if (isP2SH) { + meaningfulScript = redeemScript!; + deps.checkRedeemScript(index, script, redeemScript!, ioType); + } else { + meaningfulScript = script; + } + return { + meaningfulScript, + type: isP2SHP2WSH + ? 'p2sh-p2wsh' + : isP2SH + ? 'p2sh' + : isP2WSH + ? 'p2wsh' + : 'raw', + }; +} + +export function checkInvalidP2WSH( + script: Uint8Array, + isP2WPKH: (script: Uint8Array) => boolean, + isP2SHScript: (script: Uint8Array) => boolean, +): void { + if (isP2WPKH(script) || isP2SHScript(script)) { + throw new Error('P2WPKH or P2SH can not be contained within P2WSH'); + } +} diff --git a/ts_src/psbt/internal/witness.ts b/ts_src/psbt/internal/witness.ts new file mode 100644 index 000000000..303c4b4ee --- /dev/null +++ b/ts_src/psbt/internal/witness.ts @@ -0,0 +1,29 @@ +import * as varuint from 'varuint-bitcoin'; + +export function scriptWitnessToWitnessStack(buffer: Uint8Array): Uint8Array[] { + let offset = 0; + + function readSlice(n: number): Uint8Array { + offset += n; + return buffer.slice(offset - n, offset); + } + + function readVarInt(): number { + const vi = varuint.decode(buffer, offset); + offset += varuint.encodingLength(vi.bigintValue); + return vi.numberValue!; + } + + function readVarSlice(): Uint8Array { + return readSlice(readVarInt()); + } + + function readVector(): Uint8Array[] { + const count = readVarInt(); + const vector: Uint8Array[] = []; + for (let i = 0; i < count; i++) vector.push(readVarSlice()); + return vector; + } + + return readVector(); +}