diff --git a/js/models/core/PrivateKey.js b/js/models/core/PrivateKey.js index c1ea17561..9d9a7b009 100644 --- a/js/models/core/PrivateKey.js +++ b/js/models/core/PrivateKey.js @@ -10,45 +10,57 @@ var util = bitcore.util; var PublicKeyRing = require('./PublicKeyRing'); function PrivateKey(opts) { + opts = opts || {}; this.network = opts.networkName === 'testnet' ? networks.testnet : networks.livenet; var init = opts.extendedPrivateKeyString || this.network.name; - this.BIP32 = opts.BIP32 || new BIP32(init); + this.bip = opts.BIP32 || new BIP32(init); + this.privateKeyCache = opts.privateKeyCache || {}; this._calcId(); }; PrivateKey.prototype._calcId = function() { - this.id = util.ripe160(this.BIP32.extendedPublicKey).toString('hex'); + this.id = util.ripe160(this.bip.extendedPublicKey).toString('hex'); }; -PrivateKey.prototype.getBIP32 = function(index,isChange) { - if (typeof index === 'undefined') { - return this.BIP32; - } - return this.BIP32.derive( isChange ? - PublicKeyRing.ChangeBranch(index):PublicKeyRing.PublicBranch(index) ); -}; - - -PrivateKey.fromObj = function(o) { - return new PrivateKey({ - extendedPrivateKeyString: o.extendedPrivateKeyString, - networkName: o.networkName, - }); +PrivateKey.fromObj = function(obj) { + return new PrivateKey(obj); }; PrivateKey.prototype.toObj = function() { return { - extendedPrivateKeyString: this.BIP32.extendedPrivateKeyString(), + extendedPrivateKeyString: this.getExtendedPrivateKeyString(), networkName: this.network.name, + privateKeyCache: this.privateKeyCache }; }; +PrivateKey.prototype.getExtendedPublicKeyString = function() { + return this.bip.extendedPublicKeyString(); +}; + +PrivateKey.prototype.getExtendedPrivateKeyString = function() { + return this.bip.extendedPrivateKeyString(); +}; + +PrivateKey.prototype._getBIP32 = function(path) { + if (typeof path === 'undefined') { + return this.bip; + } + return this.bip.derive(path); +}; + PrivateKey.prototype.get = function(index,isChange) { - var derivedBIP32 = this.getBIP32(index,isChange); + var path = PublicKeyRing.Branch(index, isChange); + var pk = this.privateKeyCache[path]; + if (!pk) { + var derivedBIP32 = this._getBIP32(path); + pk = this.privateKeyCache[path] = derivedBIP32.eckey.private.toString('hex'); + } else { + //console.log('cache hit!'); + } var wk = new WalletKey({network: this.network}); - var p = derivedBIP32.eckey.private.toString('hex'); - wk.fromObj({priv: p}); + wk.fromObj({priv: pk}); return wk; }; diff --git a/js/models/core/PublicKeyRing.js b/js/models/core/PublicKeyRing.js index 25feab792..9a52cb9cf 100644 --- a/js/models/core/PublicKeyRing.js +++ b/js/models/core/PublicKeyRing.js @@ -25,10 +25,12 @@ function PublicKeyRing(opts) { this.requiredCopayers = opts.requiredCopayers || 3; this.totalCopayers = opts.totalCopayers || 5; - this.copayersBIP32 = []; + this.copayersBIP32 = opts.copayersBIP32 || []; - this.changeAddressIndex=0; - this.addressIndex=0; + this.changeAddressIndex= opts.changeAddressIndex || 0; + this.addressIndex= opts.addressIndex || 0; + + this.publicKeysCache = opts.publicKeysCache || {}; } /* @@ -40,33 +42,18 @@ function PublicKeyRing(opts) { * */ -PublicKeyRing.PublicBranch = function (index) { - return 'm/0/'+index; -}; - -PublicKeyRing.ChangeBranch = function (index) { - return 'm/1/'+index; +PublicKeyRing.Branch = function (index, isChange) { + return 'm/'+(isChange?1:0)+'/'+index; }; PublicKeyRing.fromObj = function (data) { - if (!data.ts) { + if (data instanceof PublicKeyRing) { throw new Error('bad data format: Did you use .toObj()?'); } - var config = { networkName: data.networkName || 'livenet' }; - - var w = new PublicKeyRing(config); - - w.walletId = data.walletId; - w.requiredCopayers = data.requiredCopayers; - w.totalCopayers = data.totalCopayers; - w.addressIndex = data.addressIndex; - w.changeAddressIndex = data.changeAddressIndex; - w.copayersBIP32 = data.copayersExtPubKeys.map( function (pk) { + data.copayersBIP32 = data.copayersExtPubKeys.map(function(pk) { return new BIP32(pk); }); - - w.ts = data.ts; - return w; + return new PublicKeyRing(data); }; PublicKeyRing.prototype.toObj = function() { @@ -81,7 +68,7 @@ PublicKeyRing.prototype.toObj = function() { copayersExtPubKeys: this.copayersBIP32.map( function (b) { return b.extendedPublicKeyString(); }), - ts: parseInt(Date.now() / 1000), + publicKeysCache: this.publicKeysCache }; }; @@ -95,7 +82,6 @@ PublicKeyRing.prototype.registeredCopayers = function () { }; - PublicKeyRing.prototype.isComplete = function () { return this.registeredCopayers() >= this.totalCopayers; }; @@ -134,12 +120,19 @@ PublicKeyRing.prototype.addCopayer = function (newEpk) { PublicKeyRing.prototype.getPubKeys = function (index, isChange) { this._checkKeys(); - var pubKeys = []; - var l = this.copayersBIP32.length; - for(var i=0; i + diff --git a/test/test.PrivateKey.js b/test/test.PrivateKey.js index 8875e7f3f..463f18054 100644 --- a/test/test.PrivateKey.js +++ b/test/test.PrivateKey.js @@ -24,8 +24,8 @@ describe('PrivateKey model', function() { it('should create an instance', function () { var w = new PrivateKey(config); should.exist(w); - should.exist(w.BIP32); - should.exist(w.BIP32.derive); + should.exist(w.bip); + should.exist(w.bip.derive); }); it('should derive priv keys', function () { @@ -76,15 +76,14 @@ describe('PrivateKey model', function() { var w1 = new PrivateKey(config); var w2 = PrivateKey.fromObj(w1.toObj()); - w2.getBIP32().extendedPrivateKeyString().should.equal(w1.getBIP32().extendedPrivateKeyString()); - w2.getBIP32().extendedPublicKeyString().should.equal(w1.getBIP32().extendedPublicKeyString()); + w2.toObj().extendedPrivateKeyString.should.equal(w1.toObj().extendedPrivateKeyString); w2.id.should.equal(w1.id); - w2.getBIP32(1,1).extendedPrivateKeyString().should - .equal(w1.getBIP32(1,1).extendedPrivateKeyString()); - w2.getBIP32(1,0).extendedPrivateKeyString().should - .equal(w1.getBIP32(1,0).extendedPrivateKeyString()); + JSON.stringify(w2.get(1,1).storeObj()).should + .equal(JSON.stringify(w1.get(1,1).storeObj())); + JSON.stringify(w2.get(1,0).storeObj()).should + .equal(JSON.stringify(w1.get(1,0).storeObj())); }); diff --git a/test/test.TxProposals.js b/test/test.TxProposals.js index aa120a17b..4c1f6c0bd 100644 --- a/test/test.TxProposals.js +++ b/test/test.TxProposals.js @@ -43,7 +43,7 @@ var createPKR = function (bip32s) { for(var i=0; i<5; i++) { if (bip32s) { var b=bip32s[i]; - w.addCopayer(b?b.extendedPublicKeyString():null); + w.addCopayer(b?b.getExtendedPublicKeyString():null); } else w.addCopayer(); @@ -110,7 +110,7 @@ describe('TxProposals model', function() { networkName: config.networkName, }); var start = new Date().getTime(); - var pkr=createPKR([priv.getBIP32()]); + var pkr=createPKR([priv]); var ts = Date.now(); var isChange=0; var index=0; @@ -150,7 +150,7 @@ describe('TxProposals model', function() { var ts = Date.now(); var isChange=0; var index=0; - var pkr = createPKR([priv.getBIP32()]); + var pkr = createPKR([priv]); var opts = {remainderOut: { address: pkr.generateAddress(true).toString() }}; @@ -225,7 +225,7 @@ var _dumpChunks = function (scriptSig, label) { var ts = Date.now(); var isChange=0; var index=0; - var pkr = createPKR([priv.getBIP32(), priv2.getBIP32()]); + var pkr = createPKR([priv, priv2]); var opts = {remainderOut: { address: pkr.generateAddress(true).toString() }}; var w = new TxProposals({ @@ -325,7 +325,7 @@ var _dumpChunks = function (scriptSig, label) { var ts = Date.now(); var isChange=0; var index=0; - var pkr = createPKR([priv.getBIP32(), priv2.getBIP32(), priv3.getBIP32() ]); + var pkr = createPKR([priv, priv2, priv3]); var opts = {remainderOut: { address: pkr.generateAddress(true).toString() }}; var w = new TxProposals({ @@ -416,7 +416,7 @@ var _dumpChunks = function (scriptSig, label) { it('#toObj #fromObj roundtrip', function () { var priv = new PrivateKey(config); - var pkr = createPKR([priv.getBIP32()]); + var pkr = createPKR([priv]); var w = new TxProposals({ walletId: 'qwerty', networkName: config.networkName, diff --git a/test/test.Wallet.js b/test/test.Wallet.js index 87423c7ee..f7eaad05a 100644 --- a/test/test.Wallet.js +++ b/test/test.Wallet.js @@ -41,7 +41,7 @@ describe('Wallet model', function() { requiredCopayers: c.requiredCopayers, totalCopayers: c.totalCopayers, }); - c.publicKeyRing.addCopayer(c.privateKey.getBIP32().extendedPublicKeyString()); + c.publicKeyRing.addCopayer(c.privateKey.getExtendedPublicKeyString()); c.txProposals = new copay.TxProposals({ networkName: c.networkName, @@ -87,16 +87,16 @@ describe('Wallet model', function() { } ]; - var createW2 = function (bip32s) { + var createW2 = function (privateKeys) { var w = createW(); should.exist(w); var pkr = w.publicKeyRing; for(var i=0; i<4; i++) { - if (bip32s) { - var b=bip32s[i]; - pkr.addCopayer(b?b.extendedPublicKeyString():null); + if (privateKeys) { + var k=privateKeys[i]; + pkr.addCopayer(k?k.getExtendedPublicKeyString():null); } else pkr.addCopayer(); diff --git a/test/test.performance.js b/test/test.performance.js new file mode 100644 index 000000000..e1cd7180c --- /dev/null +++ b/test/test.performance.js @@ -0,0 +1,69 @@ +'use strict'; + +var chai = chai || require('chai'); +var should = chai.should(); +var PrivateKey = require('../js/models/core/PrivateKey'); +var PublicKeyRing = require('../js/models/core/PublicKeyRing'); + +describe('Performance tests', function() { + describe('PrivateKey', function() { + it('should optimize BIP32 private key gen time with cache', function() { + var k1 = new PrivateKey(); + var generateN = 25; + var generated = []; + var start1 = new Date().getTime(); + for (var i = 0; i < generateN; i++) { + var k = JSON.stringify(k1.get(i, false).storeObj()); + generated.push(k); + } + var delta1 = new Date().getTime() - start1; + var backup = k1.toObj(); + var k2 = PrivateKey.fromObj(backup); + var start2 = new Date().getTime(); + for (var i = 0; i < generateN; i++) { + var k = JSON.stringify(k2.get(i, false).storeObj()); + generated[i].should.equal(k); + } + var delta2 = new Date().getTime() - start2; + delta2.should.be.below(delta1); + }); + }); + describe('PublicKeyRing', function() { + var maxN = 7; + for (var n = 1; n < maxN; n++) { + for (var m = 1; m <= n; m++) { + var M = m; + var N = n; + (function(M, N) { + it('should optimize BIP32 publickey gen time with cache for ' + M + '-of-' + N, function() { + var pkr1 = new PublicKeyRing({ + totalCopayers: N, + requiredCopayers: M + }); + for (var i = 0; i < N; i++) { + pkr1.addCopayer(); // add new random ext public key + } + var generateN = 5; + var generated = []; + var start1 = new Date().getTime(); + for (var i = 0; i < generateN; i++) { + var pubKeys = JSON.stringify(pkr1.getPubKeys(i, false)); + generated.push(pubKeys); + } + var delta1 = new Date().getTime() - start1; + var backup = pkr1.toObj(); + var pkr2 = PublicKeyRing.fromObj(backup); + var start2 = new Date().getTime(); + for (var i = 0; i < generateN; i++) { + var pubKeys = JSON.stringify(pkr2.getPubKeys(i, false)); + generated[i].should.equal(pubKeys); + } + var delta2 = new Date().getTime() - start2; + delta2.should.be.below(delta1); + }); + })(M, N); + } + } + + }); +}); diff --git a/util/build.js b/util/build.js index 509c43466..75d761858 100755 --- a/util/build.js +++ b/util/build.js @@ -50,6 +50,18 @@ var createBundle = function(opts) { b.require('./test/mocks/FakeStorage', { expose: './mocks/FakeStorage' }); + b.require('./js/models/network/WebRTC', { + expose: '../js/models/network/WebRTC' + }); + b.require('./js/models/blockchain/Insight', { + expose: '../js/models/blockchain/Insight' + }); + b.require('./js/models/core/PrivateKey', { + expose: '../js/models/core/PrivateKey' + }); + b.require('./js/models/core/PublicKeyRing', { + expose: '../js/models/core/PublicKeyRing' + }); if (!opts.dontminify) {