diff --git a/lib/model/copayer.js b/lib/model/copayer.js index bd956f8..ee587b7 100644 --- a/lib/model/copayer.js +++ b/lib/model/copayer.js @@ -18,8 +18,8 @@ function Copayer() { Copayer.create = function(opts) { opts = opts || {}; - $.checkArgument(opts.xPubKey, 'Missing extended public key'); - $.checkArgument(opts.requestPubKey, 'Missing request public key'); + $.checkArgument(opts.xPubKey, 'Missing copayer extended public key'); + $.checkArgument(opts.requestPubKey, 'Missing copayer request public key'); opts.copayerIndex = opts.copayerIndex || 0; @@ -35,6 +35,7 @@ Copayer.create = function(opts) { x.addressManager = AddressManager.create({ copayerIndex: opts.copayerIndex }); + x.isTemporaryRequestKey = opts.isTemporaryRequestKey || false; return x; }; @@ -48,6 +49,8 @@ Copayer.fromObj = function(obj) { x.xPubKey = obj.xPubKey; x.requestPubKey = obj.requestPubKey; x.signature = obj.signature; + x.isTemporaryRequestKey = obj.isTemporaryRequestKey || false; + x.addressManager = AddressManager.fromObj(obj.addressManager); return x; diff --git a/lib/model/wallet.js b/lib/model/wallet.js index 711b1d3..51ed4e2 100644 --- a/lib/model/wallet.js +++ b/lib/model/wallet.js @@ -20,7 +20,7 @@ Wallet.create = function(opts) { var x = new Wallet(); x.createdOn = Math.floor(Date.now() / 1000); - x.id = Uuid.v4(); + x.id = opts.id || Uuid.v4(); x.name = opts.name; x.m = opts.m; x.n = opts.n; @@ -100,6 +100,23 @@ Wallet.prototype.addCopayer = function(copayer) { }); }; +Wallet.prototype.updateCopayerRequestKey = function(copayerId, requestPubKey) { + $.checkState(this.copayers.length == this.n); + + var c = _.find(this.copayers, { + id: copayerId, + }); + + $.checkState(c) + .checkState(c.isTemporaryRequestKey); + + c.requestPubKey = requestPubKey; + c.isTemporaryRequestKey = false; + this.publicKeyRing = _.map(this.copayers, function(copayer) { + return _.pick(copayer, ['xPubKey', 'requestPubKey']); + }); +}; + Wallet.prototype.getCopayer = function(copayerId) { return _.find(this.copayers, { id: copayerId diff --git a/lib/server.js b/lib/server.js index 78eb298..bcde6b4 100644 --- a/lib/server.js +++ b/lib/server.js @@ -121,17 +121,35 @@ WalletService.prototype.createWallet = function(opts, cb) { return cb(new ClientError('Invalid public key')); }; - var wallet = Wallet.create({ - name: opts.name, - m: opts.m, - n: opts.n, - network: network, - pubKey: pubKey.toString(), - }); + var newWallet; + async.series([ - self.storage.storeWallet(wallet, function(err) { - log.debug('Wallet created', wallet.id, network); - return cb(err, wallet.id); + function(acb) { + if (!opts.id) + return acb(); + + self.storage.fetchWallet(opts.id, function(err, wallet) { + if (wallet) return acb(new ClientError('Wallet already exists')); + return acb(err); + }); + }, + function(acb) { + var wallet = Wallet.create({ + name: opts.name, + m: opts.m, + n: opts.n, + network: network, + pubKey: pubKey.toString(), + id: opts.id, + }); + self.storage.storeWallet(wallet, function(err) { + log.debug('Wallet created', wallet.id, network); + newWallet = wallet; + return acb(err); + }); + } + ], function(err) { + return cb(err, newWallet ? newWallet.id : null); }); }; @@ -151,6 +169,63 @@ WalletService.prototype.getWallet = function(opts, cb) { }; +/** + * Replace temporary request key + * @param {Object} opts + * @param {string} opts.name - The copayer name. + * @param {string} opts.xPubKey - Extended Public Key for this copayer. + * @param {string} opts.requestPubKey - Public Key used to check requests from this copayer. + * @param {string} opts.copayerSignature - S(name|xPubKey|requestPubKey). Used by other copayers to verify the that the copayer joining knows the wallet secret. + */ +WalletService.prototype.replaceTemporaryRequestKey = function(opts, cb) { + var self = this; + + if (!Utils.checkRequired(opts, ['name', 'xPubKey', 'requestPubKey', 'copayerSignature'])) + return cb(new ClientError('Required argument missing')); + + + if (_.isEmpty(opts.name)) + return cb(new ClientError('Invalid copayer name')); + + + if (opts.isTemporaryRequestKey) + return cb(new ClientError('Bad arguments')); + + Utils.runLocked(self.walletId, cb, function(cb) { + self.storage.fetchWallet(self.walletId, function(err, wallet) { + if (err) return cb(err); + + if (!wallet) return cb(new ClientError('Wallet not found')); + var hash = WalletUtils.getCopayerHash(opts.name, opts.xPubKey, opts.requestPubKey); + if (!self._verifySignature(hash, opts.copayerSignature, wallet.pubKey)) { + return cb(new ClientError()); + } + + var oldCopayerData = _.find(wallet.copayers, { + id: self.copayerId + }); + $.checkState(oldCopayerData); + + if (oldCopayerData.xPubKey !== opts.xPubKey || oldCopayerData.name !== opts.name || !oldCopayerData.isTemporaryRequestKey) + return cb(new ClientError('CDATAMISMATCH', 'Copayer data mismatch')); + + if (wallet.copayers.length != wallet.n) + return cb(new ClientError('WNOTFULL', 'Replace only works on full wallets')); + + wallet.updateCopayerRequestKey(self.copayerId, opts.requestPubKey); + + self.storage.storeWalletAndUpdateCopayersLookup(wallet, function(err) { + if (err) return cb(err); + + return cb(null, { + copayerId: self.copayerId, + wallet: wallet + }); + }); + }); + }); +}; + /** * Verifies a signature * @param text @@ -206,6 +281,7 @@ WalletService.prototype._notify = function(type, data) { * @param {string} opts.xPubKey - Extended Public Key for this copayer. * @param {string} opts.requestPubKey - Public Key used to check requests from this copayer. * @param {string} opts.copayerSignature - S(name|xPubKey|requestPubKey). Used by other copayers to verify the that the copayer joining knows the wallet secret. + * @param {string} opts.isTemporaryRequestKey - requestPubKey will be marked as 'temporary' (only used for Copay migration) */ WalletService.prototype.joinWallet = function(opts, cb) { var self = this; @@ -218,6 +294,7 @@ WalletService.prototype.joinWallet = function(opts, cb) { Utils.runLocked(opts.walletId, cb, function(cb) { self.storage.fetchWallet(opts.walletId, function(err, wallet) { + if (err) return cb(err); if (!wallet) return cb(new ClientError('Wallet not found')); @@ -239,6 +316,7 @@ WalletService.prototype.joinWallet = function(opts, cb) { xPubKey: opts.xPubKey, requestPubKey: opts.requestPubKey, signature: opts.copayerSignature, + isTemporaryRequestKey: !!opts.isTemporaryRequestKey, }); self.storage.fetchCopayerLookup(copayer.id, function(err, res) { diff --git a/package.json b/package.json index 93d5b1d..1aa796c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "bitcore-wallet-service", "description": "A service for Mutisig HD Bitcoin Wallets", "author": "BitPay Inc", - "version": "0.0.17", + "version": "0.0.18", "keywords": [ "bitcoin", "copay", diff --git a/test/integration/server.js b/test/integration/server.js index ed74cd9..a5558b6 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -299,6 +299,43 @@ describe('Wallet service', function() { }); }); + + it('should create wallet with given id', function(done) { + var opts = { + name: 'my wallet', + m: 2, + n: 3, + pubKey: TestData.keyPair.pub, + id: '1234', + }; + server.createWallet(opts, function(err, walletId) { + should.not.exist(err); + server.storage.fetchWallet('1234', function(err, wallet) { + should.not.exist(err); + wallet.id.should.equal(walletId); + wallet.name.should.equal('my wallet'); + done(); + }); + }); + }); + + it('should fail to create wallets with same id', function(done) { + var opts = { + name: 'my wallet', + m: 2, + n: 3, + pubKey: TestData.keyPair.pub, + id: '1234', + }; + server.createWallet(opts, function(err, walletId) { + server.createWallet(opts, function(err, walletId) { + err.message.should.contain('Wallet already exists'); + done(); + }); + }); + }); + + it('should fail to create wallet with no name', function(done) { var opts = { name: '', @@ -2595,6 +2632,207 @@ describe('Wallet service', function() { }); }); it.skip('should abort scan if there is an error checking address activity', function(done) {}); + + }); + + + describe('#replaceTemporaryRequestKey', function() { + var server, walletId; + beforeEach(function(done) { + server = new WalletService(); + var walletOpts = { + name: 'my wallet', + m: 2, + n: 2, + pubKey: TestData.keyPair.pub, + }; + server.createWallet(walletOpts, function(err, wId) { + should.not.exist(err); + should.exist.walletId; + walletId = wId; + done(); + }); + }); + + it('should join existing wallet with temporaryRequestKey', function(done) { + var copayerOpts = helpers.getSignedCopayerOpts({ + walletId: walletId, + name: 'me', + xPubKey: TestData.copayers[0].xPubKey_45H, + requestPubKey: TestData.copayers[0].pubKey_1H_0, + }); + copayerOpts.isTemporaryRequestKey = true; + + server.joinWallet(copayerOpts, function(err, result) { + should.not.exist(err); + var copayerId = result.copayerId; + helpers.getAuthServer(copayerId, function(server) { + server.getWallet({}, function(err, wallet) { + wallet.id.should.equal(walletId); + var copayer = wallet.copayers[0]; + copayer.isTemporaryRequestKey.should.equal(true); + done(); + }); + }); + }); + }); + + it('should fail to replace a temporaryRequestKey on a not-complete wallet', function(done) { + var copayerOpts = helpers.getSignedCopayerOpts({ + walletId: walletId, + name: 'me', + xPubKey: TestData.copayers[0].xPubKey_45H, + requestPubKey: TestData.copayers[0].pubKey_1_0, + }); + copayerOpts.isTemporaryRequestKey = true; + + server.joinWallet(copayerOpts, function(err, result) { + should.not.exist(err); + var copayerId = result.copayerId; + helpers.getAuthServer(copayerId, function(server) { + server.getWallet({}, function(err, wallet) { + + var copayerOpts = helpers.getSignedCopayerOpts({ + walletId: walletId, + name: 'me', + xPubKey: TestData.copayers[0].xPubKey_45H, + requestPubKey: TestData.copayers[0].pubKey_1H_0, + }); + copayerOpts.isTemporaryRequestKey = false; + server.replaceTemporaryRequestKey(copayerOpts, function(err, wallet) { + err.code.should.equal('WNOTFULL'); + done(); + }); + }); + }); + }); + }); + + + it('should fail to replace a temporaryRequestKey is Copayer is not in wallet', function(done) { + var copayerOpts = helpers.getSignedCopayerOpts({ + walletId: walletId, + name: 'me', + xPubKey: TestData.copayers[0].xPubKey_45H, + requestPubKey: TestData.copayers[0].pubKey_1_0, + }); + copayerOpts.isTemporaryRequestKey = true; + + server.joinWallet(copayerOpts, function(err, result) { + should.not.exist(err); + var copayerId = result.copayerId; + helpers.getAuthServer(copayerId, function(server) { + server.getWallet({}, function(err, wallet) { + + var copayerOpts = helpers.getSignedCopayerOpts({ + walletId: walletId, + name: 'me', + xPubKey: TestData.copayers[1].xPubKey_45H, + requestPubKey: TestData.copayers[1].pubKey_1H_0, + }); + copayerOpts.isTemporaryRequestKey = false; + server.replaceTemporaryRequestKey(copayerOpts, function(err, wallet) { + err.code.should.equal('CDATAMISMATCH'); + done(); + }); + }); + }); + }); + }); + + it('should fail replace a temporaryRequestKey with invalid copayer', function(done) { + var copayerOpts = helpers.getSignedCopayerOpts({ + walletId: walletId, + name: 'me', + xPubKey: TestData.copayers[0].xPubKey_45H, + requestPubKey: TestData.copayers[0].pubKey_1_0, + }); + copayerOpts.isTemporaryRequestKey = true; + + server.joinWallet(copayerOpts, function(err, result) { + should.not.exist(err); + + var copayerOpts2 = helpers.getSignedCopayerOpts({ + walletId: walletId, + name: 'me', + xPubKey: TestData.copayers[1].xPubKey_45H, + requestPubKey: TestData.copayers[1].pubKey_1H_0, + }); + copayerOpts2.isTemporaryRequestKey = false; + + server.joinWallet(copayerOpts2, function(err, result) { + should.not.exist(err); + + var copayerId = result.copayerId; + helpers.getAuthServer(copayerId, function(server) { + server.getWallet({}, function(err, wallet) { + + var copayerOpts = helpers.getSignedCopayerOpts({ + walletId: walletId, + name: 'me', + xPubKey: TestData.copayers[1].xPubKey_45H, + requestPubKey: TestData.copayers[1].pubKey_1H_0, + }); + copayerOpts.isTemporaryRequestKey = false; + server.replaceTemporaryRequestKey(copayerOpts, function(err, wallet) { + err.code.should.equal('CDATAMISMATCH'); + done(); + }); + }); + }); + }); + }); + }); + + it('should replace a temporaryRequestKey', function(done) { + var copayerOpts = helpers.getSignedCopayerOpts({ + walletId: walletId, + name: 'me', + xPubKey: TestData.copayers[0].xPubKey_45H, + requestPubKey: TestData.copayers[0].pubKey_1_0, + }); + copayerOpts.isTemporaryRequestKey = true; + + server.joinWallet(copayerOpts, function(err, result) { + should.not.exist(err); + var copayerId = result.copayerId; + + var copayerOpts2 = helpers.getSignedCopayerOpts({ + walletId: walletId, + name: 'me', + xPubKey: TestData.copayers[1].xPubKey_45H, + requestPubKey: TestData.copayers[1].pubKey_1H_0, + }); + copayerOpts2.isTemporaryRequestKey = false; + + server.joinWallet(copayerOpts2, function(err, result) { + should.not.exist(err); + var copayerId2 = result.copayerId; + + + helpers.getAuthServer(copayerId, function(server) { + server.getWallet({}, function(err, wallet) { + + var copayerOpts = helpers.getSignedCopayerOpts({ + walletId: walletId, + name: 'me', + xPubKey: TestData.copayers[0].xPubKey_45H, + requestPubKey: TestData.copayers[0].pubKey_1H_0, + }); + copayerOpts.isTemporaryRequestKey = false; + server.replaceTemporaryRequestKey(copayerOpts, function(err, wallet) { + should.not.exist(err); + server.getWallet({}, function(err, wallet) { + wallet.copayers[0].isTemporaryRequestKey.should.equal(false); + wallet.copayers[1].isTemporaryRequestKey.should.equal(false); + done(); + }); + }); + }); + }); + }); + }); + }); }); }); diff --git a/test/testdata.js b/test/testdata.js index ca9a279..8fc9fb8 100644 --- a/test/testdata.js +++ b/test/testdata.js @@ -17,7 +17,9 @@ var copayers = [{ xPrivKey_1H: 'xprv9uZiJ2ZYFNQ1WeKrKfTxrbUnXfNnCtpbdHVuUMSVGB57nhbmjqWbJBQbgGNa2jSbrgQX9NFHXtAoQ3y1hsEgwjKHxUU52ZYTC6m5kjXoNpJ', xPubKey_1H: 'xpub68Z4hY6S5jxJj8QKRgzyDjRX5hDGcMYSzWRWGjr6pWc6fVvvHNpqqyj5XXbMcfW737beYZcd7EQau5HS74Ws6Ctx9XwFn2wHQjSUKLbfdFk', privKey_1H_0: 'e334a0ce3d573bd99fc4cd7e2065e39dc7851cb61da7f381431c253c3e230828', - pubKey_1H_0: '02db35c363e2c904bba3cd0eadb6b8d68fc1d8e6160d632b8fb91a471b80e40c86' + pubKey_1H_0: '02db35c363e2c904bba3cd0eadb6b8d68fc1d8e6160d632b8fb91a471b80e40c86', + privKey_1_0: '89833f39998ff14f2cbb461365ccba30583abfd2a7845e0bb61849275802005d', + pubKey_1_0: '022ef899f914c9b90a5544923a3aa6de5ddcd6c9d0b603b63ab9fd0c2cdc5d3772', }, { id: 'cf652642ebf997460d642231f9929c39257bcd4c42e9ecb312c226961e21c882', xPrivKey: 'xprv9s21ZrQH143K2XtR13LpXVAPaGTFsPY5dJfNvNTShfhdVW16vdDaYGjLwHmyKLCtv3F6h9NpKsAGusmKCZNQvKcYqYf9MjCLwJwsoEyAuge',