diff --git a/js/models/core/TxProposal.js b/js/models/core/TxProposal.js index 5ee3432b4..3efad2b89 100644 --- a/js/models/core/TxProposal.js +++ b/js/models/core/TxProposal.js @@ -24,7 +24,7 @@ function TxProposal(opts) { this.builder = opts.builder; this.inputChainPaths = opts.inputChainPaths; - this._inputSignedBy = []; + this._inputSignatures = []; this.seenBy = opts.seenBy || {}; this.signedBy = opts.signedBy || {}; this.rejectedBy = opts.rejectedBy || {}; @@ -69,25 +69,24 @@ TxProposal.fromObj = function(o, forceOpts) { }; } var t = new TxProposal(o); - t._throwIfInvalid(); + t._check(); t._updateSignedBy(); return t; }; -TxProposal._formatKeys = function(allowedPubKeys) { - var keys = []; - for (var i in allowedPubKeys) { - if (!Buffer.isBuffer(allowedPubKeys[i])) - throw new Error('allowedPubKeys must be buffers'); +TxProposal._formatKeys = function(keys) { + var ret = []; + for (var i in keys) { + if (!Buffer.isBuffer(keys[i])) + throw new Error('keys must be buffers'); var k = new Key(); - k.public = allowedPubKeys[i]; - keys.push(k); + k.public = keys[i]; + ret.push(k); }; - - return keys; + return ret; }; TxProposal._verifySignatures = function(inKeys, scriptSig, txSigHash) { @@ -107,13 +106,13 @@ TxProposal._verifySignatures = function(inKeys, scriptSig, txSigHash) { if (k.verifySignatureSync(txSigHash, sigRaw)) { ret.push(parseInt(j)); break; - } + } } } return ret; }; -TxProposal._keysFromRedeemScript = function(s) { +TxProposal._infoFromRedeemScript = function(s) { var redeemScript = new Script(s.chunks[s.chunks.length - 1]); if (!redeemScript) throw new Error('Bad scriptSig'); @@ -121,71 +120,66 @@ TxProposal._keysFromRedeemScript = function(s) { if (!pubkeys || !pubkeys.length) throw new Error('Bad scriptSig'); - return pubkeys; + return { + keys: pubkeys, + scriptBuf: redeemScript.getBuffer() + }; }; TxProposal.prototype._updateSignedBy = function() { - this._inputSignedBy = []; + this._inputSignatures = []; var tx = this.builder.build(); for (var i in tx.ins) { var scriptSig = new Script(tx.ins[i].s); - - var keys = TxProposal._keysFromRedeemScript(scriptSig); - var txSigHash = tx.hashForSignature(this.builder.inputMap[i].scriptPubKey, i, Transaction.SIGHASH_ALL); - var copayerIndex = this._verifySignatures(keys, scriptSig, txSigHash); - if (typeof copayerIndex === 'undefined') + var signatureCount = scriptSig.countSignatures(); + var info = TxProposal._infoFromRedeemScript(scriptSig); + var txSigHash = tx.hashForSignature(info.scriptBuf, i, Transaction.SIGHASH_ALL); + var signatureIndexes = TxProposal._verifySignatures(info.keys, scriptSig, txSigHash); + if (signatureIndexes.length !== signatureCount) throw new Error('Invalid signature'); - this._inputSignedBy[i] = this._inputSignedBy[i] || {}; - this._inputSignedBy[i][copayerIndex] = true; + this._inputSignatures[i] = signatureIndexes.map(function(i) { + return info.keys[i].toString('hex'); + }); }; }; -TxProposal.prototype.isValid = function() { +TxProposal.prototype._check = function() { if (this.builder.signhash && this.builder.signhash !== Transaction.SIGHASH_ALL) { - return false; + throw new Error('Invalid tx proposal'); } var tx = this.builder.build(); if (!tx.ins.length) - return false; + throw new Error('Invalid tx proposal: no ins'); + + var scriptSigs = this.builder.vanilla.scriptSigs; + if (!scriptSigs || !scriptSigs.length) { + throw new Error('Invalid tx proposal: no signatures'); + } for (var i = 0; i < tx.ins.length; i++) { var hashType = tx.getHashType(i); - if (hashType && hashType !== Transaction.SIGHASH_ALL) { - return false; - } + if (hashType && hashType !== Transaction.SIGHASH_ALL) + throw new Error('Invalid tx proposal: bad signatures'); } - - console.log('[TxProposal.js.145]'); //TODO - - //Should be signed - var scriptSigs = this.builder.vanilla.scriptSigs; - if (!scriptSigs) - return false; - - - console.log('[TxProposal.js.153]'); //TODO - return true; }; -TxProposal.prototype._throwIfInvalid = function(allowedPubKeys) { - if (!this.isValid(allowedPubKeys)) - throw new Error('Invalid tx proposal'); +TxProposal.prototype.mergeBuilder = function(incoming) { + var b0 = this.builder; + var b1 = incoming.builder; + + var before = JSON.stringify(b0.toObj()); + b0.merge(b1); + var after = JSON.stringify(b0.toObj()); + return after !== before; }; -TxProposal.prototype.merge = function(incoming, allowedPubKeys) { - var ret = {}; - ret.events = []; - incoming._throwIfInvalid(allowedPubKeys); - - /* TODO */ - - /* - events.push({ +/* OTDO + events.push({ type: 'seen', cId: k, txId: ntxid @@ -202,29 +196,48 @@ txId: ntxid }); ret.events = this.mergeMetadata(incoming); */ - ret.hasChanged = this.mergeBuilder(incoming); + + +TxProposal.prototype._allSignatures = function() { + var ret = {}; + for(var i in this._inputSignatures) + for (var j in this._inputSignatures[i]) + ret[this._inputSignatures[i][j]] = true; + return ret; }; -TxProposal.prototype.mergeBuilder = function(incoming) { - var b0 = this.builder; - var b1 = incoming.builder; +TxProposal.prototype.merge = function(incoming) { + var ret = {}; + var newSignatures = []; - var before = JSON.stringify(b0.toObj()); - b0.merge(b1); - var after = JSON.stringify(b0.toObj()); - return after !== before; + incoming._check(); + incoming._updateSignedBy(); + + var prevInputSignatures = this._allSignatures(); + + ret.hasChanged = this.mergeBuilder(incoming); + this._updateSignedBy(); + + if (ret.hasChanged) + for(var i in this._inputSignatures) + for (var j in this._inputSignatures[i]) + if (!prevInputSignatures[this._inputSignatures[i][j]]) + newSignatures.push(this._inputSignatures[i][j]); + + ret.newSignatures = newSignatures; + + return ret; }; - //This should be on bitcore / Transaction TxProposal.prototype.countSignatures = function() { var tx = this.builder.build(); - var ret = 0; for (var i in tx.ins) { ret += tx.countInputSignatures(i); } return ret; }; + module.exports = TxProposal; diff --git a/test/mocks/FakeBuilder.js b/test/mocks/FakeBuilder.js index 40fcdf5e7..15e38d5e5 100644 --- a/test/mocks/FakeBuilder.js +++ b/test/mocks/FakeBuilder.js @@ -2,16 +2,22 @@ var bitcore = bitcore || require('bitcore'); var Script = bitcore.Script; +var VALID_SCRIPTSIG_BUF = new Buffer('0048304502200708a381dde585ef7fdfaeaeb5da9b451d3e22b01eac8a5e3d03b959e24a7478022100c90e76e423523a54a9e9c43858337ebcef1a539a7fc685c2698dd8648fcf1b9101473044022030a77c9613d6ee010717c1abc494668d877e3fa0ae4c520f65cc3b308754c98c02205219d387bcb291bd44805b9468439e4168b02a6a180cdbcc24d84d71d696c1ae014cad532103197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d210380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127210392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed032103a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e32103e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e455ae','hex'); function Tx() { - this.ins = [{s: new Buffer('0048304502200708a381dde585ef7fdfaeaeb5da9b451d3e22b01eac8a5e3d03b959e24a7478022100c90e76e423523a54a9e9c43858337ebcef1a539a7fc685c2698dd8648fcf1b9101473044022030a77c9613d6ee010717c1abc494668d877e3fa0ae4c520f65cc3b308754c98c02205219d387bcb291bd44805b9468439e4168b02a6a180cdbcc24d84d71d696c1ae014cad532103197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d210380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127210392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed032103a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e32103e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e455ae','hex')}]; + this.ins = [{s: VALID_SCRIPTSIG_BUF }]; +}; + + +Tx.prototype.getHashType = function() { + return 1; }; Tx.prototype.getNormalizedHash = function() { return '123456'; }; Tx.prototype.hashForSignature = function() { - return new Buffer('72403de587240b85855da2ebd29b7dab5d170a9662d4bf7c4980358b14091696','hex'); + return new Buffer('31103626e162f1cbfab6b95b08c9f6e78aae128523261cb37f8dfd4783cb09a7', 'hex'); }; @@ -23,8 +29,16 @@ function FakeBuilder() { scriptPubKey: new Script(new Buffer('a914dc0623476aefb049066b09b0147a022e6eb8429187', 'hex')), scriptType: 4, i: 0 }]; + + this.vanilla = { + scriptSigs: [VALID_SCRIPTSIG_BUF], + } } + +FakeBuilder.prototype.merge = function() { +}; + FakeBuilder.prototype.build = function() { return this.tx; }; @@ -33,5 +47,5 @@ FakeBuilder.prototype.build = function() { FakeBuilder.prototype.toObj = function() { return this; }; - +FakeBuilder.VALID_SCRIPTSIG_BUF = VALID_SCRIPTSIG_BUF; module.exports = FakeBuilder; diff --git a/test/test.TxProposal.js b/test/test.TxProposal.js index 138439ba2..b62ce7bf0 100644 --- a/test/test.TxProposal.js +++ b/test/test.TxProposal.js @@ -132,7 +132,7 @@ describe('TxProposal', function() { describe('Signature verification', function() { - var validScriptSig = new bitcore.Script(new Buffer('00483045022100a35a5cbe37e39caa62bf1c347eae9c72be827c190b31494b184943b3012757a8022008a1ff72a34a5bf2fc955aa5b6f8a4c32cb0fab7e54c212a5f6f645bb95b8ef10149304602210092347916c3c3e6f1692bf9447b973779c28ce9985baaa3940b483af573f464b4022100ab91062796ab8acb32a0fa90e00627db5be77d9722400b3ecfd9c5f34a8092b1014cad532103197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d210380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127210392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed032103a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e32103e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e455ae', 'hex')); + var validScriptSig = new bitcore.Script(FakeBuilder.VALID_SCRIPTSIG_BUF); var pubkeys = [ '03197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d', @@ -167,15 +167,86 @@ describe('TxProposal', function() { it('#_verifyScriptSig, two signatures', function() { // Data taken from bitcore's TransactionBuilder test var txp = dummyProposal; - var ret = TxProposal._verifySignatures(pubkeys,validScriptSig, new Buffer('31103626e162f1cbfab6b95b08c9f6e78aae128523261cb37f8dfd4783cb09a7', 'hex')); + var tx = dummyProposal.builder.build(); + var ret = TxProposal._verifySignatures(pubkeys,validScriptSig, tx.hashForSignature()); ret.should.deep.equal([0, 3]); }); - it('#_keysFromRedeemScript', function() { - var keys = TxProposal._keysFromRedeemScript(validScriptSig); + it('#_infoFromRedeemScript', function() { + var info = TxProposal._infoFromRedeemScript(validScriptSig); + var keys = info.keys; keys.length.should.equal(5); for(var i in keys){ keys[i].toString('hex').should.equal(pubkeys[i].toString('hex')); } + Buffer.isBuffer(info.scriptBuf).should.equal(true); }); + it('#_updateSignedBy', function() { + var txp = dummyProposal; + txp._inputSignatures.should.deep.equal([]); + txp._updateSignedBy(); + txp._inputSignatures.should.deep.equal([[ '03197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d', '03a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e3' ]]); + }); + describe('#_check', function() { + var txp = dummyProposal; + var backup = txp.builder.tx.ins; + + it('OK', function() { + txp._check(); + }); + it('FAIL ins', function() { + txp.builder.tx.ins = []; + (function() { txp._check();} ).should.throw('no ins'); + txp.builder.tx.ins = backup; + }); + it('FAIL signhash', function() { + sinon.stub(txp.builder.tx,'getHashType').returns(2); + (function() { txp._check();} ).should.throw('signatures'); + txp.builder.tx.getHashType.restore(); + }); + it('FAIL no signatures', function() { + var backup = txp.builder.vanilla.scriptSigs; + txp.builder.vanilla.scriptSigs = []; + (function() { txp._check();} ).should.throw('no signatures'); + txp.builder.vanilla.scriptSigs = backup; + }); + }); + describe('#merge', function() { + var txp = dummyProposal; + var backup = txp.builder.tx.ins; + it('with self', function() { + var ret = txp.merge(txp); + ret.newSignatures.length.should.equal(0); + ret.hasChanged.should.equal(false); + }); + + it('with less signatures', function() { + var backup = txp.builder.vanilla.scriptSigs[0]; + txp.builder.merge = function() { + // 3 signatures. + this.vanilla.scriptSigs=['0048304502207d8e832bd576c93300e53ab6cbd68641961bec60690c358fd42d8e42b7d7d687022100a1daa89923efdb4c9b615d065058d9e1644f67000694a7d0806759afa7bef19b014cad532103197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d210380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127210392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed032103a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e32103e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e455ae']; + this.tx.ins[0].s=new Buffer(this.vanilla.scriptSigs[0],'hex'); + }; + var ret = txp.merge(txp); + ret.hasChanged.should.equal(true); + ret.newSignatures.length.should.equal(0); + + txp.builder.vanilla.scriptSigs = [backup]; + txp.builder.tx.ins[0].s = new Buffer(backup,'hex'); + }); + + + it('with more signatures', function() { + txp.builder.merge = function() { + // 3 signatures. + this.vanilla.scriptSigs=['00483045022100f75bd3eb92d8c9be9a94d848bbd1985fc0eaf4c47fb470a0b222881802a1f03802204eb239ae3082779b1ec4f2e69baa0362494071e707e1696c14ad23c8f2e184e20148304502201981482db0f369ce943293b6fec06a0347918663c766a79d4cbd0457801768d1022100aedf8d7c51d55a9ddbdcc0067ed6b648b77ce9660447bbcf4e2c209698efa0a30148304502203f0ddad47757f8705cb40e7c706590d2e2028a7027ffdb26dd208fd6155e0d28022100ccd206f9b969ab7f88ee4c5c6cee48c800a62dda024c5a8de7eb8612b833a0c0014cad532103197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d210380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127210392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed032103a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e32103e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e455ae']; + this.tx.ins[0].s=new Buffer(this.vanilla.scriptSigs[0],'hex'); + }; + var ret = txp.merge(txp); + ret.hasChanged.should.equal(true); + ret.newSignatures.length.should.equal(1); + ret.newSignatures[0].should.equal('0392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed03'); + }); + }); + }); });