Updated coin configs

This commit is contained in:
Jerry Brady 2014-05-01 23:25:41 +00:00
commit 12946fdcf0
48 changed files with 3780 additions and 587 deletions

4
.gitignore vendored
View File

@ -1,3 +1,5 @@
node_modules/
.idea/
config.json
config.json
pool_configs/*.json
!pool_configs/litecoin_example.json

142
README.md
View File

@ -7,7 +7,7 @@ responsive user-friendly front-end website featuring mining instructions, in-dep
#### Table of Contents
* [Features](#features)
* [Attack Mitigation](##attack-mitigation)
* [Attack Mitigation](#attack-mitigation)
* [Security](#security)
* [Planned Features](#planned-features)
* [Community Support](#community--support)
@ -45,17 +45,15 @@ coins at once. The pools use clustering to load balance across multiple CPU core
* For reward/payment processing, shares are inserted into Redis (a fast NoSQL key/value store). The PROP (proportional)
reward system is used with [Redis Transactions](http://redis.io/topics/transactions) for secure and super speedy payouts.
Each and every share will be rewarded - even for rounds resulting in orphaned blocks.
There is zero risk to the pool operator. Shares from rounds resulting in orphaned blocks will be merged into share in the
current round so that each and every share will be rewarded
* This portal does not have user accounts/logins/registrations. Instead, miners simply use their coin address for stratum
authentication. A minimalistic HTML5 front-end connects to the portals statistics API to display stats from from each
pool such as connected miners, network/pool difficulty/hash rate, etc.
* Automated switching of connected miners to different pools/coins is also easily done due to the multi-pool architecture
of this software. To use this feature the switching must be controlled by your own script, such as one that calculates
coin profitability via an API such as CoinChoose.com or CoinWarz.com (or calculated locally using daemon-reported network
difficulties and exchange APIs). NOMP's regular payment processing and miner authentication which using coin address as stratum
username will obviously not work with this coin switching feature - so you must control those with your own script as well.
* Coin-switching ports using coin-networks and crypto-exchange APIs to detect profitability. Miner's connect to these ports
with their public key which NOMP uses to derive an address for any coin needed to be paid out.
#### Attack Mitigation
@ -108,9 +106,15 @@ If your pool uses NOMP let us know and we will list your website here.
* http://suchpool.pw
* http://hashfaster.com
* http://miningpoolhub.com
* http://teamdoge.com
* http://miningwith.us
* http://kryptochaos.com
* http://pool.uberpools.org
* http://uberpools.org
* http://onebtcplace.com
* http://minr.es
* http://mining.theminingpools.com
* http://www.omargpools.ca/pools.html
* http://pool.trademybit.com/
Usage
=====
@ -163,7 +167,12 @@ Explanation for each field:
/* Specifies the level of log output verbosity. Anything more severy than the level specified
will also be logged. */
"logLevel": "debug", //or "warning", "error"
/* The NOMP CLI (command-line interface) will listen for commands on this port. For example,
blocknotify messages are sent to NOMP through this. */
"cliPort": 17117,
/* By default 'forks' is set to "auto" which will spawn one process/fork/worker for each CPU
core in your system. Each of these workers will run a separate instance of your pool(s),
and the kernel will load balance miners using these forks. Optionally, the 'forks' field
@ -202,65 +211,67 @@ Explanation for each field:
"port": 6379
},
/* With this enabled, the master process listen on the configured port for messages from the
'scripts/blockNotify.js' script which your coin daemons can be configured to run when a
new block is available. When a blocknotify message is received, the master process uses
IPC (inter-process communication) to notify each thread about the message. Each thread
then sends the message to the appropriate coin pool. See "Setting up blocknotify" below to
set up your daemon to use this feature. */
"blockNotifyListener": {
"enabled": true,
"port": 8117,
"password": "test"
},
/* With this enabled, the master process will listen on the configured port for messages from
the 'scripts/coinSwitch.js' script which will trigger your proxy pools to switch to the
specified coin (non-case-sensitive). This setting is used in conjuction with the proxy
feature below. */
"coinSwitchListener": {
"enabled": false,
"port": 8118,
"password": "test"
},
/* In a proxy configuration, you can setup ports that accept miners for work based on a
specific algorithm instead of a specific coin. Miners that connect to these ports are
/* With this switching configuration, you can setup ports that accept miners for work based on
a specific algorithm instead of a specific coin. Miners that connect to these ports are
automatically switched a coin determined by the server. The default coin is the first
configured pool for each algorithm and coin switching can be triggered using the
coinSwitch.js script in the scripts folder.
cli.js script in the scripts folder.
Please note miner address authentication must be disabled when using NOMP in a proxy
configuration and that payout processing is left up to the server administrator. */
"proxy": {
"sha256": {
Miners connecting to these switching ports must use their public key in the format of
RIPEMD160(SHA256(public-key)). An address for each type of coin is derived from the miner's
public key, and payments are sent to that address. */
"switching": {
"switch1": {
"enabled": false,
"port": "3333",
"diff": 10,
"varDiff": {
"minDiff": 16, //Minimum difficulty
"maxDiff": 512, //Network difficulty will be used if it is lower than this
"targetTime": 15, //Try to get 1 share per this many seconds
"retargetTime": 90, //Check to see if we should retarget every this many seconds
"variancePercent": 30 //Allow time to very this % from target without retargeting
"algorithm": "sha256",
"ports": {
"3333": {
"diff": 10,
"varDiff": {
"minDiff": 16,
"maxDiff": 512,
"targetTime": 15,
"retargetTime": 90,
"variancePercent": 30
}
}
}
},
"scrypt": {
"switch2": {
"enabled": false,
"port": "4444",
"diff": 10,
"varDiff": {
"minDiff": 16, //Minimum difficulty
"maxDiff": 512, //Network difficulty will be used if it is lower than this
"targetTime": 15, //Try to get 1 share per this many seconds
"retargetTime": 90, //Check to see if we should retarget every this many seconds
"variancePercent": 30 //Allow time to very this % from target without retargeting
"algorithm": "scrypt",
"ports": {
"4444": {
"diff": 10,
"varDiff": {
"minDiff": 16,
"maxDiff": 512,
"targetTime": 15,
"retargetTime": 90,
"variancePercent": 30
}
}
}
},
"scrypt-n": {
"switch3": {
"enabled": false,
"port": "5555"
"algorithm": "x11",
"ports": {
"5555": {
"diff": 0.001
}
}
}
},
"profitSwitch": {
"enabled": false,
"updateInterval": 600,
"depth": 0.90,
"usePoloniex": true,
"useCryptsy": true,
"useMintpal": true
}
}
````
@ -302,8 +313,8 @@ Description of options:
job broadcast. */
"txRefreshInterval": 20000,
/* Some miner software is bugged and will consider the pool offline if it doesn't receive
anything for around a minute, so every time we broadcast jobs, set a timeout to rebroadcast
/* Some miner apps will consider the pool dead/offline if it doesn't receive anything new jobs
for around a minute, so every time we broadcast jobs, set a timeout to rebroadcast
in this many seconds unless we find a new job. Set to zero or remove to disable this. */
"jobRebroadcastTimeout": 55,
@ -400,6 +411,10 @@ Description of options:
"password": "mypass", //MySQL db password
"database": "ltc", //MySQL db database name
/* Unregistered workers can automatically be registered (added to database) on stratum
worker authentication if this is true. */
"autoCreateWorker": false,
/* For when miner's authenticate: set to "password" for both worker name and password to
be checked for in the database, set to "worker" for only work name to be checked, or
don't use this option (set to "none") for no auth checks */
@ -498,11 +513,11 @@ For more information on these configuration options see the [pool module documen
1. In `config.json` set the port and password for `blockNotifyListener`
2. In your daemon conf file set the `blocknotify` command to use:
```
node [path to scripts/blockNotify.js] [listener host]:[listener port] [listener password] [coin name in config] %s
node [path to cli.js] [coin name in config] [block hash symbol]
```
Example: inside `dogecoin.conf` add the line
```
blocknotify=node scripts/blockNotify.js 127.0.0.1:8117 mySuperSecurePassword dogecoin %s
blocknotify=node /home/nomp/scripts/cli.js blocknotify dogecoin %s
```
Alternatively, you can use a more efficient block notify script written in pure C. Build and usage instructions
@ -526,7 +541,8 @@ output from NOMP.
#### Upgrading NOMP
When updating NOMP to the latest code its important to not only `git pull` the latest from this repo, but to also update the `node-statum-pool` module and any config files that may have been changed.
When updating NOMP to the latest code its important to not only `git pull` the latest from this repo, but to also update
the `node-statum-pool` and `node-multi-hashing` modules, and any config files that may have been changed.
* Inside your NOMP directory (where the init.js script is) do `git pull` to get the latest NOMP code.
* Remove the dependenices by deleting the `node_modules` directory with `rm -r node_modules`.
* Run `npm update` to force updating/reinstalling of the dependencies.
@ -549,7 +565,7 @@ Credits
-------
* [Jerry Brady / mintyfresh68](https://github.com/bluecircle) - got coin-switching fully working and developed proxy-per-algo feature
* [Tony Dobbs](http://anthonydobbs.com) - designs for front-end and created the NOMP logo
* [LucasJones(//github.com/LucasJones) - getting p2p block notify script working
* [LucasJones](//github.com/LucasJones) - got p2p block notify working and implemented additional hashing algos
* [vekexasia](//github.com/vekexasia) - co-developer & great tester
* [TheSeven](//github.com/TheSeven) - answering an absurd amount of my questions and being a very helpful gentleman
* [UdjinM6](//github.com/UdjinM6) - helped implement fee withdrawal in payment processing

5
coins/asiccoin.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "ASICcoin",
"symbol": "ASC",
"algorithm": "sha256"
}

5
coins/battlecoin.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "Battlecoin",
"symbol": "BCX",
"algorithm": "sha256"
}

5
coins/benjamins.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "Benjamins",
"symbol": "BEN",
"algorithm": "sha256"
}

5
coins/betacoin.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "Betacoin",
"symbol": "BET",
"algorithm": "sha256"
}

5
coins/devcoin.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "Devcoin",
"symbol": "DVC",
"algorithm": "sha256"
}

View File

@ -1,7 +0,0 @@
{
"name": "Ecoin",
"symbol": "ECN",
"algorithm": "keccak",
"normalHashing": true,
"diffShift": 32
}

5
coins/emark.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "eMark",
"symbol": "DEM",
"algorithm": "sha256"
}

5
coins/fedoracoin.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "FedoraCoin",
"symbol": "TiPS",
"algorithm": "scrypt"
}

5
coins/fireflycoin.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "Fireflycoin",
"symbol": "FFC",
"algorithm": "sha256"
}

5
coins/freicoin.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "Freicoin",
"symbol": "FRC",
"algorithm": "sha256"
}

5
coins/giarcoin.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "Giarcoin",
"symbol": "GIAR",
"algorithm": "scrypt-n"
}

5
coins/globalboost.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "GlobalBoost",
"symbol": "BST",
"algorithm": "scrypt"
}

View File

@ -0,0 +1,5 @@
{
"name": "GlobalDenomination",
"symbol": "GDN",
"algorithm": "x11"
}

5
coins/guarantcoin.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "Guarantcoin",
"symbol": "GTC",
"algorithm": "scrypt-n"
}

5
coins/ixcoin.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "Ixcoin",
"symbol": "IXC",
"algorithm": "sha256"
}

5
coins/joulecoin.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "Joulecoin",
"symbol": "XJO",
"algorithm": "sha256"
}

5
coins/mazacoin.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "Mazacoin",
"symbol": "MZC",
"algorithm": "sha256"
}

5
coins/mintcoin.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "Mintcoin",
"symbol": "MINT",
"algorithm": "scrypt"
}

View File

@ -0,0 +1,5 @@
{
"name": "OpenSourcecoin",
"symbol": "OSC",
"algorithm": "sha256"
}

View File

@ -1,5 +1,6 @@
{
"name": "Quarkcoin",
"symbol": "QRK",
"algorithm": "quark"
}
"algorithm": "quark",
"mposDiffMultiplier": 256
}

5
coins/starcoin.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "Starcoin",
"symbol": "STR",
"algorithm": "scrypt"
}

5
coins/takcoin.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "Takcoin",
"symbol": "TAK",
"algorithm": "sha256"
}

5
coins/teacoin.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "Teacoin",
"symbol": "TEA",
"algorithm": "sha256"
}

5
coins/tekcoin.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "Tekcoin",
"symbol": "TEK",
"algorithm": "sha256"
}

5
coins/tigercoin.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "Tigercoin",
"symbol": "TGC",
"algorithm": "sha256"
}

View File

@ -1,6 +1,8 @@
{
"logLevel": "debug",
"cliPort": 17117,
"clustering": {
"enabled": true,
"forks": "auto"
@ -26,47 +28,47 @@
"port": 6379
},
"blockNotifyListener": {
"enabled": false,
"port": 8117,
"password": "test"
},
"coinSwitchListener": {
"enabled": false,
"host": "127.0.0.1",
"port": 8118,
"password": "test"
},
"proxy": {
"sha256": {
"switching": {
"switch1": {
"enabled": false,
"port": "3333",
"diff": 10,
"varDiff": {
"minDiff": 16,
"maxDiff": 512,
"targetTime": 15,
"retargetTime": 90,
"variancePercent": 30
"algorithm": "sha256",
"ports": {
"3333": {
"diff": 10,
"varDiff": {
"minDiff": 16,
"maxDiff": 512,
"targetTime": 15,
"retargetTime": 90,
"variancePercent": 30
}
}
}
},
"scrypt": {
"switch2": {
"enabled": false,
"port": "4444",
"diff": 10,
"varDiff": {
"minDiff": 16,
"maxDiff": 512,
"targetTime": 15,
"retargetTime": 90,
"variancePercent": 30
"algorithm": "scrypt",
"ports": {
"4444": {
"diff": 10,
"varDiff": {
"minDiff": 16,
"maxDiff": 512,
"targetTime": 15,
"retargetTime": 90,
"variancePercent": 30
}
}
}
},
"scrypt-n": {
"switch3": {
"enabled": false,
"port": "5555"
"algorithm": "x11",
"ports": {
"5555": {
"diff": 0.001
}
}
}
},

219
init.js
View File

@ -5,8 +5,7 @@ var cluster = require('cluster');
var async = require('async');
var PoolLogger = require('./libs/logUtil.js');
var BlocknotifyListener = require('./libs/blocknotifyListener.js');
var CoinswitchListener = require('./libs/coinswitchListener.js');
var CliListener = require('./libs/cliListener.js');
var RedisBlocknotifyListener = require('./libs/redisblocknotifyListener.js');
var PoolWorker = require('./libs/poolWorker.js');
var PaymentProcessor = require('./libs/paymentProcessor.js');
@ -23,6 +22,7 @@ if (!fs.existsSync('config.json')){
}
var portalConfig = JSON.parse(JSON.minify(fs.readFileSync("config.json", {encoding: 'utf8'})));
var poolConfigs;
var logger = new PoolLogger({
@ -82,18 +82,68 @@ if (cluster.isWorker){
var buildPoolConfigs = function(){
var configs = {};
var configDir = 'pool_configs/';
var poolConfigFiles = [];
/* Get filenames of pool config json files that are enabled */
fs.readdirSync(configDir).forEach(function(file){
if (!fs.existsSync(configDir + file) || path.extname(configDir + file) !== '.json') return;
var poolOptions = JSON.parse(JSON.minify(fs.readFileSync(configDir + file, {encoding: 'utf8'})));
if (!poolOptions.enabled) return;
var coinFilePath = 'coins/' + poolOptions.coin;
poolOptions.fileName = file;
poolConfigFiles.push(poolOptions);
});
/* Ensure no pool uses any of the same ports as another pool */
for (var i = 0; i < poolConfigFiles.length; i++){
var ports = Object.keys(poolConfigFiles[i].ports);
for (var f = 0; f < poolConfigFiles.length; f++){
if (f === i) continue;
var portsF = Object.keys(poolConfigFiles[f].ports);
for (var g = 0; g < portsF.length; g++){
if (ports.indexOf(portsF[g]) !== -1){
logger.error('Master', poolConfigFiles[f].fileName, 'Has same configured port of ' + portsF[g] + ' as ' + poolConfigFiles[i].fileName);
process.exit(1);
return;
}
}
if (poolConfigFiles[f].coin === poolConfigFiles[i].coin){
logger.error('Master', poolConfigFiles[f].fileName, 'Pool has same configured coin file coins/' + poolConfigFiles[f].coin + ' as ' + poolConfigFiles[i].fileName + ' pool');
process.exit(1);
return;
}
}
}
poolConfigFiles.forEach(function(poolOptions){
poolOptions.coinFileName = poolOptions.coin;
var coinFilePath = 'coins/' + poolOptions.coinFileName;
if (!fs.existsSync(coinFilePath)){
logger.error('Master', poolOptions.coin, 'could not find file: ' + coinFilePath);
logger.error('Master', poolOptions.coinFileName, 'could not find file: ' + coinFilePath);
return;
}
var coinProfile = JSON.parse(JSON.minify(fs.readFileSync(coinFilePath, {encoding: 'utf8'})));
poolOptions.coin = coinProfile;
if (poolOptions.coin.name in configs){
logger.error('Master', poolOptions.fileName, 'coins/' + poolOptions.coinFileName
+ ' has same configured coin name ' + poolOptions.coin.name + ' as coins/'
+ configs[poolOptions.coin.name].coinFileName + ' used by pool config '
+ configs[poolOptions.coin.name].fileName);
process.exit(1);
return;
}
configs[poolOptions.coin.name] = poolOptions;
if (!(coinProfile.algorithm in algos)){
@ -107,7 +157,7 @@ var buildPoolConfigs = function(){
var spawnPoolWorkers = function(portalConfig, poolConfigs){
var spawnPoolWorkers = function(){
Object.keys(poolConfigs).forEach(function(coin){
var p = poolConfigs[coin];
@ -130,6 +180,7 @@ var spawnPoolWorkers = function(portalConfig, poolConfigs){
return;
}
var serializedConfigs = JSON.stringify(poolConfigs);
var numForks = (function(){
@ -185,51 +236,111 @@ var spawnPoolWorkers = function(portalConfig, poolConfigs){
};
var startBlockListener = function(portalConfig){
//block notify options
//setup block notify here and use IPC to tell appropriate pools
var listener = new BlocknotifyListener(portalConfig.blockNotifyListener);
var startCliListener = function(){
var cliPort = portalConfig.cliPort;
var listener = new CliListener(cliPort);
listener.on('log', function(text){
logger.debug('Master', 'Blocknotify', text);
});
listener.on('hash', function(message){
logger.debug('Master', 'CLI', text);
}).on('command', function(command, params, options, reply){
var ipcMessage = {type:'blocknotify', coin: message.coin, hash: message.hash};
Object.keys(cluster.workers).forEach(function(id) {
cluster.workers[id].send(ipcMessage);
});
});
listener.start();
switch(command){
case 'blocknotify':
Object.keys(cluster.workers).forEach(function(id) {
cluster.workers[id].send({type: 'blocknotify', coin: params[0], hash: params[1]});
});
reply('Pool workers notified');
break;
case 'coinswitch':
processCoinSwitchCommand(params, options, reply);
break;
case 'reloadpool':
Object.keys(cluster.workers).forEach(function(id) {
cluster.workers[id].send({type: 'reloadpool', coin: params[0] });
});
reply('reloaded pool ' + params[0]);
break;
default:
reply('unrecognized command "' + command + '"');
break;
}
}).start();
};
//
// Receives authenticated events from coin switch listener and triggers proxy
// to swtich to a new coin.
//
var startCoinswitchListener = function(portalConfig){
var listener = new CoinswitchListener(portalConfig.coinSwitchListener);
listener.on('log', function(text){
logger.debug('Master', 'Coinswitch', text);
});
listener.on('switchcoin', function(message){
var ipcMessage = {type:'blocknotify', coin: message.coin, hash: message.hash};
Object.keys(cluster.workers).forEach(function(id) {
cluster.workers[id].send(ipcMessage);
});
var ipcMessage = {
type:'switch',
coin: message.coin
};
Object.keys(cluster.workers).forEach(function(id) {
cluster.workers[id].send(ipcMessage);
var processCoinSwitchCommand = function(params, options, reply){
var logSystem = 'CLI';
var logComponent = 'coinswitch';
var replyError = function(msg){
reply(msg);
logger.error(logSystem, logComponent, msg);
};
if (!params[0]) {
replyError('Coin name required');
return;
}
if (!params[1] && !options.algorithm){
replyError('If switch key is not provided then algorithm options must be specified');
return;
}
else if (params[1] && !portalConfig.switching[params[1]]){
replyError('Switch key not recognized: ' + params[1]);
return;
}
else if (options.algorithm && !Object.keys(portalConfig.switching).filter(function(s){
return portalConfig.switching[s].algorithm === options.algorithm;
})[0]){
replyError('No switching options contain the algorithm ' + options.algorithm);
return;
}
var messageCoin = params[0].toLowerCase();
var newCoin = Object.keys(poolConfigs).filter(function(p){
return p.toLowerCase() === messageCoin;
})[0];
if (!newCoin){
replyError('Switch message to coin that is not recognized: ' + messageCoin);
return;
}
var switchNames = [];
if (params[1]) {
switchNames.push(params[1]);
}
else{
for (var name in portalConfig.switching){
if (portalConfig.switching[name].enabled && portalConfig.switching[name].algorithm === options.algorithm)
switchNames.push(name);
}
}
switchNames.forEach(function(name){
if (poolConfigs[newCoin].coin.algorithm !== portalConfig.switching[name].algorithm){
replyError('Cannot switch a '
+ portalConfig.switching[name].algorithm
+ ' algo pool to coin ' + newCoin + ' with ' + poolConfigs[newCoin].coin.algorithm + ' algo');
return;
}
Object.keys(cluster.workers).forEach(function (id) {
cluster.workers[id].send({type: 'coinswitch', coin: newCoin, switchName: name });
});
});
listener.start();
reply('Switch message sent to pool workers');
};
var startRedisBlockListener = function(portalConfig){
var startRedisBlockListener = function(){
//block notify options
//setup block notify here and use IPC to tell appropriate pools
@ -248,7 +359,7 @@ var startRedisBlockListener = function(portalConfig){
};
var startPaymentProcessor = function(poolConfigs){
var startPaymentProcessor = function(){
var enabledForAny = false;
for (var pool in poolConfigs){
@ -276,7 +387,7 @@ var startPaymentProcessor = function(poolConfigs){
};
var startWebsite = function(portalConfig, poolConfigs){
var startWebsite = function(){
if (!portalConfig.website.enabled) return;
@ -294,10 +405,10 @@ var startWebsite = function(portalConfig, poolConfigs){
};
var startProfitSwitch = function(portalConfig, poolConfigs){
var startProfitSwitch = function(){
if (!portalConfig.profitSwitch.enabled){
logger.error('Master', 'Profit', 'Profit auto switching disabled');
if (!portalConfig.profitSwitch || !portalConfig.profitSwitch.enabled){
//logger.error('Master', 'Profit', 'Profit auto switching disabled');
return;
}
@ -318,20 +429,18 @@ var startProfitSwitch = function(portalConfig, poolConfigs){
(function init(){
var poolConfigs = buildPoolConfigs();
poolConfigs = buildPoolConfigs();
spawnPoolWorkers(portalConfig, poolConfigs);
spawnPoolWorkers();
startPaymentProcessor(poolConfigs);
startPaymentProcessor();
startBlockListener(portalConfig);
startRedisBlockListener();
startCoinswitchListener(portalConfig);
startWebsite();
startRedisBlockListener(portalConfig);
startProfitSwitch();
startWebsite(portalConfig, poolConfigs);
startProfitSwitch(portalConfig, poolConfigs);
startCliListener();
})();

View File

@ -1,69 +0,0 @@
var events = require('events');
var net = require('net');
var listener = module.exports = function listener(options){
var _this = this;
var emitLog = function(text){
_this.emit('log', text);
};
this.start = function(){
if (!options || !options.enabled){
emitLog('Blocknotify listener disabled');
return;
}
var blockNotifyServer = net.createServer(function(c) {
emitLog('Block listener has incoming connection');
var data = '';
try {
c.on('data', function (d) {
emitLog('Block listener received blocknotify data');
data += d;
if (data.slice(-1) === '\n') {
c.end();
}
});
c.on('end', function () {
emitLog('Block listener connection ended');
var message;
try{
message = JSON.parse(data);
}
catch(e){
emitLog('Block listener failed to parse message ' + data);
return;
}
if (message.password === options.password) {
_this.emit('hash', message);
}
else
emitLog('Block listener received notification with incorrect password');
});
}
catch(e){
emitLog('Block listener had an error: ' + e);
}
});
blockNotifyServer.listen(options.port, function() {
emitLog('Block notify listener server started on port ' + options.port)
});
emitLog("Block listener is enabled, starting server on port " + options.port);
}
};
listener.prototype.__proto__ = events.EventEmitter.prototype;

42
libs/cliListener.js Normal file
View File

@ -0,0 +1,42 @@
var events = require('events');
var net = require('net');
var listener = module.exports = function listener(port){
var _this = this;
var emitLog = function(text){
_this.emit('log', text);
};
this.start = function(){
net.createServer(function(c) {
var data = '';
try {
c.on('data', function (d) {
data += d;
if (data.slice(-1) === '\n') {
var message = JSON.parse(data);
_this.emit('command', message.command, message.params, message.options, function(message){
c.end(message);
});
}
});
c.on('end', function () {
});
}
catch(e){
emitLog('CLI listener failed to parse message ' + data);
}
}).listen(port, '127.0.0.1', function() {
emitLog('CLI listening on port ' + port)
});
}
};
listener.prototype.__proto__ = events.EventEmitter.prototype;

View File

@ -1,56 +0,0 @@
var events = require('events');
var net = require('net');
var listener = module.exports = function listener(options){
var _this = this;
var emitLog = function(text){
_this.emit('log', text);
};
this.start = function(){
if (!options || !options.enabled){
emitLog('Coinswitch listener disabled');
return;
}
var coinswitchServer = net.createServer(function(c) {
emitLog('Coinswitch listener has incoming connection');
var data = '';
try {
c.on('data', function (d) {
emitLog('Coinswitch listener received switch request');
data += d;
if (data.slice(-1) === '\n') {
c.end();
}
});
c.on('end', function () {
var message = JSON.parse(data);
if (message.password === options.password) {
_this.emit('switchcoin', message);
}
else
emitLog('Coinswitch listener received notification with incorrect password');
});
}
catch(e){
emitLog('Coinswitch listener failed to parse message ' + data);
}
});
coinswitchServer.listen(options.port, function() {
emitLog('Coinswitch notify listener server started on port ' + options.port)
});
emitLog("Coinswitch listener is enabled, starting server on port " + options.port);
}
};
listener.prototype.__proto__ = events.EventEmitter.prototype;

View File

@ -7,38 +7,26 @@ module.exports = function(logger, poolConfig){
var connection;
var logIdentify = 'MySQL';
var logComponent = coin;
function connect(){
connection = mysql.createConnection({
connection = mysql.createPool({
host: mposConfig.host,
port: mposConfig.port,
user: mposConfig.user,
password: mposConfig.password,
database: mposConfig.database
});
connection.connect(function(err){
if (err)
logger.error(logIdentify, logComponent, 'Could not connect to mysql database: ' + JSON.stringify(err))
else{
logger.debug(logIdentify, logComponent, 'Successful connection to MySQL database');
}
});
connection.on('error', function(err){
if(err.code === 'PROTOCOL_CONNECTION_LOST') {
logger.warning(logIdentify, logComponent, 'Lost connection to MySQL database, attempting reconnection...');
connect();
}
else{
logger.error(logIdentify, logComponent, 'Database error: ' + JSON.stringify(err))
}
});
}
connect();
this.handleAuth = function(workerName, password, authCallback){
connection.query(
'SELECT password FROM pool_worker WHERE username = LOWER(?)',
[workerName.toLowerCase()],
@ -48,8 +36,39 @@ module.exports = function(logger, poolConfig){
JSON.stringify(err));
authCallback(false);
}
else if (!result[0])
authCallback(false);
else if (!result[0]){
if(mposConfig.autoCreateWorker){
var account = workerName.split('.')[0];
connection.query(
'SELECT id,username FROM accounts WHERE username = LOWER(?)',
[account.toLowerCase()],
function(err, result){
if (err){
logger.error(logIdentify, logComponent, 'Database error when authenticating account: ' +
JSON.stringify(err));
authCallback(false);
}else if(!result[0]){
authCallback(false);
}else{
connection.query(
"INSERT INTO `pool_worker` (`account_id`, `username`, `password`) VALUES (?, ?, ?);",
[result[0].id,workerName.toLowerCase(),password],
function(err, result){
if (err){
logger.error(logIdentify, logComponent, 'Database error when insert worker: ' +
JSON.stringify(err));
authCallback(false);
}else {
authCallback(true);
}
})
}
}
);
}else{
authCallback(false);
}
}
else if (mposConfig.stratumAuth === 'worker')
authCallback(true);
else if (result[0].password === password)

View File

@ -2,7 +2,7 @@ var redis = require('redis');
var async = require('async');
var Stratum = require('stratum-pool');
var util = require('stratum-pool/lib/util.js');
module.exports = function(logger){
@ -38,8 +38,6 @@ module.exports = function(logger){
});
});
};
@ -216,6 +214,8 @@ function SetupForPool(logger, poolOptions, setupFinished){
return ['gettransaction', [r.txHash]];
});
batchRPCcommand.push(['getaccount', [poolOptions.address]]);
daemon.batchCmd(batchRPCcommand, function(error, txDetails){
if (error || !txDetails){
@ -224,7 +224,15 @@ function SetupForPool(logger, poolOptions, setupFinished){
return;
}
var addressAccount;
txDetails.forEach(function(tx, i){
if (i === txDetails.length - 1){
addressAccount = tx.result;
return;
}
var round = rounds[i];
if (tx.error && tx.error.code === -5 || round.blockHash !== tx.result.blockhash){
@ -273,7 +281,7 @@ function SetupForPool(logger, poolOptions, setupFinished){
rpc.gettransaction.amount tells us how much we get in whole coin units. Therefore,
we simply divide the two to get the magnitude. I don't know math, there is probably
a better term than 'magnitude'. Sue me or do a pull request to fix it. */
var roundMagnitude = r.reward / r.amount;
var roundMagnitude = Math.round(r.reward / r.amount);
if (!magnitude) {
magnitude = roundMagnitude;
@ -303,7 +311,7 @@ function SetupForPool(logger, poolOptions, setupFinished){
callback('Check finished - no confirmed or orphaned blocks found');
}
else{
callback(null, rounds, magnitude);
callback(null, rounds, magnitude, addressAccount);
}
});
},
@ -311,7 +319,7 @@ function SetupForPool(logger, poolOptions, setupFinished){
/* Does a batch redis call to get shares contributed to each round. Then calculates the reward
amount owned to each miner for each round. */
function(rounds, magnitude, callback){
function(rounds, magnitude, addressAccount, callback){
var shareLookups = rounds.map(function(r){
@ -356,11 +364,11 @@ function SetupForPool(logger, poolOptions, setupFinished){
var reward = round.reward * (1 - processingConfig.feePercent);
var totalShares = Object.keys(workerShares).reduce(function(p, c){
return p + parseInt(workerShares[c])
return p + parseFloat(workerShares[c])
}, 0);
for (var worker in workerShares){
var percent = parseInt(workerShares[worker]) / totalShares;
var percent = parseFloat(workerShares[worker]) / totalShares;
var workerRewardTotal = Math.floor(reward * percent);
if (!(worker in workerRewards)) workerRewards[worker] = 0;
workerRewards[worker] += workerRewardTotal;
@ -370,13 +378,13 @@ function SetupForPool(logger, poolOptions, setupFinished){
});
callback(null, rounds, magnitude, workerRewards, orphanMergeCommands);
callback(null, rounds, magnitude, workerRewards, orphanMergeCommands, addressAccount);
});
},
/* Does a batch call to redis to get worker existing balances from coin_balances*/
function(rounds, magnitude, workerRewards, orphanMergeCommands, callback){
function(rounds, magnitude, workerRewards, orphanMergeCommands, addressAccount, callback){
var workers = Object.keys(workerRewards);
@ -394,7 +402,7 @@ function SetupForPool(logger, poolOptions, setupFinished){
}
callback(null, rounds, magnitude, workerRewards, orphanMergeCommands, workerBalances);
callback(null, rounds, magnitude, workerRewards, orphanMergeCommands, workerBalances, addressAccount);
});
},
@ -406,12 +414,11 @@ function SetupForPool(logger, poolOptions, setupFinished){
when deciding the sent balance, it the difference should be -1*amount they had in db,
if not sending the balance, the differnce should be +(the amount they earned this round)
*/
function(rounds, magnitude, workerRewards, orphanMergeCommands, workerBalances, callback){
function(rounds, magnitude, workerRewards, orphanMergeCommands, workerBalances, addressAccount, callback){
//number of satoshis in a single coin unit - this can be different for coins so we calculate it :)
daemon.cmd('getbalance', [''], function(results){
daemon.cmd('getbalance', [addressAccount || ''], function(results){
var totalBalance = results[0].response * magnitude;
var toBePaid = 0;
@ -474,9 +481,9 @@ function SetupForPool(logger, poolOptions, setupFinished){
/* TODO: Need to convert all these variables into whole coin units before displaying because
humans aren't good at reading satoshi units. */
callback('Check finished - payments would wipe out minimum reserve, tried to pay out ' +
toBePaid + ' and collect ' + feeAmountToBeCollected + ' as fees' +
' but only have ' + totalBalance + '. Left over balance would be ' + balanceLeftOver +
', needs to be at least ' + minReserveSatoshis);
(toBePaid/magnitude) + ' and collect ' + (feeAmountToBeCollected/magnitude) + ' as fees' +
' but only have ' + (totalBalance/magnitude) + '. Left over balance would be ' + (balanceLeftOver/magnitude) +
', needs to be at least ' + (minReserveSatoshis/magnitude));
return;
}
@ -495,7 +502,8 @@ function SetupForPool(logger, poolOptions, setupFinished){
}
})();
movePendingCommands.push(['smove', coin + '_blocksPending', coin + destinationSet, r.serialized]);
roundsToDelete.push(coin + '_shares:round' + r.height)
if (r.category === 'generate')
roundsToDelete.push(coin + '_shares:round' + r.height)
});
var finalRedisCommands = [];
@ -522,23 +530,23 @@ function SetupForPool(logger, poolOptions, setupFinished){
finalRedisCommands.push(['bgsave']);
callback(null, magnitude, workerPayments, finalRedisCommands);
callback(null, magnitude, workerPayments, finalRedisCommands, addressAccount);
});
},
function(magnitude, workerPayments, finalRedisCommands, callback) {
function(magnitude, workerPayments, finalRedisCommands, addressAccount, callback) {
/* Save final redis cleanout commands in case something goes wrong during payments */
redisClient.set(coin + '_finalRedisCommands', JSON.stringify(finalRedisCommands), function(error, reply) {
if (error){
callback('Check finished - error with saving finalRedisCommands' + JSON.stringify(error));
return;
}
callback(null, magnitude, workerPayments, finalRedisCommands);
callback(null, magnitude, workerPayments, finalRedisCommands, addressAccount);
});
},
function(magnitude, workerPayments, finalRedisCommands, callback){
function(magnitude, workerPayments, finalRedisCommands, addressAccount, callback){
//This does the final all-or-nothing atom transaction if block deamon sent payments
var finalizeRedisTx = function(){
@ -563,14 +571,19 @@ function SetupForPool(logger, poolOptions, setupFinished){
var totalAmountUnits = 0;
for (var address in workerPayments){
var coinUnits = toPrecision(workerPayments[address] / magnitude, coinPrecision);
addressAmounts[address] = coinUnits;
var properAddress = getProperAddress(address);
if (!properAddress){
logger.error(logSystem, logComponent, 'Could not convert pubkey ' + address + ' into address');
continue;
}
addressAmounts[properAddress] = coinUnits;
totalAmountUnits += coinUnits;
}
logger.debug(logSystem, logComponent, 'Payments to be sent to: ' + JSON.stringify(addressAmounts));
processingPayments = true;
daemon.cmd('sendmany', ['', addressAmounts], function(results){
daemon.cmd('sendmany', [addressAccount || '', addressAmounts], function(results){
if (results[0].error){
callback('Check finished - error with sendmany ' + JSON.stringify(results[0].error));
@ -591,7 +604,7 @@ function SetupForPool(logger, poolOptions, setupFinished){
}
var feeAmountUnits = parseFloat((totalAmountUnits / (1 - processingConfig.feePercent) * processingConfig.feePercent).toFixed(coinPrecision));
var poolFees = feeAmountUnits - results[0].response.fee;
daemon.cmd('move', ['', processingConfig.feeCollectAccount, poolFees], function(results){
daemon.cmd('move', [addressAccount || '', processingConfig.feeCollectAccount, poolFees], function(results){
if (results[0].error){
callback('Check finished - error with move ' + JSON.stringify(results[0].error));
return;
@ -619,6 +632,13 @@ function SetupForPool(logger, poolOptions, setupFinished){
};
var getProperAddress = function(address){
if (address.length === 40){
return util.addressFromEx(poolOptions.address, address);
}
else return address;
};
var withdrawalProfit = function(){
if (!processingConfig.feeWithdrawalThreshold) return;
@ -641,7 +661,7 @@ function SetupForPool(logger, poolOptions, setupFinished){
daemon.cmd('sendmany', [processingConfig.feeCollectAccount, withdrawal], function(results){
if (results[0].error){
logger.debug(logSystem, logComponent, 'Profit withdrawal finished - error with sendmany '
logger.debug(logSystem, logComponent, 'Profit withdrawal of ' + withdrawalAmount + ' failed - error with sendmany '
+ JSON.stringify(results[0].error));
return;
}
@ -652,5 +672,4 @@ function SetupForPool(logger, poolOptions, setupFinished){
});
};
};
}

View File

@ -18,6 +18,8 @@ module.exports = function(logger){
var proxySwitch = {};
var redisClient = redis.createClient(portalConfig.redis.port, portalConfig.redis.host);
//Handle messages from master process sent via IPC
process.on('message', function(message) {
switch(message.type){
@ -42,26 +44,21 @@ module.exports = function(logger){
break;
// IPC message for pool switching
case 'switch':
case 'coinswitch':
var logSystem = 'Proxy';
var logComponent = 'Switch';
var logSubCat = 'Thread ' + (parseInt(forkId) + 1);
var messageCoin = message.coin.toLowerCase();
var newCoin = Object.keys(pools).filter(function(p){
return p.toLowerCase() === messageCoin;
})[0];
var switchName = message.switchName;
if (!newCoin){
logger.debug(logSystem, logComponent, logSubCat, 'Switch message to coin that is not recognized: ' + messageCoin);
break;
}
var newCoin = message.coin;
var algo = poolConfigs[newCoin].coin.algorithm;
var newPool = pools[newCoin];
var oldCoin = proxySwitch[algo].currentPool;
var oldCoin = proxySwitch[switchName].currentPool;
var oldPool = pools[oldCoin];
var proxyPort = proxySwitch[algo].port;
var proxyPorts = Object.keys(proxySwitch[switchName].ports);
if (newCoin == oldCoin) {
logger.debug(logSystem, logComponent, logSubCat, 'Switch message would have no effect - ignoring ' + newCoin);
@ -74,25 +71,23 @@ module.exports = function(logger){
oldPool.relinquishMiners(
function (miner, cback) {
// relinquish miners that are attached to one of the "Auto-switch" ports and leave the others there.
cback(miner.client.socket.localPort == proxyPort)
cback(proxyPorts.indexOf(miner.client.socket.localPort.toString()) !== -1)
},
function (clients) {
newPool.attachMiners(clients);
}
);
proxySwitch[algo].currentPool = newCoin;
proxySwitch[switchName].currentPool = newCoin;
var redisClient = redis.createClient(portalConfig.redis.port, portalConfig.redis.host)
redisClient.on('ready', function(){
redisClient.hset('proxyState', algo, newCoin, function(error, obj) {
if (error) {
logger.error(logSystem, logComponent, logSubCat, 'Redis error writing proxy config: ' + JSON.stringify(err))
}
else {
logger.debug(logSystem, logComponent, logSubCat, 'Last proxy state saved to redis for ' + algo);
}
});
redisClient.hset('proxyState', algo, newCoin, function(error, obj) {
if (error) {
logger.error(logSystem, logComponent, logSubCat, 'Redis error writing proxy config: ' + JSON.stringify(err))
}
else {
logger.debug(logSystem, logComponent, logSubCat, 'Last proxy state saved to redis for ' + algo);
}
});
}
break;
}
@ -117,9 +112,9 @@ module.exports = function(logger){
//Functions required for MPOS compatibility
if (shareProcessing && shareProcessing.mpos && shareProcessing.mpos.enabled){
var mposCompat = new MposCompatibility(logger, poolOptions)
var mposCompat = new MposCompatibility(logger, poolOptions);
handlers.auth = function(workerName, password, authCallback){
handlers.auth = function(port, workerName, password, authCallback){
mposCompat.handleAuth(workerName, password, authCallback);
};
@ -135,12 +130,32 @@ module.exports = function(logger){
//Functions required for internal payment processing
else if (shareProcessing && shareProcessing.internal && shareProcessing.internal.enabled){
var shareProcessor = new ShareProcessor(logger, poolOptions)
var shareProcessor = new ShareProcessor(logger, poolOptions);
handlers.auth = function(workerName, password, authCallback){
handlers.auth = function(port, workerName, password, authCallback){
if (shareProcessing.internal.validateWorkerAddress !== true)
authCallback(true);
else {
port = port.toString();
if (portalConfig.switching) {
for (var switchName in portalConfig.switching) {
if (portalConfig.switching[switchName].enabled && Object.keys(portalConfig.switching[switchName].ports).indexOf(port) !== -1) {
if (workerName.length === 40) {
try {
new Buffer(workerName, 'hex');
authCallback(true);
}
catch (e) {
authCallback(false);
}
}
else
authCallback(false);
return;
}
}
}
pool.daemon.cmd('validateaddress', [workerName], function(results){
var isValid = results.filter(function(r){return r.response.isvalid}).length > 0;
authCallback(isValid);
@ -153,8 +168,8 @@ module.exports = function(logger){
};
}
var authorizeFN = function (ip, workerName, password, callback) {
handlers.auth(workerName, password, function(authorized){
var authorizeFN = function (ip, port, workerName, password, callback) {
handlers.auth(port, workerName, password, function(authorized){
var authString = authorized ? 'Authorized' : 'Unauthorized ';
@ -202,9 +217,9 @@ module.exports = function(logger){
});
if (typeof(portalConfig.proxy) !== 'undefined') {
if (portalConfig.switching) {
var logSystem = 'Proxy';
var logSystem = 'Switching';
var logComponent = 'Setup';
var logSubCat = 'Thread ' + (parseInt(forkId) + 1);
@ -215,73 +230,93 @@ module.exports = function(logger){
// on the last pool it was using when reloaded or restarted
//
logger.debug(logSystem, logComponent, logSubCat, 'Loading last proxy state from redis');
var redisClient = redis.createClient(portalConfig.redis.port, portalConfig.redis.host)
redisClient.on('ready', function(){
redisClient.hgetall("proxyState", function(error, obj) {
if (error || obj == null) {
logger.debug(logSystem, logComponent, logSubCat, 'No last proxy state found in redis');
}
else {
proxyState = obj;
logger.debug(logSystem, logComponent, logSubCat, 'Last proxy state loaded from redis');
}
//
// Setup proxySwitch object to control proxy operations from configuration and any restored
// state. Each algorithm has a listening port, current coin name, and an active pool to
// which traffic is directed when activated in the config.
//
// In addition, the proxy config also takes diff and varDiff parmeters the override the
// defaults for the standard config of the coin.
//
Object.keys(portalConfig.proxy).forEach(function(algorithm) {
if (portalConfig.proxy[algorithm].enabled === true) {
var initalPool = proxyState.hasOwnProperty(algorithm) ? proxyState[algorithm] : _this.getFirstPoolForAlgorithm(algorithm);
proxySwitch[algorithm] = {
port: portalConfig.proxy[algorithm].port,
currentPool: initalPool,
proxy: {}
};
// Copy diff and vardiff configuation into pools that match our algorithm so the stratum server can pick them up
//
// Note: This seems a bit wonky and brittle - better if proxy just used the diff config of the port it was
// routed into instead.
//
if (portalConfig.proxy[algorithm].hasOwnProperty('varDiff')) {
proxySwitch[algorithm].varDiff = new Stratum.varDiff(proxySwitch[algorithm].port, portalConfig.proxy[algorithm].varDiff);
proxySwitch[algorithm].diff = portalConfig.proxy[algorithm].diff;
}
Object.keys(pools).forEach(function (coinName) {
var a = poolConfigs[coinName].coin.algorithm;
var p = pools[coinName];
if (a === algorithm) {
p.setVarDiff(proxySwitch[algorithm].port, proxySwitch[algorithm].varDiff);
/*redisClient.on('error', function(err){
logger.debug(logSystem, logComponent, logSubCat, 'Pool configuration failed: ' + err);
});*/
redisClient.hgetall("proxyState", function(error, obj) {
if (error || obj == null) {
//logger.debug(logSystem, logComponent, logSubCat, 'No last proxy state found in redis');
}
else {
proxyState = obj;
logger.debug(logSystem, logComponent, logSubCat, 'Last proxy state loaded from redis');
}
//
// Setup proxySwitch object to control proxy operations from configuration and any restored
// state. Each algorithm has a listening port, current coin name, and an active pool to
// which traffic is directed when activated in the config.
//
// In addition, the proxy config also takes diff and varDiff parmeters the override the
// defaults for the standard config of the coin.
//
Object.keys(portalConfig.switching).forEach(function(switchName) {
var algorithm = portalConfig.switching[switchName].algorithm;
if (portalConfig.switching[switchName].enabled === true) {
var initalPool = proxyState.hasOwnProperty(algorithm) ? proxyState[algorithm] : _this.getFirstPoolForAlgorithm(algorithm);
proxySwitch[switchName] = {
algorithm: algorithm,
ports: portalConfig.switching[switchName].ports,
currentPool: initalPool,
servers: []
};
// Copy diff and vardiff configuation into pools that match our algorithm so the stratum server can pick them up
//
// Note: This seems a bit wonky and brittle - better if proxy just used the diff config of the port it was
// routed into instead.
//
/*if (portalConfig.proxy[algorithm].hasOwnProperty('varDiff')) {
proxySwitch[algorithm].varDiff = new Stratum.varDiff(proxySwitch[algorithm].port, portalConfig.proxy[algorithm].varDiff);
proxySwitch[algorithm].diff = portalConfig.proxy[algorithm].diff;
}*/
Object.keys(pools).forEach(function (coinName) {
var p = pools[coinName];
if (poolConfigs[coinName].coin.algorithm === algorithm) {
for (var port in portalConfig.switching[switchName].ports) {
if (portalConfig.switching[switchName].ports[port].varDiff)
p.setVarDiff(port, portalConfig.switching[switchName].ports[port].varDiff);
}
});
}
});
proxySwitch[algorithm].proxy = net.createServer(function(socket) {
var currentPool = proxySwitch[algorithm].currentPool;
var logSubCat = 'Thread ' + (parseInt(forkId) + 1);
logger.debug(logSystem, 'Connect', logSubCat, 'Proxy connect from ' + socket.remoteAddress + ' on ' + proxySwitch[algorithm].port
+ ' routing to ' + currentPool);
Object.keys(proxySwitch[switchName].ports).forEach(function(port){
var f = net.createServer(function(socket) {
var currentPool = proxySwitch[switchName].currentPool;
logger.debug(logSystem, 'Connect', logSubCat, 'Connection to '
+ switchName + ' from '
+ socket.remoteAddress + ' on '
+ port + ' routing to ' + currentPool);
pools[currentPool].getStratumServer().handleNewClient(socket);
}).listen(parseInt(proxySwitch[algorithm].port), function() {
logger.debug(logSystem, logComponent, logSubCat, 'Proxy listening for ' + algorithm + ' on port ' + proxySwitch[algorithm].port
+ ' into ' + proxySwitch[algorithm].currentPool);
}).listen(parseInt(port), function() {
logger.debug(logSystem, logComponent, logSubCat, 'Switching "' + switchName
+ '" listening for ' + algorithm
+ ' on port ' + port
+ ' into ' + proxySwitch[switchName].currentPool);
});
}
else {
logger.debug(logSystem, logComponent, logSubCat, 'Proxy pool for ' + algorithm + ' disabled.');
}
});
proxySwitch[switchName].servers.push(f);
});
}
else {
//logger.debug(logSystem, logComponent, logSubCat, 'Proxy pool for ' + algorithm + ' disabled.');
}
});
}).on('error', function(err){
logger.debug(logSystem, logComponent, logSubCat, 'Pool configuration failed: ' + err);
});
}

View File

@ -420,31 +420,28 @@ module.exports = function(logger){
};
this.getDaemonInfoForCoin = function(symbol, cfg, callback){
var daemon = new Stratum.daemon.interface([cfg]);
daemon.once('online', function(){
daemon.cmd('getblocktemplate', [{"capabilities": [ "coinbasetxn", "workid", "coinbase/append" ]}], function(result){
if (result[0].error != null){
logger.error(logSystem, symbol, 'Error while reading daemon info: ' + JSON.stringify(result[0]));
callback(null); // fail gracefully for each coin
return;
}
var coinStatus = profitStatus[symbolToAlgorithmMap[symbol]][symbol];
var response = result[0].response;
// some shitcoins dont provide target, only bits, so we need to deal with both
var target = response.target ? bignum(response.target, 16) : util.bignumFromBitsHex(response.bits);
coinStatus.difficulty = parseFloat((diff1.toNumber() / target.toNumber()).toFixed(9));
logger.debug(logSystem, symbol, 'difficulty is ' + coinStatus.difficulty);
coinStatus.reward = new Number(response.coinbasevalue / 100000000);
callback(null);
});
}).once('connectionFailed', function(error){
logger.error(logSystem, symbol, JSON.stringify(error));
callback(null); // fail gracefully for each coin
}).on('error', function(error){
daemon.on('error', function(error){
logger.error(logSystem, symbol, JSON.stringify(error));
callback(null); // fail gracefully for each coin
}).init();
daemon.cmd('getblocktemplate', [{"capabilities": [ "coinbasetxn", "workid", "coinbase/append" ]}], function(result) {
if (result[0].error != null) {
logger.error(logSystem, symbol, 'Error while reading daemon info: ' + JSON.stringify(result[0]));
callback(null); // fail gracefully for each coin
return;
}
var coinStatus = profitStatus[symbolToAlgorithmMap[symbol]][symbol];
var response = result[0].response;
// some shitcoins dont provide target, only bits, so we need to deal with both
var target = response.target ? bignum(response.target, 16) : util.bignumFromBitsHex(response.bits);
coinStatus.difficulty = parseFloat((diff1 / target.toNumber()).toFixed(9));
logger.debug(logSystem, symbol, 'difficulty is ' + coinStatus.difficulty);
coinStatus.reward = response.coinbasevalue / 100000000;
callback(null);
});
};
@ -453,8 +450,8 @@ module.exports = function(logger){
Object.keys(profitStatus).forEach(function(algo){
Object.keys(profitStatus[algo]).forEach(function(symbol){
var coinStatus = profitStatus[symbolToAlgorithmMap[symbol]][symbol];
coinStatus.blocksPerMhPerHour = new Number(86400 / ((coinStatus.difficulty * Math.pow(2,32)) / (1 * 1000 * 1000)));
coinStatus.coinsPerMhPerHour = new Number(coinStatus.reward * coinStatus.blocksPerMhPerHour);
coinStatus.blocksPerMhPerHour = 86400 / ((coinStatus.difficulty * Math.pow(2,32)) / (1 * 1000 * 1000));
coinStatus.coinsPerMhPerHour = coinStatus.reward * coinStatus.blocksPerMhPerHour;
});
});
callback(null);
@ -467,7 +464,7 @@ module.exports = function(logger){
var bestExchange;
var bestCoin;
var bestBtcPerMhPerHour = new Number(0);
var bestBtcPerMhPerHour = 0;
Object.keys(profitStatus[algo]).forEach(function(symbol) {
var coinStatus = profitStatus[algo][symbol];
@ -475,7 +472,7 @@ module.exports = function(logger){
Object.keys(coinStatus.exchangeInfo).forEach(function(exchange){
var exchangeData = coinStatus.exchangeInfo[exchange];
if (exchangeData.hasOwnProperty('BTC') && exchangeData['BTC'].hasOwnProperty('weightedBid')){
var btcPerMhPerHour = new Number(exchangeData['BTC'].weightedBid * coinStatus.coinsPerMhPerHour);
var btcPerMhPerHour = exchangeData['BTC'].weightedBid * coinStatus.coinsPerMhPerHour;
if (btcPerMhPerHour > bestBtcPerMhPerHour){
bestBtcPerMhPerHour = btcPerMhPerHour;
bestExchange = exchange;
@ -485,7 +482,7 @@ module.exports = function(logger){
logger.debug(logSystem, 'CALC', 'BTC/' + symbol + ' on ' + exchange + ' with ' + coinStatus.btcPerMhPerHour.toFixed(8) + ' BTC/day per Mh/s');
}
if (exchangeData.hasOwnProperty('LTC') && exchangeData['LTC'].hasOwnProperty('weightedBid')){
var btcPerMhPerHour = new Number((exchangeData['LTC'].weightedBid * coinStatus.coinsPerMhPerHour) * exchangeData['LTC'].ltcToBtc);
var btcPerMhPerHour = (exchangeData['LTC'].weightedBid * coinStatus.coinsPerMhPerHour) * exchangeData['LTC'].ltcToBtc;
if (btcPerMhPerHour > bestBtcPerMhPerHour){
bestBtcPerMhPerHour = btcPerMhPerHour;
bestExchange = exchange;
@ -497,14 +494,21 @@ module.exports = function(logger){
});
});
logger.debug(logSystem, 'RESULT', 'Best coin for ' + algo + ' is ' + bestCoin + ' on ' + bestExchange + ' with ' + bestBtcPerMhPerHour.toFixed(8) + ' BTC/day per Mh/s');
if (portalConfig.coinSwitchListener.enabled){
var client = net.connect(portalConfig.coinSwitchListener.port, portalConfig.coinSwitchListener.host, function () {
client.write(JSON.stringify({
password: portalConfig.coinSwitchListener.password,
coin: bestCoin
}) + '\n');
});
}
var client = net.connect(portalConfig.cliPort, function () {
client.write(JSON.stringify({
command: 'coinswitch',
params: [bestCoin],
options: {algorithm: algo}
}) + '\n');
}).on('error', function(error){
if (error.code === 'ECONNREFUSED')
logger.error(logSystem, 'CLI', 'Could not connect to NOMP instance on port ' + portalConfig.cliPort);
else
logger.error(logSystem, 'CLI', 'Socket error ' + JSON.stringify(error));
});
});
};

View File

@ -148,7 +148,11 @@ module.exports = function(logger, portalConfig, poolConfigs){
symbol: poolConfigs[coinName].coin.symbol.toUpperCase(),
algorithm: poolConfigs[coinName].coin.algorithm,
hashrates: replies[i + 1],
poolStats: replies[i + 2] != null ? replies[i + 2] : { validShares: 0, validBlocks: 0, invalidShares: 0 },
poolStats: {
validShares: replies[i + 2] ? (replies[i + 2].validShares || 0) : 0,
validBlocks: replies[i + 2] ? (replies[i + 2].validBlocks || 0) : 0,
invalidShares: replies[i + 2] ? (replies[i + 2].invalidShares || 0) : 0
},
blocks: {
pending: replies[i + 3],
confirmed: replies[i + 4],
@ -191,9 +195,10 @@ module.exports = function(logger, portalConfig, poolConfigs){
else
coinStats.workers[worker] = workerShares;
});
var shareMultiplier = algos[coinStats.algorithm].multiplier || 0;
var hashratePre = shareMultiplier * coinStats.shares / portalConfig.website.stats.hashrateWindow;
coinStats.hashrate = hashratePre | 0;
var shareMultiplier = Math.pow(2, 32) / algos[coinStats.algorithm].multiplier;
coinStats.hashrate = shareMultiplier * coinStats.shares / portalConfig.website.stats.hashrateWindow;
coinStats.workerCount = Object.keys(coinStats.workers).length;
portalStats.global.workers += coinStats.workerCount;

View File

@ -3,18 +3,24 @@ var fs = require('fs');
var path = require('path');
var async = require('async');
var watch = require('node-watch');
var redis = require('redis');
var dot = require('dot');
var express = require('express');
var bodyParser = require('body-parser');
var compress = require('compression');
var watch = require('node-watch');
var Stratum = require('stratum-pool');
var util = require('stratum-pool/lib/util.js');
var api = require('./api.js');
module.exports = function(logger){
dot.templateSettings.strip = false;
var portalConfig = JSON.parse(process.env.portalConfig);
var poolConfigs = JSON.parse(process.env.pools);
@ -31,8 +37,10 @@ module.exports = function(logger){
'home.html': '',
'getting_started.html': 'getting_started',
'stats.html': 'stats',
'tbs.html': 'tbs',
'api.html': 'api',
'admin.html': 'admin'
'admin.html': 'admin',
'mining_key.html': 'mining_key'
};
var pageTemplates = {};
@ -40,6 +48,9 @@ module.exports = function(logger){
var pageProcessed = {};
var indexesProcessed = {};
var keyScriptTemplate = '';
var keyScriptProcessed = '';
var processTemplates = function(){
@ -112,6 +123,87 @@ module.exports = function(logger){
setInterval(buildUpdatedWebsite, websiteConfig.stats.updateInterval * 1000);
var buildKeyScriptPage = function(){
async.waterfall([
function(callback){
var client = redis.createClient(portalConfig.redis.port, portalConfig.redis.host);
client.hgetall('coinVersionBytes', function(err, coinBytes){
if (err){
client.quit();
return callback('Failed grabbing coin version bytes from redis ' + JSON.stringify(err));
}
callback(null, client, coinBytes || {});
});
},
function (client, coinBytes, callback){
var enabledCoins = Object.keys(poolConfigs).map(function(c){return c.toLowerCase()});
var missingCoins = [];
enabledCoins.forEach(function(c){
if (!(c in coinBytes))
missingCoins.push(c);
});
callback(null, client, coinBytes, missingCoins);
},
function(client, coinBytes, missingCoins, callback){
var coinsForRedis = {};
async.each(missingCoins, function(c, cback){
var coinInfo = (function(){
for (var pName in poolConfigs){
if (pName.toLowerCase() === c)
return {
daemon: poolConfigs[pName].shareProcessing.internal.daemon,
address: poolConfigs[pName].address
}
}
})();
var daemon = new Stratum.daemon.interface([coinInfo.daemon]);
daemon.cmd('dumpprivkey', [coinInfo.address], function(result){
if (result[0].error){
logger.error(logSystem, 'daemon', 'Could not dumpprivkey for ' + c + ' ' + JSON.stringify(result[0].error));
cback();
return;
}
var vBytePub = util.getVersionByte(coinInfo.address)[0];
var vBytePriv = util.getVersionByte(result[0].response)[0];
coinBytes[c] = vBytePub.toString() + ',' + vBytePriv.toString();
coinsForRedis[c] = coinBytes[c];
cback();
});
}, function(err){
callback(null, client, coinBytes, coinsForRedis);
});
},
function(client, coinBytes, coinsForRedis, callback){
if (Object.keys(coinsForRedis).length > 0){
client.hmset('coinVersionBytes', coinsForRedis, function(err){
if (err)
logger.error(logSystem, 'Init', 'Failed inserting coin byte version into redis ' + JSON.stringify(err));
client.quit();
});
}
else{
client.quit();
}
callback(null, coinBytes);
}
], function(err, coinBytes){
if (err){
logger.error(logSystem, 'Init', err);
return;
}
try{
keyScriptTemplate = dot.template(fs.readFileSync('website/key.html', {encoding: 'utf8'}));
keyScriptProcessed = keyScriptTemplate({coins: coinBytes});
}
catch(e){
logger.error(logSystem, 'Init', 'Failed to read key.html file');
}
});
};
buildKeyScriptPage();
var getPage = function(pageId){
if (pageId in pageProcessed){
@ -146,6 +238,10 @@ module.exports = function(logger){
next();
});
app.get('/key.html', function(reg, res, next){
res.end(keyScriptProcessed);
});
app.get('/:page', route);
app.get('/', route);
@ -181,4 +277,4 @@ module.exports = function(logger){
});
};
};

View File

@ -1,6 +1,6 @@
{
"name": "node-open-mining-portal",
"version": "0.0.3",
"version": "0.0.4",
"description": "An extremely efficient, highly scalable, all-in-one, easy to setup cryptocurrency mining pool",
"keywords": [
"stratum",

View File

@ -1,34 +0,0 @@
/*
This script should be hooked to the coin daemon as follow:
litecoind -blocknotify="node /path/to/this/script/blockNotify.js 127.0.0.1:8117 password litecoin %s"
The above will send tell litecoin to launch this script with those parameters every time a block is found.
This script will then send the blockhash along with other information to a listening tcp socket
*/
var net = require('net');
var config = process.argv[2];
var parts = config.split(':');
var host = parts[0];
var port = parts[1];
var password = process.argv[3];
var coin = process.argv[4];
var blockHash = process.argv[5];
var client = net.connect(port, host, function () {
console.log('client connected');
client.write(JSON.stringify({
password: password,
coin: coin,
hash: blockHash
}) + '\n');
});
client.on('data', function (data) {
console.log(data.toString());
//client.end();
});
client.on('end', function () {
console.log('client disconnected');
//process.exit();
});

View File

@ -16,76 +16,69 @@ Simple lightweight & fast - a more efficient block notify script in pure C.
(may also work as coin switch)
Platforms : Linux,BSD,Solaris (mostly OS independent)
Platforms : Linux, BSD, Solaris (mostly OS independent)
Build with:
gcc blocknotify.c -o blocknotify
Usage in daemon coin.conf
blocknotify="/bin/blocknotify 127.0.0.1:8117 mySuperSecurePassword dogecoin %s"
Example usage in daemon coin.conf using default NOMP CLI port of 17117
blocknotify="/bin/blocknotify 127.0.0.1:17117 dogecoin %s"
*NOTE* If you use "localhost" as hostname you may get a "13" error (socket / connect / send may consider "localhost" as a broadcast address)
// {"password":"notepas","coin":"Xcoin","hash":"d2191a8b644c9cd903439edf1d89ee060e196b3e116e0d48a3f11e5e3987a03b"}
// simplest connect + send json string to server
# $Id: blocknotify.c,v 0.1 2014/04/07 22:38:09 sysman Exp $
*/
int main(int argc, char **argv)
{
int sockfd,n;
struct sockaddr_in servaddr,cliaddr;
char sendline[1000];
char recvline[1000];
char host[200];
char *p,*arg,*errptr;
int port;
int sockfd,n;
struct sockaddr_in servaddr, cliaddr;
char sendline[1000];
char recvline[1000];
char host[200];
char *p, *arg, *errptr;
int port;
if (argc < 4)
{
// print help
printf("NOMP pool block notify\n usage: <host:port> <password> <coin> <block>\n");
exit(1);
}
if (argc < 3)
{
// print help
printf("NOMP pool block notify\n usage: <host:port> <coin> <block>\n");
exit(1);
}
strncpy(host,argv[1],(sizeof(host)-1));
p=host;
strncpy(host, argv[1], (sizeof(host)-1));
p = host;
if ( (arg=strchr(p,':')) )
{
*arg='\0';
if ( (arg = strchr(p,':')) )
{
*arg = '\0';
errno=0; // reset errno
port=strtol(++arg,&errptr,10);
errno = 0; // reset errno
port = strtol(++arg, &errptr, 10);
if ( (errno != 0) || (errptr == arg) ) { fprintf(stderr, "port number fail [%s]\n",errptr); }
// if(strlen(arg) > (errptr-arg) ) also fail, but we ignore it for now
// printf("host %s:%d\n",host,port);
}
if ( (errno != 0) || (errptr == arg) )
{
fprintf(stderr, "port number fail [%s]\n", errptr);
}
// printf("pass: %s coin: %s block:[%s]\n",argv[2],argv[3],argv[4]);
snprintf(sendline,sizeof(sendline)-1,
"{\"password\":\"%s\",\"coin\":\"%s\",\"hash\":\"%s\"}\n",
argv[2], argv[3], argv[4]);
}
// printf("sendline:[%s]",sendline);
snprintf(sendline, sizeof(sendline) - 1, "{\"command\":\"blocknotify\",\"params\":[\"%s\",\"%s\"]}\n", argv[2], argv[3]);
sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr=inet_addr(host);
servaddr.sin_port=htons(port);
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr(host);
servaddr.sin_port = htons(port);
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
int result = send(sockfd,sendline,strlen(sendline),0);
close(sockfd);
int result = send(sockfd, sendline, strlen(sendline), 0);
close(sockfd);
if(result == -1) {
printf("Error sending: %i\n",errno);
if(result == -1) {
printf("Error sending: %i\n", errno);
exit(-1);
}
exit(0);
}
exit(0);
}

37
scripts/cli.js Normal file
View File

@ -0,0 +1,37 @@
var net = require('net');
var defaultPort = 17117;
var defaultHost = '127.0.0.1';
var args = process.argv.slice(2);
var params = [];
var options = {};
for(var i = 0; i < args.length; i++){
if (args[i].indexOf('-') === 0 && args[i].indexOf('=') !== -1){
var s = args[i].substr(1).split('=');
options[s[0]] = s[1];
}
else
params.push(args[i]);
}
var command = params.shift();
var client = net.connect(options.port || defaultPort, options.host || defaultHost, function () {
client.write(JSON.stringify({
command: command,
params: params,
options: options
}) + '\n');
}).on('error', function(error){
if (error.code === 'ECONNREFUSED')
console.log('Could not connect to NOMP instance at ' + defaultHost + ':' + defaultPort);
else
console.log('Socket error ' + JSON.stringify(error));
}).on('data', function(data) {
console.log(data.toString());
}).on('close', function () {
});

View File

@ -1,37 +0,0 @@
/*
This script demonstrates sending a coin switch request and can be invoked from the command line
with:
"node coinSwitch.js 127.0.0.1:8118 password %s"
where <%s> is the name of the coin proxy miners will be switched onto.
If the coin name is not configured, disabled or matches the existing proxy setting, no action
will be taken by NOMP on receipt of the message.
*/
var net = require('net');
var config = process.argv[2];
var parts = config.split(':');
var host = parts[0];
var port = parts[1];
var password = process.argv[3];
var coin = process.argv[4];
var client = net.connect(port, host, function () {
console.log('client connected');
client.write(JSON.stringify({
password: password,
coin: coin
}) + '\n');
});
client.on('data', function (data) {
console.log(data.toString());
//client.end();
});
client.on('end', function () {
console.log('client disconnected');
//process.exit();
});

View File

@ -42,7 +42,13 @@
<li class="{{? it.selected === 'stats' }}pure-menu-selected{{?}}">
<a class="hot-swapper" href="/stats">
<i class="fa fa-bar-chart-o"></i>&nbsp;
Stats
Graph Stats
</a>
</li>
<li class="{{? it.selected === 'tbs' }}pure-menu-selected{{?}}">
<a class="hot-swapper" href="/tbs">
<i class="fa fa-table"></i>&nbsp;
Tab Stats
</a>
</li>
<li class="{{? it.selected === 'api' }}pure-menu-selected{{?}}">

2798
website/key.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,10 @@
<div>
API Docs here
</div>
<ul>
<li>/stats - raw json statistic</li>
<li>/pool_stats - historical time per pool json </li>
<li>/live_stats - live stats </li>
</ul>
</div>

View File

@ -0,0 +1,25 @@
<style>
#miningKeyPage{
margin: 15px;
}
#keyFrame{
padding: 0;
border: 0;
width: 100%;
height: 750px;
display: block;
}
</style>
<div id="miningKeyPage">
<p>
This script run client-side (in your browser). For maximum security <a href="/key.html" download="key.html">download</a> the script and run it locally and
offline in a modern web browser.
</p>
<iframe id="keyFrame" src="/key.html">
</iframe>
</div>

65
website/pages/tbs.html Normal file
View File

@ -0,0 +1,65 @@
<style>
#topCharts {
padding: 18px;
}
#topCharts > div > div > svg {
display: block;
height: 280px;
}
.chartWrapper {
border: solid 1px #c7c7c7;
border-radius: 5px;
padding: 5px;
margin-bottom: 18px;
}
.chartLabel {
font-size: 1.2em;
text-align: center;
padding: 4px;
}
.chartHolder {
}
table {
width: 100%;
}
</style>
<table class="pure-table">
<thead>
<tr>
<th>Pool</th>
<th>Algo</th>
<th>Workers</th>
<th>Valid Shares</th>
<th>Invalid Shares</th>
<th>Total Blocks</th>
<th>Pending</th>
<th>Confirmed</th>
<th>Orphaned</th>
<th>Hashrate</th>
</tr>
</tr>
</thead>
{{ for(var pool in it.stats.pools) { }}
<tr class="pure-table-odd">
<td>{{=it.stats.pools[pool].name}}</td>
<td>{{=it.stats.pools[pool].algorithm}}</td>
<td>{{=Object.keys(it.stats.pools[pool].workers).length}}</td>
<td>{{=it.stats.pools[pool].poolStats.validShares}}</td>
<td>{{=it.stats.pools[pool].poolStats.invalidShares}}</td>
<td>{{=it.stats.pools[pool].poolStats.validBlocks}}</td>
<td>{{=it.stats.pools[pool].blocks.pending}}</td>
<td>{{=it.stats.pools[pool].blocks.confirmed}}</td>
<td>{{=it.stats.pools[pool].blocks.orphaned}}</td>
<td>{{=it.stats.pools[pool].hashrateString}}</td>
</tr>
{{ } }}
</table>