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