UNPKG

node-merged-pooler

Version:

High performance Stratum poolserver in Node.js for merged mining

324 lines (253 loc) 10.7 kB
var events = require('events'); var crypto = require('crypto'); var bignum = require('bignum'); var util = require('./util.js'); var blockTemplate = require('./blockTemplate.js'); var merkleTree = require('./merkleTree'); //Unique extranonce per subscriber var ExtraNonceCounter = function(configInstanceId){ var instanceId = configInstanceId || crypto.randomBytes(4).readUInt32LE(0); var counter = instanceId << 27; this.next = function(){ var extraNonce = util.packUInt32BE(Math.abs(counter++)); return extraNonce.toString('hex'); }; this.size = 4; //bytes }; //Unique job per new block template var JobCounter = function(){ var counter = 0; this.next = function(){ counter++; if (counter % 0xffff === 0) counter = 1; return this.cur(); }; this.cur = function () { return counter.toString(16); }; }; /** * 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){ //private members var _this = this; var jobCounter = new JobCounter(); var shareMultiplier = algos[options.coin.algorithm].multiplier; //public members this.extraNonceCounter = new ExtraNonceCounter(options.instanceId); this.extraNoncePlaceholder = new Buffer('f000000ff111111f', 'hex'); this.extraNonce2Size = this.extraNoncePlaceholder.length - this.extraNonceCounter.size; this.currentJob; this.validJobs = {}; var hashDigest = algos[options.coin.algorithm].hash(options.coin); var coinbaseHasher = (function(){ switch(options.coin.algorithm){ case 'keccak': case 'blake': case 'fugue': case 'groestl': if (options.coin.normalHashing === true) return util.sha256d; else return util.sha256; default: return util.sha256d; } })(); var blockHasher = (function () { switch (options.coin.algorithm) { case 'scrypt': if (options.coin.reward === 'POS') { return function (d) { return util.reverseBuffer(hashDigest.apply(this, arguments)); }; } case 'scrypt-jane': if (options.coin.reward === 'POS') { return function (d) { return util.reverseBuffer(hashDigest.apply(this, arguments)); }; } case 'scrypt-n': case 'sha1': return function (d) { return util.reverseBuffer(util.sha256d(d)); }; default: return function () { return util.reverseBuffer(hashDigest.apply(this, arguments)); }; } })(); function buildMerkleTree(auxData) { // Determine which slots the merkle hashes will go into in the merkle tree // Strategy derived from p2pool var size = 1; for(;size < Math.pow(2, 32);size *= 2) { if(size < auxData.length) continue; var res = new Array(size); for(var i = 0;i < size;i++) res[i] = new Buffer(32); var c = []; for(var i = 0;i < auxData.length;i++) { var pos = util.getAuxMerklePosition(auxData[i].chainid, size); if(c.indexOf(pos) != -1) break; c.push(pos); var d = util.uint256BufferFromHash(auxData[i].hash); d.copy(res[pos]); } if(c.length == auxData.length) { // all coins added successfully to the tree, return a generated merkle tree var auxMerkleTree = new merkleTree(res); return auxMerkleTree; } } } // rpcData - is a object with parent and aux chain RPC data this.updateCurrentJob = function(rpcData){ var auxMerkleTree = buildMerkleTree(rpcData.auxes); // MERGED MINING - include merged mining data here var tmpBlockTemplate = new blockTemplate( jobCounter.next(), rpcData, options.poolAddressScript, _this.extraNoncePlaceholder, options.coin.reward, options.coin.txMessages, options.recipients, auxMerkleTree ); _this.currentJob = tmpBlockTemplate; _this.emit('updatedBlock', tmpBlockTemplate, true); _this.validJobs[tmpBlockTemplate.jobId] = tmpBlockTemplate; _this.auxMerkleTree = auxMerkleTree; }; this.isNewWork = 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; } return isNewBlock; } //returns true if processed a new block this.processTemplate = function(rpcData){ // Since an aux block can make a block new, this is disabled. //if (!isNewBlock) return false; var auxMerkleTree = buildMerkleTree(rpcData.auxes); // MERGED MINING - include merged mining data here var tmpBlockTemplate = new blockTemplate( jobCounter.next(), rpcData, options.poolAddressScript, _this.extraNoncePlaceholder, options.coin.reward, options.coin.txMessages, options.recipients, auxMerkleTree ); this.currentJob = tmpBlockTemplate; this.validJobs = {}; _this.emit('newBlock', tmpBlockTemplate); this.validJobs[tmpBlockTemplate.jobId] = tmpBlockTemplate; this.auxMerkleTree = auxMerkleTree; return true; }; this.processShare = function(jobId, previousDifficulty, difficulty, extraNonce1, extraNonce2, nTime, nonce, ipAddress, port, workerName){ var shareError = function(error){ _this.emit('share', { job: jobId, ip: ipAddress, worker: workerName, difficulty: difficulty, error: error[1] }); return {error: error, result: null}; }; var submitTime = Date.now() / 1000 | 0; if (extraNonce2.length / 2 !== _this.extraNonce2Size) return shareError([20, 'incorrect size of extranonce2']); var job = this.validJobs[jobId]; if (typeof job === 'undefined' || job.jobId != jobId ) { return shareError([21, 'job not found']); } if (nTime.length !== 8) { return shareError([20, 'incorrect size of ntime']); } var nTimeInt = parseInt(nTime, 16); if (nTimeInt < job.rpcData.curtime || nTimeInt > submitTime + 7200) { return shareError([20, 'ntime out of range']); } if (nonce.length !== 8) { return shareError([20, 'incorrect size of nonce']); } if (!job.registerSubmit(extraNonce1, extraNonce2, nTime, nonce)) { return shareError([22, 'duplicate share']); } var extraNonce1Buffer = new Buffer(extraNonce1, 'hex'); var extraNonce2Buffer = new Buffer(extraNonce2, 'hex'); var coinbaseBuffer = job.serializeCoinbase(extraNonce1Buffer, extraNonce2Buffer); var coinbaseHash = coinbaseHasher(coinbaseBuffer); var merkleRoot = util.reverseBuffer(job.merkleTree.withFirst(coinbaseHash)).toString('hex'); var headerBuffer = job.serializeHeader(merkleRoot, nTime, nonce); var headerHash = hashDigest(headerBuffer, nTimeInt); var headerBigNum = bignum.fromBuffer(headerHash, {endian: 'little', size: 32}); var blockHashInvalid; var blockHexInvalid; var blockHash; var blockHex; var shareDiff = diff1 / headerBigNum.toNumber() * shareMultiplier; var blockDiffAdjusted = job.difficulty * shareMultiplier; blockHexInvalid = job.serializeBlock(headerBuffer, coinbaseBuffer).toString('hex'); blockHashInvalid = blockHasher(headerBuffer, nTime).toString('hex'); //Check if share is a block candidate (matched network difficulty) if (job.target.ge(headerBigNum)){ blockHex = job.serializeBlock(headerBuffer, coinbaseBuffer).toString('hex'); blockHash = blockHasher(headerBuffer, nTime).toString('hex'); //blockHash = util.reverseBuffer(util.sha256d(headerBuffer, nTime)).toString('hex'); } else { //Check if share didn't reached the miner's difficulty) if (shareDiff / difficulty < 0.99){ //Check if share matched a previous difficulty from before a vardiff retarget if (previousDifficulty && shareDiff >= previousDifficulty){ difficulty = previousDifficulty; } else{ return shareError([23, 'low difficulty share of ' + shareDiff]); } } } _this.emit('share', { job: jobId, ip: ipAddress, port: port, worker: workerName, height: job.rpcData.height, blockReward: job.rpcData.coinbasevalue, difficulty: difficulty, shareDiff: shareDiff.toFixed(8), blockDiff : blockDiffAdjusted, blockDiffActual: job.difficulty, coinbaseBuffer: coinbaseBuffer, txHash: coinbaseHash.toString('hex'), headerHash: headerHash, blockBigNum: headerBigNum, blockHash: blockHash, blockHashInvalid: blockHashInvalid, time:submitTime // hashgoal addition for getblocksstats }, blockHexInvalid, blockHex); return {result: true, error: null, blockHash: blockHash}; }; }; JobManager.prototype.__proto__ = events.EventEmitter.prototype;