UNPKG

stratum-pool-sha256

Version:

High performance SHA-256 Stratum poolserver in Node.js - zero dependencies, pure JavaScript

343 lines (286 loc) 12 kB
var http = require('http'); var cp = require('child_process'); var events = require('events'); var async = require('async'); // Constants for security and reliability const REQUEST_TIMEOUT = 30000; // 30 seconds const MAX_RETRIES = 3; const RETRY_DELAY = 1000; // Initial retry delay in ms // Whitelist of allowed RPC methods const ALLOWED_RPC_METHODS = [ 'getinfo', 'getmininginfo', 'getblocktemplate', 'submitblock', 'validateaddress', 'getaddressinfo', 'getbalance', 'getpeerinfo', 'getnetworkinfo', 'getrawtransaction', 'sendrawtransaction', 'getblock', 'getblockcount', 'getblockhash', 'getblockheader', 'gettransaction', 'getwork', 'help', 'stop', 'getgenerate', 'gethashespersec', 'getdifficulty', 'getnetworkhashps', 'setgenerate', 'getaccountaddress', 'getaccount', 'importprivkey', 'dumpprivkey', 'keypoolrefill', 'walletpassphrase', 'walletlock', 'sendtoaddress', 'sendfrom', 'sendmany', 'addmultisigaddress', 'getreceivedbyaddress', 'getreceivedbyaccount', 'listreceivedbyaddress', 'listreceivedbyaccount', 'backupwallet', 'encryptwallet', 'walletpassphrasechange', 'getnewaddress', 'signmessage', 'verifymessage', 'listaccounts', 'listaddressgroupings', 'settxfee', 'listsinceblock', 'listtransactions', 'listlockunspent', 'listunspent', 'lockunspent', 'move', 'createmultisig', 'createrawtransaction', 'decoderawtransaction', 'decodescript', 'fundrawtransaction', 'signrawtransaction', 'signrawtransactionwithkey', 'signrawtransactionwithwallet', 'getrawchangeaddress', 'gettxout', 'gettxoutsetinfo', 'getblockchaininfo', 'verifychain', 'getbestblockhash', 'getconnectioncount', 'ping', 'addnode', 'getaddednodeinfo', 'getchaintips', 'getmempoolinfo', 'getrawmempool', 'getmempoolentry', 'prioritisetransaction', 'estimatefee', 'estimatepriority', 'estimatesmartfee', 'estimatesmartpriority' ]; /** * The daemon interface interacts with cryptocurrency daemons via JSON-RPC. * It supports multiple daemon instances for redundancy and load balancing. * * @class DaemonInterface * @extends {EventEmitter} * @param {Array<Object>} daemons - Array of daemon configurations * @param {string} daemons[].host - Hostname where the daemon is running * @param {number} daemons[].port - Port for RPC connections * @param {string} daemons[].user - RPC username * @param {string} daemons[].password - RPC password * @param {Function} [logger] - Optional logging function * * @fires DaemonInterface#online - When daemon connection is established * @fires DaemonInterface#connectionFailed - When daemon connection fails * @fires DaemonInterface#error - On RPC errors */ 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('getpeerinfo', [], function(results){ var allOnline = results.every(function(result){ return !results.error; }); callback(allOnline); if (!allOnline) _this.emit('connectionFailed', results); }); } function validateInput(method, params) { // Validate method name if (typeof method !== 'string' || method.length === 0) { return { valid: false, error: 'Invalid method name' }; } // Check if method is in whitelist if (!ALLOWED_RPC_METHODS.includes(method)) { return { valid: false, error: 'Method not allowed: ' + method }; } // Validate params is an array if (!Array.isArray(params)) { return { valid: false, error: 'Parameters must be an array' }; } // Validate parameter types and values for (var i = 0; i < params.length; i++) { var param = params[i]; // Check for potentially dangerous values if (typeof param === 'string') { // Prevent injection attacks if (param.includes('\n') || param.includes('\r')) { return { valid: false, error: 'Invalid parameter: contains newline' }; } // Limit string length if (param.length > 10000) { return { valid: false, error: 'Parameter too long' }; } } // Validate numbers if (typeof param === 'number') { if (!isFinite(param)) { return { valid: false, error: 'Invalid number parameter' }; } } } return { valid: true }; } function performHttpRequest(instance, jsonData, callback, retryCount){ retryCount = retryCount || 0; 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, 'Content-Type': 'application/json' }, timeout: REQUEST_TIMEOUT }; 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 + '\nReponse Data: ' + data); } 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 if ((e.code === 'ETIMEDOUT' || e.code === 'ESOCKETTIMEDOUT') && retryCount < MAX_RETRIES) { // Retry with exponential backoff var delay = RETRY_DELAY * Math.pow(2, retryCount); logger('warn', 'Request timeout, retrying in ' + delay + 'ms (attempt ' + (retryCount + 1) + '/' + MAX_RETRIES + ')'); setTimeout(function() { performHttpRequest(instance, jsonData, callback, retryCount + 1); }, delay); } else { callback({type: 'request error', message: e.message}, null); } }); req.on('timeout', function() { req.abort(); logger('warn', 'Request timeout after ' + REQUEST_TIMEOUT + 'ms'); }); 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++){ // Validate each command var validation = validateInput(cmdArray[i][0], cmdArray[i][1]); if (!validation.valid) { callback({type: 'validation error', message: validation.error}, null); return; } 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){ // Validate input var validation = validateInput(method, params); if (!validation.valid) { var error = {type: 'validation error', message: validation.error}; if (streamResults) { callback({error: error, response: null, instance: null}); } else { callback([{error: error, response: null, instance: null}]); } return; } 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({ method: method, params: params, id: Date.now() + Math.floor(Math.random() * 10) }); performHttpRequest(instance, requestJson, function(error, result, data){ itemFinished(error, result, data); }); }, function(){ if (!streamResults){ callback(results); } }); } //public members /** * Initializes the daemon interface and checks connection status. * * @method init * @fires DaemonInterface#online - If connection is successful * @fires DaemonInterface#connectionFailed - If connection fails */ this.init = init; /** * Checks if all daemon instances are online. * * @method isOnline * @param {Function} callback - Callback with boolean indicating online status */ this.isOnline = isOnline; /** * Executes an RPC command on all daemon instances. * * @method cmd * @param {string} method - RPC method name * @param {Array} params - Array of parameters for the RPC method * @param {Function} callback - Callback with results from all daemons * @param {boolean} [streamResults=false] - If true, callback is called for each daemon * @param {boolean} [returnRawData=false] - If true, includes raw response data */ this.cmd = cmd; /** * Executes a batch of RPC commands on the first daemon instance. * * @method batchCmd * @param {Array} cmdArray - Array of [method, params] pairs * @param {Function} callback - Callback with batch results */ this.batchCmd = batchCmd; } DaemonInterface.prototype.__proto__ = events.EventEmitter.prototype; exports.interface = DaemonInterface;