├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib ├── algoProperties.js ├── blockMake.js ├── blockTemplate.js ├── daemon.js ├── gwMake.js ├── index.js ├── jobManager.js ├── merkleTree.js ├── moniterConnection.js ├── peer.js ├── pool.js ├── stratum.js ├── transactions.js ├── util.js └── varDiff.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | test/ 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UlordChain/node-merged-pool/0f2cb6af43a7a31e2d60feaeee1456dbc3752575/.npmignore -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | deploy: 5 | provider: npm 6 | email: jackchen1615@gmail.com 7 | api_key: 8 | secure: D9lpUDAx1OudPBji3mapnAhOug3wcEBqFrNgWaFh5XiYesa/f/X0gMOJggLlvzyhLHKs8VdTHShdu3XzlC3EDwr5wCdgYO1JSOyDo93FG7Y/qhPDVFnzdtsKmr813Qtj2UDKIh2ZP+JnjKaITrvUwRmdi/8+B9Enr5o9ulFb/a0= -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ulord 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ulord's merge mining pool 2 | High performance Stratum poolserver in Node.js. One instance of this software can startup and manage multiple coin 3 | pools, each with their own daemon and stratum port :) 4 | 5 | #### Notice 6 | This is a module for Node.js that will do nothing on its own. Unless you're a Node.js developer who would like to 7 | handle stratum authentication and raw share data then this module will not be of use to you. For a full featured portal 8 | that uses this module, see [the merge mining pool for Ulord and USC )](https://github.com/UlordChain/ulord-mm-pool). It 9 | handles payments, website front-end, database layer, mutli-coin/pool support, auto-switching miners between coins/pools, 10 | etc.. The portal also has an [MPOS](https://github.com/MPOS/php-mpos) compatibility mode so that the it can function as 11 | a drop-in-replacement for [python-stratum-mining](https://github.com/Crypto-Expert/stratum-mining). 12 | 13 | 14 | #### Why 15 | This server was built to be more efficient and easier to setup, maintain and scale than existing stratum poolservers 16 | which are written in python. Compared to the spaghetti state of the latest 17 | [stratum-mining python server](https://github.com/Crypto-Expert/stratum-mining/), this software should also have a 18 | lower barrier to entry for other developers to fork and add features or fix bugs. 19 | 20 | 21 | Features 22 | ---------------------------------- 23 | * Merged Mining Support 24 | * Daemon RPC interface 25 | * Stratum TCP socket server 26 | * Block template / job manager 27 | * P2P to get block notifications as peer node 28 | * Optimized generation transaction building 29 | * Connecting to multiple daemons for redundancy 30 | * Process share submissions 31 | * Session managing for purging DDoS/flood initiated zombie workers 32 | * Auto ban IPs that are flooding with invalid shares 33 | * __POW__ (proof-of-work) & __POS__ (proof-of-stake) support 34 | * Transaction messages support 35 | * Vardiff (variable difficulty / share limiter) 36 | * When started with a coin deamon that hasn't finished syncing to the network it shows the blockchain download progress and initializes once synced 37 | 38 | #### Hashing algorithms supported: 39 | * ✓ __CryptoHello__ (Ulord Token[UT]) 40 | * ✓ __SHA256__ (Bitcoin, Freicoin, Peercoin/PPCoin, Terracoin, etc..) 41 | * ✓ __Scrypt__ (Litecoin, Dogecoin, Feathercoin, etc..) 42 | * ✓ __Scrypt-Jane__ (YaCoin, CopperBars, Pennies, Tickets, etc..) 43 | * ✓ __Scrypt-N__ (Vertcoin [VTC]) 44 | * ✓ __Quark__ (Quarkcoin [QRK]) 45 | * ✓ __X13__ (MaruCoin, BoostCoin) 46 | * ✓ __NIST5__ (Talkcoin) 47 | * ✓ __Keccak__ (Maxcoin [MAX], HelixCoin, CryptoMeth, Galleon, 365coin, Slothcoin, BitcointalkCoin) 48 | * ✓ __Skein__ (Skeincoin [SKC]) 49 | * ✓ __Groestl__ (Groestlcoin [GRS]) 50 | 51 | May be working (needs additional testing): 52 | * ? *Blake* (Blakecoin [BLC]) 53 | * ? *Fugue* (Fuguecoin [FC]) 54 | * ? *Qubit* (Qubitcoin [Q2C], Myriadcoin [MYR]) 55 | * ? *SHAvite-3* (INKcoin [INK]) 56 | * ? *Sha1* (Sha1coin [SHA], Yaycoin [YAY]) 57 | 58 | Not working currently: 59 | * *Groestl* - for Myriadcoin 60 | * *Keccak* - for eCoin & Copperlark 61 | * *Hefty1* (Heavycoin [HVC]) 62 | 63 | 64 | Requirements 65 | ------------ 66 | * node v0.10+ 67 | * coin daemon (preferably one with a relatively updated API and not some crapcoin :p) 68 | 69 | 70 | Example Usage 71 | ------------- 72 | 73 | #### Install as a node module by cloning repository 74 | 75 | ```bash 76 | git clone https://github.com/UlordChain/node-merged-pool node_modules/stratum-pool 77 | npm update 78 | ``` 79 | 80 | #### Module usage 81 | 82 | Create the configuration for your coin: 83 | 84 | Possible options for `algorithm`: *sha256, scrypt, scrypt-jane, scrypt-n, quark, keccak, blake, 85 | skein, groestl, fugue, shavite3, hefty1, qubit, or sha1*. 86 | 87 | ```javascript 88 | var myCoin = { 89 | "name": "Ulord", 90 | "symbol": "UT", 91 | "algorithm": "cryptohello", 92 | "nValue": 1024, //optional - defaults to 1024 93 | "rValue": 1, //optional - defaults to 1 94 | "txMessages": false, //optional - defaults to false, 95 | 96 | /* Magic value only required for setting up p2p block notifications. It is found in the daemon 97 | source code as the pchMessageStart variable. 98 | For example, litecoin mainnet magic: http://git.io/Bi8YFw 99 | And for litecoin testnet magic: http://git.io/NXBYJA */ 100 | "peerMagic": "fbc0b6db" //optional 101 | "peerMagicTestnet": "fcc1b7dc" //optional 102 | }; 103 | ``` 104 | 105 | If you are using the `scrypt-jane` algorithm there are additional configurations: 106 | 107 | ```javascript 108 | var myCoin = { 109 | "name": "Freecoin", 110 | "symbol": "FEC", 111 | "algorithm": "scrypt-jane", 112 | "chainStartTime": 1375801200, //defaults to 1367991200 (YACoin) if not used 113 | "nMin": 6, //defaults to 4 if not used 114 | "nMax": 32 //defaults to 30 if not used 115 | }; 116 | ``` 117 | 118 | If you are using the `scrypt-n` algorithm there is an additional configuration: 119 | ```javascript 120 | var myCoin = { 121 | "name": "Execoin", 122 | "symbol": "EXE", 123 | "algorithm": "scrypt-n", 124 | /* This defaults to Vertcoin's timetable if not used. It is required for scrypt-n coins that 125 | have modified their N-factor timetable to be different than Vertcoin's. */ 126 | "timeTable": { 127 | "2048": 1390959880, 128 | "4096": 1438295269, 129 | "8192": 1485630658, 130 | "16384": 1532966047, 131 | "32768": 1580301436, 132 | "65536": 1627636825, 133 | "131072": 1674972214, 134 | "262144": 1722307603 135 | } 136 | }; 137 | ``` 138 | 139 | If you are using the `keccak` algorithm there are additional configurations *(The rare `normalHashing` keccak coins 140 | such as Copperlark and eCoin don't appear to work yet - only the popular ones like Maxcoin are)*: 141 | ```javascript 142 | var myCoin = { 143 | "name": "eCoin", 144 | "symbol": "ECN", 145 | "algorithm": "keccak", 146 | 147 | /* This is not required and set to false by default. Some coins such as Copperlark and eCoin 148 | require it to be set to true. Maxcoin and most others are false. */ 149 | "normalHashing": true 150 | }; 151 | ``` 152 | 153 | 154 | Create and start new pool with configuration options and authentication function 155 | 156 | ```javascript 157 | var Stratum = require('stratum-pool'); 158 | 159 | var pool = Stratum.createPool({ 160 | 161 | "coin": myCoin, 162 | 163 | "address": "mi4iBXbBsydtcc5yFmsff2zCFVX4XG7qJc", //Address to where block rewards are given 164 | 165 | /* Block rewards go to the configured pool wallet address to later be paid out to miners, 166 | except for a percentage that can go to, for examples, pool operator(s) as pool fees or 167 | or to donations address. Addresses or hashed public keys can be used. Here is an example 168 | of rewards going to the main pool op, a pool co-owner, and NOMP donation. */ 169 | "rewardRecipients": { 170 | "n37vuNFkXfk15uFnGoVyHZ6PYQxppD3QqK": 1.5, //1.5% goes to pool op 171 | "mirj3LtZxbSTharhtXvotqtJXUY7ki5qfx": 0.5, //0.5% goes to a pool co-owner 172 | 173 | /* 0.1% donation to NOMP. This pubkey can accept any type of coin, please leave this in 174 | your config to help support NOMP development. */ 175 | "22851477d63a085dbc2398c8430af1c09e7343f6": 0.1 176 | }, 177 | 178 | "blockRefreshInterval": 1000, //How often to poll RPC daemons for new blocks, in milliseconds 179 | 180 | 181 | /* Some miner apps will consider the pool dead/offline if it doesn't receive anything new jobs 182 | for around a minute, so every time we broadcast jobs, set a timeout to rebroadcast 183 | in this many seconds unless we find a new job. Set to zero or remove to disable this. */ 184 | "jobRebroadcastTimeout": 55, 185 | 186 | //instanceId: 37, //Recommend not using this because a crypto-random one will be generated 187 | 188 | /* Some attackers will create thousands of workers that use up all available socket connections, 189 | usually the workers are zombies and don't submit shares after connecting. This features 190 | detects those and disconnects them. */ 191 | "connectionTimeout": 600, //Remove workers that haven't been in contact for this many seconds 192 | 193 | /* Sometimes you want the block hashes even for shares that aren't block candidates. */ 194 | "emitInvalidBlockHashes": false, 195 | 196 | /* Enable for client IP addresses to be detected when using a load balancer with TCP proxy 197 | protocol enabled, such as HAProxy with 'send-proxy' param: 198 | http://haproxy.1wt.eu/download/1.5/doc/configuration.txt */ 199 | "tcpProxyProtocol": false, 200 | 201 | /* If a worker is submitting a high threshold of invalid shares we can temporarily ban their IP 202 | to reduce system/network load. Also useful to fight against flooding attacks. If running 203 | behind something like HAProxy be sure to enable 'tcpProxyProtocol', otherwise you'll end up 204 | banning your own IP address (and therefore all workers). */ 205 | "banning": { 206 | "enabled": true, 207 | "time": 600, //How many seconds to ban worker for 208 | "invalidPercent": 50, //What percent of invalid shares triggers ban 209 | "checkThreshold": 500, //Check invalid percent when this many shares have been submitted 210 | "purgeInterval": 300 //Every this many seconds clear out the list of old bans 211 | }, 212 | 213 | /* Each pool can have as many ports for your miners to connect to as you wish. Each port can 214 | be configured to use its own pool difficulty and variable difficulty settings. varDiff is 215 | optional and will only be used for the ports you configure it for. */ 216 | "ports": { 217 | "3032": { //A port for your miners to connect to 218 | "diff": 32, //the pool difficulty for this port 219 | 220 | /* Variable difficulty is a feature that will automatically adjust difficulty for 221 | individual miners based on their hashrate in order to lower networking overhead */ 222 | "varDiff": { 223 | "minDiff": 8, //Minimum difficulty 224 | "maxDiff": 512, //Network difficulty will be used if it is lower than this 225 | "targetTime": 15, //Try to get 1 share per this many seconds 226 | "retargetTime": 90, //Check to see if we should retarget every this many seconds 227 | "variancePercent": 30 //Allow time to very this % from target without retargeting 228 | } 229 | }, 230 | "3256": { //Another port for your miners to connect to, this port does not use varDiff 231 | "diff": 256 //The pool difficulty 232 | } 233 | }, 234 | 235 | /* Recommended to have at least two daemon instances running in case one drops out-of-sync 236 | or offline. For redundancy, all instances will be polled for block/transaction updates 237 | and be used for submitting blocks. Creating a backup daemon involves spawning a daemon 238 | using the "-datadir=/backup" argument which creates a new daemon instance with it's own 239 | RPC config. For more info on this see: 240 | - https://en.bitcoin.it/wiki/Data_directory 241 | - https://en.bitcoin.it/wiki/Running_bitcoind */ 242 | "daemons": [ 243 | { //Main daemon instance 244 | "host": "127.0.0.1", 245 | "port": 19332, 246 | "user": "litecoinrpc", 247 | "password": "testnet" 248 | }, 249 | { //Backup daemon instance 250 | "host": "127.0.0.1", 251 | "port": 19344, 252 | "user": "litecoinrpc", 253 | "password": "testnet" 254 | } 255 | ], 256 | 257 | 258 | /* This allows the pool to connect to the daemon as a node peer to receive block updates. 259 | It may be the most efficient way to get block updates (faster than polling, less 260 | intensive than blocknotify script). It requires the additional field "peerMagic" in 261 | the coin config. */ 262 | "p2p": { 263 | "enabled": false, 264 | 265 | /* Host for daemon */ 266 | "host": "127.0.0.1", 267 | 268 | /* Port configured for daemon (this is the actual peer port not RPC port) */ 269 | "port": 19333, 270 | 271 | /* If your coin daemon is new enough (i.e. not a shitcoin) then it will support a p2p 272 | feature that prevents the daemon from spamming our peer node with unnecessary 273 | transaction data. Assume its supported but if you have problems try disabling it. */ 274 | "disableTransactions": true 275 | 276 | } 277 | 278 | }, function(ip, port , workerName, password, callback){ //stratum authorization function 279 | console.log("Authorize " + workerName + ":" + password + "@" + ip); 280 | callback({ 281 | error: null, 282 | authorized: true, 283 | disconnect: false 284 | }); 285 | }); 286 | ``` 287 | 288 | 289 | Listen to pool events 290 | ```javascript 291 | 292 | pool.on('share', function(isValidShare, isValidBlock, data){ 293 | 294 | if (isValidBlock) 295 | console.log('Block found'); 296 | else if (isValidShare) 297 | console.log('Valid share submitted'); 298 | else if (data.blockHash) 299 | console.log('We thought a block was found but it was rejected by the daemon'); 300 | else 301 | console.log('Invalid share submitted') 302 | 303 | console.log('share data: ' + JSON.stringify(data)); 304 | }); 305 | 306 | 307 | 308 | pool.on('log', function(severity, logKey, logText){ 309 | console.log(severity + ': ' + '[' + logKey + '] ' + logText); 310 | }); 311 | ``` 312 | 313 | Start pool 314 | ```javascript 315 | pool.start(); 316 | ``` 317 | 318 | Donations 319 | ------- 320 | To support development of this project feel free to donate :) 321 | * UT: UbZnu7QCHcate5RMmequjt1W4zuVT6KDSa 322 | 323 | PS: The donated UT flows to Ulord community foundation and is supervised by the community. 324 | 325 | 326 | License 327 | ------- 328 | Released under the GNU General Public License v2 329 | 330 | http://www.gnu.org/licenses/gpl-2.0.html 331 | -------------------------------------------------------------------------------- /lib/algoProperties.js: -------------------------------------------------------------------------------- 1 | var bignum = require('bignum'); 2 | var multiHashing = require('node-multi-hashing'); 3 | var cryptoHello = multiHashing['cryptohello']; 4 | var util = require('./util.js'); 5 | 6 | var diff1 = global.diff1 = 0x00000000ffff0000000000000000000000000000000000000000000000000000; 7 | 8 | var algos = module.exports = global.algos = { 9 | sha256: { 10 | //Uncomment diff if you want to use hardcoded truncated diff 11 | //diff: '00000000ffff0000000000000000000000000000000000000000000000000000', 12 | hash: function(){ 13 | return function(){ 14 | console.log('sha256'); 15 | return util.sha256d.apply(this, arguments); 16 | } 17 | } 18 | }, 19 | cryptohello: { 20 | //diff: '0000ffff00000000000000000000000000000000000000000000000000000000', 21 | multiplier: Math.pow(2, 16), 22 | hash: function(){ 23 | return function(data){ 24 | return multiHashing.cryptohello(data, false); 25 | } 26 | } 27 | }, 28 | 'scrypt': { 29 | //Uncomment diff if you want to use hardcoded truncated diff 30 | //diff: '0000ffff00000000000000000000000000000000000000000000000000000000', 31 | multiplier: Math.pow(2, 16), 32 | hash: function(coinConfig){ 33 | var nValue = coinConfig.nValue || 1024; 34 | var rValue = coinConfig.rValue || 1; 35 | return function(data){ 36 | return multiHashing.scrypt(data,nValue,rValue); 37 | } 38 | } 39 | }, 40 | 'scrypt-og': { 41 | //Aiden settings 42 | //Uncomment diff if you want to use hardcoded truncated diff 43 | //diff: '0000ffff00000000000000000000000000000000000000000000000000000000', 44 | multiplier: Math.pow(2, 16), 45 | hash: function(coinConfig){ 46 | var nValue = coinConfig.nValue || 64; 47 | var rValue = coinConfig.rValue || 1; 48 | return function(data){ 49 | return multiHashing.scrypt(data,nValue,rValue); 50 | } 51 | } 52 | }, 53 | 'scrypt-jane': { 54 | multiplier: Math.pow(2, 16), 55 | hash: function(coinConfig){ 56 | var nTimestamp = coinConfig.chainStartTime || 1367991200; 57 | var nMin = coinConfig.nMin || 4; 58 | var nMax = coinConfig.nMax || 30; 59 | return function(data, nTime){ 60 | return multiHashing.scryptjane(data, nTime, nTimestamp, nMin, nMax); 61 | } 62 | } 63 | }, 64 | 'scrypt-n': { 65 | multiplier: Math.pow(2, 16), 66 | hash: function(coinConfig){ 67 | 68 | var timeTable = coinConfig.timeTable || { 69 | "2048": 1389306217, "4096": 1456415081, "8192": 1506746729, "16384": 1557078377, "32768": 1657741673, 70 | "65536": 1859068265, "131072": 2060394857, "262144": 1722307603, "524288": 1769642992 71 | }; 72 | 73 | var nFactor = (function(){ 74 | var n = Object.keys(timeTable).sort().reverse().filter(function(nKey){ 75 | return Date.now() / 1000 > timeTable[nKey]; 76 | })[0]; 77 | 78 | var nInt = parseInt(n); 79 | return Math.log(nInt) / Math.log(2); 80 | })(); 81 | 82 | return function(data) { 83 | return multiHashing.scryptn(data, nFactor); 84 | } 85 | } 86 | }, 87 | sha1: { 88 | hash: function(){ 89 | return function(){ 90 | return multiHashing.sha1.apply(this, arguments); 91 | } 92 | } 93 | }, 94 | x13: { 95 | hash: function(){ 96 | return function(){ 97 | return multiHashing.x13.apply(this, arguments); 98 | } 99 | } 100 | }, 101 | x15: { 102 | hash: function(){ 103 | return function(){ 104 | return multiHashing.x15.apply(this, arguments); 105 | } 106 | } 107 | }, 108 | nist5: { 109 | hash: function(){ 110 | return function(){ 111 | return multiHashing.nist5.apply(this, arguments); 112 | } 113 | } 114 | }, 115 | quark: { 116 | hash: function(){ 117 | return function(){ 118 | return multiHashing.quark.apply(this, arguments); 119 | } 120 | } 121 | }, 122 | keccak: { 123 | multiplier: Math.pow(2, 8), 124 | hash: function(coinConfig){ 125 | if (coinConfig.normalHashing === true) { 126 | return function (data, nTimeInt) { 127 | return multiHashing.keccak(multiHashing.keccak(Buffer.concat([data, new Buffer(nTimeInt.toString(16), 'hex')]))); 128 | }; 129 | } 130 | else { 131 | return function () { 132 | return multiHashing.keccak.apply(this, arguments); 133 | } 134 | } 135 | } 136 | }, 137 | blake: { 138 | multiplier: Math.pow(2, 8), 139 | hash: function(){ 140 | return function(){ 141 | return multiHashing.blake.apply(this, arguments); 142 | } 143 | } 144 | }, 145 | skein: { 146 | hash: function(){ 147 | return function(){ 148 | return multiHashing.skein.apply(this, arguments); 149 | } 150 | } 151 | }, 152 | groestl: { 153 | multiplier: Math.pow(2, 8), 154 | hash: function(){ 155 | return function(){ 156 | return multiHashing.groestl.apply(this, arguments); 157 | } 158 | } 159 | }, 160 | fugue: { 161 | multiplier: Math.pow(2, 8), 162 | hash: function(){ 163 | return function(){ 164 | return multiHashing.fugue.apply(this, arguments); 165 | } 166 | } 167 | }, 168 | shavite3: { 169 | hash: function(){ 170 | return function(){ 171 | return multiHashing.shavite3.apply(this, arguments); 172 | } 173 | } 174 | }, 175 | hefty1: { 176 | hash: function(){ 177 | return function(){ 178 | return multiHashing.hefty1.apply(this, arguments); 179 | } 180 | } 181 | }, 182 | qubit: { 183 | hash: function(){ 184 | return function(){ 185 | return multiHashing.qubit.apply(this, arguments); 186 | } 187 | } 188 | } 189 | }; 190 | 191 | 192 | for (var algo in algos){ 193 | if (!algos[algo].multiplier) 194 | algos[algo].multiplier = 1; 195 | 196 | /*if (algos[algo].diff){ 197 | algos[algo].maxDiff = bignum(algos[algo].diff, 16); 198 | } 199 | else if (algos[algo].shift){ 200 | algos[algo].nonTruncatedDiff = util.shiftMax256Right(algos[algo].shift); 201 | algos[algo].bits = util.bufferToCompactBits(algos[algo].nonTruncatedDiff); 202 | algos[algo].maxDiff = bignum.fromBuffer(util.convertBitsToBuff(algos[algo].bits)); 203 | } 204 | else if (algos[algo].multiplier){ 205 | algos[algo].maxDiff = diff1.mul(Math.pow(2, 32) / algos[algo].multiplier); 206 | } 207 | else{ 208 | algos[algo].maxDiff = diff1; 209 | }*/ 210 | } 211 | -------------------------------------------------------------------------------- /lib/blockMake.js: -------------------------------------------------------------------------------- 1 | var usc_rpc = require('node-bitcoin-rpc'); 2 | 3 | var blockMake = module.exports = function(uscOptions){ 4 | usc_rpc.init(uscOptions.rpcIp, uscOptions.rpcPort, '', ''); 5 | 6 | this.makeSubmitMsg = function(params, callback){ 7 | /* 8 | * @params: [params.blockHashHex, params.blockHeaderHex, params.coinbaseHex, params.merkleHashesHex, params.blockTxnCountHex] 9 | */ 10 | 11 | usc_rpc.call('mnr_submitUlordBlockPartialMerkle', params, function (err, res) { 12 | var uscBlock = res; 13 | if (res === undefined && err !== null || res === null){ 14 | return; 15 | } else if (res.result){ 16 | var rawbm = {}; 17 | rawbm["created_at_ts"] = (new Date().getTime()/1000); 18 | rawbm["blockImportedResult"] = uscBlock.result.blockImportedResult; 19 | rawbm["blockHash"] = uscBlock.result.blockHash; 20 | rawbm["blockIncludedHeight"] = uscBlock.result.blockIncludedHeight; 21 | callback(err, rawbm); 22 | } 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/blockTemplate.js: -------------------------------------------------------------------------------- 1 | var bignum = require('bignum'); 2 | 3 | var merkleTree = require('./merkleTree.js'); 4 | var transactions = require('./transactions.js'); 5 | var util = require('./util.js'); 6 | 7 | 8 | /** 9 | * The BlockTemplate class holds a single job. 10 | * and provides several methods to validate and submit it to the daemon coin 11 | **/ 12 | var BlockTemplate = module.exports = function BlockTemplate(jobId, rpcData, poolAddressScript, extraNonce, 13 | reward, txMessages, recipients){ 14 | 15 | /* private members */ 16 | 17 | var submits = []; 18 | 19 | function getMerkleHashes(steps){ 20 | return steps.map(function(step){ 21 | return step.toString('hex'); 22 | }); 23 | } 24 | 25 | function getTransactionBuffers(txs){ 26 | var txHashes = txs.map(function(tx){ 27 | if (tx.txid !== undefined) { 28 | return util.uint256BufferFromHash(tx.txid); 29 | } 30 | return util.uint256BufferFromHash(tx.hash); 31 | }); 32 | return [null].concat(txHashes); 33 | } 34 | /* 35 | function getVoteData(){ 36 | if (!rpcData.masternode_payments) return new Buffer([]); 37 | 38 | return Buffer.concat( 39 | [util.varIntBuffer(rpcData.votes.length)].concat( 40 | rpcData.votes.map(function (vt) { 41 | return new Buffer(vt, 'hex'); 42 | }) 43 | ) 44 | ); 45 | } 46 | */ 47 | /* public members */ 48 | this.rpcData = rpcData; 49 | this.jobId = jobId; 50 | this.target = rpcData.target ? bignum(rpcData.target, 16) : util.bignumFromBitsHex(rpcData.bits); 51 | this.difficulty = parseFloat((diff1 / this.target.toNumber()).toFixed(9)); 52 | if(rpcData.uscData){ 53 | this.uscTarget = bignum(rpcData.uscData.target.substr(2), 16); 54 | this.uscDifficulty = util.numToString(parseFloat((diff1 / this.uscTarget.toNumber()).toFixed(9))); 55 | } 56 | 57 | /* generate coinbase tx */ 58 | 59 | this.coinbaseTx = transactions.createGeneration(rpcData, poolAddressScript, reward, 60 | txMessages, recipients).toString('hex'); 61 | 62 | this.coinbaseTxBuffer = new Buffer(this.coinbaseTx, 'hex'); 63 | this.coinbaseTxHash = util.sha256d(this.coinbaseTxBuffer); 64 | 65 | this.transactionData = Buffer.concat(rpcData.transactions.map(function(tx){ 66 | return new Buffer(tx.data, 'hex'); 67 | })); 68 | 69 | /* collect the header's data */ 70 | this.prevHashReversed = util.reverseBuffer(new Buffer(rpcData.previousblockhash, 'hex')).toString('hex'); 71 | 72 | this.merkleTree = new merkleTree(getTransactionBuffers(rpcData.transactions)); 73 | this.merkleRoot = this.merkleTree.withFirst(this.coinbaseTxHash).toString('hex'); 74 | 75 | var txsWithCoinbase = []; 76 | if(rpcData.transactions.length > 0) { 77 | txsWithCoinbase = txsWithCoinbase.concat(getTransactionBuffers(rpcData.transactions)); 78 | txsWithCoinbase[0] = this.coinbaseTxHash; 79 | } else { 80 | txsWithCoinbase = [this.coinbaseTxHash] 81 | } 82 | 83 | this.cTotalTxCountHex = txsWithCoinbase.length.toString(16); 84 | 85 | var cMerkleTree = new merkleTree(txsWithCoinbase); 86 | 87 | if(cMerkleTree.steps.length > 0) 88 | this.merkleBranch = [this.coinbaseTxHash.toString("hex")].concat(getMerkleHashes(cMerkleTree.steps)); 89 | else 90 | this.merkleBranch = [this.coinbaseTxHash.toString("hex")]; 91 | this.merkleBranch = this.merkleBranch.join(" "); 92 | 93 | this.claimtrieReversed = util.reverseBuffer(new Buffer(rpcData.claimtrie, 'hex')).toString('hex'); 94 | 95 | this.serializeCoinbase = function(){ 96 | return this.coinbaseTxBuffer; 97 | } 98 | 99 | /* https://en.bitcoin.it/wiki/Protocol_specification#Block_Headers */ 100 | this.serializeHeader = function(extraNonce1, extraNonce2){ 101 | var nonce = extraNonce2; 102 | if(extraNonce1){ 103 | nonce += extraNonce1; 104 | } 105 | 106 | /* 4+32+32+32+4+4 + 32 = 140 */ 107 | var header = new Buffer(140).fill(0); 108 | header.write(this.serializeIncompleteHeader().toString('hex'), 0, 108, 'hex'); 109 | header.write(nonce, 108, 32, 'hex'); 110 | return header; 111 | }; 112 | 113 | this.serializeIncompleteHeader = function(){ 114 | if(!this.incompleteHeader){ 115 | var incompleteHeader = new Buffer(108).fill(0); 116 | var position = 0; 117 | incompleteHeader.writeInt32LE(rpcData.version, position, 4, 'hex'); 118 | incompleteHeader.write(this.prevHashReversed, position += 4, 32, 'hex'); 119 | incompleteHeader.write(this.merkleRoot, position += 32, 32, 'hex'); 120 | incompleteHeader.write(this.claimtrieReversed, position += 32, 32, 'hex'); 121 | incompleteHeader.writeUInt32LE(rpcData.curtime, position += 32, 4, 'hex'); 122 | incompleteHeader.writeUInt32LE(parseInt(rpcData.bits, 16), position += 4, 4, 'hex'); 123 | this.incompleteHeader = incompleteHeader; 124 | } 125 | 126 | return this.incompleteHeader; 127 | }; 128 | 129 | // var extraNonce1 = extraNonce.next(); 130 | 131 | this.serializeRawHeader = function(){ 132 | var rawHeader = new Buffer(140).fill(0); 133 | rawHeader.write(this.serializeIncompleteHeader().toString('hex'), 0, 108, 'hex'); 134 | 135 | return rawHeader; 136 | }; 137 | 138 | this.serializeBlock = function(header){ 139 | return Buffer.concat([ 140 | header, 141 | util.varIntBuffer(this.rpcData.transactions.length + 1), 142 | this.coinbaseTxBuffer, 143 | this.transactionData, 144 | /* getVoteData(), */ 145 | /* POS coins require a zero byte appended to block which the daemon replaces with the signature */ 146 | new Buffer(reward === 'POS' ? [0] : []) 147 | ]); 148 | }; 149 | 150 | this.registerSubmit = function(extraNonce1, extraNonce2){ 151 | var submission = extraNonce2.toLowerCase() + extraNonce1; 152 | if (submits.indexOf(submission) === -1){ 153 | submits.push(submission); 154 | return true; 155 | } 156 | return false; 157 | }; 158 | 159 | this.getJobParams = function(){ 160 | if (!this.jobParams){ 161 | this.jobParams = [ 162 | this.jobId, 163 | this.serializeIncompleteHeader().toString('hex'), 164 | this.merkleBranch, 165 | true, 166 | ]; 167 | } 168 | return this.jobParams; 169 | }; 170 | 171 | }; 172 | -------------------------------------------------------------------------------- /lib/daemon.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var cp = require('child_process'); 3 | var events = require('events'); 4 | var fs =require('fs'); 5 | var async = require('async'); 6 | var dateFormat = require('../../dateformat'); 7 | function logToFile(str, file){ 8 | if(!file){ file = 'debug.log' } 9 | fs.appendFile(file, dateFormat(new Date(), 'yyyy-mm-dd HH:MM:ss') + '\t' + str + '\n', (err)=>{ 10 | if(err) throw err; 11 | }); 12 | }; 13 | /** 14 | * The daemon interface interacts with the coin daemon by using the rpc interface. 15 | * in order to make it work it needs, as constructor, an array of objects containing 16 | * - 'host' : hostname where the coin lives 17 | * - 'port' : port where the coin accepts rpc connections 18 | * - 'user' : username of the coin for the rpc interface 19 | * - 'password': password for the rpc interface of the coin 20 | **/ 21 | 22 | function DaemonInterface(daemons, logger){ 23 | 24 | //private members 25 | var _this = this; 26 | logger = logger || function(severity, message){ 27 | console.log(severity + ': ' + message); 28 | }; 29 | 30 | 31 | var instances = (function(){ 32 | for (var i = 0; i < daemons.length; i++) 33 | daemons[i]['index'] = i; 34 | return daemons; 35 | })(); 36 | 37 | 38 | function init(){ 39 | isOnline(function(online){ 40 | if (online) 41 | _this.emit('online'); 42 | }); 43 | } 44 | 45 | function isOnline(callback){ 46 | cmd('getinfo', [], function(results){ 47 | var allOnline = results.every(function(result){ 48 | return !results.error; 49 | }); 50 | callback(allOnline); 51 | if (!allOnline) 52 | _this.emit('connectionFailed', results); 53 | }); 54 | } 55 | 56 | 57 | function performHttpRequest(instance, jsonData, callback){ 58 | var options = { 59 | hostname: (typeof(instance.host) === 'undefined' ? '127.0.0.1' : instance.host), 60 | port : instance.port, 61 | method : 'POST', 62 | auth : instance.user + ':' + instance.password, 63 | headers : { 64 | 'Content-Length': jsonData.length 65 | } 66 | }; 67 | 68 | var parseJson = function(res, data){ 69 | var dataJson; 70 | 71 | if (res.statusCode === 401){ 72 | logger('error', 'Unauthorized RPC access - invalid RPC username or password'); 73 | return; 74 | } 75 | 76 | try{ 77 | dataJson = JSON.parse(data); 78 | } 79 | catch(e){ 80 | if (data.indexOf(':-nan') !== -1){ 81 | data = data.replace(/:-nan,/g, ":0"); 82 | parseJson(res, data); 83 | return; 84 | } 85 | logger('error', 'Could not parse rpc data from daemon instance ' + instance.index 86 | + '\nRequest Data: ' + jsonData 87 | + '\nReponse Data: ' + data); 88 | 89 | } 90 | if (dataJson) 91 | callback(dataJson.error, dataJson, data); 92 | }; 93 | 94 | var req = http.request(options, function(res) { 95 | var data = ''; 96 | res.setEncoding('utf8'); 97 | res.on('data', function (chunk) { 98 | data += chunk; 99 | }); 100 | res.on('end', function(){ 101 | parseJson(res, data); 102 | }); 103 | }); 104 | 105 | req.on('error', function(e) { 106 | if (e.code === 'ECONNREFUSED') 107 | callback({type: 'offline', message: e.message}, null); 108 | else 109 | callback({type: 'request error', message: e.message}, null); 110 | }); 111 | 112 | req.end(jsonData); 113 | } 114 | 115 | 116 | 117 | //Performs a batch JSON-RPC command - only uses the first configured rpc daemon 118 | /* First argument must have: 119 | [ 120 | [ methodName, [params] ], 121 | [ methodName, [params] ] 122 | ] 123 | */ 124 | 125 | function batchCmd(cmdArray, callback){ 126 | 127 | var requestJson = []; 128 | 129 | for (var i = 0; i < cmdArray.length; i++){ 130 | requestJson.push({ 131 | method: cmdArray[i][0], 132 | params: cmdArray[i][1], 133 | id: Date.now() + Math.floor(Math.random() * 10) + i 134 | }); 135 | } 136 | 137 | var serializedRequest = JSON.stringify(requestJson); 138 | 139 | performHttpRequest(instances[0], serializedRequest, function(error, result){ 140 | callback(error, result); 141 | }); 142 | 143 | } 144 | 145 | /* Sends a JSON RPC (http://json-rpc.org/wiki/specification) command to every configured daemon. 146 | The callback function is fired once with the result from each daemon unless streamResults is 147 | set to true. */ 148 | function cmd(method, params, callback, streamResults, returnRawData){ 149 | 150 | var results = []; 151 | 152 | async.each(instances, function(instance, eachCallback){ 153 | 154 | var itemFinished = function(error, result, data){ 155 | 156 | var returnObj = { 157 | error: error, 158 | response: (result || {}).result, 159 | instance: instance 160 | }; 161 | if (returnRawData) returnObj.data = data; 162 | if (streamResults) callback(returnObj); 163 | else results.push(returnObj); 164 | eachCallback(); 165 | itemFinished = function(){}; 166 | }; 167 | 168 | var requestJson = JSON.stringify({ 169 | method: method, 170 | params: params, 171 | id: Date.now() + Math.floor(Math.random() * 10) 172 | }); 173 | 174 | performHttpRequest(instance, requestJson, function(error, result, data){ 175 | itemFinished(error, result, data); 176 | }); 177 | 178 | 179 | }, function(){ 180 | if (!streamResults){ 181 | callback(results); 182 | } 183 | }); 184 | 185 | } 186 | 187 | 188 | //public members 189 | 190 | this.init = init; 191 | this.isOnline = isOnline; 192 | this.cmd = cmd; 193 | this.batchCmd = batchCmd; 194 | } 195 | 196 | DaemonInterface.prototype.__proto__ = events.EventEmitter.prototype; 197 | 198 | exports.interface = DaemonInterface; 199 | -------------------------------------------------------------------------------- /lib/gwMake.js: -------------------------------------------------------------------------------- 1 | var usc_rpc = require('node-bitcoin-rpc'); 2 | 3 | var GwMaker = module.exports = function(uscOptions){ 4 | usc_rpc.init(uscOptions.rpcIp, uscOptions.rpcPort, '', ''); 5 | this.makeRawGwMsg = function(callback){ 6 | usc_rpc.call('mnr_getWork', [], function (err, res) { 7 | if(res === undefined && err !== null || res === null){ 8 | console.log("=======================Unable to get new USC work.======================"); 9 | callback(undefined); 10 | }else{ 11 | var gw = res, rawgw = {}; 12 | rawgw["created_at_ts"] = (new Date().getTime()/1000); 13 | rawgw["target"] = gw.result.target; 14 | rawgw["parentBlockHash"] = gw.result.parentBlockHash; 15 | rawgw["blockHashForMergedMining"] = gw.result.blockHashForMergedMining; 16 | rawgw["feesPaidToMiner"] = gw.result.feesPaidToMiner; 17 | rawgw["notify"] = gw.result.notify; 18 | callback(rawgw); 19 | } 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var net = require('net'); 2 | var events = require('events'); 3 | 4 | //Gives us global access to everything we need for each hashing algorithm 5 | require('./algoProperties.js'); 6 | require('./moniterConnection.js') 7 | var pool = require('./pool.js'); 8 | 9 | exports.daemon = require('./daemon.js'); 10 | exports.varDiff = require('./varDiff.js'); 11 | 12 | 13 | exports.createPool = function(poolOptions, authorizeFn){ 14 | var newPool = new pool(poolOptions, authorizeFn); 15 | return newPool; 16 | }; 17 | -------------------------------------------------------------------------------- /lib/jobManager.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | var crypto = require('crypto'); 3 | var bignum = require('bignum'); 4 | 5 | var util = require('./util.js'); 6 | var blockTemplate = require('./blockTemplate.js'); 7 | var connection = require('./moniterConnection.js') 8 | 9 | 10 | //Unique extranonce per subscriber 11 | var ExtraNonceCounter = function(configInstanceId){ 12 | 13 | var maxBignum = bignum('ffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 16); 14 | var makeExtraNonce = function(){ 15 | var extraNonce = bignum.rand(maxBignum).toString(16); 16 | if(extraNonce.length !== 56){ 17 | return makeExtraNonce(); 18 | }; 19 | return util.reverseBuffer(Buffer.from(extraNonce.toString(), 'hex')).toString('hex'); 20 | }; 21 | 22 | this.next = function(){ return makeExtraNonce() }; 23 | this.size = 28; //bytes 24 | }; 25 | 26 | //Unique job id per new block template 27 | var JobCounter = function(){ 28 | var counter = 0; 29 | 30 | this.next = function(){ 31 | counter++; 32 | if (counter % 0xffff === 0) 33 | counter = 1; 34 | return this.cur(); 35 | }; 36 | 37 | this.cur = function () { 38 | return counter.toString(16); 39 | }; 40 | }; 41 | 42 | /** 43 | * Emits: 44 | * - newBlock(blockTemplate) - When a new block (previously unknown to the JobManager) is added, use this event to broadcast new jobs 45 | * - share(shareData, blockHex) - When a worker submits a share. It will have blockHex if a block was found 46 | **/ 47 | var JobManager = module.exports = function JobManager(options){ 48 | 49 | 50 | //private members 51 | 52 | var _this = this; 53 | var jobCounter = new JobCounter(); 54 | 55 | var shareMultiplier = algos[options.coin.algorithm].multiplier; 56 | 57 | //public members 58 | 59 | this.extraNonceCounter = new ExtraNonceCounter(options.instanceId); 60 | this.moniter = ''; 61 | 62 | this.currentJob; 63 | this.validJobs = {}; 64 | this.uscTag = ''; 65 | var hashDigest = algos[options.coin.algorithm].hash(options.coin); 66 | 67 | var blockHasher = (function () { 68 | switch (options.coin.algorithm) { 69 | case 'scrypt': 70 | if (options.coin.reward === 'POS') { 71 | return function (d) { 72 | return util.reverseBuffer(hashDigest.apply(this, arguments)); 73 | }; 74 | } 75 | case 'scrypt-og': 76 | if (options.coin.reward === 'POS') { 77 | return function (d) { 78 | return util.reverseBuffer(hashDigest.apply(this, arguments)); 79 | }; 80 | } 81 | case 'scrypt-jane': 82 | if (options.coin.reward === 'POS') { 83 | return function (d) { 84 | return util.reverseBuffer(hashDigest.apply(this, arguments)); 85 | }; 86 | } 87 | case 'scrypt-n': 88 | case 'sha1': 89 | return function (d) { 90 | return util.reverseBuffer(util.sha256d(d)); 91 | }; 92 | default: 93 | return function () { 94 | return util.reverseBuffer(hashDigest.apply(this, arguments)); 95 | }; 96 | } 97 | })(); 98 | this.addMoniter = function(address){ 99 | this.moniter=address; 100 | } 101 | this.removeMoniter = function(){ 102 | this.moniter=''; 103 | 104 | } 105 | this.updateCurrentJob = function(rpcData){ 106 | if(rpcData.uscData){ 107 | var uscTag = rpcData.uscData.blockHashForMergedMining; 108 | }else{ 109 | var uscTag = ''; 110 | } 111 | var tmpBlockTemplate = new blockTemplate( 112 | jobCounter.next(), 113 | rpcData, 114 | options.poolAddressScript, 115 | _this.extraNonceCounter, 116 | options.coin.reward, 117 | options.coin.txMessages, 118 | options.recipients 119 | ); 120 | 121 | _this.currentJob = tmpBlockTemplate; 122 | 123 | _this.emit('updatedBlock', tmpBlockTemplate, true); 124 | 125 | _this.validJobs[tmpBlockTemplate.jobId] = tmpBlockTemplate; 126 | 127 | _this.uscTag = uscTag; 128 | }; 129 | 130 | //returns true if processed a new block 131 | this.processTemplate = function(rpcData){ 132 | /* Block is new if A) its the first block we have seen so far or B) the blockhash is different and the 133 | block height is greater than the one we have */ 134 | var isNewBlock = typeof(_this.currentJob) === 'undefined'; 135 | if (!isNewBlock && _this.currentJob.rpcData.previousblockhash !== rpcData.previousblockhash){ 136 | isNewBlock = true; 137 | 138 | //If new block is outdated/out-of-sync than return 139 | if (rpcData.height < _this.currentJob.rpcData.height) 140 | return false; 141 | } 142 | 143 | if (!isNewBlock) return false; 144 | if(rpcData.uscData){ 145 | var uscTag = rpcData.uscData.blockHashForMergedMining; 146 | }else{ 147 | var uscTag = ''; 148 | } 149 | 150 | var tmpBlockTemplate = new blockTemplate( 151 | jobCounter.next(), 152 | rpcData, 153 | options.poolAddressScript, 154 | _this.extraNonceCounter, 155 | options.coin.reward, 156 | options.coin.txMessages, 157 | options.recipients 158 | ); 159 | 160 | this.currentJob = tmpBlockTemplate; 161 | 162 | this.validJobs = {}; 163 | _this.emit('newBlock', tmpBlockTemplate); 164 | this.validJobs[tmpBlockTemplate.jobId] = tmpBlockTemplate; 165 | 166 | _this.uscTag = uscTag; 167 | 168 | return true; 169 | 170 | }; 171 | 172 | this.processSecondShare = function(jobId, previousDifficulty, difficulty, ipAddress, port, workerName, nTime, nonce, extraNonce1, extraNonce2, hash){ 173 | var shareError = function(error){ 174 | _this.emit('share', { 175 | job: jobId, 176 | ip: ipAddress, 177 | worker: workerName, 178 | difficulty: difficulty, 179 | error: error[1] 180 | }); 181 | return {error: error, result: null}; 182 | }; 183 | 184 | var submitTime = Date.now() / 1000 | 0; 185 | var job = this.validJobs[jobId]; 186 | var minerAddress = workerName.split('.')[0]; 187 | if (typeof job === 'undefined' || job.jobId != jobId ) { 188 | return shareError([21, 'job not found']); 189 | } 190 | if(extraNonce2.length!==8) { 191 | return shareError([20, 'incorrect size of nonce']); 192 | } 193 | extraNonce2 = Buffer.from(extraNonce2,'hex').toString('hex'); 194 | 195 | if(extraNonce2.length!==8) { 196 | return shareError([20, 'incorrect size of nonce']); 197 | } 198 | 199 | if (!job.registerSubmit(extraNonce1,extraNonce2)){ 200 | return shareError([22, 'duplicate share']); 201 | } 202 | 203 | var headerBuffer = job.serializeHeader(extraNonce1, extraNonce2); 204 | 205 | var headerHash = hashDigest(headerBuffer,null); 206 | 207 | if (hash && headerHash.toString('hex') !== hash) { 208 | return shareError([31, 'incorrect hash of ' + hash]); 209 | } 210 | if(_this.moniter.indexOf(minerAddress)!=-1){ 211 | connection.zadd(_this.moniter+"_hash",Date.now(),JSON.stringify({ 212 | workerName:workerName, 213 | jobId:jobId, 214 | extraNonce1:extraNonce1, 215 | extraNonce2:extraNonce2, 216 | submitHash:hash, 217 | headerBuffer:headerBuffer.toString('hex'), 218 | time:Date.now() 219 | })) 220 | } 221 | var headerBigNum = bignum.fromBuffer(headerHash, {endian: 'little', size: 32}); 222 | 223 | var blockHashInvalid; 224 | var blockHexInvalid; 225 | var blockHash; 226 | var blockHex; 227 | 228 | var shareDiff = diff1 / headerBigNum.toNumber() * shareMultiplier; 229 | if(_this.uscTag !== '' && job.uscTarget !== undefined){ 230 | var isUscTarget = job.uscTarget.ge(headerBigNum); //true 231 | }else{ 232 | var isUscTarget = false; 233 | } 234 | var blockDiffAdjusted = job.difficulty * shareMultiplier; 235 | blockHexInvalid = job.serializeBlock(headerBuffer).toString('hex'); 236 | blockHashInvalid = util.reverseBuffer(util.sha256d(headerBuffer)).toString('hex'); 237 | //Check if share is a block candidate (matched network difficulty) 238 | if (job.target.ge(headerBigNum)){ 239 | blockHex = job.serializeBlock(headerBuffer).toString('hex'); 240 | blockHash = blockHasher(headerBuffer, /*nTime*/null).toString('hex'); 241 | } 242 | else { 243 | if (options.emitInvalidBlockHashes) 244 | blockHashInvalid = util.reverseBuffer(util.sha256d(headerBuffer)).toString('hex'); 245 | 246 | //Check if share didn't reached the miner's difficulty) 247 | if (shareDiff / difficulty < 0.99){ 248 | 249 | //Check if share matched a previous difficulty from before a vardiff retarget 250 | if (previousDifficulty && shareDiff >= previousDifficulty){ 251 | difficulty = previousDifficulty; 252 | } 253 | else{ 254 | return shareError([23, 'low difficulty share of ' + shareDiff]); 255 | } 256 | 257 | } 258 | } 259 | 260 | 261 | _this.emit('share', { 262 | job: jobId, 263 | ip: ipAddress, 264 | port: port, 265 | worker: workerName, 266 | height: job.rpcData.height, 267 | blockReward: job.rpcData.coinbasevalue, 268 | difficulty: difficulty, 269 | shareDiff: shareDiff.toFixed(8), 270 | blockDiff: blockDiffAdjusted, 271 | blockDiffActual: job.difficulty, 272 | coinbaseTxHex: job.coinbaseTx, 273 | isUscTarget: isUscTarget, 274 | isDuplicate: job.rpcData.isDuplicate, 275 | merkleHashes: job.merkleBranch, 276 | headerBuffer: headerBuffer.toString('hex'), 277 | blockHash: blockHash, 278 | blockHashInvalid: blockHashInvalid, 279 | cTotalTxCountHex: job.cTotalTxCountHex 280 | }, blockHex); 281 | return {result: true, error: null, blockHash: blockHash,difficulty:difficulty,height:job.rpcData.height,worker:workerName,ip:ipAddress,shareDiff:shareDiff,blockDiff:blockDiffAdjusted}; 282 | }; 283 | 284 | 285 | }; 286 | JobManager.prototype.__proto__ = events.EventEmitter.prototype; 287 | -------------------------------------------------------------------------------- /lib/merkleTree.js: -------------------------------------------------------------------------------- 1 | var util = require('./util.js'); 2 | 3 | var MerkleTree = module.exports = function MerkleTree(data){ 4 | 5 | function merkleJoin(h1, h2){ 6 | var joined = Buffer.concat([h1, h2]); 7 | var dhashed = util.sha256d(joined); 8 | return dhashed; 9 | } 10 | 11 | function calculateSteps(data){ 12 | var L = data; 13 | var steps = []; 14 | var PreL = [null]; 15 | var StartL = 2; 16 | var Ll = L.length; 17 | 18 | if (Ll > 1){ 19 | while (true){ 20 | if (Ll === 1) 21 | break; 22 | steps.push(L[1]); 23 | if (Ll % 2) 24 | L.push(L[L.length - 1]); 25 | var Ld = []; 26 | var r = util.range(StartL, Ll, 2); 27 | r.forEach(function(i){ 28 | Ld.push(merkleJoin(L[i], L[i + 1])); 29 | }); 30 | L = PreL.concat(Ld); 31 | Ll = L.length; 32 | } 33 | } 34 | return steps; 35 | } 36 | 37 | this.data = data; 38 | this.steps = calculateSteps(data); 39 | 40 | } 41 | MerkleTree.prototype = { 42 | withFirst: function(f){ 43 | this.steps.forEach(function(s){ 44 | f = util.sha256d(Buffer.concat([f, s])); 45 | }); 46 | return f; 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /lib/moniterConnection.js: -------------------------------------------------------------------------------- 1 | var redis = require('redis'); 2 | var fs = require('fs'); 3 | JSON.minify = JSON.minify || require("node-json-minify"); 4 | var configs = JSON.parse(JSON.minify(fs.readFileSync("config.json", {encoding: 'utf8'}))); 5 | var connection = redis.createClient(configs.redis.port,configs.redis.host) 6 | if(configs.redis.password){ 7 | connection.auth(configs.redis.password) 8 | } 9 | 10 | module.exports=connection; -------------------------------------------------------------------------------- /lib/peer.js: -------------------------------------------------------------------------------- 1 | var net = require('net'); 2 | var crypto = require('crypto'); 3 | var events = require('events'); 4 | 5 | var util = require('./util.js'); 6 | 7 | 8 | //Example of p2p in node from TheSeven: http://paste.pm/e54.js 9 | 10 | 11 | var fixedLenStringBuffer = function(s, len) { 12 | var buff = new Buffer(len); 13 | buff.fill(0); 14 | buff.write(s); 15 | return buff; 16 | }; 17 | 18 | var commandStringBuffer = function (s) { 19 | return fixedLenStringBuffer(s, 12); 20 | }; 21 | 22 | /* Reads a set amount of bytes from a flowing stream, argument descriptions: 23 | - stream to read from, must have data emitter 24 | - amount of bytes to read 25 | - preRead argument can be used to set start with an existing data buffer 26 | - callback returns 1) data buffer and 2) lopped/over-read data */ 27 | var readFlowingBytes = function (stream, amount, preRead, callback) { 28 | 29 | var buff = preRead ? preRead : new Buffer([]); 30 | 31 | var readData = function (data) { 32 | buff = Buffer.concat([buff, data]); 33 | if (buff.length >= amount) { 34 | var returnData = buff.slice(0, amount); 35 | var lopped = buff.length > amount ? buff.slice(amount) : null; 36 | callback(returnData, lopped); 37 | } 38 | else 39 | stream.once('data', readData); 40 | }; 41 | 42 | readData(new Buffer([])); 43 | }; 44 | 45 | var Peer = module.exports = function (options) { 46 | 47 | var _this = this; 48 | var client; 49 | var magic = new Buffer(options.testnet ? options.coin.peerMagicTestnet : options.coin.peerMagic, 'hex'); 50 | var magicInt = magic.readUInt32LE(0); 51 | var verack = false; 52 | var validConnectionConfig = true; 53 | 54 | //https://en.bitcoin.it/wiki/Protocol_specification#Inventory_Vectors 55 | var invCodes = { 56 | error: 0, 57 | tx: 1, 58 | block: 2 59 | }; 60 | 61 | var networkServices = new Buffer('0100000000000000', 'hex'); //NODE_NETWORK services (value 1 packed as uint64) 62 | var emptyNetAddress = new Buffer('010000000000000000000000000000000000ffff000000000000', 'hex'); 63 | var userAgent = util.varStringBuffer('/node-stratum/'); 64 | var blockStartHeight = new Buffer('00000000', 'hex'); //block start_height, can be empty 65 | 66 | //If protocol version is new enough, add do not relay transactions flag byte, outlined in BIP37 67 | //https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki#extensions-to-existing-messages 68 | var relayTransactions = options.p2p.disableTransactions === true ? new Buffer([false]) : new Buffer([]); 69 | 70 | var commands = { 71 | version: commandStringBuffer('version'), 72 | inv: commandStringBuffer('inv'), 73 | verack: commandStringBuffer('verack'), 74 | addr: commandStringBuffer('addr'), 75 | getblocks: commandStringBuffer('getblocks') 76 | }; 77 | 78 | 79 | (function init() { 80 | Connect(); 81 | })(); 82 | 83 | 84 | function Connect() { 85 | 86 | client = net.connect({ 87 | host: options.p2p.host, 88 | port: options.p2p.port 89 | }, function () { 90 | SendVersion(); 91 | }); 92 | client.on('close', function () { 93 | if (verack) { 94 | _this.emit('disconnected'); 95 | verack = false; 96 | Connect(); 97 | } 98 | else if (validConnectionConfig) 99 | _this.emit('connectionRejected'); 100 | 101 | }); 102 | client.on('error', function (e) { 103 | if (e.code === 'ECONNREFUSED') { 104 | validConnectionConfig = false; 105 | _this.emit('connectionFailed'); 106 | } 107 | else 108 | _this.emit('socketError', e); 109 | }); 110 | 111 | 112 | SetupMessageParser(client); 113 | 114 | } 115 | 116 | function SetupMessageParser(client) { 117 | 118 | var beginReadingMessage = function (preRead) { 119 | 120 | readFlowingBytes(client, 24, preRead, function (header, lopped) { 121 | var msgMagic = header.readUInt32LE(0); 122 | if (msgMagic !== magicInt) { 123 | _this.emit('error', 'bad magic number from peer'); 124 | while (header.readUInt32LE(0) !== magicInt && header.length >= 4) { 125 | header = header.slice(1); 126 | } 127 | if (header.readUInt32LE(0) === magicInt) { 128 | beginReadingMessage(header); 129 | } else { 130 | beginReadingMessage(new Buffer([])); 131 | } 132 | return; 133 | } 134 | var msgCommand = header.slice(4, 16).toString(); 135 | var msgLength = header.readUInt32LE(16); 136 | var msgChecksum = header.readUInt32LE(20); 137 | readFlowingBytes(client, msgLength, lopped, function (payload, lopped) { 138 | if (util.sha256d(payload).readUInt32LE(0) !== msgChecksum) { 139 | _this.emit('error', 'bad payload - failed checksum'); 140 | beginReadingMessage(null); 141 | return; 142 | } 143 | HandleMessage(msgCommand, payload); 144 | beginReadingMessage(lopped); 145 | }); 146 | }); 147 | }; 148 | 149 | beginReadingMessage(null); 150 | } 151 | 152 | 153 | //Parsing inv message https://en.bitcoin.it/wiki/Protocol_specification#inv 154 | function HandleInv(payload) { 155 | //sloppy varint decoding 156 | var count = payload.readUInt8(0); 157 | payload = payload.slice(1); 158 | if (count >= 0xfd) 159 | { 160 | count = payload.readUInt16LE(0); 161 | payload = payload.slice(2); 162 | } 163 | while (count--) { 164 | switch(payload.readUInt32LE(0)) { 165 | case invCodes.error: 166 | break; 167 | case invCodes.tx: 168 | var tx = payload.slice(4, 36).toString('hex'); 169 | break; 170 | case invCodes.block: 171 | var block = payload.slice(4, 36).toString('hex'); 172 | _this.emit('blockFound', block); 173 | break; 174 | } 175 | payload = payload.slice(36); 176 | } 177 | } 178 | 179 | function HandleMessage(command, payload) { 180 | _this.emit('peerMessage', {command: command, payload: payload}); 181 | switch (command) { 182 | case commands.inv.toString(): 183 | HandleInv(payload); 184 | break; 185 | case commands.verack.toString(): 186 | if(!verack) { 187 | verack = true; 188 | _this.emit('connected'); 189 | } 190 | break; 191 | default: 192 | break; 193 | } 194 | 195 | } 196 | 197 | //Message structure defined at: https://en.bitcoin.it/wiki/Protocol_specification#Message_structure 198 | function SendMessage(command, payload) { 199 | var message = Buffer.concat([ 200 | magic, 201 | command, 202 | util.packUInt32LE(payload.length), 203 | util.sha256d(payload).slice(0, 4), 204 | payload 205 | ]); 206 | client.write(message); 207 | _this.emit('sentMessage', message); 208 | } 209 | 210 | function SendVersion() { 211 | var payload = Buffer.concat([ 212 | util.packUInt32LE(options.protocolVersion), 213 | networkServices, 214 | util.packInt64LE(Date.now() / 1000 | 0), 215 | emptyNetAddress, //addr_recv, can be empty 216 | emptyNetAddress, //addr_from, can be empty 217 | crypto.pseudoRandomBytes(8), //nonce, random unique ID 218 | userAgent, 219 | blockStartHeight, 220 | relayTransactions 221 | ]); 222 | SendMessage(commands.version, payload); 223 | } 224 | 225 | }; 226 | 227 | Peer.prototype.__proto__ = events.EventEmitter.prototype; 228 | -------------------------------------------------------------------------------- /lib/pool.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | var async = require('async'); 3 | var fs = require('fs'); 4 | var varDiff = require('./varDiff.js'); 5 | var daemon = require('./daemon.js'); 6 | var peer = require('./peer.js'); 7 | var stratum = require('./stratum.js'); 8 | var jobManager = require('./jobManager.js'); 9 | var util = require('./util.js'); 10 | var connection = require('./moniterConnection.js') 11 | var GwMaker = require('./gwMake.js'); 12 | var blockMake = require('./blockMake.js'); 13 | var getArr = global.getArr = []; 14 | 15 | var pool = module.exports = function pool(options, authorizeFn){ 16 | 17 | this.options = options; 18 | 19 | var _this = this; 20 | _this.nSubmitted = 0; 21 | var blockPollingIntervalId; 22 | 23 | 24 | var emitLog = function(text) { _this.emit('log', 'debug' , text); }; 25 | var emitWarningLog = function(text) { _this.emit('log', 'warning', text); }; 26 | var emitErrorLog = function(text) { _this.emit('log', 'error' , text); }; 27 | var emitSpecialLog = function(text) { _this.emit('log', 'special', text); }; 28 | 29 | 30 | 31 | if (!(options.coin.algorithm in algos)){ 32 | emitErrorLog('The ' + options.coin.algorithm + ' hashing algorithm is not supported.'); 33 | throw new Error(); 34 | } 35 | 36 | 37 | 38 | this.start = function(){ 39 | SetupVarDiff(); 40 | SetupApi(); 41 | SetupDaemonInterface(function(){ 42 | DetectCoinData(function(){ 43 | SetupRecipients(); 44 | SetupJobManager(); 45 | OnBlockchainSynced(function(){ 46 | GetFirstJob(function(){ 47 | SetupBlockPolling(); 48 | if(options.usc.enabled){ SetupNewUscWorkBroadcast(); } 49 | SetupPeer(); 50 | StartStratumServer(function(){ 51 | OutputPoolInfo(); 52 | _this.emit('started'); 53 | }); 54 | }); 55 | }); 56 | }); 57 | }); 58 | }; 59 | 60 | 61 | 62 | function GetFirstJob(finishedCallback){ 63 | 64 | GetBlockTemplate(function(error, result){ 65 | if (error) { 66 | emitErrorLog('Error with getblocktemplate on creating first job, server cannot start'); 67 | return; 68 | } 69 | 70 | var portWarnings = []; 71 | 72 | var networkDiffAdjusted = options.initStats.difficulty; 73 | 74 | Object.keys(options.ports).forEach(function(port){ 75 | var portDiff = options.ports[port].diff; 76 | if (networkDiffAdjusted < portDiff) 77 | portWarnings.push('port ' + port + ' w/ diff ' + portDiff); 78 | }); 79 | 80 | //Only let the first fork show synced status or the log wil look flooded with it 81 | if (portWarnings.length > 0 && (!process.env.forkId || process.env.forkId === '0')) { 82 | var warnMessage = 'Network diff of ' + networkDiffAdjusted + ' is lower than ' 83 | + portWarnings.join(' and '); 84 | emitWarningLog(warnMessage); 85 | } 86 | 87 | finishedCallback(); 88 | 89 | }); 90 | } 91 | 92 | 93 | function OutputPoolInfo(){ 94 | 95 | var startMessage = 'Stratum Pool Server Started for ' + options.coin.name + 96 | ' [' + options.coin.symbol.toUpperCase() + '] {' + options.coin.algorithm + '}'; 97 | if (process.env.forkId && process.env.forkId !== '0'){ 98 | emitLog(startMessage); 99 | return; 100 | } 101 | var infoLines = [startMessage, 102 | 'Network Connected:\t' + (options.testnet ? 'Testnet' : 'Mainnet'), 103 | 'Detected Reward Type:\t' + options.coin.reward, 104 | 'Current Block Height:\t' + _this.jobManager.currentJob.rpcData.height, 105 | 'Current Connect Peers:\t' + options.initStats.connections, 106 | 'Current Block Diff:\t' + _this.jobManager.currentJob.difficulty * algos[options.coin.algorithm].multiplier, 107 | 'Network Difficulty:\t' + options.initStats.difficulty, 108 | 'Network Hash Rate:\t' + util.getReadableHashRateString(options.initStats.networkHashRate), 109 | 'Stratum Port(s):\t' + _this.options.initStats.stratumPorts.join(', '), 110 | 'Pool Fee Percent:\t' + _this.options.feePercent + '%' 111 | ]; 112 | 113 | if (typeof options.blockRefreshInterval === "number" && options.blockRefreshInterval > 0) 114 | infoLines.push('Block polling every:\t' + options.blockRefreshInterval + ' ms'); 115 | 116 | emitSpecialLog(infoLines.join('\n\t\t\t\t\t\t')); 117 | } 118 | 119 | 120 | function OnBlockchainSynced(syncedCallback){ 121 | 122 | var checkSynced = function(displayNotSynced){ 123 | _this.daemon.cmd('getblocktemplate', [], function(results){ 124 | var synced = results.every(function(r){ 125 | return !r.error || r.error.code !== -10; 126 | }); 127 | if (synced){ 128 | syncedCallback(); 129 | } 130 | else{ 131 | if (displayNotSynced) displayNotSynced(); 132 | setTimeout(checkSynced, 5000); 133 | 134 | //Only let the first fork show synced status or the log wil look flooded with it 135 | if (!process.env.forkId || process.env.forkId === '0') 136 | generateProgress(); 137 | } 138 | 139 | }); 140 | }; 141 | checkSynced(function(){ 142 | //Only let the first fork show synced status or the log wil look flooded with it 143 | if (!process.env.forkId || process.env.forkId === '0') 144 | emitErrorLog('Daemon is still syncing with network (download blockchain) - server will be started once synced'); 145 | }); 146 | 147 | 148 | var generateProgress = function(){ 149 | 150 | _this.daemon.cmd('getinfo', [], function(results) { 151 | var blockCount = results.sort(function (a, b) { 152 | return b.response.blocks - a.response.blocks; 153 | })[0].response.blocks; 154 | 155 | //get list of peers and their highest block height to compare to ours 156 | _this.daemon.cmd('getpeerinfo', [], function(results){ 157 | 158 | var peers = results[0].response; 159 | var totalBlocks = peers.sort(function(a, b){ 160 | return b.startingheight - a.startingheight; 161 | })[0].startingheight; 162 | 163 | var percent = (blockCount / totalBlocks * 100).toFixed(2); 164 | emitWarningLog('Downloaded ' + percent + '% of blockchain from ' + peers.length + ' peers'); 165 | }); 166 | 167 | }); 168 | }; 169 | 170 | } 171 | 172 | 173 | function SetupApi() { 174 | if (typeof(options.api) !== 'object' || typeof(options.api.start) !== 'function') { 175 | return; 176 | } else { 177 | options.api.start(_this); 178 | } 179 | } 180 | 181 | 182 | function SetupPeer(){ 183 | if (!options.p2p || !options.p2p.enabled) 184 | return; 185 | 186 | if (options.testnet && !options.coin.peerMagicTestnet){ 187 | emitErrorLog('p2p cannot be enabled in testnet without peerMagicTestnet set in coin configuration'); 188 | return; 189 | } 190 | else if (!options.coin.peerMagic){ 191 | emitErrorLog('p2p cannot be enabled without peerMagic set in coin configuration'); 192 | return; 193 | } 194 | 195 | _this.peer = new peer(options); 196 | _this.peer.on('connected', function() { 197 | emitLog('p2p connection successful'); 198 | }).on('connectionRejected', function(){ 199 | emitErrorLog('p2p connection failed - likely incorrect p2p magic value'); 200 | }).on('disconnected', function(){ 201 | emitWarningLog('p2p peer node disconnected - attempting reconnection...'); 202 | }).on('connectionFailed', function(e){ 203 | emitErrorLog('p2p connection failed - likely incorrect host or port'); 204 | }).on('socketError', function(e){ 205 | emitErrorLog('p2p had a socket error ' + JSON.stringify(e)); 206 | }).on('error', function(msg){ 207 | emitWarningLog('p2p had an error ' + msg); 208 | }).on('blockFound', function(hash){ 209 | _this.processBlockNotify(hash, 'p2p'); 210 | }); 211 | } 212 | 213 | 214 | function SetupVarDiff(){ 215 | _this.varDiff = {}; 216 | Object.keys(options.ports).forEach(function(port) { 217 | if (options.ports[port].varDiff) 218 | _this.setVarDiff(port, options.ports[port].varDiff); 219 | }); 220 | } 221 | 222 | 223 | /* 224 | Coin daemons either use submitblock or getblocktemplate for submitting new blocks 225 | */ 226 | function SubmitBlock(blockHex, callback){ 227 | 228 | var rpcCommand, rpcArgs; 229 | if (options.hasSubmitMethod){ 230 | rpcCommand = 'submitblock'; 231 | rpcArgs = [blockHex]; 232 | } 233 | else{ 234 | rpcCommand = 'getblocktemplate'; 235 | rpcArgs = [{'mode': 'submit', 'data': blockHex}]; 236 | } 237 | 238 | 239 | _this.daemon.cmd(rpcCommand, 240 | rpcArgs, 241 | function(results){ 242 | for (var i = 0; i < results.length; i++){ 243 | var result = results[i]; 244 | if (result.error) { 245 | emitErrorLog('rpc error with daemon instance ' + 246 | result.instance.index + ' when submitting block with ' + rpcCommand + ' ' + 247 | JSON.stringify(result.error) 248 | ); 249 | return; 250 | } 251 | else if (result.response === 'rejected') { 252 | emitErrorLog('Daemon instance ' + result.instance.index + ' rejected a supposedly valid block'); 253 | return; 254 | } 255 | } 256 | emitLog('Submitted Ulord Block using ' + rpcCommand + ' successfully to daemon instance(s)'); 257 | callback(); 258 | } 259 | ); 260 | 261 | } 262 | 263 | // USC SubmitBlock 264 | function SubmitUscBlock(params) { 265 | var blockM = new blockMake(options.usc); 266 | //if(_this.nSubmitted < 5) { 267 | _this.nSubmitted++; 268 | emitLog("USC Submitted: " + _this.nSubmitted); 269 | blockM.makeSubmitMsg(params, function(err, rawbm){ 270 | if(rawbm === undefined && err !== null) { 271 | emitErrorLog('Failed to submit potential usc block: ' + JSON.stringify(err)); 272 | return; 273 | } else { 274 | _this.nSubmitted--; 275 | emitLog('Submitted usc block successfully to the USC daemon instance(s) with BlockHash: ' + rawbm.blockHash); 276 | } 277 | }); 278 | //} 279 | } 280 | 281 | 282 | function SetupRecipients(){ 283 | var recipients = []; 284 | options.feePercent = 0; 285 | options.rewardRecipients = options.rewardRecipients || {}; 286 | for (var r in options.rewardRecipients){ 287 | var percent = options.rewardRecipients[r]; 288 | var rObj = { 289 | percent: percent / 100 290 | }; 291 | try { 292 | if (r.length === 40) 293 | rObj.script = util.miningKeyToScript(r); 294 | else 295 | rObj.script = util.addressToScript(r); 296 | recipients.push(rObj); 297 | options.feePercent += percent; 298 | } 299 | catch(e){ 300 | emitErrorLog('Error generating transaction output script for ' + r + ' in rewardRecipients'); 301 | } 302 | } 303 | if (recipients.length === 0){ 304 | emitErrorLog('No rewardRecipients have been setup which means no fees will be taken'); 305 | } 306 | options.recipients = recipients; 307 | } 308 | 309 | function SetupJobManager(){ 310 | 311 | _this.jobManager = new jobManager(options); 312 | 313 | _this.jobManager.on('newBlock', function(blockTemplate){ 314 | //Check if stratumServer has been initialized yet 315 | if (_this.stratumServer) { 316 | _this.stratumServer.broadcastMiningJobs(blockTemplate.getJobParams()); 317 | } 318 | }).on('updatedBlock', function(blockTemplate){ 319 | //Check if stratumServer has been initialized yet 320 | if (_this.stratumServer) { 321 | var job = blockTemplate.getJobParams(); 322 | job[2] = false; 323 | _this.stratumServer.broadcastMiningJobs(job); 324 | } 325 | }).on('share', function(shareData, blockHex){ 326 | var isValidShare = !shareData.error; 327 | var isValidBlock = !!blockHex; 328 | if(options.usc.enabled){ 329 | if(isValidShare && !shareData.isDuplicate){ 330 | if(shareData.isUscTarget){ 331 | 332 | if(shareData.blockHash === undefined){ 333 | var blockHashHex = shareData.blockHashInvalid; 334 | }else{ 335 | var blockHashHex = shareData.blockHash; 336 | } 337 | var params = [ 338 | blockHashHex, //blockHashHex 339 | shareData.headerBuffer, //blockHeaderHex 340 | shareData.coinbaseTxHex, //coinbaseHex 341 | shareData.merkleHashes, //merkleHashesHex 342 | shareData.cTotalTxCountHex //blockTxnCountHex 343 | ] 344 | 345 | SubmitUscBlock(params); 346 | } 347 | } 348 | } 349 | 350 | var emitShare = function(){ 351 | _this.emit('share', isValidShare, isValidBlock, shareData); 352 | }; 353 | 354 | /* 355 | If we calculated that the block solution was found, 356 | before we emit the share, lets submit the block, 357 | then check if it was accepted using RPC getblock 358 | */ 359 | if (!isValidBlock){ 360 | emitShare(); 361 | }else{ 362 | SubmitBlock(blockHex, function(){ 363 | CheckBlockAccepted(shareData.blockHash, function(isAccepted, tx, height){ 364 | isValidBlock = isAccepted; 365 | shareData.txHash = tx; 366 | emitShare(); 367 | 368 | GetBlockTemplate(function(error, result, foundNewBlock){ 369 | if (foundNewBlock) 370 | emitLog('Block notification via RPC after block submission'); 371 | }); 372 | 373 | }); 374 | }); 375 | } 376 | }).on('log', function(severity, message){ 377 | _this.emit('log', severity, message); 378 | }); 379 | } 380 | 381 | 382 | function SetupDaemonInterface(finishedCallback){ 383 | 384 | if (!Array.isArray(options.daemons) || options.daemons.length < 1){ 385 | emitErrorLog('No daemons have been configured - pool cannot start'); 386 | return; 387 | } 388 | 389 | _this.daemon = new daemon.interface(options.daemons, function(severity, message){ 390 | _this.emit('log', severity , message); 391 | }); 392 | 393 | _this.daemon.once('online', function(){ 394 | finishedCallback(); 395 | 396 | }).on('connectionFailed', function(error){ 397 | emitErrorLog('Failed to connect daemon(s): ' + JSON.stringify(error)); 398 | 399 | }).on('error', function(message){ 400 | emitErrorLog(message); 401 | 402 | }); 403 | 404 | _this.daemon.init(); 405 | } 406 | 407 | 408 | function DetectCoinData(finishedCallback){ 409 | 410 | var batchRpcCalls = [ 411 | ['validateaddress', [options.address]], 412 | ['getdifficulty', []], 413 | ['getinfo', []], 414 | ['getmininginfo', []], 415 | ['submitblock', []] 416 | ]; 417 | 418 | _this.daemon.batchCmd(batchRpcCalls, function(error, results){ 419 | if (error || !results){ 420 | emitErrorLog('Could not start pool, error with init batch RPC call: ' + JSON.stringify(error)); 421 | return; 422 | } 423 | 424 | var rpcResults = {}; 425 | 426 | for (var i = 0; i < results.length; i++){ 427 | var rpcCall = batchRpcCalls[i][0]; 428 | var r = results[i]; 429 | rpcResults[rpcCall] = r.result || r.error; 430 | 431 | if (rpcCall !== 'submitblock' && (r.error || !r.result)){ 432 | emitErrorLog('Could not start pool, error with init RPC ' + rpcCall + ' - ' + JSON.stringify(r.error)); 433 | return; 434 | } 435 | } 436 | 437 | if (!rpcResults.validateaddress.isvalid){ 438 | emitErrorLog('Daemon reports address is not valid'); 439 | return; 440 | } 441 | 442 | if (!options.coin.reward) { 443 | if (isNaN(rpcResults.getdifficulty) && 'proof-of-stake' in rpcResults.getdifficulty) 444 | options.coin.reward = 'POS'; 445 | else 446 | options.coin.reward = 'POW'; 447 | } 448 | 449 | 450 | /* POS coins must use the pubkey in coinbase transaction, and pubkey is 451 | only given if address is owned by wallet.*/ 452 | if (options.coin.reward === 'POS' && typeof(rpcResults.validateaddress.pubkey) == 'undefined') { 453 | emitErrorLog('The address provided is not from the daemon wallet - this is required for POS coins.'); 454 | return; 455 | } 456 | 457 | options.poolAddressScript = (function(){ 458 | switch(options.coin.reward){ 459 | case 'POS': 460 | return util.pubkeyToScript(rpcResults.validateaddress.pubkey); 461 | case 'POW': 462 | return util.addressToScript(rpcResults.validateaddress.address); 463 | } 464 | })(); 465 | 466 | options.testnet = rpcResults.getinfo.testnet; 467 | options.protocolVersion = rpcResults.getinfo.protocolversion; 468 | 469 | options.initStats = { 470 | connections: rpcResults.getinfo.connections, 471 | difficulty: rpcResults.getinfo.difficulty * algos[options.coin.algorithm].multiplier, 472 | networkHashRate: rpcResults.getmininginfo.networkhashps 473 | }; 474 | 475 | 476 | if (rpcResults.submitblock.message === 'Method not found'){ 477 | options.hasSubmitMethod = false; 478 | } 479 | else if (rpcResults.submitblock.code === -1){ 480 | options.hasSubmitMethod = true; 481 | } 482 | else { 483 | emitErrorLog('Could not detect block submission RPC method, ' + JSON.stringify(results)); 484 | return; 485 | } 486 | 487 | finishedCallback(); 488 | 489 | }); 490 | } 491 | 492 | 493 | 494 | function StartStratumServer(finishedCallback){ 495 | _this.stratumServer = new stratum.Server(options, authorizeFn); 496 | 497 | _this.stratumServer.on('started', function(){ 498 | options.initStats.stratumPorts = Object.keys(options.ports); 499 | _this.stratumServer.broadcastMiningJobs(_this.jobManager.currentJob.getJobParams()); 500 | finishedCallback(); 501 | 502 | }).on('broadcastTimeout', function(){ 503 | emitLog('No new blocks for ' + options.jobRebroadcastTimeout + ' seconds - updating transactions & rebroadcasting work'); 504 | GetBlockTemplate(function(error, rpcData, processedBlock){ 505 | if (error || processedBlock) return; 506 | _this.jobManager.updateCurrentJob(rpcData); 507 | }); 508 | 509 | }).on('client.connected', function(client){ 510 | if (typeof(_this.varDiff[client.socket.localPort]) !== 'undefined') { 511 | _this.varDiff[client.socket.localPort].manageClient(client); 512 | } 513 | 514 | client.on('difficultyChanged', function(diff){ 515 | _this.emit('difficultyUpdate', client.workerName, diff); 516 | 517 | }).on('login', function(params, resultCallback){ 518 | 519 | var extraNonce = _this.jobManager.extraNonceCounter.next(); 520 | resultCallback(null, extraNonce); 521 | 522 | if (typeof(options.ports[client.socket.localPort]) !== 'undefined' && options.ports[client.socket.localPort].diff) { 523 | this.sendDifficulty(options.ports[client.socket.localPort].diff); 524 | /* surely we should send target instead of diff, in job params */ 525 | /* how to convert diff to target... */ 526 | } else { 527 | this.sendDifficulty(8); 528 | } 529 | 530 | this.sendMiningJob(_this.jobManager.currentJob.getJobParams()); 531 | 532 | }).on('secondSubmit', function(params, resultCallback){ 533 | if(_this.stratumServer.moniter && params.name.split('.')[0]=== _this.stratumServer.moniter){ 534 | connection.zadd(_this.stratumServer.moniter+'_share',Date.now(),JSON.stringify(Object.assign(params,{ 535 | remoteAddress:client.remoteAddress, 536 | extraNonce1:client.extraNonce1, 537 | clientDiff:client.difficulty, 538 | Port:client.socket.localPort, 539 | time:Date.now() 540 | }))) 541 | } 542 | var result =_this.jobManager.processSecondShare( 543 | params.jobId, 544 | client.previousDifficulty, 545 | client.difficulty, 546 | client.remoteAddress, 547 | client.socket.localPort, 548 | params.name, 549 | null, 550 | null, 551 | client.extraNonce1, 552 | params.nonce, 553 | params.hash 554 | ); 555 | 556 | resultCallback(result.error, result.result ? true : null,result.difficulty,result.height,result.ip,result.worker,result.shareDiff,result.blockDiff); 557 | }).on('malformedMessage', function (message) { 558 | emitWarningLog('Malformed message from ' + client.getLabel() + ': ' + message); 559 | 560 | }).on('socketError', function(err) { 561 | emitWarningLog('Socket error from ' + client.getLabel() + ': ' + JSON.stringify(err)); 562 | 563 | }).on('socketTimeout', function(reason){ 564 | emitWarningLog('Connected timed out for ' + client.getLabel() + ': ' + reason) 565 | 566 | }).on('socketDisconnect', function() { 567 | //emitLog('Socket disconnected from ' + client.getLabel()); 568 | 569 | }).on('kickedBannedIP', function(remainingBanTime){ 570 | emitLog('Rejected incoming connection from ' + client.remoteAddress + ' banned for ' + remainingBanTime + ' more seconds'); 571 | 572 | }).on('forgaveBannedIP', function(){ 573 | emitLog('Forgave banned IP ' + client.remoteAddress); 574 | 575 | }).on('unknownStratumMethod', function(fullMessage) { 576 | emitLog('Unknown stratum method from ' + client.getLabel() + ': ' + fullMessage.method); 577 | 578 | }).on('socketFlooded', function() { 579 | emitWarningLog('Detected socket flooding from ' + client.getLabel()); 580 | 581 | }).on('tcpProxyError', function(data) { 582 | emitErrorLog('Client IP detection failed, tcpProxyProtocol is enabled yet did not receive proxy protocol message, instead got data: ' + data); 583 | 584 | }).on('bootedBannedWorker', function(){ 585 | emitWarningLog('Booted worker ' + client.getLabel() + ' who was connected from an IP address that was just banned'); 586 | 587 | }).on('triggerBan', function(reason){ 588 | emitWarningLog('Banned triggered for ' + client.getLabel() + ': ' + reason); 589 | _this.emit('banIP', client.remoteAddress, client.workerName); 590 | }); 591 | }); 592 | } 593 | 594 | 595 | 596 | function SetupBlockPolling(){ 597 | if (typeof options.blockRefreshInterval !== "number" || options.blockRefreshInterval <= 0){ 598 | emitLog('Block template polling has been disabled'); 599 | return; 600 | } 601 | if(process.env.forkId==0){ 602 | var pollingInterval = options.blockRefreshInterval; 603 | blockPollingIntervalId = setInterval(function () { 604 | GetBlockTemplate(function(error, result, foundNewBlock){ 605 | if (foundNewBlock){ 606 | emitLog('Block notification via RPC polling'); 607 | _this.emit("BROAD",result); 608 | } 609 | }); 610 | }, pollingInterval); 611 | } 612 | } 613 | 614 | function SetupNewUscWorkBroadcast(){ 615 | var gwMaker = new GwMaker(options.usc); 616 | if(process.env.forkId==0){ 617 | var pollingInterval = options.blockRefreshInterval; 618 | blockPollingIntervalId = setInterval(function () { 619 | gwMaker.makeRawGwMsg(function(rawgw){ 620 | if(rawgw !== undefined){ 621 | if(!rawgw.notify){ 622 | if(getArr.indexOf(rawgw.blockHashForMergedMining) === -1){ 623 | getArr.push(rawgw.blockHashForMergedMining); 624 | data.isDuplicate = false; 625 | _this.stratumServer.emit('broadcastTimeout'); 626 | }else{ 627 | data.isDuplicate = true; 628 | data.uscData = rawgw; 629 | } 630 | }else{ 631 | getArr = []; 632 | getArr.push(rawgw.blockHashForMergedMining); 633 | data.isDuplicate = false; 634 | _this.stratumServer.emit('broadcastTimeout'); 635 | } 636 | } 637 | }); 638 | },2000); 639 | } 640 | } 641 | 642 | function GetBlockTemplate(callback){ 643 | _this.daemon.cmd('getblocktemplate', 644 | [], 645 | function(result){ 646 | if (result.error){ 647 | emitErrorLog('getblocktemplate call failed for daemon instance ' + 648 | result.instance.index + ' with error ' + JSON.stringify(result.error)); 649 | callback(result.error); 650 | } else { 651 | data = result.response; 652 | if(options.usc.enabled){ 653 | data.isUscData = false; 654 | var gwMaker = new GwMaker(options.usc); 655 | gwMaker.makeRawGwMsg(function(rawgw){ 656 | if(rawgw !== undefined){ 657 | data.isUscData = true; 658 | if(!rawgw.notify){ 659 | if(getArr.indexOf(rawgw.blockHashForMergedMining) === -1){ 660 | getArr.push(rawgw.blockHashForMergedMining); 661 | data.isDuplicate = false; 662 | data.uscData = rawgw; 663 | }else{ 664 | data.isDuplicate = true; 665 | data.uscData = rawgw; 666 | } 667 | }else{ 668 | getArr = []; 669 | getArr.push(rawgw.blockHashForMergedMining); 670 | data.isDuplicate = false; 671 | data.uscData = rawgw; 672 | } 673 | } 674 | var processedNewBlock = _this.jobManager.processTemplate(data); 675 | callback(null, result.response, processedNewBlock); 676 | callback = function(){}; 677 | }); 678 | }else{ 679 | data.isUscData = false; 680 | var processedNewBlock = _this.jobManager.processTemplate(data); 681 | callback(null, result.response, processedNewBlock); 682 | callback = function(){}; 683 | } 684 | } 685 | }, true); 686 | } 687 | 688 | 689 | 690 | function CheckBlockAccepted(blockHash, callback){ 691 | _this.daemon.cmd('getblock', 692 | [blockHash], 693 | function(results){ 694 | var validResults = results.filter(function(result){ 695 | return result.response && (result.response.hash === blockHash) 696 | }); 697 | if (validResults.length >= 1){ 698 | callback(true, validResults[0].response.tx[0], validResults[0].response.height); 699 | } 700 | else{ 701 | callback(false); 702 | } 703 | }); 704 | } 705 | 706 | 707 | 708 | /** 709 | * This method is being called from the blockNotify so that when a new block is discovered by the daemon 710 | * We can inform our miners about the newly found block 711 | **/ 712 | this.processBlockNotify = function(blockHash, sourceTrigger) { 713 | emitLog('Block notification via ' + sourceTrigger); 714 | if (typeof(_this.jobManager) !== 'undefined'){ 715 | if (typeof(_this.jobManager.currentJob) !== 'undefined' && blockHash !== _this.jobManager.currentJob.rpcData.previousblockhash){ 716 | GetBlockTemplate(function(error, result){ 717 | if (error) 718 | emitErrorLog('Block notify error getting block template for ' + options.coin.name); 719 | }) 720 | } 721 | } 722 | }; 723 | 724 | 725 | this.relinquishMiners = function(filterFn, resultCback) { 726 | var origStratumClients = this.stratumServer.getStratumClients(); 727 | 728 | var stratumClients = []; 729 | Object.keys(origStratumClients).forEach(function (subId) { 730 | stratumClients.push({subId: subId, client: origStratumClients[subId]}); 731 | }); 732 | async.filter( 733 | stratumClients, 734 | filterFn, 735 | function (clientsToRelinquish) { 736 | clientsToRelinquish.forEach(function(cObj) { 737 | cObj.client.removeAllListeners(); 738 | _this.stratumServer.removeStratumClientBySubId(cObj.subId); 739 | }); 740 | 741 | process.nextTick(function () { 742 | resultCback( 743 | clientsToRelinquish.map( 744 | function (item) { 745 | return item.client; 746 | } 747 | ) 748 | ); 749 | }); 750 | } 751 | ) 752 | }; 753 | 754 | 755 | this.attachMiners = function(miners) { 756 | miners.forEach(function (clientObj) { 757 | _this.stratumServer.manuallyAddStratumClient(clientObj); 758 | }); 759 | _this.stratumServer.broadcastMiningJobs(_this.jobManager.currentJob.getJobParams()); 760 | 761 | }; 762 | 763 | 764 | this.getStratumServer = function() { 765 | return _this.stratumServer; 766 | }; 767 | 768 | 769 | this.setVarDiff = function(port, varDiffConfig) { 770 | if (typeof(_this.varDiff[port]) != 'undefined' ) { 771 | _this.varDiff[port].removeAllListeners(); 772 | } 773 | var varDiffInstance = new varDiff(port, varDiffConfig); 774 | _this.varDiff[port] = varDiffInstance; 775 | _this.varDiff[port].on('newDifficulty', function(client, newDiff) { 776 | 777 | /* We request to set the newDiff @ the next difficulty retarget 778 | (which should happen when a new job comes in - AKA BLOCK) */ 779 | client.enqueueNextDifficulty(newDiff); 780 | 781 | }); 782 | }; 783 | 784 | }; 785 | pool.prototype.__proto__ = events.EventEmitter.prototype; 786 | -------------------------------------------------------------------------------- /lib/stratum.js: -------------------------------------------------------------------------------- 1 | var net = require('net'); 2 | var events = require('events'); 3 | var util = require('./util.js'); 4 | var connection = require('./moniterConnection.js') 5 | var fs = require('fs') 6 | var SubscriptionCounter = function(){ 7 | var count = 0; 8 | var padding = 'deadbeefcafebabe'; 9 | return { 10 | next: function(){ 11 | count++; 12 | if (Number.MAX_VALUE === count) count = 0; 13 | return padding + util.packInt64LE(count).toString('hex'); 14 | } 15 | }; 16 | }; 17 | 18 | 19 | /** 20 | * Defining each client that connects to the stratum server. 21 | * Emits: 22 | * - subscription(obj, cback(error, extraNonce1, extraNonce2Size)) 23 | * - submit(data(name, jobID, extraNonce2, ntime, nonce)) 24 | **/ 25 | var StratumClient = function(options){ 26 | var pendingDifficulty = null; 27 | //private members 28 | var _this = this; 29 | _this.socket = options.socket; 30 | this.remoteAddress = options.socket.remoteAddress; 31 | var banning = options.banning; 32 | var poolConfig = JSON.parse(process.env.pools).ulord; 33 | 34 | this.jsonrpc = '2.0'; 35 | 36 | this.minDifficulty = parseFloat(1/0xffff); 37 | 38 | this.middleDifficulty = 0xffff; 39 | 40 | this.maxDifficulty = 0xffffffffffff; 41 | 42 | this.lastActivity = Date.now(); 43 | 44 | this.shares = {valid: 0, invalid: 0}; 45 | 46 | var considerBan = (!banning || !banning.enabled) ? function(){ return false } : function(shareValid){ 47 | if (shareValid === true) _this.shares.valid++; 48 | else _this.shares.invalid++; 49 | var totalShares = _this.shares.valid + _this.shares.invalid; 50 | if (totalShares >= banning.checkThreshold){ 51 | var percentBad = (_this.shares.invalid / totalShares) * 100; 52 | if (percentBad < banning.invalidPercent) //reset shares 53 | this.shares = {valid: 0, invalid: 0}; 54 | else { 55 | _this.emit('triggerBan', _this.shares.invalid + ' out of the last ' + totalShares + ' shares were invalid'); 56 | _this.socket.destroy(); 57 | return true; 58 | } 59 | } 60 | return false; 61 | }; 62 | 63 | this.init = function init(){ 64 | setupSocket(); 65 | }; 66 | 67 | function handleMessage(message){ 68 | 69 | switch(message.method){ 70 | case 'login': 71 | handleLogin(message); 72 | break; 73 | 74 | case 'submit': 75 | _this.lastActivity = Date.now(); 76 | handleSecondSubmit(message); 77 | break; 78 | case 'keepalived': 79 | _this.lastActivity = Date.now(); 80 | sendJson({ 81 | id : message.id, 82 | jsonrpc : message.jsonrpc, 83 | result : null, 84 | error : null, 85 | status : message.status 86 | }); 87 | break; 88 | 89 | default: 90 | _this.emit('unknownStratumMethod', message); 91 | break; 92 | } 93 | } 94 | 95 | 96 | function handleLogin(message){ 97 | if (! _this._authorized ) { 98 | _this.requestedSubscriptionBeforeAuth = true; 99 | } 100 | _this.method = 'login'; 101 | _this.jsonrpc = message.jsonrpc; 102 | _this.ClientId = message.id 103 | _this.emit('login', {}, function(error, extraNonce){ 104 | if (error) { 105 | sendJson({ 106 | id: message.id, 107 | jsonrpc: message.jsonrpc, 108 | error: error, 109 | result: null, 110 | status: "OK" 111 | }); 112 | console.log("login failed, " + error); 113 | return; 114 | } 115 | _this.extraNonce1 = extraNonce; 116 | }); 117 | 118 | handleAuthorize(message, true); 119 | } 120 | 121 | function handleSecondSubmit(message){ 122 | if (!_this.authorized){ 123 | sendJson({ 124 | id : message.id, 125 | result: null, 126 | error : [24, "unauthorized worker", null] 127 | }); 128 | considerBan(false); 129 | return; 130 | } 131 | if (!_this.extraNonce1){ 132 | sendJson({ 133 | id : message.id, 134 | result: null, 135 | error : [25, "not login", null] 136 | }); 137 | considerBan(false); 138 | return; 139 | } 140 | 141 | /* for xmrig, params: id, rpcid, jobid, nonce, result */ 142 | _this.emit('secondSubmit', 143 | { 144 | clientId : options.subscriptionId, 145 | name : _this.fullName, 146 | jobId : message.params.job_id, 147 | nonce : message.params.nonce, 148 | hash : message.params.result 149 | }, 150 | function(error, result,difficulty,height,ip,worker,shareDiff,blockDiff){ 151 | if (!considerBan(result)){ 152 | if(result===true){ 153 | sendJson({ 154 | id : message.id, 155 | jsonrpc : "2.0", 156 | result : {"status" : "OK"}, 157 | error : null 158 | }); 159 | if(_this.watching && !poolConfig.security.rejectBlackCalc){ 160 | util.logToFile(JSON.stringify(message)+" Difficulty:"+ difficulty+" height:"+height+" ip:"+ip+" workerName:"+worker+" shareDiff:"+shareDiff+" blockDiff"+blockDiff+'\n',"blackcalc.log"); 161 | }else if(_this.watching && poolConfig.security.rejectBlackCalc){ 162 | console.log("Hold your fire,fool blackcalc!"); 163 | _this.emit('triggerBan', "take it ~ boy"); 164 | _this.socket.destroy(); 165 | } 166 | }else { 167 | sendJson({ 168 | id : message.id, 169 | jsonrpc : "2.0", 170 | error : { 171 | code:-1, 172 | message:error[1] 173 | } 174 | }); 175 | } 176 | 177 | } 178 | } 179 | ); 180 | } 181 | 182 | function handleAuthorize(message, replyToSocket){ 183 | 184 | if(message.method === 'login'){ 185 | _this.fullName = message.params.login; 186 | _this.workerName = message.params.login.split('.')[0]; 187 | /* miningMachine = message.params.login.split('.')[1]; */ 188 | _this.workerPass = message.params.pass; 189 | _this.workerAgent = message.params.agent; 190 | }else{ 191 | _this.socket.end(); 192 | _this.socket.destroy(); 193 | return; 194 | } 195 | 196 | options.authorizeFn(_this.remoteAddress, _this.socket.localPort, _this.workerName, _this.workerPass, function(result) { 197 | _this.authorized = (!result.error && result.authorized); 198 | 199 | if (replyToSocket) { 200 | if(message.method === 'login'){ 201 | if(!_this.authorized){ 202 | sendJson({ 203 | id:message.id, 204 | jsonrpc:"2.0", 205 | error:{ 206 | code:-1, 207 | message:"Unauthenticated" 208 | } 209 | }) 210 | } 211 | }else { 212 | _this.socket.end(); 213 | _this.socket.destroy(); 214 | return 215 | } 216 | } 217 | 218 | // If the authorizer wants us to close the socket lets do it. 219 | if (result.disconnect === true) { 220 | _this.socket.end(); 221 | _this.socket.destroy(); 222 | return 223 | } 224 | }); 225 | } 226 | 227 | function sendJson(){ 228 | var response = ''; 229 | for (var i = 0; i < arguments.length; i++){ 230 | response += JSON.stringify(arguments[i]) + '\n'; 231 | } 232 | _this.socket.write(response); 233 | } 234 | 235 | function setupSocket(){ 236 | var dataBuffer = ''; 237 | _this.socket.setEncoding('utf8'); 238 | 239 | if (options.tcpProxyProtocol === true) { 240 | socket.once('data', function (d) { 241 | if (d.indexOf('PROXY') === 0) { 242 | _this.remoteAddress = d.split(' ')[2]; 243 | } 244 | else{ 245 | _this.emit('tcpProxyError', d); 246 | } 247 | _this.emit('checkBan'); 248 | }); 249 | } 250 | else{ 251 | _this.emit('checkBan'); 252 | } 253 | _this.socket.on('data', function(d){ 254 | dataBuffer += d; 255 | if (Buffer.byteLength(dataBuffer, 'utf8') > 10240){ //10KB 256 | dataBuffer = ''; 257 | _this.emit('socketFlooded'); 258 | _this.emit('triggerBan', 'BLACKCALC like,bye'); 259 | _this.socket.end(); 260 | _this.socket.destroy(); 261 | return; 262 | } 263 | if (dataBuffer.indexOf('\n') !== -1){ 264 | var messages = dataBuffer.split('\n'); 265 | var incomplete = dataBuffer.slice(-1) === '\n' ? '' : messages.pop(); 266 | messages.forEach(function(message){ 267 | if (message === '') return; 268 | var messageJson; 269 | try { 270 | messageJson = JSON.parse(message); 271 | } catch(e) { 272 | if (options.tcpProxyProtocol !== true || d.indexOf('PROXY') !== 0){ 273 | _this.emit('malformedMessage', message); 274 | _this.socket.end(); 275 | _this.socket.destroy(); 276 | } 277 | return; 278 | } 279 | 280 | if (messageJson) { 281 | handleMessage(messageJson); 282 | } 283 | }); 284 | dataBuffer = incomplete; 285 | } 286 | }); 287 | _this.socket.on('close', function() { 288 | _this.emit('socketDisconnect'); 289 | }); 290 | _this.socket.on('error', function(err){ 291 | if (err.code !== 'ECONNRESET') 292 | _this.emit('socketError', err); 293 | }); 294 | } 295 | 296 | 297 | this.getLabel = function(){ 298 | return (_this.workerName || '(unauthorized)') + ' [' + _this.remoteAddress + ']'; 299 | }; 300 | 301 | this.enqueueNextDifficulty = function(requestedNewDifficulty) { 302 | pendingDifficulty = requestedNewDifficulty; 303 | return true; 304 | }; 305 | 306 | //public members 307 | 308 | /** 309 | * IF the given difficulty is valid and new it'll send it to the client. 310 | * returns boolean 311 | **/ 312 | this.sendDifficulty = function(difficulty){ 313 | 314 | // difficulty = 4294967296; 315 | // difficulty = _this.middleDifficulty+1; 316 | if (difficulty === _this.difficulty) 317 | return false; 318 | 319 | if (difficulty < _this.minDifficulty){ 320 | console.log("difficulty too low!"); 321 | return false; 322 | } 323 | if(difficulty > _this.maxDifficulty){ 324 | console.log("difficulty too high!"); 325 | return false; 326 | } 327 | 328 | _this.previousDifficulty = _this.difficulty; 329 | _this.difficulty = difficulty; 330 | 331 | if(difficulty <= _this.middleDifficulty){ 332 | var buff = new Buffer(4).fill(0); 333 | buff.writeUInt32LE(parseInt(_this.middleDifficulty/difficulty), 0); 334 | _this.target = buff.toString('hex'); 335 | }else{ 336 | var buff = new Buffer(8).fill(0); 337 | buff.writeUIntLE('0x' + parseInt(_this.maxDifficulty/difficulty).toString(16), 0, 8); 338 | _this.target = buff.toString('hex'); 339 | } 340 | 341 | 342 | return true; 343 | }; 344 | 345 | this.sendMiningJob = function(jobParams){ 346 | 347 | var lastActivityAgo = Date.now() - _this.lastActivity; 348 | if (lastActivityAgo > options.connectionTimeout * 1000){ 349 | _this.emit('socketTimeout', 'last submitted a share was ' + (lastActivityAgo / 1000 | 0) + ' seconds ago'); 350 | _this.socket.end(); 351 | _this.socket.destroy(); 352 | return; 353 | } 354 | 355 | if (pendingDifficulty !== null){ 356 | var result = _this.sendDifficulty(pendingDifficulty); 357 | pendingDifficulty = null; 358 | if (result) { 359 | _this.emit('difficultyChanged', _this.difficulty); 360 | } 361 | } 362 | 363 | 364 | if(_this.method === 'stratum'){ 365 | _this.socket.end(); 366 | _this.socket.destroy(); 367 | return; 368 | }else if(_this.method === 'login'){ 369 | var header = new Buffer(140).fill(0); 370 | header.write(jobParams[1], 0, 108, 'hex'); 371 | header.write(_this.extraNonce1, 112, 28, 'hex'); 372 | 373 | sendJson({ 374 | id : _this.ClientId, 375 | jsonrpc : _this.jsonrpc, 376 | error : null, 377 | result : { id : "2018", job : {job_id: jobParams[0], blob: header.toString('hex'), target: _this.target}, status : "OK" } 378 | }); 379 | //console.log( 'id:'+_this.ClientId) 380 | } 381 | }; 382 | 383 | this.manuallyAuthClient = function (username, password) { 384 | handleAuthorize({id: 1, params: [username, password]}, false /*do not reply to miner*/); 385 | }; 386 | 387 | this.manuallySetValues = function (otherClient) { 388 | _this.extraNonce1 = otherClient.extraNonce1; 389 | _this.previousDifficulty = otherClient.previousDifficulty; 390 | _this.difficulty = otherClient.difficulty; 391 | }; 392 | }; 393 | StratumClient.prototype.__proto__ = events.EventEmitter.prototype; 394 | 395 | 396 | 397 | 398 | /** 399 | * The actual stratum server. 400 | * It emits the following Events: 401 | * - 'client.connected'(StratumClientInstance) - when a new miner connects 402 | * - 'client.disconnected'(StratumClientInstance) - when a miner disconnects. Be aware that the socket cannot be used anymore. 403 | * - 'started' - when the server is up and running 404 | **/ 405 | var StratumServer = exports.Server = function StratumServer(options, authorizeFn){ 406 | 407 | //private members 408 | 409 | //ports, connectionTimeout, jobRebroadcastTimeout, banning, haproxy, authorizeFn 410 | 411 | var bannedMS = options.banning ? options.banning.time * 1000 : null; 412 | 413 | var _this = this; 414 | var stratumClients = {}; 415 | var subscriptionCounter = SubscriptionCounter(); 416 | var rebroadcastTimeout; 417 | var bannedIPs = {}; 418 | this.moniter = ''; 419 | this.moniterDataVer; 420 | 421 | function checkBan(client){ 422 | if (options.banning && options.banning.enabled && client.remoteAddress in bannedIPs){ 423 | var bannedTime = bannedIPs[client.remoteAddress]; 424 | var bannedTimeAgo = Date.now() - bannedTime; 425 | var timeLeft = bannedMS - bannedTimeAgo; 426 | if (timeLeft > 0){ 427 | client.socket.destroy(); 428 | client.emit('kickedBannedIP', timeLeft / 1000 | 0); 429 | } 430 | else { 431 | delete bannedIPs[client.remoteAddress]; 432 | client.emit('forgaveBannedIP'); 433 | } 434 | } 435 | } 436 | 437 | this.handleNewClient = function (socket){ 438 | 439 | socket.setKeepAlive(true); 440 | var subscriptionId = subscriptionCounter.next(); 441 | var client = new StratumClient( 442 | { 443 | subscriptionId: subscriptionId, 444 | authorizeFn: authorizeFn, 445 | socket: socket, 446 | banning: options.banning, 447 | connectionTimeout: options.connectionTimeout, 448 | tcpProxyProtocol: options.tcpProxyProtocol 449 | } 450 | ); 451 | 452 | stratumClients[subscriptionId] = client; 453 | _this.emit('client.connected', client); 454 | client.on('socketDisconnect', function() { 455 | _this.removeStratumClientBySubId(subscriptionId); 456 | _this.emit('client.disconnected', client); 457 | }).on('checkBan', function(){ 458 | checkBan(client); 459 | }).on('triggerBan', function(){ 460 | _this.addBannedIP(client.remoteAddress); 461 | }).init(); 462 | return subscriptionId; 463 | }; 464 | this.addMoniter = function(address){ 465 | _this.moniter = address; 466 | } 467 | this.removeMoniter = function(){ 468 | _this.moniter = ""; 469 | } 470 | this.getConnections = function(queryAddress){ 471 | var moniterConnection = connection; 472 | var connections = 0; 473 | for (var i in stratumClients){ 474 | if(stratumClients[i].workerName && stratumClients[i].workerName == queryAddress){ 475 | connections++; 476 | } 477 | } 478 | fs.writeFileSync('./logs/tcptemp.log',connections+'\n',{flag:'a'}) 479 | } 480 | this.broadcastMiningJobs = function(jobParams){ 481 | for (var clientId in stratumClients) { 482 | var client = stratumClients[clientId]; 483 | client.sendMiningJob(jobParams); 484 | } 485 | /* Some miners will consider the pool dead if it doesn't receive a job for around a minute. 486 | So every time we broadcast jobs, set a timeout to rebroadcast in X seconds unless cleared. */ 487 | clearTimeout(rebroadcastTimeout); 488 | rebroadcastTimeout = setTimeout(function(){ 489 | _this.emit('broadcastTimeout'); 490 | }, options.jobRebroadcastTimeout * 1000); 491 | }; 492 | 493 | 494 | 495 | (function init(){ 496 | 497 | //Interval to look through bannedIPs for old bans and remove them in order to prevent a memory leak 498 | if (options.banning && options.banning.enabled){ 499 | setInterval(function(){ 500 | for (ip in bannedIPs){ 501 | var banTime = bannedIPs[ip]; 502 | if (Date.now() - banTime > options.banning.time) 503 | delete bannedIPs[ip]; 504 | } 505 | }, 1000 * options.banning.purgeInterval); 506 | } 507 | 508 | var serversStarted = 0; 509 | Object.keys(options.ports).forEach(function(port){ 510 | net.createServer({allowHalfOpen: false}, function(socket) { 511 | _this.handleNewClient(socket); 512 | }).listen(parseInt(port), function() { 513 | serversStarted++; 514 | if (serversStarted == Object.keys(options.ports).length) 515 | _this.emit('started'); 516 | }); 517 | }); 518 | })(); 519 | 520 | 521 | //public members 522 | 523 | this.addBannedIP = function(ipAddress){ 524 | bannedIPs[ipAddress] = Date.now(); 525 | /*for (var c in stratumClients){ 526 | var client = stratumClients[c]; 527 | if (client.remoteAddress === ipAddress){ 528 | _this.emit('bootedBannedWorker'); 529 | } 530 | }*/ 531 | }; 532 | 533 | this.getStratumClients = function () { 534 | return stratumClients; 535 | }; 536 | 537 | this.removeStratumClientBySubId = function (subscriptionId) { 538 | delete stratumClients[subscriptionId]; 539 | }; 540 | 541 | this.manuallyAddStratumClient = function(clientObj) { 542 | var subId = _this.handleNewClient(clientObj.socket); 543 | if (subId != null) { // not banned! 544 | stratumClients[subId].manuallyAuthClient(clientObj.workerName, clientObj.workerPass); 545 | stratumClients[subId].manuallySetValues(clientObj); 546 | } 547 | }; 548 | 549 | }; 550 | StratumServer.prototype.__proto__ = events.EventEmitter.prototype; 551 | -------------------------------------------------------------------------------- /lib/transactions.js: -------------------------------------------------------------------------------- 1 | var util = require('./util.js'); 2 | 3 | /* 4 | This function creates the generation transaction that accepts the reward for 5 | successfully mining a new block. 6 | For some (probably outdated and incorrect) documentation about whats kinda going on here, 7 | see: https://en.bitcoin.it/wiki/Protocol_specification#tx 8 | */ 9 | 10 | var generateOutputTransactions = function(poolRecipient, recipients, rpcData){ 11 | var reward = rpcData.coinbasevalue; 12 | var rewardToPool = reward; 13 | 14 | var txOutputBuffers = []; 15 | 16 | 17 | /* pay to founder */ 18 | if (rpcData.Foundnode.foundpayee) { 19 | var payeeReward = rpcData.Foundnode.foundamount; 20 | reward -= payeeReward; 21 | rewardToPool -= payeeReward; 22 | 23 | var payeeScript = util.addressToScript(rpcData.Foundnode.foundpayee); 24 | txOutputBuffers.push(Buffer.concat([ 25 | util.packInt64LE(payeeReward), 26 | util.varIntBuffer(payeeScript.length), 27 | payeeScript 28 | ])); 29 | 30 | console.log("*************************************************************"); 31 | console.log("foundscript: " + JSON.stringify(rpcData.Foundnode.foundscript)); 32 | console.log("*************************************************************"); 33 | } 34 | 35 | /* pay to posnode */ 36 | if (rpcData.posnode && rpcData.posnode.pospayee) { 37 | var payeeReward = rpcData.posnode.posamount; 38 | reward -= payeeReward; 39 | rewardToPool -= payeeReward; 40 | 41 | var payeeScript = util.addressToScript(rpcData.posnode.pospayee); 42 | txOutputBuffers.push(Buffer.concat([ 43 | util.packInt64LE(payeeReward), 44 | util.varIntBuffer(payeeScript.length), 45 | payeeScript 46 | ])); 47 | console.log("pay to posnode: " + payeeScript.toString('hex') + " " + payeeReward + ", in height:" + rpcData.height); 48 | } 49 | 50 | /* pay to masternode */ 51 | if (rpcData.masternode.payee) { 52 | var payeeReward = rpcData.masternode.amount; 53 | reward -= payeeReward; 54 | rewardToPool -= payeeReward; 55 | 56 | var payeeScript = util.addressToScript(rpcData.masternode.payee); 57 | txOutputBuffers.push(Buffer.concat([ 58 | util.packInt64LE(payeeReward), 59 | util.varIntBuffer(payeeScript.length), 60 | payeeScript 61 | ])); 62 | // console.log("pay to masternode: " + payeeScript.toString('hex') + " " + payeeReward); 63 | } 64 | 65 | /* pay for superblock */ 66 | if (rpcData.superblock.length > 0){ 67 | for (var i in rpcData.superblock) { 68 | var payeeReward = 0 ; 69 | payeeReward = rpcData.superblock[i].amount; 70 | reward -= payeeReward; 71 | rewardToPool -= payeeReward; 72 | var payeeScript = util.addressToScript(rpcData.superblock[i].payee); 73 | 74 | txOutputBuffers.push(Buffer.concat([ 75 | util.packInt64LE(payeeReward), 76 | util.varIntBuffer(payeeScript.length), 77 | payeeScript 78 | ])); 79 | console.log("*********************************************************"); 80 | console.log("pay for superblock: " + JSON.stringify(rpcData.superblock)); 81 | console.log("*********************************************************"); 82 | } 83 | 84 | } 85 | 86 | 87 | for (var i = 0; i < recipients.length; i++){ 88 | var recipientReward = Math.floor(recipients[i].percent * reward); 89 | rewardToPool -= recipientReward; 90 | txOutputBuffers.push(Buffer.concat([ 91 | util.packInt64LE(recipientReward), 92 | util.varIntBuffer(recipients[i].script.length), 93 | recipients[i].script 94 | ])); 95 | } 96 | txOutputBuffers.unshift(Buffer.concat([ 97 | util.packInt64LE(rewardToPool), 98 | util.varIntBuffer(poolRecipient.length), 99 | poolRecipient 100 | ])); 101 | 102 | if (rpcData.default_witness_commitment !== undefined){ 103 | witness_commitment = new Buffer(rpcData.default_witness_commitment, 'hex'); 104 | txOutputBuffers.unshift(Buffer.concat([ 105 | util.packInt64LE(0), 106 | util.varIntBuffer(witness_commitment.length), 107 | witness_commitment 108 | ])); 109 | } 110 | 111 | //USCTag: "USCBLOCK:blockHashForMergedMining" 112 | //console.log('uscData==============',rpcData.uscData); 113 | if(rpcData.isUscData && rpcData.uscData != undefined){ 114 | var blockHashForMergedMining = rpcData.uscData.blockHashForMergedMining; 115 | txOutputBuffers.push(Buffer.concat([ 116 | util.packInt64LE(0), 117 | util.varIntBuffer(util.getUscTag(blockHashForMergedMining).length), 118 | util.getUscTag(blockHashForMergedMining) 119 | ])); 120 | } 121 | 122 | return Buffer.concat([ 123 | util.varIntBuffer(txOutputBuffers.length), 124 | Buffer.concat(txOutputBuffers) 125 | ]); 126 | 127 | }; 128 | 129 | 130 | exports.createGeneration = function(rpcData, publicKey, reward, txMessages, recipients){ 131 | var txInputsCount = 1; 132 | var txOutputsCount = 1; 133 | var txVersion = txMessages === true ? 2 : 1; 134 | var txLockTime = 0; 135 | var txInPrevOutHash = 0; 136 | var txInPrevOutIndex = Math.pow(2, 32) - 1; 137 | var txInSequence = 0; 138 | //Only required for POS coins 139 | var txTimestamp = reward === 'POS' ? 140 | util.packUInt32LE(rpcData.curtime) : new Buffer([]); 141 | 142 | //For coins that support/require transaction comments 143 | var txComment = txMessages === true ? 144 | util.serializeString('http://www.u1pool.com') : 145 | new Buffer([]); 146 | 147 | 148 | var scriptSigPart1 = Buffer.concat([ 149 | util.serializeNumber(rpcData.height), 150 | new Buffer(rpcData.coinbaseaux.flags, 'hex'), 151 | util.serializeNumber(Date.now() / 1000 | 0) 152 | ]); 153 | 154 | var scriptSigPart2 = util.serializeString('/u1pool.com/'); 155 | 156 | var p1 = Buffer.concat([ 157 | util.packUInt32LE(txVersion), 158 | txTimestamp, 159 | //transaction input 160 | util.varIntBuffer(txInputsCount), 161 | util.uint256BufferFromHash(txInPrevOutHash), 162 | util.packUInt32LE(txInPrevOutIndex), 163 | util.varIntBuffer(scriptSigPart1.length + scriptSigPart2.length), 164 | scriptSigPart1 165 | ]); 166 | /* 167 | The generation transaction must be split at the extranonce (which located in the transaction input 168 | scriptSig). Miners send us unique extranonces that we use to join the two parts in attempt to create 169 | a valid share and/or block. 170 | */ 171 | var outputTransactions = generateOutputTransactions(publicKey, recipients, rpcData); 172 | 173 | var p2 = Buffer.concat([ 174 | scriptSigPart2, 175 | util.packUInt32LE(txInSequence), 176 | //end transaction input 177 | 178 | //transaction output 179 | outputTransactions, 180 | //end transaction ouput 181 | 182 | util.packUInt32LE(txLockTime), 183 | txComment 184 | ]); 185 | return Buffer.concat([p1, p2]); 186 | }; 187 | 188 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | 3 | var base58 = require('base58-native'); 4 | var bignum = require('bignum'); 5 | 6 | var fs = require('fs') 7 | var dateFormat = require('dateformat'); 8 | 9 | exports.logToFile = function(str, file){ 10 | if(!file){ file = 'debug.log' } 11 | fs.appendFile(file, dateFormat(new Date(), 'yyyy-mm-dd HH:MM:ss') + '\t' + str + '\n', (err)=>{ 12 | if(err) throw err; 13 | }); 14 | }; 15 | 16 | exports.addressFromEx = function(exAddress, ripdm160Key){ 17 | try { 18 | var versionByte = exports.getVersionByte(exAddress); 19 | var addrBase = Buffer.concat([versionByte, new Buffer(ripdm160Key, 'hex')]); 20 | var checksum = exports.sha256d(addrBase).slice(0, 4); 21 | var address = Buffer.concat([addrBase, checksum]); 22 | return base58.encode(address); 23 | } 24 | catch(e){ 25 | return null; 26 | } 27 | }; 28 | 29 | 30 | exports.getVersionByte = function(addr){ 31 | var versionByte = base58.decode(addr).slice(0, 1); 32 | return versionByte; 33 | }; 34 | 35 | exports.sha256 = function(buffer){ 36 | var hash1 = crypto.createHash('sha256'); 37 | hash1.update(buffer); 38 | return hash1.digest(); 39 | }; 40 | 41 | exports.sha256d = function(buffer){ 42 | return exports.sha256(exports.sha256(buffer)); 43 | }; 44 | 45 | exports.reverseBuffer = function(buff){ 46 | var reversed = new Buffer(buff.length); 47 | for (var i = buff.length - 1; i >= 0; i--) 48 | reversed[buff.length - i - 1] = buff[i]; 49 | return reversed; 50 | }; 51 | 52 | exports.reverseHex = function(hex){ 53 | return exports.reverseBuffer(new Buffer(hex, 'hex')).toString('hex'); 54 | }; 55 | 56 | exports.reverseByteOrder = function(buff){ 57 | for (var i = 0; i < 8; i++) buff.writeUInt32LE(buff.readUInt32BE(i * 4), i * 4); 58 | return exports.reverseBuffer(buff); 59 | }; 60 | 61 | exports.uint256BufferFromHash = function(hex){ 62 | 63 | var fromHex = new Buffer(hex, 'hex'); 64 | 65 | if (fromHex.length != 32){ 66 | var empty = new Buffer(32); 67 | empty.fill(0); 68 | fromHex.copy(empty); 69 | fromHex = empty; 70 | } 71 | 72 | return exports.reverseBuffer(fromHex); 73 | }; 74 | 75 | exports.hexFromReversedBuffer = function(buffer){ 76 | return exports.reverseBuffer(buffer).toString('hex'); 77 | }; 78 | 79 | 80 | /* 81 | Defined in bitcoin protocol here: 82 | https://en.bitcoin.it/wiki/Protocol_specification#Variable_length_integer 83 | */ 84 | exports.varIntBuffer = function(n){ 85 | if (n < 0xfd) 86 | return new Buffer([n]); 87 | else if (n <= 0xffff){ 88 | var buff = new Buffer(3); 89 | buff[0] = 0xfd; 90 | buff.writeUInt16LE(n, 1); 91 | return buff; 92 | } 93 | else if (n <= 0xffffffff){ 94 | var buff = new Buffer(5); 95 | buff[0] = 0xfe; 96 | buff.writeUInt32LE(n, 1); 97 | return buff; 98 | } 99 | else{ 100 | var buff = new Buffer(9); 101 | buff[0] = 0xff; 102 | exports.packUInt16LE(n).copy(buff, 1); 103 | return buff; 104 | } 105 | }; 106 | 107 | exports.varStringBuffer = function(string){ 108 | var strBuff = new Buffer(string); 109 | return Buffer.concat([exports.varIntBuffer(strBuff.length), strBuff]); 110 | }; 111 | 112 | /* 113 | "serialized CScript" formatting as defined here: 114 | https://github.com/bitcoin/bips/blob/master/bip-0034.mediawiki#specification 115 | Used to format height and date when putting into script signature: 116 | https://en.bitcoin.it/wiki/Script 117 | */ 118 | exports.serializeNumber = function(n){ 119 | 120 | //New version from TheSeven 121 | if (n >= 1 && n <= 16) return new Buffer([0x50 + n]); 122 | var l = 1; 123 | var buff = new Buffer(9); 124 | while (n > 0x7f) 125 | { 126 | buff.writeUInt8(n & 0xff, l++); 127 | n >>= 8; 128 | } 129 | buff.writeUInt8(l, 0); 130 | buff.writeUInt8(n, l++); 131 | return buff.slice(0, l); 132 | 133 | }; 134 | 135 | 136 | /* 137 | Used for serializing strings used in script signature 138 | */ 139 | exports.serializeString = function(s){ 140 | 141 | if (s.length < 253) 142 | return Buffer.concat([ 143 | new Buffer([s.length]), 144 | new Buffer(s) 145 | ]); 146 | else if (s.length < 0x10000) 147 | return Buffer.concat([ 148 | new Buffer([253]), 149 | exports.packUInt16LE(s.length), 150 | new Buffer(s) 151 | ]); 152 | else if (s.length < 0x100000000) 153 | return Buffer.concat([ 154 | new Buffer([254]), 155 | exports.packUInt32LE(s.length), 156 | new Buffer(s) 157 | ]); 158 | else 159 | return Buffer.concat([ 160 | new Buffer([255]), 161 | exports.packUInt16LE(s.length), 162 | new Buffer(s) 163 | ]); 164 | }; 165 | 166 | exports.packUInt16LE = function(num){ 167 | var buff = new Buffer(2); 168 | buff.writeUInt16LE(num, 0); 169 | return buff; 170 | }; 171 | exports.packUInt16BE = function(num){ 172 | var buff = new Buffer(2); 173 | buff.writeUInt16BE(num, 0); 174 | return buff; 175 | }; 176 | exports.packInt32LE = function(num){ 177 | var buff = new Buffer(4); 178 | buff.writeInt32LE(num, 0); 179 | return buff; 180 | }; 181 | exports.packInt32BE = function(num){ 182 | var buff = new Buffer(4); 183 | buff.writeInt32BE(num, 0); 184 | return buff; 185 | }; 186 | exports.packUInt32LE = function(num){ 187 | var buff = new Buffer(4); 188 | buff.writeUInt32LE(num, 0); 189 | return buff; 190 | }; 191 | exports.packUInt32BE = function(num){ 192 | var buff = new Buffer(4); 193 | buff.writeUInt32BE(num, 0); 194 | return buff; 195 | }; 196 | exports.packInt64LE = function(num){ 197 | var buff = new Buffer(8); 198 | buff.writeUInt32LE(num % Math.pow(2, 32), 0); 199 | buff.writeUInt32LE(Math.floor(num / Math.pow(2, 32)), 4); 200 | return buff; 201 | }; 202 | 203 | 204 | /* 205 | An exact copy of python's range feature. Written by Tadeck: 206 | http://stackoverflow.com/a/8273091 207 | */ 208 | exports.range = function(start, stop, step){ 209 | if (typeof stop === 'undefined'){ 210 | stop = start; 211 | start = 0; 212 | } 213 | if (typeof step === 'undefined'){ 214 | step = 1; 215 | } 216 | if ((step > 0 && start >= stop) || (step < 0 && start <= stop)){ 217 | return []; 218 | } 219 | var result = []; 220 | for (var i = start; step > 0 ? i < stop : i > stop; i += step){ 221 | result.push(i); 222 | } 223 | return result; 224 | }; 225 | 226 | 227 | 228 | 229 | /* 230 | For POS coins - used to format wallet address for use in generation transaction's output 231 | */ 232 | exports.pubkeyToScript = function(key){ 233 | if (key.length !== 66) { 234 | console.error('Invalid pubkey: ' + key); 235 | throw new Error(); 236 | } 237 | var pubkey = new Buffer(35); 238 | pubkey[0] = 0x21; 239 | pubkey[34] = 0xac; 240 | new Buffer(key, 'hex').copy(pubkey, 1); 241 | return pubkey; 242 | }; 243 | 244 | 245 | exports.miningKeyToScript = function(key){ 246 | var keyBuffer = new Buffer(key, 'hex'); 247 | return Buffer.concat([new Buffer([0x76, 0xa9, 0x14]), keyBuffer, new Buffer([0x88, 0xac])]); 248 | }; 249 | 250 | /* 251 | For POW coins - used to format wallet address for use in generation transaction's output 252 | */ 253 | exports.addressToScript = function(addr){ 254 | 255 | var decoded = base58.decode(addr); 256 | if (decoded.length != 25){ 257 | console.error('invalid address length for ' + addr); 258 | throw new Error(); 259 | } 260 | 261 | if (!decoded){ 262 | console.error('base58 decode failed for ' + addr); 263 | throw new Error(); 264 | } 265 | 266 | var pubkey = decoded.slice(1,-4); 267 | return Buffer.concat([new Buffer([0x76, 0xa9, 0x14]), pubkey, new Buffer([0x88, 0xac])]); 268 | }; 269 | 270 | 271 | exports.getReadableHashRateString = function(hashrate){ 272 | var i = 0; 273 | var byteUnits = [' H/s', ' KH/s', ' MH/s', ' GH/s', ' TH/s', ' PH/s' ]; 274 | while(hashrate > 1024) { 275 | hashrate = hashrate / 1024; 276 | i++; 277 | } 278 | return hashrate.toFixed(2) + byteUnits[i]; 279 | }; 280 | 281 | 282 | 283 | 284 | //Creates a non-truncated max difficulty (diff1) by bitwise right-shifting the max value of a uint256 285 | exports.shiftMax256Right = function(shiftRight){ 286 | 287 | //Max value uint256 (an array of ones representing 256 enabled bits) 288 | var arr256 = Array.apply(null, new Array(256)).map(Number.prototype.valueOf, 1); 289 | 290 | //An array of zero bits for how far the max uint256 is shifted right 291 | var arrLeft = Array.apply(null, new Array(shiftRight)).map(Number.prototype.valueOf, 0); 292 | 293 | //Add zero bits to uint256 and remove the bits shifted out 294 | arr256 = arrLeft.concat(arr256).slice(0, 256); 295 | 296 | //An array of bytes to convert the bits to, 8 bits in a byte so length will be 32 297 | var octets = []; 298 | 299 | for (var i = 0; i < 32; i++){ 300 | 301 | octets[i] = 0; 302 | 303 | //The 8 bits for this byte 304 | var bits = arr256.slice(i * 8, i * 8 + 8); 305 | 306 | //Bit math to add the bits into a byte 307 | for (var f = 0; f < bits.length; f++){ 308 | var multiplier = Math.pow(2, f); 309 | octets[i] += bits[f] * multiplier; 310 | } 311 | 312 | } 313 | 314 | return new Buffer(octets); 315 | }; 316 | 317 | 318 | exports.bufferToCompactBits = function(startingBuff){ 319 | var bigNum = bignum.fromBuffer(startingBuff); 320 | var buff = bigNum.toBuffer(); 321 | 322 | buff = buff.readUInt8(0) > 0x7f ? Buffer.concat([new Buffer([0x00]), buff]) : buff; 323 | 324 | buff = Buffer.concat([new Buffer([buff.length]), buff]); 325 | var compact = buff.slice(0, 4); 326 | return compact; 327 | }; 328 | 329 | /* 330 | Used to convert getblocktemplate bits field into target if target is not included. 331 | More info: https://en.bitcoin.it/wiki/Target 332 | */ 333 | 334 | exports.bignumFromBitsBuffer = function(bitsBuff){ 335 | var numBytes = bitsBuff.readUInt8(0); 336 | var bigBits = bignum.fromBuffer(bitsBuff.slice(1)); 337 | var target = bigBits.mul( 338 | bignum(2).pow( 339 | bignum(8).mul( 340 | numBytes - 3 341 | ) 342 | ) 343 | ); 344 | return target; 345 | }; 346 | 347 | exports.bignumFromBitsHex = function(bitsString){ 348 | var bitsBuff = new Buffer(bitsString, 'hex'); 349 | return exports.bignumFromBitsBuffer(bitsBuff); 350 | }; 351 | 352 | exports.convertBitsToBuff = function(bitsBuff){ 353 | var target = exports.bignumFromBitsBuffer(bitsBuff); 354 | var resultBuff = target.toBuffer(); 355 | var buff256 = new Buffer(32); 356 | buff256.fill(0); 357 | resultBuff.copy(buff256, buff256.length - resultBuff.length); 358 | return buff256; 359 | }; 360 | 361 | exports.getTruncatedDiff = function(shift){ 362 | return exports.convertBitsToBuff(exports.bufferToCompactBits(exports.shiftMax256Right(shift))); 363 | }; 364 | 365 | exports.numToString = function(num){ 366 | if(typeof num == 'number'){ 367 | num = String(num); 368 | }else if (typeof num == 'string') { 369 | num = num; 370 | } 371 | if (num.indexOf('-') >= 0) { 372 | num = '0' + String(Number(num) + 1).substr(1); 373 | } 374 | return num; 375 | } 376 | 377 | exports.hex2a = function(hexx) { 378 | var hex = hexx.toString();//force conversion 379 | var str = ''; 380 | for (var i = 0; (i < hex.length && hex.substr(i, 2) !== '00'); i += 2) 381 | str += String.fromCharCode(parseInt(hex.substr(i, 2), 16)); 382 | 383 | return str; 384 | } 385 | 386 | //get usc tag asm 387 | exports.getUscTag = function(string){ 388 | var blockHash = string.substr(2); 389 | var tagBuffer = this.serializeString("USCBLOCK:"); 390 | var blockHashBuffer = this.serializeString(new Buffer(blockHash,"hex")); 391 | tagBuffer = tagBuffer.slice(1,tagBuffer.length); 392 | blockHashBuffer = blockHashBuffer.slice(1,blockHashBuffer.length); 393 | var tag = Buffer.concat([ tagBuffer, blockHashBuffer ]); 394 | var tagLengthInHex = (tag.toString("hex").length/2).toString(16); 395 | 396 | return Buffer.concat([new Buffer([0x6a, 0x4c]), new Buffer(tagLengthInHex,"hex"), tag]); 397 | } 398 | 399 | 400 | 401 | 402 | -------------------------------------------------------------------------------- /lib/varDiff.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | 3 | /* 4 | 5 | Vardiff ported from stratum-mining share-limiter 6 | https://github.com/ahmedbodi/stratum-mining/blob/master/mining/basic_share_limiter.py 7 | 8 | */ 9 | 10 | 11 | function RingBuffer(maxSize){ 12 | var data = []; 13 | var cursor = 0; 14 | var isFull = false; 15 | this.append = function(x){ 16 | if (isFull){ 17 | data[cursor] = x; 18 | cursor = (cursor + 1) % maxSize; 19 | } 20 | else{ 21 | data.push(x); 22 | cursor++; 23 | if (data.length === maxSize){ 24 | cursor = 0; 25 | isFull = true; 26 | } 27 | } 28 | }; 29 | this.avg = function(){ 30 | var sum = data.reduce(function(a, b){ return a + b }); 31 | return sum / (isFull ? maxSize : cursor); 32 | }; 33 | this.size = function(){ 34 | return isFull ? maxSize : cursor; 35 | }; 36 | this.clear = function(){ 37 | data = []; 38 | cursor = 0; 39 | isFull = false; 40 | }; 41 | } 42 | 43 | // Truncate a number to a fixed amount of decimal places 44 | function toFixed(num, len) { 45 | return parseFloat(num.toFixed(len)); 46 | } 47 | 48 | var varDiff = module.exports = function varDiff(port, varDiffOptions){ 49 | var _this = this; 50 | 51 | var bufferSize, tMin, tMax; 52 | 53 | var variance = varDiffOptions.targetTime * (varDiffOptions.variancePercent / 100); 54 | 55 | 56 | bufferSize = varDiffOptions.retargetTime / varDiffOptions.targetTime * 4; 57 | tMin = varDiffOptions.targetTime - variance; 58 | tMax = varDiffOptions.targetTime + variance; 59 | 60 | 61 | 62 | this.manageClient = function(client){ 63 | 64 | var stratumPort = client.socket.localPort; 65 | 66 | if (stratumPort != port) { 67 | console.error("Handling a client which is not of this vardiff?"); 68 | } 69 | var options = varDiffOptions; 70 | 71 | var lastTs; 72 | var lastRtc; 73 | var timeBuffer; 74 | 75 | client.on('secondSubmit', function(){ 76 | 77 | var ts = (Date.now() / 1000) | 0; 78 | 79 | if (!lastRtc){ 80 | lastRtc = ts - options.retargetTime / 2; 81 | lastTs = ts; 82 | timeBuffer = new RingBuffer(bufferSize); 83 | return; 84 | } 85 | var sinceLast = ts - lastTs; 86 | 87 | timeBuffer.append(sinceLast); 88 | lastTs = ts; 89 | 90 | if ((ts - lastRtc) < options.retargetTime && timeBuffer.size() > 0){ 91 | return; 92 | } 93 | 94 | lastRtc = ts; 95 | var avg = timeBuffer.avg(); 96 | var ddiff = options.targetTime / avg; 97 | if (avg > tMax && client.difficulty > options.minDiff) { 98 | if (options.x2mode) { 99 | ddiff = 0.5; 100 | } 101 | if (ddiff * client.difficulty < options.minDiff) { 102 | ddiff = options.minDiff / client.difficulty; 103 | } 104 | } else if (avg < tMin) { 105 | if (options.x2mode) { 106 | ddiff = 2; 107 | } 108 | var diffMax = options.maxDiff; 109 | if (ddiff * client.difficulty > diffMax) { 110 | ddiff = diffMax / client.difficulty; 111 | } 112 | } 113 | else{ 114 | return; 115 | } 116 | var newDiff = toFixed(client.difficulty * ddiff, 8); 117 | timeBuffer.clear(); 118 | _this.emit('newDifficulty', client, newDiff); 119 | }); 120 | }; 121 | }; 122 | varDiff.prototype.__proto__ = events.EventEmitter.prototype; 123 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stratum-pool", 3 | "version": "0.1.0", 4 | "description": "High performance Stratum poolserver in Node.js", 5 | "keywords": [ 6 | "ulord", 7 | "stratum", 8 | "mining", 9 | "pool", 10 | "server", 11 | "poolserver", 12 | "cryptohello", 13 | "scrypt" 14 | ], 15 | "homepage": "https://github.com/UlordChain/node-merged-pool", 16 | "bugs": { 17 | "url": "https://github.com/UlordChain/node-merged-pool/issues" 18 | }, 19 | "license": "GPL-2.0", 20 | "author": { 21 | "name": "Jack Chen" 22 | }, 23 | "contributors": [ 24 | { 25 | "name": "vekexasia" 26 | }, 27 | { 28 | "name": "TheSeven" 29 | } 30 | ], 31 | "main": "lib/index.js", 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/UlordChain/node-stratum-pool.git" 35 | }, 36 | "dependencies": { 37 | "multi-hashing": "git+https://github.com/UlordChain/node-multi-hashing.git", 38 | "bignum": "*", 39 | "base58-native": "*", 40 | "async": "*", 41 | "redis": "*", 42 | "node-json-minify": "*" 43 | }, 44 | "engines": { 45 | "node": ">=0.10" 46 | } 47 | } 48 | --------------------------------------------------------------------------------