flocore-node/test/services/block/index.unit.js
Chris Kleeschulte 67e9d63f4b wip
2017-07-16 17:29:58 -04:00

658 lines
23 KiB
JavaScript

'use strict';
var expect = require('chai').expect;
var BlockService = require('../../../lib/services/block');
var BN = require('bn.js');
var assert = require('chai').assert;
var crypto = require('crypto');
var sinon = require('sinon');
var Block = require('bitcore-lib').Block;
var Encoding = require('../../../lib/services/block/encoding');
var LRU = require('lru-cache');
var constants = require('../../../lib/constants');
describe('Block Service', function() {
var blockService;
beforeEach(function() {
blockService = new BlockService({
node: {
getNetworkName: function() { return 'regtest'; },
services: []
}
});
blockService._chainTips = ['00'];
blockService._encoding = new Encoding(new Buffer('0000', 'hex'));
});
describe('#_blockAlreadyProcessed', function() {
it('should detect that a block has already been delivered to us', function() {
blockService._blockQueue = LRU(1);
blockService._blockQueue.set('aa', '00');
expect(blockService._blockAlreadyProcessed({ hash: 'aa' })).to.be.true;
expect(blockService._blockAlreadyProcessed('bb')).to.be.false;
blockService._blockQueue.reset();
});
});
describe('#_cacheBlock', function() {
it('should set the block in the block queue and db', function() {
var sandbox = sinon.sandbox.create();
var spy1 = sandbox.spy();
var stub1 = sandbox.stub();
blockService._blockQueue = { set: stub1 };
var block = {};
sandbox.stub(blockService, '_getBlockOperations');
blockService._db = { batch: spy1 };
blockService._cacheBlock(block);
expect(spy1.calledOnce).to.be.true;
expect(stub1.calledOnce).to.be.true;
sandbox.restore();
});
});
describe('#_computeChainwork', function() {
it('should calculate chain work correctly', function() {
var expected = new BN(new Buffer('000000000000000000000000000000000000000000677c7b8122f9902c79f4e0', 'hex'));
var prev = new BN(new Buffer('000000000000000000000000000000000000000000677bd68118a98f8779ea90', 'hex'));
var actual = blockService._computeChainwork(0x18018d30, prev);
assert(actual.eq(expected), 'not equal: actual: ' + actual + ' expected: ' + expected);
});
});
describe('#_determineBlockState', function() {
it('should determine the block in a normal state', function() {
var sandbox = sinon.sandbox.create();
var stub1 = sandbox.stub(blockService, '_isChainReorganizing').returns(false);
var stub2 = sandbox.stub(blockService, '_isOutOfOrder').returns(false);
expect(blockService._determineBlockState({})).to.equal('normal');
sandbox.restore();
});
it('should determine the block in a outoforder state', function() {
var sandbox = sinon.sandbox.create();
var stub1 = sandbox.stub(blockService, '_isChainReorganizing').returns(false);
var stub2 = sandbox.stub(blockService, '_isOutOfOrder').returns(true);
expect(blockService._determineBlockState({})).to.equal('outoforder');
sandbox.restore();
});
it('should determine the block in a reorg state', function() {
var sandbox = sinon.sandbox.create();
var stub1 = sandbox.stub(blockService, '_isChainReorganizing').returns(true);
var stub2 = sandbox.stub(blockService, '_isOutOfOrder').returns(false);
expect(blockService._determineBlockState({})).to.equal('reorg');
sandbox.restore();
});
});
describe('#_findCommonAncestor', function() {
var sandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
});
after(function() {
sandbox.restore();
});
it('should find the common ancestor between the current chain and the new chain', function() {
var block = { hash: 'cc' };
blockService._tip = { hash: 'bb' }
blockService._blockQueue = new LRU(5);
blockService._blockQueue.set('aa', { prevHash: '00' });
blockService._blockQueue.set('bb', { prevHash: 'aa' });
blockService._blockQueue.set('cc', { prevHash: 'aa' });
blockService._chainTips = [ 'cc', 'bb' ];
var commonAncestor = blockService._findCommonAncestor(block);
expect(commonAncestor).to.equal('aa');
});
});
describe('#getBestBlockHash', function() {
it('should get best block hash', function() {
});
});
describe('#getBlock', function() {
it('should get block', function() {
});
});
describe('#getBlockHashesByTimestamp', function() {
it('should get block hashes by timestamp', function() {
});
});
describe('#getBlockHeader', function() {
it('should get block header', function() {
});
});
describe('#_getBlockOperations', function() {
it('should get block operations when given one block', function() {
var block = new Block(new Buffer('0100000095194b8567fe2e8bbda931afd01a7acd399b9325cb54683e64129bcd00000000660802c98f18fd34fd16d61c63cf447568370124ac5f3be626c2e1c3c9f0052d19a76949ffff001d33f3c25d0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d014dffffffff0100f2052a01000000434104e70a02f5af48a1989bf630d92523c9d14c45c75f7d1b998e962bff6ff9995fc5bdb44f1793b37495d80324acba7c8f537caaf8432b8d47987313060cc82d8a93ac00000000', 'hex'));
var ops = blockService._getBlockOperations(block);
expect(ops[0]).to.deep.equal({ type: 'put', key: blockService._encoding.encodeBlockKey(block.hash), value: blockService._encoding.encodeBlockValue(block) });
});
it('should get block operations when given more than one block', function() {
var block = new Block(new Buffer('0100000095194b8567fe2e8bbda931afd01a7acd399b9325cb54683e64129bcd00000000660802c98f18fd34fd16d61c63cf447568370124ac5f3be626c2e1c3c9f0052d19a76949ffff001d33f3c25d0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d014dffffffff0100f2052a01000000434104e70a02f5af48a1989bf630d92523c9d14c45c75f7d1b998e962bff6ff9995fc5bdb44f1793b37495d80324acba7c8f537caaf8432b8d47987313060cc82d8a93ac00000000', 'hex'));
var ops = blockService._getBlockOperations([block, block]);
expect(ops[0]).to.deep.equal({ type: 'put', key: blockService._encoding.encodeBlockKey(block.hash), value: blockService._encoding.encodeBlockValue(block) });
expect(ops[1]).to.deep.equal({ type: 'put', key: blockService._encoding.encodeBlockKey(block.hash), value: blockService._encoding.encodeBlockValue(block) });
});
});
describe('#getBlockOverview', function() {
it('should get block overview', function() {
});
};
describe('#_getChainwork', function() {
it('should get chainwork', function() {
var expected = new BN(new Buffer('000000000000000000000000000000000000000000677c7b8122f9902c79f4e0', 'hex'));
blockService._meta = [ { chainwork: '000000000000000000000000000000000000000000677bd68118a98f8779ea90', hash: 'aa' } ];
blockService._blockQueue = LRU(1);
blockService._blockQueue.set('bb', { header: { bits: 0x18018d30 }});
var actual = blockService._getChainwork('bb');
assert(actual.eq(expected), 'not equal: actual: ' + actual + ' expected: ' + expected);
});
});
describe('#_getDelta', function() {
var sandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
});
after(function() {
sandbox.restore();
});
it('should get all unsent blocks for the active chain', function() {
var block1 = { header: { prevHash: new Buffer('00', 'hex') }};
var block2 = { header: { prevHash: new Buffer('aa', 'hex') }};
var block3 = { header: { prevHash: new Buffer('bb', 'hex') }};
var expected = [ block2, block3 ];
blockService._tip = { hash: 'aa' };
blockService._blockQueue = LRU(3);
blockService._blockQueue.set('aa', block1);
blockService._blockQueue.set('bb', block2);
blockService._blockQueue.set('cc', block3);
var actual = blockService._getDelta('cc');
expect(actual).to.deep.equal(expected);
});
});
describe('#getRawBlock', function() {
});
describe('#_isChainReorganizing', function() {
it('should decide that chain is reorging', function() {
blockService._tip = { hash: 'aa' };
var block = { header: { prevHash: new Buffer('00', 'hex') }};
expect(blockService._isChainReorganizing(block)).to.be.true;
});
it('should decide that chain is not reorging', function() {
blockService._tip = { hash: 'aa' };
var block = { header: { prevHash: new Buffer('aa', 'hex') }};
expect(blockService._isChainReorganizing(block)).to.be.false;
});
});
describe('#_isOutOfOrder', function() {
beforeEach(function() {
var prevHash = '00000000';
for(var i = 0; i < 110; i++) {
var newHash = crypto.randomBytes(4);
var mock = { toBuffer: function() { return new Buffer(new Array(10), 'hex') } };
blockService._blockQueue.set(newHash, mock);
prevHash = newHash;
}
});
it('should detect an orphaned block', function() {
var block = { hash: 'ee', header: { prevHash: new Buffer('aa', 'hex') }};
expect(blockService._isOutOfOrder(block)).to.be.true;
});
it('should not detect an orphaned block', function() {
var block = { hash: 'new', header: { prevHash: '00' }};
expect(blockService._isOutOfOrder(block)).to.be.true;
});
});
describe('#_onBestHeight', function() {
var sandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
});
after(function() {
sandbox.restore();
});
it('should set best height', function() {
var startSync = sandbox.stub(blockService, '_startSync');
blockService._onBestHeight(123);
expect(blockService._bestHeight).to.equal(123);
expect(startSync.calledOnce).to.be.true;
});
});
describe('#_onBlock', function() {
it('should perform all the steps for onBlock handler (normal)', function() {
var sandbox = sinon.sandbox.create();
var alreadyProcessed = sandbox.stub(blockService, '_blockAlreadyProcessed').returns(false);
var cacheBlock = sandbox.stub(blockService, '_cacheBlock');
var blockState = sandbox.stub(blockService, '_determineBlockState').returns('normal');
var updateChainTips = sandbox.stub(blockService, '_updateChainInfo');
var sendAllUnsent = sandbox.stub(blockService, '_sendDelta');
var saveMetaData = sandbox.stub(blockService, '_saveMetaData');
blockService._onBlock({ hash: 'aa' });
expect(alreadyProcessed.callCount).to.equal(1);
expect(cacheBlock.callCount).to.equal(1);
expect(blockState.callCount).to.equal(1);
expect(updateChainTips.callCount).to.equal(1);
expect(sendAllUnsent.callCount).to.equal(1);
expect(saveMetaData.callCount).to.equal(1);
sandbox.restore();
});
it('should perform all the steps for onBlock handler (reorg)', function() {
var sandbox = sinon.sandbox.create();
var block = { hash: 'aa' };
var alreadyProcessed = sandbox.stub(blockService, '_blockAlreadyProcessed').returns(false);
var cacheBlock = sandbox.stub(blockService, '_cacheBlock');
var blockState = sandbox.stub(blockService, '_determineBlockState').returns('reorg');
var updateChainTips = sandbox.stub(blockService, '_updateChainInfo');
var reorgListener = blockService.on('reorg', function(block) {
expect(block).to.equal(block);
});
blockService._onBlock(block);
expect(alreadyProcessed.callCount).to.equal(1);
expect(cacheBlock.callCount).to.equal(1);
expect(blockState.callCount).to.equal(1);
expect(updateChainTips.callCount).to.equal(1);
sandbox.restore();
});
it('should perform all the steps for onBlock handler (orphaned)', function() {
var sandbox = sinon.sandbox.create();
var alreadyProcessed = sandbox.stub(blockService, '_blockAlreadyProcessed').returns(false);
var cacheBlock = sandbox.stub(blockService, '_cacheBlock');
var blockState = sandbox.stub(blockService, '_determineBlockState').returns('outoforder');
var updateChainTips = sandbox.stub(blockService, '_updateChainInfo');
blockService._onBlock({ hash: 'aa' });
expect(alreadyProcessed.callCount).to.equal(1);
expect(cacheBlock.callCount).to.equal(1);
expect(blockState.callCount).to.equal(1);
expect(updateChainTips.callCount).to.equal(1);
sandbox.restore();
});
});
describe('#_onDbError', function() {
it('should handle db errors', function() {
var stop = sinon.stub();
blockService.node = { stop: stop };
expect(stop.calledOnce);
});
});
describe('#_selectActiveChain', function() {
var sandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
});
after(function() {
sandbox.restore();
});
it('should select active chain based on most chain work', function() {
blockService._chainTips = [ 'aa', 'cc' ];
var getCW = sandbox.stub(blockService, '_getChainwork');
getCW.onCall(0).returns(new BN(1));
getCW.onCall(1).returns(new BN(2));
var expected = 'cc';
var actual = blockService._selectActiveChain();
expect(actual).to.equal(expected);
});
});
describe('#_sendDelta', function() {
var sandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
});
after(function() {
sandbox.restore();
});
it('should send all unsent blocks for the active chain', function() {
blockService._meta = [ { hash: 'aa' } ];
var activeChain = sandbox.stub(blockService, '_selectActiveChain').returns('aa');
var getDelta = sandbox.stub(blockService, '_getDelta').returns(['aa', '00']);
var broadcast = sandbox.stub(blockService, '_broadcast');
var setTip = sandbox.stub(blockService, '_setTip');
blockService._sendDelta();
expect(activeChain.calledOnce).to.be.true;
expect(getDelta.calledOnce).to.be.true;
expect(broadcast.calledTwice).to.be.true;
expect(setTip.calledOnce).to.be.true;
});
});
describe('#_setListeners', function() {
var sandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
});
after(function() {
sandbox.restore();
});
it('should set listeners for bestHeight, db error and reorg', function() {
var bestHeight = sandbox.stub();
var dbError = sandbox.stub();
var on = sandbox.stub(blockService, 'on');
blockService._p2p = { once: bestHeight };
blockService._db = { on: dbError };
blockService._setListeners();
expect(bestHeight.calledOnce).to.be.true;
expect(dbError.calledOnce).to.be.true;
expect(on.calledOnce).to.be.true;
});
});
describe('#_setTip', function() {
it('should set the tip if given a block', function() {
var stub = sinon.stub();
blockService._db = {};
blockService._db.setServiceTip = stub;
blockService._tip = { height: 99, hash: '00' };
blockService._setTip({ height: 100, hash: 'aa' });
expect(stub.calledOnce).to.be.true;
});
});
describe('#_startSubscriptions', function() {
it('should start the subscriptions if not already subscribed', function() {
var on = sinon.stub();
var subscribe = sinon.stub();
var openBus = sinon.stub().returns({ on: on, subscribe: subscribe });
blockService.node = { openBus: openBus };
blockService._startSubscriptions();
expect(blockService._subscribed).to.be.true;
expect(openBus.calledOnce).to.be.true;
expect(on.calledOnce).to.be.true;
expect(subscribe.calledOnce).to.be.true;
});
});
describe('#_startSync', function() {
var sandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
blockService.node.getNetworkName = function() { return 'regtest'; };
blockService._tip = { height: 100 };
blockService._bestHeight = 201;
});
after(function() {
sandbox.restore();
});
it('should start the sync of blocks if type set', function() {
var stub = sandbox.stub(blockService, '_sync');
blockService._startSync();
expect(stub.calledOnce).to.be.true;
expect(blockService._latestBlockHash).to.equal(constants.BITCOIN_GENESIS_HASH[blockService.node.getNetworkName()]);
expect(blockService._p2pBlockCallsNeeded).to.equal(1);
});
});
describe('#_sync', function() {
it('should sync blocks', function() {
blockService._p2pBlockCallsNeeded = 2;
var getBlocks = sinon.stub();
blockService._p2p = { getBlocks: getBlocks };
blockService._sync();
expect(blockService._p2pBlockCallsNeeded).to.equal(1);
expect(getBlocks.calledOnce).to.be.true;
});
});
describe('#start', function() {
var sandbox;
var getServiceTip;
var getServicePrefix;
var loadMeta;
var startSubs;
beforeEach(function() {
sandbox = sinon.sandbox.create();
getServiceTip = sandbox.stub().callsArgWith(1, null, { height: 100, hash: 'aa' });
getServicePrefix = sandbox.stub().callsArgWith(1, null, new Buffer('0000', 'hex'));
loadMeta = sandbox.stub(blockService, '_loadMeta').callsArgWith(0, null);
startSubs = sandbox.stub(blockService, '_startSubscriptions');
blockService._db = {
getPrefix: getServicePrefix,
getServiceTip: getServiceTip
};
var setListeners = sandbox.stub(blockService, '_setListeners');
});
after(function() {
sandbox.restore();
});
it('should get the prefix', function(done) {
blockService.start(function() {
expect(blockService._encoding).to.be.an.instanceof(Encoding);
expect(getServiceTip.calledOnce).to.be.true;
expect(getServicePrefix.calledOnce).to.be.true;
expect(loadMeta.calledOnce).to.be.true;
expect(startSubs.calledOnce).to.be.true;
done();
});
});
});
describe('#stop', function() {
it('should call stop', function(done) {
blockService.stop(done);
});
});
describe('#_updateChainInfo', function() {
var sandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
});
after(function() {
sandbox.restore();
});
it('should update chain tips and incomplete block list for the out of order state', function() {
var stub = sandbox.stub(blockService, '_updateOutOfOrderStateChainInfo');
blockService._updateChainInfo({ header: { prevHash: new Buffer('aa', 'hex')}}, 'outoforder');
expect(stub.calledOnce).to.be.true;
});
it('should update chain tips and incomplete block list for normal state', function() {
var stub = sandbox.stub(blockService, '_updateNormalStateChainInfo');
blockService._updateChainInfo({ header: { prevHash: new Buffer('aa', 'hex')}}, 'normal');
expect(stub.calledOnce).to.be.true;
});
it('should update chain tips and incomplete block list for reorg state', function() {
var stub = sandbox.stub(blockService, '_updateReorgStateChainInfo');
blockService._updateChainInfo({ header: { prevHash: new Buffer('aa', 'hex')}}, 'reorg');
expect(stub.calledOnce).to.be.true;
});
});
describe('#_updateOutOfOrderStateChainInfo', function() {
var block1 = { hash: 'aa', header: { prevHash: new Buffer('99', 'hex') } };
var block2 = { hash: 'bb', header: { prevHash: new Buffer('aa', 'hex') } };
var block3 = { hash: 'cc', header: { prevHash: new Buffer('bb', 'hex') } };
var block4 = { hash: 'dd', header: { prevHash: new Buffer('cc', 'hex') } };
var block5 = { hash: 'ee', header: { prevHash: new Buffer('dd', 'hex') } };
var block6_1 = { hash: '11', header: { prevHash: new Buffer('ee', 'hex') } };
var block6 = { hash: 'ff', header: { prevHash: new Buffer('ee', 'hex') } };
it('should join chains if needed', function() {
blockService._incompleteChains = [ [block6, block5], [block3, block2] ];
blockService._updateOutOfOrderStateChainInfo(block4);
expect(blockService._incompleteChains).to.deep.equal([ [ block6, block5, block4, block3, block2] ]);
});
it('should join chains if needed different order', function() {
blockService._incompleteChains = [ [block3, block2], [block6, block5] ];
blockService._updateOutOfOrderStateChainInfo(block4);
expect(blockService._incompleteChains).to.deep.equal([ [ block6, block5, block4, block3, block2] ]);
});
it('should not join chains', function() {
blockService._incompleteChains = [ [block2, block1], [block6, block5] ];
blockService._updateOutOfOrderStateChainInfo(block4);
expect(blockService._incompleteChains).to.deep.equal([ [ block2, block1 ], [ block6, block5, block4 ] ]);
});
it('should not join chains, only add a new chain', function() {
blockService._incompleteChains = [ [block2, block1] ];
blockService._updateOutOfOrderStateChainInfo(block6);
expect(blockService._incompleteChains).to.deep.equal([ [ block2, block1 ], [ block6 ] ]);
});
it('should join multiple different chains', function() {
blockService._incompleteChains = [ [block2, block1], [block6], [block6_1], [block4] ];
blockService._updateOutOfOrderStateChainInfo(block5);
expect(blockService._incompleteChains).to.deep.equal([
[ block2, block1 ],
[ block6, block5, block4 ],
[ block6_1, block5, block4 ]
]);
});
});
describe('#_updateReorgStateChainInfo', function() {
it('should update chain tips for reorg situation', function() {
blockService._chainTips = [];
blockService._updateReorgStateChainInfo({ hash: 'aa' });
expect(blockService._chainTips).to.deep.equal([ 'aa' ]);
});
});
describe('#_updateNormalStateChainInfo', function() {
it('should update chain tips when there is no incomplete chains', function() {
var block2 = { hash: 'bb', header: { prevHash: new Buffer('aa', 'hex') } };
blockService._incompleteChains = [];
blockService._chainTips = [ 'aa' ];
blockService._updateNormalStateChainInfo(block2, 'aa');
expect(blockService._chainTips).to.deep.equal([ 'bb' ]);
expect(blockService._incompleteChains).to.deep.equal([]);
});
it('should update chain tips when there is an incomplete chain', function() {
var block2 = { hash: 'bb', header: { prevHash: new Buffer('aa', 'hex') } };
var block3 = { hash: 'cc', header: { prevHash: new Buffer('bb', 'hex') } };
var block4 = { hash: 'dd', header: { prevHash: new Buffer('cc', 'hex') } };
blockService._incompleteChains = [ [ block4, block3 ] ];
blockService._chainTips = [ 'aa' ];
blockService._updateNormalStateChainInfo(block2, 'aa');
expect(blockService._chainTips).to.deep.equal([ 'dd' ]);
expect(blockService._incompleteChains).to.deep.equal([]);
});
it('should update chain tip when there are mulitipla chain tips', function() {
var block4 = { hash: 'dd', header: { prevHash: new Buffer('cc', 'hex') } };
var block5 = { hash: 'ee', header: { prevHash: new Buffer('dd', 'hex') } };
var block6_1 = { hash: '11', header: { prevHash: new Buffer('ee', 'hex') } };
var block6 = { hash: 'ff', header: { prevHash: new Buffer('ee', 'hex') } };
blockService._incompleteChains = [ [ block6, block5 ], [ block6_1, block5 ] ];
blockService._chainTips = [ 'cc' ];
blockService._updateNormalStateChainInfo(block4, 'cc');
expect(blockService._chainTips).to.deep.equal([ '11', 'ff' ]);
expect(blockService._incompleteChains).to.deep.equal([]);
});
});
});