diff --git a/js/models/TxProposal.js b/js/models/TxProposal.js index 34cb8b322..e1b6679c0 100644 --- a/js/models/TxProposal.js +++ b/js/models/TxProposal.js @@ -38,9 +38,12 @@ function TxProposal(opts) { this.comment = opts.comment || null; this.readonly = opts.readonly || null; this.merchant = opts.merchant || null; - this.paymentAckMemo = null; + this.paymentAckMemo = opts.paymentAckMemo || null; this.paymentProtocolURL = opts.paymentProtocolURL || null; + // not from obj + this._pubkeysForScriptCache = {}; + // New Tx Proposal if (_.isEmpty(this.seenBy) && opts.creator) { var now = Date.now(); @@ -93,13 +96,32 @@ TxProposal.prototype.isFullySigned = function() { return this.builder && this.builder.isFullySigned(); }; + +TxProposal.prototype.getMySignatures = function() { + preconditions.checkState(this._mySignatures, 'Still no signatures from us'); + return _.clone(this._mySignatures); +}; + +TxProposal.prototype._setMySignatures = function(signaturesBefore) { + var mySigs = []; + _.each(this.getSignatures(), function(signatures, index) { + var diff = _.difference(signatures, signaturesBefore[index]); + preconditions.checkState(diff.length == 1, 'more that one signature added!'); + mySigs.push(diff[0].toString('hex')); + }) + this._mySignatures = mySigs; + return; +}; + TxProposal.prototype.sign = function(keys, signerId) { var before = this.countSignatures(); + var signaturesBefore = this.getSignatures(); this.builder.sign(keys); var signaturesAdded = this.countSignatures() > before; if (signaturesAdded) { this.signedBy[signerId] = Date.now(); + this._setMySignatures(signaturesBefore); } return signaturesAdded; }; @@ -153,10 +175,14 @@ TxProposal.prototype.addMerchantData = function(merchantData) { this._checkPayPro(); }; -TxProposal.prototype.getScriptSigs = function() { - var tx = this.builder.build(); - var sigs = _.map(tx.ins, function(value) { - value.s.toString('hex'); +TxProposal.prototype.getSignatures = function() { + var ins = this.builder.build().ins; + var sigs = _.map(ins, function(value) { + var script = new bitcore.Script(value.s); + var nchunks = script.chunks.length; + return _.map(script.chunks.slice(1, nchunks - 1), function(buffer) { + return buffer.toString('hex'); + }); }); return sigs; @@ -185,36 +211,42 @@ TxProposal.prototype.isPending = function(maxRejectCount) { * getSignersPubKey * @desc get Pubkeys of signers, for each input * - * @return {string[][]} array of arrays for pubkeys for each input + * @return {string[][]} array of hashes for signing pubkeys for each input */ TxProposal.prototype.getSignersPubKeys = function(forceUpdate) { + var self = this; + var signersPubKey = []; - if (!this._signersPubKey || forceUpdate) { + if (!self._signersPubKey || forceUpdate) { - log.debug('Verifing signatures...'); + log.debug('PERFORMANCE WARN: Verifying *all* TX signatures:', self.getId()); - var tx = this.builder.build(); + var tx = self.builder.build(); _.each(tx.ins, function(input, index) { - var scriptSig = new Script(input.s); - var signatureCount = scriptSig.countSignatures(); + if (!self._pubkeysForScriptCache[input.s]) { + var scriptSig = new Script(input.s); + var signatureCount = scriptSig.countSignatures(); - var info = TxProposal._infoFromRedeemScript(scriptSig); - var txSigHash = tx.hashForSignature(info.script, parseInt(index), Transaction.SIGHASH_ALL); - var inputSignersPubKey = TxProposal._verifySignatures(info.keys, scriptSig, txSigHash); + var info = TxProposal._infoFromRedeemScript(scriptSig); + var txSigHash = tx.hashForSignature(info.script, parseInt(index), Transaction.SIGHASH_ALL); + var inputSignersPubKey = TxProposal._verifySignatures(info.keys, scriptSig, txSigHash); - // Does scriptSig has strings that are not signatures? - if (inputSignersPubKey.length !== signatureCount) - throw new Error('Invalid signature'); + // Does scriptSig has strings that are not signatures? + if (inputSignersPubKey.length !== signatureCount) + throw new Error('Invalid signature'); - signersPubKey[index] = inputSignersPubKey; + self._pubkeysForScriptCache[input.s] = inputSignersPubKey; + } + + signersPubKey[index] = self._pubkeysForScriptCache[input.s]; }); - this._signersPubKey = signersPubKey; + self._signersPubKey = signersPubKey; } - return this._signersPubKey; + return self._signersPubKey; }; TxProposal.prototype.getId = function() { @@ -229,6 +261,7 @@ TxProposal.prototype.getId = function() { TxProposal.prototype.toObj = function() { var o = JSON.parse(JSON.stringify(this)); delete o['builder']; + delete o['_pubkeysForScriptCache']; o.builderObj = this.builder.toObj(); return o; }; @@ -313,6 +346,8 @@ TxProposal._verifySignatures = function(inKeys, scriptSig, txSigHash) { for (var i = 1; i <= scriptSig.countSignatures(); i++) { var chunk = scriptSig.chunks[i]; var sigRaw = new Buffer(chunk.slice(0, chunk.length - 1)); + + log.debug('\t Verifying CHUNK:', i); for (var j in keys) { var k = keys[j]; if (k.keyObj.verifySignatureSync(txSigHash, sigRaw)) { diff --git a/js/models/Wallet.js b/js/models/Wallet.js index ff3c85d93..7db731745 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -337,34 +337,6 @@ Wallet.prototype._onPublicKeyRing = function(senderId, data) { } }; -/** - * @desc - * Demultiplexes calls to update TxProposal updates - * - * @param {string} senderId - the copayer that sent this event - * @param {Object} m - the data received - * @emits txProposalEvent - */ -Wallet.prototype._processProposalEvents = function(senderId, m) { - var ev; - if (m) { - if (m.new) { - ev = { - type: 'new', - cId: senderId - } - } else if (m.newCopayer && m.newCopayer.length) { - ev = { - type: 'signed', - cId: m.newCopayer[0] - }; - } - } - if (ev) - this.emitAndKeepAlive('txProposalEvent', ev); -}; - - /** * @desc * Retrieves a keymap from a transaction proposal set extracts a maps from @@ -373,12 +345,12 @@ Wallet.prototype._processProposalEvents = function(senderId, m) { * @param {TxProposals} txp - the transaction proposals * @return {Object} [pubkey] -> copayerId */ -Wallet.prototype._getKeyMap = function(txp) { +Wallet.prototype._getPubkeyToCopayerMap = function(txp) { preconditions.checkArgument(txp); var inSig0, keyMapAll = {}, self = this; - var signersPubKeys = txp.getSignersPubKeys(); + var signersPubKeys = txp.getSignersPubKeys(); _.each(signersPubKeys, function(inputSignersPubKey, i) { var keyMap = self.publicKeyRing.copayersForPubkeys(inputSignersPubKey, txp.inputChainPaths); @@ -520,7 +492,7 @@ Wallet.prototype._processIncomingNewTxProposal = function(txp, cb) { /** * @desc - * Handles a 'TXPROPOSAL' network message + * Handles a NEW 'TXPROPOSAL' network message * * @param {string} senderId - the id of the sender * @param {Object} data - the data received @@ -538,7 +510,6 @@ Wallet.prototype._onTxProposal = function(senderId, data) { if (localTx) { log.debug('Ignoring existing tx Proposal:' + incomingNtxid); - console.log('') return; } @@ -548,7 +519,7 @@ Wallet.prototype._onTxProposal = function(senderId, data) { return; } - var keyMap = self._getKeyMap(incomingTx); + var keyMap = self._getPubkeyToCopayerMap(incomingTx); incomingTx.setCopayers(senderId, keyMap); self.txProposals.add(incomingTx); @@ -559,6 +530,27 @@ Wallet.prototype._onTxProposal = function(senderId, data) { }); }; + +Wallet.prototype._onSignatures = function(senderId, data) { + var self = this; + try { + var localTx = this.txProposals.get(data.ntxid); + } catch (e) { + log.info('Ignoring signature for unknown tx Proposal:' + data.ntxid); + return; + }; + + var keyMap = self._getPubkeyToCopayerMap(locaTx); + + // TODO look senderIdin keyMap + localTx.addSignature(pubkeys, data.signature, keyMap); + + this.emitAndKeepAlive('txProposalEvent', { + type: 'signed', + cId: senderId, + }); +}; + /** * @desc * Handle a REJECT message received @@ -730,6 +722,9 @@ Wallet.prototype._onData = function(senderId, data, ts) { case 'txProposal': this._onTxProposal(senderId, data); break; + case 'signature': + this._onSignature(senderId, data); + break; case 'indexes': this._onIndexes(senderId, data); break; @@ -1266,8 +1261,8 @@ Wallet.prototype.sendSignature = function(ntxid) { var txp = this.txProposals.get(ntxid); this._sendToPeers(null, { - type: 'sign', - signatures: txp.getScriptSigs(), + type: 'signature', + signatures: txp.getMySignatures(), walletId: this.id, }); }; diff --git a/test/TxProposal.js b/test/TxProposal.js index d0152f06c..76ed3c7b5 100644 --- a/test/TxProposal.js +++ b/test/TxProposal.js @@ -299,6 +299,17 @@ describe('TxProposal', function() { var pubkeys = txp.getSignersPubKeys(); pubkeys.should.deep.equal([PUBKEYS]); }); + + describe('#getSignatures', function() { + it('should', function() { + var txp = dummyProposal(); + var sigs = txp.getSignatures(); + sigs.length.should.equal(1); + sigs[0].length.should.equal(2); + sigs[0][0].should.equal('304502200708a381dde585ef7fdfaeaeb5da9b451d3e22b01eac8a5e3d03b959e24a7478022100c90e76e423523a54a9e9c43858337ebcef1a539a7fc685c2698dd8648fcf1b9101'); + sigs[0][1].should.equal('3044022030a77c9613d6ee010717c1abc494668d877e3fa0ae4c520f65cc3b308754c98c02205219d387bcb291bd44805b9468439e4168b02a6a180cdbcc24d84d71d696c1ae01'); + }); + }); describe('#_check', function() { it('OK', function() { dummyProposal({})._check(); @@ -451,7 +462,6 @@ describe('TxProposal', function() { txp.builder.tx.ins[0].s = new Buffer(backup, 'hex'); }); - it('with more signatures', function() { txp.builder.merge = function() { // 3 signatures. diff --git a/test/Wallet.js b/test/Wallet.js index a54872eb4..8e3cf9d95 100644 --- a/test/Wallet.js +++ b/test/Wallet.js @@ -233,7 +233,7 @@ describe('Wallet model', function() { unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true); var f = function() { - var ntxid = w._createTxProposal( + w._createTxProposal( '15q6HKjWHAksHcH91JW23BJEuzZgFwydBt', '123456789', null, @@ -1508,7 +1508,7 @@ describe('Wallet model', function() { }); }); - describe('_getKeymap', function() { + describe('_getPubkeyToCopayerMap', function() { var w = cachedCreateW(); it('should set keymap', function() { @@ -1521,7 +1521,7 @@ describe('Wallet model', function() { ]), inputChainPaths: ['/m/1'], }; - var map = w._getKeyMap(txp); + var map = w._getPubkeyToCopayerMap(txp); console.log('[Wallet.js.1526:map:]', map); //TODO Object.keys(map).length.should.equal(1); map['123'].should.equal('juan'); @@ -1539,7 +1539,7 @@ describe('Wallet model', function() { inputChainPaths: ['/m/1'], }; (function() { - w._getKeyMap(txp); + w._getPubkeyToCopayerMap(txp); }).should.throw('does not match known copayers'); stub.restore(); }); @@ -1556,7 +1556,7 @@ describe('Wallet model', function() { inputChainPaths: ['/m/1'], }; (function() { - w._getKeyMap(txp); + w._getPubkeyToCopayerMap(txp); }).should.throw('does not match known copayers'); stub.restore(); }); @@ -1574,7 +1574,7 @@ describe('Wallet model', function() { ]), inputChainPaths: ['/m/1'], }; - var map = w._getKeyMap(txp); + var map = w._getPubkeyToCopayerMap(txp); Object.keys(map).length.should.equal(2); map['123'].should.equal('juan'); map['234'].should.equal('pepe'); @@ -1596,13 +1596,12 @@ describe('Wallet model', function() { ['234', '123'], ['555'] ]), - - _inputSigners: [ - ], + + _inputSigners: [], inputChainPaths: ['/m/1'], }; (function() { - w._getKeyMap(txp); + w._getPubkeyToCopayerMap(txp); }).should.throw('different sig'); stub.restore(); }); @@ -1627,7 +1626,7 @@ describe('Wallet model', function() { inputChainPaths: ['/m/1'], }; (function() { - w._getKeyMap(txp); + w._getPubkeyToCopayerMap(txp); }).should.throw('different sig'); stub.restore(); }); @@ -1651,7 +1650,7 @@ describe('Wallet model', function() { ]), inputChainPaths: ['/m/1'], }; - var gk = w._getKeyMap(txp); + var gk = w._getPubkeyToCopayerMap(txp); gk.should.deep.equal({ '123': 'pedro', '234': 'pepe', @@ -1667,7 +1666,7 @@ describe('Wallet model', function() { beforeEach(function() { w = cachedCreateW(); - w._getKeyMap = sinon.stub(); + w._getPubkeyToCopayerMap = sinon.stub(); w.sendSeen = sinon.spy(); w.sendTxProposal = sinon.spy(); data = { @@ -1715,7 +1714,7 @@ describe('Wallet model', function() { }; sinon.stub(w.txProposals, 'merge').returns(args); sinon.stub(w, '_processIncomingTxProposal').yields(null); - sinon.stub(w, '_getKeyMap').returns(null); + sinon.stub(w, '_getPubkeyToCopayerMap').returns(null); w.on('txProposalEvent', function(e) { e.type.should.equal('new'); @@ -1730,7 +1729,7 @@ describe('Wallet model', function() { ntxid: '1' }); sinon.stub(w, 'on'); - sinon.stub(w, '_getKeyMap').throws(new Error('test error')); + sinon.stub(w, '_getPubkeyToCopayerMap').throws(new Error('test error')); w._onTxProposal('senderID', data); w.on.called.should.equal(false); }); @@ -1979,6 +1978,70 @@ describe('Wallet model', function() { }); + describe('sendMesages', function() { + var w, txp; + beforeEach(function() { + w = createW2(null, 1); + var utxo = createUTXO(w); + txp = w._createTxProposal( + '2MtP8WyiwG7ZdVWM96CVsk2M1N8zyfiVQsY', + '123456789', + null, + utxo + ); + }); + + it('should be able to sendReject', function() { + w.sendReject(txp.getId()); + w.network.send.calledOnce.should.equal(true); + var payload = w.network.send.getCall(0).args[1]; + payload.type.should.equal('reject'); + payload.walletId.should.equal(w.id); + payload.ntxid.should.equal(txp.getId()); + }); + + + it('should be able to sendSend', function() { + w.sendSeen(txp.getId()); + w.network.send.calledOnce.should.equal(true); + var payload = w.network.send.getCall(0).args[1]; + payload.type.should.equal('seen'); + payload.walletId.should.equal(w.id); + payload.ntxid.should.equal(txp.getId()); + }); + + it('should be able to sendTxProposal', function() { + w.txProposals.add(txp); + w.sendTxProposal(txp.getId()); + w.network.send.calledOnce.should.equal(true); + var payload = w.network.send.getCall(0).args[1]; + payload.type.should.equal('txProposal'); + payload.walletId.should.equal(w.id); + payload.txProposal.should.deep.equal(txp.toObjTrim()); + }); + it('should be able to sendAllTxProposals', function() { + w.txProposals.add(txp); + w.sendAllTxProposals(); + w.network.send.calledOnce.should.equal(true); + var payload = w.network.send.getCall(0).args[1]; + payload.type.should.equal('txProposal'); + payload.walletId.should.equal(w.id); + payload.txProposal.should.deep.equal(txp.toObjTrim()); + }); + it('should be able to sendSignature', function() { + w.txProposals.add(txp); + w.sendSignature(txp.getId()); + w.network.send.calledOnce.should.equal(true); + var payload = w.network.send.getCall(0).args[1]; + payload.type.should.equal('signature'); + payload.walletId.should.equal(w.id); + payload.signatures.length.should.equal(1); + var sig = new Buffer(payload.signatures[0],'hex'); + sig.length.should.be.above(70); + sig.length.should.be.below(74); + }); + }); + describe('#obtainNetworkName', function() { it('should return the networkname', function() { Wallet.obtainNetworkName({