├── .npmignore ├── .gitignore ├── .travis.yml ├── lib ├── moniterConnection.js ├── index.js ├── merkleTree.js ├── varDiff.js ├── blockTemplate.js ├── transactions.js ├── daemon.js ├── algoProperties.js ├── peer.js ├── jobManager.js ├── util.js ├── stratum.js └── pool.js ├── LICENSE ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ -------------------------------------------------------------------------------- /.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= -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | "bitcoin", 13 | "litecoin", 14 | "cryptohello", 15 | "scrypt" 16 | ], 17 | "homepage": "https://github.com/UlordChain/node-stratum-pool", 18 | "bugs": { 19 | "url": "https://github.com/UlordChain/node-stratum-pool/issues" 20 | }, 21 | "license": "GPL-2.0", 22 | "author": "Jackchen", 23 | "contributors": [ 24 | "vekexasia", 25 | "TheSeven" 26 | ], 27 | "main": "lib/index.js", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/UlordChain/node-stratum-pool.git" 31 | }, 32 | "dependencies": { 33 | "multi-hashing": "git+https://github.com/UlordChain/node-multi-hashing.git", 34 | "bignum": "*", 35 | "base58-native": "*", 36 | "async": "*", 37 | "redis": "*", 38 | "node-json-minify":"*" 39 | }, 40 | "engines": { 41 | "node": ">=0.10" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | 53 | /* generate coinbase tx */ 54 | 55 | this.coinbaseTx = transactions.createGeneration(rpcData, poolAddressScript, reward, 56 | txMessages, recipients).toString('hex'); 57 | 58 | this.coinbaseTxBuffer = new Buffer(this.coinbaseTx, 'hex'); 59 | this.coinbaseTxHash = util.sha256d(this.coinbaseTxBuffer); 60 | 61 | this.transactionData = Buffer.concat(rpcData.transactions.map(function(tx){ 62 | return new Buffer(tx.data, 'hex'); 63 | })); 64 | 65 | /* collect the header's data */ 66 | this.prevHashReversed = util.reverseBuffer(new Buffer(rpcData.previousblockhash, 'hex')).toString('hex'); 67 | 68 | this.merkleTree = new merkleTree(getTransactionBuffers(rpcData.transactions)); 69 | this.merkleRoot = this.merkleTree.withFirst(this.coinbaseTxHash).toString('hex'); 70 | 71 | this.claimtrieReversed = util.reverseBuffer(new Buffer(rpcData.claimtrie, 'hex')).toString('hex'); 72 | 73 | this.serializeCoinbase = function(){ 74 | return this.coinbaseTxBuffer; 75 | } 76 | 77 | /* https://en.bitcoin.it/wiki/Protocol_specification#Block_Headers */ 78 | this.serializeHeader = function(extraNonce1, extraNonce2){ 79 | 80 | var nonce = extraNonce2; 81 | if(extraNonce1){ 82 | nonce += extraNonce1; 83 | } 84 | 85 | /* 4+32+32+32+4+4 + 32 = 140 */ 86 | var header = new Buffer(140).fill(0); 87 | header.write(this.serializeIncompleteHeader().toString('hex'), 0, 108, 'hex'); 88 | header.write(nonce, 108, 32, 'hex'); 89 | 90 | /* console.log("serialize header = " + header.toString('hex')); */ 91 | return header; 92 | }; 93 | 94 | this.serializeIncompleteHeader = function(){ 95 | if(!this.incompleteHeader){ 96 | var incompleteHeader = new Buffer(108).fill(0); 97 | var position = 0; 98 | incompleteHeader.writeInt32LE(rpcData.version, position, 4, 'hex'); 99 | incompleteHeader.write(this.prevHashReversed, position += 4, 32, 'hex'); 100 | incompleteHeader.write(this.merkleRoot, position += 32, 32, 'hex'); 101 | incompleteHeader.write(this.claimtrieReversed, position += 32, 32, 'hex'); 102 | incompleteHeader.writeUInt32LE(rpcData.curtime, position += 32, 4, 'hex'); 103 | incompleteHeader.writeUInt32LE(parseInt(rpcData.bits, 16), position += 4, 4, 'hex'); 104 | this.incompleteHeader = incompleteHeader; 105 | } 106 | /* console.log("incompleteHeader: " + this.incompleteHeader.toString('hex')); */ 107 | return this.incompleteHeader; 108 | }; 109 | 110 | // var extraNonce1 = extraNonce.next(); 111 | 112 | this.serializeRawHeader = function(){ 113 | var rawHeader = new Buffer(140).fill(0); 114 | rawHeader.write(this.serializeIncompleteHeader().toString('hex'), 0, 108, 'hex'); 115 | /* rawHeader.write(extraNonce.next(), 112, 28, 'hex'); */ 116 | return rawHeader; 117 | }; 118 | 119 | this.serializeBlock = function(header){ 120 | return Buffer.concat([ 121 | header, 122 | util.varIntBuffer(this.rpcData.transactions.length + 1), 123 | this.coinbaseTxBuffer, 124 | this.transactionData, 125 | /* getVoteData(), */ 126 | /* POS coins require a zero byte appended to block which the daemon replaces with the signature */ 127 | new Buffer(reward === 'POS' ? [0] : []) 128 | ]); 129 | }; 130 | 131 | this.registerSubmit = function(extraNonce1, extraNonce2){ 132 | var submission = extraNonce2.toLowerCase() + extraNonce1; 133 | if (submits.indexOf(submission) === -1){ 134 | submits.push(submission); 135 | return true; 136 | } 137 | return false; 138 | }; 139 | 140 | this.getJobParams = function(){ 141 | if (!this.jobParams){ 142 | this.jobParams = [ 143 | this.jobId, 144 | this.serializeIncompleteHeader().toString('hex'), 145 | true, 146 | ]; 147 | } 148 | return this.jobParams; 149 | }; 150 | 151 | }; 152 | -------------------------------------------------------------------------------- /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 | 12 | var reward = rpcData.coinbasevalue; 13 | var rewardToPool = reward; 14 | 15 | var txOutputBuffers = []; 16 | 17 | 18 | /* pay to founder */ 19 | if (rpcData.Foundnode.foundpayee) { 20 | var payeeReward = rpcData.Foundnode.foundamount; 21 | reward -= payeeReward; 22 | rewardToPool -= payeeReward; 23 | 24 | var payeeScript = util.addressToScript(rpcData.Foundnode.foundpayee); 25 | txOutputBuffers.push(Buffer.concat([ 26 | util.packInt64LE(payeeReward), 27 | util.varIntBuffer(payeeScript.length), 28 | payeeScript 29 | ])); 30 | 31 | console.log("*************************************************************"); 32 | console.log("foundscript: " + JSON.stringify(rpcData.Foundnode.foundscript)); 33 | console.log("*************************************************************"); 34 | } 35 | 36 | /* pay to masternode */ 37 | if (rpcData.masternode.payee) { 38 | var payeeReward = rpcData.masternode.amount; 39 | reward -= payeeReward; 40 | rewardToPool -= payeeReward; 41 | 42 | var payeeScript = util.addressToScript(rpcData.masternode.payee); 43 | txOutputBuffers.push(Buffer.concat([ 44 | util.packInt64LE(payeeReward), 45 | util.varIntBuffer(payeeScript.length), 46 | payeeScript 47 | ])); 48 | // console.log("pay to masternode: " + payeeScript.toString('hex') + " " + payeeReward); 49 | } 50 | 51 | /* pay for superblock */ 52 | if (rpcData.superblock.length > 0){ 53 | for (var i in rpcData.superblock) { 54 | var payeeReward = 0 ; 55 | payeeReward = rpcData.superblock[i].amount; 56 | reward -= payeeReward; 57 | rewardToPool -= payeeReward; 58 | var payeeScript = util.addressToScript(rpcData.superblock[i].payee); 59 | 60 | txOutputBuffers.push(Buffer.concat([ 61 | util.packInt64LE(payeeReward), 62 | util.varIntBuffer(payeeScript.length), 63 | payeeScript 64 | ])); 65 | console.log("*********************************************************"); 66 | console.log("pay for superblock: " + JSON.stringify(rpcData.superblock)); 67 | console.log("*********************************************************"); 68 | } 69 | 70 | } 71 | 72 | 73 | for (var i = 0; i < recipients.length; i++){ 74 | var recipientReward = Math.floor(recipients[i].percent * reward); 75 | rewardToPool -= recipientReward; 76 | 77 | txOutputBuffers.push(Buffer.concat([ 78 | util.packInt64LE(recipientReward), 79 | util.varIntBuffer(recipients[i].script.length), 80 | recipients[i].script 81 | ])); 82 | } 83 | 84 | txOutputBuffers.unshift(Buffer.concat([ 85 | util.packInt64LE(rewardToPool), 86 | util.varIntBuffer(poolRecipient.length), 87 | poolRecipient 88 | ])); 89 | 90 | if (rpcData.default_witness_commitment !== undefined){ 91 | witness_commitment = new Buffer(rpcData.default_witness_commitment, 'hex'); 92 | txOutputBuffers.unshift(Buffer.concat([ 93 | util.packInt64LE(0), 94 | util.varIntBuffer(witness_commitment.length), 95 | witness_commitment 96 | ])); 97 | } 98 | 99 | return Buffer.concat([ 100 | util.varIntBuffer(txOutputBuffers.length), 101 | Buffer.concat(txOutputBuffers) 102 | ]); 103 | 104 | }; 105 | 106 | 107 | exports.createGeneration = function(rpcData, publicKey, reward, txMessages, recipients){ 108 | var txInputsCount = 1; 109 | var txOutputsCount = 1; 110 | var txVersion = txMessages === true ? 2 : 1; 111 | var txLockTime = 0; 112 | 113 | var txInPrevOutHash = 0; 114 | var txInPrevOutIndex = Math.pow(2, 32) - 1; 115 | var txInSequence = 0; 116 | 117 | //Only required for POS coins 118 | var txTimestamp = reward === 'POS' ? 119 | util.packUInt32LE(rpcData.curtime) : new Buffer([]); 120 | 121 | //For coins that support/require transaction comments 122 | var txComment = txMessages === true ? 123 | util.serializeString('http://testnet-pool.ulord.one') : 124 | new Buffer([]); 125 | 126 | 127 | var scriptSigPart1 = Buffer.concat([ 128 | util.serializeNumber(rpcData.height), 129 | new Buffer(rpcData.coinbaseaux.flags, 'hex'), 130 | util.serializeNumber(Date.now() / 1000 | 0) 131 | ]); 132 | 133 | var scriptSigPart2 = util.serializeString('/testnet-pool.ulord.one/'); 134 | 135 | var p1 = Buffer.concat([ 136 | util.packUInt32LE(txVersion), 137 | txTimestamp, 138 | //transaction input 139 | util.varIntBuffer(txInputsCount), 140 | util.uint256BufferFromHash(txInPrevOutHash), 141 | util.packUInt32LE(txInPrevOutIndex), 142 | util.varIntBuffer(scriptSigPart1.length + scriptSigPart2.length), 143 | scriptSigPart1 144 | ]); 145 | 146 | /* 147 | The generation transaction must be split at the extranonce (which located in the transaction input 148 | scriptSig). Miners send us unique extranonces that we use to join the two parts in attempt to create 149 | a valid share and/or block. 150 | */ 151 | 152 | var outputTransactions = generateOutputTransactions(publicKey, recipients, rpcData); 153 | 154 | var p2 = Buffer.concat([ 155 | scriptSigPart2, 156 | util.packUInt32LE(txInSequence), 157 | //end transaction input 158 | 159 | //transaction output 160 | outputTransactions, 161 | //end transaction ouput 162 | 163 | util.packUInt32LE(txLockTime), 164 | txComment 165 | ]); 166 | 167 | return Buffer.concat([p1, p2]); 168 | }; 169 | 170 | -------------------------------------------------------------------------------- /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/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/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/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 | 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 | 107 | var tmpBlockTemplate = new blockTemplate( 108 | jobCounter.next(), 109 | rpcData, 110 | options.poolAddressScript, 111 | _this.extraNonceCounter, 112 | options.coin.reward, 113 | options.coin.txMessages, 114 | options.recipients 115 | ); 116 | 117 | _this.currentJob = tmpBlockTemplate; 118 | 119 | _this.emit('updatedBlock', tmpBlockTemplate, true); 120 | 121 | _this.validJobs[tmpBlockTemplate.jobId] = tmpBlockTemplate; 122 | 123 | }; 124 | 125 | //returns true if processed a new block 126 | this.processTemplate = function(rpcData){ 127 | 128 | /* Block is new if A) its the first block we have seen so far or B) the blockhash is different and the 129 | block height is greater than the one we have */ 130 | var isNewBlock = typeof(_this.currentJob) === 'undefined'; 131 | if (!isNewBlock && _this.currentJob.rpcData.previousblockhash !== rpcData.previousblockhash){ 132 | isNewBlock = true; 133 | 134 | //If new block is outdated/out-of-sync than return 135 | if (rpcData.height < _this.currentJob.rpcData.height) 136 | return false; 137 | } 138 | 139 | if (!isNewBlock) return false; 140 | 141 | 142 | var tmpBlockTemplate = new blockTemplate( 143 | jobCounter.next(), 144 | rpcData, 145 | options.poolAddressScript, 146 | _this.extraNonceCounter, 147 | options.coin.reward, 148 | options.coin.txMessages, 149 | options.recipients 150 | ); 151 | 152 | this.currentJob = tmpBlockTemplate; 153 | 154 | this.validJobs = {}; 155 | _this.emit('newBlock', tmpBlockTemplate); 156 | this.validJobs[tmpBlockTemplate.jobId] = tmpBlockTemplate; 157 | 158 | return true; 159 | 160 | }; 161 | 162 | this.processSecondShare = function(jobId, previousDifficulty, difficulty, ipAddress, port, workerName, nTime, nonce, extraNonce1, extraNonce2, hash){ 163 | var shareError = function(error){ 164 | _this.emit('share', { 165 | job: jobId, 166 | ip: ipAddress, 167 | worker: workerName, 168 | difficulty: difficulty, 169 | error: error[1] 170 | }); 171 | return {error: error, result: null}; 172 | }; 173 | 174 | var submitTime = Date.now() / 1000 | 0; 175 | var job = this.validJobs[jobId]; 176 | var minerAddress = workerName.split('.')[0]; 177 | if (typeof job === 'undefined' || job.jobId != jobId ) { 178 | return shareError([21, 'job not found']); 179 | } 180 | if(extraNonce2.length!==8) { 181 | return shareError([20, 'incorrect size of nonce']); 182 | } 183 | extraNonce2 = Buffer.from(extraNonce2,'hex').toString('hex'); 184 | 185 | if(extraNonce2.length!==8) { 186 | return shareError([20, 'incorrect size of nonce']); 187 | } 188 | 189 | if (!job.registerSubmit(extraNonce1,extraNonce2)){ 190 | return shareError([22, 'duplicate share']); 191 | } 192 | 193 | var headerBuffer = job.serializeHeader(extraNonce1, extraNonce2); 194 | 195 | var headerHash = hashDigest(headerBuffer,null); 196 | 197 | if (hash && headerHash.toString('hex') !== hash) { 198 | return shareError([31, 'incorrect hash of ' + hash]); 199 | } 200 | if(_this.moniter.indexOf(minerAddress)!=-1){ 201 | connection.zadd(_this.moniter+"_hash",Date.now(),JSON.stringify({ 202 | workerName:workerName, 203 | jobId:jobId, 204 | extraNonce1:extraNonce1, 205 | extraNonce2:extraNonce2, 206 | submitHash:hash, 207 | headerBuffer:headerBuffer.toString('hex'), 208 | time:Date.now() 209 | })) 210 | } 211 | var headerBigNum = bignum.fromBuffer(headerHash, {endian: 'little', size: 32}); 212 | 213 | var blockHashInvalid; 214 | var blockHash; 215 | var blockHex; 216 | 217 | var shareDiff = diff1 / headerBigNum.toNumber() * shareMultiplier; 218 | 219 | var blockDiffAdjusted = job.difficulty * shareMultiplier; 220 | 221 | //Check if share is a block candidate (matched network difficulty) 222 | if (job.target.ge(headerBigNum)){ 223 | blockHex = job.serializeBlock(headerBuffer).toString('hex'); 224 | blockHash = blockHasher(headerBuffer, /*nTime*/null).toString('hex'); 225 | } 226 | else { 227 | if (options.emitInvalidBlockHashes) 228 | blockHashInvalid = util.reverseBuffer(util.sha256d(headerBuffer)).toString('hex'); 229 | 230 | //Check if share didn't reached the miner's difficulty) 231 | if (shareDiff / difficulty < 0.99){ 232 | 233 | //Check if share matched a previous difficulty from before a vardiff retarget 234 | if (previousDifficulty && shareDiff >= previousDifficulty){ 235 | difficulty = previousDifficulty; 236 | } 237 | else{ 238 | return shareError([23, 'low difficulty share of ' + shareDiff]); 239 | } 240 | 241 | } 242 | } 243 | 244 | 245 | _this.emit('share', { 246 | job: jobId, 247 | ip: ipAddress, 248 | port: port, 249 | worker: workerName, 250 | height: job.rpcData.height, 251 | blockReward: job.rpcData.coinbasevalue, 252 | difficulty: difficulty, 253 | shareDiff: shareDiff.toFixed(8), 254 | blockDiff : blockDiffAdjusted, 255 | blockDiffActual: job.difficulty, 256 | blockHash: blockHash, 257 | blockHashInvalid: blockHashInvalid 258 | }, blockHex); 259 | return {result: true, error: null, blockHash: blockHash,difficulty:difficulty,height:job.rpcData.height,worker:workerName,ip:ipAddress,shareDiff:shareDiff,blockDiff:blockDiffAdjusted}; 260 | }; 261 | 262 | 263 | }; 264 | JobManager.prototype.__proto__ = events.EventEmitter.prototype; 265 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | High performance Stratum poolserver in Node.js. One instance of this software can startup and manage multiple coin 2 | pools, each with their own daemon and stratum port :) 3 | 4 | #### Notice 5 | 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 6 | handle stratum authentication and raw share data then this module will not be of use to you. For a full featured portal 7 | that uses this module, see [Ulord (Node Open Mining Portal)](https://github.com/UlordChain/ulord-node-stratum-pool). It 8 | handles payments, website front-end, database layer, mutli-coin/pool support, auto-switching miners between coins/pools, 9 | etc.. The portal also has an [MPOS](https://github.com/MPOS/php-mpos) compatibility mode so that the it can function as 10 | a drop-in-replacement for [python-stratum-mining](https://github.com/Crypto-Expert/stratum-mining). 11 | 12 | 13 | #### Why 14 | This server was built to be more efficient and easier to setup, maintain and scale than existing stratum poolservers 15 | which are written in python. Compared to the spaghetti state of the latest 16 | [stratum-mining python server](https://github.com/Crypto-Expert/stratum-mining/), this software should also have a 17 | lower barrier to entry for other developers to fork and add features or fix bugs. 18 | 19 | 20 | Features 21 | ---------------------------------- 22 | * Daemon RPC interface 23 | * Stratum TCP socket server 24 | * Block template / job manager 25 | * P2P to get block notifications as peer node 26 | * Optimized generation transaction building 27 | * Connecting to multiple daemons for redundancy 28 | * Process share submissions 29 | * Session managing for purging DDoS/flood initiated zombie workers 30 | * Auto ban IPs that are flooding with invalid shares 31 | * __POW__ (proof-of-work) & __POS__ (proof-of-stake) support 32 | * Transaction messages support 33 | * Vardiff (variable difficulty / share limiter) 34 | * When started with a coin deamon that hasn't finished syncing to the network it shows the blockchain download progress and initializes once synced 35 | 36 | #### Hashing algorithms supported: 37 | * ✓ __CryptoHello__ (Ulord [UT]) 38 | * ✓ __SHA256__ (Bitcoin, Freicoin, Peercoin/PPCoin, Terracoin, etc..) 39 | * ✓ __Scrypt__ (Litecoin, Dogecoin, Feathercoin, etc..) 40 | * ✓ __Scrypt-Jane__ (YaCoin, CopperBars, Pennies, Tickets, etc..) 41 | * ✓ __Scrypt-N__ (Vertcoin [VTC]) 42 | * ✓ __Quark__ (Quarkcoin [QRK]) 43 | * ✓ __X13__ (MaruCoin, BoostCoin) 44 | * ✓ __NIST5__ (Talkcoin) 45 | * ✓ __Keccak__ (Maxcoin [MAX], HelixCoin, CryptoMeth, Galleon, 365coin, Slothcoin, BitcointalkCoin) 46 | * ✓ __Skein__ (Skeincoin [SKC]) 47 | * ✓ __Groestl__ (Groestlcoin [GRS]) 48 | 49 | May be working (needs additional testing): 50 | * ? *Blake* (Blakecoin [BLC]) 51 | * ? *Fugue* (Fuguecoin [FC]) 52 | * ? *Qubit* (Qubitcoin [Q2C], Myriadcoin [MYR]) 53 | * ? *SHAvite-3* (INKcoin [INK]) 54 | * ? *Sha1* (Sha1coin [SHA], Yaycoin [YAY]) 55 | 56 | Not working currently: 57 | * *Groestl* - for Myriadcoin 58 | * *Keccak* - for eCoin & Copperlark 59 | * *Hefty1* (Heavycoin [HVC]) 60 | 61 | 62 | Requirements 63 | ------------ 64 | * node v0.10+ 65 | * coin daemon (preferably one with a relatively updated API and not some crapcoin :p) 66 | 67 | 68 | Example Usage 69 | ------------- 70 | 71 | #### Install as a node module by cloning repository 72 | 73 | ```bash 74 | git clone https://github.com/UlordChain/node-stratum-pool node_modules/stratum-pool 75 | npm update 76 | ``` 77 | 78 | #### Module usage 79 | 80 | Create the configuration for your coin: 81 | 82 | Possible options for `algorithm`: *sha256, scrypt, scrypt-jane, scrypt-n, quark, keccak, blake, 83 | skein, groestl, fugue, shavite3, hefty1, qubit, or sha1*. 84 | 85 | ```javascript 86 | var myCoin = { 87 | "name": "Ulord", 88 | "symbol": "ULD", 89 | "algorithm": "cryptohello", 90 | "nValue": 1024, //optional - defaults to 1024 91 | "rValue": 1, //optional - defaults to 1 92 | "txMessages": false, //optional - defaults to false, 93 | 94 | /* Magic value only required for setting up p2p block notifications. It is found in the daemon 95 | source code as the pchMessageStart variable. 96 | For example, litecoin mainnet magic: http://git.io/Bi8YFw 97 | And for litecoin testnet magic: http://git.io/NXBYJA */ 98 | "peerMagic": "fbc0b6db" //optional 99 | "peerMagicTestnet": "fcc1b7dc" //optional 100 | }; 101 | ``` 102 | 103 | If you are using the `scrypt-jane` algorithm there are additional configurations: 104 | 105 | ```javascript 106 | var myCoin = { 107 | "name": "Freecoin", 108 | "symbol": "FEC", 109 | "algorithm": "scrypt-jane", 110 | "chainStartTime": 1375801200, //defaults to 1367991200 (YACoin) if not used 111 | "nMin": 6, //defaults to 4 if not used 112 | "nMax": 32 //defaults to 30 if not used 113 | }; 114 | ``` 115 | 116 | If you are using the `scrypt-n` algorithm there is an additional configuration: 117 | ```javascript 118 | var myCoin = { 119 | "name": "Execoin", 120 | "symbol": "EXE", 121 | "algorithm": "scrypt-n", 122 | /* This defaults to Vertcoin's timetable if not used. It is required for scrypt-n coins that 123 | have modified their N-factor timetable to be different than Vertcoin's. */ 124 | "timeTable": { 125 | "2048": 1390959880, 126 | "4096": 1438295269, 127 | "8192": 1485630658, 128 | "16384": 1532966047, 129 | "32768": 1580301436, 130 | "65536": 1627636825, 131 | "131072": 1674972214, 132 | "262144": 1722307603 133 | } 134 | }; 135 | ``` 136 | 137 | If you are using the `keccak` algorithm there are additional configurations *(The rare `normalHashing` keccak coins 138 | such as Copperlark and eCoin don't appear to work yet - only the popular ones like Maxcoin are)*: 139 | ```javascript 140 | var myCoin = { 141 | "name": "eCoin", 142 | "symbol": "ECN", 143 | "algorithm": "keccak", 144 | 145 | /* This is not required and set to false by default. Some coins such as Copperlark and eCoin 146 | require it to be set to true. Maxcoin and most others are false. */ 147 | "normalHashing": true 148 | }; 149 | ``` 150 | 151 | 152 | Create and start new pool with configuration options and authentication function 153 | 154 | ```javascript 155 | var Stratum = require('stratum-pool'); 156 | 157 | var pool = Stratum.createPool({ 158 | 159 | "coin": myCoin, 160 | 161 | "address": "mi4iBXbBsydtcc5yFmsff2zCFVX4XG7qJc", //Address to where block rewards are given 162 | 163 | /* Block rewards go to the configured pool wallet address to later be paid out to miners, 164 | except for a percentage that can go to, for examples, pool operator(s) as pool fees or 165 | or to donations address. Addresses or hashed public keys can be used. Here is an example 166 | of rewards going to the main pool op, a pool co-owner, and NOMP donation. */ 167 | "rewardRecipients": { 168 | "n37vuNFkXfk15uFnGoVyHZ6PYQxppD3QqK": 1.5, //1.5% goes to pool op 169 | "mirj3LtZxbSTharhtXvotqtJXUY7ki5qfx": 0.5, //0.5% goes to a pool co-owner 170 | 171 | /* 0.1% donation to NOMP. This pubkey can accept any type of coin, please leave this in 172 | your config to help support NOMP development. */ 173 | "22851477d63a085dbc2398c8430af1c09e7343f6": 0.1 174 | }, 175 | 176 | "blockRefreshInterval": 1000, //How often to poll RPC daemons for new blocks, in milliseconds 177 | 178 | 179 | /* Some miner apps will consider the pool dead/offline if it doesn't receive anything new jobs 180 | for around a minute, so every time we broadcast jobs, set a timeout to rebroadcast 181 | in this many seconds unless we find a new job. Set to zero or remove to disable this. */ 182 | "jobRebroadcastTimeout": 55, 183 | 184 | //instanceId: 37, //Recommend not using this because a crypto-random one will be generated 185 | 186 | /* Some attackers will create thousands of workers that use up all available socket connections, 187 | usually the workers are zombies and don't submit shares after connecting. This features 188 | detects those and disconnects them. */ 189 | "connectionTimeout": 600, //Remove workers that haven't been in contact for this many seconds 190 | 191 | /* Sometimes you want the block hashes even for shares that aren't block candidates. */ 192 | "emitInvalidBlockHashes": false, 193 | 194 | /* Enable for client IP addresses to be detected when using a load balancer with TCP proxy 195 | protocol enabled, such as HAProxy with 'send-proxy' param: 196 | http://haproxy.1wt.eu/download/1.5/doc/configuration.txt */ 197 | "tcpProxyProtocol": false, 198 | 199 | /* If a worker is submitting a high threshold of invalid shares we can temporarily ban their IP 200 | to reduce system/network load. Also useful to fight against flooding attacks. If running 201 | behind something like HAProxy be sure to enable 'tcpProxyProtocol', otherwise you'll end up 202 | banning your own IP address (and therefore all workers). */ 203 | "banning": { 204 | "enabled": true, 205 | "time": 600, //How many seconds to ban worker for 206 | "invalidPercent": 50, //What percent of invalid shares triggers ban 207 | "checkThreshold": 500, //Check invalid percent when this many shares have been submitted 208 | "purgeInterval": 300 //Every this many seconds clear out the list of old bans 209 | }, 210 | 211 | /* Each pool can have as many ports for your miners to connect to as you wish. Each port can 212 | be configured to use its own pool difficulty and variable difficulty settings. varDiff is 213 | optional and will only be used for the ports you configure it for. */ 214 | "ports": { 215 | "3032": { //A port for your miners to connect to 216 | "diff": 32, //the pool difficulty for this port 217 | 218 | /* Variable difficulty is a feature that will automatically adjust difficulty for 219 | individual miners based on their hashrate in order to lower networking overhead */ 220 | "varDiff": { 221 | "minDiff": 8, //Minimum difficulty 222 | "maxDiff": 512, //Network difficulty will be used if it is lower than this 223 | "targetTime": 15, //Try to get 1 share per this many seconds 224 | "retargetTime": 90, //Check to see if we should retarget every this many seconds 225 | "variancePercent": 30 //Allow time to very this % from target without retargeting 226 | } 227 | }, 228 | "3256": { //Another port for your miners to connect to, this port does not use varDiff 229 | "diff": 256 //The pool difficulty 230 | } 231 | }, 232 | 233 | /* Recommended to have at least two daemon instances running in case one drops out-of-sync 234 | or offline. For redundancy, all instances will be polled for block/transaction updates 235 | and be used for submitting blocks. Creating a backup daemon involves spawning a daemon 236 | using the "-datadir=/backup" argument which creates a new daemon instance with it's own 237 | RPC config. For more info on this see: 238 | - https://en.bitcoin.it/wiki/Data_directory 239 | - https://en.bitcoin.it/wiki/Running_bitcoind */ 240 | "daemons": [ 241 | { //Main daemon instance 242 | "host": "127.0.0.1", 243 | "port": 19332, 244 | "user": "litecoinrpc", 245 | "password": "testnet" 246 | }, 247 | { //Backup daemon instance 248 | "host": "127.0.0.1", 249 | "port": 19344, 250 | "user": "litecoinrpc", 251 | "password": "testnet" 252 | } 253 | ], 254 | 255 | 256 | /* This allows the pool to connect to the daemon as a node peer to receive block updates. 257 | It may be the most efficient way to get block updates (faster than polling, less 258 | intensive than blocknotify script). It requires the additional field "peerMagic" in 259 | the coin config. */ 260 | "p2p": { 261 | "enabled": false, 262 | 263 | /* Host for daemon */ 264 | "host": "127.0.0.1", 265 | 266 | /* Port configured for daemon (this is the actual peer port not RPC port) */ 267 | "port": 19333, 268 | 269 | /* If your coin daemon is new enough (i.e. not a shitcoin) then it will support a p2p 270 | feature that prevents the daemon from spamming our peer node with unnecessary 271 | transaction data. Assume its supported but if you have problems try disabling it. */ 272 | "disableTransactions": true 273 | 274 | } 275 | 276 | }, function(ip, port , workerName, password, callback){ //stratum authorization function 277 | console.log("Authorize " + workerName + ":" + password + "@" + ip); 278 | callback({ 279 | error: null, 280 | authorized: true, 281 | disconnect: false 282 | }); 283 | }); 284 | ``` 285 | 286 | 287 | Listen to pool events 288 | ```javascript 289 | 290 | pool.on('share', function(isValidShare, isValidBlock, data){ 291 | 292 | if (isValidBlock) 293 | console.log('Block found'); 294 | else if (isValidShare) 295 | console.log('Valid share submitted'); 296 | else if (data.blockHash) 297 | console.log('We thought a block was found but it was rejected by the daemon'); 298 | else 299 | console.log('Invalid share submitted') 300 | 301 | console.log('share data: ' + JSON.stringify(data)); 302 | }); 303 | 304 | 305 | 306 | pool.on('log', function(severity, logKey, logText){ 307 | console.log(severity + ': ' + '[' + logKey + '] ' + logText); 308 | }); 309 | ``` 310 | 311 | Start pool 312 | ```javascript 313 | pool.start(); 314 | ``` 315 | 316 | License 317 | ------- 318 | Released under the GNU General Public License v2 319 | 320 | http://www.gnu.org/licenses/gpl-2.0.html 321 | -------------------------------------------------------------------------------- /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 | 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 | console.log("send job, " + _this.method); 364 | 365 | if(_this.method === 'stratum'){ 366 | _this.socket.end(); 367 | _this.socket.destroy(); 368 | return; 369 | }else if(_this.method === 'login'){ 370 | var header = new Buffer(140).fill(0); 371 | header.write(jobParams[1], 0, 108, 'hex'); 372 | header.write(_this.extraNonce1, 112, 28, 'hex'); 373 | 374 | sendJson({ 375 | id : 1, 376 | jsonrpc : _this.jsonrpc, 377 | error : null, 378 | result : { id : "2018", job : {job_id: jobParams[0], blob: header.toString('hex'), target: _this.target}, status : "OK" } 379 | }); 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/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 | 12 | var pool = module.exports = function pool(options, authorizeFn){ 13 | 14 | this.options = options; 15 | 16 | var _this = this; 17 | var blockPollingIntervalId; 18 | 19 | 20 | var emitLog = function(text) { _this.emit('log', 'debug' , text); }; 21 | var emitWarningLog = function(text) { _this.emit('log', 'warning', text); }; 22 | var emitErrorLog = function(text) { _this.emit('log', 'error' , text); }; 23 | var emitSpecialLog = function(text) { _this.emit('log', 'special', text); }; 24 | 25 | 26 | 27 | if (!(options.coin.algorithm in algos)){ 28 | emitErrorLog('The ' + options.coin.algorithm + ' hashing algorithm is not supported.'); 29 | throw new Error(); 30 | } 31 | 32 | 33 | 34 | this.start = function(){ 35 | SetupVarDiff(); 36 | SetupApi(); 37 | SetupDaemonInterface(function(){ 38 | DetectCoinData(function(){ 39 | SetupRecipients(); 40 | SetupJobManager(); 41 | OnBlockchainSynced(function(){ 42 | GetFirstJob(function(){ 43 | SetupBlockPolling(); 44 | SetupPeer(); 45 | StartStratumServer(function(){ 46 | OutputPoolInfo(); 47 | _this.emit('started'); 48 | }); 49 | }); 50 | }); 51 | }); 52 | }); 53 | }; 54 | 55 | 56 | 57 | function GetFirstJob(finishedCallback){ 58 | 59 | GetBlockTemplate(function(error, result){ 60 | if (error) { 61 | emitErrorLog('Error with getblocktemplate on creating first job, server cannot start'); 62 | return; 63 | } 64 | 65 | var portWarnings = []; 66 | 67 | var networkDiffAdjusted = options.initStats.difficulty; 68 | 69 | Object.keys(options.ports).forEach(function(port){ 70 | var portDiff = options.ports[port].diff; 71 | if (networkDiffAdjusted < portDiff) 72 | portWarnings.push('port ' + port + ' w/ diff ' + portDiff); 73 | }); 74 | 75 | //Only let the first fork show synced status or the log wil look flooded with it 76 | if (portWarnings.length > 0 && (!process.env.forkId || process.env.forkId === '0')) { 77 | var warnMessage = 'Network diff of ' + networkDiffAdjusted + ' is lower than ' 78 | + portWarnings.join(' and '); 79 | emitWarningLog(warnMessage); 80 | } 81 | 82 | finishedCallback(); 83 | 84 | }); 85 | } 86 | 87 | 88 | function OutputPoolInfo(){ 89 | 90 | var startMessage = 'Stratum Pool Server Started for ' + options.coin.name + 91 | ' [' + options.coin.symbol.toUpperCase() + '] {' + options.coin.algorithm + '}'; 92 | if (process.env.forkId && process.env.forkId !== '0'){ 93 | emitLog(startMessage); 94 | return; 95 | } 96 | var infoLines = [startMessage, 97 | 'Network Connected:\t' + (options.testnet ? 'Testnet' : 'Mainnet'), 98 | 'Detected Reward Type:\t' + options.coin.reward, 99 | 'Current Block Height:\t' + _this.jobManager.currentJob.rpcData.height, 100 | 'Current Connect Peers:\t' + options.initStats.connections, 101 | 'Current Block Diff:\t' + _this.jobManager.currentJob.difficulty * algos[options.coin.algorithm].multiplier, 102 | 'Network Difficulty:\t' + options.initStats.difficulty, 103 | 'Network Hash Rate:\t' + util.getReadableHashRateString(options.initStats.networkHashRate), 104 | 'Stratum Port(s):\t' + _this.options.initStats.stratumPorts.join(', '), 105 | 'Pool Fee Percent:\t' + _this.options.feePercent + '%' 106 | ]; 107 | 108 | if (typeof options.blockRefreshInterval === "number" && options.blockRefreshInterval > 0) 109 | infoLines.push('Block polling every:\t' + options.blockRefreshInterval + ' ms'); 110 | 111 | emitSpecialLog(infoLines.join('\n\t\t\t\t\t\t')); 112 | } 113 | 114 | 115 | function OnBlockchainSynced(syncedCallback){ 116 | 117 | var checkSynced = function(displayNotSynced){ 118 | _this.daemon.cmd('getblocktemplate', [], function(results){ 119 | var synced = results.every(function(r){ 120 | return !r.error || r.error.code !== -10; 121 | }); 122 | if (synced){ 123 | syncedCallback(); 124 | } 125 | else{ 126 | if (displayNotSynced) displayNotSynced(); 127 | setTimeout(checkSynced, 5000); 128 | 129 | //Only let the first fork show synced status or the log wil look flooded with it 130 | if (!process.env.forkId || process.env.forkId === '0') 131 | generateProgress(); 132 | } 133 | 134 | }); 135 | }; 136 | checkSynced(function(){ 137 | //Only let the first fork show synced status or the log wil look flooded with it 138 | if (!process.env.forkId || process.env.forkId === '0') 139 | emitErrorLog('Daemon is still syncing with network (download blockchain) - server will be started once synced'); 140 | }); 141 | 142 | 143 | var generateProgress = function(){ 144 | 145 | _this.daemon.cmd('getinfo', [], function(results) { 146 | var blockCount = results.sort(function (a, b) { 147 | return b.response.blocks - a.response.blocks; 148 | })[0].response.blocks; 149 | 150 | //get list of peers and their highest block height to compare to ours 151 | _this.daemon.cmd('getpeerinfo', [], function(results){ 152 | 153 | var peers = results[0].response; 154 | var totalBlocks = peers.sort(function(a, b){ 155 | return b.startingheight - a.startingheight; 156 | })[0].startingheight; 157 | 158 | var percent = (blockCount / totalBlocks * 100).toFixed(2); 159 | emitWarningLog('Downloaded ' + percent + '% of blockchain from ' + peers.length + ' peers'); 160 | }); 161 | 162 | }); 163 | }; 164 | 165 | } 166 | 167 | 168 | function SetupApi() { 169 | if (typeof(options.api) !== 'object' || typeof(options.api.start) !== 'function') { 170 | return; 171 | } else { 172 | options.api.start(_this); 173 | } 174 | } 175 | 176 | 177 | function SetupPeer(){ 178 | if (!options.p2p || !options.p2p.enabled) 179 | return; 180 | 181 | if (options.testnet && !options.coin.peerMagicTestnet){ 182 | emitErrorLog('p2p cannot be enabled in testnet without peerMagicTestnet set in coin configuration'); 183 | return; 184 | } 185 | else if (!options.coin.peerMagic){ 186 | emitErrorLog('p2p cannot be enabled without peerMagic set in coin configuration'); 187 | return; 188 | } 189 | 190 | _this.peer = new peer(options); 191 | _this.peer.on('connected', function() { 192 | emitLog('p2p connection successful'); 193 | }).on('connectionRejected', function(){ 194 | emitErrorLog('p2p connection failed - likely incorrect p2p magic value'); 195 | }).on('disconnected', function(){ 196 | emitWarningLog('p2p peer node disconnected - attempting reconnection...'); 197 | }).on('connectionFailed', function(e){ 198 | emitErrorLog('p2p connection failed - likely incorrect host or port'); 199 | }).on('socketError', function(e){ 200 | emitErrorLog('p2p had a socket error ' + JSON.stringify(e)); 201 | }).on('error', function(msg){ 202 | emitWarningLog('p2p had an error ' + msg); 203 | }).on('blockFound', function(hash){ 204 | _this.processBlockNotify(hash, 'p2p'); 205 | }); 206 | } 207 | 208 | 209 | function SetupVarDiff(){ 210 | _this.varDiff = {}; 211 | Object.keys(options.ports).forEach(function(port) { 212 | if (options.ports[port].varDiff) 213 | _this.setVarDiff(port, options.ports[port].varDiff); 214 | }); 215 | } 216 | 217 | 218 | /* 219 | Coin daemons either use submitblock or getblocktemplate for submitting new blocks 220 | */ 221 | function SubmitBlock(blockHex, callback){ 222 | 223 | var rpcCommand, rpcArgs; 224 | if (options.hasSubmitMethod){ 225 | rpcCommand = 'submitblock'; 226 | rpcArgs = [blockHex]; 227 | } 228 | else{ 229 | rpcCommand = 'getblocktemplate'; 230 | rpcArgs = [{'mode': 'submit', 'data': blockHex}]; 231 | } 232 | 233 | 234 | _this.daemon.cmd(rpcCommand, 235 | rpcArgs, 236 | function(results){ 237 | for (var i = 0; i < results.length; i++){ 238 | var result = results[i]; 239 | if (result.error) { 240 | emitErrorLog('rpc error with daemon instance ' + 241 | result.instance.index + ' when submitting block with ' + rpcCommand + ' ' + 242 | JSON.stringify(result.error) 243 | ); 244 | return; 245 | } 246 | else if (result.response === 'rejected') { 247 | emitErrorLog('Daemon instance ' + result.instance.index + ' rejected a supposedly valid block'); 248 | return; 249 | } 250 | } 251 | emitLog('Submitted Block using ' + rpcCommand + ' successfully to daemon instance(s)'); 252 | callback(); 253 | } 254 | ); 255 | 256 | } 257 | 258 | 259 | function SetupRecipients(){ 260 | var recipients = []; 261 | options.feePercent = 0; 262 | options.rewardRecipients = options.rewardRecipients || {}; 263 | for (var r in options.rewardRecipients){ 264 | var percent = options.rewardRecipients[r]; 265 | var rObj = { 266 | percent: percent / 100 267 | }; 268 | try { 269 | if (r.length === 40) 270 | rObj.script = util.miningKeyToScript(r); 271 | else 272 | rObj.script = util.addressToScript(r); 273 | recipients.push(rObj); 274 | options.feePercent += percent; 275 | } 276 | catch(e){ 277 | emitErrorLog('Error generating transaction output script for ' + r + ' in rewardRecipients'); 278 | } 279 | } 280 | if (recipients.length === 0){ 281 | emitErrorLog('No rewardRecipients have been setup which means no fees will be taken'); 282 | } 283 | options.recipients = recipients; 284 | } 285 | 286 | function SetupJobManager(){ 287 | 288 | _this.jobManager = new jobManager(options); 289 | 290 | _this.jobManager.on('newBlock', function(blockTemplate){ 291 | //Check if stratumServer has been initialized yet 292 | if (_this.stratumServer) { 293 | _this.stratumServer.broadcastMiningJobs(blockTemplate.getJobParams()); 294 | } 295 | }).on('updatedBlock', function(blockTemplate){ 296 | //Check if stratumServer has been initialized yet 297 | if (_this.stratumServer) { 298 | var job = blockTemplate.getJobParams(); 299 | job[2] = false; 300 | _this.stratumServer.broadcastMiningJobs(job); 301 | } 302 | }).on('share', function(shareData, blockHex){ 303 | var isValidShare = !shareData.error; 304 | var isValidBlock = !!blockHex; 305 | var emitShare = function(){ 306 | _this.emit('share', isValidShare, isValidBlock, shareData); 307 | }; 308 | 309 | /* 310 | If we calculated that the block solution was found, 311 | before we emit the share, lets submit the block, 312 | then check if it was accepted using RPC getblock 313 | */ 314 | if (!isValidBlock) 315 | emitShare(); 316 | else{ 317 | SubmitBlock(blockHex, function(){ 318 | CheckBlockAccepted(shareData.blockHash, function(isAccepted, tx){ 319 | isValidBlock = isAccepted; 320 | shareData.txHash = tx; 321 | emitShare(); 322 | 323 | GetBlockTemplate(function(error, result, foundNewBlock){ 324 | if (foundNewBlock) 325 | emitLog('Block notification via RPC after block submission'); 326 | }); 327 | 328 | }); 329 | }); 330 | } 331 | }).on('log', function(severity, message){ 332 | _this.emit('log', severity, message); 333 | }); 334 | } 335 | 336 | 337 | function SetupDaemonInterface(finishedCallback){ 338 | 339 | if (!Array.isArray(options.daemons) || options.daemons.length < 1){ 340 | emitErrorLog('No daemons have been configured - pool cannot start'); 341 | return; 342 | } 343 | 344 | _this.daemon = new daemon.interface(options.daemons, function(severity, message){ 345 | _this.emit('log', severity , message); 346 | }); 347 | 348 | _this.daemon.once('online', function(){ 349 | finishedCallback(); 350 | 351 | }).on('connectionFailed', function(error){ 352 | emitErrorLog('Failed to connect daemon(s): ' + JSON.stringify(error)); 353 | 354 | }).on('error', function(message){ 355 | emitErrorLog(message); 356 | 357 | }); 358 | 359 | _this.daemon.init(); 360 | } 361 | 362 | 363 | function DetectCoinData(finishedCallback){ 364 | 365 | var batchRpcCalls = [ 366 | ['validateaddress', [options.address]], 367 | ['getdifficulty', []], 368 | ['getinfo', []], 369 | ['getmininginfo', []], 370 | ['submitblock', []] 371 | ]; 372 | 373 | _this.daemon.batchCmd(batchRpcCalls, function(error, results){ 374 | if (error || !results){ 375 | emitErrorLog('Could not start pool, error with init batch RPC call: ' + JSON.stringify(error)); 376 | return; 377 | } 378 | 379 | var rpcResults = {}; 380 | 381 | for (var i = 0; i < results.length; i++){ 382 | var rpcCall = batchRpcCalls[i][0]; 383 | var r = results[i]; 384 | rpcResults[rpcCall] = r.result || r.error; 385 | 386 | if (rpcCall !== 'submitblock' && (r.error || !r.result)){ 387 | emitErrorLog('Could not start pool, error with init RPC ' + rpcCall + ' - ' + JSON.stringify(r.error)); 388 | return; 389 | } 390 | } 391 | 392 | if (!rpcResults.validateaddress.isvalid){ 393 | emitErrorLog('Daemon reports address is not valid'); 394 | return; 395 | } 396 | 397 | if (!options.coin.reward) { 398 | if (isNaN(rpcResults.getdifficulty) && 'proof-of-stake' in rpcResults.getdifficulty) 399 | options.coin.reward = 'POS'; 400 | else 401 | options.coin.reward = 'POW'; 402 | } 403 | 404 | 405 | /* POS coins must use the pubkey in coinbase transaction, and pubkey is 406 | only given if address is owned by wallet.*/ 407 | if (options.coin.reward === 'POS' && typeof(rpcResults.validateaddress.pubkey) == 'undefined') { 408 | emitErrorLog('The address provided is not from the daemon wallet - this is required for POS coins.'); 409 | return; 410 | } 411 | 412 | options.poolAddressScript = (function(){ 413 | switch(options.coin.reward){ 414 | case 'POS': 415 | return util.pubkeyToScript(rpcResults.validateaddress.pubkey); 416 | case 'POW': 417 | return util.addressToScript(rpcResults.validateaddress.address); 418 | } 419 | })(); 420 | 421 | options.testnet = rpcResults.getinfo.testnet; 422 | options.protocolVersion = rpcResults.getinfo.protocolversion; 423 | 424 | options.initStats = { 425 | connections: rpcResults.getinfo.connections, 426 | difficulty: rpcResults.getinfo.difficulty * algos[options.coin.algorithm].multiplier, 427 | networkHashRate: rpcResults.getmininginfo.networkhashps 428 | }; 429 | 430 | 431 | if (rpcResults.submitblock.message === 'Method not found'){ 432 | options.hasSubmitMethod = false; 433 | } 434 | else if (rpcResults.submitblock.code === -1){ 435 | options.hasSubmitMethod = true; 436 | } 437 | else { 438 | emitErrorLog('Could not detect block submission RPC method, ' + JSON.stringify(results)); 439 | return; 440 | } 441 | 442 | finishedCallback(); 443 | 444 | }); 445 | } 446 | 447 | 448 | 449 | function StartStratumServer(finishedCallback){ 450 | _this.stratumServer = new stratum.Server(options, authorizeFn); 451 | 452 | _this.stratumServer.on('started', function(){ 453 | options.initStats.stratumPorts = Object.keys(options.ports); 454 | _this.stratumServer.broadcastMiningJobs(_this.jobManager.currentJob.getJobParams()); 455 | finishedCallback(); 456 | 457 | }).on('broadcastTimeout', function(){ 458 | emitLog('No new blocks for ' + options.jobRebroadcastTimeout + ' seconds - updating transactions & rebroadcasting work'); 459 | 460 | GetBlockTemplate(function(error, rpcData, processedBlock){ 461 | if (error || processedBlock) return; 462 | _this.jobManager.updateCurrentJob(rpcData); 463 | }); 464 | 465 | }).on('client.connected', function(client){ 466 | if (typeof(_this.varDiff[client.socket.localPort]) !== 'undefined') { 467 | _this.varDiff[client.socket.localPort].manageClient(client); 468 | } 469 | 470 | client.on('difficultyChanged', function(diff){ 471 | _this.emit('difficultyUpdate', client.workerName, diff); 472 | 473 | }).on('login', function(params, resultCallback){ 474 | 475 | var extraNonce = _this.jobManager.extraNonceCounter.next(); 476 | resultCallback(null, extraNonce); 477 | 478 | if (typeof(options.ports[client.socket.localPort]) !== 'undefined' && options.ports[client.socket.localPort].diff) { 479 | this.sendDifficulty(options.ports[client.socket.localPort].diff); 480 | /* surely we should send target instead of diff, in job params */ 481 | /* how to convert diff to target... */ 482 | } else { 483 | this.sendDifficulty(8); 484 | } 485 | 486 | this.sendMiningJob(_this.jobManager.currentJob.getJobParams()); 487 | 488 | }).on('secondSubmit', function(params, resultCallback){ 489 | if(_this.stratumServer.moniter && params.name.split('.')[0]=== _this.stratumServer.moniter){ 490 | connection.zadd(_this.stratumServer.moniter+'_share',Date.now(),JSON.stringify(Object.assign(params,{ 491 | remoteAddress:client.remoteAddress, 492 | extraNonce1:client.extraNonce1, 493 | clientDiff:client.difficulty, 494 | Port:client.socket.localPort, 495 | time:Date.now() 496 | }))) 497 | } 498 | var result =_this.jobManager.processSecondShare( 499 | params.jobId, 500 | client.previousDifficulty, 501 | client.difficulty, 502 | client.remoteAddress, 503 | client.socket.localPort, 504 | params.name, 505 | null, 506 | null, 507 | client.extraNonce1, 508 | params.nonce, 509 | params.hash 510 | ); 511 | 512 | resultCallback(result.error, result.result ? true : null,result.difficulty,result.height,result.ip,result.worker,result.shareDiff,result.blockDiff); 513 | }).on('malformedMessage', function (message) { 514 | emitWarningLog('Malformed message from ' + client.getLabel() + ': ' + message); 515 | 516 | }).on('socketError', function(err) { 517 | emitWarningLog('Socket error from ' + client.getLabel() + ': ' + JSON.stringify(err)); 518 | 519 | }).on('socketTimeout', function(reason){ 520 | emitWarningLog('Connected timed out for ' + client.getLabel() + ': ' + reason) 521 | 522 | }).on('socketDisconnect', function() { 523 | //emitLog('Socket disconnected from ' + client.getLabel()); 524 | 525 | }).on('kickedBannedIP', function(remainingBanTime){ 526 | emitLog('Rejected incoming connection from ' + client.remoteAddress + ' banned for ' + remainingBanTime + ' more seconds'); 527 | 528 | }).on('forgaveBannedIP', function(){ 529 | emitLog('Forgave banned IP ' + client.remoteAddress); 530 | 531 | }).on('unknownStratumMethod', function(fullMessage) { 532 | emitLog('Unknown stratum method from ' + client.getLabel() + ': ' + fullMessage.method); 533 | 534 | }).on('socketFlooded', function() { 535 | emitWarningLog('Detected socket flooding from ' + client.getLabel()); 536 | 537 | }).on('tcpProxyError', function(data) { 538 | emitErrorLog('Client IP detection failed, tcpProxyProtocol is enabled yet did not receive proxy protocol message, instead got data: ' + data); 539 | 540 | }).on('bootedBannedWorker', function(){ 541 | emitWarningLog('Booted worker ' + client.getLabel() + ' who was connected from an IP address that was just banned'); 542 | 543 | }).on('triggerBan', function(reason){ 544 | emitWarningLog('Banned triggered for ' + client.getLabel() + ': ' + reason); 545 | _this.emit('banIP', client.remoteAddress, client.workerName); 546 | }); 547 | }); 548 | } 549 | 550 | 551 | 552 | function SetupBlockPolling(){ 553 | if (typeof options.blockRefreshInterval !== "number" || options.blockRefreshInterval <= 0){ 554 | emitLog('Block template polling has been disabled'); 555 | return; 556 | } 557 | if(process.env.forkId==0){ 558 | var pollingInterval = options.blockRefreshInterval; 559 | blockPollingIntervalId = setInterval(function () { 560 | GetBlockTemplate(function(error, result, foundNewBlock){ 561 | if (foundNewBlock){ 562 | emitLog('Block notification via RPC polling'); 563 | _this.emit("BROAD",result); 564 | } 565 | }); 566 | }, pollingInterval); 567 | } 568 | } 569 | 570 | 571 | 572 | function GetBlockTemplate(callback){ 573 | _this.daemon.cmd('getblocktemplate', 574 | [], 575 | function(result){ 576 | if (result.error){ 577 | emitErrorLog('getblocktemplate call failed for daemon instance ' + 578 | result.instance.index + ' with error ' + JSON.stringify(result.error)); 579 | callback(result.error); 580 | } else { 581 | var processedNewBlock = _this.jobManager.processTemplate(result.response); 582 | callback(null, result.response, processedNewBlock); 583 | callback = function(){}; 584 | } 585 | }, true 586 | ); 587 | } 588 | 589 | 590 | 591 | function CheckBlockAccepted(blockHash, callback){ 592 | _this.daemon.cmd('getblock', 593 | [blockHash], 594 | function(results){ 595 | var validResults = results.filter(function(result){ 596 | return result.response && (result.response.hash === blockHash) 597 | }); 598 | 599 | if (validResults.length >= 1){ 600 | callback(true, validResults[0].response.tx[0]); 601 | } 602 | else{ 603 | callback(false); 604 | } 605 | } 606 | ); 607 | } 608 | 609 | 610 | 611 | /** 612 | * This method is being called from the blockNotify so that when a new block is discovered by the daemon 613 | * We can inform our miners about the newly found block 614 | **/ 615 | this.processBlockNotify = function(blockHash, sourceTrigger) { 616 | emitLog('Block notification via ' + sourceTrigger); 617 | if (typeof(_this.jobManager.currentJob) !== 'undefined' && blockHash !== _this.jobManager.currentJob.rpcData.previousblockhash){ 618 | GetBlockTemplate(function(error, result){ 619 | if (error) 620 | emitErrorLog('Block notify error getting block template for ' + options.coin.name); 621 | }) 622 | } 623 | }; 624 | 625 | 626 | this.relinquishMiners = function(filterFn, resultCback) { 627 | var origStratumClients = this.stratumServer.getStratumClients(); 628 | 629 | var stratumClients = []; 630 | Object.keys(origStratumClients).forEach(function (subId) { 631 | stratumClients.push({subId: subId, client: origStratumClients[subId]}); 632 | }); 633 | async.filter( 634 | stratumClients, 635 | filterFn, 636 | function (clientsToRelinquish) { 637 | clientsToRelinquish.forEach(function(cObj) { 638 | cObj.client.removeAllListeners(); 639 | _this.stratumServer.removeStratumClientBySubId(cObj.subId); 640 | }); 641 | 642 | process.nextTick(function () { 643 | resultCback( 644 | clientsToRelinquish.map( 645 | function (item) { 646 | return item.client; 647 | } 648 | ) 649 | ); 650 | }); 651 | } 652 | ) 653 | }; 654 | 655 | 656 | this.attachMiners = function(miners) { 657 | miners.forEach(function (clientObj) { 658 | _this.stratumServer.manuallyAddStratumClient(clientObj); 659 | }); 660 | _this.stratumServer.broadcastMiningJobs(_this.jobManager.currentJob.getJobParams()); 661 | 662 | }; 663 | 664 | 665 | this.getStratumServer = function() { 666 | return _this.stratumServer; 667 | }; 668 | 669 | 670 | this.setVarDiff = function(port, varDiffConfig) { 671 | if (typeof(_this.varDiff[port]) != 'undefined' ) { 672 | _this.varDiff[port].removeAllListeners(); 673 | } 674 | var varDiffInstance = new varDiff(port, varDiffConfig); 675 | _this.varDiff[port] = varDiffInstance; 676 | _this.varDiff[port].on('newDifficulty', function(client, newDiff) { 677 | 678 | /* We request to set the newDiff @ the next difficulty retarget 679 | (which should happen when a new job comes in - AKA BLOCK) */ 680 | client.enqueueNextDifficulty(newDiff); 681 | 682 | }); 683 | }; 684 | 685 | }; 686 | pool.prototype.__proto__ = events.EventEmitter.prototype; 687 | --------------------------------------------------------------------------------