From 7aaae4c001bd2bb604e634e0346d71cc513207a0 Mon Sep 17 00:00:00 2001 From: Ruben de Vries Date: Mon, 8 Feb 2016 15:12:02 +0100 Subject: [PATCH] Refactoring `extractInput` and `__build` to enable nested scripts (`P2SH[ P2PKH / multsig ]`). This is preperation for the segwit support which will have `P2SH[ P2WSH[ P2PKH / multsig ] ]` scripts --- src/transaction_builder.js | 263 ++++++++++++++++++++----------------- 1 file changed, 146 insertions(+), 117 deletions(-) diff --git a/src/transaction_builder.js b/src/transaction_builder.js index 64069f2..0ca9b0c 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -44,86 +44,98 @@ function fixMSSignatures (transaction, vin, pubKeys, signatures, prevOutScript, } function extractInput (transaction, txIn, vin) { - var redeemScript - var scriptSig = txIn.script - var scriptSigChunks = bscript.decompile(scriptSig) + var scriptSigChunks = bscript.decompile(txIn.script) + var prevOutType = bscript.classifyInput(scriptSigChunks, true) - var prevOutScript - var prevOutType = bscript.classifyInput(scriptSig, true) - var scriptType + if (txIn.script.length === 0) { + return {} + } - // Re-classify if scriptHash - if (prevOutType === 'scripthash') { - redeemScript = scriptSigChunks.slice(-1)[0] - prevOutScript = bscript.scriptHashOutput(bcrypto.hash160(redeemScript)) + var processScript = function (scriptType, scriptSigChunks, redeemScriptChunks) { + // ensure chunks are decompiled + scriptSigChunks = bscript.decompile(scriptSigChunks) + redeemScriptChunks = redeemScriptChunks ? bscript.decompile(redeemScriptChunks) : undefined - scriptSig = bscript.compile(scriptSigChunks.slice(0, -1)) - scriptSigChunks = scriptSigChunks.slice(0, -1) + var hashType, pubKeys, signatures, prevOutScript, redeemScript, redeemScriptType, result, parsed - scriptType = bscript.classifyInput(scriptSig, true) - } else { - scriptType = prevOutType - } + switch (scriptType) { + case 'scripthash': + redeemScript = scriptSigChunks.slice(-1)[0] + scriptSigChunks = bscript.compile(scriptSigChunks.slice(0, -1)) - // pre-empt redeemScript decompilation - var redeemScriptChunks - if (redeemScript) { - redeemScriptChunks = bscript.decompile(redeemScript) - } + redeemScriptType = bscript.classifyInput(scriptSigChunks, true) + prevOutScript = bscript.scriptHashOutput(bcrypto.hash160(redeemScript)) - // Extract hashType, pubKeys and signatures - var hashType, parsed, pubKeys, signatures + result = processScript(redeemScriptType, scriptSigChunks, bscript.decompile(redeemScript)) - switch (scriptType) { - case 'pubkeyhash': - parsed = ECSignature.parseScriptSignature(scriptSigChunks[0]) - hashType = parsed.hashType - pubKeys = scriptSigChunks.slice(1) - signatures = [parsed.signature] - prevOutScript = bscript.pubKeyHashOutput(bcrypto.hash160(pubKeys[0])) + result.prevOutScript = prevOutScript + result.redeemScript = redeemScript + result.redeemScriptType = redeemScriptType - break + return result - case 'pubkey': - parsed = ECSignature.parseScriptSignature(scriptSigChunks[0]) - hashType = parsed.hashType - signatures = [parsed.signature] + case 'pubkeyhash': + parsed = ECSignature.parseScriptSignature(scriptSigChunks[0]) + hashType = parsed.hashType + pubKeys = scriptSigChunks.slice(1) + signatures = [parsed.signature] + prevOutScript = bscript.pubKeyHashOutput(bcrypto.hash160(pubKeys[0])) - if (redeemScript) { - pubKeys = redeemScriptChunks.slice(0, 1) - } + break - break + case 'pubkey': + parsed = ECSignature.parseScriptSignature(scriptSigChunks[0]) + hashType = parsed.hashType + signatures = [parsed.signature] - case 'multisig': - signatures = scriptSigChunks.slice(1).map(function (chunk) { - if (chunk === ops.OP_0) return undefined + if (redeemScriptChunks) { + pubKeys = redeemScriptChunks.slice(0, 1) + } - var parsed = ECSignature.parseScriptSignature(chunk) - hashType = parsed.hashType + 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 - }) + return parsed.signature + }) - if (redeemScript) { - pubKeys = redeemScriptChunks.slice(1, -2) + if (redeemScriptChunks) { + pubKeys = redeemScriptChunks.slice(1, -2) - if (pubKeys.length !== signatures.length) { - signatures = fixMSSignatures(transaction, vin, pubKeys, signatures, redeemScript, hashType, redeemScript) + if (pubKeys.length !== signatures.length) { + signatures = fixMSSignatures(transaction, vin, pubKeys, signatures, bscript.compile(redeemScriptChunks), hashType, redeemScript) + } } - } - break + 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: hashType, - prevOutScript: prevOutScript, + hashType: result.hashType, + prevOutScript: result.prevOutScript, prevOutType: prevOutType, - pubKeys: pubKeys, - redeemScript: redeemScript, - scriptType: scriptType, - signatures: signatures + pubKeys: result.pubKeys, + redeemScript: result.redeemScript, + redeemScriptType: result.redeemScriptType, + signatures: result.signatures } } @@ -176,9 +188,6 @@ TransactionBuilder.fromTransaction = function (transaction, network) { throw new Error('coinbase inputs not supported') } - // Ignore empty scripts - if (txIn.script.length === 0) return {} - return extractInput(transaction, txIn, vin) }) @@ -298,7 +307,7 @@ TransactionBuilder.prototype.__build = function (allowIncomplete) { // Create script signatures from inputs this.inputs.forEach(function (input, index) { - var scriptType = input.scriptType + var scriptType = input.redeemScriptType || input.prevOutType var scriptSig if (!allowIncomplete) { @@ -310,46 +319,55 @@ TransactionBuilder.prototype.__build = function (allowIncomplete) { } if (input.signatures) { - switch (scriptType) { - case 'pubkeyhash': - var pkhSignature = input.signatures[0].toScriptSignature(input.hashType) - scriptSig = bscript.pubKeyHashInput(pkhSignature, input.pubKeys[0]) - break - - case 'multisig': - var msSignatures = input.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 + var processScript = function (scriptType, parentType, redeemScript) { + var scriptSig + var pkhSignature + + switch (scriptType) { + case 'pubkeyhash': + pkhSignature = input.signatures[0].toScriptSignature(input.hashType) + scriptSig = bscript.pubKeyHashInput(pkhSignature, input.pubKeys[0]) + break + + case 'multisig': + var msSignatures = input.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 + } + + // remove blank signatures + } else { + msSignatures = msSignatures.filter(function (x) { return x }) } - // remove blank signatures - } else { - msSignatures = msSignatures.filter(function (x) { return x }) - } + scriptSig = bscript.multisigInput(msSignatures, allowIncomplete ? undefined : redeemScript) - var redeemScript = allowIncomplete ? undefined : input.redeemScript - scriptSig = bscript.multisigInput(msSignatures, redeemScript) - break + break - case 'pubkey': - var pkSignature = input.signatures[0].toScriptSignature(input.hashType) - scriptSig = bscript.pubKeyInput(pkSignature) - break + case 'pubkey': + var pkSignature = input.signatures[0].toScriptSignature(input.hashType) + scriptSig = bscript.pubKeyInput(pkSignature) + break + } + + // wrap as scriptHash if necessary + if (parentType === 'scripthash') { + scriptSig = bscript.scriptHashInput(scriptSig, redeemScript) + } + + return scriptSig } + + scriptSig = processScript(scriptType, input.prevOutType, input.redeemScript) } - // did we build a scriptSig? + // did we build a scriptSig? Buffer('') is allowed if (scriptSig) { - // wrap as scriptHash if necessary - if (input.prevOutType === 'scripthash') { - scriptSig = bscript.scriptHashInput(scriptSig, input.redeemScript) - } - tx.setInputScript(index, scriptSig) } }) @@ -367,11 +385,12 @@ TransactionBuilder.prototype.sign = function (index, keyPair, redeemScript, hash input.prevOutScript && input.prevOutType && input.pubKeys && - input.scriptType && + input.redeemScriptType && input.signatures && input.signatures.length === input.pubKeys.length var kpPubKey = keyPair.getPublicKeyBuffer() + var signatureScript // are we ready to sign? if (canSign) { @@ -385,6 +404,7 @@ 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 if (input.prevOutScript) { @@ -394,34 +414,43 @@ TransactionBuilder.prototype.sign = function (index, keyPair, redeemScript, hash if (!bufferEquals(scriptHash, bcrypto.hash160(redeemScript))) throw new Error('RedeemScript does not match ' + scriptHash.toString('hex')) } - var scriptType = bscript.classifyOutput(redeemScript) - var redeemScriptChunks = bscript.decompile(redeemScript) - var pubKeys + var pubKeys, pkh1, pkh2 - switch (scriptType) { - case 'multisig': - pubKeys = redeemScriptChunks.slice(1, -2) + var redeemScriptType - break + var processScript = function (redeemScript) { + var scriptType = bscript.classifyOutput(redeemScript) + var redeemScriptChunks = bscript.decompile(redeemScript) - case 'pubkeyhash': - var pkh1 = redeemScriptChunks[2] - var pkh2 = bcrypto.hash160(keyPair.getPublicKeyBuffer()) + switch (scriptType) { + case 'multisig': + pubKeys = redeemScriptChunks.slice(1, -2) - if (!bufferEquals(pkh1, pkh2)) throw new Error('privateKey cannot sign for this input') - pubKeys = [kpPubKey] + break - break + case 'pubkeyhash': + pkh1 = redeemScriptChunks[2] + pkh2 = bcrypto.hash160(keyPair.getPublicKeyBuffer()) - case 'pubkey': - pubKeys = redeemScriptChunks.slice(0, 1) + if (!bufferEquals(pkh1, pkh2)) throw new Error('privateKey cannot sign for this input') + pubKeys = [kpPubKey] + + break - break + case 'pubkey': + pubKeys = redeemScriptChunks.slice(0, 1) - default: - throw new Error('RedeemScript not supported (' + scriptType + ')') + break + + default: + throw new Error('RedeemScript not supported (' + scriptType + ')') + } + + return scriptType } + redeemScriptType = processScript(redeemScript) + // if we don't have a prevOutScript, generate a P2SH script if (!input.prevOutScript) { input.prevOutScript = bscript.scriptHashOutput(bcrypto.hash160(redeemScript)) @@ -430,7 +459,7 @@ TransactionBuilder.prototype.sign = function (index, keyPair, redeemScript, hash input.pubKeys = pubKeys input.redeemScript = redeemScript - input.scriptType = scriptType + input.redeemScriptType = redeemScriptType input.signatures = pubKeys.map(function () { return undefined }) } else { // pay-to-scriptHash is not possible without a redeemScript @@ -440,6 +469,7 @@ TransactionBuilder.prototype.sign = function (index, keyPair, redeemScript, hash if (!input.scriptType) { input.prevOutScript = bscript.pubKeyHashOutput(bcrypto.hash160(keyPair.getPublicKeyBuffer())) input.prevOutType = 'pubkeyhash' + input.pubKeys = [kpPubKey] input.scriptType = input.prevOutType input.signatures = [undefined] @@ -453,7 +483,7 @@ TransactionBuilder.prototype.sign = function (index, keyPair, redeemScript, hash } // ready to sign? - var signatureScript = input.redeemScript || input.prevOutScript + signatureScript = signatureScript || input.redeemScript || input.prevOutScript var signatureHash = this.tx.hashForSignature(index, signatureScript, hashType) // enforce in order signing of public keys @@ -461,8 +491,7 @@ TransactionBuilder.prototype.sign = function (index, keyPair, redeemScript, hash if (!bufferEquals(kpPubKey, pubKey)) return false if (input.signatures[i]) throw new Error('Signature already exists') - var signature = keyPair.sign(signatureHash) - input.signatures[i] = signature + input.signatures[i] = keyPair.sign(signatureHash) return true })