332 lines
11 KiB
JavaScript
332 lines
11 KiB
JavaScript
var events = require('events');
|
|
var crypto = require('crypto');
|
|
|
|
var bignum = require('bignum');
|
|
|
|
var util = require('./util.js');
|
|
var blockTemplate = require('./blockTemplate.js');
|
|
|
|
const EH_PARAMS_MAP = {
|
|
"144_5": {
|
|
SOLUTION_LENGTH: 202,
|
|
SOLUTION_SLICE: 2,
|
|
},
|
|
"192_7": {
|
|
SOLUTION_LENGTH: 806,
|
|
// Unknown at this time.
|
|
// SOLUTION_SLICE: 2,
|
|
},
|
|
"200_9": {
|
|
SOLUTION_LENGTH: 2694,
|
|
SOLUTION_SLICE: 6,
|
|
}
|
|
}
|
|
|
|
//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 = 0x0000cccc;
|
|
|
|
this.next = function () {
|
|
counter++;
|
|
if (counter % 0xffffffffff === 0)
|
|
counter = 1;
|
|
return this.cur();
|
|
};
|
|
|
|
this.cur = function () {
|
|
return counter.toString(16);
|
|
};
|
|
};
|
|
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) {
|
|
|
|
|
|
//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.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,
|
|
_this.extraNoncePlaceholder,
|
|
options.coin.reward,
|
|
options.recipients,
|
|
options.address,
|
|
options.coin.payFoundersReward,
|
|
options.coin.percentFoundersReward,
|
|
options.coin.maxFoundersRewardBlockHeight,
|
|
options.coin.foundersRewardAddressChangeInterval,
|
|
options.coin.vFoundersRewardAddress,
|
|
options.coin.percentTreasuryReward,
|
|
options.coin.treasuryRewardStartBlockHeight,
|
|
options.coin.treasuryRewardAddressChangeInterval,
|
|
options.coin.vTreasuryRewardAddress
|
|
);
|
|
|
|
_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,
|
|
_this.extraNoncePlaceholder,
|
|
options.coin.reward,
|
|
options.recipients,
|
|
options.address,
|
|
options.coin.payFoundersReward,
|
|
options.coin.percentFoundersReward,
|
|
options.coin.maxFoundersRewardBlockHeight,
|
|
options.coin.foundersRewardAddressChangeInterval,
|
|
options.coin.vFoundersRewardAddress,
|
|
options.coin.percentTreasuryReward,
|
|
options.coin.treasuryRewardStartBlockHeight,
|
|
options.coin.treasuryRewardAddressChangeInterval,
|
|
options.coin.vTreasuryRewardAddress
|
|
);
|
|
|
|
this.currentJob = tmpBlockTemplate;
|
|
|
|
this.validJobs = {};
|
|
_this.emit('newBlock', tmpBlockTemplate);
|
|
|
|
this.validJobs[tmpBlockTemplate.jobId] = tmpBlockTemplate;
|
|
|
|
return true;
|
|
|
|
};
|
|
|
|
this.processShare = function (jobId, previousDifficulty, difficulty, extraNonce1, extraNonce2, nTime, nonce, ipAddress, port, workerName, soln) {
|
|
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;
|
|
|
|
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(util.reverseBuffer(new Buffer(nTime, 'hex')), 16);
|
|
if (nTimeInt < job.rpcData.curtime || nTimeInt > submitTime + 7200) {
|
|
return shareError([20, 'ntime out of range']);
|
|
}
|
|
|
|
if (nonce.length !== 64) {
|
|
return shareError([20, 'incorrect size of nonce']);
|
|
}
|
|
|
|
/**
|
|
* TODO: This is currently accounting only for equihash. make it smarter.
|
|
*/
|
|
let parameters = options.coin.parameters
|
|
if (!parameters) {
|
|
parameters = {
|
|
N: 200,
|
|
K: 9,
|
|
personalization: 'ZcashPoW'
|
|
}
|
|
}
|
|
|
|
let N = parameters.N || 200
|
|
let K = parameters.K || 9
|
|
let expectedLength = EH_PARAMS_MAP[`${N}_${K}`].SOLUTION_LENGTH || -1
|
|
let solutionSlice = EH_PARAMS_MAP[`${N}_${K}`].SOLUTION_SLICE || 0
|
|
|
|
if (soln.length !== expectedLength) {
|
|
return shareError([20, 'Error: Incorrect size of solution (' + soln.length + '), expected ' + expectedLength]);
|
|
}
|
|
|
|
if (!isHexString(extraNonce2)) {
|
|
return shareError([20, 'invalid hex in extraNonce2']);
|
|
}
|
|
|
|
if (!job.registerSubmit(extraNonce1.toLowerCase(), extraNonce2.toLowerCase(), nTime, nonce)) {
|
|
return shareError([22, 'duplicate share']);
|
|
}
|
|
|
|
var extraNonce1Buffer = new Buffer(extraNonce1, 'hex');
|
|
var extraNonce2Buffer = new Buffer(extraNonce2, 'hex');
|
|
|
|
var headerBuffer = job.serializeHeader(nTime, nonce); // 144 bytes (doesn't contain soln)
|
|
var headerSolnBuffer = new Buffer.concat([headerBuffer, new Buffer(soln, 'hex')]);
|
|
var headerHash = util.sha256d(headerSolnBuffer);
|
|
var headerBigNum = bignum.fromBuffer(headerHash, {endian: 'little', size: 32});
|
|
|
|
var blockHashInvalid;
|
|
var blockHash;
|
|
var blockHex;
|
|
|
|
var shareDiff = diff1 / headerBigNum.toNumber() * shareMultiplier;
|
|
var blockDiffAdjusted = job.difficulty * shareMultiplier;
|
|
|
|
// check if valid solution
|
|
if (hashDigest(headerBuffer, new Buffer(soln.slice(solutionSlice), 'hex')) !== true) {
|
|
return shareError([20, 'invalid solution']);
|
|
}
|
|
|
|
//check if block candidate
|
|
if (headerBigNum.le(job.target)) {
|
|
blockHex = job.serializeBlock(headerBuffer, new Buffer(soln, 'hex')).toString('hex');
|
|
blockHash = util.reverseBuffer(headerHash).toString('hex');
|
|
} else {
|
|
if (options.emitInvalidBlockHashes)
|
|
blockHashInvalid = util.reverseBuffer(util.sha256d(headerSolnBuffer)).toString('hex');
|
|
|
|
//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]);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
/*
|
|
console.log('validSoln: ' + hashDigest(headerBuffer, new Buffer(soln.slice(6), 'hex')));
|
|
console.log('job: ' + jobId);
|
|
console.log('ip: ' + ipAddress);
|
|
console.log('port: ' + port);
|
|
console.log('worker: ' + workerName);
|
|
console.log('height: ' + job.rpcData.height);
|
|
console.log('blockReward: ' + job.rpcData.reward);
|
|
console.log('difficulty: ' + difficulty);
|
|
console.log('shareDiff: ' + shareDiff.toFixed(8));
|
|
console.log('blockDiff: ' + blockDiffAdjusted);
|
|
console.log('blockDiffActual: ' + job.difficulty);
|
|
console.log('blockHash: ' + blockHash);
|
|
console.log('blockHashInvalid: ' + blockHashInvalid);
|
|
*/
|
|
|
|
_this.emit('share', {
|
|
job: jobId,
|
|
ip: ipAddress,
|
|
port: port,
|
|
worker: workerName,
|
|
height: job.rpcData.height,
|
|
blockReward: job.rpcData.reward,
|
|
difficulty: difficulty,
|
|
shareDiff: shareDiff.toFixed(8),
|
|
blockDiff: blockDiffAdjusted,
|
|
blockDiffActual: job.difficulty,
|
|
blockHash: blockHash,
|
|
blockHashInvalid: blockHashInvalid
|
|
}, blockHex);
|
|
|
|
return {result: true, error: null, blockHash: blockHash};
|
|
};
|
|
};
|
|
JobManager.prototype.__proto__ = events.EventEmitter.prototype;
|