Front-end with live stats
parent
b1bb3108ff
commit
c027c2b8f9
37
README.md
37
README.md
|
@ -3,11 +3,15 @@ node-cryptonote-pool
|
|||
|
||||
Mining pool for CryptoNote based coins such as Bytecoin and Monero
|
||||
|
||||
#### TODO
|
||||
#### Features
|
||||
|
||||
* Flooding detection
|
||||
* IP banning for low-diff shares (prevent CPU overload with low-diff share attacks)
|
||||
* Collecting stats and exposing via ajax/rest API
|
||||
* Variable difficulty / share limiter
|
||||
* IP banning to prevent low-diff share attacks
|
||||
* Socket flooding detection
|
||||
* Payment processing
|
||||
* Detailed logging
|
||||
* Clustering for vertical scaling
|
||||
* Live stats API
|
||||
* Currency network/block difficulty
|
||||
* Current block height
|
||||
* Network hashrate
|
||||
|
@ -15,11 +19,11 @@ Mining pool for CryptoNote based coins such as Bytecoin and Monero
|
|||
* Each miners' hashrate
|
||||
* Blocks found (pending, confirmed, and orphaned)
|
||||
* Total paid out
|
||||
* Worker login validation (make sure miners are uing proper wallet addresses for mining)
|
||||
|
||||
#### TODO
|
||||
|
||||
* Worker login validation (make sure miners are using proper wallet addresses for mining)
|
||||
* Light-weight front-end using API to display pool data
|
||||
* Sending payments
|
||||
* Add pool fee percent to config
|
||||
* Use redis data and wallet API to send out payments
|
||||
|
||||
|
||||
Usage
|
||||
|
@ -60,6 +64,12 @@ Explanation for each field:
|
|||
/* Port that simpleminer is pointed to. */
|
||||
"poolPort": 5555,
|
||||
|
||||
/* Host that simpleminer is pointed to. */
|
||||
"poolHost": "example.com",
|
||||
|
||||
/* Contact email address. */
|
||||
"email": "support@cryppit.com",
|
||||
|
||||
/* Address where block rewards go, and miner payments come from. */
|
||||
"poolAddress": "4AsBy39rpUMTmgTUARGq2bFQWhDhdQNekK5v4uaLU699NPAnx9CubEJ82AkvD5ScoAZNYRwBxybayainhyThHAZWCdKmPYn"
|
||||
|
||||
|
@ -82,8 +92,7 @@ Explanation for each field:
|
|||
"enabled": true,
|
||||
"time": 600, //How many seconds to ban worker for
|
||||
"invalidPercent": 50, //What percent of invalid shares triggers ban
|
||||
"checkThreshold": 30, //Perform check when this many shares have been submitted
|
||||
"purgeInterval": 300 //Every this many seconds clear out the list of old bans
|
||||
"checkThreshold": 30 //Perform check when this many shares have been submitted
|
||||
},
|
||||
|
||||
/* Set to "auto" by default which will spawn one process/fork/worker for each CPU
|
||||
|
@ -118,6 +127,14 @@ Explanation for each field:
|
|||
/* Poll RPC daemons for new blocks every this many milliseconds. */
|
||||
"blockRefreshInterval": 1000,
|
||||
|
||||
/* REST API used for front-end website. */
|
||||
"api": {
|
||||
"enabled": true,
|
||||
"hashrateWindow": 15,
|
||||
"updateInterval": 0.5,
|
||||
"port": 8117
|
||||
},
|
||||
|
||||
/* Coin daemon connection details. */
|
||||
"daemon": {
|
||||
"host": "127.0.0.1",
|
||||
|
|
21
config.json
21
config.json
|
@ -6,15 +6,18 @@
|
|||
"transferFee": 1000000,
|
||||
|
||||
"poolPort": 5555,
|
||||
"poolHost": "cryppit.com",
|
||||
|
||||
"poolAddress": "KdvRKTrXzyC8eW76eCzcWNWdYBnpCs6bAZeheF2LdJj5bBLry4zs8MoV6uqMLBYPZPSJPU3RtZMjHgWn5qWwvYdTHKVxLKw",
|
||||
"email": "support@cryppit.com",
|
||||
|
||||
"difficulty": 10,
|
||||
"poolAddress": "4AsBy39rpUMTmgTUARGq2bFQWhDhdQNekK5v4uaLU699NPAnx9CubEJ82AkvD5ScoAZNYRwBxybayainhyThHAZWCdKmPYn",
|
||||
|
||||
"difficulty": 200,
|
||||
"varDiff": {
|
||||
"minDiff": 2,
|
||||
"maxDiff": 512,
|
||||
"targetTime": 15,
|
||||
"retargetTime": 30,
|
||||
"targetTime": 60,
|
||||
"retargetTime": 10,
|
||||
"variancePercent": 30
|
||||
},
|
||||
|
||||
|
@ -22,8 +25,7 @@
|
|||
"enabled": true,
|
||||
"time": 600,
|
||||
"invalidPercent": 50,
|
||||
"checkThreshold": 30,
|
||||
"purgeInterval": 300
|
||||
"checkThreshold": 30
|
||||
},
|
||||
|
||||
"clusterForks": "auto",
|
||||
|
@ -45,6 +47,13 @@
|
|||
|
||||
"blockRefreshInterval": 1000,
|
||||
|
||||
"api": {
|
||||
"enabled": true,
|
||||
"hashrateWindow": 300,
|
||||
"updateInterval": 3.5,
|
||||
"port": 8117
|
||||
},
|
||||
|
||||
"daemon": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 18081
|
||||
|
|
|
@ -0,0 +1,217 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head lang="en">
|
||||
<meta charset="UTF-8">
|
||||
<title>Cryptonote Mining Pool</title>
|
||||
|
||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
|
||||
|
||||
<link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
|
||||
|
||||
<link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body {
|
||||
padding-top: 90px;
|
||||
}
|
||||
.container{
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.page{
|
||||
display: none;
|
||||
}
|
||||
.stats {
|
||||
display: inline-block;
|
||||
width: 40%;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.stats:last-child{
|
||||
width: auto;
|
||||
}
|
||||
.stats > div{
|
||||
|
||||
padding: 5px 0;
|
||||
}
|
||||
.stats > div > span{
|
||||
font-weight: bold;
|
||||
}
|
||||
#yourStatsInput{
|
||||
width: 550px;
|
||||
}
|
||||
#yourHashrateHolder, #yourAddressDisplay{
|
||||
display: none;
|
||||
}
|
||||
#addressError{
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
|
||||
var api = 'http://cryppit.com:8117';
|
||||
|
||||
$(function(){
|
||||
$.get(api + '/stats', function(data){
|
||||
renderStats(data);
|
||||
});
|
||||
|
||||
var source = new EventSource(api + '/live_stats');
|
||||
source.addEventListener('message', function(e){
|
||||
var data = JSON.parse(e.data);
|
||||
renderStats(data);
|
||||
});
|
||||
|
||||
window.onhashchange = function(){
|
||||
pageRouter();
|
||||
};
|
||||
|
||||
pageRouter();
|
||||
|
||||
|
||||
var addressEventSource;
|
||||
$('#lookUp').click(function(){
|
||||
$(this).text('Searching...');
|
||||
var address = $('#yourStatsInput').val().trim();
|
||||
if (!address) return;
|
||||
if (addressEventSource) addressEventSource.close();
|
||||
addressEventSource = new EventSource(api + '/stats_address?address=' + address);
|
||||
addressEventSource.addEventListener('message', function(e){
|
||||
$('#lookUp').text('Lookup');
|
||||
var data = JSON.parse(e.data);
|
||||
if (data.stats){
|
||||
$('#addressError').hide();
|
||||
$('#yourHashrateHolder').show().children(":first").text(data.stats + '/sec');
|
||||
$('#yourAddressDisplay').show().children(":first").text(address);
|
||||
return;
|
||||
}
|
||||
$('#yourHashrateHolder').hide();
|
||||
$('#yourAddressDisplay').hide();
|
||||
$('#addressError').text(data.error).show();
|
||||
});
|
||||
addressEventSource.addEventListener('open', function(e){
|
||||
$('#lookUp').text('Lookup');
|
||||
});
|
||||
addressEventSource.addEventListener('error', function(e){
|
||||
$('#lookUp').text('Lookup');
|
||||
$('#addressError').text('Connection error').show();
|
||||
});
|
||||
addressEventSource.addEventListener('close', function(e){
|
||||
$('#lookUp').text('Lookup');
|
||||
$('#addressError').text('Connection closed').show();
|
||||
})
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
function getReadableHashRateString(hashrate){
|
||||
var i = 0;
|
||||
var byteUnits = [' H', ' KH', ' MH', ' GH', ' TH', ' PH' ];
|
||||
while (hashrate > 1024){
|
||||
hashrate = hashrate / 1024;
|
||||
i++;
|
||||
}
|
||||
return hashrate.toFixed(2) + byteUnits[i];
|
||||
}
|
||||
|
||||
function renderStats(stats){
|
||||
$('#networkHashrate').text(getReadableHashRateString(stats.network.difficulty / 60) + '/sec');
|
||||
$('#networkDifficulty').text(stats.network.difficulty);
|
||||
$('#blockchainHeight').text(stats.network.height);
|
||||
|
||||
$('#poolHashrate').text(stats.pool.hashrate + '/sec');
|
||||
$('#poolMiners').text(stats.pool.miners);
|
||||
$('#poolFee').text(stats.config.fee + '%');
|
||||
|
||||
$('#simpleminer_code').text('simpleminer --pool-addr=' + stats.config.poolHost + ':' + stats.config.poolPort + ' --login=address --pass x');
|
||||
|
||||
$('#emailLink').attr('href', 'mailto:' + stats.config.email).text(stats.config.email);
|
||||
}
|
||||
|
||||
function pageRouter(){
|
||||
$('.page').hide();
|
||||
$('.hot_link').parent().removeClass('active');
|
||||
var $link = $('a.hot_link[href="' + (window.location.hash || '#') + '"]');
|
||||
|
||||
$link.parent().addClass('active');
|
||||
var page = $link.data('page');
|
||||
$('#' + page).show();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand">CryptoNote Mining Pool</a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="active"><a class="hot_link" data-page="page_home" href="#"><i class="fa fa-home"></i> Home</a></li>
|
||||
<li><a class="hot_link" data-page="page_getting_started" href="#getting_started"><i class="fa fa-rocket"></i> Getting Started</a></li>
|
||||
<li><a class="hot_link" data-page="page_contact" href="#contact"><i class="fa fa-pencil"></i> Contact</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<div class="page" id="page_home">
|
||||
|
||||
<div class="stats">
|
||||
<h3>Network</h3>
|
||||
<div>Hash Rate: <span id="networkHashrate"></span></div>
|
||||
<div>Difficulty: <span id="networkDifficulty"></span></div>
|
||||
<div>Blockchain Height: <span id="blockchainHeight"></span></div>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<h3>Pool</h3>
|
||||
<div>Hash Rate: <span id="poolHashrate"></span></div>
|
||||
<div>Connected Miners: <span id="poolMiners"></span></div>
|
||||
<div>Mining Fee: <span id="poolFee"></span></div>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<h3>Your Stats</h3>
|
||||
<div>Address: <input id="yourStatsInput" type="text"> <button id="lookUp">Lookup</button></div>
|
||||
<div id="addressError"></div>
|
||||
<div id="yourAddressDisplay">Address: <span></span></div>
|
||||
<div id="yourHashrateHolder">Hash Rate: <span></span></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="page" id="page_getting_started">
|
||||
<h1>Getting Started</h1>
|
||||
<br>
|
||||
<ol>
|
||||
<li><a target="_blank" href="http://bit.ly/monerostarterpack">Download simpleminer and simplewallet</a> for your system<br><br></li>
|
||||
<li>Run simplewallet to generate your public address<br><br></li>
|
||||
<li>Run simpleminer pointed to our pool with your address:
|
||||
<br><br>
|
||||
<code id="simpleminer_code"></code>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="page" id="page_contact">
|
||||
<h1>Contact</h1>
|
||||
<p>Email pool support at <a id="emailLink" href=""></a></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
27
init.js
27
init.js
|
@ -12,16 +12,16 @@ var redis = require('redis');
|
|||
if (cluster.isWorker){
|
||||
switch(process.env.workerType){
|
||||
case 'pool':
|
||||
require('./pool.js');
|
||||
require('./lib/pool.js');
|
||||
break;
|
||||
case 'paymentProcessor':
|
||||
require('./paymentProcessor.js');
|
||||
require('./lib/paymentProcessor.js');
|
||||
break;
|
||||
case 'api':
|
||||
require('./api.js');
|
||||
require('./lib/api.js');
|
||||
break;
|
||||
case 'cli':
|
||||
require('./cli.js');
|
||||
require('./lib/cli.js');
|
||||
break
|
||||
}
|
||||
return;
|
||||
|
@ -29,7 +29,7 @@ if (cluster.isWorker){
|
|||
|
||||
var config = JSON.parse(fs.readFileSync('config.json'));
|
||||
|
||||
var logger = require('./logUtil.js')({
|
||||
var logger = require('./lib/logUtil.js')({
|
||||
logLevel: config.logLevel,
|
||||
logColors: config.logColors
|
||||
});
|
||||
|
@ -111,7 +111,12 @@ function spawnPoolWorkers(){
|
|||
}, 2000);
|
||||
}).on('message', function(msg){
|
||||
switch(msg.type){
|
||||
case 'none':
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
@ -144,7 +149,17 @@ function spawnPaymentProcessor(){
|
|||
}
|
||||
|
||||
function spawnApi(){
|
||||
if (!config.api || !config.api.enabled) return;
|
||||
|
||||
var worker = cluster.fork({
|
||||
workerType: 'api'
|
||||
});
|
||||
worker.on('exit', function(code, signal){
|
||||
logger.error(logSystem, logSubsystem, 'API', 'API died, spawning replacement...');
|
||||
setTimeout(function(){
|
||||
spawnApi();
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function spawnCli(){
|
||||
|
|
|
@ -0,0 +1,236 @@
|
|||
var fs = require('fs');
|
||||
var http = require('http');
|
||||
var url = require("url");
|
||||
|
||||
var async = require('async');
|
||||
var redis = require('redis');
|
||||
|
||||
var config = JSON.parse(fs.readFileSync('config.json'));
|
||||
|
||||
var logger = require('./logUtil.js')({
|
||||
logLevel: config.logLevel,
|
||||
logColors: config.logColors
|
||||
});
|
||||
|
||||
|
||||
function log(severity, message){
|
||||
logger[severity]('API', null, null, message);
|
||||
}
|
||||
|
||||
|
||||
var apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet);
|
||||
|
||||
var redisClient = redis.createClient(config.redis.port, config.redis.host);
|
||||
|
||||
var redisCommands = [
|
||||
['zremrangebyscore', config.coin + ':hashrate', '-inf', ''],
|
||||
['zrangebyscore', config.coin + ':hashrate', '', '+inf'],
|
||||
['hgetall', config.coin + ':stats'],
|
||||
['smembers', config.coin + ':blocksPending'],
|
||||
['smembers', config.coin + ':blocksUnlocked'],
|
||||
['smembers', config.coin + ':blocksOrphaned']
|
||||
];
|
||||
|
||||
var currentStats = "";
|
||||
|
||||
var minerStats = {};
|
||||
|
||||
var liveConnections = {};
|
||||
var addressConnections = {};
|
||||
|
||||
|
||||
|
||||
function collectStats(){
|
||||
|
||||
var windowTime = (((Date.now() / 1000) - config.api.hashrateWindow) | 0).toString();
|
||||
redisCommands[0][3] = '(' + windowTime;
|
||||
redisCommands[1][2] = windowTime;
|
||||
|
||||
|
||||
async.parallel({
|
||||
pool: function(callback){
|
||||
redisClient.multi(redisCommands).exec(function(error, replies){
|
||||
if (error){
|
||||
log('error', 'Error getting redis data ' + JSON.stringify(error));
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
|
||||
var data = {
|
||||
stats: replies[2],
|
||||
blocks: {
|
||||
pending: replies[3],
|
||||
unlocked: replies[4],
|
||||
orphaned: replies[5]
|
||||
}
|
||||
};
|
||||
|
||||
var hashrates = replies[1];
|
||||
|
||||
minerStats = {};
|
||||
|
||||
for (var i = 0; i < hashrates.length; i++){
|
||||
var hashParts = hashrates[i].split(':');
|
||||
minerStats[hashParts[1]] = (minerStats[hashParts[1]] || 0) + parseInt(hashParts[0]);
|
||||
}
|
||||
|
||||
var totalShares = 0;
|
||||
|
||||
for (var miner in minerStats){
|
||||
var shares = minerStats[miner];
|
||||
totalShares += shares;
|
||||
minerStats[miner] = getReadableHashRateString(shares / config.api.hashrateWindow);
|
||||
}
|
||||
|
||||
data.miners = Object.keys(minerStats).length;
|
||||
data.hashrate = getReadableHashRateString(totalShares / config.api.hashrateWindow);
|
||||
|
||||
callback(null, data);
|
||||
});
|
||||
},
|
||||
network: function(callback){
|
||||
apiInterfaces.rpcDaemon('getlastblockheader', {}, function(error, reply){
|
||||
if (error){
|
||||
log('error', 'Error getting daemon data ' + JSON.stringify(error));
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
var blockHeader = reply.block_header;
|
||||
callback(null, {
|
||||
difficulty: blockHeader.difficulty,
|
||||
height: blockHeader.height
|
||||
});
|
||||
});
|
||||
},
|
||||
config: function(callback){
|
||||
callback(null, {
|
||||
poolPort: config.poolPort,
|
||||
poolHost: config.poolHost,
|
||||
hashrateWindow: config.api.hashrateWindow,
|
||||
fee: config.payments.poolFee,
|
||||
email: config.email
|
||||
});
|
||||
}
|
||||
}, function(error, results){
|
||||
if (!error){
|
||||
currentStats = JSON.stringify(results);
|
||||
broadcastLiveStats();
|
||||
}
|
||||
setTimeout(collectStats, config.api.updateInterval * 1000);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function getReadableHashRateString(hashrate){
|
||||
var i = 0;
|
||||
var byteUnits = [' H', ' KH', ' MH', ' GH', ' TH', ' PH' ];
|
||||
while (hashrate > 1024){
|
||||
hashrate = hashrate / 1024;
|
||||
i++;
|
||||
}
|
||||
return hashrate.toFixed(2) + byteUnits[i];
|
||||
}
|
||||
|
||||
function broadcastLiveStats(){
|
||||
var statData = 'data: ' + currentStats + '\n\n';
|
||||
for (var uid in liveConnections){
|
||||
var res = liveConnections[uid];
|
||||
res.write(statData);
|
||||
}
|
||||
|
||||
for (var address in addressConnections){
|
||||
var res = addressConnections[address];
|
||||
var stats = minerStats[address];
|
||||
if (!stats) res.end();
|
||||
else res.write('data: ' + JSON.stringify({stats: stats}) + '\n\n');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
collectStats();
|
||||
|
||||
var server = http.createServer(function(request, response){
|
||||
|
||||
var origin = (request.headers.origin || "*");
|
||||
|
||||
|
||||
if (request.method.toUpperCase() === "OPTIONS"){
|
||||
|
||||
response.writeHead("204", "No Content", {
|
||||
"access-control-allow-origin": origin,
|
||||
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||
"access-control-allow-headers": "content-type, accept",
|
||||
"access-control-max-age": 10, // Seconds.
|
||||
"content-length": 0
|
||||
});
|
||||
|
||||
return(response.end());
|
||||
}
|
||||
|
||||
|
||||
|
||||
var urlParts = url.parse(request.url, true);
|
||||
|
||||
switch(urlParts.pathname){
|
||||
case '/stats':
|
||||
var reply = currentStats;
|
||||
response.writeHead("200", {
|
||||
'Access-Control-Allow-Origin': origin,
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': reply.length
|
||||
});
|
||||
response.end(reply);
|
||||
break;
|
||||
case '/live_stats':
|
||||
response.writeHead(200, {
|
||||
'Access-Control-Allow-Origin': origin,
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive'
|
||||
});
|
||||
response.write('\n');
|
||||
var uid = Math.random().toString();
|
||||
liveConnections[uid] = response;
|
||||
response.on("close", function() {
|
||||
delete liveConnections[uid];
|
||||
});
|
||||
break;
|
||||
case '/stats_address':
|
||||
response.writeHead(200, {
|
||||
'Access-Control-Allow-Origin': origin,
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive'
|
||||
});
|
||||
response.write('\n');
|
||||
var address = urlParts.query.address;
|
||||
var stats = minerStats[address];
|
||||
|
||||
if (!stats){
|
||||
var error = JSON.stringify({error: 'not found'});
|
||||
var statData = 'data: ' + error + '\n\n';
|
||||
response.end(statData);
|
||||
return;
|
||||
}
|
||||
response.write('data: ' + JSON.stringify({stats:stats}) + '\n\n');
|
||||
addressConnections[address] = response;
|
||||
response.on("close", function() {
|
||||
delete addressConnections[address];
|
||||
});
|
||||
break;
|
||||
default:
|
||||
response.writeHead(404, {
|
||||
'Access-Control-Allow-Origin': origin
|
||||
});
|
||||
response.end('Invalid API call');
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
server.listen(config.api.port, function(){
|
||||
log('debug', 'API listening on port ' + config.api.port);
|
||||
});
|
|
@ -16,7 +16,6 @@ var logger = require('./logUtil.js')({
|
|||
|
||||
var logSubSystem = 'Thread ' + (parseInt(process.env.forkId) + 1);
|
||||
|
||||
var instanceId = crypto.randomBytes(4);
|
||||
|
||||
function log(severity, component, message){
|
||||
logger[severity]('Pool', logSubSystem, component, message);
|
||||
|
@ -33,12 +32,16 @@ var cryptoNight = multiHashing['cryptonight'];
|
|||
|
||||
var diff1 = bignum('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 16);
|
||||
|
||||
var instanceId = crypto.randomBytes(4);
|
||||
|
||||
var validBlockTemplates = [];
|
||||
var currentBlockTemplate;
|
||||
|
||||
var connectedMiners = {};
|
||||
|
||||
/* Every 10 seconds clear out timed-out miners */
|
||||
var bannedIPs = {};
|
||||
|
||||
/* Every 10 seconds clear out timed-out miners and old bans */
|
||||
setInterval(function(){
|
||||
var now = Date.now();
|
||||
var timeout = config.minerTimeout * 1000;
|
||||
|
@ -49,8 +52,44 @@ setInterval(function(){
|
|||
delete connectedMiners[minerId];
|
||||
}
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
if (config.banning && config.banning.enabled){
|
||||
for (ip in bannedIPs){
|
||||
var banTime = bannedIPs[ip];
|
||||
if (now - banTime > config.banning.time * 1000) {
|
||||
delete bannedIPs[ip];
|
||||
log('debug', 'Ban Hammer', 'Ban dropped for ' + ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}, 30000);
|
||||
|
||||
|
||||
process.on('message', function(message) {
|
||||
switch (message.type) {
|
||||
case 'banIP':
|
||||
bannedIPs[message.ip] = Date.now();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function IsBannedIp(ip){
|
||||
if (!config.banning || !config.banning.enabled || !bannedIPs[ip]) return false;
|
||||
|
||||
var bannedTime = bannedIPs[ip];
|
||||
var bannedTimeAgo = Date.now() - bannedTime;
|
||||
var timeLeft = config.banning.time * 1000 - bannedTimeAgo;
|
||||
if (timeLeft > 0){
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
delete bannedIPs[ip];
|
||||
log('debug', 'Ban Hammer', 'Ban dropped for ' + ip);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function BlockTemplate(template){
|
||||
|
@ -95,6 +134,7 @@ function jobRefresh(callback){
|
|||
}
|
||||
|
||||
|
||||
|
||||
function processBlockTemplate(template){
|
||||
|
||||
if (currentBlockTemplate)
|
||||
|
@ -150,6 +190,9 @@ function Miner(id, login, pass, ip){
|
|||
this.heartbeat();
|
||||
this.difficulty = config.difficulty;
|
||||
this.validJobs = [];
|
||||
|
||||
this.validShares = 0;
|
||||
this.invalidShares = 0;
|
||||
}
|
||||
Miner.prototype = {
|
||||
heartbeat: function(){
|
||||
|
@ -201,6 +244,22 @@ Miner.prototype = {
|
|||
};
|
||||
|
||||
},
|
||||
checkBan: function(validShare){
|
||||
if (!config.banning || !config.banning.enabled) return;
|
||||
validShare ? this.validShares++ : this.invalidShares++;
|
||||
if (this.validShares + this.invalidShares >= config.banning.checkThreshold){
|
||||
if (this.invalidShares / this.validShares >= config.banning.invalidPercent / 100){
|
||||
log('warn', 'Ban Hammer', 'Banned ' + this.login + '@' + this.ip);
|
||||
bannedIPs[this.ip] = Date.now();
|
||||
delete connectedMiners[this.id];
|
||||
process.send({type: 'banIP', ip: this.ip});
|
||||
}
|
||||
else{
|
||||
this.invalidShares = 0;
|
||||
this.validShares = 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
retarget: function(){
|
||||
|
||||
var options = config.varDiff;
|
||||
|
@ -352,14 +411,14 @@ function handleMinerMethod(id, method, params, req, res){
|
|||
sendReply('missing login');
|
||||
return;
|
||||
}
|
||||
if (!params.pass){
|
||||
sendReply('missing pass');
|
||||
return;
|
||||
}
|
||||
if (!utils.isValidAddress(params.login)){
|
||||
sendReply('invalid address used for login');
|
||||
return;
|
||||
}
|
||||
if (IsBannedIp(req.connection.remoteAddress)){
|
||||
sendReply('your IP is banned');
|
||||
return;
|
||||
}
|
||||
var minerId = utils.uid();
|
||||
miner = new Miner(minerId, params.login, params.pass, req.connection.remoteAddress);
|
||||
connectedMiners[minerId] = miner;
|
||||
|
@ -397,7 +456,6 @@ function handleMinerMethod(id, method, params, req, res){
|
|||
return;
|
||||
}
|
||||
miner.heartbeat();
|
||||
miner.retarget();
|
||||
|
||||
var job = miner.validJobs.filter(function(job){
|
||||
return job.id === params.job_id;
|
||||
|
@ -418,10 +476,12 @@ function handleMinerMethod(id, method, params, req, res){
|
|||
}
|
||||
|
||||
var shareAccepted = processShare(miner, job, blockTemplate, params.nonce, params.result);
|
||||
miner.checkBan(shareAccepted);
|
||||
if (!shareAccepted){
|
||||
sendReply('Low difficulty share');
|
||||
return;
|
||||
}
|
||||
miner.retarget();
|
||||
sendReply(null, {status: 'OK'});
|
||||
break;
|
||||
default:
|
||||
|
@ -438,6 +498,11 @@ var getworkServer = http.createServer(function(req, res){
|
|||
req.setEncoding('utf8');
|
||||
req.on('data', function(chunk){
|
||||
data += chunk;
|
||||
if (Buffer.byteLength(data, 'utf8') > 10240){ //10KB
|
||||
data = null;
|
||||
log.warn('Server', 'Socket flooding detected and prevented from ' + req.connection.remoteAddress);
|
||||
req.connection.destroy();
|
||||
}
|
||||
});
|
||||
req.on('end', function(){
|
||||
var jsonData;
|
Loading…
Reference in New Issue