diff --git a/src/transaction_builder.js b/src/transaction_builder.js index 6e1865a..7b599e8 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -9,7 +9,39 @@ var ECPair = require('./ecpair') var ECSignature = require('./ecsignature') var Transaction = require('./transaction') -function extractInput (txIn) { +// 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 cache = {} + + return pubKeys.map(function (pubKey) { + // skip optionally provided pubKey + if (skipPubKey && bufferutils.equal(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 + + var signatureHash = cache[hashType] = cache[hashType] || transaction.hashForSignature(vin, prevOutScript, hashType) + if (!keyPair2.verify(signatureHash, signature)) return false + + // remove matched signature from unmatched + unmatched[i] = undefined + matched = signature + + return true + }) + + return matched || undefined + }) +} + +function extractInput (transaction, txIn, vin) { var redeemScript var scriptSig = txIn.script var scriptSigChunks = bscript.decompile(scriptSig) @@ -63,7 +95,7 @@ function extractInput (txIn) { case 'multisig': signatures = scriptSigChunks.slice(1).map(function (chunk) { - if (chunk === ops.OP_0) return chunk + if (chunk === ops.OP_0) return undefined var parsed = ECSignature.parseScriptSignature(chunk) hashType = parsed.hashType @@ -73,6 +105,10 @@ function extractInput (txIn) { if (redeemScript) { pubKeys = redeemScriptChunks.slice(1, -2) + + if (pubKeys.length !== signatures.length) { + signatures = fixMSSignatures(transaction, vin, pubKeys, signatures, redeemScript, hashType, redeemScript) + } } break @@ -117,7 +153,7 @@ TransactionBuilder.fromTransaction = function (transaction, network) { }) // Extract/add signatures - txb.inputs = transaction.ins.map(function (txIn) { + 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') @@ -126,7 +162,7 @@ TransactionBuilder.fromTransaction = function (transaction, network) { // Ignore empty scripts if (txIn.script.length === 0) return {} - return extractInput(txIn) + return extractInput(transaction, txIn, vin) }) return txb @@ -154,10 +190,14 @@ TransactionBuilder.prototype.addInput = function (txHash, vout, sequence, prevOu 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 } @@ -211,10 +251,10 @@ TransactionBuilder.prototype.buildIncomplete = function () { return this.__build(true) } -var canSignTypes = { - 'pubkeyhash': true, +var canBuildTypes = { 'multisig': true, - 'pubkey': true + 'pubkey': true, + 'pubkeyhash': true } TransactionBuilder.prototype.__build = function (allowIncomplete) { @@ -225,14 +265,16 @@ TransactionBuilder.prototype.__build = function (allowIncomplete) { var tx = this.tx.clone() - // Create script signatures from signature meta-data + // Create script signatures from inputs this.inputs.forEach(function (input, index) { var scriptType = input.scriptType var scriptSig if (!allowIncomplete) { if (!scriptType) throw new Error('Transaction is not complete') - if (!canSignTypes[scriptType]) throw new Error(scriptType + ' not supported') + if (!canBuildTypes[scriptType]) throw new Error(scriptType + ' not supported') + + // XXX: only relevant to types that need signatures if (!input.signatures) throw new Error('Transaction is missing signatures') } @@ -244,7 +286,6 @@ TransactionBuilder.prototype.__build = function (allowIncomplete) { break case 'multisig': - // Array.prototype.map is sparse-compatible var msSignatures = input.signatures.map(function (signature) { return signature && signature.toScriptSignature(input.hashType) }) @@ -252,12 +293,11 @@ TransactionBuilder.prototype.__build = function (allowIncomplete) { // fill in blanks with OP_0 if (allowIncomplete) { for (var i = 0; i < msSignatures.length; ++i) { - if (msSignatures[i]) continue - - msSignatures[i] = ops.OP_0 + msSignatures[i] = msSignatures[i] || ops.OP_0 } + + // remove blank signatures } else { - // Array.prototype.filter returns non-sparse array msSignatures = msSignatures.filter(function (x) { return x }) } @@ -297,11 +337,12 @@ TransactionBuilder.prototype.sign = function (index, keyPair, redeemScript, hash input.prevOutType && input.pubKeys && input.scriptType && - input.signatures + input.signatures && + input.signatures.length === input.pubKeys.length var kpPubKey = keyPair.getPublicKeyBuffer() - // are we almost ready to sign? + // are we ready to sign? if (canSign) { // if redeemScript was provided, enforce consistency if (redeemScript) { @@ -323,13 +364,13 @@ TransactionBuilder.prototype.sign = function (index, keyPair, redeemScript, hash } var scriptType = bscript.classifyOutput(redeemScript) - if (!canSignTypes[scriptType]) throw new Error('RedeemScript not supported (' + scriptType + ')') - var redeemScriptChunks = bscript.decompile(redeemScript) - var pubKeys = [] + var pubKeys + switch (scriptType) { case 'multisig': pubKeys = redeemScriptChunks.slice(1, -2) + break case 'pubkeyhash': @@ -338,11 +379,16 @@ TransactionBuilder.prototype.sign = function (index, keyPair, redeemScript, hash if (!bufferutils.equal(pkh1, pkh2)) throw new Error('privateKey cannot sign for this input') pubKeys = [kpPubKey] + break case 'pubkey': pubKeys = redeemScriptChunks.slice(0, 1) + break + + default: + throw new Error('RedeemScript not supported (' + scriptType + ')') } // if we don't have a prevOutScript, generate a P2SH script @@ -354,55 +400,31 @@ TransactionBuilder.prototype.sign = function (index, keyPair, redeemScript, hash input.pubKeys = pubKeys input.redeemScript = redeemScript input.scriptType = scriptType - - // cannot be pay-to-scriptHash + input.signatures = pubKeys.map(function () { return undefined }) } else { + // pay-to-scriptHash is not possible without a redeemScript if (input.prevOutType === 'scripthash') throw new Error('PrevOutScript is P2SH, missing redeemScript') - // can we otherwise sign this? - if (input.scriptType) { - if (!input.pubKeys) throw new Error(input.scriptType + ' not supported') - - // we know nothin' Jon Snow, assume pubKeyHash - } else { + // if we don't have a scriptType, assume pubKeyHash otherwise + if (!input.scriptType) { input.prevOutScript = bscript.pubKeyHashOutput(bcrypto.hash160(keyPair.getPublicKeyBuffer())) 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') } } input.hashType = hashType - input.signatures = input.signatures || [] } + // ready to sign? var signatureScript = input.redeemScript || input.prevOutScript var signatureHash = this.tx.hashForSignature(index, signatureScript, hashType) - // enforce signature order matches public keys - if (input.scriptType === 'multisig' && input.redeemScript && input.signatures.length !== input.pubKeys.length) { - // maintain a local copy of unmatched signatures - var unmatched = input.signatures.slice() - - input.signatures = input.pubKeys.map(function (pubKey) { - var match - var keyPair2 = ECPair.fromPublicKeyBuffer(pubKey) - - // check for any matching signatures - unmatched.some(function (signature, i) { - if (!signature || !keyPair2.verify(signatureHash, signature)) return false - match = signature - - // remove matched signature from unmatched - unmatched.splice(i, 1) - - return true - }) - - return match || undefined - }) - } - // enforce in order signing of public keys var valid = input.pubKeys.some(function (pubKey, i) { if (!bufferutils.equal(kpPubKey, pubKey)) return false diff --git a/test/fixtures/transaction_builder.json b/test/fixtures/transaction_builder.json index e530f54..2046474 100644 --- a/test/fixtures/transaction_builder.json +++ b/test/fixtures/transaction_builder.json @@ -118,6 +118,32 @@ } ] }, + { + "description": "Transaction w/ multisig 2-of-3 -> pubKeyHash", + "network": "testnet", + "txHex": "0100000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000910047304402206b2fc7d3182e2853cab5bcffb85c3ef5470d2d05c496295538c9947af3bfd0ec0220300aa705a985c74f76c26c6d68da9b61b5c4cd5432e8c6a54623f376c8bf8cde01473044022031059c4dd6a97d84e3a4eb1ca21a9870bd1762fbd5db7c1932d75e56da78794502200f22d85be3c5f7035e89a147ee2619a066df19aff14a62e6bb3f649b6da19edf01ffffffff0110270000000000001976a914faf1d99bf040ea9c7f8cc9f14ac6733ad75ce24688ac00000000", + "inputs": [ + { + "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "vout": 0, + "prevTxScript": "OP_2 0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 04c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a 04f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e672 OP_3 OP_CHECKMULTISIG", + "signs": [ + { + "keyPair": "91avARGdfge8E4tZfYLoxeJ5sGBdNJQH4kvjJoQFacbgwmaKkrx" + }, + { + "keyPair": "91avARGdfge8E4tZfYLoxeJ5sGBdNJQH4kvjJoQFacbgww7vXtT" + } + ] + } + ], + "outputs": [ + { + "script": "OP_DUP OP_HASH160 faf1d99bf040ea9c7f8cc9f14ac6733ad75ce246 OP_EQUALVERIFY OP_CHECKSIG", + "value": 10000 + } + ] + }, { "description": "Transaction w/ multisig 2-of-2 (reverse order) -> pubKeyHash", "network": "testnet", @@ -239,6 +265,64 @@ ] } ], + "fromTransaction": [ + { + "description": "Transaction w/ scriptHash(multisig 2-of-2) -> OP_RETURN | 1 OP_0, no signatures", + "network": "testnet", + "incomplete": true, + "inputs": [ + { + "txId": "4971f016798a167331bcbc67248313fbc444c6e92e4416efd06964425588f5cf", + "vout": 0, + "scriptSig": "OP_0 OP_0 52410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b84104c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a52ae", + "scriptSigAfter": "OP_0 OP_0 OP_0 52410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b84104c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a52ae" + } + ], + "outputs": [ + { + "script": "OP_DUP OP_HASH160 aa4d7985c57e011a8b3dd8e0e5a73aaef41629c5 OP_EQUALVERIFY OP_CHECKSIG", + "value": 1000 + } + ] + }, + { + "description": "Transaction w/ scriptHash(multisig 2-of-2) -> OP_RETURN | missing OP_0, 1 signature", + "network": "testnet", + "incomplete": true, + "inputs": [ + { + "txId": "4971f016798a167331bcbc67248313fbc444c6e92e4416efd06964425588f5cf", + "vout": 0, + "scriptSig": "OP_0 3045022100aa0c323bc639d3d71591be98ccaf7b8cb8c86aa95f060aef5e36fc3f04c4d029022010e2b18de17e307a12ae7e0e88518fe814f18a0ca1ee4510ba23a65628b0657601 52410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b84104c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a52ae", + "scriptSigAfter": "OP_0 OP_0 3045022100aa0c323bc639d3d71591be98ccaf7b8cb8c86aa95f060aef5e36fc3f04c4d029022010e2b18de17e307a12ae7e0e88518fe814f18a0ca1ee4510ba23a65628b0657601 52410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b84104c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a52ae" + } + ], + "outputs": [ + { + "script": "OP_DUP OP_HASH160 aa4d7985c57e011a8b3dd8e0e5a73aaef41629c5 OP_EQUALVERIFY OP_CHECKSIG", + "value": 1000 + } + ] + }, + { + "description": "Transaction w/ scriptHash(multisig 2-of-2) -> OP_RETURN | no OP_0, 2 signatures", + "network": "testnet", + "inputs": [ + { + "txId": "4971f016798a167331bcbc67248313fbc444c6e92e4416efd06964425588f5cf", + "vout": 0, + "scriptSig": "OP_0 3045022100e37e33a4fe5fccfb87afb0e951e83fcea4820d73b327d21edc1adec3b916c437022061c5786908b674e323a1863cc2feeb60e1679f1892c10ee08ac21e51fd50ba9001 3045022100e37e33a4fe5fccfb87afb0e951e83fcea4820d73b327d21edc1adec3b916c437022061c5786908b674e323a1863cc2feeb60e1679f1892c10ee08ac21e51fd50ba9001 52410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b84104c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a52ae", + "scriptSigAfter": "OP_0 3045022100e37e33a4fe5fccfb87afb0e951e83fcea4820d73b327d21edc1adec3b916c437022061c5786908b674e323a1863cc2feeb60e1679f1892c10ee08ac21e51fd50ba9001 3045022100e37e33a4fe5fccfb87afb0e951e83fcea4820d73b327d21edc1adec3b916c437022061c5786908b674e323a1863cc2feeb60e1679f1892c10ee08ac21e51fd50ba9001 52410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b84104c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a52ae" + } + ], + "outputs": [ + { + "script": "OP_DUP OP_HASH160 aa4d7985c57e011a8b3dd8e0e5a73aaef41629c5 OP_EQUALVERIFY OP_CHECKSIG", + "value": 1000 + } + ] + } + ], "multisig": [ { "description": "P2SH 2-of-2 multisig, signed in correct order", diff --git a/test/transaction_builder.js b/test/transaction_builder.js index 06b070e..12c6b3d 100644 --- a/test/transaction_builder.js +++ b/test/transaction_builder.js @@ -3,6 +3,7 @@ var assert = require('assert') var baddress = require('../src/address') var bscript = require('../src/script') +var bufferutils = require('../src/bufferutils') var ops = require('../src/opcodes') var BigInteger = require('bigi') @@ -76,7 +77,7 @@ describe('TransactionBuilder', function () { describe('fromTransaction', function () { fixtures.valid.build.forEach(function (f) { - it('builds the correct TransactionBuilder for ' + f.description, function () { + it('builds TransactionBuilder, with ' + f.description, function () { var network = NETWORKS[f.network || 'bitcoin'] var tx = Transaction.fromHex(f.txHex) var txb = TransactionBuilder.fromTransaction(tx, network) @@ -86,6 +87,33 @@ describe('TransactionBuilder', function () { }) }) + fixtures.valid.fromTransaction.forEach(function (f) { + it('builds TransactionBuilder, with ' + f.description, function () { + var tx = new Transaction() + + f.inputs.forEach(function (input) { + var txHash = bufferutils.reverse(new Buffer(input.txId, 'hex')) + + tx.addInput(txHash, input.vout, undefined, bscript.fromASM(input.scriptSig)) + }) + + f.outputs.forEach(function (output) { + tx.addOutput(bscript.fromASM(output.script), output.value) + }) + + var txb = TransactionBuilder.fromTransaction(tx) + var txAfter = f.incomplete ? txb.buildIncomplete() : txb.build() + + txAfter.ins.forEach(function (input, i) { + assert.equal(bscript.toASM(input.script), f.inputs[i].scriptSigAfter) + }) + + txAfter.outs.forEach(function (output, i) { + assert.equal(bscript.toASM(output.script), f.outputs[i].script) + }) + }) + }) + fixtures.invalid.fromTransaction.forEach(function (f) { it('throws on ' + f.exception, function () { var tx = Transaction.fromHex(f.txHex)