You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1034 lines
41 KiB
1034 lines
41 KiB
"use strict";
|
|
const debug = require('debug')('pool');
|
|
const uuidV4 = require('uuid/v4');
|
|
const crypto = require('crypto');
|
|
const bignum = require('bignum');
|
|
const cluster = require('cluster');
|
|
const btcValidator = require('wallet-address-validator');
|
|
const async = require('async');
|
|
const net = require('net');
|
|
const tls = require('tls');
|
|
const fs = require('fs');
|
|
const multiHashing = require("cryptonight-hashing");
|
|
|
|
let nonceCheck = new RegExp("^[0-9a-f]{8}$");
|
|
let bannedIPs = [];
|
|
let bannedAddresses = [];
|
|
let baseDiff = global.coinFuncs.baseDiff();
|
|
let pastBlockTemplates = global.support.circularBuffer(4);
|
|
let activeMiners = [];
|
|
let activeBlockTemplate;
|
|
let workerList = [];
|
|
let httpResponse = ' 200 OK\nContent-Type: text/plain\nContent-Length: 18\n\nMining Pool Online';
|
|
let threadName;
|
|
let minerCount = [];
|
|
let BlockTemplate = global.coinFuncs.BlockTemplate;
|
|
let hexMatch = new RegExp("^[0-9a-f]+$");
|
|
let totalShares = 0, trustedShares = 0, normalShares = 0, invalidShares = 0;
|
|
|
|
|
|
Buffer.prototype.toByteArray = function () {
|
|
return Array.prototype.slice.call(this, 0);
|
|
};
|
|
|
|
|
|
if (cluster.isMaster) {
|
|
threadName = "(Master) ";
|
|
setInterval(function () {
|
|
console.log(`Processed ${trustedShares}/${normalShares}/${invalidShares}/${totalShares} Trusted/Validated/Invalid/Total shares in the last 30 seconds`);
|
|
totalShares = 0;
|
|
trustedShares = 0;
|
|
normalShares = 0;
|
|
invalidShares = 0;
|
|
}, 30000);
|
|
} else {
|
|
threadName = "(Worker " + cluster.worker.id + " - " + process.pid + ") ";
|
|
}
|
|
|
|
global.database.thread_id = threadName;
|
|
|
|
function registerPool() {
|
|
global.mysql.query("SELECT * FROM pools WHERE id = ?", [global.config.pool_id]).then(function (rows) {
|
|
rows.forEach(function (row) {
|
|
if (row.ip !== global.config.bind_ip) {
|
|
console.error("Pool ID in use already for a different IP. Update MySQL or change pool ID.");
|
|
process.exit(1);
|
|
}
|
|
});
|
|
}).then(function () {
|
|
global.mysql.query("INSERT INTO pools (id, ip, last_checkin, active, hostname) VALUES (?, ?, now(), ?, ?) ON DUPLICATE KEY UPDATE last_checkin=now(), active=?",
|
|
[global.config.pool_id, global.config.bind_ip, true, global.config.hostname, true]);
|
|
global.mysql.query("DELETE FROM ports WHERE pool_id = ?", [global.config.pool_id]).then(function () {
|
|
global.config.ports.forEach(function (port) {
|
|
if ('ssl' in port && port.ssl === true) {
|
|
global.mysql.query("INSERT INTO ports (pool_id, network_port, starting_diff, port_type, description, hidden, ip_address, ssl_port) values (?, ?, ?, ?, ?, ?, ?, 1)",
|
|
[global.config.pool_id, port.port, port.difficulty, port.portType, port.desc, port.hidden, global.config.bind_ip]);
|
|
} else {
|
|
global.mysql.query("INSERT INTO ports (pool_id, network_port, starting_diff, port_type, description, hidden, ip_address, ssl_port) values (?, ?, ?, ?, ?, ?, ?, 0)",
|
|
[global.config.pool_id, port.port, port.difficulty, port.portType, port.desc, port.hidden, global.config.bind_ip]);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// Master/Slave communication Handling
|
|
function messageHandler(message) {
|
|
switch (message.type) {
|
|
case 'banIP':
|
|
debug(threadName + "Received ban IP update from nodes");
|
|
if (cluster.isMaster) {
|
|
sendToWorkers(message);
|
|
} else {
|
|
bannedIPs.push(message.data);
|
|
}
|
|
break;
|
|
case 'newBlockTemplate':
|
|
debug(threadName + "Received new block template");
|
|
if (cluster.isMaster) {
|
|
sendToWorkers(message);
|
|
newBlockTemplate(message.data);
|
|
} else {
|
|
newBlockTemplate(message.data);
|
|
}
|
|
break;
|
|
case 'removeMiner':
|
|
if (cluster.isMaster) {
|
|
minerCount[message.data] -= 1;
|
|
}
|
|
break;
|
|
case 'newMiner':
|
|
if (cluster.isMaster) {
|
|
minerCount[message.data] += 1;
|
|
}
|
|
break;
|
|
case 'sendRemote':
|
|
if (cluster.isMaster) {
|
|
global.database.sendQueue.push({body: Buffer.from(message.body, 'hex')});
|
|
}
|
|
break;
|
|
case 'trustedShare':
|
|
trustedShares += 1;
|
|
totalShares += 1;
|
|
break;
|
|
case 'normalShare':
|
|
normalShares += 1;
|
|
totalShares += 1;
|
|
break;
|
|
case 'invalidShare':
|
|
invalidShares += 1;
|
|
totalShares += 1;
|
|
}
|
|
}
|
|
|
|
process.on('message', messageHandler);
|
|
|
|
function sendToWorkers(data) {
|
|
workerList.forEach(function (worker) {
|
|
worker.send(data);
|
|
});
|
|
}
|
|
|
|
function retargetMiners() {
|
|
debug(threadName + "Performing difficulty check on miners");
|
|
console.log('Performing difficulty update on miners');
|
|
for (let minerId in activeMiners) {
|
|
if (activeMiners.hasOwnProperty(minerId)) {
|
|
let miner = activeMiners[minerId];
|
|
if (!miner.fixed_diff) {
|
|
miner.updateDifficulty();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function checkAliveMiners() {
|
|
debug(threadName + "Verifying if miners are still alive");
|
|
for (let minerId in activeMiners) {
|
|
if (activeMiners.hasOwnProperty(minerId)) {
|
|
let miner = activeMiners[minerId];
|
|
if (Date.now() - miner.lastContact > global.config.pool.minerTimeout * 1000) {
|
|
process.send({type: 'removeMiner', data: miner.port});
|
|
delete activeMiners[minerId];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function templateUpdate(repeating) {
|
|
global.coinFuncs.getBlockTemplate(global.config.pool.address, function (rpcResponse) {
|
|
if (rpcResponse && typeof rpcResponse.result !== 'undefined') {
|
|
rpcResponse = rpcResponse.result;
|
|
let buffer = new Buffer(rpcResponse.blocktemplate_blob, 'hex');
|
|
let new_hash = new Buffer(32);
|
|
buffer.copy(new_hash, 0, 7, 39);
|
|
if (!activeBlockTemplate || new_hash.toString('hex') !== activeBlockTemplate.previous_hash.toString('hex')) {
|
|
debug(threadName + "New block template found at " + rpcResponse.height + " height with hash: " + new_hash.toString('hex'));
|
|
if (cluster.isMaster) {
|
|
sendToWorkers({type: 'newBlockTemplate', data: rpcResponse});
|
|
newBlockTemplate(rpcResponse);
|
|
} else {
|
|
process.send({type: 'newBlockTemplate', data: rpcResponse});
|
|
newBlockTemplate(rpcResponse);
|
|
}
|
|
}
|
|
} else {
|
|
if (repeating !== true) {
|
|
setTimeout(templateUpdate, 300);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function newBlockTemplate(template) {
|
|
let buffer = new Buffer(template.blocktemplate_blob, 'hex');
|
|
let previous_hash = new Buffer(32);
|
|
buffer.copy(previous_hash, 0, 7, 39);
|
|
console.log(threadName + 'New block to mine at height: ' + template.height + '. Difficulty: ' + template.difficulty);
|
|
if (activeBlockTemplate) {
|
|
pastBlockTemplates.enq(activeBlockTemplate);
|
|
}
|
|
activeBlockTemplate = new BlockTemplate(template);
|
|
for (let minerId in activeMiners) {
|
|
if (activeMiners.hasOwnProperty(minerId)) {
|
|
let miner = activeMiners[minerId];
|
|
debug(threadName + "Updating worker " + miner.payout + " With new work at height: " + template.height);
|
|
miner.sendNewJob();
|
|
}
|
|
}
|
|
}
|
|
|
|
let VarDiff = (function () {
|
|
let variance = global.config.pool.varDiffVariance / 100 * global.config.pool.targetTime;
|
|
return {
|
|
tMin: global.config.pool.targetTime - variance,
|
|
tMax: global.config.pool.targetTime + variance
|
|
};
|
|
})();
|
|
|
|
function Miner(id, login, pass, ipAddress, startingDiff, messageSender, protoVersion, portType, port, agent) {
|
|
// Username Layout - <address in BTC or XMR>.<Difficulty>
|
|
// Password Layout - <password>.<miner identifier>.<payment ID for XMR>
|
|
// Default function is to use the password so they can login. Identifiers can be unique, payment ID is last.
|
|
// If there is no miner identifier, then the miner identifier is set to the password
|
|
// If the password is x, aka, old-logins, we're not going to allow detailed review of miners.
|
|
|
|
// Miner Variables
|
|
// prevent pool crash when pass is null
|
|
if (!pass) pass = 'x';
|
|
let pass_split = pass.split(":");
|
|
this.oldVersion = false;
|
|
this.error = "";
|
|
this.identifier = pass_split[0];
|
|
this.proxy = false;
|
|
if (agent && agent.includes('MinerGate')) {
|
|
this.identifier = "MinerGate";
|
|
}
|
|
if (agent && agent.includes('xmr-node-proxy')) {
|
|
this.proxy = true;
|
|
}
|
|
this.paymentID = null;
|
|
this.valid_miner = true;
|
|
this.port = port;
|
|
this.portType = portType;
|
|
this.incremented = false;
|
|
switch (portType) {
|
|
case 'pplns':
|
|
this.poolTypeEnum = global.protos.POOLTYPE.PPLNS;
|
|
break;
|
|
case 'pps':
|
|
this.poolTypeEnum = global.protos.POOLTYPE.PPS;
|
|
break;
|
|
case 'solo':
|
|
this.poolTypeEnum = global.protos.POOLTYPE.SOLO;
|
|
break;
|
|
case 'prop':
|
|
this.poolTypeEnum = global.protos.POOLTYPE.PROP;
|
|
break;
|
|
}
|
|
let diffSplit = login.split("+");
|
|
let addressSplit = diffSplit[0].split('.');
|
|
this.address = addressSplit[0];
|
|
this.payout = addressSplit[0];
|
|
this.fixed_diff = false;
|
|
this.difficulty = startingDiff;
|
|
this.connectTime = Date.now();
|
|
if (agent && agent.includes('NiceHash')) {
|
|
this.fixed_diff = true;
|
|
this.difficulty = global.coinFuncs.niceHashDiff;
|
|
}
|
|
if (diffSplit.length === 2) {
|
|
this.fixed_diff = true;
|
|
this.difficulty = Number(diffSplit[1]);
|
|
if (this.difficulty < global.config.pool.minDifficulty) {
|
|
this.difficulty = global.config.pool.minDifficulty;
|
|
}
|
|
if (this.difficulty > global.config.pool.maxDifficulty) {
|
|
this.difficulty = global.config.pool.maxDifficulty;
|
|
}
|
|
} else if (diffSplit.length > 2) {
|
|
this.error = "Too many options in the login field";
|
|
this.valid_miner = false;
|
|
}
|
|
if (typeof(addressSplit[1]) !== 'undefined' && addressSplit[1].length === 64 && hexMatch.test(addressSplit[1])) {
|
|
this.paymentID = addressSplit[1];
|
|
this.payout = this.address + "." + this.paymentID;
|
|
} else if (typeof(addressSplit[1]) !== 'undefined') {
|
|
this.identifier = pass_split[0] === 'x' ? addressSplit[1] : pass_split[0];
|
|
}
|
|
if (typeof(addressSplit[2]) !== 'undefined') {
|
|
this.identifier = pass_split[0] === 'x' ? addressSplit[2] : pass_split[0];
|
|
}
|
|
|
|
if (pass_split.length === 2) {
|
|
/*
|
|
Email address is: pass_split[1]
|
|
Need to do an initial registration call here. Might as well do it right...
|
|
*/
|
|
let payoutAddress = this.payout;
|
|
global.mysql.query("SELECT id FROM users WHERE username = ? LIMIT 1", [this.payout]).then(function (rows) {
|
|
if (rows.length > 0) {
|
|
return;
|
|
}
|
|
if (global.coinFuncs.blockedAddresses.indexOf(payoutAddress) !== -1) {
|
|
return;
|
|
}
|
|
global.mysql.query("INSERT INTO users (username, email) VALUES (?, ?)", [payoutAddress, pass_split[1]]);
|
|
});
|
|
} else if (pass_split.length > 2) {
|
|
this.error = "Too many options in the password field";
|
|
this.valid_miner = false;
|
|
}
|
|
|
|
if (global.coinFuncs.validateAddress(this.address)) {
|
|
this.bitcoin = 0;
|
|
} else if (btcValidator.validate(this.address) && global.config.general.allowBitcoin && global.coinFuncs.supportsAutoExchange) {
|
|
this.bitcoin = 1;
|
|
} else if (btcValidator.validate(this.address)) {
|
|
this.error = "This pool does not allow payouts to bitcoin.";
|
|
this.valid_miner = false;
|
|
} else {
|
|
// Invalid Addresses
|
|
this.error = "Invalid payment address provided";
|
|
this.valid_miner = false;
|
|
}
|
|
if (bannedAddresses.indexOf(this.address) !== -1) {
|
|
// Banned Address
|
|
this.error = "Banned payment address provided";
|
|
this.valid_miner = false;
|
|
}
|
|
if (global.coinFuncs.exchangeAddresses.indexOf(this.address) !== -1 && !(this.paymentID)) {
|
|
this.error = "Exchange addresses need payment IDs";
|
|
this.valid_miner = false;
|
|
}
|
|
if (!activeBlockTemplate) {
|
|
this.error = "No active block template";
|
|
this.valid_miner = false;
|
|
}
|
|
|
|
this.id = id;
|
|
this.ipAddress = ipAddress;
|
|
this.messageSender = messageSender;
|
|
this.heartbeat = function () {
|
|
this.lastContact = Date.now();
|
|
};
|
|
this.heartbeat();
|
|
|
|
// VarDiff System
|
|
this.shareTimeBuffer = global.support.circularBuffer(8);
|
|
this.shareTimeBuffer.enq(global.config.pool.targetTime);
|
|
this.lastShareTime = Math.floor(Date.now() / 1000);
|
|
|
|
this.validShares = 0;
|
|
this.invalidShares = 0;
|
|
this.hashes = 0;
|
|
this.logString = this.address + " ID: " + this.identifier + " IP: " + this.ipAddress;
|
|
|
|
if (global.config.pool.trustedMiners) {
|
|
this.trust = {
|
|
threshold: global.config.pool.trustThreshold,
|
|
probability: 256,
|
|
penalty: 0
|
|
};
|
|
}
|
|
|
|
this.validJobs = global.support.circularBuffer(4);
|
|
this.sentJobs = global.support.circularBuffer(8);
|
|
|
|
this.cachedJob = null;
|
|
|
|
this.invalidShareProto = global.protos.InvalidShare.encode({
|
|
paymentAddress: this.address,
|
|
paymentID: this.paymentID,
|
|
identifier: this.identifier
|
|
});
|
|
|
|
// Support functions for how miners activate and run.
|
|
this.updateDifficultyOld = function () {
|
|
let now = Math.round(Date.now() / 1000);
|
|
let avg = this.shareTimeBuffer.average(this.lastShareTime);
|
|
|
|
let sinceLast = now - this.lastShareTime;
|
|
let decreaser = sinceLast > VarDiff.tMax;
|
|
|
|
let newDiff;
|
|
let direction;
|
|
|
|
if (avg > VarDiff.tMax && this.difficulty > global.config.pool.minDifficulty) {
|
|
newDiff = global.config.pool.targetTime / avg * this.difficulty;
|
|
direction = -1;
|
|
}
|
|
else if (avg < VarDiff.tMin && this.difficulty < global.config.pool.maxDifficulty) {
|
|
newDiff = global.config.pool.targetTime / avg * this.difficulty;
|
|
direction = 1;
|
|
}
|
|
else {
|
|
return;
|
|
}
|
|
|
|
if (Math.abs(newDiff - this.difficulty) / this.difficulty * 100 > global.config.pool.maxDiffChange) {
|
|
let change = global.config.pool.maxDiffChange / 100 * this.difficulty * direction;
|
|
newDiff = this.difficulty + change;
|
|
}
|
|
|
|
this.setNewDiff(newDiff);
|
|
this.shareTimeBuffer.clear();
|
|
if (decreaser) {
|
|
this.lastShareTime = now;
|
|
}
|
|
};
|
|
|
|
this.updateDifficulty = function () {
|
|
if (this.hashes > 0) {
|
|
let newDiff = 0;
|
|
if (this.proxy) {
|
|
newDiff = Math.floor(Math.floor(this.hashes / (Math.floor((Date.now() - this.connectTime) / 1000)))* 5);
|
|
} else {
|
|
newDiff = Math.floor(this.hashes / (Math.floor((Date.now() - this.connectTime) / 1000))) * global.config.pool.targetTime;
|
|
}
|
|
this.setNewDiff(newDiff);
|
|
} else {
|
|
this.updateDifficultyOld();
|
|
}
|
|
};
|
|
|
|
this.setNewDiff = function (difficulty) {
|
|
this.newDiff = Math.round(difficulty);
|
|
debug(threadName + "Difficulty: " + this.newDiff + " For: " + this.logString + " Time Average: " + this.shareTimeBuffer.average(this.lastShareTime) + " Entries: " + this.shareTimeBuffer.size() + " Sum: " + this.shareTimeBuffer.sum());
|
|
if (this.newDiff > global.config.pool.maxDifficulty && !this.proxy) {
|
|
this.newDiff = global.config.pool.maxDifficulty;
|
|
}
|
|
if (this.difficulty === this.newDiff) {
|
|
return;
|
|
}
|
|
if (this.newDiff < global.config.pool.minDifficulty) {
|
|
this.newDiff = global.config.pool.minDifficulty;
|
|
}
|
|
debug(threadName + "Difficulty change to: " + this.newDiff + " For: " + this.logString);
|
|
if (this.hashes > 0) {
|
|
debug(threadName + "Hashes: " + this.hashes + " in: " + Math.floor((Date.now() - this.connectTime) / 1000) + " seconds gives: " +
|
|
Math.floor(this.hashes / (Math.floor((Date.now() - this.connectTime) / 1000))) + " hashes/second or: " +
|
|
Math.floor(this.hashes / (Math.floor((Date.now() - this.connectTime) / 1000))) * global.config.pool.targetTime + " difficulty versus: " + this.newDiff);
|
|
}
|
|
this.sendNewJob();
|
|
};
|
|
|
|
this.checkBan = function (validShare) {
|
|
if (!global.config.pool.banEnabled) {
|
|
return;
|
|
}
|
|
|
|
// Valid stats are stored by the pool.
|
|
if (validShare) {
|
|
this.validShares += 1;
|
|
} else {
|
|
this.invalidShares += 1;
|
|
}
|
|
if (this.validShares + this.invalidShares >= global.config.pool.banThreshold) {
|
|
if (this.invalidShares / this.validShares >= global.config.pool.banPercent / 100) {
|
|
delete activeMiners[this.id];
|
|
process.send({type: 'banIP', data: this.ipAddress});
|
|
}
|
|
else {
|
|
this.invalidShares = 0;
|
|
this.validShares = 0;
|
|
}
|
|
}
|
|
};
|
|
|
|
if (protoVersion === 1) {
|
|
this.getTargetHex = function () {
|
|
if (this.newDiff) {
|
|
this.difficulty = this.newDiff;
|
|
this.newDiff = null;
|
|
}
|
|
let padded = new Buffer(32);
|
|
padded.fill(0);
|
|
let diffBuff = baseDiff.div(this.difficulty).toBuffer();
|
|
diffBuff.copy(padded, 32 - diffBuff.length);
|
|
|
|
let buff = padded.slice(0, 4);
|
|
let buffArray = buff.toByteArray().reverse();
|
|
let buffReversed = new Buffer(buffArray);
|
|
this.target = buffReversed.readUInt32BE(0);
|
|
return buffReversed.toString('hex');
|
|
};
|
|
this.getJob = function () {
|
|
|
|
if (this.lastBlockHeight === activeBlockTemplate.height && activeBlockTemplate.idHash === this.validJobs.get(0).blockHash && !this.newDiff && this.cachedJob !== null) {
|
|
return this.cachedJob;
|
|
}
|
|
|
|
if (!this.proxy) {
|
|
let blob = activeBlockTemplate.nextBlob();
|
|
let target = this.getTargetHex();
|
|
this.lastBlockHeight = activeBlockTemplate.height;
|
|
|
|
|
|
let newJob = {
|
|
id: crypto.pseudoRandomBytes(21).toString('base64'),
|
|
extraNonce: activeBlockTemplate.extraNonce,
|
|
height: activeBlockTemplate.height,
|
|
difficulty: this.difficulty,
|
|
diffHex: this.diffHex,
|
|
submissions: [],
|
|
seed_hash: activeBlockTemplate.seedHash ? activeBlockTemplate.seedHash.toString('hex') : null,
|
|
blockHash: activeBlockTemplate.idHash
|
|
};
|
|
|
|
this.validJobs.enq(newJob);
|
|
this.cachedJob = {
|
|
blob: blob,
|
|
job_id: newJob.id,
|
|
target: target,
|
|
id: this.id,
|
|
seed_hash: activeBlockTemplate.seedHash ? activeBlockTemplate.seedHash.toString('hex') : null,
|
|
height: newJob.height
|
|
};
|
|
} else {
|
|
let blob = activeBlockTemplate.nextBlobWithChildNonce();
|
|
if (this.newDiff) {
|
|
this.difficulty = this.newDiff;
|
|
this.newDiff = null;
|
|
}
|
|
this.lastBlockHeight = activeBlockTemplate.height;
|
|
|
|
let newJob = {
|
|
id: crypto.pseudoRandomBytes(21).toString('base64'),
|
|
extraNonce: activeBlockTemplate.extraNonce,
|
|
height: activeBlockTemplate.height,
|
|
difficulty: this.difficulty,
|
|
diffHex: this.diffHex,
|
|
clientPoolLocation: activeBlockTemplate.clientPoolLocation,
|
|
clientNonceLocation: activeBlockTemplate.clientNonceLocation,
|
|
seed_hash: activeBlockTemplate.seedHash ? activeBlockTemplate.seedHash.toString('hex') : null,
|
|
submissions: []
|
|
};
|
|
this.validJobs.enq(newJob);
|
|
this.cachedJob = {
|
|
blocktemplate_blob: blob,
|
|
difficulty: activeBlockTemplate.difficulty,
|
|
height: activeBlockTemplate.height,
|
|
reserved_offset: activeBlockTemplate.reserveOffset,
|
|
client_nonce_offset: activeBlockTemplate.clientNonceLocation,
|
|
client_pool_offset: activeBlockTemplate.clientPoolLocation,
|
|
target_diff: this.difficulty,
|
|
target_diff_hex: this.diffHex,
|
|
seed_hash: activeBlockTemplate.seedHash ? activeBlockTemplate.seedHash.toString('hex') : null,
|
|
job_id: newJob.id,
|
|
id: this.id
|
|
};
|
|
}
|
|
return this.cachedJob;
|
|
};
|
|
|
|
this.sendNewJob = function() {
|
|
let job = this.getJob();
|
|
let tempJob = this.sentJobs.toarray().filter(function (intJob) {
|
|
return intJob.id === job.job_id;
|
|
})[0];
|
|
|
|
if (tempJob) {
|
|
console.error(`Tried sending a duped job to: ${this.address}, stopped by Snipa!`);
|
|
return;
|
|
}
|
|
this.sentJobs.enq(job);
|
|
return this.messageSender('job', job);
|
|
};
|
|
}
|
|
}
|
|
|
|
function recordShareData(miner, job, shareDiff, blockCandidate, hashHex, shareType, blockTemplate) {
|
|
miner.hashes += job.difficulty;
|
|
global.database.storeShare(job.height, global.protos.Share.encode({
|
|
shares: job.difficulty,
|
|
paymentAddress: miner.address,
|
|
paymentID: miner.paymentID,
|
|
foundBlock: blockCandidate,
|
|
trustedShare: shareType,
|
|
poolType: miner.poolTypeEnum,
|
|
poolID: global.config.pool_id,
|
|
blockDiff: activeBlockTemplate.difficulty,
|
|
bitcoin: miner.bitcoin,
|
|
blockHeight: job.height,
|
|
timestamp: Date.now(),
|
|
identifier: miner.identifier
|
|
}));
|
|
if (blockCandidate) {
|
|
global.database.storeBlock(job.height, global.protos.Block.encode({
|
|
hash: hashHex,
|
|
difficulty: blockTemplate.difficulty,
|
|
unlockHeight: blockTemplate.unlockHeight,
|
|
shares: 0,
|
|
timestamp: Date.now(),
|
|
poolType: miner.poolTypeEnum,
|
|
unlocked: false,
|
|
valid: true
|
|
}));
|
|
}
|
|
if (shareType) {
|
|
process.send({type: 'trustedShare'});
|
|
debug(threadName + "Accepted trusted share at difficulty: " + job.difficulty + "/" + shareDiff + " from: " + miner.logString);
|
|
} else {
|
|
process.send({type: 'normalShare'});
|
|
debug(threadName + "Accepted valid share at difficulty: " + job.difficulty + "/" + shareDiff + " from: " + miner.logString);
|
|
}
|
|
|
|
}
|
|
|
|
function processShare(miner, job, blockTemplate, params, sendReply) {
|
|
if (miner.oldVersion) {
|
|
sendReply("Incorrect hashing protocol in use. Please upgrade/fix your miner");
|
|
process.send({type: "invalidShare"});
|
|
return false;
|
|
}
|
|
let nonce = params.nonce;
|
|
let resultHash = params.result;
|
|
let template = new Buffer(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);
|
|
}
|
|
let shareBuffer = global.coinFuncs.constructNewBlob(template, new Buffer(nonce, 'hex'));
|
|
|
|
let convertedBlob;
|
|
let hash;
|
|
let shareType;
|
|
|
|
if (global.config.pool.trustedMiners && miner.trust.threshold <= 0 && miner.trust.penalty <= 0 &&
|
|
crypto.randomBytes(1).readUIntBE(0, 1) > miner.trust.probability) {
|
|
hash = new Buffer(resultHash, 'hex');
|
|
shareType = true;
|
|
}
|
|
else {
|
|
convertedBlob = global.coinFuncs.convertBlob(shareBuffer);
|
|
if (blockTemplate.seedHash && blockTemplate.seedHash.length == 32) {
|
|
hash = multiHashing.randomx(convertedBlob, blockTemplate.seedHash, 0);
|
|
shareType = false;
|
|
} else {
|
|
hash = multiHashing.cryptonight(convertedBlob, convertedBlob[0] >= 10 ? 13 : 8, job.height);
|
|
shareType = false;
|
|
}
|
|
}
|
|
if (hash.toString('hex') !== resultHash) {
|
|
if (job.height >= 1546000 && coinFuncs.isxmr) {
|
|
if (multiHashing.cryptonight(convertedBlob, 0).toString("hex") === resultHash) {
|
|
console.error(threadName + "Bad hashing algo (CN/0) from miner " + miner.logString);
|
|
process.send({type: "invalidShare"});
|
|
miner.oldVersion = true;
|
|
return false;
|
|
}
|
|
if (multiHashing.cryptonight(convertedBlob, 1).toString("hex") === resultHash) {
|
|
console.error(threadName + "Bad hashing algo (CN/1) from miner " + miner.logString);
|
|
process.send({type: "invalidShare"});
|
|
miner.oldVersion = true;
|
|
return false;
|
|
}
|
|
if (multiHashing.cryptonight(convertedBlob, 8).toString("hex") === resultHash) {
|
|
console.error(threadName + "Bad hashing algo (CN/2) from miner " + miner.logString);
|
|
process.send({type: "invalidShare"});
|
|
miner.oldVersion = true;
|
|
return false;
|
|
}
|
|
}
|
|
console.error(threadName + "Bad share from miner " + miner.logString);
|
|
process.send({type: 'invalidShare'});
|
|
if (miner.incremented === false) {
|
|
miner.newDiff = miner.difficulty + 1;
|
|
miner.incremented = true;
|
|
} else {
|
|
miner.newDiff = miner.difficulty - 1;
|
|
miner.incremented = false;
|
|
}
|
|
miner.sendNewJob();
|
|
return false;
|
|
}
|
|
|
|
let hashArray = hash.toByteArray().reverse();
|
|
let hashNum = bignum.fromBuffer(new Buffer(hashArray));
|
|
let hashDiff = baseDiff.div(hashNum);
|
|
|
|
|
|
if (hashDiff.ge(blockTemplate.difficulty)) {
|
|
// Submit block to the RPC Daemon.
|
|
// Todo: Implement within the coins/<coin>.js file.
|
|
global.support.rpcDaemon('submitblock', [shareBuffer.toString('hex')], function (rpcResult) {
|
|
if (rpcResult.error) {
|
|
// Did not manage to submit a block. Log and continue on.
|
|
console.error(threadName + "Error submitting block at height " + job.height + " from " + miner.logString + ", share type: " + shareType + " error: " + JSON.stringify(rpcResult.error));
|
|
recordShareData(miner, job, hashDiff.toString(), false, null, shareType);
|
|
// Error on submit, so we'll submit a sanity check for good measure.
|
|
templateUpdate();
|
|
} else if (rpcResult) {
|
|
//Success! Submitted a block without an issue.
|
|
let blockFastHash = global.coinFuncs.getBlockID(shareBuffer).toString('hex');
|
|
console.log(threadName + "Block " + blockFastHash.substr(0, 6) + " found at height " + job.height + " by " + miner.logString +
|
|
", share type: " + shareType + " - submit result: " + JSON.stringify(rpcResult.result));
|
|
recordShareData(miner, job, hashDiff.toString(), true, blockFastHash, shareType, blockTemplate);
|
|
templateUpdate();
|
|
} else {
|
|
// RPC bombed out massively.
|
|
console.error(threadName + "RPC Error. Please check logs for details");
|
|
}
|
|
});
|
|
}
|
|
else if (hashDiff.lt(job.difficulty)) {
|
|
process.send({type: 'invalidShare'});
|
|
console.warn(threadName + "Rejected low diff share of " + hashDiff.toString() + " from: " + miner.address + " ID: " +
|
|
miner.identifier + " IP: " + miner.ipAddress);
|
|
return false;
|
|
}
|
|
else {
|
|
recordShareData(miner, job, hashDiff.toString(), false, null, shareType);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function handleMinerData(method, params, ip, portData, sendReply, pushMessage) {
|
|
let miner = activeMiners[params.id];
|
|
// Check for ban here, so preconnected attackers can't continue to screw you
|
|
if (bannedIPs.indexOf(ip) !== -1) {
|
|
// Handle IP ban off clip.
|
|
sendReply("IP Address currently banned");
|
|
return;
|
|
}
|
|
switch (method) {
|
|
case 'login':
|
|
if (!params.login || (!params.pass && params.agent && !params.agent.includes('MinerGate'))) {
|
|
sendReply("No login/password specified");
|
|
return;
|
|
}
|
|
let difficulty = portData.difficulty;
|
|
let minerId = uuidV4();
|
|
miner = new Miner(minerId, params.login, params.pass, ip, difficulty, pushMessage, 1, portData.portType, portData.port, params.agent);
|
|
if (!miner.valid_miner) {
|
|
console.log("Invalid miner, disconnecting due to: " + miner.error);
|
|
sendReply(miner.error);
|
|
return;
|
|
}
|
|
process.send({type: 'newMiner', data: miner.port});
|
|
activeMiners[minerId] = miner;
|
|
sendReply(null, {
|
|
id: minerId,
|
|
job: miner.getJob(),
|
|
status: 'OK'
|
|
});
|
|
break;
|
|
case 'getjob':
|
|
if (!miner) {
|
|
sendReply('Unauthenticated');
|
|
return;
|
|
}
|
|
miner.heartbeat();
|
|
miner.sendNewJob();
|
|
break;
|
|
case 'submit':
|
|
if (!miner) {
|
|
sendReply('Unauthenticated');
|
|
return;
|
|
}
|
|
miner.heartbeat();
|
|
|
|
let job = miner.validJobs.toarray().filter(function (job) {
|
|
return job.id === params.job_id;
|
|
})[0];
|
|
|
|
if (!job) {
|
|
sendReply('Invalid job id');
|
|
return;
|
|
}
|
|
|
|
params.nonce = params.nonce.substr(0, 8).toLowerCase();
|
|
if (!nonceCheck.test(params.nonce)) {
|
|
console.warn(threadName + 'Malformed nonce: ' + JSON.stringify(params) + ' from ' + miner.logString);
|
|
miner.checkBan(false);
|
|
sendReply('Duplicate share');
|
|
global.database.storeInvalidShare(miner.invalidShareProto);
|
|
return;
|
|
}
|
|
if (!miner.proxy) {
|
|
if (job.submissions.indexOf(params.nonce) !== -1) {
|
|
console.warn(threadName + 'Duplicate share: ' + JSON.stringify(params) + ' from ' + miner.logString);
|
|
miner.checkBan(false);
|
|
sendReply('Duplicate share');
|
|
global.database.storeInvalidShare(miner.invalidShareProto);
|
|
return;
|
|
}
|
|
job.submissions.push(params.nonce);
|
|
} else {
|
|
if (!Number.isInteger(params.poolNonce) || !Number.isInteger(params.workerNonce)) {
|
|
console.warn(threadName + 'Malformed nonce: ' + JSON.stringify(params) + ' from ' + miner.logString);
|
|
miner.checkBan(false);
|
|
sendReply('Duplicate share');
|
|
global.database.storeInvalidShare(miner.invalidShareProto);
|
|
return;
|
|
}
|
|
let nonce_test = `${params.nonce}_${params.poolNonce}_${params.workerNonce}`;
|
|
if (job.submissions.indexOf(nonce_test) !== -1) {
|
|
console.warn(threadName + 'Duplicate share: ' + JSON.stringify(params) + ' from ' + miner.logString);
|
|
miner.checkBan(false);
|
|
sendReply('Duplicate share');
|
|
global.database.storeInvalidShare(miner.invalidShareProto);
|
|
return;
|
|
}
|
|
job.submissions.push(nonce_test);
|
|
}
|
|
|
|
let blockTemplate = activeBlockTemplate.height === job.height ? activeBlockTemplate : pastBlockTemplates.toarray().filter(function (t) {
|
|
return t.height === job.height;
|
|
})[0];
|
|
|
|
if (!blockTemplate) {
|
|
console.warn(threadName + 'Block expired, Height: ' + job.height + ' from ' + miner.logString);
|
|
if (miner.incremented === false) {
|
|
miner.newDiff = miner.difficulty + 1;
|
|
miner.incremented = true;
|
|
} else {
|
|
miner.newDiff = miner.difficulty - 1;
|
|
miner.incremented = false;
|
|
}
|
|
miner.sendNewJob();
|
|
sendReply('Block expired');
|
|
global.database.storeInvalidShare(miner.invalidShareProto);
|
|
return;
|
|
}
|
|
|
|
let shareAccepted = processShare(miner, job, blockTemplate, params, sendReply);
|
|
miner.checkBan(shareAccepted);
|
|
|
|
if (global.config.pool.trustedMiners) {
|
|
if (shareAccepted) {
|
|
miner.trust.probability -= global.config.pool.trustChange;
|
|
if (miner.trust.probability < (global.config.pool.trustMin)) {
|
|
miner.trust.probability = global.config.pool.trustMin;
|
|
}
|
|
miner.trust.penalty--;
|
|
miner.trust.threshold--;
|
|
}
|
|
else {
|
|
console.log(threadName + "Share trust broken by " + miner.logString);
|
|
global.database.storeInvalidShare(miner.invalidShareProto);
|
|
miner.trust.probability = 256;
|
|
miner.trust.penalty = global.config.pool.trustPenalty;
|
|
miner.trust.threshold = global.config.pool.trustThreshold;
|
|
}
|
|
}
|
|
|
|
if (!shareAccepted) {
|
|
sendReply('Low difficulty share');
|
|
return;
|
|
}
|
|
|
|
let now = Date.now() / 1000 || 0;
|
|
miner.shareTimeBuffer.enq(now - miner.lastShareTime);
|
|
miner.lastShareTime = now;
|
|
|
|
sendReply(null, {status: 'OK'});
|
|
break;
|
|
case 'keepalived':
|
|
if (!miner) {
|
|
sendReply('Unauthenticated');
|
|
return;
|
|
}
|
|
sendReply(null, {
|
|
status: 'KEEPALIVED'
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (cluster.isMaster) {
|
|
let numWorkers = require('os').cpus().length;
|
|
global.config.ports.forEach(function (portData) {
|
|
minerCount[portData.port] = 0;
|
|
});
|
|
registerPool();
|
|
setInterval(function () {
|
|
global.mysql.query("UPDATE pools SET last_checkin = ?, active = ? WHERE id = ?", [global.support.formatDate(Date.now()), true, global.config.pool_id]);
|
|
if (activeBlockTemplate) {
|
|
global.mysql.query("UPDATE pools SET blockIDTime = now(), blockID = ? where id = ?", [activeBlockTemplate.height, global.config.pool_id]);
|
|
}
|
|
global.config.ports.forEach(function (portData) {
|
|
global.mysql.query("UPDATE ports SET lastSeen = now(), miners = ? WHERE pool_id = ? AND network_port = ?", [minerCount[portData.port], global.config.pool_id, portData.port]);
|
|
});
|
|
}, 10000);
|
|
console.log('Master cluster setting up ' + numWorkers + ' workers...');
|
|
|
|
for (let i = 0; i < numWorkers; i++) {
|
|
let worker = cluster.fork();
|
|
worker.on('message', messageHandler);
|
|
workerList.push(worker);
|
|
}
|
|
|
|
cluster.on('online', function (worker) {
|
|
console.log('Worker ' + worker.process.pid + ' is online');
|
|
});
|
|
|
|
cluster.on('exit', function (worker, code, signal) {
|
|
console.log('Worker ' + worker.process.pid + ' died with code: ' + code + ', and signal: ' + signal);
|
|
console.log('Starting a new worker');
|
|
worker = cluster.fork();
|
|
worker.on('message', messageHandler);
|
|
workerList.push(worker);
|
|
});
|
|
templateUpdate();
|
|
setInterval(templateUpdate, 300, true);
|
|
global.support.sendEmail(global.config.general.adminEmail, "Pool server " + global.config.hostname + " online", "The pool server: " + global.config.hostname + " with IP: " + global.config.bind_ip + " is online");
|
|
} else {
|
|
setInterval(checkAliveMiners, 30000);
|
|
setInterval(retargetMiners, global.config.pool.retargetTime * 1000);
|
|
templateUpdate();
|
|
setInterval(function () {
|
|
bannedIPs = [];
|
|
templateUpdate();
|
|
}, 60000);
|
|
async.each(global.config.ports, function (portData) {
|
|
if (global.config[portData.portType].enable !== true) {
|
|
return;
|
|
}
|
|
let handleMessage = function (socket, jsonData, pushMessage) {
|
|
if (!jsonData.id) {
|
|
console.warn('Miner RPC request missing RPC id');
|
|
return;
|
|
}
|
|
else if (!jsonData.method) {
|
|
console.warn('Miner RPC request missing RPC method');
|
|
return;
|
|
}
|
|
else if (!jsonData.params) {
|
|
console.warn('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);
|
|
};
|
|
handleMinerData(jsonData.method, jsonData.params, socket.remoteAddress, portData, sendReply, pushMessage);
|
|
};
|
|
|
|
function socketConn(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;
|
|
console.warn(threadName + 'Excessive packet size from: ' + 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;
|
|
}
|
|
}
|
|
|
|
console.warn(threadName + "Malformed message from " + socket.remoteAddress + " Message: " + message);
|
|
socket.destroy();
|
|
|
|
break;
|
|
}
|
|
handleMessage(socket, jsonData, pushMessage);
|
|
}
|
|
dataBuffer = incomplete;
|
|
}
|
|
}).on('error', function (err) {
|
|
if (err.code !== 'ECONNRESET') {
|
|
console.warn(threadName + "Socket Error from " + socket.remoteAddress + " Error: " + err);
|
|
}
|
|
}).on('close', function () {
|
|
pushMessage = function () {
|
|
};
|
|
});
|
|
}
|
|
|
|
if ('ssl' in portData && portData.ssl === true) {
|
|
tls.createServer({
|
|
key: fs.readFileSync('cert.key'),
|
|
cert: fs.readFileSync('cert.pem')
|
|
}, socketConn).listen(portData.port, global.config.bind_ip, function (error) {
|
|
if (error) {
|
|
console.error(threadName + "Unable to start server on: " + portData.port + " Message: " + error);
|
|
return;
|
|
}
|
|
console.log(threadName + "Started server on port: " + portData.port);
|
|
});
|
|
} else {
|
|
net.createServer(socketConn).listen(portData.port, global.config.bind_ip, function (error) {
|
|
if (error) {
|
|
console.error(threadName + "Unable to start server on: " + portData.port + " Message: " + error);
|
|
return;
|
|
}
|
|
console.log(threadName + "Started server on port: " + portData.port);
|
|
});
|
|
}
|
|
});
|
|
}
|