This commit is contained in:
Chris Kleeschulte 2017-07-27 19:12:23 -04:00
parent f8df875c34
commit 2bd769cce0
11 changed files with 186 additions and 436 deletions

View File

@ -66,6 +66,9 @@ Logger.prototype._log = function(color) {
args[0] = '[' + date.toISOString() + ']' + ' ' + typeString + ' ' + args[0];
}
var fn = console.log;
if (level === 'error') {
fn = console.error;
}
fn.apply(console, args);
};

View File

@ -3,9 +3,8 @@
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var async = require('async');
var assert = require('assert');
var bitcore = require('bitcore-lib');
var Networks = bitcore.Networks;
var $ = bitcore.util.preconditions;
var _ = bitcore.deps._;
var index = require('./');
var log = index.log;
@ -25,7 +24,6 @@ function Node(config) {
}
if (config.services) {
$.checkArgument(Array.isArray(config.services));
this._unloadedServices = config.services;
}
}
@ -49,15 +47,7 @@ Node.prototype._init = function(config) {
};
Node.prototype._setNetwork = function(config) {
if (config.network === 'testnet') {
this.network = Networks.get('testnet');
} else if (config.network === 'regtest') {
Networks.enableRegtest();
this.network = Networks.get('regtest');
} else {
this.network = Networks.defaultNetwork;
}
$.checkState(this.network, 'Unrecognized network');
this.network = config.network;
};
Node.prototype.openBus = function(options) {
@ -107,7 +97,7 @@ Node.prototype._getServiceOrder = function(services) {
var name = names[i];
var service = servicesByName[name];
$.checkState(service, 'Required dependency "' + name + '" not available.');
assert(service, 'Required dependency "' + name + '" not available.');
addToStack(service.module.dependencies);
@ -131,9 +121,9 @@ Node.prototype._startService = function(serviceInfo, callback) {
var config;
if (serviceInfo.config) {
$.checkState(_.isObject(serviceInfo.config));
$.checkState(!serviceInfo.config.node);
$.checkState(!serviceInfo.config.name);
assert(_.isObject(serviceInfo.config));
assert(serviceInfo.config.node);
assert(serviceInfo.config.name);
config = serviceInfo.config;
} else {
config = {};

View File

@ -371,7 +371,7 @@ AddressService.prototype._onReorg = function(commonAncestorHeader, oldBlockList)
var address;
for(var k = 0; k < tx.inputs.length; k++) {
var input = tx.inputs[k];
address = utils.getAddressString({ tx: tx, item: input, network: this._network });
address = input.address;
if (!address) {
continue;
@ -386,7 +386,7 @@ AddressService.prototype._onReorg = function(commonAncestorHeader, oldBlockList)
//outputs
for(k = 0; k < tx.outputs.length; k++) {
var output = tx.outputs[k];
address = utils.getAddressString({ tx: tx, item: output, network: this._network });
address = output.address;
if (!address) {
continue;
@ -431,7 +431,7 @@ AddressService.prototype.onBlock = function(block, callback) {
AddressService.prototype._processInput = function(tx, input, opts) {
var address = utils.getAddressString({ item: input });
var address = input.address;
if(!address) {
return;
@ -464,7 +464,7 @@ AddressService.prototype._processInput = function(tx, input, opts) {
AddressService.prototype._processOutput = function(tx, output, index, opts) {
var address = utils.getAddressString({ item: output });
var address = output.address;
if(!address) {
return;

View File

@ -317,6 +317,7 @@ BlockService.prototype._handleReorg = function(hash, allHeaders) {
this._reorging = false;
});
};
// get the blocks from our current tip to the given hash, non-inclusive

View File

@ -6,9 +6,6 @@ var async = require('async');
var levelup = require('levelup');
var leveldown = require('leveldown');
var mkdirp = require('mkdirp');
var bitcore = require('bitcore-lib');
var Networks = bitcore.Networks;
var $ = bitcore.util.preconditions;
var Service = require('../../service');
var constants = require('../../constants');
var log = require('../../index').log;
@ -38,10 +35,10 @@ function DB(options) {
this.subscriptions = {};
this.GENESIS_HASH = constants.BITCOIN_GENESIS_HASH[this.node.getNetworkName()];
this.GENESIS_HASH = constants.BITCOIN_GENESIS_HASH[this.node.network];
this.node.on('stopping', function() {
log.warn('Node is stopping, gently closing the database.');
log.warn('Node is stopping, gently closing the database. Please wait, this could take a while.');
});
}
@ -52,19 +49,16 @@ DB.dependencies = [];
DB.prototype._onError = function(err) {
log.error('Db Service: error: ' + err);
this.node.stop();
process.exit(-1);
};
DB.prototype._setDataPath = function() {
$.checkState(this.node.datadir, 'Node is expected to have a "datadir" property');
if (this.node.network === Networks.livenet) {
assert(fs.existsSync(this.node.datadir), 'Node is expected to have a "datadir" property');
if (this.node.network === 'livenet' || this.node.network === 'mainnet') {
this.dataPath = this.node.datadir + '/bitcore-node.db';
} else if (this.node.network === Networks.testnet) {
if (this.node.network.regtestEnabled) {
} else if (this.node.network === 'regtest') {
this.dataPath = this.node.datadir + '/regtest/bitcore-node.db';
} else {
this.dataPath = this.node.datadir + '/testnet3/bitcore-node.db';
}
} else if (this.node.network === 'testnet') {
this.dataPath = this.node.datadir + '/testnet/bitcore-node.db';
} else {
throw new Error('Unknown network: ' + this.network);
}
@ -77,13 +71,12 @@ DB.prototype._setVersion = function(callback) {
};
DB.prototype.start = function(callback) {
var self = this;
if (!fs.existsSync(self.dataPath)) {
if (!fs.existsSync(this.dataPath)) {
mkdirp.sync(this.dataPath);
}
self._store = levelup(self.dataPath, { db: self.levelupStore, keyEncoding: 'binary', valueEncoding: 'binary'});
this._store = levelup(this.dataPath, { db: this.levelupStore, keyEncoding: 'binary', valueEncoding: 'binary'});
setImmediate(callback);
@ -135,7 +128,7 @@ DB.prototype.put = function(key, value, callback) {
var self = this;
if (self._stopping) {
return;
callback();
}
self._store.put(key, value, callback);
@ -195,6 +188,7 @@ DB.prototype.close = function(callback) {
if (this._store && this._store.isOpen()) {
this._store.close(callback);
}
setImmediate(callback);
};
DB.prototype.getAPIMethods = function() {

View File

@ -16,7 +16,7 @@ MempoolService.dependencies = ['db', 'block'];
MempoolService.prototype.getAPIMethods = function() {
var methods = [
['getMempoolTransaction', this, this.getTransaction, 1]
['getMempoolTransaction', this, this.getMempoolTransaction, 1]
];
return methods;
};

View File

@ -7,6 +7,7 @@ var utils = require('../../utils');
var _ = require('lodash');
var LRU = require('lru-cache');
var Unit = require('bitcore-lib').Unit;
var log = require('../../index').log;
function TransactionService(options) {
BaseService.call(this, options);
@ -93,7 +94,6 @@ TransactionService.prototype.sendTransaction = function(tx, callback) {
TransactionService.prototype.start = function(callback) {
var self = this;
self._setListeners();
self._db.getPrefix(self.name, function(err, prefix) {
@ -161,12 +161,6 @@ TransactionService.prototype.onBlock = function(block, callback) {
return self._processTransaction(tx, { block: block });
});
if (operations && operations.length > 0) {
self._db.batch(operations);
}
setImmediate(function() {
callback(null, operations);
});
@ -175,13 +169,14 @@ TransactionService.prototype.onBlock = function(block, callback) {
TransactionService.prototype._onReorg = function(commonAncestorHeader, oldBlockList) {
var self = this;
// if the common ancestor block height is greater than our own, then nothing to do for the reorg
if (this._tip.height <= commonAncestorHeader.height) {
if (self._tip.height <= commonAncestorHeader.height) {
return;
}
// set the tip to the common ancestor in case something goes wrong with the reorg
var tipOps = utils.encodeTip({ hash: commonAncestorHeader.hash, height: commonAncestorHeader.height }, this.name);
var tipOps = utils.encodeTip({ hash: commonAncestorHeader.hash, height: commonAncestorHeader.height }, self.name);
var removalOps = [{
type: 'put',
@ -195,12 +190,18 @@ TransactionService.prototype._onReorg = function(commonAncestorHeader, oldBlockL
var tx = block.transactions[j];
removalOps.push({
type: 'del',
key: this._encoding.encodeTransactionKey(tx.id)
key: self._encoding.encodeTransactionKey(tx.id)
});
}
}
this._db.batch(removalOps);
self._db.batch(removalOps, function(err) {
if (err) {
log.error('Transaction Service: error removing operations during reorg.' + err);
self.node.stop();
return;
}
});
};
@ -227,14 +228,6 @@ TransactionService.prototype._processTransaction = function(tx, opts) {
};
TransactionService.prototype._setListeners = function() {
var self = this;
self.on('reorg', self._onReorg.bind(self));
};
TransactionService.prototype._startSubscriptions = function() {
if (this._subscribed) {

View File

@ -1,36 +1,22 @@
'use strict';
var bitcore = require('bitcore-lib');
var BufferUtil = bitcore.util.buffer;
var MAX_SAFE_INTEGER = 0x1fffffffffffff; // 2 ^ 53 - 1
var crypto = require('crypto');
var _ = require('lodash');
var constants = require('./constants');
var BN = require('bn.js');
var utils = {};
utils.isHash = function(value) {
return typeof value === 'string' && value.length === 64 && /^[0-9a-fA-F]+$/.test(value);
};
utils.isSafeNatural = function(value) {
return typeof value === 'number' &&
isFinite(value) &&
Math.floor(value) === value &&
value >= 0 &&
value <= MAX_SAFE_INTEGER;
};
utils.startAtZero = function(obj, key) {
if (!obj.hasOwnProperty(key)) {
obj[key] = 0;
}
utils.isHeight = function(blockArg) {
return _.isNumber(blockArg) || (blockArg.length < 40 && /^[0-9]+$/.test(blockArg));
};
//start
utils.isAbsolutePath = require('path').isAbsolute;
if (!utils.isAbsolutePath) {
utils.isAbsolutePath = require('path-is-absolute');
}
//main
utils.parseParamsWithJSON = function(paramsArg) {
var params = paramsArg.map(function(paramArg) {
var param;
@ -45,9 +31,12 @@ utils.parseParamsWithJSON = function(paramsArg) {
};
utils.getTerminalKey = function(startKey) {
var endKey = Buffer.from(startKey);
endKey.writeUInt8(startKey.readUInt8(startKey.length - 1) + 1, startKey.length - 1);
return endKey;
if (!startKey || !Buffer.isBuffer(startKey)) {
return;
}
var bn = new BN(startKey);
var endBN = bn.iaddn(1);
return endBN.toBuffer();
};
utils.diffTime = function(time) {
@ -55,27 +44,6 @@ utils.diffTime = function(time) {
return (diff[0] * 1E9 + diff[1])/(1E9 * 1.0);
};
utils.reverseBufferToString = function(buf) {
if (_.isString(buf)) {
buf = new Buffer(buf, 'hex');
}
return BufferUtil.reverse(buf).toString('hex');
};
utils.getAddressString = function(opts) {
if (!opts.item) {
return;
}
// TODO: this does not handle P2PK correctly, it uses the address and not the
// pubkey
if (opts.address) {
return address.toString();
}
};
utils.sendError = function(err, res) {
if (err.statusCode) {
res.status(err.statusCode).send(err.message);
@ -85,108 +53,6 @@ utils.sendError = function(err, res) {
}
};
utils.getWalletId = exports.generateJobId = function() {
return crypto.randomBytes(16).toString('hex');
};
utils.getWalletId = exports.generateJobId = function() {
return crypto.randomBytes(16).toString('hex');
};
utils.toJSONL = function(obj) {
var str = JSON.stringify(obj);
str = str.replace(/\n/g, '');
return str + '\n';
};
utils.normalizeTimeStamp = function(addressArg) {
var addresses = [addressArg];
if (Array.isArray(addressArg)) {
addresses = addressArg;
}
return addresses;
};
utils.normalizeTimeStamp = function(value) {
if (value > 0xffffffff) {
value = Math.round(value/1000);
}
return value;
};
utils.delimitedStringParse = function(delim, str) {
function tryJSONparse(str) {
try {
return JSON.parse(str);
} catch(e) {
return false;
}
}
var ret = [];
if (delim === null) {
return tryJSONparse(str);
}
var list = str.split(delim);
for(var i = 0; i < list.length; i++) {
ret.push(tryJSONparse(list[i]));
}
ret = _.compact(ret);
return ret.length === 0 ? false : ret;
};
utils.toIntIfNumberLike = function(a) {
if (!/[^\d]+/.test(a)) {
return parseInt(a);
}
return a;
};
utils.getBlockInfoString = function(tip, best) {
var diff = best - tip;
var astr = diff + ' blocks behind.';
if (diff === -1) {
astr = Math.abs(diff) + ' block ahead. Peer may be syncing or we may need to reorganize our chain after new blocks arrive.';
} else if (diff < 1) {
astr = Math.abs(diff) + ' blocks ahead. Peer may be syncing or we may need to reorganize our chain after new blocks arrive.';
} else if (diff === 1) {
astr = diff + ' block behind.';
}
};
utils.joinListsOnIndex = function(indexList, list1, list2, direction) {
var newChains = [];
indexList.forEach(function(index) {
var otherList = list2[index];
if (direction && direction === 'reverse') {
newChains.push(otherList.concat(list1));
} else {
newChains.push(list1.concat(otherList));
}
});
return newChains;
};
utils.removeItemsByIndexList = function(indexList, list) {
var ret = [];
for(var i = 0; i < list.length; i++) {
var item = list[i];
if (indexList.indexOf(i) === -1) {
ret.push(item);
}
}
return ret;
};
utils.encodeTip = function(tip, name) {
var key = Buffer.concat([ constants.DB_PREFIX,
new Buffer('tip-' + name, 'utf8') ]);
@ -199,10 +65,6 @@ utils.encodeTip = function(tip, name) {
};
utils.isHeight = function(blockArg) {
return _.isNumber(blockArg) || (blockArg.length < 40 && /^[0-9]+$/.test(blockArg))
};
utils.SimpleMap = function SimpleMap() {
var object = {};
var array = [];

View File

@ -1,56 +1,154 @@
'use strict';
var expect = require('chai').expect;
var bitcore = require('bitcore-lib');
var DB = require('../../../lib/services/db');
var chai = require('chai');
var should = chai.should();
var assert = chai.assert;
var expect = chai.expect;
var DBService = require('../../../lib/services/db');
var sinon = require('sinon');
var Levelup = require('levelup');
describe('DB', function() {
describe('Reorg', function() {
var dbService;
before(function() {
this.db = new DB({
node: {
network: bitcore.Networks.testnet,
datadir: '/tmp',
services: ''
}
var sandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
dbService = new DBService({
node: {
services: [],
datadir: '/tmp',
network: 'regtest',
on: sinon.stub()
}
});
});
afterEach(function() {
sandbox.restore();
});
describe('#start', function() {
it('should start the db service by creating a db dir, ' +
' if necessary, and setting the store', function(done) {
dbService.start(function() {
dbService._store.should.be.instanceOf(Levelup);
done();
});
});
});
describe('#stop', function() {
it('should stop if store not open', function(done) {
dbService.stop(function() {
var close = sandbox.stub().callsArg(0);
dbService._store = { close: close };
dbService._stopping.should.be.true;
done();
});
this.db.tip = { hash: 'ff', height: 444 };
});
it('should detect a reorg from a common ancenstor that is in our set', function() {
var block1 = { hash: '11', header: { prevHash: new Buffer('ff', 'hex') } };
var block2 = { hash: '22', header: { prevHash: new Buffer('11', 'hex') } };
var block3 = { hash: '33', header: { prevHash: new Buffer('22', 'hex') } };
var block4 = { hash: '44', header: { prevHash: new Buffer('22', 'hex') } };
//blocks must be passed in the order that they are received.
var blocks = [ block3, block2, block1, block4 ];
expect(this.db.detectReorg(blocks)).to.deep.equal(block3);
it('should stop if store open', function(done) {
dbService.stop(function() {
var close = sandbox.stub().callsArg(0);
dbService._store = { close: close, isOpen: sinon.stub().returns(true) };
dbService._stopping.should.be.true;
done();
});
});
});
it('should detect a reorg from a common ancenstor that is not in our set', function() {
var block1 = { hash: '11', header: { prevHash: new Buffer('ff', 'hex') } };
var block2 = { hash: '22', header: { prevHash: new Buffer('11', 'hex') } };
var block3 = { hash: '33', header: { prevHash: new Buffer('22', 'hex') } };
var block4 = { hash: '44', header: { prevHash: new Buffer('ee', 'hex') } };
var blocks = [ block3, block2, block1, block4 ];
expect(this.db.detectReorg(blocks)).to.deep.equal(block4);
describe('#_onError', function() {
it('should stop the db', function() {
var stop = sandbox.stub();
dbService.node = { stop: stop };
dbService._onError(new Error('some error'));
stop.should.be.calledOnce;
});
});
it('should not detect a reorg', function() {
this.db.reorgTipHash = null;
var block1 = { hash: '11', header: { prevHash: new Buffer('ff', 'hex') } };
var block2 = { hash: '22', header: { prevHash: new Buffer('11', 'hex') } };
var block3 = { hash: '33', header: { prevHash: new Buffer('22', 'hex') } };
var block4 = { hash: '44', header: { prevHash: new Buffer('33', 'hex') } };
var blocks = [ block3, block2, block1, block4 ];
var actual = this.db.detectReorg(blocks);
expect(actual).to.be.undefined;
describe('#_setDataPath', function() {
it('should set the data path', function() {
dbService._setDataPath();
dbService.dataPath.should.equal('/tmp/regtest/bitcore-node.db');
});
});
describe('#_setVersion', function() {
it('should set the version', function(done) {
var put = sandbox.stub(dbService, 'put').callsArgWith(2, null);
dbService._setVersion(function(err) {
put.should.be.calledOnce;
put.args[0][0].toString('hex').should.deep.equal('ffff76657273696f6e');
put.args[0][1].toString('hex').should.deep.equal('00000001');
done();
});
});
});
describe('#get', function() {
it('should get a value from the db', function(done) {
var get = sandbox.stub().callsArgWith(2, null, 'data');
dbService._store = { get: get };
dbService.get('key', function(err, value) {
if (err) {
return done(err);
}
value.should.equal('data');
done();
});
});
it('should not get a value while the node is shutting down', function(done) {
dbService._stopping = true;
dbService.get('key', function(err, value) {
err.message.should.equal('Shutdown sequence underway, not able to complete the query');
done();
});
});
});
describe('#put', function() {
it('should put a value in the db', function(done) {
var put = sandbox.stub().callsArgWith(2, null);
dbService._store = { put: put };
dbService.put(new Buffer('key'), new Buffer('value'), function(err) {
if (err) {
return done(err);
}
put.should.be.calledOnce;
done();
});
});
it('should not allow an operation while the node is shutting down', function(done) {
dbService._stopping = true;
dbService.put(new Buffer('key'), new Buffer('value'), function(err) {
done();
});
});
});
describe('#batch', function() {
});
describe('#createReadStream', function() {
});
describe('#createKeyStream', function() {
});
describe('#close', function() {
});
describe('#getServiceTip', function() {
});
describe('#getPrefix', function() {
});
});

View File

@ -1,56 +0,0 @@
'use strict';
var should = require('chai').should();
var sinon = require('sinon');
var bitcore = require('bitcore-lib');
var BufferUtil = bitcore.util.buffer;
var DB = require('../../../lib/services/db');
var Networks = bitcore.Networks;
var EventEmitter = require('events').EventEmitter;
var rimraf = require('rimraf');
var mkdirp = require('mkdirp');
var blocks = require('../../data/blocks.json');
describe('DB', function() {
var bitcoind = {
on: function(event, callback) {
},
genesisBuffer: blocks.genesis
};
var node = {
network: Networks.testnet,
datadir: '/tmp/datadir',
services: { bitcoind: bitcoind },
on: sinon.stub(),
once: sinon.stub()
};
before(function(done) {
var self = this;
rimraf(node.datadir, function(err) {
if(err) {
return done(err);
}
mkdirp(node.datadir, done);
});
this.db = new DB({node: node});
this.emitter = new EventEmitter();
});
describe('Reorg', function() {
it('should start db service', function(done) {
this.db.start(done);
});
});
});

View File

@ -1,135 +0,0 @@
'use strict';
var should = require('chai').should();
var sinon = require('sinon');
var bitcore = require('bitcore-lib');
var BufferUtil = bitcore.util.buffer;
var Reorg = require('../../../lib/services/db/reorg');
describe('Reorg', function() {
describe('full-test', function() {
before(function() {
sinon.stub(BufferUtil, 'reverse', function(input) {
return {
toString: function() {
return input;
}
};
});
});
after(function() {
BufferUtil.reverse.restore();
});
it('should handle a reorg correctly', function(done) {
var tipBlocks = [
{hash: 'main0', header: {prevHash: null}},
{hash: 'main1', header: {prevHash: 'main0'}},
{hash: 'fork1', header: {prevHash: 'main1'}},
{hash: 'fork2', header: {prevHash: 'fork1'}}
];
var concurrentBlocks = [
{hash: 'main0', header: {prevHash: null}},
{hash: 'main1', header: {prevHash: 'main0'}},
{hash: 'fork1', header: {prevHash: 'main1'}},
{hash: 'fork2', header: {prevHash: 'fork1'}},
{hash: 'fork3', header: {prevHash: 'fork2'}}
];
var bitcoindBlocks = [
{hash: 'main0', header: {prevHash: null}},
{hash: 'main1', header: {prevHash: 'main0'}},
{hash: 'main2', header: {prevHash: 'main1'}},
{hash: 'main3', header: {prevHash: 'main2'}}
];
var allBlocks = tipBlocks.concat(concurrentBlocks, bitcoindBlocks);
var db = {
tip: tipBlocks[3],
concurrentTip: concurrentBlocks[4],
store: {
batch: sinon.stub().callsArg(1)
},
getConcurrentBlockOperations: sinon.stub().callsArgWith(2, null, []),
getSerialBlockOperations: sinon.stub().callsArgWith(2, null, []),
getConcurrentTipOperation: sinon.stub().returns(null),
getTipOperation: sinon.stub().returns(null)
};
var node = {
services: {
bitcoind: {
getBlock: function(hash, callback) {
var block;
for(var i = 0; i < allBlocks.length; i++) {
if(allBlocks[i].hash === hash) {
block = allBlocks[i];
}
}
setImmediate(function() {
if(!block) {
return callback(new Error('Block not found: ' + hash));
}
callback(null, block);
});
},
getBlockHeader: function(hash, callback) {
var header;
for(var i = 0; i < allBlocks.length; i++) {
if(allBlocks[i].hash === hash) {
header = allBlocks[i].header;
}
}
setImmediate(function() {
if(!header) {
return callback(new Error('Block header not found: ' + hash));
}
callback(null, header);
});
}
}
}
};
var reorg = new Reorg(node, db);
reorg.handleReorg(bitcoindBlocks[3].hash, function(err) {
should.not.exist(err);
db.tip.hash.should.equal('main3');
db.concurrentTip.hash.should.equal('main3');
db.getConcurrentBlockOperations.callCount.should.equal(5);
db.getConcurrentBlockOperations.args[0][0].should.equal(concurrentBlocks[4]);
db.getConcurrentBlockOperations.args[0][1].should.equal(false);
db.getConcurrentBlockOperations.args[1][0].should.equal(concurrentBlocks[3]);
db.getConcurrentBlockOperations.args[1][1].should.equal(false);
db.getConcurrentBlockOperations.args[2][0].should.equal(concurrentBlocks[2]);
db.getConcurrentBlockOperations.args[2][1].should.equal(false);
db.getConcurrentBlockOperations.args[3][0].should.equal(bitcoindBlocks[2]);
db.getConcurrentBlockOperations.args[3][1].should.equal(true);
db.getConcurrentBlockOperations.args[4][0].should.equal(bitcoindBlocks[3]);
db.getConcurrentBlockOperations.args[4][1].should.equal(true);
db.getSerialBlockOperations.callCount.should.equal(4);
db.getSerialBlockOperations.args[0][0].should.deep.equal(tipBlocks[3]);
db.getSerialBlockOperations.args[0][1].should.equal(false);
db.getSerialBlockOperations.args[1][0].should.deep.equal(tipBlocks[2]);
db.getSerialBlockOperations.args[1][1].should.equal(false);
db.getSerialBlockOperations.args[2][0].should.deep.equal(bitcoindBlocks[2]);
db.getSerialBlockOperations.args[2][1].should.equal(true);
db.getSerialBlockOperations.args[3][0].should.deep.equal(bitcoindBlocks[3]);
db.getSerialBlockOperations.args[3][1].should.equal(true);
done();
});
});
});
});