/*!
* node.js - node object for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const EventEmitter = require('events');
const fs = require('bfile');
const Logger = require('blgr');
const Config = require('bcfg');
const secp256k1 = require('bcrypto/lib/secp256k1');
const Network = require('../protocol/network');
const WorkerPool = require('../workers/workerpool');
const {ownership} = require('../covenants/ownership');
/**
* Node
* Base class from which every other
* Node-like object inherits.
* @alias module:node.Node
* @extends EventEmitter
* @abstract
*/
class Node extends EventEmitter {
/**
* Create a node.
* @constructor
* @param {Object} options
*/
constructor(module, config, file, options) {
super();
this.config = new Config(module, {
suffix: 'network',
fallback: 'main',
alias: { 'n': 'network' }
});
this.config.inject(options);
this.config.load(options);
if (options.config)
this.config.open(config);
this.network = Network.get(this.config.getSuffix());
this.memory = this.config.bool('memory', true);
this.identityKey = this.loadKey();
this.startTime = -1;
this.bound = [];
this.plugins = Object.create(null);
this.stack = [];
this.logger = null;
this.workers = null;
this.spv = false;
this.blocks = null;
this.chain = null;
this.fees = null;
this.mempool = null;
this.pool = null;
this.miner = null;
this.http = null;
this._init(file);
}
/**
* Initialize node.
* @private
* @param {Object} options
*/
_init(file) {
const config = this.config;
let logger = new Logger();
if (config.has('logger'))
logger = config.obj('logger');
logger.set({
filename: !this.memory && config.bool('log-file')
? config.location(file)
: null,
level: config.str('log-level'),
console: config.bool('log-console'),
shrink: config.bool('log-shrink'),
maxFileSize: config.mb('log-max-file-size'),
maxFiles: config.uint('log-max-files')
});
this.logger = logger.context('node');
if (config.bool('ignore-forged')) {
if (this.network.type !== 'regtest')
throw new Error('Forged claims are regtest-only.');
ownership.ignore = true;
}
this.workers = new WorkerPool({
enabled: config.bool('workers'),
size: config.uint('workers-size'),
timeout: config.uint('workers-timeout'),
file: config.str('worker-file')
});
this.on('error', () => {});
this.workers.on('spawn', (child) => {
this.logger.info('Spawning worker process: %d.', child.id);
});
this.workers.on('exit', (code, child) => {
this.logger.warning('Worker %d exited: %s.', child.id, code);
});
this.workers.on('log', (text, child) => {
this.logger.debug('Worker %d says:', child.id);
this.logger.debug(text);
});
this.workers.on('error', (err, child) => {
if (child) {
this.logger.error('Worker %d error: %s', child.id, err.message);
return;
}
this.emit('error', err);
});
}
/**
* Ensure prefix directory.
* @returns {Promise}
*/
async ensure() {
if (fs.unsupported)
return undefined;
if (this.memory)
return undefined;
if (this.blocks)
await this.blocks.ensure();
return fs.mkdirp(this.config.prefix);
}
/**
* Create a file path using `prefix`.
* @param {String} file
* @returns {String}
*/
location(name) {
return this.config.location(name);
}
/**
* Generate identity key.
* @private
* @returns {Buffer}
*/
genKey() {
if (this.network.identityKey)
return this.network.identityKey;
return secp256k1.privateKeyGenerate();
}
/**
* Load identity key.
* @private
* @returns {Buffer}
*/
loadKey() {
if (this.memory || fs.unsupported)
return this.genKey();
const filename = this.location('key');
let key = this.config.buf('identity-key');
let fresh = false;
if (!key) {
try {
const data = fs.readFileSync(filename, 'utf8');
key = Buffer.from(data.trim(), 'hex');
} catch (e) {
if (e.code !== 'ENOENT')
throw e;
key = this.genKey();
fresh = true;
}
} else {
fresh = true;
}
if (key.length !== 32 || !secp256k1.privateKeyVerify(key))
throw new Error('Invalid identity key.');
if (fresh) {
// XXX Shouldn't be doing this.
fs.mkdirpSync(this.config.prefix);
fs.writeFileSync(filename, key.toString('hex') + '\n');
}
return key;
}
/**
* Open node. Bind all events.
* @private
*/
async handlePreopen() {
await this.logger.open();
await this.workers.open();
this._bind(this.network.time, 'offset', (offset) => {
this.logger.info(
'Time offset: %d (%d minutes).',
offset, offset / 60 | 0);
});
this._bind(this.network.time, 'sample', (sample, total) => {
this.logger.debug(
'Added time data: samples=%d, offset=%d (%d minutes).',
total, sample, sample / 60 | 0);
});
this._bind(this.network.time, 'mismatch', () => {
this.logger.warning('Adjusted time mismatch!');
this.logger.warning('Please make sure your system clock is correct!');
});
}
/**
* Open node.
* @private
*/
async handleOpen() {
this.startTime = Date.now();
if (!this.workers.enabled) {
this.logger.warning('Warning: worker pool is disabled.');
this.logger.warning('Verification will be slow.');
}
}
/**
* Open node. Bind all events.
* @private
*/
async handlePreclose() {
;
}
/**
* Close node. Unbind all events.
* @private
*/
async handleClose() {
for (const [obj, event, listener] of this.bound)
obj.removeListener(event, listener);
this.bound.length = 0;
this.startTime = -1;
await this.workers.close();
await this.logger.close();
}
/**
* Bind to an event on `obj`, save listener for removal.
* @private
* @param {EventEmitter} obj
* @param {String} event
* @param {Function} listener
*/
_bind(obj, event, listener) {
this.bound.push([obj, event, listener]);
obj.on(event, listener);
}
/**
* Emit and log an error.
* @private
* @param {Error} err
*/
error(err) {
this.logger.error(err);
this.emit('error', err);
}
/**
* Emit and log an abort error.
* @private
* @param {Error} err
*/
abort(err) {
this.logger.error(err);
this.emit('abort', err);
}
/**
* Get node uptime in seconds.
* @returns {Number}
*/
uptime() {
if (this.startTime === -1)
return 0;
return Math.floor((Date.now() - this.startTime) / 1000);
}
/**
* Attach a plugin.
* @param {Object} plugin
* @returns {Object} Plugin instance.
*/
use(plugin) {
assert(plugin, 'Plugin must be an object.');
assert(typeof plugin.init === 'function', '`init` must be a function.');
assert(!this.loaded, 'Cannot add plugin after node is loaded.');
const instance = plugin.init(this);
assert(!instance.open || typeof instance.open === 'function',
'`open` must be a function.');
assert(!instance.close || typeof instance.close === 'function',
'`close` must be a function.');
if (plugin.id) {
assert(typeof plugin.id === 'string', '`id` must be a string.');
// Reserved names
switch (plugin.id) {
case 'chain':
case 'fees':
case 'mempool':
case 'miner':
case 'pool':
case 'rpc':
case 'http':
case 'ns':
case 'rs':
assert(false, `${plugin.id} is already added.`);
break;
}
assert(!this.plugins[plugin.id], `${plugin.id} is already added.`);
this.plugins[plugin.id] = instance;
}
this.stack.push(instance);
if (typeof instance.on === 'function') {
instance.on('error', err => this.error(err));
instance.on('abort', msg => this.abort(msg));
}
return instance;
}
/**
* Test whether a plugin is available.
* @param {String} name
* @returns {Boolean}
*/
has(name) {
return this.plugins[name] != null;
}
/**
* Get a plugin.
* @param {String} name
* @returns {Object|null}
*/
get(name) {
assert(typeof name === 'string', 'Plugin name must be a string.');
// Reserved names.
switch (name) {
case 'chain':
assert(this.chain, 'chain is not loaded.');
return this.chain;
case 'fees':
assert(this.fees, 'fees is not loaded.');
return this.fees;
case 'mempool':
assert(this.mempool, 'mempool is not loaded.');
return this.mempool;
case 'miner':
assert(this.miner, 'miner is not loaded.');
return this.miner;
case 'pool':
assert(this.pool, 'pool is not loaded.');
return this.pool;
case 'rpc':
assert(this.rpc, 'rpc is not loaded.');
return this.rpc;
case 'http':
assert(this.http, 'http is not loaded.');
return this.http;
case 'rs':
assert(this.rs, 'rs is not loaded.');
return this.rs;
case 'ns':
assert(this.ns, 'ns is not loaded.');
return this.ns;
}
return this.plugins[name] || null;
}
/**
* Require a plugin.
* @param {String} name
* @returns {Object}
* @throws {Error} on onloaded plugin
*/
require(name) {
const plugin = this.get(name);
assert(plugin, `${name} is not loaded.`);
return plugin;
}
/**
* Load plugins.
* @private
*/
loadPlugins() {
const plugins = this.config.array('plugins', []);
const loader = this.config.func('loader');
for (let plugin of plugins) {
if (typeof plugin === 'string') {
assert(loader, 'Must pass a loader function.');
plugin = loader(plugin);
}
this.use(plugin);
}
}
/**
* Open plugins.
* @private
*/
async openPlugins() {
for (const plugin of this.stack) {
if (plugin.open)
await plugin.open();
}
}
/**
* Close plugins.
* @private
*/
async closePlugins() {
for (const plugin of this.stack) {
if (plugin.close)
await plugin.close();
}
}
}
/*
* Expose
*/
module.exports = Node;