├── fix_daemon.sh ├── block_notify.sh ├── lib ├── payments.js ├── data.proto ├── block_fix_raw_reward.js ├── remote_comms.js ├── remoteShare.js ├── longRunner.js ├── support.js ├── worker.js └── pool_stats.js ├── coinConfig.json ├── manage_scripts ├── news.sh ├── get_block_template.js ├── block_unlock_without_pay.js ├── altblock_unlock_without_pay.js ├── cache_get.js ├── block_del.js ├── get_block_header.js ├── altblock_del.js ├── get_last_block.js ├── dump_blocks.js ├── dump_altblocks.js ├── find_missed_xmr_blocks.sh ├── cache_set.js ├── dump_cache.js ├── get_block_hash.js ├── get_block_height.js ├── find_missed_xtm_blocks.sh ├── block_lock_to_pay.js ├── fix_negative_ex_xmr_balance.js ├── altblock_pay_manually.js ├── clean_altblocks.js ├── dump_shares_all.js ├── altblock_change_stage.js ├── block_add.js ├── altblock_fix_raw_reward.js ├── altblock_recalc_distro.js ├── altblocks_pay_manually.js ├── dump_shares_port.js ├── block_add_auto.js ├── altblock_revalidate.js ├── dump_shares.js ├── altblock_add.js ├── altblock_add_auto.js ├── cache_upgrade.js ├── mdb_copy.js ├── cache_clean.js ├── user_del.js └── user_del_force.js ├── config_example.json ├── user_scripts ├── show_bans.js ├── pay_set.js ├── unban_user.js ├── email_disable.js ├── lock_pay.js ├── ban_user.js ├── pass_set.js ├── pass_reset.js ├── balance_move_force.js └── balance_move.js ├── deployment ├── upgrade_monero.bash ├── leaf.bash └── deploy.bash ├── ex_keys.example.json ├── package.json ├── .gitignore ├── init_mini.js ├── LICENSE ├── block_share_dumps ├── calc_mo_cvs_top.js └── calc_mo_cvs.js ├── init.js └── README.md /fix_daemon.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sudo service monero restart 3 | -------------------------------------------------------------------------------- /block_notify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | /bin/echo 18081 | /bin/nc -N localhost 2223 -------------------------------------------------------------------------------- /lib/payments.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | require(global.config.coin.paymentFile); -------------------------------------------------------------------------------- /coinConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "xmr": { 3 | "funcFile": "./lib/coins/xmr.js", 4 | "paymentFile": "./payment_systems/xmr.js", 5 | "sigDigits": 1000000000000, 6 | "shapeshift": "xmr_btc", 7 | "xmrTo": true, 8 | "name": "Monero", 9 | "mixIn": 4, 10 | "shortCode": "XMR" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /manage_scripts/news.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | subject=$1 4 | body=$2 5 | 6 | if [ -z "$subject" ]; then echo "Set subject as first script parameter"; exit 1; fi 7 | if [ -z "$body" ]; then echo "Set bosy as second script parameter"; exit 1; fi 8 | 9 | node cache_set.js --key=news --value='{"created": "'$(date +%s)'", "subject": "'"$subject"'", "body": "'"$body"'"}' 10 | -------------------------------------------------------------------------------- /config_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "pool_id": 0, 3 | "eth_pool_support": 1, 4 | "worker_num": 0, 5 | "bind_ip": "127.0.0.1", 6 | "hostname": "testpool.com", 7 | "db_storage_path": "CHANGEME", 8 | "verify_shares_host": null, 9 | "coin": "xmr", 10 | "mysql": { 11 | "connectionLimit": 20, 12 | "host": "127.0.0.1", 13 | "database": "pool", 14 | "user": "pool", 15 | "password": "98erhfiuehw987fh23d" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /manage_scripts/get_block_template.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | if (!argv.port) { 6 | console.error("Please specify port"); 7 | process.exit(1); 8 | } 9 | const port = argv.port; 10 | 11 | require("../init_mini.js").init(function() { 12 | global.coinFuncs.getPortBlockTemplate(port, function (body_header) { 13 | console.log("body:" + JSON.stringify(body_header)); 14 | process.exit(0); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /manage_scripts/block_unlock_without_pay.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | if (!argv.hash) { 6 | console.error("Please specify block hash to unlock it (and avoid payment)"); 7 | process.exit(1); 8 | } 9 | const hash = argv.hash; 10 | 11 | require("../init_mini.js").init(function() { 12 | global.database.unlockBlock(hash); 13 | console.log("Block on " + hash + " height un-locked! Exiting!"); 14 | process.exit(0); 15 | }); 16 | -------------------------------------------------------------------------------- /manage_scripts/altblock_unlock_without_pay.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2), { '--': true }); 4 | 5 | let hashes = []; 6 | for (const h of argv['--']) { 7 | hashes.push(h); 8 | } 9 | 10 | require("../init_mini.js").init(function() { 11 | hashes.forEach(function(hash) { 12 | global.database.unlockAltBlock(hash); 13 | console.log("Altblock with " + hash + " hash un-locked!"); 14 | }) 15 | process.exit(0); 16 | }); 17 | -------------------------------------------------------------------------------- /manage_scripts/cache_get.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | if (!argv.key) { 6 | console.error("Please specify key"); 7 | process.exit(1); 8 | } 9 | const key = argv.key; 10 | 11 | require("../init_mini.js").init(function() { 12 | let value = global.database.getCache(key); 13 | if (value !== false) { 14 | console.log(JSON.stringify(value)); 15 | process.exit(0); 16 | } else { 17 | console.error("Key is not found"); 18 | process.exit(1); 19 | } 20 | }); 21 | 22 | -------------------------------------------------------------------------------- /manage_scripts/block_del.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | if (!argv.height) { 6 | console.error("Please specify block height"); 7 | process.exit(1); 8 | } 9 | const height = argv.height; 10 | 11 | require("../init_mini.js").init(function() { 12 | let txn = global.database.env.beginTxn(); 13 | txn.del(global.database.blockDB, height); 14 | txn.commit(); 15 | console.log("Block with " + height + " height removed! Exiting!"); 16 | process.exit(0); 17 | }); 18 | -------------------------------------------------------------------------------- /manage_scripts/get_block_header.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | if (!argv.port) { 6 | console.error("Please specify port"); 7 | process.exit(1); 8 | } 9 | const port = argv.port; 10 | 11 | require("../init_mini.js").init(function() { 12 | global.coinFuncs.getPortLastBlockHeader(port, function (err_header, body_header) { 13 | console.log("err:" + JSON.stringify(err_header)); 14 | console.log("body:" + JSON.stringify(body_header)); 15 | process.exit(0); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /manage_scripts/altblock_del.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | if (!argv.timestamp) { 6 | console.error("Please specify altblock time"); 7 | process.exit(1); 8 | } 9 | const timestamp = argv.timestamp; 10 | 11 | require("../init_mini.js").init(function() { 12 | let txn = global.database.env.beginTxn(); 13 | txn.del(global.database.altblockDB, timestamp); 14 | txn.commit(); 15 | console.log("Altblock with " + timestamp + " timestamp removed! Exiting!"); 16 | process.exit(0); 17 | }); 18 | -------------------------------------------------------------------------------- /manage_scripts/get_last_block.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | if (!argv.port) { 6 | console.error("Please specify port"); 7 | process.exit(1); 8 | } 9 | const port = argv.port; 10 | 11 | require("../init_mini.js").init(function() { 12 | global.coinFuncs.getPortLastBlockHeader(port, function (err_header, body_header) { 13 | console.log("err:" + JSON.stringify(err_header)); 14 | console.log("body:" + JSON.stringify(body_header)); 15 | process.exit(0); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /user_scripts/show_bans.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const mysql = require("promise-mysql"); 3 | const async = require("async"); 4 | 5 | require("../init_mini.js").init(function() { 6 | async.waterfall([ 7 | function (callback) { 8 | global.mysql.query("SELECT * FROM bans").then(function (rows) { 9 | for (let i in rows) { 10 | const row = rows[i]; 11 | console.log(row.mining_address + ": " + row.reason); 12 | } 13 | callback(); 14 | }); 15 | }, 16 | function (callback) { 17 | console.log("Done."); 18 | process.exit(0); 19 | } 20 | ]); 21 | }); 22 | -------------------------------------------------------------------------------- /manage_scripts/dump_blocks.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | require("../init_mini.js").init(function() { 4 | let txn = global.database.env.beginTxn({readOnly: true}); 5 | 6 | let cursor = new global.database.lmdb.Cursor(txn, global.database.blockDB); 7 | for (let found = cursor.goToFirst(); found; found = cursor.goToNext()) { 8 | cursor.getCurrentBinary(function(key, data){ // jshint ignore:line 9 | let blockData = global.protos.Block.decode(data); 10 | console.log(key + ": " + JSON.stringify(blockData)) 11 | }); 12 | } 13 | cursor.close(); 14 | txn.commit(); 15 | process.exit(0); 16 | }); 17 | -------------------------------------------------------------------------------- /manage_scripts/dump_altblocks.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | require("../init_mini.js").init(function() { 4 | let txn = global.database.env.beginTxn({readOnly: true}); 5 | 6 | let cursor = new global.database.lmdb.Cursor(txn, global.database.altblockDB); 7 | for (let found = cursor.goToFirst(); found; found = cursor.goToNext()) { 8 | cursor.getCurrentBinary(function(key, data){ // jshint ignore:line 9 | let blockData = global.protos.AltBlock.decode(data); 10 | console.log(key + ": " + JSON.stringify(blockData)) 11 | }); 12 | } 13 | cursor.close(); 14 | txn.commit(); 15 | process.exit(0); 16 | }); 17 | -------------------------------------------------------------------------------- /manage_scripts/find_missed_xmr_blocks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | node dump_blocks.js >/tmp/blocks; grep '"unlocked":true,"valid":true' /tmp/blocks | sed 's,:.\+,,' >/tmp/xmr_blocks 3 | sort /tmp/xmr_blocks >/tmp/xmr_blocks2 4 | curl -X POST http://localhost:18082/json_rpc -d '{"jsonrpc":"2.0","id":"0","method":"get_transfers","params":{"coinbase": true,"in":true}}' -H 'Content-Type: application/json' | jq '.result.in[] | select(.fee == 0)' | grep height | sed 's, \+"height": \+,,' | sed 's/,//' >/tmp/wallet_xmr_blocks 5 | sort /tmp/wallet_xmr_blocks >/tmp/wallet_xmr_blocks2 6 | echo Missed XMR blocks 7 | comm -23 /tmp/wallet_xmr_blocks2 /tmp/xmr_blocks2 8 | -------------------------------------------------------------------------------- /manage_scripts/cache_set.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | if (!argv.key) { 6 | console.error("Please specify key"); 7 | process.exit(1); 8 | } 9 | const key = argv.key; 10 | 11 | if (!argv.value) { 12 | console.error("Please specify value"); 13 | process.exit(1); 14 | } 15 | const value = argv.value; 16 | 17 | require("../init_mini.js").init(function() { 18 | try { 19 | let value2 = JSON.parse(value); 20 | global.database.setCache(key, value2); 21 | process.exit(0); 22 | } catch(e) { 23 | console.error("Can't parse your value: " + value); 24 | process.exit(1); 25 | } 26 | }); 27 | 28 | -------------------------------------------------------------------------------- /manage_scripts/dump_cache.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | const user = argv.user ? argv.user : null; 5 | 6 | require("../init_mini.js").init(function() { 7 | let txn = global.database.env.beginTxn({readOnly: true}); 8 | let cursor = new global.database.lmdb.Cursor(txn, global.database.cacheDB); 9 | for (let found = cursor.goToFirst(); found; found = cursor.goToNext()) { 10 | cursor.getCurrentString(function(key, data){ // jshint ignore:line 11 | if (!user || key.includes(user)) console.log(key + ": " + data); 12 | }); 13 | } 14 | cursor.close(); 15 | txn.commit(); 16 | process.exit(0); 17 | }); 18 | -------------------------------------------------------------------------------- /manage_scripts/get_block_hash.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | if (!argv.port) { 6 | console.error("Please specify port"); 7 | process.exit(1); 8 | } 9 | const port = argv.port; 10 | 11 | if (!argv.hash) { 12 | console.error("Please specify hash"); 13 | process.exit(1); 14 | } 15 | const hash = argv.hash; 16 | 17 | require("../init_mini.js").init(function() { 18 | global.coinFuncs.getPortAnyBlockHeaderByHash(port, hash, false, function (err_header, body_header) { 19 | console.log("err:" + JSON.stringify(err_header)); 20 | console.log("body:" + JSON.stringify(body_header)); 21 | process.exit(0); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /manage_scripts/get_block_height.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | if (!argv.port) { 6 | console.error("Please specify port"); 7 | process.exit(1); 8 | } 9 | const port = argv.port; 10 | 11 | if (!argv.height) { 12 | console.error("Please specify height"); 13 | process.exit(1); 14 | } 15 | const height = argv.height; 16 | 17 | require("../init_mini.js").init(function() { 18 | global.coinFuncs.getPortBlockHeaderByID(port, height, function (err_header, body_header) { 19 | console.log("err:" + JSON.stringify(err_header)); 20 | console.log("body:" + JSON.stringify(body_header)); 21 | process.exit(0); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /manage_scripts/find_missed_xtm_blocks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | node dump_altblocks.js >/tmp/altblocks; egrep '"port":(18144|18146),' /tmp/altblocks | grep '"unlocked":true,"valid":true' | sed 's,.\+"height":,,' | sed 's/,.\+//' >/tmp/xtm_blocks 3 | sort /tmp/xtm_blocks >/tmp/xtm_blocks2 4 | curl -X POST http://$1:18145/json_rpc -d '{"jsonrpc":"2.0","id":"0","method":"GetCompletedTransactions"}' -H 'Content-Type: application/json' | jq '.result[] | select(.transaction.direction == 1 and .transaction.status == 13)' | jq | grep mined_in_block_height | sed 's, \+"mined_in_block_height": \+",,' | sed 's,",,' >/tmp/wallet_xtm_blocks 5 | sort /tmp/wallet_xtm_blocks >/tmp/wallet_xtm_blocks2 6 | echo Missed XTM blocks 7 | comm -23 /tmp/wallet_xtm_blocks2 /tmp/xtm_blocks2 8 | -------------------------------------------------------------------------------- /deployment/upgrade_monero.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "This assumes that you have a standard nodejs-pool install, and will patch and update it to the latest stable builds of Monero." 3 | sleep 15 4 | echo "Continuing install, this will prompt you for your password if you didn't enable passwordless sudo. Please do not run me as root!" 5 | cd /usr/local/src/monero &&\ 6 | sudo git reset --hard origin/master &&\ 7 | sudo git checkout master &&\ 8 | sudo git pull &&\ 9 | sudo git checkout v0.18.4.3 &&\ 10 | sudo git submodule update --force --recursive --init &&\ 11 | sudo rm -rf build &&\ 12 | sudo USE_SINGLE_BUILDDIR=1 nice make release &&\ 13 | echo "Done building the new Monero daemon! Please go ahead and reboot monero with: sudo systemctl restart monero as soon as the pool source is updated!" 14 | -------------------------------------------------------------------------------- /user_scripts/pay_set.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const mysql = require("promise-mysql"); 3 | const async = require("async"); 4 | const argv = require('minimist')(process.argv.slice(2)); 5 | 6 | if (!argv.user) { 7 | console.error("Please specify user address to set"); 8 | process.exit(1); 9 | } 10 | const user = argv.user; 11 | 12 | require("../init_mini.js").init(function() { 13 | const pay = global.support.decimalToCoin(argv.pay ? argv.pay : 0.003); 14 | async.waterfall([ 15 | function (callback) { 16 | global.mysql.query("UPDATE users SET payout_threshold=? WHERE username=?", [pay, user]).then(function (rows) { 17 | console.log("UPDATE users SET payout_threshold=" + pay + " WHERE username=" + user); 18 | callback(); 19 | }); 20 | }, 21 | function (callback) { 22 | console.log("Done."); 23 | process.exit(0); 24 | } 25 | ]); 26 | }); 27 | -------------------------------------------------------------------------------- /manage_scripts/block_lock_to_pay.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | if (!argv.height) { 6 | console.error("Please specify block height to lock again (to pay it again)"); 7 | process.exit(1); 8 | } 9 | const height = argv.height; 10 | 11 | require("../init_mini.js").init(function() { 12 | let txn = global.database.env.beginTxn(); 13 | let blockProto = txn.getBinary(global.database.blockDB, parseInt(height)); 14 | if (blockProto !== null) { 15 | let blockData = global.protos.Block.decode(blockProto); 16 | blockData.unlocked = false; 17 | txn.putBinary(global.database.blockDB, height, global.protos.Block.encode(blockData)); 18 | } 19 | txn.commit(); 20 | console.log("Block on " + height + " height re-locked! Exiting!"); 21 | process.exit(0); 22 | }); 23 | -------------------------------------------------------------------------------- /user_scripts/unban_user.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const mysql = require("promise-mysql"); 3 | const async = require("async"); 4 | const argv = require('minimist')(process.argv.slice(2)); 5 | 6 | if (!argv.user) { 7 | console.error("Please specify user address to unban"); 8 | process.exit(1); 9 | } 10 | const user = argv.user; 11 | 12 | require("../init_mini.js").init(function() { 13 | async.waterfall([ 14 | function (callback) { 15 | global.mysql.query('DELETE FROM bans WHERE mining_address = ?', [user]).then(function (rows) { 16 | callback(); 17 | }); 18 | }, 19 | function (callback) { 20 | global.mysql.query("SELECT * FROM bans").then(function (rows) { 21 | for (let i in rows) { 22 | const row = rows[i]; 23 | console.log(row.mining_address + ": " + row.reason); 24 | } 25 | callback(); 26 | }); 27 | }, 28 | function (callback) { 29 | console.log("Done. User was unbanned."); 30 | process.exit(0); 31 | } 32 | ]); 33 | }); 34 | -------------------------------------------------------------------------------- /manage_scripts/fix_negative_ex_xmr_balance.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | require("../init_mini.js").init(function() { 6 | const xmr_balance = global.database.getCache("xmr_balance"); 7 | if (xmr_balance !== false) { 8 | if (!xmr_balance.value || xmr_balance.value < 0) { 9 | console.error("Can't fix xmr_balance: " + JSON.stringify(xmr_balance)); 10 | process.exit(1); 11 | return; 12 | } 13 | const xmr_balance2 = { value: -xmr_balance.expected_increase, expected_increase: xmr_balance.expected_increase }; 14 | console.log("In 10 seconds is going to change xmr_balance from " + JSON.stringify(xmr_balance) + " into " + JSON.stringify(xmr_balance2)); 15 | setTimeout(function() { 16 | global.database.setCache("xmr_balance", xmr_balance2); 17 | console.log("Done."); 18 | process.exit(0); 19 | }, 10*1000); 20 | } else { 21 | console.error("Key xmr_balance is not found"); 22 | process.exit(1); 23 | } 24 | }); 25 | 26 | -------------------------------------------------------------------------------- /user_scripts/email_disable.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const mysql = require("promise-mysql"); 3 | const async = require("async"); 4 | const argv = require('minimist')(process.argv.slice(2)); 5 | 6 | if (!argv.user) { 7 | console.error("Please specify user address to set"); 8 | process.exit(1); 9 | } 10 | 11 | const user = argv.user; 12 | 13 | require("../init_mini.js").init(function() { 14 | async.waterfall([ 15 | function (callback) { 16 | global.mysql.query("SELECT * FROM users WHERE username = ?", [user]).then(function (rows) { 17 | if (rows.length != 1) { 18 | console.error("User password and thus email is not yet set"); 19 | process.exit(1); 20 | } 21 | callback(); 22 | }); 23 | }, 24 | function (callback) { 25 | global.mysql.query("UPDATE users SET enable_email = '0' WHERE username = ?", [user]).then(function (rows) { 26 | console.log("UPDATE users SET enable_email = '0' WHERE username = " + user); 27 | callback(); 28 | }); 29 | }, 30 | function (callback) { 31 | console.log("Done."); 32 | process.exit(0); 33 | } 34 | ]); 35 | }); 36 | -------------------------------------------------------------------------------- /ex_keys.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "CRYPTOPIA": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 3 | "CRYPTOPIA_SECRET": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=", 4 | "TRADEOGRE": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 5 | "TRADEOGRE_SECRET": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 6 | "QRYPTOS": "NNNNNN", 7 | "QRYPTOS_SECRET": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX==", 8 | "LIVECOIN": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 9 | "LIVECOIN_SECRET": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 10 | "COINEX": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 11 | "COINEX_SECRET": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 12 | "SEVENSEAS": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 13 | "SEVENSEAS_SECRET": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 14 | "XEGGEX": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 15 | "XEGGEX_SECRET": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 16 | "TXBIT": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 17 | "TXBIT_SECRET": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 18 | } -------------------------------------------------------------------------------- /user_scripts/lock_pay.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const mysql = require("promise-mysql"); 3 | const async = require("async"); 4 | const argv = require('minimist')(process.argv.slice(2)); 5 | 6 | if (!argv.user) { 7 | console.error("Please specify user address to set"); 8 | process.exit(1); 9 | } 10 | 11 | const user = argv.user; 12 | 13 | require("../init_mini.js").init(function() { 14 | async.waterfall([ 15 | function (callback) { 16 | global.mysql.query("SELECT * FROM users WHERE username = ?", [user]).then(function (rows) { 17 | if (rows.length != 1) { 18 | console.error("User password and thus email is not yet set"); 19 | process.exit(1); 20 | } 21 | callback(); 22 | }); 23 | }, 24 | function (callback) { 25 | global.mysql.query("UPDATE users SET payout_threshold_lock = '1' WHERE username = ?", [user]).then(function (rows) { 26 | console.log("UPDATE users SET payout_threshold_lock = '1' WHERE username = " + user); 27 | callback(); 28 | }); 29 | }, 30 | function (callback) { 31 | console.log("Done."); 32 | process.exit(0); 33 | } 34 | ]); 35 | }); 36 | -------------------------------------------------------------------------------- /user_scripts/ban_user.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const mysql = require("promise-mysql"); 3 | const async = require("async"); 4 | const argv = require('minimist')(process.argv.slice(2)); 5 | 6 | if (!argv.user) { 7 | console.error("Please specify user address to ban"); 8 | process.exit(1); 9 | } 10 | const user = argv.user; 11 | 12 | if (!argv.reason) { 13 | console.error("Please specify reason to ban"); 14 | process.exit(1); 15 | } 16 | const reason = argv.reason; 17 | 18 | require("../init_mini.js").init(function() { 19 | async.waterfall([ 20 | function (callback) { 21 | global.mysql.query('INSERT INTO bans (mining_address, reason) VALUES (?, ?)', [user, reason]).then(function (rows) { 22 | callback(); 23 | }); 24 | }, 25 | function (callback) { 26 | global.mysql.query("SELECT * FROM bans").then(function (rows) { 27 | for (let i in rows) { 28 | const row = rows[i]; 29 | console.log(row.mining_address + ": " + row.reason); 30 | } 31 | callback(); 32 | }); 33 | }, 34 | function (callback) { 35 | console.log("Done. User was banned."); 36 | process.exit(0); 37 | } 38 | ]); 39 | }); 40 | -------------------------------------------------------------------------------- /user_scripts/pass_set.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const mysql = require("promise-mysql"); 3 | const async = require("async"); 4 | const argv = require('minimist')(process.argv.slice(2)); 5 | 6 | if (!argv.user) { 7 | console.error("Please specify user address to set"); 8 | process.exit(1); 9 | } 10 | if (!argv.pass) { 11 | console.error("Please specify user pass to set"); 12 | process.exit(1); 13 | } 14 | const user = argv.user; 15 | const pass = argv.pass; 16 | 17 | require("../init_mini.js").init(function() { 18 | async.waterfall([ 19 | function (callback) { 20 | global.mysql.query("SELECT * FROM users WHERE username = ?", [user]).then(function (rows) { 21 | if (rows.length == 1) { 22 | console.error("Your password is already set, so can not set it again"); 23 | console.log("Found rows in users table: " + rows.length); 24 | process.exit(1); 25 | } 26 | callback(); 27 | }); 28 | }, 29 | function (callback) { 30 | global.mysql.query("INSERT INTO users (username, email, enable_email) VALUES (?, ?, 0)", [user, pass]).then(function (rows) { 31 | console.log("INSERT INTO users (username, email, enable_email) VALUES (" + user + ", " + pass + ", 0)"); 32 | callback(); 33 | }); 34 | }, 35 | function (callback) { 36 | console.log("Done."); 37 | process.exit(0); 38 | } 39 | ]); 40 | }); 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-pool", 3 | "version": "0.0.1", 4 | "description": "Improved version of Snipa22 nodejs-pool", 5 | "main": "init.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/MoneroOcean/nodejs-pool.git" 9 | }, 10 | "author": "Multiple", 11 | "license": "MIT", 12 | "dependencies": { 13 | "apicache": "1.2.1", 14 | "async": "3.2.2", 15 | "bignum": "^0.13.1", 16 | "body-parser": "^1.16.0", 17 | "circular-buffer": "1.0.2", 18 | "cluster": "0.7.7", 19 | "concat-stream": "^1.6.0", 20 | "cors": "^2.8.1", 21 | "crypto-js": "^4.2.0", 22 | "cryptoforknote-util": "git+https://github.com/MoneroOcean/node-cryptoforknote-util.git#v15.8.4", 23 | "cryptonight-hashing": "git+https://github.com/MoneroOcean/node-cryptonight-hashing.git#v29.1.1", 24 | "debug": "2.6.9", 25 | "express": "^4.17.1", 26 | "jsonwebtoken": "^9.0.2", 27 | "minimist": ">=1.2.6", 28 | "moment": "2.29.4", 29 | "mysql": "2.18.1", 30 | "node-lmdb": "git+https://github.com/Venemo/node-lmdb.git#c3135a3809da1d64ce1f0956b37b618711e33519", 31 | "promise-mysql": "3.0.0", 32 | "protocol-buffers": "5.0.0", 33 | "range": "0.0.3", 34 | "request": "^2.79.0", 35 | "request-json": "^0.1.0", 36 | "sprintf-js": "^1.0.3", 37 | "utf8": "^3.0.0", 38 | "wallet-address-validator": "0.1.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /manage_scripts/altblock_pay_manually.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | if (!argv.hash) { 6 | console.error("Please specify altblock hash"); 7 | process.exit(1); 8 | } 9 | const hash = argv.hash; 10 | 11 | if (!argv.pay) { 12 | console.error("Please specify pay value in main currency"); 13 | process.exit(1); 14 | } 15 | const pay = argv.pay; 16 | 17 | require("../init_mini.js").init(function() { 18 | let txn = global.database.env.beginTxn(); 19 | let cursor = new global.database.lmdb.Cursor(txn, global.database.altblockDB); 20 | for (let found = cursor.goToFirst(); found; found = cursor.goToNext()) { 21 | cursor.getCurrentBinary(function(key, data){ // jshint ignore:line 22 | let blockData = global.protos.AltBlock.decode(data); 23 | if (blockData.hash === hash) { 24 | console.log("Found altblock with " + blockData.hash + " hash"); 25 | blockData.pay_value = global.support.decimalToCoin(pay); 26 | blockData.unlocked = false; 27 | console.log("Put " + blockData.pay_value + " pay_value to block"); 28 | txn.putBinary(global.database.altblockDB, key, global.protos.AltBlock.encode(blockData)); 29 | txn.commit(); 30 | cursor.close(); 31 | console.log("Changed altblock"); 32 | process.exit(0); 33 | } 34 | }); 35 | } 36 | cursor.close(); 37 | txn.commit(); 38 | console.log("Not found altblock with " + hash + " hash"); 39 | process.exit(1); 40 | }); 41 | -------------------------------------------------------------------------------- /manage_scripts/clean_altblocks.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | require("../init_mini.js").init(function() { 4 | console.log("Cleaning up the alt block DB. Searching for items to delete"); 5 | let txn = global.database.env.beginTxn({readOnly: true}); 6 | let cursor = new global.database.lmdb.Cursor(txn, global.database.altblockDB); 7 | let deleted = []; 8 | let block_count = {}; 9 | for (let found = cursor.goToLast(); found; found = cursor.goToPrev()) { 10 | cursor.getCurrentBinary(function(key, data){ // jshint ignore:line 11 | let blockData = global.protos.AltBlock.decode(data); 12 | if (!(blockData.port in block_count)) block_count[blockData.port] = 0; 13 | ++ block_count[blockData.port]; 14 | if (blockData.unlocked && (block_count[blockData.port] > 20000 || Date.now() - blockData.timestamp > 3*365*24*60*60*1000)) { 15 | deleted.push(key); 16 | } 17 | }); 18 | } 19 | 20 | cursor.close(); 21 | txn.commit(); 22 | 23 | console.log("Deleting altblock items: " + deleted.length); 24 | 25 | let chunkSize = 0; 26 | txn = global.database.env.beginTxn(); 27 | deleted.forEach(function(key) { 28 | ++ chunkSize; 29 | txn.del(global.database.altblockDB, key); 30 | if (chunkSize > 500) { 31 | txn.commit(); 32 | txn = global.database.env.beginTxn(); 33 | chunkSize = 0; 34 | } 35 | }); 36 | txn.commit(); 37 | process.exit(0); 38 | }); 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 2 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 3 | 4 | # User-specific stuff: 5 | .idea 6 | 7 | ## File-based project format: 8 | *.iws 9 | 10 | ## Plugin-specific files: 11 | 12 | # IntelliJ 13 | /out/ 14 | 15 | # mpeltonen/sbt-idea plugin 16 | .idea_modules/ 17 | 18 | # JIRA plugin 19 | atlassian-ide-plugin.xml 20 | 21 | # Crashlytics plugin (for Android Studio and IntelliJ) 22 | com_crashlytics_export_strings.xml 23 | crashlytics.properties 24 | crashlytics-build.properties 25 | fabric.properties 26 | 27 | #NodeJs Ignores 28 | 29 | # Logs 30 | logs 31 | *.log 32 | npm-debug.log* 33 | 34 | # Runtime data 35 | pids 36 | *.pid 37 | *.seed 38 | *.pid.lock 39 | 40 | # Directory for instrumented libs generated by jscoverage/JSCover 41 | lib-cov 42 | 43 | # Coverage directory used by tools like istanbul 44 | coverage 45 | 46 | # nyc test coverage 47 | .nyc_output 48 | 49 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 50 | .grunt 51 | 52 | # node-waf configuration 53 | .lock-wscript 54 | 55 | # Compiled binary addons (http://nodejs.org/api/addons.html) 56 | build/Release 57 | 58 | # Dependency directories 59 | node_modules 60 | jspm_packages 61 | 62 | # Optional npm cache directory 63 | .npm 64 | 65 | # Optional eslint cache 66 | .eslintcache 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | config.json 78 | -------------------------------------------------------------------------------- /manage_scripts/dump_shares_all.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let range = require('range'); 4 | const argv = require('minimist')(process.argv.slice(2)); 5 | 6 | let depth = 10; 7 | if (argv.depth) depth = argv.depth; 8 | 9 | console.log("Dumping shares"); 10 | 11 | require("../init_mini.js").init(function() { 12 | 13 | global.coinFuncs.getLastBlockHeader(function (err, body) { 14 | if (err !== null) { 15 | console.error("Invalid block header"); 16 | process.exit(1); 17 | } 18 | let lastBlock = body.height + 1; 19 | let txn = global.database.env.beginTxn({readOnly: true}); 20 | 21 | let cursor = new global.database.lmdb.Cursor(txn, global.database.shareDB); 22 | range.range(lastBlock, lastBlock - depth, -1).forEach(function (blockID) { 23 | for (let found = (cursor.goToRange(parseInt(blockID)) === blockID); found; found = cursor.goToNextDup()) { 24 | cursor.getCurrentBinary(function(key, data){ // jshint ignore:line 25 | let shareData = global.protos.Share.decode(data); 26 | var d = new Date(shareData.timestamp); 27 | console.log(d.toString() + ": " + JSON.stringify(shareData)) 28 | }); 29 | } 30 | }); 31 | cursor.close(); 32 | txn.commit(); 33 | process.exit(0); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /init_mini.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function init(callback) { 4 | 5 | let fs = require("fs"); 6 | let mysql = require("promise-mysql"); 7 | 8 | let config = fs.readFileSync("../config.json"); 9 | let coinConfig = fs.readFileSync("../coinConfig.json"); 10 | let protobuf = require('protocol-buffers'); 11 | 12 | global.support = require("./lib/support.js")(); 13 | global.config = JSON.parse(config); 14 | global.mysql = mysql.createPool(global.config.mysql); 15 | global.protos = protobuf(fs.readFileSync('../lib/data.proto')); 16 | 17 | global.mysql.query("SELECT * FROM config").then(function (rows) { 18 | rows.forEach(function (row){ 19 | if (!global.config.hasOwnProperty(row.module)) global.config[row.module] = {}; 20 | if (global.config[row.module].hasOwnProperty(row.item)) return; 21 | switch(row.item_type){ 22 | case 'int': global.config[row.module][row.item] = parseInt(row.item_value); break; 23 | case 'bool': global.config[row.module][row.item] = (row.item_value === "true"); break; 24 | case 'string': global.config[row.module][row.item] = row.item_value; break; 25 | case 'float': global.config[row.module][row.item] = parseFloat(row.item_value); break; 26 | } 27 | }); 28 | 29 | }).then(function(){ 30 | global.config['coin'] = JSON.parse(coinConfig)[global.config.coin]; 31 | let coinInc = require(global.config.coin.funcFile); 32 | global.coinFuncs = new coinInc(); 33 | let comms = require('./lib/local_comms'); 34 | global.database = new comms(); 35 | global.database.initEnv(); 36 | 37 | }).then(function(){ 38 | callback(); 39 | }); 40 | } 41 | 42 | module.exports = { 43 | init: init 44 | }; -------------------------------------------------------------------------------- /manage_scripts/altblock_change_stage.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2), { '--': true }); 4 | 5 | if (!argv.stage) { 6 | console.error("Please specify new stage value"); 7 | process.exit(1); 8 | } 9 | const stage = argv.stage; 10 | 11 | let hashes = {}; 12 | for (const h of argv['--']) { 13 | hashes[h] = 1; 14 | } 15 | 16 | require("../init_mini.js").init(function() { 17 | let changed = 0; 18 | let txn = global.database.env.beginTxn(); 19 | let cursor = new global.database.lmdb.Cursor(txn, global.database.altblockDB); 20 | for (let found = cursor.goToFirst(); found; found = cursor.goToNext()) { 21 | cursor.getCurrentBinary(function(key, data){ // jshint ignore:line 22 | let blockData = global.protos.AltBlock.decode(data); 23 | if (blockData.hash in hashes) { 24 | console.log("Found altblock with " + blockData.hash + " hash"); 25 | blockData.pay_stage = stage; 26 | console.log("Put \"" + blockData.pay_stage + "\" stage to block"); 27 | txn.putBinary(global.database.altblockDB, key, global.protos.AltBlock.encode(blockData)); 28 | console.log("Changed altblock"); 29 | changed = 1; 30 | } 31 | }); 32 | } 33 | cursor.close(); 34 | txn.commit(); 35 | if (!changed) console.log("Not found altblocks with specified hashes"); 36 | process.exit(0); 37 | }); -------------------------------------------------------------------------------- /manage_scripts/block_add.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | if (!argv.height) { 6 | console.error("Please specify block height"); 7 | process.exit(1); 8 | } 9 | const height = argv.height; 10 | 11 | if (!argv.body) { 12 | console.error("Please specify block body"); 13 | process.exit(1); 14 | } 15 | const body = argv.body; 16 | let body2; 17 | 18 | try { body2 = JSON.parse(body); } catch(e) { 19 | console.error("Can't parse block body: " + body); 20 | process.exit(1); 21 | } 22 | 23 | require("../init_mini.js").init(function() { 24 | const body3 = { 25 | "hash": body2.hash, 26 | "difficulty": body2.difficulty, 27 | "shares": body2.shares, 28 | "timestamp": body2.timestamp, 29 | "poolType": body2.poolType, 30 | "unlocked": body2.unlocked, 31 | "valid": body2.valid, 32 | "value": body2.value 33 | }; 34 | if (typeof (body3.hash) === 'undefined' || 35 | typeof (body3.difficulty) === 'undefined' || 36 | typeof (body3.shares) === 'undefined' || 37 | typeof (body3.timestamp) === 'undefined' || 38 | typeof (body3.poolType) === 'undefined' || 39 | typeof (body3.unlocked) === 'undefined' || 40 | typeof (body3.valid) === 'undefined' || 41 | typeof (body3.value) === 'undefined') { 42 | console.error("Block body is invalid: " + JSON.stringify(body3)); 43 | process.exit(1); 44 | } 45 | const body4 = global.protos.Block.encode(body3); 46 | let txn = global.database.env.beginTxn(); 47 | txn.putBinary(global.database.blockDB, height, body4); 48 | txn.commit(); 49 | console.log("Block on " + height + " height added! Exiting!"); 50 | process.exit(0); 51 | }); 52 | -------------------------------------------------------------------------------- /user_scripts/pass_reset.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const mysql = require("promise-mysql"); 3 | const async = require("async"); 4 | const argv = require('minimist')(process.argv.slice(2)); 5 | 6 | if (!argv.user) { 7 | console.error("Please specify user address to delete"); 8 | process.exit(1); 9 | } 10 | const user = argv.user; 11 | 12 | require("../init_mini.js").init(function() { 13 | async.waterfall([ 14 | function (callback) { 15 | global.mysql.query("SELECT * FROM users WHERE username = ?", [user]).then(function (rows) { 16 | if (rows.length != 1) { 17 | console.error("Your password is not yet set. To do that you need to set password field in your miner to \":\", where is any name (without : character) and is your password (depending on miner password can be in in command line, config.json or config.txt files). Optionally you can use your email as your password if you want notifications about miner downtimes from the pool. You need to make sure you restart your miner and your miner submits at least one valid share for password to be set."); 18 | process.exit(1); 19 | } 20 | console.log("Found rows in users table: " + rows.length); 21 | callback(); 22 | }); 23 | }, 24 | function (callback) { 25 | global.mysql.query("DELETE FROM users WHERE username = ?", [user]).then(function (rows) { 26 | console.log("DELETE FROM users WHERE username = " + user); 27 | callback(); 28 | }); 29 | }, 30 | function (callback) { 31 | console.log("Done. Please do not forget to restart your miner to apply new password and set payment threshold since it was reset as well"); 32 | process.exit(0); 33 | } 34 | ]); 35 | }); 36 | -------------------------------------------------------------------------------- /manage_scripts/altblock_fix_raw_reward.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | if (!argv.hash) { 6 | console.error("Please specify altblock hash"); 7 | process.exit(1); 8 | } 9 | const hash = argv.hash; 10 | 11 | require("../init_mini.js").init(function() { 12 | let txn = global.database.env.beginTxn(); 13 | let cursor = new global.database.lmdb.Cursor(txn, global.database.altblockDB); 14 | let is_found = 0; 15 | for (let found = cursor.goToFirst(); found; found = cursor.goToNext()) { 16 | cursor.getCurrentBinary(function(key, data){ // jshint ignore:line 17 | let blockData = global.protos.AltBlock.decode(data); 18 | if (!is_found && blockData.hash === hash) { 19 | console.log("Found altblock with " + blockData.hash + " hash"); 20 | is_found = 1; 21 | global.coinFuncs.getPortAnyBlockHeaderByHash(blockData.port, argv.hash, false, function (err, body) { 22 | if (err) { 23 | cursor.close(); 24 | txn.commit(); 25 | console.error("Can't get block header"); 26 | process.exit(1); 27 | } 28 | console.log("Changing raw block reward from " + blockData.value + " to " + body.reward); 29 | blockData.value = body.reward; 30 | txn.putBinary(global.database.altblockDB, key, global.protos.AltBlock.encode(blockData)); 31 | txn.commit(); 32 | cursor.close(); 33 | console.log("Changed altblock"); 34 | process.exit(0); 35 | }); 36 | } 37 | }); 38 | } 39 | if (!is_found) { 40 | cursor.close(); 41 | txn.commit(); 42 | console.log("Not found altblock with " + hash + " hash"); 43 | process.exit(1); 44 | } 45 | }); 46 | -------------------------------------------------------------------------------- /manage_scripts/altblock_recalc_distro.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | if (!argv.hash) { 6 | console.error("Please specify altblock hash"); 7 | process.exit(1); 8 | } 9 | const hash = argv.hash; 10 | 11 | require("../init_mini.js").init(function() { 12 | let txn = global.database.env.beginTxn(); 13 | let cursor = new global.database.lmdb.Cursor(txn, global.database.altblockDB); 14 | let is_found = true; 15 | for (let found = cursor.goToFirst(); found; found = cursor.goToNext()) { 16 | cursor.getCurrentBinary(function(key, data){ // jshint ignore:line 17 | let blockData = global.protos.AltBlock.decode(data); 18 | if (blockData.hash === hash) { 19 | is_found = true; 20 | global.coinFuncs.getPortBlockHeaderByHash(blockData.port, hash, (err, body) => { 21 | if (err !== null) { 22 | console.log("Altblock with " + hash + " hash still has invalid hash for " + blockData.port + " port! Exiting!"); 23 | cursor.close(); 24 | txn.commit(); 25 | process.exit(1); 26 | } 27 | console.log("Changing alt-block pay_ready from " + blockData.pay_ready + " to false"); 28 | blockData.pay_ready = false; 29 | txn.putBinary(global.database.altblockDB, key, global.protos.AltBlock.encode(blockData)); 30 | cursor.close(); 31 | txn.commit(); 32 | console.log("Altblock with " + hash + " hash was validated! Exiting!"); 33 | process.exit(0); 34 | }); 35 | } 36 | }); 37 | } 38 | if (!is_found) { 39 | cursor.close(); 40 | txn.commit(); 41 | console.log("Not found altblock with " + hash + " hash"); 42 | process.exit(1); 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /manage_scripts/altblocks_pay_manually.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2), { '--': true }); 4 | 5 | let hashes = {}; 6 | for (const h of argv['--']) { 7 | hashes[h] = 1; 8 | } 9 | 10 | if (!argv.pay) { 11 | console.error("Please specify pay value in main currency"); 12 | process.exit(1); 13 | } 14 | const pay = argv.pay; 15 | 16 | require("../init_mini.js").init(function() { 17 | let changed = 0; 18 | let txn = global.database.env.beginTxn(); 19 | let cursor = new global.database.lmdb.Cursor(txn, global.database.altblockDB); 20 | for (let found = cursor.goToFirst(); found; found = cursor.goToNext()) { 21 | cursor.getCurrentBinary(function(key, data){ // jshint ignore:line 22 | let blockData = global.protos.AltBlock.decode(data); 23 | if (blockData.hash in hashes) { 24 | console.log("Found altblock with " + blockData.hash + " hash"); 25 | blockData.pay_value = global.support.decimalToCoin(pay); 26 | blockData.unlocked = false; 27 | console.log("Put " + blockData.pay_value + " pay_value to block"); 28 | txn.putBinary(global.database.altblockDB, key, global.protos.AltBlock.encode(blockData)); 29 | console.log("Changed altblock"); 30 | changed += 1; 31 | } 32 | }); 33 | } 34 | cursor.close(); 35 | txn.commit(); 36 | if (!changed) console.log("Not found altblocks with specified hashes"); 37 | else console.log("Changed " + changed + " blocks"); 38 | process.exit(0); 39 | }); -------------------------------------------------------------------------------- /manage_scripts/dump_shares_port.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let range = require('range'); 4 | const argv = require('minimist')(process.argv.slice(2)); 5 | 6 | if (!argv.port) { 7 | console.error("Please specify port to dump"); 8 | process.exit(1); 9 | } 10 | const port = argv.port; 11 | 12 | let depth = 10; 13 | if (argv.depth) depth = argv.depth; 14 | 15 | console.log("Dumping shares for " + port + " port"); 16 | 17 | require("../init_mini.js").init(function() { 18 | 19 | global.coinFuncs.getLastBlockHeader(function (err, body) { 20 | if (err !== null) { 21 | console.error("Invalid block header"); 22 | process.exit(1); 23 | } 24 | let lastBlock = body.height + 1; 25 | let txn = global.database.env.beginTxn({readOnly: true}); 26 | 27 | let cursor = new global.database.lmdb.Cursor(txn, global.database.shareDB); 28 | range.range(lastBlock, lastBlock - depth, -1).forEach(function (blockID) { 29 | for (let found = (cursor.goToRange(parseInt(blockID)) === blockID); found; found = cursor.goToNextDup()) { 30 | cursor.getCurrentBinary(function(key, data){ // jshint ignore:line 31 | let shareData = global.protos.Share.decode(data); 32 | if (shareData.port === port) { 33 | var d = new Date(shareData.timestamp); 34 | console.log(d.toString() + ": " + JSON.stringify(shareData)) 35 | } 36 | }); 37 | } 38 | }); 39 | cursor.close(); 40 | txn.commit(); 41 | process.exit(0); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /lib/data.proto: -------------------------------------------------------------------------------- 1 | enum POOLTYPE { 2 | PPLNS = 0; 3 | PPS = 1; 4 | PROP = 2; 5 | SOLO = 3; 6 | } 7 | 8 | enum MESSAGETYPE { 9 | SHARE = 0; 10 | BLOCK = 1; 11 | INVALIDSHARE = 2; 12 | ALTBLOCK = 3; 13 | } 14 | 15 | message WSData { 16 | required MESSAGETYPE msgType = 1; 17 | required string key = 2; 18 | required bytes msg = 3; 19 | required int32 exInt = 4; 20 | } 21 | 22 | message InvalidShare{ 23 | required string paymentAddress = 1; 24 | optional string paymentID = 2; 25 | required string identifier = 3; 26 | optional int32 count = 4; 27 | } 28 | 29 | message Share { 30 | optional int64 shares = 1; 31 | required string paymentAddress = 2; 32 | required bool foundBlock = 3; 33 | optional string paymentID = 4; 34 | required bool trustedShare = 5; 35 | required POOLTYPE poolType = 6; 36 | required int32 poolID = 7; 37 | required int64 blockDiff = 8; 38 | required int32 blockHeight = 10; 39 | required int64 timestamp = 11; 40 | required string identifier = 12; 41 | optional int32 port = 13; 42 | optional int64 shares2 = 14; 43 | optional int64 share_num = 15; 44 | optional float raw_shares = 16; 45 | } 46 | 47 | message Block { 48 | required string hash = 1; 49 | required int64 difficulty = 2; 50 | required int64 shares = 3; 51 | required int64 timestamp = 4; 52 | required POOLTYPE poolType = 5; 53 | required bool unlocked = 6; 54 | required bool valid = 7; 55 | optional int64 value = 8; 56 | optional bool pay_ready = 9; 57 | } 58 | 59 | message AltBlock { 60 | required string hash = 1; 61 | required int64 difficulty = 2; 62 | required int64 shares = 3; 63 | required int64 timestamp = 4; 64 | required POOLTYPE poolType = 5; 65 | required bool unlocked = 6; 66 | required bool valid = 7; 67 | required int32 port = 8; 68 | required int32 height = 9; 69 | required int32 anchor_height = 10; 70 | optional uint64 value = 11; 71 | optional uint64 pay_value = 12; 72 | optional string pay_stage = 13; 73 | optional string pay_status = 14; 74 | optional bool pay_ready = 15; 75 | } 76 | -------------------------------------------------------------------------------- /manage_scripts/block_add_auto.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | if (!argv.height) { 6 | console.error("Please specify block height"); 7 | process.exit(1); 8 | } 9 | const height = argv.height; 10 | 11 | require("../init_mini.js").init(function() { 12 | global.coinFuncs.getBlockHeaderByID(height, function (err, body) { 13 | if (err) { 14 | console.error("Can't get block header"); 15 | process.exit(1); 16 | } 17 | global.coinFuncs.getPortAnyBlockHeaderByHash(18081, body.hash, true, function (err, body) { 18 | if (err) { 19 | console.error("Can't get block header"); 20 | process.exit(1); 21 | } 22 | const body2 = { 23 | "hash": body.hash, 24 | "difficulty": body.difficulty, 25 | "shares": 0, 26 | "timestamp": body.timestamp * 1000, 27 | "poolType": 0, 28 | "unlocked": false, 29 | "valid": true, 30 | "value": body.reward 31 | }; 32 | const body3 = global.protos.Block.encode(body2); 33 | let txn = global.database.env.beginTxn(); 34 | let blockProto = txn.getBinary(global.database.blockDB, parseInt(height)); 35 | if (blockProto === null) { 36 | txn.putBinary(global.database.blockDB, height, body3); 37 | console.log("Block with " + height + " height added! Exiting!"); 38 | } else { 39 | console.log("Block with " + height + " height already exists! Exiting!"); 40 | } 41 | txn.commit(); 42 | process.exit(0); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /manage_scripts/altblock_revalidate.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | if (!argv.hash) { 6 | console.error("Please specify altblock hash"); 7 | process.exit(1); 8 | } 9 | const hash = argv.hash; 10 | 11 | require("../init_mini.js").init(function() { 12 | let txn = global.database.env.beginTxn(); 13 | let cursor = new global.database.lmdb.Cursor(txn, global.database.altblockDB); 14 | let is_found = true; 15 | for (let found = cursor.goToFirst(); found; found = cursor.goToNext()) { 16 | cursor.getCurrentBinary(function(key, data){ // jshint ignore:line 17 | let blockData = global.protos.AltBlock.decode(data); 18 | if (blockData.hash === hash) { 19 | is_found = true; 20 | global.coinFuncs.getPortBlockHeaderByHash(blockData.port, hash, (err, body) => { 21 | if (err !== null || !body.reward) { 22 | if (blockData.valid) { 23 | blockData.valid = false; 24 | blockData.unlocked = true; 25 | txn.putBinary(global.database.altblockDB, key, global.protos.AltBlock.encode(blockData)); 26 | console.log("Altblock with " + hash + " hash became invalid for " + blockData.port + " port! Exiting!"); 27 | } else { 28 | console.log("Altblock with " + hash + " hash still has invalid hash for " + blockData.port + " port! Exiting!"); 29 | } 30 | cursor.close(); 31 | txn.commit(); 32 | process.exit(1); 33 | } 34 | blockData.valid = true; 35 | blockData.unlocked = false; 36 | //if (blockData.value != body.reward) console.log("Changing alt-block value from " + blockData.value + " to " + body.reward); 37 | //blockData.value = body.reward; 38 | txn.putBinary(global.database.altblockDB, key, global.protos.AltBlock.encode(blockData)); 39 | cursor.close(); 40 | txn.commit(); 41 | console.log("Altblock with " + hash + " hash was validated! Exiting!"); 42 | process.exit(0); 43 | }); 44 | } 45 | }); 46 | } 47 | if (!is_found) { 48 | cursor.close(); 49 | txn.commit(); 50 | console.log("Not found altblock with " + hash + " hash"); 51 | process.exit(1); 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /manage_scripts/dump_shares.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let range = require('range'); 4 | const argv = require('minimist')(process.argv.slice(2)); 5 | 6 | if (!argv.user) { 7 | console.error("Please specify user address to dump"); 8 | process.exit(1); 9 | } 10 | const user = argv.user; 11 | 12 | let paymentid; 13 | if (argv.paymentid) paymentid = argv.paymentid; 14 | 15 | let worker; 16 | if (argv.worker) worker = argv.worker; 17 | 18 | let depth = 10; 19 | if (argv.depth) depth = argv.depth; 20 | 21 | console.log("Dumping shares for " + user + " user"); 22 | if (paymentid) console.log("Dumping shares for " + paymentid + " paymentid"); 23 | if (worker) console.log("Dumping shares for " + worker + " worker"); 24 | 25 | require("../init_mini.js").init(function() { 26 | 27 | global.coinFuncs.getLastBlockHeader(function (err, body) { 28 | if (err !== null) { 29 | console.error("Invalid block header"); 30 | process.exit(1); 31 | } 32 | let lastBlock = body.height + 1; 33 | let txn = global.database.env.beginTxn({readOnly: true}); 34 | 35 | let cursor = new global.database.lmdb.Cursor(txn, global.database.shareDB); 36 | range.range(lastBlock, lastBlock - depth, -1).forEach(function (blockID) { 37 | for (let found = (cursor.goToRange(parseInt(blockID)) === blockID); found; found = cursor.goToNextDup()) { 38 | cursor.getCurrentBinary(function(key, data){ // jshint ignore:line 39 | let shareData = global.protos.Share.decode(data); 40 | if (shareData.paymentAddress === user && (!paymentid || shareData.paymentID === paymentid) && (!worker || shareData.identifier === worker)) { 41 | var d = new Date(shareData.timestamp); 42 | console.log(d.toString() + ": " + JSON.stringify(shareData)) 43 | } 44 | }); 45 | } 46 | }); 47 | cursor.close(); 48 | txn.commit(); 49 | process.exit(0); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /manage_scripts/altblock_add.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | if (!argv.timestamp) { 6 | console.error("Please specify altblock time"); 7 | process.exit(1); 8 | } 9 | const timestamp = argv.timestamp; 10 | 11 | if (!argv.body) { 12 | console.error("Please specify altblock body"); 13 | process.exit(1); 14 | } 15 | const body = argv.body; 16 | let body2; 17 | 18 | try { body2 = JSON.parse(body); } catch(e) { 19 | console.error("Can't parse altblock body: " + body); 20 | process.exit(1); 21 | } 22 | 23 | require("../init_mini.js").init(function() { 24 | const body3 = { 25 | "hash": body2.hash, 26 | "difficulty": body2.difficulty, 27 | "port": body2.port, 28 | "height": body2.height, 29 | "value": body2.value, 30 | "anchor_height": body2.anchor_height, 31 | "timestamp": timestamp * 1000, 32 | "shares": body2.shares || body2.difficulty, 33 | "poolType": body2.poolType || 0, 34 | "unlocked": body2.unlocked || false, 35 | "valid": body2.valid || true, 36 | "pay_value": body2.pay_value || 0, 37 | "pay_stage": body2.pay_stage || "", 38 | "pay_status": body2.pay_status || "" 39 | }; 40 | if (typeof (body3.hash) === 'undefined' || 41 | typeof (body3.difficulty) === 'undefined' || 42 | typeof (body3.shares) === 'undefined' || 43 | typeof (body3.timestamp) === 'undefined' || 44 | typeof (body3.poolType) === 'undefined' || 45 | typeof (body3.unlocked) === 'undefined' || 46 | typeof (body3.valid) === 'undefined' || 47 | typeof (body3.port) === 'undefined' || 48 | typeof (body3.height) === 'undefined' || 49 | typeof (body3.anchor_height) === 'undefined' || 50 | typeof (body3.value) === 'undefined' || 51 | typeof (body3.pay_value) === 'undefined' || 52 | typeof (body3.pay_stage) === 'undefined' || 53 | typeof (body3.pay_status) === 'undefined') { 54 | console.error("Altblock body is invalid: " + JSON.stringify(body3)); 55 | process.exit(1); 56 | } 57 | const body4 = global.protos.AltBlock.encode(body3); 58 | let txn = global.database.env.beginTxn(); 59 | txn.putBinary(global.database.altblockDB, timestamp, body4); 60 | txn.commit(); 61 | console.log("Altblock with " + timestamp + " timestamp added! Exiting!"); 62 | process.exit(0); 63 | }); 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Snipa22 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | This work is a derivitive work from Zone117x's node-cryptonote-pool, permission 24 | has been granted from Zone117x via IRC conversation to re-license this code as 25 | MIT for all portions of the work derived from his original source as of 2/11/2017 26 | Original work: https://github.com/zone117x/node-cryptonote-pool 27 | 28 | Log snippet: 29 | Feb 11 15:59:02 Hallo! Wanted to reach out as we finally released the pool software w/ the FFS funded at this point. https://github.com/Snipa22/nodejs-pool . The following code: https://pb.junaos.xyz/view/cd3d7e27 is what was snagged, and mostly rebuilt along the way for good measure. 30 | Feb 11 15:59:02 I'm not sure how deeply the GPL requires me to go to claim it as "new code" given that the logic behind it all is about the same. 31 | 32 | Feb 11 16:09:39 .fr is now 1/5th of the overall mining power, DP is another 1/5th. 33 | Feb 11 16:09:48 We're getting scary-close to the 50% attacks. 34 | Feb 11 16:10:08 You have my permission to MIT 35 | Feb 11 16:10:31 Thank you. I'll get our licenses updated and such today. 36 | Feb 11 16:12:08 What payment systems did you get implemented? 37 | -------------------------------------------------------------------------------- /lib/block_fix_raw_reward.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | if (!argv.hash) { 6 | console.error("Please specify block hash"); 7 | process.exit(1); 8 | } 9 | const hash = argv.hash; 10 | 11 | require("../init_mini.js").init(function() { 12 | let txn = global.database.env.beginTxn(); 13 | let cursor = new global.database.lmdb.Cursor(txn, global.database.blockDB); 14 | let is_found = 0; 15 | for (let found = cursor.goToFirst(); found; found = cursor.goToNext()) { 16 | cursor.getCurrentBinary(function(key, data){ // jshint ignore:line 17 | let blockData = global.protos.Block.decode(data); 18 | if (!is_found && blockData.hash === hash) { 19 | console.log("Found block with " + blockData.hash + " hash"); 20 | is_found = 1; 21 | global.coinFuncs.getPortAnyBlockHeaderByHash(18081, argv.hash, false, function (err, body) { 22 | if (err) { 23 | cursor.close(); 24 | txn.commit(); 25 | console.error("Can't get block header"); 26 | process.exit(1); 27 | } 28 | console.log("Changing raw block reward from " + blockData.value + " to " + body.reward); 29 | blockData.value = body.reward; 30 | txn.putBinary(global.database.blockDB, key, global.protos.Block.encode(blockData)); 31 | txn.commit(); 32 | cursor.close(); 33 | console.log("Changed block"); 34 | process.exit(0); 35 | }); 36 | } 37 | }); 38 | } 39 | if (!is_found) { 40 | cursor.close(); 41 | txn.commit(); 42 | console.log("Not found block with " + hash + " hash"); 43 | process.exit(1); 44 | } 45 | }); 46 | -------------------------------------------------------------------------------- /manage_scripts/altblock_add_auto.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | if (!argv.port) { 6 | console.error("Please specify port"); 7 | process.exit(1); 8 | } 9 | const port = argv.port; 10 | 11 | if (!argv.hash) { 12 | console.error("Please specify hash"); 13 | process.exit(1); 14 | } 15 | const hash = argv.hash; 16 | 17 | require("../init_mini.js").init(function() { 18 | global.coinFuncs.getLastBlockHeader(function (err, last_block_body) { 19 | if (err !== null){ 20 | console.error("Can't get last block info"); 21 | process.exit(0); 22 | } 23 | global.coinFuncs.getPortBlockHeaderByHash(port, hash, function (err_header, body_header) { 24 | if (err_header) { 25 | console.error("Can't get block info"); 26 | console.error("err:" + JSON.stringify(err_header)); 27 | console.error("body:" + JSON.stringify(body_header)); 28 | process.exit(0); 29 | } 30 | if (!body_header.timestamp) body_header.timestamp = body_header.time; 31 | if (!body_header.timestamp) body_header.timestamp = body_header.mediantime; 32 | if (!body_header.timestamp) { 33 | console.error("Can't get block timestamp: " + JSON.stringify(body_header)); 34 | process.exit(0); 35 | } 36 | if ((Date.now() / 1000) < body_header.timestamp) body_header.timestamp = parseInt(body_header.timestamp / 1000); 37 | if (!body_header.difficulty) body_header.difficulty = argv.diff; 38 | if (!body_header.difficulty) { 39 | console.error("Can't get block difficilty: " + JSON.stringify(body_header)); 40 | process.exit(0); 41 | } 42 | if (!body_header.height) { 43 | console.error("Can't get block height: " + JSON.stringify(body_header)); 44 | process.exit(0); 45 | } 46 | body_header.difficulty = parseInt(body_header.difficulty); 47 | body_header.timestamp = parseInt(body_header.timestamp); 48 | global.database.storeAltBlock(body_header.timestamp, global.protos.AltBlock.encode({ 49 | hash: hash, 50 | difficulty: body_header.difficulty, 51 | shares: 0, 52 | timestamp: body_header.timestamp * 1000, 53 | poolType: global.protos.POOLTYPE.PPLNS, 54 | unlocked: false, 55 | valid: true, 56 | port: port, 57 | height: body_header.height, 58 | anchor_height: last_block_body.height 59 | }), function(data){ 60 | if (!data){ 61 | console.error("Block not stored"); 62 | } else { 63 | console.log("Block with " + port + " port and " + hash + " stored"); 64 | } 65 | process.exit(0); 66 | }); 67 | }); 68 | }); 69 | }); 70 | 71 | -------------------------------------------------------------------------------- /lib/remote_comms.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const request = require('request'); 3 | const async = require('async'); 4 | 5 | function Database() { 6 | 7 | let thread_id=''; 8 | 9 | this.sendQueue = async.queue(function (task, callback) { 10 | async.doUntil( 11 | function (intCallback) { 12 | request.post({url: global.config.general.shareHost, body: task.body, forever: true}, function (error, response, body) { 13 | return intCallback(null, error ? 0 : response.statusCode); 14 | }); 15 | }, 16 | function (data, untilCB) { 17 | return untilCB(null, data === 200); 18 | }, 19 | function () { 20 | return callback(); 21 | }); 22 | }, require('os').cpus().length*32); 23 | 24 | this.storeShare = function (blockId, shareData) { 25 | let wsData = global.protos.WSData.encode({ 26 | msgType: global.protos.MESSAGETYPE.SHARE, 27 | key: global.config.api.authKey, 28 | msg: shareData, 29 | exInt: blockId 30 | }); 31 | process.send({type: 'sendRemote', body: wsData.toString('hex')}); 32 | }; 33 | 34 | this.storeBlock = function (blockId, blockData) { 35 | let wsData = global.protos.WSData.encode({ 36 | msgType: global.protos.MESSAGETYPE.BLOCK, 37 | key: global.config.api.authKey, 38 | msg: blockData, 39 | exInt: blockId 40 | }); 41 | process.send({type: 'sendRemote', body: wsData.toString('hex')}); 42 | }; 43 | 44 | this.storeAltBlock = function (blockId, blockData) { 45 | let wsData = global.protos.WSData.encode({ 46 | msgType: global.protos.MESSAGETYPE.ALTBLOCK, 47 | key: global.config.api.authKey, 48 | msg: blockData, 49 | exInt: blockId 50 | }); 51 | process.send({type: 'sendRemote', body: wsData.toString('hex')}); 52 | }; 53 | 54 | this.storeInvalidShare = function (minerData) { 55 | let wsData = global.protos.WSData.encode({ 56 | msgType: global.protos.MESSAGETYPE.INVALIDSHARE, 57 | key: global.config.api.authKey, 58 | msg: minerData, 59 | exInt: 1 60 | }); 61 | process.send({type: 'sendRemote', body: wsData.toString('hex')}); 62 | }; 63 | 64 | setInterval(function(queue_obj){ 65 | if ((queue_obj.length() > 20 || queue_obj.running() > 20) && global.database.thread_id === '(Master) '){ 66 | console.log(global.database.thread_id + "Remote queue state: " + queue_obj.length() + " items in the queue " + queue_obj.running() + " items being processed"); 67 | } 68 | }, 30*1000, this.sendQueue); 69 | 70 | 71 | this.initEnv = function(){ 72 | this.data = null; 73 | }; 74 | } 75 | 76 | module.exports = Database; -------------------------------------------------------------------------------- /block_share_dumps/calc_mo_cvs_top.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | if (Boolean(process.stdin.isTTY) || process.argv.length !== 2) { 4 | console.log("Usage: unxz -c .cvs.xz | node calc_mo_cvs_top.js"); 5 | console.log(" wget -O - https://block-share-dumps.moneroocean.stream/.cvs.xz | unxz -c | node calc_mo_cvs_top.js"); 6 | process.exit(1); 7 | } 8 | 9 | let stdin = ""; 10 | 11 | process.stdin.on('data', function(data) { 12 | stdin += data.toString(); 13 | }); 14 | 15 | function human_hashrate(hashes) { 16 | const power = Math.pow(10, 2 || 0); 17 | if (hashes > 1000000000000) return String(Math.round((hashes / 1000000000000) * power) / power) + " TH/s"; 18 | if (hashes > 1000000000) return String(Math.round((hashes / 1000000000) * power) / power) + " GH/s"; 19 | if (hashes > 1000000) return String(Math.round((hashes / 1000000) * power) / power) + " MH/s"; 20 | if (hashes > 1000) return String(Math.round((hashes / 1000) * power) / power) + " KH/s"; 21 | return Math.floor( hashes || 0 ) + " H/s" 22 | }; 23 | 24 | process.stdin.on('end', function() { 25 | let pplns_window = 0; 26 | let oldest_timestamp = 0; 27 | let newest_timestamp = 0; 28 | 29 | let wallets = {}; 30 | 31 | let my_share_count = 0; 32 | let my_xmr_diff = 0; 33 | let my_xmr_diff_payed = 0; 34 | let my_coin_raw_diff = {}; 35 | let my_coin_xmr_diff = {}; 36 | 37 | for (let line of stdin.split("\n")) { 38 | if (line.substring(0, 1) == "#") continue; 39 | const items = line.split('\t'); 40 | if (items.length < 7) { 41 | console.error("Skipped invalid line: " + line); 42 | continue; 43 | } 44 | const wallet = items[0]; 45 | const timestamp = parseInt(items[1], 16); 46 | const raw_diff = parseInt(items[2]); 47 | const count = parseInt(items[3]); 48 | const coin = items[4]; 49 | const xmr_diff = parseInt(items[5]); 50 | const xmr_diff_payed = items[6] == "" ? xmr_diff : parseInt(items[6]); 51 | pplns_window += xmr_diff; 52 | if (!oldest_timestamp || timestamp < oldest_timestamp) oldest_timestamp = timestamp; 53 | if (newest_timestamp < timestamp) newest_timestamp = timestamp; 54 | if (!(wallet in wallets)) wallets[wallet] = { 55 | share_count: 0, 56 | xmr_diff: 0, 57 | xmr_diff_payed: 0, 58 | coin_raw_diff: {}, 59 | coin_xmr_diff: {}, 60 | }; 61 | wallets[wallet].share_count += count; 62 | wallets[wallet].xmr_diff += xmr_diff; 63 | wallets[wallet].xmr_diff_payed += xmr_diff_payed; 64 | if (!(coin in wallets[wallet].coin_raw_diff)) wallets[wallet].coin_raw_diff[coin] = 0; 65 | wallets[wallet].coin_raw_diff[coin] += raw_diff; 66 | if (!(coin in wallets[wallet].coin_xmr_diff)) wallets[wallet].coin_xmr_diff[coin] = 0; 67 | wallets[wallet].coin_xmr_diff[coin] += xmr_diff; 68 | } 69 | 70 | for (let wallet of Object.keys(wallets).sort((a, b) => (wallets[a].xmr_diff < wallets[b].xmr_diff) ? 1 : -1)) { 71 | console.log(wallet + ": " + wallets[wallet].xmr_diff); 72 | } 73 | 74 | process.exit(0); 75 | }); -------------------------------------------------------------------------------- /manage_scripts/cache_upgrade.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | const user = argv.user ? argv.user : null; 5 | 6 | require("../init_mini.js").init(function() { 7 | let txn = global.database.env.beginTxn({readOnly: true}); 8 | let cursor = new global.database.lmdb.Cursor(txn, global.database.cacheDB); 9 | for (let found = cursor.goToFirst(); found; found = cursor.goToNext()) { 10 | cursor.getCurrentString(function(key, data){ // jshint ignore:line 11 | if (key.length < 95) { // min XMR address length 12 | if (key.includes("history:") || key.includes("stats:") || key.includes("identifiers:")) { 13 | console.log(key + ": removing bad key"); 14 | let txn2 = global.database.env.beginTxn(); 15 | txn2.del(global.database.cacheDB, key); 16 | txn2.commit(); 17 | return; 18 | } 19 | console.log("Skipping " + key + " key"); 20 | return; 21 | } 22 | if (key.includes("history:") || key.includes("stats:") || key.includes("identifiers:")) return; 23 | if (user && !key.includes(user)) return; 24 | 25 | let txn2 = global.database.env.beginTxn(); 26 | if (key.includes("_identifiers")) { 27 | let parts = key.split("_"); 28 | let key2 = parts[0]; 29 | if (global.database.getCache("identifiers:" + key2)) { 30 | console.log(key2 + ": removing outdated _identifiers"); 31 | txn2.del(global.database.cacheDB, key); 32 | } else { 33 | console.log(key2 + ": moving _identifiers to identifiers:"); 34 | txn2.putString(global.database.cacheDB, "identifiers:" + key2, data); 35 | txn2.del(global.database.cacheDB, key); 36 | } 37 | } else { 38 | try { 39 | let data2 = JSON.parse(data); 40 | if ("hash" in data2 && "lastHash" in data2) { 41 | if (global.database.getCache("stats:" + key)) { 42 | console.log(key + ": removing outdated stats"); 43 | delete data2["hash"]; 44 | delete data2["lastHash"]; 45 | txn2.putString(global.database.cacheDB, key, JSON.stringify(data2)); 46 | } else { 47 | console.log(key + ": moving old stats to stats:"); 48 | let data3 = { hash: data2.hash, lastHash: data2.lastHash }; 49 | delete data2["hash"]; 50 | delete data2["lastHash"]; 51 | txn2.putString(global.database.cacheDB, key, JSON.stringify(data2)); 52 | txn2.putString(global.database.cacheDB, "stats:" + key, JSON.stringify(data3)); 53 | } 54 | } 55 | if ("hashHistory" in data2) { 56 | if (global.database.getCache("history:" + key)) { 57 | console.log(key + ": removing outdated history"); 58 | delete data2["hashHistory"]; 59 | txn2.putString(global.database.cacheDB, key, JSON.stringify(data2)); 60 | } else { 61 | console.log(key + ": moving old history to history:"); 62 | let data3 = { hashHistory: data2.hashHistory }; 63 | delete data2["hashHistory"]; 64 | txn2.putString(global.database.cacheDB, key, JSON.stringify(data2)); 65 | txn2.putString(global.database.cacheDB, "history:" + key, JSON.stringify(data3)); 66 | } 67 | } 68 | } catch (e) { 69 | console.error("Bad cache data with " + key + " key"); 70 | } 71 | } 72 | txn2.commit(); 73 | }); 74 | } 75 | cursor.close(); 76 | txn.commit(); 77 | process.exit(0); 78 | }); 79 | -------------------------------------------------------------------------------- /user_scripts/balance_move_force.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const mysql = require("promise-mysql"); 3 | const async = require("async"); 4 | const argv = require('minimist')(process.argv.slice(2)); 5 | 6 | if (!argv.old_user) { 7 | console.error("Please specify old_user address to move balance from"); 8 | process.exit(1); 9 | } 10 | const old_user = argv.old_user; 11 | 12 | if (!argv.new_user) { 13 | console.error("Please specify new_user address to move balance to"); 14 | process.exit(1); 15 | } 16 | const new_user = argv.new_user; 17 | 18 | require("../init_mini.js").init(function() { 19 | const old_parts = old_user.split("."); 20 | const old_address = old_parts.length === 1 ? old_user : old_parts[0]; 21 | const old_payment_id = old_parts.length === 2 ? old_parts[1] : null; 22 | 23 | const new_parts = new_user.split("."); 24 | const new_address = new_parts.length === 1 ? new_user : new_parts[0]; 25 | const new_payment_id = new_parts.length === 2 ? new_parts[1] : null; 26 | 27 | console.log("Old Address: " + old_address); 28 | console.log("Old PaymentID: " + old_payment_id); 29 | console.log("New Address: " + new_address); 30 | console.log("New PaymentID: " + new_payment_id); 31 | 32 | const old_where_str = old_payment_id === null ? "payment_address = '" + old_address + "' AND payment_id IS NULL" 33 | : "payment_address = '" + old_address + "' AND payment_id = '" + old_payment_id + "'"; 34 | 35 | const new_where_str = new_payment_id === null ? "payment_address = '" + new_address + "' AND payment_id IS NULL" 36 | : "payment_address = '" + new_address + "' AND payment_id = '" + new_payment_id + "'"; 37 | 38 | let old_amount; 39 | 40 | async.waterfall([ 41 | function (callback) { 42 | global.mysql.query("SELECT * FROM balance WHERE " + old_where_str).then(function (rows) { 43 | if (rows.length != 1) { 44 | console.error("Can't find old_user!"); 45 | process.exit(1); 46 | } 47 | old_amount = rows[0].amount; 48 | console.log("Old address amount: " + global.support.coinToDecimal(old_amount)); 49 | console.log("Old address last update time: " + rows[0].last_edited); 50 | callback(); 51 | }); 52 | }, 53 | function (callback) { 54 | global.mysql.query("SELECT * FROM balance WHERE " + new_where_str).then(function (rows) { 55 | if (rows.length != 1) { 56 | console.error("Can't find new_user!"); 57 | process.exit(1); 58 | } 59 | console.log("New address amount: " + global.support.coinToDecimal(rows[0].amount)); 60 | callback(); 61 | }); 62 | }, 63 | function (callback) { 64 | global.mysql.query("UPDATE balance SET amount = '0' WHERE " + old_where_str).then(function (rows) { 65 | console.log("UPDATE balance SET amount = '0' WHERE " + old_where_str); 66 | callback(); 67 | }); 68 | }, 69 | function (callback) { 70 | global.mysql.query("UPDATE balance SET amount = amount + " + old_amount + " WHERE " + new_where_str).then(function (rows) { 71 | console.log("UPDATE balance SET amount = amount + " + old_amount + " WHERE " + new_where_str); 72 | callback(); 73 | }); 74 | }, 75 | function (callback) { 76 | global.mysql.query("SELECT * FROM balance WHERE " + old_where_str).then(function (rows) { 77 | console.log("New old address amount: " + global.support.coinToDecimal(rows[0].amount)); 78 | callback(); 79 | }); 80 | }, 81 | function (callback) { 82 | global.mysql.query("SELECT * FROM balance WHERE " + new_where_str).then(function (rows) { 83 | console.log("New new address amount: " + global.support.coinToDecimal(rows[0].amount)); 84 | callback(); 85 | }); 86 | }, 87 | function (callback) { 88 | console.log("DONE"); 89 | process.exit(0); 90 | } 91 | ]); 92 | }); 93 | -------------------------------------------------------------------------------- /block_share_dumps/calc_mo_cvs.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | if (Boolean(process.stdin.isTTY) || process.argv.length !== 3) { 4 | console.log("Usage: unxz -c .cvs.xz | node calc_mo_cvs.js "); 5 | console.log(" wget -O - https://block-share-dumps.moneroocean.stream/.cvs.xz | unxz -c | node calc_mo_cvs.js "); 6 | process.exit(1); 7 | } 8 | 9 | const my_wallet = process.argv[2].slice(-16); 10 | 11 | let stdin = ""; 12 | 13 | process.stdin.on('data', function(data) { 14 | stdin += data.toString(); 15 | }); 16 | 17 | function human_hashrate(hashes) { 18 | const power = Math.pow(10, 2 || 0); 19 | if (hashes > 1000000000000) return String(Math.round((hashes / 1000000000000) * power) / power) + " TH/s"; 20 | if (hashes > 1000000000) return String(Math.round((hashes / 1000000000) * power) / power) + " GH/s"; 21 | if (hashes > 1000000) return String(Math.round((hashes / 1000000) * power) / power) + " MH/s"; 22 | if (hashes > 1000) return String(Math.round((hashes / 1000) * power) / power) + " KH/s"; 23 | return Math.floor( hashes || 0 ) + " H/s" 24 | }; 25 | 26 | process.stdin.on('end', function() { 27 | let pplns_window = 0; 28 | let oldest_timestamp = 0; 29 | let newest_timestamp = 0; 30 | 31 | let my_share_count = 0; 32 | let my_xmr_diff = 0; 33 | let my_xmr_diff_payed = 0; 34 | let my_coin_raw_diff = {}; 35 | let my_coin_xmr_diff = {}; 36 | 37 | for (let line of stdin.split("\n")) { 38 | if (line.substring(0, 1) == "#") continue; 39 | const items = line.split('\t'); 40 | if (items.length < 7) { 41 | console.error("Skipped invalid line: " + line); 42 | continue; 43 | } 44 | const wallet = items[0]; 45 | const timestamp = parseInt(items[1], 16); 46 | const raw_diff = parseInt(items[2]); 47 | const count = parseInt(items[3]); 48 | const coin = items[4]; 49 | const xmr_diff = parseInt(items[5]); 50 | const xmr_diff_payed = items[6] == "" ? xmr_diff : parseInt(items[6]); 51 | pplns_window += xmr_diff; 52 | if (!oldest_timestamp || timestamp < oldest_timestamp) oldest_timestamp = timestamp; 53 | if (newest_timestamp < timestamp) newest_timestamp = timestamp; 54 | if (wallet === my_wallet) { 55 | my_share_count += count; 56 | my_xmr_diff += xmr_diff; 57 | my_xmr_diff_payed += xmr_diff_payed; 58 | if (!(coin in my_coin_raw_diff)) my_coin_raw_diff[coin] = 0; 59 | my_coin_raw_diff[coin] += raw_diff; 60 | if (!(coin in my_coin_xmr_diff)) my_coin_xmr_diff[coin] = 0; 61 | my_coin_xmr_diff[coin] += xmr_diff; 62 | } 63 | } 64 | 65 | console.log("PPLNS window size: \t" + ((newest_timestamp - oldest_timestamp)/1000/60/60).toFixed(2) + " hours"); 66 | console.log("PPLNS window size: \t" + pplns_window + " xmr hashes"); 67 | console.log("Pool XMR normalized hashrate: \t" + human_hashrate(pplns_window / (newest_timestamp - oldest_timestamp) * 1000)); 68 | console.log(""); 69 | console.log("Your submitted shares: \t" + my_share_count); 70 | console.log("Your payment: \t" + ((my_xmr_diff_payed / pplns_window) * 100).toFixed(6) + "% (" + my_xmr_diff_payed + " xmr hashes)"); 71 | console.log("Your XMR normalized hashrate: \t" + human_hashrate(my_xmr_diff_payed / (newest_timestamp - oldest_timestamp) * 1000)); 72 | console.log(""); 73 | console.log("You mined these coins:"); 74 | for (let coin of Object.keys(my_coin_raw_diff).sort()) { 75 | console.log("\t" + coin + ": " + my_coin_raw_diff[coin] + " raw coin hashes (" + ((my_coin_xmr_diff[coin] / my_xmr_diff) * 100).toFixed(6) + "% of XMR normalized hashrate)"); 76 | } 77 | 78 | process.exit(0); 79 | }); -------------------------------------------------------------------------------- /deployment/leaf.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | 3 | NODEJS_VERSION=v20.12.2 4 | 5 | if [[ $(whoami) != "root" ]]; then 6 | echo "Please run this script as root" 7 | exit 1 8 | fi 9 | 10 | DEBIAN_FRONTEND=noninteractive apt-get update 11 | DEBIAN_FRONTEND=noninteractive apt-get full-upgrade -y 12 | DEBIAN_FRONTEND=noninteractive apt-get install -y ntp sudo ufw 13 | timedatectl set-timezone Etc/UTC 14 | 15 | adduser --disabled-password --gecos "" user 16 | grep -q "user ALL=(ALL) NOPASSWD:ALL" /etc/sudoers || echo "user ALL=(ALL) NOPASSWD:ALL" >>/etc/sudoers 17 | su user -c "mkdir /home/user/.ssh" 18 | if [ -f "/root/.ssh/authorized_keys" ]; then 19 | mv /root/.ssh/authorized_keys /home/user/.ssh/authorized_keys 20 | chown user:user /home/user/.ssh/authorized_keys 21 | chmod 600 /home/user/.ssh/authorized_keys 22 | sed -i 's/#\?PasswordAuthentication yes/PasswordAuthentication no/g' /etc/ssh/sshd_config 23 | sed -i 's/#\?PermitRootLogin .\+/PermitRootLogin no/g' /etc/ssh/sshd_config 24 | sed -i 's/#\? .\+/PermitEmptyPasswords no/g' /etc/ssh/sshd_config 25 | service ssh restart 26 | fi 27 | 28 | ufw default deny incoming 29 | ufw default allow outgoing 30 | ufw allow ssh 31 | ufw allow 3333 32 | ufw allow 5555 33 | ufw allow 7777 34 | ufw allow 9000 35 | ufw --force enable 36 | 37 | cat >/root/.vimrc <<'EOF' 38 | colorscheme desert 39 | set fo-=ro 40 | EOF 41 | 42 | cat >/home/user/.vimrc <<'EOF' 43 | colorscheme desert 44 | set fo-=ro 45 | EOF 46 | chown user:user /home/user/.vimrc 47 | 48 | DEBIAN_FRONTEND=noninteractive apt-get install -y vim git make g++ cmake libssl-dev libunbound-dev libboost-dev libboost-system-dev libboost-date-time-dev libboost-dev libboost-system-dev libboost-date-time-dev libboost-filesystem-dev libboost-thread-dev libboost-chrono-dev libboost-locale-dev libboost-regex-dev libboost-regex-dev libboost-program-options-dev libzmq3-dev 49 | cd /usr/local/src 50 | git clone https://github.com/monero-project/monero.git 51 | cd monero 52 | git checkout v0.18.4.3 53 | git submodule update --init 54 | USE_SINGLE_BUILDDIR=1 make -j$(nproc) release || USE_SINGLE_BUILDDIR=1 make -j1 release 55 | 56 | cat >/lib/systemd/system/monero.service <<'EOF' 57 | [Unit] 58 | Description=Monero Daemon 59 | After=network.target 60 | 61 | [Service] 62 | ExecStart=/usr/local/src/monero/build/release/bin/monerod --hide-my-port --prune-blockchain --enable-dns-blocklist --no-zmq --out-peers 64 --non-interactive --restricted-rpc --block-notify '/bin/bash /home/user/nodejs-pool/block_notify.sh' 63 | Restart=always 64 | User=monerodaemon 65 | Nice=10 66 | CPUQuota=400% 67 | 68 | [Install] 69 | WantedBy=multi-user.target 70 | EOF 71 | 72 | useradd -m monerodaemon -d /home/monerodaemon 73 | systemctl daemon-reload 74 | systemctl enable monero 75 | systemctl start monero 76 | 77 | sleep 30 78 | echo "Please wait until Monero daemon is fully synced" 79 | tail -f /home/monerodaemon/.bitmonero/bitmonero.log 2>/dev/null | grep Synced & 80 | ( tail -F -n0 /home/monerodaemon/.bitmonero/bitmonero.log & ) | egrep -q "You are now synchronized with the network" 81 | killall tail 2>/dev/null 82 | echo "Monero daemon is synced" 83 | 84 | (cat < 7*24*60*60*1000) { 70 | console.log(key + ": found outdated key"); 71 | let txn2 = global.database.env.beginTxn(); 72 | txn2.del(global.database.cacheDB, key); 73 | txn2.del(global.database.cacheDB, "history:" + key); 74 | txn2.del(global.database.cacheDB, "stats:" + key); 75 | txn2.commit(); 76 | ++ count; 77 | } 78 | 79 | } 80 | 81 | } else if (key.includes("stats:")) { 82 | try { 83 | let data2 = JSON.parse(data); 84 | if ((data2.hash || data2.hash2) && Date.now() - data2.lastHash > 24*60*60*1000) { 85 | console.log(key + ": found dead account"); 86 | data2.hash = data2.hash2 = 0; 87 | let txn2 = global.database.env.beginTxn(); 88 | txn2.putString(global.database.cacheDB, key, JSON.stringify(data2)); 89 | txn2.commit(); 90 | } 91 | } catch (e) { 92 | console.error("Bad cache data with " + key + " key"); 93 | } 94 | 95 | } 96 | }); 97 | } 98 | 99 | cursor.close(); 100 | txn.commit(); 101 | console.log("Deleted items: " + count); 102 | process.exit(0); 103 | }); 104 | -------------------------------------------------------------------------------- /user_scripts/balance_move.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const mysql = require("promise-mysql"); 3 | const async = require("async"); 4 | const argv = require('minimist')(process.argv.slice(2)); 5 | 6 | if (!argv.old_user) { 7 | console.error("Please specify old_user address to move balance from"); 8 | process.exit(1); 9 | } 10 | const old_user = argv.old_user; 11 | 12 | if (!argv.new_user) { 13 | console.error("Please specify new_user address to move balance to"); 14 | process.exit(1); 15 | } 16 | const new_user = argv.new_user; 17 | 18 | require("../init_mini.js").init(function() { 19 | const old_parts = old_user.split("."); 20 | const old_address = old_parts.length === 1 ? old_user : old_parts[0]; 21 | const old_payment_id = old_parts.length === 2 ? old_parts[1] : null; 22 | 23 | const new_parts = new_user.split("."); 24 | const new_address = new_parts.length === 1 ? new_user : new_parts[0]; 25 | const new_payment_id = new_parts.length === 2 ? new_parts[1] : null; 26 | 27 | console.log("Old Address: " + old_address); 28 | console.log("Old PaymentID: " + old_payment_id); 29 | console.log("New Address: " + new_address); 30 | console.log("New PaymentID: " + new_payment_id); 31 | 32 | const old_where_str = old_payment_id === null ? "payment_address = '" + old_address + "' AND payment_id IS NULL" 33 | : "payment_address = '" + old_address + "' AND payment_id = '" + old_payment_id + "'"; 34 | 35 | const new_where_str = new_payment_id === null ? "payment_address = '" + new_address + "' AND payment_id IS NULL" 36 | : "payment_address = '" + new_address + "' AND payment_id = '" + new_payment_id + "'"; 37 | 38 | let old_amount; 39 | 40 | async.waterfall([ 41 | function (callback) { 42 | global.mysql.query("SELECT * FROM balance WHERE " + old_where_str).then(function (rows) { 43 | if (rows.length != 1) { 44 | console.error("Can't find old_user!"); 45 | process.exit(1); 46 | } 47 | old_amount = rows[0].amount; 48 | console.log("Old address amount: " + global.support.coinToDecimal(old_amount)); 49 | console.log("Old address last update time: " + rows[0].last_edited); 50 | if (Date.now()/1000 - global.support.formatDateFromSQL(rows[0].last_edited) < 24*60*60) { 51 | console.error("There was recent amount update. Refusing to continue!"); 52 | process.exit(1); 53 | } 54 | callback(); 55 | }); 56 | }, 57 | function (callback) { 58 | global.mysql.query("SELECT * FROM balance WHERE " + new_where_str).then(function (rows) { 59 | if (rows.length != 1) { 60 | console.error("Can't find new_user!"); 61 | process.exit(1); 62 | } 63 | console.log("New address amount: " + global.support.coinToDecimal(rows[0].amount)); 64 | callback(); 65 | }); 66 | }, 67 | function (callback) { 68 | global.mysql.query("UPDATE balance SET amount = '0' WHERE " + old_where_str).then(function (rows) { 69 | console.log("UPDATE balance SET amount = '0' WHERE " + old_where_str); 70 | callback(); 71 | }); 72 | }, 73 | function (callback) { 74 | global.mysql.query("UPDATE balance SET amount = amount + " + old_amount + " WHERE " + new_where_str).then(function (rows) { 75 | console.log("UPDATE balance SET amount = amount + " + old_amount + " WHERE " + new_where_str); 76 | callback(); 77 | }); 78 | }, 79 | function (callback) { 80 | global.mysql.query("SELECT * FROM balance WHERE " + old_where_str).then(function (rows) { 81 | console.log("New old address amount: " + global.support.coinToDecimal(rows[0].amount)); 82 | callback(); 83 | }); 84 | }, 85 | function (callback) { 86 | global.mysql.query("SELECT * FROM balance WHERE " + new_where_str).then(function (rows) { 87 | console.log("New new address amount: " + global.support.coinToDecimal(rows[0].amount)); 88 | callback(); 89 | }); 90 | }, 91 | function (callback) { 92 | console.log("DONE"); 93 | process.exit(0); 94 | } 95 | ]); 96 | }); 97 | -------------------------------------------------------------------------------- /lib/remoteShare.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const express = require('express'); // call express 3 | const app = express(); // define our app using express 4 | const cluster = require('cluster'); 5 | const debug = require("debug")("remoteShare"); 6 | let concat = require('concat-stream'); 7 | 8 | let threadName = ""; 9 | let workerList = []; 10 | if (cluster.isMaster) { 11 | threadName = "(Master) "; 12 | } else { 13 | threadName = "(Worker " + cluster.worker.id + " - " + process.pid + ") "; 14 | } 15 | let shareData = []; 16 | 17 | // Websocket Stuffs. 18 | app.use(function(req, res, next){ 19 | req.pipe(concat(function(data){ 20 | req.body = data; 21 | next(); 22 | })); 23 | }); 24 | 25 | // Master/Slave communication Handling 26 | function messageHandler(message) { 27 | if (typeof message.raw_shares === "number"){ 28 | shareData.push(message); 29 | } 30 | } 31 | 32 | process.on('message', messageHandler); 33 | 34 | app.post('/leafApi', function (req, res) { 35 | try { 36 | let msgData = global.protos.WSData.decode(req.body); 37 | if (msgData.key !== global.config.api.authKey) { 38 | return res.status(403).end(); 39 | } 40 | switch (msgData.msgType) { 41 | case global.protos.MESSAGETYPE.SHARE: 42 | try { 43 | process.send(global.protos.Share.decode(msgData.msg)); 44 | } catch (e){ 45 | } 46 | return res.json({'success': true}); 47 | case global.protos.MESSAGETYPE.BLOCK: 48 | global.database.storeBlock(msgData.exInt, msgData.msg, function(data){ 49 | if (!data){ 50 | return res.status(400).end(); 51 | } else { 52 | return res.json({'success': true}); 53 | } 54 | }); 55 | break; 56 | case global.protos.MESSAGETYPE.ALTBLOCK: 57 | global.database.storeAltBlock(msgData.exInt, msgData.msg, function(data){ 58 | if (!data){ 59 | return res.status(400).end(); 60 | } else { 61 | return res.json({'success': true}); 62 | } 63 | }); 64 | break; 65 | case global.protos.MESSAGETYPE.INVALIDSHARE: 66 | global.database.storeInvalidShare(msgData.msg, function(data){ 67 | if (!data){ 68 | return res.status(400).end(); 69 | } else { 70 | return res.json({'success': true}); 71 | } 72 | }); 73 | break; 74 | default: 75 | return res.status(400).end(); 76 | } 77 | } catch (e) { 78 | console.log("Invalid WS frame"); 79 | return res.status(400).end(); 80 | } 81 | }); 82 | 83 | function storeShares(){ 84 | if (Object.keys(shareData).length > 0){ 85 | console.log('Storing ' + Object.keys(shareData).length + ' shares'); 86 | global.database.storeBulkShares(shareData); 87 | shareData = []; 88 | } 89 | setTimeout(storeShares, 1000); 90 | } 91 | 92 | if (cluster.isMaster) { 93 | let numWorkers = require('os').cpus().length; 94 | console.log('Master cluster setting up ' + numWorkers + ' workers...'); 95 | storeShares(); 96 | 97 | for (let i = 0; i < numWorkers; i++) { 98 | let worker = cluster.fork(); 99 | worker.on('message', messageHandler); 100 | workerList.push(worker); 101 | } 102 | 103 | cluster.on('online', function (worker) { 104 | console.log('Worker ' + worker.process.pid + ' is online'); 105 | }); 106 | 107 | cluster.on('exit', function (worker, code, signal) { 108 | console.log('Worker ' + worker.process.pid + ' died with code: ' + code + ', and signal: ' + signal); 109 | console.log('Starting a new worker'); 110 | worker = cluster.fork(); 111 | worker.on('message', messageHandler); 112 | workerList.push(worker); 113 | }); 114 | } else { 115 | app.listen(8000, 'localhost', function () { 116 | console.log('Process ' + process.pid + ' is listening to all incoming requests'); 117 | }); 118 | } 119 | -------------------------------------------------------------------------------- /init.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let mysql = require("promise-mysql"); 3 | let fs = require("fs"); 4 | let argv = require('minimist')(process.argv.slice(2)); 5 | let config = fs.readFileSync("./config.json"); 6 | let coinConfig = fs.readFileSync("./coinConfig.json"); 7 | let protobuf = require('protocol-buffers'); 8 | let path = require('path'); 9 | 10 | global.support = require("./lib/support.js")(); 11 | global.config = JSON.parse(config); 12 | global.mysql = mysql.createPool(global.config.mysql); 13 | global.protos = protobuf(fs.readFileSync('./lib/data.proto')); 14 | global.argv = argv; 15 | let comms; 16 | let coinInc; 17 | 18 | // Config Table Layout 19 | // . 20 | 21 | global.mysql.query("SELECT * FROM config").then(function (rows) { 22 | rows.forEach(function (row){ 23 | if (!global.config.hasOwnProperty(row.module)){ 24 | global.config[row.module] = {}; 25 | } 26 | if (global.config[row.module].hasOwnProperty(row.item)){ 27 | return; 28 | } 29 | switch(row.item_type){ 30 | case 'int': 31 | global.config[row.module][row.item] = parseInt(row.item_value); 32 | break; 33 | case 'bool': 34 | global.config[row.module][row.item] = (row.item_value === "true"); 35 | break; 36 | case 'string': 37 | global.config[row.module][row.item] = row.item_value; 38 | break; 39 | case 'float': 40 | global.config[row.module][row.item] = parseFloat(row.item_value); 41 | break; 42 | } 43 | }); 44 | }).then(function(){ 45 | global.config['coin'] = JSON.parse(coinConfig)[global.config.coin]; 46 | coinInc = require(global.config.coin.funcFile); 47 | global.coinFuncs = new coinInc(); 48 | if (argv.module === 'pool'){ 49 | comms = require('./lib/remote_comms'); 50 | } else { 51 | comms = require('./lib/local_comms'); 52 | } 53 | global.database = new comms(); 54 | global.database.initEnv(); 55 | global.coinFuncs.blockedAddresses.push(global.config.pool.address); 56 | global.coinFuncs.blockedAddresses.push(global.config.payout.feeAddress); 57 | if (argv.hasOwnProperty('tool') && fs.existsSync('./tools/'+argv.tool+'.js')) { 58 | require('./tools/'+argv.tool+'.js'); 59 | } else if (argv.hasOwnProperty('module')){ 60 | switch(argv.module){ 61 | case 'pool': 62 | global.config.ports = []; 63 | global.mysql.query("SELECT * FROM port_config").then(function(rows){ 64 | rows.forEach(function(row){ 65 | row.hidden = row.hidden === 1; 66 | row.ssl = row.ssl === 1; 67 | global.config.ports.push({ 68 | port: row.poolPort, 69 | difficulty: row.difficulty, 70 | desc: row.portDesc, 71 | portType: row.portType, 72 | hidden: row.hidden, 73 | ssl: row.ssl 74 | }); 75 | }); 76 | }).then(function(){ 77 | require('./lib/pool.js'); 78 | }); 79 | break; 80 | case 'blockManager': 81 | require('./lib/blockManager.js'); 82 | break; 83 | case 'altblockManager': 84 | require('./lib2/altblockManager.js'); 85 | break; 86 | case 'altblockExchange': 87 | require('./lib2/altblockExchange.js'); 88 | break; 89 | case 'payments': 90 | require('./lib/payments.js'); 91 | break; 92 | case 'api': 93 | require('./lib/api.js'); 94 | break; 95 | case 'remoteShare': 96 | require('./lib/remoteShare.js'); 97 | break; 98 | case 'worker': 99 | require('./lib/worker.js'); 100 | break; 101 | case 'pool_stats': 102 | require('./lib/pool_stats.js'); 103 | break; 104 | case 'longRunner': 105 | require('./lib/longRunner.js'); 106 | break; 107 | default: 108 | console.error("Invalid module provided. Please provide a valid module"); 109 | process.exit(1); 110 | } 111 | } else { 112 | console.error("Invalid module/tool provided. Please provide a valid module/tool"); 113 | console.error("Valid Modules: pool, blockManager, payments, api, remoteShare, worker, longRunner"); 114 | let valid_tools = "Valid Tools: "; 115 | fs.readdirSync('./tools/').forEach(function(line){ 116 | valid_tools += path.parse(line).name + ", "; 117 | }); 118 | valid_tools = valid_tools.slice(0, -2); 119 | console.error(valid_tools); 120 | process.exit(1); 121 | } 122 | }); -------------------------------------------------------------------------------- /manage_scripts/user_del.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const mysql = require("promise-mysql"); 3 | const async = require("async"); 4 | const argv = require('minimist')(process.argv.slice(2)); 5 | 6 | if (!argv.user) { 7 | console.error("Please specify user address to delete"); 8 | process.exit(1); 9 | } 10 | const user = argv.user; 11 | 12 | require("../init_mini.js").init(function() { 13 | const parts = user.split("."); 14 | const address = parts.length === 1 ? user : parts[0]; 15 | const payment_id = parts.length === 2 ? parts[1] : null; 16 | 17 | console.log("Address: " + address); 18 | console.log("PaymentID: " + payment_id); 19 | console.log("Max payment to remove: " + global.config.payout.walletMin); 20 | let rows2remove = 0; 21 | 22 | const where_str = payment_id === null ? "payment_address = '" + address + "' AND (payment_id IS NULL OR payment_id = '')" 23 | : "payment_address = '" + address + "' AND payment_id = '" + payment_id + "'"; 24 | 25 | async.waterfall([ 26 | function (callback) { 27 | global.mysql.query("SELECT * FROM users WHERE username = ?", [user]).then(function (rows) { 28 | if (rows.length > 1) { 29 | console.error("Too many users were selected!"); 30 | process.exit(1); 31 | } 32 | console.log("Found rows in users table: " + rows.length); 33 | rows2remove += rows.length; 34 | callback(); 35 | }); 36 | }, 37 | function (callback) { 38 | global.mysql.query("SELECT * FROM balance WHERE " + where_str).then(function (rows) { 39 | if (rows.length > 1) { 40 | console.error("Too many users were selected!"); 41 | process.exit(1); 42 | } 43 | if (rows.length === 1 && rows[0].amount >= global.support.decimalToCoin(global.config.payout.walletMin)) { 44 | console.error("Too big payment left: " + global.support.coinToDecimal(rows[0].amount)); 45 | process.exit(1); 46 | } 47 | if (rows.length) { 48 | console.log("Balance last update time: " + rows[0].last_edited); 49 | if (Date.now()/1000 - global.support.formatDateFromSQL(rows[0].last_edited) < 12*60*60) { 50 | console.error("There was recent amount update. Refusing to continue!"); 51 | process.exit(1); 52 | } 53 | } 54 | console.log("Found rows in balance table: " + rows.length); 55 | rows2remove += rows.length; 56 | callback(); 57 | }); 58 | }, 59 | function (callback) { 60 | global.mysql.query("SELECT * FROM payments WHERE " + where_str).then(function (rows) { 61 | console.log("Found rows in payments table: " + rows.length); 62 | rows2remove += rows.length; 63 | callback(); 64 | }); 65 | }, 66 | function (callback) { 67 | const address = global.database.getCache(user); 68 | const stats = global.database.getCache("stats:" + user); 69 | const history = global.database.getCache("history:" + user); 70 | const identifiers = global.database.getCache("identifiers:" + user); 71 | 72 | if (address != false) console.log("Cache key is not empty: " + user); 73 | if (stats != false) console.log("Cache key is not empty: " + "stats:" + user); 74 | if (history != false) console.log("Cache key is not empty: " + "history:" + user); 75 | if (identifiers != false) console.log("Cache key is not empty: " + "identifiers:" + user); 76 | callback(); 77 | 78 | }, 79 | function (callback) { 80 | if (!rows2remove) { // to check that we accidently do not remove something usefull from LMDB cache 81 | console.error("User was not found in SQL. Refusing to proceed to LMDB cache cleaning"); 82 | process.exit(1); 83 | } 84 | callback(); 85 | 86 | }, 87 | function (callback) { 88 | global.mysql.query("DELETE FROM users WHERE username = ?", [user]).then(function (rows) { 89 | console.log("DELETE FROM users WHERE username = " + user); 90 | callback(); 91 | }); 92 | }, 93 | function (callback) { 94 | global.mysql.query("DELETE FROM balance WHERE " + where_str).then(function (rows) { 95 | console.log("DELETE FROM balance WHERE " + where_str); 96 | callback(); 97 | }); 98 | }, 99 | function (callback) { 100 | global.mysql.query("DELETE FROM payments WHERE " + where_str).then(function (rows) { 101 | console.log("DELETE FROM payments WHERE " + where_str); 102 | callback(); 103 | }); 104 | }, 105 | function (callback) { 106 | console.log("Deleting LMDB cache keys"); 107 | let txn = global.database.env.beginTxn(); 108 | if (global.database.getCache(user)) txn.del(global.database.cacheDB, user); 109 | if (global.database.getCache("stats:" + user)) txn.del(global.database.cacheDB, "stats:" + user); 110 | if (global.database.getCache("history:" + user)) txn.del(global.database.cacheDB, "history:" + user); 111 | if (global.database.getCache("identifiers:" + user)) txn.del(global.database.cacheDB, "identifiers:" + user); 112 | txn.commit(); 113 | callback(); 114 | }, 115 | function (callback) { 116 | console.log("DONE"); 117 | process.exit(0); 118 | } 119 | ]); 120 | }); 121 | -------------------------------------------------------------------------------- /manage_scripts/user_del_force.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const mysql = require("promise-mysql"); 3 | const async = require("async"); 4 | const argv = require('minimist')(process.argv.slice(2)); 5 | 6 | if (!argv.user) { 7 | console.error("Please specify user address to delete"); 8 | process.exit(1); 9 | } 10 | const user = argv.user; 11 | 12 | require("../init_mini.js").init(function() { 13 | const parts = user.split("."); 14 | const address = parts.length === 1 ? user : parts[0]; 15 | const payment_id = parts.length === 2 ? parts[1] : null; 16 | 17 | console.log("Address: " + address); 18 | console.log("PaymentID: " + payment_id); 19 | console.log("Max payment to remove: " + global.config.payout.walletMin); 20 | let rows2remove = 0; 21 | 22 | const where_str = payment_id === null ? "payment_address = '" + address + "' AND (payment_id IS NULL OR payment_id = '')" 23 | : "payment_address = '" + address + "' AND payment_id = '" + payment_id + "'"; 24 | 25 | async.waterfall([ 26 | function (callback) { 27 | global.mysql.query("SELECT * FROM users WHERE username = ?", [user]).then(function (rows) { 28 | if (rows.length > 1) { 29 | console.error("Too many users were selected!"); 30 | process.exit(1); 31 | } 32 | console.log("Found rows in users table: " + rows.length); 33 | rows2remove += rows.length; 34 | callback(); 35 | }); 36 | }, 37 | function (callback) { 38 | global.mysql.query("SELECT * FROM balance WHERE " + where_str).then(function (rows) { 39 | if (rows.length > 1) { 40 | console.error("Too many users were selected!"); 41 | process.exit(1); 42 | } 43 | if (rows.length === 1 && rows[0].amount >= global.support.decimalToCoin(global.config.payout.walletMin)) { 44 | console.error("Too big payment left: " + global.support.coinToDecimal(rows[0].amount)); 45 | process.exit(1); 46 | } 47 | console.log("Found rows in balance table: " + rows.length); 48 | rows2remove += rows.length; 49 | callback(); 50 | }); 51 | }, 52 | function (callback) { 53 | global.mysql.query("SELECT * FROM payments WHERE " + where_str).then(function (rows) { 54 | console.log("Found rows in payments table: " + rows.length); 55 | rows2remove += rows.length; 56 | callback(); 57 | }); 58 | }, 59 | function (callback) { 60 | global.mysql.query("SELECT * FROM block_balance WHERE " + where_str).then(function (rows) { 61 | console.log("Found rows in block_balance table: " + rows.length); 62 | rows2remove += rows.length; 63 | callback(); 64 | }); 65 | }, 66 | function (callback) { 67 | const address = global.database.getCache(user); 68 | const stats = global.database.getCache("stats:" + user); 69 | const history = global.database.getCache("history:" + user); 70 | const identifiers = global.database.getCache("identifiers:" + user); 71 | 72 | if (address != false) console.log("Cache key is not empty: " + user); 73 | if (stats != false) console.log("Cache key is not empty: " + "stats:" + user); 74 | if (history != false) console.log("Cache key is not empty: " + "history:" + user); 75 | if (identifiers != false) console.log("Cache key is not empty: " + "identifiers:" + user); 76 | callback(); 77 | 78 | }, 79 | function (callback) { 80 | if (!rows2remove) { // to check that we accidently do not remove something usefull from LMDB cache 81 | console.error("User was not found in SQL. Refusing to proceed to LMDB cache cleaning"); 82 | process.exit(1); 83 | } 84 | callback(); 85 | 86 | }, 87 | function (callback) { 88 | global.mysql.query("DELETE FROM users WHERE username = ?", [user]).then(function (rows) { 89 | console.log("DELETE FROM users WHERE username = " + user); 90 | callback(); 91 | }); 92 | }, 93 | function (callback) { 94 | global.mysql.query("DELETE FROM balance WHERE " + where_str).then(function (rows) { 95 | console.log("DELETE FROM balance WHERE " + where_str); 96 | callback(); 97 | }); 98 | }, 99 | function (callback) { 100 | global.mysql.query("DELETE FROM payments WHERE " + where_str).then(function (rows) { 101 | console.log("DELETE FROM payments WHERE " + where_str); 102 | callback(); 103 | }); 104 | }, 105 | function (callback) { 106 | global.mysql.query("DELETE FROM block_balance WHERE " + where_str).then(function (rows) { 107 | console.log("DELETE FROM block_balance WHERE " + where_str); 108 | callback(); 109 | }); 110 | }, 111 | function (callback) { 112 | console.log("Deleting LMDB cache keys"); 113 | let txn = global.database.env.beginTxn(); 114 | if (global.database.getCache(user)) txn.del(global.database.cacheDB, user); 115 | if (global.database.getCache("stats:" + user)) txn.del(global.database.cacheDB, "stats:" + user); 116 | if (global.database.getCache("history:" + user)) txn.del(global.database.cacheDB, "history:" + user); 117 | if (global.database.getCache("identifiers:" + user)) txn.del(global.database.cacheDB, "identifiers:" + user); 118 | txn.commit(); 119 | callback(); 120 | }, 121 | function (callback) { 122 | console.log("DONE"); 123 | process.exit(0); 124 | } 125 | ]); 126 | }); 127 | -------------------------------------------------------------------------------- /lib/longRunner.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const async = require("async"); 4 | 5 | function cleanCacheDB() { 6 | console.log("Cleaning up the cache DB. Searching for items to delete/update"); 7 | let txn = global.database.env.beginTxn({readOnly: true}); 8 | let cursor = new global.database.lmdb.Cursor(txn, global.database.cacheDB); 9 | let updated = {}; 10 | let deleted = []; 11 | for (let found = cursor.goToFirst(); found; found = cursor.goToNext()) { 12 | cursor.getCurrentString(function(key, data){ // jshint ignore:line 13 | if (key.length < global.config.pool.address.length) return; // min XMR address length 14 | 15 | if (key.includes("identifiers:")) { // remove frozen worker names after 24h 16 | let parts = key.split(/:(.+)/); 17 | let key2 = parts[1]; 18 | 19 | try { 20 | let data2 = JSON.parse(data); 21 | if (data2.length == 0) return; 22 | let isAlive = false; 23 | for (let i in data2) { 24 | let stats = global.database.getCache("stats:" + key2 + "_" + data2[i]); 25 | if (stats && Date.now() - stats.lastHash <= 24*60*60*1000) isAlive = true; 26 | } 27 | if (!isAlive) { 28 | updated[key] = JSON.stringify([]); 29 | } 30 | 31 | } catch (e) { 32 | console.error("Bad cache data with " + key + " key"); 33 | } 34 | 35 | } else if (key.includes("_") && !key.includes("history:") && !key.includes("stats:")) { // remove week old workers 36 | let stats = global.database.getCache("stats:" + key); 37 | if (!stats) return; 38 | if (!global.database.getCache("history:" + key)) return; 39 | if (Date.now() - stats.lastHash > 7*24*60*60*1000) { 40 | deleted.push(key); 41 | deleted.push("history:" + key); 42 | deleted.push("stats:" + key); 43 | } 44 | 45 | } else if (!key.includes("_") && key.includes("stats:")) { // zero frozen account hashrate after 24h 46 | try { 47 | let data2 = JSON.parse(data); 48 | if ((data2.hash || data2.hash2) && Date.now() - data2.lastHash > 24*60*60*1000) { 49 | data2.hash = data2.hash2 = 0; 50 | updated[key] = JSON.stringify(data2); 51 | } 52 | } catch (e) { 53 | console.error("Bad cache data with " + key + " key"); 54 | } 55 | 56 | } 57 | }); 58 | } 59 | 60 | cursor.close(); 61 | txn.commit(); 62 | 63 | console.log("Deleting cache items: " + deleted.length); 64 | 65 | let chunkSize = 0; 66 | txn = global.database.env.beginTxn(); 67 | deleted.forEach(function(key) { 68 | ++ chunkSize; 69 | txn.del(global.database.cacheDB, key); 70 | if (chunkSize > 500) { 71 | txn.commit(); 72 | txn = global.database.env.beginTxn(); 73 | chunkSize = 0; 74 | } 75 | }); 76 | txn.commit(); 77 | 78 | console.log("Updating cache items: " + Object.keys(updated).length); 79 | 80 | chunkSize = 0; 81 | txn = global.database.env.beginTxn(); 82 | for (const [key, value] of Object.entries(updated)) { 83 | ++ chunkSize; 84 | txn.putString(global.database.cacheDB, key, value); 85 | if (chunkSize > 500) { 86 | txn.commit(); 87 | txn = global.database.env.beginTxn(); 88 | chunkSize = 0; 89 | } 90 | } 91 | txn.commit(); 92 | } 93 | 94 | function cleanAltBlockDB() { 95 | console.log("Cleaning up the alt block DB. Searching for items to delete"); 96 | let txn = global.database.env.beginTxn({readOnly: true}); 97 | let cursor = new global.database.lmdb.Cursor(txn, global.database.altblockDB); 98 | let deleted = []; 99 | 100 | let block_count = {}; 101 | for (let found = cursor.goToLast(); found; found = cursor.goToPrev()) { 102 | cursor.getCurrentBinary(function(key, data){ // jshint ignore:line 103 | let blockData = global.protos.AltBlock.decode(data); 104 | if (!(blockData.port in block_count)) block_count[blockData.port] = 0; 105 | ++ block_count[blockData.port]; 106 | if (blockData.unlocked && (block_count[blockData.port] > 20000 || Date.now() - blockData.timestamp > 3*365*24*60*60*1000)) { 107 | deleted.push(key); 108 | } 109 | }); 110 | } 111 | 112 | cursor.close(); 113 | txn.commit(); 114 | 115 | console.log("Deleting altblock items: " + deleted.length); 116 | 117 | let chunkSize = 0; 118 | txn = global.database.env.beginTxn(); 119 | deleted.forEach(function(key) { 120 | ++ chunkSize; 121 | txn.del(global.database.altblockDB, key); 122 | if (chunkSize > 500) { 123 | txn.commit(); 124 | txn = global.database.env.beginTxn(); 125 | chunkSize = 0; 126 | } 127 | }); 128 | txn.commit(); 129 | } 130 | 131 | //let saw_block_hash_before = {}; 132 | 133 | let cleanBlockBalanceTableQueue = async.queue(function (task, callback) { 134 | global.mysql.query("DELETE FROM block_balance WHERE hex = ?", [task.hex]).then(function () { 135 | return callback(true); 136 | }).catch(function (error) { 137 | console.error("SQL query failed: " + error); 138 | return callback(false); 139 | }); 140 | }, 10); 141 | 142 | setInterval(function(queue_obj){ 143 | if (queue_obj.length()){ 144 | console.log("Remove block balance queue length: " + queue_obj.length()); 145 | } 146 | }, 60*1000, cleanBlockBalanceTableQueue); 147 | 148 | function cleanBlockBalanceTable() { 149 | console.log("Cleaning up the block balance table"); 150 | 151 | let locked_block_hashes = {}; 152 | global.database.getValidLockedBlocks().forEach(function (block) { locked_block_hashes[block.hash] = 1; }); 153 | global.database.getValidLockedAltBlocks().forEach(function (block) { locked_block_hashes[block.hash] = 1; }); 154 | 155 | console.log("Starting cleaning the block balance table. Found " + Object.keys(locked_block_hashes).length + " locked blocks"); 156 | global.mysql.query("SELECT hex FROM paid_blocks WHERE paid_time > (NOW() - INTERVAL 2 DAY)").then(function (rows_keep) { 157 | console.log("Got " + rows_keep.length + " recent blocks"); 158 | rows_keep.forEach(function (row) { locked_block_hashes[row.hex] = 1; }); 159 | let deleted_row_count = 0; 160 | global.mysql.query("SELECT DISTINCT hex FROM block_balance").then(function (rows) { 161 | console.log("Got " + rows.length + " block balance blocks"); 162 | rows.forEach(function (row) { 163 | if (row.hex in locked_block_hashes) return; 164 | //if (row.hex in saw_block_hash_before) { 165 | cleanBlockBalanceTableQueue.push(row, function () {}); 166 | //delete saw_block_hash_before[row.hex]; 167 | ++ deleted_row_count; 168 | //} else { 169 | // saw_block_hash_before[row.hex] = 1; 170 | //} 171 | }); 172 | console.log("Finished preparing the block balance table. Removing " + deleted_row_count + " rows (" + Object.keys(locked_block_hashes).length + " locked)."); 173 | }).catch(function (error) { 174 | console.error("SQL query failed: " + error); 175 | }); 176 | }).catch(function (error) { 177 | console.error("SQL query failed: " + error); 178 | }); 179 | } 180 | 181 | console.log("Cleaning up the share DB"); 182 | global.database.cleanShareDB(); 183 | cleanCacheDB(); 184 | cleanAltBlockDB(); 185 | cleanBlockBalanceTable(); 186 | 187 | setInterval(function(){ 188 | console.log("Cleaning up the share DB"); 189 | global.database.cleanShareDB(); 190 | }, 4*60*60*1000); 191 | 192 | // clean cache items 193 | setInterval(function(){ 194 | cleanCacheDB(); 195 | }, 24*60*60*1000); 196 | 197 | // clean altblock items 198 | setInterval(function(){ 199 | cleanAltBlockDB(); 200 | }, 7*24*60*60*1000); 201 | 202 | // clean block balance table 203 | setInterval(function(){ 204 | cleanBlockBalanceTable(); 205 | }, 24*60*60*1000); 206 | -------------------------------------------------------------------------------- /deployment/deploy.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | 3 | NODEJS_VERSION=v20.12.2 4 | 5 | WWW_DNS=$1 6 | API_DNS=$2 7 | CF_DNS_API_TOKEN=$3 8 | CERTBOT_EMAIL=$4 9 | 10 | test -z $WWW_DNS && WWW_DNS="moneroocean.stream" 11 | test -z $API_DNS && API_DNS="api.moneroocean.stream" 12 | test -z $CF_DNS_API_TOKEN && CF_DNS_API_TOKEN="n/a" 13 | test -z $CERTBOT_EMAIL && CERTBOT_EMAIL="support@moneroocean.stream" 14 | 15 | if [[ $(whoami) != "root" ]]; then 16 | echo "Please run this script as root" 17 | exit 1 18 | fi 19 | 20 | DEBIAN_FRONTEND=noninteractive apt-get update 21 | DEBIAN_FRONTEND=noninteractive apt-get full-upgrade -y 22 | timedatectl set-timezone Etc/UTC 23 | 24 | adduser --disabled-password --gecos "" user 25 | grep -q "user ALL=(ALL) NOPASSWD:ALL" /etc/sudoers || echo "user ALL=(ALL) NOPASSWD:ALL" >>/etc/sudoers 26 | su user -c "mkdir /home/user/.ssh" 27 | if [ -f "/root/.ssh/authorized_keys" ]; then 28 | mv /root/.ssh/authorized_keys /home/user/.ssh/authorized_keys 29 | chown user:user /home/user/.ssh/authorized_keys 30 | chmod 600 /home/user/.ssh/authorized_keys 31 | sed -i 's/#\?PasswordAuthentication yes/PasswordAuthentication no/g' /etc/ssh/sshd_config 32 | sed -i 's/#\?PermitRootLogin .\+/PermitRootLogin no/g' /etc/ssh/sshd_config 33 | sed -i 's/#\?PermitEmptyPasswords .\+/PermitEmptyPasswords no/g' /etc/ssh/sshd_config 34 | service ssh restart 35 | fi 36 | 37 | ufw default deny incoming 38 | ufw default allow outgoing 39 | ufw allow ssh 40 | ufw allow 443 41 | ufw --force enable 42 | 43 | cat >/root/.vimrc <<'EOF' 44 | colorscheme desert 45 | set fo-=ro 46 | EOF 47 | 48 | cat >/home/user/.vimrc <<'EOF' 49 | colorscheme desert 50 | set fo-=ro 51 | EOF 52 | chown user:user /home/user/.vimrc 53 | 54 | DEBIAN_FRONTEND=noninteractive apt-get install -y nginx ntp sudo 55 | snap install --classic certbot 56 | snap set certbot trust-plugin-with-root=ok 57 | snap install certbot-dns-cloudflare 58 | find /snap/certbot -name options-ssl-nginx.conf | xargs -I{} cp {} /etc/letsencrypt/options-ssl-nginx.conf 59 | echo "dns_cloudflare_api_token=$CF_DNS_API_TOKEN" >/root/dns_cloudflare_api_token.ini 60 | chmod 600 /root/dns_cloudflare_api_token.ini 61 | certbot certonly --non-interactive --agree-tos --email "$CERTBOT_EMAIL" --dns-cloudflare --dns-cloudflare-propagation-seconds 30 --dns-cloudflare-credentials /root/dns_cloudflare_api_token.ini -d $WWW_DNS 62 | certbot certonly --non-interactive --agree-tos --email "$CERTBOT_EMAIL" --dns-cloudflare --dns-cloudflare-propagation-seconds 30 --dns-cloudflare-credentials /root/dns_cloudflare_api_token.ini -d $API_DNS 63 | cat >/etc/nginx/sites-enabled/default <~/wallets/wallet_pass 131 | echo 1 | /usr/local/src/monero/build/release/bin/monero-wallet-cli --offline --create-address-file --generate-new-wallet ~/wallets/wallet --password-file ~/wallets/wallet_pass --command address 132 | echo 1 | /usr/local/src/monero/build/release/bin/monero-wallet-cli --offline --create-address-file --generate-new-wallet ~/wallets/wallet_fee --password-file ~/wallets/wallet_pass --command address 133 | EOF 134 | ) | su user -l 135 | echo; echo; echo 136 | read -p "*** Write down your seeds for wallet and wallet_fee listed above and press ENTER to continue ***" 137 | 138 | cat >/lib/systemd/system/monero.service <<'EOF' 139 | [Unit] 140 | Description=Monero Daemon 141 | After=network.target 142 | 143 | [Service] 144 | ExecStart=/usr/local/src/monero/build/release/bin/monerod --hide-my-port --prune-blockchain --enable-dns-blocklist --no-zmq --out-peers 64 --non-interactive --restricted-rpc --block-notify '/bin/bash /home/user/nodejs-pool/block_notify.sh' 145 | Restart=always 146 | User=monerodaemon 147 | Nice=10 148 | CPUQuota=400% 149 | 150 | [Install] 151 | WantedBy=multi-user.target 152 | EOF 153 | 154 | useradd -m monerodaemon -d /home/monerodaemon 155 | systemctl daemon-reload 156 | systemctl enable monero 157 | systemctl start monero 158 | 159 | sleep 30 160 | echo "Please wait until Monero daemon is fully synced" 161 | tail -f /home/monerodaemon/.bitmonero/bitmonero.log 2>/dev/null | grep Synced & 162 | ( tail -F -n0 /home/monerodaemon/.bitmonero/bitmonero.log & ) | egrep -q "You are now synchronized with the network" 163 | killall tail 2>/dev/null 164 | echo "Monero daemon is synced" 165 | 166 | DEBIAN_FRONTEND=noninteractive apt-get install -y mysql-server 167 | ROOT_SQL_PASS=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) 168 | (cat </root/mysql_pass 174 | chmod 600 /root/mysql_pass 175 | grep max_connections /etc/mysql/my.cnf || cat >>/etc/mysql/my.cnf <<'EOF' 176 | [mysqld] 177 | max_connections = 10000 178 | EOF 179 | systemctl restart mysql 180 | 181 | (cat <>/home/user/.bashrc 201 | echo 'export PATH=/home/user/.bin:$PATH' >>/home/user/.bashrc 202 | for i in mdb_copy mdb_dump mdb_load mdb_stat; do cp \$i /home/user/.bin/; done 203 | ) 204 | npm install -g pm2 205 | pm2 install pm2-logrotate 206 | openssl req -subj "/C=IT/ST=Pool/L=Daemon/O=Mining Pool/CN=mining.pool" -newkey rsa:2048 -nodes -keyout cert.key -x509 -out cert.pem -days 36500 207 | mkdir /home/user/pool_db 208 | sed -r 's#("db_storage_path": ).*#\1"/home/user/pool_db/",#' config_example.json >config.json 209 | mysql -u root --password=$ROOT_SQL_PASS 0) { 33 | this.deq(); 34 | i = this.size(); 35 | } 36 | }; 37 | 38 | return buffer; 39 | } 40 | 41 | // accumulates email notifications up to one hour (email/subject -> body) 42 | let emailAcc = {}; 43 | // last send time of email (email/subject -> time) 44 | let emailLastSendTime = {}; 45 | let lastEmailSendTime; 46 | 47 | function sendEmailReal(toAddress, subject, email_body, retry) { 48 | if (lastEmailSendTime && Date.now() - lastEmailSendTime < 1000) { 49 | setTimeout(sendEmailReal, 1000, toAddress, subject, email_body, retry); 50 | return; 51 | } 52 | lastEmailSendTime = Date.now(); 53 | const body = JSON.stringify({ 54 | from: global.config.general.emailFrom, 55 | to: toAddress, 56 | subject: subject, 57 | text: email_body 58 | }) + "\n"; 59 | request.post(global.config.general.mailgunURL, { 60 | body: body, 61 | agentOptions: { 62 | rejectUnauthorized: global.config.general.mailgunNoCert === true ? false : true 63 | }, 64 | headers: { 65 | "Content-Type": "application/json", 66 | "Accept": "application/json", 67 | "Content-Length": body.length, 68 | "Connection": "close" 69 | } 70 | }, function(err, response, body) { 71 | if (!err && response.statusCode === 200) { 72 | debug(email_body); 73 | console.log("Email to '" + toAddress + "' was sent successfully! Response: " + body); 74 | } else { 75 | if (retry) { 76 | console.error("Did not send e-mail to '" + toAddress + "' successfully! Response: " + body + " Response: "+JSON.stringify(response)); 77 | } else { 78 | setTimeout(sendEmailReal, 50*1000, toAddress, subject, email_body, 1); 79 | } 80 | } 81 | }); 82 | } 83 | 84 | function sendEmail(toAddress, subject, body, wallet){ 85 | if (toAddress === global.config.general.adminEmail && !subject.includes("FYI")) { 86 | sendEmailReal(toAddress, subject, body); 87 | } else { 88 | let reEmail = /^([a-zA-Z0-9_\.-])+@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/; 89 | if (!reEmail.test(toAddress)) { 90 | debug("Avoid sending email to invalid address '" + toAddress + "'"); 91 | return; 92 | } 93 | let key = toAddress + "\t" + subject; 94 | if (!(key in emailAcc)) { 95 | emailAcc[key] = body; 96 | let time_now = Date.now(); 97 | let is_fast_email = !(key in emailLastSendTime) || time_now - emailLastSendTime[key] > 6*60*60*1000; 98 | emailLastSendTime[key] = time_now; 99 | setTimeout(function(email_address, email_subject, wallet) { 100 | let key2 = email_address + "\t" + email_subject; 101 | let email_body = emailAcc[key2]; 102 | delete emailAcc[key2]; 103 | let emailData = { 104 | wallet: wallet 105 | }; 106 | sendEmailReal(email_address, email_subject, "Hello,\n\n" + email_body + "\n\nThank you,\n" + sprintf(global.config.general.emailSig, emailData)); 107 | }, (is_fast_email ? 5 : 30)*60*1000, toAddress, subject, wallet); 108 | } else { 109 | emailAcc[key] += body; 110 | } 111 | } 112 | } 113 | 114 | function sendEmailAdmin(subject, body){ 115 | sendEmail(global.config.general.adminEmail, subject, body); 116 | } 117 | 118 | function jsonRequest(host, port, data, callback, path, timeout) { 119 | let options = { 120 | url: (global.config.rpc.https ? "https://" : "http://") + host + ":" + port + "/" + path, 121 | method: data ? "POST" : "GET", 122 | timeout: timeout, 123 | headers: { 124 | "Content-Type": "application/json", 125 | "Accept": "application/json", 126 | "Connection": "close", 127 | } 128 | }; 129 | if (global.config.daemon.basicAuth) { 130 | options.headers["Authorization"] = global.config.daemon.basicAuth; 131 | } 132 | if (global.config.daemon["X-API-KEY"]) { 133 | options.headers["X-API-KEY"] = global.config.daemon["X-API-KEY"]; 134 | options.headers["api_key"] = global.config.daemon["X-API-KEY"]; 135 | } 136 | 137 | if (data) { 138 | const data2 = typeof data === 'string' ? data : JSON.stringify(data); 139 | options.headers["Content-Length"] = data2.length; 140 | options.body = data2; 141 | } 142 | let reply_fn = function (err, res, body) { 143 | if (err) { 144 | if (typeof(err) === "string") console.error("Error doing " + uri + path + " request: " + err); 145 | return callback(err); 146 | } 147 | let json; 148 | try { 149 | json = JSON.parse(body); 150 | } catch (e) { 151 | debug("JSON parse exception: " + body); 152 | return callback("JSON parse exception: " + body); 153 | } 154 | debug("JSON result: " + JSON.stringify(json)); 155 | return callback(json, res.statusCode); 156 | }; 157 | debug("JSON REQUST: " + JSON.stringify(options)); 158 | request(options, reply_fn); 159 | } 160 | 161 | function rpc(host, port, method, params, callback, timeout) { 162 | let data = { 163 | id: "0", 164 | jsonrpc: "2.0", 165 | method: method, 166 | params: params 167 | }; 168 | return jsonRequest(host, port, data, callback, 'json_rpc', timeout); 169 | } 170 | 171 | function rpc2(host, port, method, params, callback, timeout) { 172 | return jsonRequest(host, port, params, callback, method, timeout); 173 | } 174 | 175 | function https_get(url, callback) { 176 | let timer; 177 | let is_callback_called = false; 178 | var req = https.get(url, function(res) { 179 | if (res.statusCode != 200) { 180 | if (timer) clearTimeout(timer); 181 | console.error("URL " + url + ": Result code: " + res.statusCode); 182 | if (!is_callback_called) { 183 | is_callback_called = true; 184 | callback(null); 185 | } 186 | return; 187 | } 188 | let str = ""; 189 | res.on('data', function(d) { str += d; }); 190 | res.on('end', function() { 191 | if (timer) clearTimeout(timer); 192 | let json; 193 | try { 194 | json = JSON.parse(str); 195 | } catch (e) { 196 | console.error("URL " + url + ": JSON parse exception: " + e); 197 | if (!is_callback_called) { 198 | is_callback_called = true; 199 | callback(str); 200 | } 201 | return; 202 | } 203 | if (!is_callback_called) { 204 | is_callback_called = true; 205 | callback(json); 206 | } 207 | return; 208 | }); 209 | res.on('error', function() { 210 | if (timer) clearTimeout(timer); 211 | console.error("URL " + url + ": RESPONSE ERROR!"); 212 | if (!is_callback_called) { 213 | is_callback_called = true; 214 | callback(null); 215 | } 216 | }); 217 | }); 218 | req.on('error', function() { 219 | if (timer) clearTimeout(timer); 220 | console.error("URL " + url + ": REQUEST ERROR!"); 221 | if (!is_callback_called) { 222 | is_callback_called = true; 223 | callback(null); 224 | } 225 | }); 226 | timer = setTimeout(function() { 227 | req.abort(); 228 | console.error("URL " + url + ": TIMEOUT!"); 229 | if (!is_callback_called) { 230 | is_callback_called = true; 231 | callback(null); 232 | } 233 | }, 30*1000); 234 | req.end(); 235 | } 236 | 237 | function getCoinHashFactor(coin, callback) { 238 | global.mysql.query("SELECT item_value FROM config WHERE module = 'daemon' and item = 'coinHashFactor" + coin + "'").then(function (rows) { 239 | if (rows.length != 1) { 240 | console.error("Can't get config.daemon.coinHashFactor" + coin + " value"); 241 | return callback(null); 242 | } 243 | callback(parseFloat(rows[0].item_value)); 244 | }).catch(function (error) { 245 | console.error("SQL query failed: " + error); 246 | return callback(0); 247 | }); 248 | } 249 | 250 | function setCoinHashFactor(coin, coinHashFactor) { 251 | global.mysql.query("UPDATE config SET item_value = ? WHERE module = 'daemon' and item = 'coinHashFactor" + coin + "'", [coinHashFactor]).catch(function (error) { 252 | console.error("SQL query failed: " + error); 253 | }); 254 | global.config.daemon["coinHashFactor" + coin] = coinHashFactor; 255 | } 256 | 257 | function formatDate(date) { 258 | // Date formatting for MySQL date time fields. 259 | return moment(date).format('YYYY-MM-DD HH:mm:ss'); 260 | } 261 | 262 | function formatDateFromSQL(date) { 263 | // Date formatting for MySQL date time fields. 264 | let ts = new Date(date); 265 | return Math.floor(ts.getTime() / 1000); 266 | } 267 | 268 | function coinToDecimal(amount) { 269 | return amount / global.config.coin.sigDigits; 270 | } 271 | 272 | function decimalToCoin(amount) { 273 | return Math.round(amount * global.config.coin.sigDigits); 274 | } 275 | 276 | function blockCompare(a, b) { 277 | if (a.height < b.height) { 278 | return 1; 279 | } 280 | 281 | if (a.height > b.height) { 282 | return -1; 283 | } 284 | return 0; 285 | } 286 | 287 | function tsCompare(a, b) { 288 | if (a.ts < b.ts) { 289 | return 1; 290 | } 291 | 292 | if (a.ts > b.ts) { 293 | return -1; 294 | } 295 | return 0; 296 | } 297 | 298 | function port_wallet_ip(port) { 299 | const ip = global.config.wallet["address_" + port.toString()]; 300 | if (ip) return ip; 301 | return global.config.wallet.address; 302 | } 303 | 304 | module.exports = function () { 305 | return { 306 | rpcDaemon: function (method, params, callback) { 307 | rpc(global.config.daemon.address, global.config.daemon.port, method, params, callback, 30*1000); 308 | }, 309 | rpcPortDaemon: function (port, method, params, callback) { 310 | rpc(global.config.daemon.address, port, method, params, callback, 30*1000); 311 | }, 312 | rpcPortDaemon2: function (port, method, params, callback) { 313 | rpc2(global.config.daemon.address, port, method, params, callback, 30*1000); 314 | }, 315 | rpcWallet: function (method, params, callback) { 316 | rpc(port_wallet_ip(global.config.wallet.port), global.config.wallet.port, method, params, callback, 30*60*1000); 317 | }, 318 | rpcPortWallet: function (port, method, params, callback) { 319 | rpc(port_wallet_ip(port), port, method, params, callback, 30*60*1000); 320 | }, 321 | rpcPortWallet2: function (port, method, params, callback) { 322 | rpc2(port_wallet_ip(port), port, method, params, callback, 30*60*1000); 323 | }, 324 | rpcPortWalletShort: function (port, method, params, callback) { 325 | rpc(port_wallet_ip(port), port, method, params, callback, 10*1000); 326 | }, 327 | rpcPortWalletShort2: function (port, method, params, callback) { 328 | rpc2(port_wallet_ip(port), port, method, params, callback, 10*1000); 329 | }, 330 | circularBuffer: circularBuffer, 331 | formatDate: formatDate, 332 | coinToDecimal: coinToDecimal, 333 | decimalToCoin: decimalToCoin, 334 | formatDateFromSQL: formatDateFromSQL, 335 | blockCompare: blockCompare, 336 | sendEmail: sendEmail, 337 | sendEmailAdmin: sendEmailAdmin, 338 | tsCompare: tsCompare, 339 | getCoinHashFactor: getCoinHashFactor, 340 | setCoinHashFactor: setCoinHashFactor, 341 | https_get: https_get, 342 | }; 343 | }; 344 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Pool Design/Theory 2 | ================== 3 | The nodejs-pool is built around a small series of core daemons that share access to a single LMDB table for tracking of shares, 4 | with MySQL being used to centralize configurations and ensure simple access from local/remote nodes. The core daemons follow: 5 | 6 | ```text 7 | api - Main API for the frontend to use and pull data from. Expects to be hosted at / 8 | remoteShare - Main API for consuming shares from remote/local pools. Expects to be hosted at /leafApi 9 | pool - Where the miners connect to. 10 | longRunner - Database share cleanup. 11 | payments - Handles all payments to workers. 12 | blockManager - Unlocks blocks and distributes payments into MySQL 13 | worker - Does regular processing of statistics and sends status e-mails for non-active miners. 14 | ``` 15 | 16 | API listens on port 8001, remoteShare listens on 8000. 17 | 18 | moneroocean.stream (The reference implementation) uses the following setup: 19 | * https://moneroocean.stream is hosted on its own server, as the main website is a static frontend 20 | * https://api.moneroocean.stream hosts api, remoteShare, longRunner, payments, blockManager, worker, as these must all be hosted with access to the same LMDB database. 21 | 22 | Setup Instructions 23 | ================== 24 | 25 | Server Requirements 26 | ------------------- 27 | * Ubuntu 24.04 (confirmed working) 28 | * 8 Gb Ram 29 | * 2 CPU Cores 30 | * 150 Gb SSD-Backed Storage - If you're doing a multi-server install, the leaf nodes do not need this much storage. They just need enough storage to hold the blockchain for your node. The pool comes configured to use up to 60Gb of storage for LMDB. Assuming you have the longRunner worker running, it should never get near this size, but be aware that it /can/ bloat readily if things error, so be ready for this! 31 | * Notably, this happens to be approximately the size of a 4Gb linode instance, which is where the majority of automated deployment testing happened! 32 | 33 | Pre-Deploy 34 | ---------- 35 | * If you're planning on using e-mail, you'll want to setup an account at https://mailgun.com (It's free for 10k e-mails/month!), so you can notify miners. This also serves as the backend for password reset emails, along with other sorts of e-mails from the pool, including pool startup, pool Monerod daemon lags, etc so it's highly suggested! 36 | * Pre-Generate the wallets, or don't, it's up to you! You'll need the addresses after the install is complete, so I'd suggest making sure you have them available. Information on suggested setups are found below. 37 | * Run installer from root user and it will make new "user" for pool usage later. 38 | 39 | Deployment via Installer 40 | ------------------------ 41 | 42 | 2. Run the [deploy script](https://raw.githubusercontent.com/MoneroOcean/nodejs-pool/master/deployment/deploy.bash) as a **ROOT USER**. This is very important! This script will install the pool to new user "user"! Also. Go get a coffee, this sucker bootstraps the monero installation. 43 | 3. Once it's complete, change as `config.json` appropriate. It is pre-loaded for a local install of everything, running on 127.0.0.1. This will work perfectly fine if you're using a single node setup. You'll also want to set `bind_ip` to the external IP of the pool server, and `hostname` to the resolvable hostname for the pool server. `pool_id` is mostly used for multi-server installations to provide unique identifiers in the backend. You will also want to run: source ~/.bashrc This will activate NVM and get things working for the following pm2 steps. 44 | 4. You'll need to change the API endpoint for the frontend code in the `moneroocean-gui/script.js` -- This will usually be `http(s)://api.` unless you tweak nginx! 45 | 5. The default database directory `/home/user/pool_db/` is already been created during startup. If you change the `db_storage_path` just make sure your user has write permissions for new path. Run: `pm2 restart api` to reload the API for usage. 46 | 8. Once you're happy with the settings, go ahead and add pool daemon on main/leaf pool node: 47 | 48 | ```shell 49 | cd ~/nodejs-pool/ 50 | pm2 start init.js --name=pool --kill-timeout 10000 --log-date-format="YYYY-MM-DD HH:mm:ss:SSS Z" -- --module=pool 51 | pm2 save 52 | pm2 startup 53 | ``` 54 | 55 | Install Script that assumes you have free Cloudflare DNS setup for your web (moneroocean.stream) and API (api.moneroocean.stream) endpoints, CLoudflare API Token from 56 | from https://dash.cloudflare.com/profile/api-tokens with "Zone.Zone (Read), Zone.DNS (Edit)" permissions and email for certbot updates: 57 | 58 | ```bash 59 | curl -L https://raw.githubusercontent.com/MoneroOcean/nodejs-pool/master/deployment/deploy.bash | bash -s -- -s moneroocean.stream api.moneroocean.stream "Cloudflare API Token" support@moneroocean.stream 60 | ``` 61 | 62 | Leaf server install script (only for pool module that you need to add/start as shown above after install and updating config.json to connect to your main pool DB server): 63 | ```bash 64 | curl -L https://raw.githubusercontent.com/MoneroOcean/nodejs-pool/master/deployment/leaf.bash | bash 65 | ``` 66 | 67 | Assumptions for the installer 68 | ----------------------------- 69 | The installer assumes that you will be running a single-node instance and using a clean Ubuntu 24.04 server install. The following system defaults are set: 70 | * MySQL Username: pool 71 | * MySQL Password: 98erhfiuehw987fh23d 72 | * MySQL Host: 127.0.0.1 73 | * MySQL root access is only permitted as the root user, the password is in `/root/mysql_pass` 74 | * SSL Certificate is generated, self-signed, but is valid for Claymore Miners. 75 | * The server installs and deploys Caddy as it's choice of web server! 76 | 77 | The pool comes pre-configured with values for Monero (XMR), these may need to be changed depending on the exact requirements of your coin. 78 | Other coins will likely be added down the road, and most likely will have configuration.sqls provided to overwrite the base configurations for their needs, but can be configured within the frontend as well. 79 | 80 | Wallet Setup 81 | ------------ 82 | The pool is designed to have a dual-wallet design, one which is a fee wallet, one which is the live pool wallet. 83 | The fee wallet is the default target for all fees owed to the pool owner. PM2 can also manage your wallet daemon, and that is the suggested run state. 84 | 85 | Manual Setup 86 | ------------ 87 | Pretty similar to the above, you may wish to dig through a few other things for sanity sake, but the installer scripts should give you a good idea of what to expect from the ground up. 88 | 89 | Manual SQL Configuration 90 | ------------------------ 91 | Until the full frontend is released, the following SQL information needs to be updated by hand in order to bring your pool online, in module/item format. You can also edit the values in sample_config.sql, then import them into SQL directly via an update. 92 | ``` 93 | Nice to have: 94 | general/mailgunKey 95 | general/mailgunURL 96 | general/emailFrom 97 | 98 | SQL import command: ```mysql -u root --password=$(sudo cat /root/mysql_pass)``` 99 | ``` 100 | 101 | The shareHost configuration is designed to be pointed at wherever the leafApi endpoint exists. For moneroocean.stream, we use https://api.moneroocean.stream/leafApi. 102 | If you're using the automated setup script, you can use: `http:///leafApi`, as Caddy will proxy it. If you're just using localhost and a local pool server, 103 | http://localhost:8000/leafApi will do you quite nicely. 104 | 105 | Additional ports can be added as desired, samples can be found at the end of base.sql. If you're not comfortable with the MySQL command line, 106 | I highly suggest MySQL Workbench or a similar piece of software (I use datagrip!). Your root MySQL password can be found in `/root/mysql_pass` 107 | 108 | Pool Update Procedures 109 | ====================== 110 | If upgrading the pool, please do a git pull to get the latest code within the pool's directory. 111 | 112 | Pool Troubleshooting 113 | ==================== 114 | 115 | API stopped updating! 116 | --------------------- 117 | This is likely due to LMDB's MDB_SIZE being hit, or due to LMDB locking up due to a reader staying open too long, possibly due to a software crash. 118 | The first step is to run: 119 | ``` 120 | mdb_stat -fear ~/pool_db/ 121 | ``` 122 | This should give you output like: 123 | ``` 124 | Environment Info 125 | Map address: (nil) 126 | Map size: 51539607552 127 | Page size: 4096 128 | Max pages: 12582912 129 | Number of pages used: 12582904 130 | Last transaction ID: 74988258 131 | Max readers: 512 132 | Number of readers used: 24 133 | Reader Table Status 134 | pid thread txnid 135 | 25763 7f4f0937b740 74988258 136 | Freelist Status 137 | Tree depth: 3 138 | Branch pages: 135 139 | Leaf pages: 29917 140 | Overflow pages: 35 141 | Entries: 591284 142 | Free pages: 12234698 143 | Status of Main DB 144 | Tree depth: 1 145 | Branch pages: 0 146 | Leaf pages: 1 147 | Overflow pages: 0 148 | Entries: 3 149 | Status of blocks 150 | Tree depth: 1 151 | Branch pages: 0 152 | Leaf pages: 1 153 | Overflow pages: 0 154 | Entries: 23 155 | Status of cache 156 | Tree depth: 3 157 | Branch pages: 16 158 | Leaf pages: 178 159 | Overflow pages: 2013 160 | Entries: 556 161 | Status of shares 162 | Tree depth: 2 163 | Branch pages: 1 164 | Leaf pages: 31 165 | Overflow pages: 0 166 | Entries: 4379344 167 | ``` 168 | The important thing to verify here is that the "Number of pages used" value is less than the "Max Pages" value, and that there are "Free pages" under "Freelist Status". 169 | If this is the case, them look at the "Reader Table Status" and look for the PID listed. Run: 170 | ```shell 171 | ps fuax | grep 172 | 173 | ex: 174 | ps fuax | grep 25763 175 | ``` 176 | 177 | If the output is not blank, then one of your node processes is reading, this is fine. If there is no output given on one of them, then proceed forwards. 178 | 179 | The second step is to run: 180 | ```shell 181 | pm2 restart blockManager worker payments remoteShare longRunner api 182 | ``` 183 | This will restart all of your related daemons, and will clear any open reader connections, allowing LMDB to get back to a normal state. 184 | 185 | If on the other hand, you have no "Free pages" and your Pages used is equal to the Max Pages, then you've run out of disk space for LMDB. 186 | You need to verify the cleaner is working. For reference, 4.3 million shares are stored within approximately 2-3 Gb of space, 187 | so if you're vastly exceeding this, then your cleaner (longRunner) is likely broken. 188 | 189 | Installation/Configuration Assistance 190 | ===================================== 191 | If you need help installing the pool from scratch, please have your servers ready, which would be Ubuntu 16.04 servers, blank and clean, DNS records pointed. 192 | These need to be x86_64 boxes with AES-NI Available. 193 | 194 | Installation assistance is 3 XMR, with a 1 XMR deposit, with remainder to be paid on completion. 195 | Configuration assistance is 2 XMR with a 1 XMR deposit, and includes debugging your pool configurations, ensuring that everything is running, and tuning for your uses/needs. 196 | altblockManager module source (that determines the most profitable coin to mine and trades them to main coin on exchanges, in particular Tradeogre, Xeggex, Binance, Coinex, Gate, Livecoin, Okex, Sevenseas) 197 | price is 20 XMR. 198 | 199 | SSH access with a sudo-enabled user will be needed, preferably the user that is slated to run the pool. 200 | 201 | Assistance is not available for frontend customization at this time. 202 | 203 | For assistance, please contact MoneroOcean at support@moneroocean.stream. 204 | 205 | Developer Donations 206 | =================== 207 | If you'd like to make a one time donation, the addresses are as follows: 208 | * XMR - ```89TxfrUmqJJcb1V124WsUzA78Xa3UYHt7Bg8RGMhXVeZYPN8cE5CZEk58Y1m23ZMLHN7wYeJ9da5n5MXharEjrm41hSnWHL``` 209 | * AEON - ```WmsEg3RuUKCcEvFBtXcqRnGYfiqGJLP1FGBYiNMgrcdUjZ8iMcUn2tdcz59T89inWr9Vae4APBNf7Bg2DReFP5jr23SQqaDMT``` 210 | * ETN - ```etnkQMp3Hmsay2p7uxokuHRKANrMDNASwQjDUgFb5L2sDM3jqUkYQPKBkooQFHVWBzEaZVzfzrXoETX6RbMEvg4R4csxfRHLo1``` 211 | * SUMO - ```Sumoo1DGS7c9LEKZNipsiDEqRzaUB3ws7YHfUiiZpx9SQDhdYGEEbZjRET26ewuYEWAZ8uKrz6vpUZkEVY7mDCZyGnQhkLpxKmy``` 212 | * GRFT - ```GACadqdXj5eNLnyNxvQ56wcmsmVCFLkHQKgtaQXNEE5zjMDJkWcMVju2aYtxbTnZgBboWYmHovuiH1Ahm4g2N5a7LuMQrpT``` 213 | * MSR - ```5hnMXUKArLDRue5tWsNpbmGLsLQibt23MEsV3VGwY6MGStYwfTqHkff4BgvziprTitbcDYYpFXw2rEgXeipsABTtEmcmnCK``` 214 | * LTHN - ```iz53aMEaKJ25zB8xku3FQK5VVvmu2v6DENnbGHRmn659jfrGWBH1beqAzEVYaKhTyMZcxLJAdaCW3Kof1DwTiTbp1DSqLae3e``` 215 | * WOW - ```Wo3yjV8UkwvbJDCB1Jy7vvXv3aaQu3K8YMG6tbY3Jo2KApfyf5RByZiBXy95bzmoR3AvPgNq6rHzm98LoHTkzjiA2dY7sqQMJ``` 216 | * XMV - ```XvyVfpAYp3zSuvdtoHgnDzMUf7GAeiumeUgVC7RTq6SfgtzGEzy4dUgfEEfD5adk1kN4dfVZdT3zZdgSD2xmVBs627Vwt2C3Ey``` 217 | * RYO - ```RYoLsi22qnoKYhnv1DwHBXcGe9QK6P9zmekwQnHdUAak7adFBK4i32wFTszivQ9wEPeugbXr2UD7tMd6ogf1dbHh76G5UszE7k1``` 218 | * XLA - ```SvkpUizij25ZGRHGb1c8ZTAHp3VyNFU3NQuQR1PtMyCqdpoZpaYAGMfG99z5guuoktY13nrhEerqYNKXvoxD7cUM1xA6Z5rRY``` 219 | * XHV - ```hvxyEmtbqs5TEk9U2tCxyfGx2dyGD1g8EBspdr3GivhPchkvnMHtpCR2fGLc5oEY42UGHVBMBANPge5QJ7BDXSMu1Ga2KFspQR``` 220 | * TUBE - ```TubedBNkgkTbd2CBmLQSwW58baJNghD9xdmctiRXjrW3dE8xpUcoXimY4J5UMrnUBrUDmfQrbxRYRX9s5tQe7pWYNF2QiAdH1Fh``` 221 | * LOKI - ```L6XqN6JDedz5Ub8KxpMYRCUoQCuyEA8EegEmeQsdP5FCNuXJavcrxPvLhpqY6emphGTYVrmAUVECsE9drafvY2hXUTJz6rW``` 222 | * TRTL - ```TRTLv2x2bac17cngo1r2wt3CaxN8ckoWHe2TX7dc8zW8Fc9dpmxAvhVX4u4zPjpv9WeALm2koBLF36REVvsLmeufZZ1Yx6uWkYG``` 223 | * XTNC - ```XtazhSxz1bbJLpT2JuiD2UWFUJYSFty5SVWuF6sy2w9v8pn69smkUxkTVCQc8NKCd6CBMNDGzgdPRYBKaHdbgZ5SNptVH1yPCTQ``` 224 | * IRD - ```ir3DHyB8Ub1aAHEewMeUxQ7b7tQdWa7VL8M5oXDPohS3Me4nhwvALXM4mym2kWg9VsceT75dm6XWiWF1K4zu8RVQ1HJD8Z3R9``` 225 | * ARQ - ```ar4Ha6ZQCkKRhkKQLfexv7VZQM2MhUmMmU9hmzswCPK4T3o2rbPKZM1GxEoYg4AFQsh57PsEets7sbpU958FAvxo2RkkTQ1gE``` 226 | * XWP - ```fh4MCJrakhWGoS6Meqp6UxGE1GNfAjKaRdPjW36rTffDiqvEq2HWEKZhrbYRw7XJb3CXxkjL3tcYGTT39m5qgjvk1ap4bVu1R``` 227 | * XEQ - ```Tvzp9tTmdGP9X8hCEw1Qzn18divQajJYTjR5HuUzHPKyLK5fzRt2X73FKBDzcnHMDJKdgsPhUDVrKHVcDJQVmLBg33NbkdjQb``` 228 | * XTA - ```ipN5cNhm7RXAGACP4ZXki4afT3iJ1A6Ka5U4cswE6fBPDcv8JpivurBj3vu1bXwPyb8KZEGsFUYMmToFG4N9V9G72X4WpAQ8L``` 229 | * DERO - ```dero1qygrgnz9gea2rqgwhdtpfpa3mvagt5uyq0g92nurwrpk6wnn7hdnzqgudsv6t``` 230 | * CCX - ```ccx7dmnBBoRPuVcpKJSAVZKdSDo9rc7HVijFbhG34jsXL3qiqfRwu7A5ecem44s2rngDd8y8N4QnYK6WR3mXAcAZ5iXun9BQBx``` 231 | * BLOC - ```abLoc5iUG4a6oAb2dqygxkS5M2uHWx16zHb9fUWMzpSEDwm6T7PSq2MLdHonWZ16CGfnJKRomq75aZyviTo6ZjHeYQMzNAEkjMg``` 232 | * ZEPH - ```ZEPHYR2nic7ULkkmgZNX8a9i2tMbkxuCqjgWZYuee3awX7RhtmhoT98CwGEGrruWZVSKtA7Z7JC8m7oeYHtBD9cBEZzdEh9BSdq4q``` 233 | * SAL - ```SaLvdWKnkz6MvVgxXr2TWSDSvESz6EBcz3wmMFch2sQuMYz2sUQGVNDYhkYaSuvkDr9GSYp5h6BeQHnGK8HzKhqGeZCZzG3AHS3``` 234 | * XTM - ```12FrDe5cUauXdMeCiG1DU3XQZdShjFd9A4p9agxsddVyAwpmz73x4b2Qdy5cPYaGmKNZ6g1fbCASJpPxnjubqjvHDa5``` 235 | * RVN - ```RLVJv9rQNHzXS3Zn4JH8hfAHmm1LfECMxy``` 236 | * XNA - ```Nb931jkFtFN7QWpu4FqSThaoKajYjS5iFZ``` 237 | * CLORE - ```AdXPHtV8yb86a8QKsbs8gmUpRpcxufRn8n``` 238 | * RTM - ```RUCyaEZxQu3Eure73XPQ57si813RYAMQKC``` 239 | * KCN - ```kc1qchtxq2gw9dc4r58hcegd6n4jspew6j9mu3yz8q``` 240 | * BTRM - ```Bfhtr2g56tg73TNZBRCu6fJUD39Kur6SGG``` 241 | * ERG - ```9fe533kUzAE57YfPP6o3nzsYMKN2W2uCxvg8KG8Vn5DDeJGetRw``` 242 | * BTC - ```3HRbMgcvbqHVW7P34MNGvF2Gh3DE26iHdw``` 243 | * BCH - ```18sKoDSjLCFW9kZrXuza1qzEERnKi7bx8S``` 244 | * ETH - ```0xfE23a61548FCCE159a541FAe9e16cEB92Da650ed``` 245 | * ETC - ```0x4480Ad73a113BEFf05B2079E38D90c9757Ecb063``` 246 | * LTC - ```MGj8PU1PpTNDDqRHmuEqfDpH3gxp6cJrUU``` 247 | 248 | Credits 249 | ======= 250 | 251 | [Zone117x](https://github.com/zone117x) - Original [node-cryptonote-pool](https://github.com/zone117x/node-cryptonote-pool) from which, the stratum implementation has been borrowed. 252 | 253 | [Snipa](https://github.com/Snipa22) - Original [nodejs-pool](https://github.com/Snipa22/nodejs-pool) from which, the original implementation has been borrowed. 254 | 255 | [Mesh00](https://github.com/mesh0000) - Frontend build in Angular JS [XMRPoolUI](https://github.com/mesh0000/poolui) 256 | 257 | [Wolf0](https://github.com/wolf9466/)/[OhGodAGirl](https://github.com/ohgodagirl) - Rebuild of node-multi-hashing with AES-NI [node-multi-hashing](https://github.com/Snipa22/node-multi-hashing-aesni) 258 | -------------------------------------------------------------------------------- /lib/worker.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const debug = require("debug")("worker"); 3 | const sprintf = require("sprintf-js").sprintf; 4 | 5 | let cycleCount = 0; 6 | let hashrate_avg_min = 10; 7 | let stat_change_alert = 0.6; 8 | 9 | let prev_pool_state_time; 10 | let prev_pool_hashrate; 11 | let prev_pool_workers; 12 | 13 | let stats_cache = {}; 14 | let miner_history_update_time = {}; 15 | 16 | let pool_type_str = {}; 17 | pool_type_str[global.protos.POOLTYPE.PPLNS] = 'pplns'; 18 | pool_type_str[global.protos.POOLTYPE.PPS] = 'pps'; 19 | pool_type_str[global.protos.POOLTYPE.SOLO] = 'solo'; 20 | 21 | let identifiers = {}; 22 | let minerSet = {}; 23 | let minerPortSet = {}; 24 | let localMinerCount = {}; 25 | let localStats = {}; 26 | let localPortHashes = {}; 27 | let localTimes = {}; 28 | 29 | let prevMinerSet = {}; 30 | let cache_updates = {}; 31 | let portMinerCount = {}; 32 | 33 | function updateShareStats2(height, callback) { 34 | // This is an omni-worker to deal with all things share-stats related 35 | // Time based averages are worked out on ring buffers. 36 | // Buffer lengths? You guessed it, configured in SQL. 37 | // Stats timeouts are 30 seconds, so everything for buffers should be there. 38 | const currentTime = Date.now(); 39 | // power to ensure we can keep up to global.config.general.statsBufferHours in global.config.general.statsBufferLength array 40 | // here N = log(history_power, global.config.general.statsBufferLength) is number of attemps required on average to remove top left history point (the oldest one) 41 | // we just select history_power so that is till happen on global.config.general.statsBufferHours * 60 attemps on average 42 | const history_power = Math.log(global.config.general.statsBufferLength) / Math.log(global.config.general.statsBufferHours * 60); 43 | 44 | console.log("Starting stats collection for " + height + " height (history power: " + history_power + ")"); 45 | 46 | const locTime = currentTime - (hashrate_avg_min*60*1000); 47 | const identifierTime = currentTime - (2*hashrate_avg_min*60*1000); 48 | 49 | let minerCount = 0; 50 | 51 | identifiers = {}; 52 | minerSet = {}; 53 | minerPortSet = {}; 54 | localMinerCount = { pplns: 0, pps: 0, solo: 0, prop: 0, global: 0 }; 55 | localStats = { pplns: 0, pps: 0, solo: 0, prop: 0, global: 0, miners: {}, miners2: {} }; 56 | localPortHashes = {}; 57 | localTimes = { pplns: locTime, pps: locTime, solo: locTime, prop: locTime, global: locTime, miners: {} }; 58 | 59 | let loopBreakout = 0; 60 | let oldestTime = currentTime; 61 | let txn = global.database.env.beginTxn({readOnly: true}); 62 | let cursor = new global.database.lmdb.Cursor(txn, global.database.shareDB); 63 | 64 | do { 65 | let count = 0; 66 | for (let found = cursor.goToRange(height) === height; found; ++ count, found = cursor.goToNextDup()) cursor.getCurrentBinary(function (key, share) { // jshint ignore:line 67 | try { 68 | share = global.protos.Share.decode(share); 69 | } catch (e) { 70 | console.error(share); 71 | return; 72 | } 73 | if (share.timestamp < oldestTime) oldestTime = share.timestamp; 74 | if (share.timestamp <= identifierTime) return; 75 | 76 | const minerID = typeof(share.paymentID) !== 'undefined' && share.paymentID.length > 10 77 | ? share.paymentAddress + '.' + share.paymentID : share.paymentAddress; 78 | 79 | const identifier = share.identifier; 80 | 81 | if (minerID in identifiers) { 82 | if (identifiers[minerID].indexOf(identifier) < 0) { 83 | identifiers[minerID].push(identifier); 84 | ++ minerCount; 85 | } 86 | } else { 87 | identifiers[minerID] = [identifier]; 88 | ++ minerCount; 89 | } 90 | 91 | if (share.timestamp <= locTime) return; 92 | 93 | const minerIDWithIdentifier = minerID + "_" + identifier; 94 | const shares2 = share.shares2; 95 | localStats.global += shares2; 96 | if (localTimes.global < share.timestamp) localTimes.global = share.timestamp; 97 | const minerType = pool_type_str[share.poolType]; 98 | if (!minerType) { 99 | console.error("Wrong share pool type found: " + share.poolType); 100 | return; 101 | } 102 | localStats[minerType] += shares2; 103 | if (localTimes[minerType] < share.timestamp) localTimes[minerType] = share.timestamp; 104 | 105 | const port = typeof(share.port) !== 'undefined' && share.port ? share.port : global.config.daemon.port; 106 | if (port in localPortHashes) localPortHashes[port] += share.raw_shares; 107 | else localPortHashes[port] = share.raw_shares; 108 | 109 | if (!shares2) return; // use virtual shares from child block mining only for global pool stats 110 | 111 | if (minerID in minerPortSet) { 112 | localStats.miners[minerID] += share.raw_shares; 113 | localStats.miners2[minerID] += shares2; 114 | if (localTimes.miners[minerID] < share.timestamp) localTimes.miners[minerID] = share.timestamp; 115 | } else { 116 | ++ localMinerCount[minerType]; 117 | ++ localMinerCount.global; 118 | localStats.miners[minerID] = share.raw_shares; 119 | localStats.miners2[minerID] = shares2; 120 | localTimes.miners[minerID] = share.timestamp; 121 | minerSet[minerID] = 1; 122 | minerPortSet[minerID] = port; 123 | } 124 | 125 | if (minerIDWithIdentifier in minerSet) { 126 | localStats.miners[minerIDWithIdentifier] += share.raw_shares; 127 | localStats.miners2[minerIDWithIdentifier] += shares2; 128 | if (localTimes.miners[minerIDWithIdentifier] < share.timestamp) localTimes.miners[minerIDWithIdentifier] = share.timestamp; 129 | } else { 130 | localStats.miners[minerIDWithIdentifier] = share.raw_shares; 131 | localStats.miners2[minerIDWithIdentifier] = shares2; 132 | localTimes.miners[minerIDWithIdentifier] = share.timestamp; 133 | minerSet[minerIDWithIdentifier] = 1; 134 | } 135 | }); 136 | debug("On " + height + " height iterated " + count + " elements"); 137 | } while (++loopBreakout <= 60 && --height >= 0 && oldestTime > identifierTime); 138 | cursor.close(); 139 | txn.abort(); 140 | 141 | debug("Share loop: " + ((Date.now() - currentTime) / 1000) + " seconds"); 142 | prevMinerSet = global.database.getCache('minerSet'); 143 | if (prevMinerSet === false) prevMinerSet = minerSet; 144 | cache_updates = {}; 145 | // pplns: 0, pps: 0, solo: 0, prop: 0, global: 0 146 | ['pplns', 'pps', 'solo', 'prop', 'global'].forEach(function (key) { 147 | const hash = localStats[key] / (hashrate_avg_min*60); 148 | const lastHash = localTimes[key]; 149 | const minerCount = localMinerCount[key]; 150 | let cachedData = global.database.getCache(key + "_stats"); 151 | if (cachedData !== false) { 152 | cachedData.hash = hash; 153 | cachedData.lastHash = lastHash; 154 | cachedData.minerCount = minerCount; 155 | if (!cachedData.hasOwnProperty("hashHistory")) { 156 | cachedData.hashHistory = []; 157 | cachedData.minerHistory = []; 158 | } 159 | if (cycleCount === 0) { 160 | cachedData.hashHistory.unshift({ts: currentTime, hs: cachedData.hash}); 161 | if (cachedData.hashHistory.length > global.config.general.statsBufferLength) { 162 | while (cachedData.hashHistory.length > global.config.general.statsBufferLength) { 163 | cachedData.hashHistory.pop(); 164 | } 165 | } 166 | cachedData.minerHistory.unshift({ts: currentTime, cn: cachedData.minerCount}); 167 | if (cachedData.minerHistory.length > global.config.general.statsBufferLength) { 168 | while (cachedData.minerHistory.length > global.config.general.statsBufferLength) { 169 | cachedData.minerHistory.pop(); 170 | } 171 | } 172 | } 173 | } else { 174 | cachedData = { 175 | hash: hash, 176 | totalHashes: 0, 177 | lastHash: lastHash, 178 | minerCount: minerCount, 179 | hashHistory: [{ts: currentTime, hs: hash}], 180 | minerHistory: [{ts: currentTime, cn: minerCount}] 181 | }; 182 | } 183 | cache_updates[key + "_stats"] = cachedData; 184 | }); 185 | for (let port in localPortHashes) localPortHashes[port] = localPortHashes[port] / (hashrate_avg_min*60); 186 | cache_updates["port_hash"] = localPortHashes; 187 | let history_update_count = 0; 188 | 189 | for (let miner in minerSet) { 190 | let stats; 191 | let keyStats = "stats:" + miner; 192 | let keyHistory = "history:" + miner; 193 | 194 | if (miner in stats_cache) { 195 | stats = stats_cache[miner]; 196 | } else { 197 | stats = global.database.getCache(keyStats); 198 | if (!stats) stats = {}; 199 | let history_stats = global.database.getCache(keyHistory); 200 | if (history_stats) { 201 | stats.hashHistory = history_stats.hashHistory; 202 | } else { 203 | stats.hashHistory = []; 204 | } 205 | } 206 | 207 | stats.hash = localStats.miners[miner] / (hashrate_avg_min*60); 208 | stats.hash2 = localStats.miners2[miner] / (hashrate_avg_min*60); 209 | stats.lastHash = localTimes.miners[miner]; 210 | cache_updates[keyStats] = { hash: stats.hash, hash2: stats.hash2, lastHash: stats.lastHash }; 211 | 212 | if (cycleCount === 0) { 213 | stats.hashHistory.unshift({ts: currentTime, hs: stats.hash, hs2: stats.hash2}); 214 | if (stats.hashHistory.length > global.config.general.statsBufferLength) { 215 | const is_worker = miner.indexOf('_') >= 0; 216 | while (stats.hashHistory.length > global.config.general.statsBufferLength) { 217 | if (is_worker) { 218 | stats.hashHistory.pop(); 219 | } else { 220 | const last_index = stats.hashHistory.length - 1; 221 | if ((currentTime - stats.hashHistory[last_index].ts) / 1000 / 3600 > global.config.general.statsBufferHours) { 222 | stats.hashHistory.pop(); 223 | } else { 224 | // here we remove larger indexes (that are more distant in time) with more probability 225 | const index_to_remove = (last_index * (1 - Math.pow(Math.random(), history_power))).toFixed(); 226 | stats.hashHistory.splice(index_to_remove, 1); 227 | } 228 | } 229 | } 230 | } 231 | if ( stats.hashHistory.length < global.config.general.statsBufferLength || 232 | !(miner in miner_history_update_time) || 233 | (history_update_count < 5000 && currentTime - miner_history_update_time[miner] > 10*60*1000) 234 | ) { 235 | cache_updates[keyHistory] = { hashHistory: stats.hashHistory }; 236 | miner_history_update_time[miner] = currentTime; 237 | ++ history_update_count; 238 | } 239 | } 240 | 241 | stats_cache[miner] = stats; 242 | } 243 | 244 | debug("History loop: " + ((Date.now() - currentTime) / 1000) + " seconds"); 245 | 246 | // remove old workers 247 | for (let miner in prevMinerSet) { 248 | if (miner in minerSet) continue; // we still have this miner in current set 249 | //debug("Removing: " + miner + " as an active miner from the cache."); 250 | let minerStats = global.database.getCache(miner); 251 | if (!minerStats) continue; 252 | minerStats.hash = 0; 253 | cache_updates[miner] = minerStats; 254 | if (miner.indexOf('_') <= -1) continue; 255 | 256 | // This is a worker case. 257 | const address_parts = miner.split(/_(.+)/); 258 | const worker = address_parts[1]; 259 | if (typeof(worker) !== 'undefined' && !worker.includes('silent')) { 260 | if (!(miner in workers_stopped_hashing_time)) { 261 | workers_stopped_hashing_time[miner] = currentTime; 262 | setTimeout(delayed_send_worker_stopped_hashing_email, 10*60*1000, miner, currentTime); 263 | } 264 | } 265 | } 266 | 267 | debug("Old worker loop: " + ((Date.now() - currentTime) / 1000) + " seconds"); 268 | 269 | // find new workers 270 | for (let miner in minerSet) { 271 | if (miner in prevMinerSet) continue; // we still have this miner in previous set 272 | //debug("Adding: " + miner + " as an active miner to the cache."); 273 | if (miner.indexOf('_') <= -1) continue; 274 | 275 | // This is a worker case. 276 | const address_parts = miner.split(/_(.+)/); 277 | const worker = address_parts[1]; 278 | if (typeof(worker) !== 'undefined' && !worker.includes('silent')) { 279 | workers_started_hashing_time[miner] = currentTime; 280 | if (miner in workers_stopped_hashing_email_time) { 281 | delete workers_stopped_hashing_time[miner]; 282 | delete workers_stopped_hashing_email_time[miner]; 283 | const address = address_parts[0]; 284 | get_address_email(address, function (email) { 285 | send_worker_started_hashing_email(miner, email, currentTime); 286 | }); 287 | } 288 | } 289 | } 290 | 291 | debug("New worker loop: " + ((Date.now() - currentTime) / 1000) + " seconds"); 292 | 293 | Object.keys(identifiers).forEach(function (key) { 294 | cache_updates['identifiers:' + key] = identifiers[key]; 295 | }); 296 | 297 | portMinerCount = {}; 298 | for (let miner in minerPortSet) { 299 | const port = minerPortSet[miner]; 300 | if (port in portMinerCount) ++ portMinerCount[port]; 301 | else portMinerCount[port] = 1; 302 | } 303 | cache_updates.portMinerCount = portMinerCount; 304 | cache_updates.minerSet = minerSet; 305 | const db_write_start_time = Date.now(); 306 | try { 307 | global.database.bulkSetCache(cache_updates); 308 | } catch (e) { 309 | console.error("Can't write to pool DB: " + e); 310 | global.support.sendEmail(global.config.general.adminEmail, "FYI: Pool DB is overflowed!", "Can't wite to pool DB: " + e); 311 | } 312 | cache_updates = {}; 313 | 314 | let pool_hashrate = localStats.global / (hashrate_avg_min*60); 315 | let pool_workers = minerCount; 316 | console.log("Processed " + minerCount + " workers (" + history_update_count + " history) for " + 317 | ((Date.now() - currentTime) / 1000) + " seconds (" + ((Date.now() - db_write_start_time) / 1000) + " seconds DB write). " + 318 | "Pool hashrate is: " + pool_hashrate 319 | ); 320 | if (!prev_pool_state_time || currentTime - prev_pool_state_time > hashrate_avg_min*60*1000) { 321 | let pool_hashrate_ratio = prev_pool_hashrate ? pool_hashrate / prev_pool_hashrate : 1; 322 | let pool_workers_ratio = prev_pool_workers ? pool_workers / prev_pool_workers : 1; 323 | if (pool_hashrate_ratio < (1-stat_change_alert) || pool_hashrate_ratio > (1+stat_change_alert) || 324 | pool_workers_ratio < (1-stat_change_alert) || pool_workers_ratio > (1+stat_change_alert)) { 325 | global.support.sendEmail(global.config.general.adminEmail, 326 | "FYI: Pool hashrate/workers changed significantly", 327 | "Pool hashrate changed from " + prev_pool_hashrate + " to " + pool_hashrate + " (" + pool_hashrate_ratio + ")\n" + 328 | "Pool number of workers changed from " + prev_pool_workers + " to " + pool_workers + " (" + pool_workers_ratio + ")\n" 329 | ); 330 | } 331 | prev_pool_hashrate = pool_hashrate; 332 | prev_pool_workers = pool_workers; 333 | prev_pool_state_time = currentTime; 334 | } 335 | return callback(); 336 | } 337 | 338 | function updateShareStats() { 339 | global.coinFuncs.getLastBlockHeader(function (err, body) { 340 | if (err !== null){ 341 | return setTimeout(updateShareStats, 20*1000); 342 | } 343 | updateShareStats2(body.height + 1, function() { 344 | if (++cycleCount === 6) cycleCount = 0; 345 | setTimeout(updateShareStats, 20*1000); 346 | }); 347 | }); 348 | } 349 | 350 | // cached email of specific address 351 | let minerEmail = {}; 352 | // time of last SQL check for specific address 353 | let minerEmailTime = {}; 354 | 355 | // worker name -> time 356 | let workers_started_hashing_time = {}; 357 | let workers_stopped_hashing_time = {}; 358 | let workers_stopped_hashing_email_time = {}; 359 | 360 | function get_address_email(address, callback) { 361 | let currentTime = Date.now(); 362 | if (!(address in minerEmailTime) || currentTime - minerEmailTime[address] > 10*60*1000) { 363 | minerEmailTime[address] = currentTime; 364 | minerEmail[address] = null; 365 | global.mysql.query("SELECT email FROM users WHERE username = ? AND enable_email IS true limit 1", [address]).then(function (rows) { 366 | if (rows.length === 0) { 367 | delete minerEmail[address]; 368 | return; 369 | } else { 370 | minerEmail[address] = rows[0].email; 371 | } 372 | return callback(minerEmail[address]); 373 | }).catch(function (error) { 374 | console.error("Can't get email address for " + address + ": " + error.message); 375 | return; 376 | }); 377 | } else if (address in minerEmail) { 378 | if (minerEmail[address] === null) { // not yet ready (retry again in 10 secs) 379 | if (currentTime - minerEmailTime[address] < 5*1000) return setTimeout(get_address_email, 10*1000, address, callback); 380 | } else { 381 | return callback(minerEmail[address]); 382 | } 383 | } 384 | } 385 | 386 | function send_worker_started_hashing_email(miner, email, currentTime) { 387 | let address_parts = miner.split(/_(.+)/); 388 | let address = address_parts[0]; 389 | let worker = address_parts[1]; 390 | // toAddress, subject, body 391 | let emailData = { 392 | worker: worker, 393 | timestamp: global.support.formatDate(currentTime), 394 | poolEmailSig: global.config.general.emailSig 395 | }; 396 | global.support.sendEmail(email, 397 | sprintf(global.config.email.workerStartHashingSubject, emailData), 398 | sprintf(global.config.email.workerStartHashingBody, emailData), 399 | address 400 | ); 401 | } 402 | 403 | function delayed_send_worker_stopped_hashing_email(miner, currentTime) { 404 | if (miner in workers_started_hashing_time && Date.now() - workers_started_hashing_time[miner] <= 10*60*1000) { 405 | delete workers_started_hashing_time[miner]; 406 | return; 407 | } 408 | 409 | delete workers_started_hashing_time[miner]; 410 | 411 | const address_parts = miner.split(/_(.+)/); 412 | const address = address_parts[0]; 413 | 414 | get_address_email(address, function (email) { 415 | workers_stopped_hashing_email_time[miner] = Date.now(); 416 | const worker = address_parts[1]; 417 | 418 | // toAddress, subject, body 419 | const emailData = { 420 | worker: worker, 421 | timestamp: global.support.formatDate(currentTime), 422 | poolEmailSig: global.config.general.emailSig 423 | }; 424 | global.support.sendEmail(email, 425 | sprintf(global.config.email.workerNotHashingSubject, emailData), 426 | sprintf(global.config.email.workerNotHashingBody, emailData), 427 | address 428 | ); 429 | }); 430 | } 431 | 432 | 433 | global.support.sendEmail(global.config.general.adminEmail, "Restarting worker module", "Restarted worker module!"); 434 | 435 | updateShareStats(); 436 | // clean caches from time to time 437 | setInterval(function() { 438 | console.log("Cleaning caches (" + Object.keys(stats_cache).length + " stats, " + Object.keys(miner_history_update_time).length + " histories)"); 439 | const currentTime = Date.now(); 440 | let stats_cache2 = {}; 441 | for (let miner in stats_cache) { 442 | if (miner in miner_history_update_time && currentTime - miner_history_update_time[miner] < 60*60*1000) { 443 | stats_cache2[miner] = stats_cache[miner]; 444 | } 445 | } 446 | stats_cache = stats_cache2; 447 | console.log("After cleaning: " + Object.keys(stats_cache).length + " stats left"); 448 | miner_history_update_time = {}; 449 | }, 2*60*60*1000); 450 | -------------------------------------------------------------------------------- /lib/pool_stats.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const debug = require("debug")("pool_stats"); 3 | const async = require("async"); 4 | 5 | const threadName = "Worker Server "; 6 | const max_blocks = 1000; 7 | const max_altblocks = 10000; 8 | 9 | let lastBlockCheckIsFailed = {}; 10 | 11 | let price_btc = 0; 12 | let price_usd = 0; 13 | let price_eur = 0; 14 | let min_block_rewards = {}; 15 | let blockList = []; 16 | let altblockList = []; 17 | let altblockFound = {}; 18 | let altblockFoundDone = 0; 19 | 20 | function get_cmc_price(symbol, callback) { 21 | const slug = global.config.coin.name.toLowerCase(); 22 | global.support.https_get("https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest?slug=" + slug + "&convert=" + symbol + "&CMC_PRO_API_KEY=" + global.config.general.cmcKey, function (res) { 23 | if (res instanceof Object && "data" in res && "quote" in res.data[Object.keys(res.data)[0]] && symbol in res.data[Object.keys(res.data)[0]].quote) { 24 | return callback(parseFloat(res.data[Object.keys(res.data)[0]].quote[symbol].price)); 25 | } else { 26 | console.error("Can't get price data from: " + JSON.stringify(res)); 27 | return callback(0); 28 | } 29 | }); 30 | } 31 | 32 | function get_cmc(callback) { 33 | get_cmc_price("USD", function(usd) { 34 | get_cmc_price("EUR", function(eur) { 35 | get_cmc_price("BTC", function(btc) { 36 | price_btc = btc ? btc : price_btc; 37 | price_usd = usd ? usd : price_usd; 38 | price_eur = eur ? eur : price_eur; 39 | return callback({ btc: price_btc, usd: price_usd, eur: price_eur }); 40 | }); 41 | }); 42 | }); 43 | } 44 | 45 | function updatePoolStats(poolType) { 46 | //console.log("Cleaned " + global.database.env.mdb_reader_check() + " stale readers"); 47 | let cache; 48 | if (typeof(poolType) !== 'undefined') { 49 | cache = global.database.getCache(poolType + "_stats"); 50 | let cache2 = global.database.getCache(poolType + "_stats2"); 51 | cache.totalHashes = cache2.totalHashes; 52 | cache.roundHashes = cache2.roundHashes; 53 | } else { 54 | console.log("Running pool stats"); 55 | cache = global.database.getCache("global_stats"); 56 | let cache2 = global.database.getCache("global_stats2"); 57 | cache.totalHashes = cache2.totalHashes; 58 | cache.roundHashes = cache2.roundHashes; 59 | } 60 | 61 | let port_hash = global.database.getCache('port_hash'); 62 | if (blockList.length > max_blocks) { 63 | const newBlocks = global.database.getBlockList(poolType, 0, max_blocks); 64 | let new_block_count = 0; 65 | let prev_block_index = 0; 66 | for (let block of newBlocks) { 67 | if (block.hash == blockList[prev_block_index].hash) ++ prev_block_index; 68 | else ++ new_block_count; 69 | } 70 | blockList = newBlocks.concat(blockList.slice(max_blocks - new_block_count)); 71 | } else { 72 | blockList = global.database.getBlockList(poolType); 73 | } 74 | if (altblockList.length > max_altblocks) { 75 | const newBlocks = global.database.getAltBlockList(poolType, null, 0, max_altblocks); 76 | let new_block_count = 0; 77 | let prev_block_index = 0; 78 | for (let block of newBlocks) { 79 | if (block.hash == altblockList[prev_block_index].hash) ++ prev_block_index; 80 | else ++ new_block_count; 81 | } 82 | altblockList = newBlocks.concat(altblockList.slice(max_altblocks - new_block_count)); 83 | } else { 84 | altblockList = global.database.getAltBlockList(poolType); 85 | } 86 | let min_block_rewards2 = global.database.getCache('min_block_rewards'); 87 | if (min_block_rewards2) min_block_rewards = min_block_rewards2; 88 | if (!(global.config.daemon.port in min_block_rewards)) min_block_rewards[global.config.daemon.port] = 0; 89 | 90 | async.series([ 91 | function (callback) { 92 | //debug(threadName + "Checking Influx for last 5min avg for pool stats (hashRate)"); 93 | return callback(null, cache.hash || 0); 94 | }, 95 | function (callback) { 96 | //debug(threadName + "Checking Influx for last 5min avg for miner count for pool stats (miners)"); 97 | return callback(null, cache.minerCount || 0); 98 | }, 99 | function (callback) { 100 | //debug(threadName + "Checking LMDB cache for totalHashes"); 101 | return callback(null, cache.totalHashes || 0); 102 | }, 103 | function (callback) { 104 | //debug(threadName + "Checking LMDB for lastBlockFoundTime for pool stats"); 105 | let max_time = 0; 106 | if (blockList.length !== 0) { 107 | max_time = Math.floor(blockList[0].ts / 1000); 108 | } 109 | if (altblockList.length !== 0) { 110 | max_time = Math.max(max_time, Math.floor(altblockList[0].ts / 1000)); 111 | } 112 | return callback(null, max_time); 113 | }, 114 | function (callback) { 115 | //debug(threadName + "Checking LMDB for lastBlockFound height for pool stats"); 116 | if (blockList.length === 0) { 117 | return callback(null, 0); 118 | } 119 | return callback(null, blockList[0].height); 120 | }, 121 | function (callback) { 122 | //debug(threadName + "Checking LMDB for totalBlocksFound for pool stats"); 123 | return callback(null, blockList.length); 124 | }, 125 | function (callback) { 126 | //debug(threadName + "Checking MySQL for total miners paid"); 127 | if (typeof(poolType) !== 'undefined') { 128 | global.mysql.query("SELECT payment_address, payment_id FROM payments WHERE pool_type = ? group by payment_address, payment_id", [poolType]).then(function (rows) { 129 | return callback(null, rows.length); 130 | }).catch(function (error) { 131 | console.error("SQL query failed: " + error); 132 | return callback(null, 0); 133 | }); 134 | } else { 135 | global.mysql.query("SELECT payment_address, payment_id FROM payments group by payment_address, payment_id").then(function (rows) { 136 | return callback(null, rows.length); 137 | }).catch(function (error) { 138 | console.error("SQL query failed: " + error); 139 | return callback(null, 0); 140 | }); 141 | } 142 | }, 143 | function (callback) { 144 | //debug(threadName + "Checking MySQL for total transactions count"); 145 | if (typeof(poolType) !== 'undefined') { 146 | global.mysql.query("SELECT distinct(transaction_id) from payments WHERE pool_type = ?", [poolType]).then(function (rows) { 147 | return callback(null, rows.length); 148 | }).catch(function (error) { 149 | console.error("SQL query failed: " + error); 150 | return callback(null, 0); 151 | }); 152 | } else { 153 | global.mysql.query("SELECT count(id) as txn_count FROM transactions").then(function (rows) { 154 | if (typeof(rows[0]) !== 'undefined') { 155 | return callback(null, rows[0].txn_count); 156 | } else { 157 | return callback(null, 0); 158 | } 159 | }).catch(function (error) { 160 | console.error("SQL query failed: " + error); 161 | return callback(null, 0); 162 | }); 163 | } 164 | }, 165 | function (callback) { 166 | //debug(threadName + "Checking LMDB cache for roundHashes"); 167 | return callback(null, cache.roundHashes || 0); 168 | }, 169 | function (callback) { 170 | //debug(threadName + "Checking LMDB for altblock count for pool stats"); 171 | return callback(null, altblockList.length); 172 | }, 173 | function (callback) { 174 | //debug(threadName + "Checking LMDB for altBlocksFound array for each specific port"); 175 | for (let i in altblockList) { 176 | if (i >= altblockList.length - altblockFoundDone) break; 177 | let block = altblockList[i]; 178 | if (altblockFound.hasOwnProperty(block.port)) ++ altblockFound[block.port]; 179 | else altblockFound[block.port] = 1; 180 | } 181 | altblockFoundDone = altblockList.length; 182 | return callback(null, altblockFound); 183 | }, 184 | function (callback) { 185 | //debug(threadName + "Checking MySQL for activePort value"); 186 | return callback(null, global.config.daemon.port); 187 | }, 188 | function (callback) { 189 | //debug(threadName + "Checking LMDB cache for active_ports value"); 190 | let active_ports = global.database.getCache('active_ports'); 191 | return callback(null, active_ports ? active_ports : []); 192 | }, 193 | function (callback) { 194 | //debug(threadName + "Checking LMDB cache for xmr_profit value"); 195 | let xmr_profit = global.database.getCache('xmr_profit'); 196 | return callback(null, xmr_profit ? xmr_profit.value : 0); 197 | }, 198 | function (callback) { 199 | //debug(threadName + "Checking LMDB cache for coin_profit value"); 200 | let coin_xmr_profit = global.database.getCache('coin_xmr_profit'); 201 | return callback(null, coin_xmr_profit ? coin_xmr_profit : {}); 202 | }, 203 | function (callback) { 204 | //debug(threadName + "Checking LMDB cache for xmr_profit_comment value"); 205 | let coin_comment = global.database.getCache('coin_comment'); 206 | return callback(null, coin_comment ? coin_comment : {}); 207 | }, 208 | function (callback) { 209 | //debug(threadName + "Checking LMDB cache for min_block_rewards value to set minBlockRewards"); 210 | return callback(null, min_block_rewards); 211 | }, 212 | function (callback) { 213 | let pending = 0; 214 | for (let i in blockList) { 215 | if (i > max_blocks) break; 216 | const block = blockList[i]; 217 | if (block.valid === true && block.unlocked === false) pending += global.support.coinToDecimal(block.value); 218 | } 219 | for (let i in altblockList) { 220 | if (i > max_altblocks) break; 221 | const altblock = altblockList[i]; 222 | if (altblock.valid === true && altblock.unlocked === false) pending += altblock.port in min_block_rewards ? min_block_rewards[altblock.port] : 0; 223 | } 224 | return callback(null, pending); 225 | }, 226 | function (callback) { 227 | if (typeof(poolType) === 'undefined' && price_btc == 0 && price_usd == 0 && price_eur == 0) { 228 | return get_cmc(function(prices) { return callback(null, prices); }); 229 | } else return callback(null, { btc: price_btc, usd: price_usd, eur: price_eur }); 230 | }, 231 | function (callback) { 232 | let currentEfforts = {}; 233 | for (let port of global.coinFuncs.getPORTS()) { 234 | const value = global.database.getCache(port != global.config.daemon.port ? "global_stats2_" + port : "global_stats2"); 235 | if (value !== false) currentEfforts[port] = value.roundHashes; 236 | } 237 | return callback(null, currentEfforts); 238 | }, 239 | function (callback) { 240 | //debug(threadName + "Checking LMDB cache for pplns_port_shares value"); 241 | let pplns_port_shares = global.database.getCache('pplns_port_shares'); 242 | return callback(null, pplns_port_shares ? pplns_port_shares : {}); 243 | }, 244 | function (callback) { 245 | //debug(threadName + "Checking LMDB cache for pplns_window_time value"); 246 | let pplns_window_time = global.database.getCache('pplns_window_time'); 247 | return callback(null, pplns_window_time ? pplns_window_time : 0); 248 | }, 249 | function (callback) { 250 | //debug(threadName + "Checking Influx for last 5min avg for pool stats (hashRate) per port"); 251 | return callback(null, port_hash || {}); 252 | }, 253 | function (callback) { 254 | //debug(threadName + "Checking LMDB cache for portMinerCount"); 255 | return callback(null, global.database.getCache('portMinerCount') || {}); 256 | }, 257 | function (callback) { 258 | let portCoinAlgo = {}; 259 | for (let port of global.coinFuncs.getPORTS()) portCoinAlgo[port] = global.coinFuncs.algoShortTypeStr(port, 0); 260 | return callback(null, portCoinAlgo); 261 | }, 262 | ], function (err, result) { 263 | global.database.setCache('pool_stats_' + (typeof(poolType) === 'undefined' ? 'global' : poolType), { 264 | hashRate: result[0], 265 | miners: result[1], 266 | totalHashes: result[2], 267 | lastBlockFoundTime: result[3] || 0, 268 | lastBlockFound: result[4] || 0, 269 | totalBlocksFound: result[5] || 0, 270 | totalMinersPaid: result[6] || 0, 271 | totalPayments: result[7] || 0, 272 | roundHashes: result[8] || 0, 273 | totalAltBlocksFound: result[9] || 0, 274 | altBlocksFound: result[10] || {}, 275 | activePort: result[11] || 0, 276 | activePorts: result[12] || [], 277 | activePortProfit: result[13] || 0, 278 | coinProfit: result[14] || {}, 279 | coinComment: result[15] || {}, 280 | minBlockRewards: result[16] || {}, 281 | pending: result[17] || 0, 282 | price: result[18] || {}, 283 | currentEfforts: result[19] || {}, 284 | pplnsPortShares: result[20] || {}, 285 | pplnsWindowTime: result[21] || 0, 286 | portHash: result[22] || {}, 287 | portMinerCount: result[23] || {}, 288 | portCoinAlgo: result[24] || {}, 289 | }); 290 | setTimeout(updatePoolStats, 60*1000, poolType); 291 | }); 292 | } 293 | 294 | function updatePoolPorts(poolServers) { 295 | //debug(threadName + "Updating pool ports"); 296 | let local_cache = {global: []}; 297 | let portCount = 0; 298 | global.mysql.query("select * from ports where hidden = 0 and pool_id < 1000 and lastSeen >= NOW() - INTERVAL 10 MINUTE").then(function (rows) { 299 | rows.forEach(function (row) { 300 | ++ portCount; 301 | if (!local_cache.hasOwnProperty(row.port_type)) { 302 | local_cache[row.port_type] = []; 303 | } 304 | local_cache[row.port_type].push({ 305 | host: poolServers[row.pool_id], 306 | port: row.network_port, 307 | difficulty: row.starting_diff, 308 | description: row.description, 309 | miners: row.miners 310 | }); 311 | if (portCount === rows.length) { 312 | let local_counts = {}; 313 | let port_diff = {}; 314 | let port_miners = {}; 315 | let pool_type_count = 0; 316 | let localPortInfo = {}; 317 | for (let pool_type in local_cache) { // jshint ignore:line 318 | ++ pool_type_count; 319 | local_cache[pool_type].forEach(function (portData) { // jshint ignore:line 320 | if (!local_counts.hasOwnProperty(portData.port)) { 321 | local_counts[portData.port] = 0; 322 | } 323 | if (!port_diff.hasOwnProperty(portData.port)) { 324 | port_diff[portData.port] = portData.difficulty; 325 | } 326 | if (!port_miners.hasOwnProperty(portData.port)) { 327 | port_miners[portData.port] = 0; 328 | } 329 | if (port_diff[portData.port] === portData.difficulty) { 330 | ++ local_counts[portData.port]; 331 | port_miners[portData.port] += portData.miners; 332 | } 333 | localPortInfo[portData.port] = portData.description; 334 | if (local_counts[portData.port] === Object.keys(poolServers).length) { 335 | local_cache.global.push({ 336 | host: { 337 | blockID: typeof(local_cache[pool_type][0].host) === 'undefined' ? 0 : local_cache[pool_type][0].host.blockID, 338 | blockIDTime: typeof(local_cache[pool_type][0].host) === 'undefined' ? 0 : local_cache[pool_type][0].host.blockIDTime, 339 | hostname: global.config.pool.geoDNS, 340 | }, 341 | port: portData.port, 342 | pool_type: pool_type, 343 | difficulty: portData.difficulty, 344 | miners: port_miners[portData.port], 345 | description: localPortInfo[portData.port] 346 | }); 347 | } 348 | }); 349 | if (pool_type_count === Object.keys(local_cache).length) { 350 | //debug(threadName + "Sending the following to the workers: " + JSON.stringify(local_cache)); 351 | global.database.setCache('poolPorts', local_cache); 352 | setTimeout(updatePoolInformation, 30*1000); 353 | } 354 | } 355 | } 356 | }); 357 | }).catch(function (error) { 358 | console.error("SQL query failed: " + error); 359 | }); 360 | } 361 | 362 | function updatePoolInformation() { 363 | let local_cache = {}; 364 | //debug(threadName + "Updating pool information"); 365 | global.mysql.query("select * from pools where id < 1000 and last_checkin >= NOW() - INTERVAL 10 MINUTE").then(function (rows) { 366 | rows.forEach(function (row) { 367 | local_cache[row.id] = { 368 | ip: row.ip, 369 | blockID: row.blockID, 370 | blockIDTime: global.support.formatDateFromSQL(row.blockIDTime), 371 | hostname: row.hostname 372 | }; 373 | if (Object.keys(local_cache).length === rows.length) { 374 | global.database.setCache('poolServers', local_cache); 375 | updatePoolPorts(local_cache); 376 | } 377 | }); 378 | }).catch(function (error) { 379 | console.error("SQL query failed: " + error); 380 | }); 381 | } 382 | 383 | let network_info = {}; 384 | 385 | function updateBlockHeader() { 386 | const ports = global.config.daemon.enableAlgoSwitching ? global.coinFuncs.getPORTS() : [ global.config.daemon.port ]; 387 | async.eachSeries(ports, function(port, next) { 388 | global.coinFuncs.getPortLastBlockHeaderWithRewardDiff(port, function(err, body){ 389 | if (err) return next(); 390 | network_info[port] = { 391 | difficulty: parseInt(body.difficulty), 392 | hash: body.hash ? body.hash : body.hashrate, 393 | height: body.height, 394 | value: body.reward, 395 | ts: parseInt(body.timestamp), 396 | }; 397 | if (port == global.config.daemon.port) { 398 | global.support.rpcPortDaemon(port, 'get_info', [], function (rpcResult) { 399 | network_info.difficulty = rpcResult.result ? rpcResult.result.difficulty : body.difficulty; 400 | network_info.hash = body.hash; 401 | network_info.main_height = body.height; 402 | network_info.height = body.height; 403 | network_info.value = body.reward; 404 | network_info.ts = body.timestamp; 405 | return next(); 406 | }); 407 | } else { 408 | return next(); 409 | } 410 | }, true); 411 | }, function(err, result) { 412 | global.database.setCache('networkBlockInfo', network_info); 413 | setTimeout(updateBlockHeader, 30*1000); 414 | }); 415 | } 416 | 417 | function bad_header_start(port) { 418 | console.error("Issue in getting block header for " + port + " port. Skipping node monitor"); 419 | if (port in lastBlockCheckIsFailed) { 420 | if (++ lastBlockCheckIsFailed[port] >= 5) global.support.sendEmail( 421 | global.config.general.adminEmail, 422 | 'Failed to query daemon for ' + port + ' port for last block header', 423 | `The worker failed to return last block header for ` + port + ` port. Please verify if the daemon is running properly.` 424 | ); 425 | } else { 426 | lastBlockCheckIsFailed[port] = 1; 427 | } 428 | return; 429 | } 430 | 431 | function bad_header_stop(port) { 432 | if (port in lastBlockCheckIsFailed) { 433 | if (lastBlockCheckIsFailed[port] >= 5) global.support.sendEmail( 434 | global.config.general.adminEmail, 435 | 'Quering daemon for ' + port + ' port for last block header is back to normal', 436 | `An warning was sent to you indicating that the the worker failed to return the last block header for ${port} port. 437 | The issue seems to be solved now.` 438 | ); 439 | delete lastBlockCheckIsFailed[port]; 440 | } 441 | } 442 | 443 | function monitorNodes() { 444 | global.mysql.query("SELECT blockID, hostname, ip, port FROM pools WHERE last_checkin > date_sub(now(), interval 30 minute)").then(function (rows) { 445 | global.coinFuncs.getPortLastBlockHeader(global.config.daemon.port, function (err, block) { 446 | if (err !== null){ 447 | bad_header_start(global.config.daemon.port); 448 | return; 449 | } 450 | bad_header_stop(); 451 | let top_height = 0; 452 | let is_master_daemon_issue = rows.length > 1 ? true : false; 453 | rows.forEach(function (row) { 454 | if (row.port && row.port != global.config.daemon.port) { 455 | console.error("INTERNAL ERROR: pool node port " + row.port + " do not match master port " + global.config.daemon.port); 456 | is_master_daemon_issue = false; 457 | return; 458 | } 459 | if (top_height < row.blockID) top_height = row.blockID; 460 | if (Math.abs(block.height - row.blockID) > 3) { 461 | global.support.sendEmail(global.config.general.adminEmail, 462 | "Pool server behind in blocks", 463 | "The pool server: " + row.hostname + " with IP: " + row.ip + " is " + (block.height - row.blockID) + " blocks behind for " + row.port + " port" 464 | ); 465 | } else { 466 | is_master_daemon_issue = false; 467 | } 468 | }); 469 | if (is_master_daemon_issue) global.coinFuncs.fixDaemonIssue(block.height, top_height, global.config.daemon.port); 470 | }); 471 | }).catch(function (error) { 472 | console.error("SQL query failed: " + error); 473 | }); 474 | } 475 | 476 | updatePoolStats(); 477 | updatePoolStats('pplns'); 478 | if (global.config.pps.enable === true) updatePoolStats('pps'); 479 | if (global.config.solo.enable === true) updatePoolStats('solo'); 480 | updatePoolInformation(); 481 | updateBlockHeader(); 482 | 483 | monitorNodes(); 484 | setInterval(monitorNodes, 5*60*1000); 485 | setInterval(get_cmc, 15*60*1000, function() {}); 486 | 487 | --------------------------------------------------------------------------------