Files
peya-nodejs-pool/lib/pool.js
2021-04-05 11:36:42 +08:00

1472 lines
49 KiB
JavaScript

/**
* Cryptonote Node.JS Pool
* https://github.com/dvandal/cryptonote-nodejs-pool
*
* Pool TCP daemon
**/
// Load required modules
let fs = require('fs');
let net = require('net');
let tls = require('tls');
let async = require('async');
let bignum = require('bignum');
let apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet, config.api);
let notifications = require('./notifications.js');
let utils = require('./utils.js');
config.hashingUtil = config.hashingUtil || false;
let cnHashing = require('cryptonight-hashing');
if (config.hashingUtil) {
cnHashing = require('turtlecoin-multi-hashing');
}
// Set nonce pattern - must exactly be 8 hex chars
let noncePattern = new RegExp("^[0-9A-Fa-f]{8}$");
// Set redis database cleanup interval
let cleanupInterval = config.redis.cleanupInterval && config.redis.cleanupInterval > 0 ? config.redis.cleanupInterval : 15;
let fallBackCoin = typeof config.poolServer.fallBackCoin !== 'undefined' && config.poolServer.fallBackCoin ? config.poolServer.fallBackCoin : 0
// Initialize log system
let logSystem = 'pool';
require('./exceptionWriter.js')(logSystem);
let threadId = '(Thread ' + process.env.forkId + ') ';
let log = function (severity, system, text, data) {
global.log(severity, system, threadId + text, data);
};
// Set cryptonight algorithm
let cnAlgorithm = config.cnAlgorithm || "cryptonight";
let cnVariant = config.cnVariant || 0;
let cnBlobType = config.cnBlobType || 0;
let cryptoNight;
if (!cnHashing || !cnHashing[cnAlgorithm]) {
log('error', logSystem, 'Invalid cryptonight algorithm: %s', [cnAlgorithm]);
} else {
cryptoNight = cnHashing[cnAlgorithm];
}
// Set instance id
let instanceId = utils.instanceId();
// Pool variables
let poolStarted = false;
let connectedMiners = {};
// Get merged mining tag reseved space size
let POOL_NONCE_SIZE = 16 + 1; // +1 for old XMR/new TRTL bugs
let EXTRA_NONCE_TEMPLATE = "02" + POOL_NONCE_SIZE.toString(16) + "00".repeat(POOL_NONCE_SIZE);
let POOL_NONCE_MM_SIZE = POOL_NONCE_SIZE + utils.cnUtil.get_merged_mining_nonce_size();
let EXTRA_NONCE_NO_CHILD_TEMPLATE = "02" + POOL_NONCE_MM_SIZE.toString(16) + "00".repeat(POOL_NONCE_MM_SIZE);
let mergedMining = config.poolServer.mergedMining && (Array.isArray(config.childPools) && config.childPools.length > 0)
function randomIntFromInterval (min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
// Pool settings
let shareTrustEnabled = config.poolServer.shareTrust && config.poolServer.shareTrust.enabled;
let shareTrustStepFloat = shareTrustEnabled ? config.poolServer.shareTrust.stepDown / 100 : 0;
let shareTrustMinFloat = shareTrustEnabled ? config.poolServer.shareTrust.min / 100 : 0;
let banningEnabled = config.poolServer.banning && config.poolServer.banning.enabled;
let bannedIPs = {};
let perIPStats = {};
let slushMiningEnabled = config.poolServer.slushMining && config.poolServer.slushMining.enabled;
if (!config.poolServer.paymentId) {
config.poolServer.paymentId = {};
}
if (!config.poolServer.paymentId.addressSeparator) {
config.poolServer.paymentId.addressSeparator = "+";
}
if (config.poolServer.paymentId.validation == null) {
config.poolServer.paymentId.validation = true;
}
if (config.poolServer.paymentId.ban == null) {
config.poolServer.paymentId.ban = false;
}
if (config.poolServer.paymentId.validations == null) {
config.poolServer.paymentId.validations = [];
config.poolServer.paymentId.validation = false;
}
config.isRandomX = config.isRandomX || false;
let previousOffset = config.previousOffset || 7;
let offset = config.offset || 2;
config.daemonType = config.daemonType || 'default';
if (config.daemonType === 'bytecoin') {
previousOffset = config.previousOffset || 3;
offset = config.offset || 3;
}
function Create2DArray (rows) {
let arr = [];
for (let i = 0; i < rows; i++) {
arr[i] = [];
}
return arr;
}
if (mergedMining) {
config.childPools = config.childPools.filter(pool => pool.enabled);
}
// Block templates
let validBlockTemplates = mergedMining ? Create2DArray(config.childPools.length) : Create2DArray(1);
let currentBlockTemplate = [];
// Child Block templates
let currentChildBlockTemplate = new Array(mergedMining ? config.childPools.length : 1);
// Difficulty buffer
let diff1 = bignum('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 16);
/**
* Convert buffer to byte array
**/
Buffer.prototype.toByteArray = function () {
return Array.prototype.slice.call(this, 0);
};
/**
* Periodical updaters
**/
// Variable difficulty retarget
setInterval(function () {
let now = Date.now() / 1000 | 0;
for (let minerId in connectedMiners) {
let miner = connectedMiners[minerId];
if (!miner.noRetarget) {
miner.retarget(now);
}
}
}, config.poolServer.varDiff.retargetTime * 1000);
// Every 30 seconds clear out timed-out miners and old bans
setInterval(function () {
let now = Date.now();
let timeout = config.poolServer.minerTimeout * 1000;
for (let minerId in connectedMiners) {
let miner = connectedMiners[minerId];
if (now - miner.lastBeat > timeout) {
log('warn', logSystem, 'Miner timed out and disconnected %s@%s', [miner.login, miner.ip]);
delete connectedMiners[minerId];
removeConnectedWorker(miner, 'timeout');
}
}
if (banningEnabled) {
for (ip in bannedIPs) {
let banTime = bannedIPs[ip];
if (now - banTime > config.poolServer.banning.time * 1000) {
delete bannedIPs[ip];
delete perIPStats[ip];
log('info', logSystem, 'Ban dropped for %s', [ip]);
}
}
}
}, 30000);
/**
* Handle multi-thread messages
**/
process.on('message', function (message) {
switch (message.type) {
case 'banIP':
bannedIPs[message.ip] = Date.now();
break;
case 'BlockTemplate':
let buffer = Buffer.from(message.block.blocktemplate_blob, 'hex');
let new_hash = Buffer.alloc(32);
buffer.copy(new_hash, 0, previousOffset, 39);
try {
if (!currentBlockTemplate[0] || new_hash.toString('hex') !== currentBlockTemplate[0].prev_hash.toString('hex') || (currentBlockTemplate[0].num_transactions == 0 && message.block.num_transactions > 0)) {
log('info', logSystem, 'New %s block to mine at height %d w/ difficulty of %d (%d transactions)', [config.coin, message.block.height, message.block.difficulty, (message.block.num_transactions || 0)]);
if (mergedMining) {
for (var childPoolIndex = 0; childPoolIndex < config.childPools.length; childPoolIndex++) {
processBlockTemplate(message.block, childPoolIndex);
}
} else {
processBlockTemplate(message.block, 0);
}
return;
} else {
return;
}
} catch (e) {
log('error', logSystem, `BlockTemplate ${e}`);
}
break;
case 'ChildBlockTemplate':
let poolIndex = parseInt(message.poolIndex);
try {
if (!currentChildBlockTemplate[poolIndex] || message.block.height > currentChildBlockTemplate[poolIndex].height || (currentChildBlockTemplate[poolIndex].num_transactions == 0 && message.block.num_transactions > 0)) {
log('info', logSystem, 'New %s child block to mine at height %d w/ difficulty of %d (%d transactions)', [config.childPools[poolIndex].coin, message.block.height, message.block.difficulty, (message.block.num_transactions || 0)]);
processChildBlockTemplate(poolIndex, message.block);
return;
} else {
return;
}
} catch (e) {
log('error', logSystem, `ChildBlockTemplate ${e}`);
}
break;
}
});
/**
* Block template
**/
function BlockTemplate (template, parent, indexOfChildPool) {
this.difficulty = template.difficulty;
this.height;
try {
this.height = template.height;
} catch (e) {
console.log(`BlockTemplate ${e}`);
}
this.num_transactions = template.num_transactions || 0;
this.blocktemplate_blob = template.blocktemplate_blob;
let blob = this.blocktemplate_blob;
this.buffer = Buffer.from(blob, 'hex');
let template_hex = EXTRA_NONCE_TEMPLATE;
if (parent && mergedMining) {
if (currentChildBlockTemplate[indexOfChildPool]) {
this.childBlockTemplate = currentChildBlockTemplate[indexOfChildPool];
this.buffer = utils.cnUtil.construct_mm_parent_block_blob(this.buffer, cnBlobType, this.childBlockTemplate.buffer);
blob = this.buffer.toString('hex');
} else {
template_hex = EXTRA_NONCE_NO_CHILD_TEMPLATE;
}
}
this.isRandomX = config.isRandomX
if (!mergedMining && this.isRandomX) {
this.seed_hash = template.seed_hash;
this.next_seed_hash = template.next_seed_hash;
}
var found_template_at = blob.indexOf(template_hex);
if (found_template_at !== -1) {
this.reserveOffset = offset + (found_template_at >> 1);
if (this.reserveOffset != template.reserved_offset && !mergedMining) {
log('error', logSystem, "INTERNAL ERROR: found reserve offset in unexpected place (found at %d, expected at %d)",
[this.reserveOffset, template.reserved_offset]);
}
} else {
log('error', logSystem, "INTERNAL ERROR: Couldn't find extra nonce data in blob!");
this.reserveOffset = template.reserveOffset || template.reserved_offset;
}
// Copy the Instance ID to the reserve offset + 4 bytes deeper. Copy in 4 bytes.
instanceId.copy(this.buffer, this.reserveOffset + 4, 0, 4);
// Reset nonce - this is the per-miner/pool nonce
this.extraNonce = 0;
// The clientNonceLocation is the location at which the client pools should set the nonces for each of their clients.
this.clientNonceLocation = this.reserveOffset + 12;
this.prev_hash = Buffer.alloc(32);
this.buffer.copy(this.prev_hash, 0, previousOffset, 39);
}
BlockTemplate.prototype = {
nextBlob: function (index) {
this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset);
if (mergedMining && this.childBlockTemplate) {
return utils.cnUtil.convert_blob(this.buffer, cnBlobType, this.childBlockTemplate.buffer).toString('hex');
}
return utils.cnUtil.convert_blob(this.buffer, cnBlobType).toString('hex');
},
nextBlobWithChildNonce: function () {
// Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset.
this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset);
// Don't convert the blob to something hashable. You bad.
return this.buffer.toString('hex');
}
};
/**
* Process block template
**/
function processBlockTemplate (template, indexOfChildPool) {
let block_template = new BlockTemplate(template, true, indexOfChildPool);
if (currentBlockTemplate[indexOfChildPool]) {
validBlockTemplates[indexOfChildPool].push(currentBlockTemplate[indexOfChildPool]);
}
while (validBlockTemplates[indexOfChildPool].length > (mergedMining ? 6 : 3)) {
validBlockTemplates[indexOfChildPool].shift();
}
currentBlockTemplate[indexOfChildPool] = block_template;
notifyConnectedMiners(indexOfChildPool);
}
/**
* Process child block template
**/
function processChildBlockTemplate (indexOfChildPool, template) {
let block_template = new BlockTemplate(template, false);
currentChildBlockTemplate[indexOfChildPool] = block_template;
// Update the parent block template to include this new child
if (currentBlockTemplate[indexOfChildPool]) {
processBlockTemplate(currentBlockTemplate[indexOfChildPool], indexOfChildPool);
}
}
function notifyConnectedMiners (indexOfChildPool) {
let now = Date.now() / 1000 | 0;
for (let minerId in connectedMiners) {
let miner = connectedMiners[minerId];
if (indexOfChildPool === miner.activeChildPool)
miner.pushMessage('job', miner.getJob());
}
}
/**
* Variable difficulty
**/
let VarDiff = (function () {
let variance = config.poolServer.varDiff.variancePercent / 100 * config.poolServer.varDiff.targetTime;
return {
variance: variance,
bufferSize: config.poolServer.varDiff.retargetTime / config.poolServer.varDiff.targetTime * 4,
tMin: config.poolServer.varDiff.targetTime - variance,
tMax: config.poolServer.varDiff.targetTime + variance,
maxJump: config.poolServer.varDiff.maxJump
};
})();
function GetRewardTypeAsKey (rewardType) {
switch (rewardType) {
case 'solo':
return ':solo'
case 'prop':
return ''
default:
return ''
}
}
/**
* Miner
**/
function Miner (rewardType, childRewardType, id, childPoolIndex, login, pass, ip, port, agent, childLogin, startingDiff, noRetarget, pushMessage) {
this.rewardType = rewardType;
this.childRewardType = childRewardType;
this.rewardTypeAsKey = GetRewardTypeAsKey(rewardType);
this.childRewardTypeAsKey = GetRewardTypeAsKey(childRewardType);
this.lastChildBlockHeight = 0;
this.id = id;
this.activeChildPool = childPoolIndex || 0;
this.login = login;
this.pass = pass;
this.ip = ip;
this.port = port;
this.proxy = false;
if (agent && agent.includes('xmr-node-proxy')) {
this.proxy = true;
}
this.workerName = 'undefined';
this.childLogin = childLogin;
if (pass.lastIndexOf('@') >= 0 && pass.lastIndexOf('@') < pass.length) {
passDelimiterPos = pass.lastIndexOf('@') + 1;
this.workerName = pass.substr(passDelimiterPos, pass.length)
.trim();
}
this.pushMessage = pushMessage;
this.heartbeat();
this.noRetarget = noRetarget;
this.difficulty = startingDiff;
this.validJobs = [];
this.workerName = pass;
// Vardiff related variables
this.shareTimeRing = utils.ringBuffer(16);
this.lastShareTime = Date.now() / 1000 | 0;
if (shareTrustEnabled) {
this.trust = {
threshold: config.poolServer.shareTrust.threshold,
probability: 1,
penalty: 0
};
}
}
Miner.prototype = {
retarget: function (now) {
let options = config.poolServer.varDiff;
let sinceLast = now - this.lastShareTime;
let decreaser = sinceLast > VarDiff.tMax;
let avg = this.shareTimeRing.avg(decreaser ? sinceLast : null);
let newDiff;
let direction;
if (avg > VarDiff.tMax && this.difficulty > options.minDiff) {
newDiff = options.targetTime / avg * this.difficulty;
newDiff = newDiff > options.minDiff ? newDiff : options.minDiff;
direction = -1;
} else if (avg < VarDiff.tMin && this.difficulty < options.maxDiff) {
newDiff = options.targetTime / avg * this.difficulty;
newDiff = newDiff < options.maxDiff ? newDiff : options.maxDiff;
direction = 1;
} else {
return;
}
if (Math.abs(newDiff - this.difficulty) / this.difficulty * 100 > options.maxJump) {
let change = options.maxJump / 100 * this.difficulty * direction;
newDiff = this.difficulty + change;
}
this.setNewDiff(newDiff);
this.shareTimeRing.clear();
if (decreaser) this.lastShareTime = now;
},
setNewDiff: function (newDiff) {
newDiff = Math.round(newDiff);
if (this.difficulty === newDiff) {
return;
}
log('info', logSystem, 'Retargetting difficulty %d to %d for %s', [this.difficulty, newDiff, this.login]);
this.pendingDifficulty = newDiff;
this.pushMessage('job', this.getJob());
},
heartbeat: function () {
this.lastBeat = Date.now();
},
getTargetHex: function () {
if (this.pendingDifficulty) {
this.lastDifficulty = this.difficulty;
this.difficulty = this.pendingDifficulty;
this.pendingDifficulty = null;
}
let padded = Buffer.alloc(32);
padded.fill(0);
let diffBuff = diff1.div(this.difficulty).toBuffer();
diffBuff.copy(padded, 32 - diffBuff.length);
let buff = padded.slice(0, 4);
let buffArray = buff.toByteArray().reverse();
let buffReversed = Buffer.from(buffArray);
this.target = buffReversed.readUInt32BE(0);
let hex = buffReversed.toString('hex');
return hex;
},
getJob: function () {
let blockTemplate = currentBlockTemplate[this.activeChildPool];
let newJob = {
id: utils.uid(),
height: blockTemplate.height,
submissions: []
};
if (mergedMining) {
if (this.lastBlockHeight === blockTemplate.height && (!currentChildBlockTemplate[this.activeChildPool] || this.lastChildBlockHeight === currentChildBlockTemplate[this.activeChildPool].height) && !this.pendingDifficulty && this.cachedJob !== null && !config.daemon.alwaysPoll) {
return this.cachedJob;
}
this.lastChildBlockHeight = currentChildBlockTemplate ? currentChildBlockTemplate[this.activeChildPool].height : -1;
newJob.activeChildPool = this.activeChildPool;
newJob.childHeight = this.lastChildBlockHeight;
} else {
if (this.lastBlockHeight === blockTemplate.height && !this.pendingDifficulty && this.cachedJob !== null && !config.daemon.alwaysPoll) {
return this.cachedJob;
}
}
let blob = this.proxy ? blockTemplate.nextBlobWithChildNonce() : blockTemplate.nextBlob();
this.lastBlockHeight = blockTemplate.height;
let target = this.getTargetHex();
newJob.difficulty = this.difficulty;
newJob.diffHex = this.diffHex;
newJob.extraNonce = blockTemplate.extraNonce;
if (blockTemplate.isRandomX) {
newJob.seed_hash = blockTemplate.seed_hash;
newJob.next_seed_hash = blockTemplate.next_seed_hash;
}
this.validJobs.push(newJob);
while (this.validJobs.length > 4)
this.validJobs.shift();
this.cachedJob = {
job_id: newJob.id,
id: this.id
};
if (this.proxy) {
newJob.clientPoolLocation = blockTemplate.clientPoolLocation
newJob.clientNonceLocation = blockTemplate.clientNonceLocation
this.cachedJob.blocktemplate_blob = blob;
this.cachedJob.difficulty = blockTemplate.difficulty;
this.cachedJob.height = blockTemplate.height;
this.cachedJob.childHeight = this.lastChildBlockHeight;
this.cachedJob.reserved_offset = blockTemplate.reserveOffset;
this.cachedJob.client_nonce_offset = blockTemplate.clientNonceLocation;
this.cachedJob.client_pool_offset = blockTemplate.clientPoolLocation;
this.cachedJob.target_diff = this.difficulty;
this.cachedJob.target_diff_hex = this.diffHex;
} else {
this.cachedJob.blob = blob;
this.cachedJob.target = target;
}
if (typeof config.includeAlgo !== "undefined" && config.includeAlgo) {
this.cachedJob.algo = config.includeAlgo
}
if (typeof config.includeHeight !== "undefined" && config.includeHeight) {
this.cachedJob.height = blockTemplate.height
}
if (newJob.seed_hash) {
this.cachedJob.seed_hash = newJob.seed_hash;
this.cachedJob.next_seed_hash = newJob.next_seed_hash;
}
return this.cachedJob;
},
checkBan: function (validShare) {
if (!banningEnabled) return;
// Init global per-ip shares stats
if (!perIPStats[this.ip]) {
perIPStats[this.ip] = {
validShares: 0,
invalidShares: 0
};
}
let stats = perIPStats[this.ip];
validShare ? stats.validShares++ : stats.invalidShares++;
if (stats.validShares + stats.invalidShares >= config.poolServer.banning.checkThreshold) {
if (stats.invalidShares / stats.validShares >= config.poolServer.banning.invalidPercent / 100) {
validShare ? this.validShares++ : this.invalidShares++;
log('warn', logSystem, 'Banned %s@%s', [this.login, this.ip]);
bannedIPs[this.ip] = Date.now();
delete connectedMiners[this.id];
process.send({
type: 'banIP',
ip: this.ip
});
removeConnectedWorker(this, 'banned');
} else {
stats.invalidShares = 0;
stats.validShares = 0;
}
}
}
};
validateMinerPaymentId_difficulty = (address, ip, poolServerConfig, coin, sendReply) => {
if (utils.characterCount(address, '\\+') > 1) {
let message = `Invalid paymentId specified for ${coin}login, ${ip}`;
if (poolServerConfig.paymentId.validation) {
process.send({
type: 'banIP',
ip: ip
});
message += ` banned for ${poolServerConfig.banning.time / 60} minutes`
}
sendReply(message);
log('warn', logSystem, message);
return false;
}
if (utils.characterCount(address, '\\.') > 1) {
log('warn', logSystem, `Invalid difficulty specified for ${coin}login`);
sendReply(`Invalid difficulty specified for ${coin}login, ${ip}`);
return false;
}
return true;
}
/**
* Handle miner method
**/
function handleMinerMethod (method, params, ip, portData, sendReply, pushMessage) {
let miner = connectedMiners[params.id];
// Check for ban here, so preconnected attackers can't continue to screw you
if (IsBannedIp(ip)) {
sendReply('Your IP is banned');
return;
}
switch (method) {
case 'login':
let login = params.login;
if (!login) {
sendReply('Missing login');
return;
}
if (!validateMinerPaymentId_difficulty(login, ip, config.poolServer, 'parent ', sendReply))
{
return;
}
let calculated = utils.determineRewardData(login);
login = calculated.address;
let rewardType = calculated.rewardType;
let address = '';
let paymentid = null;
let port = portData.port;
let pass = params.pass;
let childLogin = pass.trim();
let childPoolIndex = 0;
let childRewardType = rewardType;
if (mergedMining) {
childPoolIndex = -1;
if (!validateMinerPaymentId_difficulty(pass, ip, config.poolServer, 'child ', sendReply))
{
return
}
calculated = utils.determineRewardData(pass);
pass = calculated.address;
childRewardType = calculated.rewardType;
if (pass.indexOf('@') >= 0 && pass.indexOf('@') >= 0) {
passDelimiterPos = pass.lastIndexOf('@');
childLogin = pass.substr(0, passDelimiterPos).trim();
}
childLogin = childLogin.replace(/\s/g, '');
childLogin = utils.cleanupSpecialChars(childLogin);
let addr = childLogin.split(config.poolServer.paymentId.addressSeparator);
address = addr[0] || null;
paymentId = addr[1] || null;
if (!address) {
log('warn', logSystem, 'No address specified for login');
sendReply('No address specified for login');
}
if (paymentId && config.poolServer.paymentId.validation) {
let valid = false;
config.poolServer.paymentId.validations.forEach(validation => {
if (paymentId.match(`^([a-zA-Z0-9]){${validation}}$`)) {
return valid = true;
}
})
if (!valid) {
log('warn', logSystem, 'Invalid paymentId specified for login');
}
if (config.poolServer.paymentId.ban) {
process.send({
type: 'banIP',
ip: ip
});
sendReply(`Invalid paymentId specified for login, ${portData.ip} banned for ${config.poolServer.banning.time / 60} minutes`);
}
}
for (i = 0; i < config.childPools.length; i++) {
if (config.childPools[i].pattern) {
if (new RegExp(config.childPools[i].pattern, 'i').test(address)) {
childPoolIndex = i;
break;
}
}
}
if (childPoolIndex < 0) {
childPoolIndex = fallBackCoin;
address = config.childPools[childPoolIndex].poolAddress;
childLogin = config.childPools[childPoolIndex].poolAddress;
}
if (!utils.validateChildMinerAddress(address, childPoolIndex)) {
let addressPrefix = utils.getAddressPrefix(address);
if (!addressPrefix) addressPrefix = 'N/A';
log('warn', logSystem, 'Invalid address used for childLogin (prefix: %s): %s', [addressPrefix, address]);
sendReply('Invalid address used for childLogin');
return;
}
}
let difficulty = portData.difficulty;
let noRetarget = false;
if (config.poolServer.fixedDiff.enabled) {
let fixedDiffCharPos = login.lastIndexOf(config.poolServer.fixedDiff.addressSeparator);
if (fixedDiffCharPos !== -1 && (login.length - fixedDiffCharPos < 32)) {
diffValue = login.substr(fixedDiffCharPos + 1);
difficulty = parseInt(diffValue);
login = login.substr(0, fixedDiffCharPos);
if (!difficulty || difficulty != diffValue) {
log('warn', logSystem, 'Invalid difficulty value "%s" for login: %s', [diffValue, login]);
difficulty = portData.difficulty;
} else {
noRetarget = true;
if (difficulty < config.poolServer.varDiff.minDiff) {
difficulty = config.poolServer.varDiff.minDiff;
}
}
}
}
addr = login.split(config.poolServer.paymentId.addressSeparator);
address = addr[0] || null;
paymentId = addr[1] || null;
if (!address) {
log('warn', logSystem, 'No address specified for login');
sendReply('Invalid address used for login');
return
}
if (paymentId && paymentId.match('^([a-zA-Z0-9]){0,15}$')) {
if (config.poolServer.paymentId.validation) {
process.send({
type: 'banIP',
ip: ip
});
log('warn', logSystem, 'Invalid paymentId specified for login');
} else {
log('warn', logSystem, 'Invalid paymentId specified for login');
}
sendReply(`Invalid paymentId specified for login, ${portData.ip} banned for ${config.poolServer.banning.time / 60} minutes`);
return;
}
if (!utils.validateMinerAddress(address)) {
let addressPrefix = utils.getAddressPrefix(address);
if (!addressPrefix) addressPrefix = 'N/A';
log('warn', logSystem, 'Invalid address used for login (prefix: %s): %s', [addressPrefix, address]);
sendReply('Invalid address used for login');
return;
}
let minerId = utils.uid();
miner = new Miner(rewardType, childRewardType, minerId, childPoolIndex, login, pass, ip, port, params.agent, childLogin, difficulty, noRetarget, pushMessage);
connectedMiners[minerId] = miner;
sendReply(null, {
id: minerId,
job: miner.getJob(),
status: 'OK'
});
newConnectedWorker(miner);
break;
case 'getjob':
if (!miner) {
sendReply('Unauthenticated');
return;
}
miner.heartbeat();
sendReply(null, miner.getJob());
break;
case 'submit':
if (!miner) {
sendReply('Unauthenticated');
return;
}
miner.heartbeat();
let job = miner.validJobs.filter(function (job) {
return job.id === params.job_id;
})[0];
if (!job) {
sendReply('Invalid job id');
return;
}
if (!params.nonce || !params.result) {
sendReply('Attack detected');
let minerText = miner ? (' ' + miner.login + '@' + miner.ip) : '';
log('warn', logSystem, 'Malformed miner share: ' + JSON.stringify(params) + ' from ' + minerText);
return;
}
if (!noncePattern.test(params.nonce)) {
let minerText = miner ? (' ' + miner.login + '@' + miner.ip) : '';
log('warn', logSystem, 'Malformed nonce: ' + JSON.stringify(params) + ' from ' + minerText);
perIPStats[miner.ip] = {
validShares: 0,
invalidShares: 999999
};
miner.checkBan(false);
sendReply('Duplicate share1');
return;
}
// Force lowercase for further comparison
params.nonce = params.nonce.toLowerCase();
if (!miner.proxy) {
if (job.submissions.indexOf(params.nonce) !== -1) {
let minerText = miner ? (' ' + miner.login + '@' + miner.ip) : '';
log('warn', logSystem, 'Duplicate share: ' + JSON.stringify(params) + ' from ' + minerText);
perIPStats[miner.ip] = {
validShares: 0,
invalidShares: 999999
};
miner.checkBan(false);
sendReply('Duplicate share2');
return;
}
job.submissions.push(params.nonce);
} else {
if (!Number.isInteger(params.poolNonce) || !Number.isInteger(params.workerNonce)) {
let minerText = miner ? (' ' + miner.login + '@' + miner.ip) : '';
log('warn', logSystem, 'Malformed nonce: ' + JSON.stringify(params) + ' from ' + minerText);
perIPStats[miner.ip] = {
validShares: 0,
invalidShares: 999999
};
miner.checkBan(false);
sendReply('Duplicate share3');
return;
}
let nonce_test = `${params.nonce}_${params.poolNonce}_${params.workerNonce}`;
if (job.submissions.indexOf(nonce_test) !== -1) {
let minerText = miner ? (' ' + miner.login + '@' + miner.ip) : '';
log('warn', logSystem, 'Duplicate share: ' + JSON.stringify(params) + ' from ' + minerText);
perIPStats[miner.ip] = {
validShares: 0,
invalidShares: 999999
};
miner.checkBan(false);
sendReply('Duplicate share4');
return;
}
job.submissions.push(nonce_test);
}
let isJobBlock = function (b) {
return b.height === job.height && job.childHeight === (
b.childBlockTemplate ? b.childBlockTemplate.height : undefined);
};
let blockTemplate = currentBlockTemplate[miner.activeChildPool];
if (job.childHeight) {
blockTemplate = isJobBlock(currentBlockTemplate[miner.activeChildPool]) ? currentBlockTemplate[miner.activeChildPool] : validBlockTemplates[miner.activeChildPool].filter(isJobBlock)[0];
}
if (!blockTemplate) {
sendReply('Block expired');
return;
}
let shareAccepted = processShare(miner, job, blockTemplate, params);
miner.checkBan(shareAccepted);
if (shareTrustEnabled) {
if (shareAccepted) {
miner.trust.probability -= shareTrustStepFloat;
if (miner.trust.probability < shareTrustMinFloat)
miner.trust.probability = shareTrustMinFloat;
miner.trust.penalty--;
miner.trust.threshold--;
} else {
log('warn', logSystem, 'Share trust broken by %s@%s', [miner.login, miner.ip]);
miner.trust.probability = 1;
miner.trust.penalty = config.poolServer.shareTrust.penalty;
}
}
if (!shareAccepted) {
sendReply('Rejected share: invalid result');
return;
}
let now = Date.now() / 1000 | 0;
miner.shareTimeRing.append(now - miner.lastShareTime);
miner.lastShareTime = now;
sendReply(null, {
status: 'OK'
});
break;
case 'keepalived':
if (!miner) {
sendReply('Unauthenticated');
return;
}
miner.heartbeat();
sendReply(null, {
status: 'KEEPALIVED'
});
break;
default:
sendReply('Invalid method');
let minerText = miner ? (' ' + miner.login + '@' + miner.ip) : '';
log('warn', logSystem, 'Invalid method: %s (%j) from %s', [method, params, minerText]);
break;
}
}
/**
* New connected worker
**/
function newConnectedWorker (miner) {
log('info', logSystem, 'Miner connected %s@%s on port', [miner.login, miner.ip, miner.port]);
if (miner.workerName !== 'undefined') log('info', logSystem, 'Worker Name: %s', [miner.workerName]);
if (miner.difficulty) log('info', logSystem, 'Miner difficulty fixed to %s', [miner.difficulty]);
redisClient.sadd(`${config.coin}:workers_ip:${miner.login}`, miner.ip);
redisClient.hincrby(`${config.coin}:ports:${miner.port}`, 'users', 1);
redisClient.hincrby(`${config.coin}:active_connections${miner.rewardTypeAsKey}`, `${miner.login}~${miner.workerName}`, 1, function (error, connectedWorkers) {
if (connectedWorkers === 1) {
notifications.sendToMiner(miner.login, 'workerConnected', {
'LOGIN': miner.login,
'MINER': `${miner.login.substring(0,7)}...${miner.login.substring(miner.login.length-7)}`,
'IP': miner.ip.replace('::ffff:', ''),
'PORT': miner.port,
'WORKER_NAME': miner.workerName !== 'undefined' ? miner.workerName : ''
});
}
});
if (config.poolServer.mergedMining) {
redisClient.sadd(`${config.childPools[miner.activeChildPool].coin}:workers_ip:${miner.childLogin}`, miner.ip);
redisClient.hincrby(`${config.childPools[miner.activeChildPool].coin}:ports:${miner.port}`, 'users', 1);
redisClient.hincrby(`${config.childPools[miner.activeChildPool].coin}:active_connections${miner.childRewardTypeAsKey}`, `${miner.childLogin}~${miner.workerName}`, 1, function (error, connectedWorkers) {});
let redisCommands = config.childPools.map(item => {
return ['hdel', `${config.coin}:workers:${miner.login}`, `${item.coin}`, ]
})
redisClient.multi(redisCommands)
.exec(function (error, replies) {
if (error) {
log('error', logSystem, 'Failed to clear childCoins from parent in redis %j \n %j', [err, redisCommands]);
}
})
redisClient.hset(`${config.coin}:workers:${miner.login}`, `${config.childPools[miner.activeChildPool].coin}`, miner.childLogin);
redisClient.hset(`${config.childPools[miner.activeChildPool].coin}:workers:${miner.childLogin}`, `${config.coin}`, miner.login);
}
}
/**
* Remove connected worker
**/
function removeConnectedWorker (miner, reason) {
redisClient.hincrby(`${config.coin}:ports:${miner.port}`, 'users', '-1');
if (mergedMining) {
redisClient.hincrby(`${config.childPools[miner.activeChildPool].coin}:ports:${miner.port}`, 'users', '-1');
redisClient.hincrby(`${config.childPools[miner.activeChildPool].coin}:active_connections${miner.childRewardTypeAsKey}`, `${miner.childLogin}~${miner.workerName}`, 1, function (error, connectedWorkers) {});
}
redisClient.hincrby(`${config.coin}:active_connections${miner.rewardTypeAsKey}`, `${miner.login}~${miner.workerName}`, -1, function (error, connectedWorkers) {
if (reason === 'banned') {
notifications.sendToMiner(miner.login, 'workerBanned', {
'LOGIN': miner.login,
'MINER': `${miner.login.substring(0,7)}...${miner.login.substring(miner.login.length-7)}`,
'IP': miner.ip.replace('::ffff:', ''),
'PORT': miner.port,
'WORKER_NAME': miner.workerName !== 'undefined' ? miner.workerName : ''
});
} else if (!connectedWorkers || connectedWorkers <= 0) {
notifications.sendToMiner(miner.login, 'workerTimeout', {
'LOGIN': miner.login,
'MINER': `${miner.login.substring(0,7)}...${miner.login.substring(miner.login.length-7)}`,
'IP': miner.ip.replace('::ffff:', ''),
'PORT': miner.port,
'WORKER_NAME': miner.workerName !== 'undefined' ? miner.workerName : '',
'LAST_HASH': utils.dateFormat(new Date(miner.lastBeat), 'yyyy-mm-dd HH:MM:ss Z')
});
}
});
}
/**
* Return if IP has been banned
**/
function IsBannedIp (ip) {
if (!banningEnabled || !bannedIPs[ip]) return false;
let bannedTime = bannedIPs[ip];
let bannedTimeAgo = Date.now() - bannedTime;
let timeLeft = config.poolServer.banning.time * 1000 - bannedTimeAgo;
if (timeLeft > 0) {
return true;
} else {
delete bannedIPs[ip];
log('info', logSystem, 'Ban dropped for %s', [ip]);
return false;
}
}
function recordShareData (miner, job, shareDiff, blockCandidate, hashHex, shareType, blockTemplate, pool) {
let dateNow = Date.now();
let dateNowSeconds = dateNow / 1000 | 0;
let coin = pool !== null ? pool.coin : config.coin;
let login = pool !== null ? miner.childLogin : miner.login;
let job_height = pool !== null ? job.childHeight : job.height;
let workerName = miner.workerName;
let rewardType = pool !== null ? miner.childRewardType : miner.rewardType;
let updateScore;
// Weighting older shares lower than newer ones to prevent pool hopping
if (slushMiningEnabled) {
// We need to do this via an eval script because we need fetching the last block time and
// calculating the score to run in a single transaction (otherwise we could have a race
// condition where a block gets discovered between the time we look up lastBlockFound and
// insert the score, which would give the miner an erroneously huge proportion on the new block)
updateScore = ['eval', `
local age = (ARGV[3] - redis.call('hget', KEYS[2], 'lastBlockFound')) / 1000
local score = string.format('%.17g', ARGV[2] * math.exp(age / ARGV[4]))
redis.call('hincrbyfloat', KEYS[1], ARGV[1], score)
return {score, tostring(age)}
`,
2 /*keys*/ , coin + ':scores:roundCurrent', coin + ':stats',
/* args */
login, job.difficulty, Date.now(), config.poolServer.slushMining.weight
];
} else {
job.score = job.difficulty;
updateScore = ['hincrbyfloat', `${coin}:scores:${rewardType}:roundCurrent`, login, job.score]
}
let redisCommands = [
updateScore,
['hincrby', `${coin}:shares_actual:${rewardType}:roundCurrent`, login, job.difficulty],
['zadd', `${coin}:hashrate`, dateNowSeconds, [job.difficulty, login, dateNow, rewardType].join(':')],
['hincrby', `${coin}:workers:${login}`, 'hashes', job.difficulty],
['hset', `${coin}:workers:${login}`, 'lastShare', dateNowSeconds],
['expire', `${coin}:workers:${login}`, (86400 * cleanupInterval)],
['expire', `${coin}:payments:${login}`, (86400 * cleanupInterval)]
];
if (workerName) {
redisCommands.push(['zadd', `${coin}:hashrate`, dateNowSeconds, [job.difficulty, login + '~' + workerName, dateNow, rewardType].join(':')]);
redisCommands.push(['hincrby', `${coin}:unique_workers:${login}~${workerName}`, 'hashes', job.difficulty]);
redisCommands.push(['hset', `${coin}:unique_workers:${login}~${workerName}`, 'lastShare', dateNowSeconds]);
redisCommands.push(['expire', `${coin}:unique_workers:${login}~${workerName}`, (86400 * cleanupInterval)]);
}
if (blockCandidate) {
redisCommands.push(['hset', `${coin}:stats`, `lastBlockFound${rewardType}`, Date.now()]);
redisCommands.push(['rename', `${coin}:scores:prop:roundCurrent`, coin + ':scores:prop:round' + job_height]);
redisCommands.push(['rename', `${coin}:scores:solo:roundCurrent`, coin + ':scores:solo:round' + job_height]);
redisCommands.push(['rename', `${coin}:shares_actual:prop:roundCurrent`, `${coin}:shares_actual:prop:round${job_height}`]);
redisCommands.push(['rename', `${coin}:shares_actual:solo:roundCurrent`, `${coin}:shares_actual:solo:round${job_height}`]);
if (rewardType === 'prop') {
redisCommands.push(['hgetall', `${coin}:scores:prop:round${job_height}`]);
redisCommands.push(['hgetall', `${coin}:shares_actual:prop:round${job_height}`]);
}
if (rewardType === 'solo') {
redisCommands.push(['hget', `${coin}:scores:solo:round${job_height}`, login]);
redisCommands.push(['hget', `${coin}:shares_actual:solo:round${job_height}`, login]);
}
}
redisClient.multi(redisCommands)
.exec(function (err, replies) {
if (err) {
log('error', logSystem, 'Failed to insert share data into redis %j \n %j', [err, redisCommands]);
return;
}
if (slushMiningEnabled) {
job.score = parseFloat(replies[0][0]);
let age = parseFloat(replies[0][1]);
log('info', logSystem, 'Submitted score ' + job.score + ' for difficulty ' + job.difficulty + ' and round age ' + age + 's');
}
if (blockCandidate) {
let workerScores = replies[replies.length - 2];
let workerShares = replies[replies.length - 1];
let totalScore = 0;
let totalShares = 0;
if (rewardType === 'solo') {
totalScore = workerScores
totalShares = workerShares
}
if (rewardType === 'prop') {
totalScore = Object.keys(workerScores)
.reduce(function (p, c) {
return p + parseFloat(workerScores[c])
}, 0);
totalShares = Object.keys(workerShares)
.reduce(function (p, c) {
return p + parseInt(workerShares[c])
}, 0);
}
redisClient.zadd(coin + ':blocks:candidates', job_height, [
rewardType,
login,
hashHex,
Date.now() / 1000 | 0,
blockTemplate.difficulty,
totalShares,
totalScore
].join(':'), function (err, result) {
if (err) {
log('error', logSystem, 'Failed inserting block candidate %s \n %j', [hashHex, err]);
}
});
notifications.sendToAll('blockFound', {
'HEIGHT': job_height,
'HASH': hashHex,
'DIFFICULTY': blockTemplate.difficulty,
'SHARES': totalShares,
'MINER': login.substring(0, 7) + '...' + login.substring(login.length - 7)
});
}
});
log('info', logSystem, 'Accepted %s share at difficulty %d/%d from %s@%s', [shareType, job.difficulty, shareDiff, login, miner.ip]);
}
function getShareBuffer (miner, job, blockTemplate, params) {
let nonce = params.nonce;
let resultHash = params.result;
let template = Buffer.alloc(blockTemplate.buffer.length);
if (!miner.proxy) {
blockTemplate.buffer.copy(template);
template.writeUInt32BE(job.extraNonce, blockTemplate.reserveOffset);
} else {
blockTemplate.buffer.copy(template);
template.writeUInt32BE(job.extraNonce, blockTemplate.reserveOffset);
template.writeUInt32BE(params.poolNonce, job.clientPoolLocation);
template.writeUInt32BE(params.workerNonce, job.clientNonceLocation);
}
try {
let shareBuffer = utils.cnUtil.construct_block_blob(template, Buffer.from(nonce, 'hex'), cnBlobType);
return shareBuffer;
} catch (e) {
log('error', logSystem, "Can't get share buffer with nonce %s from %s@%s: %s", [nonce, miner.login, miner.ip, e]);
return null;
}
}
/**
* Process miner share data
**/
function processShare (miner, job, blockTemplate, params) {
let shareBuffer = getShareBuffer(miner, job, blockTemplate, params)
if (!shareBuffer) {
return false
}
let resultHash = params.result
let hash;
let shareType;
if (shareTrustEnabled && miner.trust.threshold <= 0 && miner.trust.penalty <= 0 && Math.random() > miner.trust.probability) {
hash = Buffer.from(resultHash, 'hex');
shareType = 'trusted';
} else {
let convertedBlob = utils.cnUtil.convert_blob(shareBuffer, cnBlobType);
let hard_fork_version = convertedBlob[0];
if (blockTemplate.isRandomX) {
hash = cryptoNight(convertedBlob, Buffer.from(blockTemplate.seed_hash, 'hex'), cnVariant);
} else {
if (typeof config.includeHeight !== "undefined" && config.includeHeight)
hash = cryptoNight(convertedBlob, cnVariant, job.height);
else
hash = cryptoNight(convertedBlob, cnVariant);
}
log('info', logSystem, 'Mining pool algorithm: %s variant %d, Hard fork version: %d', [cnAlgorithm, cnVariant, hard_fork_version]);
shareType = 'valid'
}
if (hash.toString('hex') !== resultHash) {
log('warn', logSystem, 'Bad hash from miner %s@%s', [miner.login, miner.ip]);
return false;
}
let hashArray = hash.toByteArray()
.reverse();
let hashNum = bignum.fromBuffer(Buffer.from(hashArray));
let hashDiff = diff1.div(hashNum);
if (hashDiff.ge(blockTemplate.difficulty)) {
apiInterfaces.rpcDaemon('submitblock', [shareBuffer.toString('hex')], function (error, result) {
if (error) {
log('error', logSystem, 'Error submitting block at height %d from %s@%s, share type: "%s" - %j', [job.height, miner.login, miner.ip, shareType, error]);
} else {
let blockFastHash = utils.cnUtil.get_block_id(shareBuffer, cnBlobType).toString('hex');
log('info', logSystem,
'Block %s found at height %d by miner %s@%s - submit result: %j',
[blockFastHash.substr(0, 6), job.height, miner.login, miner.ip, result]
);
recordShareData(miner, job, hashDiff.toString(), true, blockFastHash, shareType, blockTemplate, null);
}
});
} else if (hashDiff.lt(job.difficulty)) {
log('warn', logSystem, 'Rejected low difficulty share of %s from %s@%s', [hashDiff.toString(), miner.login, miner.ip]);
return false;
} else {
recordShareData(miner, job, hashDiff.toString(), false, null, shareType, null, null);
}
if (!job.childHeight) {
return true;
}
var childBlockTemplate = blockTemplate.childBlockTemplate;
if (childBlockTemplate) {
if (mergedMining) {
let pool = config.childPools[miner.activeChildPool]
if (hashDiff.ge(childBlockTemplate.difficulty)) {
let mergedBuffer = null
try {
mergedBuffer = utils.cnUtil.construct_mm_child_block_blob(shareBuffer, cnBlobType, childBlockTemplate.buffer);
} catch (e) {
log('error', logSystem, "Failed to construct MM child block: " + e);
}
if (mergedBuffer === null) {
recordShareStatusMerged(miner, 'invalid');
} else {
let onChildSuccess = (result) => {
let blockFastHash = utils.cnUtil.get_block_id(mergedBuffer, 2).toString('hex')
log('info', logSystem,
'Child Block %s found at height %d by miner %s@%s - submit result: %j',
[blockFastHash.substr(0, 6), job.childHeight, miner.workerName, miner.ip, result]);
recordShareData(miner, job, hashDiff.toString(), true, blockFastHash, shareType, childBlockTemplate, pool);
}
apiInterfaces.rpcDaemon('submitblock', [mergedBuffer.toString('hex')], function (error, result) {
if (error) {
log('error', logSystem, 'Error submitting child block at height %d from %s@%s, share type: "%s" - %j', [job.childHeight, miner.login, miner.ip, shareType, error]);
} else {
onChildSuccess(result)
}
}, pool.childDaemon);
}
} else if (hashDiff.lt(job.difficulty)) {
log('warn', logSystem, 'Rejected low difficulty share of %s from %s@%s', [hashDiff.toString(), miner.workerName, miner.ip]);
return false;
} else {
recordShareData(miner, job, hashDiff.toString(), false, null, shareType, null, pool);
}
}
return true;
}
return true;
}
/**
* Start pool server on TCP ports
**/
let httpResponse = ' 200 OK\nContent-Type: text/plain\nContent-Length: 20\n\nMining server online';
function startPoolServerTcp (callback) {
log('info', logSystem, 'Clear values for connected workers in redis database.');
redisClient.del(config.coin + ':active_connections');
async.each(config.poolServer.ports, function (portData, cback) {
let handleMessage = function (socket, jsonData, pushMessage) {
if (!jsonData.id) {
log('warn', logSystem, 'Miner RPC request missing RPC id');
return;
} else if (!jsonData.method) {
log('warn', logSystem, 'Miner RPC request missing RPC method');
return;
} else if (!jsonData.params) {
log('warn', logSystem, 'Miner RPC request missing RPC params');
return;
}
let sendReply = function (error, result) {
if (!socket.writable) return;
let sendData = JSON.stringify({
id: jsonData.id,
jsonrpc: "2.0",
error: error ? {
code: -1,
message: error
} : null,
result: result
}) + "\n";
socket.write(sendData);
};
handleMinerMethod(jsonData.method, jsonData.params, socket.remoteAddress, portData, sendReply, pushMessage);
};
let socketResponder = function (socket) {
socket.setKeepAlive(true);
socket.setEncoding('utf8');
let dataBuffer = '';
let pushMessage = function (method, params) {
if (!socket.writable) return;
let sendData = JSON.stringify({
jsonrpc: "2.0",
method: method,
params: params
}) + "\n";
socket.write(sendData);
};
socket.on('data', function (d) {
dataBuffer += d;
if (Buffer.byteLength(dataBuffer, 'utf8') > 10240) { //10KB
dataBuffer = null;
log('warn', logSystem, 'Socket flooding detected and prevented from %s', [socket.remoteAddress]);
socket.destroy();
return;
}
if (dataBuffer.indexOf('\n') !== -1) {
let messages = dataBuffer.split('\n');
let incomplete = dataBuffer.slice(-1) === '\n' ? '' : messages.pop();
for (let i = 0; i < messages.length; i++) {
let message = messages[i];
if (message.trim() === '') continue;
let jsonData;
try {
jsonData = JSON.parse(message);
} catch (e) {
if (message.indexOf('GET /') === 0) {
if (message.indexOf('HTTP/1.1') !== -1) {
socket.end('HTTP/1.1' + httpResponse);
break;
} else if (message.indexOf('HTTP/1.0') !== -1) {
socket.end('HTTP/1.0' + httpResponse);
break;
}
}
log('warn', logSystem, 'Malformed message from %s: %s', [socket.remoteAddress, message]);
socket.destroy();
break;
}
try {
handleMessage(socket, jsonData, pushMessage);
} catch (e) {
log('warn', logSystem, 'Malformed message from ' + socket.remoteAddress + ' generated an exception. Message: ' + message);
if (e.message) log('warn', logSystem, 'Exception: ' + e.message);
}
}
dataBuffer = incomplete;
}
})
.on('error', function (err) {
if (err.code !== 'ECONNRESET')
log('warn', logSystem, 'Socket error from %s %j', [socket.remoteAddress, err]);
})
.on('close', function () {
pushMessage = function () {};
});
};
if (portData.ssl) {
if (!config.poolServer.sslCert) {
log('error', logSystem, 'Could not start server listening on port %d (SSL): SSL certificate not configured', [portData.port]);
cback(true);
} else if (!config.poolServer.sslKey) {
log('error', logSystem, 'Could not start server listening on port %d (SSL): SSL key not configured', [portData.port]);
cback(true);
} else if (!fs.existsSync(config.poolServer.sslCert)) {
log('error', logSystem, 'Could not start server listening on port %d (SSL): SSL certificate file not found (configuration error)', [portData.port]);
cback(true);
} else if (!fs.existsSync(config.poolServer.sslKey)) {
log('error', logSystem, 'Could not start server listening on port %d (SSL): SSL key file not found (configuration error)', [portData.port]);
cback(true);
} else {
let options = {
key: fs.readFileSync(config.poolServer.sslKey),
cert: fs.readFileSync(config.poolServer.sslCert),
};
if (config.poolServer.sslCA && fs.existsSync(config.poolServer.sslCA)) {
options.ca = fs.readFileSync(config.poolServer.sslCA)
}
tls.createServer(options, socketResponder)
.listen(portData.port, function (error, result) {
if (error) {
log('error', logSystem, 'Could not start server listening on port %d (SSL), error: $j', [portData.port, error]);
cback(true);
return;
}
log('info', logSystem, 'Clear values for SSL port %d in redis database.', [portData.port]);
redisClient.del(config.coin + ':ports:' + portData.port);
redisClient.hset(config.coin + ':ports:' + portData.port, 'port', portData.port);
log('info', logSystem, 'Started server listening on port %d (SSL)', [portData.port]);
cback();
});
}
} else {
net.createServer(socketResponder)
.listen(portData.port, function (error, result) {
if (error) {
log('error', logSystem, 'Could not start server listening on port %d, error: $j', [portData.port, error]);
cback(true);
return;
}
log('info', logSystem, 'Clear values for port %d in redis database.', [portData.port]);
redisClient.del(config.coin + ':ports:' + portData.port);
redisClient.hset(config.coin + ':ports:' + portData.port, 'port', portData.port);
log('info', logSystem, 'Started server listening on port %d', [portData.port]);
cback();
});
}
}, function (err) {
if (err) {
callback(false);
} else {
callback(true);
}
});
}
/**
* Initialize pool server
**/
(function init (loop) {
async.waterfall([
function (callback) {
if (!poolStarted) {
startPoolServerTcp(function (successful) {
poolStarted = true
});
setTimeout(init, 1000, loop);
return;
}
callback(true)
}
],
function (err) {
if (loop === true) {
setTimeout(function () {
init(true);
}, config.poolServer.blockRefreshInterval);
}
}
);
})();