knomp
Version:
An extremely efficient, highly scalable, KawPoW algorithm mining pool
406 lines (404 loc) • 13.5 kB
JavaScript
var fs = require('fs');
var path = require('path');
var os = require('os');
var cluster = require('cluster');
var async = require('async');
var CliListener = require('./libs/cliListener.js');
var PoolWorker = require('./libs/poolWorker.js');
var PaymentProcessor = require('./libs/paymentProcessor.js');
var Website = require('./libs/website.js');
var ProfitSwitch = require('./libs/profitSwitch.js');
const loggerFactory = require('./libs/logger.js');
const logger = loggerFactory.getLogger('init.js', 'system');
var algos = require('stratum-pool/lib/algoProperties.js');
JSON.minify = JSON.minify || require("node-json-minify");
if (!fs.existsSync('config.json')) {
console.log('config.json file does not exist. Read the installation/setup instructions.');
return;
}
var portalConfig = JSON.parse(JSON.minify(fs.readFileSync("config.json", {encoding: 'utf8'})));
var poolConfigs;
try {
var posix = require('posix');
try {
posix.setrlimit('nofile', {soft: 100000, hard: 100000});
} catch (e) {
if (cluster.isMaster) {
logger.warn('POSIX Connection Limit (Safe to ignore) Must be ran as root to increase resource limits');
}
}
finally {
var uid = parseInt(process.env.SUDO_UID);
if (uid) {
process.setuid(uid);
logger.debug('POSIX Connection Limit Raised to 100K concurrent connections, now running as non-root user: %s', process.getuid());
}
}
}
catch (e) {
if (cluster.isMaster) {
logger.debug('POSIX Connection Limit (Safe to ignore) POSIX module not installed and resource (connection) limit was not raised');
}
}
if (cluster.isWorker) {
switch (process.env.workerType) {
case 'pool':
new PoolWorker();
break;
case 'paymentProcessor':
new PaymentProcessor();
break;
case 'website':
new Website();
break;
}
return;
}
var buildPoolConfigs = function () {
var configs = {};
var configDir = 'pool_configs/';
var poolConfigFiles = [];
fs.readdirSync(configDir).forEach(function (file) {
if (!fs.existsSync(configDir + file) || path.extname(configDir + file) !== '.json') return;
var poolOptions = JSON.parse(JSON.minify(fs.readFileSync(configDir + file, {encoding: 'utf8'})));
if (!poolOptions.enabled) return;
poolOptions.fileName = file;
poolConfigFiles.push(poolOptions);
});
for (var i = 0; i < poolConfigFiles.length; i++) {
var ports = Object.keys(poolConfigFiles[i].ports);
for (var f = 0; f < poolConfigFiles.length; f++) {
if (f === i) continue;
var portsF = Object.keys(poolConfigFiles[f].ports);
for (var g = 0; g < portsF.length; g++) {
if (ports.indexOf(portsF[g]) !== -1) {
logger.error(poolConfigFiles[f].fileName, 'Has same configured port of ' + portsF[g] + ' as ' + poolConfigFiles[i].fileName);
process.exit(1);
return;
}
}
if (poolConfigFiles[f].coin === poolConfigFiles[i].coin) {
logger.error(poolConfigFiles[f].fileName, 'Pool has same configured coin file coins/' + poolConfigFiles[f].coin + ' as ' + poolConfigFiles[i].fileName + ' pool');
process.exit(1);
return;
}
}
}
poolConfigFiles.forEach(function (poolOptions) {
poolOptions.coinFileName = poolOptions.coin;
var coinFilePath = 'coins/' + poolOptions.coinFileName;
if (!fs.existsSync(coinFilePath)) {
logger.error('[%s] could not find file %s ', poolOptions.coinFileName, coinFilePath);
return;
}
var coinProfile = JSON.parse(JSON.minify(fs.readFileSync(coinFilePath, {encoding: 'utf8'})));
poolOptions.coin = coinProfile;
poolOptions.coin.name = poolOptions.coin.name.toLowerCase();
if (poolOptions.coin.name in configs) {
logger.error('%s coins/' + poolOptions.coinFileName + ' has same configured coin name '
+ poolOptions.coin.name + ' as coins/'
+ configs[poolOptions.coin.name].coinFileName + ' used by pool config '
+ configs[poolOptions.coin.name].fileName, poolOptions.fileName);
process.exit(1);
return;
}
for (var option in portalConfig.defaultPoolConfigs) {
if (!(option in poolOptions)) {
var toCloneOption = portalConfig.defaultPoolConfigs[option];
var clonedOption = {};
if (toCloneOption.constructor === Object) {
Object.assign(clonedOption, toCloneOption);
} else {
clonedOption = toCloneOption;
}
poolOptions[option] = clonedOption;
}
}
configs[poolOptions.coin.name] = poolOptions;
if (!(coinProfile.algorithm in algos)) {
logger.error('[%s] Cannot run a pool for unsupported algorithm "' + coinProfile.algorithm + '"', coinProfile.name);
delete configs[poolOptions.coin.name];
}
});
return configs;
};
var buildAuxConfigs = function() {
var configs = {};
var configDir = 'aux_configs/';
var poolConfigFiles = [];
fs.readdirSync(configDir).forEach(function(file) {
if (!fs.existsSync(configDir + file) || path.extname(configDir + file) !== '.json') return;
var poolOptions = JSON.parse(JSON.minify(fs.readFileSync(configDir + file, {encoding: 'utf8'})));
if (!poolOptions.enabled) return;
poolOptions.fileName = file;
poolConfigFiles.push(poolOptions);
});
poolConfigFiles.forEach(function(poolOptions) {
poolOptions.coinFileName = poolOptions.coin;
var poolFilePath = 'coins/' + poolOptions.coinFileName;
if (!fs.existsSync(poolFilePath)) {
logger.warn('Master', poolOptions.coinFileName, 'could not find file: ' + poolFilePath);
return;
}
var poolProfile = JSON.parse(JSON.minify(fs.readFileSync(poolFilePath, {encoding: 'utf8'})));
poolOptions.coin = poolProfile;
poolOptions.coin.name = poolOptions.coin.name.toLowerCase();
configs[poolOptions.coin.name] = poolOptions;
for (var option in portalConfig.defaultPoolConfigs) {
if (!(option in poolOptions)) {
var toCloneOption = portalConfig.defaultPoolConfigs[option];
var clonedOption = {};
if (toCloneOption.constructor === Object)
extend(true, clonedOption, toCloneOption);
else
clonedOption = toCloneOption;
poolOptions[option] = clonedOption;
}
}
if (!(poolProfile.algorithm in algos)) {
logger.warn('Master', coinProfile.name, 'Cannot run a pool for unsupported algorithm "' + coinProfile.algorithm + '"');
delete configs[poolOptions.coin.name];
}
});
return configs;
};
var spawnPoolWorkers = function () {
Object.keys(poolConfigs).forEach(function (coin) {
var p = poolConfigs[coin];
if (!Array.isArray(p.daemons) || p.daemons.length < 1) {
logger.error('[%s] No daemons configured so a pool cannot be started for this coin.', coin);
delete poolConfigs[coin];
}
});
if (Object.keys(poolConfigs).length === 0) {
logger.warn('PoolSpawner: No pool configs exists or are enabled in pool_configs folder. No pools spawned.');
return;
}
var serializedConfigs = JSON.stringify(poolConfigs);
var numForks = (function () {
if (!portalConfig.clustering || !portalConfig.clustering.enabled) {
return 1;
}
if (portalConfig.clustering.forks === 'auto') {
return os.cpus().length;
}
if (!portalConfig.clustering.forks || isNaN(portalConfig.clustering.forks)) {
return 1;
}
return portalConfig.clustering.forks;
})();
var poolWorkers = {};
var createPoolWorker = function (forkId) {
var worker = cluster.fork({
workerType: 'pool',
forkId: forkId,
pools: serializedConfigs,
portalConfig: JSON.stringify(portalConfig)
});
worker.forkId = forkId;
worker.type = 'pool';
poolWorkers[forkId] = worker;
worker.on('exit', function (code, signal) {
logger.error('PoolSpawner: Fork %s died, spawning replacement worker...', forkId);
setTimeout(function () {
createPoolWorker(forkId);
}, 2000);
}).on('message', function (msg) {
switch (msg.type) {
case 'banIP':
Object.keys(cluster.workers).forEach(function (id) {
if (cluster.workers[id].type === 'pool') {
cluster.workers[id].send({type: 'banIP', ip: msg.ip});
}
});
break;
}
});
};
var i = 0;
var spawnInterval = setInterval(function () {
createPoolWorker(i);
i++;
if (i === numForks) {
clearInterval(spawnInterval);
logger.debug('Master', 'PoolSpawner', 'Spawned ' + Object.keys(poolConfigs).length + ' pool(s) on ' + numForks + ' thread(s)');
}
}, 250);
};
var startCliListener = function () {
let cliHost = '';
if (portalConfig.cliHost) {
cliHost = portalConfig.cliHost;
} else {
cliHost = '127.0.0.1';
}
var cliPort = portalConfig.cliPort;
var listener = new CliListener(cliHost, cliPort);
listener.on('log', function (text) {
logger.debug('Master', 'CLI', text);
}).on('command', function (command, params, options, reply) {
switch (command) {
case 'blocknotify':
Object.keys(cluster.workers).forEach(function (id) {
cluster.workers[id].send({type: 'blocknotify', coin: params[0], hash: params[1]});
});
reply('Pool workers notified');
break;
case 'coinswitch':
processCoinSwitchCommand(params, options, reply);
break;
case 'reloadpool':
Object.keys(cluster.workers).forEach(function (id) {
cluster.workers[id].send({type: 'reloadpool', coin: params[0]});
});
reply('reloaded pool ' + params[0]);
break;
default:
reply('unrecognized command "' + command + '"');
break;
}
}).start();
};
var processCoinSwitchCommand = function (params, options, reply) {
var logSystem = 'CLI';
var logComponent = 'coinswitch';
var replyError = function (msg) {
reply(msg);
logger.error(logSystem, logComponent, msg);
};
if (!params[0]) {
replyError('Coin name required');
return;
}
if (!params[1] && !options.algorithm) {
replyError('If switch key is not provided then algorithm options must be specified');
return;
}
else if (params[1] && !portalConfig.switching[params[1]]) {
replyError('Switch key not recognized: ' + params[1]);
return;
}
else if (options.algorithm && !Object.keys(portalConfig.switching).filter(function (s) {
return portalConfig.switching[s].algorithm === options.algorithm;
})[0]) {
replyError('No switching options contain the algorithm ' + options.algorithm);
return;
}
var messageCoin = params[0].toLowerCase();
var newCoin = Object.keys(poolConfigs).filter(function (p) {
return p.toLowerCase() === messageCoin;
})[0];
if (!newCoin) {
replyError('Switch message to coin that is not recognized: ' + messageCoin);
return;
}
var switchNames = [];
if (params[1]) {
switchNames.push(params[1]);
} else {
for (var name in portalConfig.switching) {
if (portalConfig.switching[name].enabled && portalConfig.switching[name].algorithm === options.algorithm)
switchNames.push(name);
}
}
switchNames.forEach(function (name) {
if (poolConfigs[newCoin].coin.algorithm !== portalConfig.switching[name].algorithm) {
replyError('Cannot switch a '
+ portalConfig.switching[name].algorithm
+ ' algo pool to coin ' + newCoin + ' with ' + poolConfigs[newCoin].coin.algorithm + ' algo');
return;
}
Object.keys(cluster.workers).forEach(function (id) {
cluster.workers[id].send({type: 'coinswitch', coin: newCoin, switchName: name});
});
});
reply('Switch message sent to pool workers');
};
var startPaymentProcessor = function() {
var enabledForAny = false;
for (var pool in poolConfigs) {
var p = poolConfigs[pool];
var enabled = p.enabled && p.paymentProcessing && p.paymentProcessing.enabled;
if (enabled) {
enabledForAny = true;
break;
}
}
if (!enabledForAny)
return;
var worker = cluster.fork({
workerType: 'paymentProcessor',
pools: JSON.stringify(poolConfigs)
});
worker.on('exit', function(code, signal) {
logger.error('Master', 'Payment Processor', 'Payment processor died, spawning replacement...');
setTimeout(function() {
startPaymentProcessor(poolConfigs);
}, 2000);
});
};
var startAuxPaymentProcessor = function() {
var enabledForAny = false;
for (var aux in auxConfigs) {
var p = auxConfigs[aux];
var enabled = p.enabled && p.paymentProcessing && p.paymentProcessing.enabled;
if (enabled) {
enabledForAny = true;
break;
}
}
if (!enabledForAny)
return;
var worker = cluster.fork({
workerType: 'paymentProcessor',
pools: JSON.stringify(auxConfigs)
});
worker.on('exit', function(code, signal) {
logger.error('Master', 'Auxilliary Payment Processor', 'Auxilliary Payment processor died, spawning replacement...');
setTimeout(function() {
startPaymentProcessor(auxConfigs);
}, 2000);
});
};
var startWebsite = function () {
if (!portalConfig.website.enabled) return;
var worker = cluster.fork({
workerType: 'website',
pools: JSON.stringify(poolConfigs),
portalConfig: JSON.stringify(portalConfig)
});
worker.on('exit', function (code, signal) {
logger.error('Master', 'Website', 'Website process died, spawning replacement...');
setTimeout(function () {
startWebsite(portalConfig, poolConfigs);
}, 2000);
});
};
var startProfitSwitch = function () {
if (!portalConfig.profitSwitch || !portalConfig.profitSwitch.enabled) {
return;
}
var worker = cluster.fork({
workerType: 'profitSwitch',
pools: JSON.stringify(poolConfigs),
portalConfig: JSON.stringify(portalConfig)
});
worker.on('exit', function (code, signal) {
logger.error('Master', 'Profit', 'Profit switching process died, spawning replacement...');
setTimeout(function () {
startWebsite(portalConfig, poolConfigs);
}, 2000);
});
};
(function init() {
poolConfigs = buildPoolConfigs();
auxConfigs = buildAuxConfigs();
spawnPoolWorkers();
setTimeout(function() {
startPaymentProcessor();
startAuxPaymentProcessor();
startWebsite();
startProfitSwitch();
startCliListener();
}, 2000);
})();