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"
+}