diff --git a/src/psbt.js b/src/psbt.js index 9d00e41..fa4ba9f 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -2,10 +2,153 @@ Object.defineProperty(exports, '__esModule', { value: true }); const bip174_1 = require('bip174'); const utils_1 = require('bip174/src/lib/utils'); -const classify = require('./classify'); const payments = require('./payments'); const bscript = require('./script'); const transaction_1 = require('./transaction'); +class Psbt extends bip174_1.Psbt { + constructor(network) { + super(); + this.network = network; + } + canFinalize(inputIndex) { + const input = utils_1.checkForInput(this.inputs, inputIndex); + const script = getScriptFromInput( + inputIndex, + input, + this.globalMap.unsignedTx, + ); + if (!script) return false; + const scriptType = classifyScript(script); + // TODO: for each type + switch (scriptType) { + case 'pubkey': + return false; + case 'pubkeyhash': + return false; + case 'multisig': + return false; + case 'witnesspubkeyhash': + return false; + default: + return false; + } + } + signInput(inputIndex, keyPair) { + const input = this.inputs[inputIndex]; + if (input === undefined) throw new Error(`No input #${inputIndex}`); + const { hash, sighashType } = getHashForSig( + inputIndex, + input, + this.globalMap.unsignedTx, + ); + const pubkey = keyPair.publicKey; + // // TODO: throw error when the pubkey or pubkey hash is not found anywhere + // // in the script + // const pubkeyHash = hash160(keyPair.publicKey); + // + // const decompiled = bscript.decompile(script); + // if (decompiled === null) throw new Error('Unknown script error'); + // + // const hasKey = decompiled.some(element => { + // if (typeof element === 'number') return false; + // return element.equals(pubkey) || element.equals(pubkeyHash); + // }); + // + // if (!hasKey) { + // throw new Error( + // `Can not sign for this input with the key ${pubkey.toString('hex')}`, + // ); + // } + const partialSig = { + pubkey, + signature: bscript.signature.encode(keyPair.sign(hash), sighashType), + }; + return this.addPartialSigToInput(inputIndex, partialSig); + } +} +exports.Psbt = Psbt; +const getHashForSig = (inputIndex, input, txBuf) => { + const unsignedTx = transaction_1.Transaction.fromBuffer(txBuf); + const sighashType = + input.sighashType || transaction_1.Transaction.SIGHASH_ALL; + let hash; + let script; + if (input.nonWitnessUtxo) { + const nonWitnessUtxoTx = transaction_1.Transaction.fromBuffer( + input.nonWitnessUtxo, + ); + const prevoutHash = unsignedTx.ins[inputIndex].hash; + const utxoHash = nonWitnessUtxoTx.getHash(); + // If a non-witness UTXO is provided, its hash must match the hash specified in the prevout + if (!prevoutHash.equals(utxoHash)) { + throw new Error( + `Non-witness UTXO hash for input #${inputIndex} doesn't match the hash specified in the prevout`, + ); + } + const prevoutIndex = unsignedTx.ins[inputIndex].index; + const prevout = nonWitnessUtxoTx.outs[prevoutIndex]; + if (input.redeemScript) { + // If a redeemScript is provided, the scriptPubKey must be for that redeemScript + checkRedeemScript(inputIndex, prevout.script, input.redeemScript); + script = input.redeemScript; + hash = unsignedTx.hashForSignature( + inputIndex, + input.redeemScript, + sighashType, + ); + } else { + script = prevout.script; + hash = unsignedTx.hashForSignature( + inputIndex, + prevout.script, + sighashType, + ); + } + } else if (input.witnessUtxo) { + let _script; // so we don't shadow the `let script` above + if (input.redeemScript) { + // If a redeemScript is provided, the scriptPubKey must be for that redeemScript + checkRedeemScript( + inputIndex, + input.witnessUtxo.script, + input.redeemScript, + ); + _script = input.redeemScript; + } else { + _script = input.witnessUtxo.script; + } + if (isP2WPKH(_script)) { + // P2WPKH uses the P2PKH template for prevoutScript when signing + const signingScript = payments.p2pkh({ hash: _script.slice(2) }).output; + hash = unsignedTx.hashForWitnessV0( + inputIndex, + signingScript, + input.witnessUtxo.value, + sighashType, + ); + script = _script; + } else { + if (!input.witnessScript) + throw new Error('Segwit input needs witnessScript if not P2WPKH'); + checkWitnessScript(inputIndex, _script, input.witnessScript); + hash = unsignedTx.hashForWitnessV0( + inputIndex, + _script, + input.witnessUtxo.value, + sighashType, + ); + // want to make sure the script we return is the actual meaningful script + script = input.witnessScript; + } + } else { + throw new Error('Need a Utxo input item for signing'); + } + return { + script, + sighashType, + hash, + }; +}; const scriptCheckerFactory = (payment, paymentScriptName) => ( inputIndex, scriptPubKey, @@ -25,7 +168,7 @@ const checkWitnessScript = scriptCheckerFactory( payments.p2wsh, 'Witness script', ); -const isPayment = (script, payment) => { +const isPaymentFactory = payment => script => { try { payment({ output: script }); return true; @@ -33,6 +176,17 @@ const isPayment = (script, payment) => { return false; } }; +const isP2WPKH = isPaymentFactory(payments.p2wpkh); +const isP2PKH = isPaymentFactory(payments.p2pkh); +const isP2MS = isPaymentFactory(payments.p2ms); +const isP2PK = isPaymentFactory(payments.p2pk); +const 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 getScriptFromInput(inputIndex, input, _unsignedTx) { let script; if (input.nonWitnessUtxo) { @@ -60,134 +214,3 @@ function getScriptFromInput(inputIndex, input, _unsignedTx) { } return script; } -class Psbt extends bip174_1.Psbt { - constructor(network) { - super(); - this.network = network; - } - canFinalize(inputIndex) { - const input = utils_1.checkForInput(this.inputs, inputIndex); - const script = getScriptFromInput( - inputIndex, - input, - this.globalMap.unsignedTx, - ); - if (!script) return false; - const scriptType = classify.output(script); - switch (scriptType) { - case 'pubkey': - return false; - case 'pubkeyhash': - return false; - case 'multisig': - return false; - case 'witnesspubkeyhash': - return false; - default: - return false; - } - } - signInput(inputIndex, keyPair) { - // TODO: Implement BIP174 pre-sign checks: - // https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#signer - // - // if non_witness_utxo.exists: - // assert(sha256d(non_witness_utxo) == psbt.tx.innput[i].prevout.hash) - // if redeemScript.exists: - // assert(non_witness_utxo.vout[psbt.tx.input[i].prevout.n].scriptPubKey == P2SH(redeemScript)) - // sign_non_witness(redeemScript) - // else: - // sign_non_witness(non_witness_utxo.vout[psbt.tx.input[i].prevout.n].scriptPubKey) - // else if witness_utxo.exists: - // if redeemScript.exists: - // assert(witness_utxo.scriptPubKey == P2SH(redeemScript)) - // script = redeemScript - // else: - // script = witness_utxo.scriptPubKey - // if IsP2WPKH(script): - // sign_witness(P2PKH(script[2:22])) - // else if IsP2WSH(script): - // assert(script == P2WSH(witnessScript)) - // sign_witness(witnessScript) - // else: - // assert False - const input = this.inputs[inputIndex]; - if (input === undefined) throw new Error(`No input #${inputIndex}`); - const unsignedTx = transaction_1.Transaction.fromBuffer( - this.globalMap.unsignedTx, - ); - const sighashType = input.sighashType || 0x01; - let hash; - if (input.nonWitnessUtxo) { - const nonWitnessUtxoTx = transaction_1.Transaction.fromBuffer( - input.nonWitnessUtxo, - ); - const prevoutHash = unsignedTx.ins[inputIndex].hash; - const utxoHash = nonWitnessUtxoTx.getHash(); - // If a non-witness UTXO is provided, its hash must match the hash specified in the prevout - if (!prevoutHash.equals(utxoHash)) { - throw new Error( - `Non-witness UTXO hash for input #${inputIndex} doesn't match the hash specified in the prevout`, - ); - } - const prevoutIndex = unsignedTx.ins[inputIndex].index; - const prevout = nonWitnessUtxoTx.outs[prevoutIndex]; - if (input.redeemScript) { - // If a redeemScript is provided, the scriptPubKey must be for that redeemScript - checkRedeemScript(inputIndex, prevout.script, input.redeemScript); - hash = unsignedTx.hashForSignature( - inputIndex, - input.redeemScript, - sighashType, - ); - } else { - hash = unsignedTx.hashForSignature( - inputIndex, - prevout.script, - sighashType, - ); - } - } else if (input.witnessUtxo) { - let script; - if (input.redeemScript) { - // If a redeemScript is provided, the scriptPubKey must be for that redeemScript - checkRedeemScript( - inputIndex, - input.witnessUtxo.script, - input.redeemScript, - ); - script = input.redeemScript; - } else { - script = input.witnessUtxo.script; - } - if (isPayment(script, payments.p2wpkh)) { - // P2WPKH uses the P2PKH template for prevoutScript when signing - const signingScript = payments.p2pkh({ hash: script.slice(2) }).output; - hash = unsignedTx.hashForWitnessV0( - inputIndex, - signingScript, - input.witnessUtxo.value, - sighashType, - ); - } else { - if (!input.witnessScript) - throw new Error('Segwit input needs witnessScript if not P2WPKH'); - checkWitnessScript(inputIndex, script, input.witnessScript); - hash = unsignedTx.hashForWitnessV0( - inputIndex, - script, - input.witnessUtxo.value, - sighashType, - ); - } - } else { - throw new Error('Need a Utxo input item for signing'); - } - const partialSig = { - pubkey: keyPair.publicKey, - signature: bscript.signature.encode(keyPair.sign(hash), sighashType), - }; - return this.addPartialSigToInput(inputIndex, partialSig); - } -} -exports.Psbt = Psbt; diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 2f0b6ea..63cc318 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1,13 +1,174 @@ import { Psbt as PsbtBase } from 'bip174'; import { PsbtInput } from 'bip174/src/lib/interfaces'; import { checkForInput } from 'bip174/src/lib/utils'; -import * as classify from './classify'; +// import { hash160 } from './crypto'; // TODO: used in pubkey check import { Signer } from './ecpair'; import { Network } from './networks'; import * as payments from './payments'; import * as bscript from './script'; import { Transaction } from './transaction'; +export class Psbt extends PsbtBase { + constructor(public network?: Network) { + super(); + } + + canFinalize(inputIndex: number): boolean { + const input = checkForInput(this.inputs, inputIndex); + const script = getScriptFromInput( + inputIndex, + input, + this.globalMap.unsignedTx!, + ); + if (!script) return false; + const scriptType = classifyScript(script); + // TODO: for each type + switch (scriptType) { + case 'pubkey': + return false; + case 'pubkeyhash': + return false; + case 'multisig': + return false; + case 'witnesspubkeyhash': + return false; + default: + return false; + } + } + + signInput(inputIndex: number, keyPair: Signer): Psbt { + const input = this.inputs[inputIndex]; + if (input === undefined) throw new Error(`No input #${inputIndex}`); + const { + hash, + sighashType, + // script, // TODO: use for pubkey check below + } = getHashForSig(inputIndex, input, this.globalMap.unsignedTx!); + + const pubkey = keyPair.publicKey; + // // TODO: throw error when the pubkey or pubkey hash is not found anywhere + // // in the script + // const pubkeyHash = hash160(keyPair.publicKey); + // + // const decompiled = bscript.decompile(script); + // if (decompiled === null) throw new Error('Unknown script error'); + // + // const hasKey = decompiled.some(element => { + // if (typeof element === 'number') return false; + // return element.equals(pubkey) || element.equals(pubkeyHash); + // }); + // + // if (!hasKey) { + // throw new Error( + // `Can not sign for this input with the key ${pubkey.toString('hex')}`, + // ); + // } + + const partialSig = { + pubkey, + signature: bscript.signature.encode(keyPair.sign(hash), sighashType), + }; + + return this.addPartialSigToInput(inputIndex, partialSig); + } +} + +interface HashForSigData { + script: Buffer; + hash: Buffer; + sighashType: number; +} + +const getHashForSig = ( + inputIndex: number, + input: PsbtInput, + txBuf: Buffer, +): HashForSigData => { + const unsignedTx = Transaction.fromBuffer(txBuf); + const sighashType = input.sighashType || Transaction.SIGHASH_ALL; + let hash: Buffer; + let script: Buffer; + + if (input.nonWitnessUtxo) { + const nonWitnessUtxoTx = Transaction.fromBuffer(input.nonWitnessUtxo); + + const prevoutHash = unsignedTx.ins[inputIndex].hash; + const utxoHash = nonWitnessUtxoTx.getHash(); + + // If a non-witness UTXO is provided, its hash must match the hash specified in the prevout + if (!prevoutHash.equals(utxoHash)) { + throw new Error( + `Non-witness UTXO hash for input #${inputIndex} doesn't match the hash specified in the prevout`, + ); + } + + const prevoutIndex = unsignedTx.ins[inputIndex].index; + const prevout = nonWitnessUtxoTx.outs[prevoutIndex]; + + if (input.redeemScript) { + // If a redeemScript is provided, the scriptPubKey must be for that redeemScript + checkRedeemScript(inputIndex, prevout.script, input.redeemScript); + script = input.redeemScript; + hash = unsignedTx.hashForSignature( + inputIndex, + input.redeemScript, + sighashType, + ); + } else { + script = prevout.script; + hash = unsignedTx.hashForSignature( + inputIndex, + prevout.script, + sighashType, + ); + } + } else if (input.witnessUtxo) { + let _script: Buffer; // so we don't shadow the `let script` above + if (input.redeemScript) { + // If a redeemScript is provided, the scriptPubKey must be for that redeemScript + checkRedeemScript( + inputIndex, + input.witnessUtxo.script, + input.redeemScript, + ); + _script = input.redeemScript; + } else { + _script = input.witnessUtxo.script; + } + if (isP2WPKH(_script)) { + // P2WPKH uses the P2PKH template for prevoutScript when signing + const signingScript = payments.p2pkh({ hash: _script.slice(2) }).output!; + hash = unsignedTx.hashForWitnessV0( + inputIndex, + signingScript, + input.witnessUtxo.value, + sighashType, + ); + script = _script; + } else { + if (!input.witnessScript) + throw new Error('Segwit input needs witnessScript if not P2WPKH'); + checkWitnessScript(inputIndex, _script, input.witnessScript); + hash = unsignedTx.hashForWitnessV0( + inputIndex, + _script, + input.witnessUtxo.value, + sighashType, + ); + // want to make sure the script we return is the actual meaningful script + script = input.witnessScript; + } + } else { + throw new Error('Need a Utxo input item for signing'); + } + return { + script, + sighashType, + hash, + }; +}; + type ScriptCheckerFunction = (idx: number, spk: Buffer, rs: Buffer) => void; const scriptCheckerFactory = ( @@ -35,7 +196,11 @@ const checkWitnessScript = scriptCheckerFactory( 'Witness script', ); -const isPayment = (script: Buffer, payment: any): boolean => { +type isPaymentFunction = (script: Buffer) => boolean; + +const isPaymentFactory = (payment: any): isPaymentFunction => ( + script: Buffer, +): boolean => { try { payment({ output: script }); return true; @@ -43,6 +208,18 @@ const isPayment = (script: Buffer, payment: any): boolean => { return false; } }; +const isP2WPKH = isPaymentFactory(payments.p2wpkh); +const isP2PKH = isPaymentFactory(payments.p2pkh); +const isP2MS = isPaymentFactory(payments.p2ms); +const isP2PK = isPaymentFactory(payments.p2pk); + +const classifyScript = (script: Buffer): string => { + if (isP2WPKH(script)) return 'witnesspubkeyhash'; + if (isP2PKH(script)) return 'pubkeyhash'; + if (isP2MS(script)) return 'multisig'; + if (isP2PK(script)) return 'pubkey'; + return 'nonstandard'; +}; function getScriptFromInput( inputIndex: number, @@ -73,140 +250,3 @@ function getScriptFromInput( } return script; } - -export class Psbt extends PsbtBase { - constructor(public network?: Network) { - super(); - } - - canFinalize(inputIndex: number): boolean { - const input = checkForInput(this.inputs, inputIndex); - const script = getScriptFromInput( - inputIndex, - input, - this.globalMap.unsignedTx!, - ); - if (!script) return false; - const scriptType = classify.output(script); - switch (scriptType) { - case 'pubkey': - return false; - case 'pubkeyhash': - return false; - case 'multisig': - return false; - case 'witnesspubkeyhash': - return false; - default: - return false; - } - } - - signInput(inputIndex: number, keyPair: Signer): Psbt { - // TODO: Implement BIP174 pre-sign checks: - // https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#signer - // - // if non_witness_utxo.exists: - // assert(sha256d(non_witness_utxo) == psbt.tx.innput[i].prevout.hash) - // if redeemScript.exists: - // assert(non_witness_utxo.vout[psbt.tx.input[i].prevout.n].scriptPubKey == P2SH(redeemScript)) - // sign_non_witness(redeemScript) - // else: - // sign_non_witness(non_witness_utxo.vout[psbt.tx.input[i].prevout.n].scriptPubKey) - // else if witness_utxo.exists: - // if redeemScript.exists: - // assert(witness_utxo.scriptPubKey == P2SH(redeemScript)) - // script = redeemScript - // else: - // script = witness_utxo.scriptPubKey - // if IsP2WPKH(script): - // sign_witness(P2PKH(script[2:22])) - // else if IsP2WSH(script): - // assert(script == P2WSH(witnessScript)) - // sign_witness(witnessScript) - // else: - // assert False - - const input = this.inputs[inputIndex]; - if (input === undefined) throw new Error(`No input #${inputIndex}`); - - const unsignedTx = Transaction.fromBuffer(this.globalMap.unsignedTx!); - const sighashType = input.sighashType || 0x01; - let hash: Buffer; - - if (input.nonWitnessUtxo) { - const nonWitnessUtxoTx = Transaction.fromBuffer(input.nonWitnessUtxo); - - const prevoutHash = unsignedTx.ins[inputIndex].hash; - const utxoHash = nonWitnessUtxoTx.getHash(); - - // If a non-witness UTXO is provided, its hash must match the hash specified in the prevout - if (!prevoutHash.equals(utxoHash)) { - throw new Error( - `Non-witness UTXO hash for input #${inputIndex} doesn't match the hash specified in the prevout`, - ); - } - - const prevoutIndex = unsignedTx.ins[inputIndex].index; - const prevout = nonWitnessUtxoTx.outs[prevoutIndex]; - - if (input.redeemScript) { - // If a redeemScript is provided, the scriptPubKey must be for that redeemScript - checkRedeemScript(inputIndex, prevout.script, input.redeemScript); - hash = unsignedTx.hashForSignature( - inputIndex, - input.redeemScript, - sighashType, - ); - } else { - hash = unsignedTx.hashForSignature( - inputIndex, - prevout.script, - sighashType, - ); - } - } else if (input.witnessUtxo) { - let script: Buffer; - if (input.redeemScript) { - // If a redeemScript is provided, the scriptPubKey must be for that redeemScript - checkRedeemScript( - inputIndex, - input.witnessUtxo.script, - input.redeemScript, - ); - script = input.redeemScript; - } else { - script = input.witnessUtxo.script; - } - if (isPayment(script, payments.p2wpkh)) { - // P2WPKH uses the P2PKH template for prevoutScript when signing - const signingScript = payments.p2pkh({ hash: script.slice(2) }).output!; - hash = unsignedTx.hashForWitnessV0( - inputIndex, - signingScript, - input.witnessUtxo.value, - sighashType, - ); - } else { - if (!input.witnessScript) - throw new Error('Segwit input needs witnessScript if not P2WPKH'); - checkWitnessScript(inputIndex, script, input.witnessScript); - hash = unsignedTx.hashForWitnessV0( - inputIndex, - script, - input.witnessUtxo.value, - sighashType, - ); - } - } else { - throw new Error('Need a Utxo input item for signing'); - } - - const partialSig = { - pubkey: keyPair.publicKey, - signature: bscript.signature.encode(keyPair.sign(hash), sighashType), - }; - - return this.addPartialSigToInput(inputIndex, partialSig); - } -}