Combine chain with db module.
This commit is contained in:
parent
df9b62acca
commit
16eef1279c
1
index.js
1
index.js
|
@ -2,7 +2,6 @@
|
|||
|
||||
module.exports = require('./lib');
|
||||
module.exports.Node = require('./lib/node');
|
||||
module.exports.Chain = require('./lib/chain');
|
||||
module.exports.Transaction = require('./lib/transaction');
|
||||
module.exports.Module = require('./lib/module');
|
||||
module.exports.errors = require('./lib/errors');
|
||||
|
|
|
@ -102,7 +102,7 @@ describe('Node Functionality', function() {
|
|||
});
|
||||
|
||||
var syncedHandler = function() {
|
||||
if (node.chain.tip.__height === 150) {
|
||||
if (node.modules.db.tip.__height === 150) {
|
||||
node.removeListener('synced', syncedHandler);
|
||||
done();
|
||||
}
|
||||
|
@ -178,18 +178,18 @@ describe('Node Functionality', function() {
|
|||
blocksRemoved++;
|
||||
};
|
||||
|
||||
node.chain.on('removeblock', removeBlock);
|
||||
node.modules.db.on('removeblock', removeBlock);
|
||||
|
||||
var addBlock = function() {
|
||||
blocksAdded++;
|
||||
if (blocksAdded === 2 && blocksRemoved === 1) {
|
||||
node.chain.removeListener('addblock', addBlock);
|
||||
node.chain.removeListener('removeblock', removeBlock);
|
||||
node.modules.db.removeListener('addblock', addBlock);
|
||||
node.modules.db.removeListener('removeblock', removeBlock);
|
||||
done();
|
||||
}
|
||||
};
|
||||
|
||||
node.chain.on('addblock', addBlock);
|
||||
node.modules.db.on('addblock', addBlock);
|
||||
|
||||
// We need to add a transaction to the mempool so that the next block will
|
||||
// have a different hash as the hash has been invalidated.
|
||||
|
|
247
lib/chain.js
247
lib/chain.js
|
@ -1,247 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var util = require('util');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var bitcore = require('bitcore');
|
||||
var BN = bitcore.crypto.BN;
|
||||
var $ = bitcore.util.preconditions;
|
||||
var Block = bitcore.Block;
|
||||
var index = require('./index');
|
||||
var log = index.log;
|
||||
var utils = require('./utils');
|
||||
|
||||
var MAX_STACK_DEPTH = 1000;
|
||||
|
||||
/**
|
||||
* Will instantiate a new Chain instance
|
||||
* @param {Object} options - The options for the chain
|
||||
* @param {Number} options.minBits - The minimum number of bits
|
||||
* @param {Number} options.maxBits - The maximum number of bits
|
||||
* @param {BN|Number} options.targetTimespan - The number of milliseconds for difficulty retargeting
|
||||
* @param {BN|Number} options.targetSpacing - The number of milliseconds between blocks
|
||||
* @returns {Chain}
|
||||
* @extends BaseChain
|
||||
* @constructor
|
||||
*/
|
||||
function Chain(opts) {
|
||||
/* jshint maxstatements: 30 */
|
||||
if (!(this instanceof Chain)) {
|
||||
return new Chain(opts);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
if(!opts) {
|
||||
opts = {};
|
||||
}
|
||||
|
||||
this.genesis = opts.genesis;
|
||||
this.genesisOptions = opts.genesisOptions;
|
||||
this.genesisWeight = new BN(0);
|
||||
this.tip = null;
|
||||
this.overrideTip = opts.overrideTip;
|
||||
this.cache = {
|
||||
hashes: {}, // dictionary of hash -> prevHash
|
||||
chainHashes: {}
|
||||
};
|
||||
this.lastSavedMetadata = null;
|
||||
this.lastSavedMetadataThreshold = 0; // Set this during syncing for faster performance
|
||||
this.blockQueue = [];
|
||||
this.processingBlockQueue = false;
|
||||
this.builder = opts.builder || false;
|
||||
this.ready = false;
|
||||
|
||||
this.on('initialized', function() {
|
||||
self.initialized = true;
|
||||
});
|
||||
|
||||
this.on('initialized', this._onInitialized.bind(this));
|
||||
|
||||
this.on('ready', function() {
|
||||
log.debug('Chain is ready');
|
||||
self.ready = true;
|
||||
self.startBuilder();
|
||||
});
|
||||
|
||||
this.minBits = opts.minBits || Chain.DEFAULTS.MIN_BITS;
|
||||
this.maxBits = opts.maxBits || Chain.DEFAULTS.MAX_BITS;
|
||||
|
||||
this.maxHashes = opts.maxHashes || Chain.DEFAULTS.MAX_HASHES;
|
||||
|
||||
this.targetTimespan = opts.targetTimespan || Chain.DEFAULTS.TARGET_TIMESPAN;
|
||||
this.targetSpacing = opts.targetSpacing || Chain.DEFAULTS.TARGET_SPACING;
|
||||
|
||||
this.node = opts.node;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
util.inherits(Chain, EventEmitter);
|
||||
|
||||
Chain.DEFAULTS = {
|
||||
MAX_HASHES: new BN('10000000000000000000000000000000000000000000000000000000000000000', 'hex'),
|
||||
TARGET_TIMESPAN: 14 * 24 * 60 * 60 * 1000, // two weeks
|
||||
TARGET_SPACING: 10 * 60 * 1000, // ten minutes
|
||||
MAX_BITS: 0x1d00ffff,
|
||||
MIN_BITS: 0x03000000
|
||||
};
|
||||
|
||||
Chain.prototype._onInitialized = function() {
|
||||
this.emit('ready');
|
||||
};
|
||||
|
||||
Chain.prototype.start = function(callback) {
|
||||
this.genesis = Block.fromBuffer(this.node.modules.bitcoind.genesisBuffer);
|
||||
this.once('initialized', callback);
|
||||
this.initialize();
|
||||
};
|
||||
|
||||
Chain.prototype.initialize = function() {
|
||||
var self = this;
|
||||
|
||||
// Does our database already have a tip?
|
||||
self.node.modules.db.getMetadata(function getMetadataCallback(err, metadata) {
|
||||
if(err) {
|
||||
return self.emit('error', err);
|
||||
} else if(!metadata || !metadata.tip) {
|
||||
self.tip = self.genesis;
|
||||
self.tip.__height = 0;
|
||||
self.tip.__weight = self.genesisWeight;
|
||||
self.node.modules.db.connectBlock(self.genesis, function(err) {
|
||||
if(err) {
|
||||
return self.emit('error', err);
|
||||
}
|
||||
|
||||
self.emit('addblock', self.genesis);
|
||||
self.saveMetadata();
|
||||
self.emit('initialized');
|
||||
});
|
||||
} else {
|
||||
metadata.tip = metadata.tip;
|
||||
self.node.modules.db.getBlock(metadata.tip, function getBlockCallback(err, tip) {
|
||||
if(err) {
|
||||
return self.emit('error', err);
|
||||
}
|
||||
|
||||
self.tip = tip;
|
||||
self.tip.__height = metadata.tipHeight;
|
||||
self.tip.__weight = new BN(metadata.tipWeight, 'hex');
|
||||
self.cache = metadata.cache;
|
||||
self.emit('initialized');
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Chain.prototype.stop = function(callback) {
|
||||
setImmediate(callback);
|
||||
};
|
||||
|
||||
Chain.prototype._validateBlock = function(block, callback) {
|
||||
// All validation is done by bitcoind
|
||||
setImmediate(callback);
|
||||
};
|
||||
|
||||
Chain.prototype.startBuilder = function() {
|
||||
// Unused in bitcoind.js
|
||||
};
|
||||
|
||||
Chain.prototype.getWeight = function getWeight(blockHash, callback) {
|
||||
var self = this;
|
||||
var blockIndex = self.node.modules.bitcoind.getBlockIndex(blockHash);
|
||||
|
||||
setImmediate(function() {
|
||||
if (blockIndex) {
|
||||
callback(null, new BN(blockIndex.chainWork, 'hex'));
|
||||
} else {
|
||||
return callback(new Error('Weight not found for ' + blockHash));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Will get an array of hashes all the way to the genesis block for
|
||||
* the chain based on "block hash" as the tip.
|
||||
*
|
||||
* @param {String} block hash - a block hash
|
||||
* @param {Function} callback - A function that accepts: Error and Array of hashes
|
||||
*/
|
||||
Chain.prototype.getHashes = function getHashes(tipHash, callback) {
|
||||
var self = this;
|
||||
|
||||
$.checkArgument(utils.isHash(tipHash));
|
||||
|
||||
var hashes = [];
|
||||
var depth = 0;
|
||||
|
||||
getHashAndContinue(null, tipHash);
|
||||
|
||||
function getHashAndContinue(err, hash) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
depth++;
|
||||
|
||||
hashes.unshift(hash);
|
||||
|
||||
if (hash === self.genesis.hash) {
|
||||
// Stop at the genesis block
|
||||
self.cache.chainHashes[tipHash] = hashes;
|
||||
callback(null, hashes);
|
||||
} else if(self.cache.chainHashes[hash]) {
|
||||
hashes.shift();
|
||||
hashes = self.cache.chainHashes[hash].concat(hashes);
|
||||
delete self.cache.chainHashes[hash];
|
||||
self.cache.chainHashes[tipHash] = hashes;
|
||||
callback(null, hashes);
|
||||
} else {
|
||||
// Continue with the previous hash
|
||||
// check cache first
|
||||
var prevHash = self.cache.hashes[hash];
|
||||
if(prevHash) {
|
||||
// Don't let the stack get too deep. Otherwise we will crash.
|
||||
if(depth >= MAX_STACK_DEPTH) {
|
||||
depth = 0;
|
||||
return setImmediate(function() {
|
||||
getHashAndContinue(null, prevHash);
|
||||
});
|
||||
} else {
|
||||
return getHashAndContinue(null, prevHash);
|
||||
}
|
||||
} else {
|
||||
// do a db call if we don't have it
|
||||
self.node.modules.db.getPrevHash(hash, function(err, prevHash) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return getHashAndContinue(null, prevHash);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Chain.prototype.saveMetadata = function saveMetadata(callback) {
|
||||
var self = this;
|
||||
|
||||
callback = callback || function() {};
|
||||
|
||||
if(self.lastSavedMetadata && Date.now() < self.lastSavedMetadata.getTime() + self.lastSavedMetadataThreshold) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
var metadata = {
|
||||
tip: self.tip ? self.tip.hash : null,
|
||||
tipHeight: self.tip && self.tip.__height ? self.tip.__height : 0,
|
||||
tipWeight: self.tip && self.tip.__weight ? self.tip.__weight.toString(16) : '0',
|
||||
cache: self.cache
|
||||
};
|
||||
|
||||
self.lastSavedMetadata = new Date();
|
||||
|
||||
self.node.modules.db.putMetadata(metadata, callback);
|
||||
};
|
||||
|
||||
module.exports = Chain;
|
|
@ -347,7 +347,7 @@ AddressModule.prototype.getOutputs = function(addressStr, queryMempool, callback
|
|||
satoshis: Number(value[0]),
|
||||
script: value[1],
|
||||
blockHeight: Number(value[2]),
|
||||
confirmations: self.node.chain.tip.__height - Number(value[2]) + 1
|
||||
confirmations: self.node.modules.db.tip.__height - Number(value[2]) + 1
|
||||
};
|
||||
|
||||
outputs.push(output);
|
||||
|
@ -504,7 +504,7 @@ AddressModule.prototype.getAddressHistoryForAddress = function(address, queryMem
|
|||
|
||||
var confirmations = 0;
|
||||
if(transaction.__height >= 0) {
|
||||
confirmations = self.node.chain.tip.__height - transaction.__height;
|
||||
confirmations = self.node.modules.db.tip.__height - transaction.__height;
|
||||
}
|
||||
|
||||
txinfos[transaction.hash] = {
|
||||
|
|
|
@ -7,6 +7,7 @@ var levelup = require('levelup');
|
|||
var leveldown = require('leveldown');
|
||||
var mkdirp = require('mkdirp');
|
||||
var bitcore = require('bitcore');
|
||||
var BufferUtil = bitcore.util.buffer;
|
||||
var Networks = bitcore.Networks;
|
||||
var Block = bitcore.Block;
|
||||
var $ = bitcore.util.preconditions;
|
||||
|
@ -15,9 +16,12 @@ var errors = index.errors;
|
|||
var log = index.log;
|
||||
var Transaction = require('../transaction');
|
||||
var Module = require('../module');
|
||||
var utils = require('../utils');
|
||||
|
||||
var MAX_STACK_DEPTH = 1000;
|
||||
|
||||
/**
|
||||
* Represents the current state of the bitcoin blockchain transaction data. Other modules
|
||||
* Represents the current state of the bitcoin blockchain. Other modules
|
||||
* can extend the data that is indexed by implementing a `blockHandler` method.
|
||||
*
|
||||
* @param {Object} options
|
||||
|
@ -25,6 +29,8 @@ var Module = require('../module');
|
|||
* @param {Node} options.node - A reference to the node
|
||||
*/
|
||||
function DB(options) {
|
||||
/* jshint maxstatements: 20 */
|
||||
|
||||
if (!(this instanceof DB)) {
|
||||
return new DB(options);
|
||||
}
|
||||
|
@ -34,11 +40,21 @@ function DB(options) {
|
|||
|
||||
Module.call(this, options);
|
||||
|
||||
this.tip = null;
|
||||
this.genesis = null;
|
||||
|
||||
$.checkState(this.node.network, 'Node is expected to have a "network" property');
|
||||
this.network = this.node.network;
|
||||
|
||||
this._setDataPath();
|
||||
|
||||
this.cache = {
|
||||
hashes: {}, // dictionary of hash -> prevHash
|
||||
chainHashes: {}
|
||||
};
|
||||
this.lastSavedMetadata = null;
|
||||
this.lastSavedMetadataThreshold = 0; // Set this during syncing for faster performance
|
||||
|
||||
this.levelupStore = leveldown;
|
||||
if (options.store) {
|
||||
this.levelupStore = options.store;
|
||||
|
@ -69,14 +85,65 @@ DB.prototype._setDataPath = function() {
|
|||
};
|
||||
|
||||
DB.prototype.start = function(callback) {
|
||||
var self = this;
|
||||
if (!fs.existsSync(this.dataPath)) {
|
||||
mkdirp.sync(this.dataPath);
|
||||
}
|
||||
|
||||
this.genesis = Block.fromBuffer(this.node.modules.bitcoind.genesisBuffer);
|
||||
this.store = levelup(this.dataPath, { db: this.levelupStore });
|
||||
this.node.modules.bitcoind.on('tx', this.transactionHandler.bind(this));
|
||||
this.emit('ready');
|
||||
log.info('Bitcoin Database Ready');
|
||||
setImmediate(callback);
|
||||
|
||||
this.once('ready', function() {
|
||||
log.info('Bitcoin Database Ready');
|
||||
|
||||
// Notify that there is a new tip
|
||||
self.node.modules.bitcoind.on('tip', function(height) {
|
||||
if(!self.node.stopping) {
|
||||
var percentage = self.node.modules.bitcoind.syncPercentage();
|
||||
log.info('Bitcoin Core Daemon New Height:', height, 'Percentage:', percentage);
|
||||
self.sync();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Does our database already have a tip?
|
||||
self.getMetadata(function(err, metadata) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
} else if(!metadata || !metadata.tip) {
|
||||
self.tip = self.genesis;
|
||||
self.tip.__height = 0;
|
||||
self.connectBlock(self.genesis, function(err) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
self.emit('addblock', self.genesis);
|
||||
self.saveMetadata();
|
||||
self.sync();
|
||||
self.emit('ready');
|
||||
setImmediate(callback);
|
||||
|
||||
});
|
||||
} else {
|
||||
metadata.tip = metadata.tip;
|
||||
self.getBlock(metadata.tip, function(err, tip) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
self.tip = tip;
|
||||
self.tip.__height = metadata.tipHeight;
|
||||
self.cache = metadata.cache;
|
||||
self.sync();
|
||||
self.emit('ready');
|
||||
setImmediate(callback);
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
DB.prototype.stop = function(callback) {
|
||||
|
@ -92,6 +159,14 @@ DB.prototype.getInfo = function(callback) {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Closes the underlying store database
|
||||
* @param {Function} callback - A function that accepts: Error
|
||||
*/
|
||||
DB.prototype.close = function(callback) {
|
||||
this.store.close(callback);
|
||||
};
|
||||
|
||||
DB.prototype.transactionHandler = function(txInfo) {
|
||||
var tx = Transaction().fromBuffer(txInfo.buffer);
|
||||
for (var i = 0; i < this.subscriptions.transaction.length; i++) {
|
||||
|
@ -102,14 +177,6 @@ DB.prototype.transactionHandler = function(txInfo) {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Closes the underlying store database
|
||||
* @param {Function} callback - A function that accepts: Error
|
||||
*/
|
||||
DB.prototype.close = function(callback) {
|
||||
this.store.close(callback);
|
||||
};
|
||||
|
||||
DB.prototype.getAPIMethods = function() {
|
||||
var methods = [
|
||||
['getBlock', this, this.getBlock, 1],
|
||||
|
@ -231,6 +298,26 @@ DB.prototype.putMetadata = function(metadata, callback) {
|
|||
this.store.put('metadata', JSON.stringify(metadata), {}, callback);
|
||||
};
|
||||
|
||||
DB.prototype.saveMetadata = function(callback) {
|
||||
var self = this;
|
||||
|
||||
callback = callback || function() {};
|
||||
|
||||
if(self.lastSavedMetadata && Date.now() < self.lastSavedMetadata.getTime() + self.lastSavedMetadataThreshold) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
var metadata = {
|
||||
tip: self.tip ? self.tip.hash : null,
|
||||
tipHeight: self.tip && self.tip.__height ? self.tip.__height : 0,
|
||||
cache: self.cache
|
||||
};
|
||||
|
||||
self.lastSavedMetadata = new Date();
|
||||
|
||||
self.putMetadata(metadata, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves metadata from the database
|
||||
* @param {Function} callback - A function that accepts: Error and Object
|
||||
|
@ -317,4 +404,267 @@ DB.prototype.runAllBlockHandlers = function(block, add, callback) {
|
|||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Will get an array of hashes all the way to the genesis block for
|
||||
* the chain based on "block hash" as the tip.
|
||||
*
|
||||
* @param {String} block hash - a block hash
|
||||
* @param {Function} callback - A function that accepts: Error and Array of hashes
|
||||
*/
|
||||
DB.prototype.getHashes = function getHashes(tipHash, callback) {
|
||||
var self = this;
|
||||
|
||||
$.checkArgument(utils.isHash(tipHash));
|
||||
|
||||
var hashes = [];
|
||||
var depth = 0;
|
||||
|
||||
function getHashAndContinue(err, hash) {
|
||||
/* jshint maxstatements: 20 */
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
depth++;
|
||||
|
||||
hashes.unshift(hash);
|
||||
|
||||
if (hash === self.genesis.hash) {
|
||||
// Stop at the genesis block
|
||||
self.cache.chainHashes[tipHash] = hashes;
|
||||
callback(null, hashes);
|
||||
} else if(self.cache.chainHashes[hash]) {
|
||||
hashes.shift();
|
||||
hashes = self.cache.chainHashes[hash].concat(hashes);
|
||||
delete self.cache.chainHashes[hash];
|
||||
self.cache.chainHashes[tipHash] = hashes;
|
||||
callback(null, hashes);
|
||||
} else {
|
||||
// Continue with the previous hash
|
||||
// check cache first
|
||||
var prevHash = self.cache.hashes[hash];
|
||||
if(prevHash) {
|
||||
// Don't let the stack get too deep. Otherwise we will crash.
|
||||
if(depth >= MAX_STACK_DEPTH) {
|
||||
depth = 0;
|
||||
return setImmediate(function() {
|
||||
getHashAndContinue(null, prevHash);
|
||||
});
|
||||
} else {
|
||||
return getHashAndContinue(null, prevHash);
|
||||
}
|
||||
} else {
|
||||
// do a db call if we don't have it
|
||||
self.getPrevHash(hash, function(err, prevHash) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return getHashAndContinue(null, prevHash);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getHashAndContinue(null, tipHash);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* This function will find the common ancestor between the current chain and a forked block,
|
||||
* by moving backwards from the forked block until it meets the current chain.
|
||||
* @param {Block} block - The new tip that forks the current chain.
|
||||
* @param {Function} done - A callback function that is called when complete.
|
||||
*/
|
||||
DB.prototype.findCommonAncestor = function(block, done) {
|
||||
|
||||
var self = this;
|
||||
|
||||
// The current chain of hashes will likely already be available in a cache.
|
||||
self.getHashes(self.tip.hash, function(err, currentHashes) {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
|
||||
// Create a hash map for faster lookups
|
||||
var currentHashesMap = {};
|
||||
var length = currentHashes.length;
|
||||
for (var i = 0; i < length; i++) {
|
||||
currentHashesMap[currentHashes[i]] = true;
|
||||
}
|
||||
|
||||
// TODO: expose prevHash as a string from bitcore
|
||||
var ancestorHash = BufferUtil.reverse(block.header.prevHash).toString('hex');
|
||||
|
||||
// We only need to go back until we meet the main chain for the forked block
|
||||
// and thus don't need to find the entire chain of hashes.
|
||||
|
||||
while(ancestorHash && !currentHashesMap[ancestorHash]) {
|
||||
var blockIndex = self.node.modules.bitcoind.getBlockIndex(ancestorHash);
|
||||
ancestorHash = blockIndex ? blockIndex.prevHash : null;
|
||||
}
|
||||
|
||||
// Hash map is no-longer needed, quickly let
|
||||
// scavenging garbage collection know to cleanup
|
||||
currentHashesMap = null;
|
||||
|
||||
if (!ancestorHash) {
|
||||
return done(new Error('Unknown common ancestor.'));
|
||||
}
|
||||
|
||||
done(null, ancestorHash);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This function will attempt to rewind the chain to the common ancestor
|
||||
* between the current chain and a forked block.
|
||||
* @param {Block} block - The new tip that forks the current chain.
|
||||
* @param {Function} done - A callback function that is called when complete.
|
||||
*/
|
||||
DB.prototype.syncRewind = function(block, done) {
|
||||
|
||||
var self = this;
|
||||
|
||||
self.findCommonAncestor(block, function(err, ancestorHash) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
// Rewind the chain to the common ancestor
|
||||
async.whilst(
|
||||
function() {
|
||||
// Wait until the tip equals the ancestor hash
|
||||
return self.tip.hash !== ancestorHash;
|
||||
},
|
||||
function(removeDone) {
|
||||
|
||||
var tip = self.tip;
|
||||
|
||||
// TODO: expose prevHash as a string from bitcore
|
||||
var prevHash = BufferUtil.reverse(tip.header.prevHash).toString('hex');
|
||||
|
||||
self.getBlock(prevHash, function(err, previousTip) {
|
||||
if (err) {
|
||||
removeDone(err);
|
||||
}
|
||||
|
||||
// Undo the related indexes for this block
|
||||
self.disconnectBlock(tip, function(err) {
|
||||
if (err) {
|
||||
return removeDone(err);
|
||||
}
|
||||
|
||||
// Set the new tip
|
||||
previousTip.__height = self.tip.__height - 1;
|
||||
self.tip = previousTip;
|
||||
self.saveMetadata();
|
||||
self.emit('removeblock', tip);
|
||||
removeDone();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}, done
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This function will synchronize additional indexes for the chain based on
|
||||
* the current active chain in the bitcoin daemon. In the event that there is
|
||||
* a reorganization in the daemon, the chain will rewind to the last common
|
||||
* ancestor and then resume syncing.
|
||||
*/
|
||||
DB.prototype.sync = function() {
|
||||
var self = this;
|
||||
|
||||
if (self.bitcoindSyncing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self.tip) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.bitcoindSyncing = true;
|
||||
self.lastSavedMetadataThreshold = 30000;
|
||||
|
||||
var height;
|
||||
|
||||
async.whilst(function() {
|
||||
height = self.tip.__height;
|
||||
return height < self.node.modules.bitcoind.height && !self.node.stopping;
|
||||
}, function(done) {
|
||||
self.node.modules.bitcoind.getBlock(height + 1, function(err, blockBuffer) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var block = Block.fromBuffer(blockBuffer);
|
||||
|
||||
// TODO: expose prevHash as a string from bitcore
|
||||
var prevHash = BufferUtil.reverse(block.header.prevHash).toString('hex');
|
||||
|
||||
if (prevHash === self.tip.hash) {
|
||||
|
||||
// This block appends to the current chain tip and we can
|
||||
// immediately add it to the chain and create indexes.
|
||||
|
||||
// Populate height
|
||||
block.__height = self.tip.__height + 1;
|
||||
|
||||
// Update cache.hashes
|
||||
self.cache.hashes[block.hash] = prevHash;
|
||||
|
||||
// Update cache.chainHashes
|
||||
self.getHashes(block.hash, function(err, hashes) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
// Create indexes
|
||||
self.connectBlock(block, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
self.tip = block;
|
||||
log.debug('Saving metadata');
|
||||
self.saveMetadata();
|
||||
log.debug('Chain added block to main chain');
|
||||
self.emit('addblock', block);
|
||||
setImmediate(done);
|
||||
});
|
||||
});
|
||||
|
||||
} else {
|
||||
// This block doesn't progress the current tip, so we'll attempt
|
||||
// to rewind the chain to the common ancestor of the block and
|
||||
// then we can resume syncing.
|
||||
self.syncRewind(block, done);
|
||||
|
||||
}
|
||||
});
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
Error.captureStackTrace(err);
|
||||
return self.node.emit('error', err);
|
||||
}
|
||||
|
||||
if(self.node.stopping) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.bitcoindSyncing = false;
|
||||
self.lastSavedMetadataThreshold = 0;
|
||||
|
||||
// If bitcoind is completely synced
|
||||
if (self.node.modules.bitcoind.isSynced()) {
|
||||
self.node.emit('synced');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
module.exports = DB;
|
||||
|
|
411
lib/node.js
411
lib/node.js
|
@ -4,12 +4,8 @@ var util = require('util');
|
|||
var EventEmitter = require('events').EventEmitter;
|
||||
var async = require('async');
|
||||
var bitcore = require('bitcore');
|
||||
var BufferUtil = bitcore.util.buffer;
|
||||
var Networks = bitcore.Networks;
|
||||
var _ = bitcore.deps._;
|
||||
var $ = bitcore.util.preconditions;
|
||||
var Block = bitcore.Block;
|
||||
var Chain = require('./chain');
|
||||
var index = require('./');
|
||||
var log = index.log;
|
||||
var Bus = require('./bus');
|
||||
|
@ -20,9 +16,9 @@ function Node(config) {
|
|||
return new Node(config);
|
||||
}
|
||||
|
||||
this.chain = null;
|
||||
this.network = null;
|
||||
var self = this;
|
||||
|
||||
this.network = null;
|
||||
this.modules = {};
|
||||
this._unloadedModules = [];
|
||||
|
||||
|
@ -35,45 +31,47 @@ function Node(config) {
|
|||
$.checkState(config.datadir, 'Node config expects "datadir"');
|
||||
this.datadir = config.datadir;
|
||||
|
||||
this._loadConfiguration(config);
|
||||
this._initialize();
|
||||
this._setNetwork(config);
|
||||
|
||||
this.start(function(err) {
|
||||
if(err) {
|
||||
return self.emit('error', err);
|
||||
}
|
||||
self.emit('ready');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
util.inherits(Node, EventEmitter);
|
||||
|
||||
Node.prototype.openBus = function() {
|
||||
return new Bus({node: this});
|
||||
util.inherits(Node, EventEmitter);
|
||||
|
||||
Node.prototype._setNetwork = function(config) {
|
||||
if (config.network === 'testnet') {
|
||||
this.network = Networks.get('testnet');
|
||||
} else if (config.network === 'regtest') {
|
||||
Networks.remove(Networks.testnet);
|
||||
Networks.add({
|
||||
name: 'regtest',
|
||||
alias: 'regtest',
|
||||
pubkeyhash: 0x6f,
|
||||
privatekey: 0xef,
|
||||
scripthash: 0xc4,
|
||||
xpubkey: 0x043587cf,
|
||||
xprivkey: 0x04358394,
|
||||
networkMagic: 0xfabfb5da,
|
||||
port: 18444,
|
||||
dnsSeeds: [ ]
|
||||
});
|
||||
this.network = Networks.get('regtest');
|
||||
} else {
|
||||
this.network = Networks.defaultNetwork;
|
||||
}
|
||||
$.checkState(this.network, 'Unrecognized network');
|
||||
};
|
||||
|
||||
Node.prototype.addModule = function(service) {
|
||||
var self = this;
|
||||
var mod = new service.module({
|
||||
node: this
|
||||
});
|
||||
|
||||
$.checkState(
|
||||
mod instanceof BaseModule,
|
||||
'Unexpected module instance type for module:' + service.name
|
||||
);
|
||||
|
||||
// include in loaded modules
|
||||
this.modules[service.name] = mod;
|
||||
|
||||
// add API methods
|
||||
var methodData = mod.getAPIMethods();
|
||||
methodData.forEach(function(data) {
|
||||
var name = data[0];
|
||||
var instance = data[1];
|
||||
var method = data[2];
|
||||
|
||||
if (self[name]) {
|
||||
throw new Error('Existing API method exists:' + name);
|
||||
} else {
|
||||
self[name] = function() {
|
||||
return method.apply(instance, arguments);
|
||||
};
|
||||
}
|
||||
});
|
||||
Node.prototype.openBus = function() {
|
||||
return new Bus({db: this.modules.db});
|
||||
};
|
||||
|
||||
Node.prototype.getAllAPIMethods = function() {
|
||||
|
@ -94,293 +92,9 @@ Node.prototype.getAllPublishEvents = function() {
|
|||
return events;
|
||||
};
|
||||
|
||||
Node.prototype._loadConfiguration = function(config) {
|
||||
this._loadNetwork(config);
|
||||
this._loadConsensus(config);
|
||||
};
|
||||
|
||||
/**
|
||||
* This function will find the common ancestor between the current chain and a forked block,
|
||||
* by moving backwards from the forked block until it meets the current chain.
|
||||
* @param {Block} block - The new tip that forks the current chain.
|
||||
* @param {Function} done - A callback function that is called when complete.
|
||||
*/
|
||||
Node.prototype._syncBitcoindAncestor = function(block, done) {
|
||||
|
||||
var self = this;
|
||||
|
||||
// The current chain of hashes will likely already be available in a cache.
|
||||
self.chain.getHashes(self.chain.tip.hash, function(err, currentHashes) {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
|
||||
// Create a hash map for faster lookups
|
||||
var currentHashesMap = {};
|
||||
var length = currentHashes.length;
|
||||
for (var i = 0; i < length; i++) {
|
||||
currentHashesMap[currentHashes[i]] = true;
|
||||
}
|
||||
|
||||
// TODO: expose prevHash as a string from bitcore
|
||||
var ancestorHash = BufferUtil.reverse(block.header.prevHash).toString('hex');
|
||||
|
||||
// We only need to go back until we meet the main chain for the forked block
|
||||
// and thus don't need to find the entire chain of hashes.
|
||||
|
||||
while(ancestorHash && !currentHashesMap[ancestorHash]) {
|
||||
var blockIndex = self.modules.bitcoind.getBlockIndex(ancestorHash);
|
||||
ancestorHash = blockIndex ? blockIndex.prevHash : null;
|
||||
}
|
||||
|
||||
// Hash map is no-longer needed, quickly let
|
||||
// scavenging garbage collection know to cleanup
|
||||
currentHashesMap = null;
|
||||
|
||||
if (!ancestorHash) {
|
||||
return done(new Error('Unknown common ancestor.'));
|
||||
}
|
||||
|
||||
done(null, ancestorHash);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This function will attempt to rewind the chain to the common ancestor
|
||||
* between the current chain and a forked block.
|
||||
* @param {Block} block - The new tip that forks the current chain.
|
||||
* @param {Function} done - A callback function that is called when complete.
|
||||
*/
|
||||
Node.prototype._syncBitcoindRewind = function(block, done) {
|
||||
|
||||
var self = this;
|
||||
|
||||
self._syncBitcoindAncestor(block, function(err, ancestorHash) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
// Rewind the chain to the common ancestor
|
||||
async.whilst(
|
||||
function() {
|
||||
// Wait until the tip equals the ancestor hash
|
||||
return self.chain.tip.hash !== ancestorHash;
|
||||
},
|
||||
function(removeDone) {
|
||||
|
||||
var tip = self.chain.tip;
|
||||
|
||||
// TODO: expose prevHash as a string from bitcore
|
||||
var prevHash = BufferUtil.reverse(tip.header.prevHash).toString('hex');
|
||||
|
||||
self.getBlock(prevHash, function(err, previousTip) {
|
||||
if (err) {
|
||||
removeDone(err);
|
||||
}
|
||||
|
||||
// Undo the related indexes for this block
|
||||
self.modules.db.disconnectBlock(tip, function(err) {
|
||||
if (err) {
|
||||
return removeDone(err);
|
||||
}
|
||||
|
||||
// Set the new tip
|
||||
previousTip.__height = self.chain.tip.__height - 1;
|
||||
self.chain.tip = previousTip;
|
||||
self.chain.saveMetadata();
|
||||
self.chain.emit('removeblock', tip);
|
||||
removeDone();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}, done
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This function will synchronize additional indexes for the chain based on
|
||||
* the current active chain in the bitcoin daemon. In the event that there is
|
||||
* a reorganization in the daemon, the chain will rewind to the last common
|
||||
* ancestor and then resume syncing.
|
||||
*/
|
||||
Node.prototype._syncBitcoind = function() {
|
||||
var self = this;
|
||||
|
||||
if (self.bitcoindSyncing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self.chain.tip) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.bitcoindSyncing = true;
|
||||
self.chain.lastSavedMetadataThreshold = 30000;
|
||||
|
||||
var height;
|
||||
|
||||
async.whilst(function() {
|
||||
height = self.chain.tip.__height;
|
||||
return height < self.modules.bitcoind.height && !self.stopping;
|
||||
}, function(done) {
|
||||
self.modules.bitcoind.getBlock(height + 1, function(err, blockBuffer) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var block = Block.fromBuffer(blockBuffer);
|
||||
|
||||
// TODO: expose prevHash as a string from bitcore
|
||||
var prevHash = BufferUtil.reverse(block.header.prevHash).toString('hex');
|
||||
|
||||
if (prevHash === self.chain.tip.hash) {
|
||||
|
||||
// This block appends to the current chain tip and we can
|
||||
// immediately add it to the chain and create indexes.
|
||||
|
||||
// Populate height
|
||||
block.__height = self.chain.tip.__height + 1;
|
||||
|
||||
// Update chain.cache.hashes
|
||||
self.chain.cache.hashes[block.hash] = prevHash;
|
||||
|
||||
// Update chain.cache.chainHashes
|
||||
self.chain.getHashes(block.hash, function(err, hashes) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
// Create indexes
|
||||
self.modules.db.connectBlock(block, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
self.chain.tip = block;
|
||||
log.debug('Saving metadata');
|
||||
self.chain.saveMetadata();
|
||||
log.debug('Chain added block to main chain');
|
||||
self.chain.emit('addblock', block);
|
||||
setImmediate(done);
|
||||
});
|
||||
});
|
||||
|
||||
} else {
|
||||
// This block doesn't progress the current tip, so we'll attempt
|
||||
// to rewind the chain to the common ancestor of the block and
|
||||
// then we can resume syncing.
|
||||
self._syncBitcoindRewind(block, done);
|
||||
|
||||
}
|
||||
});
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
Error.captureStackTrace(err);
|
||||
return self.emit('error', err);
|
||||
}
|
||||
|
||||
if(self.stopping) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.bitcoindSyncing = false;
|
||||
self.chain.lastSavedMetadataThreshold = 0;
|
||||
|
||||
// If bitcoind is completely synced
|
||||
if (self.modules.bitcoind.isSynced()) {
|
||||
self.emit('synced');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
Node.prototype._loadNetwork = function(config) {
|
||||
if (config.network === 'testnet') {
|
||||
this.network = Networks.get('testnet');
|
||||
} else if (config.network === 'regtest') {
|
||||
Networks.remove(Networks.testnet);
|
||||
Networks.add({
|
||||
name: 'regtest',
|
||||
alias: 'regtest',
|
||||
pubkeyhash: 0x6f,
|
||||
privatekey: 0xef,
|
||||
scripthash: 0xc4,
|
||||
xpubkey: 0x043587cf,
|
||||
xprivkey: 0x04358394,
|
||||
networkMagic: 0xfabfb5da,
|
||||
port: 18444,
|
||||
dnsSeeds: [ ]
|
||||
});
|
||||
this.network = Networks.get('regtest');
|
||||
} else {
|
||||
this.network = Networks.get('livenet');
|
||||
}
|
||||
$.checkState(this.network, 'Unrecognized network');
|
||||
};
|
||||
|
||||
Node.prototype._loadConsensus = function(config) {
|
||||
var options;
|
||||
if (!config) {
|
||||
options = {};
|
||||
} else {
|
||||
options = _.clone(config.consensus || {});
|
||||
}
|
||||
options.node = this;
|
||||
this.chain = new Chain(options);
|
||||
};
|
||||
|
||||
Node.prototype._initialize = function() {
|
||||
var self = this;
|
||||
|
||||
this._initializeChain();
|
||||
|
||||
this.start(function(err) {
|
||||
if(err) {
|
||||
return self.emit('error', err);
|
||||
}
|
||||
self.emit('ready');
|
||||
});
|
||||
};
|
||||
|
||||
Node.prototype._initializeChain = function() {
|
||||
|
||||
var self = this;
|
||||
this.chain.on('ready', function() {
|
||||
log.info('Bitcoin Chain Ready');
|
||||
|
||||
// Notify that there is a new tip
|
||||
self.modules.bitcoind.on('tip', function(height) {
|
||||
if(!self.stopping) {
|
||||
var percentage = self.modules.bitcoind.syncPercentage();
|
||||
log.info('Bitcoin Core Daemon New Height:', height, 'Percentage:', percentage);
|
||||
self._syncBitcoind();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
this.chain.on('error', function(err) {
|
||||
Error.captureStackTrace(err);
|
||||
self.emit('error', err);
|
||||
});
|
||||
};
|
||||
|
||||
Node.prototype.getServices = function() {
|
||||
var services = [
|
||||
{
|
||||
name: 'chain',
|
||||
dependencies: ['db']
|
||||
}
|
||||
];
|
||||
|
||||
services = services.concat(this._unloadedModules);
|
||||
|
||||
return services;
|
||||
};
|
||||
|
||||
Node.prototype.getServiceOrder = function() {
|
||||
|
||||
var services = this.getServices();
|
||||
var services = this._unloadedModules;
|
||||
|
||||
// organize data for sorting
|
||||
var names = [];
|
||||
|
@ -418,6 +132,37 @@ Node.prototype.getServiceOrder = function() {
|
|||
return stack;
|
||||
};
|
||||
|
||||
Node.prototype._instantiateModule = function(service) {
|
||||
var self = this;
|
||||
var mod = new service.module({
|
||||
node: this
|
||||
});
|
||||
|
||||
$.checkState(
|
||||
mod instanceof BaseModule,
|
||||
'Unexpected module instance type for module:' + service.name
|
||||
);
|
||||
|
||||
// include in loaded modules
|
||||
this.modules[service.name] = mod;
|
||||
|
||||
// add API methods
|
||||
var methodData = mod.getAPIMethods();
|
||||
methodData.forEach(function(data) {
|
||||
var name = data[0];
|
||||
var instance = data[1];
|
||||
var method = data[2];
|
||||
|
||||
if (self[name]) {
|
||||
throw new Error('Existing API method exists: ' + name);
|
||||
} else {
|
||||
self[name] = function() {
|
||||
return method.apply(instance, arguments);
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Node.prototype.start = function(callback) {
|
||||
var self = this;
|
||||
var servicesOrder = this.getServiceOrder();
|
||||
|
@ -426,14 +171,12 @@ Node.prototype.start = function(callback) {
|
|||
servicesOrder,
|
||||
function(service, next) {
|
||||
log.info('Starting ' + service.name);
|
||||
|
||||
if (service.module) {
|
||||
self.addModule(service);
|
||||
self.modules[service.name].start(next);
|
||||
} else {
|
||||
// TODO: implement bitcoind, chain and db as modules
|
||||
self[service.name].start(next);
|
||||
try {
|
||||
self._instantiateModule(service);
|
||||
} catch(err) {
|
||||
return callback(err);
|
||||
}
|
||||
self.modules[service.name].start(next);
|
||||
},
|
||||
callback
|
||||
);
|
||||
|
@ -451,11 +194,7 @@ Node.prototype.stop = function(callback) {
|
|||
services,
|
||||
function(service, next) {
|
||||
log.info('Stopping ' + service.name);
|
||||
if (service.module) {
|
||||
self.modules[service.name].stop(next);
|
||||
} else {
|
||||
self[service.name].stop(next);
|
||||
}
|
||||
self.modules[service.name].stop(next);
|
||||
},
|
||||
callback
|
||||
);
|
||||
|
|
|
@ -68,8 +68,8 @@ function start(options) {
|
|||
|
||||
function logSyncStatus() {
|
||||
log.info(
|
||||
'Sync Status: Tip:', node.chain.tip.hash,
|
||||
'Height:', node.chain.tip.__height,
|
||||
'Sync Status: Tip:', node.modules.db.tip.hash,
|
||||
'Height:', node.modules.db.tip.__height,
|
||||
'Rate:', count/10, 'blocks per second'
|
||||
);
|
||||
}
|
||||
|
@ -184,15 +184,17 @@ function start(options) {
|
|||
log.error(err);
|
||||
});
|
||||
|
||||
node.chain.on('addblock', function(block) {
|
||||
count++;
|
||||
// Initialize logging if not already instantiated
|
||||
if (!interval) {
|
||||
interval = setInterval(function() {
|
||||
logSyncStatus();
|
||||
count = 0;
|
||||
}, 10000);
|
||||
}
|
||||
node.on('ready', function() {
|
||||
node.modules.db.on('addblock', function(block) {
|
||||
count++;
|
||||
// Initialize logging if not already instantiated
|
||||
if (!interval) {
|
||||
interval = setInterval(function() {
|
||||
logSyncStatus();
|
||||
count = 0;
|
||||
}, 10000);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
node.on('stopping', function() {
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
"package": "node bin/package.js",
|
||||
"upload": "node bin/upload.js",
|
||||
"start": "node bin/start.js",
|
||||
"test": "NODE_ENV=test mocha --recursive",
|
||||
"test": "NODE_ENV=test mocha -R spec --recursive",
|
||||
"coverage": "istanbul cover _mocha -- --recursive",
|
||||
"libbitcoind": "node bin/start-libbitcoind.js"
|
||||
},
|
||||
|
|
|
@ -1,249 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var chai = require('chai');
|
||||
var should = chai.should();
|
||||
var sinon = require('sinon');
|
||||
var memdown = require('memdown');
|
||||
|
||||
var index = require('../');
|
||||
var DB = index.DB;
|
||||
var Chain = index.Chain;
|
||||
var bitcore = require('bitcore');
|
||||
var BufferUtil = bitcore.util.buffer;
|
||||
var Block = bitcore.Block;
|
||||
var BN = bitcore.crypto.BN;
|
||||
|
||||
var chainData = require('./data/testnet-blocks.json');
|
||||
|
||||
describe('Bitcoin Chain', function() {
|
||||
|
||||
describe('@constructor', function() {
|
||||
|
||||
it('can create a new instance with and without `new`', function() {
|
||||
var chain = new Chain();
|
||||
chain = Chain();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#start', function() {
|
||||
it('should call the callback when base chain is initialized', function(done) {
|
||||
var chain = new Chain();
|
||||
chain.node = {};
|
||||
chain.node.modules = {};
|
||||
chain.node.modules.bitcoind = {};
|
||||
chain.node.modules.bitcoind.genesisBuffer = new Buffer('0100000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000bac8b0fa927c0ac8234287e33c5f74d38d354820e24756ad709d7038fc5f31f020e7494dffff001d03e4b6720101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0420e7494d017f062f503253482fffffffff0100f2052a010000002321021aeaf2f8638a129a3156fbe7e5ef635226b0bafd495ff03afe2c843d7e3a4b51ac00000000', 'hex');
|
||||
chain.initialize = function() {
|
||||
chain.emit('initialized');
|
||||
};
|
||||
|
||||
chain.start(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#initialize', function() {
|
||||
|
||||
it('should initialize the chain with the genesis block if no metadata is found in the db', function(done) {
|
||||
var db = {};
|
||||
db.getMetadata = sinon.stub().callsArgWith(0, null, {});
|
||||
db.putMetadata = sinon.stub().callsArg(1);
|
||||
db.getTransactionsFromBlock = sinon.stub();
|
||||
db.connectBlock = sinon.stub().callsArg(1);
|
||||
db.mempool = {
|
||||
on: sinon.spy()
|
||||
};
|
||||
var node = {
|
||||
modules: {
|
||||
db: db
|
||||
}
|
||||
};
|
||||
var chain = new Chain({node: node, genesis: {hash: 'genesis'}});
|
||||
|
||||
chain.on('ready', function() {
|
||||
should.exist(chain.tip);
|
||||
chain.tip.hash.should.equal('genesis');
|
||||
Number(chain.tip.__weight.toString(10)).should.equal(0);
|
||||
done();
|
||||
});
|
||||
chain.on('error', function(err) {
|
||||
should.not.exist(err);
|
||||
done();
|
||||
});
|
||||
|
||||
chain.initialize();
|
||||
});
|
||||
|
||||
it('should initialize the chain with the metadata from the database if it exists', function(done) {
|
||||
var db = {};
|
||||
db.getMetadata = sinon.stub().callsArgWith(0, null, {tip: 'block2', tipWeight: 2});
|
||||
db.putMetadata = sinon.stub().callsArg(1);
|
||||
db.getBlock = sinon.stub().callsArgWith(1, null, {hash: 'block2', prevHash: 'block1'});
|
||||
db.getTransactionsFromBlock = sinon.stub();
|
||||
db.mempool = {
|
||||
on: sinon.spy()
|
||||
};
|
||||
var node = {
|
||||
modules: {
|
||||
db: db
|
||||
}
|
||||
};
|
||||
var chain = new Chain({node: node, genesis: {hash: 'genesis'}});
|
||||
chain.getHeightForBlock = sinon.stub().callsArgWith(1, null, 10);
|
||||
chain.getWeight = sinon.stub().callsArgWith(1, null, new BN(50));
|
||||
chain.on('ready', function() {
|
||||
should.exist(chain.tip);
|
||||
chain.tip.hash.should.equal('block2');
|
||||
done();
|
||||
});
|
||||
chain.on('error', function(err) {
|
||||
should.not.exist(err);
|
||||
done();
|
||||
});
|
||||
chain.initialize();
|
||||
});
|
||||
|
||||
it('emit error from getMetadata', function(done) {
|
||||
var db = {
|
||||
getMetadata: function(cb) {
|
||||
cb(new Error('getMetadataError'));
|
||||
}
|
||||
};
|
||||
db.getTransactionsFromBlock = sinon.stub();
|
||||
db.mempool = {
|
||||
on: sinon.spy()
|
||||
};
|
||||
var node = {
|
||||
modules: {
|
||||
db: db
|
||||
}
|
||||
};
|
||||
var chain = new Chain({node: node, genesis: {hash: 'genesis'}});
|
||||
chain.on('error', function(error) {
|
||||
should.exist(error);
|
||||
error.message.should.equal('getMetadataError');
|
||||
done();
|
||||
});
|
||||
chain.initialize();
|
||||
});
|
||||
|
||||
it('emit error from getBlock', function(done) {
|
||||
var db = {
|
||||
getMetadata: function(cb) {
|
||||
cb(null, {tip: 'tip'});
|
||||
},
|
||||
getBlock: function(tip, cb) {
|
||||
cb(new Error('getBlockError'));
|
||||
}
|
||||
};
|
||||
db.getTransactionsFromBlock = sinon.stub();
|
||||
db.mempool = {
|
||||
on: sinon.spy()
|
||||
};
|
||||
var node = {
|
||||
modules: {
|
||||
db: db
|
||||
}
|
||||
};
|
||||
var chain = new Chain({node: node, genesis: {hash: 'genesis'}});
|
||||
chain.on('error', function(error) {
|
||||
should.exist(error);
|
||||
error.message.should.equal('getBlockError');
|
||||
done();
|
||||
});
|
||||
chain.initialize();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#stop', function() {
|
||||
it('should call the callback', function(done) {
|
||||
var chain = new Chain();
|
||||
chain.stop(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#_validateBlock', function() {
|
||||
it('should call the callback', function(done) {
|
||||
var chain = new Chain();
|
||||
chain._validateBlock('block', function(err) {
|
||||
should.not.exist(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getWeight', function() {
|
||||
var work = '000000000000000000000000000000000000000000005a7b3c42ea8b844374e9';
|
||||
var chain = new Chain();
|
||||
chain.node = {};
|
||||
chain.node.modules = {};
|
||||
chain.node.modules.db = {};
|
||||
chain.node.modules.bitcoind = {
|
||||
getBlockIndex: sinon.stub().returns({
|
||||
chainWork: work
|
||||
})
|
||||
};
|
||||
|
||||
it('should give the weight as a BN', function(done) {
|
||||
chain.getWeight('hash', function(err, weight) {
|
||||
should.not.exist(err);
|
||||
weight.toString(16, 64).should.equal(work);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should give an error if the weight is undefined', function(done) {
|
||||
chain.node.modules.bitcoind.getBlockIndex = sinon.stub().returns(undefined);
|
||||
chain.getWeight('hash2', function(err, weight) {
|
||||
should.exist(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getHashes', function() {
|
||||
|
||||
it('should get an array of chain hashes', function(done) {
|
||||
|
||||
var blocks = {};
|
||||
var genesisBlock = Block.fromBuffer(new Buffer(chainData[0], 'hex'));
|
||||
var block1 = Block.fromBuffer(new Buffer(chainData[1], 'hex'));
|
||||
var block2 = Block.fromBuffer(new Buffer(chainData[2], 'hex'));
|
||||
blocks[genesisBlock.hash] = genesisBlock;
|
||||
blocks[block1.hash] = block1;
|
||||
blocks[block2.hash] = block2;
|
||||
|
||||
var db = {};
|
||||
db.getPrevHash = function(blockHash, cb) {
|
||||
// TODO: expose prevHash as a string from bitcore
|
||||
var prevHash = BufferUtil.reverse(blocks[blockHash].header.prevHash).toString('hex');
|
||||
cb(null, prevHash);
|
||||
};
|
||||
|
||||
var node = {
|
||||
modules: {
|
||||
db: db
|
||||
}
|
||||
};
|
||||
|
||||
var chain = new Chain({
|
||||
node: node,
|
||||
genesis: genesisBlock
|
||||
});
|
||||
|
||||
chain.tip = block2;
|
||||
|
||||
delete chain.cache.hashes[block1.hash];
|
||||
|
||||
// the test
|
||||
chain.getHashes(block2.hash, function(err, hashes) {
|
||||
should.not.exist(err);
|
||||
should.exist(hashes);
|
||||
hashes.length.should.equal(3);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
|
@ -23,7 +23,7 @@ var mocknode = {
|
|||
}
|
||||
};
|
||||
|
||||
describe('AddressModule', function() {
|
||||
describe('Address Module', function() {
|
||||
|
||||
describe('#getAPIMethods', function() {
|
||||
it('should return the correct methods', function() {
|
||||
|
@ -424,13 +424,12 @@ describe('AddressModule', function() {
|
|||
describe('#getOutputs', function() {
|
||||
var am;
|
||||
var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W';
|
||||
var db = {};
|
||||
var db = {
|
||||
tip: {
|
||||
__height: 1
|
||||
}
|
||||
};
|
||||
var testnode = {
|
||||
chain: {
|
||||
tip: {
|
||||
__height: 1
|
||||
}
|
||||
},
|
||||
modules: {
|
||||
db: db,
|
||||
bitcoind: {
|
||||
|
@ -819,6 +818,9 @@ describe('AddressModule', function() {
|
|||
];
|
||||
|
||||
var db = {
|
||||
tip: {
|
||||
__height: 1
|
||||
},
|
||||
getTransactionWithBlockInfo: function(txid, queryMempool, callback) {
|
||||
var transaction = {
|
||||
populateInputs: sinon.stub().callsArg(2)
|
||||
|
@ -853,11 +855,6 @@ describe('AddressModule', function() {
|
|||
}
|
||||
};
|
||||
var testnode = {
|
||||
chain: {
|
||||
tip: {
|
||||
__height: 1
|
||||
}
|
||||
},
|
||||
modules: {
|
||||
db: db,
|
||||
bitcoind: {
|
||||
|
|
|
@ -2,13 +2,18 @@
|
|||
|
||||
var should = require('chai').should();
|
||||
var sinon = require('sinon');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var proxyquire = require('proxyquire');
|
||||
var index = require('../../');
|
||||
var DB = index.modules.DBModule;
|
||||
var blockData = require('../data/livenet-345003.json');
|
||||
var bitcore = require('bitcore');
|
||||
var Networks = bitcore.Networks;
|
||||
var Block = bitcore.Block;
|
||||
var BufferUtil = bitcore.util.buffer;
|
||||
var transactionData = require('../data/bitcoin-transactions.json');
|
||||
var chainHashes = require('../data/hashes.json');
|
||||
var chainData = require('../data/testnet-blocks.json');
|
||||
var errors = index.errors;
|
||||
var memdown = require('memdown');
|
||||
var bitcore = require('bitcore');
|
||||
|
@ -16,6 +21,14 @@ var Transaction = bitcore.Transaction;
|
|||
|
||||
describe('DB Module', function() {
|
||||
|
||||
function hexlebuf(hexString){
|
||||
return BufferUtil.reverse(new Buffer(hexString, 'hex'));
|
||||
}
|
||||
|
||||
function lebufhex(buf) {
|
||||
return BufferUtil.reverse(buf).toString('hex');
|
||||
}
|
||||
|
||||
var baseConfig = {
|
||||
node: {
|
||||
network: Networks.testnet,
|
||||
|
@ -61,7 +74,7 @@ describe('DB Module', function() {
|
|||
});
|
||||
it('should load the db with regtest', function() {
|
||||
// Switch to use regtest
|
||||
Networks.remove(Networks.testnet);
|
||||
// Networks.remove(Networks.testnet);
|
||||
Networks.add({
|
||||
name: 'regtest',
|
||||
alias: 'regtest',
|
||||
|
@ -85,36 +98,36 @@ describe('DB Module', function() {
|
|||
var db = new DB(config);
|
||||
db.dataPath.should.equal(process.env.HOME + '/.bitcoin/regtest/bitcore-node.db');
|
||||
Networks.remove(regtest);
|
||||
// Add testnet back
|
||||
Networks.add({
|
||||
name: 'testnet',
|
||||
alias: 'testnet',
|
||||
pubkeyhash: 0x6f,
|
||||
privatekey: 0xef,
|
||||
scripthash: 0xc4,
|
||||
xpubkey: 0x043587cf,
|
||||
xprivkey: 0x04358394,
|
||||
networkMagic: 0x0b110907,
|
||||
port: 18333,
|
||||
dnsSeeds: [
|
||||
'testnet-seed.bitcoin.petertodd.org',
|
||||
'testnet-seed.bluematt.me',
|
||||
'testnet-seed.alexykot.me',
|
||||
'testnet-seed.bitcoin.schildbach.de'
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#start', function() {
|
||||
var TestDB;
|
||||
var genesisBuffer;
|
||||
|
||||
before(function() {
|
||||
TestDB = proxyquire('../../lib/modules/db', {
|
||||
fs: {
|
||||
existsSync: sinon.stub().returns(true)
|
||||
},
|
||||
levelup: sinon.stub()
|
||||
});
|
||||
genesisBuffer = new Buffer('0100000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000bac8b0fa927c0ac8234287e33c5f74d38d354820e24756ad709d7038fc5f31f020e7494dffff001d03e4b6720101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0420e7494d017f062f503253482fffffffff0100f2052a010000002321021aeaf2f8638a129a3156fbe7e5ef635226b0bafd495ff03afe2c843d7e3a4b51ac00000000', 'hex');
|
||||
});
|
||||
|
||||
it('should emit ready', function(done) {
|
||||
var db = new DB(baseConfig);
|
||||
var db = new TestDB(baseConfig);
|
||||
db.node = {};
|
||||
db.node.modules = {};
|
||||
db.node.modules.bitcoind = {
|
||||
on: sinon.spy()
|
||||
on: sinon.spy(),
|
||||
genesisBuffer: genesisBuffer
|
||||
};
|
||||
db.addModule = sinon.spy();
|
||||
db._addModule = sinon.spy();
|
||||
db.getMetadata = sinon.stub().callsArg(0);
|
||||
db.connectBlock = sinon.stub().callsArg(1);
|
||||
db.saveMetadata = sinon.stub();
|
||||
db.sync = sinon.stub();
|
||||
var readyFired = false;
|
||||
db.on('ready', function() {
|
||||
readyFired = true;
|
||||
|
@ -124,6 +137,143 @@ describe('DB Module', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('genesis block if no metadata is found in the db', function(done) {
|
||||
var node = {
|
||||
network: Networks.testnet,
|
||||
datadir: 'testdir',
|
||||
modules: {
|
||||
bitcoind: {
|
||||
genesisBuffer: genesisBuffer,
|
||||
on: sinon.stub()
|
||||
}
|
||||
}
|
||||
};
|
||||
var db = new TestDB({node: node});
|
||||
db.getMetadata = sinon.stub().callsArgWith(0, null, null);
|
||||
db.connectBlock = sinon.stub().callsArg(1);
|
||||
db.saveMetadata = sinon.stub();
|
||||
db.sync = sinon.stub();
|
||||
db.start(function() {
|
||||
should.exist(db.tip);
|
||||
db.tip.hash.should.equal('00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('metadata from the database if it exists', function(done) {
|
||||
var node = {
|
||||
network: Networks.testnet,
|
||||
datadir: 'testdir',
|
||||
modules: {
|
||||
bitcoind: {
|
||||
genesisBuffer: genesisBuffer,
|
||||
on: sinon.stub()
|
||||
}
|
||||
}
|
||||
};
|
||||
var tip = Block.fromBuffer(genesisBuffer);
|
||||
var db = new TestDB({node: node});
|
||||
var tipHash = '00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206';
|
||||
db.getMetadata = sinon.stub().callsArgWith(0, null, {
|
||||
tip: tipHash,
|
||||
tipHeight: 0
|
||||
});
|
||||
db.getBlock = sinon.stub().callsArgWith(1, null, tip);
|
||||
db.saveMetadata = sinon.stub();
|
||||
db.sync = sinon.stub();
|
||||
db.start(function() {
|
||||
should.exist(db.tip);
|
||||
db.tip.hash.should.equal(tipHash);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('emit error from getMetadata', function(done) {
|
||||
var node = {
|
||||
network: Networks.testnet,
|
||||
datadir: 'testdir',
|
||||
modules: {
|
||||
bitcoind: {
|
||||
genesisBuffer: genesisBuffer,
|
||||
on: sinon.stub()
|
||||
}
|
||||
}
|
||||
};
|
||||
var db = new TestDB({node: node});
|
||||
db.getMetadata = sinon.stub().callsArgWith(0, new Error('test'));
|
||||
db.start(function(err) {
|
||||
should.exist(err);
|
||||
err.message.should.equal('test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('emit error from getBlock', function(done) {
|
||||
var node = {
|
||||
network: Networks.testnet,
|
||||
datadir: 'testdir',
|
||||
modules: {
|
||||
bitcoind: {
|
||||
genesisBuffer: genesisBuffer,
|
||||
on: sinon.stub()
|
||||
}
|
||||
}
|
||||
};
|
||||
var db = new TestDB({node: node});
|
||||
var tipHash = '00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206';
|
||||
db.getMetadata = sinon.stub().callsArgWith(0, null, {
|
||||
tip: tipHash,
|
||||
tipHeigt: 0
|
||||
});
|
||||
db.getBlock = sinon.stub().callsArgWith(1, new Error('test'));
|
||||
db.start(function(err) {
|
||||
should.exist(err);
|
||||
err.message.should.equal('test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('will call sync when there is a new tip', function(done) {
|
||||
var db = new TestDB(baseConfig);
|
||||
db.node.modules = {};
|
||||
db.node.modules.bitcoind = new EventEmitter();
|
||||
db.node.modules.bitcoind.syncPercentage = sinon.spy();
|
||||
db.node.modules.bitcoind.genesisBuffer = genesisBuffer;
|
||||
db.getMetadata = sinon.stub().callsArg(0);
|
||||
db.connectBlock = sinon.stub().callsArg(1);
|
||||
db.saveMetadata = sinon.stub();
|
||||
db.sync = sinon.stub();
|
||||
db.start(function() {
|
||||
db.sync = function() {
|
||||
db.node.modules.bitcoind.syncPercentage.callCount.should.equal(1);
|
||||
done();
|
||||
};
|
||||
db.node.modules.bitcoind.emit('tip', 10);
|
||||
});
|
||||
});
|
||||
|
||||
it('will not call sync when there is a new tip and shutting down', function(done) {
|
||||
var db = new TestDB(baseConfig);
|
||||
db.node.modules = {};
|
||||
db.node.modules.bitcoind = new EventEmitter();
|
||||
db.node.modules.bitcoind.syncPercentage = sinon.spy();
|
||||
db.node.modules.bitcoind.genesisBuffer = genesisBuffer;
|
||||
db.getMetadata = sinon.stub().callsArg(0);
|
||||
db.connectBlock = sinon.stub().callsArg(1);
|
||||
db.saveMetadata = sinon.stub();
|
||||
db.node.stopping = true;
|
||||
db.sync = sinon.stub();
|
||||
db.start(function() {
|
||||
db.sync.callCount.should.equal(1);
|
||||
db.node.modules.bitcoind.once('tip', function() {
|
||||
db.sync.callCount.should.equal(1);
|
||||
done();
|
||||
});
|
||||
db.node.modules.bitcoind.emit('tip', 10);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#stop', function() {
|
||||
|
@ -404,4 +554,237 @@ describe('DB Module', function() {
|
|||
methods.length.should.equal(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getHashes', function() {
|
||||
|
||||
it('should get an array of chain hashes', function(done) {
|
||||
|
||||
var blocks = {};
|
||||
var genesisBlock = Block.fromBuffer(new Buffer(chainData[0], 'hex'));
|
||||
var block1 = Block.fromBuffer(new Buffer(chainData[1], 'hex'));
|
||||
var block2 = Block.fromBuffer(new Buffer(chainData[2], 'hex'));
|
||||
blocks[genesisBlock.hash] = genesisBlock;
|
||||
blocks[block1.hash] = block1;
|
||||
blocks[block2.hash] = block2;
|
||||
|
||||
var db = new DB(baseConfig);
|
||||
db.genesis = genesisBlock;
|
||||
db.getPrevHash = function(blockHash, cb) {
|
||||
// TODO: expose prevHash as a string from bitcore
|
||||
var prevHash = BufferUtil.reverse(blocks[blockHash].header.prevHash).toString('hex');
|
||||
cb(null, prevHash);
|
||||
};
|
||||
|
||||
db.tip = block2;
|
||||
|
||||
// the test
|
||||
db.getHashes(block2.hash, function(err, hashes) {
|
||||
should.not.exist(err);
|
||||
should.exist(hashes);
|
||||
hashes.length.should.equal(3);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('#findCommonAncestor', function() {
|
||||
it('will find an ancestor 6 deep', function() {
|
||||
var db = new DB(baseConfig);
|
||||
db.getHashes = function(tipHash, callback) {
|
||||
callback(null, chainHashes);
|
||||
};
|
||||
db.tip = {
|
||||
hash: chainHashes[chainHashes.length]
|
||||
};
|
||||
var expectedAncestor = chainHashes[chainHashes.length - 6];
|
||||
|
||||
var forkedBlocks = {
|
||||
'd7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82': {
|
||||
header: {
|
||||
prevHash: hexlebuf('76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a')
|
||||
}
|
||||
},
|
||||
'76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a': {
|
||||
header: {
|
||||
prevHash: hexlebuf('f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c')
|
||||
}
|
||||
},
|
||||
'f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c': {
|
||||
header: {
|
||||
prevHash: hexlebuf('2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31')
|
||||
}
|
||||
},
|
||||
'2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31': {
|
||||
header: {
|
||||
prevHash: hexlebuf('adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453')
|
||||
}
|
||||
},
|
||||
'adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453': {
|
||||
header: {
|
||||
prevHash: hexlebuf('3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618')
|
||||
}
|
||||
},
|
||||
'3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618': {
|
||||
header: {
|
||||
prevHash: hexlebuf(expectedAncestor)
|
||||
}
|
||||
},
|
||||
};
|
||||
db.node.modules = {};
|
||||
db.node.modules.bitcoind = {
|
||||
getBlockIndex: function(hash) {
|
||||
var block = forkedBlocks[hash];
|
||||
return {
|
||||
prevHash: BufferUtil.reverse(block.header.prevHash).toString('hex')
|
||||
};
|
||||
}
|
||||
};
|
||||
var block = forkedBlocks['d7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82'];
|
||||
db.findCommonAncestor(block, function(err, ancestorHash) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
ancestorHash.should.equal(expectedAncestor);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#syncRewind', function() {
|
||||
it('will undo blocks 6 deep', function() {
|
||||
var db = new DB(baseConfig);
|
||||
var ancestorHash = chainHashes[chainHashes.length - 6];
|
||||
db.tip = {
|
||||
__height: 10,
|
||||
hash: chainHashes[chainHashes.length],
|
||||
header: {
|
||||
prevHash: hexlebuf(chainHashes[chainHashes.length - 1])
|
||||
}
|
||||
};
|
||||
db.saveMetadata = sinon.stub();
|
||||
db.emit = sinon.stub();
|
||||
db.getBlock = function(hash, callback) {
|
||||
setImmediate(function() {
|
||||
for(var i = chainHashes.length; i > 0; i--) {
|
||||
var block = {
|
||||
hash: chainHashes[i],
|
||||
header: {
|
||||
prevHash: hexlebuf(chainHashes[i - 1])
|
||||
}
|
||||
};
|
||||
if (chainHashes[i] === hash) {
|
||||
callback(null, block);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
db.node.modules = {};
|
||||
db.disconnectBlock = function(block, callback) {
|
||||
setImmediate(callback);
|
||||
};
|
||||
db.findCommonAncestor = function(block, callback) {
|
||||
setImmediate(function() {
|
||||
callback(null, ancestorHash);
|
||||
});
|
||||
};
|
||||
var forkedBlock = {};
|
||||
db.syncRewind(forkedBlock, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
db.tip.__height.should.equal(4);
|
||||
db.tip.hash.should.equal(ancestorHash);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#sync', function() {
|
||||
var node = new EventEmitter();
|
||||
var syncConfig = {
|
||||
node: node,
|
||||
store: memdown
|
||||
};
|
||||
syncConfig.node.network = Networks.testnet;
|
||||
syncConfig.node.datadir = 'testdir';
|
||||
it('will get and add block up to the tip height', function(done) {
|
||||
var db = new DB(syncConfig);
|
||||
var blockBuffer = new Buffer(blockData, 'hex');
|
||||
var block = Block.fromBuffer(blockBuffer);
|
||||
db.node.modules = {};
|
||||
db.node.modules.bitcoind = {
|
||||
getBlock: sinon.stub().callsArgWith(1, null, blockBuffer),
|
||||
isSynced: sinon.stub().returns(true),
|
||||
height: 1
|
||||
};
|
||||
db.tip = {
|
||||
__height: 0,
|
||||
hash: lebufhex(block.header.prevHash)
|
||||
};
|
||||
db.getHashes = sinon.stub().callsArgWith(1, null);
|
||||
db.saveMetadata = sinon.stub();
|
||||
db.emit = sinon.stub();
|
||||
db.cache = {
|
||||
hashes: {}
|
||||
};
|
||||
db.connectBlock = function(block, callback) {
|
||||
db.tip.__height += 1;
|
||||
callback();
|
||||
};
|
||||
db.node.once('synced', function() {
|
||||
done();
|
||||
});
|
||||
db.sync();
|
||||
});
|
||||
it('will exit and emit error with error from bitcoind.getBlock', function(done) {
|
||||
var db = new DB(syncConfig);
|
||||
db.node.modules = {};
|
||||
db.node.modules.bitcoind = {
|
||||
getBlock: sinon.stub().callsArgWith(1, new Error('test error')),
|
||||
height: 1
|
||||
};
|
||||
db.tip = {
|
||||
__height: 0
|
||||
};
|
||||
db.node.on('error', function(err) {
|
||||
err.message.should.equal('test error');
|
||||
done();
|
||||
});
|
||||
db.sync();
|
||||
});
|
||||
it('will stop syncing when the node is stopping', function(done) {
|
||||
var db = new DB(syncConfig);
|
||||
var blockBuffer = new Buffer(blockData, 'hex');
|
||||
var block = Block.fromBuffer(blockBuffer);
|
||||
db.node.modules = {};
|
||||
db.node.modules.bitcoind = {
|
||||
getBlock: sinon.stub().callsArgWith(1, null, blockBuffer),
|
||||
isSynced: sinon.stub().returns(true),
|
||||
height: 1
|
||||
};
|
||||
db.tip = {
|
||||
__height: 0,
|
||||
hash: block.prevHash
|
||||
};
|
||||
db.saveMetadata = sinon.stub();
|
||||
db.emit = sinon.stub();
|
||||
db.cache = {
|
||||
hashes: {}
|
||||
};
|
||||
db.connectBlock = function(block, callback) {
|
||||
db.tip.__height += 1;
|
||||
callback();
|
||||
};
|
||||
db.node.stopping = true;
|
||||
var synced = false;
|
||||
db.node.once('synced', function() {
|
||||
synced = true;
|
||||
});
|
||||
db.sync();
|
||||
setTimeout(function() {
|
||||
synced.should.equal(false);
|
||||
done();
|
||||
}, 10);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -2,16 +2,9 @@
|
|||
|
||||
var should = require('chai').should();
|
||||
var sinon = require('sinon');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var bitcore = require('bitcore');
|
||||
var Networks = bitcore.Networks;
|
||||
var BufferUtil = bitcore.util.buffer;
|
||||
var Block = bitcore.Block;
|
||||
var blockData = require('./data/livenet-345003.json');
|
||||
var proxyquire = require('proxyquire');
|
||||
var index = require('..');
|
||||
var fs = require('fs');
|
||||
var chainHashes = require('./data/hashes.json');
|
||||
var util = require('util');
|
||||
var BaseModule = require('../lib/module');
|
||||
|
||||
|
@ -23,30 +16,43 @@ describe('Bitcore Node', function() {
|
|||
|
||||
var Node;
|
||||
|
||||
function hexlebuf(hexString){
|
||||
return BufferUtil.reverse(new Buffer(hexString, 'hex'));
|
||||
}
|
||||
|
||||
function lebufhex(buf) {
|
||||
return BufferUtil.reverse(buf).toString('hex');
|
||||
}
|
||||
|
||||
before(function() {
|
||||
Node = proxyquire('../lib/node', {});
|
||||
Node.prototype._loadConfiguration = sinon.spy();
|
||||
Node.prototype._initialize = sinon.spy();
|
||||
});
|
||||
after(function() {
|
||||
var regtest = Networks.get('regtest');
|
||||
if (regtest) {
|
||||
Networks.remove(regtest);
|
||||
}
|
||||
// restore testnet
|
||||
Networks.add({
|
||||
name: 'testnet',
|
||||
alias: 'testnet',
|
||||
pubkeyhash: 0x6f,
|
||||
privatekey: 0xef,
|
||||
scripthash: 0xc4,
|
||||
xpubkey: 0x043587cf,
|
||||
xprivkey: 0x04358394,
|
||||
networkMagic: 0x0b110907,
|
||||
port: 18333,
|
||||
dnsSeeds: [
|
||||
'testnet-seed.bitcoin.petertodd.org',
|
||||
'testnet-seed.bluematt.me',
|
||||
'testnet-seed.alexykot.me',
|
||||
'testnet-seed.bitcoin.schildbach.de'
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('@constructor', function() {
|
||||
it('will set properties', function() {
|
||||
function TestModule() {}
|
||||
var TestModule;
|
||||
before(function() {
|
||||
TestModule = function TestModule() {};
|
||||
util.inherits(TestModule, BaseModule);
|
||||
TestModule.prototype.getData = function() {};
|
||||
TestModule.prototype.getAPIMethods = function() {
|
||||
return [
|
||||
['getData', this, this.getData, 1]
|
||||
];
|
||||
};
|
||||
});
|
||||
it('will set properties', function() {
|
||||
var config = {
|
||||
datadir: 'testdir',
|
||||
modules: [
|
||||
|
@ -57,14 +63,70 @@ describe('Bitcore Node', function() {
|
|||
],
|
||||
};
|
||||
var TestNode = proxyquire('../lib/node', {});
|
||||
TestNode.prototype._loadConfiguration = sinon.spy();
|
||||
TestNode.prototype._initialize = sinon.spy();
|
||||
TestNode.prototype.start = sinon.spy();
|
||||
var node = new TestNode(config);
|
||||
TestNode.prototype._loadConfiguration.callCount.should.equal(1);
|
||||
TestNode.prototype._initialize.callCount.should.equal(1);
|
||||
TestNode.prototype.start.callCount.should.equal(1);
|
||||
node._unloadedModules.length.should.equal(1);
|
||||
node._unloadedModules[0].name.should.equal('test1');
|
||||
node._unloadedModules[0].module.should.equal(TestModule);
|
||||
node.network.should.equal(Networks.defaultNetwork);
|
||||
});
|
||||
it('will set network to testnet', function() {
|
||||
var config = {
|
||||
network: 'testnet',
|
||||
datadir: 'testdir',
|
||||
modules: [
|
||||
{
|
||||
name: 'test1',
|
||||
module: TestModule
|
||||
}
|
||||
],
|
||||
};
|
||||
var TestNode = proxyquire('../lib/node', {});
|
||||
TestNode.prototype.start = sinon.spy();
|
||||
var node = new TestNode(config);
|
||||
node.network.should.equal(Networks.testnet);
|
||||
});
|
||||
it('will set network to regtest', function() {
|
||||
var config = {
|
||||
network: 'regtest',
|
||||
datadir: 'testdir',
|
||||
modules: [
|
||||
{
|
||||
name: 'test1',
|
||||
module: TestModule
|
||||
}
|
||||
],
|
||||
};
|
||||
var TestNode = proxyquire('../lib/node', {});
|
||||
TestNode.prototype.start = sinon.spy();
|
||||
var node = new TestNode(config);
|
||||
var regtest = Networks.get('regtest');
|
||||
should.exist(regtest);
|
||||
node.network.should.equal(regtest);
|
||||
});
|
||||
it('should emit error if an error occurred starting services', function(done) {
|
||||
var config = {
|
||||
datadir: 'testdir',
|
||||
modules: [
|
||||
{
|
||||
name: 'test1',
|
||||
module: TestModule
|
||||
}
|
||||
],
|
||||
};
|
||||
var TestNode = proxyquire('../lib/node', {});
|
||||
TestNode.prototype.start = function(callback) {
|
||||
setImmediate(function() {
|
||||
callback(new Error('error'));
|
||||
});
|
||||
};
|
||||
var node = new TestNode(config);
|
||||
node.once('error', function(err) {
|
||||
should.exist(err);
|
||||
err.message.should.equal('error');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -76,27 +138,6 @@ describe('Bitcore Node', function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#addModule', function() {
|
||||
it('will instantiate an instance and load api methods', function() {
|
||||
var node = new Node(baseConfig);
|
||||
function TestModule() {}
|
||||
util.inherits(TestModule, BaseModule);
|
||||
TestModule.prototype.getData = function() {};
|
||||
TestModule.prototype.getAPIMethods = function() {
|
||||
return [
|
||||
['getData', this, this.getData, 1]
|
||||
];
|
||||
};
|
||||
var service = {
|
||||
name: 'testmodule',
|
||||
module: TestModule
|
||||
};
|
||||
node.addModule(service);
|
||||
should.exist(node.modules.testmodule);
|
||||
should.exist(node.getData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getAllAPIMethods', function() {
|
||||
it('should return db methods and modules methods', function() {
|
||||
var node = new Node(baseConfig);
|
||||
|
@ -116,6 +157,7 @@ describe('Bitcore Node', function() {
|
|||
methods.should.deep.equal(['db1', 'db2', 'mda1', 'mda2', 'mdb1', 'mdb2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getAllPublishEvents', function() {
|
||||
it('should return modules publish events', function() {
|
||||
var node = new Node(baseConfig);
|
||||
|
@ -134,381 +176,28 @@ describe('Bitcore Node', function() {
|
|||
events.should.deep.equal(['db1', 'db2', 'mda1', 'mda2', 'mdb1', 'mdb2']);
|
||||
});
|
||||
});
|
||||
describe('#_loadConfiguration', function() {
|
||||
it('should call the necessary methods', function() {
|
||||
var TestNode = proxyquire('../lib/node', {});
|
||||
TestNode.prototype._initialize = sinon.spy();
|
||||
TestNode.prototype._loadConsensus = sinon.spy();
|
||||
var node = new TestNode(baseConfig);
|
||||
node._loadConsensus.callCount.should.equal(1);
|
||||
});
|
||||
});
|
||||
describe('#_syncBitcoindAncestor', function() {
|
||||
it('will find an ancestor 6 deep', function() {
|
||||
var node = new Node(baseConfig);
|
||||
node.chain = {
|
||||
getHashes: function(tipHash, callback) {
|
||||
callback(null, chainHashes);
|
||||
},
|
||||
tip: {
|
||||
hash: chainHashes[chainHashes.length]
|
||||
}
|
||||
};
|
||||
var expectedAncestor = chainHashes[chainHashes.length - 6];
|
||||
|
||||
var forkedBlocks = {
|
||||
'd7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82': {
|
||||
header: {
|
||||
prevHash: hexlebuf('76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a')
|
||||
}
|
||||
},
|
||||
'76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a': {
|
||||
header: {
|
||||
prevHash: hexlebuf('f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c')
|
||||
}
|
||||
},
|
||||
'f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c': {
|
||||
header: {
|
||||
prevHash: hexlebuf('2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31')
|
||||
}
|
||||
},
|
||||
'2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31': {
|
||||
header: {
|
||||
prevHash: hexlebuf('adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453')
|
||||
}
|
||||
},
|
||||
'adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453': {
|
||||
header: {
|
||||
prevHash: hexlebuf('3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618')
|
||||
}
|
||||
},
|
||||
'3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618': {
|
||||
header: {
|
||||
prevHash: hexlebuf(expectedAncestor)
|
||||
}
|
||||
},
|
||||
};
|
||||
node.modules = {};
|
||||
node.modules.bitcoind = {
|
||||
getBlockIndex: function(hash) {
|
||||
var block = forkedBlocks[hash];
|
||||
return {
|
||||
prevHash: BufferUtil.reverse(block.header.prevHash).toString('hex')
|
||||
};
|
||||
}
|
||||
};
|
||||
var block = forkedBlocks['d7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82'];
|
||||
node._syncBitcoindAncestor(block, function(err, ancestorHash) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
ancestorHash.should.equal(expectedAncestor);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#_syncBitcoindRewind', function() {
|
||||
it('will undo blocks 6 deep', function() {
|
||||
var node = new Node(baseConfig);
|
||||
var ancestorHash = chainHashes[chainHashes.length - 6];
|
||||
node.chain = {
|
||||
tip: {
|
||||
__height: 10,
|
||||
hash: chainHashes[chainHashes.length],
|
||||
header: {
|
||||
prevHash: hexlebuf(chainHashes[chainHashes.length - 1])
|
||||
}
|
||||
},
|
||||
saveMetadata: sinon.stub(),
|
||||
emit: sinon.stub()
|
||||
};
|
||||
node.getBlock = function(hash, callback) {
|
||||
setImmediate(function() {
|
||||
for(var i = chainHashes.length; i > 0; i--) {
|
||||
var block = {
|
||||
hash: chainHashes[i],
|
||||
header: {
|
||||
prevHash: hexlebuf(chainHashes[i - 1])
|
||||
}
|
||||
};
|
||||
if (chainHashes[i] === hash) {
|
||||
callback(null, block);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
node.modules = {};
|
||||
node.modules.db = {
|
||||
disconnectBlock: function(block, callback) {
|
||||
setImmediate(callback);
|
||||
}
|
||||
};
|
||||
node._syncBitcoindAncestor = function(block, callback) {
|
||||
setImmediate(function() {
|
||||
callback(null, ancestorHash);
|
||||
});
|
||||
};
|
||||
var forkedBlock = {};
|
||||
node._syncBitcoindRewind(forkedBlock, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
node.chain.tip.__height.should.equal(4);
|
||||
node.chain.tip.hash.should.equal(ancestorHash);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#_syncBitcoind', function() {
|
||||
it('will get and add block up to the tip height', function(done) {
|
||||
var node = new Node(baseConfig);
|
||||
var blockBuffer = new Buffer(blockData, 'hex');
|
||||
var block = Block.fromBuffer(blockBuffer);
|
||||
node.modules = {};
|
||||
node.modules.bitcoind = {
|
||||
getBlock: sinon.stub().callsArgWith(1, null, blockBuffer),
|
||||
isSynced: sinon.stub().returns(true),
|
||||
height: 1
|
||||
};
|
||||
node.chain = {
|
||||
tip: {
|
||||
__height: 0,
|
||||
hash: lebufhex(block.header.prevHash)
|
||||
},
|
||||
getHashes: sinon.stub().callsArgWith(1, null),
|
||||
saveMetadata: sinon.stub(),
|
||||
emit: sinon.stub(),
|
||||
cache: {
|
||||
hashes: {}
|
||||
}
|
||||
};
|
||||
node.modules.db = {
|
||||
connectBlock: function(block, callback) {
|
||||
node.chain.tip.__height += 1;
|
||||
callback();
|
||||
}
|
||||
};
|
||||
node.on('synced', function() {
|
||||
done();
|
||||
});
|
||||
node._syncBitcoind();
|
||||
});
|
||||
it('will exit and emit error with error from bitcoind.getBlock', function(done) {
|
||||
var node = new Node(baseConfig);
|
||||
node.modules = {};
|
||||
node.modules.bitcoind = {
|
||||
getBlock: sinon.stub().callsArgWith(1, new Error('test error')),
|
||||
height: 1
|
||||
};
|
||||
node.chain = {
|
||||
tip: {
|
||||
__height: 0
|
||||
}
|
||||
};
|
||||
node.on('error', function(err) {
|
||||
err.message.should.equal('test error');
|
||||
done();
|
||||
});
|
||||
node._syncBitcoind();
|
||||
});
|
||||
it('will stop syncing when the node is stopping', function(done) {
|
||||
var node = new Node(baseConfig);
|
||||
var blockBuffer = new Buffer(blockData, 'hex');
|
||||
var block = Block.fromBuffer(blockBuffer);
|
||||
node.modules = {};
|
||||
node.modules.bitcoind = {
|
||||
getBlock: sinon.stub().callsArgWith(1, null, blockBuffer),
|
||||
isSynced: sinon.stub().returns(true),
|
||||
height: 1
|
||||
};
|
||||
node.chain = {
|
||||
tip: {
|
||||
__height: 0,
|
||||
hash: block.prevHash
|
||||
},
|
||||
saveMetadata: sinon.stub(),
|
||||
emit: sinon.stub(),
|
||||
cache: {
|
||||
hashes: {}
|
||||
}
|
||||
};
|
||||
node.modules.db = {
|
||||
connectBlock: function(block, callback) {
|
||||
node.chain.tip.__height += 1;
|
||||
callback();
|
||||
}
|
||||
};
|
||||
node.stopping = true;
|
||||
|
||||
var synced = false;
|
||||
|
||||
node.on('synced', function() {
|
||||
synced = true;
|
||||
});
|
||||
|
||||
node._syncBitcoind();
|
||||
|
||||
setTimeout(function() {
|
||||
synced.should.equal(false);
|
||||
done();
|
||||
}, 10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#_loadNetwork', function() {
|
||||
it('should use the testnet network if testnet is specified', function() {
|
||||
var config = {
|
||||
datadir: 'testdir',
|
||||
network: 'testnet'
|
||||
};
|
||||
var node = new Node(config);
|
||||
node._loadNetwork(config);
|
||||
node.network.name.should.equal('testnet');
|
||||
});
|
||||
it('should use the regtest network if regtest is specified', function() {
|
||||
var config = {
|
||||
datadir: 'testdir',
|
||||
network: 'regtest'
|
||||
};
|
||||
var node = new Node(config);
|
||||
node._loadNetwork(config);
|
||||
node.network.name.should.equal('regtest');
|
||||
});
|
||||
it('should use the livenet network if nothing is specified', function() {
|
||||
var config = {
|
||||
datadir: 'testdir'
|
||||
};
|
||||
var node = new Node(config);
|
||||
node._loadNetwork(config);
|
||||
node.network.name.should.equal('livenet');
|
||||
});
|
||||
});
|
||||
describe('#_loadConsensus', function() {
|
||||
var node;
|
||||
before(function() {
|
||||
node = new Node(baseConfig);
|
||||
});
|
||||
it('will set properties', function() {
|
||||
node._loadConsensus();
|
||||
should.exist(node.chain);
|
||||
});
|
||||
});
|
||||
describe('#_initialize', function() {
|
||||
var node;
|
||||
before(function() {
|
||||
var TestNode = proxyquire('../lib/node', {});
|
||||
TestNode.prototype._loadConfiguration = sinon.spy();
|
||||
TestNode.prototype._initializeChain = sinon.spy();
|
||||
|
||||
// mock the _initialize during construction
|
||||
var _initialize = TestNode.prototype._initialize;
|
||||
TestNode.prototype._initialize = sinon.spy();
|
||||
|
||||
node = new TestNode(baseConfig);
|
||||
node.chain = {
|
||||
on: sinon.spy()
|
||||
};
|
||||
node.modules = {};
|
||||
node.modules.bitcoind = {
|
||||
on: sinon.spy()
|
||||
};
|
||||
node.modules.db = {
|
||||
on: sinon.spy()
|
||||
};
|
||||
// restore the original method
|
||||
node._initialize = _initialize;
|
||||
});
|
||||
|
||||
it('should initialize', function(done) {
|
||||
node.once('ready', function() {
|
||||
done();
|
||||
});
|
||||
node.start = sinon.stub().callsArg(0);
|
||||
node._initialize();
|
||||
node._initializeChain.callCount.should.equal(1);
|
||||
});
|
||||
|
||||
it('should emit an error if an error occurred starting services', function(done) {
|
||||
node.once('error', function(err) {
|
||||
should.exist(err);
|
||||
err.message.should.equal('error');
|
||||
done();
|
||||
});
|
||||
node.start = sinon.stub().callsArgWith(0, new Error('error'));
|
||||
node._initialize();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#_initializeChain', function() {
|
||||
|
||||
it('will call sync when there is a new tip', function(done) {
|
||||
var node = new Node(baseConfig);
|
||||
node.chain = new EventEmitter();
|
||||
node.modules = {};
|
||||
node.modules.bitcoind = new EventEmitter();
|
||||
node.modules.bitcoind.syncPercentage = sinon.spy();
|
||||
node._syncBitcoind = function() {
|
||||
node.modules.bitcoind.syncPercentage.callCount.should.equal(1);
|
||||
done();
|
||||
};
|
||||
node._initializeChain();
|
||||
node.chain.emit('ready');
|
||||
node.modules.bitcoind.emit('tip', 10);
|
||||
});
|
||||
it('will not call sync when there is a new tip and shutting down', function(done) {
|
||||
var node = new Node(baseConfig);
|
||||
node.chain = new EventEmitter();
|
||||
node.modules = {};
|
||||
node.modules.bitcoind = new EventEmitter();
|
||||
node._syncBitcoind = sinon.spy();
|
||||
node.modules.bitcoind.syncPercentage = sinon.spy();
|
||||
node.stopping = true;
|
||||
node.modules.bitcoind.on('tip', function() {
|
||||
setImmediate(function() {
|
||||
node.modules.bitcoind.syncPercentage.callCount.should.equal(0);
|
||||
node._syncBitcoind.callCount.should.equal(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
node._initializeChain();
|
||||
node.chain.emit('ready');
|
||||
node.modules.bitcoind.emit('tip', 10);
|
||||
});
|
||||
it('will emit an error from the chain', function(done) {
|
||||
var node = new Node(baseConfig);
|
||||
node.chain = new EventEmitter();
|
||||
node.on('error', function(err) {
|
||||
should.exist(err);
|
||||
err.message.should.equal('test error');
|
||||
done();
|
||||
});
|
||||
node._initializeChain();
|
||||
node.chain.emit('error', new Error('test error'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getServiceOrder', function() {
|
||||
it('should return the services in the correct order', function() {
|
||||
var node = new Node(baseConfig);
|
||||
node.getServices = function() {
|
||||
return [
|
||||
{
|
||||
name: 'chain',
|
||||
dependencies: ['db']
|
||||
},
|
||||
{
|
||||
name: 'db',
|
||||
node._unloadedModules = [
|
||||
{
|
||||
name: 'chain',
|
||||
dependencies: ['db']
|
||||
},
|
||||
{
|
||||
name: 'db',
|
||||
dependencies: ['daemon', 'p2p']
|
||||
},
|
||||
{
|
||||
name:'daemon',
|
||||
dependencies: []
|
||||
},
|
||||
{
|
||||
name: 'p2p',
|
||||
dependencies: []
|
||||
}
|
||||
];
|
||||
};
|
||||
},
|
||||
{
|
||||
name:'daemon',
|
||||
dependencies: []
|
||||
},
|
||||
{
|
||||
name: 'p2p',
|
||||
dependencies: []
|
||||
}
|
||||
];
|
||||
var order = node.getServiceOrder();
|
||||
order[0].name.should.equal('daemon');
|
||||
order[1].name.should.equal('p2p');
|
||||
|
@ -517,9 +206,31 @@ describe('Bitcore Node', function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#_instantiateModule', function() {
|
||||
it('will instantiate an instance and load api methods', function() {
|
||||
var node = new Node(baseConfig);
|
||||
function TestModule() {}
|
||||
util.inherits(TestModule, BaseModule);
|
||||
TestModule.prototype.getData = function() {};
|
||||
TestModule.prototype.getAPIMethods = function() {
|
||||
return [
|
||||
['getData', this, this.getData, 1]
|
||||
];
|
||||
};
|
||||
var service = {
|
||||
name: 'testmodule',
|
||||
module: TestModule
|
||||
};
|
||||
node._instantiateModule(service);
|
||||
should.exist(node.modules.testmodule);
|
||||
should.exist(node.getData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#start', function() {
|
||||
it('will call start for each module', function(done) {
|
||||
var node = new Node(baseConfig);
|
||||
|
||||
function TestModule() {}
|
||||
util.inherits(TestModule, BaseModule);
|
||||
TestModule.prototype.start = sinon.stub().callsArg(0);
|
||||
|
@ -529,23 +240,76 @@ describe('Bitcore Node', function() {
|
|||
['getData', this, this.getData, 1]
|
||||
];
|
||||
};
|
||||
node.test2 = {};
|
||||
node.test2.start = sinon.stub().callsArg(0);
|
||||
|
||||
function TestModule2() {}
|
||||
util.inherits(TestModule2, BaseModule);
|
||||
TestModule2.prototype.start = sinon.stub().callsArg(0);
|
||||
TestModule2.prototype.getData2 = function() {};
|
||||
TestModule2.prototype.getAPIMethods = function() {
|
||||
return [
|
||||
['getData2', this, this.getData2, 1]
|
||||
];
|
||||
};
|
||||
|
||||
node.getServiceOrder = sinon.stub().returns([
|
||||
{
|
||||
name: 'test1',
|
||||
module: TestModule
|
||||
},
|
||||
{
|
||||
name: 'test2'
|
||||
name: 'test2',
|
||||
module: TestModule2
|
||||
}
|
||||
]);
|
||||
node.start(function() {
|
||||
node.test2.start.callCount.should.equal(1);
|
||||
TestModule2.prototype.start.callCount.should.equal(1);
|
||||
TestModule.prototype.start.callCount.should.equal(1);
|
||||
should.exist(node.getData2);
|
||||
should.exist(node.getData);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('will error if there are conflicting API methods', function(done) {
|
||||
var node = new Node(baseConfig);
|
||||
|
||||
function TestModule() {}
|
||||
util.inherits(TestModule, BaseModule);
|
||||
TestModule.prototype.start = sinon.stub().callsArg(0);
|
||||
TestModule.prototype.getData = function() {};
|
||||
TestModule.prototype.getAPIMethods = function() {
|
||||
return [
|
||||
['getData', this, this.getData, 1]
|
||||
];
|
||||
};
|
||||
|
||||
function ConflictModule() {}
|
||||
util.inherits(ConflictModule, BaseModule);
|
||||
ConflictModule.prototype.start = sinon.stub().callsArg(0);
|
||||
ConflictModule.prototype.getData = function() {};
|
||||
ConflictModule.prototype.getAPIMethods = function() {
|
||||
return [
|
||||
['getData', this, this.getData, 1]
|
||||
];
|
||||
};
|
||||
|
||||
node.getServiceOrder = sinon.stub().returns([
|
||||
{
|
||||
name: 'test',
|
||||
module: TestModule
|
||||
},
|
||||
{
|
||||
name: 'conflict',
|
||||
module: ConflictModule
|
||||
}
|
||||
]);
|
||||
|
||||
node.start(function(err) {
|
||||
should.exist(err);
|
||||
err.message.should.match(/^Existing API method exists/);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('#stop', function() {
|
||||
|
@ -566,20 +330,15 @@ describe('Bitcore Node', function() {
|
|||
node.test2 = {};
|
||||
node.test2.stop = sinon.stub().callsArg(0);
|
||||
node.getServiceOrder = sinon.stub().returns([
|
||||
{
|
||||
name: 'test2'
|
||||
},
|
||||
{
|
||||
name: 'test1',
|
||||
module: TestModule
|
||||
}
|
||||
]);
|
||||
node.stop(function() {
|
||||
node.test2.stop.callCount.should.equal(1);
|
||||
TestModule.prototype.stop.callCount.should.equal(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue