From b291dc0c11a542bfcd264e6f6ce862543b8c1378 Mon Sep 17 00:00:00 2001 From: Traysi Date: Fri, 13 Mar 2020 16:44:11 -0500 Subject: [PATCH] Initial commit --- CHANGELOG.md | 29 ++ LICENSE | 10 +- README.md | 3 + lib/algoProperties.js | 20 ++ lib/blockTemplate.js | 183 ++++++++++++ lib/daemon.js | 194 +++++++++++++ lib/index.js | 16 + lib/jobManager.js | 332 +++++++++++++++++++++ lib/merkleTree.js | 26 ++ lib/peer.js | 226 +++++++++++++++ lib/pool.js | 659 ++++++++++++++++++++++++++++++++++++++++++ lib/stratum.js | 518 +++++++++++++++++++++++++++++++++ lib/transactions.js | 118 ++++++++ lib/util.js | 369 +++++++++++++++++++++++ lib/varDiff.js | 128 ++++++++ package.json | 84 ++++++ 16 files changed, 2910 insertions(+), 5 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100644 lib/algoProperties.js create mode 100644 lib/blockTemplate.js create mode 100644 lib/daemon.js create mode 100644 lib/index.js create mode 100644 lib/jobManager.js create mode 100644 lib/merkleTree.js create mode 100644 lib/peer.js create mode 100644 lib/pool.js create mode 100644 lib/stratum.js create mode 100644 lib/transactions.js create mode 100644 lib/util.js create mode 100644 lib/varDiff.js create mode 100644 package.json diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1be4e4c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,29 @@ +## v1.0.0 +* Started versioning +* Fixed Daemon reports invaid transaction +* Fixed mainnet\testnet for zcoin + +## v1.0.1 +* Updated lyra2z multiplier parameters to (2,8), that i forgot to include in previous version + +## v1.0.2 + +* Added x16r (Ravencoin) support. Thanks to a2hill + +## v1.0.3 +* Logging switched to winston + +## v1.0.4 +* https://github.com/foxer666/node-open-mining-portal/issues/40 + +## v1.0.5 +* https://github.com/foxer666/node-stratum-pool/issues/13 + +## v1.0.6 +* Update multihashing (fix neoscrypt) + +## v1.0.7 +* Update multihashing module + +## v1.0.8 +* https://github.com/foxer666/node-stratum-pool/issues/20 \ No newline at end of file diff --git a/LICENSE b/LICENSE index d159169..d7f1051 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ - GNU GENERAL PUBLIC LICENSE +GNU GENERAL PUBLIC LICENSE Version 2, June 1991 - Copyright (C) 1989, 1991 Free Software Foundation, Inc., + Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. @@ -290,8 +290,8 @@ to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - - Copyright (C) + {description} + Copyright (C) {year} {fullname} This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -329,7 +329,7 @@ necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. - , 1 April 1989 + {signature of Ty Coon}, 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f0d41b --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +High performance Stratum poolserver in Node.js for [NOMP (Node Open Mining Portal)](https://github.com/foxer666/node-open-mining-portal) + +Current version: v1.0.8 \ No newline at end of file diff --git a/lib/algoProperties.js b/lib/algoProperties.js new file mode 100644 index 0000000..53f439d --- /dev/null +++ b/lib/algoProperties.js @@ -0,0 +1,20 @@ +var util = require('./util.js'); + +var diff1 = global.diff1 = 0x00000fffff000000000000000000000000000000000000000000000000000000; + +var algos = module.exports = global.algos = { + 'kawpow': { + multiplier: 1, + diff: parseInt('0x00000fffff000000000000000000000000000000000000000000000000000000'), + hash: function(){ + return function(){ + return; + } + } + } +}; + +for (var algo in algos){ + if (!algos[algo].multiplier) + algos[algo].multiplier = 1; +} diff --git a/lib/blockTemplate.js b/lib/blockTemplate.js new file mode 100644 index 0000000..f7bf08c --- /dev/null +++ b/lib/blockTemplate.js @@ -0,0 +1,183 @@ +var bignum = require('bignum'); +var crypto = require('crypto'); +var SHA3 = require('sha3'); + +var merkle = require('./merkleTree.js'); +var transactions = require('./transactions.js'); +var util = require('./util.js'); + + +/** + * The BlockTemplate class holds a single job. + * and provides several methods to validate and submit it to the daemon coin +**/ +var BlockTemplate = module.exports = function BlockTemplate(jobId, rpcData, reward, recipients, poolAddress){ + + //epoch length + const EPOCH_LENGTH = 100; + + //private members + var submits = []; + + //public members + this.rpcData = rpcData; + this.jobId = jobId; + + // get target info + this.target = bignum(rpcData.target, 16); + this.target_hex = rpcData.target; + + this.difficulty = parseFloat((diff1 / this.target.toNumber()).toFixed(9)); + + //nTime + var nTime = util.packUInt32BE(rpcData.curtime).toString('hex'); + + //current time of issuing the template + var curTime = Date.now() / 1000 | 0; + + // generate the fees and coinbase tx + var blockReward = this.rpcData.coinbasevalue; + + var fees = []; + rpcData.transactions.forEach(function(value) { + fees.push(value); + }); + this.rewardFees = transactions.getFees(fees); + rpcData.rewardFees = this.rewardFees; + + if (typeof this.genTx === 'undefined') { + this.genTx = transactions.createGeneration(rpcData, blockReward, this.rewardFees, recipients, poolAddress).toString('hex'); + this.genTxHash = transactions.txHash(); + + // console.log('this.genTxHash: ' + transactions.txHash()); + // console.log('this.merkleRoot: ' + merkle.getRoot(rpcData, this.genTxHash)); + } + + // generate the merkle root + this.prevHashReversed = util.reverseBuffer(new Buffer(rpcData.previousblockhash, 'hex')).toString('hex'); + this.merkleRoot = merkle.getRoot(rpcData, this.genTxHash); + this.txCount = this.rpcData.transactions.length + 1; // add total txs and new coinbase + this.merkleRootReversed = util.reverseBuffer(new Buffer(this.merkleRoot, 'hex')).toString('hex'); + // we can't do anything else until we have a submission + + // console.log('this.prevHashReversed: ' + this.prevHashReversed); + + this.serializeHeader = function() { + var header = new Buffer(80); + var position = 0; + + header.write(util.packUInt32BE(this.rpcData.height).toString('hex'), position, 4, 'hex'); // height 42-46 + header.write(this.rpcData.bits, position += 4, 4, 'hex'); // bits 47-50 + header.write(nTime, position += 4, 4, 'hex'); // nTime 51-54 + header.write(this.merkleRoot, position += 4, 32, 'hex'); // merkelRoot 55-87 + header.write(this.rpcData.previousblockhash, position += 32, 32, 'hex'); // prevblockhash 88-120 + header.writeUInt32BE(this.rpcData.version, position + 32, 4); // version 121-153 + + header = util.reverseBuffer(header); + return header; + }; + + // join the header and txs together + this.serializeBlock = function(header_hash, nonce, mixhash) { + + header = this.serializeHeader(); + var foo = new Buffer(40); + + foo.write(util.reverseBuffer(nonce).toString('hex'), 0, 8, 'hex'); + foo.write(util.reverseBuffer(mixhash).toString('hex'), 8, 32,'hex'); + + buf = new Buffer.concat([ + header, + foo, + util.varIntBuffer(this.rpcData.transactions.length + 1), + new Buffer(this.genTx, 'hex') + ]); + + if (this.rpcData.transactions.length > 0) { + this.rpcData.transactions.forEach(function (value) { + tmpBuf = new Buffer.concat([buf, new Buffer(value.data, 'hex')]); + buf = tmpBuf; + }); + } + + /* + console.log('header: ' + header.toString('hex')); + console.log('soln: ' + soln.toString('hex')); + console.log('varInt: ' + varInt.toString('hex')); + console.log('this.genTx: ' + this.genTx); + console.log('data: ' + value.data); + console.log('buf_block: ' + buf.toString('hex')); + console.log('blockhex: ' + buf.toString('hex')); + */ + return buf; + }; + + // submit header_hash and nonce + this.registerSubmit = function(header, nonce){ + var submission = header + nonce; + if (submits.indexOf(submission) === -1){ + submits.push(submission); + return true; + } + return false; + }; + + + //powLimit * difficulty + var powLimit = algos.kawpow.diff; // TODO: Get algos object from argument + var adjPow = (powLimit / this.difficulty); + if ((64 - adjPow.toString(16).length) === 0) { + var zeroPad = ''; + } + else { + var zeroPad = '0'; + zeroPad = zeroPad.repeat((64 - (adjPow.toString(16).length))); + } + var target = (zeroPad + adjPow.toString(16)).substr(0,64); + //this.target_share_hex = target; + + let d = new SHA3.SHA3Hash(256); + var seedhash_buf = new Buffer(32); + var seedhash = seedhash_buf.toString('hex'); + this.epoch_number = Math.floor(this.rpcData.height / EPOCH_LENGTH); + for (var i=0; i override_target)) { + zeroPad = '0'; + zeroPad = zeroPad.repeat((64 - (override_target.toString(16).length))); + target = (zeroPad + override_target.toString(16)).substr(0,64); + } + + // used for mining.notify + this.getJobParams = function(){ + // console.log("RPC DATA IN job params: "+JSON.stringify(this.rpcData)); + if (!this.jobParams){ + this.jobParams = [ + this.jobId, + header_hash, + seedhash, + target, //target is overridden later to match miner varDiff + true, + this.rpcData.height, + this.rpcData.bits + ]; + } + return this.jobParams; + }; +}; + diff --git a/lib/daemon.js b/lib/daemon.js new file mode 100644 index 0000000..e992744 --- /dev/null +++ b/lib/daemon.js @@ -0,0 +1,194 @@ +var http = require('http'); +var cp = require('child_process'); +var events = require('events'); + +var async = require('async'); + +/** + * The daemon interface interacts with the coin daemon by using the rpc interface. + * in order to make it work it needs, as constructor, an array of objects containing + * - 'host' : hostname where the coin lives + * - 'port' : port where the coin accepts rpc connections + * - 'user' : username of the coin for the rpc interface + * - 'password': password for the rpc interface of the coin + **/ + +function DaemonInterface(daemons, logger) { + + //private members + var _this = this; + logger = logger || function (severity, message) { + console.log(severity + ': ' + message); + }; + + + var instances = (function () { + for (var i = 0; i < daemons.length; i++) + daemons[i]['index'] = i; + return daemons; + })(); + + + function init() { + isOnline(function (online) { + if (online) + _this.emit('online'); + }); + } + + function isOnline(callback) { + cmd('getinfo', [], function (results) { + var allOnline = results.every(function (result) { + return !results.error; + }); + callback(allOnline); + if (!allOnline) + _this.emit('connectionFailed', results); + }); + } + + + function performHttpRequest(instance, jsonData, callback) { + var options = { + hostname: (typeof(instance.host) === 'undefined' ? '127.0.0.1' : instance.host), + port: instance.port, + method: 'POST', + auth: instance.user + ':' + instance.password, + headers: { + 'Content-Length': jsonData.length + } + }; + + var parseJson = function (res, data) { + var dataJson; + + if (res.statusCode === 401) { + logger('error', 'Unauthorized RPC access - invalid RPC username or password'); + return; + } + + try { + dataJson = JSON.parse(data); + } + catch (e) { + if (data.indexOf(':-nan') !== -1) { + data = data.replace(/:-nan,/g, ":0"); + parseJson(res, data); + return; + } + logger('error', 'Could not parse rpc data from daemon instance ' + instance.index + + '\nRequest Data: ' + jsonData.substr(0,200) + + '\nResponse Data: ' + data.substr(0,200)); + + } + if (dataJson) + callback(dataJson.error, dataJson, data); + }; + + var req = http.request(options, function (res) { + var data = ''; + res.setEncoding('utf8'); + res.on('data', function (chunk) { + data += chunk; + }); + res.on('end', function () { + parseJson(res, data); + }); + }); + + req.on('error', function (e) { + if (e.code === 'ECONNREFUSED') + callback({type: 'offline', message: e.message}, null); + else + callback({type: 'request error', message: e.message}, null); + }); + + req.end(jsonData); + } + + + //Performs a batch JSON-RPC command - only uses the first configured rpc daemon + /* First argument must have: + [ + [ methodName, [params] ], + [ methodName, [params] ] + ] + */ + + function batchCmd(cmdArray, callback) { + + var requestJson = []; + + for (var i = 0; i < cmdArray.length; i++) { + requestJson.push({ + method: cmdArray[i][0], + params: cmdArray[i][1], + id: Date.now() + Math.floor(Math.random() * 10) + i + }); + } + + var serializedRequest = JSON.stringify(requestJson); + + performHttpRequest(instances[0], serializedRequest, function (error, result) { + callback(error, result); + }); + + } + + /* Sends a JSON RPC (http://json-rpc.org/wiki/specification) command to every configured daemon. + The callback function is fired once with the result from each daemon unless streamResults is + set to true. */ + function cmd(method, params, callback, streamResults, returnRawData) { + + var results = []; + + async.each(instances, function (instance, eachCallback) { + + var itemFinished = function (error, result, data) { + + var returnObj = { + error: error, + response: (result || {}).result, + instance: instance + }; + if (returnRawData) returnObj.data = data; + if (streamResults) callback(returnObj); + else results.push(returnObj); + eachCallback(); + itemFinished = function () { + }; + }; + + var requestJson = JSON.stringify({ + jsonrpc: '1.0', + method: method, + params: params, + id: Date.now() + Math.floor(Math.random() * 10) + }); + // console.log("RPC Request to daemon: "+requestJson); + + performHttpRequest(instance, requestJson, function (error, result, data) { + itemFinished(error, result, data); + }); + + + }, function () { + if (!streamResults) { + callback(results); + } + }); + + } + + + //public members + + this.init = init; + this.isOnline = isOnline; + this.cmd = cmd; + this.batchCmd = batchCmd; +} + +DaemonInterface.prototype.__proto__ = events.EventEmitter.prototype; + +exports.interface = DaemonInterface; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..a0871af --- /dev/null +++ b/lib/index.js @@ -0,0 +1,16 @@ +var net = require('net'); +var events = require('events'); + +//Gives us global access to everything we need for each hashing algorithm +require('./algoProperties.js'); + +var pool = require('./pool.js'); + +exports.daemon = require('./daemon.js'); +exports.varDiff = require('./varDiff.js'); + + +exports.createPool = function (poolOptions, authorizeFn) { + var newPool = new pool(poolOptions, authorizeFn); + return newPool; +}; diff --git a/lib/jobManager.js b/lib/jobManager.js new file mode 100644 index 0000000..920d1b0 --- /dev/null +++ b/lib/jobManager.js @@ -0,0 +1,332 @@ +var events = require('events'); +var crypto = require('crypto'); +var SHA3 = require('sha3'); +var async = require('async'); +var http = require('http'); + +var bignum = require('bignum'); +var BigInt = require('big-integer'); + +var util = require('./util.js'); +var daemon = require('./daemon.js'); +var blockTemplate = require('./blockTemplate.js'); + +// Unique extranonce per subscriber +var ExtraNonceCounter = function () { + this.next = function () { + return(crypto.randomBytes(3).toString('hex')); + }; +}; + +//Unique job per new block template +var JobCounter = function () { + var counter = 0x0000cccc; + + this.next = function () { + counter++; + if (counter % 0xffffffffff === 0) counter = 1; + return this.cur(); + }; + + this.cur = function () { + var counter_buf = new Buffer(32); + counter_buf.writeUIntBE('000000000000000000000000', 0, 24); + counter_buf.writeUIntBE(counter, 24, 8); + return counter_buf.toString('hex'); + }; +}; +function isHexString(s) { + var check = String(s).toLowerCase(); + if(check.length % 2) { + return false; + } + for (i = 0; i < check.length; i=i+2) { + var c = check[i] + check[i+1]; + if (!isHex(c)) + return false; + } + return true; +} +function isHex(c) { + var a = parseInt(c,16); + var b = a.toString(16).toLowerCase(); + if(b.length % 2) { b = '0' + b; } + if (b !== c) { return false; } + return true; +} + +/** + * Emits: + * - newBlock(blockTemplate) - When a new block (previously unknown to the JobManager) is added, use this event to broadcast new jobs + * - share(shareData, blockHex) - When a worker submits a share. It will have blockHex if a block was found + **/ +var JobManager = module.exports = function JobManager(options) { + + var emitLog = function (text) { _this.emit('log', 'debug', text); }; + var emitWarningLog = function (text) { _this.emit('log', 'warning', text); }; + var emitErrorLog = function (text) { _this.emit('log', 'error', text); }; + var emitSpecialLog = function (text) { _this.emit('log', 'special', text); }; + + //private members + var _this = this; + var jobCounter = new JobCounter(); + + function SetupJobDaemonInterface(finishedCallback) { + + if (!Array.isArray(options.daemons) || options.daemons.length < 1) { + emitErrorLog('No daemons have been configured - pool cannot start'); + return; + } + + _this.daemon = new daemon.interface(options.daemons, function (severity, message) { + _this.emit('log', severity, message); + }); + + _this.daemon.once('online', function () { + // console.log("The util daemon is alive."); + finishedCallback(); + }).on('connectionFailed', function (error) { + emitErrorLog('Failed to connect daemon(s): ' + JSON.stringify(error)); + }).on('error', function (message) { + emitErrorLog(message); + }); + _this.daemon.init(); + } + + SetupJobDaemonInterface(function () {}); + + var shareMultiplier = algos[options.coin.algorithm].multiplier; + + //public members + + this.extraNonceCounter = new ExtraNonceCounter(); + + this.currentJob; + this.validJobs = {}; + + var hashDigest = algos[options.coin.algorithm].hash(options.coin); + + var coinbaseHasher = (function () { + switch (options.coin.algorithm) { + default: + return util.sha256d; + } + })(); + + + var blockHasher = (function () { + switch (options.coin.algorithm) { + case 'sha1': + return function (d) { + return util.reverseBuffer(util.sha256d(d)); + }; + default: + return function (d) { + return util.reverseBuffer(util.sha256(d)); + }; + } + })(); + + this.updateCurrentJob = function (rpcData) { + var tmpBlockTemplate = new blockTemplate( + jobCounter.next(), + rpcData, + options.coin.reward, + options.recipients, + options.address + ); + + _this.currentJob = tmpBlockTemplate; + _this.emit('updatedBlock', tmpBlockTemplate, true); + _this.validJobs[tmpBlockTemplate.jobId] = tmpBlockTemplate; + + }; + + //returns true if processed a new block + this.processTemplate = function (rpcData) { + + /* Block is new if A) its the first block we have seen so far or B) the blockhash is different and the + block height is greater than the one we have */ + var isNewBlock = typeof(_this.currentJob) === 'undefined'; + if (!isNewBlock && _this.currentJob.rpcData.previousblockhash !== rpcData.previousblockhash) { + isNewBlock = true; + + //If new block is outdated/out-of-sync than return + if (rpcData.height < _this.currentJob.rpcData.height) return false; + } + + if (!isNewBlock) return false; + + + var tmpBlockTemplate = new blockTemplate( + jobCounter.next(), + rpcData, + options.coin.reward, + options.recipients, + options.address + ); + + this.currentJob = tmpBlockTemplate; + + this.validJobs = {}; + _this.emit('newBlock', tmpBlockTemplate); + + this.validJobs[tmpBlockTemplate.jobId] = tmpBlockTemplate; + + return true; + + }; + + this.processShare = function (miner_given_jobId, previousDifficulty, difficulty, miner_given_nonce, ipAddress, port, workerName, miner_given_header, miner_given_mixhash, callback_parent) { + + var submitTime = Date.now() / 1000 | 0; + + var shareError = function (error) { + _this.emit('share', { + job: miner_given_jobId, + ip: ipAddress, + worker: workerName, + difficulty: difficulty, + error: error[1] + }); + callback_parent( {error: error, result: null}); + return; + }; + + var job = this.validJobs[miner_given_jobId]; + + if (typeof job === 'undefined' || job.jobId != miner_given_jobId) + return shareError([20, 'job not found']); + + //calculate our own header hash, do not trust miner-given value + var headerBuffer = job.serializeHeader(); // 140 bytes, doesn't contain nonce or mixhash/solution + var header_hash = util.reverseBuffer(util.sha256d(headerBuffer)).toString('hex'); + + if (job.curTime < (submitTime - 600)) + return shareError([20, 'job is too old']); + + if (!isHexString(miner_given_header)) + return shareError([20, 'invalid header hash, must be hex']); + + if (header_hash != miner_given_header) + return shareError([20, 'invalid header hash']); + + if (!isHexString(miner_given_nonce)) + return shareError([20, 'invalid nonce, must be hex']); + + if (!isHexString(miner_given_mixhash)) + return shareError([20, 'invalid mixhash, must be hex']); + + if (miner_given_nonce.length !== 16) + return shareError([20, 'incorrect size of nonce, must be 8 bytes']); + + if (miner_given_mixhash.length !== 64) + return shareError([20, 'incorrect size of mixhash, must be 32 bytes']); + + if (!job.registerSubmit(header_hash.toLowerCase(), miner_given_nonce.toLowerCase())) + return shareError([22, 'duplicate share']); + + var powLimit = algos.kawpow.diff; // TODO: Get algos object from argument + var adjPow = powLimit / difficulty; + if ((64 - adjPow.toString(16).length) === 0) { + var zeroPad = ''; + } + else { + var zeroPad = '0'; + zeroPad = zeroPad.repeat((64 - (adjPow.toString(16).length))); + } + var target_share_hex = (zeroPad + adjPow.toString(16)).substr(0,64); + + var blockHashInvalid; + var blockHash; + var blockHex; + + + async.series([ + function(callback) { + var kawpowd_url = 'http://'+options.kawpow_wrapper_host+":"+options.kawpow_wrapper_port+'/'+'?header_hash='+header_hash+'&mix_hash='+miner_given_mixhash+'&nonce='+miner_given_nonce+'&height='+job.rpcData.height+'&share_boundary='+target_share_hex+'&block_boundary='+job.target_hex; + + http.get(kawpowd_url, function (res) { + res.setEncoding("utf8"); + let body = ""; + res.on("data", data => { + body += data; + }); + res.on("end", () => { + body = JSON.parse(body); + // console.log("JSON RESULT FROM KAWPOWD: "+JSON.stringify(body)); + console.log("********** INCOMING SHARE FROM WORKER ************"); + console.log("header_hash = " + header_hash); + console.log("miner_sent_header_hash = " + miner_given_header); + console.log("miner_sent_mixhash = " + miner_given_mixhash); + console.log("miner_sent_nonce = " + miner_given_nonce); + console.log("height = " + job.rpcData.height); + console.log("BLOCK.target = " + job.target_hex); + console.log('SHARE.target = ' + target_share_hex); + console.log('digest = ' + body.digest); + console.log("miner_sent_jobid = " + miner_given_jobId); + console.log('job = ' + miner_given_jobId); + console.log('worker = ' + workerName); + console.log('height = ' + job.rpcData.height); + console.log('difficulty = ' + difficulty); + console.log('kawpowd_url = ' + kawpowd_url); + console.log("********** END INCOMING SHARE FROM WORKER ************"); + if (body.share == false) { + if (body.block == false) { + // It didn't meet either requirement. + callback('kawpow share didn\'t meet job or block difficulty level', false); + return shareError([20, 'kawpow validation failed']); + } + } + + // At this point, either share or block is true (or both) + + if (body.block == true) { + // Good block. + blockHex = job.serializeBlock(new Buffer(header_hash, 'hex'), new Buffer(miner_given_nonce, 'hex'), new Buffer(miner_given_mixhash, 'hex')).toString('hex'); + blockHash = body.digest; + } + callback(null, true); + return; + }); + }); + }, + function(callback) { + + var blockDiffAdjusted = job.difficulty * shareMultiplier + var shareDiffFixed = undefined; + + if (blockHash !== undefined) { + var headerBigNum = bignum.fromBuffer(blockHash, {endian: 'little', size: 32}); + var shareDiff = diff1 / headerBigNum.toNumber() * shareMultiplier; + shareDiffFixed = shareDiff.toFixed(8); + } + + _this.emit('share', { + job: miner_given_jobId, + ip: ipAddress, + port: port, + worker: workerName, + height: job.rpcData.height, + blockReward: job.rpcData.coinbasevalue, + difficulty: difficulty, + shareDiff: shareDiffFixed, + blockDiff: blockDiffAdjusted, + blockDiffActual: job.difficulty, + blockHash: blockHash, + blockHashInvalid: blockHashInvalid + }, blockHex); + + callback_parent({result: true, error: null, blockHash: blockHash}); + callback(null, true); + return; + } + ], function(err, results) { + if (err != null) { + emitErrorLog("kawpow verify failed, ERRORS: "+err); + return; + } + }); + } +}; +JobManager.prototype.__proto__ = events.EventEmitter.prototype; diff --git a/lib/merkleTree.js b/lib/merkleTree.js new file mode 100644 index 0000000..8b48e1b --- /dev/null +++ b/lib/merkleTree.js @@ -0,0 +1,26 @@ +var Promise = require('promise'); +var merklebitcoin = Promise.denodeify(require('merkle-bitcoin')); +var util = require('./util.js'); + + +function calcRoot(hashes) { + var result = merklebitcoin(hashes); + //console.log(Object.values(result)[2].root); + return Object.values(result)[2].root; +} + +exports.getRoot = function (rpcData, generateTxRaw) { + hashes = [util.reverseBuffer(new Buffer(generateTxRaw, 'hex')).toString('hex')]; + rpcData.transactions.forEach(function (value) { + if (value.txid !== undefined) { + hashes.push(value.txid); + } else { + hashes.push(value.hash); + } + }); + if (hashes.length === 1) { + return hashes[0]; + } + var result = calcRoot(hashes); + return result; +}; diff --git a/lib/peer.js b/lib/peer.js new file mode 100644 index 0000000..ef63485 --- /dev/null +++ b/lib/peer.js @@ -0,0 +1,226 @@ +var net = require('net'); +var crypto = require('crypto'); +var events = require('events'); + +var util = require('./util.js'); + + +//Example of p2p in node from TheSeven: http://paste.pm/e54.js + + +var fixedLenStringBuffer = function (s, len) { + var buff = new Buffer(len); + buff.fill(0); + buff.write(s); + return buff; +}; + +var commandStringBuffer = function (s) { + return fixedLenStringBuffer(s, 12); +}; + +/* Reads a set amount of bytes from a flowing stream, argument descriptions: + - stream to read from, must have data emitter + - amount of bytes to read + - preRead argument can be used to set start with an existing data buffer + - callback returns 1) data buffer and 2) lopped/over-read data */ +var readFlowingBytes = function (stream, amount, preRead, callback) { + + var buff = preRead ? preRead : new Buffer([]); + + var readData = function (data) { + buff = Buffer.concat([buff, data]); + if (buff.length >= amount) { + var returnData = buff.slice(0, amount); + var lopped = buff.length > amount ? buff.slice(amount) : null; + callback(returnData, lopped); + } + else + stream.once('data', readData); + }; + + readData(new Buffer([])); +}; + +var Peer = module.exports = function (options) { + + var _this = this; + var client; + var magic = new Buffer(options.testnet ? options.coin.peerMagicTestnet : options.coin.peerMagic, 'hex'); + var magicInt = magic.readUInt32LE(0); + var verack = false; + var validConnectionConfig = true; + + //https://en.bitcoin.it/wiki/Protocol_specification#Inventory_Vectors + var invCodes = { + error: 0, + tx: 1, + block: 2 + }; + + var networkServices = new Buffer('0100000000000000', 'hex'); //NODE_NETWORK services (value 1 packed as uint64) + var emptyNetAddress = new Buffer('010000000000000000000000000000000000ffff000000000000', 'hex'); + var userAgent = util.varStringBuffer('/node-stratum/'); + var blockStartHeight = new Buffer('00000000', 'hex'); //block start_height, can be empty + + //If protocol version is new enough, add do not relay transactions flag byte, outlined in BIP37 + //https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki#extensions-to-existing-messages + var relayTransactions = options.p2p.disableTransactions === true ? new Buffer([false]) : new Buffer([]); + + var commands = { + version: commandStringBuffer('version'), + inv: commandStringBuffer('inv'), + verack: commandStringBuffer('verack'), + addr: commandStringBuffer('addr'), + getblocks: commandStringBuffer('getblocks') + }; + + + (function init() { + Connect(); + })(); + + + function Connect() { + + client = net.connect({ + host: options.p2p.host, + port: options.p2p.port + }, function () { + SendVersion(); + }); + client.on('close', function () { + if (verack) { + _this.emit('disconnected'); + verack = false; + Connect(); + } + else if (validConnectionConfig) + _this.emit('connectionRejected'); + + }); + client.on('error', function (e) { + if (e.code === 'ECONNREFUSED') { + validConnectionConfig = false; + _this.emit('connectionFailed'); + } + else + _this.emit('socketError', e); + }); + + + SetupMessageParser(client); + + } + + function SetupMessageParser(client) { + + var beginReadingMessage = function (preRead) { + + readFlowingBytes(client, 24, preRead, function (header, lopped) { + var msgMagic = header.readUInt32LE(0); + if (msgMagic !== magicInt) { + _this.emit('error', 'bad magic number from peer'); + while (header.readUInt32LE(0) !== magicInt && header.length >= 4) { + header = header.slice(1); + } + if (header.readUInt32LE(0) === magicInt) { + beginReadingMessage(header); + } else { + beginReadingMessage(new Buffer([])); + } + return; + } + var msgCommand = header.slice(4, 16).toString(); + var msgLength = header.readUInt32LE(16); + var msgChecksum = header.readUInt32LE(20); + readFlowingBytes(client, msgLength, lopped, function (payload, lopped) { + if (util.sha256d(payload).readUInt32LE(0) !== msgChecksum) { + _this.emit('error', 'bad payload - failed checksum'); + beginReadingMessage(null); + return; + } + HandleMessage(msgCommand, payload); + beginReadingMessage(lopped); + }); + }); + }; + + beginReadingMessage(null); + } + + + //Parsing inv message https://en.bitcoin.it/wiki/Protocol_specification#inv + function HandleInv(payload) { + //sloppy varint decoding + var count = payload.readUInt8(0); + payload = payload.slice(1); + if (count >= 0xfd) { + count = payload.readUInt16LE(0); + payload = payload.slice(2); + } + while (count--) { + switch (payload.readUInt32LE(0)) { + case invCodes.error: + break; + case invCodes.tx: + var tx = payload.slice(4, 36).toString('hex'); + break; + case invCodes.block: + var block = payload.slice(4, 36).toString('hex'); + _this.emit('blockFound', block); + break; + } + payload = payload.slice(36); + } + } + + function HandleMessage(command, payload) { + _this.emit('peerMessage', {command: command, payload: payload}); + switch (command) { + case commands.inv.toString(): + HandleInv(payload); + break; + case commands.verack.toString(): + if (!verack) { + verack = true; + _this.emit('connected'); + } + break; + default: + break; + } + + } + + //Message structure defined at: https://en.bitcoin.it/wiki/Protocol_specification#Message_structure + function SendMessage(command, payload) { + var message = Buffer.concat([ + magic, + command, + util.packUInt32LE(payload.length), + util.sha256d(payload).slice(0, 4), + payload + ]); + client.write(message); + _this.emit('sentMessage', message); + } + + function SendVersion() { + var payload = Buffer.concat([ + util.packUInt32LE(options.protocolVersion), + networkServices, + util.packInt64LE(Date.now() / 1000 | 0), + emptyNetAddress, //addr_recv, can be empty + emptyNetAddress, //addr_from, can be empty + crypto.pseudoRandomBytes(8), //nonce, random unique ID + userAgent, + blockStartHeight, + relayTransactions + ]); + SendMessage(commands.version, payload); + } + +}; + +Peer.prototype.__proto__ = events.EventEmitter.prototype; diff --git a/lib/pool.js b/lib/pool.js new file mode 100644 index 0000000..6d80048 --- /dev/null +++ b/lib/pool.js @@ -0,0 +1,659 @@ +var events = require('events'); +var async = require('async'); +const { spawn } = require('child_process'); + +var varDiff = require('./varDiff.js'); +var daemon = require('./daemon.js'); +var stratum = require('./stratum.js'); +var jobManager = require('./jobManager.js'); +var util = require('./util.js'); + +/*process.on('uncaughtException', function(err) { + console.log(err.stack); + throw err; + });*/ + +var pool = module.exports = function pool(options, authorizeFn) { + + this.options = options; + + var _this = this; + var blockPollingIntervalId; + + this.progpow_wrapper = null; + + + var emitLog = function (text) { + _this.emit('log', 'debug', text); + }; + var emitWarningLog = function (text) { + _this.emit('log', 'warning', text); + }; + var emitErrorLog = function (text) { + _this.emit('log', 'error', text); + }; + var emitSpecialLog = function (text) { + _this.emit('log', 'special', text); + }; + + + if (!(options.coin.algorithm in algos)) { + emitErrorLog('The ' + options.coin.algorithm + ' hashing algorithm is not supported.'); + throw new Error(); + } + + + this.start = function () { + SetupVarDiff(); + SetupApi(); + SetupDaemonInterface(function () { + DetectCoinData(function () { + SetupRecipients(); + SetupJobManager(); + OnBlockchainSynced(function () { + GetFirstJob(function () { + SetupBlockPolling(); + StartStratumServer(function () { + OutputPoolInfo(); + _this.emit('started'); + }); + }); + }); + }); + }); + }; + + + function GetFirstJob(finishedCallback) { + + GetBlockTemplate(function (error, result) { + if (error) { + emitErrorLog('Error with getblocktemplate on creating first job, server cannot start'); + return; + } + + var portWarnings = []; + + var networkDiffAdjusted = options.initStats.difficulty; + + Object.keys(options.ports).forEach(function (port) { + var portDiff = options.ports[port].diff; + if (networkDiffAdjusted < portDiff) + portWarnings.push('port ' + port + ' w/ diff ' + portDiff); + }); + + //Only let the first fork show synced status or the log wil look flooded with it + if (portWarnings.length > 0 && (!process.env.forkId || process.env.forkId === '0')) { + var warnMessage = 'Network diff of ' + networkDiffAdjusted + ' is lower than ' + + portWarnings.join(' and '); + emitWarningLog(warnMessage); + } + + finishedCallback(); + + }); + } + + + function OutputPoolInfo() { + + var startMessage = 'Stratum Pool Server Started for ' + options.coin.name + + ' [' + options.coin.symbol.toUpperCase() + '] {' + options.coin.algorithm + '}'; + if (process.env.forkId && process.env.forkId !== '0') { + emitLog(startMessage); + return; + } + var infoLines = [startMessage, + 'Network Connected:\t' + (options.testnet ? 'Testnet' : 'Mainnet'), + 'Detected Reward Type:\t' + options.coin.reward, + 'Current Block Height:\t' + _this.jobManager.currentJob.rpcData.height, + 'Current Block Diff:\t' + _this.jobManager.currentJob.difficulty * algos[options.coin.algorithm].multiplier, + 'Current Connect Peers:\t' + options.initStats.connections, + 'Network Hash Rate:\t' + util.getReadableHashRateString(options.initStats.networkHashRate), + 'Stratum Port(s):\t' + _this.options.initStats.stratumPorts.join(', ') + ]; + + if (typeof options.blockRefreshInterval === "number" && options.blockRefreshInterval > 0) + infoLines.push('Block polling every:\t' + options.blockRefreshInterval + ' ms'); + + emitSpecialLog(infoLines.join('\n\t\t\t\t\t\t')); + } + + + function OnBlockchainSynced(syncedCallback) { + + var checkSynced = function (displayNotSynced) { + _this.daemon.cmd('getblocktemplate', [{"capabilities": [ "coinbasetxn", "workid", "coinbase/append" ], "rules": [ "segwit" ]}], function (results) { + var synced = results.every(function (r) { + return !r.error || r.error.code !== -10; + }); + if (synced) { + syncedCallback(); + } + else { + if (displayNotSynced) displayNotSynced(); + setTimeout(checkSynced, 5000); + + //Only let the first fork show synced status or the log wil look flooded with it + if (!process.env.forkId || process.env.forkId === '0') + generateProgress(); + } + + }); + }; + checkSynced(function () { + //Only let the first fork show synced status or the log wil look flooded with it + if (!process.env.forkId || process.env.forkId === '0') + emitErrorLog('Daemon is still syncing with network (download blockchain) - server will be started once synced'); + }); + + + var generateProgress = function () { + + _this.daemon.cmd('getinfo', [], function (results) { + var blockCount = results.sort(function (a, b) { + return b.response.blocks - a.response.blocks; + })[0].response.blocks; + + //get list of peers and their highest block height to compare to ours + _this.daemon.cmd('getpeerinfo', [], function (results) { + + var peers = results[0].response; + var totalBlocks = peers.sort(function (a, b) { + return b.startingheight - a.startingheight; + })[0].startingheight; + + var percent = (blockCount / totalBlocks * 100).toFixed(2); + emitWarningLog('Downloaded ' + percent + '% of blockchain from ' + peers.length + ' peers'); + }); + + }); + }; + + } + + + function SetupApi() { + if (typeof(options.api) !== 'object' || typeof(options.api.start) !== 'function') { + } else { + options.api.start(_this); + } + } + + function SetupVarDiff() { + _this.varDiff = {}; + Object.keys(options.ports).forEach(function (port) { + if (options.ports[port].varDiff) + _this.setVarDiff(port, options.ports[port].varDiff); + }); + } + + /* + Coin daemons either use submitblock or getblocktemplate for submitting new blocks + */ + function SubmitBlock(blockHex, callback) { + + console.log("submitblock "+blockHex); + + var rpcCommand, rpcArgs; + if (options.hasSubmitMethod) { + rpcCommand = 'submitblock'; + rpcArgs = [blockHex]; + } + else { + rpcCommand = 'getblocktemplate'; + rpcArgs = [{'mode': 'submit', 'data': blockHex}]; + } + + + _this.daemon.cmd(rpcCommand, + rpcArgs, + function (results) { + for (var i = 0; i < results.length; i++) { + var result = results[i]; + if ((result.error) || (result.response === 'invalid-progpow')) { + emitErrorLog('rpc error with daemon instance ' + + result.instance.index + ' when submitting block with ' + rpcCommand + ' ' + + JSON.stringify(result.error + " result.response="+result.response) + ); + return; + } + else if (result.response === 'rejected') { + emitErrorLog('Daemon instance ' + result.instance.index + ' rejected a supposedly valid block'); + return; + } + } + emitLog('Submitted Block using ' + rpcCommand + ' successfully to daemon instance(s)'); + callback(); + } + ); + } + + function SetupRecipients() { + var recipients = []; + options.feePercent = 0; + options.rewardRecipients = options.rewardRecipients || {}; + + for (var r in options.rewardRecipients) { + var percent = options.rewardRecipients[r]; + var rObj = { + percent: percent, + address: r + }; + recipients.push(rObj); + options.feePercent += percent; + } + options.recipients = recipients; + } + + var jobManagerLastSubmitBlockHex = false; + + + function SetupJobManager() { + + _this.jobManager = new jobManager(options); + + _this.jobManager.on('newBlock', function (blockTemplate) { + //Check if stratumServer has been initialized yet + if (_this.stratumServer) { + _this.stratumServer.broadcastMiningJobs(blockTemplate.getJobParams()); + } + }).on('updatedBlock', function (blockTemplate) { + //Check if stratumServer has been initialized yet + if (_this.stratumServer) { + var job = blockTemplate.getJobParams(); + // Let the miners keep existing work. + job[4] = false; + + _this.stratumServer.broadcastMiningJobs(job); + } + }).on('share', function (shareData, blockHex) { + var isValidShare = !shareData.error; + var isValidBlock = !!blockHex; + var emitShare = function () { + _this.emit('share', isValidShare, isValidBlock, shareData); + }; + + /* + If we calculated that the block solution was found, + before we emit the share, lets submit the block, + then check if it was accepted using RPC getblock + */ + if (!isValidBlock) + emitShare(); + else { + if (jobManagerLastSubmitBlockHex === blockHex) { + emitWarningLog('Warning, ignored duplicate submit block'); // + blockHex); //<< blockHex could be huge + } else { + jobManagerLastSubmitBlockHex = blockHex; + SubmitBlock(blockHex, function () { + CheckBlockAccepted(shareData.blockHash, function (isAccepted, tx) { + isValidBlock = isAccepted === true; + if (isValidBlock === true) { + shareData.txHash = tx; + } else { + shareData.error = tx; + } + emitShare(); + GetBlockTemplate(function (error, result, foundNewBlock) { + if (foundNewBlock) { + emitLog('Block notification via RPC after block submission'); + } + }); + }); + }); + } + } + }).on('log', function (severity, message) { + _this.emit('log', severity, message); + }); + } + + + function SetupDaemonInterface(finishedCallback) { + + if (!Array.isArray(options.daemons) || options.daemons.length < 1) { + emitErrorLog('No daemons have been configured - pool cannot start'); + return; + } + + _this.daemon = new daemon.interface(options.daemons, function (severity, message) { + _this.emit('log', severity, message); + }); + + _this.daemon.once('online', function () { + finishedCallback(); + + }).on('connectionFailed', function (error) { + emitErrorLog('Failed to connect daemon(s): ' + JSON.stringify(error)); + + }).on('error', function (message) { + emitErrorLog(message); + + }); + + _this.daemon.init(); + } + + + function DetectCoinData(finishedCallback) { + + var batchRpcCalls = [ + ['validateaddress', [options.address]], + ['getdifficulty', []], + ['getinfo', []], + ['getmininginfo', []], + ['submitblock', []] + ]; + + _this.daemon.batchCmd(batchRpcCalls, function (error, results) { + if (error || !results) { + emitErrorLog('Could not start pool, error with init batch RPC call: ' + JSON.stringify(error)); + return; + } + + var rpcResults = {}; + + for (var i = 0; i < results.length; i++) { + var rpcCall = batchRpcCalls[i][0]; + var r = results[i]; + rpcResults[rpcCall] = r.result || r.error; + + if (rpcCall !== 'submitblock' && (r.error || !r.result)) { + emitErrorLog('Could not start pool, error with init RPC ' + rpcCall + ' - ' + JSON.stringify(r.error)); + return; + } + } + + if (!rpcResults.validateaddress.isvalid) { + emitErrorLog('Daemon reports address is not valid'); + return; + } + + if (isNaN(rpcResults.getdifficulty) && 'proof-of-stake' in rpcResults.getdifficulty) + options.coin.reward = 'POS'; + else + options.coin.reward = 'POW'; + + + /* POS coins must use the pubkey in coinbase transaction, and pubkey is + only given if address is owned by wallet.*/ + if (options.coin.reward === 'POS' && typeof(rpcResults.validateaddress.pubkey) === 'undefined') { + emitErrorLog('The address provided is not from the daemon wallet - this is required for POS coins.'); + return; + } + + options.poolAddressScript = (function () { + return util.addressToScript(rpcResults.validateaddress.address); + })(); + + options.testnet = rpcResults.getinfo.testnet; + options.protocolVersion = rpcResults.getinfo.protocolversion; + + options.initStats = { + connections: rpcResults.getinfo.connections, + difficulty: rpcResults.getinfo.difficulty * algos[options.coin.algorithm].multiplier, + networkHashRate: rpcResults.getmininginfo.networkhashps + }; + + + if (rpcResults.submitblock.message === 'Method not found') { + options.hasSubmitMethod = false; + } + else if (rpcResults.submitblock.code === -1) { + options.hasSubmitMethod = true; + } + else { + emitErrorLog('Could not detect block submission RPC method, ' + JSON.stringify(results)); + return; + } + + finishedCallback(); + + }); + } + + + function StartStratumServer(finishedCallback) { + _this.stratumServer = new stratum.Server(options, authorizeFn); + + _this.stratumServer.on('started', function () { + options.initStats.stratumPorts = Object.keys(options.ports); + _this.stratumServer.broadcastMiningJobs(_this.jobManager.currentJob.getJobParams()); + finishedCallback(); + + }).on('broadcastTimeout', function () { + emitLog('No new blocks for ' + options.jobRebroadcastTimeout + ' seconds - updating transactions & rebroadcasting work'); + + GetBlockTemplate(function (error, rpcData, processedBlock) { + if (error || processedBlock) return; + _this.jobManager.updateCurrentJob(rpcData); + }); + + }).on('client.connected', function (client) { + if (typeof(_this.varDiff[client.socket.localPort]) !== 'undefined') { + _this.varDiff[client.socket.localPort].manageClient(client); + } + + client.on('difficultyChanged', function (diff) { + _this.emit('difficultyUpdate', client.workerName, diff); + + }).on('subscription', function (params, resultCallback) { + + var extraNonce = _this.jobManager.extraNonceCounter.next(); + resultCallback(null, + extraNonce, + extraNonce + ); + + if (typeof(options.ports[client.socket.localPort]) !== 'undefined' && options.ports[client.socket.localPort].diff) { + this.sendDifficulty(options.ports[client.socket.localPort].diff); + } else { + this.sendDifficulty(8); + } + + + this.sendMiningJob(_this.jobManager.currentJob.getJobParams()); + + }).on('authorization', function (params) { + }).on('submit', function (params, resultCallback) { + var result = _this.jobManager.processShare( + params.jobId, + client.previousDifficulty, + client.difficulty, + params.nonce, + client.remoteAddress, + client.socket.localPort, + params.name, + params.header, + params.mixhash + , function (result) { resultCallback(result.error, result.result ? true : null) }); + + }).on('malformedMessage', function (message) { + emitWarningLog('Malformed message from ' + client.getLabel() + ': ' + message); + + }).on('socketError', function (err) { + emitWarningLog('Socket error from ' + client.getLabel() + ': ' + JSON.stringify(err)); + + }).on('socketTimeout', function (reason) { + emitWarningLog('Connected timed out for ' + client.getLabel() + ': ' + reason) + + }).on('socketDisconnect', function () { + //emitLog('Socket disconnected from ' + client.getLabel()); + + }).on('kickedBannedIP', function (remainingBanTime) { + emitLog('Rejected incoming connection from ' + client.remoteAddress + ' banned for ' + remainingBanTime + ' more seconds'); + + }).on('forgaveBannedIP', function () { + emitLog('Forgave banned IP ' + client.remoteAddress); + + }).on('unknownStratumMethod', function (fullMessage) { + emitLog('Unknown stratum method from ' + client.getLabel() + ': ' + fullMessage.method); + + }).on('socketFlooded', function () { + emitWarningLog('Detected socket flooding from ' + client.getLabel()); + + }).on('tcpProxyError', function (data) { + emitErrorLog('Client IP detection failed, tcpProxyProtocol is enabled yet did not receive proxy protocol message, instead got data: ' + data); + + }).on('bootedBannedWorker', function () { + emitWarningLog('Booted worker ' + client.getLabel() + ' who was connected from an IP address that was just banned'); + + }).on('triggerBan', function (reason) { + emitWarningLog('Banned triggered for ' + client.getLabel() + ': ' + reason); + _this.emit('banIP', client.remoteAddress, client.workerName); + }); + }); + } + + + function SetupBlockPolling() { + if (typeof options.blockRefreshInterval !== "number" || options.blockRefreshInterval <= 0) { + emitLog('Block template polling has been disabled'); + return; + } + + var pollingInterval = options.blockRefreshInterval; + + blockPollingIntervalId = setInterval(function () { + GetBlockTemplate(function (error, result, foundNewBlock) { + if (foundNewBlock) + emitLog('Block notification via RPC polling'); + }); + }, pollingInterval); + } + + + function GetBlockTemplate(callback) { + _this.daemon.cmd('getblocktemplate', + [{"capabilities": ["coinbasetxn", "workid", "coinbase/append"], "rules": ["segwit"]}], + function (result) { + if (result.error) { + logger.debug("result.error = %s", result); + //todo to string interpolation, i'm tired + logger.error('getblocktemplate call failed for daemon instance ' + + result.instance.index + ' with error ' + JSON.stringify(result.error)); + callback(result.error); + } else { + var processedNewBlock = _this.jobManager.processTemplate(result.response); + callback(null, result.response, processedNewBlock); + callback = function () { + }; + } + }, true + ); + } + + function CheckBlockAccepted(blockHash, callback) { + //setTimeout(function(){ + _this.daemon.cmd('getblock', + [blockHash], + function (results) { + var validResults = results.filter(function (result) { + return result.response && (result.response.hash === blockHash) + }); + // do we have any results? + if (validResults.length >= 1) { + // check for invalid blocks with negative confirmations + if (validResults[0].response.confirmations >= 0) { + // accepted valid block! + callback(true, validResults[0].response.tx[0]); + } else { + // reject invalid block, due to confirmations + callback(false, {"confirmations": validResults[0].response.confirmations}); + } + return; + } + // invalid block, rejected + callback(false, {"unknown": "check coin daemon logs"}); + } + ); + } + + + /** + * This method is being called from the blockNotify so that when a new block is discovered by the daemon + * We can inform our miners about the newly found block + **/ + this.processBlockNotify = function(blockHash, sourceTrigger) { + emitLog('Block notification via ' + sourceTrigger); + if (typeof(_this.jobManager) !== 'undefined'){ + if (typeof(_this.jobManager.currentJob) !== 'undefined' && blockHash !== _this.jobManager.currentJob.rpcData.previousblockhash){ + GetBlockTemplate(function(error, result){ + if (error) + emitErrorLog('Block notify error getting block template for ' + options.coin.name); + }); + } + } + }; + + this.relinquishMiners = function (filterFn, resultCback) { + var origStratumClients = this.stratumServer.getStratumClients(); + + var stratumClients = []; + Object.keys(origStratumClients).forEach(function (subId) { + stratumClients.push({subId: subId, client: origStratumClients[subId]}); + }); + async.filter( + stratumClients, + filterFn, + function (clientsToRelinquish) { + clientsToRelinquish.forEach(function (cObj) { + cObj.client.removeAllListeners(); + _this.stratumServer.removeStratumClientBySubId(cObj.subId); + }); + + process.nextTick(function () { + resultCback( + clientsToRelinquish.map( + function (item) { + return item.client; + } + ) + ); + }); + } + ) + }; + + + this.attachMiners = function (miners) { + miners.forEach(function (clientObj) { + _this.stratumServer.manuallyAddStratumClient(clientObj); + }); + _this.stratumServer.broadcastMiningJobs(_this.jobManager.currentJob.getJobParams()); + + }; + + + this.getStratumServer = function () { + return _this.stratumServer; + }; + + + this.setVarDiff = function (port, varDiffConfig) { + if (typeof(_this.varDiff[port]) !== 'undefined') { + _this.varDiff[port].removeAllListeners(); + } + _this.varDiff[port] = new varDiff(port, varDiffConfig); + _this.varDiff[port].on('newDifficulty', function (client, newDiff) { + + /* We request to set the newDiff @ the next difficulty retarget + (which should happen when a new job comes in - AKA BLOCK) */ + client.enqueueNextDifficulty(newDiff); + + /*if (options.varDiff.mode === 'fast'){ + //Send new difficulty, then force miner to use new diff by resending the + //current job parameters but with the "clean jobs" flag set to false + //so the miner doesn't restart work and submit duplicate shares + client.sendDifficulty(newDiff); + var job = _this.jobManager.currentJob.getJobParams(); + job[8] = false; + client.sendMiningJob(job); + }*/ + + }); + }; + +}; +pool.prototype.__proto__ = events.EventEmitter.prototype; diff --git a/lib/stratum.js b/lib/stratum.js new file mode 100644 index 0000000..e922b7b --- /dev/null +++ b/lib/stratum.js @@ -0,0 +1,518 @@ +var BigNum = require('bignum'); +var net = require('net'); +var events = require('events'); +var tls = require('tls'); +var fs = require('fs'); + +var util = require('./util.js'); + +var TLSoptions; + +var SubscriptionCounter = function(){ + var count = 0; + var padding = 'deadbeefcafebabe'; + return { + next: function(){ + count++; + if (Number.MAX_VALUE === count) count = 0; + return padding + util.packInt64LE(count).toString('hex'); + } + }; +}; + + +/** + * Defining each client that connects to the stratum server. + * Emits: + * - subscription(obj, cback(error, extraNonce1, extraNonce2Size)) + * - submit(data(name, jobID, extraNonce2, ntime, nonce)) +**/ +var StratumClient = function(options){ + var pendingDifficulty = null; + //private members + this.socket = options.socket; + this.remoteAddress = options.socket.remoteAddress; + var banning = options.banning; + var _this = this; + this.lastActivity = Date.now(); + this.shares = {valid: 0, invalid: 0}; + + var considerBan = (!banning || !banning.enabled) ? function(){ return false } : function(shareValid){ + if (shareValid === true) _this.shares.valid++; + else _this.shares.invalid++; + var totalShares = _this.shares.valid + _this.shares.invalid; + if (totalShares >= banning.checkThreshold){ + var percentBad = (_this.shares.invalid / totalShares) * 100; + if (percentBad < banning.invalidPercent) //reset shares + this.shares = {valid: 0, invalid: 0}; + else { + _this.emit('triggerBan', _this.shares.invalid + ' out of the last ' + totalShares + ' shares were invalid'); + _this.socket.destroy(); + return true; + } + } + return false; + }; + + this.init = function init(){ + setupSocket(); + }; + + function handleMessage(message){ + // console.log("Received message: "+message); + switch(message.method){ + case 'mining.subscribe': + handleSubscribe(message); + break; + case 'mining.authorize': + handleAuthorize(message); + break; + case 'mining.submit': + _this.lastActivity = Date.now(); + handleSubmit(message); + break; + case 'mining.get_transactions': + sendJson({ + id : null, + result : [], + error : true + }); + break; + case 'mining.extranonce.subscribe': + sendJson({ + id: message.id, + result: false, + error: [20, "Not supported.", null] + }); + break; + default: + _this.emit('unknownStratumMethod', message); + break; + } + } + + function handleSubscribe(message){ + if (!_this.authorized) { + _this.requestedSubscriptionBeforeAuth = true; + } + _this.emit('subscription', + {}, + function(error, extraNonce1, extraNonce1){ + if (error){ + sendJson({ + id: message.id, + result: null, + error: error + }); + return; + } + _this.extraNonce1 = extraNonce1; + + sendJson({ + id: message.id, + result: [ + null, //sessionId + extraNonce1 + ], + error: null + }); + }); + } + + function getSafeString(s) { + return s.toString().replace(/[^a-zA-Z0-9._]+/g, ''); + } + + function getSafeWorkerString(raw) { + var s = getSafeString(raw).split("."); + var addr = s[0]; + var wname = "noname"; + if (s.length > 1) + wname = s[1]; + return addr+"."+wname; + } + + function handleAuthorize(message){ + // console.log("Handing authorize"); + + _this.workerName = getSafeWorkerString(message.params[0]); + _this.workerPass = message.params[1]; + + options.authorizeFn(_this.remoteAddress, options.socket.localPort, _this.workerName, _this.workerPass, _this.extraNonce1, _this.version, function(result) { + _this.authorized = (!result.error && result.authorized); + + sendJson({ + id : message.id, + result : _this.authorized, + error : result.error + }); + + _this.emit('authorization'); + + // If the authorizer wants us to close the socket lets do it. + if (result.disconnect === true) { + options.socket.destroy(); + } + }); + } + + function handleSubmit(message){ + if (_this.authorized === false){ + sendJson({ + id : message.id, + result: null, + error : [24, "unauthorized worker", null] + }); + considerBan(false); + return; + } + if (!_this.extraNonce1){ + sendJson({ + id : message.id, + result: null, + error : [25, "not subscribed", null] + }); + considerBan(false); + return; + } + + _this.emit('submit', + { + name : message.params[0], + jobId : message.params[1], + nonce : message.params[2].substr(2), + header : message.params[3].substr(2), + mixhash : message.params[4].substr(2) + }, + function(error, result){ + // if (!considerBan(result)){ + sendJson({ + id: message.id, + result: result, + error: error + }); + // } + } + ); + + } + + function sendJson(){ + var response = ''; + for (var i = 0; i < arguments.length; i++){ + response += JSON.stringify(arguments[i]) + '\n'; + } + console.log("sendJson (>>>): "+response); + options.socket.write(response); + } + + function setupSocket(){ + // console.log("Setup socket"); + var socket = options.socket; + var dataBuffer = ''; + socket.setEncoding('utf8'); + + if (options.tcpProxyProtocol === true) { + socket.once('data', function (d) { + if (d.indexOf('PROXY') === 0) { + _this.remoteAddress = d.split(' ')[2]; + } + else{ + _this.emit('tcpProxyError', d); + } + _this.emit('checkBan'); + }); + } + else{ + _this.emit('checkBan'); + } + socket.on('data', function(d){ + dataBuffer += d; + if (new Buffer.byteLength(dataBuffer, 'utf8') > 10240){ //10KB + dataBuffer = ''; + _this.emit('socketFlooded'); + socket.destroy(); + return; + } + if (dataBuffer.indexOf('\n') !== -1){ + var messages = dataBuffer.split('\n'); + var incomplete = dataBuffer.slice(-1) === '\n' ? '' : messages.pop(); + messages.forEach(function(message){ + if (message.length < 1) return; + var messageJson; + try { + messageJson = JSON.parse(message); + } catch(e) { + if (options.tcpProxyProtocol !== true || d.indexOf('PROXY') !== 0){ + _this.emit('malformedMessage', message); + socket.destroy(); + } + + return; + } + if (messageJson) { + console.log("handleMessage (<<<): "+JSON.stringify(messageJson)); + handleMessage(messageJson); + } + }); + dataBuffer = incomplete; + } + }); + socket.on('close', function() { + _this.emit('socketDisconnect'); + }); + socket.on('error', function(err){ + if (err.code !== 'ECONNRESET') + _this.emit('socketError', err); + }); + } + + + this.getLabel = function(){ + return (_this.workerName || '(unauthorized)') + ' [' + _this.remoteAddress + ']'; + }; + + this.enqueueNextDifficulty = function(requestedNewDifficulty) { + pendingDifficulty = requestedNewDifficulty; + return true; + }; + + //public members + + /** + * IF the given difficulty is valid and new it'll send it to the client. + * returns boolean + **/ + this.sendDifficulty = function(difficulty){ + if (difficulty === this.difficulty) + return false; + _this.previousDifficulty = _this.difficulty; + _this.difficulty = difficulty; + + //powLimit * difficulty + var powLimit = algos.kawpow.diff; // TODO: Get algos object from argument + var adjPow = powLimit / difficulty; + if ((64 - adjPow.toString(16).length) === 0) { + var zeroPad = ''; + } + else { + var zeroPad = '0'; + zeroPad = zeroPad.repeat((64 - (adjPow.toString(16).length))); + } + var target = (zeroPad + adjPow.toString(16)).substr(0,64); + + + sendJson({ + id : null, + method: "mining.set_target", + params: [target] + }); + + return true; + }; + + this.sendMiningJob = function(jobParams){ + + var lastActivityAgo = Date.now() - _this.lastActivity; + if (lastActivityAgo > options.connectionTimeout * 1000){ + _this.socket.destroy(); + return; + } + + if (pendingDifficulty !== null){ + var result = _this.sendDifficulty(pendingDifficulty); + pendingDifficulty = null; + if (result) { + _this.emit('difficultyChanged', _this.difficulty); + } + } + + //change target to miners' personal varDiff target + var personal_jobParams = jobParams; + var powLimit = algos.kawpow.diff; + var adjPow = powLimit / _this.difficulty; + if ((64 - adjPow.toString(16).length) === 0) { + var zeroPad = ''; + } + else { + var zeroPad = '0'; + zeroPad = zeroPad.repeat((64 - (adjPow.toString(16).length))); + } + personal_jobParams[3] = (zeroPad + adjPow.toString(16)).substr(0,64); + + sendJson({ + id : null, + method: "mining.notify", + params: personal_jobParams + }); + + }; + + this.manuallyAuthClient = function (username, password) { + handleAuthorize({id: 1, params: [username, password]}, false /*do not reply to miner*/); + }; + + this.manuallySetValues = function (otherClient) { + _this.extraNonce1 = otherClient.extraNonce1; + _this.previousDifficulty = otherClient.previousDifficulty; + _this.difficulty = otherClient.difficulty; + }; +}; +StratumClient.prototype.__proto__ = events.EventEmitter.prototype; + + + + +/** + * The actual stratum server. + * It emits the following Events: + * - 'client.connected'(StratumClientInstance) - when a new miner connects + * - 'client.disconnected'(StratumClientInstance) - when a miner disconnects. Be aware that the socket cannot be used anymore. + * - 'started' - when the server is up and running + **/ +var StratumServer = exports.Server = function StratumServer(options, authorizeFn){ + + //private members + + //ports, connectionTimeout, jobRebroadcastTimeout, banning, haproxy, authorizeFn + + var bannedMS = options.banning ? options.banning.time * 1000 : null; + + var _this = this; + var stratumClients = {}; + var subscriptionCounter = SubscriptionCounter(); + var rebroadcastTimeout; + var bannedIPs = {}; + + function checkBan(client){ + if (options.banning && options.banning.enabled && client.remoteAddress in bannedIPs){ + var bannedTime = bannedIPs[client.remoteAddress]; + var bannedTimeAgo = Date.now() - bannedTime; + var timeLeft = bannedMS - bannedTimeAgo; + if (timeLeft > 0){ + client.socket.destroy(); + client.emit('kickedBannedIP', timeLeft / 1000 | 0); + } + else { + delete bannedIPs[client.remoteAddress]; + client.emit('forgaveBannedIP'); + } + } + } + + this.handleNewClient = function (socket){ + + // console.log("Handling new client"); + + socket.setKeepAlive(true); + var subscriptionId = subscriptionCounter.next(); + var client = new StratumClient( + { + coin: options.coin, + subscriptionId: subscriptionId, + authorizeFn: authorizeFn, + socket: socket, + banning: options.banning, + connectionTimeout: options.connectionTimeout, + tcpProxyProtocol: options.tcpProxyProtocol + } + ); + + stratumClients[subscriptionId] = client; + _this.emit('client.connected', client); + client.on('socketDisconnect', function() { + _this.removeStratumClientBySubId(subscriptionId); + _this.emit('client.disconnected', client); + }).on('checkBan', function(){ + checkBan(client); + }).on('triggerBan', function(){ + _this.addBannedIP(client.remoteAddress); + }).init(); + return subscriptionId; + }; + + + this.broadcastMiningJobs = function(jobParams){ + for (var clientId in stratumClients) { + var client = stratumClients[clientId]; + client.sendMiningJob(jobParams); + } + /* Some miners will consider the pool dead if it doesn't receive a job for around a minute. + So every time we broadcast jobs, set a timeout to rebroadcast in X seconds unless cleared. */ + clearTimeout(rebroadcastTimeout); + rebroadcastTimeout = setTimeout(function(){ + _this.emit('broadcastTimeout'); + }, options.jobRebroadcastTimeout * 1000); + }; + + + + (function init(){ + + //Interval to look through bannedIPs for old bans and remove them in order to prevent a memory leak + if (options.banning && options.banning.enabled){ + setInterval(function(){ + for (ip in bannedIPs){ + var banTime = bannedIPs[ip]; + if (Date.now() - banTime > options.banning.time) + delete bannedIPs[ip]; + } + }, 1000 * options.banning.purgeInterval); + } + + var serversStarted = 0; + Object.keys(options.ports).forEach(function(port){ + if (options.ports[port].tls) { + // console.log("TLS port for "+port); + tls.createServer(TLSoptions, function(socket) { + _this.handleNewClient(socket); + }).listen(parseInt(port), function() { + serversStarted++; + if (serversStarted == Object.keys(options.ports).length) + _this.emit('started'); + }); + } else { + // console.log("TCP port for "+port); + net.createServer({allowHalfOpen: false}, function(socket) { + _this.handleNewClient(socket); + }).listen(parseInt(port), function() { + serversStarted++; + if (serversStarted == Object.keys(options.ports).length) + _this.emit('started'); + }); + } + }); + })(); + + + + //public members + + this.addBannedIP = function(ipAddress){ + bannedIPs[ipAddress] = Date.now(); + /*for (var c in stratumClients){ + var client = stratumClients[c]; + if (client.remoteAddress === ipAddress){ + _this.emit('bootedBannedWorker'); + } + }*/ + }; + + this.getStratumClients = function () { + return stratumClients; + }; + + this.removeStratumClientBySubId = function (subscriptionId) { + delete stratumClients[subscriptionId]; + }; + + this.manuallyAddStratumClient = function(clientObj) { + var subId = _this.handleNewClient(clientObj.socket); + if (subId != null) { // not banned! + stratumClients[subId].manuallyAuthClient(clientObj.workerName, clientObj.workerPass); + stratumClients[subId].manuallySetValues(clientObj); + } + }; + +}; +StratumServer.prototype.__proto__ = events.EventEmitter.prototype; diff --git a/lib/transactions.js b/lib/transactions.js new file mode 100644 index 0000000..829bb5c --- /dev/null +++ b/lib/transactions.js @@ -0,0 +1,118 @@ +var bitcoin = require('bitcoinjs-lib'); +var util = require('./util.js'); + +// public members +var txHash; + +exports.txHash = function(){ + return txHash; +}; + +function scriptCompile(addrHash){ + script = bitcoin.script.compile( + [ + bitcoin.opcodes.OP_DUP, + bitcoin.opcodes.OP_HASH160, + addrHash, + bitcoin.opcodes.OP_EQUALVERIFY, + bitcoin.opcodes.OP_CHECKSIG + ]); + return script; +} + +function scriptFoundersCompile(address){ + script = bitcoin.script.compile( + [ + bitcoin.opcodes.OP_HASH160, + address, + bitcoin.opcodes.OP_EQUAL + ]); + return script; +} + + +exports.createGeneration = function(rpcData, blockReward, feeReward, recipients, poolAddress){ + var _this = this; + var blockPollingIntervalId; + + var emitLog = function (text) { + _this.emit('log', 'debug', text); + }; + var emitWarningLog = function (text) { + _this.emit('log', 'warning', text); + }; + var emitErrorLog = function (text) { + _this.emit('log', 'error', text); + }; + var emitSpecialLog = function (text) { + _this.emit('log', 'special', text); + }; + + var poolAddrHash = bitcoin.address.fromBase58Check(poolAddress).hash; + + var tx = new bitcoin.Transaction(); + var blockHeight = rpcData.height; + // input for coinbase tx + if (blockHeight.toString(16).length % 2 === 0) { + var blockHeightSerial = blockHeight.toString(16); + } else { + var blockHeightSerial = '0' + blockHeight.toString(16); + } + var height = Math.ceil((blockHeight << 1).toString(2).length / 8); + var lengthDiff = blockHeightSerial.length/2 - height; + for (var i = 0; i < lengthDiff; i++) { + blockHeightSerial = blockHeightSerial + '00'; + } + length = '0' + height; + var serializedBlockHeight = new Buffer.concat([ + new Buffer(length, 'hex'), + util.reverseBuffer(new Buffer(blockHeightSerial, 'hex')), + new Buffer('00', 'hex') // OP_0 + ]); + + tx.addInput(new Buffer('0000000000000000000000000000000000000000000000000000000000000000', 'hex'), + 0xFFFFFFFF, + 0xFFFFFFFF, + new Buffer.concat([serializedBlockHeight, + Buffer('6d696e65726d6f72652e636f6d', 'hex')]) + ); + + // calculate total fees + var feePercent = 0; + for (var i = 0; i < recipients.length; i++) { + feePercent = feePercent + recipients[i].percent; + } + + tx.addOutput( + scriptCompile(poolAddrHash), + Math.floor(blockReward * (1 - (feePercent / 100))) + ); + + + for (var i = 0; i < recipients.length; i++) { + tx.addOutput( + scriptCompile(bitcoin.address.fromBase58Check(recipients[i].address).hash), + Math.round((blockReward) * (recipients[i].percent / 100)) + ); + } + + + if (rpcData.default_witness_commitment !== undefined) { + tx.addOutput(new Buffer(rpcData.default_witness_commitment, 'hex'), 0); + } + + txHex = tx.toHex(); + + // this txHash is used elsewhere. Don't remove it. + txHash = tx.getHash().toString('hex'); + + return txHex; +}; + +module.exports.getFees = function(feeArray){ + var fee = Number(); + feeArray.forEach(function(value) { + fee = fee + Number(value.fee); + }); + return fee; +}; diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 0000000..f6f0deb --- /dev/null +++ b/lib/util.js @@ -0,0 +1,369 @@ +var crypto = require('crypto'); + +var base58 = require('base58-native'); +var bignum = require('bignum'); + + +exports.addressFromEx = function (exAddress, ripdm160Key) { + try { + var versionByte = exports.getVersionByte(exAddress); + var addrBase = new Buffer.concat([versionByte, new Buffer(ripdm160Key, 'hex')]); + var checksum = exports.sha256d(addrBase).slice(0, 4); + var address = new Buffer.concat([addrBase, checksum]); + return base58.encode(address); + } + catch (e) { + return null; + } +}; + + +exports.getVersionByte = function (addr) { + var versionByte = base58.decode(addr).slice(0, 1); + return versionByte; +}; + +exports.sha256 = function (buffer) { + var hash1 = crypto.createHash('sha256'); + hash1.update(buffer); + return hash1.digest(); +}; + +exports.sha256d = function (buffer) { + return exports.sha256(exports.sha256(buffer)); +}; + +exports.reverseBuffer = function (buff) { + var reversed = new Buffer(buff.length); + for (var i = buff.length - 1; i >= 0; i--) + reversed[buff.length - i - 1] = buff[i]; + return reversed; +}; + +exports.reverseHex = function (hex) { + return exports.reverseBuffer(new Buffer(hex, 'hex')).toString('hex'); +}; + +exports.reverseByteOrder = function (buff) { + for (var i = 0; i < 8; i++) buff.writeUInt32LE(buff.readUInt32BE(i * 4), i * 4); + return exports.reverseBuffer(buff); +}; + +exports.uint256BufferFromHash = function (hex) { + + var fromHex = new Buffer(hex, 'hex'); + + if (fromHex.length != 32) { + var empty = new Buffer(32); + empty.fill(0); + fromHex.copy(empty); + fromHex = empty; + } + + return exports.reverseBuffer(fromHex); +}; + +exports.hexFromReversedBuffer = function (buffer) { + return exports.reverseBuffer(buffer).toString('hex'); +}; + + +/* + Defined in bitcoin protocol here: + https://en.bitcoin.it/wiki/Protocol_specification#Variable_length_integer + */ +exports.varIntBuffer = function (n) { + if (n < 0xfd) + return new Buffer([n]); + else if (n <= 0xffff) { + var buff = new Buffer(3); + buff[0] = 0xfd; + buff.writeUInt16LE(n, 1); + return buff; + } + else if (n <= 0xffffffff) { + var buff = new Buffer(5); + buff[0] = 0xfe; + buff.writeUInt32LE(n, 1); + return buff; + } + else { + var buff = new Buffer(9); + buff[0] = 0xff; + exports.packUInt16LE(n).copy(buff, 1); + return buff; + } +}; + +exports.varStringBuffer = function (string) { + var strBuff = new Buffer(string); + return new Buffer.concat([exports.varIntBuffer(strBuff.length), strBuff]); +}; + +/* + "serialized CScript" formatting as defined here: + https://github.com/bitcoin/bips/blob/master/bip-0034.mediawiki#specification + Used to format height and date when putting into script signature: + https://en.bitcoin.it/wiki/Script + */ +exports.serializeNumber = function (n) { + + /* Old version that is bugged + if (n < 0xfd){ + var buff = new Buffer(2); + buff[0] = 0x1; + buff.writeUInt8(n, 1); + return buff; + } + else if (n <= 0xffff){ + var buff = new Buffer(4); + buff[0] = 0x3; + buff.writeUInt16LE(n, 1); + return buff; + } + else if (n <= 0xffffffff){ + var buff = new Buffer(5); + buff[0] = 0x4; + buff.writeUInt32LE(n, 1); + return buff; + } + else{ + return new Buffer.concat([new Buffer([0x9]), binpack.packUInt64(n, 'little')]); + }*/ + + //New version from TheSeven + if (n >= 1 && n <= 16) return new Buffer([0x50 + n]); + var l = 1; + var buff = new Buffer(9); + while (n > 0x7f) { + buff.writeUInt8(n & 0xff, l++); + n >>= 8; + } + buff.writeUInt8(l, 0); + buff.writeUInt8(n, l++); + return buff.slice(0, l); + +}; + + +/* + Used for serializing strings used in script signature + */ +exports.serializeString = function (s) { + + if (s.length < 253) + return new Buffer.concat([ + new Buffer([s.length]), + new Buffer(s) + ]); + else if (s.length < 0x10000) + return new Buffer.concat([ + new Buffer([253]), + exports.packUInt16LE(s.length), + new Buffer(s) + ]); + else if (s.length < 0x100000000) + return new Buffer.concat([ + new Buffer([254]), + exports.packUInt32LE(s.length), + new Buffer(s) + ]); + else + return new Buffer.concat([ + new Buffer([255]), + exports.packUInt16LE(s.length), + new Buffer(s) + ]); +}; + + +exports.packUInt16LE = function (num) { + var buff = new Buffer(2); + buff.writeUInt16LE(num, 0); + return buff; +}; +exports.packInt32LE = function (num) { + var buff = new Buffer(4); + buff.writeInt32LE(num, 0); + return buff; +}; +exports.packInt32BE = function (num) { + var buff = new Buffer(4); + buff.writeInt32BE(num, 0); + return buff; +}; +exports.packUInt32LE = function (num) { + var buff = new Buffer(4); + buff.writeUInt32LE(num, 0); + return buff; +}; +exports.packUInt32BE = function (num) { + var buff = new Buffer(4); + buff.writeUInt32BE(num, 0); + return buff; +}; +exports.packInt64LE = function (num) { + var buff = new Buffer(8); + buff.writeUInt32LE(num % Math.pow(2, 32), 0); + buff.writeUInt32LE(Math.floor(num / Math.pow(2, 32)), 4); + return buff; +}; + + +/* + An exact copy of python's range feature. Written by Tadeck: + http://stackoverflow.com/a/8273091 + */ +exports.range = function (start, stop, step) { + if (typeof stop === 'undefined') { + stop = start; + start = 0; + } + if (typeof step === 'undefined') { + step = 1; + } + if ((step > 0 && start >= stop) || (step < 0 && start <= stop)) { + return []; + } + var result = []; + for (var i = start; step > 0 ? i < stop : i > stop; i += step) { + result.push(i); + } + return result; +}; + + +/* + For POS coins - used to format wallet address for use in generation transaction's output + */ +exports.pubkeyToScript = function (key) { + if (key.length !== 66) { + console.error('Invalid pubkey: ' + key); + throw new Error(); + } + var pubkey = new Buffer(35); + pubkey[0] = 0x21; + pubkey[34] = 0xac; + new Buffer(key, 'hex').copy(pubkey, 1); + return pubkey; +}; + + +exports.miningKeyToScript = function (key) { + var keyBuffer = new Buffer(key, 'hex'); + return new Buffer.concat([new Buffer([0x76, 0xa9, 0x14]), keyBuffer, new Buffer([0x88, 0xac])]); +}; + +/* + For POW coins - used to format wallet address for use in generation transaction's output + */ +exports.addressToScript = function (addr) { + + var decoded = base58.decode(addr); + + if (decoded.length !== 25 && decoded.length !== 26) { + console.error('invalid address length for ' + addr); + throw new Error(); + } + + if (!decoded) { + console.error('base58 decode failed for ' + addr); + throw new Error(); + } + + var pubkey = decoded.slice(1, -4); + + return new Buffer.concat([new Buffer([0x76, 0xa9, 0x14]), pubkey, new Buffer([0x88, 0xac])]); +}; + + +exports.getReadableHashRateString = function (hashrate) { + var i = -1; + var byteUnits = [' KH', ' MH', ' GH', ' TH', ' PH']; + do { + hashrate = hashrate / 1024; + i++; + } while (hashrate > 1024); + return hashrate.toFixed(2) + byteUnits[i]; +}; + + +//Creates a non-truncated max difficulty (diff1) by bitwise right-shifting the max value of a uint256 +exports.shiftMax256Right = function (shiftRight) { + + //Max value uint256 (an array of ones representing 256 enabled bits) + var arr256 = Array.apply(null, new Array(256)).map(Number.prototype.valueOf, 1); + + //An array of zero bits for how far the max uint256 is shifted right + var arrLeft = Array.apply(null, new Array(shiftRight)).map(Number.prototype.valueOf, 0); + + //Add zero bits to uint256 and remove the bits shifted out + arr256 = arrLeft.concat(arr256).slice(0, 256); + + //An array of bytes to convert the bits to, 8 bits in a byte so length will be 32 + var octets = []; + + for (var i = 0; i < 32; i++) { + + octets[i] = 0; + + //The 8 bits for this byte + var bits = arr256.slice(i * 8, i * 8 + 8); + + //Bit math to add the bits into a byte + for (var f = 0; f < bits.length; f++) { + var multiplier = Math.pow(2, f); + octets[i] += bits[f] * multiplier; + } + + } + + return new Buffer(octets); +}; + + +exports.bufferToCompactBits = function (startingBuff) { + var bigNum = bignum.fromBuffer(startingBuff); + var buff = bigNum.toBuffer(); + + buff = buff.readUInt8(0) > 0x7f ? Buffer.concat([new Buffer([0x00]), buff]) : buff; + + buff = new Buffer.concat([new Buffer([buff.length]), buff]); + return compact = buff.slice(0, 4); +}; + +/* + Used to convert getblocktemplate bits field into target if target is not included. + More info: https://en.bitcoin.it/wiki/Target + */ + +exports.bignumFromBitsBuffer = function (bitsBuff) { + var numBytes = bitsBuff.readUInt8(0); + var bigBits = bignum.fromBuffer(bitsBuff.slice(1)); + var target = bigBits.mul( + bignum(2).pow( + bignum(8).mul( + numBytes - 3 + ) + ) + ); + return target; +}; + +exports.bignumFromBitsHex = function (bitsString) { + var bitsBuff = new Buffer(bitsString, 'hex'); + return exports.bignumFromBitsBuffer(bitsBuff); +}; + +exports.convertBitsToBuff = function (bitsBuff) { + var target = exports.bignumFromBitsBuffer(bitsBuff); + var resultBuff = target.toBuffer(); + var buff256 = new Buffer(32); + buff256.fill(0); + resultBuff.copy(buff256, buff256.length - resultBuff.length); + return buff256; +}; + +exports.getTruncatedDiff = function (shift) { + return exports.convertBitsToBuff(exports.bufferToCompactBits(exports.shiftMax256Right(shift))); +}; diff --git a/lib/varDiff.js b/lib/varDiff.js new file mode 100644 index 0000000..d05df13 --- /dev/null +++ b/lib/varDiff.js @@ -0,0 +1,128 @@ +var events = require('events'); + +/* + + Vardiff ported from stratum-mining share-limiter + https://github.com/ahmedbodi/stratum-mining/blob/master/mining/basic_share_limiter.py + + */ + + +function RingBuffer(maxSize) { + var data = []; + var cursor = 0; + var isFull = false; + this.append = function (x) { + if (isFull) { + data[cursor] = x; + cursor = (cursor + 1) % maxSize; + } + else { + data.push(x); + cursor++; + if (data.length === maxSize) { + cursor = 0; + isFull = true; + } + } + }; + this.avg = function () { + var sum = data.reduce(function (a, b) { + return a + b + }); + return sum / (isFull ? maxSize : cursor); + }; + this.size = function () { + return isFull ? maxSize : cursor; + }; + this.clear = function () { + data = []; + cursor = 0; + isFull = false; + }; +} + +// Truncate a number to a fixed amount of decimal places +function toFixed(num, len) { + return parseFloat(num.toFixed(len)); +} + +var varDiff = module.exports = function varDiff(port, varDiffOptions) { + var _this = this; + + var bufferSize, tMin, tMax; + + //if (!varDiffOptions) return; + + var variance = varDiffOptions.targetTime * (varDiffOptions.variancePercent / 100); + + + bufferSize = varDiffOptions.retargetTime / varDiffOptions.targetTime * 4; + tMin = varDiffOptions.targetTime - variance; + tMax = varDiffOptions.targetTime + variance; + + + this.manageClient = function (client) { + + var stratumPort = client.socket.localPort; + + if (stratumPort != port) { + console.error("Handling a client which is not of this vardiff?"); + } + var options = varDiffOptions; + + var lastTs; + var lastRtc; + var timeBuffer; + + client.on('submit', function () { + + var ts = (Date.now() / 1000) | 0; + + if (!lastRtc) { + lastRtc = ts - options.retargetTime / 2; + lastTs = ts; + timeBuffer = new RingBuffer(bufferSize); + return; + } + + var sinceLast = ts - lastTs; + + timeBuffer.append(sinceLast); + lastTs = ts; + + if ((ts - lastRtc) < options.retargetTime && timeBuffer.size() > 0) + return; + + lastRtc = ts; + var avg = timeBuffer.avg(); + var ddiff = options.targetTime / avg; + + if (avg > tMax && client.difficulty > options.minDiff) { + if (options.x2mode) { + ddiff = 0.5; + } + if (ddiff * client.difficulty < options.minDiff) { + ddiff = options.minDiff / client.difficulty; + } + } else if (avg < tMin) { + if (options.x2mode) { + ddiff = 2; + } + var diffMax = options.maxDiff; + if (ddiff * client.difficulty > diffMax) { + ddiff = diffMax / client.difficulty; + } + } + else { + return; + } + + var newDiff = toFixed(client.difficulty * ddiff, 8); + timeBuffer.clear(); + console.log("Emitting new difficulty to "+newDiff); + _this.emit('newDifficulty', client, newDiff); + }); + }; +}; +varDiff.prototype.__proto__ = events.EventEmitter.prototype; diff --git a/package.json b/package.json new file mode 100644 index 0000000..df99ce1 --- /dev/null +++ b/package.json @@ -0,0 +1,84 @@ +{ + "_from": "git+https://github.com/RavenCommunity/kawpow-stratum-pool.git", + "_id": "stratum-pool@0.1.6", + "_inBundle": false, + "_location": "/stratum-pool", + "_phantomChildren": {}, + "_requested": { + "type": "git", + "raw": "stratum-pool@git+https://github.com/RavenCommunity/kawpow-stratum-pool.git", + "name": "stratum-pool", + "escapedName": "stratum-pool", + "rawSpec": "git+https://github.com/RavenCommunity/kawpow-stratum-pool.git", + "saveSpec": "git+https://github.com/RavenCommunity/kawpow-stratum-pool.git", + "fetchSpec": "https://github.com/RavenCommunity/kawpow-stratum-pool.git", + "gitCommittish": null + }, + "_requiredBy": [ + "/" + ], + "_resolved": "git+https://github.com/RavenCommunity/kawpow-stratum-pool.git#c66ba109e7de88edc46dd70f36aa9c7cea7d7add", + "_spec": "stratum-pool@git+https://github.com/RavenCommunity/kawpow-stratum-pool.git", + "_where": "", + "author": { + "name": "Traysi" + }, + "bugs": { + "url": "https://github.com/RavenCommunity/kawpow-stratum-pool/issues" + }, + "bundleDependencies": false, + "contributors": [ + { + "name": "Jeremy Anderson" + }, + { + "name": "Matthew Little" + }, + { + "name": "vekexasia" + }, + { + "name": "TheSeven" + }, + { + "name": "fellu" + }, + { + "name": "anduck" + } + ], + "dependencies": { + "async": "*", + "base58-native": "*", + "big-integer": "^1.6.48", + "bignum": "^0.13.1", + "bitcoinjs-lib": "git+https://github.com/bitcoinjs/bitcoinjs-lib.git", + "merkle-bitcoin": "git+https://github.com/joshuayabut/merkle-bitcoin.git", + "promise": "*", + "sha3": "git+https://github.com/phusion/node-sha3.git" + }, + "deprecated": false, + "description": "High performance Stratum poolserver in Node.js", + "engines": { + "node": ">=7" + }, + "homepage": "https://github.com/RavenCommunity/kawpow-stratum-pool", + "keywords": [ + "stratum", + "mining", + "pool", + "server", + "poolserver", + "bitcoin", + "Ravencoin", + "kawpow" + ], + "license": "GPL-2.0", + "main": "lib/index.js", + "name": "stratum-pool", + "repository": { + "type": "git", + "url": "git+https://github.com/RavenCommunity/kawpow-stratum-pool.git" + }, + "version": "0.1.6" +}