Merge branch 'blocks'

This commit is contained in:
Chris Kleeschulte 2017-08-18 18:30:52 -04:00
commit b89864dc4b
No known key found for this signature in database
GPG Key ID: 33195D27EF6BDB7F
98 changed files with 10976 additions and 12663 deletions

4
.gitignore vendored
View File

@ -27,3 +27,7 @@ bin/SHA256SUMS.asc
regtest/data/node1/regtest
regtest/data/node2/regtest
regtest/data/node3/regtest
bitcore-node.json*
*.bak
*.orig
lib/services/insight-api

View File

@ -22,11 +22,11 @@
"trailing": true,
"undef": true,
"unused": true,
"maxparams": 4,
"maxstatements": 15,
"maxparams": 6,
"maxstatements": 25,
"maxcomplexity": 10,
"maxdepth": 3,
"maxlen": 120,
"maxdepth": 4,
"maxlen": 140,
"multistr": true,
"predef": [
"after",
@ -39,4 +39,4 @@
"module",
"require"
]
}
}

View File

@ -1,22 +1,10 @@
dist: trusty
sudo: false
language: node_js
env:
- CXX=g++-4.8 CC=gcc-4.8
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8
- gcc-4.8
- libzmq3-dev
node_js:
- "v0.10.25"
- "v0.12.7"
- "v4"
- 8
script:
- npm run regtest
- npm run test
- npm run coverage
- npm run jshint
after_success:
- npm run coveralls
- npm run coveralls

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,169 +0,0 @@
'use strict';
var benchmark = require('benchmark');
var bitcoin = require('bitcoin');
var async = require('async');
var maxTime = 20;
console.log('Bitcoin Service native interface vs. Bitcoin JSON RPC interface');
console.log('----------------------------------------------------------------------');
// To run the benchmarks a fully synced Bitcore Core directory is needed. The RPC comands
// can be modified to match the settings in bitcoin.conf.
var fixtureData = {
blockHashes: [
'00000000fa7a4acea40e5d0591d64faf48fd862fa3561d111d967fc3a6a94177',
'000000000017e9e0afc4bc55339f60ffffb9cbe883f7348a9fbc198a486d5488',
'000000000019ddb889b534c5d85fca2c91a73feef6fd775cd228dea45353bae1',
'0000000000977ac3d9f5261efc88a3c2d25af92a91350750d00ad67744fa8d03'
],
txHashes: [
'5523b432c1bd6c101bee704ad6c560fd09aefc483f8a4998df6741feaa74e6eb',
'ff48393e7731507c789cfa9cbfae045b10e023ce34ace699a63cdad88c8b43f8',
'5d35c5eebf704877badd0a131b0a86588041997d40dbee8ccff21ca5b7e5e333',
'88842f2cf9d8659c3434f6bc0c515e22d87f33e864e504d2d7117163a572a3aa',
]
};
var bitcoind = require('../').services.Bitcoin({
node: {
datadir: process.env.HOME + '/.bitcoin',
network: {
name: 'testnet'
}
}
});
bitcoind.on('error', function(err) {
console.error(err.message);
});
bitcoind.start(function(err) {
if (err) {
throw err;
}
console.log('Bitcoin Core started');
});
bitcoind.on('ready', function() {
console.log('Bitcoin Core ready');
var client = new bitcoin.Client({
host: 'localhost',
port: 18332,
user: 'bitcoin',
pass: 'local321'
});
async.series([
function(next) {
var c = 0;
var hashesLength = fixtureData.blockHashes.length;
var txLength = fixtureData.txHashes.length;
function bitcoindGetBlockNative(deffered) {
if (c >= hashesLength) {
c = 0;
}
var hash = fixtureData.blockHashes[c];
bitcoind.getBlock(hash, function(err, block) {
if (err) {
throw err;
}
deffered.resolve();
});
c++;
}
function bitcoindGetBlockJsonRpc(deffered) {
if (c >= hashesLength) {
c = 0;
}
var hash = fixtureData.blockHashes[c];
client.getBlock(hash, false, function(err, block) {
if (err) {
throw err;
}
deffered.resolve();
});
c++;
}
function bitcoinGetTransactionNative(deffered) {
if (c >= txLength) {
c = 0;
}
var hash = fixtureData.txHashes[c];
bitcoind.getTransaction(hash, true, function(err, tx) {
if (err) {
throw err;
}
deffered.resolve();
});
c++;
}
function bitcoinGetTransactionJsonRpc(deffered) {
if (c >= txLength) {
c = 0;
}
var hash = fixtureData.txHashes[c];
client.getRawTransaction(hash, function(err, tx) {
if (err) {
throw err;
}
deffered.resolve();
});
c++;
}
var suite = new benchmark.Suite();
suite.add('bitcoind getblock (native)', bitcoindGetBlockNative, {
defer: true,
maxTime: maxTime
});
suite.add('bitcoind getblock (json rpc)', bitcoindGetBlockJsonRpc, {
defer: true,
maxTime: maxTime
});
suite.add('bitcoind gettransaction (native)', bitcoinGetTransactionNative, {
defer: true,
maxTime: maxTime
});
suite.add('bitcoind gettransaction (json rpc)', bitcoinGetTransactionJsonRpc, {
defer: true,
maxTime: maxTime
});
suite
.on('cycle', function(event) {
console.log(String(event.target));
})
.on('complete', function() {
console.log('Fastest is ' + this.filter('fastest').pluck('name'));
console.log('----------------------------------------------------------------------');
next();
})
.run();
}
], function(err) {
if (err) {
throw err;
}
console.log('Finished');
bitcoind.stop(function(err) {
if (err) {
console.error('Fail to stop services: ' + err);
process.exit(1);
}
process.exit(0);
});
});
});

22
bitcore-node.json.sample Normal file
View File

@ -0,0 +1,22 @@
{
"network": "livenet",
"port": 3001,
"datadir": "/tmp",
"services": [
"p2p",
"db",
"header",
"block",
"transaction",
"timestamp",
"mempool",
"address"
],
"servicesConfig": {
"p2p": {
"peers": [
{ "ip": { "v4": "<some trusted full node>" } }
]
}
}
}

48
contrib/printKeys.js Normal file
View File

@ -0,0 +1,48 @@
'use strict';
var levelup = require('levelup');
var leveldown = require('leveldown');
var Encoding = require('../lib/services/address/encoding');
var dbPath = '/Users/chrisk/.bwdb/bitcore-node.db';
var bitcore = require('bitcore-lib');
var db = levelup(dbPath, {keyEncoding: 'binary', valueEncoding: 'binary'});
var prefix = new Buffer('0002', 'hex');
var encoding = new Encoding(prefix);
var address = '1MfDRRVVKXUe5KNVZzu8CBzUZDHTTYZM94';
var addressLength = new Buffer(1);
addressLength.writeUInt8(address.length);
//var startBuffer = prefix;
//var endBuffer = Buffer.concat([prefix, new Buffer('ff', 'hex')]);
//var startBuffer = Buffer.concat([prefix, addressLength, new Buffer(address, 'utf8'), new Buffer('00', 'hex')]);
//var endBuffer = Buffer.concat([prefix, addressLength, new Buffer(address, 'utf8'), new Buffer('01', 'hex')]);
var start = Buffer.concat([prefix, new Buffer('0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9', 'hex')]);
var end = Buffer.concat([prefix, new Buffer('0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9', 'hex'), new Buffer('01', 'hex')]);
var stream = db.createReadStream({
gte: start,
lt: end
});
stream.on('data', function(data) {
var txkey = data.key.slice(2).toString('hex');
var height = data.value.readUInt32BE();
var timestamp = data.value.readDoubleBE(4);
var inputValues = [];
var inputValuesLength = data.value.readUInt16BE(12);
for(var i = 0; i < inputValuesLength / 8; i++) {
inputValues.push(buffer.readDoubleBE(i * 8 + 14));
}
var transaction = new bitcore.Transaction(data.value.slice(inputValues.length * 8 + 14));
transaction.__height = height;
transaction.__inputValues = inputValues;
transaction.__timestamp = timestamp;
//console.log(txkey, transaction.toObject());
console.log(data.value);
console.log(transaction.__height, transaction.__inputValues, transaction.__timestamp);
//console.log(data.key.toString('hex'), data.value.toString('hex'));
});
stream.on('end', function() {
console.log('end');
});

11
contrib/restart_bitcore_node.js Executable file
View File

@ -0,0 +1,11 @@
#!/bin/bash
# helper script to run bwdb and/or restart it
# execute thie script and then simply tail /tmp/bwdb-out
# e.g. ./contrib/restart_bwdb.sh && tail -f /tmp/bwdb-out
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
pkill -2 -x bitcore
wait
exec $DIR/../bin/bitcore-node start >> /tmp/bwdb-out 2>&1 &

149
contrib/streamtest.js Normal file
View File

@ -0,0 +1,149 @@
'use strict'
var Readable = require('stream').Readable;
var Writable = require('stream').Writable;
var Transform = require('stream').Transform;
var inherits = require('util').inherits;
var async = require('async');
function main() {
var blockStream = new BlockStream();
var processConcurrent = new ProcessConcurrent();
var processSerial = new ProcessSerial();
var writeStreamFast = new WriteStreamFast();
var writeStreamSlow = new WriteStreamSlow();
var start = Date.now();
writeStreamFast.on('finish', function() {
var end = Date.now();
console.log('Total time: ', (end - start) + ' ms');
console.log('Concurrent write time: ', writeStreamSlow.writeTime + ' ms');
console.log('Serial write time: ', writeStreamFast.writeTime + ' ms');
});
blockStream
.pipe(processConcurrent)
.pipe(writeStreamSlow);
blockStream
.pipe(processSerial)
.pipe(writeStreamFast);
}
function BlockStream() {
Readable.call(this, {objectMode: true, highWaterMark: 10});
this.height = 0;
}
inherits(BlockStream, Readable);
BlockStream.prototype._read = function() {
var self = this;
console.log('_read');
setTimeout(function() {
self.height++;
if(self.height > 40) {
self.push(null);
return;
}
console.log('ReadStream block ', self.height);
console.log(self.push({height: self.height}));
}, 500);
};
function ProcessSerial() {
Transform.call(this, {objectMode: true, highWaterMark: 10});
}
inherits(ProcessSerial, Transform);
ProcessSerial.prototype._transform = function(block, enc, callback) {
var operations = [{index1: block.height}, {index2: block.height}];
setTimeout(function() {
var obj = {
tipHeight: block.height,
operations: operations
};
callback(null, obj);
}, 100);
};
function ProcessConcurrent() {
Transform.call(this, {objectMode: true, highWaterMark: 10});
this.operations = [];
this.lastHeight = 0;
};
inherits(ProcessConcurrent, Transform);
ProcessConcurrent.prototype._transform = function(block, enc, callback) {
var self = this;
self.lastHeight = block.height;
setTimeout(function() {
self.operations = self.operations.concat([{index3: block.height}, {index4: block.height}]);
console.log(self.operations.length);
if(self.operations.length >= 10) {
var obj = {
concurrentTipHeight: self.lastHeight,
operations: self.operations
};
self.operations = [];
return callback(null, obj);
}
callback();
}, 100);
};
ProcessConcurrent.prototype._flush = function(callback) {
if(this.operations.length) {
var obj = {
concurrentTipHeight: this.lastHeight,
operations: this.operations
};
this.operations = [];
return callback(null, operations);
}
};
function WriteStreamSlow() {
Writable.call(this, {objectMode: true, highWaterMark: 10});
this.writeTime = 0;
}
inherits(WriteStreamSlow, Writable);
WriteStreamSlow.prototype._write = function(operations, enc, callback) {
var self = this;
setTimeout(function() {
console.log('WriteStreamSlow block ', operations.concurrentTipHeight);
self.writeTime += 2000;
callback();
}, 2000);
};
function WriteStreamFast() {
Writable.call(this, {objectMode: true, highWaterMark: 1});
this.writeTime = 0;
}
inherits(WriteStreamFast, Writable);
WriteStreamFast.prototype._write = function(operations, enc, callback) {
var self = this;
setTimeout(function() {
console.log('WriteStreamFast block ', operations.tipHeight);
self.writeTime += 1000;
callback();
}, 1000);
};
main();

View File

@ -0,0 +1,20 @@
[Unit]
Description=BWDB
Requires=network.target
[Service]
Type=simple
WorkingDirectory=/usr/opt/bitcore
ExecStart=/usr/bin/bwdb
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=15
User=bitcore
ExecStartPre=/bin/mkdir -p /run/bwdb
ExecStartPre=/bin/chown bitcore:bitcore /run/bwdb
ExecStartPre=/bin/chmod 755 /run/bwdb
PermissionsStartOnly=true
TimeoutStopSec=300
[Install]
WantedBy=multi-user.target

1
contrib/weirdtx.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -6,7 +6,6 @@ module.exports.Service = require('./lib/service');
module.exports.errors = require('./lib/errors');
module.exports.services = {};
module.exports.services.Bitcoin = require('./lib/services/bitcoind');
module.exports.services.Web = require('./lib/services/web');
module.exports.scaffold = {};

13
lib/constants.js Normal file
View File

@ -0,0 +1,13 @@
'use strict';
module.exports = {
BITCOIN_GENESIS_HASH: {
livenet: '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f',
regtest: '0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206',
testnet: '000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943', //this is testnet3
testnet5: '000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943' //this is testnet5
},
DB_PREFIX: new Buffer('ffff', 'hex')
};

View File

@ -38,7 +38,9 @@ Logger.prototype.error = function() {
* #debug
*/
Logger.prototype.debug = function() {
this._log.apply(this, ['magenta', 'debug'].concat(Array.prototype.slice.call(arguments)));
if (process.env.BITCORE_ENV === 'debug') {
this._log.apply(this, ['green', 'debug'].concat(Array.prototype.slice.call(arguments)));
}
};
/**
@ -63,7 +65,10 @@ Logger.prototype._log = function(color) {
var typeString = colors[color].italic(level + ':');
args[0] = '[' + date.toISOString() + ']' + ' ' + typeString + ' ' + args[0];
}
var fn = console[level] || console.log;
var fn = console.log;
if (level === 'error') {
fn = console.error;
}
fn.apply(console, args);
};

View File

@ -3,90 +3,53 @@
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var async = require('async');
var assert = require('assert');
var bitcore = require('bitcore-lib');
var Networks = bitcore.Networks;
var $ = bitcore.util.preconditions;
var _ = bitcore.deps._;
var index = require('./');
var log = index.log;
var Bus = require('./bus');
var errors = require('./errors');
/**
* A node is a hub of services, and will manage starting and stopping the services in
* the correct order based the the dependency chain. The node also holds common configuration
* properties that can be shared across services, such as network settings.
*
* The array of services should have the format:
* ```js
* {
* name: 'bitcoind',
* config: {}, // options to pass into constructor
* module: ServiceConstructor
* }
* ```
*
* @param {Object} config - The configuration of the node
* @param {Array} config.formatLogs - Option to disable formatting of logs
* @param {Array} config.services - The array of services
* @param {Number} config.port - The HTTP port for services
* @param {Boolean} config.https - Enable https
* @param {Object} config.httpsOptions - Options for https
* @param {String} config.httpsOptions.key - Path to key file
* @param {String} config.httpsOptions.cert - Path to cert file
* @param {}
*/
function Node(config) {
/* jshint maxstatements: 20 */
if(!(this instanceof Node)) {
return new Node(config);
}
this.configPath = config.path;
this.errors = errors;
this.log = log;
this._init(config);
if (!_.isUndefined(config.formatLogs)) {
this.log.formatting = config.formatLogs ? true : false;
}
this.network = null;
this.services = {};
this._unloadedServices = [];
// TODO type check the arguments of config.services
if (config.services) {
$.checkArgument(Array.isArray(config.services));
this._unloadedServices = config.services;
}
this.port = config.port;
this.https = config.https;
this.httpsOptions = config.httpsOptions;
this._setNetwork(config);
}
util.inherits(Node, EventEmitter);
/**
* Will set the this.network based on a network string.
* @param {Object} config
* @param {String} config.network - Possible options "testnet", "regtest" or "livenet"
*/
Node.prototype._setNetwork = function(config) {
if (config.network === 'testnet') {
this.network = Networks.get('testnet');
} else if (config.network === 'regtest') {
Networks.enableRegtest();
this.network = Networks.get('regtest');
} else {
this.network = Networks.defaultNetwork;
}
$.checkState(this.network, 'Unrecognized network');
Node.prototype._init = function(config) {
this.configPath = config.path;
this.errors = errors;
this.log = log;
this.datadir = config.datadir;
this.network = null;
this.services = {};
this._unloadedServices = [];
this.port = config.port;
this.https = config.https;
this.httpsOptions = config.httpsOptions;
this._setNetwork(config);
};
Node.prototype._setNetwork = function(config) {
this.network = config.network;
};
/**
* Will instantiate a new Bus for this node.
* @returns {Bus}
*/
Node.prototype.openBus = function(options) {
if (!options) {
options = {};
@ -94,10 +57,6 @@ Node.prototype.openBus = function(options) {
return new Bus({node: this, remoteAddress: options.remoteAddress});
};
/**
* Will get an array of API method descriptions from all of the available services.
* @returns {Array}
*/
Node.prototype.getAllAPIMethods = function() {
var methods = [];
for(var i in this.services) {
@ -109,10 +68,6 @@ Node.prototype.getAllAPIMethods = function() {
return methods;
};
/**
* Will get an array of events from all of the available services.
* @returns {Array}
*/
Node.prototype.getAllPublishEvents = function() {
var events = [];
for (var i in this.services) {
@ -124,16 +79,8 @@ Node.prototype.getAllPublishEvents = function() {
return events;
};
/**
* Will organize services into the order that they should be started
* based on the service's dependencies.
* @returns {Array}
*/
Node.prototype.getServiceOrder = function() {
Node.prototype._getServiceOrder = function(services) {
var services = this._unloadedServices;
// organize data for sorting
var names = [];
var servicesByName = {};
for (var i = 0; i < services.length; i++) {
@ -150,12 +97,10 @@ Node.prototype.getServiceOrder = function() {
var name = names[i];
var service = servicesByName[name];
$.checkState(service, 'Required dependency "' + name + '" not available.');
assert(service, 'Required dependency "' + name + '" not available.');
// first add the dependencies
addToStack(service.module.dependencies);
// add to the stack if it hasn't been added
if(!stackNames[name]) {
stack.push(service);
stackNames[name] = true;
@ -169,17 +114,6 @@ Node.prototype.getServiceOrder = function() {
return stack;
};
/**
* Will instantiate an instance of the service module, add it to the node
* services, start the service and add available API methods to the node and
* checking for any conflicts.
* @param {Object} serviceInfo
* @param {String} serviceInfo.name - The name of the service
* @param {Object} serviceInfo.module - The service module constructor
* @param {Object} serviceInfo.config - Options to pass into the constructor
* @param {Function} callback - Called when the service is started
* @private
*/
Node.prototype._startService = function(serviceInfo, callback) {
var self = this;
@ -187,9 +121,9 @@ Node.prototype._startService = function(serviceInfo, callback) {
var config;
if (serviceInfo.config) {
$.checkState(_.isObject(serviceInfo.config));
$.checkState(!serviceInfo.config.node);
$.checkState(!serviceInfo.config.name);
assert(_.isObject(serviceInfo.config));
assert(!serviceInfo.config.node);
assert(!serviceInfo.config.name);
config = serviceInfo.config;
} else {
config = {};
@ -199,7 +133,6 @@ Node.prototype._startService = function(serviceInfo, callback) {
config.name = serviceInfo.name;
var service = new serviceInfo.module(config);
// include in loaded services
self.services[serviceInfo.name] = service;
service.start(function(err) {
@ -207,7 +140,6 @@ Node.prototype._startService = function(serviceInfo, callback) {
return callback(err);
}
// add API methods
if (service.getAPIMethods) {
var methodData = service.getAPIMethods();
var methodNameConflicts = [];
@ -239,18 +171,16 @@ Node.prototype._startService = function(serviceInfo, callback) {
Node.prototype._logTitle = function() {
if (this.configPath) {
log.info('Using config:', this.configPath);
log.info('Using network:', this.getNetworkName());
log.info('Using network:', this.network);
}
};
/**
* Will start all running services in the order based on the dependency chain.
* @param {Function} callback - Called when all services are started
*/
Node.prototype.start = function(callback) {
var self = this;
var servicesOrder = this.getServiceOrder();
var services = this._unloadedServices;
var servicesOrder = this._getServiceOrder(services);
self._logTitle();
@ -260,9 +190,11 @@ Node.prototype.start = function(callback) {
self._startService(service, next);
},
function(err) {
if (err) {
return callback(err);
}
self.emit('ready');
callback();
}
@ -277,32 +209,32 @@ Node.prototype.getNetworkName = function() {
return network;
};
/**
* Will stop all running services in the reverse order that they
* were initially started.
* @param {Function} callback - Called when all services are stopped
*/
Node.prototype.stop = function(callback) {
log.info('Beginning shutdown');
var self = this;
var services = this.getServiceOrder().reverse();
var services = this._getServiceOrder(this._unloadedServices).reverse();
this.stopping = true;
this.emit('stopping');
async.eachSeries(
services,
function(service, next) {
if (self.services[service.name]) {
log.info('Stopping ' + service.name);
self.services[service.name].stop(next);
} else {
log.info('Stopping ' + service.name + ' (not started)');
setImmediate(next);
}
},
callback
);
services,
function(service, next) {
if (self.services[service.name]) {
log.info('Stopping ' + service.name);
self.services[service.name].stop(next);
} else {
log.info('Stopping ' + service.name + ' (not started)');
setImmediate(next);
}
},
function() {
if (callback) {
callback();
}
});
};
module.exports = Node;

View File

@ -9,7 +9,7 @@ var path = require('path');
var packageFile = require('../../package.json');
var mkdirp = require('mkdirp');
var fs = require('fs');
var defaultBaseConfig = require('./default-base-config');
var defaultConfig = require('./default-config');
var version = '^' + packageFile.version;
@ -55,7 +55,7 @@ function createConfigDirectory(options, configDir, isGlobal, done) {
if (err) {
throw err;
}
var configInfo = defaultBaseConfig(options);
var configInfo = defaultConfig(options);
var config = configInfo.config;
var configJSON = JSON.stringify(config, null, 2);

View File

@ -1,34 +0,0 @@
'use strict';
var path = require('path');
/**
* Will return the path and default bitcore-node configuration on environment variables
* or default locations.
* @param {Object} options
* @param {String} options.network - "testnet" or "livenet"
* @param {String} options.datadir - Absolute path to bitcoin database directory
*/
function getDefaultBaseConfig(options) {
if (!options) {
options = {};
}
return {
path: process.cwd(),
config: {
network: options.network || 'livenet',
port: 3001,
services: ['bitcoind', 'web'],
servicesConfig: {
bitcoind: {
spawn: {
datadir: options.datadir || path.resolve(process.env.HOME, '.bitcoin'),
exec: path.resolve(__dirname, '../../bin/bitcoind')
}
}
}
}
};
}
module.exports = getDefaultBaseConfig;

View File

@ -3,6 +3,11 @@
var path = require('path');
var mkdirp = require('mkdirp');
var fs = require('fs');
var packageJson = require('../../package');
function getMajorVersion(versionString) {
return parseInt(versionString.split('.')[0]);
}
/**
* Will return the path and default bitcore-node configuration. It will search for the
@ -24,27 +29,41 @@ function getDefaultConfig(options) {
mkdirp.sync(defaultPath);
}
var defaultServices = ['bitcoind', 'web'];
if (options.additionalServices) {
defaultServices = defaultServices.concat(options.additionalServices);
if (fs.existsSync(defaultConfigFile)) {
var currentConfig = require(defaultConfigFile);
// config must have a `version` field with major equal to package major version
if(currentConfig.version && getMajorVersion(packageJson.version) === getMajorVersion(currentConfig.version)) {
return {
path: defaultPath,
config: currentConfig
};
}
console.log(`The configuration file at '${defaultConfigFile}' is incompatible with this version of Bitcore.`);
var now = new Date();
// bitcore-node.YYYY-MM-DD.UnixTimestamp.json
var backupFileName = `bitcore-node.${now.getUTCFullYear()}-${now.getUTCMonth()}-${now.getUTCDate()}.${now.getTime()}.json`;
var backupFile = path.resolve(defaultPath, backupFileName);
fs.renameSync(defaultConfigFile, backupFile);
console.log(`The previous configuration file has been moved to: ${backupFile}.`);
}
if (!fs.existsSync(defaultConfigFile)) {
var defaultConfig = {
network: 'livenet',
port: 3001,
services: defaultServices,
servicesConfig: {
bitcoind: {
spawn: {
datadir: path.resolve(defaultPath, './data'),
exec: path.resolve(__dirname, '../../bin/bitcoind')
}
}
}
};
fs.writeFileSync(defaultConfigFile, JSON.stringify(defaultConfig, null, 2));
}
console.log(`Creating a new configuration file at: ${defaultConfigFile}.`);
var defaultServices = [
'address',
'block',
'db',
'fee',
'header',
'mempool',
'p2p',
'timestamp',
'transaction',
'web'
];
var defaultDataDir = path.resolve(defaultPath, './data');
@ -52,6 +71,23 @@ function getDefaultConfig(options) {
mkdirp.sync(defaultDataDir);
}
var defaultConfig = {
version: packageJson.version,
network: 'livenet',
port: 3001,
services: options.additionalServices ? defaultServices.concat(options.additionalServices) : defaultServices,
datadir: defaultDataDir,
servicesConfig: {
'insight-api': {
cwdRequirePath: 'node_modules/insight-api'
},
'insight-ui': {
cwdRequirePath: 'node_modules/insight-ui'
}
}
};
fs.writeFileSync(defaultConfigFile, JSON.stringify(defaultConfig, null, 2));
var config = JSON.parse(fs.readFileSync(defaultConfigFile, 'utf-8'));
return {

View File

@ -7,81 +7,21 @@ var bitcore = require('bitcore-lib');
var _ = bitcore.deps._;
var log = index.log;
var shuttingDown = false;
var fs = require('fs');
log.debug = function() {};
/**
* Checks for configuration options from version 2. This includes an "address" and
* "db" service, or having "datadir" at the root of the config.
*/
function checkConfigVersion2(fullConfig) {
var datadirUndefined = _.isUndefined(fullConfig.datadir);
var addressDefined = (fullConfig.services.indexOf('address') >= 0);
var dbDefined = (fullConfig.services.indexOf('db') >= 0);
if (!datadirUndefined || addressDefined || dbDefined) {
console.warn('\nConfiguration file is not compatible with this version. \n' +
'A reindex for bitcoind is necessary for this upgrade with the "reindex=1" bitcoin.conf option. \n' +
'There are changes necessary in both bitcoin.conf and bitcore-node.json. \n\n' +
'To upgrade please see the details below and documentation at: \n' +
'https://github.com/bitpay/bitcore-node/blob/bitcoind/docs/upgrade.md \n');
if (!datadirUndefined) {
console.warn('Please remove "datadir" and add it to the config at ' + fullConfig.path + ' with:');
var missingConfig = {
servicesConfig: {
bitcoind: {
spawn: {
datadir: fullConfig.datadir,
exec: path.resolve(__dirname, '../../bin/bitcoind')
}
}
}
};
console.warn(JSON.stringify(missingConfig, null, 2) + '\n');
}
if (addressDefined || dbDefined) {
console.warn('Please remove "address" and/or "db" from "services" in: ' + fullConfig.path + '\n');
}
return true;
}
return false;
}
/**
* This function will instantiate and start a Node, requiring the necessary service
* modules, and registering event handlers.
* @param {Object} options
* @param {Object} options.servicesPath - The path to the location of service modules
* @param {String} options.path - The absolute path of the configuration file
* @param {Object} options.config - The parsed bitcore-node.json configuration file
* @param {Array} options.config.services - An array of services names.
* @param {Object} options.config.servicesConfig - Parameters to pass to each service
* @param {String} options.config.network - 'livenet', 'testnet' or 'regtest
* @param {Number} options.config.port - The port to use for the web service
*/
function start(options) {
/* jshint maxstatements: 20 */
var fullConfig = _.clone(options.config);
var servicesPath;
if (options.servicesPath) {
servicesPath = options.servicesPath; // services are in a different directory than the config
servicesPath = options.servicesPath;
} else {
servicesPath = options.path; // defaults to the same directory
servicesPath = options.path;
}
fullConfig.path = path.resolve(options.path, './bitcore-node.json');
if (checkConfigVersion2(fullConfig)) {
process.exit(1);
}
fullConfig.services = start.setupServices(require, servicesPath, options.config);
var node = new BitcoreNode(fullConfig);
@ -116,39 +56,106 @@ function start(options) {
* @param {Object} service
*/
function checkService(service) {
// check that the service supports expected methods
if (!service.module.prototype ||
!service.module.dependencies ||
!service.module.prototype.start ||
!service.module.prototype.stop) {
throw new Error(
'Could not load service "' + service.name + '" as it does not support necessary methods and properties.'
);
'Could not load service "' +
service.name +
'" as it does not support necessary methods and properties.');
}
}
/**
* Will require a module from local services directory first
* and then from available node_modules
* @param {Function} req
* @param {Object} service
*/
function loadModule(req, service) {
try {
// first try in the built-in bitcore-node services directory
service.module = req(path.resolve(__dirname, '../services/' + service.name));
} catch(e) {
function lookInRequirePathConfig(req, service) {
if (!service.config.requirePath) {
return;
}
// check if the package.json specifies a specific file to use
try {
if (fs.statSync(service.config.requirePath).isDirectory()) {
return req(service.config.requirePath);
}
var serviceFile = service.config.requirePath.replace(/.js$/, '');
return req(serviceFile);
} catch(e) {
log.info('Checked the service\'s requirePath value, ' +
'but could not find the service, checking elsewhere. ' +
'Error caught: ' + e.message);
}
}
function lookInCwd(req, service) {
var location = service.config.cwdRequirePath ? service.config.cwdRequirePath : service.name;
try {
return req(process.cwd() + '/' + location);
} catch(e) {
if(e.code !== 'MODULE_NOT_FOUND') {
log.error(e);
}
log.info('Checked the current working directory for service: ' + location);
}
}
function lookInBuiltInPath(req, service) {
try {
var serviceFile = path.resolve(__dirname, '../services/' + service.name);
return req(serviceFile);
} catch(e) {
console.log(e);
if(e.code !== 'MODULE_NOT_FOUND') {
log.error(e);
}
log.info('Checked the built-in path: lib/services, for service: ' + service.name);
}
}
function lookInModuleManifest(req, service) {
try {
var servicePackage = req(service.name + '/package.json');
var serviceModule = service.name;
if (servicePackage.bitcoreNode) {
serviceModule = service.name + '/' + servicePackage.bitcoreNode;
serviceModule = serviceModule + '/' + servicePackage.bitcoreNode;
return req(serviceModule);
}
service.module = req(serviceModule);
} catch(e) {
log.info('Checked the module\'s package.json for service: ' + service.name);
}
}
function loadModule(req, service) {
var serviceCode;
//first, if we have explicitly set the require path for our service:
serviceCode = lookInRequirePathConfig(req, service);
//second, look in the current working directory (of the controlling terminal, if there is one) for the service code
if(!serviceCode) {
serviceCode = lookInCwd(req, service);
}
//third, try the built-in services
if(!serviceCode) {
serviceCode = lookInBuiltInPath(req, service);
}
//fourth, see if there is directory in our module search path that has a
//package.json file, if so, then see if there is a bitcoreNode field, if so
//use this as the path to the service module
if(!serviceCode) {
serviceCode = lookInModuleManifest(req, service);
}
if (!serviceCode) {
throw new Error('Attempted to load the ' + service.name + ' service from: ' +
'the requirePath in the services\' config, then "' +
process.cwd() + '" then from: "' + __dirname + '/../lib/services' + '" finally from: "' +
process.cwd() + '/package.json" - bitcoreNode field. All paths failed to find valid nodeJS code.');
}
service.module = serviceCode;
}
/**
* This function will loop over the configuration for services and require the
* specified modules, and assemble an array in this format:
@ -173,6 +180,7 @@ function setupServices(req, servicesPath, config) {
if (config.services) {
for (var i = 0; i < config.services.length; i++) {
var service = {};
service.name = config.services[i];
var hasConfig = config.servicesConfig && config.servicesConfig[service.name];
@ -187,11 +195,6 @@ function setupServices(req, servicesPath, config) {
return services;
}
/**
* Will shutdown a node and then the process
* @param {Object} _process - The Node.js process object
* @param {Node} node - The Bitcore Node instance
*/
function cleanShutdown(_process, node) {
node.stop(function(err) {
if(err) {
@ -203,15 +206,6 @@ function cleanShutdown(_process, node) {
});
}
/**
* Will handle all the shutdown tasks that need to take place to ensure a safe exit
* @param {Object} options
* @param {String} options.sigint - The signal given was a SIGINT
* @param {Array} options.exit - The signal given was an uncaughtException
* @param {Object} _process - The Node.js process
* @param {Node} node
* @param {Error} error
*/
function exitHandler(options, _process, node, err) {
if (err) {
log.error('uncaught exception:', err);
@ -233,17 +227,8 @@ function exitHandler(options, _process, node, err) {
}
}
/**
* Will register event handlers to stop the node for `process` events
* `uncaughtException` and `SIGINT`.
* @param {Object} _process - The Node.js process
* @param {Node} node
*/
function registerExitHandlers(_process, node) {
//catches uncaught exceptions
_process.on('uncaughtException', exitHandler.bind(null, {exit:true}, _process, node));
//catches ctrl+c event
_process.on('SIGINT', exitHandler.bind(null, {sigint:true}, _process, node));
}
@ -252,4 +237,3 @@ module.exports.registerExitHandlers = registerExitHandlers;
module.exports.exitHandler = exitHandler;
module.exports.setupServices = setupServices;
module.exports.cleanShutdown = cleanShutdown;
module.exports.checkConfigVersion2 = checkConfigVersion2;

View File

@ -1,7 +1,11 @@
'use strict';
/* exported LRU, assert, constants */
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var LRU = require('lru-cache');
var assert = require('assert');
var constants = require('./constants');
var Service = function(options) {
EventEmitter.call(this);

View File

@ -0,0 +1,118 @@
'use strict';
function Encoding(servicePrefix) {
this.servicePrefix = servicePrefix;
}
Encoding.prototype.encodeAddressIndexKey = function(address, height, txid, index, input, timestamp) {
var prefix = new Buffer('00', 'hex');
var buffers = [this.servicePrefix, prefix];
var addressSizeBuffer = new Buffer(1);
addressSizeBuffer.writeUInt8(address.length);
var addressBuffer = new Buffer(address, 'utf8');
buffers.push(addressSizeBuffer);
buffers.push(addressBuffer);
var heightBuffer = new Buffer(4);
heightBuffer.writeUInt32BE(height || 0);
buffers.push(heightBuffer);
var txidBuffer = new Buffer(txid || Array(65).join('0'), 'hex');
buffers.push(txidBuffer);
var indexBuffer = new Buffer(4);
indexBuffer.writeUInt32BE(index || 0);
buffers.push(indexBuffer);
// this is whether the address appears in an input (1) or output (0)
var inputBuffer = new Buffer(1);
inputBuffer.writeUInt8(input || 0);
buffers.push(inputBuffer);
var timestampBuffer = new Buffer(4);
timestampBuffer.writeUInt32BE(timestamp || 0);
buffers.push(timestampBuffer);
return Buffer.concat(buffers);
};
Encoding.prototype.decodeAddressIndexKey = function(buffer) {
var addressSize = buffer.readUInt8(3);
var address = buffer.slice(4, addressSize + 4).toString('utf8');
var height = buffer.readUInt32BE(addressSize + 4);
var txid = buffer.slice(addressSize + 8, addressSize + 40).toString('hex');
var index = buffer.readUInt32BE(addressSize + 40);
var input = buffer.readUInt8(addressSize + 44);
var timestamp = buffer.readUInt32BE(addressSize + 45);
return {
address: address,
height: height,
txid: txid,
index: index,
input: input,
timestamp: timestamp
};
};
Encoding.prototype.encodeUtxoIndexKey = function(address, txid, outputIndex) {
var prefix = new Buffer('01', 'hex');
var buffers = [this.servicePrefix, prefix];
var addressSizeBuffer = new Buffer(1);
addressSizeBuffer.writeUInt8(address.length);
var addressBuffer = new Buffer(address, 'utf8');
buffers.push(addressSizeBuffer);
buffers.push(addressBuffer);
var txidBuffer = new Buffer(txid || new Array(65).join('0'), 'hex');
buffers.push(txidBuffer);
var outputIndexBuffer = new Buffer(4);
outputIndexBuffer.writeUInt32BE(outputIndex || 0);
buffers.push(outputIndexBuffer);
return Buffer.concat(buffers);
};
Encoding.prototype.decodeUtxoIndexKey = function(buffer) {
var addressSize = buffer.readUInt8(3);
var address = buffer.slice(4, addressSize + 4).toString('utf8');
var txid = buffer.slice(addressSize + 4, addressSize + 36).toString('hex');
var outputIndex = buffer.readUInt32BE(addressSize + 36);
return {
address: address,
txid: txid,
outputIndex: outputIndex
};
};
Encoding.prototype.encodeUtxoIndexValue = function(height, satoshis, timestamp, scriptBuffer) {
var heightBuffer = new Buffer(4);
heightBuffer.writeUInt32BE(height);
var satoshisBuffer = new Buffer(8);
satoshisBuffer.writeDoubleBE(satoshis);
var timestampBuffer = new Buffer(4);
timestampBuffer.writeUInt32BE(timestamp || 0);
return Buffer.concat([heightBuffer, satoshisBuffer, timestampBuffer, scriptBuffer]);
};
Encoding.prototype.decodeUtxoIndexValue = function(buffer) {
var height = buffer.readUInt32BE();
var satoshis = buffer.readDoubleBE(4);
var timestamp = buffer.readUInt32BE(12);
var scriptBuffer = buffer.slice(16);
return {
height: height,
satoshis: satoshis,
timestamp: timestamp,
script: scriptBuffer
};
};
module.exports = Encoding;

View File

@ -0,0 +1,556 @@
'use strict';
var BaseService = require('../../service');
var inherits = require('util').inherits;
var async = require('async');
var index = require('../../');
var log = index.log;
var bitcore = require('bitcore-lib');
var Unit = bitcore.Unit;
var _ = bitcore.deps._;
var Encoding = require('./encoding');
var utils = require('../../utils');
var Transform = require('stream').Transform;
var assert = require('assert');
var AddressService = function(options) {
BaseService.call(this, options);
this._db = this.node.services.db;
this._tx = this.node.services.transaction;
this._header = this.node.services.header;
this._timestamp = this.node.services.timestamp;
this._network = this.node.network;
if (this._network === 'livenet') {
this._network = 'main';
}
};
inherits(AddressService, BaseService);
AddressService.dependencies = [
'p2p',
'db',
'block',
'transaction',
'timestamp'
];
// ---- public function prototypes
AddressService.prototype.getAddressHistory = function(addresses, options, callback) {
var self = this;
options = options || {};
options.from = options.from || 0;
options.to = options.to || 0xffffffff;
options.queryMempool = _.isUndefined(options.queryMempool) ? true : false;
async.mapLimit(addresses, 4, function(address, next) {
self._getAddressHistory(address, options, next);
}, function(err, txLists) {
if(err) {
return callback(err);
}
var txList = _.flatten(txLists);
var results = {
totalItems: txList.length,
from: options.from,
to: options.to,
items: txList
};
callback(null, results);
});
};
AddressService.prototype.getAddressSummary = function(address, options, callback) {
var self = this;
options = options || {};
options.from = options.from || 0;
options.to = options.to || 0xffffffff;
options.queryMempool = _.isUndefined(options.queryMempool) ? true : false;
var result = {
addrStr: address,
balance: 0,
balanceSat: 0,
totalReceived: 0,
totalReceivedSat: 0,
totalSent: 0,
totalSentSat: 0,
unconfirmedBalance: 0,
unconfirmedBalanceSat: 0,
unconfirmedTxApperances: 0,
txApperances: 0,
transactions: []
};
// txid criteria
var start = self._encoding.encodeAddressIndexKey(address, options.from);
var end = self._encoding.encodeAddressIndexKey(address, options.to);
var criteria = {
gte: start,
lt: end
};
// txid stream
var txidStream = self._db.createKeyStream(criteria);
txidStream.on('close', function() {
txidStream.unpipe();
});
// tx stream
var txStream = new Transform({ objectMode: true, highWaterMark: 1000 });
txStream.on('end', function() {
result.balance = Unit.fromSatoshis(result.balanceSat).toBTC();
result.totalReceived = Unit.fromSatoshis(result.totalReceivedSat).toBTC();
result.totalSent = result.totalSentSat;
result.unconfirmedBalance = result.unconfirmedBalanceSat;
callback(null, result);
});
// pipe txids into tx stream for processing
txidStream.pipe(txStream);
txStream._transform = function(chunk, enc, callback) {
var key = self._encoding.decodeAddressIndexKey(chunk);
self._tx.getTransaction(key.txid, options, function(err, tx) {
if(err) {
log.error(err);
txStream.emit('error', err);
return;
}
if (!tx) {
log.error('Could not find tx for txid: ' + key.txid + '. This should not be possible, check indexes.');
txStream.emit('error', new Error('Txid should map to a tx.'));
return;
}
var confirmations = self._header.getBestHeight() - key.height;
result.transactions.push(tx.txid());
result.txApperances++;
// is this an input?
if (key.input) {
return self._transaction.getInputValues(key.txid, null, function(err, tx) {
if(err) {
log.error(err);
txStream.emit('error', err);
return;
}
result.balanceSat -= tx.__inputValues[key.index];
result.totalSentSat += tx.__inputValues[key.index];
if (confirmations < 1) {
result.unconfirmedBalanceSat -= tx.__inputValues[key.index];
result.unconfirmedTxApperances++;
}
callback();
});
}
result.balanceSat += tx.outputs[key.index].value;
result.totalReceivedSat += tx.outputs[key.index].value;
if (confirmations < 1) {
result.unconfirmedBalanceSat += tx.__inputValues[key.index];
result.unconfirmedTxApperances++;
}
callback();
});
};
txStream.on('error', function(err) {
log.error(err);
txStream.unpipe();
});
txStream._flush = function(callback) {
txStream.emit('end');
callback();
};
};
AddressService.prototype.getAddressUnspentOutputs = function(address, options, callback) {
var self = this;
options = options || {};
options.from = options.from || 0;
options.to = options.to || 0xffffffff;
options.queryMempool = _.isUndefined(options.queryMempool) ? true : false;
var results = [];
var start = self._encoding.encodeUtxoIndexKey(address);
var final = new Buffer(new Array(73).join('f'), 'hex');
var end = Buffer.concat([ start.slice(0, -36), final ]);
var criteria = {
gte: start,
lt: end
};
var utxoStream = self._db.createReadStream(criteria);
var streamErr;
utxoStream.on('end', function() {
if (streamErr) {
return callback(streamErr);
}
callback(null, results);
});
utxoStream.on('error', function(err) {
streamErr = err;
});
utxoStream.on('data', function(data) {
var key = self._encoding.decodeUtxoIndexKey(data.key);
var value = self._encoding.decodeUtxoIndexValue(data.value);
results.push({
address: address,
txid: key.txid,
vout: key.outputIndex,
ts: value.timestamp,
scriptPubKey: value.script.toString('hex'),
amount: Unit.fromSatoshis(value.satoshis).toBTC(),
confirmations: self._header.getBestHeight() - value.height,
satoshis: value.satoshis,
confirmationsFromCache: true
});
});
};
AddressService.prototype.getAPIMethods = function() {
return [
['getAddressHistory', this, this.getAddressHistory, 2],
['getAddressSummary', this, this.getAddressSummary, 1],
['getAddressUnspentOutputs', this, this.getAddressUnspentOutputs, 1]
];
};
AddressService.prototype.start = function(callback) {
var self = this;
this._db.getPrefix(this.name, function(err, prefix) {
if(err) {
return callback(err);
}
self._encoding = new Encoding(prefix);
callback();
});
};
AddressService.prototype.stop = function(callback) {
setImmediate(callback);
};
// ---- start private function prototypes
AddressService.prototype._getAddressHistory = function(address, options, callback) {
var self = this;
options = options || {};
options.start = options.start || 0;
options.end = options.end || 0xffffffff;
var endHeightBuf = new Buffer(4);
endHeightBuf.writeUInt32BE(options.end);
if (_.isUndefined(options.queryMempool)) {
options.queryMempool = true;
}
var results = [];
var start = self._encoding.encodeAddressIndexKey(address, options.start);
var end = Buffer.concat([
start.slice(0, address.length + 4),
endHeightBuf,
new Buffer(new Array(83).join('f'), 'hex')
]);
var criteria = {
gte: start,
lte: end
};
// txid stream
var txidStream = self._db.createKeyStream(criteria);
txidStream.on('close', function() {
txidStream.unpipe();
});
// tx stream
var txStream = new Transform({ objectMode: true, highWaterMark: 1000 });
var streamErr;
txStream.on('end', function() {
if (streamErr) {
return callback(streamErr);
}
callback(null, results);
});
// pipe txids into tx stream for processing
txidStream.pipe(txStream);
txStream._transform = function(chunk, enc, callback) {
var key = self._encoding.decodeAddressIndexKey(chunk);
self._tx.getTransaction(key.txid, options, function(err, tx) {
if(err) {
log.error(err);
txStream.emit('error', err);
return callback();
}
if (!tx) {
log.error('Could not find tx for txid: ' + key.txid + '. This should not be possible, check indexes.');
txStream.emit('error', err);
return callback();
}
results.push(tx);
callback();
});
};
txStream.on('error', function(err) {
log.error(err);
txStream.unpipe();
});
txStream._flush = function(callback) {
txStream.emit('end');
callback();
};
};
AddressService.prototype.onReorg = function(args, callback) {
var self = this;
var oldBlockList = args[1];
var removalOps = [];
// for every tx, remove the address index key for every input and output
// TODO: DRY self up!
for(var i = 0; i < oldBlockList.length; i++) {
var block = oldBlockList[i];
//txs
for(var j = 0; j < block.txs.length; j++) {
var tx = block.txs[j];
//inputs
var address;
for(var k = 0; k < tx.inputs.length; k++) {
var input = tx.inputs[k];
address = input.getAddress();
if (!address) {
continue;
}
address.network = self._network;
address = address.toString();
removalOps.push({
type: 'del',
key: self._encoding.encodeAddressIndexKey(address, block.height, tx.txid(), k, 1, block.ts)
});
}
//outputs
for(k = 0; k < tx.outputs.length; k++) {
var output = tx.outputs[k];
address = output.getAddress();
if (!address) {
continue;
}
address.network = self._network;
address = address.toString();
removalOps.push({
type: 'del',
key: self._encoding.encodeAddressIndexKey(address, block.height, tx.txid(), k, 0, block.ts)
});
}
}
}
callback(null, removalOps);
};
AddressService.prototype.onBlock = function(block, callback) {
var self = this;
if (self.node.stopping) {
return callback();
}
var operations = [];
for(var i = 0; i < block.txs.length; i++) {
var tx = block.txs[i];
var ops = self._processTransaction(tx, { block: block });
operations.push(ops);
}
operations = _.flatten(operations);
callback(null, operations);
};
AddressService.prototype._processInput = function(tx, input, opts) {
var address = input.getAddress();
if(!address) {
return;
}
address.network = this._network;
address = address.toString();
var txid = tx.txid();
var timestamp = this._timestamp.getTimestampSync(opts.block.rhash());
assert(timestamp, 'Must have a timestamp in order to process input.');
// address index
var addressKey = this._encoding.encodeAddressIndexKey(address, opts.block.height, txid, opts.inputIndex, 1, timestamp);
var operations = [{
type: 'put',
key: addressKey
}];
// prev utxo
var rec = {
type: 'del',
key: this._encoding.encodeUtxoIndexKey(address, input.prevout.txid(), input.prevout.index)
};
// In the event where we are reorg'ing,
// this is where we are putting a utxo back in, we don't know what the original height, sats, or scriptBuffer
// since this only happens on reorg and the utxo that was spent in the chain we are reorg'ing away from will likely
// be spent again sometime soon, we will not add the value back in, just the key
operations.push(rec);
return operations;
};
AddressService.prototype._processOutput = function(tx, output, index, opts) {
var address = output.getAddress();
if(!address) {
return;
}
address.network = this._network;
address = address.toString();
var txid = tx.txid();
var timestamp = this._timestamp.getTimestampSync(opts.block.rhash());
assert(timestamp, 'Must have a timestamp in order to process output.');
var addressKey = this._encoding.encodeAddressIndexKey(address, opts.block.height, txid, opts.outputIndex, 0, timestamp);
var utxoKey = this._encoding.encodeUtxoIndexKey(address, txid, index);
var utxoValue = this._encoding.encodeUtxoIndexValue(
opts.block.height,
output.value,
timestamp,
output.script.toRaw()
);
var operations = [{
type: 'put',
key: addressKey
}];
operations.push({
type: 'put',
key: utxoKey,
value: utxoValue
});
return operations;
};
AddressService.prototype._processTransaction = function(tx, opts) {
var self = this;
var _opts = { block: opts.block };
var outputOperations = tx.outputs.map(function(output, index) {
return self._processOutput(tx, output, index, _opts);
});
outputOperations = _.flatten(_.compact(outputOperations));
var inputOperations = tx.inputs.map(function(input, index) {
_opts.inputIndex = index;
return self._processInput(tx, input, _opts);
});
inputOperations = _.flatten(_.compact(inputOperations));
outputOperations.concat(inputOperations);
return outputOperations;
};
module.exports = AddressService;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,375 @@
'use strict';
var Readable = require('stream').Readable;
var Writable = require('stream').Writable;
var Transform = require('stream').Transform;
var inherits = require('util').inherits;
var EventEmitter = require('events').EventEmitter;
var async = require('async');
var index = require('../../index');
var log = index.log;
var _ = require('lodash');
function BlockStream(highWaterMark, sync) {
Readable.call(this, {objectMode: true, highWaterMark: highWaterMark});
this.sync = sync;
this.block = this.sync.block;
this.dbTip = this.block.tip;
this.lastReadHeight = this._getTipHeight();
this.lastEmittedHash = this.dbTip.hash;
this.queue = [];
this.processing = false;
var self = this;
self.block.on('reorg', function() {
self.push(null);
});
}
inherits(BlockStream, Readable);
function ProcessConcurrent(highWaterMark, sync) {
Transform.call(this, {objectMode: true, highWaterMark: highWaterMark});
this.block = sync.block;
this.db = sync.db;
this.operations = [];
this.lastBlock = 0;
this.blockCount = 0;
}
inherits(ProcessConcurrent, Transform);
function ProcessSerial(highWaterMark, sync) {
Writable.call(this, {objectMode: true, highWaterMark: highWaterMark});
this.block = sync.block;
this.db = sync.db;
this.block = sync.block;
this.tip = sync.block.tip;
this.processBlockStartTime = [];
this._lastReportedTime = Date.now();
}
inherits(ProcessSerial, Writable);
function ProcessBoth(highWaterMark, sync) {
Writable.call(this, {objectMode: true, highWaterMark: highWaterMark});
this.db = sync.db;
}
inherits(ProcessBoth, Writable);
function WriteStream(highWaterMark, sync) {
Writable.call(this, {objectMode: true, highWaterMark: highWaterMark});
this.db = sync.db;
this.writeTime = 0;
this.lastConcurrentOutputHeight = 0;
this.block = sync.block;
}
inherits(WriteStream, Writable);
function BlockHandler(node, block) {
this.node = node;
this.db = this.node.services.db;
this.block = block;
this.syncing = false;
this.paused = false;
this.blockQueue = [];
this.highWaterMark = 10;
}
inherits(BlockHandler, EventEmitter);
BlockHandler.prototype.sync = function(block) {
var self = this;
if (block) {
self.blockQueue.push(block);
}
if(this.syncing || this.paused) {
log.debug('Sync lock held, not able to sync at the moment');
return;
}
self.syncing = true;
self._setupStreams();
};
BlockHandler.prototype._setupStreams = function() {
var self = this;
var blockStream = new BlockStream(self.highWaterMark, self);
var processConcurrent = new ProcessConcurrent(self.highWaterMark, self);
var writeStream = new WriteStream(self.highWaterMark, self);
var processSerial = new ProcessSerial(self.highWaterMark, self);
self._handleErrors(blockStream);
self._handleErrors(processConcurrent);
self._handleErrors(processSerial);
self._handleErrors(writeStream);
blockStream
.pipe(processConcurrent)
.pipe(writeStream);
blockStream
.pipe(processSerial);
processSerial.on('finish', self._onFinish.bind(self));
};
BlockHandler.prototype._onFinish = function() {
var self = this;
self.syncing = false;
self.emit('synced');
};
BlockHandler.prototype._handleErrors = function(stream) {
var self = this;
stream.on('error', function(err) {
self.syncing = false;
self.emit('error', err);
});
};
BlockStream.prototype._read = function() {
if (this.lastEmittedHash === this.block.getNetworkTipHash()) {
return this.push(null);
}
if (this.sync.blockQueue.length === 0) {
this.queue.push(++this.lastReadHeight);
} else {
var block = this.sync.blockQueue.shift();
if (block) {
this.lastReadHeight = block.__height;
this.queue.push(block);
}
}
this._process();
};
BlockStream.prototype._process = function() {
var self = this;
if(self.processing) {
return;
}
this.processing = true;
async.whilst(
function() {
return self.queue.length;
}, function(next) {
var blockArgs = self.queue.slice(0, Math.min(5, self.queue.length));
self.queue = self.queue.slice(blockArgs.length);
if (_.isNumber(blockArgs[0])) {
self.block.getBlocks(blockArgs, function(err, blocks) {
if(err) {
return next(err);
}
self._pushBlocks(blocks);
next();
});
} else {
self._pushBlocks(blockArgs);
next();
}
}, function(err) {
if(err) {
return self.emit('error', err);
}
self.processing = false;
}
);
};
BlockStream.prototype._pushBlocks = function(blocks) {
var self = this;
for(var i = 0; i < blocks.length; i++) {
self.lastEmittedHash = blocks[i].hash;
self.push(blocks[i]);
}
};
BlockStream.prototype._getTipHeight = function() {
if (this.dbTip.__height === 0) {
return -1;
}
return this.dbTip.__height;
};
ProcessSerial.prototype._reportStatus = function() {
if ((Date.now() - this._lastReportedTime) > 1000) {
this._lastReportedTime = Date.now();
log.info('Sync: current height is: ' + this.block.tip.__height);
}
};
ProcessSerial.prototype._write = function(block, enc, callback) {
var self = this;
function check() {
return self.block.concurrentTip.__height >= block.__height;
}
if(check()) {
return self._process(block, callback);
}
self.block.once('concurrentaddblock', function() {
if(!check()) {
var err = new Error('Concurrent block ' + self.block.concurrentTip.__height + ' is less than ' + block.__height);
return self.emit('error', err);
}
self._process(block, callback);
});
};
ProcessSerial.prototype._process = function(block, callback) {
var self = this;
self.block.getBlockOperations(block, true, 'serial', function(err, operations) {
if(err) {
return callback(err);
}
operations.push(self.block.getTipOperation(block, true));
var obj = {
tip: block,
operations: operations
};
self.tip = block;
self.db.batch(obj.operations, function(err) {
if(err) {
return callback(err);
}
self.block.tip = block;
self._reportStatus();
self.block.emit('addblock');
callback();
});
});
};
ProcessConcurrent.prototype._transform = function(block, enc, callback) {
var self = this;
this.lastBlock = block;
self.block.getBlockOperations(block, true, 'concurrent', function(err, operations) {
if(err) {
return callback(err);
}
self.blockCount++;
self.operations = self.operations.concat(operations);
if(self.blockCount >= 1) {
self.operations.push(self.block.getTipOperation(block, true, 'concurrentTip'));
var obj = {
concurrentTip: block,
operations: self.operations
};
self.operations = [];
self.blockCount = 0;
return callback(null, obj);
}
callback();
});
};
ProcessConcurrent.prototype._flush = function(callback) {
if(this.operations.length) {
this.operations.push(this.block.getTipOperation(this.lastBlock, true));
this.operations = [];
return callback(null, this.operations);
}
};
WriteStream.prototype._write = function(obj, enc, callback) {
var self = this;
if (self.db.node.stopping) {
return setImmediate(callback);
}
self.db.batch(obj.operations, function(err) {
if(err) {
return callback(err);
}
self.block.concurrentTip = obj.concurrentTip;
self.block.emit('concurrentaddblock');
self.lastConcurrentOutputHeight = self.block.concurrentTip.__height;
callback();
});
};
ProcessBoth.prototype._write = function(block, encoding, callback) {
var self = this;
async.parallel([function(next) {
self.block.getBlockOperations(block, true, 'concurrent', function(err, operations) {
if(err) {
return callback(err);
}
operations.push(self.block.getTipOperation(block, true, 'concurrentTip'));
next(null, operations);
});
}, function(next) {
self.block.getBlockOperations(block, true, 'serial', function(err, operations) {
if(err) {
return callback(err);
}
operations.push(self.block.getTipOperation(block, true));
next(null, operations);
});
}], function(err, results) {
if(err) {
return callback(err);
}
var operations = results[0].concat(results[1]);
self.db.batch(operations, function(err) {
if(err) {
return callback(err);
}
self.block.tip = block;
self.block.concurrentTip = block;
callback();
});
});
};
module.exports = BlockHandler;

View File

@ -0,0 +1,28 @@
'use strict';
var Block = require('bcoin').block;
// stores -- block header as key, block itself as value (optionally)
function Encoding(servicePrefix) {
this._servicePrefix = servicePrefix;
}
// ---- hash --> rawblock
Encoding.prototype.encodeBlockKey = function(hash) {
return Buffer.concat([ this._servicePrefix, new Buffer(hash, 'hex') ]);
};
Encoding.prototype.decodeBlockKey = function(buffer) {
return buffer.slice(2).toString('hex');
};
Encoding.prototype.encodeBlockValue = function(block) {
return block.toRaw();
};
Encoding.prototype.decodeBlockValue = function(buffer) {
return Block.fromRaw(buffer);
};
module.exports = Encoding;

631
lib/services/block/index.js Normal file
View File

@ -0,0 +1,631 @@
'use strict';
var async = require('async');
var BaseService = require('../../service');
var inherits = require('util').inherits;
var Encoding = require('./encoding');
var index = require('../../');
var log = index.log;
var utils = require('../../utils');
var assert = require('assert');
var constants = require('../../constants');
var bcoin = require('bcoin');
var BlockService = function(options) {
BaseService.call(this, options);
this._tip = null;
this._db = this.node.services.db;
this._p2p = this.node.services.p2p;
this._header = this.node.services.header;
this._timestamp = this.node.services.timestamp;
this._subscriptions = {};
this._subscriptions.block = [];
this._subscriptions.reorg = [];
this._blockCount = 0;
this.GENESIS_HASH = constants.BITCOIN_GENESIS_HASH[this.node.network];
this._initialSync = true;
};
inherits(BlockService, BaseService);
BlockService.dependencies = [ 'timestamp', 'p2p', 'db', 'header' ];
// --- public prototype functions
BlockService.prototype.getAPIMethods = function() {
var methods = [
['getBlock', this, this.getBlock, 1],
['getRawBlock', this, this.getRawBlock, 1],
['getBlockOverview', this, this.getBlockOverview, 1],
['getBestBlockHash', this, this.getBestBlockHash, 0],
['syncPercentage', this, this.syncPercentage, 0],
['isSynced', this, this.isSynced, 0]
];
return methods;
};
BlockService.prototype.isSynced = function(callback) {
callback(null, !this._initialSync);
};
BlockService.prototype.getBestBlockHash = function(callback) {
var hash = this._header.getLastHeader().hash;
callback(null, hash);
};
BlockService.prototype.getTip = function() {
return this._tip;
};
BlockService.prototype.getBlock = function(arg, callback) {
var self = this;
self._getHash(arg, function(err, hash) {
if (err) {
return callback(err);
}
if (!hash) {
return callback();
}
self._getBlock(hash, callback);
});
};
BlockService.prototype.getBlockOverview = function(hash, callback) {
this._getBlock(hash, function(err, block) {
if (err) {
return callback(err);
}
var header = block.toHeaders().toJSON();
var blockOverview = {
hash: block.rhash(),
version: block.version,
confirmations: null,
height: header.height,
chainWork: header.chainwork,
prevHash: header.prevBlock,
nextHash: null,
merkleRoot: block.merkleroot,
time: block.ts,
medianTime: null,
nonce: block.nonce,
bits: block.bits,
difficulty: null,
txids: null
};
callback(null, blockOverview);
});
};
BlockService.prototype.getPublishEvents = function() {
return [
{
name: 'block/block',
scope: this,
subscribe: this.subscribe.bind(this, 'block'),
unsubscribe: this.unsubscribe.bind(this, 'block')
}
];
};
BlockService.prototype.getRawBlock = function(hash, callback) {
this.getBlock(hash, function(err, block) {
if(err) {
return callback(err);
}
callback(null, block.toRaw().toString('hex'));
});
};
BlockService.prototype.start = function(callback) {
var self = this;
async.waterfall([
function(next) {
self._db.getPrefix(self.name, next);
},
function(prefix, next) {
self._prefix = prefix;
self._encoding = new Encoding(self._prefix);
self._db.getServiceTip('block', next);
}
], function(err, tip) {
if(err) {
return callback(err);
}
assert(tip.height >= 0, 'tip is not initialized');
self._setTip(tip);
self._setListeners();
self._startSubscriptions();
callback();
});
};
BlockService.prototype.stop = function(callback) {
setImmediate(callback);
};
BlockService.prototype.subscribe = function(name, emitter) {
this._subscriptions[name].push(emitter);
log.info(emitter.remoteAddress, 'subscribe:', 'block/' + name, 'total:', this._subscriptions[name].length);
};
BlockService.prototype._syncPercentage = function() {
var height = this._header.getLastHeader().height;
var ratio = this._tip.height/height;
return (ratio*100).toFixed(2);
};
BlockService.prototype.syncPercentage = function(callback) {
callback(null, this._syncPercentage());
};
BlockService.prototype.unsubscribe = function(name, emitter) {
var index = this._subscriptions[name].indexOf(emitter);
if (index > -1) {
this._subscriptions[name].splice(index, 1);
}
log.info(emitter.remoteAddress, 'unsubscribe:', 'block/' + name, 'total:', this._subscriptions[name].length);
};
// --- start private prototype functions
BlockService.prototype._broadcast = function(subscribers, name, entity) {
for (var i = 0; i < subscribers.length; i++) {
subscribers[i].emit(name, entity);
}
};
BlockService.prototype._detectReorg = function(block) {
var prevHash = bcoin.util.revHex(block.prevBlock);
if (this._tip.hash !== prevHash) {
return true;
}
return false;
};
BlockService.prototype._findCommonAncestor = function(hash, allHeaders, callback) {
var self = this;
var count = 0;
var _oldTip = this._tip.hash;
var _newTip = hash;
var oldBlocks = [];
assert(_oldTip, 'We don\'t have a tip hash to reorg away from!');
async.whilst(
// test case
function() {
return _oldTip !== _newTip && ++count < allHeaders.size;
},
// get block
function(next) {
// old tip (our current tip) has to be in database
self._db.get(self._encoding.encodeBlockKey(_oldTip), function(err, data) {
if (err || !data) {
return next(err || new Error('missing block'));
}
// once we've found the old tip, we will find its prev and check to see if matches new tip's prev
var block = self._encoding.decodeBlockValue(data);
// apply the block's height
var blockHdr = allHeaders.get(block.rhash());
if (!blockHdr) {
return next(new Error('Could not find block in list of headers: ' + block.rhash()));
}
block.height = blockHdr.height;
assert(block.height >= 0, 'We mamaged to save a header with an incorrect height.');
// apply the block's timestamp
self._timestamp.getTimestamp(block.rhash(), function(err, timestamp) {
if (err || !timestamp) {
return next(err || new Error('missing timestamp'));
}
block.ts = timestamp;
// we will squirrel away the block because our services will need to remove it after we've found the common ancestor
oldBlocks.push(block);
// this is our current tip's prev hash
_oldTip = bcoin.util.revHex(block.prevBlock);
// our current headers have the correct state of the chain, so consult that for its prev aash
var header = allHeaders.get(_newTip);
if (!header) {
return next(new Error('Header missing from list of headers'));
}
// set new tip to the prev hash
_newTip = header.prevHash;
next();
});
});
}, function(err) {
if (err) {
return callback(err);
}
var commonAncestorHash = _newTip;
callback(null, commonAncestorHash, oldBlocks);
});
};
BlockService.prototype._getBlock = function(hash, callback) {
var self = this;
this._db.get(this._encoding.encodeBlockKey(hash), function(err, data) {
if(err) {
return callback(err);
}
if (!data) {
return callback();
}
var block = self._encoding.decodeBlockValue(data);
callback(null, block);
});
};
BlockService.prototype._getHash = function(blockArg, callback) {
if (utils.isHeight(blockArg)) {
this._header.getHeaderByHeight(blockArg, function(err, header) {
if(err) {
return callback(err);
}
if (!header) {
return callback();
}
callback(null, header.hash);
});
}
return callback(null, blockArg);
};
BlockService.prototype._handleReorg = function(hash, allHeaders, block) {
// hash is the hash of the new block that we are reorging to.
assert(hash, 'We were asked to reorg to a non-existent hash.');
var self = this;
self._reorging = true;
log.warn('Block Service: Chain reorganization detected! Our current block tip is: ' +
self._tip.hash + ' the current block: ' + hash + '.');
self._findCommonAncestor(hash, allHeaders, function(err, commonAncestorHash, oldBlocks) {
if (err) {
log.error('Block Service: A common ancestor block between hash: ' +
self._tip.hash + ' (our current tip) and: ' + hash +
' (the forked block) could not be found. Bitcore-node must exit.');
self.node.stop();
return;
}
var commonAncestorHeader = allHeaders.get(commonAncestorHash);
log.info('Block Service: A common ancestor block was found to at hash: ' + commonAncestorHeader.hash);
self._processReorg(commonAncestorHeader, oldBlocks, block);
});
};
// this JUST rewinds the chain back to the common ancestor block, nothing more
BlockService.prototype._onReorg = function(commonAncestorHeader, oldBlockList, newBlock) {
// set the tip to the common ancestor in case something goes wrong with the reorg
var self = this;
self._setTip({ hash: commonAncestorHeader.hash, height: commonAncestorHeader.height });
var tipOps = utils.encodeTip(self._tip, self.name);
var removalOps = [{
type: 'put',
key: tipOps.key,
value: tipOps.value
}];
// remove all the old blocks that we reorg from
oldBlockList.forEach(function(block) {
removalOps.push({
type: 'del',
key: self._encoding.encodeBlockKey(block.rhash()),
});
});
self._db.batch(removalOps, function() {
self._reorging = false;
if (newBlock) {
self._onBlock(newBlock);
}
});
};
BlockService.prototype._onAllHeaders = function() {
this._startSync();
};
BlockService.prototype._processReorg = function(commonAncestorHeader, oldBlocks, newBlock) {
var self = this;
var operations = [];
var services = self.node.services;
async.eachSeries(
services,
function(mod, next) {
if(mod.onReorg) {
mod.onReorg.call(mod, [commonAncestorHeader, oldBlocks], function(err, ops) {
if (err) {
return next(err);
}
if (ops) {
operations = operations.concat(ops);
}
next();
});
} else {
setImmediate(next);
}
},
function(err) {
if (err) {
if (!self.node.stopping) {
log.error('Block Service: Error: ' + err);
self.node.stop();
}
return;
}
self._db.batch(operations, function(err) {
if (err && !self.node.stopping) {
log.error('Block Service: Error: ' + err);
self.node.stop();
}
self._onReorg(commonAncestorHeader, oldBlocks, newBlock);
});
}
);
};
BlockService.prototype._processBlock = function(block) {
var self = this;
var operations = [];
var services = self.node.services;
async.eachSeries(
services,
function(mod, next) {
if(mod.onBlock) {
mod.onBlock.call(mod, block, function(err, ops) {
if (err) {
return next(err);
}
if (ops) {
operations = operations.concat(ops);
}
next();
});
} else {
setImmediate(next);
}
},
function(err) {
if (err) {
if (!self.node.stopping) {
log.error('Block Service: Error: ' + err);
self.node.stop();
}
return;
}
self._db.batch(operations, function(err) {
if (err) {
if (!self.node.stopping) {
log.error('Block Service: Error: ' + err);
self.node.stop();
}
return;
}
self._tip.height = self._tip.height + 1;
self._tip.hash = block.rhash();
var tipOps = utils.encodeTip(self._tip, self.name);
self._db.put(tipOps.key, tipOps.value, function(err) {
if (err) {
if (!self.node.stopping) {
log.error('Block Service: Error: ' + err);
self.node.stop();
}
return;
}
self._sync();
});
});
}
);
};
BlockService.prototype.onBlock = function(block, callback) {
var self = this;
setImmediate(function() {
callback(null, [{
type: 'put',
key: self._encoding.encodeBlockKey(block.rhash()),
value: self._encoding.encodeBlockValue(block)
}]);
});
};
BlockService.prototype._onBlock = function(block) {
if (this.node.stopping || this._reorging) {
return;
}
// this service must receive blocks in order
var prevHash = bcoin.util.revHex(block.prevBlock);
if (this._tip.hash !== prevHash) {
return;
}
log.debug('Block Service: new block: ' + block.rhash());
block.height = this._tip.height + 1;
this._processBlock(block);
};
BlockService.prototype._setListeners = function() {
var self = this;
self._header.once('headers', self._onAllHeaders.bind(self));
self._header.on('reorg', function(hash, headers, block) {
if (!self._reorging && !this._initialSync) {
log.debug('Block Service: detected a reorg from the header service.');
self._handleReorg(hash, headers, block);
}
});
};
BlockService.prototype._setTip = function(tip) {
log.debug('Block Service: Setting tip to height: ' + tip.height);
log.debug('Block Service: Setting tip to hash: ' + tip.hash);
this._tip = tip;
};
BlockService.prototype._startSync = function() {
this._numNeeded = this._header.getLastHeader().height - this._tip.height;
log.info('Block Service: Gathering: ' + this._numNeeded + ' block(s) from the peer-to-peer network.');
this._sync();
};
BlockService.prototype._startSubscriptions = function() {
if (this._subscribed) {
return;
}
this._subscribed = true;
if (!this._bus) {
this._bus = this.node.openBus({remoteAddress: 'localhost-block'});
}
this._bus.on('header/block', this._onBlock.bind(this));
this._bus.subscribe('header/block');
};
BlockService.prototype._sync = function() {
var self = this;
if (self.node.stopping) {
return;
}
var lastHeaderIndex = self._header.getLastHeader().height;
if (self._tip.height < lastHeaderIndex) {
if (self._tip.height % 144 === 0) {
log.info('Block Service: Blocks download progress: ' +
self._tip.height + '/' + lastHeaderIndex +
' (' + self._syncPercentage() + '%)');
}
return self._header.getNextHash(self._tip, function(err, hash) {
if(err) {
log.error(err);
self.node.stop();
return;
}
self._p2p.getBlocks({ startHash: self._tip.hash, endHash: hash });
});
}
this._header.blockServiceSyncing = false;
this._initialSync = false;
log.info('Block Service: The best block hash is: ' + self._tip.hash +
' at height: ' + self._tip.height);
};
module.exports = BlockService;

347
lib/services/block/reorg.js Normal file
View File

@ -0,0 +1,347 @@
'use strict';
var bitcore = require('bitcore-lib');
var BufferUtil = bitcore.util.buffer;
var async = require('async');
function Reorg(node, block) {
this.node = node;
this.block = block;
this.db = block.db;
}
Reorg.prototype.handleReorg = function(newBlockHash, callback) {
var self = this;
self.handleConcurrentReorg(function(err) {
if(err) {
return callback(err);
}
self.findCommonAncestorAndNewHashes(self.block.tip.hash, newBlockHash, function(err, commonAncestor, newHashes) {
if(err) {
return callback(err);
}
self.rewindBothTips(commonAncestor, function(err) {
if(err) {
return callback(err);
}
self.fastForwardBothTips(newHashes, callback);
});
});
});
};
Reorg.prototype.handleConcurrentReorg = function(callback) {
var self = this;
if(self.block.concurrentTip.hash === self.block.tip.hash) {
return callback();
}
self.findCommonAncestorAndNewHashes(
self.block.concurrentTip.hash,
self.block.tip.hash,
function(err, commonAncestor, newHashes) {
if(err) {
return callback(err);
}
self.rewindConcurrentTip(commonAncestor, function(err) {
if(err) {
return callback(err);
}
self.fastForwardConcurrentTip(newHashes, callback);
});
});
};
Reorg.prototype.rewindConcurrentTip = function(commonAncestor, callback) {
var self = this;
async.whilst(
function() {
return self.block.concurrentTip.hash !== commonAncestor;
},
function(next) {
self.block.getBlockOperations(self.block.concurrentTip, false, 'concurrent', function(err, operations) {
if(err) {
return next(err);
}
operations.push(self.block.getTipOperation(self.block.concurrentTip, false, 'concurrentTip'));
self.db.batch(operations, function(err) {
if(err) {
return next(err);
}
var prevHash = BufferUtil.reverse(self.block.concurrentTip.header.prevHash).toString('hex');
self.block.getBlocks([prevHash], function(err, blocks) {
if(err) {
return next(err);
}
self.block.concurrentTip = blocks[0];
next();
});
});
});
},
callback
);
};
Reorg.prototype.fastForwardConcurrentTip = function(newHashes, callback) {
var self = this;
async.eachSeries(newHashes, function(hash, next) {
self.block.getBlocks([hash], function(err, blocks) {
if(err) {
return next(err);
}
self.block.getBlockOperations(blocks[0], true, 'concurrent', function(err, operations) {
if(err) {
return next(err);
}
operations.push(self.block.getTipOperation(blocks[0], true, 'concurrentTip'));
self.db.batch(operations, function(err) {
if(err) {
return next(err);
}
self.block.concurrentTip = blocks[0];
next();
});
});
});
}, callback);
};
Reorg.prototype.rewindBothTips = function(commonAncestor, callback) {
var self = this;
async.whilst(
function() {
return self.block.tip.hash !== commonAncestor;
},
function(next) {
async.parallel(
[
function(next) {
self.block.getBlockOperations(self.block.concurrentTip, false, 'concurrent', function(err, operations) {
if(err) {
return next(err);
}
operations.push(self.block.getTipOperation(self.block.concurrentTip, false, 'concurrentTip'));
next(null, operations);
});
},
function(next) {
self.block.getBlockOperations(self.block.tip, false, 'serial', function(err, operations) {
if(err) {
return next(err);
}
operations.push(self.block.getTipOperation(self.block.tip, false));
next(null, operations);
});
}
],
function(err, results) {
if(err) {
return callback(err);
}
var operations = results[0].concat(results[1]);
self.db.batch(operations, function(err) {
if(err) {
return next(err);
}
var prevHash = BufferUtil.reverse(self.block.tip.header.prevHash).toString('hex');
self.block.getBlocks([prevHash], function(err, blocks) {
if(err) {
return next(err);
}
self.block.concurrentTip = blocks[0];
self.block.tip = blocks[0];
next();
});
});
}
);
},
callback
);
};
Reorg.prototype.fastForwardBothTips = function(newHashes, callback) {
var self = this;
async.eachSeries(newHashes, function(hash, next) {
self.block.getBlocks([hash], function(err, blocks) {
if(err) {
return next(err);
}
async.parallel(
[
function(next) {
self.block.getBlockOperations(blocks[0], true, 'concurrent', function(err, operations) {
if(err) {
return next(err);
}
operations.push(self.block.getTipOperation(blocks[0], true, 'concurrentTip'));
next(null, operations);
});
},
function(next) {
self.block.getBlockOperations(blocks[0], true, 'serial', function(err, operations) {
if(err) {
return next(err);
}
operations.push(self.block.getTipOperation(blocks[0], true));
next(null, operations);
});
}
],
function(err, results) {
if(err) {
return next(err);
}
var operations = results[0].concat(results[1]);
self.db.batch(operations, function(err) {
if(err) {
return next(err);
}
self.block.concurrentTip = blocks[0];
self.block.tip = blocks[0];
next();
});
}
);
});
}, callback);
};
Reorg.prototype.findCommonAncestorAndNewHashes = function(oldTipHash, newTipHash, callback) {
var self = this;
var mainPosition = oldTipHash;
var forkPosition = newTipHash;
var mainHashesMap = {};
var forkHashesMap = {};
mainHashesMap[mainPosition] = true;
forkHashesMap[forkPosition] = true;
var commonAncestor = null;
var newHashes = [forkPosition];
async.whilst(
function() {
return !commonAncestor;
},
function(next) {
async.parallel(
[
function(next) {
if(!mainPosition) {
return next();
}
self.block.getBlockHeader(mainPosition, function(err, mainBlockHeader) {
if(err) {
return next(err);
}
if(mainBlockHeader && mainBlockHeader.prevHash) {
mainHashesMap[mainBlockHeader.prevHash] = true;
mainPosition = mainBlockHeader.prevHash;
} else {
mainPosition = null;
}
next();
});
},
function(next) {
if(!forkPosition) {
return next();
}
self.block.getBlockHeader(forkPosition, function(err, forkBlockHeader) {
if(err) {
return next(err);
}
if(forkBlockHeader && forkBlockHeader.prevHash) {
forkHashesMap[forkBlockHeader.prevHash] = true;
forkPosition = forkBlockHeader.prevHash;
newHashes.unshift(forkPosition);
} else {
forkPosition = null;
}
next();
});
}
],
function(err) {
if(err) {
return next(err);
}
if(forkPosition && mainHashesMap[forkPosition]) {
commonAncestor = forkPosition;
}
if(mainPosition && forkHashesMap[mainPosition]) {
commonAncestor = mainPosition;
}
if(!mainPosition && !forkPosition) {
return next(new Error('Unknown common ancestor'));
}
next();
}
);
},
function(err) {
if(err) {
return callback(err);
}
// New hashes are those that are > common ancestor
var commonAncestorFound = false;
for(var i = newHashes.length - 1; i >= 0; i--) {
if(newHashes[i] === commonAncestor) {
commonAncestorFound = true;
}
if(commonAncestorFound) {
newHashes.shift();
}
}
callback(null, commonAncestor, newHashes);
}
);
};
module.exports = Reorg;

View File

315
lib/services/db/index.js Normal file
View File

@ -0,0 +1,315 @@
'use strict';
var util = require('util');
var fs = require('fs');
var async = require('async');
var levelup = require('levelup');
var leveldown = require('leveldown');
var mkdirp = require('mkdirp');
var Service = require('../../service');
var constants = require('../../constants');
var log = require('../../index').log;
var assert = require('assert');
function DB(options) {
if (!(this instanceof DB)) {
return new DB(options);
}
options = options || {};
Service.call(this, options);
this._dbPrefix = constants.DB_PREFIX;
this.version = 1;
this.network = this.node.network;
this._setDataPath();
this.levelupStore = leveldown;
if (options.store) {
this.levelupStore = options.store;
}
this.subscriptions = {};
this.GENESIS_HASH = constants.BITCOIN_GENESIS_HASH[this.node.network];
this.node.on('stopping', function() {
log.warn('Node is stopping, gently closing the database. Please wait, this could take a while.');
});
}
util.inherits(DB, Service);
DB.dependencies = [];
DB.prototype._onError = function(err) {
if (!this._stopping) {
log.error('Db Service: error: ' + err);
this.node.stop();
}
};
DB.prototype._setDataPath = function() {
assert(fs.existsSync(this.node.datadir), 'Node is expected to have a "datadir" property');
if (this.node.network === 'livenet' || this.node.network === 'mainnet') {
this.dataPath = this.node.datadir + '/bitcorenode.db';
} else if (this.node.network === 'regtest') {
this.dataPath = this.node.datadir + '/regtest/bitcorenode.db';
} else if (this.node.network === 'testnet') {
this.dataPath = this.node.datadir + '/testnet/bitcorenode.db';
} else {
throw new Error('Unknown network: ' + this.network);
}
};
DB.prototype._setVersion = function(callback) {
var versionBuffer = new Buffer(new Array(4));
versionBuffer.writeUInt32BE(this.version);
this.put(Buffer.concat([ this._dbPrefix, new Buffer('version', 'utf8') ]), versionBuffer, callback);
};
DB.prototype.start = function(callback) {
if (!fs.existsSync(this.dataPath)) {
mkdirp.sync(this.dataPath);
}
this._store = levelup(this.dataPath, { db: this.levelupStore, keyEncoding: 'binary', valueEncoding: 'binary'});
setImmediate(callback);
};
DB.prototype.get = function(key, options, callback) {
var cb = callback;
var opts = options;
if (typeof callback !== 'function') {
cb = options;
opts = {};
}
if (!this._stopping) {
this._store.get(key, opts, function(err, data) {
if(err && err instanceof levelup.errors.NotFoundError) {
return cb();
}
if (err) {
return cb(err);
}
cb(null, data);
});
} else {
cb(new Error('Shutdown sequence underway, not able to complete the query'));
}
};
DB.prototype.put = function(key, value, callback) {
assert(Buffer.isBuffer(key), 'key NOT a buffer as expected.');
if (value) {
assert(Buffer.isBuffer(value), 'value exists but NOT a buffer as expected.');
}
var self = this;
if (self._stopping) {
callback();
}
self._store.put(key, value, callback);
};
DB.prototype.batch = function(ops, callback) {
var self = this;
if (self._stopping) {
return callback();
}
for(var i = 0; i < ops.length; i++) {
assert(Buffer.isBuffer(ops[i].key), 'key NOT a buffer as expected.');
if (ops[i].value) {
assert(Buffer.isBuffer(ops[i].value), 'value exists but NOT a buffer as expected.');
}
}
self._store.batch(ops, callback);
};
DB.prototype.createReadStream = function(op) {
if (this._stopping) {
return;
}
var stream = this._store.createReadStream(op);
stream.on('error', this._onError.bind(this));
return stream;
};
DB.prototype.createKeyStream = function(op) {
if (this._stopping) {
return;
}
var stream = this._store.createKeyStream(op);
stream.on('error', this._onError.bind(this));
return stream;
};
DB.prototype.stop = function(callback) {
this._stopping = true;
this.close(callback);
};
DB.prototype.close = function(callback) {
if (this._store && this._store.isOpen()) {
this._store.close(callback);
return;
}
setImmediate(callback);
};
DB.prototype.getAPIMethods = function() {
return [];
};
DB.prototype.getPublishEvents = function() {
return [];
};
DB.prototype.getServiceTip = function(serviceName, callback) {
var keyBuf = Buffer.concat([ this._dbPrefix, new Buffer('tip-' + serviceName, 'utf8') ]);
var self = this;
self.get(keyBuf, function(err, tipBuf) {
if (err) {
return callback(err);
}
var tip;
if (tipBuf) {
tip = {
height: tipBuf.readUInt32BE(0,4),
hash: tipBuf.slice(4).toString('hex')
};
} else {
tip = {
height: 0,
hash: self.GENESIS_HASH
};
}
callback(null, tip);
});
};
DB.prototype.getPrefix = function(service, callback) {
var self = this;
var keyBuf = Buffer.concat([ self._dbPrefix, new Buffer('prefix-', 'utf8'), new Buffer(service, 'utf8') ]);
var unusedBuf = Buffer.concat([ self._dbPrefix, new Buffer('nextUnused', 'utf8') ]);
function getPrefix(next) {
self.get(keyBuf, function(err, buf) {
if (err) {
return callback(err);
}
if (!buf) {
return next();
}
callback(null, buf);
});
}
function getUnused(next) {
self.get(unusedBuf, function(err, buffer) {
if(err) {
return callback(err);
}
if(!buffer) {
return next(null, new Buffer('0001', 'hex'));
}
next(null, buffer);
});
}
function putPrefix(buffer, next) {
self.put(keyBuf, buffer, function(err) {
if (err) {
return callback(err);
}
next(null, buffer);
});
}
function putUnused(buffer, next) {
var prefix = buffer.readUInt16BE();
var nextUnused = new Buffer(2);
nextUnused.writeUInt16BE(prefix + 1);
self.put(unusedBuf, nextUnused, function(err) {
if (err) {
return callback(err);
}
next(null, buffer);
});
}
async.waterfall(
[
getPrefix,
getUnused,
putPrefix,
putUnused
],
callback
);
};
module.exports = DB;

60
lib/services/fee/index.js Normal file
View File

@ -0,0 +1,60 @@
'use strict';
var BaseService = require('../../service');
var inherits = require('util').inherits;
var BitcoreRPC = require('bitcoind-rpc');
var FeeService = function(options) {
this._config = options.rpc || {
user: 'bitcoin',
pass: 'local321',
host: 'localhost',
protocol: 'http',
port: 8332
};
BaseService.call(this, options);
this._client = new BitcoreRPC(this._config);
};
inherits(FeeService, BaseService);
FeeService.dependencies = [];
FeeService.prototype.start = function() {
return this.node.network.port - 1;
};
FeeService.prototype.start = function(callback) {
callback();
};
FeeService.prototype.stop = function(callback) {
callback();
};
FeeService.prototype.getAPIMethods = function() {
return [
['estimateFee', this, this.estimateFee, 1]
];
};
FeeService.prototype.estimateFee = function(blocks, callback) {
this._client.estimateFee(blocks || 4, function(err, res) {
if (err) {
return callback(err);
}
if (!res) {
callback();
}
callback(null, res.result);
});
};
module.exports = FeeService;

View File

@ -0,0 +1,73 @@
'use strict';
function Encoding(servicePrefix) {
this._servicePrefix = servicePrefix;
this._hashPrefix = new Buffer('00', 'hex');
this._heightPrefix = new Buffer('01', 'hex');
}
// ---- hash --> header
Encoding.prototype.encodeHeaderHashKey = function(hash) {
var hashBuf = new Buffer(hash, 'hex');
return Buffer.concat([ this._servicePrefix, this._hashPrefix, hashBuf ]);
};
Encoding.prototype.decodeHeaderHashKey = function(buffer) {
return buffer.slice(3).toString('hex');
};
// ---- height --> header
Encoding.prototype.encodeHeaderHeightKey = function(height) {
var heightBuf = new Buffer(4);
heightBuf.writeUInt32BE(height);
return Buffer.concat([ this._servicePrefix, this._heightPrefix, heightBuf ]);
};
Encoding.prototype.decodeHeaderHeightKey = function(buffer) {
return buffer.readUInt32BE(3);
};
Encoding.prototype.encodeHeaderValue = function(header) {
var hashBuf = new Buffer(header.hash, 'hex');
var versionBuf = new Buffer(4);
versionBuf.writeInt32BE(header.version);
var prevHash = new Buffer(header.prevHash, 'hex');
var merkleRoot = new Buffer(header.merkleRoot, 'hex');
var tsBuf = new Buffer(4);
tsBuf.writeUInt32BE(header.timestamp || header.time);
var bitsBuf = new Buffer(4);
bitsBuf.writeUInt32BE(header.bits);
var nonceBuf = new Buffer(4);
nonceBuf.writeUInt32BE(header.nonce);
var heightBuf = new Buffer(4);
heightBuf.writeUInt32BE(header.height);
var chainworkBuf = new Buffer(header.chainwork, 'hex');
return Buffer.concat([hashBuf, versionBuf, prevHash, merkleRoot, tsBuf, bitsBuf, nonceBuf, heightBuf, chainworkBuf ]);
};
Encoding.prototype.decodeHeaderValue = function(buffer) {
var hash = buffer.slice(0, 32).toString('hex');
var version = buffer.readInt32BE(32);
var prevHash = buffer.slice(36, 68).toString('hex');
var merkleRoot = buffer.slice(68, 100).toString('hex');
var ts = buffer.readUInt32BE(100);
var bits = buffer.readUInt32BE(104);
var nonce = buffer.readUInt32BE(108);
var height = buffer.readUInt32BE(112);
var chainwork = buffer.slice(116).toString('hex');
return {
hash: hash,
version: version,
prevHash: prevHash,
merkleRoot: merkleRoot,
timestamp: ts,
bits: bits,
nonce: nonce,
height: height,
chainwork: chainwork
};
};
module.exports = Encoding;

View File

@ -0,0 +1,718 @@
'use strict';
var BaseService = require('../../service');
var inherits = require('util').inherits;
var Encoding = require('./encoding');
var index = require('../../');
var log = index.log;
var utils = require('../../utils');
var async = require('async');
var BN = require('bn.js');
var consensus = require('bcoin').consensus;
var assert = require('assert');
var constants = require('../../constants');
var bcoin = require('bcoin');
var HeaderService = function(options) {
BaseService.call(this, options);
this._tip = null;
this._p2p = this.node.services.p2p;
this._db = this.node.services.db;
this._hashes = [];
this.subscriptions = {};
this.subscriptions.block = [];
this._checkpoint = options.checkpoint || 2000; // set to -1 to resync all headers.
this.GENESIS_HASH = constants.BITCOIN_GENESIS_HASH[this.node.network];
this._lastHeader = null;
this.blockServiceSyncing = true;
};
inherits(HeaderService, BaseService);
HeaderService.dependencies = [ 'p2p', 'db' ];
HeaderService.MAX_CHAINWORK = new BN(1).ushln(256);
HeaderService.STARTING_CHAINWORK = '0000000000000000000000000000000000000000000000000000000100010001';
// --- public prototype functions
HeaderService.prototype.subscribe = function(name, emitter) {
this.subscriptions[name].push(emitter);
log.info(emitter.remoteAddress, 'subscribe:', 'header/' + name, 'total:', this.subscriptions[name].length);
};
HeaderService.prototype.unsubscribe = function(name, emitter) {
var index = this.subscriptions[name].indexOf(emitter);
if (index > -1) {
this.subscriptions[name].splice(index, 1);
}
log.info(emitter.remoteAddress, 'unsubscribe:', 'header/' + name, 'total:', this.subscriptions[name].length);
};
HeaderService.prototype.getAPIMethods = function() {
var methods = [
['getAllHeaders', this, this.getAllHeaders, 0],
['getBestHeight', this, this.getBestHeight, 0],
['getInfo', this, this.getInfo, 0],
['getBlockHeader', this, this.getBlockHeader, 1]
];
return methods;
};
HeaderService.prototype.getCurrentDifficulty = function() {
var target = bcoin.mining.common.getTarget(this._lastHeader.bits);
return bcoin.mining.common.getDifficulty(target);
};
HeaderService.prototype.getInfo = function(callback) {
callback(null, {
blocks: this._lastHeader.height,
connections: this._p2p.getNumberOfPeers(),
timeoffset: 0,
proxy: '',
testnet: this.node.network === 'livenet' ? false: true,
errors: '',
network: this.node.network,
relayFee: 0,
version: 'bitcore-1.1.2',
protocolversion: 700001,
difficulty: this.getCurrentDifficulty()
});
};
HeaderService.prototype.getAllHeaders = function(callback) {
var self = this;
var start = self._encoding.encodeHeaderHeightKey(0);
var end = self._encoding.encodeHeaderHeightKey(self._tip.height + 1);
var allHeaders = new utils.SimpleMap();
var criteria = {
gte: start,
lt: end
};
var stream = self._db.createReadStream(criteria);
var streamErr;
stream.on('error', function(error) {
streamErr = error;
});
stream.on('data', function(data) {
var header = self._encoding.decodeHeaderValue(data.value);
allHeaders.set(header.hash, header, header.height);
});
stream.on('end', function() {
if (streamErr) {
return streamErr;
}
callback(null, allHeaders);
});
};
HeaderService.prototype.getBlockHeader = function(arg, callback) {
if (utils.isHeight(arg)) {
return this._getHeader(arg, null, callback);
}
return this._getHeader(null, arg, callback);
};
HeaderService.prototype.getBestHeight = function() {
return this._tip.height;
};
HeaderService.prototype.start = function(callback) {
var self = this;
async.waterfall([
function(next) {
self._db.getPrefix(self.name, next);
},
function(prefix, next) {
self._encoding = new Encoding(prefix);
self._db.getServiceTip(self.name, next);
},
function(tip, next) {
self._tip = tip;
log.debug('Header Service: original tip height is: ' + self._tip.height);
log.debug('Header Service: original tip hash is: ' + self._tip.hash);
self._originalTip = { height: self._tip.height, hash: self._tip.hash };
if (self._tip.height === 0) {
assert(self._tip.hash === self.GENESIS_HASH, 'Expected tip hash to be genesis hash, but it was not.');
var genesisHeader = {
hash: self.GENESIS_HASH,
height: 0,
chainwork: HeaderService.STARTING_CHAINWORK,
version: 1,
prevHash: new Array(65).join('0'),
timestamp: 1231006505,
nonce: 2083236893,
bits: 0x1d00ffff,
merkleRoot: '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b'
};
self._lastHeader = genesisHeader;
var dbOps = [
{
type: 'put',
key: self._encoding.encodeHeaderHeightKey(0),
value: self._encoding.encodeHeaderValue(genesisHeader)
},
{
type: 'put',
key: self._encoding.encodeHeaderHashKey(self.GENESIS_HASH),
value: self._encoding.encodeHeaderValue(genesisHeader)
}
];
return self._db.batch(dbOps, next);
}
self._getLastHeader(next);
},
], function(err) {
if (err) {
return callback(err);
}
self._setListeners();
self._bus = self.node.openBus({remoteAddress: 'localhost-header'});
self._startHeaderSubscription();
callback();
});
};
HeaderService.prototype.stop = function(callback) {
callback();
};
HeaderService.prototype._startHeaderSubscription = function() {
this._bus.on('p2p/headers', this._onHeaders.bind(this));
this._bus.subscribe('p2p/headers');
};
HeaderService.prototype.getPublishEvents = function() {
return [
{
name: 'header/block',
scope: this,
subscribe: this.subscribe.bind(this, 'block'),
unsubscribe: this.unsubscribe.bind(this, 'block')
}
];
};
HeaderService.prototype._onBlock = function(block) {
var self = this;
var hash = block.rhash();
var prevHash = bcoin.util.revHex(block.prevBlock);
var newBlock = prevHash === self._lastHeader.hash;
var header = block.toHeaders().toJSON();
header.timestamp = header.ts;
header.prevHash = header.prevBlock;
if (newBlock) {
log.debug('Header Service: new block: ' + hash);
self._saveHeaders(self._onHeader(header));
}
// this is the rare case that a block comes to us out of order or is a reorg'ed block
// in almost all cases, this will be a reorg
if (!newBlock && !self.blockServiceSyncing) {
return self._detectReorg(block, function(err, reorg) {
if (err) {
log.error(err);
self.node.stop();
return;
}
if (reorg) {
return self._handleReorg(block, header, function(err) {
if (err) {
log.error(err);
self.node.stop();
return;
}
self._saveHeaders(self._onHeader(header));
}); // this sets the last header
}
self._broadcast(block);
});
}
setImmediate(function() {
self._broadcast(block);
});
};
HeaderService.prototype._broadcast = function(block) {
for (var i = 0; i < this.subscriptions.block.length; i++) {
this.subscriptions.block[i].emit('header/block', block);
}
};
HeaderService.prototype._onHeader = function(header) {
if (!header) {
return;
}
header.height = this._lastHeader.height + 1;
header.chainwork = this._getChainwork(header, this._lastHeader).toString(16, 64);
if (!header.timestamp) {
header.timestamp = header.time;
}
this._lastHeader = header;
return [
{
type: 'put',
key: this._encoding.encodeHeaderHashKey(header.hash),
value: this._encoding.encodeHeaderValue(header)
},
{
type: 'put',
key: this._encoding.encodeHeaderHeightKey(header.height),
value: this._encoding.encodeHeaderValue(header)
}
];
};
HeaderService.prototype._onHeaders = function(headers) {
log.debug('Header Service: Received: ' + headers.length + ' header(s).');
var dbOps = [];
for(var i = 0; i < headers.length; i++) {
var header = headers[i];
header = header.toObject();
var ops = this._onHeader(header);
dbOps = dbOps.concat(ops);
this._tip.height = header.height;
this._tip.hash = header.hash;
}
this._saveHeaders(dbOps);
};
HeaderService.prototype._saveHeaders = function(dbOps) {
var tipOps = utils.encodeTip(this._tip, this.name);
dbOps.push({
type: 'put',
key: tipOps.key,
value: tipOps.value
});
this._db.batch(dbOps, this._onHeadersSave.bind(this));
};
HeaderService.prototype._onHeadersSave = function(err) {
var self = this;
if (err) {
log.error(err);
self.node.stop();
return;
}
if (!self._syncComplete()) {
self._sync();
return;
}
self._startBlockSubscription();
self._setBestHeader();
self._detectStartupReorg(function(err, reorg) {
if (err) {
log.error(err);
self.node.stop();
return;
}
if (reorg) {
return self._handleReorg(null, null, function(err) {
if (err) {
log.error(err);
this.node.stop();
return;
}
});
}
log.debug('Header Service: emitting headers to block service.');
self.emit('headers');
});
};
HeaderService.prototype._startBlockSubscription = function() {
if (this._subscribedBlock) {
return;
}
this._subscribedBlock = true;
this._bus.on('p2p/block', this._onBlock.bind(this));
this._bus.subscribe('p2p/block');
};
HeaderService.prototype._syncComplete = function() {
return this._tip.height >= this._bestHeight;
};
HeaderService.prototype._setBestHeader = function() {
var bestHeader = this._lastHeader;
this._tip.height = bestHeader.height;
this._tip.hash = bestHeader.hash;
log.debug('Header Service: ' + bestHeader.hash + ' is the best block hash.');
};
HeaderService.prototype._getHeader = function(height, hash, callback) {
var self = this;
/*jshint -W018 */
if (!hash && !(height >= 0)) {
/*jshint +W018 */
return callback(new Error('invalid arguments'));
}
var key;
if (hash) {
key = self._encoding.encodeHeaderHashKey(hash);
} else {
key = self._encoding.encodeHeaderHeightKey(height);
}
self._db.get(key, function(err, data) {
if (err) {
return callback(err);
}
if (!data) {
return callback();
}
callback(null, self._encoding.decodeHeaderValue(data));
});
};
HeaderService.prototype._detectReorg = function(block, callback) {
assert(block, 'Block is needed to detect reorg.');
var key = this._encoding.encodeHeaderHashKey(bcoin.util.revHex(block.prevBlock));
this._db.get(key, function(err, val) {
if (err) {
return callback(err);
}
// is this block's prevHash already referenced in the database? If so, reorg
if (val) {
return callback(null, true);
}
callback(null, false);
});
};
HeaderService.prototype._detectStartupReorg = function(callback) {
var self = this;
self._getHeader(self._originalTip.height, null, function(err, header) {
if (err) {
return callback(err);
}
if (!header) {
return callback(null, true);
}
if (header.hash !== self._originalTip.hash) {
return callback(null, true);
}
callback(null, false);
});
};
HeaderService.prototype._handleReorg = function(block, header, callback) {
var self = this;
self.getAllHeaders(function(err, headers) {
if (err || !headers) {
return callback(err || new Error('Missing headers'));
}
var hash = headers.getIndex(self._originalTip.height).hash;
if (block && header) {
hash = block.rhash();
self._lastHeader = headers.get(header.prevHash);
assert(self._lastHeader, 'Expected our reorg block to have a header entry, but it did not.');
headers.set(hash, header); // appends to the end
self.emit('reorg', hash, headers, block);
}
assert(hash, 'To reorg, we need a hash to reorg to.');
self.emit('reorg', hash, headers);
callback();
});
};
HeaderService.prototype._setListeners = function() {
this._p2p.once('bestHeight', this._onBestHeight.bind(this));
};
HeaderService.prototype._onBestHeight = function(height) {
assert(height >= this._tip.height, 'Our peer does not seem to be fully synced: best height: ' +
height + ' tip height: ' + this._tip.height);
log.debug('Header Service: Best Height is: ' + height);
this._bestHeight = height;
this._startSync();
};
HeaderService.prototype._startSync = function() {
this._numNeeded = this._bestHeight - this._tip.height;
log.info('Header Service: Gathering: ' + this._numNeeded + ' ' + 'header(s) from the peer-to-peer network.');
this._sync();
};
HeaderService.prototype._sync = function() {
log.info('Header Service: download progress: ' + this._tip.height + '/' +
this._bestHeight + ' (' + (this._tip.height / this._bestHeight*100.00).toFixed(2) + '%)');
this._p2p.getHeaders({ startHash: this._tip.hash });
};
// this gets the header that is +2 places from hash or returns 0 if there is no such
HeaderService.prototype.getNextHash = function(tip, callback) {
var self = this;
// if the tip being passed in is the second to last block, then return 0 because there isn't a block
// after the last block
if (tip.height + 1 === self._tip.height) {
return callback(null, 0);
}
var start = self._encoding.encodeHeaderHeightKey(tip.height + 2);
var end = self._encoding.encodeHeaderHeightKey(tip.height + 3);
var result = 0;
var criteria = {
gte: start,
lt: end
};
var stream = self._db.createReadStream(criteria);
var streamErr;
stream.on('error', function(error) {
streamErr = error;
});
stream.on('data', function(data) {
result = self._encoding.decodeHeaderValue(data.value).hash;
});
stream.on('end', function() {
if (streamErr) {
return streamErr;
}
callback(null, result);
});
};
HeaderService.prototype.getLastHeader = function() {
assert(this._lastHeader, 'Last headers should be populated.');
return this._lastHeader;
};
HeaderService.prototype._getLastHeader = function(callback) {
var self = this;
// redo all headers
if (this._checkpoint === -1) {
this._checkpoint = this._tip.height;
}
if (self._tip.height >= self._checkpoint) {
self._tip.height -= self._checkpoint;
}
var removalOps = [];
var start = self._encoding.encodeHeaderHeightKey(self._tip.height);
var end = self._encoding.encodeHeaderHeightKey(0xffffffff);
log.info('Getting last header synced at height: ' + self._tip.height);
var criteria = {
gte: start,
lte: end
};
var stream = self._db.createReadStream(criteria);
var streamErr;
stream.on('error', function(error) {
streamErr = error;
});
stream.on('data', function(data) {
var header = self._encoding.decodeHeaderValue(data.value);
// any records with a height greater than our current tip height can be scheduled for removal
// because they will be replaced shortly
if (header.height > self._tip.height) {
removalOps.push({
type: 'del',
key: data.key
});
return;
} else if (header.height === self._tip.height) {
self._lastHeader = header;
}
});
stream.on('end', function() {
if (streamErr) {
return streamErr;
}
assert(self._lastHeader, 'The last synced header was not in the database.');
self._tip.hash = self._lastHeader.hash;
self._db.batch(removalOps, callback);
});
};
HeaderService.prototype._getChainwork = function(header, prevHeader) {
var prevChainwork = new BN(new Buffer(prevHeader.chainwork, 'hex'));
return this._computeChainwork(header.bits, prevChainwork);
};
HeaderService.prototype._computeChainwork = function(bits, prev) {
var target = consensus.fromCompact(bits);
if (target.isNeg() || target.cmpn(0) === 0) {
return new BN(0);
}
var proof = HeaderService.MAX_CHAINWORK.div(target.iaddn(1));
if (!prev) {
return proof;
}
return proof.iadd(prev);
};
module.exports = HeaderService;

View File

@ -0,0 +1,29 @@
'use strict';
var tx = require('bcoin').tx;
function Encoding(servicePrefix) {
this.servicePrefix = servicePrefix;
}
Encoding.prototype.encodeMempoolTransactionKey = function(txid) {
var buffers = [this.servicePrefix];
var txidBuffer = new Buffer(txid, 'hex');
buffers.push(txidBuffer);
return Buffer.concat(buffers);
};
Encoding.prototype.decodeMempoolTransactionKey = function(buffer) {
return buffer.slice(2).toString('hex');
};
Encoding.prototype.encodeMempoolTransactionValue = function(transaction) {
return transaction.toRaw();
};
Encoding.prototype.decodeMempoolTransactionValue = function(buffer) {
return tx.fromRaw(buffer);
};
module.exports = Encoding;

View File

@ -0,0 +1,158 @@
'use strict';
var BaseService = require('../../service');
var util = require('util');
var utils = require('../../utils');
var Encoding = require('./encoding');
var index = require('../../');
var log = index.log;
var MempoolService = function(options) {
BaseService.call(this, options);
this._subscriptions = {};
this._subscriptions.transaction = [];
this._db = this.node.services.db;
};
util.inherits(MempoolService, BaseService);
MempoolService.dependencies = ['db', 'block'];
MempoolService.prototype.getAPIMethods = function() {
var methods = [
['getMempoolTransaction', this, this.getMempoolTransaction, 1]
];
return methods;
};
MempoolService.prototype.getPublishEvents = function() {
return [
{
name: 'mempool/transaction',
scope: this,
subscribe: this.subscribe.bind(this, 'transaction'),
unsubscribe: this.unsubscribe.bind(this, 'transaction')
}
];
};
MempoolService.prototype.subscribe = function(name, emitter) {
this._subscriptions[name].push(emitter);
log.info(emitter.remoteAddress, 'subscribe:', 'mempool/' + name, 'total:', this._subscriptions[name].length);
};
MempoolService.prototype.unsubscribe = function(name, emitter) {
var index = this._subscriptions[name].indexOf(emitter);
if (index > -1) {
this._subscriptions[name].splice(index, 1);
}
log.info(emitter.remoteAddress, 'unsubscribe:', 'mempool/' + name, 'total:', this._subscriptions[name].length);
};
MempoolService.prototype.start = function(callback) {
var self = this;
self._db.getPrefix(self.name, function(err, prefix) {
if(err) {
return callback(err);
}
self._encoding = new Encoding(prefix);
self._startSubscriptions();
callback();
});
};
MempoolService.prototype.onReorg = function(args, callback) {
var oldBlockList = args[1];
var removalOps = [];
for(var i = 0; i < oldBlockList.length; i++) {
var block = oldBlockList[i];
for(var j = 0; j < block.txs.length; j++) {
var tx = block.txs[j];
var key = this._encoding.encodeMempoolTransactionKey(tx.txid());
var value = this._encoding.encodeMempoolTransactionValue(tx);
removalOps.push({
type: 'put',
key: key,
value: value
});
}
}
callback(null, removalOps);
};
MempoolService.prototype._startSubscriptions = function() {
if (this._subscribed) {
return;
}
this._subscribed = true;
if (!this._bus) {
this._bus = this.node.openBus({remoteAddress: 'localhost-mempool'});
}
this._bus.on('p2p/transaction', this._onTransaction.bind(this));
this._bus.subscribe('p2p/transaction');
};
MempoolService.prototype.onBlock = function(block, callback) {
// remove this block's txs from mempool
var self = this;
var ops = block.txs.map(function(tx) {
return {
type: 'del',
key: self._encoding.encodeMempoolTransactionKey(tx.txid())
};
});
callback(null, ops);
};
MempoolService.prototype._onTransaction = function(tx) {
this._db.put(this._encoding.encodeMempoolTransactionKey(tx.txid()),
this._encoding.encodeMempoolTransactionValue(tx));
};
MempoolService.prototype.getMempoolTransaction = function(txid, callback) {
var self = this;
self._db.get(self._encoding.encodeMempoolTransactionKey(txid), function(err, tx) {
if (err) {
return callback(err);
}
if (!tx) {
return callback();
}
callback(null, self._encoding.decodeMempoolTransactionValue(tx));
});
};
MempoolService.prototype.stop = function(callback) {
callback();
};
module.exports = MempoolService;

59
lib/services/p2p/bcoin.js Normal file
View File

@ -0,0 +1,59 @@
'use strict';
var index = require('../../');
var log = index.log;
var bcoin = require('bcoin');
var EE = require('events').EventEmitter;
var Bcoin = function(options) {
this._config = this._getConfig(options);
this.emitter = new EE();
};
Bcoin.prototype.start = function(done) {
var self = this;
self._bcoin = bcoin.fullnode(self._config);
log.info('Starting Bcoin full node...');
self._bcoin.open().then(function() {
self._bcoin.connect().then(function() {
log.info('Waiting for Bcoin to sync');
self._bcoin.startSync();
if (self._bcoin.chain.synced){
return done();
}
self._bcoin.chain.once('full', function() {
done();
});
});
});
};
Bcoin.prototype.stop = function() {
this._bcoin.stopSync();
this._bcoin.disconnect();
this._bcoin.close();
};
// --- privates
Bcoin.prototype._getConfig = function(options) {
var config = {
db: 'leveldb',
checkpoints: true,
network: options.network || 'main',
listen: true,
logConsole: true,
logLevel: 'info',
port: options.port,
persistent: true,
workers: true
};
if (options.prefix) {
config.prefix = options.prefix;
}
return config;
};
module.exports = Bcoin;

View File

362
lib/services/p2p/index.js Normal file
View File

@ -0,0 +1,362 @@
'use strict';
var p2p = require('bitcore-p2p');
var LRU = require('lru-cache');
var util = require('util');
var index = require('../../');
var log = index.log;
var BaseService = require('../../service');
var assert = require('assert');
var Bcoin = require('./bcoin');
var Networks = require('bitcore-lib').Networks;
var P2P = function(options) {
if (!(this instanceof P2P)) {
return new P2P(options);
}
BaseService.call(this, options);
this._options = options;
this._initP2P();
this._initPubSub();
this._bcoin = null;
this._currentBestHeight = null;
this._latestBits = 0x1d00ffff;
};
util.inherits(P2P, BaseService);
P2P.dependencies = [];
P2P.prototype.clearInventoryCache = function() {
this._inv.reset();
};
P2P.prototype.getAPIMethods = function() {
var methods = [
['clearInventoryCache', this, this.clearInventoryCache, 0],
['getBlocks', this, this.getBlocks, 1],
['getHeaders', this, this.getHeaders, 1],
['getMempool', this, this.getMempool, 0],
['sendTransaction', this, this.sendTransaction, 1]
];
return methods;
};
P2P.prototype.getNumberOfPeers = function() {
return this._pool.numberConnected;
};
P2P.prototype.getBlocks = function(filter) {
var peer = this._getPeer();
var blockFilter = this._setResourceFilter(filter, 'blocks');
peer.sendMessage(this.messages.GetBlocks(blockFilter));
};
P2P.prototype.getHeaders = function(filter) {
var peer = this._getPeer();
var headerFilter = this._setResourceFilter(filter, 'headers');
peer.sendMessage(this.messages.GetHeaders(headerFilter));
};
P2P.prototype.getMempool = function(filter) {
var peer = this._getPeer();
this._setResourceFilter(filter, 'mempool');
peer.sendMessage(this.messages.MemPool());
};
P2P.prototype.getPublishEvents = function() {
return [
{
name: 'p2p/transaction',
scope: this,
subscribe: this.subscribe.bind(this, 'transaction'),
unsubscribe: this.unsubscribe.bind(this, 'transaction')
},
{
name: 'p2p/block',
scope: this,
subscribe: this.subscribe.bind(this, 'block'),
unsubscribe: this.unsubscribe.bind(this, 'block')
},
{
name: 'p2p/headers',
scope: this,
subscribe: this.subscribe.bind(this, 'headers'),
unsubscribe: this.unsubscribe.bind(this, 'headers')
}
];
};
P2P.prototype.sendTransaction = function(tx) {
p2p.sendMessage(this.messages.Inventory(tx));
};
P2P.prototype.start = function(callback) {
var self = this;
self._startBcoinIfNecessary(function(){
self._initCache();
self._initPool();
self._setListeners();
callback();
});
};
P2P.prototype._disconnectPool = function() {
log.info('P2P Service: disconnecting pool and peers. SIGINT issued, system shutdown initiated');
this._pool.disconnect();
};
P2P.prototype.stop = function(callback) {
if (this._bcoin){
return this._bcoin.stop(callback);
}
setImmediate(callback);
};
P2P.prototype.subscribe = function(name, emitter) {
this.subscriptions[name].push(emitter);
log.info(emitter.remoteAddress, 'subscribe:', 'p2p/' + name, 'total:', this.subscriptions[name].length);
};
P2P.prototype.unsubscribe = function(name, emitter) {
var index = this.subscriptions[name].indexOf(emitter);
if (index > -1) {
this.subscriptions[name].splice(index, 1);
}
log.info(emitter.remoteAddress, 'unsubscribe:', 'p2p/' + name, 'total:', this.subscriptions[name].length);
};
// --- privates
P2P.prototype._addPeer = function(peer) {
this._peers.push(peer);
};
P2P.prototype._applyMempoolFilter = function(message) {
if (!this._mempoolFilter) {
return message;
}
var txIndex = this._mempoolFilter.indexOf(message.transaction.hash);
if (txIndex >= 0) {
this._mempoolFilter.splice(txIndex, 1);
return;
}
return message;
};
P2P.prototype._broadcast = function(subscribers, name, entity) {
for (var i = 0; i < subscribers.length; i++) {
subscribers[i].emit(name, entity);
}
};
P2P.prototype._connect = function() {
var self = this;
log.info('Connecting to p2p network.');
self._pool.connect();
var retryInterval = setInterval(function() {
self._pool.connect();
}, 5000);
self._pool.once('peerready', function() {
clearInterval(retryInterval);
});
};
P2P.prototype._getBestHeight = function() {
if (this._peers === 0) {
return 0;
}
var maxHeight = -1;
for(var i = 0; i < this._peers.length; i++) {
if (this._peers[i].bestHeight > maxHeight) {
maxHeight = this._peers[i].bestHeight;
this._peer = this._peers[i];
}
}
return maxHeight;
};
// we should only choose from a list of peers that sync'ed
P2P.prototype._getPeer = function() {
return this._peer;
};
P2P.prototype._hasPeers = function() {
return this._options &&
this._options.peers &&
this._options.peers.length > 0;
};
P2P.prototype._initCache = function() {
this._inv = LRU(1000);
};
P2P.prototype._initP2P = function() {
this._maxPeers = this._options.maxPeers || 60;
this._minPeers = this._options.minPeers || 0;
this._configPeers = this._options.peers;
if (this.node.network === 'regtest') {
Networks.enableRegtest();
}
this.messages = new p2p.Messages({ network: Networks.get(this.node.network) });
this._peerHeights = [];
this._peers = [];
this._peerIndex = 0;
this._mempoolFilter = [];
};
P2P.prototype._initPool = function() {
var opts = {};
if (this._configPeers) {
opts.addrs = this._configPeers;
}
opts.dnsSeed = false;
opts.maxPeers = this._maxPeers;
opts.network = this.node.network;
this._pool = new p2p.Pool(opts);
};
P2P.prototype._initPubSub = function() {
this.subscriptions = {};
this.subscriptions.block = [];
this.subscriptions.headers = [];
this.subscriptions.transaction = [];
};
P2P.prototype._onPeerBlock = function(peer, message) {
this._broadcast(this.subscriptions.block, 'p2p/block', message.block);
};
P2P.prototype._onPeerDisconnect = function(peer, addr) {
if (!this.node.stopping) {
this._connect();
return;
}
this._removePeer(peer);
log.info('Disconnected from peer: ' + addr.ip.v4);
};
P2P.prototype._onPeerHeaders = function(peer, message) {
this._broadcast(this.subscriptions.headers, 'p2p/headers', message.headers);
};
P2P.prototype._onPeerInventory = function(peer, message) {
var self = this;
var newDataNeeded = [];
message.inventory.forEach(function(inv) {
if (!self._inv.get(inv.hash)) {
self._inv.set(inv.hash, true);
newDataNeeded.push(inv);
}
});
if (newDataNeeded.length > 0) {
peer.sendMessage(self.messages.GetData(newDataNeeded));
}
};
P2P.prototype._onPeerReady = function(peer, addr) {
log.info('Connected to peer: ' + addr.ip.v4 + ', network: ' +
peer.network.alias + ', version: ' + peer.version + ', subversion: ' +
peer.subversion + ', status: ' + peer.status + ', port: ' +
peer.port + ', best height: ' + peer.bestHeight);
this._addPeer(peer);
var bestHeight = this._getBestHeight();
if (bestHeight >= 0) {
this.emit('bestHeight', bestHeight);
}
};
P2P.prototype._onPeerTx = function(peer, message) {
var filteredMessage = this._applyMempoolFilter(message);
if (filteredMessage) {
this._broadcast(this.subscriptions.transaction, 'p2p/transaction', message.transaction);
}
};
P2P.prototype._removePeer = function(peer) {
this._peers.splice(this._peers.indexOf(peer), 1);
};
P2P.prototype._setListeners = function() {
var self = this;
self.node.on('stopping', self._disconnectPool.bind(self));
self._pool.on('peerready', self._onPeerReady.bind(self));
self._pool.on('peerdisconnect', self._onPeerDisconnect.bind(self));
self._pool.on('peerinv', self._onPeerInventory.bind(self));
self._pool.on('peertx', self._onPeerTx.bind(self));
self._pool.on('peerblock', self._onPeerBlock.bind(self));
self._pool.on('peerheaders', self._onPeerHeaders.bind(self));
self.node.on('ready', self._connect.bind(self));
};
P2P.prototype._setResourceFilter = function(filter, resource) {
if (resource === 'headers' || resource === 'blocks') {
assert(filter && filter.startHash, 'A "startHash" field is required to retrieve headers or blocks');
if (!filter.endHash) {
filter.endHash = 0;
}
return { starts: [filter.startHash], stop: filter.endHash };
}
if (resource === 'mempool') {
this._mempoolFilter = filter;
return;
}
};
P2P.prototype._startBcoin = function(callback) {
var self = this;
const network = ['livenet', 'live', 'main', 'mainnet'].indexOf(this.node.network) !== -1? 'main' : 'testnet';
self._bcoin = new Bcoin({
network: network,
prefix: self.node.datadir,
port: 48333
});
self._bcoin.start(callback);
};
P2P.prototype._startBcoinIfNecessary = function(callback) {
if (!this._hasPeers()) {
log.info('Peers not explicitly configured, starting a local bcoin node.');
this._configPeers = [{ip: {v4: '127.0.0.1'}, port: 48333}];
return this._startBcoin(callback);
}
setImmediate(callback);
};
module.exports = P2P;

View File

@ -0,0 +1,48 @@
'use strict';
function Encoding(servicePrefix) {
this._servicePrefix = servicePrefix;
this._blockPrefix = new Buffer('00', 'hex');
this._timestampPrefix = new Buffer('01', 'hex');
}
// ---- block hash -> timestamp
Encoding.prototype.encodeBlockTimestampKey = function(hash) {
return Buffer.concat([this._servicePrefix, this._blockPrefix, new Buffer(hash, 'hex')]);
};
Encoding.prototype.decodeBlockTimestampKey = function(buffer) {
return buffer.slice(3).toString('hex');
};
Encoding.prototype.encodeBlockTimestampValue = function(timestamp) {
var timestampBuffer = new Buffer(4);
timestampBuffer.writeUInt32BE(timestamp);
return timestampBuffer;
};
Encoding.prototype.decodeBlockTimestampValue = function(buffer) {
return buffer.readUInt32BE();
};
// ---- timestamp -> block hash
Encoding.prototype.encodeTimestampBlockKey = function(timestamp) {
var timestampBuffer = new Buffer(4);
timestampBuffer.writeUInt32BE(timestamp);
return Buffer.concat([this._servicePrefix, this._timestampPrefix, timestampBuffer]);
};
Encoding.prototype.decodeTimestampBlockKey = function(buffer) {
return buffer.readUInt32BE(3);
};
Encoding.prototype.encodeTimestampBlockValue = function(hash) {
return new Buffer(hash, 'hex');
};
Encoding.prototype.decodeTimestampBlockValue = function(buffer) {
return buffer.toString('hex');
};
module.exports = Encoding;

View File

@ -0,0 +1,180 @@
'use strict';
var BaseService = require('../../service');
var Encoding = require('./encoding');
var assert = require('assert');
var _ = require('lodash');
var LRU = require('lru-cache');
var inherits = require('util').inherits;
function TimestampService(options) {
BaseService.call(this, options);
this._db = this.node.services.db;
this._lastBlockTimestamp = 0;
this._cache = new LRU(10);
}
inherits(TimestampService, BaseService);
TimestampService.dependencies = [ 'db' ];
TimestampService.prototype.getAPIMethods = function() {
return [
['getBlockHashesByTimestamp', this, this.getBlockHashesByTimestamp, 2]
];
};
TimestampService.prototype.getBlockHashesByTimestamp = function(high, low, callback) {
assert(_.isNumber(low) && _.isNumber(high) && low < high,
'start time and end time must be integers representing the number of seconds since epoch.');
var self = this;
var result = [];
var start = self._encoding.encodeTimestampBlockKey(low);
var end = self._encoding.encodeTimestampBlockKey(high);
var criteria = {
gte: start,
lte: end
};
var tsStream = self._db.createReadStream(criteria);
tsStream.on('data', function(data) {
var value = self._encoding.decodeTimestampBlockValue(data.value);
result.push(value);
});
var streamErr;
tsStream.on('error', function(err) {
streamErr = err;
});
tsStream.on('end', function() {
if(streamErr) {
return callback(streamErr);
}
if (!result) {
return callback();
}
return callback(null, result);
});
};
TimestampService.prototype.start = function(callback) {
var self = this;
self._db.getPrefix(self.name, function(err, prefix) {
if(err) {
return callback(err);
}
self._prefix = prefix;
self._encoding = new Encoding(self._prefix);
callback();
});
};
TimestampService.prototype.onBlock = function(block, callback) {
var operations = [];
var ts = block.ts;
var hash = block.rhash();
if (ts <= this._lastBlockTimestamp) {
ts = this._lastBlockTimestamp + 1;
}
this._lastBlockTimestamp = ts;
this._cache.set(hash, ts);
operations = operations.concat([
{
type: 'put',
key: this._encoding.encodeTimestampBlockKey(ts),
value: this._encoding.encodeTimestampBlockValue(hash)
},
{
type: 'put',
key: this._encoding.encodeBlockTimestampKey(hash),
value: this._encoding.encodeBlockTimestampValue(ts)
}
]);
callback(null, operations);
};
TimestampService.prototype.onReorg = function(args, callback) {
var self = this;
var commonAncestorHeader = args[0];
var oldBlockList = args[1];
var removalOps = [];
// remove all the old blocks that we reorg from
oldBlockList.forEach(function(block) {
removalOps.concat([
{
type: 'del',
key: self._encoding.encodeTimestampBlockKey(block.ts),
},
{
type: 'del',
key: self._encoding.encodeBlockTimestampKey(block.rhash()),
}
]);
});
// look up the adjusted timestamp from our own database and set the lastTimestamp to it
self.getTimestamp(commonAncestorHeader.hash, function(err, timestamp) {
if (err) {
return callback(err);
}
self._lastBlockTimestamp = timestamp;
callback(null, removalOps);
});
};
TimestampService.prototype.getTimestampSync = function(hash) {
return this._cache.get(hash);
};
TimestampService.prototype.getTimestamp = function(hash, callback) {
var self = this;
self._db.get(self._encoding.encodeBlockTimestampKey(hash), function(err, data) {
if (err) {
return callback(err);
}
callback(null, self._encoding.decodeBlockTimestampValue(data));
});
};
TimestampService.prototype.getHash = function(timestamp, callback) {
var self = this;
self._db.get(self._encoding.encodeTimestampBlockKey(timestamp), function(err, data) {
if (err) {
return callback(err);
}
callback(null, self._encoding.decodeTimestampBlockValue(data));
});
};
module.exports = TimestampService;

View File

@ -0,0 +1,57 @@
'use strict';
var Tx = require('bcoin').tx;
function Encoding(servicePrefix) {
this.servicePrefix = servicePrefix;
}
Encoding.prototype.encodeTransactionKey = function(txid) {
return Buffer.concat([this.servicePrefix, new Buffer(txid, 'hex')]);
};
Encoding.prototype.decodeTransactionKey = function(buffer) {
return buffer.slice(2).toString('hex');
};
Encoding.prototype.encodeTransactionValue = function(transaction) {
var heightBuffer = new Buffer(4);
heightBuffer.writeUInt32BE(transaction.__height);
var timestampBuffer = new Buffer(4);
timestampBuffer.writeUInt32BE(transaction.__timestamp);
var inputValues = transaction.__inputValues;
var inputValuesBuffer = new Buffer(8 * inputValues.length);
for(var i = 0; i < inputValues.length; i++) {
inputValuesBuffer.writeDoubleBE(inputValues[i], i * 8);
}
var inputValuesLengthBuffer = new Buffer(2);
inputValuesLengthBuffer.writeUInt16BE(inputValues.length);
return new Buffer.concat([heightBuffer, timestampBuffer,
inputValuesLengthBuffer, inputValuesBuffer, transaction.toRaw()]);
};
Encoding.prototype.decodeTransactionValue = function(buffer) {
var height = buffer.readUInt32BE();
var timestamp = buffer.readUInt32BE(4);
var inputValuesLength = buffer.readUInt16BE(8);
var inputValues = [];
for(var i = 0; i < inputValuesLength; i++) {
inputValues.push(buffer.readDoubleBE(i * 8 + 10));
}
var txBuf = buffer.slice(inputValues.length * 8 + 10);
var transaction = Tx.fromRaw(txBuf);
transaction.__height = height;
transaction.__inputValues = inputValues;
transaction.__timestamp = timestamp;
return transaction;
};
module.exports = Encoding;

View File

@ -0,0 +1,321 @@
'use strict';
var BaseService = require('../../service');
var inherits = require('util').inherits;
var Encoding = require('./encoding');
var utils = require('../../utils');
var _ = require('lodash');
var log = require('../../index').log;
var async = require('async');
var assert = require('assert');
function TransactionService(options) {
BaseService.call(this, options);
this._db = this.node.services.db;
this._mempool = this.node.services.mempool;
this._block = this.node.services.block;
this._header = this.node.services.header;
this._p2p = this.node.services.p2p;
this._timestamp = this.node.services.timestamp;
}
inherits(TransactionService, BaseService);
TransactionService.dependencies = [
'p2p',
'db',
'block',
'timestamp',
'mempool'
];
// ---- start public function protorypes
TransactionService.prototype.getAPIMethods = function() {
return [
['getRawTransaction', this, this.getRawTransaction, 1],
['getTransaction', this, this.getTransaction, 1],
['getDetailedTransaction', this, this.getDetailedTransaction, 1],
['getInputValues', this, this.getInputValues, 1]
];
};
TransactionService.prototype.getDetailedTransaction = function(txid, options, callback) {
this.getTransaction(txid, options, callback);
};
TransactionService.prototype.getTransaction = function(txid, options, callback) {
var self = this;
if (typeof callback !== 'function') {
callback = options;
}
async.waterfall([
function(next) {
self._getTransaction(txid, options, next);
},
self._getMempoolTransaction.bind(self),
self.getInputValues.bind(self),
self._setMetaInfo.bind(self)
], callback);
};
TransactionService.prototype._setMetaInfo = function(tx, options, callback) {
if (!tx) {
return callback();
}
// output values
var outputSatoshis = 0;
tx.outputs.forEach(function(output) {
outputSatoshis += output.value;
});
tx.outputSatoshis = outputSatoshis;
//input values
if (!tx.inputs[0].isCoinbase()) {
var inputSatoshis = 0;
tx.__inputValues.forEach(function(val) {
if (val >+ 0) {
inputSatoshis += val;
}
});
var feeSatoshis = inputSatoshis - outputSatoshis;
tx.inputSatoshis = inputSatoshis;
tx.feeSatoshis = feeSatoshis;
}
callback(null, tx);
};
TransactionService.prototype._getMempoolTransaction = function(txid, tx, options, callback) {
var self = this;
var queryMempool = _.isUndefined(options.queryMempool) ? true : options.queryMempool;
if (tx || !queryMempool) {
return callback(null, tx, options);
}
self._mempool.getMempoolTransaction(txid, function(err, tx) {
if (err) {
return callback(err);
}
if (!tx) {
return callback(null, tx, options);
}
tx.confirmations = 0;
callback(null, tx, options);
});
};
TransactionService.prototype._getTransaction = function(txid, options, callback) {
var self = this;
var key = self._encoding.encodeTransactionKey(txid);
self._db.get(key, function(err, tx) {
if (err) {
return callback(err);
}
if (!tx) {
return callback(null, txid, tx, options);
}
tx = self._encoding.decodeTransactionValue(tx);
tx.confirmations = self._header.getBestHeight() - tx.__height;
self._header.getBlockHeader(tx.__height, function(err, header) {
if (err) {
return callback(err);
}
if (header) {
tx.blockHash = header.hash;
}
callback(null, txid, tx, options);
});
});
};
TransactionService.prototype.getInputValues = function(tx, options, callback) {
var self = this;
if (!tx) {
return callback(null, tx, options);
}
async.eachOfLimit(tx.inputs, 4, function(input, index, next) {
if (!tx.__inputValues) {
tx.__inputValues = [];
}
var inputSatoshis = tx.__inputValues[index];
if (inputSatoshis >= 0 || input.isCoinbase()) {
return next();
}
var outputIndex = input.prevout.index;
self._getTransaction(input.prevout.txid(), options, function(err, txid, _tx) {
if (err || !_tx) {
return next(err || new Error('tx not found for tx id: ' + input.prevout.txid()));
}
var output = _tx.outputs[outputIndex];
assert(output, 'Expected an output, but did not get one for tx: ' + _tx.txid() + ' outputIndex: ' + outputIndex);
tx.__inputValues[index] = output.value;
next();
});
}, function(err) {
if (err) {
return callback(err);
}
var key = self._encoding.encodeTransactionKey(tx.txid());
var value = self._encoding.encodeTransactionValue(tx);
self._db.put(key, value, function(err) {
if (err) {
return callback(err);
}
callback(null, tx, options);
});
});
};
TransactionService.prototype.sendTransaction = function(tx, callback) {
this._p2p.sendTransaction(tx, callback);
};
TransactionService.prototype.start = function(callback) {
var self = this;
self._db.getPrefix(self.name, function(err, prefix) {
if(err) {
return callback(err);
}
self.prefix = prefix;
self._encoding = new Encoding(self.prefix);
callback();
});
};
TransactionService.prototype.stop = function(callback) {
setImmediate(callback);
};
// --- start private prototype functions
TransactionService.prototype._getBlockTimestamp = function(hash) {
return this._timestamp.getTimestampSync(hash);
};
TransactionService.prototype.onBlock = function(block, callback) {
var self = this;
if (self.node.stopping) {
return callback();
}
var operations = block.txs.map(function(tx) {
return self._processTransaction(tx, { block: block });
});
callback(null, operations);
};
TransactionService.prototype.onReorg = function(args, callback) {
var self = this;
var oldBlockList = args[1];
var removalOps = [];
for(var i = 0; i < oldBlockList.length; i++) {
var block = oldBlockList[i];
for(var j = 0; j < block.txs.length; j++) {
var tx = block.txs[j];
removalOps.push({
type: 'del',
key: self._encoding.encodeTransactionKey(tx.txid())
});
}
}
callback(null, removalOps);
};
TransactionService.prototype._processTransaction = function(tx, opts) {
// this index is very simple txid -> tx, but we also need to find each
// input's prev output value, the adjusted timestamp for the block and
// the tx's block height
// input values
tx.__inputValues = []; // these are lazy-loaded on the first access of the tx
// timestamp
tx.__timestamp = this._getBlockTimestamp(opts.block.rhash());
assert(tx.__timestamp, 'Timestamp is required when saving a transaction.');
// height
tx.__height = opts.block.height;
assert(tx.__height, 'Block height is required when saving a trasnaction.');
return {
key: this._encoding.encodeTransactionKey(tx.txid()),
value: this._encoding.encodeTransactionValue(tx)
};
};
module.exports = TransactionService;

View File

@ -8,10 +8,10 @@ var bodyParser = require('body-parser');
var socketio = require('socket.io');
var inherits = require('util').inherits;
var BaseService = require('../service');
var BaseService = require('../../service');
var bitcore = require('bitcore-lib');
var _ = bitcore.deps._;
var index = require('../');
var index = require('../../');
var log = index.log;
@ -50,6 +50,7 @@ var WebService = function(options) {
self.server.listen(self.port);
self.createMethodsMap();
});
BaseService.call(this, options);
};
inherits(WebService, BaseService);

View File

@ -1,32 +1,23 @@
'use strict';
var MAX_SAFE_INTEGER = 0x1fffffffffffff; // 2 ^ 53 - 1
var _ = require('lodash');
var constants = require('./constants');
var BN = require('bn.js');
var utils = {};
utils.isHash = function isHash(value) {
return typeof value === 'string' && value.length === 64 && /^[0-9a-fA-F]+$/.test(value);
};
utils.isSafeNatural = function isSafeNatural(value) {
return typeof value === 'number' &&
isFinite(value) &&
Math.floor(value) === value &&
value >= 0 &&
value <= MAX_SAFE_INTEGER;
};
utils.startAtZero = function startAtZero(obj, key) {
if (!obj.hasOwnProperty(key)) {
obj[key] = 0;
}
utils.isHeight = function(blockArg) {
return _.isNumber(blockArg) || (blockArg.length < 40 && /^[0-9]+$/.test(blockArg));
};
//start
utils.isAbsolutePath = require('path').isAbsolute;
if (!utils.isAbsolutePath) {
utils.isAbsolutePath = require('path-is-absolute');
}
utils.parseParamsWithJSON = function parseParamsWithJSON(paramsArg) {
//main
utils.parseParamsWithJSON = function(paramsArg) {
var params = paramsArg.map(function(paramArg) {
var param;
try {
@ -39,4 +30,77 @@ utils.parseParamsWithJSON = function parseParamsWithJSON(paramsArg) {
return params;
};
utils.getTerminalKey = function(startKey) {
if (!startKey || !Buffer.isBuffer(startKey)) {
return;
}
var bn = new BN(startKey);
var endBN = bn.iaddn(1);
return endBN.toBuffer();
};
utils.diffTime = function(time) {
var diff = process.hrtime(time);
return (diff[0] * 1E9 + diff[1])/(1E9 * 1.0);
};
utils.sendError = function(err, res) {
if (err.statusCode) {
res.status(err.statusCode).send(err.message);
} else {
console.error(err.stack);
res.status(503).send(err.message);
}
};
utils.encodeTip = function(tip, name) {
var key = Buffer.concat([ constants.DB_PREFIX,
new Buffer('tip-' + name, 'utf8') ]);
var heightBuf = new Buffer(4);
heightBuf.writeUInt32BE(tip.height);
var value = Buffer.concat([ heightBuf, new Buffer(tip.hash, 'hex') ]);
return { key: key, value: value };
};
utils.SimpleMap = function SimpleMap() {
var object = {};
var array = [];
this.size = 0;
this.length = 0;
this.hasNullItems = function() {
return array.length !== _.compact(array).length;
};
this.get = function (key) {
return array[object[key]];
};
this.set = function (key, value, pos) {
if (pos >= 0) {
object[key] = pos;
array[pos] = value;
} else {
object[key] = array.length;
array.push(value);
}
this.size = array.length;
this.length = array.length;
};
this.getIndex = function (index) {
return array[index];
};
this.getLastIndex = function () {
return array[array.length - 1];
};
};
module.exports = utils;

3929
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,11 @@
{
"name": "bitcore-node",
"description": "Full node with extended capabilities using Bitcore and Bitcoin Core",
"engines": {
"node": ">=8.2.0"
},
"author": "BitPay <dev@bitpay.com>",
"version": "3.1.3",
"version": "5.0.0",
"main": "./index.js",
"repository": "git://github.com/bitpay/bitcore-node.git",
"homepage": "https://github.com/bitpay/bitcore-node",
@ -27,53 +30,52 @@
}
],
"bin": {
"bitcore-node": "./bin/bitcore-node",
"bitcoind": "./bin/bitcoind"
"bitcore-node": "./bin/bitcore-node"
},
"scripts": {
"preinstall": "./scripts/download",
"verify": "./scripts/download --skip-bitcoin-download --verify-bitcoin-download",
"test": "mocha -R spec --recursive",
"regtest": "./scripts/regtest",
"jshint": "jshint --reporter=node_modules/jshint-stylish ./lib",
"coverage": "istanbul cover _mocha -- --recursive",
"coveralls": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- --recursive -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js"
},
"tags": [
"bitcoin",
"bitcoind"
"bitcoind",
"bcoin",
"bitcoin full node",
"bitcoin index",
"block explorer",
"wallet backend"
],
"dependencies": {
"async": "^1.3.0",
"async": "^2.5.0",
"bcoin": "bcoin-org/bcoin#886008a1822ce1da7fa8395ee7db4bcc1750a28a",
"bitcoind-rpc": "^0.6.0",
"bitcore-lib": "^0.13.13",
"bitcore-lib": "bitpay/bitcore-lib#transitional",
"bitcore-p2p": "bitpay/bitcore-p2p#bcoin",
"body-parser": "^1.13.3",
"colors": "^1.1.2",
"commander": "^2.8.1",
"errno": "^0.1.4",
"express": "^4.13.3",
"leveldown": "",
"levelup": "",
"liftoff": "^2.2.0",
"lru-cache": "^4.0.1",
"lodash": "^4.17.4",
"lru-cache": "^4.0.2",
"memwatch-next": "^0.3.0",
"mkdirp": "0.5.0",
"path-is-absolute": "^1.0.0",
"semver": "^5.0.1",
"socket.io": "^1.4.5",
"socket.io-client": "^1.4.5",
"zmq": "^2.14.0"
},
"optionalDependencies": {
"bufferutil": "~1.2.1",
"utf-8-validate": "~1.2.1"
"socket.io-client": "^1.4.5"
},
"devDependencies": {
"benchmark": "1.0.0",
"bitcore-p2p": "^1.1.0",
"chai": "^3.5.0",
"coveralls": "^2.11.9",
"istanbul": "^0.4.3",
"jshint": "^2.9.2",
"jshint-stylish": "^2.1.0",
"mocha": "^2.4.5",
"mocha": "",
"proxyquire": "^1.3.1",
"rimraf": "^2.4.2",
"sinon": "^1.15.4"

View File

@ -1,485 +0,0 @@
'use strict';
// To run the tests: $ mocha -R spec regtest/bitcoind.js
var path = require('path');
var index = require('..');
var log = index.log;
var chai = require('chai');
var bitcore = require('bitcore-lib');
var BN = bitcore.crypto.BN;
var async = require('async');
var rimraf = require('rimraf');
var bitcoind;
/* jshint unused: false */
var should = chai.should();
var assert = chai.assert;
var sinon = require('sinon');
var BitcoinRPC = require('bitcoind-rpc');
var transactionData = [];
var blockHashes = [];
var utxos;
var client;
var coinbasePrivateKey;
var privateKey = bitcore.PrivateKey();
var destKey = bitcore.PrivateKey();
describe('Bitcoind Functionality', function() {
before(function(done) {
this.timeout(60000);
// Add the regtest network
bitcore.Networks.enableRegtest();
var regtestNetwork = bitcore.Networks.get('regtest');
var datadir = __dirname + '/data';
rimraf(datadir + '/regtest', function(err) {
if (err) {
throw err;
}
bitcoind = require('../').services.Bitcoin({
spawn: {
datadir: datadir,
exec: path.resolve(__dirname, '../bin/bitcoind')
},
node: {
network: regtestNetwork,
getNetworkName: function() {
return 'regtest';
}
}
});
bitcoind.on('error', function(err) {
log.error('error="%s"', err.message);
});
log.info('Waiting for Bitcoin Core to initialize...');
bitcoind.start(function() {
log.info('Bitcoind started');
client = new BitcoinRPC({
protocol: 'http',
host: '127.0.0.1',
port: 30331,
user: 'bitcoin',
pass: 'local321',
rejectUnauthorized: false
});
log.info('Generating 100 blocks...');
// Generate enough blocks so that the initial coinbase transactions
// can be spent.
setImmediate(function() {
client.generate(150, function(err, response) {
if (err) {
throw err;
}
blockHashes = response.result;
log.info('Preparing test data...');
// Get all of the unspent outputs
client.listUnspent(0, 150, function(err, response) {
utxos = response.result;
async.mapSeries(utxos, function(utxo, next) {
async.series([
function(finished) {
// Load all of the transactions for later testing
client.getTransaction(utxo.txid, function(err, txresponse) {
if (err) {
throw err;
}
// add to the list of transactions for testing later
transactionData.push(txresponse.result.hex);
finished();
});
},
function(finished) {
// Get the private key for each utxo
client.dumpPrivKey(utxo.address, function(err, privresponse) {
if (err) {
throw err;
}
utxo.privateKeyWIF = privresponse.result;
finished();
});
}
], next);
}, function(err) {
if (err) {
throw err;
}
done();
});
});
});
});
});
});
});
after(function(done) {
this.timeout(60000);
bitcoind.node.stopping = true;
bitcoind.stop(function(err, result) {
done();
});
});
describe('get blocks by hash', function() {
[0,1,2,3,5,6,7,8,9].forEach(function(i) {
it('generated block ' + i, function(done) {
bitcoind.getBlock(blockHashes[i], function(err, block) {
if (err) {
throw err;
}
should.exist(block);
block.hash.should.equal(blockHashes[i]);
done();
});
});
});
});
describe('get blocks as buffers', function() {
[0,1,2,3,5,6,7,8,9].forEach(function(i) {
it('generated block ' + i, function(done) {
bitcoind.getRawBlock(blockHashes[i], function(err, block) {
if (err) {
throw err;
}
should.exist(block);
(block instanceof Buffer).should.equal(true);
done();
});
});
});
});
describe('get errors as error instances', function() {
it('will wrap an rpc into a javascript error', function(done) {
bitcoind.client.getBlock(1000000000, function(err, response) {
var error = bitcoind._wrapRPCError(err);
(error instanceof Error).should.equal(true);
error.message.should.equal(err.message);
error.code.should.equal(err.code);
should.exist(error.stack);
done();
});
});
});
describe('get blocks by height', function() {
[0,1,2,3,4,5,6,7,8,9].forEach(function(i) {
it('generated block ' + i, function(done) {
// add the genesis block
var height = i + 1;
bitcoind.getBlock(i + 1, function(err, block) {
if (err) {
throw err;
}
should.exist(block);
block.hash.should.equal(blockHashes[i]);
done();
});
});
});
it('will get error with number greater than tip', function(done) {
bitcoind.getBlock(1000000000, function(err, response) {
should.exist(err);
err.code.should.equal(-8);
done();
});
});
});
describe('get transactions by hash', function() {
[0,1,2,3,4,5,6,7,8,9].forEach(function(i) {
it('for tx ' + i, function(done) {
var txhex = transactionData[i];
var tx = new bitcore.Transaction();
tx.fromString(txhex);
bitcoind.getTransaction(tx.hash, function(err, response) {
if (err) {
throw err;
}
assert(response.toString('hex') === txhex, 'incorrect tx data result');
done();
});
});
});
it('will return error if the transaction does not exist', function(done) {
var txid = '6226c407d0e9705bdd7158e60983e37d0f5d23529086d6672b07d9238d5aa618';
bitcoind.getTransaction(txid, function(err, response) {
should.exist(err);
done();
});
});
});
describe('get transactions as buffers', function() {
[0,1,2,3,4,5,6,7,8,9].forEach(function(i) {
it('for tx ' + i, function(done) {
var txhex = transactionData[i];
var tx = new bitcore.Transaction();
tx.fromString(txhex);
bitcoind.getRawTransaction(tx.hash, function(err, response) {
if (err) {
throw err;
}
response.should.be.instanceOf(Buffer);
assert(response.toString('hex') === txhex, 'incorrect tx data result');
done();
});
});
});
it('will return error if the transaction does not exist', function(done) {
var txid = '6226c407d0e9705bdd7158e60983e37d0f5d23529086d6672b07d9238d5aa618';
bitcoind.getRawTransaction(txid, function(err, response) {
should.exist(err);
done();
});
});
});
describe('get block header', function() {
var expectedWork = new BN(6);
[1,2,3,4,5,6,7,8,9].forEach(function(i) {
it('generate block ' + i, function(done) {
bitcoind.getBlockHeader(blockHashes[i], function(err, blockIndex) {
if (err) {
return done(err);
}
should.exist(blockIndex);
should.exist(blockIndex.chainWork);
var work = new BN(blockIndex.chainWork, 'hex');
work.toString(16).should.equal(expectedWork.toString(16));
expectedWork = expectedWork.add(new BN(2));
should.exist(blockIndex.prevHash);
blockIndex.hash.should.equal(blockHashes[i]);
blockIndex.prevHash.should.equal(blockHashes[i - 1]);
blockIndex.height.should.equal(i + 1);
done();
});
});
});
it('will get null prevHash for the genesis block', function(done) {
bitcoind.getBlockHeader(0, function(err, header) {
if (err) {
return done(err);
}
should.exist(header);
should.equal(header.prevHash, undefined);
done();
});
});
it('will get error for block not found', function(done) {
bitcoind.getBlockHeader('notahash', function(err, header) {
should.exist(err);
done();
});
});
});
describe('get block index by height', function() {
var expectedWork = new BN(6);
[2,3,4,5,6,7,8,9].forEach(function(i) {
it('generate block ' + i, function() {
bitcoind.getBlockHeader(i, function(err, header) {
should.exist(header);
should.exist(header.chainWork);
var work = new BN(header.chainWork, 'hex');
work.toString(16).should.equal(expectedWork.toString(16));
expectedWork = expectedWork.add(new BN(2));
should.exist(header.prevHash);
header.hash.should.equal(blockHashes[i - 1]);
header.prevHash.should.equal(blockHashes[i - 2]);
header.height.should.equal(i);
});
});
});
it('will get error with number greater than tip', function(done) {
bitcoind.getBlockHeader(100000, function(err, header) {
should.exist(err);
done();
});
});
});
describe('send transaction functionality', function() {
it('will not error and return the transaction hash', function(done) {
// create and sign the transaction
var tx = bitcore.Transaction();
tx.from(utxos[0]);
tx.change(privateKey.toAddress());
tx.to(destKey.toAddress(), utxos[0].amount * 1e8 - 1000);
tx.sign(bitcore.PrivateKey.fromWIF(utxos[0].privateKeyWIF));
// test sending the transaction
bitcoind.sendTransaction(tx.serialize(), function(err, hash) {
if (err) {
return done(err);
}
hash.should.equal(tx.hash);
done();
});
});
it('will throw an error if an unsigned transaction is sent', function(done) {
var tx = bitcore.Transaction();
tx.from(utxos[1]);
tx.change(privateKey.toAddress());
tx.to(destKey.toAddress(), utxos[1].amount * 1e8 - 1000);
bitcoind.sendTransaction(tx.uncheckedSerialize(), function(err, hash) {
should.exist(err);
(err instanceof Error).should.equal(true);
should.not.exist(hash);
done();
});
});
it('will throw an error for unexpected types (tx decode failed)', function(done) {
var garbage = new Buffer('abcdef', 'hex');
bitcoind.sendTransaction(garbage, function(err, hash) {
should.exist(err);
should.not.exist(hash);
var num = 23;
bitcoind.sendTransaction(num, function(err, hash) {
should.exist(err);
(err instanceof Error).should.equal(true);
should.not.exist(hash);
done();
});
});
});
it('will emit "tx" events', function(done) {
var tx = bitcore.Transaction();
tx.from(utxos[2]);
tx.change(privateKey.toAddress());
tx.to(destKey.toAddress(), utxos[2].amount * 1e8 - 1000);
tx.sign(bitcore.PrivateKey.fromWIF(utxos[2].privateKeyWIF));
var serialized = tx.serialize();
bitcoind.once('tx', function(buffer) {
buffer.toString('hex').should.equal(serialized);
done();
});
bitcoind.sendTransaction(serialized, function(err, hash) {
if (err) {
return done(err);
}
should.exist(hash);
});
});
});
describe('fee estimation', function() {
it('will estimate fees', function(done) {
bitcoind.estimateFee(1, function(err, fees) {
if (err) {
return done(err);
}
fees.should.equal(-1);
done();
});
});
});
describe('tip updates', function() {
it('will get an event when the tip is new', function(done) {
this.timeout(4000);
bitcoind.on('tip', function(height) {
if (height === 151) {
done();
}
});
client.generate(1, function(err, response) {
if (err) {
throw err;
}
});
});
});
describe('get detailed transaction', function() {
it('should include details for coinbase tx', function(done) {
bitcoind.getDetailedTransaction(utxos[0].txid, function(err, tx) {
if (err) {
return done(err);
}
should.exist(tx.height);
tx.height.should.be.a('number');
should.exist(tx.blockTimestamp);
should.exist(tx.blockHash);
tx.coinbase.should.equal(true);
tx.version.should.equal(1);
tx.hex.should.be.a('string');
tx.locktime.should.equal(0);
tx.feeSatoshis.should.equal(0);
tx.outputSatoshis.should.equal(50 * 1e8);
tx.inputSatoshis.should.equal(0);
tx.inputs.length.should.equal(1);
tx.outputs.length.should.equal(1);
should.equal(tx.inputs[0].prevTxId, null);
should.equal(tx.inputs[0].outputIndex, null);
tx.inputs[0].script.should.be.a('string');
should.equal(tx.inputs[0].scriptAsm, null);
should.equal(tx.inputs[0].address, null);
should.equal(tx.inputs[0].satoshis, null);
tx.outputs[0].satoshis.should.equal(50 * 1e8);
tx.outputs[0].script.should.be.a('string');
tx.outputs[0].scriptAsm.should.be.a('string');
tx.outputs[0].spentTxId.should.be.a('string');
tx.outputs[0].spentIndex.should.equal(0);
tx.outputs[0].spentHeight.should.be.a('number');
tx.outputs[0].address.should.be.a('string');
done();
});
});
});
describe('#getInfo', function() {
it('will get information', function(done) {
bitcoind.getInfo(function(err, info) {
if (err) {
return done(err);
}
info.network.should.equal('regtest');
should.exist(info);
should.exist(info.version);
should.exist(info.blocks);
should.exist(info.timeOffset);
should.exist(info.connections);
should.exist(info.difficulty);
should.exist(info.testnet);
should.exist(info.relayFee);
should.exist(info.errors);
done();
});
});
});
});

View File

@ -1,183 +0,0 @@
'use strict';
var path = require('path');
var async = require('async');
var spawn = require('child_process').spawn;
var BitcoinRPC = require('bitcoind-rpc');
var rimraf = require('rimraf');
var bitcore = require('bitcore-lib');
var chai = require('chai');
var should = chai.should();
var index = require('..');
var log = index.log;
log.debug = function() {};
var BitcoreNode = index.Node;
var BitcoinService = index.services.Bitcoin;
describe('Bitcoin Cluster', function() {
var node;
var daemons = [];
var execPath = path.resolve(__dirname, '../bin/bitcoind');
var nodesConf = [
{
datadir: path.resolve(__dirname, './data/node1'),
conf: path.resolve(__dirname, './data/node1/bitcoin.conf'),
rpcuser: 'bitcoin',
rpcpassword: 'local321',
rpcport: 30521,
zmqpubrawtx: 'tcp://127.0.0.1:30611',
zmqpubhashblock: 'tcp://127.0.0.1:30611'
},
{
datadir: path.resolve(__dirname, './data/node2'),
conf: path.resolve(__dirname, './data/node2/bitcoin.conf'),
rpcuser: 'bitcoin',
rpcpassword: 'local321',
rpcport: 30522,
zmqpubrawtx: 'tcp://127.0.0.1:30622',
zmqpubhashblock: 'tcp://127.0.0.1:30622'
},
{
datadir: path.resolve(__dirname, './data/node3'),
conf: path.resolve(__dirname, './data/node3/bitcoin.conf'),
rpcuser: 'bitcoin',
rpcpassword: 'local321',
rpcport: 30523,
zmqpubrawtx: 'tcp://127.0.0.1:30633',
zmqpubhashblock: 'tcp://127.0.0.1:30633'
}
];
before(function(done) {
log.info('Starting 3 bitcoind daemons');
this.timeout(60000);
async.each(nodesConf, function(nodeConf, next) {
var opts = [
'--regtest',
'--datadir=' + nodeConf.datadir,
'--conf=' + nodeConf.conf
];
rimraf(path.resolve(nodeConf.datadir, './regtest'), function(err) {
if (err) {
return done(err);
}
var process = spawn(execPath, opts, {stdio: 'inherit'});
var client = new BitcoinRPC({
protocol: 'http',
host: '127.0.0.1',
port: nodeConf.rpcport,
user: nodeConf.rpcuser,
pass: nodeConf.rpcpassword
});
daemons.push(process);
async.retry({times: 10, interval: 5000}, function(ready) {
client.getInfo(ready);
}, next);
});
}, done);
});
after(function(done) {
this.timeout(10000);
setTimeout(function() {
async.each(daemons, function(process, next) {
process.once('exit', next);
process.kill('SIGINT');
}, done);
}, 1000);
});
it('step 1: will connect to three bitcoind daemons', function(done) {
this.timeout(20000);
var configuration = {
network: 'regtest',
services: [
{
name: 'bitcoind',
module: BitcoinService,
config: {
connect: [
{
rpchost: '127.0.0.1',
rpcport: 30521,
rpcuser: 'bitcoin',
rpcpassword: 'local321',
zmqpubrawtx: 'tcp://127.0.0.1:30611'
},
{
rpchost: '127.0.0.1',
rpcport: 30522,
rpcuser: 'bitcoin',
rpcpassword: 'local321',
zmqpubrawtx: 'tcp://127.0.0.1:30622'
},
{
rpchost: '127.0.0.1',
rpcport: 30523,
rpcuser: 'bitcoin',
rpcpassword: 'local321',
zmqpubrawtx: 'tcp://127.0.0.1:30633'
}
]
}
}
]
};
var regtest = bitcore.Networks.get('regtest');
should.exist(regtest);
node = new BitcoreNode(configuration);
node.on('error', function(err) {
log.error(err);
});
node.on('ready', function() {
done();
});
node.start(function(err) {
if (err) {
return done(err);
}
});
});
it('step 2: receive block events', function(done) {
this.timeout(10000);
node.services.bitcoind.once('tip', function(height) {
height.should.equal(1);
done();
});
node.generateBlock(1, function(err, hashes) {
if (err) {
return done(err);
}
should.exist(hashes);
});
});
it('step 3: get blocks', function(done) {
async.times(3, function(n, next) {
node.getBlock(1, function(err, block) {
if (err) {
return next(err);
}
should.exist(block);
next();
});
}, done);
});
});

View File

@ -1,3 +0,0 @@
.lock
blocks
regtest

View File

@ -1,12 +0,0 @@
server=1
whitelist=127.0.0.1
txindex=1
addressindex=1
timestampindex=1
spentindex=1
zmqpubrawtx=tcp://127.0.0.1:30332
zmqpubhashblock=tcp://127.0.0.1:30332
rpcallowip=127.0.0.1
rpcport=30331
rpcuser=bitcoin
rpcpassword=local321

View File

@ -1,14 +0,0 @@
-----BEGIN CERTIFICATE-----
MIICDTCCAXYCCQCsGf/7CM97gDANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJV
UzELMAkGA1UECBMCR0ExDDAKBgNVBAcTA2ZvbzEhMB8GA1UEChMYSW50ZXJuZXQg
V2lkZ2l0cyBQdHkgTHRkMB4XDTE1MDgyNjE3NTAwOFoXDTE1MDkyNTE3NTAwOFow
SzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkdBMQwwCgYDVQQHEwNmb28xITAfBgNV
BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOB
jQAwgYkCgYEA7SXnPpKk+0qXTTa42jzvu/C/gdGby0arY50bE2A+epI7FlI5YqKd
hQWoNRpuehF3jH6Ij3mbeeImtTA7TaUYlgHKpn63xfJ0cRj55+6vqq09nDxf0Lm9
IpTbgllu1l+SHtSuzFBVtGuNRSqObf8gD5XCD5lWK1vXHQ6PFSnAakMCAwEAATAN
BgkqhkiG9w0BAQUFAAOBgQBNARLDgsw7NCBVkn57AEgwZptxeyvFWlGZCd0BmYIX
ZFk7T1OQDwn7GlHry2IBswI0QRi076RQ4oJq+fg2O3XdFvEYV0cyypW7AxrnYTHP
m1h2xr6Y5vhxFKP8DxpAxST27DHbR18YvTD+IGtp2UjLj646587N0MWxt8vmaU3c
og==
-----END CERTIFICATE-----

View File

@ -1,15 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXwIBAAKBgQDtJec+kqT7SpdNNrjaPO+78L+B0ZvLRqtjnRsTYD56kjsWUjli
op2FBag1Gm56EXeMfoiPeZt54ia1MDtNpRiWAcqmfrfF8nRxGPnn7q+qrT2cPF/Q
ub0ilNuCWW7WX5Ie1K7MUFW0a41FKo5t/yAPlcIPmVYrW9cdDo8VKcBqQwIDAQAB
AoGBAOzYFxyCPu2OMI/4ICQuCcwtBEa2Ph+Fo/Rn2ru+OogV9Zc0ZYWiHSnWXYkz
rbSSL1CMqvyIGoRfHgOFeSTxxxtVyRo5LayI6Ce6V04yUFua16uo6hMX5bfGKZ9d
uq/HCDmdQvgifxUFpTpoSencuxwVCSYstMqjGfpobc5nxN2RAkEA+23BWNyA2qcq
yqSplgTQO0laXox9ksr1k2mJB2HtG3GNs1kapP+Z8AVtn/Mf9Va0hgbSnqpF9fll
1xpBqfidSQJBAPF1rizAm7xP7AeRRET36YKjZCVayA/g4rp7kecrNJTTupJIuxHr
JUlOTtXEWjIVcutSM7bP7bytPv4SAaApBysCQQDEZhqnCC+bHQO/IVrbNc1W0ljG
DGY22VV1HfYND0CAtHXkx9CZXJPpusPEMs0e/uiq3P9/MzDNEFCt8vOiCvMJAkEA
65oDIKGzk/R7/wpMjetEyva5AgXpjizFrmZigCjVPp61voT/G8XQ9Q1WuRjFVXc+
UcU8tpV+iIqXG3vgYDGITwJBANb0NFFF8QsygbENtad1tw1C/hNabHk8n9hu+Z8+
OSzEMlP7SsvddPaqusGydhTUxazoG3s4kEh5WmCWKgZGKO0=
-----END RSA PRIVATE KEY-----

View File

@ -1,16 +0,0 @@
server=1
whitelist=127.0.0.1
txindex=1
addressindex=1
timestampindex=1
spentindex=1
addnode=127.0.0.1:30432
addnode=127.0.0.1:30433
port=30431
rpcport=30521
zmqpubrawtx=tcp://127.0.0.1:30611
zmqpubhashblock=tcp://127.0.0.1:30611
rpcallowip=127.0.0.1
rpcuser=bitcoin
rpcpassword=local321
keypool=3

View File

@ -1,16 +0,0 @@
server=1
whitelist=127.0.0.1
txindex=1
addressindex=1
timestampindex=1
spentindex=1
addnode=127.0.0.1:30431
addnode=127.0.0.1:30433
port=30432
rpcport=30522
zmqpubrawtx=tcp://127.0.0.1:30622
zmqpubhashblock=tcp://127.0.0.1:30622
rpcallowip=127.0.0.1
rpcuser=bitcoin
rpcpassword=local321
keypool=3

View File

@ -1,16 +0,0 @@
server=1
whitelist=127.0.0.1
txindex=1
addressindex=1
timestampindex=1
spentindex=1
addnode=127.0.0.1:30431
addnode=127.0.0.1:30432
port=30433
rpcport=30523
zmqpubrawtx=tcp://127.0.0.1:30633
zmqpubhashblock=tcp://127.0.0.1:30633
rpcallowip=127.0.0.1
rpcuser=bitcoin
rpcpassword=local321
keypool=3

View File

@ -1,758 +0,0 @@
'use strict';
// To run the tests: $ mocha -R spec regtest/node.js
var path = require('path');
var index = require('..');
var async = require('async');
var log = index.log;
log.debug = function() {};
var chai = require('chai');
var bitcore = require('bitcore-lib');
var rimraf = require('rimraf');
var node;
var should = chai.should();
var BitcoinRPC = require('bitcoind-rpc');
var index = require('..');
var Transaction = bitcore.Transaction;
var BitcoreNode = index.Node;
var BitcoinService = index.services.Bitcoin;
var testWIF = 'cSdkPxkAjA4HDr5VHgsebAPDEh9Gyub4HK8UJr2DFGGqKKy4K5sG';
var testKey;
var client;
var outputForIsSpentTest1;
var unspentOutputSpentTxId;
describe('Node Functionality', function() {
var regtest;
before(function(done) {
this.timeout(20000);
var datadir = __dirname + '/data';
testKey = bitcore.PrivateKey(testWIF);
rimraf(datadir + '/regtest', function(err) {
if (err) {
throw err;
}
var configuration = {
network: 'regtest',
services: [
{
name: 'bitcoind',
module: BitcoinService,
config: {
spawn: {
datadir: datadir,
exec: path.resolve(__dirname, '../bin/bitcoind')
}
}
}
]
};
node = new BitcoreNode(configuration);
regtest = bitcore.Networks.get('regtest');
should.exist(regtest);
node.on('error', function(err) {
log.error(err);
});
node.start(function(err) {
if (err) {
return done(err);
}
client = new BitcoinRPC({
protocol: 'http',
host: '127.0.0.1',
port: 30331,
user: 'bitcoin',
pass: 'local321',
rejectUnauthorized: false
});
var syncedHandler = function() {
if (node.services.bitcoind.height === 150) {
node.services.bitcoind.removeListener('synced', syncedHandler);
done();
}
};
node.services.bitcoind.on('synced', syncedHandler);
client.generate(150, function(err) {
if (err) {
throw err;
}
});
});
});
});
after(function(done) {
this.timeout(20000);
node.stop(function(err, result) {
if(err) {
throw err;
}
done();
});
});
describe('Bus Functionality', function() {
it('subscribes and unsubscribes to an event on the bus', function(done) {
var bus = node.openBus();
var blockExpected;
var blockReceived;
bus.subscribe('bitcoind/hashblock');
bus.on('bitcoind/hashblock', function(data) {
bus.unsubscribe('bitcoind/hashblock');
if (blockExpected) {
data.should.be.equal(blockExpected);
done();
} else {
blockReceived = data;
}
});
client.generate(1, function(err, response) {
if (err) {
throw err;
}
if (blockReceived) {
blockReceived.should.be.equal(response.result[0]);
done();
} else {
blockExpected = response.result[0];
}
});
});
});
describe('Address Functionality', function() {
var address;
var unspentOutput;
before(function(done) {
this.timeout(10000);
address = testKey.toAddress(regtest).toString();
var startHeight = node.services.bitcoind.height;
node.services.bitcoind.on('tip', function(height) {
if (height === startHeight + 3) {
done();
}
});
client.sendToAddress(testKey.toAddress(regtest).toString(), 10, function(err) {
if (err) {
throw err;
}
client.generate(3, function(err) {
if (err) {
throw err;
}
});
});
});
it('should be able to get the balance of the test address', function(done) {
node.getAddressBalance(address, false, function(err, data) {
if (err) {
throw err;
}
data.balance.should.equal(10 * 1e8);
done();
});
});
it('can get unspent outputs for address', function(done) {
node.getAddressUnspentOutputs(address, false, function(err, results) {
if (err) {
throw err;
}
results.length.should.equal(1);
unspentOutput = outputForIsSpentTest1 = results[0];
done();
});
});
it('correctly give the history for the address', function(done) {
var options = {
from: 0,
to: 10,
queryMempool: false
};
node.getAddressHistory(address, options, function(err, results) {
if (err) {
throw err;
}
var items = results.items;
items.length.should.equal(1);
var info = items[0];
should.exist(info.addresses[address]);
info.addresses[address].outputIndexes.length.should.equal(1);
info.addresses[address].outputIndexes[0].should.be.within(0, 1);
info.addresses[address].inputIndexes.should.deep.equal([]);
info.satoshis.should.equal(10 * 1e8);
info.confirmations.should.equal(3);
info.tx.blockTimestamp.should.be.a('number');
info.tx.feeSatoshis.should.be.within(950, 4000);
done();
});
});
it('correctly give the summary for the address', function(done) {
var options = {
queryMempool: false
};
node.getAddressSummary(address, options, function(err, results) {
if (err) {
throw err;
}
results.totalReceived.should.equal(1000000000);
results.totalSpent.should.equal(0);
results.balance.should.equal(1000000000);
should.not.exist(results.unconfirmedBalance);
results.appearances.should.equal(1);
should.not.exist(results.unconfirmedAppearances);
results.txids.length.should.equal(1);
done();
});
});
describe('History', function() {
this.timeout(20000);
var testKey2;
var address2;
var testKey3;
var address3;
var testKey4;
var address4;
var testKey5;
var address5;
var testKey6;
var address6;
var tx2Amount;
var tx2Hash;
before(function(done) {
/* jshint maxstatements: 50 */
// Finished once all blocks have been mined
var startHeight = node.services.bitcoind.height;
node.services.bitcoind.on('tip', function(height) {
if (height === startHeight + 5) {
done();
}
});
testKey2 = bitcore.PrivateKey.fromWIF('cNfF4jXiLHQnFRsxaJyr2YSGcmtNYvxQYSakNhuDGxpkSzAwn95x');
address2 = testKey2.toAddress(regtest).toString();
testKey3 = bitcore.PrivateKey.fromWIF('cVTYQbaFNetiZcvxzXcVMin89uMLC43pEBMy2etgZHbPPxH5obYt');
address3 = testKey3.toAddress(regtest).toString();
testKey4 = bitcore.PrivateKey.fromWIF('cPNQmfE31H2oCUFqaHpfSqjDibkt7XoT2vydLJLDHNTvcddCesGw');
address4 = testKey4.toAddress(regtest).toString();
testKey5 = bitcore.PrivateKey.fromWIF('cVrzm9gCmnzwEVMGeCxY6xLVPdG3XWW97kwkFH3H3v722nb99QBF');
address5 = testKey5.toAddress(regtest).toString();
testKey6 = bitcore.PrivateKey.fromWIF('cPfMesNR2gsQEK69a6xe7qE44CZEZavgMUak5hQ74XDgsRmmGBYF');
address6 = testKey6.toAddress(regtest).toString();
var tx = new Transaction();
tx.from(unspentOutput);
tx.to(address, 1 * 1e8);
tx.to(address, 2 * 1e8);
tx.to(address, 0.5 * 1e8);
tx.to(address, 3 * 1e8);
tx.fee(10000);
tx.change(address);
tx.sign(testKey);
unspentOutputSpentTxId = tx.id;
function mineBlock(next) {
client.generate(1, function(err, response) {
if (err) {
throw err;
}
should.exist(response);
next();
});
}
node.sendTransaction(tx.serialize(), function(err, hash) {
if (err) {
return done(err);
}
client.generate(1, function(err, response) {
if (err) {
throw err;
}
should.exist(response);
node.getAddressUnspentOutputs(address, false, function(err, results) {
/* jshint maxstatements: 50 */
if (err) {
throw err;
}
results.length.should.equal(5);
async.series([
function(next) {
var tx2 = new Transaction();
tx2Amount = results[0].satoshis - 10000;
tx2.from(results[0]);
tx2.to(address2, tx2Amount);
tx2.change(address);
tx2.sign(testKey);
tx2Hash = tx2.hash;
node.sendTransaction(tx2.serialize(), function(err) {
if (err) {
return next(err);
}
mineBlock(next);
});
}, function(next) {
var tx3 = new Transaction();
tx3.from(results[1]);
tx3.to(address3, results[1].satoshis - 10000);
tx3.change(address);
tx3.sign(testKey);
node.sendTransaction(tx3.serialize(), function(err) {
if (err) {
return next(err);
}
mineBlock(next);
});
}, function(next) {
var tx4 = new Transaction();
tx4.from(results[2]);
tx4.to(address4, results[2].satoshis - 10000);
tx4.change(address);
tx4.sign(testKey);
node.sendTransaction(tx4.serialize(), function(err) {
if (err) {
return next(err);
}
mineBlock(next);
});
}, function(next) {
var tx5 = new Transaction();
tx5.from(results[3]);
tx5.from(results[4]);
tx5.to(address5, results[3].satoshis - 10000);
tx5.to(address6, results[4].satoshis - 10000);
tx5.change(address);
tx5.sign(testKey);
node.sendTransaction(tx5.serialize(), function(err) {
if (err) {
return next(err);
}
mineBlock(next);
});
}
], function(err) {
if (err) {
throw err;
}
});
});
});
});
});
it('five addresses', function(done) {
var addresses = [
address2,
address3,
address4,
address5,
address6
];
var options = {};
node.getAddressHistory(addresses, options, function(err, results) {
if (err) {
throw err;
}
results.totalCount.should.equal(4);
var history = results.items;
history.length.should.equal(4);
history[0].tx.height.should.equal(159);
history[0].confirmations.should.equal(1);
history[1].tx.height.should.equal(158);
should.exist(history[1].addresses[address4]);
history[2].tx.height.should.equal(157);
should.exist(history[2].addresses[address3]);
history[3].tx.height.should.equal(156);
should.exist(history[3].addresses[address2]);
history[3].satoshis.should.equal(tx2Amount);
history[3].tx.hash.should.equal(tx2Hash);
history[3].confirmations.should.equal(4);
done();
});
});
it('five addresses (limited by height)', function(done) {
var addresses = [
address2,
address3,
address4,
address5,
address6
];
var options = {
start: 158,
end: 157
};
node.getAddressHistory(addresses, options, function(err, results) {
if (err) {
throw err;
}
results.totalCount.should.equal(2);
var history = results.items;
history.length.should.equal(2);
history[0].tx.height.should.equal(158);
history[0].confirmations.should.equal(2);
history[1].tx.height.should.equal(157);
should.exist(history[1].addresses[address3]);
done();
});
});
it('five addresses (limited by height 155 to 154)', function(done) {
var addresses = [
address2,
address3,
address4,
address5,
address6
];
var options = {
start: 157,
end: 156
};
node.getAddressHistory(addresses, options, function(err, results) {
if (err) {
throw err;
}
results.totalCount.should.equal(2);
var history = results.items;
history.length.should.equal(2);
history[0].tx.height.should.equal(157);
history[1].tx.height.should.equal(156);
done();
});
});
it('five addresses (paginated by index)', function(done) {
var addresses = [
address2,
address3,
address4,
address5,
address6
];
var options = {
from: 0,
to: 3
};
node.getAddressHistory(addresses, options, function(err, results) {
if (err) {
throw err;
}
results.totalCount.should.equal(4);
var history = results.items;
history.length.should.equal(3);
history[0].tx.height.should.equal(159);
history[0].confirmations.should.equal(1);
history[1].tx.height.should.equal(158);
should.exist(history[1].addresses[address4]);
done();
});
});
it('one address with sending and receiving', function(done) {
var addresses = [
address
];
var options = {};
node.getAddressHistory(addresses, options, function(err, results) {
if (err) {
throw err;
}
results.totalCount.should.equal(6);
var history = results.items;
history.length.should.equal(6);
history[0].tx.height.should.equal(159);
history[0].addresses[address].inputIndexes.should.deep.equal([0, 1]);
history[0].addresses[address].outputIndexes.should.deep.equal([2]);
history[0].confirmations.should.equal(1);
history[1].tx.height.should.equal(158);
history[2].tx.height.should.equal(157);
history[3].tx.height.should.equal(156);
history[4].tx.height.should.equal(155);
history[4].satoshis.should.equal(-10000);
history[4].addresses[address].outputIndexes.should.deep.equal([0, 1, 2, 3, 4]);
history[4].addresses[address].inputIndexes.should.deep.equal([0]);
history[5].tx.height.should.equal(152);
history[5].satoshis.should.equal(10 * 1e8);
done();
});
});
it('summary for an address (sending and receiving)', function(done) {
node.getAddressSummary(address, {}, function(err, results) {
if (err) {
throw err;
}
results.totalReceived.should.equal(2000000000);
results.totalSpent.should.equal(1999990000);
results.balance.should.equal(10000);
results.unconfirmedBalance.should.equal(0);
results.appearances.should.equal(6);
results.unconfirmedAppearances.should.equal(0);
results.txids.length.should.equal(6);
done();
});
});
it('total transaction count (sending and receiving)', function(done) {
var addresses = [
address
];
var options = {};
node.getAddressHistory(addresses, options, function(err, results) {
if (err) {
throw err;
}
results.totalCount.should.equal(6);
done();
});
});
describe('Pagination', function() {
it('from 0 to 1', function(done) {
var options = {
from: 0,
to: 1
};
node.getAddressHistory(address, options, function(err, results) {
if (err) {
throw err;
}
var history = results.items;
history.length.should.equal(1);
history[0].tx.height.should.equal(159);
done();
});
});
it('from 1 to 2', function(done) {
var options = {
from: 1,
to: 2
};
node.getAddressHistory(address, options, function(err, results) {
if (err) {
throw err;
}
var history = results.items;
history.length.should.equal(1);
history[0].tx.height.should.equal(158);
done();
});
});
it('from 2 to 3', function(done) {
var options = {
from: 2,
to: 3
};
node.getAddressHistory(address, options, function(err, results) {
if (err) {
throw err;
}
var history = results.items;
history.length.should.equal(1);
history[0].tx.height.should.equal(157);
done();
});
});
it('from 3 to 4', function(done) {
var options = {
from: 3,
to: 4
};
node.getAddressHistory(address, options, function(err, results) {
if (err) {
throw err;
}
var history = results.items;
history.length.should.equal(1);
history[0].tx.height.should.equal(156);
done();
});
});
it('from 4 to 5', function(done) {
var options = {
from: 4,
to: 5
};
node.getAddressHistory(address, options, function(err, results) {
if (err) {
throw err;
}
var history = results.items;
history.length.should.equal(1);
history[0].tx.height.should.equal(155);
history[0].satoshis.should.equal(-10000);
history[0].addresses[address].outputIndexes.should.deep.equal([0, 1, 2, 3, 4]);
history[0].addresses[address].inputIndexes.should.deep.equal([0]);
done();
});
});
it('from 5 to 6', function(done) {
var options = {
from: 5,
to: 6
};
node.getAddressHistory(address, options, function(err, results) {
if (err) {
throw err;
}
var history = results.items;
history.length.should.equal(1);
history[0].tx.height.should.equal(152);
history[0].satoshis.should.equal(10 * 1e8);
done();
});
});
});
});
describe('Mempool Index', function() {
var unspentOutput;
before(function(done) {
node.getAddressUnspentOutputs(address, false, function(err, results) {
if (err) {
throw err;
}
results.length.should.equal(1);
unspentOutput = results[0];
done();
});
});
it('will update the mempool index after new tx', function(done) {
var memAddress = bitcore.PrivateKey().toAddress(node.network).toString();
var tx = new Transaction();
tx.from(unspentOutput);
tx.to(memAddress, unspentOutput.satoshis - 1000);
tx.fee(1000);
tx.sign(testKey);
node.services.bitcoind.sendTransaction(tx.serialize(), function(err, hash) {
node.getAddressTxids(memAddress, {}, function(err, txids) {
if (err) {
return done(err);
}
txids.length.should.equal(1);
txids[0].should.equal(hash);
done();
});
});
});
});
});
describe('Orphaned Transactions', function() {
this.timeout(8000);
var orphanedTransaction;
before(function(done) {
var count;
var invalidatedBlockHash;
async.series([
function(next) {
client.sendToAddress(testKey.toAddress(regtest).toString(), 10, function(err) {
if (err) {
return next(err);
}
client.generate(1, next);
});
},
function(next) {
client.getBlockCount(function(err, response) {
if (err) {
return next(err);
}
count = response.result;
next();
});
},
function(next) {
client.getBlockHash(count, function(err, response) {
if (err) {
return next(err);
}
invalidatedBlockHash = response.result;
next();
});
},
function(next) {
client.getBlock(invalidatedBlockHash, function(err, response) {
if (err) {
return next(err);
}
orphanedTransaction = response.result.tx[1];
should.exist(orphanedTransaction);
next();
});
},
function(next) {
client.invalidateBlock(invalidatedBlockHash, next);
}
], function(err) {
if (err) {
throw err;
}
done();
});
});
it('will not show confirmation count for orphaned transaction', function(done) {
// This test verifies that in the situation that the transaction is not in the mempool and
// is included in an orphaned block transaction index that the confirmation count will be unconfirmed.
node.getDetailedTransaction(orphanedTransaction, function(err, data) {
if (err) {
return done(err);
}
should.exist(data);
should.exist(data.height);
data.height.should.equal(-1);
done();
});
});
});
});

View File

@ -1,217 +0,0 @@
'use strict';
// To run the tests: $ mocha -R spec regtest/p2p.js
var path = require('path');
var index = require('..');
var log = index.log;
var p2p = require('bitcore-p2p');
var Peer = p2p.Peer;
var Messages = p2p.Messages;
var chai = require('chai');
var bitcore = require('bitcore-lib');
var Transaction = bitcore.Transaction;
var BN = bitcore.crypto.BN;
var async = require('async');
var rimraf = require('rimraf');
var bitcoind;
/* jshint unused: false */
var should = chai.should();
var assert = chai.assert;
var sinon = require('sinon');
var BitcoinRPC = require('bitcoind-rpc');
var transactionData = [];
var blockHashes = [];
var txs = [];
var client;
var messages;
var peer;
var coinbasePrivateKey;
var privateKey = bitcore.PrivateKey();
var destKey = bitcore.PrivateKey();
var BufferUtil = bitcore.util.buffer;
var blocks;
describe('P2P Functionality', function() {
before(function(done) {
this.timeout(100000);
// enable regtest
bitcore.Networks.enableRegtest();
var regtestNetwork = bitcore.Networks.get('regtest');
var datadir = __dirname + '/data';
rimraf(datadir + '/regtest', function(err) {
if (err) {
throw err;
}
bitcoind = require('../').services.Bitcoin({
spawn: {
datadir: datadir,
exec: path.resolve(__dirname, '../bin/bitcoind')
},
node: {
network: bitcore.Networks.testnet
}
});
bitcoind.on('error', function(err) {
log.error('error="%s"', err.message);
});
log.info('Waiting for Bitcoin Core to initialize...');
bitcoind.start(function(err) {
if (err) {
throw err;
}
log.info('Bitcoind started');
client = new BitcoinRPC({
protocol: 'http',
host: '127.0.0.1',
port: 30331,
user: 'bitcoin',
pass: 'local321',
rejectUnauthorized: false
});
peer = new Peer({
host: '127.0.0.1',
port: '18444',
network: regtestNetwork
});
messages = new Messages({
network: regtestNetwork
});
blocks = 500;
log.info('Generating ' + blocks + ' blocks...');
// Generate enough blocks so that the initial coinbase transactions
// can be spent.
setImmediate(function() {
client.generate(blocks, function(err, response) {
if (err) {
throw err;
}
blockHashes = response.result;
log.info('Preparing test data...');
// Get all of the unspent outputs
client.listUnspent(0, blocks, function(err, response) {
var utxos = response.result;
async.mapSeries(utxos, function(utxo, next) {
async.series([
function(finished) {
// Load all of the transactions for later testing
client.getTransaction(utxo.txid, function(err, txresponse) {
if (err) {
throw err;
}
// add to the list of transactions for testing later
transactionData.push(txresponse.result.hex);
finished();
});
},
function(finished) {
// Get the private key for each utxo
client.dumpPrivKey(utxo.address, function(err, privresponse) {
if (err) {
throw err;
}
utxo.privateKeyWIF = privresponse.result;
var tx = bitcore.Transaction();
tx.from(utxo);
tx.change(privateKey.toAddress());
tx.to(destKey.toAddress(), utxo.amount * 1e8 - 1000);
tx.sign(bitcore.PrivateKey.fromWIF(utxo.privateKeyWIF));
txs.push(tx);
finished();
});
}
], next);
}, function(err) {
if (err) {
throw err;
}
peer.on('ready', function() {
log.info('Peer ready');
done();
});
log.info('Connecting to peer');
peer.connect();
});
});
});
});
});
});
});
after(function(done) {
this.timeout(20000);
peer.on('disconnect', function() {
log.info('Peer disconnected');
bitcoind.node.stopping = true;
bitcoind.stop(function(err, result) {
done();
});
});
peer.disconnect();
});
it('will be able to handle many inventory messages and be able to send getdata messages and received the txs', function(done) {
this.timeout(100000);
var usedTxs = {};
bitcoind.on('tx', function(buffer) {
var txFromResult = new Transaction().fromBuffer(buffer);
var tx = usedTxs[txFromResult.id];
should.exist(tx);
buffer.toString('hex').should.equal(tx.serialize());
delete usedTxs[tx.id];
if (Object.keys(usedTxs).length === 0) {
done();
}
});
peer.on('getdata', function(message) {
var hash = message.inventory[0].hash;
var reversedHash = BufferUtil.reverse(hash).toString('hex');
var tx = usedTxs[reversedHash];
if (reversedHash === tx.id) {
var txMessage = messages.Transaction(tx);
peer.sendMessage(txMessage);
}
});
async.whilst(
function() {
return txs.length > 0;
},
function(callback) {
var tx = txs.pop();
usedTxs[tx.id] = tx;
var message = messages.Inventory.forTransaction(tx.hash);
peer.sendMessage(message);
callback();
},
function(err) {
if (err) {
throw err;
}
});
});
});

View File

@ -1,119 +0,0 @@
#!/bin/bash
set -e
root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.."
platform=`uname -a | awk '{print tolower($1)}'`
arch=`uname -m`
version="0.12.1"
url="https://github.com/bitpay/bitcoin/releases/download"
tag="v0.12.1-bitcore-4"
if [ "${platform}" == "linux" ]; then
if [ "${arch}" == "x86_64" ]; then
tarball_name="bitcoin-${version}-linux64.tar.gz"
elif [ "${arch}" == "x86_32" ]; then
tarball_name="bitcoin-${version}-linux32.tar.gz"
fi
elif [ "${platform}" == "darwin" ]; then
tarball_name="bitcoin-${version}-osx64.tar.gz"
else
echo "Bitcoin binary distribution not available for platform and architecture"
exit -1
fi
binary_url="${url}/${tag}/${tarball_name}"
shasums_url="${url}/${tag}/SHA256SUMS.asc"
download_bitcoind() {
cd "${root_dir}/bin"
echo "Downloading bitcoin: ${binary_url}"
is_curl=true
if hash curl 2>/dev/null; then
curl --fail -I $binary_url >/dev/null 2>&1
else
is_curl=false
wget --server-response --spider $binary_url >/dev/null 2>&1
fi
if test $? -eq 0; then
if [ "${is_curl}" = true ]; then
curl -L $binary_url > $tarball_name
curl -L $shasums_url > SHA256SUMS.asc
else
wget $binary_url
wget $shasums_url
fi
if test -e "${tarball_name}"; then
echo "Unpacking bitcoin distribution"
tar -xvzf $tarball_name
if test $? -eq 0; then
ln -sf "bitcoin-${version}/bin/bitcoind"
return;
fi
fi
fi
echo "Bitcoin binary distribution could not be downloaded"
exit -1
}
verify_download() {
echo "Verifying signatures of bitcoin download"
gpg --verify "${root_dir}/bin/SHA256SUMS.asc"
if hash shasum 2>/dev/null; then
shasum_cmd="shasum -a 256"
else
shasum_cmd="sha256sum"
fi
download_sha=$(${shasum_cmd} "${root_dir}/bin/${tarball_name}" | awk '{print $1}')
expected_sha=$(cat "${root_dir}/bin/SHA256SUMS.asc" | grep "${tarball_name}" | awk '{print $1}')
echo "Checksum (download): ${download_sha}"
echo "Checksum (verified): ${expected_sha}"
if [ "${download_sha}" != "${expected_sha}" ]; then
echo -e "\033[1;31mChecksums did NOT match!\033[0m\n"
exit 1
else
echo -e "\033[1;32mChecksums matched!\033[0m\n"
fi
}
download=1
verify=0
if [ "${SKIP_BITCOIN_DOWNLOAD}" = 1 ]; then
download=0;
fi
if [ "${VERIFY_BITCOIN_DOWNLOAD}" = 1 ]; then
verify=1;
fi
while [ -n "$1" ]; do
param="$1"
value="$2"
case $param in
--skip-bitcoin-download)
download=0
;;
--verify-bitcoin-download)
verify=1
;;
esac
shift
done
if [ "${download}" = 1 ]; then
download_bitcoind
fi
if [ "${verify}" = 1 ]; then
verify_download
fi
exit 0

View File

@ -1,8 +0,0 @@
#!/bin/bash
set -e
_mocha -R spec regtest/p2p.js
_mocha -R spec regtest/bitcoind.js
_mocha -R spec regtest/cluster.js
_mocha -R spec regtest/node.js

View File

@ -1,104 +0,0 @@
'use strict';
var sinon = require('sinon');
var Service = require('../lib/service');
var BitcoreNode = require('../lib/node');
var util = require('util');
var should = require('chai').should();
var index = require('../lib');
var log = index.log;
var TestService = function(options) {
this.node = options.node;
};
util.inherits(TestService, Service);
TestService.dependencies = [];
TestService.prototype.start = function(callback) {
callback();
};
TestService.prototype.stop = function(callback) {
callback();
};
TestService.prototype.close = function(callback) {
callback();
};
TestService.prototype.getPublishEvents = function() {
return [
{
name: 'test/testEvent',
scope: this,
subscribe: this.subscribe.bind(this, 'test/testEvent'),
unsubscribe: this.unsubscribe.bind(this, 'test/testEvent')
}
];
};
TestService.prototype.subscribe = function(name, emitter, params) {
emitter.emit(name, params);
};
TestService.prototype.unsubscribe = function(name, emitter) {
emitter.emit('unsubscribe');
};
describe('Bus Functionality', function() {
var sandbox = sinon.sandbox.create();
beforeEach(function() {
sandbox.stub(log, 'info');
});
afterEach(function() {
sandbox.restore();
});
it('should subscribe to testEvent', function(done) {
var node = new BitcoreNode({
datadir: './',
network: 'testnet',
port: 8888,
services: [
{
name: 'testService',
config: {},
module: TestService
}
]
});
node.start(function() {
var bus = node.openBus();
var params = 'somedata';
bus.on('test/testEvent', function(data) {
data.should.be.equal(params);
done();
});
bus.subscribe('test/testEvent', params);
});
});
it('should unsubscribe from a testEvent', function(done) {
var node = new BitcoreNode({
datadir: './',
network: 'testnet',
port: 8888,
services: [
{
name: 'testService',
config: {},
module: TestService
}
]
});
node.start(function() {
var bus = node.openBus();
var params = 'somedata';
bus.on('unsubscribe', function() {
done();
});
bus.subscribe('test/testEvent');
bus.unsubscribe('test/testEvent');
});
});
});

View File

@ -1,131 +0,0 @@
'use strict';
var should = require('chai').should();
var sinon = require('sinon');
var Bus = require('../lib/bus');
describe('Bus', function() {
describe('#subscribe', function() {
it('will call db and services subscribe function with the correct arguments', function() {
var subscribeDb = sinon.spy();
var subscribeService = sinon.spy();
var node = {
services: {
db: {
getPublishEvents: sinon.stub().returns([
{
name: 'dbtest',
scope: this,
subscribe: subscribeDb
}
])
},
service1: {
getPublishEvents: sinon.stub().returns([
{
name: 'test',
scope: this,
subscribe: subscribeService,
}
])
}
}
};
var bus = new Bus({node: node});
bus.subscribe('dbtest', 'a', 'b', 'c');
bus.subscribe('test', 'a', 'b', 'c');
subscribeService.callCount.should.equal(1);
subscribeDb.callCount.should.equal(1);
subscribeDb.args[0][0].should.equal(bus);
subscribeDb.args[0][1].should.equal('a');
subscribeDb.args[0][2].should.equal('b');
subscribeDb.args[0][3].should.equal('c');
subscribeService.args[0][0].should.equal(bus);
subscribeService.args[0][1].should.equal('a');
subscribeService.args[0][2].should.equal('b');
subscribeService.args[0][3].should.equal('c');
});
});
describe('#unsubscribe', function() {
it('will call db and services unsubscribe function with the correct arguments', function() {
var unsubscribeDb = sinon.spy();
var unsubscribeService = sinon.spy();
var node = {
services: {
db: {
getPublishEvents: sinon.stub().returns([
{
name: 'dbtest',
scope: this,
unsubscribe: unsubscribeDb
}
])
},
service1: {
getPublishEvents: sinon.stub().returns([
{
name: 'test',
scope: this,
unsubscribe: unsubscribeService,
}
])
}
}
};
var bus = new Bus({node: node});
bus.unsubscribe('dbtest', 'a', 'b', 'c');
bus.unsubscribe('test', 'a', 'b', 'c');
unsubscribeService.callCount.should.equal(1);
unsubscribeDb.callCount.should.equal(1);
unsubscribeDb.args[0][0].should.equal(bus);
unsubscribeDb.args[0][1].should.equal('a');
unsubscribeDb.args[0][2].should.equal('b');
unsubscribeDb.args[0][3].should.equal('c');
unsubscribeService.args[0][0].should.equal(bus);
unsubscribeService.args[0][1].should.equal('a');
unsubscribeService.args[0][2].should.equal('b');
unsubscribeService.args[0][3].should.equal('c');
});
});
describe('#close', function() {
it('will unsubscribe from all events', function() {
var unsubscribeDb = sinon.spy();
var unsubscribeService = sinon.spy();
var node = {
services: {
db: {
getPublishEvents: sinon.stub().returns([
{
name: 'dbtest',
scope: this,
unsubscribe: unsubscribeDb
}
])
},
service1: {
getPublishEvents: sinon.stub().returns([
{
name: 'test',
scope: this,
unsubscribe: unsubscribeService
}
])
}
}
};
var bus = new Bus({node: node});
bus.close();
unsubscribeDb.callCount.should.equal(1);
unsubscribeService.callCount.should.equal(1);
unsubscribeDb.args[0].length.should.equal(1);
unsubscribeDb.args[0][0].should.equal(bus);
unsubscribeService.args[0].length.should.equal(1);
unsubscribeService.args[0][0].should.equal(bus);
});
});
});

View File

@ -1,16 +0,0 @@
#testnet=1
#irc=0
#upnp=0
server=1
whitelist=127.0.0.1
# listen on different ports
port=20000
rpcallowip=127.0.0.1
rpcuser=bitcoin
rpcpassword=local321

View File

@ -1,14 +0,0 @@
[
{
"comment": "sends to tx[1]",
"hex":"0100000001dee8f4266e83072e0ad258125cc5a42ac25d2d2c73e6e2e873413b3939af1605000000006b483045022100ae987d056f81d2c982b71b0406f2374c1958b24bd289d77371347e275d2a62c002205148b17173be18af4e1e73ce2b0fd600734ea77087754bdba5dc7d645b01880a01210226ab3b46f85bf32f63778c680e16ef8b3fcb51d638a7980d651bfaeae6c17752ffffffff0170820300000000001976a9142baf68e3681df183375a4f4c10306de9a5c6cc7788ac00000000"
},
{
"comment": "spends from tx[0] (valid)",
"hex":"0100000001f77c71cf8c272d22471f054cae7fb48561ebcf004b8ec8f9f65cd87af82a2944000000006a47304402203c2bc91a170facdc5ef4b5b94c413bc7a10f65e09b326d205f070b17aa94d67102205b684111af2a20171eb65db73e6c73f9e77e6e6f739e050bc052ed6ecc9feb4a01210365d8756a4f3fc738105cfab8d80a85189bdb4db5af83374e645b79e2aadd976effffffff01605b0300000000001976a9149e84d1295471958e5ffccd8d36a57bd5d220f8ed88ac00000000"
},
{
"comment": "spends from tx[0] (missing signature)",
"hex":"0100000001f77c71cf8c272d22471f054cae7fb48561ebcf004b8ec8f9f65cd87af82a29440000000000ffffffff01605b0300000000001976a9149e84d1295471958e5ffccd8d36a57bd5d220f8ed88ac00000000"
}
]

View File

@ -1,23 +0,0 @@
#testnet=1
#irc=0
upnp=0
server=1
whitelist=127.0.0.1
txindex=1
addressindex=1
timestampindex=1
spentindex=1
dbcache=8192
checkblocks=144
maxuploadtarget=1024
zmqpubrawtx=tcp://127.0.0.1:28332
zmqpubhashblock=tcp://127.0.0.1:28332
port=20000
rpcport=50001
rpcallowip=127.0.0.1
rpcuser=bitcoin
rpcpassword=local321

View File

@ -1,12 +0,0 @@
server=1
whitelist=127.0.0.1
txindex=1
addressindex=1
timestampindex=1
spentindex=1
zmqpubrawtx=tcp://127.0.0.1:28332
zmqpubhashblock=tcp://127.0.0.1:28332
rpcallowip=127.0.0.1
rpcuser=bitcoin
rpcpassword=local321
uacomment=bitcore

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,3 +0,0 @@
[
"010000000ec9e1d96d51f7d0a5b726184cda744207e51a596f13b564109de6ffc0653055cf000000006a4730440220263d48a2f4c3a2aa6032f96253c853531131171d8ae3d30586a45de7ba7c4006022077db4b39926877939baf59e8d357effe7674e7b12ad986c0a4cd99f2b7acafca012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff0c16800642bdf90cbbd340be2e801bee9b907db6d59dc4c7cb269358c1e2593a000000006a4730440220085e39cb3a948559c1b1c88ba635eeef37a767322c1e4d96556e998302acb5dc0220421c03de78121692c538a9bc85a58fbe796384541fe87b6e91c39404318c390d012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff559fde3733e950db9e3faea1a27872047a5c8bc14e8e6ac4a7b4f7b5f90c42c2000000006b483045022100a46a109a5acfc34b13c591ab69392e2dc2c3ea12d0900c5f7a539ea888e57ae0022015372bad56d63c6d08a5e66e0c9a63b2fc8dce245aa765e37dac06acb84c18d501210207e2a01d4a334c2d7c9ebacc5ea8a0d4b86fd54599b1aebe125d9e664be012c2ffffffff92760c236f6f18751c4ecab1dfcfebac7358a431b31bcd72b0a09d336233bdce000000006a47304402200857fa82e5b287c6ed5624cbc4fcd068a756a7a9ef785a73ce4e51b15a8aa34b022042cbc6f478b711539c6345d0b05d4bc9a9b5c34b2e4d25f234cf6784ff2eed19012103cab1f64f3d5f20a3f4a070694e6f93f5248b502b3842e369d81f05d07dec01e3ffffffff158669557e8a0b71288df37626601e636c5a8b3797f7f3357590313e8efe790a000000006b48304502210085e62cb95066540730b74aeae133277e511b5bf79de8c0ad60e784638e681ddf022043b456285569e0da133f527412218893f05a781d6f77f8cddd12eb90bfdc5937012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffffc1f9f57f1eda20f3b45669b3f0d1eae73b410ddf2b4fc1cfe10051a6051eff68000000006a47304402200fbe15c73446309040f4264567d0e8cc46691cf5d0626c443fc2dde716211e5402207f84e68e273755d140f346e029213dbc42b65cfb1e2ac41f72402c7ff45feffc012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff39fa27e16c540a9bb796e65d4ac624fc33878e522461d6955764bf7b83c03ce8000000006a4730440220131e6aed76da389f21dfd7cd271fad73c5c533b1c18fbfabd6799a0d0e7dc80602205c38855bea0f1dfbbb608bc1b56c75b91a39980de121ffd9f880b522d412d821012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff375fb7ae26eb637ccc007a669b9f50ed10fa53127504b80e0fd2be34c50becd7000000006b483045022100f0a9e585aa3113eae4bfb204f38f96e60dc613c04313aae931b677e4d8d7081d022014664874859f3d47447c6c0f257c28c74e8fdaedd5f781d752f3a4b651d3d179012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff6f9d6a3c847e6112bb920424ca0d8d6e0956b128acb206e8fb58b2d2f2d7d46b000000006a4730440220736b198484cf5226616a539146e037a93cc75963885eefe58fc29a7be8123c750220619a456c0fe7437ec67c642d88e890344fc1c51a7b3cfc0ae283d61d0f176c5e012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff3cccbd8090d60fcf28064333cf2f51ef0f838ba5e26a5e0f38807ee16d38a649000000006b483045022100e1ed25e9365e596d4fc3cbf278490d8ea765c4266c55f19311cf5da33f8d00750220361888a1738ebba827c0c947690b5f2a5f20e9f1be8956c3a34a4ba03f9e60f5012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff7f4d60a2e961490aa465a7df461bf09b151bdc0c162f3bef0c1cbed6160d02c7000000006a47304402204e79b15a1db9a356f00dc9f2d559e31561cad1343ba5809a65b52bd868e3963e022055b9326ed5de9aa9970ec67a2ebf1a9dbf9ee513b64bd13837c87320bb4d6947012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffffe63b9370ba49a129e071750cbb300128015fdd90d7399f9c4e44934eabbaa2f7000000006b483045022100b9ceb2e376c0334d45bf08bfeb06dc250e7cb01d3a08e2fb3506388683552417022024c7c5bda385b904cca691fb6e1ad8c5eba5858a88a2112cb824dca72793b7a7012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffffc78b96fddededb6cbc1dff9de51f2743fd42e91de2506794b121928af4729528000000006a47304402201f05caddee5a0ff590b27c4ce25be1cbbeb45dc39679a1b8b0e10b1a378d84bc02203e31b01e14d891e9809c43a4df54494c626c5e47eaeeeb99ab4e02bd73c3d6cd012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff30093916240e981a77cb869924fa0c38a894c24b1a6e7d26b117bb9caa7d5bbe000000006a4730440220483f297379daacee14babbf929708a861a991373bca3ed4eef240e2c156a162602205f1e93e375a897c6a9ddc3dc616ccf14137096ebd7888040e1053a769d21b945012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff022897d411000000001976a91415354ee1828ed12f243f80dcb92c3a8205285f0188ac3be68c02000000001976a9143ecf8ff79932c3de33829a001236985872d10be188ac00000000"
]

View File

@ -6,6 +6,7 @@ var should = chai.should();
var Logger = require('../lib/logger');
describe('Logger', function() {
process.env.BITCORE_ENV = 'debug';
var sandbox = sinon.sandbox.create();
afterEach(function() {
sandbox.restore();
@ -31,10 +32,10 @@ describe('Logger', function() {
it('will log with formatting', function() {
var logger = new Logger({formatting: true});
sandbox.stub(console, 'info');
sandbox.stub(console, 'log');
logger.info('Test info log');
console.info.callCount.should.equal(1);
console.info.restore();
console.log.callCount.should.equal(1);
console.log.restore();
sandbox.stub(console, 'error');
logger.error(new Error('Test error log'));
@ -46,20 +47,20 @@ describe('Logger', function() {
console.log.callCount.should.equal(1);
console.log.restore();
sandbox.stub(console, 'warn');
sandbox.stub(console, 'log');
logger.warn('Test warn log');
console.warn.callCount.should.equal(1);
console.warn.restore();
console.log.callCount.should.equal(1);
console.log.restore();
});
it('will log without formatting', function() {
var logger = new Logger({formatting: false});
sandbox.stub(console, 'info');
sandbox.stub(console, 'log');
logger.info('Test info log');
console.info.callCount.should.equal(1);
should.not.exist(console.info.args[0][0].match(/^\[/));
console.info.restore();
console.log.callCount.should.equal(1);
should.not.exist(console.log.args[0][0].match(/^\[/));
console.log.restore();
sandbox.stub(console, 'error');
logger.error(new Error('Test error log'));
@ -73,11 +74,11 @@ describe('Logger', function() {
should.equal(console.log.args[0][0].match(/^\[/), null);
console.log.restore();
sandbox.stub(console, 'warn');
sandbox.stub(console, 'log');
logger.warn('Test warn log');
console.warn.callCount.should.equal(1);
should.equal(console.warn.args[0][0].match(/^\[/), null);
console.warn.restore();
console.log.callCount.should.equal(1);
should.equal(console.log.args[0][0].match(/^\[/), null);
console.log.restore();
});
});

View File

@ -2,15 +2,13 @@
var should = require('chai').should();
var sinon = require('sinon');
var bitcore = require('bitcore-lib');
var Networks = bitcore.Networks;
var proxyquire = require('proxyquire');
var util = require('util');
var BaseService = require('../lib/service');
var index = require('../lib');
var log = index.log;
describe('Bitcore Node', function() {
describe('Node', function() {
var baseConfig = {};
@ -22,10 +20,6 @@ describe('Bitcore Node', function() {
Node.prototype._initialize = sinon.spy();
});
after(function() {
Networks.disableRegtest();
});
describe('@constructor', function() {
var TestService;
before(function() {
@ -34,12 +28,13 @@ describe('Bitcore Node', function() {
});
it('will set properties', function() {
var config = {
network: 'testnet',
services: [
{
name: 'test1',
module: TestService
}
],
]
};
var TestNode = proxyquire('../lib/node', {});
TestNode.prototype.start = sinon.spy();
@ -47,12 +42,12 @@ describe('Bitcore Node', function() {
node._unloadedServices.length.should.equal(1);
node._unloadedServices[0].name.should.equal('test1');
node._unloadedServices[0].module.should.equal(TestService);
node.network.should.equal(Networks.defaultNetwork);
node.network.should.equal('testnet');
var node2 = TestNode(config);
node2._unloadedServices.length.should.equal(1);
node2._unloadedServices[0].name.should.equal('test1');
node2._unloadedServices[0].module.should.equal(TestService);
node2.network.should.equal(Networks.defaultNetwork);
node2.network.should.equal('testnet');
});
it('will set network to testnet', function() {
var config = {
@ -67,7 +62,7 @@ describe('Bitcore Node', function() {
var TestNode = proxyquire('../lib/node', {});
TestNode.prototype.start = sinon.spy();
var node = new TestNode(config);
node.network.should.equal(Networks.testnet);
node.network.should.equal('testnet');
});
it('will set network to regtest', function() {
var config = {
@ -82,9 +77,7 @@ describe('Bitcore Node', function() {
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);
node.network.should.equal('regtest');
});
it('will be able to disable log formatting', function() {
var config = {
@ -97,11 +90,12 @@ describe('Bitcore Node', function() {
],
formatLogs: false
};
var TestNode = proxyquire('../lib/node', {});
var node = new TestNode(config);
node.log.formatting.should.equal(false);
var TestNode = proxyquire('../lib/node', {});
TestNode = proxyquire('../lib/node', {});
config.formatLogs = true;
var node2 = new TestNode(config);
node2.log.formatting.should.equal(true);
@ -189,7 +183,7 @@ describe('Bitcore Node', function() {
});
});
describe('#getServiceOrder', function() {
describe('#_getServiceOrder', function() {
it('should return the services in the correct order', function() {
var node = new Node(baseConfig);
node._unloadedServices = [
@ -218,7 +212,7 @@ describe('Bitcore Node', function() {
}
}
];
var order = node.getServiceOrder();
var order = node._getServiceOrder(node._unloadedServices);
order[0].name.should.equal('daemon');
order[1].name.should.equal('p2p');
order[2].name.should.equal('db');
@ -336,7 +330,7 @@ describe('Bitcore Node', function() {
];
};
node.getServiceOrder = sinon.stub().returns([
node._getServiceOrder = sinon.stub().returns([
{
name: 'test1',
module: TestService,
@ -379,7 +373,7 @@ describe('Bitcore Node', function() {
];
};
node.getServiceOrder = sinon.stub().returns([
node._getServiceOrder = sinon.stub().returns([
{
name: 'test',
module: TestService,
@ -399,6 +393,7 @@ describe('Bitcore Node', function() {
});
});
it('will handle service with getAPIMethods undefined', function(done) {
var node = new Node(baseConfig);
@ -407,7 +402,7 @@ describe('Bitcore Node', function() {
TestService.prototype.start = sinon.stub().callsArg(0);
TestService.prototype.getData = function() {};
node.getServiceOrder = sinon.stub().returns([
node._getServiceOrder = sinon.stub().returns([
{
name: 'test',
module: TestService,
@ -423,30 +418,6 @@ describe('Bitcore Node', function() {
});
});
describe('#getNetworkName', function() {
afterEach(function() {
bitcore.Networks.disableRegtest();
});
it('it will return the network name for livenet', function() {
var node = new Node(baseConfig);
node.getNetworkName().should.equal('livenet');
});
it('it will return the network name for testnet', function() {
var baseConfig = {
network: 'testnet'
};
var node = new Node(baseConfig);
node.getNetworkName().should.equal('testnet');
});
it('it will return the network for regtest', function() {
var baseConfig = {
network: 'regtest'
};
var node = new Node(baseConfig);
node.getNetworkName().should.equal('regtest');
});
});
describe('#stop', function() {
var sandbox = sinon.sandbox.create();
beforeEach(function() {
@ -471,7 +442,7 @@ describe('Bitcore Node', function() {
};
node.test2 = {};
node.test2.stop = sinon.stub().callsArg(0);
node.getServiceOrder = sinon.stub().returns([
node._getServiceOrder = sinon.stub().returns([
{
name: 'test1',
module: TestService

30
test/regtest/comms.txt Normal file
View File

@ -0,0 +1,30 @@
[2017-08-16T13:44:43.245Z] info: Connecting to p2p network.
client sending: magic:: 0b110907 command:: 76657273696f6e0000000000 length:: 65000000 checksum:: 735475bc message:: 7111010001000000000000004b4c945900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e2b8235df4c158a0f2f626974636f72653a312e312e322f0000000001
server sending: magic:: 0b110907 command:: 76657273696f6e0000000000 length:: 67000000 checksum:: 46b5c9ae
client sending: magic:: 0b110907 command:: 76657261636b000000000000 length:: 00000000 checksum:: 5df6e0e2 message::
server sending: magic:: 0b110907 command:: 76657261636b000000000000 length:: 00000000 checksum:: 5df6e0e2
[2017-08-16T13:44:43.261Z] info: Connected to peer: 192.168.3.5, network: regtest, version: 70015, subversion: /Satoshi:0.14.99/, status: ready, port: 18333, best height: 1178711
[2017-08-16T13:44:43.262Z] info: Header Service: Gathering: 2001 header(s) from the peer-to-peer network.
[2017-08-16T13:44:43.262Z] info: Header Service: download progress: 1176710/1178711 (99.83%)
client sending: magic:: 0b110907 command:: 676574686561646572730000 length:: 45000000 checksum:: 857caf8b message:: 7111010001145ed5b8587723d506f208c0aaf9c4d628bcba4bacd1d30f90270000000000000000000000000000000000000000000000000000000000000000000000000000
server sending: magic:: command:: length:: checksum::
server sending: magic:: 0b110907 command:: 616c65727400000000000000 length:: a8000000 checksum:: 1bf9aaea
server sending: magic:: 0b110907 command:: 70696e670000000000000000 length:: 08000000 checksum:: 7c640b03
client sending: magic:: 0b110907 command:: 706f6e670000000000000000 length:: 08000000 checksum:: 7c640b03 message:: e79ac440be90a476
server sending: magic:: 0b110907 command:: 676574686561646572730000 length:: 25040000 checksum:: 552dc886
server sending: magic:: command:: length:: checksum::
server sending: magic:: 0b110907 command:: 686561646572730000000000 length:: d3780200 checksum:: 29213586
server sending: magic:: 0b110907 command:: 686561646572730000000000 length:: d3780200 checksum:: 29213586
server sending: magic:: 0b110907 command:: 686561646572730000000000 length:: d3780200 checksum:: 29213586
server sending: magic:: 0b110907 command:: 686561646572730000000000 length:: d3780200 checksum:: 29213586
server sending: magic:: command:: length:: checksum::
[2017-08-16T13:44:43.411Z] info: Header Service: download progress: 1178710/1178711 (100.00%)
client sending: magic:: 0b110907 command:: 676574686561646572730000 length:: 45000000 checksum:: cd33b9da message:: 71110100019f1309c60de611c5cdec7e0b24fb00da0d16fb706f1ae21a500f0000000000000000000000000000000000000000000000000000000000000000000000000000
server sending: magic:: 0b110907 command:: 686561646572730000000000 length:: 52000000 checksum:: a4022af1
server sending: magic:: 0b110907 command:: 686561646572730000000000 length:: 52000000 checksum:: a4022af1
server sending: magic:: command:: length:: checksum::
[2017-08-16T13:44:43.419Z] info: localhost-header subscribe: p2p/block total: 1
[2017-08-16T13:44:43.419Z] info: Header Service: emitting headers to block service.
[2017-08-16T13:44:43.419Z] info: Block Service: Gathering: 0 block(s) from the peer-to-peer network.
[2017-08-16T13:44:43.419Z] info: Block Service: The best block hash is: 00000000000004842ea914123b8010541a41174a11ba62b244d0aec19840467c at height: 1178711

View File

@ -0,0 +1,10 @@
[
"0000002006226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f0520bf1be2e869376908f7c637214f9f0f9f4fd86c819a8c98a3c5f8d70a65f805ba9559ffff7f20000000000102000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03510101ffffffff0200f2052a01000000232103e91a98edf4e457b5d5aa1e50e35ce6afb67bf7a9ff98b5c36dabbe994b080205ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf900000000",
"00000020bd5725c4bc8fb66032523ca18eb2c809ca1935ab35baf5a1bbaf60ef9a616b2b373d5a12492652b1d22c28ce5c6eb6b22f03c69db1eae9a368e1b544147583eb06ba9559ffff7f20030000000102000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03520101ffffffff0200f2052a01000000232103e91a98edf4e457b5d5aa1e50e35ce6afb67bf7a9ff98b5c36dabbe994b080205ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf900000000",
"00000020af97cfd581a5f0da7925c346c371f6c58131a6b00a95060562f90607aa111e5f97c7b866d2503ee4982569a486f6e25023e72bef588bdef60a06fc740e50001906ba9559ffff7f20020000000102000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03530101ffffffff0200f2052a01000000232103e91a98edf4e457b5d5aa1e50e35ce6afb67bf7a9ff98b5c36dabbe994b080205ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf900000000",
"000000201c9d9eb8064b9508b3cd10de5927cc47347a6899b66946ecc22d52b04c2f3c5f99435c3a200a2419695565432e3115a5ff0d57ed1975b9efe0c7ad1a709a6a4e07ba9559ffff7f20000000000102000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03540101ffffffff0200f2052a01000000232103e91a98edf4e457b5d5aa1e50e35ce6afb67bf7a9ff98b5c36dabbe994b080205ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf900000000",
"00000020de37a9c9fbb1dc3ea5ca9e148e5310d639649814b45a424afe1d58a18db5d8327b529f7d58238330d55b82228ea14abcab965319de7e59c6534758c91c137fcb07ba9559ffff7f20020000000102000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03550101ffffffff0200f2052a01000000232103e91a98edf4e457b5d5aa1e50e35ce6afb67bf7a9ff98b5c36dabbe994b080205ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf900000000",
"00000020e3aa93afbba267bec962a7a38bd064478e5c082e9a73e434b20544950ad8395b3b4a71816a4ce7973d0bcc6a583441fca260f71a175bd2887a9e3e40e4badcd355bb9559ffff7f20000000000102000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03560101ffffffff0200f2052a010000002321023886024ea5984e57b35c3b339f5aee097819ac55235e4fd5822a6ad0a4de1b55ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf900000000",
"000000201a3c951a20b5d603144ce060c86e95fed1869524e66acfc46bdf08d96f6642094916aa5b965b0016fb8ba8b58e99f6b3edbe1a844aa7948adaccf7f28f08f914b9cb9559ffff7f20030000000102000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03570101ffffffff0200f2052a01000000232102a8ef631320be3e6203329acba88fe0a663c19d59fee8240592dc2a32553a4159ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf900000000"
]

View File

@ -0,0 +1,4 @@
[
"000000201a3c951a20b5d603144ce060c86e95fed1869524e66acfc46bdf08d96f664209b4b1c32ec485f4ad27c5402a1b16a0b1135364b7c9b0dcf4276f9fa3fd215d1b08cc9559ffff7f20000000000102000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03570101ffffffff0200f2052a01000000232102a5566542d1f0f202541d98755628a41dcd4416b50db820e2b04d5ecb0bd02b73ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf900000000"
]

276
test/regtest/reorg.js Normal file
View File

@ -0,0 +1,276 @@
'use strict';
var expect = require('chai').expect;
var net = require('net');
var spawn = require('child_process').spawn;
var path = require('path');
var rimraf = require('rimraf');
var mkdirp = require('mkdirp');
var fs = require('fs');
var p2p = require('bitcore-p2p');
var bitcore = require('bitcore-lib');
var Networks = bitcore.Networks;
var Header = bitcore.BlockHeader;
var Block = bitcore.Block;
var BcoinBlock = require('bcoin').block;
var http = require('http');
Networks.enableRegtest();
var messages = new p2p.Messages({ network: Networks.get('regtest'), Block: BcoinBlock });
var server;
var rawBlocks = require('./data/blocks.json');
var rawReorgBlocks = require('./data/blocks_reorg.json')[0];
var reorgBlock = BcoinBlock.fromRaw(rawReorgBlocks, 'hex');
var blocks = rawBlocks.map(function(rawBlock) {
return new Block(new Buffer(rawBlock, 'hex'));
});
var headers = blocks.map(function(block) {
return block.header;
});
var debug = true;
var bitcoreDataDir = '/tmp/bitcore';
var bitcore = {
configFile: {
file: bitcoreDataDir + '/bitcore-node.json',
conf: {
network: 'regtest',
port: 53001,
datadir: bitcoreDataDir,
services: [
'p2p',
'db',
'header',
'block',
'address',
'transaction',
'mempool',
'web',
'insight-api',
'fee',
'timestamp'
],
servicesConfig: {
'p2p': {
'peers': [
{ 'ip': { 'v4': '127.0.0.1' }, port: 18444 }
]
},
'insight-api': {
'routePrefix': 'api'
}
}
}
},
httpOpts: {
protocol: 'http:',
hostname: 'localhost',
port: 53001,
},
opts: { cwd: bitcoreDataDir },
datadir: bitcoreDataDir,
exec: path.resolve(__dirname, '../../bin/bitcore-node'),
args: ['start'],
process: null
};
var blockIndex = 0;
var tcpSocket;
var startFakeNode = function() {
server = net.createServer(function(socket) {
tcpSocket = socket;
socket.on('end', function() {
console.log('bitcore-node has ended the connection');
});
socket.on('data', function(data) {
var command = data.slice(4, 16).toString('hex');
var message;
if (command === '76657273696f6e0000000000') { //version
message = messages.Version();
}
if (command === '76657261636b000000000000') { //verack
message = messages.VerAck();
}
if (command === '676574686561646572730000') { //getheaders
message = messages.Headers(headers, { BlockHeader: Header });
}
if (command === '676574626c6f636b73000000') { //getblocks
var block = blocks[blockIndex];
if (!block) {
return;
}
var blockHash = block.hash;
var inv = p2p.Inventory.forBlock(blockHash);
message = messages.Inventory([inv]);
}
if (command === '676574646174610000000000') { //getdata
var raw = rawBlocks[blockIndex++];
var blk = BcoinBlock.fromRaw(raw, 'hex');
message = messages.Block(blk, { Block: BcoinBlock });
}
if (message) {
socket.write(message.toBuffer());
}
});
socket.pipe(socket);
});
server.listen(18444, '127.0.0.1');
};
var shutdownFakeNode = function() {
server.close();
};
var shutdownBitcore = function(callback) {
if (bitcore.process) {
bitcore.process.kill();
}
callback();
};
var startBitcore = function(callback) {
rimraf(bitcoreDataDir, function(err) {
if(err) {
return callback(err);
}
mkdirp(bitcoreDataDir, function(err) {
if(err) {
return callback(err);
}
fs.writeFileSync(bitcore.configFile.file, JSON.stringify(bitcore.configFile.conf));
var args = bitcore.args;
bitcore.process = spawn(bitcore.exec, args, bitcore.opts);
bitcore.process.stdout.on('data', function(data) {
if (debug) {
process.stdout.write(data.toString());
}
});
bitcore.process.stderr.on('data', function(data) {
if (debug) {
process.stderr.write(data.toString());
}
});
callback();
});
});
};
describe('Reorg', function() {
// 1. spin up bitcore-node and have it connect to our custom tcp socket
// 2. feed it a few headers
// 3. feed it a few blocks
// 4. feed it a block that reorgs
this.timeout(60000);
before(function(done) {
startFakeNode();
startBitcore(done);
});
after(function(done) {
shutdownFakeNode();
shutdownBitcore(done);
});
it('should reorg correctly when already synced', function(done) {
// at this point we have a fully synced chain at height 7....
// we now want to send a new block number 7 whose prev hash is block 6 (it should be block 7)
// we then should reorg back to block 6 then back up to the new block 7
setTimeout(function() {
console.log('From Test: reorging to block: ' + reorgBlock.rhash());
// send the reorg block
rawBlocks.push(rawReorgBlocks);
var blockHash = reorgBlock.rhash();
var inv = p2p.Inventory.forBlock(blockHash);
var msg = messages.Inventory([inv]);
tcpSocket.write(msg.toBuffer());
// wait 2 secs until the reorg happens, if it takes any longer the test ought to fail anyway
setTimeout(function() {
var error;
var request = http.request('http://localhost:53001/api/block/' + reorgBlock.rhash(), function(res) {
if (res.statusCode !== 200 && res.statusCode !== 201) {
if (error) {
return;
}
return done('Error from bitcore-node webserver: ' + res.statusCode);
}
var resError;
var resData = '';
res.on('error', function(e) {
resError = e;
});
res.on('data', function(data) {
resData += data;
});
res.on('end', function() {
if (error) {
return;
}
var data = JSON.parse(resData);
expect(data.height).to.equal(7);
expect(data.hash).to.equal(reorgBlock.rhash());
done(resError, resData);
});
});
request.on('error', function(e) {
error = e;
done(error);
});
request.write('');
request.end();
}, 2000);
}, 2000);
});
});

View File

@ -1,28 +0,0 @@
'use strict';
var should = require('chai').should();
var path = require('path');
var defaultBaseConfig = require('../../lib/scaffold/default-base-config');
describe('#defaultBaseConfig', function() {
it('will return expected configuration', function() {
var cwd = process.cwd();
var home = process.env.HOME;
var info = defaultBaseConfig();
info.path.should.equal(cwd);
info.config.network.should.equal('livenet');
info.config.port.should.equal(3001);
info.config.services.should.deep.equal(['bitcoind', 'web']);
var bitcoind = info.config.servicesConfig.bitcoind;
bitcoind.spawn.datadir.should.equal(home + '/.bitcoin');
bitcoind.spawn.exec.should.equal(path.resolve(__dirname, '../../bin/bitcoind'));
});
it('be able to specify a network', function() {
var info = defaultBaseConfig({network: 'testnet'});
info.config.network.should.equal('testnet');
});
it('be able to specify a datadir', function() {
var info = defaultBaseConfig({datadir: './data2', network: 'testnet'});
info.config.servicesConfig.bitcoind.spawn.datadir.should.equal('./data2');
});
});

View File

@ -1,106 +0,0 @@
'use strict';
var path = require('path');
var should = require('chai').should();
var sinon = require('sinon');
var proxyquire = require('proxyquire');
describe('#defaultConfig', function() {
var expectedExecPath = path.resolve(__dirname, '../../bin/bitcoind');
it('will return expected configuration', function() {
var config = JSON.stringify({
network: 'livenet',
port: 3001,
services: [
'bitcoind',
'web'
],
servicesConfig: {
bitcoind: {
spawn: {
datadir: process.env.HOME + '/.bitcore/data',
exec: expectedExecPath
}
}
}
}, null, 2);
var defaultConfig = proxyquire('../../lib/scaffold/default-config', {
fs: {
existsSync: sinon.stub().returns(false),
writeFileSync: function(path, data) {
path.should.equal(process.env.HOME + '/.bitcore/bitcore-node.json');
data.should.equal(config);
},
readFileSync: function() {
return config;
}
},
mkdirp: {
sync: sinon.stub()
}
});
var home = process.env.HOME;
var info = defaultConfig();
info.path.should.equal(home + '/.bitcore');
info.config.network.should.equal('livenet');
info.config.port.should.equal(3001);
info.config.services.should.deep.equal(['bitcoind', 'web']);
var bitcoind = info.config.servicesConfig.bitcoind;
should.exist(bitcoind);
bitcoind.spawn.datadir.should.equal(home + '/.bitcore/data');
bitcoind.spawn.exec.should.equal(expectedExecPath);
});
it('will include additional services', function() {
var config = JSON.stringify({
network: 'livenet',
port: 3001,
services: [
'bitcoind',
'web',
'insight-api',
'insight-ui'
],
servicesConfig: {
bitcoind: {
spawn: {
datadir: process.env.HOME + '/.bitcore/data',
exec: expectedExecPath
}
}
}
}, null, 2);
var defaultConfig = proxyquire('../../lib/scaffold/default-config', {
fs: {
existsSync: sinon.stub().returns(false),
writeFileSync: function(path, data) {
path.should.equal(process.env.HOME + '/.bitcore/bitcore-node.json');
data.should.equal(config);
},
readFileSync: function() {
return config;
}
},
mkdirp: {
sync: sinon.stub()
}
});
var home = process.env.HOME;
var info = defaultConfig({
additionalServices: ['insight-api', 'insight-ui']
});
info.path.should.equal(home + '/.bitcore');
info.config.network.should.equal('livenet');
info.config.port.should.equal(3001);
info.config.services.should.deep.equal([
'bitcoind',
'web',
'insight-api',
'insight-ui'
]);
var bitcoind = info.config.servicesConfig.bitcoind;
should.exist(bitcoind);
bitcoind.spawn.datadir.should.equal(home + '/.bitcore/data');
bitcoind.spawn.exec.should.equal(expectedExecPath);
});
});

View File

@ -1,79 +0,0 @@
'use strict';
var fs = require('fs');
var path = require('path');
var should = require('chai').should();
var sinon = require('sinon');
var mkdirp = require('mkdirp');
var rimraf = require('rimraf');
var findConfig = require('../../lib/scaffold/find-config');
describe('#findConfig', function() {
var testDir = path.resolve(__dirname, '../temporary-test-data');
var expectedConfig = {
name: 'My Node'
};
before(function(done) {
// setup testing directories
mkdirp(testDir + '/p2/p1/p0', function(err) {
if (err) {
throw err;
}
fs.writeFile(
testDir + '/p2/bitcore-node.json',
JSON.stringify(expectedConfig),
function() {
mkdirp(testDir + '/e0', function(err) {
if (err) {
throw err;
}
done();
});
}
);
});
});
after(function(done) {
// cleanup testing directories
rimraf(testDir, function(err) {
if (err) {
throw err;
}
done();
});
});
describe('will find a configuration file', function() {
it('in the current directory', function() {
var config = findConfig(path.resolve(testDir, 'p2'));
config.path.should.equal(path.resolve(testDir, 'p2'));
config.config.should.deep.equal(expectedConfig);
});
it('in a parent directory', function() {
var config = findConfig(path.resolve(testDir, 'p2/p1'));
config.path.should.equal(path.resolve(testDir, 'p2'));
config.config.should.deep.equal(expectedConfig);
});
it('recursively find in parent directories', function() {
var config = findConfig(path.resolve(testDir, 'p2/p1/p0'));
config.path.should.equal(path.resolve(testDir, 'p2'));
config.config.should.deep.equal(expectedConfig);
});
});
it('will return false if missing a configuration', function() {
var config = findConfig(path.resolve(testDir, 'e0'));
config.should.equal(false);
});
});

View File

@ -1,136 +0,0 @@
'use strict';
var should = require('chai').should();
var sinon = require('sinon');
var proxyquire = require('proxyquire');
var BitcoinService = require('../../lib/services/bitcoind');
var index = require('../../lib');
var log = index.log;
describe('#start', function() {
var sandbox = sinon.sandbox.create();
beforeEach(function() {
sandbox.stub(log, 'error');
});
afterEach(function() {
sandbox.restore();
});
describe('will dynamically create a node from a configuration', function() {
it('require each bitcore-node service with default config', function(done) {
var node;
var TestNode = function(options) {
options.services[0].should.deep.equal({
name: 'bitcoind',
module: BitcoinService,
config: {
spawn: {
datadir: './data'
}
}
});
};
TestNode.prototype.start = sinon.stub().callsArg(0);
TestNode.prototype.on = sinon.stub();
TestNode.prototype.chain = {
on: sinon.stub()
};
var starttest = proxyquire('../../lib/scaffold/start', {
'../node': TestNode
});
starttest.registerExitHandlers = sinon.stub();
node = starttest({
path: __dirname,
config: {
services: [
'bitcoind'
],
servicesConfig: {
bitcoind: {
spawn: {
datadir: './data'
}
}
}
}
});
node.should.be.instanceof(TestNode);
done();
});
it('shutdown with an error from start', function(done) {
var TestNode = proxyquire('../../lib/node', {});
TestNode.prototype.start = function(callback) {
setImmediate(function() {
callback(new Error('error'));
});
};
var starttest = proxyquire('../../lib/scaffold/start', {
'../node': TestNode
});
starttest.cleanShutdown = sinon.stub();
starttest.registerExitHandlers = sinon.stub();
starttest({
path: __dirname,
config: {
services: [],
servicesConfig: {}
}
});
setImmediate(function() {
starttest.cleanShutdown.callCount.should.equal(1);
done();
});
});
it('require each bitcore-node service with explicit config', function(done) {
var node;
var TestNode = function(options) {
options.services[0].should.deep.equal({
name: 'bitcoind',
module: BitcoinService,
config: {
param: 'test',
spawn: {
datadir: './data'
}
}
});
};
TestNode.prototype.start = sinon.stub().callsArg(0);
TestNode.prototype.on = sinon.stub();
TestNode.prototype.chain = {
on: sinon.stub()
};
var starttest = proxyquire('../../lib/scaffold/start', {
'../node': TestNode
});
starttest.registerExitHandlers = sinon.stub();
node = starttest({
path: __dirname,
config: {
services: [
'bitcoind'
],
servicesConfig: {
'bitcoind': {
param: 'test',
spawn: {
datadir: './data'
}
}
},
}
});
node.should.be.instanceof(TestNode);
done();
});
});
});

View File

@ -1,274 +0,0 @@
'use strict';
var should = require('chai').should();
var EventEmitter = require('events').EventEmitter;
var path = require('path');
var sinon = require('sinon');
var proxyquire = require('proxyquire');
var start = require('../../lib/scaffold/start');
describe('#start', function() {
describe('#checkConfigVersion2', function() {
var sandbox = sinon.sandbox.create();
beforeEach(function() {
sandbox.stub(console, 'warn');
});
afterEach(function() {
sandbox.restore();
});
it('will give true with "datadir" at root', function() {
var checkConfigVersion2 = proxyquire('../../lib/scaffold/start', {}).checkConfigVersion2;
var v2 = checkConfigVersion2({datadir: '/home/user/.bitcore/data', services: []});
v2.should.equal(true);
});
it('will give true with "address" service enabled', function() {
var checkConfigVersion2 = proxyquire('../../lib/scaffold/start', {}).checkConfigVersion2;
var v2 = checkConfigVersion2({services: ['address']});
v2.should.equal(true);
});
it('will give true with "db" service enabled', function() {
var checkConfigVersion2 = proxyquire('../../lib/scaffold/start', {}).checkConfigVersion2;
var v2 = checkConfigVersion2({services: ['db']});
v2.should.equal(true);
});
it('will give false without "datadir" at root and "address", "db" services disabled', function() {
var checkConfigVersion2 = proxyquire('../../lib/scaffold/start', {}).checkConfigVersion2;
var v2 = checkConfigVersion2({services: []});
v2.should.equal(false);
});
});
describe('#setupServices', function() {
var cwd = process.cwd();
var setupServices = proxyquire('../../lib/scaffold/start', {}).setupServices;
it('will require an internal module', function() {
function InternalService() {}
InternalService.dependencies = [];
InternalService.prototype.start = sinon.stub();
InternalService.prototype.stop = sinon.stub();
var expectedPath = path.resolve(__dirname, '../../lib/services/internal');
var testRequire = function(p) {
p.should.equal(expectedPath);
return InternalService;
};
var config = {
services: ['internal'],
servicesConfig: {
internal: {
param: 'value'
}
}
};
var services = setupServices(testRequire, cwd, config);
services[0].name.should.equal('internal');
services[0].config.should.deep.equal({param: 'value'});
services[0].module.should.equal(InternalService);
});
it('will require a local module', function() {
function LocalService() {}
LocalService.dependencies = [];
LocalService.prototype.start = sinon.stub();
LocalService.prototype.stop = sinon.stub();
var notfoundPath = path.resolve(__dirname, '../../lib/services/local');
var testRequire = function(p) {
if (p === notfoundPath) {
throw new Error();
} else if (p === 'local') {
return LocalService;
} else if (p === 'local/package.json') {
return {
name: 'local'
};
}
};
var config = {
services: ['local']
};
var services = setupServices(testRequire, cwd, config);
services[0].name.should.equal('local');
services[0].module.should.equal(LocalService);
});
it('will require a local module with "bitcoreNode" in package.json', function() {
function LocalService() {}
LocalService.dependencies = [];
LocalService.prototype.start = sinon.stub();
LocalService.prototype.stop = sinon.stub();
var notfoundPath = path.resolve(__dirname, '../../lib/services/local');
var testRequire = function(p) {
if (p === notfoundPath) {
throw new Error();
} else if (p === 'local/package.json') {
return {
name: 'local',
bitcoreNode: 'lib/bitcoreNode.js'
};
} else if (p === 'local/lib/bitcoreNode.js') {
return LocalService;
}
};
var config = {
services: ['local']
};
var services = setupServices(testRequire, cwd, config);
services[0].name.should.equal('local');
services[0].module.should.equal(LocalService);
});
it('will throw error if module is incompatible', function() {
var internal = {};
var testRequire = function() {
return internal;
};
var config = {
services: ['bitcoind']
};
(function() {
setupServices(testRequire, cwd, config);
}).should.throw('Could not load service');
});
});
describe('#cleanShutdown', function() {
it('will call node stop and process exit', function() {
var log = {
info: sinon.stub(),
error: sinon.stub()
};
var cleanShutdown = proxyquire('../../lib/scaffold/start', {
'../': {
log: log
}
}).cleanShutdown;
var node = {
stop: sinon.stub().callsArg(0)
};
var _process = {
exit: sinon.stub()
};
cleanShutdown(_process, node);
setImmediate(function() {
node.stop.callCount.should.equal(1);
_process.exit.callCount.should.equal(1);
_process.exit.args[0][0].should.equal(0);
});
});
it('will log error during shutdown and exit with status 1', function() {
var log = {
info: sinon.stub(),
error: sinon.stub()
};
var cleanShutdown = proxyquire('../../lib/scaffold/start', {
'../': {
log: log
}
}).cleanShutdown;
var node = {
stop: sinon.stub().callsArgWith(0, new Error('test'))
};
var _process = {
exit: sinon.stub()
};
cleanShutdown(_process, node);
setImmediate(function() {
node.stop.callCount.should.equal(1);
log.error.callCount.should.equal(1);
_process.exit.callCount.should.equal(1);
_process.exit.args[0][0].should.equal(1);
});
});
});
describe('#registerExitHandlers', function() {
var log = {
info: sinon.stub(),
error: sinon.stub()
};
var registerExitHandlers = proxyquire('../../lib/scaffold/start', {
'../': {
log: log
}
}).registerExitHandlers;
it('log, stop and exit with an `uncaughtException`', function(done) {
var proc = new EventEmitter();
proc.exit = sinon.stub();
var node = {
stop: sinon.stub().callsArg(0)
};
registerExitHandlers(proc, node);
proc.emit('uncaughtException', new Error('test'));
setImmediate(function() {
node.stop.callCount.should.equal(1);
proc.exit.callCount.should.equal(1);
done();
});
});
it('stop and exit on `SIGINT`', function(done) {
var proc = new EventEmitter();
proc.exit = sinon.stub();
var node = {
stop: sinon.stub().callsArg(0)
};
registerExitHandlers(proc, node);
proc.emit('SIGINT');
setImmediate(function() {
node.stop.callCount.should.equal(1);
proc.exit.callCount.should.equal(1);
done();
});
});
});
describe('#registerExitHandlers', function() {
var stub;
var registerExitHandlers = require('../../lib/scaffold/start').registerExitHandlers;
before(function() {
stub = sinon.stub(process, 'on');
});
after(function() {
stub.restore();
});
it('should setup two listeners on process when registering exit handlers', function() {
registerExitHandlers(process, {});
stub.callCount.should.equal(2);
});
describe('#exitHandler', function() {
var sandbox;
var cleanShutdown;
var exitHandler;
var logStub;
before(function() {
sandbox = sinon.sandbox.create();
var start = require('../../lib/scaffold/start');
var log = require('../../lib').log;
logStub = sandbox.stub(log, 'error');
cleanShutdown = sandbox.stub(start, 'cleanShutdown', function() {});
exitHandler = require('../../lib/scaffold/start').exitHandler;
});
after(function() {
sandbox.restore();
});
it('should replace the listener for SIGINT after the first SIGINT is handled', function() {
var options = { sigint: true };
var node = {};
exitHandler(options, process, node);
cleanShutdown.callCount.should.equal(1);
exitHandler(options, process, node);
cleanShutdown.callCount.should.equal(1);
});
it('should log all errors and stops the services nonetheless', function() {
var options = { sigint: true };
var stop = sinon.stub();
var node = {
stop: stop
};
exitHandler(options, process, node, new Error('some error'));
logStub.callCount.should.equal(2);
stop.callCount.should.equal(1);
});
});
});
});

View File

@ -0,0 +1,84 @@
'use strict';
var bitcore = require('bitcore-lib');
var should = require('chai').should();
var Encoding = require('../../../lib/services/address/encoding');
describe('Address service encoding', function() {
var servicePrefix = new Buffer('0000', 'hex');
var encoding = new Encoding(servicePrefix);
var txid = '91b58f19b6eecba94ed0f6e463e8e334ec0bcda7880e2985c82a8f32e4d03add';
var address = '1EZBqbJSHFKSkVPNKzc5v26HA6nAHiTXq6';
var height = 1;
var addressSizeBuf = new Buffer(1);
var prefix0 = new Buffer('00', 'hex');
var prefix1 = new Buffer('01', 'hex');
var ts = Math.floor(new Date('2017-02-28').getTime() / 1000);
var tsBuf = new Buffer(4);
tsBuf.writeUInt32BE(ts);
addressSizeBuf.writeUInt8(address.length);
var addressIndexKeyBuf = Buffer.concat([
servicePrefix,
prefix0,
addressSizeBuf,
new Buffer(address),
new Buffer('00000001', 'hex'),
new Buffer(txid, 'hex'),
new Buffer('00000000', 'hex'),
new Buffer('00', 'hex'),
tsBuf
]);
var outputIndex = 5;
var utxoKeyBuf = Buffer.concat([
servicePrefix,
prefix1,
addressSizeBuf,
new Buffer(address),
new Buffer(txid, 'hex'),
new Buffer('00000005', 'hex')]);
var txHex = '0100000001cc3ffe0638792c8b39328bb490caaefe2cf418f2ce0144956e0c22515f29724d010000006a473044022030ce9fa68d1a32abf0cd4adecf90fb998375b64fe887c6987278452b068ae74c022036a7d00d1c8af19e298e04f14294c807ebda51a20389ad751b4ff3c032cf8990012103acfcb348abb526526a9f63214639d79183871311c05b2eebc727adfdd016514fffffffff02f6ae7d04000000001976a9144455183e407ee4d3423858c8a3275918aedcd18e88aca99b9b08010000001976a9140beceae2c29bfde08d2b6d80b33067451c5887be88ac00000000';
var tx = new bitcore.Transaction(txHex);
var sats = tx.outputs[0].satoshis;
var satsBuf = new Buffer(8);
satsBuf.writeDoubleBE(sats);
var utxoValueBuf = Buffer.concat([new Buffer('00000001', 'hex'), satsBuf, tsBuf, tx.outputs[0]._scriptBuffer]);
it('should encode address key' , function() {
encoding.encodeAddressIndexKey(address, height, txid, 0, 0, ts).should.deep.equal(addressIndexKeyBuf);
});
it('should decode address key', function() {
var addressIndexKey = encoding.decodeAddressIndexKey(addressIndexKeyBuf);
addressIndexKey.address.should.equal(address);
addressIndexKey.txid.should.equal(txid);
addressIndexKey.height.should.equal(height);
});
it('should encode utxo key', function() {
encoding.encodeUtxoIndexKey(address, txid, outputIndex).should.deep.equal(utxoKeyBuf);
});
it('should decode utxo key', function() {
var utxoKey = encoding.decodeUtxoIndexKey(utxoKeyBuf);
utxoKey.address.should.equal(address);
utxoKey.txid.should.equal(txid);
utxoKey.outputIndex.should.equal(outputIndex);
});
it('should encode utxo value', function() {
encoding.encodeUtxoIndexValue(
height,
tx.outputs[0].satoshis,
ts,
tx.outputs[0]._scriptBuffer).should.deep.equal(utxoValueBuf);
});
it('should decode utxo value', function() {
var utxoValue = encoding.decodeUtxoIndexValue(utxoValueBuf);
utxoValue.height.should.equal(height);
utxoValue.satoshis.should.equal(sats);
utxoValue.script.should.deep.equal(tx.outputs[0]._scriptBuffer);
utxoValue.timestamp.should.equal(ts);
});
});

View File

@ -0,0 +1,224 @@
'use strict';
var sinon = require('sinon');
var AddressService = require('../../../lib/services/address');
var Tx = require('bcoin').tx;
var expect = require('chai').expect;
var Encoding = require('../../../lib/services/address/encoding');
var Readable = require('stream').Readable;
var EventEmitter = require('events').EventEmitter;
var bcoin = require('bcoin');
describe('Address Service', function() {
var tx = Tx.fromRaw( '0100000004de9b4bb17f627096a9ee0b4528e4eae17df5b5c69edc29704c2e84a7371db29f010000006b483045022100f5b1a0d33b7be291c3953c25f8ae39d98601aa7099a8674daf638a08b86c7173022006ce372da5ad088a1cc6e5c49c2760a1b6f085eb1b51b502211b6bc9508661f9012102ec5e3731e54475dd2902326f43602a03ae3d62753324139163f81f20e787514cffffffff7a1d4e5fc2b8177ec738cd723a16cf2bf493791e55573445fc0df630fe5e2d64010000006b483045022100cf97f6cb8f126703e9768545dfb20ffb10ba78ae3d101aa46775f5a239b075fc02203150c4a89a11eaf5e404f4f96b62efa4455e9525765a025525c7105a7e47b6db012102c01e11b1d331f999bbdb83e8831de503cd52a01e3834a95ccafd615c67703d77ffffffff9e52447116415ca0d0567418a1a4ef8f27be3ff5a96bf87c922f3723d7db5d7c000000006b483045022100f6c117e536701be41a6b0b544d7c3b1091301e4e64a6265b6eb167b15d16959d022076916de4b115e700964194ce36a24cb9105f86482f4abbc63110c3f537cd5770012102ddf84cc7bee2d6a82ac09628a8ad4a26cd449fc528b81e7e6cc615707b8169dfffffffff5815d9750eb3572e30d6fd9df7afb4dbd76e042f3aa4988ac763b3fdf8397f80010000006a473044022028f4402b736066d93d2a32b28ccd3b7a21d84bb58fcd07fe392a611db94cdec5022018902ee0bf2c3c840c1b81ead4e6c87c88c48b2005bf5eea796464e561a620a8012102b6cdd1a6cd129ef796faeedb0b840fcd0ca00c57e16e38e46ee7028d59812ae7ffffffff0220a10700000000001976a914c342bcd1a7784d9842f7386b8b3b8a3d4171a06e88ac59611100000000001976a91449f8c749a9960dc29b5cbe7d2397cea7d26611bb88ac00000000', 'hex');
var blocks = require('../../regtest/data/blocks.json');
var addressService;
var sandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
addressService = new AddressService({
node: {
getNetworkName: function() { return 'regtest'; },
services: []
}
});
addressService._encoding = new Encoding(new Buffer('0000', 'hex'));
});
afterEach(function() {
sandbox.restore();
});
describe('#start', function() {
it('should get prefix for database', function(done) {
var getPrefix = sandbox.stub().callsArgWith(1, null, new Buffer('ffee', 'hex'));
addressService._db = { getPrefix: getPrefix };
addressService.start(function() {
expect(getPrefix.calledOnce).to.be.true;
done();
});
});
});
describe('#stop', function() {
it('should stop the service', function(done) {
addressService.stop(function() {
done();
});
});
});
describe('#getAddressHistory', function() {
it('should get the address history', function(done) {
sandbox.stub(addressService, '_getAddressHistory').callsArgWith(2, null, {});
addressService.getAddressHistory(['a', 'b', 'c'], { from: 12, to: 14 }, function(err, res) {
if (err) {
return done(err);
}
expect(res).to.be.deep.equal({
totalItems: 3,
from: 12,
to: 14,
items: [ {}, {}, {} ]
});
done();
});
});
});
describe('#_getAddresHistory', function() {
it('should get the address history', function(done) {
var encoding = new Encoding(new Buffer('0001', 'hex'));
addressService._encoding = encoding;
var address = 'a';
var opts = { from: 12, to: 14 };
var txid = '1c6ea4a55a3edaac0a05e93b52908f607376a8fdc5387c492042f8baa6c05085';
var data = [ null, encoding.encodeAddressIndexKey(address, 123, txid, 1, 1) ];
var getTransaction = sandbox.stub().callsArgWith(2, null, {});
addressService._tx = { getTransaction: getTransaction };
var txidStream = new Readable();
txidStream._read = function() {
txidStream.push(data.pop());
}
var createReadStream = sandbox.stub().returns(txidStream);
addressService._db = { createKeyStream: createReadStream };
addressService._getAddressHistory(address, opts, function(err, res) {
if (err) {
return done(err);
}
expect(getTransaction.calledOnce).to.be.true;
expect(res).to.deep.equal([{}]);
done();
});
});
});
describe('#AddressSummary', function() {
it('should get the address summary', function(done) {
var encoding = new Encoding(new Buffer('0001', 'hex'));
addressService._encoding = encoding;
var address = 'a';
var txid = tx.txid();
var data = [ null, encoding.encodeAddressIndexKey(address, 123, txid, 1, 0) ];
var inputValues = [120, 0, 120, 120];
tx.__inputValues = inputValues;
var getTransaction = sandbox.stub().callsArgWith(2, null, tx);
addressService._tx = { getTransaction: getTransaction };
addressService._header = { getBestHeight: function() { return 150; } };
var txidStream = new Readable();
txidStream._read = function() {
txidStream.push(data.pop());
}
var createReadStream = sandbox.stub().returns(txidStream);
addressService._db = { createKeyStream: createReadStream };
addressService.getAddressSummary(address, {}, function(err, res) {
if (err) {
return done(err);
}
expect(getTransaction.calledOnce).to.be.true;
expect(res).to.deep.equal({ addrStr: 'a',
balance: 0.01139033,
balanceSat: 1139033,
totalReceived: 0.01139033,
totalReceivedSat: 1139033,
totalSent: 0,
totalSentSat: 0,
unconfirmedBalance: 0,
unconfirmedBalanceSat: 0,
unconfirmedTxApperances: 0,
txApperances: 1,
transactions: [ '25e28f9fb0ada5353b7d98d85af5524b2f8df5b0b0e2d188f05968bceca603eb' ]
});
done();
});
});
});
describe('#getAddressUnspentOutputs', function() {
it('should get address utxos', function(done) {
var encoding = new Encoding(new Buffer('0001', 'hex'));
addressService._encoding = encoding;
var address = 'a';
var txid = tx.txid();
var ts = Math.floor(new Date('2019-01-01').getTime() / 1000);
var data = {
key: encoding.encodeUtxoIndexKey(address, txid, 1),
value: encoding.encodeUtxoIndexValue(123, 120000, ts, tx.outputs[1].script.raw)
};
addressService._header = { getBestHeight: function() { return 150; } };
var txidStream = new EventEmitter();
var createReadStream = sandbox.stub().returns(txidStream);
addressService._db = { createReadStream: createReadStream };
addressService.getAddressUnspentOutputs(address, {}, function(err, res) {
if (err) {
return done(err);
}
expect(res[0]).to.deep.equal({
address: "a",
amount: 0.0012,
confirmations: 27,
confirmationsFromCache: true,
satoshis: 120000,
scriptPubKey: "76a91449f8c749a9960dc29b5cbe7d2397cea7d26611bb88ac",
ts: 1546300800,
txid: "25e28f9fb0ada5353b7d98d85af5524b2f8df5b0b0e2d188f05968bceca603eb",
vout: 1
});
done();
});
txidStream.emit('data', data);
txidStream.emit('end');
});
});
describe('#onReorg', function() {
it('should reorg', function(done ) {
var commonAncestorHeader = bcoin.block.fromRaw(blocks[5], 'hex').toHeaders().toJSON();
var oldBlocks = [bcoin.block.fromRaw(blocks[6], 'hex')];
addressService.onReorg([commonAncestorHeader, oldBlocks], function(err, ops) {
expect(ops.length).to.equal(1);
expect(ops[0].type).to.equal('del');
done();
});
});
});
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,49 @@
'use strict';
var should = require('chai').should();
var Block = require('bcoin').block;
var Encoding = require('../../../lib/services/block/encoding');
describe('Block service encoding', function() {
var servicePrefix = new Buffer('0000', 'hex');
var encoding = new Encoding(servicePrefix);
var hash = '91b58f19b6eecba94ed0f6e463e8e334ec0bcda7880e2985c82a8f32e4d03add';
var height = 1;
var block = Block.fromRaw('0100000095194b8567fe2e8bbda931afd01a7acd399b9325cb54683e64129bcd00000000660802c98f18fd34fd16d61c63cf447568370124ac5f3be626c2e1c3c9f0052d19a76949ffff001d33f3c25d0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d014dffffffff0100f2052a01000000434104e70a02f5af48a1989bf630d92523c9d14c45c75f7d1b998e962bff6ff9995fc5bdb44f1793b37495d80324acba7c8f537caaf8432b8d47987313060cc82d8a93ac00000000', 'hex');
describe('Block', function() {
it('should encode block key' , function() {
encoding.encodeBlockKey(hash).should.deep.equal(Buffer.concat([
servicePrefix,
new Buffer(hash, 'hex')
]));
});
it('should decode block key' , function() {
var buf = Buffer.concat([
servicePrefix,
new Buffer(hash, 'hex')
]);
var actual = encoding.decodeBlockKey(buf);
actual.should.deep.equal(hash);
});
it('should encode block value', function() {
encoding.encodeBlockValue(block).should.deep.equal(
block.toRaw());
});
it('shound decode block value', function() {
var ret = encoding.decodeBlockValue(block.toRaw());
ret.should.deep.equal(ret);
});
});
});

View File

@ -0,0 +1,196 @@
'use strict';
var expect = require('chai').expect;
var BlockService = require('../../../lib/services/block');
var sinon = require('sinon');
var bcoin = require('bcoin');
var Block = bcoin.block;
var Encoding = require('../../../lib/services/block/encoding');
describe('Block Service', function() {
var blockService;
var blocks = require('../../regtest/data/blocks.json');
var block1 = Block.fromRaw('010000006a39821735ec18a366d95b391a7ff10dee181a198f1789b0550e0d00000000002b0c80fa52b669022c344c3e09e6bb9698ab90707bb4bb412af3fbf31cfd2163a601514c5a0c011c572aef0f0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08045a0c011c022003ffffffff0100f2052a01000000434104c5b694d72e601091fd733c6b18b94795c13e2db6b1474747e7be914b407854cad37cee3058f85373b9f9dbb0014e541c45851d5f85e83a1fd7c45e54423718f3ac00000000', 'hex');
var block2 = Block.fromRaw('01000000fb3c5deea3902d5e6e0222435688795152ae0f737715b0bed6a88b00000000008ec0f92d33b05617cb3c3b4372aa0c2ae3aeb8aa7f34fe587db8e55b578cfac6b601514c5a0c011c98a831000101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08045a0c011c027f01ffffffff0100f2052a0100000043410495fee5189566db550919ad2b4e5f9111dbdc2cb60b5c71ea4c0fdad59a961c42eb289e5b9fdc4cb3f3fec6dd866172720bae3e3b881fc203fcaf98bf902c53f1ac00000000', 'hex');
var sandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
blockService = new BlockService({
node: {
getNetworkName: function() { return 'regtest'; },
services: []
}
});
blockService._encoding = new Encoding(new Buffer('0000', 'hex'));
});
afterEach(function() {
sandbox.restore();
});
describe('#_detectReorg', function() {
it('should detect reorg', function() {
var block = Block.fromRaw(blocks[6], 'hex');
blockService._tip = { hash: bcoin.util.revHex(Block.fromRaw(blocks[5], 'hex').prevBlock) };
expect(blockService._detectReorg(block)).to.be.true;
});
it('should not detect reorg', function() {
var block = Block.fromRaw(blocks[6], 'hex');
blockService._tip = { hash: bcoin.util.revHex(Block.fromRaw(blocks[6], 'hex').prevBlock) };
expect(blockService._detectReorg(block)).to.be.false;
});
});
describe('#_findCommonAncestor', function() {
it('should find the common ancestor between the current chain and the new chain', function(done) {
blockService._tip = { hash: block2.rhash(), height: 70901 };
var encodedData = blockService._encoding.encodeBlockValue(block2);
var get = sandbox.stub().callsArgWith(1, null, encodedData);
var headers = { get: sandbox.stub().returns({ prevHash: block1.rhash() }) };
blockService._db = { get: get };
blockService._findCommonAncestor('aa', headers, function(err, common, oldBlocks) {
if (err) {
return done(err);
}
expect(common).to.equal('aa');
expect(oldBlocks).to.deep.equal([]);
done();
});
});
});
describe('#getBestBlockHash', function() {
it('should get best block hash', function() {
});
});
describe('#getBlock', function() {
it('should get block', function() {
});
});
describe('#getBlockHashesByTimestamp', function() {
it('should get block hashes by timestamp', function() {
});
});
describe('#getBlockHeader', function() {
it('should get block header', function() {
});
});
describe('#getBlockOverview', function() {
it('should get block overview', function() {
});
});
describe('#getRawBlock', function() {
});
describe('#_onBlock', function() {
it('should process blocks', function() {
var processBlock = sandbox.stub(blockService, '_processBlock');
blockService._tip = { hash: block1.rhash(), height: 1 };
blockService._onBlock(block2);
expect(processBlock.calledOnce).to.be.true;
});
it('should not process blocks', function() {
var processBlock = sandbox.stub(blockService, '_processBlock');
blockService._tip = { hash: block2.rhash(), height: 1 };
blockService._onBlock(block1);
expect(processBlock.calledOnce).to.be.false;
});
});
describe('#_setListeners', function() {
it('should set listeners for headers, reorg', function() {
var on = sandbox.stub();
var once = sandbox.stub();
blockService._header = { on: on, once: once };
blockService._setListeners();
expect(on.calledOnce).to.be.true;
expect(once.calledOnce).to.be.true;
});
});
describe('#_setTip', function() {
it('should set the tip if given a block', function() {
blockService._db = {};
blockService._tip = { height: 99, hash: '00' };
blockService._setTip({ height: 100, hash: 'aa' });
expect(blockService._tip).to.deep.equal({ height: 100, hash: 'aa' });
});
});
describe('#_startSubscriptions', function() {
it('should start the subscriptions if not already subscribed', function() {
var on = sinon.stub();
var subscribe = sinon.stub();
var openBus = sinon.stub().returns({ on: on, subscribe: subscribe });
blockService.node = { openBus: openBus };
blockService._startSubscriptions();
expect(blockService._subscribed).to.be.true;
expect(openBus.calledOnce).to.be.true;
expect(on.calledOnce).to.be.true;
expect(subscribe.calledOnce).to.be.true;
});
});
describe('#_startSync', function() {
it('should start the sync of blocks if type set', function() {
blockService._header = { getLastHeader: sinon.stub.returns({ height: 100 }) };
blockService._tip = { height: 98 };
var sync = sandbox.stub(blockService, '_sync');
blockService._startSync();
expect(sync.calledOnce).to.be.true;
});
});
describe('#start', function() {
it('should get the prefix', function(done) {
var getPrefix = sandbox.stub().callsArgWith(1, null, blockService._encoding);
var getServiceTip = sandbox.stub().callsArgWith(1, null, { height: 1, hash: 'aa' });
var setListeners = sandbox.stub(blockService, '_setListeners');
var startSub = sandbox.stub(blockService, '_startSubscriptions');
var setTip = sandbox.stub(blockService, '_setTip');
blockService._db = { getPrefix: getPrefix, getServiceTip: getServiceTip };
blockService.start(function() {
expect(blockService._encoding).to.be.an.instanceof(Encoding);
expect(getServiceTip.calledOnce).to.be.true;
expect(getPrefix.calledOnce).to.be.true;
expect(startSub.calledOnce).to.be.true;
expect(setTip.calledOnce).to.be.true;
done();
});
});
});
describe('#stop', function() {
it('should call stop', function(done) {
blockService.stop(done);
});
});
});

View File

@ -0,0 +1,324 @@
'use strict';
var chai = require('chai');
var should = chai.should();
var assert = chai.assert;
var expect = chai.expect;
var DBService = require('../../../lib/services/db');
var sinon = require('sinon');
var Levelup = require('levelup');
var Tx = require('bcoin').tx;
describe('DB', function() {
var dbService;
var tx = Tx.fromRaw( '0100000004de9b4bb17f627096a9ee0b4528e4eae17df5b5c69edc29704c2e84a7371db29f010000006b483045022100f5b1a0d33b7be291c3953c25f8ae39d98601aa7099a8674daf638a08b86c7173022006ce372da5ad088a1cc6e5c49c2760a1b6f085eb1b51b502211b6bc9508661f9012102ec5e3731e54475dd2902326f43602a03ae3d62753324139163f81f20e787514cffffffff7a1d4e5fc2b8177ec738cd723a16cf2bf493791e55573445fc0df630fe5e2d64010000006b483045022100cf97f6cb8f126703e9768545dfb20ffb10ba78ae3d101aa46775f5a239b075fc02203150c4a89a11eaf5e404f4f96b62efa4455e9525765a025525c7105a7e47b6db012102c01e11b1d331f999bbdb83e8831de503cd52a01e3834a95ccafd615c67703d77ffffffff9e52447116415ca0d0567418a1a4ef8f27be3ff5a96bf87c922f3723d7db5d7c000000006b483045022100f6c117e536701be41a6b0b544d7c3b1091301e4e64a6265b6eb167b15d16959d022076916de4b115e700964194ce36a24cb9105f86482f4abbc63110c3f537cd5770012102ddf84cc7bee2d6a82ac09628a8ad4a26cd449fc528b81e7e6cc615707b8169dfffffffff5815d9750eb3572e30d6fd9df7afb4dbd76e042f3aa4988ac763b3fdf8397f80010000006a473044022028f4402b736066d93d2a32b28ccd3b7a21d84bb58fcd07fe392a611db94cdec5022018902ee0bf2c3c840c1b81ead4e6c87c88c48b2005bf5eea796464e561a620a8012102b6cdd1a6cd129ef796faeedb0b840fcd0ca00c57e16e38e46ee7028d59812ae7ffffffff0220a10700000000001976a914c342bcd1a7784d9842f7386b8b3b8a3d4171a06e88ac59611100000000001976a91449f8c749a9960dc29b5cbe7d2397cea7d26611bb88ac00000000', 'hex');
var sandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
dbService = new DBService({
node: {
services: [],
datadir: '/tmp',
network: 'regtest',
on: sinon.stub()
}
});
});
afterEach(function() {
sandbox.restore();
});
describe('#start', function() {
it('should start the db service by creating a db dir, ' +
' if necessary, and setting the store', function(done) {
dbService.start(function() {
dbService._store.should.be.instanceOf(Levelup);
done();
});
});
});
describe('#stop', function() {
it('should stop if store not open', function(done) {
dbService.stop(function() {
var close = sandbox.stub().callsArg(0);
dbService._store = { close: close };
dbService._stopping.should.be.true;
done();
});
});
it('should stop if store open', function(done) {
dbService.stop(function() {
var close = sandbox.stub().callsArg(0);
dbService._store = { close: close, isOpen: sinon.stub().returns(true) };
dbService._stopping.should.be.true;
done();
});
});
});
describe('#_onError', function() {
it('should stop the db', function() {
var stop = sandbox.stub();
dbService.node = { stop: stop };
dbService._onError(new Error('some error'));
stop.should.be.calledOnce;
});
});
describe('#_setDataPath', function() {
it('should set the data path', function() {
dbService._setDataPath();
dbService.dataPath.should.equal('/tmp/regtest/bitcorenode.db');
});
});
describe('#_setVersion', function() {
it('should set the version', function(done) {
var put = sandbox.stub(dbService, 'put').callsArgWith(2, null);
dbService._setVersion(function(err) {
put.should.be.calledOnce;
put.args[0][0].toString('hex').should.deep.equal('ffff76657273696f6e');
put.args[0][1].toString('hex').should.deep.equal('00000001');
done();
});
});
});
describe('#get', function() {
it('should get a value from the db', function(done) {
var get = sandbox.stub().callsArgWith(2, null, 'data');
dbService._store = { get: get };
dbService.get('key', function(err, value) {
if (err) {
return done(err);
}
value.should.equal('data');
done();
});
});
it('should not get a value while the node is shutting down', function(done) {
dbService._stopping = true;
dbService.get('key', function(err, value) {
err.message.should.equal('Shutdown sequence underway, not able to complete the query');
done();
});
});
});
describe('#put', function() {
it('should put a value in the db', function(done) {
var put = sandbox.stub().callsArgWith(2, null);
dbService._store = { put: put };
dbService.put(new Buffer('key'), new Buffer('value'), function(err) {
if (err) {
return done(err);
}
put.should.be.calledOnce;
done();
});
});
it('should not allow an operation while the node is shutting down', function(done) {
dbService._stopping = true;
dbService.put(new Buffer('key'), new Buffer('value'), function(err) {
done();
});
});
});
describe('#batch', function() {
it('should save a batch of operations', function(done) {
var batch = sandbox.stub().callsArgWith(1, null);
dbService._store = { batch: batch };
dbService.batch([], function(err) {
if(err) {
return done(err);
}
batch.callCount.should.equal(1);
done();
});
});
it('should not call batch whilst shutting down', function(done) {
dbService._stopping = true;
var batch = sandbox.stub().callsArgWith(1, null);
dbService._store = { batch: batch };
dbService.batch(batch, function(err) {
if(err) {
return done(err);
}
batch.callCount.should.equal(0);
done();
});
});
});
describe('#createReadStream', function() {
it('should get a read stream', function() {
var on = sandbox.stub();
var stream = { on: on };
var createReadStream = sandbox.stub().returns(stream);
dbService._store = { createReadStream: createReadStream };
dbService.createReadStream([]).should.deep.equal(stream);
createReadStream.callCount.should.equal(1);
});
it('should not get a read stream if the node is stopping', function() {
dbService._stopping = true;
var on = sandbox.stub();
var stream = { on: on };
var createReadStream = sandbox.stub().returns(stream);
dbService._store = { createReadStream: createReadStream };
var stream = dbService.createReadStream([]);
expect(stream).to.be.undefined;
});
});
describe('#createKeyStream', function() {
it('should get a key stream', function() {
var on = sandbox.stub();
var stream = { on: on };
var createKeyStream = sandbox.stub().returns(stream);
dbService._store = { createKeyStream: createKeyStream };
dbService.createKeyStream([]).should.deep.equal(stream);
createKeyStream.callCount.should.equal(1);
});
it('should not get a key stream if the node is stopping', function() {
dbService._stopping = true;
var on = sandbox.stub();
var stream = { on: on };
var createKeyStream = sandbox.stub().returns(stream);
dbService._store = { createKeyStream: createKeyStream };
var stream = dbService.createKeyStream([]);
expect(stream).to.be.undefined;
});
});
describe('#close', function() {
it('should close the store if there is a store and it is open', function(done) {
var close = sandbox.stub().callsArgWith(0, null);
dbService._store = { isOpen: sinon.stub().returns(true), close: close };
dbService.close(function(err) {
if(err) {
return done(err);
}
close.callCount.should.equal(1);
done();
});
});
});
describe('#getServiceTip', function() {
it('should get service tip for previously saved', function(done) {
var tipBuf = Buffer.concat([ new Buffer('deadbeef', 'hex'), new Buffer(tx.txid(), 'hex') ]);
var get = sandbox.stub(dbService, 'get').callsArgWith(1, null, tipBuf);
dbService.getServiceTip('test', function(err, tip) {
if(err) {
return done(err);
}
get.callCount.should.equal(1);
tip.height.should.equal(0xdeadbeef);
tip.hash.should.equal(tx.txid());
done();
});
});
it('should get service tip for not previously saved', function(done) {
var get = sandbox.stub(dbService, 'get').callsArgWith(1, null, null);
dbService.getServiceTip('test', function(err, tip) {
if(err) {
return done(err);
}
get.callCount.should.equal(1);
tip.height.should.equal(0);
tip.hash.should.equal('0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206');
done();
});
});
});
describe('#getPrefix', function() {
it('should get the db prefix for a service when one already exists', function(done) {
var get = sandbox.stub(dbService, 'get').callsArgWith(1, null, new Buffer('0000', 'hex'));
dbService.getPrefix('test', function(err, prefix) {
if(err) {
return done(err);
}
get.callCount.should.equal(1);
prefix.toString('hex').should.equal('0000');
done();
});
});
it('should get the db prefix for a service when one does not already exist', function(done) {
var put = sandbox.stub(dbService, 'put').callsArgWith(2, null);
var get = sandbox.stub(dbService, 'get');
get.onCall(0).callsArgWith(1, null, null);
get.onCall(1).callsArgWith(1, null, new Buffer('eeee', 'hex'));
dbService.getPrefix('test', function(err, prefix) {
if(err) {
return done(err);
}
get.callCount.should.equal(2);
put.callCount.should.equal(2);
put.args[1][1].toString('hex').should.equal('eeef');
prefix.toString('hex').should.equal('eeee');
done();
});
});
});
});

View File

@ -0,0 +1,54 @@
'use strict';
var sinon = require('sinon');
var FeeService = require('../../../lib/services/fee');
var expect = require('chai').expect;
describe('#Fee Service', function() {
var feeService;
var sandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
feeService = new FeeService({
rpc: {
user: 'bitcoin',
pass: 'local321',
host: 'localhost',
protocol: 'http',
port: 8332
}
});
});
afterEach(function() {
sandbox.restore();
});
/*
Running in regtest mode or unsync'd will return -1
*/
it('Has an estimateFee method', function() {
var method = feeService.getAPIMethods()[0][0];
expect(method).to.equal('estimateFee');
});
it('Can estimate fees', function(done) {
var estimateFee = sinon.stub().callsArgWith(1, null, { result: 0.1 });
feeService._client = { estimateFee: estimateFee };
feeService.estimateFee(4, function(err, fee) {
if (err) {
return done(err);
}
expect(fee).to.equal(0.1);
done();
});
});
});

View File

@ -0,0 +1,88 @@
'use strict';
var should = require('chai').should();
var Encoding = require('../../../lib/services/header/encoding');
describe('Header service encoding', function() {
var servicePrefix = new Buffer('0000', 'hex');
var hashPrefix = new Buffer('00', 'hex');
var heightPrefix = new Buffer('01', 'hex');
var encoding = new Encoding(servicePrefix);
var hash = '91b58f19b6eecba94ed0f6e463e8e334ec0bcda7880e2985c82a8f32e4d03add';
var hashBuf = new Buffer(hash, 'hex');
var header = {
hash: hash,
prevHash: '91b58f19b6eecba94ed0f6e463e8e334ec0bcda7880e2985c82a8f32e4d03ade',
version: 0x2000012,
merkleRoot: '91b58f19b6eecba94ed0f6e463e8e334ec0bcda7880e2985c82a8f32e4d03adf',
timestamp: 1E9,
bits: 400000,
nonce: 123456,
height: 123,
chainwork: '0000000000000000000000000000000000000000000000000000000200020002'
};
var versionBuf = new Buffer(4);
var prevHashBuf = new Buffer(header.prevHash, 'hex');
var merkleRootBuf = new Buffer(header.merkleRoot, 'hex');
var tsBuf = new Buffer(4);
var bitsBuf = new Buffer(4);
var nonceBuf = new Buffer(4);
var heightBuf = new Buffer(4);
var chainBuf = new Buffer('0000000000000000000000000000000000000000000000000000000200020002', 'hex');
heightBuf.writeUInt32BE(header.height);
it('should encode header hash key' , function() {
encoding.encodeHeaderHashKey(hash).should.deep.equal(Buffer.concat([servicePrefix, hashPrefix, hashBuf]));
});
it('should decode header hash key', function() {
encoding.decodeHeaderHashKey(Buffer.concat([servicePrefix, hashPrefix, hashBuf]))
.should.deep.equal(hash);
});
it('should encode header height key' , function() {
encoding.encodeHeaderHeightKey(header.height).should.deep.equal(Buffer.concat([servicePrefix, heightPrefix, heightBuf]));
});
it('should decode header height key', function() {
encoding.decodeHeaderHeightKey(Buffer.concat([servicePrefix, heightPrefix, heightBuf]))
.should.deep.equal(header.height);
});
it('should encode header value', function() {
var prevHashBuf = new Buffer(header.prevHash, 'hex');
versionBuf.writeInt32BE(header.version); // signed
tsBuf.writeUInt32BE(header.timestamp);
bitsBuf.writeUInt32BE(header.bits);
nonceBuf.writeUInt32BE(header.nonce);
heightBuf.writeUInt32BE(header.height);
encoding.encodeHeaderValue(header).should.deep.equal(Buffer.concat([
hashBuf,
versionBuf,
prevHashBuf,
merkleRootBuf,
tsBuf,
bitsBuf,
nonceBuf,
heightBuf,
chainBuf
]));
});
it('should decode header value', function() {
encoding.decodeHeaderValue(Buffer.concat([
hashBuf,
versionBuf,
prevHashBuf,
merkleRootBuf,
tsBuf,
bitsBuf,
nonceBuf,
heightBuf,
chainBuf
])).should.deep.equal(header);
});
});

View File

@ -0,0 +1,162 @@
'use strict';
var sinon = require('sinon');
var HeaderService = require('../../../lib/services/header');
var chai = require('chai');
var assert = chai.assert;
var expect = chai.expect;
var Encoding = require('../../../lib/services/header/encoding');
var utils = require('../../../lib/utils');
var Block = require('bitcore-lib').Block;
var BN = require('bn.js');
var Emitter = require('events').EventEmitter;
describe('Header Service', function() {
var headerService;
var sandbox;
var prevHeader = new Block(new Buffer('01000000b25c0849b469983b4a5b90a49e4c0e4ba3853122ed141b5bd92d14000000000021a8aaa4995e4ce3b885677730b153741feda66a08492287a45c6a131671ba5a72ff504c5a0c011c456e4d060201000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08045a0c011c028208ffffffff0100f2052a010000004341041994d910507ec4b2135dd32a4723caf00f8567f356ffbd5e703786d856b49a89d6597c280d8981238fbde81fa3767161bc3e994c17be41b42235a61c24c73459ac0000000001000000013b517d1aebd89b4034e0cf9b25ecbe82ef162ce71284e92a1f1adebf44ea1409000000008b483045022100c7ebc62e89740ddab42a64435c996e1c91a063f9f2cc004b4f023f7a1be5234402207608837faebec16049461d4ef7de807ce217040fd2a823a29da16ec07e463d440141048f108c0da4b5be3308e2e0b521d02d341de85b36a29285b47f00bc33e57a89cf4b6e76aa4a48ddc9a5e882620779e0f1b19dc98d478052fbd544167c745be1d8ffffffff010026e85a050000001976a914f760ef90462b0a4bde26d597c1f29324f5cd0fc488ac00000000', 'hex')).header.toObject();
var preObjectHeader = new Block(new Buffer('010000006a39821735ec18a366d95b391a7ff10dee181a198f1789b0550e0d00000000002b0c80fa52b669022c344c3e09e6bb9698ab90707bb4bb412af3fbf31cfd2163a601514c5a0c011c572aef0f0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08045a0c011c022003ffffffff0100f2052a01000000434104c5b694d72e601091fd733c6b18b94795c13e2db6b1474747e7be914b407854cad37cee3058f85373b9f9dbb0014e541c45851d5f85e83a1fd7c45e54423718f3ac00000000', 'hex')).header;
var header = preObjectHeader.toObject();
beforeEach(function() {
sandbox = sinon.sandbox.create();
headerService = new HeaderService({
node: {
getNetworkName: function() { return 'regtest'; },
services: []
}
});
headerService._encoding = new Encoding(new Buffer('0000', 'hex'));
});
afterEach(function() {
sandbox.restore();
});
describe('#start', function() {
it('should get prefix for database', function(done) {
var getServiceTip = sandbox.stub().callsArgWith(1, null, { height: 123, hash: 'a' });
var setListeners = sandbox.stub(headerService, '_setListeners');
var getPrefix = sandbox.stub().callsArgWith(1, null, new Buffer('ffee', 'hex'));
var getLastHeader = sandbox.stub(headerService, '_getLastHeader').callsArgWith(0, null);
var openBus = sandbox.stub();
headerService.node = { openBus: openBus };
var _startHeaderSubscription = sandbox.stub(headerService, '_startHeaderSubscription');
headerService._db = { getPrefix: getPrefix, getServiceTip: getServiceTip, batch: sinon.stub() };
headerService.start(function() {
expect(_startHeaderSubscription.calledOnce).to.be.true;
expect(getLastHeader.calledOnce).to.be.true;
expect(setListeners.calledOnce).to.be.true;
expect(headerService._tip).to.be.deep.equal({ height: 123, hash: 'a' });
expect(headerService._encoding).to.be.instanceOf(Encoding);
done();
});
});
});
describe('#stop', function() {
it('should stop the service', function(done) {
headerService.stop(function() {
done();
});
});
});
describe('#getAllHeaders', function() {
it('should get all the headers', function(done) {
headerService._tip = { height: 123 };
var fakeStream = new Emitter();
var createReadStream = sandbox.stub().returns(fakeStream);
headerService._db = { createReadStream: createReadStream };
headerService.getAllHeaders(function(err, headers) {
if (err) {
return done(err);
}
expect(headers.length).to.deep.equal(1);
expect(headers.get(header.hash).hash).to.equal('00000000008ba8d6beb01577730fae52517988564322026e5e2d90a3ee5d3cfb');
done();
});
header.chainwork = '00';
fakeStream.emit('data', { value: headerService._encoding.encodeHeaderValue(header) });
fakeStream.emit('end');
});
});
describe('#_startSync', function() {
it('should start the sync process', function() {
headerService._bestHeight = 123;
headerService._tip = { height: 120 };
var getHeaders = sandbox.stub();
headerService._p2p = { getHeaders: getHeaders };
headerService._startSync();
expect(getHeaders.calledOnce).to.be.true;
});
});
describe('#_sync', function() {
it('should sync header', function() {
headerService._numNeeded = 1000;
headerService._tip = { height: 121, hash: 'a' };
var getHeaders = sandbox.stub();
headerService._p2p = { getHeaders: getHeaders };
headerService._sync();
expect(getHeaders.calledOnce).to.be.true;
});
});
describe('#_onHeaders', function() {
it('should handle new headers received', function() {
var headers = [preObjectHeader];
var onHeader = sandbox.stub(headerService, '_onHeader');
var saveHeaders = sandbox.stub(headerService, '_saveHeaders');
headerService._tip = { height: 123, hash: 'aa' };
headerService._onHeaders(headers);
expect(onHeader.calledOnce).to.be.true;
expect(saveHeaders.calledOnce).to.be.true;
});
});
describe('#_getChainwork', function() {
it('should get chainwork', function() {
prevHeader.chainwork = '000000000000000000000000000000000000000000000000000d4b2e8ee30c08';
var actual = headerService._getChainwork(header, prevHeader);
prevHeader.chainwork = '000000000000000000000000000000000000000000000000000d4b2e8ee30c08';
expect(actual.toString(16, 64)).to.equal('000000000000000000000000000000000000000000000000000d4c22c66d0d72');
});
});
describe('#_computeChainwork', function() {
it('should calculate chain work correctly', function() {
var expected = new BN(new Buffer('000000000000000000000000000000000000000000677c7b8122f9902c79f4e0', 'hex'));
var prev = new BN(new Buffer('000000000000000000000000000000000000000000677bd68118a98f8779ea90', 'hex'));
var actual = headerService._computeChainwork(0x18018d30, prev);
assert(actual.eq(expected), 'not equal: actual: ' + actual + ' expected: ' + expected);
});
});
});

View File

@ -0,0 +1,41 @@
'use strict';
var should = require('chai').should();
var tx = require('bcoin').tx;
var Encoding = require('../../../lib/services/mempool/encoding');
describe('Block service encoding', function() {
var servicePrefix = new Buffer('0000', 'hex');
var encoding = new Encoding(servicePrefix);
var hash = '25e28f9fb0ada5353b7d98d85af5524b2f8df5b0b0e2d188f05968bceca603eb';
var txString = '0100000004de9b4bb17f627096a9ee0b4528e4eae17df5b5c69edc29704c2e84a7371db29f010000006b483045022100f5b1a0d33b7be291c3953c25f8ae39d98601aa7099a8674daf638a08b86c7173022006ce372da5ad088a1cc6e5c49c2760a1b6f085eb1b51b502211b6bc9508661f9012102ec5e3731e54475dd2902326f43602a03ae3d62753324139163f81f20e787514cffffffff7a1d4e5fc2b8177ec738cd723a16cf2bf493791e55573445fc0df630fe5e2d64010000006b483045022100cf97f6cb8f126703e9768545dfb20ffb10ba78ae3d101aa46775f5a239b075fc02203150c4a89a11eaf5e404f4f96b62efa4455e9525765a025525c7105a7e47b6db012102c01e11b1d331f999bbdb83e8831de503cd52a01e3834a95ccafd615c67703d77ffffffff9e52447116415ca0d0567418a1a4ef8f27be3ff5a96bf87c922f3723d7db5d7c000000006b483045022100f6c117e536701be41a6b0b544d7c3b1091301e4e64a6265b6eb167b15d16959d022076916de4b115e700964194ce36a24cb9105f86482f4abbc63110c3f537cd5770012102ddf84cc7bee2d6a82ac09628a8ad4a26cd449fc528b81e7e6cc615707b8169dfffffffff5815d9750eb3572e30d6fd9df7afb4dbd76e042f3aa4988ac763b3fdf8397f80010000006a473044022028f4402b736066d93d2a32b28ccd3b7a21d84bb58fcd07fe392a611db94cdec5022018902ee0bf2c3c840c1b81ead4e6c87c88c48b2005bf5eea796464e561a620a8012102b6cdd1a6cd129ef796faeedb0b840fcd0ca00c57e16e38e46ee7028d59812ae7ffffffff0220a10700000000001976a914c342bcd1a7784d9842f7386b8b3b8a3d4171a06e88ac59611100000000001976a91449f8c749a9960dc29b5cbe7d2397cea7d26611bb88ac00000000'
describe('Mempool', function() {
it('should encode mempool transaction key', function() {
encoding.encodeMempoolTransactionKey(hash).should.deep.equal(Buffer.concat([ servicePrefix, new Buffer(hash, 'hex') ]));
});
it('should decode mempool transaction key', function() {
encoding.decodeMempoolTransactionKey(Buffer.concat([ servicePrefix, new Buffer(hash, 'hex') ])).should.deep.equal(hash);
});
it('should encode mempool transaction value', function() {
var mytx = tx.fromRaw(txString, 'hex');
mytx.__inputValues = [1012955, 447698, 446664, 391348];
encoding.encodeMempoolTransactionValue(mytx).should.deep.equal(new Buffer(txString, 'hex'));
});
it('should decode mempool transaction value', function() {
var mytx = encoding.decodeMempoolTransactionValue(new Buffer(txString, 'hex'));
mytx.should.deep.equal(tx.fromRaw(txString, 'hex'));
});
});
});

View File

@ -0,0 +1,95 @@
'use strict';
var expect = require('chai').expect;
var MempoolService = require('../../../lib/services/mempool');
var sinon = require('sinon');
var Encoding = require('../../../lib/services/mempool/encoding');
var bcoin = require('bcoin');
var Tx = bcoin.tx;
var Block = bcoin.block;
describe('Mempool Service', function() {
var mempoolService;
var sandbox;
var block = Block.fromRaw('010000006a39821735ec18a366d95b391a7ff10dee181a198f1789b0550e0d00000000002b0c80fa52b669022c344c3e09e6bb9698ab90707bb4bb412af3fbf31cfd2163a601514c5a0c011c572aef0f0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08045a0c011c022003ffffffff0100f2052a01000000434104c5b694d72e601091fd733c6b18b94795c13e2db6b1474747e7be914b407854cad37cee3058f85373b9f9dbb0014e541c45851d5f85e83a1fd7c45e54423718f3ac00000000', 'hex');
var tx = Tx.fromRaw( '0100000004de9b4bb17f627096a9ee0b4528e4eae17df5b5c69edc29704c2e84a7371db29f010000006b483045022100f5b1a0d33b7be291c3953c25f8ae39d98601aa7099a8674daf638a08b86c7173022006ce372da5ad088a1cc6e5c49c2760a1b6f085eb1b51b502211b6bc9508661f9012102ec5e3731e54475dd2902326f43602a03ae3d62753324139163f81f20e787514cffffffff7a1d4e5fc2b8177ec738cd723a16cf2bf493791e55573445fc0df630fe5e2d64010000006b483045022100cf97f6cb8f126703e9768545dfb20ffb10ba78ae3d101aa46775f5a239b075fc02203150c4a89a11eaf5e404f4f96b62efa4455e9525765a025525c7105a7e47b6db012102c01e11b1d331f999bbdb83e8831de503cd52a01e3834a95ccafd615c67703d77ffffffff9e52447116415ca0d0567418a1a4ef8f27be3ff5a96bf87c922f3723d7db5d7c000000006b483045022100f6c117e536701be41a6b0b544d7c3b1091301e4e64a6265b6eb167b15d16959d022076916de4b115e700964194ce36a24cb9105f86482f4abbc63110c3f537cd5770012102ddf84cc7bee2d6a82ac09628a8ad4a26cd449fc528b81e7e6cc615707b8169dfffffffff5815d9750eb3572e30d6fd9df7afb4dbd76e042f3aa4988ac763b3fdf8397f80010000006a473044022028f4402b736066d93d2a32b28ccd3b7a21d84bb58fcd07fe392a611db94cdec5022018902ee0bf2c3c840c1b81ead4e6c87c88c48b2005bf5eea796464e561a620a8012102b6cdd1a6cd129ef796faeedb0b840fcd0ca00c57e16e38e46ee7028d59812ae7ffffffff0220a10700000000001976a914c342bcd1a7784d9842f7386b8b3b8a3d4171a06e88ac59611100000000001976a91449f8c749a9960dc29b5cbe7d2397cea7d26611bb88ac00000000', 'hex');
beforeEach(function() {
sandbox = sinon.sandbox.create();
mempoolService = new MempoolService({
node: {
getNetworkName: function() { return 'regtest'; },
services: []
}
});
mempoolService._encoding = new Encoding(new Buffer('0000', 'hex'));
});
afterEach(function() {
sandbox.restore();
});
describe('#start', function() {
it('should get the db prefix', function(done) {
var getPrefix = sandbox.stub().callsArgWith(1, null, new Buffer('0001', 'hex'));
var startSubs = sandbox.stub(mempoolService, '_startSubscriptions');
mempoolService._db = { getPrefix: getPrefix };
mempoolService.start(function() {
expect(getPrefix.calledOnce).to.be.true;
expect(startSubs.calledOnce).to.be.true;
done();
});
});
});
describe('#stop', function() {
it('should stop the service', function(done) {
mempoolService.stop(function() {
done();
});
});
});
describe('#getMempoolTransaction', function() {
it('should get a mempool transaction', function(done) {
var get = sandbox.stub().callsArgWith(1, null, tx.toJSON());
var key = sandbox.stub();
var val = sandbox.stub().returns(tx.toJSON());
mempoolService._encoding = { encodeMempoolTransactionKey: key, decodeMempoolTransactionValue: val };
mempoolService._db = { get: get };
mempoolService.getMempoolTransaction(tx.hash, function(err, mytx) {
if(err) {
return done(err);
}
expect(mytx).to.deep.equal(tx.toJSON());
done();
});
});
});
describe('#_onTransaction', function() {
it('should add the transaction to the database', function() {
var put = sandbox.stub();
mempoolService._db = { put: put };
mempoolService._onTransaction(tx);
expect(put.calledOnce).to.be.true;
});
});
describe('#_onBlock', function() {
it('should remove block\'s txs from database', function(done) {
mempoolService.onBlock(block, function(err, ops) {
expect(ops[0].type).to.deep.equal('del');
expect(ops[0].key.toString('hex')).to.deep.equal('00006321fd1cf3fbf32a41bbb47b7090ab9896bbe6093e4c342c0269b652fa800c2b');
done();
});
});
});
});

View File

@ -0,0 +1,33 @@
'use strict';
var expect = require('chai').expect;
var sinon = require('sinon');
var EventEmitter = require('events').EventEmitter;
var P2PService = require('../../../lib/services/p2p');
describe('P2P Service', function() {
var p2p;
var testEmitter;
before(function(done) {
p2p = new P2PService({
node: {
name: 'p2p',
on: sinon.stub()
}
});
sinon.stub(p2p, '_initPool');
p2p._pool = new EventEmitter();
done();
});
it('should get the mempool from the network', function() {
var sendMessage = sinon.stub();
var peer = { sendMessage: sendMessage };
var getPeer = sinon.stub(p2p, '_getPeer').returns(peer);
p2p.getMempool();
expect(getPeer.calledOnce).to.be.true;
expect(sendMessage.calledOnce).to.be.true;
});
});

View File

@ -0,0 +1,49 @@
'use strict';
var should = require('chai').should();
var Encoding = require('../../../lib/services/timestamp/encoding');
describe('Timestamp service encoding', function() {
var servicePrefix = new Buffer('0000', 'hex');
var blockPrefix = new Buffer('00', 'hex');
var timestampPrefix = new Buffer('01', 'hex');
var encoding = new Encoding(servicePrefix);
var blockhash = '00000000000000000115b92b1ff4377441049bff75c6c48b626eb99e8b744297';
var timestamp = 5;
var timestampBuf = new Buffer(4);
timestampBuf.writeUInt32BE(timestamp);
it('should encode block timestamp key' , function() {
encoding.encodeBlockTimestampKey(blockhash).should.deep.equal(Buffer.concat([servicePrefix, blockPrefix, new Buffer(blockhash, 'hex')]));
});
it('should decode block timestamp key', function() {
var blockTimestampKey = encoding.decodeBlockTimestampKey(Buffer.concat([servicePrefix, blockPrefix, new Buffer(blockhash, 'hex')]));
blockTimestampKey.should.equal(blockhash);
});
it('should encode block timestamp value', function() {
encoding.encodeBlockTimestampValue(timestamp).should.deep.equal(timestampBuf);
});
it('should decode block timestamp value', function() {
encoding.decodeBlockTimestampValue(timestampBuf).should.equal(timestamp);
});
it('should encode timestamp block key', function() {
encoding.encodeTimestampBlockKey(timestamp).should.deep.equal(Buffer.concat([servicePrefix, timestampPrefix, timestampBuf]));
});
it('should decode timestamp block key', function() {
encoding.decodeTimestampBlockKey(Buffer.concat([servicePrefix, timestampPrefix, timestampBuf])).should.equal(timestamp);
});
it('should encode timestamp block value', function() {
encoding.encodeTimestampBlockValue(blockhash).should.deep.equal(new Buffer(blockhash, 'hex'));
});
it('should decode timestamp block value', function() {
encoding.decodeTimestampBlockValue(new Buffer(blockhash, 'hex')).should.equal(blockhash);
});
});

View File

@ -0,0 +1,42 @@
'use strict';
var should = require('chai').should();
var Tx = require('bcoin').tx;
var Encoding = require('../../../lib/services/transaction/encoding');
describe('Transaction service encoding', function() {
var servicePrefix = new Buffer('0000', 'hex');
var encoding = new Encoding(servicePrefix);
var txid = '91b58f19b6eecba94ed0f6e463e8e334ec0bcda7880e2985c82a8f32e4d03add';
var txHex = '0100000001cc3ffe0638792c8b39328bb490caaefe2cf418f2ce0144956e0c22515f29724d010000006a473044022030ce9fa68d1a32abf0cd4adecf90fb998375b64fe887c6987278452b068ae74c022036a7d00d1c8af19e298e04f14294c807ebda51a20389ad751b4ff3c032cf8990012103acfcb348abb526526a9f63214639d79183871311c05b2eebc727adfdd016514fffffffff02f6ae7d04000000001976a9144455183e407ee4d3423858c8a3275918aedcd18e88aca99b9b08010000001976a9140beceae2c29bfde08d2b6d80b33067451c5887be88ac00000000';
var tx = Tx.fromRaw(txHex, 'hex');
var txEncoded = Buffer.concat([new Buffer('00000002', 'hex'), new Buffer('00000001', 'hex'), new Buffer('0002', 'hex'), new Buffer('40000000000000004008000000000000', 'hex'), tx.toRaw()]);
it('should encode transaction key' , function() {
var txBuf = new Buffer(txid, 'hex');
encoding.encodeTransactionKey(txid).should.deep.equal(Buffer.concat([servicePrefix, txBuf]));
});
it('should decode transaction key', function() {
encoding.decodeTransactionKey(Buffer.concat([servicePrefix, new Buffer(txid, 'hex')]))
.should.equal(txid);
});
it('should encode transaction value', function() {
tx.__height = 2;
tx.__timestamp = 1;
tx.__inputValues = [ 2, 3 ];
encoding.encodeTransactionValue(tx).should.deep.equal(txEncoded);
});
it('should decode transaction value', function() {
var tx = encoding.decodeTransactionValue(txEncoded);
tx.__height.should.equal(2);
tx.__timestamp.should.equal(1);
tx.__inputValues.should.deep.equal([2,3]);
tx.toRaw().toString('hex').should.equal(txHex);
});
});

View File

@ -0,0 +1,126 @@
'use strict';
var should = require('chai').should();
var bcoin = require('bcoin');
var Tx = bcoin.tx;
var Block = bcoin.block;
var sinon = require('sinon');
var TxService = require('../../../lib/services/transaction');
var Encoding = require('../../../lib/services/transaction/encoding');
describe('Transaction Service', function() {
var block = Block.fromRaw('010000006a39821735ec18a366d95b391a7ff10dee181a198f1789b0550e0d00000000002b0c80fa52b669022c344c3e09e6bb9698ab90707bb4bb412af3fbf31cfd2163a601514c5a0c011c572aef0f0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08045a0c011c022003ffffffff0100f2052a01000000434104c5b694d72e601091fd733c6b18b94795c13e2db6b1474747e7be914b407854cad37cee3058f85373b9f9dbb0014e541c45851d5f85e83a1fd7c45e54423718f3ac00000000', 'hex');
var tx = Tx.fromRaw( '0100000004de9b4bb17f627096a9ee0b4528e4eae17df5b5c69edc29704c2e84a7371db29f010000006b483045022100f5b1a0d33b7be291c3953c25f8ae39d98601aa7099a8674daf638a08b86c7173022006ce372da5ad088a1cc6e5c49c2760a1b6f085eb1b51b502211b6bc9508661f9012102ec5e3731e54475dd2902326f43602a03ae3d62753324139163f81f20e787514cffffffff7a1d4e5fc2b8177ec738cd723a16cf2bf493791e55573445fc0df630fe5e2d64010000006b483045022100cf97f6cb8f126703e9768545dfb20ffb10ba78ae3d101aa46775f5a239b075fc02203150c4a89a11eaf5e404f4f96b62efa4455e9525765a025525c7105a7e47b6db012102c01e11b1d331f999bbdb83e8831de503cd52a01e3834a95ccafd615c67703d77ffffffff9e52447116415ca0d0567418a1a4ef8f27be3ff5a96bf87c922f3723d7db5d7c000000006b483045022100f6c117e536701be41a6b0b544d7c3b1091301e4e64a6265b6eb167b15d16959d022076916de4b115e700964194ce36a24cb9105f86482f4abbc63110c3f537cd5770012102ddf84cc7bee2d6a82ac09628a8ad4a26cd449fc528b81e7e6cc615707b8169dfffffffff5815d9750eb3572e30d6fd9df7afb4dbd76e042f3aa4988ac763b3fdf8397f80010000006a473044022028f4402b736066d93d2a32b28ccd3b7a21d84bb58fcd07fe392a611db94cdec5022018902ee0bf2c3c840c1b81ead4e6c87c88c48b2005bf5eea796464e561a620a8012102b6cdd1a6cd129ef796faeedb0b840fcd0ca00c57e16e38e46ee7028d59812ae7ffffffff0220a10700000000001976a914c342bcd1a7784d9842f7386b8b3b8a3d4171a06e88ac59611100000000001976a91449f8c749a9960dc29b5cbe7d2397cea7d26611bb88ac00000000', 'hex');
var txService;
var sandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
txService = new TxService({
node: {
getNetworkName: function() { return 'regtest'; },
services: []
}
});
txService._encoding = new Encoding(new Buffer('0000', 'hex'));
});
afterEach(function() {
sandbox.restore();
});
describe('#start', function() {
it('should get the prefix and the service tip', function(done) {
var getPrefix = sandbox.stub().callsArgWith(1, null, new Buffer('ffee', 'hex'));
txService._db = { getPrefix: getPrefix };
txService.start(function() {
getPrefix.calledOnce.should.be.true;
txService._encoding.should.be.instanceOf(Encoding);
done();
});
});
});
describe('#stop', function() {
it('should stop the service', function(done) {
txService.stop(done);
});
});
describe('#sendTransaction', function() {
it('should send a raw transaction', function(done) {
var sendTransaction = sandbox.stub().callsArg(0);
txService._p2p = { sendTransaction: sendTransaction };
txService.sendTransaction(function(err) {
if (err) {
return done(err);
}
done();
});
});
});
describe('#_getBlockTimestamp', function() {
it('should get the block\'s timestamp', function() {
var getTimestamp = sandbox.stub().returns(1);
txService._timestamp = { getTimestampSync: getTimestamp };
var timestamp = txService._getBlockTimestamp('aa');
timestamp.should.equal(1);
});
});
describe('#onBlock', function() {
it('should process new blocks that come in from the block service', function(done) {
var _processTransaction = sandbox.stub(txService, '_processTransaction');
txService.onBlock(block, function(err, ops) {
if (err) {
return done(err);
}
_processTransaction.calledOnce.should.be.true;
done();
});
});
});
describe('#_onReorg', function() {
it('should perform a reorg', function(done) {
var oldList = [];
var ops = txService.onReorg([ null, oldList ], function(err, ops) {
if (err) {
return done(err);
}
ops.should.deep.equal([]);
done();
});
});
});
describe('#getInputValues', function() {
it('should add missing input values on a tx', function(done) {
var put = sandbox.stub().callsArgWith(2, null);
txService._db = { put: put };
sandbox.stub(txService, '_getTransaction').callsArgWith(2, null, tx.txid(), tx);
tx.__inputValues = [];
txService.getInputValues(tx, {}, function(err, tx) {
if (err) {
return done(err);
}
tx.__inputValues.should.deep.equal([1139033, 1139033, 500000, 1139033]);
done();
});
});
});
});

View File

@ -5,7 +5,7 @@ var sinon = require('sinon');
var EventEmitter = require('events').EventEmitter;
var proxyquire = require('proxyquire');
var index = require('../../lib');
var index = require('../../../lib');
var log = index.log;
var httpStub = {
@ -30,7 +30,7 @@ fakeSocket.on('test/event1', function(data) {
fakeSocketListener.emit('connection', fakeSocket);
fakeSocket.emit('subscribe', 'test/event1');
var WebService = proxyquire('../../lib/services/web', {http: httpStub, https: httpsStub, fs: fsStub});
var WebService = proxyquire('../../../lib/services/web', {http: httpStub, https: httpsStub, fs: fsStub});
describe('WebService', function() {
var defaultNode = new EventEmitter();
@ -82,7 +82,7 @@ describe('WebService', function() {
it('should pass json request limit to json body parser', function(done) {
var node = new EventEmitter();
var jsonStub = sinon.stub();
var TestWebService = proxyquire('../../lib/services/web', {
var TestWebService = proxyquire('../../../lib/services/web', {
http: {
createServer: sinon.stub()
},

View File

@ -2,150 +2,124 @@
var should = require('chai').should();
var utils = require('../lib/utils');
var sinon = require('sinon');
describe('Utils', function() {
describe('#isHash', function() {
describe('#isHeight', function() {
it('false for short string', function() {
var a = utils.isHash('ashortstring');
a.should.equal(false);
it('should detect a height', function() {
utils.isHeight(12).should.be.true;
});
it('false for long string', function() {
var a = utils.isHash('00000000000000000000000000000000000000000000000000000000000000000');
a.should.equal(false);
});
it('false for correct length invalid char', function() {
var a = utils.isHash('z000000000000000000000000000000000000000000000000000000000000000');
a.should.equal(false);
});
it('false for invalid type (buffer)', function() {
var a = utils.isHash(new Buffer('abcdef', 'hex'));
a.should.equal(false);
});
it('false for invalid type (number)', function() {
var a = utils.isHash(123456);
a.should.equal(false);
});
it('true for hash', function() {
var a = utils.isHash('fc63629e2106c3440d7e56751adc8cfa5266a5920c1b54b81565af25aec1998b');
a.should.equal(true);
it('should detect a non-height', function() {
utils.isHeight('aaaaaa').should.be.false;
});
});
describe('#isSafeNatural', function() {
describe('#isAbsolutePath', function() {
it('false for float', function() {
var a = utils.isSafeNatural(0.1);
a.should.equal(false);
it('should detect absolute path', function() {
utils.isAbsolutePath('/').should.be.true;
});
it('false for string float', function() {
var a = utils.isSafeNatural('0.1');
a.should.equal(false);
});
it('false for string integer', function() {
var a = utils.isSafeNatural('1');
a.should.equal(false);
});
it('false for negative integer', function() {
var a = utils.isSafeNatural(-1);
a.should.equal(false);
});
it('false for negative integer string', function() {
var a = utils.isSafeNatural('-1');
a.should.equal(false);
});
it('false for infinity', function() {
var a = utils.isSafeNatural(Infinity);
a.should.equal(false);
});
it('false for NaN', function() {
var a = utils.isSafeNatural(NaN);
a.should.equal(false);
});
it('false for unsafe number', function() {
var a = utils.isSafeNatural(Math.pow(2, 53));
a.should.equal(false);
});
it('true for positive integer', function() {
var a = utils.isSafeNatural(1000);
a.should.equal(true);
});
});
describe('#startAtZero', function() {
it('will set key to zero if not set', function() {
var obj = {};
utils.startAtZero(obj, 'key');
obj.key.should.equal(0);
});
it('not if already set', function() {
var obj = {
key: 10
};
utils.startAtZero(obj, 'key');
obj.key.should.equal(10);
});
it('not if set to false', function() {
var obj = {
key: false
};
utils.startAtZero(obj, 'key');
obj.key.should.equal(false);
});
it('not if set to undefined', function() {
var obj = {
key: undefined
};
utils.startAtZero(obj, 'key');
should.equal(obj.key, undefined);
});
it('not if set to null', function() {
var obj = {
key: null
};
utils.startAtZero(obj, 'key');
should.equal(obj.key, null);
it('should not detect absolute path', function() {
utils.isAbsolutePath('.').should.be.false;
});
});
describe('#parseParamsWithJSON', function() {
it('will parse object', function() {
var paramsArg = ['3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou', '{"start": 100, "end": 1}'];
var params = utils.parseParamsWithJSON(paramsArg);
params.should.deep.equal(['3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou', {start: 100, end: 1}]);
});
it('will parse array', function() {
var paramsArg = ['3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou', '[0, 1]'];
var params = utils.parseParamsWithJSON(paramsArg);
params.should.deep.equal(['3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou', [0, 1]]);
});
it('will parse numbers', function() {
var paramsArg = ['3', 0, 'b', '0', 0x12, '0.0001'];
var params = utils.parseParamsWithJSON(paramsArg);
params.should.deep.equal([3, 0, 'b', 0, 0x12, 0.0001]);
it('should parse json params', function() {
utils.parseParamsWithJSON([ '{"test":"1"}', '{}', '[]' ])
.should.deep.equal([{test:'1'}, {}, []]);
});
});
describe('#getTerminalKey', function() {
it('should get the terminal key for a buffer', function() {
utils.getTerminalKey(new Buffer('ffff', 'hex'))
.should.deep.equal(new Buffer('010000', 'hex'));
});
it('should get the terminal key for a large buffer', function() {
utils.getTerminalKey(Buffer.concat([ new Buffer(new Array(64).join('f'), 'hex'), new Buffer('fe', 'hex') ]))
.should.deep.equal(new Buffer('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 'hex'));
});
});
describe('#diffTime', function() {
it('should get the difference in time in seconds', function(done) {
var time = process.hrtime();
setTimeout(function() {
var res = utils.diffTime(time);
res.should.be.greaterThan(0.1);
res.should.be.lessThan(0.5);
done();
}, 100);
});
});
describe('#sendError', function() {
it('should send a web-style error', function() {
var err = { statusCode: 500, message: 'hi there', stack: 'some stack' };
var status = sinon.stub().returnsThis();
var send = sinon.stub();
var res = { status: status, send: send };
utils.sendError(err, res);
send.should.be.calledOnce;
status.should.be.calledOnce;
status.args[0][0].should.equal(500);
send.args[0][0].should.equal('hi there');
});
it('should send a 503 in the case where there is no given status code', function() {
var err = { message: 'hi there', stack: 'some stack' };
var status = sinon.stub().returnsThis();
var send = sinon.stub();
var res = { status: status, send: send };
utils.sendError(err, res);
send.should.be.calledOnce;
status.should.be.calledOnce;
status.args[0][0].should.equal(503);
send.args[0][0].should.equal('hi there');
});
});
describe('#encodeTip', function() {
it('should encode tip', function() {
var res = utils.encodeTip({ height: 0xdeadbeef, hash: new Array(65).join('0') }, 'test');
res.should.deep.equal({
key: new Buffer('ffff7469702d74657374', 'hex'),
value: new Buffer('deadbeef00000000000000000000000000000000000000000000000000000000000000000', 'hex')
});
});
});
describe('#SimpleMap', function() {
var map = new utils.SimpleMap();
it('should build a simple map', function() {
map.should.be.instanceOf(Object);
});
it('should set a key and value', function() {
map.set('key', 'value');
map.getIndex(0).should.equal('value');
});
it('should get a value for key', function() {
map.get('key').should.equal('value');
});
it('should get a get a value at a specific index', function() {
map.getIndex(0).should.equal('value');
});
it('should get the last index', function() {
map.set('last key', 'last value');
map.getLastIndex().should.equal('last value');
});
});
});