From b3fd50ffd62a8921446e91309c052ecf4c8501cc Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Wed, 28 Sep 2016 00:46:37 +1000 Subject: [PATCH] TransactionBuilder: refactor extractInput/extractFromOutput --- src/transaction_builder.js | 486 +++++++++++++++++++------------------ 1 file changed, 245 insertions(+), 241 deletions(-) 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 +// 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 pubKeys.map(function (pubKey) { - // skip optionally provided pubKey - if (skipPubKey && bufferEquals(skipPubKey, pubKey)) return undefined + var hashType, pubKeys, signatures, prevOutScript - var matched - var keyPair2 = ECPair.fromPublicKeyBuffer(pubKey) + switch (scriptSigType) { + case 'scripthash': + // FIXME: maybe depth limit instead, how possible is this anyway? + if (redeemScript) throw new Error('Recursive P2SH script') - // check for a matching signature - unmatched.some(function (signature, i) { - // skip if undefined || OP_0 - if (!signature) return false + var redeemScriptSig = scriptSigChunks.slice(0, -1) + redeemScript = scriptSigChunks[scriptSigChunks.length - 1] - if (!signatureHash) { - signatureHash = transaction.hashForSignature(vin, prevOutScript, hashType) - } - if (!keyPair2.verify(signatureHash, signature)) return false + var result = expandInput(redeemScriptSig, redeemScript) + result.redeemScript = redeemScript + result.redeemScriptType = result.prevOutType + result.prevOutScript = bscript.scriptHashOutput(bcrypto.hash160(redeemScript)) + result.prevOutType = 'scripthash' + return result - // remove matched signature from unmatched - unmatched[i] = undefined - matched = signature - - return true - }) - - return matched || undefined - }) -} - -function extractInput (transaction, txIn, vin) { - if (txIn.script.length === 0) return {} - - var scriptSigChunks = bscript.decompile(txIn.script) - var prevOutType = bscript.classifyInput(scriptSigChunks, true) - - 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 'pubkey': - parsed = ECSignature.parseScriptSignature(scriptSigChunks[0]) - hashType = parsed.hashType - signatures = [parsed.signature] - - if (redeemScriptChunks) { - pubKeys = redeemScriptChunks.slice(0, 1) - } - - break - - case 'multisig': - signatures = scriptSigChunks.slice(1).map(function (chunk) { - if (chunk === ops.OP_0) return undefined - - parsed = ECSignature.parseScriptSignature(chunk) - hashType = parsed.hashType - - return parsed.signature - }) - - if (redeemScriptChunks) { - pubKeys = redeemScriptChunks.slice(1, -2) - - if (pubKeys.length !== signatures.length) { - signatures = fixMSSignatures(transaction, vin, pubKeys, signatures, bscript.compile(redeemScriptChunks), hashType, redeemScript) - } - } - - break - } - - return { - hashType: hashType, - pubKeys: pubKeys, - signatures: signatures, - prevOutScript: prevOutScript, - redeemScript: redeemScript, - redeemScriptType: redeemScriptType - } - } - - // 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 - } -} - -function extractFromOutputScript (outputScript, kpPubKey) { - var scriptType = bscript.classifyOutput(outputScript) - var outputScriptChunks = bscript.decompile(outputScript) - - var pubKeys - switch (scriptType) { case 'pubkeyhash': - var pkh1 = outputScriptChunks[2] - var pkh2 = bcrypto.hash160(kpPubKey) + // if (redeemScript) throw new Error('Nonstandard... P2SH(P2PKH)') + var s = ECSignature.parseScriptSignature(scriptSigChunks[0]) + hashType = s.hashType + pubKeys = scriptSigChunks.slice(1) + signatures = [s.signature] - if (!bufferEquals(pkh1, pkh2)) throw new Error('privateKey cannot sign for this input') - pubKeys = [kpPubKey] + if (redeemScript) break + + prevOutScript = bscript.pubKeyHashOutput(bcrypto.hash160(pubKeys[0])) break case 'pubkey': - pubKeys = outputScriptChunks.slice(0, 1) + if (redeemScript) { + pubKeys = bscript.decompile(redeemScript).slice(0, 1) + } + + var ss = ECSignature.parseScriptSignature(scriptSigChunks[0]) + hashType = ss.hashType + signatures = [ss.signature] break case 'multisig': - pubKeys = outputScriptChunks.slice(1, -2) + if (redeemScript) { + pubKeys = bscript.decompile(redeemScript).slice(1, -2) + } + + signatures = scriptSigChunks.slice(1).map(function (chunk) { + if (chunk === ops.OP_0) return undefined + + var sss = ECSignature.parseScriptSignature(chunk) + + if (hashType !== undefined) { + if (sss.hashType !== hashType) throw new Error('Inconsistent hashType') + } else { + hashType = sss.hashType + } + + return sss.signature + }) + + break + } + + return { + hashType: hashType, + pubKeys: pubKeys, + signatures: signatures, + prevOutScript: prevOutScript, + prevOutType: scriptSigType + } +} + +function expandOutput (script, ourPubKey) { + typeforce(types.Buffer, script) + + var scriptChunks = bscript.decompile(script) + var scriptType = bscript.classifyOutput(script) + + var pubKeys = [] + + switch (scriptType) { + // does our hash160(pubKey) match the output scripts? + case 'pubkeyhash': + if (!ourPubKey) break + + var pkh1 = scriptChunks[2] + var pkh2 = bcrypto.hash160(ourPubKey) + if (bufferEquals(pkh1, pkh2)) pubKeys = [ourPubKey] + break + + case 'pubkey': + pubKeys = scriptChunks.slice(0, 1) + break + + case 'multisig': + 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,73 +157,36 @@ 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 + // Copy transaction fields + txb.setVersion(transaction.version) + txb.setLockTime(transaction.locktime) - // Extract/add inputs - transaction.ins.forEach(function (txIn) { - txb.addInput(txIn.hash, txIn.index, txIn.sequence) - }) - - // 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) + }) - return extractInput(transaction, txIn, vin) + // 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 + + txb.__fixMultisigOrder(transaction, i) + } }) return txb } TransactionBuilder.prototype.addInput = function (txHash, vout, sequence, prevOutScript) { - // is it a hex string? - if (typeof txHash === 'string') { - // transaction hashs's are displayed in reverse order, un-reverse it - txHash = bufferReverse(new Buffer(txHash, 'hex')) - - // is it a Transaction object? - } else if (txHash instanceof Transaction) { - prevOutScript = txHash.outs[vout].script - txHash = txHash.getHash() - } - - var input = {} - if (prevOutScript) { - var prevOutScriptChunks = bscript.decompile(prevOutScript) - var prevOutType = bscript.classifyOutput(prevOutScriptChunks) - - // if we can, extract pubKey information - switch (prevOutType) { - case 'multisig': - input.pubKeys = prevOutScriptChunks.slice(1, -2) - input.signatures = input.pubKeys.map(function () { return undefined }) - - break - - case 'pubkey': - input.pubKeys = prevOutScriptChunks.slice(0, 1) - input.signatures = [undefined] - - break - } - - if (prevOutType !== 'scripthash') { - input.scriptType = prevOutType - } - - 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) { @@ -282,10 +198,53 @@ TransactionBuilder.prototype.addInput = function (txHash, vout, sequence, prevOu 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 + txHash = bufferReverse(new Buffer(txHash, 'hex')) + + // is it a Transaction object? + } else if (txHash instanceof Transaction) { + prevOutScript = txHash.outs[vout].script + txHash = txHash.getHash() + } + + return this.__addInputUnsafe(txHash, vout, sequence, null, prevOutScript) +} + +TransactionBuilder.prototype.__addInputUnsafe = function (txHash, vout, sequence, scriptSig, prevOutScript) { + if (Transaction.isCoinbaseHash(txHash)) { + throw new Error('coinbase inputs not supported') + } + 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 input = {} + + // derive what we can from the scriptSig + if (scriptSig) { + input = expandInput(scriptSig) + } + + // 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 + } + + 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 prevOutScriptScriptHash = bscript.decompile(input.prevOutScript)[1] + if (!bufferEquals(prevOutScriptScriptHash, redeemScriptHash)) throw new Error('Inconsistent hash160(RedeemScript)') - var extracted = extractFromOutputScript(redeemScript, kpPubKey) - if (!extracted) throw new Error('RedeemScript not supported "' + bscript.toASM(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