From 400be7114b7d2f88a1285799bee308f3182c6b49 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Wed, 27 Jun 2018 17:29:18 +1000 Subject: [PATCH] use payments in TxBuilder --- src/payments/p2ms.js | 1 + src/payments/p2pk.js | 1 + src/transaction_builder.js | 781 ++++++++++++------------- test/fixtures/transaction_builder.json | 22 +- test/integration/transactions.js | 20 +- test/transaction_builder.js | 8 +- 6 files changed, 389 insertions(+), 444 deletions(-) diff --git a/src/payments/p2ms.js b/src/payments/p2ms.js index 4fba429..a17d422 100644 --- a/src/payments/p2ms.js +++ b/src/payments/p2ms.js @@ -19,6 +19,7 @@ function stacksEqual (a, b) { // output: m [pubKeys ...] n OP_CHECKMULTISIG function p2ms (a, opts) { if ( + !a.input && !a.output && !(a.pubkeys && a.m !== undefined) && !a.signatures diff --git a/src/payments/p2pk.js b/src/payments/p2pk.js index eacccd6..9cddc81 100644 --- a/src/payments/p2pk.js +++ b/src/payments/p2pk.js @@ -10,6 +10,7 @@ let BITCOIN_NETWORK = require('../networks').bitcoin // output: {pubKey} OP_CHECKSIG function p2pk (a, opts) { if ( + !a.input && !a.output && !a.pubkey && !a.input && diff --git a/src/transaction_builder.js b/src/transaction_builder.js index 2e11fc5..0f6b1ce 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -5,188 +5,133 @@ const bscript = require('./script') const btemplates = require('./templates') const networks = require('./networks') const ops = require('bitcoin-ops') +const payments = require('./payments') +const SCRIPT_TYPES = btemplates.types const typeforce = require('typeforce') const types = require('./types') -const scriptTypes = btemplates.types -const SIGNABLE = [btemplates.types.P2PKH, btemplates.types.P2PK, btemplates.types.MULTISIG] -const P2SH = SIGNABLE.concat([btemplates.types.P2WPKH, btemplates.types.P2WSH]) const ECPair = require('./ecpair') const Transaction = require('./transaction') -function supportedType (type) { - return [ - btemplates.types.P2PKH, - btemplates.types.P2PK, - btemplates.types.MULTISIG - ].indexOf(type) !== -1 -} +function expandInput (scriptSig, witnessStack, type, scriptPubKey) { + if (scriptSig.length === 0 && witnessStack.length === 0) return {} + if (!type) { + let ssType = btemplates.classifyInput(scriptSig, true) + let wsType = btemplates.classifyWitness(witnessStack, true) + if (ssType === SCRIPT_TYPES.NONSTANDARD) ssType = undefined + if (wsType === SCRIPT_TYPES.NONSTANDARD) wsType = undefined + type = ssType || wsType + } -function supportedP2SHType (type) { - return supportedType(type) || [ - btemplates.types.P2WPKH, - btemplates.types.P2WSH - ].indexOf(type) !== -1 -} - -function extractChunks (type, chunks, script) { - let pubKeys = [] - let signatures = [] switch (type) { - case scriptTypes.P2PKH: - // if (redeemScript) throw new Error('Nonstandard... P2SH(P2PKH)') - pubKeys = chunks.slice(1) - signatures = chunks.slice(0, 1) - break + case SCRIPT_TYPES.P2WPKH: { + const { output, pubkey, signature } = payments.p2wpkh({ witness: witnessStack }) - case scriptTypes.P2PK: - pubKeys[0] = script ? btemplates.pubKey.output.decode(script) : undefined - signatures = chunks.slice(0, 1) - break - - case scriptTypes.MULTISIG: - if (script) { - const multisig = btemplates.multisig.output.decode(script) - pubKeys = multisig.pubKeys + return { + prevOutScript: output, + prevOutType: SCRIPT_TYPES.P2WPKH, + pubkeys: [pubkey], + signatures: [signature] } + } - signatures = chunks.slice(1).map(function (chunk) { - return chunk.length === 0 ? undefined : chunk - }) - break + case SCRIPT_TYPES.P2PKH: { + const { output, pubkey, signature } = payments.p2pkh({ input: scriptSig }) + + return { + prevOutScript: output, + prevOutType: SCRIPT_TYPES.P2PKH, + pubkeys: [pubkey], + signatures: [signature] + } + } + + case SCRIPT_TYPES.P2PK: { + const { signature } = payments.p2pk({ input: scriptSig }) + + return { + prevOutType: SCRIPT_TYPES.P2PK, + pubkeys: [undefined], + signatures: [signature] + } + } + + case SCRIPT_TYPES.MULTISIG: { + const { pubkeys, signatures } = payments.p2ms({ + input: scriptSig, + output: scriptPubKey + }, { allowIncomplete: true }) + + return { + prevOutType: SCRIPT_TYPES.MULTISIG, + pubkeys: pubkeys, + signatures: signatures + } + } + } + + if (type === SCRIPT_TYPES.P2SH) { + const { output, redeem } = payments.p2sh({ + input: scriptSig, + witness: witnessStack + }) + + const outputType = btemplates.classifyOutput(redeem.output) + const expanded = expandInput(redeem.input, redeem.witness, outputType, redeem.output) + if (!expanded.prevOutType) return {} + + return { + prevOutScript: output, + prevOutType: SCRIPT_TYPES.P2SH, + redeemScript: redeem.output, + redeemScriptType: expanded.prevOutType, + witnessScript: expanded.witnessScript, + witnessScriptType: expanded.witnessScriptType, + + pubkeys: expanded.pubkeys, + signatures: expanded.signatures + } + } + + if (type === SCRIPT_TYPES.P2WSH) { + const { output, redeem } = payments.p2wsh({ + input: scriptSig, + witness: witnessStack + }) + const outputType = btemplates.classifyOutput(redeem.output) + let expanded + if (outputType === SCRIPT_TYPES.P2WPKH) { + expanded = expandInput(redeem.input, redeem.witness, outputType) + } else { + expanded = expandInput(bscript.compile(redeem.witness), [], outputType, redeem.output) + } + if (!expanded.prevOutType) return {} + + return { + prevOutScript: output, + prevOutType: SCRIPT_TYPES.P2WSH, + witnessScript: redeem.output, + witnessScriptType: expanded.prevOutType, + + pubkeys: expanded.pubkeys, + signatures: expanded.signatures + } } return { - pubKeys: pubKeys, - signatures: signatures + prevOutType: SCRIPT_TYPES.NONSTANDARD, + prevOutScript: scriptSig } } -function expandInput (scriptSig, witnessStack) { - if (scriptSig.length === 0 && witnessStack.length === 0) return {} - - let prevOutScript - let prevOutType - let scriptType - let script - let redeemScript - let witnessScript - let witnessScriptType - let redeemScriptType - let witness = false - let p2wsh = false - let p2sh = false - let witnessProgram - let chunks - - const scriptSigChunks = bscript.decompile(scriptSig) || [] - const sigType = btemplates.classifyInput(scriptSigChunks, true) - if (sigType === scriptTypes.P2SH) { - p2sh = true - redeemScript = scriptSigChunks[scriptSigChunks.length - 1] - redeemScriptType = btemplates.classifyOutput(redeemScript) - prevOutScript = btemplates.scriptHash.output.encode(bcrypto.hash160(redeemScript)) - prevOutType = scriptTypes.P2SH - script = redeemScript - } - - const classifyWitness = btemplates.classifyWitness(witnessStack, true) - if (classifyWitness === scriptTypes.P2WSH) { - witnessScript = witnessStack[witnessStack.length - 1] - witnessScriptType = btemplates.classifyOutput(witnessScript) - p2wsh = true - witness = true - if (scriptSig.length === 0) { - prevOutScript = btemplates.witnessScriptHash.output.encode(bcrypto.sha256(witnessScript)) - prevOutType = scriptTypes.P2WSH - if (redeemScript !== undefined) { - throw new Error('Redeem script given when unnecessary') - } - // bare witness - } else { - if (!redeemScript) { - throw new Error('No redeemScript provided for P2WSH, but scriptSig non-empty') - } - witnessProgram = btemplates.witnessScriptHash.output.encode(bcrypto.sha256(witnessScript)) - if (!redeemScript.equals(witnessProgram)) { - throw new Error('Redeem script didn\'t match witnessScript') - } - } - - if (!supportedType(btemplates.classifyOutput(witnessScript))) { - throw new Error('unsupported witness script') - } - - script = witnessScript - scriptType = witnessScriptType - chunks = witnessStack.slice(0, -1) - } else if (classifyWitness === scriptTypes.P2WPKH) { - witness = true - const key = witnessStack[witnessStack.length - 1] - const keyHash = bcrypto.hash160(key) - if (scriptSig.length === 0) { - prevOutScript = btemplates.witnessPubKeyHash.output.encode(keyHash) - prevOutType = scriptTypes.P2WPKH - if (typeof redeemScript !== 'undefined') { - throw new Error('Redeem script given when unnecessary') - } - } else { - if (!redeemScript) { - throw new Error('No redeemScript provided for P2WPKH, but scriptSig wasn\'t empty') - } - witnessProgram = btemplates.witnessPubKeyHash.output.encode(keyHash) - if (!redeemScript.equals(witnessProgram)) { - throw new Error('Redeem script did not have the right witness program') - } - } - - scriptType = scriptTypes.P2PKH - chunks = witnessStack - } else if (redeemScript) { - if (!supportedP2SHType(redeemScriptType)) { - throw new Error('Bad redeemscript!') - } - - script = redeemScript - scriptType = redeemScriptType - chunks = scriptSigChunks.slice(0, -1) - } else { - prevOutType = scriptType = btemplates.classifyInput(scriptSig) - chunks = scriptSigChunks - } - - const expanded = extractChunks(scriptType, chunks, script) - - const result = { - pubKeys: expanded.pubKeys, - signatures: expanded.signatures, - prevOutScript: prevOutScript, - prevOutType: prevOutType, - signType: scriptType, - signScript: script, - witness: Boolean(witness) - } - - if (p2sh) { - result.redeemScript = redeemScript - result.redeemScriptType = redeemScriptType - } - - if (p2wsh) { - result.witnessScript = witnessScript - result.witnessScriptType = witnessScriptType - } - - return result -} // could be done in expandInput, but requires the original Transaction for hashForSignature function fixMultisigOrder (input, transaction, vin) { - if (input.redeemScriptType !== scriptTypes.MULTISIG || !input.redeemScript) return - if (input.pubKeys.length === input.signatures.length) return + if (input.redeemScriptType !== SCRIPT_TYPES.MULTISIG || !input.redeemScript) return + if (input.pubkeys.length === input.signatures.length) return const unmatched = input.signatures.concat() - input.signatures = input.pubKeys.map(function (pubKey) { + input.signatures = input.pubkeys.map(function (pubKey) { const keyPair = ECPair.fromPublicKey(pubKey) let match @@ -213,265 +158,262 @@ function fixMultisigOrder (input, transaction, vin) { }) } -function expandOutput (script, scriptType, ourPubKey) { +function expandOutput (script, ourPubKey) { typeforce(types.Buffer, script) + const type = btemplates.classifyOutput(script) - const scriptChunks = bscript.decompile(script) || [] - if (!scriptType) { - scriptType = btemplates.classifyOutput(script) - } + switch (type) { + case SCRIPT_TYPES.P2PKH: { + if (!ourPubKey) return { type } - let pubKeys = [] - - switch (scriptType) { - // does our hash160(pubKey) match the output scripts? - case scriptTypes.P2PKH: - if (!ourPubKey) break - - const pkh1 = scriptChunks[2] + // does our hash160(pubKey) match the output scripts? + const pkh1 = payments.p2pkh({ output: script }).hash const pkh2 = bcrypto.hash160(ourPubKey) - if (pkh1.equals(pkh2)) pubKeys = [ourPubKey] - break + if (!pkh1.equals(pkh2)) return { type } - // does our hash160(pubKey) match the output scripts? - case scriptTypes.P2WPKH: - if (!ourPubKey) break + return { + type, + pubkeys: [ourPubKey], + signatures: [undefined] + } + } - const wpkh1 = scriptChunks[1] + case SCRIPT_TYPES.P2WPKH: { + if (!ourPubKey) return { type } + + // does our hash160(pubKey) match the output scripts? + const wpkh1 = payments.p2wpkh({ output: script }).hash const wpkh2 = bcrypto.hash160(ourPubKey) - if (wpkh1.equals(wpkh2)) pubKeys = [ourPubKey] - break + if (!wpkh1.equals(wpkh2)) return { type } - case scriptTypes.P2PK: - pubKeys = scriptChunks.slice(0, 1) - break + return { + type, + pubkeys: [ourPubKey], + signatures: [undefined] + } + } - case scriptTypes.MULTISIG: - pubKeys = scriptChunks.slice(1, -2) - break + case SCRIPT_TYPES.P2PK: { + const p2pk = payments.p2pk({ output: script }) + return { + type, + pubkeys: [p2pk.pubkey], + signatures: [undefined] + } + } - default: return { scriptType: scriptType } + case SCRIPT_TYPES.MULTISIG: { + const p2ms = payments.p2ms({ output: script }) + return { + type, + pubkeys: p2ms.pubkeys, + signatures: p2ms.pubkeys.map(() => undefined) + } + } } - return { - pubKeys: pubKeys, - scriptType: scriptType, - signatures: pubKeys.map(function () { return undefined }) - } + return { type } } -function checkP2SHInput (input, redeemScriptHash) { - if (input.prevOutType) { - if (input.prevOutType !== scriptTypes.P2SH) throw new Error('PrevOutScript must be P2SH') - - const chunks = bscript.decompile(input.prevOutScript) - if (!chunks) throw new Error('Invalid prevOutScript') - if (!chunks[1].equals(redeemScriptHash)) throw new Error('Inconsistent hash160(redeemScript)') - } -} - -function checkP2WSHInput (input, witnessScriptHash) { - if (input.prevOutType) { - if (input.prevOutType !== scriptTypes.P2WSH) throw new Error('PrevOutScript must be P2WSH') - - const chunks = bscript.decompile(input.prevOutScript) - if (!chunks) throw new Error('Invalid witnessScript') - if (!chunks[1].equals(witnessScriptHash)) throw new Error('Inconsistent sha256(witnessScript)') - } -} - -function prepareInput (input, kpPubKey, redeemScript, witnessValue, witnessScript) { - let expanded - let prevOutType - let prevOutScript - - let p2sh = false - let p2shType - let redeemScriptHash - - let witness = false - let p2wsh = false - let witnessType - let witnessScriptHash - - let signType - let signScript - +function prepareInput (input, ourPubKey, redeemScript, witnessValue, witnessScript) { if (redeemScript && witnessScript) { - redeemScriptHash = bcrypto.hash160(redeemScript) - witnessScriptHash = bcrypto.sha256(witnessScript) - checkP2SHInput(input, redeemScriptHash) + const p2wsh = payments.p2wsh({ redeem: { output: witnessScript } }) + const p2wshAlt = payments.p2wsh({ output: redeemScript }) + const p2sh = payments.p2sh({ redeem: { output: redeemScript } }) + const p2shAlt = payments.p2sh({ redeem: p2wsh }) - if (!redeemScript.equals(btemplates.witnessScriptHash.output.encode(witnessScriptHash))) throw new Error('Witness script inconsistent with redeem script') + // enforces P2SH(P2WSH(...)) + if (!p2wsh.hash.equals(p2wshAlt.hash)) throw new Error('Witness script inconsistent with prevOutScript') + if (!p2sh.hash.equals(p2shAlt.hash)) throw new Error('Redeem script inconsistent with prevOutScript') - expanded = expandOutput(witnessScript, undefined, kpPubKey) - if (!expanded.pubKeys) throw new Error(expanded.scriptType + ' not supported as witnessScript (' + bscript.toASM(witnessScript) + ')') - - prevOutType = btemplates.types.P2SH - prevOutScript = btemplates.scriptHash.output.encode(redeemScriptHash) - p2sh = witness = p2wsh = true - p2shType = btemplates.types.P2WSH - signType = witnessType = expanded.scriptType - signScript = witnessScript - } else if (redeemScript) { - redeemScriptHash = bcrypto.hash160(redeemScript) - checkP2SHInput(input, redeemScriptHash) - - expanded = expandOutput(redeemScript, undefined, kpPubKey) - if (!expanded.pubKeys) throw new Error(expanded.scriptType + ' not supported as redeemScript (' + bscript.toASM(redeemScript) + ')') - - prevOutType = btemplates.types.P2SH - prevOutScript = btemplates.scriptHash.output.encode(redeemScriptHash) - p2sh = true - signType = p2shType = expanded.scriptType - signScript = redeemScript - witness = signType === btemplates.types.P2WPKH - } else if (witnessScript) { - witnessScriptHash = bcrypto.sha256(witnessScript) - checkP2WSHInput(input, witnessScriptHash) - - expanded = expandOutput(witnessScript, undefined, kpPubKey) - if (!expanded.pubKeys) throw new Error(expanded.scriptType + ' not supported as witnessScript (' + bscript.toASM(witnessScript) + ')') - - prevOutType = btemplates.types.P2WSH - prevOutScript = btemplates.witnessScriptHash.output.encode(witnessScriptHash) - witness = p2wsh = true - signType = witnessType = expanded.scriptType - signScript = witnessScript - } else if (input.prevOutType) { - // embedded scripts are not possible without a redeemScript - if (input.prevOutType === scriptTypes.P2SH) { - throw new Error('PrevOutScript is ' + input.prevOutType + ', requires redeemScript') + const expanded = expandOutput(p2wsh.redeem.output, ourPubKey) + if (!expanded.pubkeys) throw new Error(expanded.type + ' not supported as witnessScript (' + bscript.toASM(witnessScript) + ')') + if (input.signatures && input.signatures.some(x => x)) { + expanded.signatures = input.signatures } - if (input.prevOutType === scriptTypes.P2WSH) { - throw new Error('PrevOutScript is ' + input.prevOutType + ', requires witnessScript') - } + return { + redeemScript: redeemScript, + redeemScriptType: SCRIPT_TYPES.P2WSH, - prevOutType = input.prevOutType - prevOutScript = input.prevOutScript - expanded = expandOutput(input.prevOutScript, input.prevOutType, kpPubKey) - if (!expanded.pubKeys) return + witnessScript: witnessScript, + witnessScriptType: expanded.type, - witness = (input.prevOutType === scriptTypes.P2WPKH) - signType = prevOutType - signScript = prevOutScript - } else { - prevOutScript = btemplates.pubKeyHash.output.encode(bcrypto.hash160(kpPubKey)) - expanded = expandOutput(prevOutScript, scriptTypes.P2PKH, kpPubKey) + prevOutType: SCRIPT_TYPES.P2SH, + prevOutScript: p2sh.output, - prevOutType = scriptTypes.P2PKH - witness = false - signType = prevOutType - signScript = prevOutScript - } + hasWitness: true, + signScript: witnessScript, + signType: expanded.type, - if (signType === scriptTypes.P2WPKH) { - signScript = btemplates.pubKeyHash.output.encode(btemplates.witnessPubKeyHash.output.decode(signScript)) - } - - if (p2sh) { - input.redeemScript = redeemScript - input.redeemScriptType = p2shType - } - - if (p2wsh) { - input.witnessScript = witnessScript - input.witnessScriptType = witnessType - } - - input.pubKeys = expanded.pubKeys - input.signatures = expanded.signatures - input.signScript = signScript - input.signType = signType - input.prevOutScript = prevOutScript - input.prevOutType = prevOutType - input.witness = witness -} - -function buildStack (type, signatures, pubKeys, allowIncomplete) { - if (type === scriptTypes.P2PKH) { - if (signatures.length === 1 && Buffer.isBuffer(signatures[0]) && pubKeys.length === 1) return btemplates.pubKeyHash.input.encodeStack(signatures[0], pubKeys[0]) - } else if (type === scriptTypes.P2PK) { - if (signatures.length === 1 && Buffer.isBuffer(signatures[0])) return btemplates.pubKey.input.encodeStack(signatures[0]) - } else if (type === scriptTypes.MULTISIG) { - if (signatures.length > 0) { - signatures = signatures.map(function (signature) { - return signature || ops.OP_0 - }) - if (!allowIncomplete) { - // remove blank signatures - signatures = signatures.filter(function (x) { return x !== ops.OP_0 }) - } - - return btemplates.multisig.input.encodeStack(signatures) - } - } else { - throw new Error('Not yet supported') - } - - if (!allowIncomplete) throw new Error('Not enough signatures provided') - return [] -} - -function buildInput (input, allowIncomplete) { - let scriptType = input.prevOutType - let sig = [] - let witness = [] - - if (supportedType(scriptType)) { - sig = buildStack(scriptType, input.signatures, input.pubKeys, allowIncomplete) - } - - let p2sh = false - if (scriptType === btemplates.types.P2SH) { - // We can remove this error later when we have a guarantee prepareInput - // rejects unsignable scripts - it MUST be signable at this point. - if (!allowIncomplete && !supportedP2SHType(input.redeemScriptType)) { - throw new Error('Impossible to sign this type') - } - - if (supportedType(input.redeemScriptType)) { - sig = buildStack(input.redeemScriptType, input.signatures, input.pubKeys, allowIncomplete) - } - - // If it wasn't SIGNABLE, it's witness, defer to that - if (input.redeemScriptType) { - p2sh = true - scriptType = input.redeemScriptType + pubkeys: expanded.pubkeys, + signatures: expanded.signatures } } - switch (scriptType) { - // P2WPKH is a special case of P2PKH - case btemplates.types.P2WPKH: - witness = buildStack(btemplates.types.P2PKH, input.signatures, input.pubKeys, allowIncomplete) - break + if (redeemScript) { + const p2sh = payments.p2sh({ redeem: { output: redeemScript } }) - case btemplates.types.P2WSH: - // We can remove this check later - if (!allowIncomplete && !supportedType(input.witnessScriptType)) { - throw new Error('Impossible to sign this type') - } + if (input.prevOutScript) { + let p2shAlt + try { + p2shAlt = payments.p2sh({ output: input.prevOutScript }) + } catch (e) { throw new Error('PrevOutScript must be P2SH') } + if (!p2sh.hash.equals(p2shAlt.hash)) throw new Error('Redeem script inconsistent with prevOutScript') + } - if (supportedType(input.witnessScriptType)) { - witness = buildStack(input.witnessScriptType, input.signatures, input.pubKeys, allowIncomplete) - witness.push(input.witnessScript) - scriptType = input.witnessScriptType - } + const expanded = expandOutput(p2sh.redeem.output, ourPubKey) + if (!expanded.pubkeys) throw new Error(expanded.type + ' not supported as redeemScript (' + bscript.toASM(redeemScript) + ')') + if (input.signatures && input.signatures.some(x => x)) { + expanded.signatures = input.signatures + } - break + let signScript = redeemScript + if (expanded.type === SCRIPT_TYPES.P2WPKH) { + signScript = payments.p2pkh({ pubkey: expanded.pubkeys[0] }).output + } + + return { + redeemScript: redeemScript, + redeemScriptType: expanded.type, + + prevOutType: SCRIPT_TYPES.P2SH, + prevOutScript: p2sh.output, + + hasWitness: expanded.type === SCRIPT_TYPES.P2WPKH, + signScript: signScript, + signType: expanded.type, + + pubkeys: expanded.pubkeys, + signatures: expanded.signatures + } } - // append redeemScript if necessary - if (p2sh) { - sig.push(input.redeemScript) + if (witnessScript) { + const p2wsh = payments.p2wsh({ redeem: { output: witnessScript } }) + + if (input.prevOutScript) { + const p2wshAlt = payments.p2wsh({ output: input.prevOutScript }) + if (!p2wsh.hash.equals(p2wshAlt.hash)) throw new Error('Witness script inconsistent with prevOutScript') + } + + const expanded = expandOutput(p2wsh.redeem.output, ourPubKey) + if (!expanded.pubkeys) throw new Error(expanded.type + ' not supported as witnessScript (' + bscript.toASM(witnessScript) + ')') + if (input.signatures && input.signatures.some(x => x)) { + expanded.signatures = input.signatures + } + + return { + witnessScript: witnessScript, + witnessScriptType: expanded.type, + + prevOutType: SCRIPT_TYPES.P2WSH, + prevOutScript: p2wsh.output, + + hasWitness: true, + signScript: witnessScript, + signType: expanded.type, + + pubkeys: expanded.pubkeys, + signatures: expanded.signatures + } } + if (input.prevOutType && input.prevOutScript) { + // embedded scripts are not possible without extra information + if (input.prevOutType === SCRIPT_TYPES.P2SH) throw new Error('PrevOutScript is ' + input.prevOutType + ', requires redeemScript') + if (input.prevOutType === SCRIPT_TYPES.P2WSH) throw new Error('PrevOutScript is ' + input.prevOutType + ', requires witnessScript') + if (!input.prevOutScript) throw new Error('PrevOutScript is missing') + + const expanded = expandOutput(input.prevOutScript, ourPubKey) + if (!expanded.pubkeys) throw new Error(expanded.type + ' not supported (' + bscript.toASM(input.prevOutScript) + ')') + if (input.signatures && input.signatures.some(x => x)) { + expanded.signatures = input.signatures + } + + return { + prevOutType: expanded.type, + prevOutScript: input.prevOutScript, + + hasWitness: expanded.type === SCRIPT_TYPES.P2WPKH, + signScript: input.prevOutScript, + signType: expanded.type, + + pubkeys: expanded.pubkeys, + signatures: expanded.signatures + } + } + + const prevOutScript = payments.p2pkh({ pubkey: ourPubKey }).output return { - type: scriptType, - script: bscript.compile(sig), - witness: witness + prevOutType: SCRIPT_TYPES.P2PKH, + prevOutScript: prevOutScript, + + hasWitness: false, + signScript: prevOutScript, + signType: SCRIPT_TYPES.P2PKH, + + pubkeys: [ourPubKey], + signatures: [undefined] + } +} + +function build (type, input, allowIncomplete) { + const pubkeys = input.pubkeys || [] + let signatures = input.signatures || [] + + switch (type) { + case SCRIPT_TYPES.P2PKH: { + if (pubkeys.length === 0) break + if (signatures.length === 0) break + + return payments.p2pkh({ pubkey: pubkeys[0], signature: signatures[0] }) + } + case SCRIPT_TYPES.P2WPKH: { + if (pubkeys.length === 0) break + if (signatures.length === 0) break + + return payments.p2wpkh({ pubkey: pubkeys[0], signature: signatures[0] }) + } + case SCRIPT_TYPES.P2PK: { + if (pubkeys.length === 0) break + if (signatures.length === 0) break + + return payments.p2pk({ signature: signatures[0] }) + } + case SCRIPT_TYPES.MULTISIG: { + if (allowIncomplete) { + signatures = signatures.map(x => x || ops.OP_0) + } else { + signatures = signatures.filter(x => x) + } + + return payments.p2ms({ signatures }, { allowIncomplete }) + } + case SCRIPT_TYPES.P2SH: { + const redeem = build(input.redeemScriptType, input, allowIncomplete) + if (!redeem) return + + return payments.p2sh({ + redeem: { + output: redeem.output || input.redeemScript, + input: redeem.input, + witness: redeem.witness + } + }) + } + case SCRIPT_TYPES.P2WSH: { + const redeem = build(input.witnessScriptType, input, allowIncomplete) + if (!redeem) return + + return payments.p2wsh({ + redeem: { + output: input.witnessScript, + input: redeem.input, + witness: redeem.witness + } + }) + } } } @@ -590,15 +532,14 @@ TransactionBuilder.prototype.__addInputUnsafe = function (txHash, vout, options) if (!input.prevOutScript && options.prevOutScript) { let prevOutType - if (!input.pubKeys && !input.signatures) { + if (!input.pubkeys && !input.signatures) { const expanded = expandOutput(options.prevOutScript) - - if (expanded.pubKeys) { - input.pubKeys = expanded.pubKeys + if (expanded.pubkeys) { + input.pubkeys = expanded.pubkeys input.signatures = expanded.signatures } - prevOutType = expanded.scriptType + prevOutType = expanded.type } input.prevOutScript = options.prevOutScript @@ -638,20 +579,19 @@ TransactionBuilder.prototype.__build = function (allowIncomplete) { } const tx = this.__tx.clone() - // Create script signatures from inputs - this.__inputs.forEach(function (input, i) { - const scriptType = input.witnessScriptType || input.redeemScriptType || input.prevOutType - if (!scriptType && !allowIncomplete) throw new Error('Transaction is not complete') - const result = buildInput(input, allowIncomplete) - // skip if no result - if (!allowIncomplete) { - if (!supportedType(result.type) && result.type !== btemplates.types.P2WPKH) { - throw new Error(result.type + ' not supported') - } + // create script signatures from inputs + this.__inputs.forEach(function (input, i) { + if (!input.prevOutType && !allowIncomplete) throw new Error('Transaction is not complete') + + const result = build(input.prevOutType, input, allowIncomplete) + if (!result) { + if (!allowIncomplete && input.prevOutType === SCRIPT_TYPES.NONSTANDARD) throw new Error('Unknown input type') + if (!allowIncomplete) throw new Error('Not enough information') + return } - tx.setInputScript(i, result.script) + tx.setInputScript(i, result.input) tx.setWitness(i, result.witness) }) @@ -666,15 +606,15 @@ TransactionBuilder.prototype.__build = function (allowIncomplete) { } function canSign (input) { - return input.prevOutScript !== undefined && - input.signScript !== undefined && - input.pubKeys !== undefined && + return input.signScript !== undefined && + input.signType !== undefined && + input.pubkeys !== undefined && input.signatures !== undefined && - input.signatures.length === input.pubKeys.length && - input.pubKeys.length > 0 && + input.signatures.length === input.pubkeys.length && + input.pubkeys.length > 0 && ( - input.witness === false || - (input.witness === true && input.value !== undefined) + input.hasWitness === false || + input.value !== undefined ) } @@ -693,7 +633,7 @@ TransactionBuilder.prototype.sign = function (vin, keyPair, redeemScript, hashTy throw new Error('Inconsistent redeemScript') } - const kpPubKey = keyPair.publicKey || keyPair.getPublicKey() + const ourPubKey = keyPair.publicKey || keyPair.getPublicKey() if (!canSign(input)) { if (witnessValue !== undefined) { if (input.value !== undefined && input.value !== witnessValue) throw new Error('Input didn\'t match witnessValue') @@ -701,28 +641,33 @@ TransactionBuilder.prototype.sign = function (vin, keyPair, redeemScript, hashTy input.value = witnessValue } - if (!canSign(input)) prepareInput(input, kpPubKey, redeemScript, witnessValue, witnessScript) + if (!canSign(input)) { + const prepared = prepareInput(input, ourPubKey, redeemScript, witnessValue, witnessScript) + + // updates inline + Object.assign(input, prepared) + } + if (!canSign(input)) throw Error(input.prevOutType + ' not supported') } // ready to sign let signatureHash - if (input.witness) { + if (input.hasWitness) { signatureHash = this.__tx.hashForWitnessV0(vin, input.signScript, input.value, hashType) } else { signatureHash = this.__tx.hashForSignature(vin, input.signScript, hashType) } // enforce in order signing of public keys - const signed = input.pubKeys.some(function (pubKey, i) { - if (!kpPubKey.equals(pubKey)) return false + const signed = input.pubkeys.some(function (pubKey, i) { + if (!ourPubKey.equals(pubKey)) return false if (input.signatures[i]) throw new Error('Signature already exists') - if (kpPubKey.length !== 33 && ( - input.signType === scriptTypes.P2WPKH || - input.redeemScriptType === scriptTypes.P2WSH || - input.prevOutType === scriptTypes.P2WSH - )) throw new Error('BIP143 rejects uncompressed public keys in P2WPKH or P2WSH') + // TODO: add tests + if (ourPubKey.length !== 33 && input.hasWitness) { + throw new Error('BIP143 rejects uncompressed public keys in P2WPKH or P2WSH') + } const signature = keyPair.sign(signatureHash) input.signatures[i] = bscript.signature.encode(signature, hashType) diff --git a/test/fixtures/transaction_builder.json b/test/fixtures/transaction_builder.json index 329b5a7..973f641 100644 --- a/test/fixtures/transaction_builder.json +++ b/test/fixtures/transaction_builder.json @@ -395,7 +395,7 @@ }, { "description": "Transaction w/ P2WPKH -> P2WPKH", - "txHex": "01000000000101ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff011027000000000000160014aa4d7985c57e011a8b3dd8e0e5a73aaef41629c502483045022100a8fc5e4c6d7073474eff2af5d756966e75be0cdfbba299518526080ce8b584be02200f26d41082764df89e3c815b8eaf51034a3b68a25f1be51208f54222c1bb6c1601210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800000000", + "txHex": "01000000000101ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff011027000000000000160014aa4d7985c57e011a8b3dd8e0e5a73aaef41629c502483045022100b4a9d46ea4d38d6b3ea098911c9f72c0ae6ebc72408e6be7880a6b22a4b3e4da02207996107d0e6437f80363f96f502a38f275156f7501ea51f67899ba78a0c129c101210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800000000", "version": 1, "inputs": [ { @@ -833,8 +833,8 @@ ] }, { - "description": "Sighash V1: ALL", - "txHex": "01000000000102fff7f7881a8099afa6940d42d1e7f6362bec38171ea3edf433541db4e4ad969f00000000484730440220691a19d365c8d75f921346c70271506bde136f13a4b566dd796902c262e2ec6d02202b00c4aa030eedf294552bdfc163936d2f4e91c59e7798c4471250cf07cb859501eeffffffef51e1b804cc89d182d279655c3aa89e815b1b309fe287d9b2b55d57b90ec68a0100000000ffffffff0230f45e13000000001976a9148280b37df378db99f66f85c95a783a76ac7a6d5988ac00e9a435000000001976a9143bde42dbee7e4dbe6a21b2d50ce2f0167faa815988ac0002483045022100fddd014889f18d489b5400bfa8cb0a32301a768d934b1a0e2b55398119f26cab02207676c64c16ffa7ffaaf8e16b3b74e916687eebdfdb36b9b7997e838384d464640121025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee635711000000", + "description": "SIGHASH V0+V1, (P2PKH, P2WPKH) -> 2x P2PKH", + "txHex": "01000000000102fff7f7881a8099afa6940d42d1e7f6362bec38171ea3edf433541db4e4ad969f00000000484730440220691a19d365c8d75f921346c70271506bde136f13a4b566dd796902c262e2ec6d02202b00c4aa030eedf294552bdfc163936d2f4e91c59e7798c4471250cf07cb859501eeffffffef51e1b804cc89d182d279655c3aa89e815b1b309fe287d9b2b55d57b90ec68a0100000000ffffffff0230f45e13000000001976a9148280b37df378db99f66f85c95a783a76ac7a6d5988ac00e9a435000000001976a9143bde42dbee7e4dbe6a21b2d50ce2f0167faa815988ac000247304402200a7b08cccedf608e279410091acbd7e990e19a8edf401c3698763d2920de5871022060462ed172a02ecef73ebc19811d8fc72ed68f4419742df70241ad0a5a6a36410121025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee635711000000", "version": 1, "inputs": [ { @@ -877,7 +877,7 @@ "locktime": 17 }, { - "description": "Sighash V1: ALL 2", + "description": "SIGHASH V0+V1, P2SH(P2WPKH) -> P2PKH", "txHex": "01000000000101db6b1b20aa0fd7b23880be2ecbd4a98130974cf4748fb66092ac4d3ceb1a5477010000001716001479091972186c449eb1ded22b78e40d009bdf0089feffffff02b8b4eb0b000000001976a914a457b684d7f0d539a46a45bbc043f35b59d0d96388ac0008af2f000000001976a914fd270b1ee6abcaea97fea7ad0402e8bd8ad6d77c88ac02473044022047ac8e878352d3ebbde1c94ce3a10d057c24175747116f8288e5d794d12d482f0220217f36a485cae903c713331d877c1f64677e3622ad4010726870540656fe9dcb012103ad1d8e89212f0b92c74d23bb710c00662ad1470198ac48c43f7d6f93a2a2687392040000", "version": 1, "inputs": [ @@ -1313,8 +1313,8 @@ "locktime": 0 }, { - "description": "P2WPKH", - "txHex": "0100000000010133defbe3e28860007ff3e21222774c220cb35d554fa3e3796d25bf8ee983e1080000000000ffffffff0160ea0000000000001976a914851a33a5ef0d4279bd5854949174e2c65b1d450088ac0248304502210097c3006f0b390982eb47f762b2853773c6cedf83668a22d710f4c13c4fd6b15502205e26ef16a81fc818a37f3a34fc6d0700e61100ea6c6773907c9c046042c440340121038de63cf582d058a399a176825c045672d5ff8ea25b64d28d4375dcdb14c02b2b00000000", + "description": "P2WPKH -> P2PKH", + "txHex": "0100000000010133defbe3e28860007ff3e21222774c220cb35d554fa3e3796d25bf8ee983e1080000000000ffffffff0160ea0000000000001976a914851a33a5ef0d4279bd5854949174e2c65b1d450088ac02483045022100834f56825e880ab7926164458e10582d9fd8df005396b7e51a1efb8db277204e02206a3610b7101c3242643ac9c9d3487c2d28ffdad19ec26a7f81fc100bdac625f10121038de63cf582d058a399a176825c045672d5ff8ea25b64d28d4375dcdb14c02b2b00000000", "version": 1, "inputs": [ { @@ -1340,7 +1340,7 @@ "locktime": 0 }, { - "description": "P2SH(P2WPKH)", + "description": "P2SH(P2WPKH) -> P2PKH", "txHex": "010000000001015df9a0b9ade2d835881704e0f53b51a4b19ecfc794ea1f3555783dd7f68659ce0000000017160014851a33a5ef0d4279bd5854949174e2c65b1d4500ffffffff0160ea0000000000001976a914851a33a5ef0d4279bd5854949174e2c65b1d450088ac02483045022100cb3929c128fec5108071b662e5af58e39ac8708882753a421455ca80462956f6022030c0f4738dd1a13fc7a34393002d25c6e8a6399f29c7db4b98f53a9475d94ca20121038de63cf582d058a399a176825c045672d5ff8ea25b64d28d4375dcdb14c02b2b00000000", "version": 1, "inputs": [ @@ -1872,7 +1872,7 @@ }, { "description": "Incomplete transaction, known prevTxScript, thereby throws for missing signatures", - "exception": "Not enough signatures provided", + "exception": "Not enough information", "inputs": [ { "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", @@ -1925,7 +1925,7 @@ }, { "description": "Complete transaction w/ non-standard inputs", - "exception": "nonstandard not supported", + "exception": "Unknown input type", "txHex": "010000000100000000171a0000e028f2000000000050178500000000000d0000000e00000000000000201ff691b2263260e71f363d1db51ff3100d285956a40cc0e4f8c8c2c4a80559b1ffffffff0110270000000000001976a914aa4d7985c57e011a8b3dd8e0e5a73aaef41629c588ac00000000" } ], @@ -2149,7 +2149,7 @@ }, { "description": "Inconsistent RedeemScript hash", - "exception": "Inconsistent hash160", + "exception": "Redeem script inconsistent with prevOutScript", "inputs": [ { "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", @@ -2174,7 +2174,7 @@ }, { "description": "Inconsistent WitnessScript hash", - "exception": "Inconsistent sha256", + "exception": "Witness script inconsistent with prevOutScript", "inputs": [ { "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", diff --git a/test/integration/transactions.js b/test/integration/transactions.js index 991e0b6..a4a1747 100644 --- a/test/integration/transactions.js +++ b/test/integration/transactions.js @@ -103,7 +103,7 @@ describe('bitcoinjs-lib (transactions)', function () { }) }) - it('can create (and broadcast via 3PBP) a Transaction with a 2-of-4 P2SH(multisig) input', function (done) { + it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2MS(2 of 4)) (multisig) input', function (done) { this.timeout(30000) const keyPairs = [ @@ -112,21 +112,19 @@ describe('bitcoinjs-lib (transactions)', function () { bitcoin.ECPair.makeRandom({ network: regtest }), bitcoin.ECPair.makeRandom({ network: regtest }) ] - const pubKeys = keyPairs.map(function (x) { return x.publicKey }) + const pubkeys = keyPairs.map(x => x.publicKey) + const p2ms = bitcoin.payments.p2ms({ m: 2, pubkeys: pubkeys, network: regtest }) + const p2sh = bitcoin.payments.p2sh({ redeem: p2ms, network: regtest }) - const redeemScript = bitcoin.script.multisig.output.encode(2, pubKeys) - const scriptPubKey = bitcoin.script.scriptHash.output.encode(bitcoin.crypto.hash160(redeemScript)) - const address = bitcoin.address.fromOutputScript(scriptPubKey, regtest) - - regtestUtils.faucet(address, 2e4, function (err, unspent) { + regtestUtils.faucet(p2sh.address, 2e4, function (err, unspent) { if (err) return done(err) const txb = new bitcoin.TransactionBuilder(regtest) txb.addInput(unspent.txId, unspent.vout) txb.addOutput(regtestUtils.RANDOM_ADDRESS, 1e4) - txb.sign(0, keyPairs[0], redeemScript) - txb.sign(0, keyPairs[2], redeemScript) + txb.sign(0, keyPairs[0], p2sh.redeem.output) + txb.sign(0, keyPairs[2], p2sh.redeem.output) const tx = txb.build() // build and broadcast to the Bitcoin RegTest network @@ -143,7 +141,7 @@ describe('bitcoinjs-lib (transactions)', function () { }) }) - it('can create (and broadcast via 3PBP) a Transaction with a SegWit P2SH(P2WPKH) input', function (done) { + it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WPKH) input', function (done) { this.timeout(30000) const keyPair = bitcoin.ECPair.makeRandom({ network: regtest }) @@ -174,7 +172,7 @@ describe('bitcoinjs-lib (transactions)', function () { }) }) - it('can create (and broadcast via 3PBP) a Transaction with a SegWit 3-of-4 P2SH(P2WSH(multisig)) input', function (done) { + it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WSH(P2MS(3 of 4))) (SegWit multisig) input', function (done) { this.timeout(50000) const keyPairs = [ diff --git a/test/transaction_builder.js b/test/transaction_builder.js index e4b5e60..e307398 100644 --- a/test/transaction_builder.js +++ b/test/transaction_builder.js @@ -149,7 +149,6 @@ describe('TransactionBuilder', function () { txb.__inputs.forEach(function (i) { assert.strictEqual(i.prevOutType, 'scripthash') assert.strictEqual(i.redeemScriptType, 'multisig') - assert.strictEqual(i.signType, 'multisig') }) }) @@ -537,14 +536,15 @@ describe('TransactionBuilder', function () { txb.addInput('a4696c4b0cd27ec2e173ab1fa7d1cc639a98ee237cec95a77ca7ff4145791529', 1, 0xffffffff, scriptPubKey) txb.addOutput(scriptPubKey, 99000) txb.sign(0, keyPair, redeemScript, null, 100000, witnessScript) + // 2-of-2 signed only once const tx = txb.buildIncomplete() + // Only input is segwit, so txid should be accurate with the final tx assert.equal(tx.getId(), 'f15d0a65b21b4471405b21a099f8b18e1ae4d46d55efbd0f4766cf11ad6cb821') + const txHex = tx.toHex() - const newTxb = TransactionBuilder.fromTransaction(Transaction.fromHex(txHex)) - // input should have the key 'witness' set to true - assert.equal(newTxb.__inputs[0].witness, true) + TransactionBuilder.fromTransaction(Transaction.fromHex(txHex)) }) it('should handle badly pre-filled OP_0s', function () {