diff --git a/src/scripts.js b/src/scripts.js index 606e14b..e58ce99 100644 --- a/src/scripts.js +++ b/src/scripts.js @@ -1,6 +1,6 @@ var assert = require('assert') -var typeForce = require('typeforce') var ops = require('./opcodes') +var typeForce = require('typeforce') var ecurve = require('ecurve') var curve = ecurve.getCurveByName('secp256k1') @@ -63,7 +63,7 @@ function isPubKeyOutput(script) { script.chunks[1] === ops.OP_CHECKSIG } -function isScriptHashInput(script) { +function isScriptHashInput(script, allowIncomplete) { if (script.chunks.length < 2) return false var lastChunk = script.chunks[script.chunks.length - 1] @@ -72,7 +72,7 @@ function isScriptHashInput(script) { var scriptSig = Script.fromChunks(script.chunks.slice(0, -1)) var scriptPubKey = Script.fromBuffer(lastChunk) - return classifyInput(scriptSig) === classifyOutput(scriptPubKey) + return classifyInput(scriptSig, allowIncomplete) === classifyOutput(scriptPubKey) } function isScriptHashOutput(script) { @@ -83,9 +83,19 @@ function isScriptHashOutput(script) { script.chunks[2] === ops.OP_EQUAL } -function isMultisigInput(script) { - return script.chunks[0] === ops.OP_0 && - script.chunks.slice(1).every(isCanonicalSignature) +// allowIncomplete is to account for combining signatures +// See https://github.com/bitcoin/bitcoin/blob/f425050546644a36b0b8e0eb2f6934a3e0f6f80f/src/script/sign.cpp#L195-L197 +function isMultisigInput(script, allowIncomplete) { + if (script.chunks.length < 2) return false + if (script.chunks[0] !== ops.OP_0) return false + + if (allowIncomplete) { + return script.chunks.slice(1).every(function(chunk) { + return chunk === ops.OP_0 || isCanonicalSignature(chunk) + }) + } + + return script.chunks.slice(1).every(isCanonicalSignature) } function isMultisigOutput(script) { @@ -134,15 +144,15 @@ function classifyOutput(script) { return 'nonstandard' } -function classifyInput(script) { +function classifyInput(script, allowIncomplete) { typeForce('Script', script) if (isPubKeyHashInput(script)) { return 'pubkeyhash' - } else if (isScriptHashInput(script)) { - return 'scripthash' - } else if (isMultisigInput(script)) { + } else if (isMultisigInput(script, allowIncomplete)) { return 'multisig' + } else if (isScriptHashInput(script, allowIncomplete)) { + return 'scripthash' } else if (isPubKeyInput(script)) { return 'pubkey' } @@ -234,8 +244,13 @@ function multisigInput(signatures, scriptPubKey) { var m = mOp - (ops.OP_1 - 1) var n = nOp - (ops.OP_1 - 1) - assert(signatures.length >= m, 'Not enough signatures provided') - assert(signatures.length <= n, 'Too many signatures provided') + var count = 0 + signatures.forEach(function(signature) { + count += (signature !== ops.OP_0) + }) + + assert(count >= m, 'Not enough signatures provided') + assert(count <= n, 'Too many signatures provided') } return Script.fromChunks([].concat(ops.OP_0, signatures)) diff --git a/src/transaction_builder.js b/src/transaction_builder.js index 71a46ec..2cf785c 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -1,4 +1,5 @@ var assert = require('assert') +var ops = require('./opcodes') var scripts = require('./scripts') var ECPubKey = require('./ecpubkey') @@ -44,23 +45,23 @@ TransactionBuilder.fromTransaction = function(transaction) { var redeemScript var scriptSig = txIn.script - var scriptType = scripts.classifyInput(scriptSig) + var scriptType = scripts.classifyInput(scriptSig, true) // Re-classify if P2SH if (scriptType === 'scripthash') { redeemScript = Script.fromBuffer(scriptSig.chunks.slice(-1)[0]) scriptSig = Script.fromChunks(scriptSig.chunks.slice(0, -1)) - scriptType = scripts.classifyInput(scriptSig) + scriptType = scripts.classifyInput(scriptSig, true) assert.equal(scripts.classifyOutput(redeemScript), scriptType, 'Non-matching scriptSig and scriptPubKey in input') } // Extract hashType, pubKeys and signatures - var hashType, pubKeys, signatures + var hashType, parsed, pubKeys, signatures switch (scriptType) { case 'pubkeyhash': - var parsed = ECSignature.parseScriptSignature(scriptSig.chunks[0]) + parsed = ECSignature.parseScriptSignature(scriptSig.chunks[0]) var pubKey = ECPubKey.fromBuffer(scriptSig.chunks[1]) hashType = parsed.hashType @@ -70,10 +71,9 @@ TransactionBuilder.fromTransaction = function(transaction) { break case 'multisig': - var scriptSigs = scriptSig.chunks.slice(1) // ignore OP_0 - var parsed = scriptSigs.map(function(scriptSig) { - return ECSignature.parseScriptSignature(scriptSig) - }) + parsed = scriptSig.chunks.slice(1).filter(function(chunk) { + return chunk !== ops.OP_0 + }).map(ECSignature.parseScriptSignature) hashType = parsed[0].hashType pubKeys = [] @@ -82,7 +82,7 @@ TransactionBuilder.fromTransaction = function(transaction) { break case 'pubkey': - var parsed = ECSignature.parseScriptSignature(scriptSig.chunks[0]) + parsed = ECSignature.parseScriptSignature(scriptSig.chunks[0]) hashType = parsed.hashType pubKeys = [] diff --git a/test/fixtures/scripts.json b/test/fixtures/scripts.json index 6ae7863..442c4a9 100644 --- a/test/fixtures/scripts.json +++ b/test/fixtures/scripts.json @@ -53,6 +53,51 @@ "type": "nulldata", "data": "deadffffffffffffffffffffffffffffffffbeef", "scriptPubKey": "OP_RETURN deadffffffffffffffffffffffffffffffffbeef" + }, + { + "type": "nonstandard", + "typeIncomplete": "multisig", + "pubKeys": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "02b80011a883a0fd621ad46dfc405df1e74bf075cbaf700fd4aebef6e96f848340", + "024289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34" + ], + "signatures": [ + null, + "3044022001ab168e80b863fdec694350b587339bb72a37108ac3c989849251444d13ebba02201811272023e3c1038478eb972a82d3ad431bfc2408e88e4da990f1a7ecbb263901", + "3045022100aaeb7204c17eee2f2c4ff1c9f8b39b79e75e7fbf33e92cc67ac51be8f15b75f90220659eee314a4943a6384d2b154fa5821ef7a084814d7ee2c6f9f7f0ffb53be34b01" + ], + "scriptSig": "OP_0 OP_0 3044022001ab168e80b863fdec694350b587339bb72a37108ac3c989849251444d13ebba02201811272023e3c1038478eb972a82d3ad431bfc2408e88e4da990f1a7ecbb263901 3045022100aaeb7204c17eee2f2c4ff1c9f8b39b79e75e7fbf33e92cc67ac51be8f15b75f90220659eee314a4943a6384d2b154fa5821ef7a084814d7ee2c6f9f7f0ffb53be34b01" + }, + { + "type": "nonstandard", + "typeIncomplete": "multisig", + "pubKeys": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "02b80011a883a0fd621ad46dfc405df1e74bf075cbaf700fd4aebef6e96f848340", + "024289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34" + ], + "signatures": [ + null, + null, + null + ], + "scriptSig": "OP_0 OP_0 OP_0 OP_0" + }, + { + "type": "nonstandard", + "typeIncomplete": "scripthash", + "pubKeys": [ + "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", + "04c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a" + ], + "signatures": [ + null, + "30450221009c92c1ae1767ac04e424da7f6db045d979b08cde86b1ddba48621d59a109d818022004f5bb21ad72255177270abaeb2d7940ac18f1e5ca1f53db4f3fd1045647a8a801" + ], + "redeemScript": "OP_2 0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 04c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a OP_2 OP_CHECKMULTISIG", + "redeemScriptSig": "OP_0 OP_0 30450221009c92c1ae1767ac04e424da7f6db045d979b08cde86b1ddba48621d59a109d818022004f5bb21ad72255177270abaeb2d7940ac18f1e5ca1f53db4f3fd1045647a8a801", + "scriptSig": "OP_0 OP_0 30450221009c92c1ae1767ac04e424da7f6db045d979b08cde86b1ddba48621d59a109d818022004f5bb21ad72255177270abaeb2d7940ac18f1e5ca1f53db4f3fd1045647a8a801 52410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b84104c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a52ae" } ], "invalid": { @@ -121,6 +166,18 @@ } ], "multisigInput": [ + { + "description": "Not enough signatures provided", + "type": "multisig", + "pubKeys": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "02b80011a883a0fd621ad46dfc405df1e74bf075cbaf700fd4aebef6e96f848340" + ], + "signatures": [ + null, + null + ] + }, { "exception": "Not enough signatures provided", "pubKeys": [ diff --git a/test/scripts.js b/test/scripts.js index ac35a65..fae41df 100644 --- a/test/scripts.js +++ b/test/scripts.js @@ -1,4 +1,5 @@ var assert = require('assert') +var ops = require('../src/opcodes') var scripts = require('../src/scripts') var ECPubKey = require('../src/ecpubkey') @@ -22,6 +23,18 @@ describe('Scripts', function() { assert.equal(type, f.type) }) }) + + fixtures.valid.forEach(function(f) { + if (!f.scriptSig) return + if (!f.typeIncomplete) return + + it('classifies incomplete ' + f.scriptSig + ' as ' + f.typeIncomplete, function() { + var script = Script.fromASM(f.scriptSig) + var type = scripts.classifyInput(script, true) + + assert.equal(type, f.typeIncomplete) + }) + }) }) describe('classifyOutput', function() { @@ -51,6 +64,16 @@ describe('Scripts', function() { assert.equal(inputFn(script), expected) }) + + if (f.typeIncomplete) { + var expectedIncomplete = type.toLowerCase() === f.typeIncomplete + + it('returns ' + expected + ' for ' + f.scriptSig, function() { + var script = Script.fromASM(f.scriptSig) + + assert.equal(inputFn(script, true), expectedIncomplete) + }) + } } }) }) @@ -131,7 +154,7 @@ describe('Scripts', function() { it('returns ' + f.scriptSig, function() { var signatures = f.signatures.map(function(signature) { - return new Buffer(signature, 'hex') + return signature ? new Buffer(signature, 'hex') : ops.OP_0 }) var scriptSig = scripts.multisigInput(signatures) @@ -145,7 +168,7 @@ describe('Scripts', function() { it('throws on ' + f.exception, function() { var signatures = f.signatures.map(function(signature) { - return new Buffer(signature, 'hex') + return signature ? new Buffer(signature, 'hex') : ops.OP_0 }) assert.throws(function() {