Node Open Mining Portal(简称NOMP)是一个由Node.js编写的高效、可扩展的加密货币挖矿池软件,专为经验丰富的系统管理员和开发者设计。它包含了Stratum挖矿池服务器、奖励处理与支付功能以及一个响应式前端网站,提供实时统计和管理中心。NOMP基于node-stratum-pool模块,支持动态难度调整(vardiff)、工作证明(POW)和权益证明(POS)。它的安全特性包括DDoS攻击防护、IP禁止列表,并采用了Redis进行内存中的数据存储以优化性能。此外,其多币种挖掘和负载均衡能力使得管理多个币种的矿池变得简单。
该项目的安装配置不再进行详细介绍,感兴趣的请参考之前写的文章:https://www.cnblogs.com/zhaoweiwei/p/nomp.html
coins目录里是各个币种的名称及算法配置,libs目录中是各大功能模块的源码,node_modules目录中是各个nodejs模块,pool_configs目录中是矿池支持币种的配置,scripts目录中是两个关键性脚本文件,website目录中是前段相关代码及资源;
config.json用于用于设置矿池全局配置,如监听端口、连接超时、任务更新间隔等,init.js是nodejs入口文件,package.json用于记录依赖包及版本等相关信息。
执行node init.js将会根据配置启动主程序,首先会解析当前目录下的config.json配置文件,并将结果存储在portalConfig变量中,还会创建PoolLogger对象(源码对应libs\logUtil.js文件)统一管理log信息
之后,会里用cluster模块,来判断当前进程是主进程(通常称为“master”),还是工作进程(“workers”),对于工作进程,按照类型创建不同的实例
1 if (cluster.isWorker){ 2 3 switch(process.env.workerType){ 4 case 'pool': 5 new PoolWorker(logger); 6 break; 7 case 'paymentProcessor': 8 new PaymentProcessor(logger); 9 break; 10 case 'website': 11 new Website(logger); 12 break; 13 case 'profitSwitch': 14 new ProfitSwitch(logger); 15 break; 16 } 17 18 return; 19 }
如果是主进程则会调用以下功能模块函数,创建不同的工作进程
1 (function init(){ 2 3 poolConfigs = buildPoolConfigs(); 4 5 spawnPoolWorkers(); 6 7 startPaymentProcessor(); 8 9 startWebsite(); 10 11 startProfitSwitch(); 12 13 startCliListener(); 14 15 })();
buildPoolConfigs函数会对相关配置文件进行解析整合
spawnPoolWorkers函数会创建PoolWorker进程,功能函数对应libs\poolWorker.js
startPaymentProcessor函数会创建paymentProcessor进程,功能函数对应libs\paymentProcessor.js
startWebsite函数会创建Website进程,功能函数对应libs\website.js
startProfitSwitch函数会创建ProfitSwitch进程,功能函数对应libs\profitSwitch.js
startCliListener函数会创建CliListener对象在cliPort端口进行监听并处理收到的消息,功能函数对应libs\cliListener.js
1 var buildPoolConfigs = function(){ 2 var configs = {}; 3 var configDir = 'pool_configs/'; 4 5 var poolConfigFiles = []; 6 7 8 /* Get filenames of pool config json files that are enabled */ 9 fs.readdirSync(configDir).forEach(function(file){ 10 if (!fs.existsSync(configDir + file) || path.extname(configDir + file) !== '.json') return; 11 var poolOptions = JSON.parse(JSON.minify(fs.readFileSync(configDir + file, {encoding: 'utf8'}))); 12 if (!poolOptions.enabled) return; 13 poolOptions.fileName = file; 14 poolConfigFiles.push(poolOptions); 15 }); 16 17 18 /* Ensure no pool uses any of the same ports as another pool */ 19 for (var i = 0; i < poolConfigFiles.length; i++){ 20 var ports = Object.keys(poolConfigFiles[i].ports); 21 for (var f = 0; f < poolConfigFiles.length; f++){ 22 if (f === i) continue; 23 var portsF = Object.keys(poolConfigFiles[f].ports); 24 for (var g = 0; g < portsF.length; g++){ 25 if (ports.indexOf(portsF[g]) !== -1){ 26 logger.error('Master', poolConfigFiles[f].fileName, 'Has same configured port of ' + portsF[g] + ' as ' + poolConfigFiles[i].fileName); 27 process.exit(1); 28 return; 29 } 30 } 31 32 if (poolConfigFiles[f].coin === poolConfigFiles[i].coin){ 33 logger.error('Master', poolConfigFiles[f].fileName, 'Pool has same configured coin file coins/' + poolConfigFiles[f].coin + ' as ' + poolConfigFiles[i].fileName + ' pool'); 34 process.exit(1); 35 return; 36 } 37 38 } 39 } 40 41 42 poolConfigFiles.forEach(function(poolOptions){ 43 44 poolOptions.coinFileName = poolOptions.coin; 45 46 var coinFilePath = 'coins/' + poolOptions.coinFileName; 47 if (!fs.existsSync(coinFilePath)){ 48 logger.error('Master', poolOptions.coinFileName, 'could not find file: ' + coinFilePath); 49 return; 50 } 51 52 var coinProfile = JSON.parse(JSON.minify(fs.readFileSync(coinFilePath, {encoding: 'utf8'}))); 53 poolOptions.coin = coinProfile; 54 poolOptions.coin.name = poolOptions.coin.name.toLowerCase(); 55 56 if (poolOptions.coin.name in configs){ 57 58 logger.error('Master', poolOptions.fileName, 'coins/' + poolOptions.coinFileName 59 + ' has same configured coin name ' + poolOptions.coin.name + ' as coins/' 60 + configs[poolOptions.coin.name].coinFileName + ' used by pool config ' 61 + configs[poolOptions.coin.name].fileName); 62 63 process.exit(1); 64 return; 65 } 66 67 for (var option in portalConfig.defaultPoolConfigs){ 68 if (!(option in poolOptions)){ 69 var toCloneOption = portalConfig.defaultPoolConfigs[option]; 70 var clonedOption = {}; 71 if (toCloneOption.constructor === Object) 72 extend(true, clonedOption, toCloneOption); 73 else 74 clonedOption = toCloneOption; 75 poolOptions[option] = clonedOption; 76 } 77 } 78 79 80 configs[poolOptions.coin.name] = poolOptions; 81 82 if (!(coinProfile.algorithm in algos)){ 83 logger.error('Master', coinProfile.name, 'Cannot run a pool for unsupported algorithm "' + coinProfile.algorithm + '"'); 84 delete configs[poolOptions.coin.name]; 85 } 86 87 }); 88 return configs; 89 };
9~15行会依次解析pool_configs中不同币种的配置文件,并将配置中使能状态为true的币种配置存储在poolConfigFiles边量。
19~39行检查每个币种会唯一的对应于coins目录的算法配置文件,且每个币种在矿池中使用不同的监听端口。
42~87行根据config.json中的全局配置,更新每个币种对应的配置(如果相应的配置项不存在),此外相应算法要在node_modules\stratum-pool\lib\algoProperties.js已实现,否则会删除对应算法的配置,即矿池不支持该算法。
88行将全局配置返回,并赋值给全局边量poolConfigs。
1 var spawnPoolWorkers = function(){ 2 3 Object.keys(poolConfigs).forEach(function(coin){ 4 var p = poolConfigs[coin]; 5 6 if (!Array.isArray(p.daemons) || p.daemons.length < 1){ 7 logger.error('Master', coin, 'No daemons configured so a pool cannot be started for this coin.'); 8 delete poolConfigs[coin]; 9 } 10 }); 11 12 if (Object.keys(poolConfigs).length === 0){ 13 logger.warning('Master', 'PoolSpawner', 'No pool configs exists or are enabled in pool_configs folder. No pools spawned.'); 14 return; 15 } 16 17 18 var serializedConfigs = JSON.stringify(poolConfigs); 19 20 var numForks = (function(){ 21 if (!portalConfig.clustering || !portalConfig.clustering.enabled) 22 return 1; 23 if (portalConfig.clustering.forks === 'auto') 24 return os.cpus().length; 25 if (!portalConfig.clustering.forks || isNaN(portalConfig.clustering.forks)) 26 return 1; 27 return portalConfig.clustering.forks; 28 })(); 29 30 var poolWorkers = {}; 31 32 var createPoolWorker = function(forkId){ 33 var worker = cluster.fork({ 34 workerType: 'pool', 35 forkId: forkId, 36 pools: serializedConfigs, 37 portalConfig: JSON.stringify(portalConfig) 38 }); 39 worker.forkId = forkId; 40 worker.type = 'pool'; 41 poolWorkers[forkId] = worker; 42 worker.on('exit', function(code, signal){ 43 logger.error('Master', 'PoolSpawner', 'Fork ' + forkId + ' died, spawning replacement worker...'); 44 setTimeout(function(){ 45 createPoolWorker(forkId); 46 }, 2000); 47 }).on('message', function(msg){ 48 switch(msg.type){ 49 case 'banIP': 50 Object.keys(cluster.workers).forEach(function(id) { 51 if (cluster.workers[id].type === 'pool'){ 52 cluster.workers[id].send({type: 'banIP', ip: msg.ip}); 53 } 54 }); 55 break; 56 } 57 }); 58 }; 59 60 var i = 0; 61 var spawnInterval = setInterval(function(){ 62 createPoolWorker(i); 63 i++; 64 if (i === numForks){ 65 clearInterval(spawnInterval); 66 logger.debug('Master', 'PoolSpawner', 'Spawned ' + Object.keys(poolConfigs).length + ' pool(s) on ' + numForks + ' thread(s)'); 67 } 68 }, 250); 69 70 };
33行会创建pool类型的worker进程,这又会对应在2.2节介绍的内容,根据worker进程类型,创建PoolWorker实例。在PoolWorker中,首先会使用process来处理其他模块发送的IPC消息;之后创建ShareProcessor对象,用于管理客户端的share提交;本地handlers对象中不同函数处理与矿池stratum交互消息,如auth、share、diff等;最后通过Stratum.createPool创建矿池对象pool,并通过pool.start启动矿池,该部分详细内容请参考node_modules\stratum-pool\lib\pool.js文件内容。
1 this.start = function(){ 2 SetupVarDiff(); 3 SetupApi(); 4 SetupDaemonInterface(function(){ 5 DetectCoinData(function(){ 6 SetupRecipients(); 7 SetupJobManager(); 8 OnBlockchainSynced(function(){ 9 GetFirstJob(function(){ 10 SetupBlockPolling(); 11 SetupPeer(); 12 StartStratumServer(function(){ 13 OutputPoolInfo(); 14 _this.emit('started'); 15 }); 16 }); 17 }); 18 }); 19 }); 20 };
第2行用于设置可变难度,即会根据客户端share的提交修改下发任务的难度。
第4行SetupDaemonInterface根据配置文件中钱包配置(钱包所在host的IP地址及监听端口,rpc用户名及密码),创建与钱包rpc通信的守护线程daemon(参看node_modules\stratum-pool\lib\daemon.js)
之后在DetectCoinData函数中,通过validateaddress、getdifficulty、getmininginfo等rpc调用来对全局配置中类似hasSubmitMethod的关键项进行初始化,在OnBlockchainSynced函数中会等待钱包数据同步,同步完成后,调用GetFirstJob函数获取第一个job,在该函数中通过调用GetBlockTemplate从钱包获取block信息,然后通过jobManager.processTemplate来处理返回值,生成blockTemplate(node_modules\stratum-pool\lib\blockTemplate.js),在通过newBlock消息通知jobManager,jobManager再通过StartStratumServer将job广播出去,这里的jobParams对应于stratum协议的mining.notify中的params内容,如下图:
至于其他内容如任务提交、难度修改等处理都可以看node_modules\stratum-pool\lib相关内容。
1 var startPaymentProcessor = function(){ 2 3 var enabledForAny = false; 4 for (var pool in poolConfigs){ 5 var p = poolConfigs[pool]; 6 var enabled = p.enabled && p.paymentProcessing && p.paymentProcessing.enabled; 7 if (enabled){ 8 enabledForAny = true; 9 break; 10 } 11 } 12 13 if (!enabledForAny) 14 return; 15 16 var worker = cluster.fork({ 17 workerType: 'paymentProcessor', 18 pools: JSON.stringify(poolConfigs) 19 }); 20 worker.on('exit', function(code, signal){ 21 logger.error('Master', 'Payment Processor', 'Payment processor died, spawning replacement...'); 22 setTimeout(function(){ 23 startPaymentProcessor(poolConfigs); 24 }, 2000); 25 }); 26 };
这部分内容是关于挖矿付款的处理,由于本人对这部分内容也没有深入了解,所以不再进行详细介绍。
1 var startWebsite = function(){ 2 3 if (!portalConfig.website.enabled) return; 4 5 var worker = cluster.fork({ 6 workerType: 'website', 7 pools: JSON.stringify(poolConfigs), 8 portalConfig: JSON.stringify(portalConfig) 9 }); 10 worker.on('exit', function(code, signal){ 11 logger.error('Master', 'Website', 'Website process died, spawning replacement...'); 12 setTimeout(function(){ 13 startWebsite(portalConfig, poolConfigs); 14 }, 2000); 15 }); 16 };
该部分利用express模块生成web前端,这部分内容相对比较独立,不再进行详细介绍,相关功能请直接参考源码。
1 var startProfitSwitch = function(){ 2 3 if (!portalConfig.profitSwitch || !portalConfig.profitSwitch.enabled){ 4 //logger.error('Master', 'Profit', 'Profit auto switching disabled'); 5 return; 6 } 7 8 var worker = cluster.fork({ 9 workerType: 'profitSwitch', 10 pools: JSON.stringify(poolConfigs), 11 portalConfig: JSON.stringify(portalConfig) 12 }); 13 worker.on('exit', function(code, signal){ 14 logger.error('Master', 'Profit', 'Profit switching process died, spawning replacement...'); 15 setTimeout(function(){ 16 startWebsite(portalConfig, poolConfigs); 17 }, 2000); 18 }); 19 };
该部分用于获取各大交易网站的实时价格信息,这部分代码已经不再更新,这里也不再详细介绍,有兴趣的请直接查看源码。
1 var startCliListener = function(){ 2 3 var cliPort = portalConfig.cliPort; 4 5 var listener = new CliListener(cliPort); 6 listener.on('log', function(text){ 7 logger.debug('Master', 'CLI', text); 8 }).on('command', function(command, params, options, reply){ 9 10 switch(command){ 11 case 'blocknotify': 12 Object.keys(cluster.workers).forEach(function(id) { 13 cluster.workers[id].send({type: 'blocknotify', coin: params[0], hash: params[1]}); 14 }); 15 reply('Pool workers notified'); 16 break; 17 case 'coinswitch': 18 processCoinSwitchCommand(params, options, reply); 19 break; 20 case 'reloadpool': 21 Object.keys(cluster.workers).forEach(function(id) { 22 cluster.workers[id].send({type: 'reloadpool', coin: params[0] }); 23 }); 24 reply('reloaded pool ' + params[0]); 25 break; 26 default: 27 reply('unrecognized command "' + command + '"'); 28 break; 29 } 30 }).start(); 31 };
第3行根据配置中的cliPort端口创建监听,在10~25行依次处理矿池具体业务相关的blocknotfy、coinswitch、reloadpool命令。
NOMP以stratum-pool高性能Stratum池服务器为核心,该部分主要对象可以用下图进行表示:
在stratum-pool基础上,nomp增加网站前端、数据库层、多币种/池支持以及自动切换矿工在不同币种/池之间的操作等功能,如想单纯的查看stratum服务器核心功能,请直接参考该项目
https://github.com/zone117x/node-stratum-pool
也即NOMP项目下node_modules\stratum-pool内容。