├── .gitignore ├── Dockerfile ├── README.md ├── accessControl_example.json ├── config_example.json ├── install.sh ├── lib ├── 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:22.04 2 | 3 | RUN rm /bin/sh && ln -s /bin/bash /bin/sh 4 | 5 | RUN apt-get update \ 6 | && apt-get install -y curl gnupg \ 7 | && curl -fsSL https://deb.nodesource.com/setup_14.x -o /tmp/node_setup.sh \ 8 | && bash /tmp/node_setup.sh \ 9 | && rm /tmp/node_setup.sh \ 10 | && apt-get install -y nodejs git make g++ libboost-dev libboost-system-dev libboost-date-time-dev libsodium-dev \ 11 | && git clone https://github.com/MoneroOcean/xmr-node-proxy /xmr-node-proxy \ 12 | && cd /xmr-node-proxy \ 13 | && npm install \ 14 | && cp -n config_example.json config.json \ 15 | && 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 16 | 17 | EXPOSE 8080 8443 3333 18 | 19 | WORKDIR /xmr-node-proxy 20 | CMD ./update.sh && node proxy.js 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xmr-node-proxy 2 | 3 | Supports all known cryptonight/heavy/light/pico coins: 4 | 5 | * Monero (XMR), MoneroV (XMV), Monero Original (XMO), Monero Classic (XMC), ... 6 | * Wownero (WOW), Masari (MSR), Electroneum (ETN), Graft (GRFT), Intense (ITNS) 7 | * Stellite (XTL) 8 | * Aeon (AEON), Turtlecoin (TRTL), IPBC/BitTube (TUBE) 9 | * Sumokoin (SUMO), Haven (XHV), Loki (LOKI) 10 | * ... 11 | 12 | ## Setup Instructions 13 | 14 | Based on a clean Ubuntu 16.04 LTS minimal install 15 | 16 | ## Switching from other xmr-node-proxy repository 17 | 18 | ```bash 19 | cd xmr-node-proxy 20 | git remote set-url origin https://github.com/MoneroOcean/xmr-node-proxy.git && git pull -X theirs --no-edit && npm update 21 | ``` 22 | 23 | ## Deployment via Installer on Linux 24 | 25 | 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) 26 | 27 | ```bash 28 | useradd -d /home/nodeproxy -m -s /bin/bash nodeproxy 29 | passwd nodeproxy 30 | ``` 31 | 32 | 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` 33 | 34 | ```bash 35 | echo "nodeproxy ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers 36 | ``` 37 | 38 | 3. Log in as the **NON-ROOT USER** you just created and run the [deploy script](https://raw.githubusercontent.com/MoneroOcean/xmr-node-proxy/master/install.sh). This is very important! This script will install the proxy to whatever user it's running under! 39 | 40 | ```bash 41 | curl -L https://raw.githubusercontent.com/MoneroOcean/xmr-node-proxy/master/install.sh | bash 42 | ``` 43 | 44 | 3. Once it's complete, copy `config_example.json` to `config.json` and edit as desired. 45 | 4. Run: `source ~/.bashrc` This will activate NVM and get things working for the following pm2 steps. 46 | 8. Once you're happy with the settings, go ahead and start all the proxy daemon, commands follow. 47 | 48 | ```shell 49 | cd ~/xmr-node-proxy/ 50 | pm2 start proxy.js --name=proxy --log-date-format="YYYY-MM-DD HH:mm:ss:SSS Z" 51 | pm2 save 52 | ``` 53 | You can check the status of your proxy by either issuing 54 | 55 | ``` 56 | pm2 logs proxy 57 | ``` 58 | 59 | or using the pm2 monitor 60 | 61 | ``` 62 | pm2 monit 63 | ``` 64 | 65 | ## Updating xmr-node-proxy 66 | 67 | ```bash 68 | cd xmr-node-proxy 69 | ./update.sh 70 | ``` 71 | 72 | ## Deployment via Docker on Windows 10 with the Fall Creators Update (or newer) 73 | 74 | 1. Install and run [Docker for Windows](https://docs.docker.com/docker-for-windows/install/) with Linux containers mode. 75 | 76 | 2. Get xmr-node-proxy sources by downloading and unpacking the latest [xmr-node-proxy](https://github.com/MoneroOcean/xmr-node-proxy/archive/master.zip) 77 | archive to xmr-node-proxy-master directory. 78 | 79 | 3. Got to xmr-node-proxy-master directory in Windows "Command Prompt" and build xmr-node-proxy Docker image: 80 | 81 | ``` 82 | docker build . -t xmr-node-proxy 83 | ``` 84 | 85 | 4. Copy config_example.json to config.json and edit config.json file as desired (do not forget to update default XMR wallet). 86 | 87 | 5. Create xnp Docker contained based on xmr-node-proxy image (make sure to update port numbers if you changed them in config.json): 88 | 89 | ``` 90 | docker create -p 3333:3333 -p 8080:8080 -p 8443:8443 --name xnp xmr-node-proxy 91 | ``` 92 | 93 | 6. Copy your modified config.json to xnp Docker container: 94 | 95 | ``` 96 | docker cp config.json xnp:/xmr-node-proxy 97 | ``` 98 | 99 | 7. Run xnp Docker container (or attach to already running one): 100 | 101 | ``` 102 | docker start --attach xnp 103 | ``` 104 | 105 | 8. Stop xnp Docker container (to start it again with update): 106 | 107 | ``` 108 | docker stop xnp 109 | ``` 110 | 111 | 9. Delete xnp Docker container (if you want to create it again with different ports): 112 | 113 | ``` 114 | docker rm xnp 115 | ``` 116 | 117 | 10. Delete xmr-node-proxy Docker image (if you no longer need proxy): 118 | 119 | ``` 120 | docker rmi xmr-node-proxy 121 | ``` 122 | 123 | 124 | ## Configuration BKMs 125 | 126 | 1. Specify at least one main pool with non zero share and "default: true". Sum of all non zero pool shares should be equal to 100 (percent). 127 | 128 | 2. There should be one pool with "default: true" (the last one will override previous ones with "default: true"). Default pool means pool that is used 129 | for all initial miner connections via proxy. 130 | 131 | 3. You can use pools with zero share as backup pools. They will be only used if all non zero share pools became down. 132 | 133 | 4. You should select pool port with difficulty that is close to hashrate of all of your miners multiplied by 10. 134 | 135 | 5. Proxy ports should have difficulty close to your individual miner hashrate multiplied by 10. 136 | 137 | 6. Algorithm names ("algo" option in pool config section) can be taken from [Algorithm names and variants](https://github.com/xmrig/xmrig-proxy/blob/dev/doc/STRATUM_EXT.md#14-algorithm-names-and-variants) table 138 | 139 | 7. Blob type ("blob_type" option in pool config section) can be as follows 140 | 141 | * cryptonote - Monero forks like Sumokoin, Electroneum, Graft, Aeon, Intense 142 | 143 | * cryptonote2 - Masari 144 | 145 | * forknote - Some old Bytecoin forks (do not even know which one) 146 | 147 | * forknote2 - Bytecoin forks like Turtlecoin, IPBC 148 | 149 | ## Known Issues 150 | 151 | VMs with 512Mb or less RAM will need some swap space in order to compile the C extensions for node. 152 | Bignum and the CN libraries can chew through some serious memory during compile. 153 | In regards to this here is guide for T2.Micro servers: [Setup of xmr-node-proxy on free tier AWS t2.micro instance](http://moneroocean.blogspot.com/2017/10/setup-of-xmr-node-proxy-on-free-tier.html). 154 | There is also more generic proxy instalation guide: [Complete guide to install and configure xmr-node-proxy on a Ubuntu 16.04 VPS](https://tjosm.com/7689/install-xmr-node-proxy-vps/) 155 | 156 | 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. 157 | 158 | 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. 159 | 160 | In your `packages.json`, do a `npm install`, and it should pass. 161 | 162 | 163 | ## Performance 164 | 165 | 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. 166 | 167 | 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. 168 | 169 | ## Configuration Guidelines 170 | 171 | Please check the [wiki](https://github.com/MoneroOcean/xmr-node-proxy/wiki/config_review) for information on configuration 172 | 173 | Developer Donations 174 | =================== 175 | If you'd like to make a one time donation, the addresses are as follows: 176 | * XMR - ```89TxfrUmqJJcb1V124WsUzA78Xa3UYHt7Bg8RGMhXVeZYPN8cE5CZEk58Y1m23ZMLHN7wYeJ9da5n5MXharEjrm41hSnWHL``` 177 | * AEON - ```WmsEg3RuUKCcEvFBtXcqRnGYfiqGJLP1FGBYiNMgrcdUjZ8iMcUn2tdcz59T89inWr9Vae4APBNf7Bg2DReFP5jr23SQqaDMT``` 178 | * ETN - ```etnkQMp3Hmsay2p7uxokuHRKANrMDNASwQjDUgFb5L2sDM3jqUkYQPKBkooQFHVWBzEaZVzfzrXoETX6RbMEvg4R4csxfRHLo1``` 179 | * SUMO - ```Sumoo1DGS7c9LEKZNipsiDEqRzaUB3ws7YHfUiiZpx9SQDhdYGEEbZjRET26ewuYEWAZ8uKrz6vpUZkEVY7mDCZyGnQhkLpxKmy``` 180 | * GRFT - ```GACadqdXj5eNLnyNxvQ56wcmsmVCFLkHQKgtaQXNEE5zjMDJkWcMVju2aYtxbTnZgBboWYmHovuiH1Ahm4g2N5a7LuMQrpT``` 181 | * MSR - ```5hnMXUKArLDRue5tWsNpbmGLsLQibt23MEsV3VGwY6MGStYwfTqHkff4BgvziprTitbcDYYpFXw2rEgXeipsABTtEmcmnCK``` 182 | * ITNS - ```iz53aMEaKJ25zB8xku3FQK5VVvmu2v6DENnbGHRmn659jfrGWBH1beqAzEVYaKhTyMZcxLJAdaCW3Kof1DwTiTbp1DSqLae3e``` 183 | * WOW - ```Wo3yjV8UkwvbJDCB1Jy7vvXv3aaQu3K8YMG6tbY3Jo2KApfyf5RByZiBXy95bzmoR3AvPgNq6rHzm98LoHTkzjiA2dY7sqQMJ``` 184 | * XMV - ```XvyVfpAYp3zSuvdtoHgnDzMUf7GAeiumeUgVC7RTq6SfgtzGEzy4dUgfEEfD5adk1kN4dfVZdT3zZdgSD2xmVBs627Vwt2C3Ey``` 185 | * RYO - ```RYoLsi22qnoKYhnv1DwHBXcGe9QK6P9zmekwQnHdUAak7adFBK4i32wFTszivQ9wEPeugbXr2UD7tMd6ogf1dbHh76G5UszE7k1``` 186 | * XLA - ```SvkpUizij25ZGRHGb1c8ZTAHp3VyNFU3NQuQR1PtMyCqdpoZpaYAGMfG99z5guuoktY13nrhEerqYNKXvoxD7cUM1xA6Z5rRY``` 187 | * XHV - ```hvxyEmtbqs5TEk9U2tCxyfGx2dyGD1g8EBspdr3GivhPchkvnMHtpCR2fGLc5oEY42UGHVBMBANPge5QJ7BDXSMu1Ga2KFspQR``` 188 | * TUBE - ```TubedBNkgkTbd2CBmLQSwW58baJNghD9xdmctiRXjrW3dE8xpUcoXimY4J5UMrnUBrUDmfQrbxRYRX9s5tQe7pWYNF2QiAdH1Fh``` 189 | * LOKI - ```L6XqN6JDedz5Ub8KxpMYRCUoQCuyEA8EegEmeQsdP5FCNuXJavcrxPvLhpqY6emphGTYVrmAUVECsE9drafvY2hXUTJz6rW``` 190 | * TRTL - ```TRTLv2x2bac17cngo1r2wt3CaxN8ckoWHe2TX7dc8zW8Fc9dpmxAvhVX4u4zPjpv9WeALm2koBLF36REVvsLmeufZZ1Yx6uWkYG``` 191 | * XTNC - ```XtazhSxz1bbJLpT2JuiD2UWFUJYSFty5SVWuF6sy2w9v8pn69smkUxkTVCQc8NKCd6CBMNDGzgdPRYBKaHdbgZ5SNptVH1yPCTQ``` 192 | * IRD - ```ir3DHyB8Ub1aAHEewMeUxQ7b7tQdWa7VL8M5oXDPohS3Me4nhwvALXM4mym2kWg9VsceT75dm6XWiWF1K4zu8RVQ1HJD8Z3R9``` 193 | * ARQ - ```ar4Ha6ZQCkKRhkKQLfexv7VZQM2MhUmMmU9hmzswCPK4T3o2rbPKZM1GxEoYg4AFQsh57PsEets7sbpU958FAvxo2RkkTQ1gE``` 194 | * XWP - ```fh4MCJrakhWGoS6Meqp6UxGE1GNfAjKaRdPjW36rTffDiqvEq2HWEKZhrbYRw7XJb3CXxkjL3tcYGTT39m5qgjvk1ap4bVu1R``` 195 | * XEQ - ```Tvzp9tTmdGP9X8hCEw1Qzn18divQajJYTjR5HuUzHPKyLK5fzRt2X73FKBDzcnHMDJKdgsPhUDVrKHVcDJQVmLBg33NbkdjQb``` 196 | * XTA - ```ipN5cNhm7RXAGACP4ZXki4afT3iJ1A6Ka5U4cswE6fBPDcv8JpivurBj3vu1bXwPyb8KZEGsFUYMmToFG4N9V9G72X4WpAQ8L``` 197 | * DERO - ```dero1qygrgnz9gea2rqgwhdtpfpa3mvagt5uyq0g92nurwrpk6wnn7hdnzqgudsv6t``` 198 | * CCX - ```ccx7dmnBBoRPuVcpKJSAVZKdSDo9rc7HVijFbhG34jsXL3qiqfRwu7A5ecem44s2rngDd8y8N4QnYK6WR3mXAcAZ5iXun9BQBx``` 199 | * BLOC - ```abLoc5iUG4a6oAb2dqygxkS5M2uHWx16zHb9fUWMzpSEDwm6T7PSq2MLdHonWZ16CGfnJKRomq75aZyviTo6ZjHeYQMzNAEkjMg``` 200 | * ZEPH - ```ZEPHYR2nic7ULkkmgZNX8a9i2tMbkxuCqjgWZYuee3awX7RhtmhoT98CwGEGrruWZVSKtA7Z7JC8m7oeYHtBD9cBEZzdEh9BSdq4q``` 201 | * SAL - ```SaLvdWKnkz6MvVgxXr2TWSDSvESz6EBcz3wmMFch2sQuMYz2sUQGVNDYhkYaSuvkDr9GSYp5h6BeQHnGK8HzKhqGeZCZzG3AHS3``` 202 | * XTM - ```12FrDe5cUauXdMeCiG1DU3XQZdShjFd9A4p9agxsddVyAwpmz73x4b2Qdy5cPYaGmKNZ6g1fbCASJpPxnjubqjvHDa5``` 203 | * RVN - ```RLVJv9rQNHzXS3Zn4JH8hfAHmm1LfECMxy``` 204 | * XNA - ```Nb931jkFtFN7QWpu4FqSThaoKajYjS5iFZ``` 205 | * CLORE - ```AdXPHtV8yb86a8QKsbs8gmUpRpcxufRn8n``` 206 | * RTM - ```RUCyaEZxQu3Eure73XPQ57si813RYAMQKC``` 207 | * KCN - ```kc1qchtxq2gw9dc4r58hcegd6n4jspew6j9mu3yz8q``` 208 | * BTRM - ```Bfhtr2g56tg73TNZBRCu6fJUD39Kur6SGG``` 209 | * ERG - ```9fe533kUzAE57YfPP6o3nzsYMKN2W2uCxvg8KG8Vn5DDeJGetRw``` 210 | * BTC - ```3HRbMgcvbqHVW7P34MNGvF2Gh3DE26iHdw``` 211 | * BCH - ```18sKoDSjLCFW9kZrXuza1qzEERnKi7bx8S``` 212 | * ETH - ```0xfE23a61548FCCE159a541FAe9e16cEB92Da650ed``` 213 | * ETC - ```0x4480Ad73a113BEFf05B2079E38D90c9757Ecb063``` 214 | * LTC - ```MGj8PU1PpTNDDqRHmuEqfDpH3gxp6cJrUU``` 215 | 216 | ## Known Working Pools 217 | 218 | * [XMRPool.net](https://xmrpool.net) 219 | * [supportXMR.com](https://supportxmr.com) 220 | * [pool.xmr.pt](https://pool.xmr.pt) 221 | * [minemonero.pro](https://minemonero.pro) 222 | * [XMRPool.xyz](https://xmrpool.xyz) 223 | * [ViaXMR.com](https://viaxmr.com) 224 | * [mine.MoneroPRO.com](https://mine.moneropro.com) 225 | * [MinerCircle.com](https://www.minercircle.com) 226 | * [xmr.p00ls.net](https://www.p00ls.net) 227 | * [MoriaXMR.com](https://moriaxmr.com) 228 | * [MoneroOcean.stream](https://moneroocean.stream) 229 | * [SECUmine.net](https://secumine.net) 230 | * [Chinaenter.cn](http://xmr.chinaenter.cn) 231 | * [XMRPool.eu](https://xmrpool.eu) 232 | 233 | If you'd like to have your pool added, please make a pull request here, or contact MoneroOcean at support@moneroocean.stream! 234 | -------------------------------------------------------------------------------- /accessControl_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "userOne_withoutFixedDiff": "passwordForUser1", 3 | "userTwo_withoutFixedDiff": "passwordForUser2", 4 | "etc": "etc" 5 | } 6 | -------------------------------------------------------------------------------- /config_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "pools": [ 3 | { 4 | "hostname": "gulf.moneroocean.stream", 5 | "port": 10032, 6 | "ssl": false, 7 | "allowSelfSignedSSL": false, 8 | "share": 100, 9 | "username": "89TxfrUmqJJcb1V124WsUzA78Xa3UYHt7Bg8RGMhXVeZYPN8cE5CZEk58Y1m23ZMLHN7wYeJ9da5n5MXharEjrm41hSnWHL", 10 | "password": "proxy", 11 | "keepAlive": true, 12 | "algo": "rx/0", 13 | "algo_perf": { "rx/0": 1, "rx/loki": 1 }, 14 | "blob_type": "cryptonote", 15 | "default": true 16 | }, 17 | { 18 | "hostname": "pool.supportxmr.com", 19 | "port": 7777, 20 | "ssl": false, 21 | "allowSelfSignedSSL": false, 22 | "share": 0, 23 | "username": "89TxfrUmqJJcb1V124WsUzA78Xa3UYHt7Bg8RGMhXVeZYPN8cE5CZEk58Y1m23ZMLHN7wYeJ9da5n5MXharEjrm41hSnWHL", 24 | "password": "proxy", 25 | "keepAlive": true, 26 | "algo": "rx/0", 27 | "algo_perf": { "rx/0": 1 }, 28 | "blob_type": "cryptonote", 29 | "default": false 30 | }, 31 | { 32 | "hostname": "mine.c3pool.com", 33 | "port": 23333, 34 | "ssl": false, 35 | "allowSelfSignedSSL": false, 36 | "share": 0, 37 | "username": "89TxfrUmqJJcb1V124WsUzA78Xa3UYHt7Bg8RGMhXVeZYPN8cE5CZEk58Y1m23ZMLHN7wYeJ9da5n5MXharEjrm41hSnWHL", 38 | "password": "proxy", 39 | "keepAlive": true, 40 | "algo": "rx/0", 41 | "algo_perf": { "rx/0": 1, "rx/loki": 1 }, 42 | "blob_type": "cryptonote", 43 | "default": false 44 | } 45 | ], 46 | "listeningPorts": [ 47 | { 48 | "port": 8080, 49 | "ssl": false, 50 | "diff": 10000 51 | }, 52 | { 53 | "port": 8443, 54 | "ssl": true, 55 | "diff": 10000 56 | }, 57 | { 58 | "port": 3333, 59 | "ssl": false, 60 | "diff": 1000 61 | }, 62 | { 63 | "port": 1111, 64 | "ssl": false, 65 | "diff": 1 66 | } 67 | ], 68 | "bindAddress": "0.0.0.0", 69 | "developerShare": 1, 70 | "daemonAddress": "127.0.0.1:18081", 71 | "accessControl": { 72 | "enabled": false, 73 | "controlFile": "accessControl.json" 74 | }, 75 | "httpEnable": false, 76 | "httpAddress": "127.0.0.1", 77 | "httpPort": "8081", 78 | "httpUser": "", 79 | "httpPass": "", 80 | "addressWorkerID": false, 81 | "minerInactivityTime": 120, 82 | "keepOfflineMiners": 0, 83 | "refreshTime": 30, 84 | "theme": "light", 85 | "coinSettings": { 86 | "xmr":{ 87 | "minDiff": 1, 88 | "maxDiff": 10000000, 89 | "shareTargetTime": 30 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /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 | 11 | if which yum >/dev/null; then 12 | sudo yum -y update 13 | sudo yum -y upgrade 14 | sudo yum -y install git curl make gcc-c++ python boost-devel boost-system-devel boost-date-time-devel libsodium-devel 15 | else 16 | sudo apt-get update 17 | sudo DEBIAN_FRONTEND=noninteractive apt-get -y upgrade 18 | sudo DEBIAN_FRONTEND=noninteractive apt-get -y install git curl make g++ python2 libboost-dev libboost-system-dev libboost-date-time-dev libsodium-dev 19 | fi 20 | cd ~ 21 | git clone https://github.com/MoneroOcean/xmr-node-proxy 22 | curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash 23 | source ~/.nvm/nvm.sh 24 | nvm install v14.17.3 25 | nvm alias default v14.17.3 26 | cd ~/xmr-node-proxy 27 | npm install || exit 1 28 | npm install -g pm2 29 | cp config_example.json config.json 30 | 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 31 | cd ~ 32 | pm2 status 33 | sudo env PATH=$PATH:`pwd`/.nvm/versions/node/v8.11.3/bin `pwd`/.nvm/versions/node/v8.11.3/lib/node_modules/pm2/bin/pm2 startup systemd -u $CURUSER --hp `pwd` 34 | sudo chown -R $CURUSER. ~/.pm2 35 | echo "Installing pm2-logrotate in the background!" 36 | pm2 install pm2-logrotate 37 | echo "You're setup with a shiny new proxy! Now, do 'source ~/.bashrc' command, go configure it and have fun." 38 | -------------------------------------------------------------------------------- /lib/support.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const CircularBuffer = require('circular-buffer'); 3 | const request = require('request'); 4 | const debug = require('debug')('support'); 5 | const fs = require('fs'); 6 | 7 | function get_new_id() { 8 | const min = 100000000000000; 9 | const max = 999999999999999; 10 | const id = Math.floor(Math.random() * (max - min + 1)) + min; 11 | return id.toString(); 12 | }; 13 | 14 | function human_hashrate(hashes, algo) { 15 | const power = Math.pow(10, 2 || 0); 16 | const unit = algo === "c29s" || algo === "c29v" ? "G" : "H"; 17 | if (algo === "c29s") hashes *= 32; 18 | if (algo === "c29v") hashes *= 16; 19 | if (hashes > 1000000000000) return String(Math.round((hashes / 1000000000000) * power) / power) + " T" + unit + "/s"; 20 | if (hashes > 1000000000) return String(Math.round((hashes / 1000000000) * power) / power) + " G" + unit + "/s"; 21 | if (hashes > 1000000) return String(Math.round((hashes / 1000000) * power) / power) + " M" + unit + "/s"; 22 | if (hashes > 1000) return String(Math.round((hashes / 1000) * power) / power) + " K" + unit + "/s"; 23 | return ( hashes || 0.0 ).toFixed(2) + " " + unit + "/s" 24 | }; 25 | 26 | function circularBuffer(size) { 27 | let buffer = CircularBuffer(size); 28 | 29 | buffer.sum = function () { 30 | if (this.size() === 0) { 31 | return 1; 32 | } 33 | return this.toarray().reduce(function (a, b) { 34 | return a + b; 35 | }); 36 | }; 37 | 38 | buffer.average = function (lastShareTime) { 39 | if (this.size() === 0) { 40 | return global.config.pool.targetTime * 1.5; 41 | } 42 | let extra_entry = (Date.now() / 1000) - lastShareTime; 43 | return (this.sum() + Math.round(extra_entry)) / (this.size() + 1); 44 | }; 45 | 46 | buffer.clear = function () { 47 | let i = this.size(); 48 | while (i > 0) { 49 | this.deq(); 50 | i = this.size(); 51 | } 52 | }; 53 | 54 | return buffer; 55 | } 56 | 57 | function sendEmail(toAddress, subject, body){ 58 | request.post(global.config.general.mailgunURL + "/messages", { 59 | auth: { 60 | user: 'api', 61 | pass: global.config.general.mailgunKey 62 | }, 63 | form: { 64 | from: global.config.general.emailFrom, 65 | to: toAddress, 66 | subject: subject, 67 | text: body 68 | } 69 | }, function(err, response, body){ 70 | if (!err && response.statusCode === 200) { 71 | console.log("Email sent successfully! Response: " + body); 72 | } else { 73 | console.error("Did not send e-mail successfully! Response: " + body + " Response: "+JSON.stringify(response)); 74 | } 75 | }); 76 | } 77 | 78 | function coinToDecimal(amount) { 79 | return amount / global.config.coin.sigDigits; 80 | } 81 | 82 | function decimalToCoin(amount) { 83 | return Math.round(amount * global.config.coin.sigDigits); 84 | } 85 | 86 | function blockCompare(a, b) { 87 | if (a.height < b.height) { 88 | return 1; 89 | } 90 | 91 | if (a.height > b.height) { 92 | return -1; 93 | } 94 | return 0; 95 | } 96 | 97 | function tsCompare(a, b) { 98 | if (a.ts < b.ts) { 99 | return 1; 100 | } 101 | 102 | if (a.ts > b.ts) { 103 | return -1; 104 | } 105 | return 0; 106 | } 107 | 108 | function currentUnixTimestamp(){ 109 | return + new Date(); 110 | } 111 | 112 | module.exports = function () { 113 | return { 114 | get_new_id: get_new_id, 115 | human_hashrate: human_hashrate, 116 | circularBuffer: circularBuffer, 117 | coinToDecimal: coinToDecimal, 118 | decimalToCoin: decimalToCoin, 119 | blockCompare: blockCompare, 120 | sendEmail: sendEmail, 121 | tsCompare: tsCompare, 122 | developerAddy: '44Ldv5GQQhP7K7t3ZBdZjkPA7Kg7dhHwk3ZM3RJqxxrecENSFx27Vq14NAMAd2HBvwEPUVVvydPRLcC69JCZDHLT2X5a4gr', 123 | currentUnixTimestamp: currentUnixTimestamp 124 | }; 125 | }; 126 | -------------------------------------------------------------------------------- /lib/xmr.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const multiHashing = require('cryptonight-hashing'); 3 | const cnUtil = require('cryptoforknote-util'); 4 | const bignum = require('bignum'); 5 | const support = require('./support.js')(); 6 | 7 | let debug = { 8 | pool: require('debug')('pool'), 9 | diff: require('debug')('diff'), 10 | blocks: require('debug')('blocks'), 11 | shares: require('debug')('shares'), 12 | miners: require('debug')('miners'), 13 | workers: require('debug')('workers') 14 | }; 15 | 16 | let baseDiff = bignum('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 16); 17 | 18 | Buffer.prototype.toByteArray = function () { 19 | return Array.prototype.slice.call(this, 0); 20 | }; 21 | 22 | function blockHeightCheck(nodeList, callback) { 23 | let randomNode = nodeList[Math.floor(Math.random() * nodeList.length)].split(':'); 24 | } 25 | 26 | function blobTypeGrin(blob_type_num) { 27 | switch (blob_type_num) { 28 | case 8: 29 | case 9: 30 | case 10: 31 | case 12: return true; 32 | default: return false; 33 | } 34 | } 35 | 36 | function nonceSize(blob_type_num) { 37 | return blob_type_num == 7 ? 8 : 4; 38 | } 39 | 40 | function blobTypeDero(blob_type_num) { 41 | return blob_type_num == 100; 42 | } 43 | 44 | function blobTypeRTM(blob_type_num) { 45 | return blob_type_num == 104; 46 | } 47 | 48 | function blobTypeKCN(blob_type_num) { 49 | return blob_type_num == 105; 50 | } 51 | 52 | function blobTypeXTM_T(blob_type_num) { 53 | return blob_type_num == 106; 54 | } 55 | 56 | function c29ProofSize(blob_type_num) { 57 | switch (blob_type_num) { 58 | case 10: return 40; 59 | case 12: return 48; 60 | default: return 32; 61 | } 62 | } 63 | 64 | function convertBlob(blobBuffer, blob_type_num){ 65 | if (blobTypeDero(blob_type_num)) return Buffer.from(blobBuffer); 66 | else if (blobTypeXTM_T(blob_type_num)) return Buffer.from(blobBuffer) 67 | else if (blobTypeRTM(blob_type_num)) return cnUtil.convertRtmBlob(blobBuffer); 68 | else if (blobTypeKCN(blob_type_num)) return cnUtil.convertKcnBlob(blobBuffer); 69 | else return cnUtil.convert_blob(blobBuffer, blob_type_num); 70 | } 71 | 72 | function constructNewBlob(blockTemplateBuffer, NonceBuffer, blob_type_num, ring){ 73 | if (blobTypeDero(blob_type_num) || blobTypeXTM_T(blob_type_num)) { 74 | let newBlob = Buffer.alloc(blockTemplateBuffer.length); 75 | blockTemplateBuffer.copy(newBlob); 76 | NonceBuffer.copy(newBlob, 39, 0, 4); 77 | return newBlob; 78 | } else if (blobTypeRTM(blob_type_num)) { 79 | return cnUtil.constructNewRtmBlob(blockTemplateBuffer, NonceBuffer); 80 | } else if (blobTypeKCN(blob_type_num)) { 81 | return cnUtil.constructNewKcnBlob(blockTemplateBuffer, NonceBuffer); 82 | } else { 83 | return cnUtil.construct_block_blob(blockTemplateBuffer, NonceBuffer, blob_type_num, ring); 84 | } 85 | } 86 | 87 | function getRemoteNodes() { 88 | let knownNodes = [ 89 | '162.213.38.245:18081', 90 | '116.93.119.79:18081', 91 | '85.204.96.231:18081', 92 | '107.167.87.242:18081', 93 | '107.167.93.58:18081', 94 | '199.231.85.122:18081', 95 | '192.110.160.146:18081' 96 | ]; // Prefill the array with known good nodes for now. Eventually will try to download them via DNS or http. 97 | } 98 | 99 | function parse_blob_type(blob_type_str) { 100 | if (typeof(blob_type_str) === 'undefined') return 0; 101 | switch (blob_type_str) { 102 | case 'cryptonote': return 0; // Monero 103 | case 'forknote1': return 1; 104 | case 'forknote2': return 2; // Almost all Forknote coins 105 | case 'cryptonote2': return 3; // Masari 106 | case 'cryptonote_ryo': return 4; // Ryo 107 | case 'cryptonote_loki': return 5; // Loki 108 | case 'cryptonote3': return 6; // Masari 109 | case 'aeon': return 7; // Aeon 110 | case 'cuckaroo': return 8; // Swap/MoneroV 111 | case 'cryptonote_xtnc': return 9; // XTNC 112 | case 'cryptonote_tube': return 10; // Tube 113 | case 'cryptonote_xhv': return 11; // Haven 114 | case 'cryptonote_xta': return 12; // Italo 115 | case 'cryptonote_zeph': return 13; // Zephyr 116 | case 'cryptonote_xla': return 14; // XLA 117 | case 'cryptonote_sal': return 15; // SAL 118 | case 'cryptonote_xeq': return 22; // XEQ 119 | case 'cryptonote_dero': return 100; // Dero 120 | case 'raptoreum': return 104; // RTM 121 | case 'raptoreum_kcn': return 105; // KCN 122 | case 'xtm-t': return 106; // XTM_T 123 | } 124 | return 0; 125 | } 126 | 127 | // Names are taken from https://github.com/xmrig/xmrig-proxy/blob/master/doc/STRATUM_EXT.md 128 | 129 | function hash_func(convertedBlob, blockTemplate) { 130 | const block_version = typeof(blockTemplate.blocktemplate_blob) !== 'undefined' ? 16 * parseInt(blockTemplate.blocktemplate_blob[0]) + parseInt(blockTemplate.blocktemplate_blob[1]) : 0; 131 | const algo2 = typeof(blockTemplate.algo) === 'undefined' ? "rx/0" : blockTemplate.algo; 132 | switch (algo2) { 133 | case 'rx/0': return multiHashing.randomx(convertedBlob, Buffer.from(blockTemplate.seed_hash, 'hex'), 0); 134 | 135 | case 'cn': 136 | case 'cryptonight': 137 | case 'cn/0': 138 | case 'cryptonight/0': return multiHashing.cryptonight(convertedBlob, 0); 139 | 140 | case 'cn/1': 141 | case 'cryptonight/1': return multiHashing.cryptonight(convertedBlob, 1); 142 | 143 | case 'cn/xtl': 144 | case 'cryptonight/xtl': return multiHashing.cryptonight(convertedBlob, 3); 145 | 146 | case 'cn/msr': 147 | case 'cryptonight/msr': return multiHashing.cryptonight(convertedBlob, 4); 148 | 149 | case 'cn/xao': 150 | case 'cryptonight/xao': return multiHashing.cryptonight(convertedBlob, 6); 151 | 152 | case 'cn/rto': 153 | case 'cryptonight/rto': return multiHashing.cryptonight(convertedBlob, 7); 154 | 155 | case 'cn/2': 156 | case 'cryptonight/2': return multiHashing.cryptonight(convertedBlob, 8); 157 | 158 | case 'cn/half': 159 | case 'cryptonight/half': return multiHashing.cryptonight(convertedBlob, 9); 160 | 161 | case 'cn/gpu': 162 | case 'cryptonight/gpu': return multiHashing.cryptonight(convertedBlob, 11); 163 | 164 | case 'cn/wow': 165 | case 'cryptonight/wow': return multiHashing.cryptonight(convertedBlob, 12, blockTemplate.height); 166 | 167 | case 'cn/r': 168 | case 'cryptonight/r': return multiHashing.cryptonight(convertedBlob, 13, blockTemplate.height); 169 | 170 | case 'cn/rwz': 171 | case 'cryptonight/rwz': return multiHashing.cryptonight(convertedBlob, 14); 172 | 173 | case 'cn/zls': 174 | case 'cryptonight/zls': return multiHashing.cryptonight(convertedBlob, 15); 175 | 176 | case 'cn/ccx': 177 | case 'cryptonight/ccx': return multiHashing.cryptonight(convertedBlob, 17); 178 | 179 | case 'cn/double': 180 | case 'cryptonight/double': return multiHashing.cryptonight(convertedBlob, 16); 181 | 182 | case 'ghostrider': return multiHashing.cryptonight(convertedBlob, 18); 183 | 184 | case 'flex': return multiHashing.cryptonight(convertedBlob, 19); 185 | 186 | case 'cn-lite': 187 | case 'cryptonight-lite': 188 | case 'cn-lite/0': 189 | case 'cryptonight-lite/0': return multiHashing.cryptonight_light(convertedBlob, 0); 190 | 191 | case 'cn-lite/1': 192 | case 'cryptonight-lite/1': return multiHashing.cryptonight_light(convertedBlob, 1); 193 | 194 | case 'cn-heavy': 195 | case 'cryptonight-heavy': 196 | case 'cn-heavy/0': 197 | case 'cryptonight-heavy/0': return multiHashing.cryptonight_heavy(convertedBlob, 0); 198 | 199 | case 'cn-heavy/xhv': 200 | case 'cryptonight-heavy/xhv': return multiHashing.cryptonight_heavy(convertedBlob, 1); 201 | 202 | case 'cn-heavy/tube': 203 | case 'cryptonight-heavy/tube': return multiHashing.cryptonight_heavy(convertedBlob, 2); 204 | 205 | case 'cn-pico/trtl': 206 | case 'cryptonight-pico/trtl': return multiHashing.cryptonight_pico(convertedBlob, 0); 207 | 208 | case 'rx/wow': 209 | case 'randomx/wow': return multiHashing.randomx(convertedBlob, Buffer.from(blockTemplate.seed_hash, 'hex'), 17); 210 | 211 | case 'rx/loki': 212 | case 'randomx/loki': return multiHashing.randomx(convertedBlob, Buffer.from(blockTemplate.seed_hash, 'hex'), 18); 213 | 214 | case 'rx/v': return multiHashing.randomx(convertedBlob, Buffer.from(blockTemplate.seed_hash, 'hex'), 19); 215 | 216 | case 'rx/graft': return multiHashing.randomx(convertedBlob, Buffer.from(blockTemplate.seed_hash, 'hex'), 20); 217 | 218 | case 'rx/xeq': return multiHashing.randomx(convertedBlob, Buffer.from(blockTemplate.seed_hash, 'hex'), 22); 219 | 220 | case 'defyx': return multiHashing.randomx(convertedBlob, Buffer.from(blockTemplate.seed_hash, 'hex'), 1); 221 | 222 | case 'panthera': return multiHashing.randomx(convertedBlob, Buffer.from(blockTemplate.seed_hash, 'hex'), 3); 223 | 224 | case 'rx/arq': return multiHashing.randomx(convertedBlob, Buffer.from(blockTemplate.seed_hash, 'hex'), 2); 225 | 226 | case 'argon2/chukwav2': 227 | case 'chukwav2': return multiHashing.argon2(convertedBlob, 2); 228 | 229 | case 'argon2/wrkz': return multiHashing.argon2(convertedBlob, 1); 230 | 231 | case 'k12': return multiHashing.k12(convertedBlob); 232 | 233 | case 'astrobwt': return multiHashing.astrobwt(convertedBlob, 0); 234 | } 235 | return ""; 236 | } 237 | 238 | function hash_func_c29(algo, header, ring) { 239 | switch (algo) { 240 | case 'c29s': return multiHashing.c29s(header, ring); 241 | case 'c29v': return multiHashing.c29v(header, ring); 242 | case 'c29b': return multiHashing.c29b(header, ring); 243 | case 'c29i': return multiHashing.c29i(header, ring); 244 | default: return 1; 245 | } 246 | } 247 | 248 | function detectAlgo(default_pool_algo_set, block_version) { 249 | if ("cn/r" in default_pool_algo_set && "rx/0" in default_pool_algo_set) return block_version >= 12 ? "rx/0" : "cn/r"; // Monero fork 250 | const default_pool_algo_arr = Object.keys(default_pool_algo_set); 251 | if (default_pool_algo_arr.length == 1) return default_pool_algo_arr[0]; 252 | console.error("Can't not correctly detect block template algorithm from the list of provided default algorithms (please reduce it to single item): " + default_pool_algo_arr.join(", ")); 253 | return default_pool_algo_arr[0]; 254 | } 255 | 256 | function BlockTemplate(template) { 257 | /* 258 | We receive something identical to the result portions of the monero GBT call. 259 | Functionally, this could act as a very light-weight solo pool, so we'll prep it as one. 260 | You know. Just in case amirite? 261 | */ 262 | this.id = template.id; 263 | this.blob = template.blocktemplate_blob; 264 | this.blob_type = template.blob_type; 265 | this.variant = template.variant; 266 | this.algo = template.algo; 267 | this.difficulty = template.difficulty; 268 | this.height = template.height; 269 | this.seed_hash = template.seed_hash; 270 | this.reservedOffset = template.reserved_offset; 271 | this.workerOffset = template.worker_offset; // clientNonceLocation 272 | this.targetDiff = template.target_diff; 273 | this.targetHex = template.target_diff_hex; 274 | this.buffer = Buffer.from(this.blob, 'hex'); 275 | this.previousHash = Buffer.alloc(32); 276 | this.workerNonce = 0; 277 | this.solo = false; 278 | if (typeof(this.workerOffset) === 'undefined') { 279 | this.solo = true; 280 | global.instanceId.copy(this.buffer, this.reservedOffset + 4, 0, 3); 281 | this.buffer.copy(this.previousHash, 0, 7, 39); 282 | } 283 | this.nextBlob = function () { 284 | if (this.solo) { 285 | // This is running in solo mode. 286 | this.buffer.writeUInt32BE(++this.workerNonce, this.reservedOffset); 287 | } else { 288 | this.buffer.writeUInt32BE(++this.workerNonce, this.workerOffset); 289 | } 290 | return convertBlob(this.buffer, this.blob_type).toString('hex'); 291 | }; 292 | } 293 | 294 | function MasterBlockTemplate(template) { 295 | /* 296 | We receive something identical to the result portions of the monero GBT call. 297 | Functionally, this could act as a very light-weight solo pool, so we'll prep it as one. 298 | You know. Just in case amirite? 299 | */ 300 | this.blob = template.blocktemplate_blob; 301 | this.blob_type = parse_blob_type(template.blob_type); 302 | this.variant = template.variant; 303 | this.algo = template.algo; 304 | this.difficulty = template.difficulty; 305 | this.height = template.height; 306 | this.seed_hash = template.seed_hash; 307 | this.reservedOffset = template.reserved_offset; // reserveOffset 308 | this.workerOffset = template.client_nonce_offset; // clientNonceLocation 309 | this.poolOffset = template.client_pool_offset; // clientPoolLocation 310 | if (!("client_pool_offset" in template)) console.error("Your pool is not compatible with xmr-node-proxy!"); 311 | this.targetDiff = template.target_diff; 312 | this.targetHex = template.target_diff_hex; 313 | this.buffer = Buffer.from(this.blob, 'hex'); 314 | this.previousHash = Buffer.alloc(32); 315 | this.job_id = template.job_id; 316 | this.workerNonce = 0; 317 | this.poolNonce = 0; 318 | this.solo = false; 319 | if (typeof(this.workerOffset) === 'undefined') { 320 | this.solo = true; 321 | global.instanceId.copy(this.buffer, this.reservedOffset + 4, 0, 3); 322 | this.buffer.copy(this.previousHash, 0, 7, 39); 323 | } 324 | this.blobForWorker = function () { 325 | this.buffer.writeUInt32BE(++this.poolNonce, this.poolOffset); 326 | return this.buffer.toString('hex'); 327 | }; 328 | } 329 | 330 | function getJob(miner, activeBlockTemplate, bashCache) { 331 | if (miner.validJobs.size() >0 && miner.validJobs.get(0).templateID === activeBlockTemplate.id && !miner.newDiff && miner.cachedJob !== null && typeof bashCache === 'undefined') { 332 | return miner.cachedJob; 333 | } 334 | 335 | const blob = activeBlockTemplate.nextBlob(); 336 | adjustMinerDiff(miner, activeBlockTemplate.targetDiff); 337 | miner.lastBlockHeight = activeBlockTemplate.height; 338 | 339 | let newJob = { 340 | id: support.get_new_id(), 341 | blob_type: activeBlockTemplate.blob_type, 342 | extraNonce: activeBlockTemplate.workerNonce, 343 | height: activeBlockTemplate.height, 344 | seed_hash: activeBlockTemplate.seed_hash, 345 | difficulty: miner.difficulty, 346 | diffHex: miner.diffHex, 347 | submissions: [], 348 | templateID: activeBlockTemplate.id 349 | }; 350 | 351 | miner.validJobs.enq(newJob); 352 | 353 | if (blobTypeGrin(activeBlockTemplate.blob_type)) miner.cachedJob = { 354 | pre_pow: blob, 355 | algo: "cuckaroo", 356 | edgebits: 29, 357 | proofsize: c29ProofSize(activeBlockTemplate.blob_type), 358 | noncebytes: 4, 359 | height: activeBlockTemplate.height, 360 | job_id: newJob.id, 361 | difficulty: miner.difficulty, 362 | id: miner.id 363 | }; else miner.cachedJob = { 364 | blob: blob, 365 | job_id: newJob.id, 366 | height: activeBlockTemplate.height, 367 | seed_hash: activeBlockTemplate.seed_hash, 368 | target: getTargetHex(miner.difficulty, nonceSize(activeBlockTemplate.blob_type)), 369 | id: miner.id 370 | }; 371 | if (typeof (activeBlockTemplate.variant) !== 'undefined') { 372 | miner.cachedJob.variant = activeBlockTemplate.variant; 373 | } 374 | if (typeof (activeBlockTemplate.algo) !== 'undefined' && miner.protocol !== "grin") { 375 | miner.cachedJob.algo = activeBlockTemplate.algo; 376 | } 377 | return miner.cachedJob; 378 | } 379 | 380 | function getMasterJob(pool, workerID) { 381 | let activeBlockTemplate = pool.activeBlocktemplate; 382 | let btBlob = activeBlockTemplate.blobForWorker(); 383 | let workerData = { 384 | id: support.get_new_id(), 385 | blocktemplate_blob: btBlob, 386 | blob_type: activeBlockTemplate.blob_type, 387 | variant: activeBlockTemplate.variant, 388 | algo: activeBlockTemplate.algo, 389 | difficulty: activeBlockTemplate.difficulty, 390 | height: activeBlockTemplate.height, 391 | seed_hash: activeBlockTemplate.seed_hash, 392 | reserved_offset: activeBlockTemplate.reservedOffset, 393 | worker_offset: activeBlockTemplate.workerOffset, 394 | target_diff: activeBlockTemplate.targetDiff, 395 | target_diff_hex: activeBlockTemplate.targetHex 396 | }; 397 | let localData = { 398 | id: workerData.id, 399 | masterJobID: activeBlockTemplate.job_id, 400 | poolNonce: activeBlockTemplate.poolNonce 401 | }; 402 | if (!(workerID in pool.poolJobs)) { 403 | pool.poolJobs[workerID] = support.circularBuffer(4); 404 | } 405 | pool.poolJobs[workerID].enq(localData); 406 | return workerData; 407 | } 408 | 409 | function adjustMinerDiff(miner, max_diff) { 410 | if (miner.newDiff) { 411 | miner.difficulty = miner.newDiff; 412 | miner.newDiff = null; 413 | } 414 | if (miner.difficulty > max_diff) { 415 | miner.difficulty = max_diff; 416 | } 417 | } 418 | 419 | function getTargetHex(difficulty, size) { 420 | let padded = Buffer.alloc(32); 421 | padded.fill(0); 422 | const diffBuff = baseDiff.div(difficulty).toBuffer(); 423 | diffBuff.copy(padded, 32 - diffBuff.length); 424 | const buff = padded.slice(0, size); 425 | const buffArray = buff.toByteArray().reverse(); 426 | const buffReversed = Buffer.from(buffArray); 427 | return buffReversed.toString('hex'); 428 | }; 429 | 430 | // MAX_VER_SHARES_PER_SEC is maximum amount of verified shares for VER_SHARES_PERIOD second period 431 | // other shares are just dumped to the pool to avoid proxy CPU overload during low difficulty adjustement period 432 | const MAX_VER_SHARES_PER_SEC = 10; // per thread 433 | const VER_SHARES_PERIOD = 5; 434 | let verified_share_start_period; 435 | let verified_share_num; 436 | 437 | // for more intellegent reporting 438 | let poolShareSize = {}; 439 | let poolShareCount = {}; 440 | let poolShareTime = {}; 441 | 442 | function hash_buff_diff(hash) { 443 | return baseDiff.div(bignum.fromBuffer(Buffer.from(hash.toByteArray().reverse()))); 444 | } 445 | 446 | function processShare(miner, job, blockTemplate, params) { 447 | const blob_type = job.blob_type; 448 | const nonce = params.nonce; 449 | 450 | let template = Buffer.alloc(blockTemplate.buffer.length); 451 | blockTemplate.buffer.copy(template); 452 | if (blockTemplate.solo) { 453 | template.writeUInt32BE(job.extraNonce, blockTemplate.reservedOffset); 454 | } else { 455 | template.writeUInt32BE(job.extraNonce, blockTemplate.workerOffset); 456 | } 457 | 458 | const hashDiff = hash_buff_diff(blobTypeGrin(blob_type) ? multiHashing.c29_cycle_hash(params.pow) : Buffer.from(params.result, 'hex')); 459 | 460 | if (hashDiff.ge(blockTemplate.targetDiff)) { 461 | let time_now = Date.now(); 462 | if (!verified_share_start_period || time_now - verified_share_start_period > VER_SHARES_PERIOD*1000) { 463 | verified_share_num = 0; 464 | verified_share_start_period = time_now; 465 | } 466 | let isVerifyFailed = false; 467 | 468 | if (blobTypeGrin(blob_type)) { 469 | const shareBuffer = constructNewBlob(template, bignum(nonce, 10).toBuffer({endian: 'little', size: 4}), blob_type, params.pow) 470 | const header = Buffer.concat([convertBlob(shareBuffer, blob_type), bignum(nonce, 10).toBuffer({endian: 'big', size: 4})]); 471 | if (hash_func_c29(blockTemplate.algo, header, params.pow)) isVerifyFailed = true; 472 | } else { 473 | if (++ verified_share_num <= MAX_VER_SHARES_PER_SEC*VER_SHARES_PERIOD) { 474 | // Validate share with CN hash, then if valid, blast it up to the master. 475 | const shareBuffer = constructNewBlob(template, Buffer.from(nonce, 'hex'), blob_type); 476 | const convertedBlob = convertBlob(shareBuffer, blob_type); 477 | const hash = hash_func(convertedBlob, blockTemplate); 478 | if (hash.toString('hex') !== params.result) isVerifyFailed = true; 479 | } else { 480 | console.error(global.threadName + "Throttling down miner share verification to avoid CPU overload: " + miner.logString); 481 | } 482 | } 483 | if (isVerifyFailed) { 484 | console.error(global.threadName + "Bad share from miner " + miner.logString); 485 | miner.pushMessage({method: 'job', params: miner.getNewJob(true)}); 486 | return false; 487 | } 488 | miner.blocks += 1; 489 | const poolName = miner.pool; 490 | process.send({ 491 | type: 'shareFind', 492 | host: poolName, 493 | data: { 494 | btID: blockTemplate.id, 495 | nonce: nonce, 496 | pow: params.pow, 497 | resultHash: params.result, 498 | workerNonce: job.extraNonce 499 | } 500 | }); 501 | 502 | if (!(poolName in poolShareTime)) { 503 | console.log(`Submitted share of ${blockTemplate.targetDiff} hashes to ${poolName} pool`); 504 | poolShareTime[poolName] = Date.now(); 505 | poolShareCount[poolName] = 0; 506 | poolShareSize[poolName] = blockTemplate.targetDiff; 507 | } else if (Date.now() - poolShareTime[poolName] > 30*1000 || (poolName in poolShareSize && poolShareSize[poolName] != blockTemplate.targetDiff)) { 508 | if (poolShareCount[poolName]) console.log(`Submitted ${poolShareCount[poolName]} share(s) of ${poolShareSize[poolName]} hashes to ${poolName} pool`); 509 | poolShareTime[poolName] = Date.now(); 510 | poolShareCount[poolName] = 1; 511 | poolShareSize[poolName] = blockTemplate.targetDiff; 512 | } else { 513 | ++ poolShareCount[poolName]; 514 | } 515 | } 516 | else if (hashDiff.lt(job.difficulty)) { 517 | process.send({type: 'invalidShare'}); 518 | console.warn(global.threadName + "Rejected low diff share of " + hashDiff.toString() + " from: " + miner.address + " ID: " + 519 | miner.identifier + " IP: " + miner.ipAddress); 520 | return false; 521 | } 522 | miner.shares += 1; 523 | miner.hashes += job.difficulty; 524 | return true; 525 | } 526 | 527 | let devPool = { 528 | "hostname": "devshare.moneroocean.stream", 529 | "port": 10032, 530 | "ssl": false, 531 | "share": 0, 532 | "username": "89TxfrUmqJJcb1V124WsUzA78Xa3UYHt7Bg8RGMhXVeZYPN8cE5CZEk58Y1m23ZMLHN7wYeJ9da5n5MXharEjrm41hSnWHL", 533 | "password": "proxy_donations", 534 | "keepAlive": true, 535 | "coin": "xmr", 536 | "default": false, 537 | "devPool": true 538 | }; 539 | 540 | module.exports = function () { 541 | return { 542 | devPool: devPool, 543 | blobTypeGrin: blobTypeGrin, 544 | hashSync: multiHashing.cryptonight, 545 | hashAsync: multiHashing.cryptonight_async, 546 | blockHeightCheck: blockHeightCheck, 547 | getRemoteNodes: getRemoteNodes, 548 | BlockTemplate: BlockTemplate, 549 | getJob: getJob, 550 | c29ProofSize: c29ProofSize, 551 | nonceSize: nonceSize, 552 | processShare: processShare, 553 | MasterBlockTemplate: MasterBlockTemplate, 554 | getMasterJob: getMasterJob, 555 | detectAlgo: detectAlgo 556 | }; 557 | }; 558 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xmr-node-proxy", 3 | "version": "0.27.0", 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/MoneroOcean/xmr-node-proxy.git" 12 | }, 13 | "author": "Multiple", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/MoneroOcean/xmr-node-proxy/issues" 17 | }, 18 | "homepage": "https://github.com/MoneroOcean/xmr-node-proxy#readme", 19 | "dependencies": { 20 | "async": "2.1.4", 21 | "bignum": "^0.13.1", 22 | "circular-buffer": "1.0.2", 23 | "debug": "2.6.9", 24 | "express": "4.14.0", 25 | "minimist": "1.2.0", 26 | "moment": "2.21.0", 27 | "request": "^2.79.0", 28 | "cryptoforknote-util": "git+https://github.com/MoneroOcean/node-cryptoforknote-util.git#v15.5.16", 29 | "cryptonight-hashing": "git+https://github.com/MoneroOcean/node-cryptonight-hashing.git#v29.0.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /proxy.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const cluster = require('cluster'); 3 | const net = require('net'); 4 | const tls = require('tls'); 5 | const http = require('http'); 6 | const moment = require('moment'); 7 | const fs = require('fs'); 8 | const async = require('async'); 9 | const support = require('./lib/support.js')(); 10 | global.config = require('./config.json'); 11 | 12 | const PROXY_VERSION = "0.27.0"; 13 | const DEFAULT_ALGO = [ "rx/0" ]; 14 | const DEFAULT_ALGO_PERF = { "rx/0": 1, "rx/loki": 1 }; 15 | 16 | /* 17 | General file design/where to find things. 18 | 19 | Internal Variables 20 | IPC Registry 21 | Combined Functions 22 | Pool Definition 23 | Master Functions 24 | Miner Definition 25 | Slave Functions 26 | API Calls (Master-Only) 27 | System Init 28 | 29 | */ 30 | let debug = { 31 | pool: require('debug')('pool'), 32 | diff: require('debug')('diff'), 33 | blocks: require('debug')('blocks'), 34 | shares: require('debug')('shares'), 35 | miners: require('debug')('miners'), 36 | workers: require('debug')('workers'), 37 | balancer: require('debug')('balancer'), 38 | misc: require('debug')('misc') 39 | }; 40 | global.threadName = ''; 41 | const nonceCheck32 = new RegExp("^[0-9a-f]{8}$"); 42 | const nonceCheck64 = new RegExp("^[0-9a-f]{16}$"); 43 | let activePorts = []; 44 | let httpResponse = ' 200 OK\nContent-Type: text/plain\nContent-Length: 19\n\nMining Proxy Online'; 45 | let activeMiners = {}; 46 | let activeCoins = {}; 47 | let bans = {}; 48 | let activePools = {}; 49 | let activeWorkers = {}; 50 | let defaultPools = {}; 51 | let accessControl = {}; 52 | let lastAccessControlLoadTime = null; 53 | let masterStats = {shares: 0, blocks: 0, hashes: 0}; 54 | 55 | // IPC Registry 56 | function masterMessageHandler(worker, message, handle) { 57 | if (typeof message !== 'undefined' && 'type' in message){ 58 | switch (message.type) { 59 | case 'blockFind': 60 | case 'shareFind': 61 | if (message.host in activePools){ 62 | activePools[message.host].sendShare(worker, message.data); 63 | } 64 | break; 65 | case 'needPoolState': 66 | worker.send({ 67 | type: 'poolState', 68 | data: Object.keys(activePools) 69 | }); 70 | for (let hostname in activePools){ 71 | if (activePools.hasOwnProperty(hostname)){ 72 | if (!is_active_pool(hostname)) continue; 73 | let pool = activePools[hostname]; 74 | worker.send({ 75 | host: hostname, 76 | type: 'newBlockTemplate', 77 | data: pool.coinFuncs.getMasterJob(pool, worker.id) 78 | }); 79 | } 80 | } 81 | break; 82 | case 'workerStats': 83 | activeWorkers[worker.id][message.minerID] = message.data; 84 | break; 85 | } 86 | } 87 | } 88 | 89 | function slaveMessageHandler(message) { 90 | switch (message.type) { 91 | case 'newBlockTemplate': 92 | if (message.host in activePools){ 93 | if(activePools[message.host].activeBlocktemplate){ 94 | debug.workers(`Received a new block template for ${message.host} and have one in cache. Storing`); 95 | activePools[message.host].pastBlockTemplates.enq(activePools[message.host].activeBlocktemplate); 96 | } else { 97 | debug.workers(`Received a new block template for ${message.host} do not have one in cache.`); 98 | } 99 | activePools[message.host].activeBlocktemplate = new activePools[message.host].coinFuncs.BlockTemplate(message.data); 100 | for (let miner in activeMiners){ 101 | if (activeMiners.hasOwnProperty(miner)){ 102 | let realMiner = activeMiners[miner]; 103 | if (realMiner.pool === message.host){ 104 | realMiner.pushNewJob(); 105 | } 106 | } 107 | } 108 | } 109 | break; 110 | case 'poolState': 111 | message.data.forEach(function(hostname){ 112 | if(!(hostname in activePools)){ 113 | global.config.pools.forEach(function(poolData){ 114 | if (!poolData.coin) poolData.coin = "xmr"; 115 | if (hostname === poolData.hostname){ 116 | activePools[hostname] = new Pool(poolData); 117 | } 118 | }); 119 | } 120 | }); 121 | break; 122 | case 'changePool': 123 | if (activeMiners.hasOwnProperty(message.worker) && activePools.hasOwnProperty(message.pool)){ 124 | activeMiners[message.worker].pool = message.pool; 125 | activeMiners[message.worker].pushNewJob(true); 126 | } 127 | break; 128 | case 'disablePool': 129 | if (activePools.hasOwnProperty(message.pool)){ 130 | activePools[message.pool].active = false; 131 | checkActivePools(); 132 | } 133 | break; 134 | case 'enablePool': 135 | if (activePools.hasOwnProperty(message.pool)){ 136 | activePools[message.pool].active = true; 137 | process.send({type: 'needPoolState'}); 138 | } 139 | break; 140 | } 141 | } 142 | 143 | // Combined Functions 144 | function readConfig() { 145 | let local_conf = JSON.parse(fs.readFileSync('config.json')); 146 | if (typeof global.config === 'undefined') { 147 | global.config = {}; 148 | } 149 | for (let key in local_conf) { 150 | if (local_conf.hasOwnProperty(key) && (typeof global.config[key] === 'undefined' || global.config[key] !== local_conf[key])) { 151 | global.config[key] = local_conf[key]; 152 | } 153 | } 154 | if (!cluster.isMaster) { 155 | activatePorts(); 156 | } 157 | } 158 | 159 | // Pool Definition 160 | function Pool(poolData){ 161 | /* 162 | Pool data is the following: 163 | { 164 | "hostname": "pool.supportxmr.com", 165 | "port": 7777, 166 | "ssl": false, 167 | "share": 80, 168 | "username": "", 169 | "password": "", 170 | "keepAlive": true, 171 | "coin": "xmr" 172 | } 173 | Client Data format: 174 | { 175 | "method":"submit", 176 | "params":{ 177 | "id":"12e168f2-db42-4eea-b56a-f1e7d57f94c9", 178 | "job_id":"/4FIQEI/Qq++EzzH1e03oTrWF5Ed", 179 | "nonce":"9e008000", 180 | "result":"4eee0b966418fdc3ec1a684322715e65765554f11ff8f7fed3f75ac45ef20300" 181 | }, 182 | "id":1 183 | } 184 | */ 185 | this.hostname = poolData.hostname; 186 | this.port = poolData.port; 187 | this.ssl = poolData.ssl; 188 | this.share = poolData.share; 189 | this.username = poolData.username; 190 | this.password = poolData.password; 191 | this.keepAlive = poolData.keepAlive; 192 | this.default = poolData.default; 193 | this.devPool = poolData.hasOwnProperty('devPool') && poolData.devPool === true; 194 | this.coin = poolData.coin; 195 | this.pastBlockTemplates = support.circularBuffer(4); 196 | this.coinFuncs = require(`./lib/${this.coin}.js`)(); 197 | this.activeBlocktemplate = null; 198 | this.active = true; 199 | this.sendId = 1; 200 | this.sendLog = {}; 201 | this.poolJobs = {}; 202 | this.socket = null; 203 | this.allowSelfSignedSSL = true; 204 | // Partial checks for people whom havn't upgraded yet 205 | if (poolData.hasOwnProperty('allowSelfSignedSSL')){ 206 | this.allowSelfSignedSSL = !poolData.allowSelfSignedSSL; 207 | } 208 | const algo_arr = poolData.algo ? (poolData.algo instanceof Array ? poolData.algo : [poolData.algo]) : DEFAULT_ALGO; 209 | this.default_algo_set = {}; 210 | this.algos = {}; 211 | for (let i in algo_arr) this.algos[algo_arr[i]] = this.default_algo_set[algo_arr[i]] = 1; 212 | this.algos_perf = this.default_algos_perf = poolData.algo_perf && poolData.algo_perf instanceof Object ? poolData.algo_perf : DEFAULT_ALGO_PERF; 213 | this.blob_type = poolData.blob_type; 214 | 215 | 216 | setInterval(function(pool) { 217 | if (pool.keepAlive && pool.socket && is_active_pool(pool.hostname)) pool.sendData('keepalived'); 218 | }, 30000, this); 219 | 220 | this.close_socket = function(){ 221 | try { 222 | if (this.socket !== null){ 223 | this.socket.end(); 224 | this.socket.destroy(); 225 | } 226 | } catch (e) { 227 | console.warn(global.threadName + "Had issues murdering the old socket. Om nom: " + e) 228 | } 229 | this.socket = null; 230 | }; 231 | 232 | this.disable = function(){ 233 | for (let worker in cluster.workers){ 234 | if (cluster.workers.hasOwnProperty(worker)){ 235 | cluster.workers[worker].send({type: 'disablePool', pool: this.hostname}); 236 | } 237 | } 238 | this.active = false; 239 | 240 | this.close_socket(); 241 | }; 242 | 243 | this.connect = function(hostname){ 244 | function connect2(pool) { 245 | pool.close_socket(); 246 | 247 | if (pool.ssl){ 248 | pool.socket = tls.connect(pool.port, pool.hostname, {rejectUnauthorized: pool.allowSelfSignedSSL}) 249 | .on('connect', () => { poolSocket(pool.hostname); }) 250 | .on('error', (err) => { 251 | setTimeout(connect2, 30*1000, pool); 252 | console.warn(`${global.threadName}SSL pool socket connect error from ${pool.hostname}: ${err}`); 253 | }); 254 | } else { 255 | pool.socket = net.connect(pool.port, pool.hostname) 256 | .on('connect', () => { poolSocket(pool.hostname); }) 257 | .on('error', (err) => { 258 | setTimeout(connect2, 30*1000, pool); 259 | console.warn(`${global.threadName}Plain pool socket connect error from ${pool.hostname}: ${err}`); 260 | }); 261 | } 262 | } 263 | 264 | let pool = activePools[hostname]; 265 | pool.disable(); 266 | connect2(pool); 267 | }; 268 | this.sendData = function (method, params) { 269 | if (typeof params === 'undefined'){ 270 | params = {}; 271 | } 272 | let rawSend = { 273 | method: method, 274 | id: this.sendId++, 275 | }; 276 | if (typeof this.id !== 'undefined'){ 277 | params.id = this.id; 278 | } 279 | rawSend.params = params; 280 | if (this.socket === null || !this.socket.writable){ 281 | return false; 282 | } 283 | this.socket.write(JSON.stringify(rawSend) + '\n'); 284 | this.sendLog[rawSend.id] = rawSend; 285 | debug.pool(`Sent ${JSON.stringify(rawSend)} to ${this.hostname}`); 286 | }; 287 | this.login = function () { 288 | this.sendData('login', { 289 | login: this.username, 290 | pass: this.password, 291 | agent: 'xmr-node-proxy/' + PROXY_VERSION, 292 | "algo": Object.keys(this.algos), 293 | "algo-perf": this.algos_perf 294 | }); 295 | this.active = true; 296 | for (let worker in cluster.workers){ 297 | if (cluster.workers.hasOwnProperty(worker)){ 298 | cluster.workers[worker].send({type: 'enablePool', pool: this.hostname}); 299 | } 300 | } 301 | }; 302 | 303 | this.update_algo_perf = function (algos, algos_perf) { 304 | // do not update not changed algo/algo-perf 305 | const prev_algos = this.algos; 306 | const prev_algos_perf = this.algos_perf; 307 | const prev_algos_str = JSON.stringify(Object.keys(prev_algos)); 308 | const prev_algos_perf_str = JSON.stringify(prev_algos_perf); 309 | const algos_str = JSON.stringify(Object.keys(algos)); 310 | const algos_perf_str = JSON.stringify(algos_perf); 311 | if ( algos_str === prev_algos_str && algos_perf_str === prev_algos_perf_str) return; 312 | const curr_time = Date.now(); 313 | if (!this.last_common_algo_notify_time || curr_time - this.last_common_algo_notify_time > 5*60*1000 || algos_str !== prev_algos_str) { 314 | console.log("Setting common algo: " + algos_str + " with algo-perf: " + algos_perf_str + " for pool " + this.hostname); 315 | this.last_common_algo_notify_time = curr_time; 316 | } 317 | this.sendData('getjob', { 318 | "algo": Object.keys(this.algos = algos), 319 | "algo-perf": (this.algos_perf = algos_perf) 320 | }); 321 | }; 322 | this.sendShare = function (worker, shareData) { 323 | //btID - Block template ID in the poolJobs circ buffer. 324 | let job = this.poolJobs[worker.id].toarray().filter(function (job) { 325 | return job.id === shareData.btID; 326 | })[0]; 327 | if (job) { 328 | let submitParams = { 329 | job_id: job.masterJobID, 330 | nonce: shareData.nonce, 331 | workerNonce: shareData.workerNonce, 332 | poolNonce: job.poolNonce 333 | }; 334 | if (shareData.resultHash) submitParams.result = shareData.resultHash; 335 | if (shareData.pow) submitParams.pow = shareData.pow; 336 | this.sendData('submit', submitParams); 337 | } 338 | }; 339 | } 340 | 341 | // Master Functions 342 | /* 343 | The master performs the following tasks: 344 | 1. Serve all API calls. 345 | 2. Distribute appropriately modified block template bases to all pool servers. 346 | 3. Handle all to/from the various pool servers. 347 | 4. Manage and suggest miner changes in order to achieve correct h/s balancing between the various systems. 348 | */ 349 | function connectPools(){ 350 | global.config.pools.forEach(function (poolData) { 351 | if (!poolData.coin) poolData.coin = "xmr"; 352 | if (activePools.hasOwnProperty(poolData.hostname)){ 353 | return; 354 | } 355 | activePools[poolData.hostname] = new Pool(poolData); 356 | activePools[poolData.hostname].connect(poolData.hostname); 357 | }); 358 | let seen_coins = {}; 359 | if (global.config.developerShare > 0){ 360 | for (let pool in activePools){ 361 | if (activePools.hasOwnProperty(pool)){ 362 | if (seen_coins.hasOwnProperty(activePools[pool].coin)){ 363 | return; 364 | } 365 | let devPool = activePools[pool].coinFuncs.devPool; 366 | if (activePools.hasOwnProperty(devPool.hostname)){ 367 | return; 368 | } 369 | activePools[devPool.hostname] = new Pool(devPool); 370 | activePools[devPool.hostname].connect(devPool.hostname); 371 | seen_coins[activePools[pool].coin] = true; 372 | } 373 | } 374 | } 375 | for (let coin in seen_coins){ 376 | if (seen_coins.hasOwnProperty(coin)){ 377 | activeCoins[coin] = true; 378 | } 379 | } 380 | } 381 | 382 | let poolStates = {}; 383 | 384 | function balanceWorkers(){ 385 | /* 386 | This function deals with handling how the pool deals with getting traffic balanced to the various pools. 387 | Step 1: Enumerate all workers (Child servers), and their miners/coins into known states 388 | Step 1: Enumerate all miners, move their H/S into a known state tagged to the coins and pools 389 | Step 2: Enumerate all pools, verify the percentages as fractions of 100. 390 | Step 3: Determine if we're sharing with the developers (Woohoo! You're the best if you do!) 391 | Step 4: Process the state information to determine splits/moves. 392 | Step 5: Notify child processes of other pools to send traffic to if needed. 393 | 394 | The Master, as the known state holder of all information, deals with handling this data. 395 | */ 396 | let minerStates = {}; 397 | poolStates = {}; 398 | for (let poolName in activePools){ 399 | if (activePools.hasOwnProperty(poolName)){ 400 | let pool = activePools[poolName]; 401 | if (!poolStates.hasOwnProperty(pool.coin)){ 402 | poolStates[pool.coin] = { 'totalPercentage': 0, 'activePoolCount': 0, 'devPool': false}; 403 | } 404 | poolStates[pool.coin][poolName] = { 405 | miners: {}, 406 | hashrate: 0, 407 | percentage: pool.share, 408 | devPool: pool.devPool, 409 | idealRate: 0 410 | }; 411 | if(pool.devPool){ 412 | poolStates[pool.coin].devPool = poolName; 413 | debug.balancer(`Found a developer pool enabled. Pool is: ${poolName}`); 414 | } else if (is_active_pool(poolName)) { 415 | poolStates[pool.coin].totalPercentage += pool.share; 416 | ++ poolStates[pool.coin].activePoolCount; 417 | } else { 418 | console.error(`${global.threadName}Pool ${poolName} is disabled due to issues with it`); 419 | } 420 | if (!minerStates.hasOwnProperty(pool.coin)){ 421 | minerStates[pool.coin] = { 422 | hashrate: 0 423 | }; 424 | } 425 | } 426 | } 427 | /* 428 | poolStates now contains an object that looks approximately like: 429 | poolStates = { 430 | 'xmr': 431 | { 432 | 'mine.xmrpool.net': { 433 | 'miners': {}, 434 | 'hashrate': 0, 435 | 'percentage': 20, 436 | 'devPool': false, 437 | 'amtChange': 0 438 | }, 439 | 'donations.xmrpool.net': { 440 | 'miners': {}, 441 | 'hashrate': 0, 442 | 'percentage': 0, 443 | 'devPool': true, 444 | 'amtChange': 0 445 | }, 446 | 'devPool': 'donations.xmrpool.net', 447 | 'totalPercentage': 20 448 | } 449 | } 450 | */ 451 | for (let coin in poolStates){ 452 | if(poolStates.hasOwnProperty(coin)){ 453 | if (poolStates[coin].totalPercentage !== 100){ 454 | debug.balancer(`Pools on ${coin} are using ${poolStates[coin].totalPercentage}% balance. Adjusting.`); 455 | // Need to adjust all the pools that aren't the dev pool. 456 | if (poolStates[coin].totalPercentage) { 457 | let percentModifier = 100 / poolStates[coin].totalPercentage; 458 | for (let pool in poolStates[coin]){ 459 | if (poolStates[coin].hasOwnProperty(pool) && activePools.hasOwnProperty(pool)){ 460 | if (poolStates[coin][pool].devPool || !is_active_pool(pool)) continue; 461 | poolStates[coin][pool].percentage *= percentModifier; 462 | } 463 | } 464 | } else if (poolStates[coin].activePoolCount) { 465 | let addModifier = 100 / poolStates[coin].activePoolCount; 466 | for (let pool in poolStates[coin]){ 467 | if (poolStates[coin].hasOwnProperty(pool) && activePools.hasOwnProperty(pool)){ 468 | if (poolStates[coin][pool].devPool || !is_active_pool(pool)) continue; 469 | poolStates[coin][pool].percentage += addModifier; 470 | } 471 | } 472 | } else { 473 | debug.balancer(`No active pools for ${coin} coin, so waiting for the next cycle.`); 474 | continue; 475 | } 476 | 477 | } 478 | delete(poolStates[coin].totalPercentage); 479 | delete(poolStates[coin].activePoolCount); 480 | } 481 | } 482 | /* 483 | poolStates now contains an object that looks approximately like: 484 | poolStates = { 485 | 'xmr': 486 | { 487 | 'mine.xmrpool.net': { 488 | 'miners': {}, 489 | 'hashrate': 0, 490 | 'percentage': 100, 491 | 'devPool': false 492 | }, 493 | 'donations.xmrpool.net': { 494 | 'miners': {}, 495 | 'hashrate': 0, 496 | 'percentage': 0, 497 | 'devPool': true 498 | }, 499 | 'devPool': 'donations.xmrpool.net', 500 | } 501 | } 502 | */ 503 | for (let workerID in activeWorkers){ 504 | if (activeWorkers.hasOwnProperty(workerID)){ 505 | for (let minerID in activeWorkers[workerID]){ 506 | if (activeWorkers[workerID].hasOwnProperty(minerID)){ 507 | let miner = activeWorkers[workerID][minerID]; 508 | try { 509 | let minerCoin = miner.coin; 510 | if (!minerStates.hasOwnProperty(minerCoin)){ 511 | minerStates[minerCoin] = { 512 | hashrate: 0 513 | }; 514 | } 515 | minerStates[minerCoin].hashrate += miner.avgSpeed; 516 | poolStates[minerCoin][miner.pool].hashrate += miner.avgSpeed; 517 | poolStates[minerCoin][miner.pool].miners[`${workerID}_${minerID}`] = miner.avgSpeed; 518 | } catch (err) {} 519 | } 520 | } 521 | } 522 | } 523 | /* 524 | poolStates now contains the hashrate per pool. This can be compared against minerStates/hashRate to determine 525 | the approximate hashrate that should be moved between pools once the general hashes/second per pool/worker 526 | is determined. 527 | */ 528 | 529 | for (let coin in poolStates){ 530 | if (poolStates.hasOwnProperty(coin) && minerStates.hasOwnProperty(coin)){ 531 | let coinMiners = minerStates[coin]; 532 | let coinPools = poolStates[coin]; 533 | let devPool = coinPools.devPool; 534 | let highPools = {}; 535 | let lowPools = {}; 536 | delete(coinPools.devPool); 537 | if (devPool){ 538 | let devHashrate = Math.floor(coinMiners.hashrate * (global.config.developerShare/100)); 539 | coinMiners.hashrate -= devHashrate; 540 | coinPools[devPool].idealRate = devHashrate; 541 | debug.balancer(`DevPool on ${coin} is enabled. Set to ${global.config.developerShare}% and ideally would have ${coinPools[devPool].idealRate}. Currently has ${coinPools[devPool].hashrate}`); 542 | if (is_active_pool(devPool) && coinPools[devPool].idealRate > coinPools[devPool].hashrate){ 543 | lowPools[devPool] = coinPools[devPool].idealRate - coinPools[devPool].hashrate; 544 | debug.balancer(`Pool ${devPool} is running a low hashrate compared to ideal. Want to increase by: ${lowPools[devPool]} h/s`); 545 | } else if (!is_active_pool(devPool) || coinPools[devPool].idealRate < coinPools[devPool].hashrate){ 546 | highPools[devPool] = coinPools[devPool].hashrate - coinPools[devPool].idealRate; 547 | debug.balancer(`Pool ${devPool} is running a high hashrate compared to ideal. Want to decrease by: ${highPools[devPool]} h/s`); 548 | } 549 | } 550 | for (let pool in coinPools){ 551 | if (coinPools.hasOwnProperty(pool) && pool !== devPool && activePools.hasOwnProperty(pool)){ 552 | coinPools[pool].idealRate = Math.floor(coinMiners.hashrate * (coinPools[pool].percentage/100)); 553 | if (is_active_pool(pool) && coinPools[pool].idealRate > coinPools[pool].hashrate){ 554 | lowPools[pool] = coinPools[pool].idealRate - coinPools[pool].hashrate; 555 | debug.balancer(`Pool ${pool} is running a low hashrate compared to ideal. Want to increase by: ${lowPools[pool]} h/s`); 556 | } else if (!is_active_pool(pool) || coinPools[pool].idealRate < coinPools[pool].hashrate){ 557 | highPools[pool] = coinPools[pool].hashrate - coinPools[pool].idealRate; 558 | debug.balancer(`Pool ${pool} is running a high hashrate compared to ideal. Want to decrease by: ${highPools[pool]} h/s`); 559 | } 560 | //activePools[pool].share = coinPools[pool].percentage; 561 | } 562 | } 563 | if (Object.keys(highPools).length === 0 && Object.keys(lowPools).length === 0){ 564 | debug.balancer(`No high or low ${coin} coin pools, so waiting for the next cycle.`); 565 | continue; 566 | } 567 | let freed_miners = {}; 568 | if (Object.keys(highPools).length > 0){ 569 | for (let pool in highPools){ 570 | if (highPools.hasOwnProperty(pool)){ 571 | for (let miner in coinPools[pool].miners){ 572 | if (coinPools[pool].miners.hasOwnProperty(miner)){ 573 | if ((!is_active_pool(pool) || coinPools[pool].miners[miner] <= highPools[pool]) && coinPools[pool].miners[miner] !== 0){ 574 | highPools[pool] -= coinPools[pool].miners[miner]; 575 | freed_miners[miner] = coinPools[pool].miners[miner]; 576 | debug.balancer(`Freeing up ${miner} on ${pool} for ${freed_miners[miner]} h/s`); 577 | delete(coinPools[pool].miners[miner]); 578 | } 579 | } 580 | } 581 | } 582 | } 583 | } 584 | let minerChanges = {}; 585 | if (Object.keys(lowPools).length > 0){ 586 | for (let pool in lowPools){ 587 | if (lowPools.hasOwnProperty(pool)){ 588 | minerChanges[pool] = []; 589 | // fit low pools without overflow 590 | if (Object.keys(freed_miners).length > 0){ 591 | for (let miner in freed_miners){ 592 | if (freed_miners.hasOwnProperty(miner)){ 593 | if (freed_miners[miner] <= lowPools[pool]){ 594 | minerChanges[pool].push(miner); 595 | lowPools[pool] -= freed_miners[miner]; 596 | debug.balancer(`Snagging up ${miner} for ${pool} for ${freed_miners[miner]} h/s`); 597 | delete(freed_miners[miner]); 598 | } 599 | } 600 | } 601 | } 602 | if(lowPools[pool] > 100){ 603 | for (let donatorPool in coinPools){ 604 | if(coinPools.hasOwnProperty(donatorPool) && !lowPools.hasOwnProperty(donatorPool)){ 605 | for (let miner in coinPools[donatorPool].miners){ 606 | if (coinPools[donatorPool].miners.hasOwnProperty(miner)){ 607 | if (coinPools[donatorPool].miners[miner] <= lowPools[pool] && coinPools[donatorPool].miners[miner] !== 0){ 608 | minerChanges[pool].push(miner); 609 | lowPools[pool] -= coinPools[donatorPool].miners[miner]; 610 | debug.balancer(`Moving ${miner} for ${pool} from ${donatorPool} for ${coinPools[donatorPool].miners[miner]} h/s`); 611 | delete(coinPools[donatorPool].miners[miner]); 612 | } 613 | if (lowPools[pool] < 50){ 614 | break; 615 | } 616 | } 617 | } 618 | if (lowPools[pool] < 50){ 619 | break; 620 | } 621 | } 622 | } 623 | } 624 | } 625 | } 626 | // fit low pools with overflow except devPool 627 | if (Object.keys(freed_miners).length > 0){ 628 | for (let pool in lowPools){ 629 | if (lowPools.hasOwnProperty(pool) && pool !== devPool){ 630 | if (!(pool in minerChanges)) minerChanges[pool] = []; 631 | for (let miner in freed_miners){ 632 | if (freed_miners.hasOwnProperty(miner)){ 633 | minerChanges[pool].push(miner); 634 | lowPools[pool] -= freed_miners[miner]; 635 | debug.balancer(`Moving overflow ${miner} for ${pool} for ${freed_miners[miner]} h/s`); 636 | delete(freed_miners[miner]); 637 | } 638 | } 639 | } 640 | } 641 | } 642 | } 643 | for (let pool in minerChanges){ 644 | if(minerChanges.hasOwnProperty(pool) && minerChanges[pool].length > 0){ 645 | minerChanges[pool].forEach(function(miner){ 646 | let minerBits = miner.split('_'); 647 | if (cluster.workers[minerBits[0]]) cluster.workers[minerBits[0]].send({ 648 | type: 'changePool', 649 | worker: minerBits[1], 650 | pool: pool 651 | }); 652 | }); 653 | } 654 | } 655 | } 656 | } 657 | } 658 | 659 | let hs_algo = ""; // common algo for human_hashrate 660 | 661 | function enumerateWorkerStats() { 662 | let stats, global_stats = {miners: 0, hashes: 0, hashRate: 0, diff: 0}; 663 | let pool_algos = {}; 664 | let pool_algos_perf = {}; 665 | for (let poolID in activeWorkers){ 666 | if (activeWorkers.hasOwnProperty(poolID)){ 667 | stats = { 668 | miners: 0, 669 | hashes: 0, 670 | hashRate: 0, 671 | diff: 0 672 | }; 673 | let inactivityDeadline = (typeof global.config.minerInactivityTime === 'undefined') ? Math.floor((Date.now())/1000) - 120 674 | : (global.config.minerInactivityTime <= 0 ? 0 : Math.floor((Date.now())/1000) - global.config.minerInactivityTime); 675 | for (let workerID in activeWorkers[poolID]){ 676 | if (activeWorkers[poolID].hasOwnProperty(workerID)) { 677 | let workerData = activeWorkers[poolID][workerID]; 678 | if (typeof workerData !== 'undefined') { 679 | try{ 680 | if (workerData.lastContact < inactivityDeadline){ 681 | delete activeWorkers[poolID][workerID]; 682 | continue; 683 | } 684 | ++ stats.miners; 685 | stats.hashes += workerData.hashes; 686 | stats.hashRate += workerData.avgSpeed; 687 | stats.diff += workerData.diff; 688 | // process smart miners and assume all other miners to only support pool algo 689 | let miner_algos = workerData.algos; 690 | if (!miner_algos) miner_algos = activePools[workerData.pool].default_algo_set; 691 | if (workerData.pool in pool_algos) { // compute union of miner_algos and pool_algos[workerData.pool] 692 | for (let algo in pool_algos[workerData.pool]) { 693 | if (!(algo in miner_algos)) delete pool_algos[workerData.pool][algo]; 694 | } 695 | } else { 696 | pool_algos[workerData.pool] = miner_algos; 697 | pool_algos_perf[workerData.pool] = {}; 698 | } 699 | if (workerData.algos_perf) { // only process smart miners and add algo_perf from all smart miners 700 | for (let algo in workerData.algos_perf) { 701 | if (algo in pool_algos_perf[workerData.pool]) pool_algos_perf[workerData.pool][algo] += workerData.algos_perf[algo]; 702 | else pool_algos_perf[workerData.pool][algo] = workerData.algos_perf[algo]; 703 | } 704 | } 705 | } catch (err) { 706 | delete activeWorkers[poolID][workerID]; 707 | } 708 | } else { 709 | delete activeWorkers[poolID][workerID]; 710 | } 711 | } 712 | } 713 | global_stats.miners += stats.miners; 714 | global_stats.hashes += stats.hashes; 715 | global_stats.hashRate += stats.hashRate; 716 | global_stats.diff += stats.diff; 717 | 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)}`); 718 | } 719 | } 720 | 721 | let pool_hs = ""; 722 | for (let coin in poolStates) { 723 | if (!poolStates.hasOwnProperty(coin)) continue; 724 | for (let pool in poolStates[coin]) { 725 | if (!poolStates[coin].hasOwnProperty(pool) || !activePools.hasOwnProperty(pool) || poolStates[coin][pool].devPool || poolStates[coin][pool].hashrate === 0) continue; 726 | if (pool_hs != "") pool_hs += ", "; 727 | pool_hs += `${pool}/${poolStates[coin][pool].percentage.toFixed(2)}%`; 728 | } 729 | } 730 | if (pool_hs != "") pool_hs = " (" + pool_hs + ")"; 731 | 732 | // do update of algo/algo-perf if it was changed 733 | hs_algo = ""; // common algo for human_hashrate 734 | for (let pool in pool_algos) { 735 | let pool_algos_perf2 = pool_algos_perf[pool]; 736 | if (Object.keys(pool_algos_perf2).length === 0) pool_algos_perf2 = activePools[pool].default_algos_perf; 737 | activePools[pool].update_algo_perf(pool_algos[pool], pool_algos_perf2); 738 | if (Object.keys(pool_algos[pool]).length == 1) { 739 | if ("c29s" in pool_algos[pool]) hs_algo = (hs_algo === "c29s" || hs_algo === "") ? "c29s" : "h/s"; 740 | else if ("c29v" in pool_algos[pool]) hs_algo = (hs_algo === "c29v" || hs_algo === "") ? "c29v" : "h/s"; 741 | else hs_algo = "h/s"; 742 | } else { 743 | hs_algo = "h/s"; 744 | } 745 | } 746 | 747 | const hs = support.human_hashrate(global_stats.hashRate, hs_algo); 748 | console.log(`The proxy currently has ${global_stats.miners} miners connected at ${hs}${pool_hs}` + (global_stats.miners ? ` with an average diff of ${Math.floor(global_stats.diff/global_stats.miners)}` : "")); 749 | } 750 | 751 | function poolSocket(hostname){ 752 | let pool = activePools[hostname]; 753 | let socket = pool.socket; 754 | let dataBuffer = ''; 755 | socket.on('data', (d) => { 756 | dataBuffer += d; 757 | if (dataBuffer.indexOf('\n') !== -1) { 758 | let messages = dataBuffer.split('\n'); 759 | let incomplete = dataBuffer.slice(-1) === '\n' ? '' : messages.pop(); 760 | for (let i = 0; i < messages.length; i++) { 761 | let message = messages[i]; 762 | if (message.trim() === '') { 763 | continue; 764 | } 765 | let jsonData; 766 | try { 767 | jsonData = JSON.parse(message); 768 | } 769 | catch (e) { 770 | if (message.indexOf('GET /') === 0) { 771 | if (message.indexOf('HTTP/1.1') !== -1) { 772 | socket.end('HTTP/1.1' + httpResponse); 773 | break; 774 | } 775 | else if (message.indexOf('HTTP/1.0') !== -1) { 776 | socket.end('HTTP/1.0' + httpResponse); 777 | break; 778 | } 779 | } 780 | 781 | console.warn(`${global.threadName}Pool wrong reply error from ${pool.hostname}: ${message}`); 782 | socket.destroy(); 783 | 784 | break; 785 | } 786 | handlePoolMessage(jsonData, pool.hostname); 787 | } 788 | dataBuffer = incomplete; 789 | } 790 | }).on('error', (err) => { 791 | console.warn(`${global.threadName}Pool socket error from ${pool.hostname}: ${err}`); 792 | activePools[pool.hostname].disable(); 793 | setTimeout(activePools[pool.hostname].connect, 30*1000, pool.hostname); 794 | }).on('close', () => { 795 | console.warn(`${global.threadName}Pool socket closed from ${pool.hostname}`); 796 | activePools[pool.hostname].disable(); 797 | setTimeout(activePools[pool.hostname].connect, 30*1000, pool.hostname); 798 | }); 799 | socket.setKeepAlive(true); 800 | socket.setEncoding('utf8'); 801 | console.log(`${global.threadName}Connected to pool: ${pool.hostname}`); 802 | pool.login(); 803 | } 804 | 805 | function handlePoolMessage(jsonData, hostname){ 806 | let pool = activePools[hostname]; 807 | debug.pool(`Received ${JSON.stringify(jsonData)} from ${pool.hostname}`); 808 | if (jsonData.hasOwnProperty('method')){ 809 | // The only time method is set, is with a push of data. Everything else is a reply/ 810 | if (jsonData.method === 'job'){ 811 | handleNewBlockTemplate(jsonData.params, hostname); 812 | } 813 | } else { 814 | if (jsonData.error !== null){ 815 | console.error(`${global.threadName}Error response from pool ${pool.hostname}: ${JSON.stringify(jsonData.error)}`); 816 | if ((jsonData.error instanceof Object) && (typeof jsonData.error.message === 'string') && jsonData.error.message.includes("Unauthenticated")) activePools[hostname].disable(); 817 | return; 818 | } 819 | let sendLog = pool.sendLog[jsonData.id]; 820 | switch(sendLog.method){ 821 | case 'login': 822 | pool.id = jsonData.result.id; 823 | handleNewBlockTemplate(jsonData.result.job, hostname); 824 | break; 825 | case 'getjob': 826 | // null for same job 827 | if (jsonData.result !== null) handleNewBlockTemplate(jsonData.result, hostname); 828 | break; 829 | case 'submit': 830 | sendLog.accepted = true; 831 | break; 832 | } 833 | } 834 | } 835 | 836 | function handleNewBlockTemplate(blockTemplate, hostname){ 837 | if (!blockTemplate) { 838 | console.error(`${global.threadName}Empty response from pool ${hostname}`); 839 | activePools[hostname].disable(); 840 | return; 841 | } 842 | let pool = activePools[hostname]; 843 | let algo_variant = ""; 844 | if (blockTemplate.algo) algo_variant += "algo: " + blockTemplate.algo; 845 | if (blockTemplate.variant) { 846 | if (algo_variant != "") algo_variant += ", "; 847 | algo_variant += "variant: " + blockTemplate.variant; 848 | } 849 | if (algo_variant != "") algo_variant = " (" + algo_variant + ")"; 850 | console.log(`Received new block template on ${blockTemplate.height} height${algo_variant} with ${blockTemplate.target_diff} target difficulty from ${pool.hostname}`); 851 | if(pool.activeBlocktemplate){ 852 | if (pool.activeBlocktemplate.job_id === blockTemplate.job_id){ 853 | debug.pool('No update with this job, it is an upstream dupe'); 854 | return; 855 | } 856 | debug.pool('Storing the previous block template'); 857 | pool.pastBlockTemplates.enq(pool.activeBlocktemplate); 858 | } 859 | if (!blockTemplate.algo) blockTemplate.algo = pool.coinFuncs.detectAlgo(pool.default_algo_set, 16 * parseInt(blockTemplate.blocktemplate_blob[0]) + parseInt(blockTemplate.blocktemplate_blob[1])); 860 | if (!blockTemplate.blob_type) blockTemplate.blob_type = pool.blob_type; 861 | pool.activeBlocktemplate = new pool.coinFuncs.MasterBlockTemplate(blockTemplate); 862 | for (let id in cluster.workers){ 863 | if (cluster.workers.hasOwnProperty(id)){ 864 | cluster.workers[id].send({ 865 | host: hostname, 866 | type: 'newBlockTemplate', 867 | data: pool.coinFuncs.getMasterJob(pool, id) 868 | }); 869 | } 870 | } 871 | } 872 | 873 | function is_active_pool(hostname) { 874 | let pool = activePools[hostname]; 875 | if ((cluster.isMaster && !pool.socket) || !pool.active || pool.activeBlocktemplate === null) return false; 876 | 877 | let top_height = 0; 878 | for (let poolName in activePools){ 879 | if (!activePools.hasOwnProperty(poolName)) continue; 880 | let pool2 = activePools[poolName]; 881 | if (pool2.coin != pool.coin) continue; 882 | if ((cluster.isMaster && !pool2.socket) || !pool2.active || pool2.activeBlocktemplate === null) continue; 883 | if (Math.abs(pool2.activeBlocktemplate.height - pool.activeBlocktemplate.height) > 1000) continue; // different coin templates, can't compare here 884 | if (pool2.activeBlocktemplate.height > top_height) top_height = pool2.activeBlocktemplate.height; 885 | } 886 | 887 | if (pool.activeBlocktemplate.height < top_height - 5) return false; 888 | return true; 889 | } 890 | 891 | // Miner Definition 892 | function Miner(id, params, ip, pushMessage, portData, minerSocket) { 893 | // Arguments 894 | // minerId, params, ip, pushMessage, portData 895 | // Username Layout -
. 896 | // Password Layout - .. 897 | // Default function is to use the password so they can login. Identifiers can be unique, payment ID is last. 898 | // If there is no miner identifier, then the miner identifier is set to the password 899 | // If the password is x, aka, old-logins, we're not going to allow detailed review of miners. 900 | 901 | const login_diff_split = params.login ? params.login.split("+") : ""; 902 | if (!params.pass) params.pass = "x"; 903 | const pass_algo_split = params.pass.split("~"); 904 | const pass_split = pass_algo_split[0].split(":"); 905 | 906 | // Miner Variables 907 | this.coin = portData.coin; 908 | this.coinFuncs = require(`./lib/${this.coin}.js`)(); 909 | this.coinSettings = global.config.coinSettings[this.coin]; 910 | this.login = login_diff_split[0]; // Documentation purposes only. 911 | this.user = login_diff_split[0]; // For accessControl and workerStats. 912 | this.password = pass_split[0]; // For accessControl and workerStats. 913 | this.agent = params.agent; // Documentation purposes only. 914 | this.ip = ip; // Documentation purposes only. 915 | 916 | if (pass_algo_split.length == 2) { 917 | const algo_name = pass_algo_split[1]; 918 | params.algo = [ algo_name ]; 919 | params["algo-perf"] = {}; 920 | params["algo-perf"][algo_name] = 1; 921 | } 922 | 923 | if (params.algo && (params.algo instanceof Array)) { // To report union of defined algo set to the pool for all its miners 924 | for (let i in params.algo) { 925 | this.algos = {}; 926 | for (let i in params.algo) this.algos[params.algo[i]] = 1; 927 | } 928 | } 929 | this.algos_perf = params["algo-perf"]; // To report sum of defined algo_perf to the pool for all its miners 930 | this.socket = minerSocket; 931 | this.pushMessage = pushMessage; 932 | this.getNewJob = function (bashCache) { 933 | return this.coinFuncs.getJob(this, activePools[this.pool].activeBlocktemplate, bashCache); 934 | }; 935 | this.pushNewJob = function (bashCache) { 936 | const job = this.getNewJob(bashCache); 937 | if (this.protocol === "grin") { 938 | this.pushMessage({method: 'getjobtemplate', result: job}); 939 | } else { 940 | this.pushMessage({method: 'job', params: job}); 941 | } 942 | }; 943 | this.error = ""; 944 | this.valid_miner = true; 945 | this.incremented = false; 946 | this.fixed_diff = false; 947 | this.difficulty = portData.diff; 948 | this.connectTime = Date.now(); 949 | 950 | if (!defaultPools.hasOwnProperty(portData.coin) || !is_active_pool(defaultPools[portData.coin])) { 951 | for (let poolName in activePools){ 952 | if (activePools.hasOwnProperty(poolName)){ 953 | let pool = activePools[poolName]; 954 | if (pool.coin != portData.coin || pool.devPool) continue; 955 | if (is_active_pool(poolName)) { 956 | this.pool = poolName; 957 | break; 958 | } 959 | } 960 | } 961 | } 962 | if (!this.pool) this.pool = defaultPools[portData.coin]; 963 | 964 | if (login_diff_split.length === 2) { 965 | this.fixed_diff = true; 966 | this.difficulty = Number(login_diff_split[1]); 967 | this.user = login_diff_split[0]; 968 | } else if (login_diff_split.length > 2) { 969 | this.error = "Too many options in the login field"; 970 | this.valid_miner = false; 971 | } 972 | 973 | if (activePools[this.pool].activeBlocktemplate === null){ 974 | this.error = "No active block template"; 975 | this.valid_miner = false; 976 | } 977 | 978 | // Verify if user/password is in allowed client connects 979 | if (!isAllowedLogin(this.user, this.password)) { 980 | this.error = "Unauthorized access"; 981 | this.valid_miner = false; 982 | } 983 | 984 | this.id = id; 985 | this.heartbeat = function () { 986 | this.lastContact = Date.now(); 987 | }; 988 | this.heartbeat(); 989 | 990 | // VarDiff System 991 | this.lastShareTime = Date.now() / 1000 || 0; 992 | 993 | this.shares = 0; 994 | this.blocks = 0; 995 | this.hashes = 0; 996 | 997 | this.validJobs = support.circularBuffer(5); 998 | 999 | this.cachedJob = null; 1000 | 1001 | this.identifier = global.config.addressWorkerID ? this.user : pass_split[0]; 1002 | 1003 | this.logString = (this.identifier && this.identifier != "x") ? this.identifier + " (" + this.ip + ")" : this.ip; 1004 | 1005 | if (this.algos) { 1006 | const pool = activePools[this.pool]; 1007 | if (pool) { 1008 | const blockTemplate = pool.activeBlocktemplate; 1009 | if (blockTemplate && blockTemplate.blob) { 1010 | const pool_algo = pool.coinFuncs.detectAlgo(pool.default_algo_set, 16 * parseInt(blockTemplate.blob[0]) + parseInt(blockTemplate.blob[1])); 1011 | if (!(pool_algo in this.algos)) { 1012 | console.warn("Your miner " + this.logString + " does not have " + pool_algo + " algo support. Please update it."); 1013 | } 1014 | } 1015 | } 1016 | } 1017 | 1018 | this.minerStats = function(){ 1019 | if (this.socket.destroyed && !global.config.keepOfflineMiners){ 1020 | delete activeMiners[this.id]; 1021 | return; 1022 | } 1023 | return { 1024 | active: !this.socket.destroyed, 1025 | shares: this.shares, 1026 | blocks: this.blocks, 1027 | hashes: this.hashes, 1028 | avgSpeed: this.hashes/(Math.floor((Date.now() - this.connectTime)/1000)), 1029 | diff: this.difficulty, 1030 | connectTime: this.connectTime, 1031 | lastContact: Math.floor(this.lastContact/1000), 1032 | lastShare: this.lastShareTime, 1033 | coin: this.coin, 1034 | pool: this.pool, 1035 | id: this.id, 1036 | identifier: this.identifier, 1037 | ip: this.ip, 1038 | agent: this.agent, 1039 | algos: this.algos, 1040 | algos_perf: this.algos_perf, 1041 | logString: this.logString, 1042 | }; 1043 | }; 1044 | 1045 | // Support functions for how miners activate and run. 1046 | this.updateDifficulty = function(){ 1047 | if (this.hashes > 0 && !this.fixed_diff) { 1048 | const new_diff = Math.floor(this.hashes / (Math.floor((Date.now() - this.connectTime) / 1000))) * this.coinSettings.shareTargetTime; 1049 | if (this.setNewDiff(new_diff)) { 1050 | this.pushNewJob(); 1051 | } 1052 | } 1053 | }; 1054 | 1055 | this.setNewDiff = function (difficulty) { 1056 | this.newDiff = Math.round(difficulty); 1057 | if (this.newDiff > this.coinSettings.maxDiff) { 1058 | this.newDiff = this.coinSettings.maxDiff; 1059 | } 1060 | if (this.newDiff < this.coinSettings.minDiff) { 1061 | this.newDiff = this.coinSettings.minDiff; 1062 | } 1063 | if (this.difficulty === this.newDiff) { 1064 | return false; 1065 | } 1066 | debug.diff(global.threadName + "Difficulty change to: " + this.newDiff + " For: " + this.logString); 1067 | if (this.hashes > 0){ 1068 | debug.diff(global.threadName + "Hashes: " + this.hashes + " in: " + Math.floor((Date.now() - this.connectTime)/1000) + " seconds gives: " + 1069 | Math.floor(this.hashes/(Math.floor((Date.now() - this.connectTime)/1000))) + " hashes/second or: " + 1070 | Math.floor(this.hashes/(Math.floor((Date.now() - this.connectTime)/1000))) *this.coinSettings.shareTargetTime + " difficulty versus: " + this.newDiff); 1071 | } 1072 | return true; 1073 | }; 1074 | } 1075 | 1076 | // Slave Functions 1077 | function isAllowedLogin(username, password) { 1078 | // If controlled login is not enabled, everybody can connnect (return true) 1079 | if (typeof global.config['accessControl'] !== 'object' 1080 | || global.config['accessControl'].enabled !== true) { 1081 | return true; 1082 | } 1083 | 1084 | // If user is in the list (return true) 1085 | if (isInAccessControl(username, password)) { 1086 | return true; 1087 | } 1088 | // If user is not in the list ... 1089 | else { 1090 | 1091 | // ... and accessControl has not been loaded in last minute (prevent HD flooding in case of attack) 1092 | if (lastAccessControlLoadTime === null 1093 | || (Date.now() - lastAccessControlLoadTime) / 1000 > 60) { 1094 | 1095 | // Take note of new load time 1096 | lastAccessControlLoadTime = Date.now(); 1097 | 1098 | // Re-load file from disk and inject in accessControl 1099 | accessControl = JSON.parse(fs.readFileSync(global.config['accessControl']['controlFile'])); 1100 | 1101 | // Re-verify if the user is in the list 1102 | return isInAccessControl(username, password); 1103 | } 1104 | 1105 | // User is not in the list, and not yet ready to re-load from disk 1106 | else { 1107 | // TODO Take notes of IP/Nb of rejections. Ultimately insert IP in bans after X threshold 1108 | return false; 1109 | } 1110 | } 1111 | } 1112 | function isInAccessControl(username, password) { 1113 | return typeof accessControl[username] !== 'undefined' && accessControl[username] === password; 1114 | } 1115 | 1116 | function handleMinerData(minerSocket, id, method, params, ip, portData, sendReply, sendReplyFinal, pushMessage) { 1117 | switch (method) { 1118 | case 'login': { // grin and default 1119 | if (ip in bans) { 1120 | sendReplyFinal("IP Address currently banned"); 1121 | return; 1122 | } 1123 | if (!params) { 1124 | sendReplyFinal("No params specified"); 1125 | return; 1126 | } 1127 | let difficulty = portData.difficulty; 1128 | const minerId = support.get_new_id(); 1129 | if (!portData.coin) portData.coin = "xmr"; 1130 | let miner = new Miner(minerId, params, ip, pushMessage, portData, minerSocket); 1131 | if (!miner.valid_miner) { 1132 | console.warn(global.threadName + "Invalid miner: " + miner.logString + ", disconnecting due to: " + miner.error); 1133 | sendReplyFinal(miner.error); 1134 | return; 1135 | } 1136 | process.send({type: 'newMiner', data: miner.port}); 1137 | activeMiners[minerId] = miner; 1138 | minerSocket.minerId = minerId; 1139 | // clean old miners with the same name/ip/agent 1140 | if (global.config.keepOfflineMiners) { 1141 | for (let miner_id in activeMiners) { 1142 | if (activeMiners.hasOwnProperty(miner_id)) { 1143 | let realMiner = activeMiners[miner_id]; 1144 | if (realMiner.socket.destroyed && realMiner.identifier === miner.identifier && realMiner.ip === miner.ip && realMiner.agent === miner.agent) { 1145 | delete activeMiners[miner_id]; 1146 | } 1147 | } 1148 | } 1149 | } 1150 | if (id === "Stratum") { 1151 | sendReply(null, "ok"); 1152 | miner.protocol = "grin"; 1153 | } else { 1154 | sendReply(null, { id: minerId, job: miner.getNewJob(), status: 'OK' }); 1155 | miner.protocol = "default"; 1156 | } 1157 | break; 1158 | } 1159 | 1160 | case 'getjobtemplate': { // grin only 1161 | const minerId = minerSocket.minerId ? minerSocket.minerId : ""; 1162 | let miner = activeMiners[minerId]; 1163 | if (!miner) { 1164 | sendReply('Unauthenticated'); 1165 | return; 1166 | } 1167 | miner.heartbeat(); 1168 | sendReply(null, miner.getNewJob()); 1169 | break; 1170 | } 1171 | 1172 | case 'getjob': { 1173 | if (!params) { 1174 | sendReplyFinal("No params specified"); 1175 | return; 1176 | } 1177 | let miner = activeMiners[params.id]; 1178 | if (!miner) { 1179 | sendReply('Unauthenticated'); 1180 | return; 1181 | } 1182 | miner.heartbeat(); 1183 | sendReply(null, miner.getNewJob()); 1184 | break; 1185 | } 1186 | 1187 | case 'submit': { // grin and default 1188 | if (!params) { 1189 | sendReplyFinal("No params specified"); 1190 | return; 1191 | } 1192 | const minerId = params.id ? params.id : (minerSocket.minerId ? minerSocket.minerId : ""); 1193 | let miner = activeMiners[minerId]; 1194 | if (!miner) { 1195 | sendReply('Unauthenticated'); 1196 | return; 1197 | } 1198 | miner.heartbeat(); 1199 | if (typeof (params.job_id) === 'number') params.job_id = params.job_id.toString(); // for grin miner 1200 | 1201 | let job = miner.validJobs.toarray().filter(function (job) { 1202 | return job.id === params.job_id; 1203 | })[0]; 1204 | 1205 | if (!job) { 1206 | sendReply('Invalid job id'); 1207 | return; 1208 | } 1209 | 1210 | const blob_type_num = job.blob_type; 1211 | const is_bad_nonce = miner.coinFuncs.blobTypeGrin(blob_type_num) ? 1212 | (typeof params.nonce !== 'number') || !(params.pow instanceof Array) || (params.pow.length != miner.coinFuncs.c29ProofSize(blob_type_num)) : 1213 | (typeof params.nonce !== 'string') || !(miner.coinFuncs.nonceSize(blob_type_num) == 8 ? nonceCheck64.test(params.nonce) : nonceCheck32.test(params.nonce) ); 1214 | 1215 | if (is_bad_nonce) { 1216 | console.warn(global.threadName + 'Malformed nonce: ' + JSON.stringify(params) + ' from ' + miner.logString); 1217 | sendReply('Duplicate share'); 1218 | return; 1219 | } 1220 | 1221 | if (job.submissions.indexOf(params.nonce) !== -1) { 1222 | console.warn(global.threadName + 'Duplicate share: ' + JSON.stringify(params) + ' from ' + miner.logString); 1223 | sendReply('Duplicate share'); 1224 | return; 1225 | } 1226 | 1227 | const nonce_test = miner.coinFuncs.blobTypeGrin(blob_type_num) ? params.pow.join(':') : params.nonce; 1228 | 1229 | job.submissions.push(nonce_test); 1230 | let activeBlockTemplate = activePools[miner.pool].activeBlocktemplate; 1231 | let pastBlockTemplates = activePools[miner.pool].pastBlockTemplates; 1232 | 1233 | let blockTemplate = activeBlockTemplate.id === job.templateID ? activeBlockTemplate : pastBlockTemplates.toarray().filter(function (t) { 1234 | return t.id === job.templateID; 1235 | })[0]; 1236 | 1237 | if (!blockTemplate) { 1238 | console.warn(global.threadName + 'Block expired, Height: ' + job.height + ' from ' + miner.logString); 1239 | if (miner.incremented === false){ 1240 | miner.newDiff = miner.difficulty + 1; 1241 | miner.incremented = true; 1242 | } else { 1243 | miner.newDiff = miner.difficulty - 1; 1244 | miner.incremented = false; 1245 | } 1246 | miner.pushNewJob(true); 1247 | sendReply('Block expired'); 1248 | return; 1249 | } 1250 | 1251 | let shareAccepted = miner.coinFuncs.processShare(miner, job, blockTemplate, params); 1252 | 1253 | if (!shareAccepted) { 1254 | sendReply('Low difficulty share'); 1255 | return; 1256 | } 1257 | 1258 | miner.lastShareTime = Date.now() / 1000 || 0; 1259 | 1260 | if (miner.protocol === "grin") { 1261 | sendReply(null, "ok"); 1262 | } else { 1263 | sendReply(null, {status: 'OK'}); 1264 | } 1265 | break; 1266 | } 1267 | 1268 | case 'keepalived': { 1269 | if (!params) { 1270 | sendReplyFinal("No params specified"); 1271 | return; 1272 | } 1273 | let miner = activeMiners[params.id]; 1274 | if (!miner) { 1275 | sendReply('Unauthenticated'); 1276 | return; 1277 | } 1278 | miner.heartbeat(); 1279 | sendReply(null, { status: 'KEEPALIVED' }); 1280 | break; 1281 | } 1282 | } 1283 | } 1284 | 1285 | function activateHTTP() { 1286 | var jsonServer = http.createServer((req, res) => { 1287 | if (global.config.httpUser && global.config.httpPass) { 1288 | var auth = req.headers['authorization']; // auth is in base64(username:password) so we need to decode the base64 1289 | if (!auth) { 1290 | res.statusCode = 401; 1291 | res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"'); 1292 | res.end('Unauthorized XNP access.'); 1293 | return; 1294 | } 1295 | debug.misc("Authorization Header is: ", auth); 1296 | var tmp = auth.split(' '); 1297 | var buf = Buffer.from(tmp[1], 'base64'); 1298 | var plain_auth = buf.toString(); 1299 | debug.misc("Decoded Authorization ", plain_auth); 1300 | var creds = plain_auth.split(':'); 1301 | var username = creds[0]; 1302 | var password = creds[1]; 1303 | if (username !== global.config.httpUser || password !== global.config.httpPass) { 1304 | res.statusCode = 401; 1305 | res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"'); 1306 | res.end('Wrong login.'); 1307 | return; 1308 | } 1309 | } 1310 | 1311 | if (req.url == "/") { 1312 | let totalWorkers = 0, totalHashrate = 0; 1313 | let poolHashrate = []; 1314 | let miners = {}; 1315 | let offline_miners = {}; 1316 | let miner_names = {}; 1317 | for (let workerID in activeWorkers) { 1318 | if (!activeWorkers.hasOwnProperty(workerID)) continue; 1319 | for (let minerID in activeWorkers[workerID]){ 1320 | if (!activeWorkers[workerID].hasOwnProperty(minerID)) continue; 1321 | let miner = activeWorkers[workerID][minerID]; 1322 | if (typeof(miner) === 'undefined' || !miner) continue; 1323 | if (miner.active) { 1324 | miners[miner.id] = miner; 1325 | const name = miner.logString; 1326 | miner_names[name] = 1; 1327 | ++ totalWorkers; 1328 | totalHashrate += miner.avgSpeed; 1329 | if (!poolHashrate[miner.pool]) poolHashrate[miner.pool] = 0; 1330 | poolHashrate[miner.pool] += miner.avgSpeed; 1331 | } else { 1332 | offline_miners[miner.id] = miner; 1333 | } 1334 | } 1335 | } 1336 | for (let offline_miner_id in offline_miners) { 1337 | const miner = offline_miners[offline_miner_id]; 1338 | const name = miner.logString; 1339 | if (name in miner_names) continue; 1340 | miners[miner.id] = miner; 1341 | miner_names[name] = 1; 1342 | } 1343 | let tablePool = ""; 1344 | let tableBody = ""; 1345 | for (let miner_id in miners) { 1346 | const miner = miners[miner_id]; 1347 | const name = miner.logString; 1348 | let avgSpeed = miner.active ? support.human_hashrate(miner.avgSpeed, activePools[miner.pool].activeBlocktemplate.algo) : "offline"; 1349 | let agent_parts = miner.agent.split(" "); 1350 | tableBody += ` 1351 | 1352 | ${name} 1353 | ${avgSpeed} 1354 | ${miner.diff} 1355 | ${miner.shares} 1356 | ${miner.hashes} 1357 | ${moment.unix(miner.lastShare).fromNow(true)} ago 1358 | ${moment.unix(miner.lastContact).fromNow(true)} ago 1359 | ${moment(miner.connectTime).fromNow(true)} ago 1360 | ${miner.pool} 1361 |
${agent_parts[0]}${miner.agent}
1362 | 1363 | `; 1364 | } 1365 | for (let poolName in poolHashrate) { 1366 | let poolPercentage = totalHashrate ? (100*poolHashrate[poolName]/totalHashrate).toFixed(2) : "100.0"; 1367 | let targetDiff = activePools[poolName].activeBlocktemplate ? activePools[poolName].activeBlocktemplate.targetDiff : "?"; 1368 | let walletId = activePools[poolName].username 1369 | const hs = support.human_hashrate(poolHashrate[poolName], activePools[poolName].activeBlocktemplate.algo); 1370 | if (poolName.includes("moneroocean")) { 1371 | let algo_variant = ""; 1372 | if (activePools[poolName].activeBlocktemplate.algo) algo_variant += "algo: " + activePools[poolName].activeBlocktemplate.algo; 1373 | if (activePools[poolName].activeBlocktemplate.variant) { 1374 | if (algo_variant != "") algo_variant += ", "; 1375 | algo_variant += "variant: " + activePools[poolName].activeBlocktemplate.variant; 1376 | } 1377 | if (algo_variant != "") algo_variant = " (" + algo_variant + ")"; 1378 | const hs = support.human_hashrate(poolHashrate[poolName], activePools[poolName].activeBlocktemplate.algo); 1379 | tablePool += `

${poolName}: ${hs} or ${poolPercentage}% (${targetDiff} diff) ${algo_variant}

`; 1380 | } else { 1381 | tablePool += `

${poolName}: ${hs} or ${poolPercentage}% (${targetDiff} diff)

`; 1382 | } 1383 | } 1384 | 1385 | //expect old config 1386 | if (!global.config.theme) global.config.theme = "light"; 1387 | if (!global.config.refreshTime) global.config.refreshTime = "60"; 1388 | 1389 | totalHashrate = support.human_hashrate(totalHashrate, hs_algo); 1390 | 1391 | // cleaner way for icon, stylesheet, scripts 1392 | let icon = `data:image/png;base64,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`; 1393 | let stylesheet = `body,html{font-family:'Saira Semi Condensed',sans-serif;font-size:14px;text-align:center}.light{color:#000;background-color:#fff}.dark{color:#ccc;background-color:#2a2a2a}.header{-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;o-user-select:none;cursor:hand}.sorted-table{margin:auto;width:95%;text-align:center}.sorted-table td,.sorted-table th{border-bottom:1px solid #d9d9d9}.hover{background-color:#eee;cursor:pointer}.tooltip{position:relative;display:inline-block;border-bottom:1px dotted #000}.tooltip .tooltiptext{visibility:hidden;width:140px;background-color:#000;color:#fff;text-align:center;padding:5px 0;border-radius:6px;position:absolute;z-index:1;bottom:125%;left:50%;margin-left:-70px;opacity:0;transition:opacity .3s}.tooltip .tooltiptext::after{content:"";position:absolute;top:100%;left:50%;margin-left:-5px;border-width:5px;border-style:solid;border-color:#000 transparent transparent}.tooltip:hover .tooltiptext{visibility:visible;opacity:1}a:link,a:visited,a:hover,a:active{color:inherit;text-decoration:none;text-shadow: 0px 0px 5px #40c4ff;}`; 1394 | let helpers = `$("table.sorted-table thead th").on("mouseover",function(){var t=$(this),e=t.index();t.addClass("hover"),$("table.sorted-table > tbody > tr > td").filter(":nth-child("+(e+1)+")").addClass("hover")}).on("mouseout",function(){$("td, th").removeClass("hover")});var thIndex=0,thInc=1,curThIndex=null;function sortIt(){for(var t=0;t tbody > tr").eq(rowId)[0].outerHTML;$("table.sorted-table > tbody").html(tbodyHtml)}function theme(t){var e=t.getAttribute("class");t.className="light"==e?"dark":"light"}$(function(){$("table.sorted-table thead th").click(function(){thIndex=$(this).index(),sorting=[],tbodyHtml=null,$("table.sorted-table > tbody > tr").each(function(){var t,e=$(this).children("td").eq(thIndex).html();if(t=/^<.+>(\\d+)<\\/.+>$/.exec(e)){var n="000000000000";e=(n+Number(t[1])).slice(-n.length)}sorting.push(e+", "+$(this).index())}),sorting=thIndex!=curThIndex||1==thInc?sorting.sort():sorting.sort(function(t,e){return e.localeCompare(t)}),thInc=thIndex==curThIndex?1-thInc:0,curThIndex=thIndex,sortIt()})});`; 1395 | 1396 | res.writeHead(200, {'Content-type':'text/html'}); 1397 | res.write(` 1398 | 1399 | XNP v${PROXY_VERSION} Hashrate Monitor 1400 | 1401 | 1402 | 1403 | 1404 | 1407 | 1408 |

XNP v${PROXY_VERSION} Hashrate Monitor

1409 |
1410 |

Workers: ${totalWorkers}, Hashrate: ${totalHashrate}

1411 | ${tablePool} 1412 | 1413 | 1414 | 1415 | 1416 | 1417 | 1418 | 1419 | 1420 | 1421 | 1422 | 1423 | 1424 | 1425 | 1426 | ${tableBody} 1427 | 1428 |
NameHashrateDifficultySharesHashesShare RecvdPing RecvdConnectedPoolAgent
1429 |
1430 | 1431 | 1434 | 1441 | 1442 | `); 1443 | res.end(); 1444 | } else if(req.url.substring(0, 5) == "/json") { 1445 | res.writeHead(200, {'Content-type':'application/json'}); 1446 | res.write(JSON.stringify(activeWorkers) + "\r\n"); 1447 | res.end(); 1448 | } else { 1449 | res.writeHead(404); 1450 | res.end(); 1451 | } 1452 | }); 1453 | 1454 | jsonServer.listen(global.config.httpPort || "8081", global.config.httpAddress || "localhost") 1455 | } 1456 | 1457 | function activatePorts() { 1458 | /* 1459 | Reads the current open ports, and then activates any that aren't active yet 1460 | { "port": 80, "ssl": false, "diff": 5000 } 1461 | and binds a listener to it. 1462 | */ 1463 | async.each(global.config.listeningPorts, function (portData) { 1464 | if (activePorts.indexOf(portData.port) !== -1) { 1465 | return; 1466 | } 1467 | let handleMessage = function (socket, jsonData, pushMessage) { 1468 | if (!jsonData.id) { 1469 | console.warn(global.threadName + 'Miner RPC request missing RPC id'); 1470 | return; 1471 | } 1472 | else if (!jsonData.method) { 1473 | console.warn(global.threadName + 'Miner RPC request missing RPC method'); 1474 | return; 1475 | } 1476 | 1477 | let sendReply = function (error, result) { 1478 | if (!socket.writable) return; 1479 | let reply = { 1480 | jsonrpc: "2.0", 1481 | id: jsonData.id, 1482 | error: error ? {code: -1, message: error} : null, 1483 | result: result 1484 | }; 1485 | if (jsonData.id === "Stratum") reply.method = jsonData.method; 1486 | socket.write(JSON.stringify(reply) + "\n"); 1487 | }; 1488 | let sendReplyFinal = function (error) { 1489 | setTimeout(function() { 1490 | if (!socket.writable) return; 1491 | let reply = { 1492 | jsonrpc: "2.0", 1493 | id: jsonData.id, 1494 | error: {code: -1, message: error}, 1495 | result: null 1496 | }; 1497 | if (jsonData.id === "Stratum") reply.method = jsonData.method; 1498 | socket.end(JSON.stringify(reply) + "\n"); 1499 | }, 9 * 1000); 1500 | }; 1501 | 1502 | handleMinerData(socket, jsonData.id, jsonData.method, jsonData.params, socket.remoteAddress, portData, sendReply, sendReplyFinal, pushMessage); 1503 | }; 1504 | 1505 | function socketConn(socket) { 1506 | socket.setKeepAlive(true); 1507 | socket.setEncoding('utf8'); 1508 | 1509 | let dataBuffer = ''; 1510 | 1511 | let pushMessage = function (body) { 1512 | if (!socket.writable) return; 1513 | body.jsonrpc = "2.0"; 1514 | debug.miners(`Data sent to miner (pushMessage): ` + JSON.stringify(body)); 1515 | socket.write(JSON.stringify(body) + "\n"); 1516 | }; 1517 | 1518 | socket.on('data', function (d) { 1519 | dataBuffer += d; 1520 | if (Buffer.byteLength(dataBuffer, 'utf8') > 102400) { //10KB 1521 | dataBuffer = null; 1522 | console.warn(global.threadName + 'Excessive packet size from: ' + socket.remoteAddress); 1523 | socket.destroy(); 1524 | return; 1525 | } 1526 | if (dataBuffer.indexOf('\n') !== -1) { 1527 | let messages = dataBuffer.split('\n'); 1528 | let incomplete = dataBuffer.slice(-1) === '\n' ? '' : messages.pop(); 1529 | for (let i = 0; i < messages.length; i++) { 1530 | let message = messages[i]; 1531 | if (message.trim() === '') { 1532 | continue; 1533 | } 1534 | let jsonData; 1535 | debug.miners(`Data from miner: ${message}`); 1536 | try { 1537 | jsonData = JSON.parse(message); 1538 | } 1539 | catch (e) { 1540 | if (message.indexOf('GET /') === 0) { 1541 | if (message.indexOf('HTTP/1.1') !== -1) { 1542 | socket.end('HTTP/1.1' + httpResponse); 1543 | break; 1544 | } 1545 | else if (message.indexOf('HTTP/1.0') !== -1) { 1546 | socket.end('HTTP/1.0' + httpResponse); 1547 | break; 1548 | } 1549 | } 1550 | console.warn(global.threadName + "Malformed message from " + socket.remoteAddress + " Message: " + message); 1551 | socket.destroy(); 1552 | break; 1553 | } 1554 | handleMessage(socket, jsonData, pushMessage); 1555 | } 1556 | dataBuffer = incomplete; 1557 | } 1558 | }).on('error', function (err) { 1559 | if (err.code !== 'ECONNRESET') { 1560 | console.warn(global.threadName + "Miner socket error from " + socket.remoteAddress + ": " + err); 1561 | } 1562 | socket.end(); 1563 | socket.destroy(); 1564 | }).on('close', function () { 1565 | pushMessage = function () {}; 1566 | debug.miners('Miner disconnected via standard close'); 1567 | socket.end(); 1568 | socket.destroy(); 1569 | }); 1570 | } 1571 | 1572 | if ('ssl' in portData && portData.ssl === true) { 1573 | let server = tls.createServer({ 1574 | key: fs.readFileSync('cert.key'), 1575 | cert: fs.readFileSync('cert.pem') 1576 | }, socketConn); 1577 | server.listen(portData.port, global.config.bindAddress, function (error) { 1578 | if (error) { 1579 | console.error(global.threadName + "Unable to start server on: " + portData.port + " Message: " + error); 1580 | return; 1581 | } 1582 | activePorts.push(portData.port); 1583 | console.log(global.threadName + "Started server on port: " + portData.port); 1584 | }); 1585 | server.on('error', function (error) { 1586 | console.error(global.threadName + "Can't bind server to " + portData.port + " SSL port!"); 1587 | }); 1588 | } else { 1589 | let server = net.createServer(socketConn); 1590 | server.listen(portData.port, global.config.bindAddress, function (error) { 1591 | if (error) { 1592 | console.error(global.threadName + "Unable to start server on: " + portData.port + " Message: " + error); 1593 | return; 1594 | } 1595 | activePorts.push(portData.port); 1596 | console.log(global.threadName + "Started server on port: " + portData.port); 1597 | }); 1598 | server.on('error', function (error) { 1599 | console.error(global.threadName + "Can't bind server to " + portData.port + " port!"); 1600 | }); 1601 | } 1602 | }); 1603 | } 1604 | 1605 | function checkActivePools() { 1606 | for (let badPool in activePools){ 1607 | if (activePools.hasOwnProperty(badPool) && !activePools[badPool].active) { 1608 | for (let pool in activePools) { 1609 | if (activePools.hasOwnProperty(pool) && !activePools[pool].devPool && activePools[pool].coin === activePools[badPool].coin && activePools[pool].active) { 1610 | for (let miner in activeMiners) { 1611 | if (activeMiners.hasOwnProperty(miner)) { 1612 | let realMiner = activeMiners[miner]; 1613 | if (realMiner.pool === badPool) { 1614 | realMiner.pool = pool; 1615 | realMiner.pushNewJob(); 1616 | } 1617 | } 1618 | } 1619 | break; 1620 | } 1621 | } 1622 | } 1623 | } 1624 | } 1625 | 1626 | // API Calls 1627 | 1628 | // System Init 1629 | 1630 | if (cluster.isMaster) { 1631 | console.log("Xmr-Node-Proxy (XNP) v" + PROXY_VERSION); 1632 | let numWorkers; 1633 | try { 1634 | let argv = require('minimist')(process.argv.slice(2)); 1635 | if (typeof argv.workers !== 'undefined') { 1636 | numWorkers = Number(argv.workers); 1637 | } else { 1638 | numWorkers = require('os').cpus().length; 1639 | } 1640 | } catch (err) { 1641 | console.error(`${global.threadName}Unable to set the number of workers via arguments. Make sure to run npm install!`); 1642 | numWorkers = require('os').cpus().length; 1643 | } 1644 | global.threadName = '[MASTER] '; 1645 | console.log('Cluster master setting up ' + numWorkers + ' workers...'); 1646 | cluster.on('message', masterMessageHandler); 1647 | for (let i = 0; i < numWorkers; i++) { 1648 | let worker = cluster.fork(); 1649 | worker.on('message', slaveMessageHandler); 1650 | } 1651 | 1652 | cluster.on('online', function (worker) { 1653 | console.log('Worker ' + worker.process.pid + ' is online'); 1654 | activeWorkers[worker.id] = {}; 1655 | }); 1656 | 1657 | cluster.on('exit', function (worker, code, signal) { 1658 | console.error('Worker ' + worker.process.pid + ' died with code: ' + code + ', and signal: ' + signal); 1659 | console.log('Starting a new worker'); 1660 | worker = cluster.fork(); 1661 | worker.on('message', slaveMessageHandler); 1662 | }); 1663 | connectPools(); 1664 | setInterval(enumerateWorkerStats, 15*1000); 1665 | setInterval(balanceWorkers, 90*1000); 1666 | if (global.config.httpEnable) { 1667 | console.log("Activating Web API server on " + (global.config.httpAddress || "localhost") + ":" + (global.config.httpPort || "8081")); 1668 | activateHTTP(); 1669 | } 1670 | } else { 1671 | /* 1672 | setInterval(checkAliveMiners, 30000); 1673 | setInterval(retargetMiners, global.config.pool.retargetTime * 1000); 1674 | */ 1675 | process.on('message', slaveMessageHandler); 1676 | global.config.pools.forEach(function(poolData){ 1677 | if (!poolData.coin) poolData.coin = "xmr"; 1678 | activePools[poolData.hostname] = new Pool(poolData); 1679 | if (poolData.default){ 1680 | defaultPools[poolData.coin] = poolData.hostname; 1681 | } 1682 | if (!activePools.hasOwnProperty(activePools[poolData.hostname].coinFuncs.devPool.hostname)){ 1683 | activePools[activePools[poolData.hostname].coinFuncs.devPool.hostname] = new Pool(activePools[poolData.hostname].coinFuncs.devPool); 1684 | } 1685 | }); 1686 | process.send({type: 'needPoolState'}); 1687 | setInterval(function(){ 1688 | for (let minerID in activeMiners){ 1689 | if (activeMiners.hasOwnProperty(minerID)){ 1690 | activeMiners[minerID].updateDifficulty(); 1691 | } 1692 | } 1693 | }, 45000); 1694 | setInterval(function(){ 1695 | for (let minerID in activeMiners){ 1696 | if (activeMiners.hasOwnProperty(minerID)){ 1697 | process.send({minerID: minerID, data: activeMiners[minerID].minerStats(), type: 'workerStats'}); 1698 | } 1699 | } 1700 | }, 10000); 1701 | setInterval(checkActivePools, 90000); 1702 | activatePorts(); 1703 | } 1704 | -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | git checkout . &&\ 3 | git pull --no-edit &&\ 4 | npm install &&\ 5 | echo "Proxy updated OK! Please go ahead and restart with the correct pm2 command" 6 | echo "This is usually pm2 restart proxy, however, you can use pm2 list to check for your exact proxy command" 7 | --------------------------------------------------------------------------------