nomp矿池源码详解

1 项目简介

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

2 源码详解

2.1 目录

coins目录里是各个币种的名称及算法配置,libs目录中是各大功能模块的源码,node_modules目录中是各个nodejs模块,pool_configs目录中是矿池支持币种的配置,scripts目录中是两个关键性脚本文件,website目录中是前段相关代码及资源;

config.json用于用于设置矿池全局配置,如监听端口、连接超时、任务更新间隔等,init.js是nodejs入口文件,package.json用于记录依赖包及版本等相关信息。

2.2 入口函数

 执行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

2.3 buildPoolConfigs

 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 };
buildPoolConfigs

9~15行会依次解析pool_configs中不同币种的配置文件,并将配置中使能状态为true的币种配置存储在poolConfigFiles边量。

19~39行检查每个币种会唯一的对应于coins目录的算法配置文件,且每个币种在矿池中使用不同的监听端口。

42~87行根据config.json中的全局配置,更新每个币种对应的配置(如果相应的配置项不存在),此外相应算法要在node_modules\stratum-pool\lib\algoProperties.js已实现,否则会删除对应算法的配置,即矿池不支持该算法。

88行将全局配置返回,并赋值给全局边量poolConfigs。

2.4 spawnPoolWorkers

 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 };
spawnPoolWorkers

 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相关内容。

2.5 startPaymentProcessor

 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 };
startPaymentProcessor

 这部分内容是关于挖矿付款的处理,由于本人对这部分内容也没有深入了解,所以不再进行详细介绍。

2.6 startWebsite

 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 };
startWebsite

 该部分利用express模块生成web前端,这部分内容相对比较独立,不再进行详细介绍,相关功能请直接参考源码。

2.7 startProfitSwitch

 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 };
startProfitSwitch

 该部分用于获取各大交易网站的实时价格信息,这部分代码已经不再更新,这里也不再详细介绍,有兴趣的请直接查看源码。

2.8 startCliListener

 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 };
startCliListener

 第3行根据配置中的cliPort端口创建监听,在10~25行依次处理矿池具体业务相关的blocknotfy、coinswitch、reloadpool命令。

3 总结

NOMP以stratum-pool高性能Stratum池服务器为核心,该部分主要对象可以用下图进行表示:

在stratum-pool基础上,nomp增加网站前端、数据库层、多币种/池支持以及自动切换矿工在不同币种/池之间的操作等功能,如想单纯的查看stratum服务器核心功能,请直接参考该项目

https://github.com/zone117x/node-stratum-pool

也即NOMP项目下node_modules\stratum-pool内容。

 

热门相关:大首长,小媳妇   暴君他偏要宠我   交易校园情侣   重生八零俏佳妻   我的阁楼通异界