From 97e63f9c6fb5c44f56497493bac81b129666d413 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 7 Sep 2015 17:18:32 -0300 Subject: [PATCH] enable both bip44/45 & p2sh/pkh --- lib/expressapp.js | 19 +++-- lib/server.js | 29 +++++--- test/integration/server.js | 142 +++++++++++++++++++++++++++++-------- 3 files changed, 145 insertions(+), 45 deletions(-) diff --git a/lib/expressapp.js b/lib/expressapp.js index 2146a95..20213d4 100644 --- a/lib/expressapp.js +++ b/lib/expressapp.js @@ -130,10 +130,10 @@ ExpressApp.prototype.start = function(opts, cb) { }); }; + // DEPRECATED router.post('/v1/wallets/', function(req, res) { var server = getServer(req, res); - req.body.supportBIP44 = false; - req.body.supportP2PKH = false; + req.body.supportBIP44AndP2PKH = false; server.createWallet(req.body, function(err, walletId) { if (err) return returnError(err, res, req); res.json({ @@ -144,8 +144,7 @@ ExpressApp.prototype.start = function(opts, cb) { router.post('/v2/wallets/', function(req, res) { var server = getServer(req, res); - req.body.supportBIP44 = true; - req.body.supportP2PKH = true; + req.body.supportBIP44AndP2PKH = true; server.createWallet(req.body, function(err, walletId) { if (err) return returnError(err, res, req); res.json({ @@ -163,6 +162,7 @@ ExpressApp.prototype.start = function(opts, cb) { }); }); + // DEPRECATED router.post('/v1/wallets/:id/copayers/', function(req, res) { req.body.walletId = req.params['id']; var server = getServer(req, res); @@ -173,6 +173,17 @@ ExpressApp.prototype.start = function(opts, cb) { }); }); + router.post('/v2/wallets/:id/copayers/', function(req, res) { + req.body.walletId = req.params['id']; + req.body.supportBIP44AndP2PKH = true; + var server = getServer(req, res); + server.joinWallet(req.body, function(err, result) { + if (err) return returnError(err, res, req); + + res.json(result); + }); + }); + // DEPRECATED router.get('/v1/wallets/', function(req, res) { getServerWithAuth(req, res, function(server) { diff --git a/lib/server.js b/lib/server.js index 2b0af0f..b434649 100644 --- a/lib/server.js +++ b/lib/server.js @@ -203,8 +203,7 @@ WalletService.prototype._runLocked = function(cb, task) { * @param {number} opts.n - Total copayers. * @param {string} opts.pubKey - Public key to verify copayers joining have access to the wallet secret. * @param {string} [opts.network = 'livenet'] - The Bitcoin network for this wallet. - * @param {string} [opts.supportBIP44 = false] - Client supports BIP44 paths for 1-of-1 wallets. - * @param {string} [opts.supportP2PKH = false] - Client supports P2PKH address type for 1-of-1 wallets. + * @param {string} [opts.supportBIP44AndP2PKH = false] - Client supports BIP44 & P2PKH for new wallets. */ WalletService.prototype.createWallet = function(opts, cb) { var self = this, @@ -221,11 +220,8 @@ WalletService.prototype.createWallet = function(opts, cb) { if (!_.contains(['livenet', 'testnet'], opts.network)) return cb(new ClientError('Invalid network')); - var derivationStrategy = (opts.n == 1 && opts.supportBIP44) ? - WalletUtils.DERIVATION_STRATEGIES.BIP44 : WalletUtils.DERIVATION_STRATEGIES.BIP45; - - var addressType = (opts.n == 1 && opts.supportP2PKH) ? - WalletUtils.SCRIPT_TYPES.P2PKH : WalletUtils.SCRIPT_TYPES.P2SH; + var derivationStrategy = opts.supportBIP44AndP2PKH ? WalletUtils.DERIVATION_STRATEGIES.BIP44 : WalletUtils.DERIVATION_STRATEGIES.BIP45; + var addressType = (opts.n == 1 && opts.supportBIP44AndP2PKH) ? WalletUtils.SCRIPT_TYPES.P2PKH : WalletUtils.SCRIPT_TYPES.P2SH; try { pubKey = new PublicKey.fromString(opts.pubKey); @@ -412,8 +408,6 @@ WalletService.prototype._notify = function(type, data, opts, cb) { WalletService.prototype._addCopayerToWallet = function(wallet, opts, cb) { var self = this; - if (wallet.copayers.length == wallet.n) return cb(Errors.WALLET_FULL); - var copayer = Model.Copayer.create({ name: opts.name, copayerIndex: wallet.copayers.length, @@ -559,6 +553,7 @@ WalletService.prototype._clientSupportsTXPv2 = function() { * @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 that the copayer joining knows the wallet secret. * @param {string} opts.customData - (optional) Custom data for this copayer. + * @param {string} [opts.supportBIP44AndP2PKH = false] - Client supports BIP44 & P2PKH for joining wallets. */ WalletService.prototype.joinWallet = function(opts, cb) { var self = this; @@ -575,11 +570,25 @@ WalletService.prototype.joinWallet = function(opts, cb) { if (err) return cb(err); if (!wallet) return cb(Errors.WALLET_NOT_FOUND); + if (opts.supportBIP44AndP2PKH) { + // New client trying to join legacy wallet + if (wallet.derivationStrategy == WalletUtils.DERIVATION_STRATEGIES.BIP45) { + return cb(new ClientError('The wallet you are trying to join was created with an older version of the client app.')); + } + } else { + // Legacy client trying to join new wallet + if (wallet.derivationStrategy == WalletUtils.DERIVATION_STRATEGIES.BIP44) { + return cb(new ClientError(Errors.codes.UPGRADE_NEEDED, 'To join this wallet you need to upgrade your client app.')); + } + } + var hash = WalletUtils.getCopayerHash(opts.name, opts.xPubKey, opts.requestPubKey); if (!self._verifySignature(hash, opts.copayerSignature, wallet.pubKey)) { return cb(new ClientError()); } + if (wallet.copayers.length == wallet.n) return cb(Errors.WALLET_FULL); + if (_.find(wallet.copayers, { xPubKey: opts.xPubKey })) return cb(Errors.COPAYER_IN_WALLET); @@ -1209,7 +1218,7 @@ WalletService.prototype.createTx = function(opts, cb) { excludeUnconfirmedUtxos: !!opts.excludeUnconfirmedUtxos, derivationStrategy: wallet.derivationStrategy, addressType: wallet.addressType, - customData: opts.customData + customData: opts.customData, }; if (signingKey.selfSigned) { diff --git a/test/integration/server.js b/test/integration/server.js index 9636ad5..7493742 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -96,15 +96,13 @@ helpers.createAndJoinWallet = function(m, n, opts, cb) { var copayerIds = []; var offset = opts.offset || 0; - var supportBIP44 = _.isBoolean(opts.supportBIP44) ? opts.supportBIP44 : true - var supportP2PKH = _.isBoolean(opts.supportP2PKH) ? opts.supportP2PKH : true + var supportBIP44AndP2PKH = _.isBoolean(opts.supportBIP44AndP2PKH) ? opts.supportBIP44AndP2PKH : true; var walletOpts = { name: 'a wallet', m: m, n: n, pubKey: TestData.keyPair.pub, - supportBIP44: supportBIP44, - supportP2PKH: supportP2PKH, + supportBIP44AndP2PKH: supportBIP44AndP2PKH, }; server.createWallet(walletOpts, function(err, walletId) { if (err) return cb(err); @@ -114,9 +112,10 @@ helpers.createAndJoinWallet = function(m, n, opts, cb) { var copayerOpts = helpers.getSignedCopayerOpts({ walletId: walletId, name: 'copayer ' + (i + 1), - xPubKey: (n == 1 && supportBIP44) ? copayerData.xPubKey_44H_0H_0H : copayerData.xPubKey_45H, + xPubKey: supportBIP44AndP2PKH ? copayerData.xPubKey_44H_0H_0H : copayerData.xPubKey_45H, requestPubKey: copayerData.pubKey_1H_0, customData: 'custom data ' + (i + 1), + supportBIP44AndP2PKH: supportBIP44AndP2PKH, }); server.joinWallet(copayerOpts, function(err, result) { @@ -135,6 +134,7 @@ helpers.createAndJoinWallet = function(m, n, opts, cb) { }); }; + helpers.randomTXID = function() { return Bitcore.crypto.Hash.sha256(new Buffer(Math.random() * 100000)).toString('hex');; }; @@ -509,7 +509,7 @@ describe('Wallet service', function() { txpId = txp.id; async.eachSeries(_.range(2), function(i, next) { var copayer = TestData.copayers[i]; - helpers.getAuthServer(copayer.id45, function(server) { + helpers.getAuthServer(copayer.id44, function(server) { var signatures = helpers.clientSign(txp, copayer.xPrivKey); server.signTx({ txProposalId: txp.id, @@ -567,7 +567,7 @@ describe('Wallet service', function() { txpId = txp.id; async.eachSeries(_.range(1, 3), function(i, next) { var copayer = TestData.copayers[i]; - helpers.getAuthServer(copayer.id45, function(server) { + helpers.getAuthServer(copayer.id44, function(server) { server.rejectTx({ txProposalId: txp.id, }, next); @@ -952,6 +952,21 @@ describe('Wallet service', function() { }); }); + it('should fail to join with mismatching address derivation strategy', function(done) { + var copayerOpts = helpers.getSignedCopayerOpts({ + walletId: walletId, + name: 'me', + xPubKey: TestData.copayers[0].xPubKey_45H, + requestPubKey: TestData.copayers[0].pubKey_1H_0, + supportBIP44AndP2PKH: true, + }); + server.joinWallet(copayerOpts, function(err, result) { + should.exist(err); + err.message.should.contain('The wallet you are trying to join was created with an older version of the client app'); + done(); + }); + }); + it('should join existing wallet', function(done) { var copayerOpts = helpers.getSignedCopayerOpts({ walletId: walletId, @@ -1028,6 +1043,7 @@ describe('Wallet service', function() { name: 'me', xPubKey: TestData.copayers[1].xPubKey_44H_0H_0H, requestPubKey: TestData.copayers[1].pubKey_1H_0, + supportBIP44AndP2PKH: true, }); server.joinWallet(copayerOpts, function(err) { should.exist(err); @@ -1176,8 +1192,7 @@ describe('Wallet service', function() { m: 1, n: 1, pubKey: TestData.keyPair.pub, - supportBIP44: true, - supportP2PKH: true, + supportBIP44AndP2PKH: true, }; server.createWallet(walletOpts, function(err, wid) { should.not.exist(err); @@ -1195,6 +1210,7 @@ describe('Wallet service', function() { m: 1, n: 1, pubKey: TestData.keyPair.pub, + supportBIP44AndP2PKH: false, }; server.createWallet(walletOpts, function(err, wid) { should.not.exist(err); @@ -1206,14 +1222,31 @@ describe('Wallet service', function() { }); }); }); - it('should always use BIP45 & P2SH for shared wallets', function(done) { + it('should use BIP44 & P2SH for shared wallet if supported', function(done) { var walletOpts = { name: 'my wallet', m: 2, n: 3, pubKey: TestData.keyPair.pub, - supportBIP44: true, - supportP2PKH: true, + supportBIP44AndP2PKH: true, + }; + server.createWallet(walletOpts, function(err, wid) { + should.not.exist(err); + server.storage.fetchWallet(wid, function(err, wallet) { + should.not.exist(err); + wallet.derivationStrategy.should.equal('BIP44'); + wallet.addressType.should.equal('P2SH'); + done(); + }); + }); + }); + it('should use BIP45 & P2SH for shared wallet if supported', function(done) { + var walletOpts = { + name: 'my wallet', + m: 2, + n: 3, + pubKey: TestData.keyPair.pub, + supportBIP44AndP2PKH: false, }; server.createWallet(walletOpts, function(err, wid) { should.not.exist(err); @@ -1278,7 +1311,6 @@ describe('Wallet service', function() { should.exist(status.wallet.copayers[0].requestPubKey); should.exist(status.wallet.copayers[0].signature); should.exist(status.wallet.copayers[0].requestPubKey); - should.exist(status.wallet.copayers[0].addressManager); should.exist(status.wallet.copayers[0].customData); // Do not return other copayer's custom data _.each(_.rest(status.wallet.copayers), function(copayer) { @@ -1353,7 +1385,9 @@ describe('Wallet service', function() { describe('shared wallets (BIP45)', function() { beforeEach(function(done) { - helpers.createAndJoinWallet(2, 2, function(s, w) { + helpers.createAndJoinWallet(2, 2, { + supportBIP44AndP2PKH: false + }, function(s, w) { server = s; wallet = w; done(); @@ -1395,6 +1429,52 @@ describe('Wallet service', function() { done(); }); }); + }); + + describe('shared wallets (BIP44)', function() { + beforeEach(function(done) { + helpers.createAndJoinWallet(2, 2, function(s, w) { + server = s; + wallet = w; + done(); + }); + }); + + it('should create address', function(done) { + server.createAddress({}, function(err, address) { + should.not.exist(err); + should.exist(address); + address.walletId.should.equal(wallet.id); + address.network.should.equal('livenet'); + address.address.should.equal('36q2G5FMGvJbPgAVEaiyAsFGmpkhPKwk2r'); + address.isChange.should.be.false; + address.path.should.equal('m/0/0'); + server.getNotifications({}, function(err, notifications) { + should.not.exist(err); + var notif = _.find(notifications, { + type: 'NewAddress' + }); + should.exist(notif); + notif.data.address.should.equal(address.address); + done(); + }); + }); + }); + + it('should create many addresses on simultaneous requests', function(done) { + var N = 5; + async.map(_.range(N), function(i, cb) { + server.createAddress({}, cb); + }, function(err, addresses) { + addresses.length.should.equal(N); + _.each(_.range(N), function(i) { + addresses[i].path.should.equal('m/0/' + i); + }); + // No two identical addresses + _.uniq(_.pluck(addresses, 'address')).length.should.equal(N); + done(); + }); + }); it('should not create address if unable to store it', function(done) { sinon.stub(server.storage, 'storeAddressAndWallet').yields('dummy error'); @@ -1667,10 +1747,10 @@ describe('Wallet service', function() { reqPrivKey = new Bitcore.PrivateKey(); var requestPubKey = reqPrivKey.toPublicKey(); - var xPrivKey = TestData.copayers[0].xPrivKey_45H; + var xPrivKey = TestData.copayers[0].xPrivKey_44H_0H_0H; var sig = WalletUtils.signRequestPubKey(requestPubKey, xPrivKey); - var copayerId = WalletUtils.xPubToCopayerId(TestData.copayers[0].xPubKey_45H); + var copayerId = WalletUtils.xPubToCopayerId(TestData.copayers[0].xPubKey_44H_0H_0H); opts = { copayerId: copayerId, requestPubKey: requestPubKey, @@ -1681,9 +1761,7 @@ describe('Wallet service', function() { describe('#addAccess 1-1', function() { beforeEach(function(done) { - helpers.createAndJoinWallet(1, 1, { - supportBIP44: false - }, function(s, w) { + helpers.createAndJoinWallet(1, 1, function(s, w) { server = s; wallet = w; @@ -4496,7 +4574,9 @@ describe('Wallet service', function() { WalletService.SCAN_CONFIG.scanWindow = 2; WalletService.SCAN_CONFIG.derivationDelay = 0; - helpers.createAndJoinWallet(1, 2, function(s, w) { + helpers.createAndJoinWallet(1, 2, { + supportBIP44AndP2PKH: false + }, function(s, w) { server = s; wallet = w; done(); @@ -4584,7 +4664,7 @@ describe('Wallet service', function() { WalletService.SCAN_CONFIG.derivationDelay = 0; helpers.createAndJoinWallet(1, 1, { - supportP2PKH: false + supportBIP44AndP2PKH: false }, function(s, w) { server = s; wallet = w; @@ -4598,17 +4678,17 @@ describe('Wallet service', function() { it('should start an asynchronous scan', function(done) { helpers.stubAddressActivity( - ['3J4J9nkFpzQjUGDh5hLKMKztFSPWMKejKE', // m/0/0 - '384JHSf9kVBs3yXsPwCzEScRs395u8hwxj', // m/0/2 - '3NgXBiMQvwcRU8khVoPFJ6gsbGg9ZYrRzH', // m/1/0 + ['3GvvHimEMk2GBZnPxTF89GHZL6QhZjUZVs', // m/2147483647/0/0 + '37pd1jjTUiGBh8JL2hKLDgsyrhBoiz5vsi', // m/2147483647/0/2 + '3C3tBn8Sr1wHTp2brMgYsj9ncB7R7paYuB', // m/2147483647/1/0 ]); var expectedPaths = [ - 'm/0/0', - 'm/0/1', - 'm/0/2', - 'm/0/3', - 'm/1/0', - 'm/1/1', + 'm/2147483647/0/0', + 'm/2147483647/0/1', + 'm/2147483647/0/2', + 'm/2147483647/0/3', + 'm/2147483647/1/0', + 'm/2147483647/1/1', ]; server.messageBroker.onMessage(function(n) { if (n.type == 'ScanFinished') { @@ -4623,7 +4703,7 @@ describe('Wallet service', function() { _.difference(paths, expectedPaths).length.should.equal(0); server.createAddress({}, function(err, address) { should.not.exist(err); - address.path.should.equal('m/0/4'); + address.path.should.equal('m/2147483647/0/4'); done(); }); })