From 966818c53ad1bb689174d3a85df19bb7ddf81176 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 2 Aug 2014 20:51:31 -0300 Subject: [PATCH] add different toObj/fromObj fn for networking --- js/models/core/PublicKeyRing.js | 38 +++++--- js/models/core/TxProposal.js | 78 +++++++++++++---- js/models/core/TxProposals.js | 22 +---- js/models/core/Wallet.js | 149 +++++++++++++++++++++++--------- test/test.TxProposal.js | 2 - test/test.Wallet.js | 62 +++++++------ 6 files changed, 234 insertions(+), 117 deletions(-) diff --git a/js/models/core/PublicKeyRing.js b/js/models/core/PublicKeyRing.js index 823315ac9..c89c2f0ac 100644 --- a/js/models/core/PublicKeyRing.js +++ b/js/models/core/PublicKeyRing.js @@ -99,14 +99,6 @@ PublicKeyRing.prototype._checkKeys = function() { throw new Error('dont have required keys yet'); }; -PublicKeyRing.prototype._newExtendedPublicKey = function() { - return new PrivateKey({ - networkName: this.network.name - }) - .deriveBIP45Branch() - .extendedPublicKeyString(); -}; - PublicKeyRing.prototype._updateBip = function(index) { var hk = this.copayersHK[index].derive(HDPath.IdBranch); this.copayerIds[index] = hk.eckey.public.toString('hex'); @@ -125,6 +117,8 @@ PublicKeyRing.prototype.nicknameForCopayer = function(copayerId) { }; PublicKeyRing.prototype.addCopayer = function(newEpk, nickname) { + preconditions.checkArgument(newEpk); + if (this.isComplete()) throw new Error('PKR already has all required key:' + this.totalCopayers); @@ -133,10 +127,6 @@ PublicKeyRing.prototype.addCopayer = function(newEpk, nickname) { throw new Error('PKR already has that key'); }); - if (!newEpk) { - newEpk = this._newExtendedPublicKey(); - } - var i = this.copayersHK.length; var bip = new HK(newEpk); this.copayersHK.push(bip); @@ -307,6 +297,30 @@ PublicKeyRing.prototype.forPaths = function(paths) { }; +PublicKeyRing.prototype.copayersForPubkeys = function(pubkeys, paths) { + + var inKeyMap = {}, ret = []; + for(var i in pubkeys ){ + inKeyMap[pubkeys[i]] = 1; + }; + + var keys = this.getForPaths(paths); + for(var i in keys ){ + for(var copayerIndex in keys[i] ){ + var kHex = keys[i][copayerIndex].toString('hex'); + if (inKeyMap[kHex]) { + ret.push(this.copayerIds[copayerIndex]); + delete inKeyMap[kHex]; + } + } + } + for(var i in inKeyMap) + throw new Error('Pubkey not identified') + + return ret; +}; + + // TODO this could be cached PublicKeyRing.prototype._addScriptMap = function(map, path) { var p = HDPath.indexesForPath(path); diff --git a/js/models/core/TxProposal.js b/js/models/core/TxProposal.js index cbebce268..da5181d0b 100644 --- a/js/models/core/TxProposal.js +++ b/js/models/core/TxProposal.js @@ -10,6 +10,9 @@ var Key = bitcore.Key; var buffertools = bitcore.buffertools; var preconditions = require('preconditions').instance(); +var VERSION = 1; +var CORE_FIELDS = ['builderObj','inputChainPaths', 'version']; + function TxProposal(opts) { preconditions.checkArgument(opts); @@ -17,22 +20,26 @@ function TxProposal(opts) { preconditions.checkArgument(opts.creator,'no creator'); preconditions.checkArgument(opts.createdTs,'no createdTs'); preconditions.checkArgument(opts.builder,'no builder'); + preconditions.checkArgument(opts.inputChainPaths,'no inputChainPaths'); - - this.creator = opts.creator; - this.createdTs = opts.createdTs; - this.builder = opts.builder; this.inputChainPaths = opts.inputChainPaths; - + this.version = opts.version; + this.builder = opts.builder; + this.createdTs = opts.createdTs; + this.createdTs = opts.createdTs; this._inputSignatures = []; - this.seenBy = opts.seenBy || {}; + + // CopayerIds + this.creator = opts.creator; this.signedBy = opts.signedBy || {}; + this.seenBy = opts.seenBy || {}; this.rejectedBy = opts.rejectedBy || {}; this.sentTs = opts.sentTs || null; this.sentTxid = opts.sentTxid || null; this.comment = opts.comment || null; this.readonly = opts.readonly || null; - // this._updateSignedBy(); + + this.sync(); } TxProposal.prototype.getId = function() { @@ -47,11 +54,24 @@ TxProposal.prototype.toObj = function() { }; -TxProposal.prototype.setSent = function(sentTxid) { - this.sentTxid = sentTxid; - this.sentTs = Date.now(); +TxProposal.prototype.toObjForNetwork = function() { + var o = this.toObj; + + var newOutput = {}; + CORE_FIELDS.forEach(function(k){ + newOutput[k] = o[k]; + }); + return newOutput; }; +TxProposal.prototype.sync = function() { + this._check(); + this._updateSignedBy(); + return this; +} + + +// fromObj => from a trusted source TxProposal.fromObj = function(o, forceOpts) { preconditions.checkArgument(o.builderObj); delete o['builder']; @@ -64,17 +84,24 @@ TxProposal.fromObj = function(o, forceOpts) { o.builder = TransactionBuilder.fromObj(o.builderObj); } catch (e) { + // backwards (V0) compatatibility fix. if (!o.version) { o.builder = new BuilderMockV0(o.builderObj); o.readonly = 1; }; } + return new TxProposal(o); +}; - var t = new TxProposal(o); - t._check(); - t._updateSignedBy(); +TxProposal.fromObjUntrusted = function(o, forceOpts, senderId) { + var newInput = {}; + CORE_FIELDS.forEach(function(k){ + newInput[k] = o[k]; + }); + if (newInput.version !== VERSION) + throw new Error('Peer using different version'); - return t; + return TxProposal.fromObj(newInput, forceOpts, senderId); }; @@ -144,7 +171,8 @@ TxProposal.prototype._updateSignedBy = function() { if (signatureIndexes.length !== signatureCount) throw new Error('Invalid signature'); this._inputSignatures[i] = signatureIndexes.map(function(i) { - return info.keys[i].toString('hex'); + var r = info.keys[i].toString('hex'); + return r; }); }; }; @@ -184,6 +212,21 @@ TxProposal.prototype.mergeBuilder = function(incoming) { }; +TxProposal.prototype.setSeen = function(copayerId) { + if (!this.seenBy[copayerId]) + this.seenBy[copayerId] = Date.now(); +}; + +TxProposal.prototype.setRejected = function(copayerId) { + if (!this.rejectedBy[copayerId] && !this.signedBy) + this.rejectedBy[copayerId] = Date.now(); +}; + +TxProposal.prototype.setSent = function(sentTxid) { + this.sentTxid = sentTxid; + this.sentTs = Date.now(); +}; + /* OTDO events.push({ type: 'seen', @@ -213,12 +256,11 @@ TxProposal.prototype._allSignatures = function() { return ret; }; +// merge will not merge any metadata. TxProposal.prototype.merge = function(incoming) { var ret = {}; var newSignatures = []; - - incoming._check(); - incoming._updateSignedBy(); + incoming.sync(); var prevInputSignatures = this._allSignatures(); diff --git a/js/models/core/TxProposals.js b/js/models/core/TxProposals.js index 5565cac77..71cfb443b 100644 --- a/js/models/core/TxProposals.js +++ b/js/models/core/TxProposals.js @@ -20,6 +20,7 @@ function TxProposals(opts) { this.txps = {}; } +// fromObj => from a trusted source TxProposals.fromObj = function(o, forceOpts) { var ret = new TxProposals({ networkName: o.networkName, @@ -60,8 +61,6 @@ TxProposals.prototype.merge = function(inTxp, allowedPubKeys) { var ntxid = inTxp.getId(); var ret = {}; - ret.events = []; - ret.events.hasChanged = false; if (myTxps[ntxid]) { var v0 = myTxps[ntxid]; @@ -70,12 +69,7 @@ TxProposals.prototype.merge = function(inTxp, allowedPubKeys) { } else { this.txps[ntxid] = inTxp; - ret.hasChanged = true; - ret.events.push({ - type: 'new', - cid: inTxp.creator, - tx: ntxid - }); + ret.new = 1; } return ret; }; @@ -88,22 +82,14 @@ TxProposals.prototype.mergeFromObj = function(txProposalObj, allowedPubKeys, opt }; - - // Add a LOCALLY CREATED (trusted) tx proposal -TxProposals.prototype.add = function(data) { - var txp = new TxProposal(data); +TxProposals.prototype.add = function(txp) { + txp.sync(); var ntxid = txp.getId(); this.txps[ntxid] = txp; return ntxid; }; -TxProposals.prototype.setSent = function(ntxid, txid) { - //sent TxProposals are local an not broadcasted. - this.txps[ntxid].setSent(txid); -}; - - TxProposals.prototype.getTxProposal = function(ntxid, copayers) { var txp = this.txps[ntxid]; var i = JSON.parse(JSON.stringify(txp)); diff --git a/js/models/core/Wallet.js b/js/models/core/Wallet.js index 8851e94ba..3f7583643 100644 --- a/js/models/core/Wallet.js +++ b/js/models/core/Wallet.js @@ -17,6 +17,7 @@ var Address = bitcore.Address; var HDParams = require('./HDParams'); var PublicKeyRing = require('./PublicKeyRing'); +var TxProposal = require('./TxProposal'); var TxProposals = require('./TxProposals'); var PrivateKey = require('./PrivateKey'); var copayConfig = require('../../../config'); @@ -129,11 +130,39 @@ Wallet.prototype._handlePublicKeyRing = function(senderId, data, isInbound) { }; +Wallet.prototype._processProposalEvents = function(mergeInfo) { + var ev = []; + if (mergeInfo.new) { + ev = { + type: 'new', + cid: senderId + } + } else { + for (var i in mergeInfo.newCopayers) { + var copayerId = mergeInfo.newCopayers[i]; + ev.push({ + type: 'signed', + cid: copayerId + }); + } + } + if (ev) + this.emit('txProposalEvent',ev); +}; + + Wallet.prototype._handleTxProposal = function(senderId, data) { this.log('RECV TXPROPOSAL: ', data); - var mergeInfo; + var mergeInfo, ntxid; + try { mergeInfo = this.txProposals.mergeFromObj(data.txProposal, senderId, Wallet.builderOpts); + mergeInfo.newCopayers=[]; + for (var i in mergeInfo.newSignatures) { + var k = mergeInfo.newSignatures[i]; + mergeInfo.newCopayers.push(this.getCopayerIdFromPubKey(k)); + }; + ntxid = mergeInfo.inTxp.getId(); } catch (e) { var corruptEvent = { type: 'corrupt', @@ -143,21 +172,36 @@ Wallet.prototype._handleTxProposal = function(senderId, data) { this.emit('txProposalEvent', corruptEvent); return; } + this.sendSeen(ntxid); - var added = this.addSeenToTxProposals(); - if (added) { - this.log('### BROADCASTING txProposals with my seenBy updated.'); - this.sendTxProposal(mergeInfo.inTxp.getId()); - } + if (mergeInfo.hasChanged) + this.sendTxProposal(ntxid); this.emit('txProposalsUpdated'); this.store(); - - for (var i = 0; i < mergeInfo.events.length; i++) { - this.emit('txProposalEvent', mergeInfo.events[i]); - } + this._processProposalEvents(senderId, mergeInfo); }; + +Wallet.prototype._handleReject = function(senderId, data, isInbound) { + this.log('RECV REJECT:', data); + // TODO check that has not signed. + // + this.txProposals.txps[data.ntxid].setRejected(senderId); + this.emit('txProposalsUpdated'); + this.store(); + +}; + +Wallet.prototype._handleSeen = function(senderId, data, isInbound) { + this.log('RECV SEEN:', data); + this.txProposals.txps[data.ntxid].setSeen(senderId); + this.emit('txProposalsUpdated'); + this.store(); +}; + + + Wallet.prototype._handleAddressBook = function(senderId, data, isInbound) { this.log('RECV ADDRESSBOOK:', data); var rcv = data.addressBook; @@ -199,6 +243,10 @@ Wallet.prototype._handleData = function(senderId, data, isInbound) { case 'publicKeyRing': this._handlePublicKeyRing(senderId, data, isInbound); break; + case 'reject': + this._handleReject(senderId, data, isInbound); + case 'seen': + this._handleReject(senderId, data, isInbound); case 'txProposal': this._handleTxProposal(senderId, data, isInbound); break; @@ -381,6 +429,7 @@ Wallet.prototype.toObj = function() { return walletObj; }; +// fromObj => from a trusted source Wallet.fromObj = function(o, storage, network, blockchain) { var opts = JSON.parse(JSON.stringify(o.opts)); opts.addressBook = o.addressBook; @@ -424,6 +473,26 @@ Wallet.prototype.sendTxProposal = function(ntxid, recipients) { }); }; +Wallet.prototype.sendSeen = function(ntxid) { + preconditions.checkArgument(ntxid); + this.log('### SENDING seen: ' + ntxid + ' TO: All'); + this.send(null, { + type: 'seen', + ntxid: ntxid, + walletId: this.id, + }); +}; + +Wallet.prototype.sendReject = function(ntxid) { + preconditions.checkArgument(ntxid); + this.log('### SENDING reject: ' + ntxid + ' TO: All'); + this.send(null, { + type: 'reject', + ntxid: ntxid, + walletId: this.id, + }); +}; + Wallet.prototype.sendWalletReady = function(recipients) { this.log('### SENDING WalletReady TO:', recipients); @@ -521,7 +590,7 @@ Wallet.prototype.reject = function(ntxid) { } txp.rejectedBy[myId] = Date.now(); - this.sendTxProposal(ntxid); + this.sendReject(ntxid); this.store(); this.emit('txProposalsUpdated'); }; @@ -534,10 +603,10 @@ Wallet.prototype.sign = function(ntxid, cb) { setTimeout(function() { var myId = self.getMyCopayerId(); var txp = self.txProposals.txps[ntxid]; - if (!txp || txp.rejectedBy[myId] || txp.signedBy[myId]) { - if (cb) cb(false); - } - + // if (!txp || txp.rejectedBy[myId] || txp.signedBy[myId]) { + // if (cb) cb(false); + // } + // var keys = self.privateKey.getForPaths(txp.inputChainPaths); var b = txp.builder; @@ -574,7 +643,7 @@ Wallet.prototype.sendTx = function(ntxid, cb) { this.blockchain.sendRawTransaction(txHex, function(txid) { self.log('BITCOIND txid:', txid); if (txid) { - self.txProposals.setSent(ntxid, txid); + self.txProposals.txps[ntxid].setSent(txid); self.sendTxProposal(ntxid); self.store(); } @@ -582,20 +651,20 @@ Wallet.prototype.sendTx = function(ntxid, cb) { }); }; -Wallet.prototype.addSeenToTxProposals = function() { - var ret = false; - var myId = this.getMyCopayerId(); - - for (var k in this.txProposals.txps) { - var txp = this.txProposals.txps[k]; - if (!txp.seenBy[myId]) { - - txp.seenBy[myId] = Date.now(); - ret = true; - } - } - return ret; -}; +// Wallet.prototype.addSeenToTxProposals = function() { +// var ret = false; +// var myId = this.getMyCopayerId(); +// +// for (var k in this.txProposals.txps) { +// var txp = this.txProposals.txps[k]; +// if (!txp.seenBy[myId]) { +// +// txp.seenBy[myId] = Date.now(); +// ret = true; +// } +// } +// return ret; +// }; // TODO: remove this method and use getAddressesInfo everywhere Wallet.prototype.getAddresses = function(opts) { @@ -718,6 +787,7 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos preconditions.checkArgument(new Address(toAddress).network().name === this.getNetworkName()); preconditions.checkState(pkr.isComplete()); + preconditions.checkState(priv); if (comment) preconditions.checkArgument(comment.length <= 100); if (!opts.remainderOut) { @@ -744,22 +814,23 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos b = b.setHashToScriptMap(pkr.getRedeemScriptMap(inputChainPaths)); - if (priv) { - var keys = priv.getForPaths(inputChainPaths); - var signed = b.sign(keys); - } + var keys = priv.getForPaths(inputChainPaths); + var signed = b.sign(keys); var myId = this.getMyCopayerId(); var now = Date.now(); - var me = {}; var tx = b.build(); - if (priv && tx.countInputSignatures(0)) me[myId] = now; + if (!tx.countInputSignatures(0)) + throw new Error ('Could not sign generated tx'); + + var me = {}; + me[myId] = now; var meSeen = {}; if (priv) meSeen[myId] = now; - var data = { + var ntxid = this.txProposals.add(new TxProposal({ inputChainPaths: inputChainPaths, signedBy: me, seenBy: meSeen, @@ -767,9 +838,7 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos createdTs: now, builder: b, comment: comment - }; - - var ntxid = this.txProposals.add(data); + })); return ntxid; }; diff --git a/test/test.TxProposal.js b/test/test.TxProposal.js index ed0032b20..79435a318 100644 --- a/test/test.TxProposal.js +++ b/test/test.TxProposal.js @@ -182,8 +182,6 @@ describe('TxProposal', function() { }); it('#_updateSignedBy', function() { var txp = dummyProposal; - txp._inputSignatures.should.deep.equal([]); - txp._updateSignedBy(); txp._inputSignatures.should.deep.equal([[ '03197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d', '03a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e3' ]]); }); describe('#_check', function() { diff --git a/test/test.Wallet.js b/test/test.Wallet.js index 144f323f5..91e2a2573 100644 --- a/test/test.Wallet.js +++ b/test/test.Wallet.js @@ -10,7 +10,7 @@ try { } var copayConfig = require('../config'); var Wallet = require('../js/models/core/Wallet'); -var Structure = copay.Structure; +var PrivateKey = copay.PrivateKey; var Storage = require('./mocks/FakeStorage'); var Network = require('./mocks/FakeNetwork'); var Blockchain = require('./mocks/FakeBlockchain'); @@ -19,22 +19,30 @@ var TransactionBuilder = bitcore.TransactionBuilder; var Transaction = bitcore.Transaction; var Address = bitcore.Address; +var config = { + requiredCopayers: 3, + totalCopayers: 5, + spendUnconfirmed: true, + reconnectDelay: 100, + networkName: 'testnet', +}; + +var getNewEpk = function() { + return new PrivateKey({ + networkName: config.networkName, + }) + .deriveBIP45Branch() + .extendedPublicKeyString(); +} + var addCopayers = function(w) { for (var i = 0; i < 4; i++) { - w.publicKeyRing.addCopayer(); + w.publicKeyRing.addCopayer(getNewEpk()); } }; describe('Wallet model', function() { - var config = { - requiredCopayers: 3, - totalCopayers: 5, - spendUnconfirmed: true, - reconnectDelay: 100, - networkName: 'testnet', - }; - it('should fail to create an instance', function() { (function() { new Wallet(config) @@ -47,12 +55,11 @@ describe('Wallet model', function() { }); - var createW = function(netKey, N, conf) { + var createW = function(N, conf) { var c = JSON.parse(JSON.stringify(conf || config)); if (!N) N = c.totalCopayers; - if (netKey) c.netKey = netKey; var mainPrivateKey = new copay.PrivateKey({ networkName: config.networkName }); @@ -148,8 +155,7 @@ describe('Wallet model', function() { var createW2 = function(privateKeys, N, conf) { if (!N) N = 3; - var netKey = 'T0FbU2JLby0='; - var w = createW(netKey, N, conf); + var w = createW(N, conf); should.exist(w); var pkr = w.publicKeyRing; @@ -157,9 +163,9 @@ describe('Wallet model', function() { for (var i = 0; i < N - 1; i++) { if (privateKeys) { var k = privateKeys[i]; - pkr.addCopayer(k ? k.deriveBIP45Branch().extendedPublicKeyString() : null); + pkr.addCopayer(k ? k.deriveBIP45Branch().extendedPublicKeyString() : getNewEpk()); } else { - pkr.addCopayer(); + pkr.addCopayer(getNewEpk()); } } @@ -212,12 +218,12 @@ describe('Wallet model', function() { var t = w.txProposals; var txp = t.txps[ntxid]; + Object.keys(txp._inputSignatures).length.should.equal(1); var tx = txp.builder.build(); should.exist(tx); chai.expect(txp.comment).to.be.null; tx.isComplete().should.equal(false); Object.keys(txp.seenBy).length.should.equal(1); - Object.keys(txp.signedBy).length.should.equal(1); }); it('#create with comment', function() { @@ -502,7 +508,8 @@ describe('Wallet model', function() { var w = createW(); var r = w.getRegisteredCopayerIds(); r.length.should.equal(1); - w.publicKeyRing.addCopayer(); + w.publicKeyRing.addCopayer(getNewEpk()); + r = w.getRegisteredCopayerIds(); r.length.should.equal(2); r[0].should.not.equal(r[1]); @@ -512,7 +519,7 @@ describe('Wallet model', function() { var w = createW(); var r = w.getRegisteredPeerIds(); r.length.should.equal(1); - w.publicKeyRing.addCopayer(); + w.publicKeyRing.addCopayer(getNewEpk()); r = w.getRegisteredPeerIds(); r.length.should.equal(2); r[0].should.not.equal(r[1]); @@ -642,10 +649,11 @@ describe('Wallet model', function() { }); }); it('should create & sign transaction from received funds', function(done) { - this.timeout(10000); - var w = cachedCreateW2(); - var pk = w.privateKey; - w.privateKey = null; + var k2 = new PrivateKey({ + networkName: config.networkName + }); + + var w = createW2([k2]); var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); w.createTx(toAddress, amountSatStr, null, function(ntxid) { @@ -654,7 +662,7 @@ describe('Wallet model', function() { w.getTxProposals()[0].rejectedByUs.should.equal(false); done(); }); - w.privateKey = pk; + w.privateKey = k2; w.sign(ntxid, function(success) { success.should.equal(true); }); @@ -1031,9 +1039,9 @@ describe('Wallet model', function() { e.type.should.equal(result); done(); }); - var txp = { - 'txProposal': { dummy: 1} - }; + var txp = {dummy:1}; + // txp.prototype.getId = function() {return 'aa'}; + var txp = { 'txProposal': txp }; var merge = sinon.stub(w.txProposals, 'mergeFromObj', function() { if (shouldThrow) throw new Error(); return {events: [{type:'new'}]};