Merge pull request #390 from isocolsky/limit-address-gap

Limit nb of consecutive addresses without activity
This commit is contained in:
Matias Alejo Garcia 2015-10-29 16:37:47 -03:00
commit 5edfe3d384
7 changed files with 146 additions and 12 deletions

View File

@ -114,7 +114,9 @@ Insight.prototype.getTransactions = function(addresses, from, to, cb) {
};
Insight.prototype.getAddressActivity = function(address, cb) {
var url = this.url + this.apiPrefix + '/addr/' + address;
var self = this;
var url = self.url + self.apiPrefix + '/addr/' + address;
var args = {
method: 'GET',
url: url,

View File

@ -17,6 +17,7 @@ var errors = {
INVALID_ADDRESS: 'Invalid address',
KEY_IN_COPAYER: 'Key already registered',
LOCKED_FUNDS: 'Funds are locked by pending transaction proposals',
MAIN_ADDRESS_GAP_REACHED: 'Maximum number of consecutive addresses without activity reached',
NOT_AUTHORIZED: 'Not authorized',
TOO_MANY_KEYS: 'Too many keys registered',
TX_ALREADY_BROADCASTED: 'The transaction proposal is already broadcasted',

View File

@ -259,8 +259,19 @@ ExpressApp.prototype.start = function(opts, cb) {
});
});
// DEPRECATED
router.post('/v1/addresses/', function(req, res) {
getServerWithAuth(req, res, function(server) {
server.createAddress({
ignoreMaxGap: true
}, function(err, address) {
if (err) return returnError(err, res, req);
res.json(address);
});
});
});
router.post('/v2/addresses/', function(req, res) {
getServerWithAuth(req, res, function(server) {
server.createAddress(req.body, function(err, address) {
if (err) return returnError(err, res, req);

View File

@ -19,6 +19,7 @@ Address.create = function(opts) {
x.publicKeys = opts.publicKeys;
x.network = Bitcore.Address(x.address).toObject().network;
x.type = opts.type || WalletUtils.SCRIPT_TYPES.P2SH;
x.hasActivity = undefined;
return x;
};
@ -34,6 +35,7 @@ Address.fromObj = function(obj) {
x.path = obj.path;
x.publicKeys = obj.publicKeys;
x.type = obj.type || WalletUtils.SCRIPT_TYPES.P2SH;
x.hasActivity = obj.hasActivity;
return x;
};

View File

@ -34,7 +34,6 @@ var blockchainExplorerOpts;
var messageBroker;
var serviceVersion;
var MAX_KEYS = 100;
/**
* Creates an instance of the Bitcore Wallet Service.
@ -53,6 +52,8 @@ function WalletService() {
};
WalletService.MAX_KEYS = 100;
// Time after which a Tx proposal can be erased by any copayer. in seconds
WalletService.DELETE_LOCKTIME = 24 * 3600;
@ -62,9 +63,11 @@ WalletService.BACKOFF_OFFSET = 3;
// Time a copayer need to wait to create a new TX after her tx previous proposal we rejected. (incremental). in Minutes.
WalletService.BACKOFF_TIME = 2;
WalletService.MAX_MAIN_ADDRESS_GAP = 20;
// Fund scanning parameters
WalletService.SCAN_CONFIG = {
maxGap: 20,
maxGap: WalletService.MAX_MAIN_ADDRESS_GAP,
};
WalletService.FEE_LEVELS = [{
@ -518,7 +521,7 @@ WalletService.prototype.addAccess = function(opts, cb) {
return cb(Errors.NOT_AUTHORIZED);
}
if (copayer.requestPubKeys.length > MAX_KEYS)
if (copayer.requestPubKeys.length > WalletService.MAX_KEYS)
return cb(Errors.TOO_MANY_KEYS);
self._addKeyToCopayer(wallet, copayer, opts, cb);
@ -689,29 +692,74 @@ WalletService.prototype.getPreferences = function(opts, cb) {
});
};
WalletService.prototype._canCreateAddress = function(ignoreMaxGap, cb) {
var self = this;
if (ignoreMaxGap) return cb(null, true);
self.storage.fetchAddresses(self.walletId, function(err, addresses) {
if (err) return cb(err);
var latestAddresses = _.takeRight(_.reject(addresses, {
isChange: true
}), WalletService.MAX_MAIN_ADDRESS_GAP);
if (latestAddresses.length < WalletService.MAX_MAIN_ADDRESS_GAP || _.any(latestAddresses, {
hasActivity: true
})) return cb(null, true);
var bc = self._getBlockchainExplorer(latestAddresses[0].network);
var activityFound = false;
var i = latestAddresses.length;
async.whilst(function() {
return i > 0 && !activityFound;
}, function(next) {
bc.getAddressActivity(latestAddresses[--i].address, function(err, res) {
if (err) return next(err);
activityFound = !!res;
return next();
});
}, function(err) {
if (err) return cb(err);
if (!activityFound) return cb(null, false);
var address = latestAddresses[i];
address.hasActivity = true;
self.storage.storeAddress(address, function(err) {
return cb(err, true);
});
});
});
};
/**
* Creates a new address.
* @param {Object} opts
* @param {Boolean} [opts.ignoreMaxGap=false] - Ignore constraint of maximum number of consecutive addresses without activity
* @returns {Address} address
*/
WalletService.prototype.createAddress = function(opts, cb) {
var self = this;
opts = opts || {};
self._runLocked(cb, function(cb) {
self.getWallet({}, function(err, wallet) {
if (err) return cb(err);
if (!wallet.isComplete()) return cb(Errors.WALLET_NOT_COMPLETE);
var address = wallet.createAddress(false);
self.storage.storeAddressAndWallet(wallet, address, function(err) {
self._canCreateAddress(opts.ignoreMaxGap, function(err, canCreate) {
if (err) return cb(err);
if (!canCreate) return cb(Errors.MAIN_ADDRESS_GAP_REACHED);
self._notify('NewAddress', {
address: address.address,
}, function() {
return cb(null, address);
var address = wallet.createAddress(false);
self.storage.storeAddressAndWallet(wallet, address, function(err) {
if (err) return cb(err);
self._notify('NewAddress', {
address: address.address,
}, function() {
return cb(null, address);
});
});
});
});

View File

@ -383,6 +383,17 @@ Storage.prototype.fetchAddresses = function(walletId, cb) {
});
};
Storage.prototype.storeAddress = function(address, cb) {
var self = this;
self.db.collection(collections.ADDRESSES).update({
address: address.address
}, address, {
w: 1,
upsert: false,
}, cb);
};
Storage.prototype.storeAddressAndWallet = function(wallet, addresses, cb) {
var self = this;

View File

@ -1715,6 +1715,65 @@ describe('Wallet service', function() {
done();
});
});
it('should fail to create more consecutive addresses with no activity than allowed', function(done) {
var MAX_MAIN_ADDRESS_GAP_old = WalletService.MAX_MAIN_ADDRESS_GAP;
WalletService.MAX_MAIN_ADDRESS_GAP = 2;
helpers.stubAddressActivity([]);
async.map(_.range(2), function(i, next) {
server.createAddress({}, next);
}, function(err, addresses) {
addresses.length.should.equal(2);
server.createAddress({}, function(err, address) {
should.exist(err);
should.not.exist(address);
err.code.should.equal('MAIN_ADDRESS_GAP_REACHED');
server.createAddress({
ignoreMaxGap: true
}, function(err, address) {
should.not.exist(err);
should.exist(address);
address.path.should.equal('m/0/2');
helpers.stubAddressActivity([
'1GdXraZ1gtoVAvBh49D4hK9xLm6SKgesoE', // m/0/2
]);
server.createAddress({}, function(err, address) {
should.not.exist(err);
should.exist(address);
address.path.should.equal('m/0/3');
WalletService.MAX_MAIN_ADDRESS_GAP = MAX_MAIN_ADDRESS_GAP_old;
done();
});
});
});
});
});
it('should cache address activity', function(done) {
var MAX_MAIN_ADDRESS_GAP_old = WalletService.MAX_MAIN_ADDRESS_GAP;
WalletService.MAX_MAIN_ADDRESS_GAP = 2;
helpers.stubAddressActivity([]);
async.map(_.range(2), function(i, next) {
server.createAddress({}, next);
}, function(err, addresses) {
addresses.length.should.equal(2);
helpers.stubAddressActivity([addresses[1].address]);
var getAddressActivitySpy = sinon.spy(blockchainExplorer, 'getAddressActivity');
server.createAddress({}, function(err, address) {
should.not.exist(err);
server.createAddress({}, function(err, address) {
should.not.exist(err);
getAddressActivitySpy.callCount.should.equal(1);
WalletService.MAX_MAIN_ADDRESS_GAP = MAX_MAIN_ADDRESS_GAP_old;
done();
});
});
});
});
});
});