Browse Source

Merge pull request #162 from matiu/feat/replaceTmpReqKey

Adds replaceTemporaryRequestKey
activeAddress
Ivan Socolsky 10 years ago
parent
commit
d11887ff66
  1. 11
      lib/expressapp.js
  2. 7
      lib/model/copayer.js
  3. 29
      lib/model/wallet.js
  4. 105
      lib/server.js
  5. 2
      package.json
  6. 238
      test/integration/server.js
  7. 4
      test/testdata.js

11
lib/expressapp.js

@ -134,6 +134,17 @@ ExpressApp.start = function(opts) {
}); });
}); });
router.put('/v1/copayers', function(req, res) {
getServerWithAuth(req, res, function(server) {
server.replaceTemporaryRequestKey(req.body, function(err, result) {
if (err) return returnError(err, res, req);
res.json(result);
});
});
});
router.get('/v1/wallets/', function(req, res) { router.get('/v1/wallets/', function(req, res) {
getServerWithAuth(req, res, function(server) { getServerWithAuth(req, res, function(server) {
var result = {}; var result = {};

7
lib/model/copayer.js

@ -18,8 +18,8 @@ function Copayer() {
Copayer.create = function(opts) { Copayer.create = function(opts) {
opts = opts || {}; opts = opts || {};
$.checkArgument(opts.xPubKey, 'Missing extended public key'); $.checkArgument(opts.xPubKey, 'Missing copayer extended public key');
$.checkArgument(opts.requestPubKey, 'Missing request public key'); $.checkArgument(opts.requestPubKey, 'Missing copayer request public key');
opts.copayerIndex = opts.copayerIndex || 0; opts.copayerIndex = opts.copayerIndex || 0;
@ -35,6 +35,7 @@ Copayer.create = function(opts) {
x.addressManager = AddressManager.create({ x.addressManager = AddressManager.create({
copayerIndex: opts.copayerIndex copayerIndex: opts.copayerIndex
}); });
x.isTemporaryRequestKey = opts.isTemporaryRequestKey || false;
return x; return x;
}; };
@ -48,6 +49,8 @@ Copayer.fromObj = function(obj) {
x.xPubKey = obj.xPubKey; x.xPubKey = obj.xPubKey;
x.requestPubKey = obj.requestPubKey; x.requestPubKey = obj.requestPubKey;
x.signature = obj.signature; x.signature = obj.signature;
x.isTemporaryRequestKey = obj.isTemporaryRequestKey;
x.addressManager = AddressManager.fromObj(obj.addressManager); x.addressManager = AddressManager.fromObj(obj.addressManager);
return x; return x;

29
lib/model/wallet.js

@ -20,7 +20,7 @@ Wallet.create = function(opts) {
var x = new Wallet(); var x = new Wallet();
x.createdOn = Math.floor(Date.now() / 1000); x.createdOn = Math.floor(Date.now() / 1000);
x.id = Uuid.v4(); x.id = opts.id || Uuid.v4();
x.name = opts.name; x.name = opts.name;
x.m = opts.m; x.m = opts.m;
x.n = opts.n; x.n = opts.n;
@ -89,15 +89,36 @@ Wallet.prototype.isShared = function() {
return this.n > 1; return this.n > 1;
}; };
Wallet.prototype._updatePublicKeyRing = function() {
this.publicKeyRing = _.map(this.copayers, function(copayer) {
return _.pick(copayer, ['xPubKey', 'requestPubKey', 'isTemporaryRequestKey']);
});
};
Wallet.prototype.addCopayer = function(copayer) { Wallet.prototype.addCopayer = function(copayer) {
this.copayers.push(copayer);
this.copayers.push(copayer);
if (this.copayers.length < this.n) return; if (this.copayers.length < this.n) return;
this.status = 'complete'; this.status = 'complete';
this.publicKeyRing = _.map(this.copayers, function(copayer) { this._updatePublicKeyRing();
return _.pick(copayer, ['xPubKey', 'requestPubKey']); };
Wallet.prototype.updateCopayerRequestKey = function(copayerId, requestPubKey, signature) {
$.checkState(this.copayers.length == this.n);
var c = _.find(this.copayers, {
id: copayerId,
}); });
$.checkState(c)
.checkState(c.isTemporaryRequestKey);
c.requestPubKey = requestPubKey;
c.isTemporaryRequestKey = false;
c.signature = signature;
this._updatePublicKeyRing();
}; };
Wallet.prototype.getCopayer = function(copayerId) { Wallet.prototype.getCopayer = function(copayerId) {

105
lib/server.js

@ -79,7 +79,6 @@ WalletService.getInstanceWithAuth = function(opts, cb) {
if (!copayer) return cb(new ClientError('NOTAUTHORIZED', 'Copayer not found')); if (!copayer) return cb(new ClientError('NOTAUTHORIZED', 'Copayer not found'));
var isValid = server._verifySignature(opts.message, opts.signature, copayer.requestPubKey); var isValid = server._verifySignature(opts.message, opts.signature, copayer.requestPubKey);
if (!isValid) if (!isValid)
return cb(new ClientError('NOTAUTHORIZED', 'Invalid signature')); return cb(new ClientError('NOTAUTHORIZED', 'Invalid signature'));
@ -121,17 +120,35 @@ WalletService.prototype.createWallet = function(opts, cb) {
return cb(new ClientError('Invalid public key')); return cb(new ClientError('Invalid public key'));
}; };
var wallet = Wallet.create({ var newWallet;
name: opts.name, async.series([
m: opts.m,
n: opts.n,
network: network,
pubKey: pubKey.toString(),
});
self.storage.storeWallet(wallet, function(err) { function(acb) {
log.debug('Wallet created', wallet.id, network); if (!opts.id)
return cb(err, wallet.id); return acb();
self.storage.fetchWallet(opts.id, function(err, wallet) {
if (wallet) return acb(new ClientError('WEXISTS', '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 +168,69 @@ 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, opts.copayerSignature);
self.storage.storeWalletAndUpdateCopayersLookup(wallet, function(err) {
if (err) return cb(err);
self._notify('CopayerUpdated', {
walletId: opts.walletId,
copayerId: self.copayerId,
copayerName: opts.name,
});
return cb(null, {
copayerId: self.copayerId,
wallet: wallet
});
});
});
});
};
/** /**
* Verifies a signature * Verifies a signature
* @param text * @param text
@ -206,6 +286,7 @@ WalletService.prototype._notify = function(type, data) {
* @param {string} opts.xPubKey - Extended Public Key for this copayer. * @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.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.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) { WalletService.prototype.joinWallet = function(opts, cb) {
var self = this; var self = this;
@ -218,6 +299,7 @@ WalletService.prototype.joinWallet = function(opts, cb) {
Utils.runLocked(opts.walletId, cb, function(cb) { Utils.runLocked(opts.walletId, cb, function(cb) {
self.storage.fetchWallet(opts.walletId, function(err, wallet) { self.storage.fetchWallet(opts.walletId, function(err, wallet) {
if (err) return cb(err); if (err) return cb(err);
if (!wallet) return cb(new ClientError('Wallet not found')); if (!wallet) return cb(new ClientError('Wallet not found'));
@ -239,6 +321,7 @@ WalletService.prototype.joinWallet = function(opts, cb) {
xPubKey: opts.xPubKey, xPubKey: opts.xPubKey,
requestPubKey: opts.requestPubKey, requestPubKey: opts.requestPubKey,
signature: opts.copayerSignature, signature: opts.copayerSignature,
isTemporaryRequestKey: !!opts.isTemporaryRequestKey,
}); });
self.storage.fetchCopayerLookup(copayer.id, function(err, res) { self.storage.fetchCopayerLookup(copayer.id, function(err, res) {

2
package.json

@ -2,7 +2,7 @@
"name": "bitcore-wallet-service", "name": "bitcore-wallet-service",
"description": "A service for Mutisig HD Bitcoin Wallets", "description": "A service for Mutisig HD Bitcoin Wallets",
"author": "BitPay Inc", "author": "BitPay Inc",
"version": "0.0.17", "version": "0.0.18",
"keywords": [ "keywords": [
"bitcoin", "bitcoin",
"copay", "copay",

238
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) { it('should fail to create wallet with no name', function(done) {
var opts = { var opts = {
name: '', name: '',
@ -2595,6 +2632,207 @@ describe('Wallet service', function() {
}); });
}); });
it.skip('should abort scan if there is an error checking address activity', function(done) {}); 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();
});
});
});
});
});
});
});
}); });
}); });

4
test/testdata.js

@ -17,7 +17,9 @@ var copayers = [{
xPrivKey_1H: 'xprv9uZiJ2ZYFNQ1WeKrKfTxrbUnXfNnCtpbdHVuUMSVGB57nhbmjqWbJBQbgGNa2jSbrgQX9NFHXtAoQ3y1hsEgwjKHxUU52ZYTC6m5kjXoNpJ', xPrivKey_1H: 'xprv9uZiJ2ZYFNQ1WeKrKfTxrbUnXfNnCtpbdHVuUMSVGB57nhbmjqWbJBQbgGNa2jSbrgQX9NFHXtAoQ3y1hsEgwjKHxUU52ZYTC6m5kjXoNpJ',
xPubKey_1H: 'xpub68Z4hY6S5jxJj8QKRgzyDjRX5hDGcMYSzWRWGjr6pWc6fVvvHNpqqyj5XXbMcfW737beYZcd7EQau5HS74Ws6Ctx9XwFn2wHQjSUKLbfdFk', xPubKey_1H: 'xpub68Z4hY6S5jxJj8QKRgzyDjRX5hDGcMYSzWRWGjr6pWc6fVvvHNpqqyj5XXbMcfW737beYZcd7EQau5HS74Ws6Ctx9XwFn2wHQjSUKLbfdFk',
privKey_1H_0: 'e334a0ce3d573bd99fc4cd7e2065e39dc7851cb61da7f381431c253c3e230828', privKey_1H_0: 'e334a0ce3d573bd99fc4cd7e2065e39dc7851cb61da7f381431c253c3e230828',
pubKey_1H_0: '02db35c363e2c904bba3cd0eadb6b8d68fc1d8e6160d632b8fb91a471b80e40c86' pubKey_1H_0: '02db35c363e2c904bba3cd0eadb6b8d68fc1d8e6160d632b8fb91a471b80e40c86',
privKey_1_0: '89833f39998ff14f2cbb461365ccba30583abfd2a7845e0bb61849275802005d',
pubKey_1_0: '022ef899f914c9b90a5544923a3aa6de5ddcd6c9d0b603b63ab9fd0c2cdc5d3772',
}, { }, {
id: 'cf652642ebf997460d642231f9929c39257bcd4c42e9ecb312c226961e21c882', id: 'cf652642ebf997460d642231f9929c39257bcd4c42e9ecb312c226961e21c882',
xPrivKey: 'xprv9s21ZrQH143K2XtR13LpXVAPaGTFsPY5dJfNvNTShfhdVW16vdDaYGjLwHmyKLCtv3F6h9NpKsAGusmKCZNQvKcYqYf9MjCLwJwsoEyAuge', xPrivKey: 'xprv9s21ZrQH143K2XtR13LpXVAPaGTFsPY5dJfNvNTShfhdVW16vdDaYGjLwHmyKLCtv3F6h9NpKsAGusmKCZNQvKcYqYf9MjCLwJwsoEyAuge',

Loading…
Cancel
Save