UNPKG

bbqpool-stratum

Version:

High performance Stratum poolserver in Node.js. Optimized to build with GCC 10 and O3 / bugfixes

396 lines (351 loc) 14.2 kB
/* * * Manager (Updated) * */ const events = require('events'); const bignum = require('bignum'); const utils = require('./utils'); const Algorithms = require('./algorithms'); const Merkle = require('./merkle'); const Template = require('./template'); //////////////////////////////////////////////////////////////////////////////// // Main Manager Function const Manager = function(poolConfig, portalConfig) { const _this = this; this.poolConfig = poolConfig; this.portalConfig = portalConfig; const algorithm = _this.poolConfig.primary.coin.algorithms.mining; const shareMultiplier = Algorithms[algorithm].multiplier; const extraNonceSize = ['kawpow', 'firopow'].includes(algorithm) ? 2 : 4; this.currentJob; this.validJobs = {}; this.jobCounter = utils.jobCounter(); this.extraNoncePlaceholder = ['kawpow', 'firopow'].includes(algorithm) ? Buffer.from('f000', 'hex') : Buffer.from('f000000ff111111f', 'hex'); this.extraNonceCounter = utils.extraNonceCounter(extraNonceSize); this.extraNonce2Size = _this.extraNoncePlaceholder.length - _this.extraNonceCounter.size; // Build Merkle Tree from Auxiliary Chain this.buildMerkleTree = function(auxData) { if (auxData) { const merkleData = [Buffer.alloc(32)]; const position = utils.getAuxMerklePosition(auxData.chainid, 1); const hash = utils.uint256BufferFromHash(auxData.hash); hash.copy(merkleData[position]); return new Merkle(merkleData); } return null; }; // Update Current Managed Job this.updateCurrentJob = function(rpcData) { const auxMerkle = _this.buildMerkleTree(rpcData.auxData); const tmpTemplate = new Template( _this.poolConfig, Object.assign({}, rpcData), _this.jobCounter.next(), _this.extraNoncePlaceholder, auxMerkle, ); _this.currentJob = tmpTemplate; _this.emit('updatedBlock', tmpTemplate); _this.validJobs[tmpTemplate.jobId] = tmpTemplate; _this.auxMerkle = auxMerkle; }; // Check if New Block is Processed this.processTemplate = function(rpcData, processNew) { // If Current Job !== Previous Job let isNewBlock = typeof(_this.currentJob) === 'undefined'; if ((!isNewBlock && _this.currentJob.rpcData.previousblockhash !== rpcData.previousblockhash) || (!isNewBlock && _this.currentJob.rpcData.bits !== rpcData.bits)) { isNewBlock = true; if (rpcData.height < _this.currentJob.rpcData.height) isNewBlock = false; } // Check for New Block if (!isNewBlock && !processNew) { return false; } // Build New Block Template const auxMerkle = _this.buildMerkleTree(rpcData.auxData); const tmpTemplate = new Template( _this.poolConfig, Object.assign({}, rpcData), _this.jobCounter.next(), _this.extraNoncePlaceholder, auxMerkle, ); // Update Current Template _this.validJobs = {}; _this.currentJob = tmpTemplate; _this.emit('newBlock', tmpTemplate); _this.validJobs[tmpTemplate.jobId] = tmpTemplate; _this.auxMerkle = auxMerkle; return true; }; // Process Submitted Share this.processShare = function( jobId, previousDifficulty, difficulty, ipAddress, port, addrPrimary, addrAuxiliary, submission) { // Main Pool Identifier const identifier = this.portalConfig.identifier || ''; // Share is Invalid const shareError = function(error) { _this.emit('share', { job: jobId, ip: ipAddress, port: port, addrPrimary: addrPrimary, addrAuxiliary: addrAuxiliary, difficulty: difficulty, identifier: identifier, error: error[1], }, null, false); return { error: error, result: null }; }; // Establish Share Variables let submitTime, job, nTimeInt, blockValid, nTime, version; let extraNonce1Buffer, extraNonce2Buffer, nonceBuffer, mixHashBuffer; let coinbaseBuffer, coinbaseHash, merkleRoot; let headerDigest, headerBuffer, headerHash, headerBigNum; let headerHashBuffer, hashOutputBuffer, combinedBuffer, isValid; let shareDiff, blockDiffAdjusted, blockHex, blockHash; let shareData, auxShareData; // Process Submitted Share /* istanbul ignore next */ switch (algorithm) { // Kawpow/Firopow Share Submission case 'kawpow': case 'firopow': // Edge Cases to Check if Share is Invalid submitTime = Date.now() / 1000 | 0; job = _this.validJobs[jobId]; if (typeof job === 'undefined' || job.jobId != jobId) { return shareError([21, 'job not found']); } if (!utils.isHexString(submission.headerHash)) { return shareError([20, 'invalid header submission [1]']); } if (!utils.isHexString(submission.mixHash)) { return shareError([20, 'invalid mixHash submission']); } if (!utils.isHexString(submission.nonce)) { return shareError([20, 'invalid nonce submission']); } if (submission.mixHash.length !== 64) { return shareError([20, 'incorrect size of mixHash']); } if (submission.nonce.length !== 16) { return shareError([20, 'incorrect size of nonce']); } if (submission.nonce.indexOf(submission.extraNonce1.substring(0, 4)) !== 0) { return shareError([24, 'nonce out of worker range']); } if (!addrPrimary && !addrAuxiliary) { return shareError([20, 'worker address isn\'t set properly']); } if (!job.registerSubmit([submission.extraNonce1, submission.nonce, submission.headerHash, submission.mixHash])) { return shareError([22, 'duplicate share']); } // Establish Share Information blockValid = false; extraNonce1Buffer = Buffer.from(submission.extraNonce1, 'hex'); nonceBuffer = utils.reverseBuffer(Buffer.from(submission.nonce, 'hex')); mixHashBuffer = Buffer.from(submission.mixHash, 'hex'); // Generate Coinbase Buffer coinbaseBuffer = job.serializeCoinbase(extraNonce1Buffer); coinbaseHash = job.coinbaseHasher(coinbaseBuffer); merkleRoot = job.merkle.withFirst(coinbaseHash); // Start Generating Block Hash version = job.rpcData.version; nTime = utils.packUInt32BE(job.rpcData.curtime).toString('hex'); headerDigest = Algorithms[algorithm].hash(_this.poolConfig.primary.coin); headerBuffer = job.serializeHeader(merkleRoot, nTime, submission.nonce, version); headerHashBuffer = utils.reverseBuffer(utils.sha256d(headerBuffer)); headerHash = headerHashBuffer.toString('hex'); // Check if Generated Header Matches if (submission.headerHash !== headerHash) { return shareError([20, 'invalid header submission [2]']); } // Check Validity of Solution hashOutputBuffer = Buffer.alloc(32); isValid = headerDigest(headerHashBuffer, nonceBuffer, job.rpcData.height, mixHashBuffer, hashOutputBuffer); headerBigNum = bignum.fromBuffer(hashOutputBuffer, {endian: 'big', size: 32}); // Check if Submission is Valid Solution if (!isValid) { return shareError([20, 'submission is not valid']); } // Calculate Share Difficulty shareDiff = Algorithms[algorithm].diff / headerBigNum.toNumber() * shareMultiplier; blockDiffAdjusted = job.difficulty * shareMultiplier; blockHex = job.serializeBlock(headerBuffer, coinbaseBuffer, nonceBuffer, mixHashBuffer).toString('hex'); // Generate Output Block Hash if (algorithm === 'firopow') { combinedBuffer = Buffer.alloc(120); headerBuffer.copy(combinedBuffer); merkleRoot.copy(combinedBuffer, 36); nonceBuffer.copy(combinedBuffer, 80); utils.reverseBuffer(mixHashBuffer).copy(combinedBuffer, 88); blockHash = utils.reverseBuffer(utils.sha256d(combinedBuffer)).toString('hex'); } else { blockHash = hashOutputBuffer.toString('hex'); } // Check if Share is Valid Block Candidate if (job.target.ge(headerBigNum)) { blockValid = true; } else { if (shareDiff / difficulty < 0.99) { if (previousDifficulty && shareDiff >= previousDifficulty) { difficulty = previousDifficulty; } else { return shareError([23, 'low difficulty share of ' + shareDiff]); } } } // Build Share Object Data shareData = { job: jobId, ip: ipAddress, port: port, addrPrimary: addrPrimary, addrAuxiliary: addrAuxiliary, blockDiffPrimary : blockDiffAdjusted, blockType: blockValid ? 'primary' : 'share', coinbase: coinbaseBuffer, difficulty: difficulty, hash: blockHash, hex: blockHex, header: headerHash, headerDiff: headerBigNum, height: job.rpcData.height, identifier: identifier, reward: job.rpcData.coinbasevalue, shareDiff: shareDiff.toFixed(8), }; auxShareData = { job: jobId, ip: ipAddress, port: port, addrPrimary: addrPrimary, addrAuxiliary: addrAuxiliary, blockDiffPrimary : blockDiffAdjusted, blockType: 'auxiliary', coinbase: coinbaseBuffer, difficulty: difficulty, hash: blockHash, hex: blockHex, header: headerHash, headerDiff: headerBigNum, identifier: identifier, shareDiff: shareDiff.toFixed(8), }; _this.emit('share', shareData, auxShareData, blockValid); return { error: null, hash: blockHash, hex: blockHex, result: true }; // Default Share Submission default: // Edge Cases to Check if Share is Invalid submitTime = Date.now() / 1000 | 0; job = _this.validJobs[jobId]; if (submission.extraNonce2.length / 2 !== _this.extraNonce2Size) return shareError([20, 'incorrect size of extranonce2']); if (typeof job === 'undefined' || job.jobId != jobId) { return shareError([21, 'job not found']); } if (submission.nTime.length !== 8) { return shareError([20, 'incorrect size of ntime']); } nTimeInt = parseInt(submission.nTime, 16); if (nTimeInt < job.rpcData.curtime || nTimeInt > submitTime + 7200) { return shareError([20, 'ntime out of range']); } if (submission.nonce.length !== 8) { return shareError([20, 'incorrect size of nonce']); } if (!addrPrimary && !addrAuxiliary) { return shareError([20, 'worker address isn\'t set properly']); } if (!job.registerSubmit([submission.extraNonce1, submission.extraNonce2, submission.nTime, submission.nonce])) { return shareError([22, 'duplicate share']); } // Check for asicboost Support version = job.rpcData.version; if (submission.asicboost && submission.versionBit !== undefined) { const vBit = parseInt('0x' + submission.versionBit); const vMask = parseInt('0x' + submission.versionMask); if ((vBit & ~vMask) !== 0) { return shareError([20, 'invalid version bit']); } version = (version & ~vMask) | (vBit & vMask); } // Establish Share Information blockValid = false; extraNonce1Buffer = Buffer.from(submission.extraNonce1, 'hex'); extraNonce2Buffer = Buffer.from(submission.extraNonce2, 'hex'); // Generate Coinbase Buffer coinbaseBuffer = job.serializeCoinbase(extraNonce1Buffer, extraNonce2Buffer); coinbaseHash = job.coinbaseHasher(coinbaseBuffer); merkleRoot = job.merkle.withFirst(coinbaseHash); // Start Generating Block Hash headerDigest = Algorithms[algorithm].hash(_this.poolConfig.primary.coin); headerBuffer = job.serializeHeader(merkleRoot, submission.nTime, submission.nonce, version); headerHash = headerDigest(headerBuffer, nTimeInt); headerBigNum = bignum.fromBuffer(headerHash, {endian: 'little', size: 32}); // Calculate Share Difficulty shareDiff = Algorithms[algorithm].diff / headerBigNum.toNumber() * shareMultiplier; blockDiffAdjusted = job.difficulty * shareMultiplier; blockHex = job.serializeBlock(headerBuffer, coinbaseBuffer, null, null).toString('hex'); blockHash = job.blockHasher(headerBuffer, submission.nTime).toString('hex'); // Check if Share is Valid Block Candidate if (job.target.ge(headerBigNum)) { blockValid = true; } else { if (shareDiff / difficulty < 0.99) { if (previousDifficulty && shareDiff >= previousDifficulty) { difficulty = previousDifficulty; } else { return shareError([23, 'low difficulty share of ' + shareDiff]); } } } // Build Share Object Data shareData = { job: jobId, ip: ipAddress, port: port, addrPrimary: addrPrimary, addrAuxiliary: addrAuxiliary, blockDiffPrimary : blockDiffAdjusted, blockType: blockValid ? 'primary' : 'share', coinbase: coinbaseBuffer, difficulty: difficulty, hash: blockHash, hex: blockHex, header: headerHash, headerDiff: headerBigNum, height: job.rpcData.height, identifier: identifier, reward: job.rpcData.coinbasevalue, shareDiff: shareDiff.toFixed(8), }; auxShareData = { job: jobId, ip: ipAddress, port: port, addrPrimary: addrPrimary, addrAuxiliary: addrAuxiliary, blockDiffPrimary : blockDiffAdjusted, blockType: 'auxiliary', coinbase: coinbaseBuffer, difficulty: difficulty, hash: blockHash, hex: blockHex, header: headerHash, headerDiff: headerBigNum, identifier: identifier, shareDiff: shareDiff.toFixed(8), }; _this.emit('share', shareData, auxShareData, blockValid); return { error: null, hash: blockHash, hex: blockHex, result: true }; } }; }; module.exports = Manager; Manager.prototype.__proto__ = events.EventEmitter.prototype;