diff --git a/src/transaction_builder.js b/src/transaction_builder.js index 259fa6b..0c02ce0 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -12,154 +12,106 @@ var ECPair = require('./ecpair') var ECSignature = require('./ecsignature') var Transaction = require('./transaction') -// re-orders signatures to match pubKeys, fills undefined otherwise -function fixMSSignatures (transaction, vin, pubKeys, signatures, prevOutScript, hashType, skipPubKey) { - // maintain a local copy of unmatched signatures - var unmatched = signatures.slice() - var signatureHash - - return pubKeys.map(function (pubKey) { - // skip optionally provided pubKey - if (skipPubKey && bufferEquals(skipPubKey, pubKey)) return undefined - - var matched - var keyPair2 = ECPair.fromPublicKeyBuffer(pubKey) - - // check for a matching signature - unmatched.some(function (signature, i) { - // skip if undefined || OP_0 - if (!signature) return false - - if (!signatureHash) { - signatureHash = transaction.hashForSignature(vin, prevOutScript, hashType) - } - if (!keyPair2.verify(signatureHash, signature)) return false - - // remove matched signature from unmatched - unmatched[i] = undefined - matched = signature +// inspects a scriptSig w/ optional provided redeemScript and derives +// all necessary input information as required by TransactionBuilder +function expandInput (scriptSig, redeemScript) { + var scriptSigChunks = bscript.decompile(scriptSig) + var scriptSigType = bscript.classifyInput(scriptSigChunks, true) - return true - }) + var hashType, pubKeys, signatures, prevOutScript - return matched || undefined - }) -} + switch (scriptSigType) { + case 'scripthash': + // FIXME: maybe depth limit instead, how possible is this anyway? + if (redeemScript) throw new Error('Recursive P2SH script') -function extractInput (transaction, txIn, vin) { - if (txIn.script.length === 0) return {} + var redeemScriptSig = scriptSigChunks.slice(0, -1) + redeemScript = scriptSigChunks[scriptSigChunks.length - 1] - var scriptSigChunks = bscript.decompile(txIn.script) - var prevOutType = bscript.classifyInput(scriptSigChunks, true) + var result = expandInput(redeemScriptSig, redeemScript) + result.redeemScript = redeemScript + result.redeemScriptType = result.prevOutType + result.prevOutScript = bscript.scriptHashOutput(bcrypto.hash160(redeemScript)) + result.prevOutType = 'scripthash' + return result - function processScript (scriptType, scriptSigChunks, redeemScriptChunks) { - // ensure chunks are decompiled - scriptSigChunks = bscript.decompile(scriptSigChunks) - redeemScriptChunks = redeemScriptChunks ? bscript.decompile(redeemScriptChunks) : undefined - - var hashType, pubKeys, signatures, prevOutScript, redeemScript, redeemScriptType, result, parsed - - switch (scriptType) { - case 'scripthash': - redeemScript = scriptSigChunks.slice(-1)[0] - scriptSigChunks = bscript.compile(scriptSigChunks.slice(0, -1)) - - redeemScriptType = bscript.classifyInput(scriptSigChunks, true) - prevOutScript = bscript.scriptHashOutput(bcrypto.hash160(redeemScript)) - - result = processScript(redeemScriptType, scriptSigChunks, bscript.decompile(redeemScript)) - - result.prevOutScript = prevOutScript - result.redeemScript = redeemScript - result.redeemScriptType = redeemScriptType - - return result - - case 'pubkeyhash': - parsed = ECSignature.parseScriptSignature(scriptSigChunks[0]) - hashType = parsed.hashType - pubKeys = scriptSigChunks.slice(1) - signatures = [parsed.signature] - prevOutScript = bscript.pubKeyHashOutput(bcrypto.hash160(pubKeys[0])) - - break + case 'pubkeyhash': + // if (redeemScript) throw new Error('Nonstandard... P2SH(P2PKH)') + var s = ECSignature.parseScriptSignature(scriptSigChunks[0]) + hashType = s.hashType + pubKeys = scriptSigChunks.slice(1) + signatures = [s.signature] - case 'pubkey': - parsed = ECSignature.parseScriptSignature(scriptSigChunks[0]) - hashType = parsed.hashType - signatures = [parsed.signature] + if (redeemScript) break - if (redeemScriptChunks) { - pubKeys = redeemScriptChunks.slice(0, 1) - } + prevOutScript = bscript.pubKeyHashOutput(bcrypto.hash160(pubKeys[0])) + break - break + case 'pubkey': + if (redeemScript) { + pubKeys = bscript.decompile(redeemScript).slice(0, 1) + } - case 'multisig': - signatures = scriptSigChunks.slice(1).map(function (chunk) { - if (chunk === ops.OP_0) return undefined + var ss = ECSignature.parseScriptSignature(scriptSigChunks[0]) + hashType = ss.hashType + signatures = [ss.signature] + break - parsed = ECSignature.parseScriptSignature(chunk) - hashType = parsed.hashType + case 'multisig': + if (redeemScript) { + pubKeys = bscript.decompile(redeemScript).slice(1, -2) + } - return parsed.signature - }) + signatures = scriptSigChunks.slice(1).map(function (chunk) { + if (chunk === ops.OP_0) return undefined - if (redeemScriptChunks) { - pubKeys = redeemScriptChunks.slice(1, -2) + var sss = ECSignature.parseScriptSignature(chunk) - if (pubKeys.length !== signatures.length) { - signatures = fixMSSignatures(transaction, vin, pubKeys, signatures, bscript.compile(redeemScriptChunks), hashType, redeemScript) - } + if (hashType !== undefined) { + if (sss.hashType !== hashType) throw new Error('Inconsistent hashType') + } else { + hashType = sss.hashType } - break - } + return sss.signature + }) - return { - hashType: hashType, - pubKeys: pubKeys, - signatures: signatures, - prevOutScript: prevOutScript, - redeemScript: redeemScript, - redeemScriptType: redeemScriptType - } + break } - // Extract hashType, pubKeys, signatures and prevOutScript - var result = processScript(prevOutType, scriptSigChunks) - return { - hashType: result.hashType, - prevOutScript: result.prevOutScript, - prevOutType: prevOutType, - pubKeys: result.pubKeys, - redeemScript: result.redeemScript, - redeemScriptType: result.redeemScriptType, - signatures: result.signatures + hashType: hashType, + pubKeys: pubKeys, + signatures: signatures, + prevOutScript: prevOutScript, + prevOutType: scriptSigType } } -function extractFromOutputScript (outputScript, kpPubKey) { - var scriptType = bscript.classifyOutput(outputScript) - var outputScriptChunks = bscript.decompile(outputScript) +function expandOutput (script, ourPubKey) { + typeforce(types.Buffer, script) + + var scriptChunks = bscript.decompile(script) + var scriptType = bscript.classifyOutput(script) + + var pubKeys = [] - var pubKeys switch (scriptType) { + // does our hash160(pubKey) match the output scripts? case 'pubkeyhash': - var pkh1 = outputScriptChunks[2] - var pkh2 = bcrypto.hash160(kpPubKey) + if (!ourPubKey) break - if (!bufferEquals(pkh1, pkh2)) throw new Error('privateKey cannot sign for this input') - pubKeys = [kpPubKey] + var pkh1 = scriptChunks[2] + var pkh2 = bcrypto.hash160(ourPubKey) + if (bufferEquals(pkh1, pkh2)) pubKeys = [ourPubKey] break case 'pubkey': - pubKeys = outputScriptChunks.slice(0, 1) + pubKeys = scriptChunks.slice(0, 1) break case 'multisig': - pubKeys = outputScriptChunks.slice(1, -2) + pubKeys = scriptChunks.slice(1, -2) break default: return @@ -167,7 +119,8 @@ function extractFromOutputScript (outputScript, kpPubKey) { return { pubKeys: pubKeys, - scriptType: scriptType + scriptType: scriptType, + signatures: pubKeys.map(function () { return undefined }) } } @@ -204,34 +157,47 @@ TransactionBuilder.prototype.setVersion = function (version) { TransactionBuilder.fromTransaction = function (transaction, network) { var txb = new TransactionBuilder(network) - // Copy other transaction fields - txb.tx.version = transaction.version - txb.tx.locktime = transaction.locktime - - // Extract/add inputs - transaction.ins.forEach(function (txIn) { - txb.addInput(txIn.hash, txIn.index, txIn.sequence) - }) + // Copy transaction fields + txb.setVersion(transaction.version) + txb.setLockTime(transaction.locktime) - // Extract/add outputs + // Copy outputs (done first to avoid signature invalidation) transaction.outs.forEach(function (txOut) { txb.addOutput(txOut.script, txOut.value) }) - // Extract/add signatures - txb.inputs = transaction.ins.map(function (txIn, vin) { - // TODO: verify whether extractInput is sane with coinbase scripts - if (Transaction.isCoinbaseHash(txIn.hash)) { - throw new Error('coinbase inputs not supported') - } + // Copy inputs + transaction.ins.forEach(function (txIn) { + txb.__addInputUnsafe(txIn.hash, txIn.index, txIn.sequence, txIn.script) + }) + + // fix some things not possible through the public API + txb.inputs.forEach(function (input, i) { + // attempt to fix any multisig inputs if they exist + if ((input.redeemScriptType || input.prevOutType) === 'multisig') { + // pubKeys will only exist for 'multisig' if a redeemScript was found + if (!input.pubKeys || !input.signatures) return + if (input.pubKeys.length === input.signatures.length) return - return extractInput(transaction, txIn, vin) + txb.__fixMultisigOrder(transaction, i) + } }) return txb } TransactionBuilder.prototype.addInput = function (txHash, vout, sequence, prevOutScript) { + // if signatures exist, adding inputs is only acceptable if SIGHASH_ANYONECANPAY is used + // throw if any signatures *didn't* use SIGHASH_ANYONECANPAY + if (!this.inputs.every(function (otherInput) { + // no signature + if (otherInput.hashType === undefined) return true + + return otherInput.hashType & Transaction.SIGHASH_ANYONECANPAY + })) { + throw new Error('No, this would invalidate signatures') + } + // is it a hex string? if (typeof txHash === 'string') { // transaction hashs's are displayed in reverse order, un-reverse it @@ -243,49 +209,42 @@ TransactionBuilder.prototype.addInput = function (txHash, vout, sequence, prevOu txHash = txHash.getHash() } - var input = {} - if (prevOutScript) { - var prevOutScriptChunks = bscript.decompile(prevOutScript) - var prevOutType = bscript.classifyOutput(prevOutScriptChunks) + return this.__addInputUnsafe(txHash, vout, sequence, null, prevOutScript) +} - // if we can, extract pubKey information - switch (prevOutType) { - case 'multisig': - input.pubKeys = prevOutScriptChunks.slice(1, -2) - input.signatures = input.pubKeys.map(function () { return undefined }) +TransactionBuilder.prototype.__addInputUnsafe = function (txHash, vout, sequence, scriptSig, prevOutScript) { + if (Transaction.isCoinbaseHash(txHash)) { + throw new Error('coinbase inputs not supported') + } - break + var prevTxOut = txHash.toString('hex') + ':' + vout + if (this.prevTxMap[prevTxOut]) throw new Error('Duplicate TxOut: ' + prevTxOut) - case 'pubkey': - input.pubKeys = prevOutScriptChunks.slice(0, 1) - input.signatures = [undefined] + var input = {} - break - } + // derive what we can from the scriptSig + if (scriptSig) { + input = expandInput(scriptSig) + } - if (prevOutType !== 'scripthash') { - input.scriptType = prevOutType + // derive what we can from the previous transactions output script + if (!input.prevOutScript && prevOutScript) { + var prevOutScriptChunks = bscript.decompile(prevOutScript) + var prevOutType = bscript.classifyOutput(prevOutScriptChunks) + + if (!input.pubKeys && !input.signatures) { + var expanded = expandOutput(prevOutScript) + if (expanded) { + input.pubKeys = expanded.pubKeys + input.signatures = expanded.signatures + } } input.prevOutScript = prevOutScript input.prevOutType = prevOutType } - // if signatures exist, adding inputs is only acceptable if SIGHASH_ANYONECANPAY is used - // throw if any signatures *didn't* use SIGHASH_ANYONECANPAY - if (!this.inputs.every(function (otherInput) { - // no signature - if (otherInput.hashType === undefined) return true - - return otherInput.hashType & Transaction.SIGHASH_ANYONECANPAY - })) { - throw new Error('No, this would invalidate signatures') - } - - var prevTxOut = txHash.toString('hex') + ':' + vout - if (this.prevTxMap[prevTxOut]) throw new Error('Duplicate TxOut: ' + prevTxOut) - - var vin = this.tx.addInput(txHash, vout, sequence) + var vin = this.tx.addInput(txHash, vout, sequence, scriptSig) this.inputs[vin] = input this.prevTxMap[prevTxOut] = vin @@ -334,43 +293,56 @@ var canBuildTypes = { 'pubkeyhash': true } -function buildFromInputData (input, scriptType, parentType, redeemScript, allowIncomplete) { +function buildFromInputData (input, scriptType, allowIncomplete) { + var signatures = input.signatures var scriptSig switch (scriptType) { case 'pubkeyhash': - var pkhSignature = input.signatures[0].toScriptSignature(input.hashType) + // remove blank signatures + signatures = signatures.filter(function (x) { return x }) + + if (signatures.length < 1) throw new Error('Not enough signatures provided') + if (signatures.length > 1) throw new Error('Too many signatures provided') + + var pkhSignature = signatures[0].toScriptSignature(input.hashType) scriptSig = bscript.pubKeyHashInput(pkhSignature, input.pubKeys[0]) break case 'pubkey': - var pkSignature = input.signatures[0].toScriptSignature(input.hashType) + // remove blank signatures + signatures = signatures.filter(function (x) { return x }) + + if (signatures.length < 1) throw new Error('Not enough signatures provided') + if (signatures.length > 1) throw new Error('Too many signatures provided') + + var pkSignature = signatures[0].toScriptSignature(input.hashType) scriptSig = bscript.pubKeyInput(pkSignature) break + // ref https://github.com/bitcoin/bitcoin/blob/d612837814020ae832499d18e6ee5eb919a87907/src/script/sign.cpp#L232 case 'multisig': - var msSignatures = input.signatures.map(function (signature) { + signatures = signatures.map(function (signature) { return signature && signature.toScriptSignature(input.hashType) }) - // fill in blanks with OP_0 if (allowIncomplete) { - for (var i = 0; i < msSignatures.length; ++i) { - msSignatures[i] = msSignatures[i] || ops.OP_0 + // fill in blanks with OP_0 + for (var i = 0; i < signatures.length; ++i) { + signatures[i] = signatures[i] || ops.OP_0 } - - // remove blank signatures } else { - msSignatures = msSignatures.filter(function (x) { return x }) + // remove blank signatures + signatures = signatures.filter(function (x) { return x }) } - scriptSig = bscript.multisigInput(msSignatures, allowIncomplete ? undefined : redeemScript) + scriptSig = bscript.multisigInput(signatures, allowIncomplete ? undefined : input.redeemScript) break } // wrap as scriptHash if necessary - if (parentType === 'scripthash') { - scriptSig = bscript.scriptHashInput(scriptSig, redeemScript) + if (input.prevOutType === 'scripthash') { + scriptSig = bscript.scriptHashInput(scriptSig, input.redeemScript) } return scriptSig @@ -387,24 +359,22 @@ TransactionBuilder.prototype.__build = function (allowIncomplete) { // Create script signatures from inputs this.inputs.forEach(function (input, index) { var scriptType = input.redeemScriptType || input.prevOutType - var scriptSig if (!allowIncomplete) { if (!scriptType) throw new Error('Transaction is not complete') if (!canBuildTypes[scriptType]) throw new Error(scriptType + ' not supported') - // XXX: only relevant to types that need signatures + // FIXME: only relevant to types that need signatures if (!input.signatures) throw new Error('Transaction is missing signatures') } - if (input.signatures) { - scriptSig = buildFromInputData(input, scriptType, input.prevOutType, input.redeemScript, allowIncomplete) - } + // FIXME: only relevant to types that need signatures + // skip if no scriptSig exists + if (!input.signatures) return - // did we build a scriptSig? Buffer('') is allowed - if (scriptSig) { - tx.setInputScript(index, scriptSig) - } + // build a scriptSig + var scriptSig = buildFromInputData(input, scriptType, allowIncomplete) + tx.setInputScript(index, scriptSig) }) return tx @@ -416,12 +386,10 @@ TransactionBuilder.prototype.sign = function (index, keyPair, redeemScript, hash hashType = hashType || Transaction.SIGHASH_ALL var input = this.inputs[index] - var canSign = input.hashType && - input.prevOutScript && - input.prevOutType && - input.pubKeys && - input.redeemScriptType && - input.signatures && + var canSign = input.hashType !== undefined && + input.prevOutScript !== undefined && + input.pubKeys !== undefined && + input.signatures !== undefined && input.signatures.length === input.pubKeys.length var kpPubKey = keyPair.getPublicKeyBuffer() @@ -437,44 +405,46 @@ TransactionBuilder.prototype.sign = function (index, keyPair, redeemScript, hash // no? prepare } else { - // must be pay-to-scriptHash? if (redeemScript) { - // if we have a prevOutScript, enforce scriptHash equality to the redeemScript + var redeemScriptHash = bcrypto.hash160(redeemScript) + + // if redeemScript exists, it is pay-to-scriptHash + // if we have a prevOutScript, enforce hash160(redeemScriptequality) to the redeemScript if (input.prevOutScript) { if (input.prevOutType !== 'scripthash') throw new Error('PrevOutScript must be P2SH') - var scriptHash = bscript.decompile(input.prevOutScript)[1] - if (!bufferEquals(scriptHash, bcrypto.hash160(redeemScript))) throw new Error('RedeemScript does not match ' + scriptHash.toString('hex')) - } - - var extracted = extractFromOutputScript(redeemScript, kpPubKey) - if (!extracted) throw new Error('RedeemScript not supported "' + bscript.toASM(redeemScript) + '"') + var prevOutScriptScriptHash = bscript.decompile(input.prevOutScript)[1] + if (!bufferEquals(prevOutScriptScriptHash, redeemScriptHash)) throw new Error('Inconsistent hash160(RedeemScript)') - // if we don't have a prevOutScript, generate a P2SH script - if (!input.prevOutScript) { - input.prevOutScript = bscript.scriptHashOutput(bcrypto.hash160(redeemScript)) + // or, we don't have a prevOutScript, so generate a P2SH script + } else { + input.prevOutScript = bscript.scriptHashOutput(redeemScriptHash) input.prevOutType = 'scripthash' } - input.pubKeys = extracted.pubKeys + var expanded = expandOutput(redeemScript, kpPubKey) + if (!expanded) throw new Error('RedeemScript not supported "' + bscript.toASM(redeemScript) + '"') + + input.pubKeys = expanded.pubKeys input.redeemScript = redeemScript - input.redeemScriptType = extracted.scriptType - input.signatures = extracted.pubKeys.map(function () { return undefined }) + input.redeemScriptType = expanded.scriptType + input.signatures = expanded.signatures + + // no redeemScript } else { // pay-to-scriptHash is not possible without a redeemScript if (input.prevOutType === 'scripthash') throw new Error('PrevOutScript is P2SH, missing redeemScript') - // if we don't have a scriptType, assume pubKeyHash otherwise - if (!input.scriptType) { + // if we don't have a scriptType, assume pubKeyHash + if (!input.prevOutType) { input.prevOutScript = bscript.pubKeyHashOutput(bcrypto.hash160(kpPubKey)) input.prevOutType = 'pubkeyhash' input.pubKeys = [kpPubKey] - input.scriptType = input.prevOutType input.signatures = [undefined] } else { // throw if we can't sign with it - if (!input.pubKeys || !input.signatures) throw new Error(input.scriptType + ' not supported') + if (!input.pubKeys || !input.signatures) throw new Error(input.prevOutType + ' not supported') } } @@ -482,8 +452,8 @@ TransactionBuilder.prototype.sign = function (index, keyPair, redeemScript, hash } // ready to sign? - var signatureScript = input.redeemScript || input.prevOutScript - var signatureHash = this.tx.hashForSignature(index, signatureScript, hashType) + var hashScript = input.redeemScript || input.prevOutScript + var signatureHash = this.tx.hashForSignature(index, hashScript, hashType) // enforce in order signing of public keys var valid = input.pubKeys.some(function (pubKey, i) { @@ -491,11 +461,45 @@ TransactionBuilder.prototype.sign = function (index, keyPair, redeemScript, hash if (input.signatures[i]) throw new Error('Signature already exists') input.signatures[i] = keyPair.sign(signatureHash) - return true }) if (!valid) throw new Error('Key pair cannot sign for this input') } +TransactionBuilder.prototype.__fixMultisigOrder = function (transaction, vin) { + var input = this.inputs[vin] + var hashScriptType = input.redeemScriptType || input.prevOutType + if (hashScriptType !== 'multisig') throw new TypeError('Expected multisig input') + + var hashType = input.hashType || Transaction.SIGHASH_ALL + var hashScript = input.redeemScript || input.prevOutScript + + // maintain a local copy of unmatched signatures + var unmatched = input.signatures.concat() + var signatureHash = transaction.hashForSignature(vin, hashScript, hashType) + + input.signatures = input.pubKeys.map(function (pubKey, y) { + var keyPair = ECPair.fromPublicKeyBuffer(pubKey) + var match + + // check for a signature + unmatched.some(function (signature, i) { + // skip if undefined || OP_0 + if (!signature) return false + + // skip if signature does not match pubKey + if (!keyPair.verify(signatureHash, signature)) return false + + // remove matched signature from unmatched + unmatched[i] = undefined + match = signature + + return true + }) + + return match || undefined + }) +} + module.exports = TransactionBuilder