Combine chain with db module.

This commit is contained in:
Braydon Fuller 2015-08-31 09:00:00 -04:00
parent df9b62acca
commit 16eef1279c
12 changed files with 1058 additions and 1325 deletions

View File

@ -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');

View File

@ -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.

View File

@ -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;

View File

@ -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] = {

View File

@ -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;

View File

@ -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
);

View File

@ -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() {

View File

@ -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"
},

View File

@ -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();
});
});
});
});

View File

@ -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: {

View File

@ -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);
});
});
});

View File

@ -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();
});
});
});
});