Merge pull request #765 from matiu/feat/interrupted-scan-cache

Feat/interrupted scan cache
This commit is contained in:
Matias Alejo Garcia 2018-02-09 18:51:31 -03:00 committed by GitHub
commit 1ab4e27bde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 202 additions and 10 deletions

View File

@ -36,6 +36,7 @@ var errors = {
WALLET_BUSY: 'Wallet is busy, try later',
WALLET_NOT_COMPLETE: 'Wallet is not complete',
WALLET_NOT_FOUND: 'Wallet not found',
WALLET_NEED_SCAN: 'Wallet needs addresses scan',
};
var errorObjects = _.zipObject(_.map(errors, function(msg, code) {

View File

@ -55,6 +55,18 @@ AddressManager.prototype.rewindIndex = function(isChange, n) {
}
};
AddressManager.prototype.getCurrentIndex = function(isChange) {
return isChange ? this.changeAddressIndex : this.receiveAddressIndex;
};
AddressManager.prototype.getBaseAddressPath = function(isChange) {
return 'm/' +
(this.derivationStrategy == Constants.DERIVATION_STRATEGIES.BIP45 ? this.copayerIndex + '/' : '') +
(isChange ? 1 : 0) + '/' +
0;
};
AddressManager.prototype.getCurrentAddressPath = function(isChange) {
return 'm/' +
(this.derivationStrategy == Constants.DERIVATION_STRATEGIES.BIP45 ? this.copayerIndex + '/' : '') +

View File

@ -1037,6 +1037,9 @@ WalletService.prototype.createAddress = function(opts, cb) {
self.getWallet({}, function(err, wallet) {
if (err) return cb(err);
if (!wallet.isComplete()) return cb(Errors.WALLET_NOT_COMPLETE);
if (wallet.scanStatus == 'error')
return cb(Errors.WALLET_NEED_SCAN);
var createFn = wallet.singleAddress ? getFirstAddress : createNewAddress;
return createFn(wallet, cb);
@ -1161,6 +1164,10 @@ WalletService.prototype._getUtxosForCurrentWallet = function(opts, cb) {
self.getWallet({}, function(err, wallet) {
if (err) return next(err);
if (wallet.scanStatus == 'error')
return cb(Errors.WALLET_NEED_SCAN);
coin = wallet.coin;
return next();
});
@ -2265,6 +2272,10 @@ WalletService.prototype.createTx = function(opts, cb) {
if (err) return cb(err);
if (!wallet.isComplete()) return cb(Errors.WALLET_NOT_COMPLETE);
if (wallet.scanStatus == 'error')
return cb(Errors.WALLET_NEED_SCAN);
checkTxpAlreadyExists(opts.txProposalId, function(err, txp) {
if (err) return cb(err);
if (txp) return cb(null, txp);
@ -3330,23 +3341,31 @@ WalletService.prototype.scan = function(opts, cb) {
var allAddresses = [];
var gap = Defaults.SCAN_ADDRESS_GAP;
async.whilst(function() {
self.logi('Scanning addr gap:'+ inactiveCounter);
self.logi('Scanning addr branch: %s index: %d gap %d ', derivator.id, derivator.index(), inactiveCounter);
return inactiveCounter < gap;
}, function(next) {
var address = derivator.derive();
checkActivity(wallet, address.address, function(err, activity) {
if (err) return next(err);
allAddresses.push(address);
inactiveCounter = activity ? 0 : inactiveCounter + 1;
next();
if (!activity)
return next();
// Store address manager cache. This is usefull for interrupted scans
// in which the addresses are not inserted.
self.storage.storeAddressIndexCache(wallet.id, derivator.id, derivator.index(),next);
});
}, function(err) {
derivator.rewind(gap);
return cb(err, _.dropRight(allAddresses, gap));
});
};
}
self._runLocked(cb, function(cb) {
@ -3363,14 +3382,18 @@ WalletService.prototype.scan = function(opts, cb) {
var derivators = [];
_.each([false, true], function(isChange) {
derivators.push({
id: wallet.addressManager.getBaseAddressPath(isChange),
derive: _.bind(wallet.createAddress, wallet, isChange),
index: _.bind(wallet.addressManager.getCurrentIndex, wallet.addressManager, isChange),
rewind: _.bind(wallet.addressManager.rewindIndex, wallet.addressManager, isChange),
});
if (opts.includeCopayerBranches) {
_.each(wallet.copayers, function(copayer) {
if (copayer.addressManager) {
derivators.push({
id: copayer.addressManager.getBaseAddressPath(isChange),
derive: _.bind(copayer.createAddress, copayer, wallet, isChange),
index: _.bind(copayer.addressManager.getCurrentIndex, copayer.addressManager, isChange),
rewind: _.bind(copayer.addressManager.rewindIndex, copayer.addressManager, isChange),
});
}
@ -3379,9 +3402,25 @@ WalletService.prototype.scan = function(opts, cb) {
});
async.eachSeries(derivators, function(derivator, next) {
scanBranch(wallet, derivator, function(err, addresses) {
if (err) return next(err);
self.storage.storeAddressAndWallet(wallet, addresses, next);
self.storage.fetchAddressIndexCache(wallet.id, derivator.id, function(err, index){
var addresses = [];
// prederive known addresses
var diff = index - derivator.index();
if (diff>0) {
self.logi('Prederiving '+ diff +' addresses for ' + derivator.id);
for(var i =0; i<diff; i++) {
var address = derivator.derive();
addresses.push(address);
}
}
scanBranch(wallet, derivator, function(err, scannedAddresses) {
if (err) return next(err);
addresses = addresses.concat(scannedAddresses);
self.storage.storeAddressAndWallet(wallet, addresses, next);
});
});
}, function(error) {
self.storage.fetchWallet(wallet.id, function(err, wallet) {

View File

@ -1070,6 +1070,36 @@ Storage.prototype._dump = function(cb, fn) {
});
};
Storage.prototype.fetchAddressIndexCache = function (walletId, key, cb) {
this.db.collection(collections.CACHE).findOne({
walletId: walletId,
type: 'addressIndexCache',
key: key,
}, function(err, ret) {
if (err) return cb(err);
if (!ret) return cb();
cb(null, ret.index);
});
}
Storage.prototype.storeAddressIndexCache = function (walletId, key, index, cb) {
this.db.collection(collections.CACHE).update({
walletId: walletId,
type: 'addressIndexCache',
key: key,
}, {
"$set":
{
index: index,
}
}, {
w: 1,
upsert: true,
}, cb);
};
Storage.prototype._addressHash = function(addresses) {
var all = addresses.join();
@ -1110,6 +1140,8 @@ Storage.prototype.checkAndUseBalanceCache = function(walletId, addresses, durati
});
};
Storage.prototype.storeBalanceCache = function (walletId, addresses, balance, cb) {
var key = this._addressHash(addresses);
var now = Date.now();

View File

@ -380,8 +380,22 @@ helpers.stubFeeLevels = function(levels) {
};
};
helpers.stubAddressActivity = function(activeAddresses) {
var stubAddressActivityFailsOn = null;
var stubAddressActivityFailsOnCount=1;
helpers.stubAddressActivity = function(activeAddresses, failsOn) {
stubAddressActivityFailsOnCount=1;
// could be null
stubAddressActivityFailsOn = failsOn;
blockchainExplorer.getAddressActivity = function(address, cb) {
if (stubAddressActivityFailsOnCount === stubAddressActivityFailsOn)
return cb('failed on request');
stubAddressActivityFailsOnCount++;
return cb(null, _.contains(activeAddresses, address));
};
};

View File

@ -7647,6 +7647,7 @@ describe('Wallet service', function() {
});
});
it('should not go beyond max gap', function(done) {
helpers.stubAddressActivity(
['1L3z9LPd861FWQhf3vDn89Fnc9dkdBo2CG', // m/0/0
@ -7720,8 +7721,8 @@ describe('Wallet service', function() {
wallet.addressManager.receiveAddressIndex.should.equal(1);
wallet.addressManager.changeAddressIndex.should.equal(0);
server.createAddress({}, function(err, address) {
should.not.exist(err);
address.path.should.equal('m/0/1');
should.exist(err);
err.code.should.equal('WALLET_NEED_SCAN');
done();
});
});
@ -7784,11 +7785,104 @@ describe('Wallet service', function() {
server.storage.fetchAddresses(wallet.id, function(err, addresses) {
should.not.exist(err);
addresses.should.be.empty;
done();
server.getStatus({}, function(err, status) {
should.exist(err);
err.code.should.equal('WALLET_NEED_SCAN');
done();
});
});
});
});
});
it('index cache: should use cache, if previous scan failed', function(done) {
helpers.stubAddressActivity(
['1L3z9LPd861FWQhf3vDn89Fnc9dkdBo2CG', // m/0/0
'1GdXraZ1gtoVAvBh49D4hK9xLm6SKgesoE', // m/0/2
'1FUzgKcyPJsYwDLUEVJYeE2N3KVaoxTjGS', // m/1/0
], 4);
// First without activity
var addr = '1KbTiFvjbN6B5reCVS4tTT49vPQkvsqnE2'; // m/0/3
server.scan({}, function(err) {
should.exist('failed on request');
server.getWallet({}, function(err, wallet) {
should.not.exist(err);
// Because it failed
wallet.addressManager.receiveAddressIndex.should.equal(0);
wallet.addressManager.changeAddressIndex.should.equal(0);
helpers.stubAddressActivity(
['1L3z9LPd861FWQhf3vDn89Fnc9dkdBo2CG', // m/0/0
'1GdXraZ1gtoVAvBh49D4hK9xLm6SKgesoE', // m/0/2
'1FUzgKcyPJsYwDLUEVJYeE2N3KVaoxTjGS', // m/1/0
], -1);
var getAddressActivitySpy = sinon.spy(blockchainExplorer, 'getAddressActivity');
server.scan({}, function(err) {
should.not.exist(err);
// should prederive 3 address, so
// First call should be m/0/3
var calls = getAddressActivitySpy.getCalls();
calls[0].args[0].should.equal(addr);
server.storage.fetchAddresses(wallet.id, function(err, addresses) {
should.exist(addresses);
server.createAddress({}, function(err, address) {
should.not.exist(err);
address.path.should.equal('m/0/3');
done();
});
});
});
});
});
});
it('index cache: should not use cache, if scan worked ok', function(done) {
helpers.stubAddressActivity(
['1L3z9LPd861FWQhf3vDn89Fnc9dkdBo2CG', // m/0/0
'1GdXraZ1gtoVAvBh49D4hK9xLm6SKgesoE', // m/0/2
'1FUzgKcyPJsYwDLUEVJYeE2N3KVaoxTjGS', // m/1/0
]);
// First without activity
var addr = '1KbTiFvjbN6B5reCVS4tTT49vPQkvsqnE2'; // m/0/3
server.scan({}, function(err) {
should.not.exist(err);
server.getWallet({}, function(err, wallet) {
should.not.exist(err);
wallet.addressManager.receiveAddressIndex.should.equal(3);
wallet.addressManager.changeAddressIndex.should.equal(1);
var getAddressActivitySpy = sinon.spy(blockchainExplorer, 'getAddressActivity');
server.scan({}, function(err) {
should.not.exist(err);
var calls = getAddressActivitySpy.getCalls();
calls[0].args[0].should.equal(addr);
server.storage.fetchAddresses(wallet.id, function(err, addresses) {
should.exist(addresses);
server.createAddress({}, function(err, address) {
should.not.exist(err);
address.path.should.equal('m/0/3');
done();
});
});
});
});
});
});
});
describe('shared wallet (BIP45)', function() {