Address Service: Removed caching and added max query limits

Querying addresses that have millions of transactions is supported however
takes hundreds of seconds to fully calculate the balance. Creating a cache of
previous results wasn't currently working because the `isSpent` query is always
based on the current bitcoind tip. Thus the balance of the outputs would be included
however wouldn't be removed when spent as the output wouldn't be checked again
when querying for blocks past the last checkpoint. Including the satoshis in the
inputs address index would make it possible to subtract the spent amount,
however this degrades optimizations elsewhere. The syncing times or querying
for addresses with 10,000 transactions per address.

It may preferrable to have an additional address service that handles high-volume
addresses be on an opt-in basis so that a custom running client could select
high volume addresses to create optimizations for querying balances and history.
The strategies for creating indexes differs on these use cases.
This commit is contained in:
Braydon Fuller 2016-01-14 17:07:44 -05:00
parent 4fcec8755c
commit ead6c2f45f
2 changed files with 92 additions and 638 deletions

View File

@ -44,12 +44,10 @@ var AddressService = function(options) {
this.node.services.bitcoind.on('tx', this.transactionHandler.bind(this)); this.node.services.bitcoind.on('tx', this.transactionHandler.bind(this));
this.node.services.bitcoind.on('txleave', this.transactionLeaveHandler.bind(this)); this.node.services.bitcoind.on('txleave', this.transactionLeaveHandler.bind(this));
this.summaryCacheThreshold = options.summaryCacheThreshold || constants.SUMMARY_CACHE_THRESHOLD;
this.maxInputsQueryLength = options.maxInputsQueryLength || constants.MAX_INPUTS_QUERY_LENGTH; this.maxInputsQueryLength = options.maxInputsQueryLength || constants.MAX_INPUTS_QUERY_LENGTH;
this.maxOutputsQueryLength = options.maxOutputsQueryLength || constants.MAX_OUTPUTS_QUERY_LENGTH; this.maxOutputsQueryLength = options.maxOutputsQueryLength || constants.MAX_OUTPUTS_QUERY_LENGTH;
this._setMempoolIndexPath(); this._setMempoolIndexPath();
this._setSummaryCachePath();
if (options.mempoolMemoryIndex) { if (options.mempoolMemoryIndex) {
this.levelupStore = memdown; this.levelupStore = memdown;
} else { } else {
@ -98,19 +96,6 @@ AddressService.prototype.start = function(callback) {
}, },
next next
); );
},
function(next) {
self.summaryCache = levelup(
self.summaryCachePath,
{
db: self.levelupStore,
keyEncoding: 'binary',
valueEncoding: 'binary',
fillCache: false,
maxOpenFiles: 200
},
next
);
} }
], callback); ], callback);
@ -121,14 +106,6 @@ AddressService.prototype.stop = function(callback) {
this.mempoolIndex.close(callback); this.mempoolIndex.close(callback);
}; };
/**
* This function will set `this.summaryCachePath` based on `this.node.network`.
* @private
*/
AddressService.prototype._setSummaryCachePath = function() {
this.summaryCachePath = this._getDBPathFor('bitcore-addresssummary.db');
};
/** /**
* This function will set `this.mempoolIndexPath` based on `this.node.network`. * This function will set `this.mempoolIndexPath` based on `this.node.network`.
* @private * @private
@ -1357,21 +1334,20 @@ AddressService.prototype.getAddressSummary = function(addressArg, options, callb
function(next) { function(next) {
self._getAddressConfirmedSummary(address, options, next); self._getAddressConfirmedSummary(address, options, next);
}, },
function(cache, next) { function(result, next) {
self._getAddressMempoolSummary(address, options, cache, next); self._getAddressMempoolSummary(address, options, result, next);
} }
], function(err, cache) { ], function(err, result) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
var summary = self._transformAddressSummaryFromCache(cache, options); var summary = self._transformAddressSummaryFromCache(result, options);
var timeDelta = new Date() - startTime; var timeDelta = new Date() - startTime;
if (timeDelta > 5000) { if (timeDelta > 5000) {
var seconds = Math.round(timeDelta / 1000); var seconds = Math.round(timeDelta / 1000);
log.warn('Slow (' + seconds + 's) getAddressSummary request for address: ' + address.toString()); log.warn('Slow (' + seconds + 's) getAddressSummary request for address: ' + address.toString());
log.warn('Address Summary:', summary);
} }
callback(null, summary); callback(null, summary);
@ -1382,108 +1358,48 @@ AddressService.prototype.getAddressSummary = function(addressArg, options, callb
AddressService.prototype._getAddressConfirmedSummary = function(address, options, callback) { AddressService.prototype._getAddressConfirmedSummary = function(address, options, callback) {
var self = this; var self = this;
var tipHeight = this.node.services.db.tip.__height; var baseResult = {
appearanceIds: {},
self._getAddressConfirmedSummaryCache(address, options, function(err, cache) { totalReceived: 0,
if (err) { balance: 0,
return callback(err); unconfirmedAppearanceIds: {},
} unconfirmedBalance: 0
// Immediately give cache is already current, otherwise update
if (cache && cache.height === tipHeight) {
return callback(null, cache);
}
self._updateAddressConfirmedSummaryCache(address, options, cache, tipHeight, callback);
});
};
AddressService.prototype._getAddressConfirmedSummaryCache = function(address, options, callback) {
var self = this;
var baseCache = {
result: {
appearanceIds: {},
totalReceived: 0,
balance: 0,
unconfirmedAppearanceIds: {},
unconfirmedBalance: 0
}
}; };
// Use the base cache if the "start" and "end" options have been used
// We only save and retrieve a cache for the summary of all history
if (options.start >= 0 || options.end >= 0) {
return callback(null, baseCache);
}
var key = encoding.encodeSummaryCacheKey(address);
this.summaryCache.get(key, {
valueEncoding: 'binary',
keyEncoding: 'binary'
}, function(err, buffer) {
if (err instanceof levelup.errors.NotFoundError) {
return callback(null, baseCache);
} else if (err) {
return callback(err);
}
var cache = encoding.decodeSummaryCacheValue(buffer);
// Use base cache if the cached tip/height doesn't match (e.g. there has been a reorg)
var blockIndex = self.node.services.bitcoind.getBlockIndex(cache.height);
if (cache.hash !== blockIndex.hash) {
return callback(null, baseCache);
}
callback(null, cache);
});
};
AddressService.prototype._updateAddressConfirmedSummaryCache = function(address, options, cache, tipHeight, callback) {
var self = this;
var optionsPartial = _.clone(options);
var isHeightQuery = (options.start >= 0 || options.end >= 0);
if (!isHeightQuery) {
// We will pick up from the last point cached and query for all blocks
// proceeding the cache
var cacheHeight = _.isUndefined(cache.height) ? 0 : cache.height + 1;
optionsPartial.start = tipHeight;
optionsPartial.end = cacheHeight;
} else {
$.checkState(_.isUndefined(cache.height));
}
async.waterfall([ async.waterfall([
function(next) { function(next) {
self._getAddressConfirmedInputsSummary(address, cache, optionsPartial, next); self._getAddressConfirmedInputsSummary(address, baseResult, options, next);
}, },
function(cache, next) { function(result, next) {
self._getAddressConfirmedOutputsSummary(address, cache, optionsPartial, next); self._getAddressConfirmedOutputsSummary(address, result, options, next);
}, },
function(cache, next) { function(result, next) {
self._setAndSortTxidsFromAppearanceIds(cache, next); self._setAndSortTxidsFromAppearanceIds(result, next);
} }
], function(err, cache) { ], callback);
// Skip saving the cache if the "start" or "end" options have been used, or
// if the transaction length does not exceed the caching threshold.
// We only want to cache full history results for addresses that have a large
// number of transactions.
var exceedsCacheThreshold = (cache.result.txids.length > self.summaryCacheThreshold);
if (exceedsCacheThreshold && !isHeightQuery) {
self._saveAddressConfirmedSummaryCache(address, cache, tipHeight, callback);
} else {
callback(null, cache);
}
});
}; };
AddressService.prototype._getAddressConfirmedInputsSummary = function(address, cache, options, callback) { AddressService.prototype._getAddressConfirmedInputsSummary = function(address, result, options, callback) {
$.checkArgument(address instanceof Address); $.checkArgument(address instanceof Address);
var self = this; var self = this;
var error = null; var error = null;
var count = 0;
var inputsStream = self.createInputsStream(address, options); var inputsStream = self.createInputsStream(address, options);
inputsStream.on('data', function(input) { inputsStream.on('data', function(input) {
var txid = input.txid; var txid = input.txid;
cache.result.appearanceIds[txid] = input.height; result.appearanceIds[txid] = input.height;
count++;
if (count > self.maxInputsQueryLength) {
log.warn('Tried to query too many inputs (' + self.maxInputsQueryLength + ') for summary of address ' + address.toString());
error = new Error('Maximum number of inputs (' + self.maxInputsQueryLength + ') per query reached');
inputsStream.pause();
inputsStream.end();
}
}); });
inputsStream.on('error', function(err) { inputsStream.on('error', function(err) {
@ -1494,17 +1410,18 @@ AddressService.prototype._getAddressConfirmedInputsSummary = function(address, c
if (error) { if (error) {
return callback(error); return callback(error);
} }
callback(null, cache); callback(null, result);
}); });
}; };
AddressService.prototype._getAddressConfirmedOutputsSummary = function(address, cache, options, callback) { AddressService.prototype._getAddressConfirmedOutputsSummary = function(address, result, options, callback) {
$.checkArgument(address instanceof Address); $.checkArgument(address instanceof Address);
$.checkArgument(!_.isUndefined(cache.result) && $.checkArgument(!_.isUndefined(result) &&
!_.isUndefined(cache.result.appearanceIds) && !_.isUndefined(result.appearanceIds) &&
!_.isUndefined(cache.result.unconfirmedAppearanceIds)); !_.isUndefined(result.unconfirmedAppearanceIds));
var self = this; var self = this;
var count = 0;
var outputStream = self.createOutputsStream(address, options); var outputStream = self.createOutputsStream(address, options);
@ -1515,13 +1432,12 @@ AddressService.prototype._getAddressConfirmedOutputsSummary = function(address,
// Bitcoind's isSpent only works for confirmed transactions // Bitcoind's isSpent only works for confirmed transactions
var spentDB = self.node.services.bitcoind.isSpent(txid, outputIndex); var spentDB = self.node.services.bitcoind.isSpent(txid, outputIndex);
cache.result.totalReceived += output.satoshis; result.totalReceived += output.satoshis;
cache.result.appearanceIds[txid] = output.height; result.appearanceIds[txid] = output.height;
if (!spentDB) { if (!spentDB) {
cache.result.balance += output.satoshis; result.balance += output.satoshis;
} }
// TODO: subtract if spent (because of cache)?
if (options.queryMempool) { if (options.queryMempool) {
// Check to see if this output is spent in the mempool and if so // Check to see if this output is spent in the mempool and if so
@ -1532,10 +1448,19 @@ AddressService.prototype._getAddressConfirmedOutputsSummary = function(address,
); );
var spentMempool = self.mempoolSpentIndex[spentIndexSyncKey]; var spentMempool = self.mempoolSpentIndex[spentIndexSyncKey];
if (spentMempool) { if (spentMempool) {
cache.result.unconfirmedBalance -= output.satoshis; result.unconfirmedBalance -= output.satoshis;
} }
} }
count++;
if (count > self.maxOutputsQueryLength) {
log.warn('Tried to query too many outputs (' + self.maxOutputsQueryLength + ') for summary of address ' + address.toString());
error = new Error('Maximum number of outputs (' + self.maxOutputsQueryLength + ') per query reached');
outputStream.pause();
outputStream.end();
}
}); });
var error = null; var error = null;
@ -1548,40 +1473,25 @@ AddressService.prototype._getAddressConfirmedOutputsSummary = function(address,
if (error) { if (error) {
return callback(error); return callback(error);
} }
callback(null, cache); callback(null, result);
}); });
}; };
AddressService.prototype._setAndSortTxidsFromAppearanceIds = function(cache, callback) { AddressService.prototype._setAndSortTxidsFromAppearanceIds = function(result, callback) {
cache.result.txids = Object.keys(cache.result.appearanceIds); result.txids = Object.keys(result.appearanceIds);
cache.result.txids.sort(function(a, b) { result.txids.sort(function(a, b) {
return cache.result.appearanceIds[a] - cache.result.appearanceIds[b]; return result.appearanceIds[a] - result.appearanceIds[b];
}); });
callback(null, cache); callback(null, result);
}; };
AddressService.prototype._saveAddressConfirmedSummaryCache = function(address, cache, tipHeight, callback) { AddressService.prototype._getAddressMempoolSummary = function(address, options, result, callback) {
log.info('Saving address summary cache for: ' + address.toString() + 'at height: ' + tipHeight);
var key = encoding.encodeSummaryCacheKey(address);
var tipBlockIndex = this.node.services.bitcoind.getBlockIndex(tipHeight);
var value = encoding.encodeSummaryCacheValue(cache, tipHeight, tipBlockIndex.hash);
this.summaryCache.put(key, value, function(err) {
if (err) {
return callback(err);
}
callback(null, cache);
});
};
AddressService.prototype._getAddressMempoolSummary = function(address, options, cache, callback) {
var self = this; var self = this;
// Skip if the options do not want to include the mempool // Skip if the options do not want to include the mempool
if (!options.queryMempool) { if (!options.queryMempool) {
return callback(null, cache); return callback(null, result);
} }
var addressStr = address.toString(); var addressStr = address.toString();
@ -1596,12 +1506,12 @@ AddressService.prototype._getAddressMempoolSummary = function(address, options,
} }
for(var i = 0; i < mempoolInputs.length; i++) { for(var i = 0; i < mempoolInputs.length; i++) {
var input = mempoolInputs[i]; var input = mempoolInputs[i];
cache.result.unconfirmedAppearanceIds[input.txid] = input.timestamp; result.unconfirmedAppearanceIds[input.txid] = input.timestamp;
} }
next(null, cache); next(null, result);
}); });
}, function(cache, next) { }, function(result, next) {
self._getOutputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolOutputs) { self._getOutputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolOutputs) {
if (err) { if (err) {
return next(err); return next(err);
@ -1609,7 +1519,7 @@ AddressService.prototype._getAddressMempoolSummary = function(address, options,
for(var i = 0; i < mempoolOutputs.length; i++) { for(var i = 0; i < mempoolOutputs.length; i++) {
var output = mempoolOutputs[i]; var output = mempoolOutputs[i];
cache.result.unconfirmedAppearanceIds[output.txid] = output.timestamp; result.unconfirmedAppearanceIds[output.txid] = output.timestamp;
var spentIndexSyncKey = encoding.encodeSpentIndexSyncKey( var spentIndexSyncKey = encoding.encodeSpentIndexSyncKey(
new Buffer(output.txid, 'hex'), // TODO: get buffer directly new Buffer(output.txid, 'hex'), // TODO: get buffer directly
@ -1618,19 +1528,18 @@ AddressService.prototype._getAddressMempoolSummary = function(address, options,
var spentMempool = self.mempoolSpentIndex[spentIndexSyncKey]; var spentMempool = self.mempoolSpentIndex[spentIndexSyncKey];
// Only add this to the balance if it's not spent in the mempool already // Only add this to the balance if it's not spent in the mempool already
if (!spentMempool) { if (!spentMempool) {
cache.result.unconfirmedBalance += output.satoshis; result.unconfirmedBalance += output.satoshis;
} }
} }
next(null, cache); next(null, result);
}); });
} }
], callback); ], callback);
}; };
AddressService.prototype._transformAddressSummaryFromCache = function(cache, options) { AddressService.prototype._transformAddressSummaryFromCache = function(result, options) {
var result = cache.result; var confirmedTxids = result.txids;
var confirmedTxids = cache.result.txids;
var unconfirmedTxids = Object.keys(result.unconfirmedAppearanceIds); var unconfirmedTxids = Object.keys(result.unconfirmedAppearanceIds);
var summary = { var summary = {

View File

@ -100,7 +100,7 @@ describe('Address Service', function() {
done(); done();
}); });
}); });
it('start levelup db for mempool and summary index', function(done) { it('start levelup db for mempool', function(done) {
var levelupStub = sinon.stub().callsArg(2); var levelupStub = sinon.stub().callsArg(2);
var TestAddressService = proxyquire('../../../lib/services/address', { var TestAddressService = proxyquire('../../../lib/services/address', {
'fs': { 'fs': {
@ -117,7 +117,7 @@ describe('Address Service', function() {
node: mocknode node: mocknode
}); });
am.start(function() { am.start(function() {
levelupStub.callCount.should.equal(2); levelupStub.callCount.should.equal(1);
var dbPath1 = levelupStub.args[0][0]; var dbPath1 = levelupStub.args[0][0];
dbPath1.should.equal('testdir/testnet3/bitcore-addressmempool.db'); dbPath1.should.equal('testdir/testnet3/bitcore-addressmempool.db');
var options = levelupStub.args[0][1]; var options = levelupStub.args[0][1];
@ -125,8 +125,6 @@ describe('Address Service', function() {
options.keyEncoding.should.equal('binary'); options.keyEncoding.should.equal('binary');
options.valueEncoding.should.equal('binary'); options.valueEncoding.should.equal('binary');
options.fillCache.should.equal(false); options.fillCache.should.equal(false);
var dbPath2 = levelupStub.args[1][0];
dbPath2.should.equal('testdir/testnet3/bitcore-addresssummary.db');
done(); done();
}); });
}); });
@ -2115,457 +2113,14 @@ describe('Address Service', function() {
addressService._getAddressMempoolSummary = sinon.stub().callsArgWith(3, null, cache); addressService._getAddressMempoolSummary = sinon.stub().callsArgWith(3, null, cache);
addressService._transformAddressSummaryFromCache = sinon.stub().returns(summary); addressService._transformAddressSummaryFromCache = sinon.stub().returns(summary);
addressService.getAddressSummary(address, options, function() { addressService.getAddressSummary(address, options, function() {
log.warn.callCount.should.equal(2); log.warn.callCount.should.equal(1);
done(); done();
}); });
clock.tick(6000); clock.tick(6000);
}); });
}); });
describe('#_getAddressConfirmedSummary', function() { describe.skip('#_getAddressConfirmedSummary', function() {
it('handle error from _getAddressConfirmedSummaryCache', function(done) {
var testnode = {
services: {
bitcoind: {
on: sinon.stub()
},
db: {
tip: {
__height: 10
}
}
},
datadir: 'testdir'
};
var addressService = new AddressService({
mempoolMemoryIndex: true,
node: testnode
});
var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX';
var options = {};
addressService._getAddressConfirmedSummaryCache = sinon.stub().callsArgWith(2, new Error('test'));
addressService._getAddressConfirmedSummary(address, options, function(err) {
should.exist(err);
err.message.should.equal('test');
done();
});
});
it('will NOT update cache if matches current tip', function(done) {
var testnode = {
services: {
bitcoind: {
on: sinon.stub()
},
db: {
tip: {
__height: 10
}
}
},
datadir: 'testdir'
};
var addressService = new AddressService({
mempoolMemoryIndex: true,
node: testnode
});
var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX';
var options = {};
var cache = {
height: 10
};
addressService._updateAddressConfirmedSummaryCache = sinon.stub();
addressService._getAddressConfirmedSummaryCache = sinon.stub().callsArgWith(2, null, cache);
addressService._getAddressConfirmedSummary(address, options, function(err, cache) {
if (err) {
return done(err);
}
should.exist(cache);
addressService._updateAddressConfirmedSummaryCache.callCount.should.equal(0);
done();
});
});
it('will call _updateAddressConfirmedSummaryCache with correct arguments', function(done) {
var testnode = {
services: {
bitcoind: {
on: sinon.stub()
},
db: {
tip: {
__height: 11
}
}
},
datadir: 'testdir'
};
var addressService = new AddressService({
mempoolMemoryIndex: true,
node: testnode
});
var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX';
var options = {};
var cache = {
height: 10
};
addressService._updateAddressConfirmedSummaryCache = sinon.stub().callsArgWith(4, null, cache);
addressService._getAddressConfirmedSummaryCache = sinon.stub().callsArgWith(2, null, cache);
addressService._getAddressConfirmedSummary(address, options, function(err, cache) {
if (err) {
return done(err);
}
should.exist(cache);
addressService._updateAddressConfirmedSummaryCache.callCount.should.equal(1);
var args = addressService._updateAddressConfirmedSummaryCache.args[0];
args[0].should.equal(address);
args[1].should.equal(options);
args[2].should.equal(cache);
args[3].should.equal(11);
done();
});
});
});
describe('#_getAddressConfirmedSummaryCache', function() {
function shouldExistBasecache(cache) {
should.exist(cache);
should.not.exist(cache.height);
should.exist(cache.result);
cache.result.appearanceIds.should.deep.equal({});
cache.result.totalReceived.should.equal(0);
cache.result.balance.should.equal(0);
cache.result.unconfirmedAppearanceIds.should.deep.equal({});
cache.result.unconfirmedBalance.should.equal(0);
}
it('give base cache if "start" or "end" options are used (e.g. >= 0)', function(done) {
var testnode = {
services: {
bitcoind: {
on: sinon.stub(),
}
},
datadir: 'testdir'
};
var addressService = new AddressService({
mempoolMemoryIndex: true,
node: testnode
});
var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX');
var options = {
start: 0,
end: 0
};
addressService._getAddressConfirmedSummaryCache(address, options, function(err, cache) {
if (err) {
return done(err);
}
shouldExistBasecache(cache);
done();
});
});
it('give base cache if "start" or "end" options are used (e.g. 10, 9)', function(done) {
var testnode = {
services: {
bitcoind: {
on: sinon.stub(),
}
},
datadir: 'testdir'
};
var addressService = new AddressService({
mempoolMemoryIndex: true,
node: testnode
});
var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX');
var options = {
start: 10,
end: 9
};
addressService._getAddressConfirmedSummaryCache(address, options, function(err, cache) {
if (err) {
return done(err);
}
shouldExistBasecache(cache);
done();
});
});
it('give base cache if cache does NOT exist', function(done) {
var testnode = {
services: {
bitcoind: {
on: sinon.stub(),
}
},
datadir: 'testdir'
};
var addressService = new AddressService({
mempoolMemoryIndex: true,
node: testnode
});
var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX');
var options = {};
addressService.summaryCache = {};
addressService.summaryCache.get = sinon.stub().callsArgWith(2, new levelup.errors.NotFoundError());
addressService._getAddressConfirmedSummaryCache(address, options, function(err, cache) {
if (err) {
return done(err);
}
shouldExistBasecache(cache);
done();
});
});
it('give base cache if cached tip hash differs (e.g. reorg)', function(done) {
var hash = '000000002c05cc2e78923c34df87fd108b22221ac6076c18f3ade378a4d915e9';
var testnode = {
services: {
bitcoind: {
on: sinon.stub(),
getBlockIndex: sinon.stub().returns({
hash: '00000000700e92a916b46b8b91a14d1303d5d91ef0b09eecc3151fb958fd9a2e'
})
},
db: {
tip: {
hash: hash
}
}
},
datadir: 'testdir'
};
var addressService = new AddressService({
mempoolMemoryIndex: true,
node: testnode
});
var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX');
var txid = '5464b1c3f25160f0183fad68a406838d2d1ac0aee05990072ece49326c26c22e';
var options = {};
var cache = {
height: 10,
hash: hash,
result: {
totalReceived: 100,
balance: 10,
txids: [txid],
appearanceIds: {
'5464b1c3f25160f0183fad68a406838d2d1ac0aee05990072ece49326c26c22e': 9
}
}
};
var cacheBuffer = encoding.encodeSummaryCacheValue(cache, 10, hash);
addressService.summaryCache = {};
addressService.summaryCache.get = sinon.stub().callsArgWith(2, null, cacheBuffer);
addressService._getAddressConfirmedSummaryCache(address, options, function(err, cache) {
if (err) {
return done(err);
}
shouldExistBasecache(cache);
done();
});
});
it('handle error from levelup', function(done) {
var testnode = {
services: {
bitcoind: {
on: sinon.stub(),
}
},
datadir: 'testdir'
};
var addressService = new AddressService({
mempoolMemoryIndex: true,
node: testnode
});
var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX');
var options = {};
addressService.summaryCache = {};
addressService.summaryCache.get = sinon.stub().callsArgWith(2, new Error('test'));
addressService._getAddressConfirmedSummaryCache(address, options, function(err) {
should.exist(err);
err.message.should.equal('test');
done();
});
});
it('call encode and decode with args and result', function(done) {
var hash = '000000002c05cc2e78923c34df87fd108b22221ac6076c18f3ade378a4d915e9';
var testnode = {
services: {
bitcoind: {
on: sinon.stub(),
getBlockIndex: sinon.stub().returns({
hash: hash
})
},
db: {
tip: {
hash: hash
}
}
},
datadir: 'testdir'
};
var addressService = new AddressService({
mempoolMemoryIndex: true,
node: testnode
});
var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX');
var txid = '5464b1c3f25160f0183fad68a406838d2d1ac0aee05990072ece49326c26c22e';
var options = {};
var cache = {
height: 10,
hash: hash,
result: {
totalReceived: 100,
balance: 10,
txids: [txid],
appearanceIds: {
'5464b1c3f25160f0183fad68a406838d2d1ac0aee05990072ece49326c26c22e': 9
}
}
};
var cacheBuffer = encoding.encodeSummaryCacheValue(cache, 10, hash);
addressService.summaryCache = {};
addressService.summaryCache.get = sinon.stub().callsArgWith(2, null, cacheBuffer);
addressService._getAddressConfirmedSummaryCache(address, options, function(err, cache) {
if (err) {
return done(err);
}
should.exist(cache);
cache.height.should.equal(10);
cache.hash.should.equal(hash);
should.exist(cache.result);
cache.result.totalReceived.should.equal(100);
cache.result.balance.should.equal(10);
cache.result.txids.should.deep.equal([txid]);
cache.result.appearanceIds.should.deep.equal({
'5464b1c3f25160f0183fad68a406838d2d1ac0aee05990072ece49326c26c22e': 9
});
done();
});
});
});
describe('#_updateAddressConfirmedSummaryCache', function() {
it('will pass partial options to input/output summary query', function(done) {
var tipHeight = 12;
var testnode = {
services: {
bitcoind: {
on: sinon.stub()
}
},
datadir: 'testdir'
};
var as = new AddressService({
mempoolMemoryIndex: true,
node: testnode
});
var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX');
var options = {};
var cache = {
height: 10,
result: {
txids: []
}
};
as._getAddressConfirmedInputsSummary = sinon.stub().callsArgWith(3, null, cache);
as._getAddressConfirmedOutputsSummary = sinon.stub().callsArgWith(3, null, cache);
as._setAndSortTxidsFromAppearanceIds = sinon.stub().callsArgWith(1, null, cache);
as._saveAddressConfirmedSummaryCache = sinon.stub().callsArg(3, null, cache);
as._updateAddressConfirmedSummaryCache(address, options, cache, tipHeight, function(err, cache) {
if (err) {
return done(err);
}
as._getAddressConfirmedInputsSummary.callCount.should.equal(1);
as._getAddressConfirmedOutputsSummary.callCount.should.equal(1);
as._getAddressConfirmedInputsSummary.args[0][2].start.should.equal(12);
as._getAddressConfirmedInputsSummary.args[0][2].end.should.equal(11);
as._getAddressConfirmedOutputsSummary.args[0][2].start.should.equal(12);
as._getAddressConfirmedOutputsSummary.args[0][2].end.should.equal(11);
done();
});
});
it('will save cache if exceeds threshold and is NOT height query', function(done) {
var tipHeight = 12;
var testnode = {
services: {
bitcoind: {
on: sinon.stub()
}
},
datadir: 'testdir'
};
var as = new AddressService({
mempoolMemoryIndex: true,
node: testnode
});
var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX');
var options = {};
var cache = {
height: 10,
result: {
txids: [
'9a816264c50910cbf57aa4637dde5f7fec03df642b822661e8bc9710475986b6',
'05c6b9ccf3fc4026391bf8f8a64b4784a95b930851359b8f85a4be7bb6bf6f1e'
]
}
};
as.summaryCacheThreshold = 1;
as._getAddressConfirmedInputsSummary = sinon.stub().callsArgWith(3, null, cache);
as._getAddressConfirmedOutputsSummary = sinon.stub().callsArgWith(3, null, cache);
as._setAndSortTxidsFromAppearanceIds = sinon.stub().callsArgWith(1, null, cache);
as._saveAddressConfirmedSummaryCache = sinon.stub().callsArg(3, null, cache);
as._updateAddressConfirmedSummaryCache(address, options, cache, tipHeight, function(err) {
if (err) {
return done(err);
}
as._saveAddressConfirmedSummaryCache.callCount.should.equal(1);
done();
});
});
it('will NOT save cache if exceeds threshold and IS height query', function(done) {
var tipHeight = 12;
var testnode = {
services: {
bitcoind: {
on: sinon.stub()
}
},
datadir: 'testdir'
};
var as = new AddressService({
mempoolMemoryIndex: true,
node: testnode
});
var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX');
var options = {};
var cache = {
result: {
txids: []
}
};
as.summaryCacheThreshold = 1;
as._getAddressConfirmedInputsSummary = sinon.stub().callsArgWith(3, null, cache);
as._getAddressConfirmedOutputsSummary = sinon.stub().callsArgWith(3, null, cache);
as._setAndSortTxidsFromAppearanceIds = sinon.stub().callsArgWith(1, null, cache);
as._saveAddressConfirmedSummaryCache = sinon.stub().callsArg(3, null, cache);
as._updateAddressConfirmedSummaryCache(address, options, cache, tipHeight, function(err) {
if (err) {
return done(err);
}
as._saveAddressConfirmedSummaryCache.callCount.should.equal(0);
done();
});
});
}); });
describe('#_getAddressConfirmedInputsSummary', function() { describe('#_getAddressConfirmedInputsSummary', function() {
@ -2584,21 +2139,18 @@ describe('Address Service', function() {
mempoolMemoryIndex: true, mempoolMemoryIndex: true,
node: testnode node: testnode
}); });
var cache = { var result = {
height: 10, appearanceIds: {}
result: {
appearanceIds: {}
}
}; };
var options = {}; var options = {};
var txid = 'f2cfc19d13f0c12199f70e420d84e2b3b1d4e499702aa9d737f8c24559c9ec47'; var txid = 'f2cfc19d13f0c12199f70e420d84e2b3b1d4e499702aa9d737f8c24559c9ec47';
var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX');
as.createInputsStream = sinon.stub().returns(streamStub); as.createInputsStream = sinon.stub().returns(streamStub);
as._getAddressConfirmedInputsSummary(address, cache, options, function(err, cache) { as._getAddressConfirmedInputsSummary(address, result, options, function(err, result) {
if (err) { if (err) {
return done(err); return done(err);
} }
cache.result.appearanceIds[txid].should.equal(10); result.appearanceIds[txid].should.equal(10);
done(); done();
}); });
@ -2655,16 +2207,14 @@ describe('Address Service', function() {
mempoolMemoryIndex: true, mempoolMemoryIndex: true,
node: testnode node: testnode
}); });
var cache = { var result = {
height: 10, appearanceIds: {},
result: { unconfirmedAppearanceIds: {},
appearanceIds: {}, balance: 0,
unconfirmedAppearanceIds: {}, totalReceived: 0,
balance: 0, unconfirmedBalance: 0
totalReceived: 0,
unconfirmedBalance: 0
}
}; };
var options = { var options = {
queryMempool: true queryMempool: true
}; };
@ -2676,14 +2226,14 @@ describe('Address Service', function() {
var spentIndexSyncKey = encoding.encodeSpentIndexSyncKey(new Buffer(txid, 'hex'), 2); var spentIndexSyncKey = encoding.encodeSpentIndexSyncKey(new Buffer(txid, 'hex'), 2);
as.mempoolSpentIndex[spentIndexSyncKey] = true; as.mempoolSpentIndex[spentIndexSyncKey] = true;
as._getAddressConfirmedOutputsSummary(address, cache, options, function(err, cache) { as._getAddressConfirmedOutputsSummary(address, result, options, function(err, cache) {
if (err) { if (err) {
return done(err); return done(err);
} }
cache.result.appearanceIds[txid].should.equal(10); result.appearanceIds[txid].should.equal(10);
cache.result.balance.should.equal(1000); result.balance.should.equal(1000);
cache.result.totalReceived.should.equal(1000); result.totalReceived.should.equal(1000);
cache.result.unconfirmedBalance.should.equal(-1000); result.unconfirmedBalance.should.equal(-1000);
done(); done();
}); });
@ -2710,20 +2260,18 @@ describe('Address Service', function() {
mempoolMemoryIndex: true, mempoolMemoryIndex: true,
node: testnode node: testnode
}); });
var cache = { var result = {
height: 10, appearanceIds: {},
result: { unconfirmedAppearanceIds: {},
appearanceIds: {}, balance: 0,
unconfirmedAppearanceIds: {}, totalReceived: 0,
balance: 0, unconfirmedBalance: 0
totalReceived: 0,
unconfirmedBalance: 0
}
}; };
var options = {}; var options = {};
var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX');
as.createOutputsStream = sinon.stub().returns(streamStub); as.createOutputsStream = sinon.stub().returns(streamStub);
as._getAddressConfirmedOutputsSummary(address, cache, options, function(err, cache) { as._getAddressConfirmedOutputsSummary(address, result, options, function(err, cache) {
should.exist(err); should.exist(err);
err.message.should.equal('test'); err.message.should.equal('test');
done(); done();
@ -2737,9 +2285,6 @@ describe('Address Service', function() {
describe.skip('#_setAndSortTxidsFromAppearanceIds', function() { describe.skip('#_setAndSortTxidsFromAppearanceIds', function() {
}); });
describe.skip('#_saveAddressConfirmedSummaryCache', function() {
});
describe.skip('#_getAddressMempoolSummary', function() { describe.skip('#_getAddressMempoolSummary', function() {
}); });