From fdb0ceeeb58b0e7b43125db551ff7d89c5f8fa7d Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Tue, 27 Sep 2016 21:19:27 +1000 Subject: [PATCH 01/12] testing: rename cltvCheckSigInput to cltvCheckSigOutput --- test/integration/cltv.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/integration/cltv.js b/test/integration/cltv.js index dcd2b6e..e1b98dd 100644 --- a/test/integration/cltv.js +++ b/test/integration/cltv.js @@ -11,7 +11,7 @@ var bob = bitcoin.ECPair.fromWIF('cMkopUXKWsEzAjfa1zApksGRwjVpJRB3831qM9W4gKZsLw describe('bitcoinjs-lib (CLTV)', function () { var hashType = bitcoin.Transaction.SIGHASH_ALL - function cltvCheckSigInput (aQ, bQ, utcSeconds) { + function cltvCheckSigOutput (aQ, bQ, utcSeconds) { return bitcoin.script.compile([ bitcoin.opcodes.OP_IF, bitcoin.script.number.encode(utcSeconds), @@ -38,7 +38,7 @@ describe('bitcoinjs-lib (CLTV)', function () { // three hours ago var timeUtc = utcNow() - (3600 * 3) - var redeemScript = cltvCheckSigInput(alice, bob, timeUtc) + var redeemScript = cltvCheckSigOutput(alice, bob, timeUtc) var scriptPubKey = bitcoin.script.scriptHashOutput(bitcoin.crypto.hash160(redeemScript)) var address = bitcoin.address.fromOutputScript(scriptPubKey, network) @@ -72,7 +72,7 @@ describe('bitcoinjs-lib (CLTV)', function () { // two hours ago var timeUtc = utcNow() - (3600 * 2) - var redeemScript = cltvCheckSigInput(alice, bob, timeUtc) + var redeemScript = cltvCheckSigOutput(alice, bob, timeUtc) var scriptPubKey = bitcoin.script.scriptHashOutput(bitcoin.crypto.hash160(redeemScript)) var address = bitcoin.address.fromOutputScript(scriptPubKey, network) @@ -104,7 +104,7 @@ describe('bitcoinjs-lib (CLTV)', function () { // two hours from now var timeUtc = utcNow() + (3600 * 2) - var redeemScript = cltvCheckSigInput(alice, bob, timeUtc) + var redeemScript = cltvCheckSigOutput(alice, bob, timeUtc) var scriptPubKey = bitcoin.script.scriptHashOutput(bitcoin.crypto.hash160(redeemScript)) var address = bitcoin.address.fromOutputScript(scriptPubKey, network) From d5eec5df70be7a6e01af0ad2b9d58863851f64a2 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Wed, 28 Sep 2016 00:16:51 +1000 Subject: [PATCH 02/12] tests: fix invalid test description --- test/transaction_builder.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/transaction_builder.js b/test/transaction_builder.js index 51cde35..412bb7b 100644 --- a/test/transaction_builder.js +++ b/test/transaction_builder.js @@ -108,7 +108,7 @@ describe('TransactionBuilder', function () { }) fixtures.invalid.fromTransaction.forEach(function (f) { - it('throws on ' + f.exception, function () { + it('throws ' + f.exception, function () { var tx = Transaction.fromHex(f.txHex) assert.throws(function () { @@ -294,7 +294,7 @@ describe('TransactionBuilder', function () { } }) - it('throws', function () { + it('throws ' + f.exception, function () { assert.throws(function () { txb.build() }, new RegExp(f.exception)) From ae63ae1131fd6a1d9ca45c472590c849085c4829 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Wed, 28 Sep 2016 00:20:36 +1000 Subject: [PATCH 03/12] tests: change error thrown for consistency with multisig --- test/fixtures/transaction_builder.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fixtures/transaction_builder.json b/test/fixtures/transaction_builder.json index f3fb578..37be385 100644 --- a/test/fixtures/transaction_builder.json +++ b/test/fixtures/transaction_builder.json @@ -632,7 +632,7 @@ }, { "description": "Incomplete transaction w/ prevTxScript defined", - "exception": "Transaction is missing signatures", + "exception": "Not enough signatures provided", "alwaysThrows": true, "inputs": [ { From 418c315c78aca114cc4f3d084b609c00e43847e5 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Wed, 28 Sep 2016 00:46:14 +1000 Subject: [PATCH 04/12] tests: clarify multisig edge case for easier debugging --- test/transaction_builder.js | 45 ++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/test/transaction_builder.js b/test/transaction_builder.js index 412bb7b..53732d4 100644 --- a/test/transaction_builder.js +++ b/test/transaction_builder.js @@ -62,11 +62,6 @@ describe('TransactionBuilder', function () { return baddress.toOutputScript(x) }) var txHash = new Buffer('0e7cea811c0be9f73c0aca591034396e7264473fc25c1ca45195d7417b36cbe2', 'hex') - var txb - - beforeEach(function () { - txb = new TransactionBuilder() - }) describe('fromTransaction', function () { fixtures.valid.build.forEach(function (f) { @@ -119,6 +114,11 @@ describe('TransactionBuilder', function () { }) describe('addInput', function () { + var txb + beforeEach(function () { + txb = new TransactionBuilder() + }) + it('accepts a txHash, index [and sequence number]', function () { var vin = txb.addInput(txHash, 1, 54) assert.strictEqual(vin, 0) @@ -172,6 +172,11 @@ describe('TransactionBuilder', function () { }) describe('addOutput', function () { + var txb + beforeEach(function () { + txb = new TransactionBuilder() + }) + it('accepts an address string and value', function () { var vout = txb.addOutput(keyPair.getAddress(), 1000) assert.strictEqual(vout, 0) @@ -237,6 +242,7 @@ describe('TransactionBuilder', function () { describe('setLockTime', function () { it('throws if if there exist any scriptSigs', function () { + var txb = new TransactionBuilder() txb.addInput(txHash, 0) txb.sign(0, keyPair) @@ -249,7 +255,7 @@ describe('TransactionBuilder', function () { describe('sign', function () { fixtures.invalid.sign.forEach(function (f) { it('throws on ' + f.exception + (f.description ? ' (' + f.description + ')' : ''), function () { - txb = construct(f, false) + var txb = construct(f, false) f.inputs.forEach(function (input, index) { input.signs.forEach(function (sign) { @@ -277,8 +283,7 @@ describe('TransactionBuilder', function () { describe('build', function () { fixtures.valid.build.forEach(function (f) { it('builds "' + f.description + '"', function () { - txb = construct(f) - + var txb = construct(f) var tx = txb.build() assert.strictEqual(tx.toHex(), f.txHex) }) @@ -286,6 +291,8 @@ describe('TransactionBuilder', function () { fixtures.invalid.build.forEach(function (f) { describe('for ' + (f.description || f.exception), function () { + var txb + beforeEach(function () { if (f.txHex) { txb = TransactionBuilder.fromTransaction(Transaction.fromHex(f.txHex)) @@ -311,8 +318,7 @@ describe('TransactionBuilder', function () { describe('multisig', function () { fixtures.valid.multisig.forEach(function (f) { it(f.description, function () { - txb = construct(f, false) - + var txb = construct(f, false) var tx var network = NETWORKS[f.network] @@ -358,19 +364,26 @@ describe('TransactionBuilder', function () { }) describe('multisig edge case', function () { + var network = NETWORKS.testnet + it('should handle badly pre-filled OP_0s', function () { - var lameTx = Transaction.fromHex('0100000001cff58855426469d0ef16442ee9c644c4fb13832467bcbc3173168a7916f0714900000000fd16010000483045022100daf0f4f3339d9fbab42b098045c1e4958ee3b308f4ae17be80b63808558d0adb02202f07e3d1f79dc8da285ae0d7f68083d769c11f5621ebd9691d6b48c0d4283d7d014cc952410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b84104c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a4104f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e67253aeffffffff01e8030000000000001976a914aa4d7985c57e011a8b3dd8e0e5a73aaef41629c588ac00000000') - var network = NETWORKS.testnet + // OP_0 is used where a signature is missing + var redeemScripSig = bscript.fromASM('OP_0 OP_0 3045022100daf0f4f3339d9fbab42b098045c1e4958ee3b308f4ae17be80b63808558d0adb02202f07e3d1f79dc8da285ae0d7f68083d769c11f5621ebd9691d6b48c0d4283d7d01 52410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b84104c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a4104f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e67253ae') + var redeemScript = bscript.fromASM('OP_2 0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 04c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a 04f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e672 OP_3 OP_CHECKMULTISIG') - txb = TransactionBuilder.fromTransaction(lameTx, network) + var tx = new Transaction() + tx.addInput(new Buffer('cff58855426469d0ef16442ee9c644c4fb13832467bcbc3173168a7916f07149', 'hex'), 0, undefined, redeemScripSig) + tx.addOutput(new Buffer('76a914aa4d7985c57e011a8b3dd8e0e5a73aaef41629c588ac', 'hex'), 1000) - var redeemScript = bscript.fromASM('OP_2 0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 04c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a 04f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e672 OP_3 OP_CHECKMULTISIG') + // now import the Transaction + var txb = TransactionBuilder.fromTransaction(tx, NETWORKS.testnet) var keyPair = ECPair.fromWIF('91avARGdfge8E4tZfYLoxeJ5sGBdNJQH4kvjJoQFacbgx3cTMqe', network) txb.sign(0, keyPair, redeemScript) - var tx = txb.build() - assert.equal(tx.toHex(), '0100000001cff58855426469d0ef16442ee9c644c4fb13832467bcbc3173168a7916f0714900000000fd5e0100483045022100daf0f4f3339d9fbab42b098045c1e4958ee3b308f4ae17be80b63808558d0adb02202f07e3d1f79dc8da285ae0d7f68083d769c11f5621ebd9691d6b48c0d4283d7d01483045022100a346c61738304eac5e7702188764d19cdf68f4466196729db096d6c87ce18cdd022018c0e8ad03054b0e7e235cda6bedecf35881d7aa7d94ff425a8ace7220f38af0014cc952410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b84104c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a4104f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e67253aeffffffff01e8030000000000001976a914aa4d7985c57e011a8b3dd8e0e5a73aaef41629c588ac00000000') + var tx2 = txb.build() + assert.equal(tx2.getId(), 'eab59618a564e361adef6d918bd792903c3d41bcf1220137364fb847880467f9') + assert.equal(bscript.toASM(tx2.ins[0].script), 'OP_0 3045022100daf0f4f3339d9fbab42b098045c1e4958ee3b308f4ae17be80b63808558d0adb02202f07e3d1f79dc8da285ae0d7f68083d769c11f5621ebd9691d6b48c0d4283d7d01 3045022100a346c61738304eac5e7702188764d19cdf68f4466196729db096d6c87ce18cdd022018c0e8ad03054b0e7e235cda6bedecf35881d7aa7d94ff425a8ace7220f38af001 52410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b84104c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a4104f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e67253ae') }) }) }) From b3fd50ffd62a8921446e91309c052ecf4c8501cc Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Wed, 28 Sep 2016 00:46:37 +1000 Subject: [PATCH 05/12] TransactionBuilder: refactor extractInput/extractFromOutput --- src/transaction_builder.js | 432 +++++++++++++++++++------------------ 1 file changed, 218 insertions(+), 214 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 - - 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 From 76c7c773110f4cbd6378f1cf338484edfe6149a0 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Wed, 28 Sep 2016 01:05:01 +1000 Subject: [PATCH 06/12] tests: add failing test for #633 --- test/fixtures/transaction_builder.json | 25 ++++++++++++- test/transaction_builder.js | 49 ++++++++++++++++++-------- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/test/fixtures/transaction_builder.json b/test/fixtures/transaction_builder.json index 37be385..e57608e 100644 --- a/test/fixtures/transaction_builder.json +++ b/test/fixtures/transaction_builder.json @@ -633,7 +633,7 @@ { "description": "Incomplete transaction w/ prevTxScript defined", "exception": "Not enough signatures provided", - "alwaysThrows": true, + "incomplete": true, "inputs": [ { "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", @@ -658,6 +658,29 @@ } ] }, + { + "description": "Duplicate transaction outs", + "exception": "Duplicate TxOut: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff:0", + "incomplete": true, + "inputs": [ + { + "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "vout": 0, + "signs": [] + }, + { + "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "vout": 0, + "signs": [] + } + ], + "outputs": [ + { + "script": "OP_DUP OP_HASH160 aa4d7985c57e011a8b3dd8e0e5a73aaef41629c5 OP_EQUALVERIFY OP_CHECKSIG", + "value": 1000 + } + ] + }, { "description": "Complete transaction w/ non-standard inputs", "exception": "nonstandard not supported", diff --git a/test/transaction_builder.js b/test/transaction_builder.js index 53732d4..534f356 100644 --- a/test/transaction_builder.js +++ b/test/transaction_builder.js @@ -285,32 +285,53 @@ describe('TransactionBuilder', function () { it('builds "' + f.description + '"', function () { var txb = construct(f) var tx = txb.build() + assert.strictEqual(tx.toHex(), f.txHex) }) }) + // TODO: remove duplicate test code fixtures.invalid.build.forEach(function (f) { describe('for ' + (f.description || f.exception), function () { - var txb - - beforeEach(function () { - if (f.txHex) { - txb = TransactionBuilder.fromTransaction(Transaction.fromHex(f.txHex)) - } else { - txb = construct(f) - } - }) - it('throws ' + f.exception, function () { assert.throws(function () { + var txb + if (f.txHex) { + txb = TransactionBuilder.fromTransaction(Transaction.fromHex(f.txHex)) + } else { + txb = construct(f) + } + txb.build() }, new RegExp(f.exception)) }) - if (f.alwaysThrows) return - it("doesn't throw if building incomplete", function () { - txb.buildIncomplete() - }) + // if throws on incomplete too, enforce that + if (f.incomplete) { + it('throws ' + f.exception, function () { + assert.throws(function () { + var txb + if (f.txHex) { + txb = TransactionBuilder.fromTransaction(Transaction.fromHex(f.txHex)) + } else { + txb = construct(f) + } + + txb.buildIncomplete() + }, new RegExp(f.exception)) + }) + } else { + it('does not throw if buildIncomplete', function () { + var txb + if (f.txHex) { + txb = TransactionBuilder.fromTransaction(Transaction.fromHex(f.txHex)) + } else { + txb = construct(f) + } + + txb.buildIncomplete() + }) + } }) }) }) From 9e5aac8bacda8e9c1fe09c172f1a3676c5127c6e Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Wed, 28 Sep 2016 01:05:15 +1000 Subject: [PATCH 07/12] TransactionBuilder: fix duplicate txOut detection --- src/transaction_builder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transaction_builder.js b/src/transaction_builder.js index 0c02ce0..e10ca64 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -246,7 +246,7 @@ TransactionBuilder.prototype.__addInputUnsafe = function (txHash, vout, sequence var vin = this.tx.addInput(txHash, vout, sequence, scriptSig) this.inputs[vin] = input - this.prevTxMap[prevTxOut] = vin + this.prevTxMap[prevTxOut] = true return vin } From d3ccbb62776d3718f8ad83ddcb28d4b8179afb27 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Wed, 28 Sep 2016 01:12:39 +1000 Subject: [PATCH 08/12] TransactionBuilder: rename index to vout internally --- src/transaction_builder.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/transaction_builder.js b/src/transaction_builder.js index e10ca64..99b261b 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -256,7 +256,7 @@ TransactionBuilder.prototype.addOutput = function (scriptPubKey, value) { // if signatures exist, adding outputs is only acceptable if SIGHASH_NONE or SIGHASH_SINGLE is used // throws if any signatures didn't use SIGHASH_NONE|SIGHASH_SINGLE - if (!this.inputs.every(function (input, index) { + if (!this.inputs.every(function (input, i) { // no signature if (input.hashType === undefined) return true @@ -264,7 +264,7 @@ TransactionBuilder.prototype.addOutput = function (scriptPubKey, value) { if (hashTypeMod === Transaction.SIGHASH_NONE) return true if (hashTypeMod === Transaction.SIGHASH_SINGLE) { // account for SIGHASH_SINGLE signing of a non-existing output, aka the "SIGHASH_SINGLE" bug - return index < nOutputs + return i < nOutputs } return false @@ -357,7 +357,7 @@ TransactionBuilder.prototype.__build = function (allowIncomplete) { var tx = this.tx.clone() // Create script signatures from inputs - this.inputs.forEach(function (input, index) { + this.inputs.forEach(function (input, i) { var scriptType = input.redeemScriptType || input.prevOutType if (!allowIncomplete) { @@ -374,18 +374,18 @@ TransactionBuilder.prototype.__build = function (allowIncomplete) { // build a scriptSig var scriptSig = buildFromInputData(input, scriptType, allowIncomplete) - tx.setInputScript(index, scriptSig) + tx.setInputScript(i, scriptSig) }) return tx } -TransactionBuilder.prototype.sign = function (index, keyPair, redeemScript, hashType) { +TransactionBuilder.prototype.sign = function (vin, keyPair, redeemScript, hashType) { if (keyPair.network !== this.network) throw new Error('Inconsistent network') - if (!this.inputs[index]) throw new Error('No input at index: ' + index) + if (!this.inputs[vin]) throw new Error('No input at index: ' + vin) hashType = hashType || Transaction.SIGHASH_ALL - var input = this.inputs[index] + var input = this.inputs[vin] var canSign = input.hashType !== undefined && input.prevOutScript !== undefined && input.pubKeys !== undefined && @@ -453,7 +453,7 @@ TransactionBuilder.prototype.sign = function (index, keyPair, redeemScript, hash // ready to sign? var hashScript = input.redeemScript || input.prevOutScript - var signatureHash = this.tx.hashForSignature(index, hashScript, hashType) + var signatureHash = this.tx.hashForSignature(vin, hashScript, hashType) // enforce in order signing of public keys var valid = input.pubKeys.some(function (pubKey, i) { From a1d3e33c84fc97df240497587f97b439fe8c913c Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Wed, 28 Sep 2016 08:44:21 +1000 Subject: [PATCH 09/12] address/crypto/script/types: adhere to no-use-before-define --- src/address.js | 18 +++++++------- src/crypto.js | 16 ++++++------ src/script.js | 66 +++++++++++++++++++++++++------------------------- src/types.js | 2 +- 4 files changed, 51 insertions(+), 51 deletions(-) diff --git a/src/address.js b/src/address.js index d7e5c6d..656ec37 100644 --- a/src/address.js +++ b/src/address.js @@ -15,15 +15,6 @@ function fromBase58Check (address) { return { hash: hash, version: version } } -function fromOutputScript (scriptPubKey, network) { - network = network || networks.bitcoin - - if (bscript.isPubKeyHashOutput(scriptPubKey)) return toBase58Check(bscript.compile(scriptPubKey).slice(3, 23), network.pubKeyHash) - if (bscript.isScriptHashOutput(scriptPubKey)) return toBase58Check(bscript.compile(scriptPubKey).slice(2, 22), network.scriptHash) - - throw new Error(bscript.toASM(scriptPubKey) + ' has no matching Address') -} - function toBase58Check (hash, version) { typeforce(types.tuple(types.Hash160bit, types.UInt8), arguments) @@ -34,6 +25,15 @@ function toBase58Check (hash, version) { return bs58check.encode(payload) } +function fromOutputScript (scriptPubKey, network) { + network = network || networks.bitcoin + + if (bscript.isPubKeyHashOutput(scriptPubKey)) return toBase58Check(bscript.compile(scriptPubKey).slice(3, 23), network.pubKeyHash) + if (bscript.isScriptHashOutput(scriptPubKey)) return toBase58Check(bscript.compile(scriptPubKey).slice(2, 22), network.scriptHash) + + throw new Error(bscript.toASM(scriptPubKey) + ' has no matching Address') +} + function toOutputScript (address, network) { network = network || networks.bitcoin diff --git a/src/crypto.js b/src/crypto.js index 3c1cb73..1bb39f1 100644 --- a/src/crypto.js +++ b/src/crypto.js @@ -1,13 +1,5 @@ var createHash = require('create-hash') -function hash160 (buffer) { - return ripemd160(sha256(buffer)) -} - -function hash256 (buffer) { - return sha256(sha256(buffer)) -} - function ripemd160 (buffer) { return createHash('rmd160').update(buffer).digest() } @@ -20,6 +12,14 @@ function sha256 (buffer) { return createHash('sha256').update(buffer).digest() } +function hash160 (buffer) { + return ripemd160(sha256(buffer)) +} + +function hash256 (buffer) { + return sha256(sha256(buffer)) +} + module.exports = { hash160: hash160, hash256: hash256, diff --git a/src/script.js b/src/script.js index 07f8551..1592a39 100644 --- a/src/script.js +++ b/src/script.js @@ -15,32 +15,6 @@ var REVERSE_OPS = (function () { var OP_INT_BASE = OPS.OP_RESERVED // OP_1 - 1 -function toASM (chunks) { - if (Buffer.isBuffer(chunks)) { - chunks = decompile(chunks) - } - - return chunks.map(function (chunk) { - // data? - if (Buffer.isBuffer(chunk)) return chunk.toString('hex') - - // opcode! - return REVERSE_OPS[chunk] - }).join(' ') -} - -function fromASM (asm) { - typeforce(types.String, asm) - - return compile(asm.split(' ').map(function (chunkStr) { - // opcode? - if (OPS[chunkStr] !== undefined) return OPS[chunkStr] - - // data! - return new Buffer(chunkStr, 'hex') - })) -} - function compile (chunks) { // TODO: remove me if (Buffer.isBuffer(chunks)) return chunks @@ -118,6 +92,32 @@ function decompile (buffer) { return chunks } +function toASM (chunks) { + if (Buffer.isBuffer(chunks)) { + chunks = decompile(chunks) + } + + return chunks.map(function (chunk) { + // data? + if (Buffer.isBuffer(chunk)) return chunk.toString('hex') + + // opcode! + return REVERSE_OPS[chunk] + }).join(' ') +} + +function fromASM (asm) { + typeforce(types.String, asm) + + return compile(asm.split(' ').map(function (chunkStr) { + // opcode? + if (OPS[chunkStr] !== undefined) return OPS[chunkStr] + + // data! + return new Buffer(chunkStr, 'hex') + })) +} + function isCanonicalPubKey (buffer) { if (!Buffer.isBuffer(buffer)) return false if (buffer.length < 33) return false @@ -133,13 +133,6 @@ function isCanonicalPubKey (buffer) { return false } -function isCanonicalSignature (buffer) { - if (!Buffer.isBuffer(buffer)) return false - if (!isDefinedHashType(buffer[buffer.length - 1])) return false - - return bip66.check(buffer.slice(0, -1)) -} - function isDefinedHashType (hashType) { var hashTypeMod = hashType & ~0x80 @@ -147,6 +140,13 @@ function isDefinedHashType (hashType) { return hashTypeMod > 0x00 && hashTypeMod < 0x04 } +function isCanonicalSignature (buffer) { + if (!Buffer.isBuffer(buffer)) return false + if (!isDefinedHashType(buffer[buffer.length - 1])) return false + + return bip66.check(buffer.slice(0, -1)) +} + function isPubKeyHashInput (script) { var chunks = decompile(script) diff --git a/src/types.js b/src/types.js index 69ab787..735f770 100644 --- a/src/types.js +++ b/src/types.js @@ -1,7 +1,7 @@ var typeforce = require('typeforce') function nBuffer (value, n) { - typeforce(types.Buffer, value) + typeforce(typeforce.Buffer, value) if (value.length !== n) throw new typeforce.TfTypeError('Expected ' + (n * 8) + '-bit Buffer, got ' + (value.length * 8) + '-bit Buffer') return true From 402e871d40dea929f88616f945294846b5a4079b Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Wed, 28 Sep 2016 15:22:47 +1000 Subject: [PATCH 10/12] TransactionBuilder: move sign bulk to prepareInput --- src/transaction_builder.js | 208 ++++++++++++++++++------------------- 1 file changed, 104 insertions(+), 104 deletions(-) diff --git a/src/transaction_builder.js b/src/transaction_builder.js index 99b261b..c2238c2 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -124,6 +124,107 @@ function expandOutput (script, ourPubKey) { } } +function buildInput (input, scriptType, allowIncomplete) { + var signatures = input.signatures + var scriptSig + + switch (scriptType) { + case 'pubkeyhash': + // 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': + // 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': + signatures = signatures.map(function (signature) { + return signature && signature.toScriptSignature(input.hashType) + }) + + if (allowIncomplete) { + // fill in blanks with OP_0 + for (var i = 0; i < signatures.length; ++i) { + signatures[i] = signatures[i] || ops.OP_0 + } + } else { + // remove blank signatures + signatures = signatures.filter(function (x) { return x }) + } + + scriptSig = bscript.multisigInput(signatures, allowIncomplete ? undefined : input.redeemScript) + break + } + + // wrap as scriptHash if necessary + if (input.prevOutType === 'scripthash') { + scriptSig = bscript.scriptHashInput(scriptSig, input.redeemScript) + } + + return scriptSig +} + +function prepareInput (input, kpPubKey, redeemScript, hashType) { + if (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.prevOutType) { + if (input.prevOutType !== 'scripthash') throw new Error('PrevOutScript must be P2SH') + + var prevOutScriptScriptHash = bscript.decompile(input.prevOutScript)[1] + if (!bufferEquals(prevOutScriptScriptHash, redeemScriptHash)) throw new Error('Inconsistent hash160(RedeemScript)') + + // or, we don't have a prevOutScript, so generate a P2SH script + } else { + input.prevOutScript = bscript.scriptHashOutput(redeemScriptHash) + input.prevOutType = 'scripthash' + } + + 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 = expanded.scriptType + input.signatures = expanded.signatures + + // maybe we have some prior knowledge + } else if (input.prevOutType) { + // pay-to-scriptHash is not possible without a redeemScript + if (input.prevOutType === 'scripthash') throw new Error('PrevOutScript is P2SH, missing redeemScript') + + // throw if we can't sign with it + if (!input.pubKeys || !input.signatures) throw new Error(input.prevOutType + ' not supported') + + // no prior knowledge, assume pubKeyHash + } else { + input.prevOutScript = bscript.pubKeyHashOutput(bcrypto.hash160(kpPubKey)) + input.prevOutType = 'pubkeyhash' + + input.pubKeys = [kpPubKey] + input.signatures = [undefined] + } + + input.hashType = hashType +} + function TransactionBuilder (network) { this.prevTxMap = {} this.network = network || networks.bitcoin @@ -293,61 +394,6 @@ var canBuildTypes = { 'pubkeyhash': true } -function buildFromInputData (input, scriptType, allowIncomplete) { - var signatures = input.signatures - var scriptSig - - switch (scriptType) { - case 'pubkeyhash': - // 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': - // 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': - signatures = signatures.map(function (signature) { - return signature && signature.toScriptSignature(input.hashType) - }) - - if (allowIncomplete) { - // fill in blanks with OP_0 - for (var i = 0; i < signatures.length; ++i) { - signatures[i] = signatures[i] || ops.OP_0 - } - } else { - // remove blank signatures - signatures = signatures.filter(function (x) { return x }) - } - - scriptSig = bscript.multisigInput(signatures, allowIncomplete ? undefined : input.redeemScript) - break - } - - // wrap as scriptHash if necessary - if (input.prevOutType === 'scripthash') { - scriptSig = bscript.scriptHashInput(scriptSig, input.redeemScript) - } - - return scriptSig -} - TransactionBuilder.prototype.__build = function (allowIncomplete) { if (!allowIncomplete) { if (!this.tx.ins.length) throw new Error('Transaction has no inputs') @@ -373,7 +419,7 @@ TransactionBuilder.prototype.__build = function (allowIncomplete) { if (!input.signatures) return // build a scriptSig - var scriptSig = buildFromInputData(input, scriptType, allowIncomplete) + var scriptSig = buildInput(input, scriptType, allowIncomplete) tx.setInputScript(i, scriptSig) }) @@ -394,7 +440,6 @@ TransactionBuilder.prototype.sign = function (vin, keyPair, redeemScript, hashTy var kpPubKey = keyPair.getPublicKeyBuffer() - // are we ready to sign? if (canSign) { // if redeemScript was provided, enforce consistency if (redeemScript) { @@ -402,56 +447,11 @@ TransactionBuilder.prototype.sign = function (vin, keyPair, redeemScript, hashTy } if (input.hashType !== hashType) throw new Error('Inconsistent hashType') - - // no? prepare } else { - if (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 prevOutScriptScriptHash = bscript.decompile(input.prevOutScript)[1] - if (!bufferEquals(prevOutScriptScriptHash, redeemScriptHash)) throw new Error('Inconsistent hash160(RedeemScript)') - - // or, we don't have a prevOutScript, so generate a P2SH script - } else { - input.prevOutScript = bscript.scriptHashOutput(redeemScriptHash) - input.prevOutType = 'scripthash' - } - - 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 = 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 - if (!input.prevOutType) { - input.prevOutScript = bscript.pubKeyHashOutput(bcrypto.hash160(kpPubKey)) - input.prevOutType = 'pubkeyhash' - - input.pubKeys = [kpPubKey] - input.signatures = [undefined] - } else { - // throw if we can't sign with it - if (!input.pubKeys || !input.signatures) throw new Error(input.prevOutType + ' not supported') - } - } - - input.hashType = hashType + prepareInput(input, kpPubKey, redeemScript, hashType) } - // ready to sign? + // ready to sign var hashScript = input.redeemScript || input.prevOutScript var signatureHash = this.tx.hashForSignature(vin, hashScript, hashType) From ccde09fdc38481a7c8cb5dcbcd513507f6c19096 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Wed, 28 Sep 2016 15:27:14 +1000 Subject: [PATCH 11/12] TransactionBuilder: move fixMultisigOrder back to free function --- src/transaction_builder.js | 71 +++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/src/transaction_builder.js b/src/transaction_builder.js index c2238c2..75f53c5 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -225,6 +225,40 @@ function prepareInput (input, kpPubKey, redeemScript, hashType) { input.hashType = hashType } +function fixMultisigOrder (input, transaction, 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 + }) +} + function TransactionBuilder (network) { this.prevTxMap = {} this.network = network || networks.bitcoin @@ -280,7 +314,7 @@ TransactionBuilder.fromTransaction = function (transaction, network) { if (!input.pubKeys || !input.signatures) return if (input.pubKeys.length === input.signatures.length) return - txb.__fixMultisigOrder(transaction, i) + fixMultisigOrder(input, transaction, i) } }) @@ -467,39 +501,4 @@ TransactionBuilder.prototype.sign = function (vin, keyPair, redeemScript, hashTy 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 From 1aab317dad208959446b2b9e1ef9a949e309c6cf Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Wed, 28 Sep 2016 16:46:35 +1000 Subject: [PATCH 12/12] TransactionBuilder: extract internal signature invalidation functions for individual testing --- src/transaction_builder.js | 59 ++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/src/transaction_builder.js b/src/transaction_builder.js index 75f53c5..efdaae0 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -322,14 +322,7 @@ TransactionBuilder.fromTransaction = function (transaction, network) { } 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 - })) { + if (!this.__canModifyInputs()) { throw new Error('No, this would invalidate signatures') } @@ -387,23 +380,7 @@ TransactionBuilder.prototype.__addInputUnsafe = function (txHash, vout, sequence } TransactionBuilder.prototype.addOutput = function (scriptPubKey, value) { - var nOutputs = this.tx.outs.length - - // if signatures exist, adding outputs is only acceptable if SIGHASH_NONE or SIGHASH_SINGLE is used - // throws if any signatures didn't use SIGHASH_NONE|SIGHASH_SINGLE - if (!this.inputs.every(function (input, i) { - // no signature - if (input.hashType === undefined) return true - - var hashTypeMod = input.hashType & 0x1f - if (hashTypeMod === Transaction.SIGHASH_NONE) return true - if (hashTypeMod === Transaction.SIGHASH_SINGLE) { - // account for SIGHASH_SINGLE signing of a non-existing output, aka the "SIGHASH_SINGLE" bug - return i < nOutputs - } - - return false - })) { + if (!this.__canModifyOutputs()) { throw new Error('No, this would invalidate signatures') } @@ -501,4 +478,36 @@ TransactionBuilder.prototype.sign = function (vin, keyPair, redeemScript, hashTy if (!valid) throw new Error('Key pair cannot sign for this input') } +TransactionBuilder.prototype.__canModifyInputs = function () { + return this.inputs.every(function (otherInput) { + // no signature + if (otherInput.hashType === undefined) return true + + // if SIGHASH_ANYONECANPAY is set, signatures would not + // be invalidated by more inputs + return otherInput.hashType & Transaction.SIGHASH_ANYONECANPAY + }) +} + +TransactionBuilder.prototype.__canModifyOutputs = function () { + var nInputs = this.tx.ins.length + var nOutputs = this.tx.outs.length + + return this.inputs.every(function (input, i) { + // any signatures? + if (input.hashType === undefined) return true + + var hashTypeMod = input.hashType & 0x1f + if (hashTypeMod === Transaction.SIGHASH_NONE) return true + if (hashTypeMod === Transaction.SIGHASH_SINGLE) { + // if SIGHASH_SINGLE is set, and nInputs > nOutputs + // some signatures would be invalidated by the addition + // of more outputs + return nInputs <= nOutputs + } + + return false + }) +} + module.exports = TransactionBuilder