├── .gitignore ├── Dockerfile ├── README.md ├── config_example.json ├── install.sh ├── lib ├── aeon.js ├── support.js └── xmr.js ├── package.json ├── proxy.js └── update.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | npm-debug.log 4 | cert* 5 | config.json -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | LABEL maintainer="Pedro Lobo " 3 | LABEL Name="Dockerized xmr-node-proxy" 4 | LABEL Version="1.4" 5 | 6 | RUN export BUILD_DEPS="cmake \ 7 | pkg-config \ 8 | git \ 9 | build-essential \ 10 | curl" \ 11 | 12 | && apt-get update && apt-get upgrade -qqy \ 13 | && apt-get install --no-install-recommends -qqy \ 14 | ${BUILD_DEPS} python-virtualenv \ 15 | python3-virtualenv ntp screen \ 16 | libboost-all-dev libevent-dev \ 17 | libunbound-dev libminiupnpc-dev \ 18 | libunwind8-dev liblzma-dev libldns-dev \ 19 | libexpat1-dev libgtest-dev libzmq3-dev \ 20 | 21 | && curl -o- https://deb.nodesource.com/setup_6.x| bash \ 22 | && apt-get install nodejs \ 23 | 24 | && git clone https://github.com/Snipa22/xmr-node-proxy /app \ 25 | && cd /app && npm install \ 26 | 27 | && openssl req -subj "/C=IT/ST=Pool/L=Daemon/O=Mining Pool/CN=mining.proxy" \ 28 | -newkey rsa:2048 -nodes -keyout cert.key -x509 -out cert.pem -days 36500 \ 29 | 30 | && apt-get --auto-remove purge -qqy ${BUILD_DEPS} \ 31 | && apt-get clean \ 32 | && rm -rf /var/lib/apt/lists/* \ 33 | && chown -R proxy.proxy /app \ 34 | && mkdir /logs && chown -R proxy.proxy /logs 35 | 36 | USER proxy 37 | WORKDIR /app 38 | 39 | ENTRYPOINT ["node","proxy.js"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xmr-node-proxy 2 | 3 | 4 | ## Setup Instructions 5 | 6 | Based on a clean Ubuntu 16.04 LTS minimal install 7 | 8 | ## Deployment via Installer 9 | 10 | 1. Create a user 'nodeproxy' and assign a password (or add an SSH key. If you prefer that, you should already know how to do it) 11 | 12 | ```bash 13 | useradd -d /home/nodeproxy -m -s /bin/bash nodeproxy 14 | passwd nodeproxy 15 | ``` 16 | 17 | 2. Add your user to `/etc/sudoers`, this must be done so the script can sudo up and do it's job. We suggest passwordless sudo. Suggested line: ` ALL=(ALL) NOPASSWD:ALL`. Our sample builds use: `nodeproxy ALL=(ALL) NOPASSWD:ALL` 18 | 19 | ```bash 20 | echo "nodeproxy ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers 21 | ``` 22 | 23 | 3. Log in as the **NON-ROOT USER** you just created and run the [deploy script](https://raw.githubusercontent.com/Snipa22/xmr-node-proxy/master/install.sh). This is very important! This script will install the proxy to whatever user it's running under! 24 | 25 | ```bash 26 | curl -L https://raw.githubusercontent.com/Snipa22/xmr-node-proxy/master/install.sh | bash 27 | ``` 28 | 29 | 3. Once it's complete, copy `example_config.json` to `config.json` and edit as desired. 30 | 4. Run: `source ~/.bashrc` This will activate NVM and get things working for the following pm2 steps. 31 | 8. Once you're happy with the settings, go ahead and start all the proxy daemon, commands follow. 32 | 33 | ```shell 34 | cd ~/xmr-node-proxy/ 35 | pm2 start proxy.js --name=proxy --log-date-format="YYYY-MM-DD HH:mm Z" 36 | pm2 save 37 | ``` 38 | You can check the status of your proxy by either issuing 39 | 40 | ``` 41 | pm2 logs proxy 42 | ``` 43 | 44 | or using the pm2 monitor 45 | 46 | ``` 47 | pm2 monit 48 | ``` 49 | 50 | ## Known Issues 51 | 52 | VMs with 512Mb or less RAM will need some swap space in order to compile the C extensions for node. Bignum and the CN libraries can chew through some serious memory during compile. In regards to this, one of our users has put together a guide for T2.Micro servers: https://docs.google.com/document/d/1m8E4_pDwKuFo0TnWJaO13LDHqOmbL6YrzyR6FvzqGgU (Credit goes to MayDay30 for his work with this!) 53 | 54 | If not running on an Ubuntu 16.04 system, please make sure your kernel is at least 3.2 or higher, as older versions will not work for this. 55 | 56 | Many smaller VMs come with ulimits set very low. We suggest looking into setting the ulimit higher. In particular, `nofile` (Number of files open) needs to be raised for high-usage instances. 57 | 58 | If your system doesn't have AES-NI, then it will throw an error during the node-multi-hashing install, as this requires AES-NI. If this is the case, go ahead and change the following line: 59 | "multi-hashing": "git+https://github.com/Snipa22/node-multi-hashing-aesni.git", 60 | to: 61 | "multi-hashing": "git://github.com/clintar/node-multi-hashing.git#Nan-2.0", 62 | 63 | In your `packages.json`, do a `npm install`, and it should pass. 64 | 65 | 66 | ## Performance 67 | 68 | The proxy gains a massive boost over a basic pool by accepting that the majority of the hashes submitted _will_ not be valid (does not exceed the required difficulty of the pool). Due to this, the proxy doesn't bother with attempting to validate the hash state nor value until the share difficulty exceeds the pool difficulty. 69 | 70 | In testing, we've seen AWS t2.micro instances take upwards of 2k connections, while t2.small taking 6k. The proxy is extremely light weight, and while there are more features on the way, it's our goal to keep the proxy as light weight as possible. 71 | 72 | ## Configuration Guidelines 73 | 74 | Please check the [wiki](https://github.com/Snipa22/xmr-node-proxy/wiki/config_review) for information on configuration 75 | 76 | ## Developer Donations 77 | 78 | The proxy is pre-configured for a 1% donation. This is easily toggled inside of it's configuration. If you'd like to make a one time donation, the addresses are as follows: 79 | 80 | * XMR - 44Ldv5GQQhP7K7t3ZBdZjkPA7Kg7dhHwk3ZM3RJqxxrecENSFx27Vq14NAMAd2HBvwEPUVVvydPRLcC69JCZDHLT2X5a4gr 81 | * BTC - 15fkPTtN8cRXD3moKWDoXjuiTaS9FgA3UE 82 | 83 | ## Installation/Configuration Assistance 84 | 85 | 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. These need to be x86_64 boxes with AES-NI Available. 86 | 87 | Installation assistance is 4 XMR, with a 2 XMR deposit, with remainder to be paid on completion. 88 | Configuration assistance is 2 XMR with a 1 XMR deposit, and includes debugging your proxy configurations, ensuring that everything is running, and tuning for your uses/needs. 89 | 90 | SSH access with a sudo-enabled user will be needed for installs, preferably the user that is slated to run the pool. 91 | 92 | Please contact Snipa at: proxy_installs@snipanet.com or via IRC on irc.freenode.net in #monero-pools 93 | 94 | ## Known Working Pools 95 | 96 | * [XMRPool.net](https://xmrpool.net) 97 | * [supportXMR.com](https://supportxmr.com) 98 | * [pool.xmr.pt](https://pool.xmr.pt) 99 | * [minemonero.pro](https://minemonero.pro) 100 | * [XMRPool.xyz](https://xmrpool.xyz) 101 | * [ViaXMR.com](https://viaxmr.com) 102 | * [mine.MoneroPRO.com](https://mine.moneropro.com) 103 | * [MinerCircle.com](https://www.minercircle.com) 104 | * [xmr.p00ls.net](https://www.p00ls.net) 105 | * [MoriaXMR.com](https://moriaxmr.com) 106 | * [MoneroOcean.stream](https://moneroocean.stream) 107 | * [SECUmine.net](https://secumine.net) 108 | * [Chinaenter.cn](http://xmr.chinaenter.cn) 109 | * [XMRPool.eu](https://xmrpool.eu) 110 | 111 | If you'd like to have your pool added, please make a pull request here, or contact Snipa on IRC! 112 | -------------------------------------------------------------------------------- /config_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "pools": [ 3 | { 4 | "hostname": "pool.supportxmr.com", 5 | "port": 7777, 6 | "ssl": false, 7 | "allowSelfSignedSSL": false, 8 | "share": 80, 9 | "username": "46XWBqE1iwsVxSDP1qDrxhE1XvsZV6eALG5LwnoMdjbT4GPdy2bZTb99kagzxp2MMjUamTYZ4WgvZdFadvMimTjvR6Gv8hL", 10 | "password": "proxy:totally.valid@snipanet.com", 11 | "keepAlive": true, 12 | "coin": "xmr", 13 | "default": false 14 | }, 15 | { 16 | "hostname": "mine.xmrpool.net", 17 | "port": 7778, 18 | "ssl": false, 19 | "allowSelfSignedSSL": false, 20 | "share": 20, 21 | "username": "46XWBqE1iwsVxSDP1qDrxhE1XvsZV6eALG5LwnoMdjbT4GPdy2bZTb99kagzxp2MMjUamTYZ4WgvZdFadvMimTjvR6Gv8hL", 22 | "password": "proxy:totally.valid@snipanet.com", 23 | "keepAlive": true, 24 | "coin": "xmr", 25 | "default": true 26 | } 27 | ], 28 | "listeningPorts": [ 29 | { 30 | "port": 8080, 31 | "ssl": false, 32 | "diff": 5000, 33 | "coin": "xmr" 34 | }, 35 | { 36 | "port": 8443, 37 | "ssl": true, 38 | "diff": 5000, 39 | "coin": "xmr" 40 | }, 41 | { 42 | "port": 3333, 43 | "ssl": false, 44 | "diff": 10000, 45 | "coin": "xmr" 46 | } 47 | ], 48 | "bindAddress": "0.0.0.0", 49 | "developerShare": 1, 50 | "daemonAddress": "127.0.0.1:18081", 51 | "coinSettings": { 52 | "xmr":{ 53 | "minDiff": 100, 54 | "maxDiff": 300000, 55 | "shareTargetTime": 15 56 | }, 57 | "aeon":{ 58 | "minDiff": 100, 59 | "maxDiff": 300000, 60 | "shareTargetTime": 15 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "This assumes that you are doing a green-field install. If you're not, please exit in the next 15 seconds." 3 | sleep 15 4 | echo "Continuing install, this will prompt you for your password if you're not already running as root and you didn't enable passwordless sudo. Please do not run me as root!" 5 | if [[ `whoami` == "root" ]]; then 6 | echo "You ran me as root! Do not run me as root!" 7 | exit 1 8 | fi 9 | CURUSER=$(whoami) 10 | sudo apt-get update 11 | sudo DEBIAN_FRONTEND=noninteractive apt-get -y upgrade 12 | sudo DEBIAN_FRONTEND=noninteractive apt-get -y install git python-virtualenv python3-virtualenv curl ntp build-essential screen cmake pkg-config libboost-all-dev libevent-dev libunbound-dev libminiupnpc-dev libunwind8-dev liblzma-dev libldns-dev libexpat1-dev libgtest-dev libzmq3-dev 13 | cd ~ 14 | git clone https://github.com/Snipa22/xmr-node-proxy 15 | curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash 16 | source ~/.nvm/nvm.sh 17 | nvm install v6.9.2 18 | cd ~/xmr-node-proxy 19 | npm install 20 | npm install -g pm2 21 | cp config_example.json config.json 22 | openssl req -subj "/C=IT/ST=Pool/L=Daemon/O=Mining Pool/CN=mining.proxy" -newkey rsa:2048 -nodes -keyout cert.key -x509 -out cert.pem -days 36500 23 | cd ~ 24 | pm2 status 25 | sudo env PATH=$PATH:`pwd`/.nvm/versions/node/v6.9.2/bin `pwd`/.nvm/versions/node/v6.9.2/lib/node_modules/pm2/bin/pm2 startup systemd -u $CURUSER --hp `pwd` 26 | sudo chown -R $CURUSER. ~/.pm2 27 | echo "Installing pm2-logrotate in the background!" 28 | pm2 install pm2-logrotate & 29 | echo "You're setup with a shiny new proxy! Now, go configure it and have fun." 30 | -------------------------------------------------------------------------------- /lib/aeon.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const multiHashing = require('multi-hashing'); 3 | const cnUtil = require('cryptonote-util'); 4 | const bignum = require('bignum'); 5 | const support = require('./support.js')(); 6 | const crypto = require('crypto'); 7 | 8 | let debug = { 9 | pool: require('debug')('pool'), 10 | diff: require('debug')('diff'), 11 | blocks: require('debug')('blocks'), 12 | shares: require('debug')('shares'), 13 | miners: require('debug')('miners'), 14 | workers: require('debug')('workers') 15 | }; 16 | 17 | let baseDiff = bignum('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 16); 18 | 19 | Buffer.prototype.toByteArray = function () { 20 | return Array.prototype.slice.call(this, 0); 21 | }; 22 | 23 | function blockHeightCheck(nodeList, callback) { 24 | let randomNode = nodeList[Math.floor(Math.random() * nodeList.length)].split(':'); 25 | 26 | } 27 | 28 | function getRemoteNodes() { 29 | let knownNodes = [ 30 | '162.213.38.245:18081', 31 | '116.93.119.79:18081', 32 | '85.204.96.231:18081', 33 | '107.167.87.242:18081', 34 | '107.167.93.58:18081', 35 | '199.231.85.122:18081', 36 | '192.110.160.146:18081' 37 | ]; // Prefill the array with known good nodes for now. Eventually will try to download them via DNS or http. 38 | } 39 | 40 | function BlockTemplate(template) { 41 | /* 42 | We receive something identical to the result portions of the monero GBT call. 43 | Functionally, this could act as a very light-weight solo pool, so we'll prep it as one. 44 | You know. Just in case amirite? 45 | */ 46 | this.id = template.id; 47 | this.blob = template.blocktemplate_blob; 48 | this.difficulty = template.difficulty; 49 | this.height = template.height; 50 | this.reservedOffset = template.reserved_offset; 51 | this.workerOffset = template.worker_offset; // clientNonceLocation 52 | this.targetDiff = template.target_diff; 53 | this.targetHex = template.target_diff_hex; 54 | this.buffer = new Buffer(this.blob, 'hex'); 55 | this.previousHash = new Buffer(32); 56 | this.workerNonce = 0; 57 | this.solo = false; 58 | if (typeof(this.workerOffset) === 'undefined') { 59 | this.solo = true; 60 | global.instanceId.copy(this.buffer, this.reservedOffset + 4, 0, 3); 61 | this.buffer.copy(this.previousHash, 0, 7, 39); 62 | } 63 | this.nextBlob = function () { 64 | if (this.solo) { 65 | // This is running in solo mode. 66 | this.buffer.writeUInt32BE(++this.workerNonce, this.reservedOffset); 67 | } else { 68 | this.buffer.writeUInt32BE(++this.workerNonce, this.workerOffset); 69 | } 70 | return cnUtil.convert_blob(this.buffer).toString('hex'); 71 | }; 72 | } 73 | 74 | function MasterBlockTemplate(template) { 75 | /* 76 | We receive something identical to the result portions of the monero GBT call. 77 | Functionally, this could act as a very light-weight solo pool, so we'll prep it as one. 78 | You know. Just in case amirite? 79 | */ 80 | this.blob = template.blocktemplate_blob; 81 | this.difficulty = template.difficulty; 82 | this.height = template.height; 83 | this.reservedOffset = template.reserved_offset; // reserveOffset 84 | this.workerOffset = template.client_nonce_offset; // clientNonceLocation 85 | this.poolOffset = template.client_pool_offset; // clientPoolLocation 86 | this.targetDiff = template.target_diff; 87 | this.targetHex = template.target_diff_hex; 88 | this.buffer = new Buffer(this.blob, 'hex'); 89 | this.previousHash = new Buffer(32); 90 | this.job_id = template.job_id; 91 | this.workerNonce = 0; 92 | this.poolNonce = 0; 93 | this.solo = false; 94 | if (typeof(this.workerOffset) === 'undefined') { 95 | this.solo = true; 96 | global.instanceId.copy(this.buffer, this.reservedOffset + 4, 0, 3); 97 | this.buffer.copy(this.previousHash, 0, 7, 39); 98 | } 99 | this.blobForWorker = function () { 100 | this.buffer.writeUInt32BE(++this.poolNonce, this.poolOffset); 101 | return this.buffer.toString('hex'); 102 | }; 103 | } 104 | 105 | function getJob(miner, activeBlockTemplate, bashCache) { 106 | if (miner.validJobs.size() >0 && miner.validJobs.get(0).templateID === activeBlockTemplate.id && !miner.newDiff && miner.cachedJob !== null && typeof bashCache === 'undefined') { 107 | return miner.cachedJob; 108 | } 109 | 110 | let blob = activeBlockTemplate.nextBlob(); 111 | let target = getTargetHex(miner); 112 | miner.lastBlockHeight = activeBlockTemplate.height; 113 | 114 | let newJob = { 115 | id: crypto.pseudoRandomBytes(21).toString('base64'), 116 | extraNonce: activeBlockTemplate.workerNonce, 117 | height: activeBlockTemplate.height, 118 | difficulty: miner.difficulty, 119 | diffHex: miner.diffHex, 120 | submissions: [], 121 | templateID: activeBlockTemplate.id 122 | }; 123 | 124 | miner.validJobs.enq(newJob); 125 | miner.cachedJob = { 126 | blob: blob, 127 | job_id: newJob.id, 128 | target: target, 129 | id: miner.id 130 | }; 131 | return miner.cachedJob; 132 | } 133 | 134 | function getMasterJob(pool, workerID) { 135 | let activeBlockTemplate = pool.activeBlocktemplate; 136 | let btBlob = activeBlockTemplate.blobForWorker(); 137 | let workerData = { 138 | id: crypto.pseudoRandomBytes(21).toString('base64'), 139 | blocktemplate_blob: btBlob, 140 | difficulty: activeBlockTemplate.difficulty, 141 | height: activeBlockTemplate.height, 142 | reserved_offset: activeBlockTemplate.reservedOffset, 143 | worker_offset: activeBlockTemplate.workerOffset, 144 | target_diff: activeBlockTemplate.targetDiff, 145 | target_diff_hex: activeBlockTemplate.targetHex 146 | }; 147 | let localData = { 148 | id: workerData.id, 149 | masterJobID: activeBlockTemplate.job_id, 150 | poolNonce: activeBlockTemplate.poolNonce 151 | }; 152 | if (!(workerID in pool.poolJobs)) { 153 | pool.poolJobs[workerID] = support.circularBuffer(4); 154 | } 155 | pool.poolJobs[workerID].enq(localData); 156 | return workerData; 157 | } 158 | 159 | function getTargetHex(miner) { 160 | if (miner.newDiff) { 161 | miner.difficulty = miner.newDiff; 162 | miner.newDiff = null; 163 | } 164 | let padded = Buffer.alloc(32); 165 | let diffBuff = baseDiff.div(miner.difficulty).toBuffer(); 166 | diffBuff.copy(padded, 32 - diffBuff.length); 167 | 168 | let buff = padded.slice(0, 4); 169 | let buffArray = buff.toByteArray().reverse(); 170 | let buffReversed = new Buffer(buffArray); 171 | miner.target = buffReversed.readUInt32BE(0); 172 | return buffReversed.toString('hex'); 173 | } 174 | 175 | function processShare(miner, job, blockTemplate, nonce, resultHash) { 176 | let template = new Buffer(blockTemplate.buffer.length); 177 | blockTemplate.buffer.copy(template); 178 | if (blockTemplate.solo) { 179 | template.writeUInt32BE(job.extraNonce, blockTemplate.reservedOffset); 180 | } else { 181 | template.writeUInt32BE(job.extraNonce, blockTemplate.workerOffset); 182 | } 183 | 184 | let hash = new Buffer(resultHash, 'hex'); 185 | let hashArray = hash.toByteArray().reverse(); 186 | let hashNum = bignum.fromBuffer(new Buffer(hashArray)); 187 | let hashDiff = baseDiff.div(hashNum); 188 | 189 | if (hashDiff.ge(blockTemplate.targetDiff)) { 190 | // Validate share with CN hash, then if valid, blast it up to the master. 191 | let shareBuffer = cnUtil.construct_block_blob(template, new Buffer(nonce, 'hex')); 192 | let convertedBlob = cnUtil.convert_blob(shareBuffer); 193 | hash = multiHashing.cryptonight_light(convertedBlob); 194 | if (hash.toString('hex') !== resultHash) { 195 | console.error(global.threadName + "Bad share from miner " + miner.logString); 196 | miner.messageSender('job', miner.getJob(miner, blockTemplate, true)); 197 | return false; 198 | } 199 | miner.blocks += 1; 200 | process.send({ 201 | type: 'shareFind', 202 | host: miner.pool, 203 | data: { 204 | btID: blockTemplate.id, 205 | nonce: nonce, 206 | resultHash: resultHash, 207 | workerNonce: job.extraNonce 208 | } 209 | }); 210 | } 211 | else if (hashDiff.lt(job.difficulty)) { 212 | process.send({type: 'invalidShare'}); 213 | console.warn(global.threadName + "Rejected low diff share of " + hashDiff.toString() + " from: " + miner.address + " ID: " + 214 | miner.identifier + " IP: " + miner.ipAddress); 215 | return false; 216 | } 217 | miner.shares += 1; 218 | miner.hashes += job.difficulty; 219 | return true; 220 | } 221 | 222 | let devPool = { 223 | "hostname": "aeon-donations.snipanet.com", 224 | "port": 3333, 225 | "ssl": false, 226 | "share": 0, 227 | "username": "WmtvM6SoYya4qzkoPB4wX7FACWcXyFPWAYzfz7CADECgKyBemAeb3dVb3QomHjRWwGS3VYzMJAnBXfUx5CfGLFZd1U7ssdXTu", 228 | "password": "proxy_donations", 229 | "keepAlive": true, 230 | "coin": "aeon", 231 | "default": false, 232 | "devPool": true 233 | }; 234 | 235 | module.exports = function () { 236 | return { 237 | devPool: devPool, 238 | hashSync: multiHashing.cryptonight_light, 239 | hashAsync: multiHashing.CNLAsync, 240 | blockHeightCheck: blockHeightCheck, 241 | getRemoteNodes: getRemoteNodes, 242 | BlockTemplate: BlockTemplate, 243 | getJob: getJob, 244 | processShare: processShare, 245 | MasterBlockTemplate: MasterBlockTemplate, 246 | getMasterJob: getMasterJob 247 | }; 248 | }; -------------------------------------------------------------------------------- /lib/support.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const CircularBuffer = require('circular-buffer'); 3 | const request = require('request'); 4 | const moment = require('moment'); 5 | const debug = require('debug')('support'); 6 | const fs = require('fs'); 7 | 8 | function circularBuffer(size) { 9 | let buffer = CircularBuffer(size); 10 | 11 | buffer.sum = function () { 12 | if (this.size() === 0) { 13 | return 1; 14 | } 15 | return this.toarray().reduce(function (a, b) { 16 | return a + b; 17 | }); 18 | }; 19 | 20 | buffer.average = function (lastShareTime) { 21 | if (this.size() === 0) { 22 | return global.config.pool.targetTime * 1.5; 23 | } 24 | let extra_entry = (Date.now() / 1000) - lastShareTime; 25 | return (this.sum() + Math.round(extra_entry)) / (this.size() + 1); 26 | }; 27 | 28 | buffer.clear = function () { 29 | let i = this.size(); 30 | while (i > 0) { 31 | this.deq(); 32 | i = this.size(); 33 | } 34 | }; 35 | 36 | return buffer; 37 | } 38 | 39 | function sendEmail(toAddress, subject, body){ 40 | request.post(global.config.general.mailgunURL + "/messages", { 41 | auth: { 42 | user: 'api', 43 | pass: global.config.general.mailgunKey 44 | }, 45 | form: { 46 | from: global.config.general.emailFrom, 47 | to: toAddress, 48 | subject: subject, 49 | text: body 50 | } 51 | }, function(err, response, body){ 52 | if (!err && response.statusCode === 200) { 53 | console.log("Email sent successfully! Response: " + body); 54 | } else { 55 | console.error("Did not send e-mail successfully! Response: " + body + " Response: "+JSON.stringify(response)); 56 | } 57 | }); 58 | } 59 | 60 | function coinToDecimal(amount) { 61 | return amount / global.config.coin.sigDigits; 62 | } 63 | 64 | function decimalToCoin(amount) { 65 | return Math.round(amount * global.config.coin.sigDigits); 66 | } 67 | 68 | function blockCompare(a, b) { 69 | if (a.height < b.height) { 70 | return 1; 71 | } 72 | 73 | if (a.height > b.height) { 74 | return -1; 75 | } 76 | return 0; 77 | } 78 | 79 | function tsCompare(a, b) { 80 | if (a.ts < b.ts) { 81 | return 1; 82 | } 83 | 84 | if (a.ts > b.ts) { 85 | return -1; 86 | } 87 | return 0; 88 | } 89 | 90 | function currentUnixTimestamp(){ 91 | return + new Date(); 92 | } 93 | 94 | module.exports = function () { 95 | return { 96 | circularBuffer: circularBuffer, 97 | coinToDecimal: coinToDecimal, 98 | decimalToCoin: decimalToCoin, 99 | blockCompare: blockCompare, 100 | sendEmail: sendEmail, 101 | tsCompare: tsCompare, 102 | developerAddy: '44Ldv5GQQhP7K7t3ZBdZjkPA7Kg7dhHwk3ZM3RJqxxrecENSFx27Vq14NAMAd2HBvwEPUVVvydPRLcC69JCZDHLT2X5a4gr', 103 | currentUnixTimestamp: currentUnixTimestamp 104 | }; 105 | }; 106 | -------------------------------------------------------------------------------- /lib/xmr.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const multiHashing = require("cryptonight-hashing"); 3 | const cnUtil = require('cryptonote-util'); 4 | const bignum = require('bignum'); 5 | const support = require('./support.js')(); 6 | const crypto = require('crypto'); 7 | 8 | let debug = { 9 | pool: require('debug')('pool'), 10 | diff: require('debug')('diff'), 11 | blocks: require('debug')('blocks'), 12 | shares: require('debug')('shares'), 13 | miners: require('debug')('miners'), 14 | workers: require('debug')('workers') 15 | }; 16 | 17 | let baseDiff = bignum('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 16); 18 | 19 | Buffer.prototype.toByteArray = function () { 20 | return Array.prototype.slice.call(this, 0); 21 | }; 22 | 23 | function blockHeightCheck(nodeList, callback) { 24 | let randomNode = nodeList[Math.floor(Math.random() * nodeList.length)].split(':'); 25 | 26 | } 27 | 28 | function getRemoteNodes() { 29 | let knownNodes = [ 30 | '162.213.38.245:18081', 31 | '116.93.119.79:18081', 32 | '85.204.96.231:18081', 33 | '107.167.87.242:18081', 34 | '107.167.93.58:18081', 35 | '199.231.85.122:18081', 36 | '192.110.160.146:18081' 37 | ]; // Prefill the array with known good nodes for now. Eventually will try to download them via DNS or http. 38 | } 39 | 40 | function BlockTemplate(template) { 41 | /* 42 | We receive something identical to the result portions of the monero GBT call. 43 | Functionally, this could act as a very light-weight solo pool, so we'll prep it as one. 44 | You know. Just in case amirite? 45 | */ 46 | this.id = template.id; 47 | this.blob = template.blocktemplate_blob; 48 | this.difficulty = template.difficulty; 49 | this.height = template.height; 50 | this.reservedOffset = template.reserved_offset; 51 | this.workerOffset = template.worker_offset; // clientNonceLocation 52 | this.targetDiff = template.target_diff; 53 | this.targetHex = template.target_diff_hex; 54 | this.buffer = new Buffer(this.blob, 'hex'); 55 | this.previousHash = new Buffer(32); 56 | this.workerNonce = 0; 57 | this.solo = false; 58 | this.seedHash = template.seed_hash ? Buffer.from(template.seed_hash, 'hex') : Buffer.from('00', 'hex'); 59 | if (typeof (this.workerOffset) === 'undefined') { 60 | this.solo = true; 61 | global.instanceId.copy(this.buffer, this.reservedOffset + 4, 0, 3); 62 | this.buffer.copy(this.previousHash, 0, 7, 39); 63 | } 64 | this.nextBlob = function () { 65 | if (this.solo) { 66 | // This is running in solo mode. 67 | this.buffer.writeUInt32BE(++this.workerNonce, this.reservedOffset); 68 | } else { 69 | this.buffer.writeUInt32BE(++this.workerNonce, this.workerOffset); 70 | } 71 | return cnUtil.convert_blob(this.buffer).toString('hex'); 72 | }; 73 | } 74 | 75 | function MasterBlockTemplate(template) { 76 | /* 77 | We receive something identical to the result portions of the monero GBT call. 78 | Functionally, this could act as a very light-weight solo pool, so we'll prep it as one. 79 | You know. Just in case amirite? 80 | */ 81 | this.blob = template.blocktemplate_blob; 82 | this.difficulty = template.difficulty; 83 | this.height = template.height; 84 | this.reservedOffset = template.reserved_offset; // reserveOffset 85 | this.workerOffset = template.client_nonce_offset; // clientNonceLocation 86 | this.poolOffset = template.client_pool_offset; // clientPoolLocation 87 | this.targetDiff = template.target_diff; 88 | this.targetHex = template.target_diff_hex; 89 | this.buffer = new Buffer(this.blob, 'hex'); 90 | this.previousHash = new Buffer(32); 91 | this.job_id = template.job_id; 92 | this.workerNonce = 0; 93 | this.poolNonce = 0; 94 | this.solo = false; 95 | this.seedHash = template.seed_hash ? Buffer.from(template.seed_hash, 'hex') : Buffer.from('00', 'hex'); 96 | if (typeof (this.workerOffset) === 'undefined') { 97 | this.solo = true; 98 | global.instanceId.copy(this.buffer, this.reservedOffset + 4, 0, 3); 99 | this.buffer.copy(this.previousHash, 0, 7, 39); 100 | } 101 | this.blobForWorker = function () { 102 | this.buffer.writeUInt32BE(++this.poolNonce, this.poolOffset); 103 | return this.buffer.toString('hex'); 104 | }; 105 | } 106 | 107 | function getJob(miner, activeBlockTemplate, bashCache) { 108 | if (miner.validJobs.size() > 0 && miner.validJobs.get(0).templateID === activeBlockTemplate.id && !miner.newDiff && miner.cachedJob !== null && typeof bashCache === 'undefined') { 109 | return miner.cachedJob; 110 | } 111 | 112 | let blob = activeBlockTemplate.nextBlob(); 113 | let target = getTargetHex(miner); 114 | miner.lastBlockHeight = activeBlockTemplate.height; 115 | 116 | let newJob = { 117 | id: crypto.pseudoRandomBytes(21).toString('base64'), 118 | extraNonce: activeBlockTemplate.workerNonce, 119 | height: activeBlockTemplate.height, 120 | difficulty: miner.difficulty, 121 | diffHex: miner.diffHex, 122 | submissions: [], 123 | seed_hash: activeBlockTemplate.seedHash ? activeBlockTemplate.seedHash.toString('hex') : null, 124 | templateID: activeBlockTemplate.id 125 | }; 126 | 127 | miner.validJobs.enq(newJob); 128 | miner.cachedJob = { 129 | blob: blob, 130 | job_id: newJob.id, 131 | target: target, 132 | id: miner.id, 133 | seed_hash: activeBlockTemplate.seedHash ? activeBlockTemplate.seedHash.toString('hex') : null, 134 | height: activeBlockTemplate.height 135 | }; 136 | return miner.cachedJob; 137 | } 138 | 139 | function getMasterJob(pool, workerID) { 140 | let activeBlockTemplate = pool.activeBlocktemplate; 141 | let btBlob = activeBlockTemplate.blobForWorker(); 142 | let workerData = { 143 | id: crypto.pseudoRandomBytes(21).toString('base64'), 144 | blocktemplate_blob: btBlob, 145 | difficulty: activeBlockTemplate.difficulty, 146 | height: activeBlockTemplate.height, 147 | reserved_offset: activeBlockTemplate.reservedOffset, 148 | worker_offset: activeBlockTemplate.workerOffset, 149 | target_diff: activeBlockTemplate.targetDiff, 150 | seed_hash: activeBlockTemplate.seedHash ? activeBlockTemplate.seedHash.toString('hex') : null, 151 | target_diff_hex: activeBlockTemplate.targetHex 152 | }; 153 | let localData = { 154 | id: workerData.id, 155 | masterJobID: activeBlockTemplate.job_id, 156 | seed_hash: activeBlockTemplate.seedHash ? activeBlockTemplate.seedHash.toString('hex') : null, 157 | poolNonce: activeBlockTemplate.poolNonce 158 | }; 159 | if (!(workerID in pool.poolJobs)) { 160 | pool.poolJobs[workerID] = support.circularBuffer(4); 161 | } 162 | pool.poolJobs[workerID].enq(localData); 163 | return workerData; 164 | } 165 | 166 | function getTargetHex(miner) { 167 | if (miner.newDiff) { 168 | miner.difficulty = miner.newDiff; 169 | miner.newDiff = null; 170 | } 171 | let padded = Buffer.alloc(32); 172 | let diffBuff = baseDiff.div(miner.difficulty).toBuffer(); 173 | diffBuff.copy(padded, 32 - diffBuff.length); 174 | 175 | let buff = padded.slice(0, 4); 176 | let buffArray = buff.toByteArray().reverse(); 177 | let buffReversed = new Buffer(buffArray); 178 | miner.target = buffReversed.readUInt32BE(0); 179 | return buffReversed.toString('hex'); 180 | } 181 | 182 | function processShare(miner, job, blockTemplate, nonce, resultHash) { 183 | let template = new Buffer(blockTemplate.buffer.length); 184 | blockTemplate.buffer.copy(template); 185 | if (blockTemplate.solo) { 186 | template.writeUInt32BE(job.extraNonce, blockTemplate.reservedOffset); 187 | } else { 188 | template.writeUInt32BE(job.extraNonce, blockTemplate.workerOffset); 189 | } 190 | 191 | let hash = new Buffer(resultHash, 'hex'); 192 | let hashArray = hash.toByteArray().reverse(); 193 | let hashNum = bignum.fromBuffer(new Buffer(hashArray)); 194 | let hashDiff = baseDiff.div(hashNum); 195 | 196 | if (hashDiff.ge(blockTemplate.targetDiff)) { 197 | // Validate share with CN hash, then if valid, blast it up to the master. 198 | let shareBuffer = cnUtil.construct_block_blob(template, new Buffer(nonce, 'hex')); 199 | let convertedBlob = cnUtil.convert_blob(shareBuffer); 200 | if (blockTemplate.seedHash == null || blockTemplate.seedHash.length != 32) { 201 | hash = multiHashing.cryptonight(convertedBlob, convertedBlob[0] >= 10 ? 13 : 8, job.height); 202 | } else { 203 | hash = multiHashing.randomx(convertedBlob, blockTemplate.seedHash, 0); 204 | } 205 | if (hash.toString("hex") !== resultHash) { 206 | if (multiHashing.cryptonight(convertedBlob, 0).toString("hex") === resultHash) { 207 | console.error(`${global.threadName} Invalid version of CN hashing algo (CN/0) used by miner ${miner.logString}`); 208 | } else if (multiHashing.cryptonight(convertedBlob, 1) === resultHash) { 209 | console.error(`${global.threadName} Invalid version of CN hashing algo (CN/1) used by miner ${miner.logString}`); 210 | } else if (multiHashing.cryptonight(convertedBlob, 8) === resultHash) { 211 | console.error(`${global.threadName} Invalid version of CN hashing algo (CN/2) used by miner ${miner.logString}`); 212 | } else { 213 | console.error(`${global.threadName} Bad share from ${miner.logString}`) 214 | } 215 | miner.messageSender('job', miner.getJob(miner, blockTemplate, true)); 216 | return false; 217 | } 218 | miner.blocks += 1; 219 | process.send({ 220 | type: 'shareFind', 221 | host: miner.pool, 222 | data: { 223 | btID: blockTemplate.id, 224 | nonce: nonce, 225 | resultHash: resultHash, 226 | workerNonce: job.extraNonce 227 | } 228 | }); 229 | } else if (hashDiff.lt(job.difficulty)) { 230 | process.send({ type: 'invalidShare' }); 231 | console.warn(global.threadName + "Rejected low diff share of " + hashDiff.toString() + " from: " + miner.address + " ID: " + 232 | miner.identifier + " IP: " + miner.ipAddress); 233 | return false; 234 | } 235 | miner.shares += 1; 236 | miner.hashes += job.difficulty; 237 | return true; 238 | } 239 | 240 | let devPool = { 241 | "hostname": "xmr-donations.snipanet.com", 242 | "port": 7777, 243 | "ssl": false, 244 | "share": 0, 245 | "username": "44Ldv5GQQhP7K7t3ZBdZjkPA7Kg7dhHwk3ZM3RJqxxrecENSFx27Vq14NAMAd2HBvwEPUVVvydPRLcC69JCZDHLT2X5a4gr", 246 | "password": "proxy_donations", 247 | "keepAlive": true, 248 | "coin": "xmr", 249 | "default": false, 250 | "devPool": true 251 | }; 252 | 253 | function syncHash(convertedBlob) { 254 | return multiHashing.cryptonight(convertedBlob, convertedBlob[0] >= 7 ? convertedBlob[0] - 6 : 0); 255 | } 256 | 257 | module.exports = function () { 258 | return { 259 | devPool: devPool, 260 | hashSync: syncHash, 261 | blockHeightCheck: blockHeightCheck, 262 | getRemoteNodes: getRemoteNodes, 263 | BlockTemplate: BlockTemplate, 264 | getJob: getJob, 265 | processShare: processShare, 266 | MasterBlockTemplate: MasterBlockTemplate, 267 | getMasterJob: getMasterJob 268 | }; 269 | }; 270 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xmr-node-proxy", 3 | "version": "0.0.2", 4 | "description": "Node proxy for XMR pools based on nodejs-pool, should support any coins that nodejs-pool does with little work", 5 | "main": "proxy.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/snipa22/xmr-node-proxy.git" 12 | }, 13 | "author": "Alexander Blair", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/snipa22/xmr-node-proxy/issues" 17 | }, 18 | "homepage": "https://github.com/snipa22/xmr-node-proxy#readme", 19 | "dependencies": { 20 | "async": "2.1.4", 21 | "bignum": "^0.12.5", 22 | "body-parser": "^1.16.0", 23 | "circular-buffer": "1.0.2", 24 | "cluster": "0.7.7", 25 | "cors": "^2.8.1", 26 | "crypto": "0.0.3", 27 | "debug": "2.5.1", 28 | "express": "4.14.0", 29 | "jsonwebtoken": "^7.2.1", 30 | "minimist": "1.2.0", 31 | "moment": "2.17.1", 32 | "range": "0.0.3", 33 | "request": "^2.79.0", 34 | "request-json": "0.6.1", 35 | "sprintf-js": "^1.0.3", 36 | "uuid": "3.0.1" 37 | }, 38 | "optionalDependencies": { 39 | "cryptonote-util": "git://github.com/Snipa22/node-cryptonote-util.git#xmr-Nan-2.0", 40 | "multi-hashing": "git+https://github.com/Snipa22/node-multi-hashing-aesni.git#v0.1", 41 | "cryptonight-hashing": "git+https://github.com/MoneroOcean/node-cryptonight-hashing.git#865a1f18e7dd5cf0513ae6becfdbeba3a10c9fb9" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /proxy.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const cluster = require('cluster'); 3 | const net = require('net'); 4 | const tls = require('tls'); 5 | const fs = require('fs'); 6 | const async = require('async'); 7 | const uuidV4 = require('uuid/v4'); 8 | const support = require('./lib/support.js')(); 9 | global.config = require('./config.json'); 10 | 11 | 12 | /* 13 | General file design/where to find things. 14 | 15 | Internal Variables 16 | IPC Registry 17 | Combined Functions 18 | Pool Definition 19 | Master Functions 20 | Miner Definition 21 | Slave Functions 22 | API Calls (Master-Only) 23 | System Init 24 | 25 | */ 26 | let debug = { 27 | pool: require('debug')('pool'), 28 | diff: require('debug')('diff'), 29 | blocks: require('debug')('blocks'), 30 | shares: require('debug')('shares'), 31 | miners: require('debug')('miners'), 32 | workers: require('debug')('workers'), 33 | balancer: require('debug')('balancer') 34 | }; 35 | global.threadName = ''; 36 | let nonceCheck = new RegExp("^[0-9a-f]{8}$"); 37 | let activePorts = []; 38 | let httpResponse = ' 200 OK\nContent-Type: text/plain\nContent-Length: 18\n\nMining Proxy Online'; 39 | let activeMiners = {}; 40 | let activeCoins = {}; 41 | let bans = {}; 42 | let activePools = {}; 43 | let activeWorkers = {}; 44 | let defaultPools = {}; 45 | let masterStats = {shares: 0, blocks: 0, hashes: 0}; 46 | 47 | // IPC Registry 48 | function masterMessageHandler(worker, message, handle) { 49 | if (typeof message !== 'undefined' && 'type' in message){ 50 | switch (message.type) { 51 | case 'blockFind': 52 | case 'shareFind': 53 | if (message.host in activePools){ 54 | activePools[message.host].sendShare(worker, message.data); 55 | } 56 | break; 57 | case 'needPoolState': 58 | worker.send({ 59 | type: 'poolState', 60 | data: Object.keys(activePools) 61 | }); 62 | for (let hostname in activePools){ 63 | if (activePools.hasOwnProperty(hostname)){ 64 | let pool = activePools[hostname]; 65 | if (!pool.active || pool.activeBlocktemplate === null){ 66 | continue; 67 | } 68 | worker.send({ 69 | host: hostname, 70 | type: 'newBlockTemplate', 71 | data: pool.coinFuncs.getMasterJob(pool, worker.id) 72 | }); 73 | } 74 | } 75 | break; 76 | case 'workerStats': 77 | activeWorkers[worker.id][message.minerID] = message.data; 78 | break; 79 | } 80 | } 81 | } 82 | 83 | function slaveMessageHandler(message) { 84 | switch (message.type) { 85 | case 'newBlockTemplate': 86 | if (message.host in activePools){ 87 | if(activePools[message.host].activeBlocktemplate){ 88 | debug.workers(`Received a new block template for ${message.host} and have one in cache. Storing`); 89 | activePools[message.host].pastBlockTemplates.enq(activePools[message.host].activeBlocktemplate); 90 | } else { 91 | debug.workers(`Received a new block template for ${message.host} do not have one in cache.`); 92 | } 93 | activePools[message.host].activeBlocktemplate = new activePools[message.host].coinFuncs.BlockTemplate(message.data); 94 | for (let miner in activeMiners){ 95 | if (activeMiners.hasOwnProperty(miner)){ 96 | let realMiner = activeMiners[miner]; 97 | if (realMiner.pool === message.host){ 98 | realMiner.messageSender('job', realMiner.getJob(realMiner, activePools[message.host].activeBlocktemplate)); 99 | } 100 | } 101 | } 102 | } 103 | break; 104 | case 'poolState': 105 | message.data.forEach(function(hostname){ 106 | if(!(hostname in activePools)){ 107 | global.config.pools.forEach(function(poolData){ 108 | if (hostname === poolData.hostname){ 109 | activePools[hostname] = new Pool(poolData); 110 | } 111 | }); 112 | } 113 | }); 114 | break; 115 | case 'changePool': 116 | if (activeMiners.hasOwnProperty(message.worker) && activePools.hasOwnProperty(message.pool)){ 117 | activeMiners[message.worker].pool = message.pool; 118 | activeMiners[message.worker].messageSender('job', 119 | activeMiners[message.worker].getJob(activeMiners[message.worker], activePools[message.pool].activeBlocktemplate, true)); 120 | } 121 | break; 122 | case 'disablePool': 123 | if (activePools.hasOwnProperty(message.pool)){ 124 | activePools[message.pool].active = false; 125 | checkActivePools(); 126 | } 127 | break; 128 | case 'enablePool': 129 | if (activePools.hasOwnProperty(message.pool)){ 130 | activePools[message.pool].active = true; 131 | process.send({type: 'needPoolState'}); 132 | } 133 | break; 134 | } 135 | } 136 | 137 | // Combined Functions 138 | function readConfig() { 139 | let local_conf = JSON.parse(fs.readFileSync('config.json')); 140 | if (typeof global.config === 'undefined') { 141 | global.config = {}; 142 | } 143 | for (let key in local_conf) { 144 | if (local_conf.hasOwnProperty(key) && (typeof global.config[key] === 'undefined' || global.config[key] !== local_conf[key])) { 145 | global.config[key] = local_conf[key]; 146 | } 147 | } 148 | if (!cluster.isMaster) { 149 | activatePorts(); 150 | } 151 | } 152 | 153 | // Pool Definition 154 | function Pool(poolData){ 155 | /* 156 | Pool data is the following: 157 | { 158 | "hostname": "pool.supportxmr.com", 159 | "port": 7777, 160 | "ssl": false, 161 | "share": 80, 162 | "username": "", 163 | "password": "", 164 | "keepAlive": true, 165 | "coin": "xmr" 166 | } 167 | Client Data format: 168 | { 169 | "method":"submit", 170 | "params":{ 171 | "id":"12e168f2-db42-4eea-b56a-f1e7d57f94c9", 172 | "job_id":"/4FIQEI/Qq++EzzH1e03oTrWF5Ed", 173 | "nonce":"9e008000", 174 | "result":"4eee0b966418fdc3ec1a684322715e65765554f11ff8f7fed3f75ac45ef20300" 175 | }, 176 | "id":1 177 | } 178 | */ 179 | this.hostname = poolData.hostname; 180 | this.port = poolData.port; 181 | this.ssl = poolData.ssl; 182 | this.share = poolData.share; 183 | this.username = poolData.username; 184 | this.password = poolData.password; 185 | this.keepAlive = poolData.keepAlive; 186 | this.default = poolData.default; 187 | this.devPool = poolData.hasOwnProperty('devPool') && poolData.devPool === true; 188 | this.coin = poolData.coin; 189 | this.pastBlockTemplates = support.circularBuffer(4); 190 | this.coinFuncs = require(`./lib/${this.coin}.js`)(); 191 | this.activeBlocktemplate = null; 192 | this.active = true; 193 | this.sendId = 1; 194 | this.sendLog = {}; 195 | this.poolJobs = {}; 196 | this.socket = null; 197 | this.allowSelfSignedSSL = true; 198 | // Partial checks for people whom havn't upgraded yet 199 | if (poolData.hasOwnProperty('allowSelfSignedSSL')){ 200 | this.allowSelfSignedSSL = !poolData.allowSelfSignedSSL; 201 | } 202 | 203 | this.connect = function(){ 204 | for (let worker in cluster.workers){ 205 | if (cluster.workers.hasOwnProperty(worker)){ 206 | cluster.workers[worker].send({type: 'disablePool', pool: this.hostname}); 207 | } 208 | } 209 | try { 210 | if (this.socket !== null){ 211 | this.socket.end(); 212 | this.socket.destroy(); 213 | } 214 | } catch (e) { 215 | console.log("Had issues murdering the old socket. Om nom: " + e) 216 | } 217 | this.socket = null; 218 | this.active = false; 219 | if (this.ssl){ 220 | this.socket = tls.connect(this.port, this.hostname, {rejectUnauthorized: this.allowSelfSignedSSL}).on('connect', ()=>{ 221 | poolSocket(this.hostname); 222 | }).on('error', (err)=>{ 223 | this.connect(); 224 | console.warn(`${global.threadName}Socket error from ${this.hostname} ${err}`); 225 | }); 226 | } else { 227 | this.socket = net.connect(this.port, this.hostname).on('connect', ()=>{ 228 | poolSocket(this.hostname); 229 | }).on('error', (err)=>{ 230 | this.connect(); 231 | console.warn(`${global.threadName}Socket error from ${this.hostname} ${err}`); 232 | }); 233 | } 234 | }; 235 | this.heartbeat = function(){ 236 | if (this.keepAlive){ 237 | this.sendData('keepalived'); 238 | } 239 | }; 240 | this.sendData = function (method, params) { 241 | if (typeof params === 'undefined'){ 242 | params = {}; 243 | } 244 | let rawSend = { 245 | method: method, 246 | id: this.sendId++, 247 | }; 248 | if (typeof this.id !== 'undefined'){ 249 | params.id = this.id; 250 | } 251 | rawSend.params = params; 252 | if (!this.socket.writable){ 253 | return false; 254 | } 255 | this.socket.write(JSON.stringify(rawSend) + '\n'); 256 | this.sendLog[rawSend.id] = rawSend; 257 | debug.pool(`Sent ${JSON.stringify(rawSend)} to ${this.hostname}`); 258 | }; 259 | this.login = function () { 260 | this.sendData('login', { 261 | login: this.username, 262 | pass: this.password, 263 | agent: 'xmr-node-proxy/0.0.3' 264 | }); 265 | this.active = true; 266 | for (let worker in cluster.workers){ 267 | if (cluster.workers.hasOwnProperty(worker)){ 268 | cluster.workers[worker].send({type: 'enablePool', pool: this.hostname}); 269 | } 270 | } 271 | }; 272 | this.sendShare = function (worker, shareData) { 273 | //btID - Block template ID in the poolJobs circ buffer. 274 | let job = this.poolJobs[worker.id].toarray().filter(function (job) { 275 | return job.id === shareData.btID; 276 | })[0]; 277 | if (job){ 278 | this.sendData('submit', { 279 | job_id: job.masterJobID, 280 | nonce: shareData.nonce, 281 | result: shareData.resultHash, 282 | workerNonce: shareData.workerNonce, 283 | poolNonce: job.poolNonce 284 | }); 285 | } 286 | }; 287 | } 288 | 289 | // Master Functions 290 | /* 291 | The master performs the following tasks: 292 | 1. Serve all API calls. 293 | 2. Distribute appropriately modified block template bases to all pool servers. 294 | 3. Handle all to/from the various pool servers. 295 | 4. Manage and suggest miner changes in order to achieve correct h/s balancing between the various systems. 296 | */ 297 | function connectPools(){ 298 | global.config.pools.forEach(function (poolData) { 299 | if (activePools.hasOwnProperty(poolData.hostname)){ 300 | return; 301 | } 302 | activePools[poolData.hostname] = new Pool(poolData); 303 | activePools[poolData.hostname].connect(); 304 | }); 305 | let seen_coins = {}; 306 | if (global.config.developerShare > 0){ 307 | for (let pool in activePools){ 308 | if (activePools.hasOwnProperty(pool)){ 309 | if (seen_coins.hasOwnProperty(activePools[pool].coin)){ 310 | return; 311 | } 312 | let devPool = activePools[pool].coinFuncs.devPool; 313 | if (activePools.hasOwnProperty(devPool.hostname)){ 314 | return; 315 | } 316 | activePools[devPool.hostname] = new Pool(devPool); 317 | activePools[devPool.hostname].connect(); 318 | seen_coins[activePools[pool].coin] = true; 319 | } 320 | } 321 | } 322 | for (let coin in seen_coins){ 323 | if (seen_coins.hasOwnProperty(coin)){ 324 | activeCoins[coin] = true; 325 | } 326 | } 327 | } 328 | 329 | function balanceWorkers(){ 330 | /* 331 | This function deals with handling how the pool deals with getting traffic balanced to the various pools. 332 | Step 1: Enumerate all workers (Child servers), and their miners/coins into known states 333 | Step 1: Enumerate all miners, move their H/S into a known state tagged to the coins and pools 334 | Step 2: Enumerate all pools, verify the percentages as fractions of 100. 335 | Step 3: Determine if we're sharing with the developers (Woohoo! You're the best if you do!) 336 | Step 4: Process the state information to determine splits/moves. 337 | Step 5: Notify child processes of other pools to send traffic to if needed. 338 | 339 | The Master, as the known state holder of all information, deals with handling this data. 340 | */ 341 | let minerStates = {}; 342 | let poolStates = {}; 343 | for (let poolName in activePools){ 344 | if (activePools.hasOwnProperty(poolName)){ 345 | let pool = activePools[poolName]; 346 | if (!poolStates.hasOwnProperty(pool.coin)){ 347 | poolStates[pool.coin] = {'percentage': 0, 'devPool': false}; 348 | } 349 | poolStates[pool.coin][poolName] = { 350 | miners: {}, 351 | hashrate: 0, 352 | percentage: pool.share, 353 | devPool: pool.devPool, 354 | idealRate: 0 355 | }; 356 | if(pool.devPool){ 357 | poolStates[pool.coin].devPool = poolName; 358 | debug.balancer(`Found a developer pool enabled. Pool is: ${poolName}`); 359 | } else { 360 | poolStates[pool.coin].percentage += pool.share; 361 | } 362 | } 363 | } 364 | /* 365 | poolStates now contains an object that looks approximately like: 366 | poolStates = { 367 | 'xmr': 368 | { 369 | 'mine.xmrpool.net': { 370 | 'miners': {}, 371 | 'hashrate': 0, 372 | 'percentage': 20, 373 | 'devPool': false, 374 | 'amtChange': 0 375 | }, 376 | 'donations.xmrpool.net': { 377 | 'miners': {}, 378 | 'hashrate': 0, 379 | 'percentage': 0, 380 | 'devPool': true, 381 | 'amtChange': 0 382 | }, 383 | 'devPool': 'donations.xmrpool.net', 384 | 'totalPercentage': 20 385 | } 386 | } 387 | */ 388 | for (let coin in poolStates){ 389 | if(poolStates.hasOwnProperty(coin)){ 390 | let percentModifier = 1; 391 | let newPercentage = 0; 392 | if (poolStates[coin].percentage !== 100){ 393 | debug.balancer(`Pools on ${coin} are using ${poolStates[coin].percentage}% balance. Adjusting.`); 394 | // Need to adjust all the pools that aren't the dev pool. 395 | percentModifier = 100/poolStates[coin].percentage; 396 | for (let pool in poolStates[coin]){ 397 | if (poolStates[coin].hasOwnProperty(pool) && activePools.hasOwnProperty(pool)){ 398 | if (poolStates[coin][pool].devPool){ 399 | continue; 400 | } 401 | poolStates[coin][pool].percentage *= percentModifier; 402 | newPercentage += poolStates[coin][pool].share; 403 | } 404 | } 405 | let finalMod = 0; 406 | if (newPercentage !== 100){ 407 | finalMod = 100 - newPercentage; 408 | } 409 | for (let pool in poolStates[coin]){ 410 | if (poolStates[coin].hasOwnProperty(pool) && activePools.hasOwnProperty(pool)){ 411 | if (poolStates[coin][pool].devPool){ 412 | continue; 413 | } 414 | poolStates[coin][pool].share += finalMod; 415 | break; 416 | } 417 | } 418 | } 419 | delete(poolStates[coin].totalPercentage); 420 | } 421 | } 422 | /* 423 | poolStates now contains an object that looks approximately like: 424 | poolStates = { 425 | 'xmr': 426 | { 427 | 'mine.xmrpool.net': { 428 | 'miners': {}, 429 | 'hashrate': 0, 430 | 'percentage': 100, 431 | 'devPool': false 432 | }, 433 | 'donations.xmrpool.net': { 434 | 'miners': {}, 435 | 'hashrate': 0, 436 | 'percentage': 0, 437 | 'devPool': true 438 | }, 439 | 'devPool': 'donations.xmrpool.net', 440 | } 441 | } 442 | */ 443 | for (let workerID in activeWorkers){ 444 | if (activeWorkers.hasOwnProperty(workerID)){ 445 | for (let minerID in activeWorkers[workerID]){ 446 | if (activeWorkers[workerID].hasOwnProperty(minerID)){ 447 | let miner = activeWorkers[workerID][minerID]; 448 | try { 449 | let minerCoin = miner.coin; 450 | if (!minerStates.hasOwnProperty(minerCoin)){ 451 | minerStates[minerCoin] = { 452 | hashrate: 0 453 | }; 454 | } 455 | minerStates[minerCoin].hashrate += miner.avgSpeed; 456 | poolStates[minerCoin][miner.pool].hashrate += miner.avgSpeed; 457 | poolStates[minerCoin][miner.pool].miners[`${workerID}_${minerID}`] = miner.avgSpeed; 458 | } catch (err) {} 459 | } 460 | } 461 | } 462 | } 463 | /* 464 | poolStates now contains the hashrate per pool. This can be compared against minerStates/hashRate to determine 465 | the approximate hashrate that should be moved between pools once the general hashes/second per pool/worker 466 | is determined. 467 | */ 468 | for (let coin in poolStates){ 469 | if (poolStates.hasOwnProperty(coin) && minerStates.hasOwnProperty(coin)){ 470 | let coinMiners = minerStates[coin]; 471 | let coinPools = poolStates[coin]; 472 | let devPool = coinPools.devPool; 473 | let highPools = {}; 474 | let lowPools = {}; 475 | delete(coinPools.devPool); 476 | if (devPool){ 477 | let devHashrate = Math.floor(coinMiners.hashrate * (global.config.developerShare/100)); 478 | coinMiners.hashrate -= devHashrate; 479 | coinPools[devPool].idealRate = devHashrate; 480 | debug.balancer(`DevPool on ${coin} is enabled. Set to ${global.config.developerShare}% and ideally would have ${coinPools[devPool].idealRate}. Currently has ${coinPools[devPool].hashrate}`); 481 | if (coinPools[devPool].idealRate > coinPools[devPool].hashrate){ 482 | lowPools[devPool] = coinPools[devPool].idealRate - coinPools[devPool].hashrate; 483 | debug.balancer(`Pool ${devPool} is running a low hashrate compared to ideal. Want to increase by: ${lowPools[devPool]} h/s`); 484 | } else if (coinPools[devPool].idealRate < coinPools[devPool].hashrate){ 485 | highPools[devPool] = coinPools[devPool].hashrate - coinPools[devPool].idealRate; 486 | debug.balancer(`Pool ${devPool} is running a high hashrate compared to ideal. Want to decrease by: ${highPools[devPool]} h/s`); 487 | } 488 | } 489 | for (let pool in coinPools){ 490 | if (coinPools.hasOwnProperty(pool) && pool !== devPool && activePools.hasOwnProperty(pool)){ 491 | coinPools[pool].idealRate = Math.floor(coinMiners.hashrate * (coinPools[pool].percentage/100)); 492 | if (coinPools[pool].idealRate > coinPools[pool].hashrate){ 493 | lowPools[pool] = coinPools[pool].idealRate - coinPools[pool].hashrate; 494 | debug.balancer(`Pool ${pool} is running a low hashrate compared to ideal. Want to increase by: ${lowPools[pool]} h/s`); 495 | } else if (coinPools[pool].idealRate < coinPools[pool].hashrate){ 496 | highPools[pool] = coinPools[pool].hashrate - coinPools[pool].idealRate; 497 | debug.balancer(`Pool ${pool} is running a high hashrate compared to ideal. Want to decrease by: ${highPools[pool]} h/s`); 498 | } 499 | activePools[pool].share = coinPools[pool].percentage; 500 | } 501 | } 502 | if (Object.keys(highPools).length === 0 && Object.keys(lowPools).length === 0){ 503 | debug.balancer(`No pools in high or low Pools, so waiting for the next cycle.`); 504 | continue; 505 | } 506 | let freed_miners = {}; 507 | if (Object.keys(highPools).length > 0){ 508 | for (let pool in highPools){ 509 | if (highPools.hasOwnProperty(pool)){ 510 | for (let miner in coinPools[pool].miners){ 511 | if (coinPools[pool].miners.hasOwnProperty(miner)){ 512 | if (coinPools[pool].miners[miner] < highPools[pool] && coinPools[pool].miners[miner] !== 0){ 513 | highPools[pool] -= coinPools[pool].miners[miner]; 514 | freed_miners[miner] = coinPools[pool].miners[miner]; 515 | debug.balancer(`Freeing up ${miner} on ${pool} for ${freed_miners[miner]} h/s`); 516 | delete(coinPools[pool].miners[miner]); 517 | } 518 | } 519 | } 520 | } 521 | } 522 | } 523 | let minerChanges = {}; 524 | if (Object.keys(lowPools).length > 0){ 525 | for (let pool in lowPools){ 526 | if (lowPools.hasOwnProperty(pool)){ 527 | minerChanges[pool] = []; 528 | if (Object.keys(freed_miners).length > 0){ 529 | for (let miner in freed_miners){ 530 | if (freed_miners.hasOwnProperty(miner)){ 531 | if (freed_miners[miner] <= lowPools[pool]){ 532 | minerChanges[pool].push(miner); 533 | lowPools[pool] -= freed_miners[miner]; 534 | debug.balancer(`Snagging up ${miner} for ${pool} for ${freed_miners[miner]} h/s`); 535 | delete(freed_miners[miner]); 536 | } 537 | } 538 | } 539 | } 540 | if(lowPools[pool] > 100){ 541 | for (let donatorPool in coinPools){ 542 | if(coinPools.hasOwnProperty(donatorPool) && !lowPools.hasOwnProperty(donatorPool)){ 543 | for (let miner in coinPools[donatorPool].miners){ 544 | if (coinPools[donatorPool].miners.hasOwnProperty(miner)){ 545 | if (coinPools[donatorPool].miners[miner] < lowPools[pool] && coinPools[donatorPool].miners[miner] !== 0){ 546 | minerChanges[pool].push(miner); 547 | lowPools[pool] -= coinPools[donatorPool].miners[miner]; 548 | debug.balancer(`Moving ${miner} for ${pool} from ${donatorPool} for ${coinPools[donatorPool].miners[miner]} h/s`); 549 | delete(coinPools[donatorPool].miners[miner]); 550 | } 551 | if (lowPools[pool] < 50){ 552 | break; 553 | } 554 | } 555 | } 556 | if (lowPools[pool] < 50){ 557 | break; 558 | } 559 | } 560 | } 561 | } 562 | } 563 | } 564 | } 565 | for (let pool in minerChanges){ 566 | if(minerChanges.hasOwnProperty(pool) && minerChanges[pool].length > 0){ 567 | minerChanges[pool].forEach(function(miner){ 568 | let minerBits = miner.split('_'); 569 | cluster.workers[minerBits[0]].send({ 570 | type: 'changePool', 571 | worker: minerBits[1], 572 | pool: pool 573 | }); 574 | }); 575 | } 576 | } 577 | } 578 | } 579 | } 580 | 581 | function enumerateWorkerStats(){ 582 | let stats, global_stats = {miners: 0, hashes: 0, hashRate: 0, diff: 0}; 583 | for (let poolID in activeWorkers){ 584 | if (activeWorkers.hasOwnProperty(poolID)){ 585 | stats = { 586 | miners: 0, 587 | hashes: 0, 588 | hashRate: 0, 589 | diff: 0 590 | }; 591 | for (let workerID in activeWorkers[poolID]){ 592 | if (activeWorkers[poolID].hasOwnProperty(workerID)) { 593 | let workerData = activeWorkers[poolID][workerID]; 594 | if (typeof workerData !== 'undefined') { 595 | try{ 596 | if (workerData.lastContact < ((Math.floor((Date.now())/1000) - 120))){ 597 | delete activeWorkers[poolID][workerID]; 598 | continue; 599 | } 600 | stats.miners += 1; 601 | stats.hashes += workerData.hashes; 602 | stats.hashRate += workerData.avgSpeed; 603 | stats.diff += workerData.diff; 604 | } catch (err) { 605 | delete activeWorkers[poolID][workerID]; 606 | } 607 | } else { 608 | delete activeWorkers[poolID][workerID]; 609 | } 610 | } 611 | } 612 | global_stats.miners += stats.miners; 613 | global_stats.hashes += stats.hashes; 614 | global_stats.hashRate += stats.hashRate; 615 | global_stats.diff += stats.diff; 616 | debug.workers(`Worker: ${poolID} currently has ${stats.miners} miners connected at ${stats.hashRate} h/s with an average diff of ${Math.floor(stats.diff/stats.miners)}`); 617 | } 618 | } 619 | console.log(`The proxy currently has ${global_stats.miners} miners connected at ${global_stats.hashRate} h/s with an average diff of ${Math.floor(global_stats.diff/global_stats.miners)}`); 620 | } 621 | 622 | function poolSocket(hostname){ 623 | let pool = activePools[hostname]; 624 | let socket = pool.socket; 625 | let dataBuffer = ''; 626 | socket.on('data', (d) => { 627 | dataBuffer += d; 628 | if (dataBuffer.indexOf('\n') !== -1) { 629 | let messages = dataBuffer.split('\n'); 630 | let incomplete = dataBuffer.slice(-1) === '\n' ? '' : messages.pop(); 631 | for (let i = 0; i < messages.length; i++) { 632 | let message = messages[i]; 633 | if (message.trim() === '') { 634 | continue; 635 | } 636 | let jsonData; 637 | try { 638 | jsonData = JSON.parse(message); 639 | } 640 | catch (e) { 641 | if (message.indexOf('GET /') === 0) { 642 | if (message.indexOf('HTTP/1.1') !== -1) { 643 | socket.end('HTTP/1.1' + httpResponse); 644 | break; 645 | } 646 | else if (message.indexOf('HTTP/1.0') !== -1) { 647 | socket.end('HTTP/1.0' + httpResponse); 648 | break; 649 | } 650 | } 651 | 652 | console.warn(`${global.threadName}Socket error from ${pool.hostname} Message: ${message}`); 653 | socket.destroy(); 654 | 655 | break; 656 | } 657 | handlePoolMessage(jsonData, pool.hostname); 658 | } 659 | dataBuffer = incomplete; 660 | } 661 | }).on('error', (err) => { 662 | activePools[pool.hostname].connect(); 663 | console.warn(`${global.threadName}Socket error from ${pool.hostname} ${err}`); 664 | }).on('close', () => { 665 | activePools[pool.hostname].connect(); 666 | console.warn(`${global.threadName}Socket closed from ${pool.hostname}`); 667 | }); 668 | socket.setKeepAlive(true); 669 | socket.setEncoding('utf8'); 670 | console.log(`${global.threadName}connected to pool: ${pool.hostname}`); 671 | pool.login(); 672 | setInterval(pool.heartbeat, 30000); 673 | } 674 | 675 | function handlePoolMessage(jsonData, hostname){ 676 | let pool = activePools[hostname]; 677 | debug.pool(`Received ${JSON.stringify(jsonData)} from ${pool.hostname}`); 678 | if (jsonData.hasOwnProperty('method')){ 679 | // The only time method is set, is with a push of data. Everything else is a reply/ 680 | if (jsonData.method === 'job'){ 681 | handleNewBlockTemplate(jsonData.params, hostname); 682 | } 683 | } else { 684 | if (jsonData.error !== null){ 685 | if (jsonData.error.message === 'Unauthenticated'){ 686 | activePools[hostname].connect(); 687 | } 688 | return console.error(`Error response from pool ${pool.hostname}: ${JSON.stringify(jsonData.error)}`); 689 | } 690 | let sendLog = pool.sendLog[jsonData.id]; 691 | switch(sendLog.method){ 692 | case 'login': 693 | pool.id = jsonData.result.id; 694 | handleNewBlockTemplate(jsonData.result.job, hostname); 695 | break; 696 | case 'getjob': 697 | handleNewBlockTemplate(jsonData.result, hostname); 698 | break; 699 | case 'submit': 700 | sendLog.accepted = true; 701 | break; 702 | } 703 | } 704 | } 705 | 706 | function handleNewBlockTemplate(blockTemplate, hostname){ 707 | let pool = activePools[hostname]; 708 | console.log(`Received new block template from ${pool.hostname}`); 709 | if(pool.activeBlocktemplate){ 710 | if (pool.activeBlocktemplate.job_id === blockTemplate.job_id){ 711 | debug.pool('No update with this job, it is an upstream dupe'); 712 | return; 713 | } 714 | debug.pool('Storing the previous block template'); 715 | pool.pastBlockTemplates.enq(pool.activeBlocktemplate); 716 | } 717 | pool.activeBlocktemplate = new pool.coinFuncs.MasterBlockTemplate(blockTemplate); 718 | for (let id in cluster.workers){ 719 | if (cluster.workers.hasOwnProperty(id)){ 720 | cluster.workers[id].send({ 721 | host: hostname, 722 | type: 'newBlockTemplate', 723 | data: pool.coinFuncs.getMasterJob(pool, id) 724 | }); 725 | } 726 | } 727 | } 728 | 729 | // Miner Definition 730 | function Miner(id, params, ip, pushMessage, portData, minerSocket) { 731 | // Arguments 732 | // minerId, params, ip, pushMessage, portData 733 | // Username Layout -
. 734 | // Password Layout - .. 735 | // Default function is to use the password so they can login. Identifiers can be unique, payment ID is last. 736 | // If there is no miner identifier, then the miner identifier is set to the password 737 | // If the password is x, aka, old-logins, we're not going to allow detailed review of miners. 738 | 739 | // Miner Variables 740 | this.coin = portData.coin; 741 | this.coinFuncs = require(`./lib/${this.coin}.js`)(); 742 | this.coinSettings = global.config.coinSettings[this.coin]; 743 | this.login = params.login; // Documentation purposes only. 744 | this.password = params.pass; // Documentation purposes only. 745 | this.agent = params.agent; // Documentation purposes only. 746 | this.ip = ip; // Documentation purposes only. 747 | this.socket = minerSocket; 748 | this.messageSender = pushMessage; 749 | this.error = ""; 750 | this.valid_miner = true; 751 | this.incremented = false; 752 | let diffSplit = this.login.split("+"); 753 | this.fixed_diff = false; 754 | this.difficulty = portData.diff; 755 | this.connectTime = Date.now(); 756 | this.pool = defaultPools[portData.coin]; 757 | 758 | if (diffSplit.length === 2) { 759 | this.fixed_diff = true; 760 | this.difficulty = Number(diffSplit[1]); 761 | } else if (diffSplit.length > 2) { 762 | this.error = "Too many options in the login field"; 763 | this.valid_miner = false; 764 | } 765 | 766 | if (activePools[this.pool].activeBlocktemplate === null){ 767 | this.error = "No active block template"; 768 | this.valid_miner = false; 769 | } 770 | 771 | this.id = id; 772 | this.heartbeat = function () { 773 | this.lastContact = Date.now(); 774 | }; 775 | this.heartbeat(); 776 | 777 | // VarDiff System 778 | this.shareTimeBuffer = support.circularBuffer(8); 779 | this.shareTimeBuffer.enq(this.coinSettings.shareTargetTime); 780 | this.lastShareTime = Date.now() / 1000 || 0; 781 | 782 | this.shares = 0; 783 | this.blocks = 0; 784 | this.hashes = 0; 785 | this.logString = this.id + " IP: " + this.ip; 786 | 787 | this.validJobs = support.circularBuffer(5); 788 | 789 | this.cachedJob = null; 790 | 791 | this.minerStats = function(){ 792 | if (this.socket.destroyed){ 793 | delete activeMiners[this.id]; 794 | return; 795 | } 796 | return { 797 | shares: this.shares, 798 | blocks: this.blocks, 799 | hashes: this.hashes, 800 | avgSpeed: Math.floor(this.hashes/(Math.floor((Date.now() - this.connectTime)/1000))), 801 | diff: this.difficulty, 802 | lastContact: Math.floor(this.lastContact/1000), 803 | lastShare: this.lastShareTime, 804 | coin: this.coin, 805 | pool: this.pool, 806 | id: this.id 807 | }; 808 | }; 809 | 810 | // Support functions for how miners activate and run. 811 | this.updateDifficulty = function(){ 812 | if (this.hashes > 0 && !this.fixed_diff) { 813 | this.setNewDiff(Math.floor(this.hashes / (Math.floor((Date.now() - this.connectTime) / 1000))) * this.coinSettings.shareTargetTime); 814 | } 815 | }; 816 | 817 | this.setNewDiff = function (difficulty) { 818 | this.newDiff = Math.round(difficulty); 819 | debug.diff(global.threadName + "Difficulty: " + this.newDiff + " For: " + this.logString + " Time Average: " + this.shareTimeBuffer.average(this.lastShareTime) + " Entries: " + this.shareTimeBuffer.size() + " Sum: " + this.shareTimeBuffer.sum()); 820 | if (this.newDiff > this.coinSettings.maxDiff) { 821 | this.newDiff = this.coinSettings.maxDiff; 822 | } 823 | if (this.newDiff < this.coinSettings.minDiff) { 824 | this.newDiff = this.coinSettings.minDiff; 825 | } 826 | if (this.difficulty === this.newDiff) { 827 | return; 828 | } 829 | debug.diff(global.threadName + "Difficulty change to: " + this.newDiff + " For: " + this.logString); 830 | if (this.hashes > 0){ 831 | debug.diff(global.threadName + "Hashes: " + this.hashes + " in: " + Math.floor((Date.now() - this.connectTime)/1000) + " seconds gives: " + 832 | Math.floor(this.hashes/(Math.floor((Date.now() - this.connectTime)/1000))) + " hashes/second or: " + 833 | Math.floor(this.hashes/(Math.floor((Date.now() - this.connectTime)/1000))) *this.coinSettings.shareTargetTime + " difficulty versus: " + this.newDiff); 834 | } 835 | this.messageSender('job', this.getJob(activeMiners[this.id], activePools[this.pool].activeBlocktemplate)); 836 | }; 837 | 838 | this.getJob = this.coinFuncs.getJob; 839 | } 840 | 841 | // Slave Functions 842 | function handleMinerData(method, params, ip, portData, sendReply, pushMessage, minerSocket) { 843 | /* 844 | Deals with handling the data from miners in a sane-ish fashion. 845 | */ 846 | let miner = activeMiners[params.id]; 847 | // Check for ban here, so preconnected attackers can't continue to screw you 848 | if (ip in bans) { 849 | // Handle IP ban off clip. 850 | sendReply("IP Address currently banned"); 851 | return; 852 | } 853 | switch (method) { 854 | case 'login': 855 | let difficulty = portData.difficulty; 856 | let minerId = uuidV4(); 857 | miner = new Miner(minerId, params, ip, pushMessage, portData, minerSocket); 858 | if (!miner.valid_miner) { 859 | console.log("Invalid miner, disconnecting due to: " + miner.error); 860 | sendReply(miner.error); 861 | return; 862 | } 863 | process.send({type: 'newMiner', data: miner.port}); 864 | activeMiners[minerId] = miner; 865 | sendReply(null, { 866 | id: minerId, 867 | job: miner.getJob(miner, activePools[miner.pool].activeBlocktemplate), 868 | status: 'OK' 869 | }); 870 | return minerId; 871 | case 'getjob': 872 | if (!miner) { 873 | sendReply('Unauthenticated'); 874 | return; 875 | } 876 | miner.heartbeat(); 877 | sendReply(null, miner.getJob(miner, activePools[miner.pool].activeBlocktemplate)); 878 | break; 879 | case 'submit': 880 | if (!miner) { 881 | sendReply('Unauthenticated'); 882 | return; 883 | } 884 | miner.heartbeat(); 885 | 886 | let job = miner.validJobs.toarray().filter(function (job) { 887 | return job.id === params.job_id; 888 | })[0]; 889 | 890 | if (!job) { 891 | sendReply('Invalid job id'); 892 | return; 893 | } 894 | 895 | params.nonce = params.nonce.substr(0, 8).toLowerCase(); 896 | if (!nonceCheck.test(params.nonce)) { 897 | console.warn(global.threadName + 'Malformed nonce: ' + JSON.stringify(params) + ' from ' + miner.logString); 898 | sendReply('Duplicate share'); 899 | return; 900 | } 901 | 902 | if (job.submissions.indexOf(params.nonce) !== -1) { 903 | console.warn(global.threadName + 'Duplicate share: ' + JSON.stringify(params) + ' from ' + miner.logString); 904 | sendReply('Duplicate share'); 905 | return; 906 | } 907 | 908 | job.submissions.push(params.nonce); 909 | let activeBlockTemplate = activePools[miner.pool].activeBlocktemplate; 910 | let pastBlockTemplates = activePools[miner.pool].pastBlockTemplates; 911 | 912 | let blockTemplate = activeBlockTemplate.id === job.templateID ? activeBlockTemplate : pastBlockTemplates.toarray().filter(function (t) { 913 | return t.id === job.templateID; 914 | })[0]; 915 | 916 | if (!blockTemplate) { 917 | console.warn(global.threadName + 'Block expired, Height: ' + job.height + ' from ' + miner.logString); 918 | if (miner.incremented === false){ 919 | miner.newDiff = miner.difficulty + 1; 920 | miner.incremented = true; 921 | } else { 922 | miner.newDiff = miner.difficulty - 1; 923 | miner.incremented = false; 924 | } 925 | miner.messageSender('job', miner.getJob(miner, activePools[miner.pool].activeBlocktemplate, true)); 926 | sendReply('Block expired'); 927 | return; 928 | } 929 | 930 | let shareAccepted = miner.coinFuncs.processShare(miner, job, blockTemplate, params.nonce, params.result); 931 | 932 | if (!shareAccepted) { 933 | sendReply('Low difficulty share'); 934 | return; 935 | } 936 | 937 | let now = Date.now() / 1000 || 0; 938 | miner.shareTimeBuffer.enq(now - miner.lastShareTime); 939 | miner.lastShareTime = now; 940 | 941 | sendReply(null, {status: 'OK'}); 942 | break; 943 | case 'keepalived': 944 | if (!miner) { 945 | sendReply('Unauthenticated'); 946 | return; 947 | } 948 | sendReply(null, { 949 | status: 'KEEPALIVED' 950 | }); 951 | break; 952 | } 953 | } 954 | 955 | function activatePorts() { 956 | /* 957 | Reads the current open ports, and then activates any that aren't active yet 958 | { "port": 80, "ssl": false, "diff": 5000 } 959 | and binds a listener to it. 960 | */ 961 | async.each(global.config.listeningPorts, function (portData) { 962 | if (activePorts.indexOf(portData.port) !== -1) { 963 | return; 964 | } 965 | let handleMessage = function (socket, jsonData, pushMessage, minerSocket) { 966 | if (!jsonData.id) { 967 | console.warn('Miner RPC request missing RPC id'); 968 | return; 969 | } 970 | else if (!jsonData.method) { 971 | console.warn('Miner RPC request missing RPC method'); 972 | return; 973 | } 974 | else if (!jsonData.params) { 975 | console.warn('Miner RPC request missing RPC params'); 976 | return; 977 | } 978 | 979 | let sendReply = function (error, result) { 980 | if (!socket.writable) { 981 | return; 982 | } 983 | let sendData = JSON.stringify({ 984 | id: jsonData.id, 985 | jsonrpc: "2.0", 986 | error: error ? {code: -1, message: error} : null, 987 | result: result 988 | }) + "\n"; 989 | debug.miners(`Data sent to miner (sendReply): ${sendData}`); 990 | socket.write(sendData); 991 | }; 992 | handleMinerData(jsonData.method, jsonData.params, socket.remoteAddress, portData, sendReply, pushMessage, minerSocket); 993 | }; 994 | 995 | function socketConn(socket) { 996 | socket.setKeepAlive(true); 997 | socket.setEncoding('utf8'); 998 | 999 | let dataBuffer = ''; 1000 | 1001 | let pushMessage = function (method, params) { 1002 | if (!socket.writable) { 1003 | return; 1004 | } 1005 | let sendData = JSON.stringify({ 1006 | jsonrpc: "2.0", 1007 | method: method, 1008 | params: params 1009 | }) + "\n"; 1010 | debug.miners(`Data sent to miner (pushMessage): ${sendData}`); 1011 | socket.write(sendData); 1012 | }; 1013 | 1014 | socket.on('data', function (d) { 1015 | dataBuffer += d; 1016 | if (Buffer.byteLength(dataBuffer, 'utf8') > 102400) { //10KB 1017 | dataBuffer = null; 1018 | console.warn(global.threadName + 'Excessive packet size from: ' + socket.remoteAddress); 1019 | socket.destroy(); 1020 | return; 1021 | } 1022 | if (dataBuffer.indexOf('\n') !== -1) { 1023 | let messages = dataBuffer.split('\n'); 1024 | let incomplete = dataBuffer.slice(-1) === '\n' ? '' : messages.pop(); 1025 | for (let i = 0; i < messages.length; i++) { 1026 | let message = messages[i]; 1027 | if (message.trim() === '') { 1028 | continue; 1029 | } 1030 | let jsonData; 1031 | debug.miners(`Data from miner: ${message}`); 1032 | try { 1033 | jsonData = JSON.parse(message); 1034 | } 1035 | catch (e) { 1036 | if (message.indexOf('GET /') === 0) { 1037 | if (message.indexOf('HTTP/1.1') !== -1) { 1038 | socket.end('HTTP/1.1' + httpResponse); 1039 | break; 1040 | } 1041 | else if (message.indexOf('HTTP/1.0') !== -1) { 1042 | socket.end('HTTP/1.0' + httpResponse); 1043 | break; 1044 | } 1045 | } 1046 | console.warn(global.threadName + "Malformed message from " + socket.remoteAddress + " Message: " + message); 1047 | socket.destroy(); 1048 | break; 1049 | } 1050 | handleMessage(socket, jsonData, pushMessage, socket); 1051 | } 1052 | dataBuffer = incomplete; 1053 | } 1054 | }).on('error', function (err) { 1055 | if (err.code !== 'ECONNRESET') { 1056 | console.warn(global.threadName + "Socket Error from " + socket.remoteAddress + " " + err); 1057 | } 1058 | socket.end(); 1059 | socket.destroy(); 1060 | }).on('close', function () { 1061 | pushMessage = function () { 1062 | }; 1063 | debug.miners('Miner disconnected via standard close'); 1064 | socket.end(); 1065 | socket.destroy(); 1066 | }); 1067 | } 1068 | 1069 | if ('ssl' in portData && portData.ssl === true) { 1070 | tls.createServer({ 1071 | key: fs.readFileSync('cert.key'), 1072 | cert: fs.readFileSync('cert.pem') 1073 | }, socketConn).listen(portData.port, global.config.bindAddress, function (error) { 1074 | if (error) { 1075 | console.error(global.threadName + "Unable to start server on: " + portData.port + " Message: " + error); 1076 | return; 1077 | } 1078 | activePorts.push(portData.port); 1079 | console.log(global.threadName + "Started server on port: " + portData.port); 1080 | }); 1081 | } else { 1082 | net.createServer(socketConn).listen(portData.port, global.config.bindAddress, function (error) { 1083 | if (error) { 1084 | console.error(global.threadName + "Unable to start server on: " + portData.port + " Message: " + error); 1085 | return; 1086 | } 1087 | activePorts.push(portData.port); 1088 | console.log(global.threadName + "Started server on port: " + portData.port); 1089 | }); 1090 | } 1091 | }); 1092 | } 1093 | 1094 | function checkActivePools() { 1095 | for (let badPool in activePools){ 1096 | if (activePools.hasOwnProperty(badPool) && !activePools[badPool].active) { 1097 | for (let pool in activePools) { 1098 | if (activePools.hasOwnProperty(pool) && !activePools[pool].devPool && activePools[pool].coin === activePools[badPool].coin && activePools[pool].active) { 1099 | for (let miner in activeMiners) { 1100 | if (activeMiners.hasOwnProperty(miner)) { 1101 | let realMiner = activeMiners[miner]; 1102 | if (realMiner.pool === badPool) { 1103 | realMiner.pool = pool; 1104 | realMiner.messageSender('job', realMiner.getJob(realMiner, activePools[pool].activeBlocktemplate)); 1105 | } 1106 | } 1107 | } 1108 | break; 1109 | } 1110 | } 1111 | } 1112 | } 1113 | } 1114 | 1115 | // API Calls 1116 | 1117 | // System Init 1118 | 1119 | if (cluster.isMaster) { 1120 | let numWorkers; 1121 | try { 1122 | let argv = require('minimist')(process.argv.slice(2)); 1123 | if (typeof argv.workers !== 'undefined') { 1124 | numWorkers = Number(argv.workers); 1125 | } else { 1126 | numWorkers = require('os').cpus().length; 1127 | } 1128 | } catch (err) { 1129 | console.error(`Unable to set the number of workers via arguments. Make sure to run npm install!`); 1130 | numWorkers = require('os').cpus().length; 1131 | } 1132 | global.threadName = 'Master '; 1133 | console.log('Cluster master setting up ' + numWorkers + ' workers...'); 1134 | cluster.on('message', masterMessageHandler); 1135 | for (let i = 0; i < numWorkers; i++) { 1136 | let worker = cluster.fork(); 1137 | worker.on('message', slaveMessageHandler); 1138 | } 1139 | 1140 | cluster.on('online', function (worker) { 1141 | console.log('Worker ' + worker.process.pid + ' is online'); 1142 | activeWorkers[worker.id] = {}; 1143 | }); 1144 | 1145 | cluster.on('exit', function (worker, code, signal) { 1146 | console.log('Worker ' + worker.process.pid + ' died with code: ' + code + ', and signal: ' + signal); 1147 | console.log('Starting a new worker'); 1148 | worker = cluster.fork(); 1149 | worker.on('message', slaveMessageHandler); 1150 | }); 1151 | connectPools(); 1152 | setInterval(enumerateWorkerStats, 15000); 1153 | setInterval(balanceWorkers, 90000); 1154 | } else { 1155 | /* 1156 | setInterval(checkAliveMiners, 30000); 1157 | setInterval(retargetMiners, global.config.pool.retargetTime * 1000); 1158 | */ 1159 | process.on('message', slaveMessageHandler); 1160 | global.config.pools.forEach(function(poolData){ 1161 | activePools[poolData.hostname] = new Pool(poolData); 1162 | if (poolData.default){ 1163 | defaultPools[poolData.coin] = poolData.hostname; 1164 | } 1165 | if (!activePools.hasOwnProperty(activePools[poolData.hostname].coinFuncs.devPool.hostname)){ 1166 | activePools[activePools[poolData.hostname].coinFuncs.devPool.hostname] = new Pool(activePools[poolData.hostname].coinFuncs.devPool); 1167 | } 1168 | }); 1169 | process.send({type: 'needPoolState'}); 1170 | setInterval(function(){ 1171 | for (let minerID in activeMiners){ 1172 | if (activeMiners.hasOwnProperty(minerID)){ 1173 | activeMiners[minerID].updateDifficulty(); 1174 | } 1175 | } 1176 | }, 45000); 1177 | setInterval(function(){ 1178 | for (let minerID in activeMiners){ 1179 | if (activeMiners.hasOwnProperty(minerID)){ 1180 | process.send({minerID: minerID, data: activeMiners[minerID].minerStats(), type: 'workerStats'}); 1181 | } 1182 | } 1183 | }, 10000); 1184 | setInterval(checkActivePools, 90000); 1185 | activatePorts(); 1186 | } 1187 | -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | git checkout . 2 | git pull 3 | npm install 4 | echo "Proxy updated! Please go ahead and restart with the correct pm2 command" 5 | echo "This is usually pm2 restart proxy, however, you can use pm2 list to check for your exact proxy command" 6 | --------------------------------------------------------------------------------