/** * Cryptonote Node.JS Pool * https://github.com/dvandal/cryptonote-nodejs-pool * * Block unlocker **/ // Load required modules let async = require('async'); let apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet, config.api); let notifications = require('./notifications.js'); let utils = require('./utils.js'); let slushMiningEnabled = config.poolServer.slushMining && config.poolServer.slushMining.enabled; // Initialize log system let logSystem = 'unlocker'; require('./exceptionWriter.js')(logSystem); /** * Run block unlocker **/ log('info', logSystem, 'Started'); function runInterval () { async.waterfall([ // Get all block candidates in redis function (callback) { redisClient.zrange(config.coin + ':blocks:candidates', 0, -1, 'WITHSCORES', function (error, results) { if (error) { log('error', logSystem, 'Error trying to get pending blocks from redis %j', [error]); callback(true); return; } if (results.length === 0) { log('info', logSystem, 'No blocks candidates in redis'); callback(true); return; } let blocks = []; for (let i = 0; i < results.length; i += 2) { let parts = results[i].split(':'); blocks.push({ serialized: results[i], height: parseInt(results[i + 1]), rewardType: parts[0], login: parts[1], hash: parts[2], time: parts[3], difficulty: parts[4], shares: parts[5], score: parts.length >= 7 ? parts[6] : parts[5], mmClassification: parts.length >= 8 ? parts[7] : 'plain', auxAccepted: parts.length >= 9 ? parts[8] : 0, parentAccepted: parts.length >= 10 ? parts[9] : 0 }); } callback(null, blocks); }) }, // Check if blocks are orphaned function (blocks, callback) { async.filter(blocks, function (block, mapCback) { let daemonType = config.daemonType ? config.daemonType.toLowerCase() : "default"; let blockHeight = ((daemonType === "forknote" || daemonType === "bytecoin") && config.blockUnlocker.fixBlockHeightRPC) ? block.height + 1 : block.height; let rpcMethod = config.blockUnlocker.useFirstVout ? 'getblock' : 'getblockheaderbyheight'; apiInterfaces.rpcDaemon(rpcMethod, { height: blockHeight }, function (error, result) { if (error) { log('error', logSystem, 'Error with %s RPC request for block %s - %j', [rpcMethod, block.serialized, error]); block.unlocked = false; mapCback(); return; } if (!result.block_header) { log('error', logSystem, 'Error with getblockheaderbyheight RPC request for block %s - %j', [block.serialized, error]); block.unlocked = false; mapCback(); return; } let blockHeader = result.block_header; block.orphaned = blockHeader.hash === block.hash ? 0 : 1; block.unlocked = blockHeader.depth >= config.blockUnlocker.depth; block.reward = blockHeader.reward; if (config.blockUnlocker.useFirstVout) { let vout = JSON.parse(result.json).miner_tx.vout; if (!vout.length) { log('error', logSystem, 'Error: tx at height %s has no vouts!', [blockHeight]); block.unlocked = false; mapCback(); return; } block.reward = vout[0].amount; } else { block.reward = blockHeader.reward; } if (config.blockUnlocker.networkFee) { let networkFeePercent = config.blockUnlocker.networkFee / 100; block.reward = block.reward - (block.reward * networkFeePercent); } mapCback(block.unlocked); }) }, function (unlockedBlocks) { if (unlockedBlocks.length === 0) { log('info', logSystem, 'No pending blocks are unlocked yet (%d pending)', [blocks.length]); callback(true); return; } callback(null, unlockedBlocks); }) }, // Get worker shares for each unlocked block function (blocks, callback) { let redisCommands = blocks.map(function (block) { if (block.rewardType === 'prop') { return ['hgetall', config.coin + ':scores:prop:round' + block.height]; } else { return ['hgetall', config.coin + ':scores:solo:round' + block.height]; } }) redisClient.multi(redisCommands) .exec(function (error, replies) { if (error) { log('error', logSystem, 'Error with getting round shares from redis %j', [error]); callback(true); return; } for (let i = 0; i < replies.length; i++) { let workerScores = replies[i]; blocks[i].workerScores = workerScores; } callback(null, blocks); }) }, // Handle orphaned blocks function (blocks, callback) { let orphanCommands = []; blocks.forEach(function (block) { if (!block.orphaned) { return; } orphanCommands.push(['del', config.coin + ':scores:solo:round' + block.height]); orphanCommands.push(['del', config.coin + ':scores:prop:round' + block.height]); orphanCommands.push(['del', config.coin + ':shares_actual:solo:round' + block.height]); orphanCommands.push(['del', config.coin + ':shares_actual:prop:round' + block.height]); orphanCommands.push(['zrem', config.coin + ':blocks:candidates', block.serialized]); orphanCommands.push(['zadd', config.coin + ':blocks:matured', block.height, [ block.rewardType, block.login, block.hash, block.time, block.difficulty, block.shares, block.orphaned, block.mmClassification || 'plain', block.auxAccepted || 0, block.parentAccepted || 0 ].join(':')]); if (block.workerScores && !slushMiningEnabled) { let workerScores = block.workerScores; Object.keys(workerScores).forEach(function (worker) { orphanCommands.push(['hincrby', config.coin + ':scores:roundCurrent', worker, workerScores[worker]]); }) } notifications.sendToAll('blockOrphaned', { 'HEIGHT': block.height, 'BLOCKTIME': utils.dateFormat(new Date(parseInt(block.time) * 1000), 'yyyy-mm-dd HH:MM:ss Z'), 'HASH': block.hash, 'DIFFICULTY': block.difficulty, 'SHARES': block.shares, 'EFFORT': Math.round(block.shares / block.difficulty * 100) + '%' }) }) if (orphanCommands.length > 0) { redisClient.multi(orphanCommands).exec(function (error, replies) { if (error) { log('error', logSystem, 'Error with cleaning up data in redis for orphan block(s) %j', [error]); callback(true); return; } callback(null, blocks); }) } else { callback(null, blocks); } }, // Handle unlocked blocks function (blocks, callback) { let unlockedBlocksCommands = []; let payments = {}; let totalBlocksUnlocked = 0; blocks.forEach(function (block) { if (block.orphaned) { return; } totalBlocksUnlocked++; unlockedBlocksCommands.push(['del', config.coin + ':scores:solo:round' + block.height]); unlockedBlocksCommands.push(['del', config.coin + ':scores:prop:round' + block.height]); unlockedBlocksCommands.push(['del', config.coin + ':shares_actual:solo:round' + block.height]); unlockedBlocksCommands.push(['del', config.coin + ':shares_actual:prop:round' + block.height]); unlockedBlocksCommands.push(['zrem', config.coin + ':blocks:candidates', block.serialized]); unlockedBlocksCommands.push(['zadd', config.coin + ':blocks:matured', block.height, [ block.rewardType, block.login, block.hash, block.time, block.difficulty, block.shares, block.orphaned, block.reward, block.mmClassification || 'plain', block.auxAccepted || 0, block.parentAccepted || 0 ].join(':')]); let feePercent = (config.blockUnlocker.poolFee > 0 ? config.blockUnlocker.poolFee : 0) / 100; if (block.rewardType === 'solo') { feePercent = (config.blockUnlocker.soloFee >= 0 ? config.blockUnlocker.soloFee : (config.blockUnlocker.poolFee > 0 ? config.blockUnlocker.poolFee : 0)) / 100; } if (Object.keys(donations).length) { for (let wallet in donations) { let percent = donations[wallet] / 100; feePercent += percent; payments[wallet] = Math.round(block.reward * percent); log('info', logSystem, 'Block %d donation to %s as %d percent of reward: %d', [block.height, wallet, percent, payments[wallet]]); } } let reward = 0; let finderReward = 0; if (block.rewardType === 'solo') { reward = Math.round(block.reward - (block.reward * feePercent)); log('info', logSystem, 'Unlocked SOLO block %d with reward %d and donation fee %d. Miners reward: %d', [block.height, block.reward, feePercent, reward]); } else { let finderPercent = (config.blockUnlocker.finderReward > 0 ? config.blockUnlocker.finderReward : 0) / 100; finderReward = Math.round(block.reward * finderPercent); reward = Math.round(block.reward - (block.reward * (feePercent + finderPercent))); log('info', logSystem, 'Unlocked PROP block %d with reward %d, finders fee %d, and donation fee %d. Miners reward: %d Finders Reward: %d', [block.height, block.reward, finderPercent, feePercent, reward, finderReward]); } if (block.workerScores) { let totalScore = parseFloat(block.score); //deal with solo block if (block.rewardType === 'solo') { let worker = block.login; payments[worker] = (payments[worker] || 0) + reward; log('info', logSystem, 'SOLO Block %d payment to %s for %d%% of total block score: %d', [block.height, worker, 100, payments[worker]]); } else { Object.keys(block.workerScores).forEach(function (worker) { let percent = block.workerScores[worker] / totalScore; let workerReward = Math.round(reward * percent); payments[worker] = block.login === worker ? (payments[worker] || 0) + (workerReward + finderReward) : (payments[worker] || 0) + workerReward; log('info', logSystem, 'PROP Block %d payment to %s for %d%% of total block score: %d', [block.height, worker, percent * 100, payments[worker]]); }); } } notifications.sendToAll('blockUnlocked', { 'HEIGHT': block.height, 'BLOCKTIME': utils.dateFormat(new Date(parseInt(block.time) * 1000), 'yyyy-mm-dd HH:MM:ss Z'), 'HASH': block.hash, 'REWARD': utils.getReadableCoins(block.reward), 'DIFFICULTY': block.difficulty, 'SHARES': block.shares, 'EFFORT': Math.round(block.shares / block.difficulty * 100) + '%' }); }) for (let worker in payments) { let amount = parseInt(payments[worker]); if (amount <= 0) { delete payments[worker]; continue; } unlockedBlocksCommands.push(['hincrby', `${config.coin}:workers:${worker}`, 'balance', amount]); } if (unlockedBlocksCommands.length === 0) { log('info', logSystem, 'No unlocked blocks yet (%d pending)', [blocks.length]); callback(true); return; } redisClient.multi(unlockedBlocksCommands) .exec(function (error, replies) { if (error) { log('error', logSystem, 'Error with unlocking blocks %j', [error]); callback(true); return; } log('info', logSystem, 'Unlocked %d blocks and update balances for %d workers', [totalBlocksUnlocked, Object.keys(payments).length ]); callback(null); }) } ], function (error, result) { setTimeout(runInterval, config.blockUnlocker.interval * 1000); }) } runInterval();