diff --git a/copay.js b/copay.js index 8f127e2d9..9149fa071 100644 --- a/copay.js +++ b/copay.js @@ -4,6 +4,7 @@ module.exports.PublicKeyRing = require('./js/models/core/PublicKeyRing'); module.exports.TxProposals = require('./js/models/core/TxProposals'); module.exports.PrivateKey = require('./js/models/core/PrivateKey'); module.exports.Passphrase = require('./js/models/core/Passphrase'); +module.exports.Structure = require('./js/models/core/Structure'); // components diff --git a/js/models/core/PrivateKey.js b/js/models/core/PrivateKey.js index 8f0643b59..086f2561b 100644 --- a/js/models/core/PrivateKey.js +++ b/js/models/core/PrivateKey.js @@ -3,11 +3,11 @@ var imports = require('soop').imports(); var bitcore = require('bitcore'); -var HK = bitcore.HierarchicalKey; +var HK = bitcore.HierarchicalKey; var WalletKey = bitcore.WalletKey; var networks = bitcore.networks; var util = bitcore.util; -var PublicKeyRing = require('./PublicKeyRing'); +var Structure = require('./Structure'); function PrivateKey(opts) { opts = opts || {}; @@ -20,13 +20,20 @@ function PrivateKey(opts) { PrivateKey.prototype.getId = function() { if (!this.id) { - var path = PublicKeyRing.ID_BRANCH; - var bip32 = this.bip.derive(path); - this.id= bip32.eckey.public.toString('hex'); + var path = Structure.IdFullBranch; + var idhk = this.bip.derive(path); + this.id= idhk.eckey.public.toString('hex'); } return this.id; }; +PrivateKey.prototype.deriveBIP45Branch = function() { + if (!this.bip45Branch) { + this.bip45Branch = this.bip.derive(Structure.BIP45_PUBLIC_PREFIX); + } + return this.bip45Branch; +} + PrivateKey.fromObj = function(obj) { return new PrivateKey(obj); }; @@ -55,13 +62,11 @@ PrivateKey.prototype._getHK = function(path) { }; PrivateKey.prototype.get = function(index,isChange) { - var path = PublicKeyRing.Branch(index, isChange); + var path = Structure.FullBranch(index, isChange); var pk = this.privateKeyCache[path]; if (!pk) { var derivedHK = this._getHK(path); pk = this.privateKeyCache[path] = derivedHK.eckey.private.toString('hex'); - } else { - //console.log('cache hit!'); } var wk = new WalletKey({network: this.network}); wk.fromObj({priv: pk}); diff --git a/js/models/core/PublicKeyRing.js b/js/models/core/PublicKeyRing.js index c76d0914d..12a5317bd 100644 --- a/js/models/core/PublicKeyRing.js +++ b/js/models/core/PublicKeyRing.js @@ -5,6 +5,8 @@ var imports = require('soop').imports(); var bitcore = require('bitcore'); var HK = bitcore.HierarchicalKey; +var PrivateKey = require('./PrivateKey'); +var Structure = require('./Structure'); var Address = bitcore.Address; var Script = bitcore.Script; var coinUtil = bitcore.util; @@ -36,22 +38,6 @@ function PublicKeyRing(opts) { this.copayerIds = []; } -/* - * This follow Electrum convetion, as described in - * https://bitcointalk.org/index.php?topic=274182.0 - * - * We should probably adopt the next standard once it's ready, as discussed in: - * http://sourceforge.net/p/bitcoin/mailman/message/32148600/ - * - */ - -PublicKeyRing.Branch = function (index, isChange) { - // first 0 is for future use: could be copayerId. - return 'm/0/'+(isChange?1:0)+'/'+index; -}; - -PublicKeyRing.ID_BRANCH = 'm/100/0/0'; - PublicKeyRing.fromObj = function (data) { if (data instanceof PublicKeyRing) { throw new Error('bad data format: Did you use .toObj()?'); @@ -109,13 +95,13 @@ PublicKeyRing.prototype._checkKeys = function() { }; PublicKeyRing.prototype._newExtendedPublicKey = function () { - return new HK(this.network.name) + return new PrivateKey({networkName: this.network.name}) + .deriveBIP45Branch() .extendedPublicKeyString(); }; PublicKeyRing.prototype._updateBip = function (index) { - var path = PublicKeyRing.ID_BRANCH; - var hk = this.copayersHK[index].derive(path); + var hk = this.copayersHK[index].derive(Structure.IdBranch); this.copayerIds[index]= hk.eckey.public.toString('hex'); }; @@ -123,41 +109,41 @@ PublicKeyRing.prototype._setNicknameForIndex = function (index, nickname) { this.nicknameFor[this.copayerIds[index]] = nickname; }; -PublicKeyRing.prototype.nicknameForIndex = function (index) { +PublicKeyRing.prototype.nicknameForIndex = function(index) { return this.nicknameFor[this.copayerIds[index]]; }; -PublicKeyRing.prototype.nicknameForCopayer = function (copayerId) { +PublicKeyRing.prototype.nicknameForCopayer = function(copayerId) { return this.nicknameFor[copayerId]; }; -PublicKeyRing.prototype.addCopayer = function (newEpk, nickname) { +PublicKeyRing.prototype.addCopayer = function(newEpk, nickname) { if (this.isComplete()) - throw new Error('already have all required key:' + this.totalCopayers); + throw new Error('PKR already has all required key:' + this.totalCopayers); + + this.copayersHK.forEach(function(b){ + if (b.extendedPublicKeyString() === newEpk) + throw new Error('PKR already has that key'); + }); if (!newEpk) { newEpk = this._newExtendedPublicKey(); } - this.copayersHK.forEach(function(b){ - if (b.extendedPublicKeyString() === newEpk) - throw new Error('already have that key'); - }); - - var i=this.copayersHK.length; + var i = this.copayersHK.length; var bip = new HK(newEpk); this.copayersHK.push(bip); this._updateBip(i); if (nickname) { - this._setNicknameForIndex(i,nickname); + this._setNicknameForIndex(i, nickname); } return newEpk; }; -PublicKeyRing.prototype.getPubKeys = function (index, isChange) { +PublicKeyRing.prototype.getPubKeys = function(index, isChange) { this._checkKeys(); - var path = PublicKeyRing.Branch(index, isChange); + var path = Structure.Branch(index, isChange); var pubKeys = this.publicKeysCache[path]; if (!pubKeys) { pubKeys = []; diff --git a/js/models/core/Structure.js b/js/models/core/Structure.js new file mode 100644 index 000000000..591360815 --- /dev/null +++ b/js/models/core/Structure.js @@ -0,0 +1,43 @@ +'use strict'; + + +var imports = require('soop').imports(); + +function Structure() { +} + + +/* + * Based on https://github.com/maraoz/bips/blob/master/bip-NNNN.mediawiki + * m / purpose' / cosigner_index / change / address_index + */ +var PURPOSE = 45; +var MAX_NON_HARDENED = 0x80000000 - 1; + +var SHARED_INDEX = MAX_NON_HARDENED - 0; +var ID_INDEX = MAX_NON_HARDENED - 1; + +var BIP45_PUBLIC_PREFIX = 'm/'+ PURPOSE+'\''; +Structure.BIP45_PUBLIC_PREFIX = BIP45_PUBLIC_PREFIX; + +Structure.Branch = function(address_index, isChange, cosigner_index) { + var ret = 'm/'+ + (typeof cosigner_index !== 'undefined'? cosigner_index: SHARED_INDEX)+'/'+ + (isChange?1:0)+'/'+ + address_index; + return ret; +}; + +Structure.FullBranch = function(address_index, isChange, cosigner_index) { + var sub = Structure.Branch(address_index, isChange, cosigner_index); + sub = sub.substring(2); + return BIP45_PUBLIC_PREFIX + '/' + sub; +}; +Structure.IdFullBranch = Structure.FullBranch(0, 0, ID_INDEX); +Structure.IdBranch = Structure.Branch(0, 0, ID_INDEX); +Structure.PURPOSE = PURPOSE; +Structure.MAX_NON_HARDENED = MAX_NON_HARDENED; +Structure.SHARED_INDEX = SHARED_INDEX; +Structure.ID_INDEX = ID_INDEX; + +module.exports = require('soop')(Structure); diff --git a/js/models/core/Wallet.js b/js/models/core/Wallet.js index 1893c6acc..7ffbfa58b 100644 --- a/js/models/core/Wallet.js +++ b/js/models/core/Wallet.js @@ -628,7 +628,7 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, utxos, opts) { var signRet; if (priv) { - b.sign(priv.getAll(pkr.addressIndex, pkr.changeAddressIndex)); + var signed = b.sign(priv.getAll(pkr.addressIndex, pkr.changeAddressIndex)); } var myId = this.getMyCopayerId(); var now = Date.now(); diff --git a/js/models/core/WalletFactory.js b/js/models/core/WalletFactory.js index 602d8c3a2..afd322d6f 100644 --- a/js/models/core/WalletFactory.js +++ b/js/models/core/WalletFactory.js @@ -53,14 +53,6 @@ WalletFactory.prototype.fromObj = function(obj) { console.log('## Decrypting'); //TODO var w = Wallet.fromObj(obj, this.storage, this.network, this.blockchain); w.verbose = this.verbose; - // JIC: Add our key - try { - w.publicKeyRing.addCopayer( - w.privateKey.getExtendedPublicKeyString() - ); - } catch (e) { - // No really an error, just to be sure. - } this.log('### WALLET OPENED:', w.id); return w; }; @@ -107,7 +99,9 @@ WalletFactory.prototype.create = function(opts) { requiredCopayers: requiredCopayers, totalCopayers: totalCopayers, }); - opts.publicKeyRing.addCopayer(opts.privateKey.getExtendedPublicKeyString(), opts.nickname); + opts.publicKeyRing.addCopayer( + opts.privateKey.deriveBIP45Branch().extendedPublicKeyString(), + opts.nickname); this.log('\t### PublicKeyRing Initialized'); opts.txProposals = opts.txProposals || new TxProposals({ diff --git a/test/test.PrivateKey.js b/test/test.PrivateKey.js index 15edb360e..0bfb820c8 100644 --- a/test/test.PrivateKey.js +++ b/test/test.PrivateKey.js @@ -28,10 +28,10 @@ describe('PrivateKey model', function() { }); it('should derive priv keys', function () { - var w = new PrivateKey(config); + var pk = new PrivateKey(config); for(var j=0; j<2; j++) { for(var i=0; i<3; i++) { - var wk = w.get(i,j); + var wk = pk.get(i,j); should.exist(wk); var o=wk.storeObj(); should.exist(o); diff --git a/test/test.Structure.js b/test/test.Structure.js new file mode 100644 index 000000000..722497ab0 --- /dev/null +++ b/test/test.Structure.js @@ -0,0 +1,37 @@ +'use strict'; + +var chai = chai || require('chai'); +var should = chai.should(); +var bitcore = bitcore || require('bitcore'); +var copay = copay || require('../copay'); +var Structure = require('../js/models/core/Structure'); + +describe('Structure model', function() { + it('should have the correct constants', function () { + Structure.MAX_NON_HARDENED.should.equal(Math.pow(2,31) - 1); + Structure.SHARED_INDEX.should.equal(Structure.MAX_NON_HARDENED); + Structure.ID_INDEX.should.equal(Structure.SHARED_INDEX - 1); + Structure.IdFullBranch.should.equal('m/45\'/2147483646/0/0'); + }); + + it('should get the correct branches', function () { + // shared branch (no cosigner index specified) + Structure.FullBranch(0,false).should.equal('m/45\'/2147483647/0/0'); + + // copayer 0, address 0, external address (receiving) + Structure.FullBranch(0,false,0).should.equal('m/45\'/0/0/0'); + + // copayer 0, address 10, external address (receiving) + Structure.FullBranch(0,false,10).should.equal('m/45\'/10/0/0'); + + // copayer 0, address 0, internal address (change) + Structure.FullBranch(0,true,0).should.equal('m/45\'/0/1/0'); + + // copayer 0, address 10, internal address (change) + Structure.FullBranch(10,true,0).should.equal('m/45\'/0/1/10'); + + // copayer 7, address 10, internal address (change) + Structure.FullBranch(10,true,7).should.equal('m/45\'/7/1/10'); + }); + +}); diff --git a/test/test.TxProposals.js b/test/test.TxProposals.js index 1d76b1bae..41ecc2002 100644 --- a/test/test.TxProposals.js +++ b/test/test.TxProposals.js @@ -42,7 +42,7 @@ var createPKR = function (bip32s) { for(var i=0; i<5; i++) { if (bip32s) { var b=bip32s[i]; - w.addCopayer(b?b.getExtendedPublicKeyString():null); + w.addCopayer(b?b.deriveBIP45Branch().extendedPublicKeyString():null); } else w.addCopayer(); @@ -72,7 +72,7 @@ describe('TxProposals model', function() { var priv = new PrivateKey(config); var priv2 = new PrivateKey(config); var priv3 = new PrivateKey(config); - var ts = Date.now(); + var ts = Date.now(); var isChange=0; var index=0; var pkr = createPKR([priv, priv2, priv3]); diff --git a/test/test.Wallet.js b/test/test.Wallet.js index 53a235fe9..1bcd60c2a 100644 --- a/test/test.Wallet.js +++ b/test/test.Wallet.js @@ -42,7 +42,8 @@ describe('Wallet model', function() { requiredCopayers: c.requiredCopayers, totalCopayers: c.totalCopayers, }); - c.publicKeyRing.addCopayer(c.privateKey.getExtendedPublicKeyString()); + var copayerEPK = c.privateKey.deriveBIP45Branch().extendedPublicKeyString() + c.publicKeyRing.addCopayer(copayerEPK); c.txProposals = new copay.TxProposals({ networkName: c.networkName, @@ -102,10 +103,10 @@ describe('Wallet model', function() { for(var i=0; i<4; i++) { if (privateKeys) { var k=privateKeys[i]; - pkr.addCopayer(k?k.getExtendedPublicKeyString():null); - } - else + pkr.addCopayer(k?k.deriveBIP45Branch().extendedPublicKeyString():null); + } else { pkr.addCopayer(); + } } pkr.generateAddress(true); pkr.generateAddress(true); @@ -125,19 +126,19 @@ describe('Wallet model', function() { unspentTest[0].address = w.publicKeyRing.getAddress(1, true).toString(); unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true); - w.createTxSync( + var ntxid = w.createTxSync( '15q6HKjWHAksHcH91JW23BJEuzZgFwydBt', '123456789', unspentTest ); var t = w.txProposals; - var k = Object.keys(t.txps)[0]; - var tx = t.txps[k].builder.build(); + var txp = t.txps[ntxid]; + var tx = txp.builder.build(); should.exist(tx); tx.isComplete().should.equal(false); - Object.keys(t.txps[k].signedBy).length.should.equal(1); - Object.keys(t.txps[k].seenBy).length.should.equal(1); + Object.keys(txp.seenBy).length.should.equal(1); + Object.keys(txp.signedBy).length.should.equal(1); }); it('#addressIsOwn', function () {