diff --git a/lib/httpClient.js b/lib/httpClient.js index fd99344..133f451 100644 --- a/lib/httpClient.js +++ b/lib/httpClient.js @@ -136,6 +136,11 @@ var HttpClient = module.exports = function HttpClient(host, port, apiKey){ this.get(path, callback); } + this.getMainChainBlockByGhostUncle = function(ghostUncleHash, callback) { + var path = '/blockflow/main-chain-block-by-ghost-uncle/' + ghostUncleHash + this.get(path, callback) + } + this.blockHashesAtHeight = function(height, fromGroup, toGroup, callback){ var path = '/blockflow/hashes?fromGroup=' + fromGroup + '&toGroup=' + toGroup + '&height=' + height; this.get(path, callback); diff --git a/lib/shareProcessor.js b/lib/shareProcessor.js index 18142b7..d34c524 100644 --- a/lib/shareProcessor.js +++ b/lib/shareProcessor.js @@ -3,6 +3,8 @@ const HttpClient = require('./httpClient'); const util = require('./util'); const { Pool } = require('pg'); +const MaxGhostUncleAge = 7; + var ShareProcessor = module.exports = function ShareProcessor(config, logger){ var confirmationTime = config.confirmationTime * 1000; var rewardPercent = 1 - config.withholdPercent; @@ -128,6 +130,7 @@ var ShareProcessor = module.exports = function ShareProcessor(config, logger){ height: block.height, rewardAmount: rewardOutput.attoAlphAmount // string }; + logger.debug('Main chain block: ' + JSON.stringify(blockData)); callback(blockData); } @@ -143,6 +146,59 @@ var ShareProcessor = module.exports = function ShareProcessor(config, logger){ }) } + function getUncleReward(ghostUncleHash, ghostUncleHashWithTs, callback) { + _this.httpClient.getMainChainBlockByGhostUncle(ghostUncleHash, function (block) { + if (block.error) { + var errorMsg = `${block.error}` + if (errorMsg.includes(`Mainchain block by ghost uncle hash ${ghostUncleHash} not found`)) { + logger.warn(`Block ${ghostUncleHash} is not a ghost uncle block`); + var [fromGroup, toGroup] = util.blockChainIndex(Buffer.from(ghostUncleHash, 'hex')); + removeBlockAndShares(fromGroup, toGroup, ghostUncleHash, ghostUncleHashWithTs); + } else { + logger.error('Get main chain block error: ' + block.error + ', ghost uncle hash: ' + ghostUncleHash); + } + callback(null); + return; + } + + var transactions = block.transactions; + var coinbaseTx = transactions[transactions.length - 1]; + var index = block.ghostUncles.findIndex((u) => u.blockHash === ghostUncleHash) + var rewardOutput = coinbaseTx.unsigned.fixedOutputs[index + 1]; + var rewardAmount = rewardOutput.attoAlphAmount; + logger.info('Found main chain block ' + block.hash + ', uncle reward: ' + rewardAmount); + callback(rewardAmount); + }) + } + + function tryHandleUncleBlock(ghostUncleHash, ghostUncleHashWithTs, callback) { + logger.info('Try handling uncle block: ' + ghostUncleHash) + _this.httpClient.getBlock(ghostUncleHash, function(ghostUncleBlock){ + if (ghostUncleBlock.error){ + logger.error('Get uncle block error: ' + ghostUncleBlock.error + ', hash: ' + ghostUncleHash); + callback(null); + return; + } + + getUncleReward(ghostUncleHash, ghostUncleHashWithTs, function (uncleReward) { + if (uncleReward) { + var blockData = { + hash: ghostUncleHash, + fromGroup: ghostUncleBlock.chainFrom, + toGroup: ghostUncleBlock.chainTo, + height: ghostUncleBlock.height, + rewardAmount: uncleReward // string + }; + logger.debug('Ghost uncle block: ' + JSON.stringify(blockData)); + callback(blockData); + return; + } + callback(null); + return; + }) + }); + } + this.getPendingBlocks = function(results, callback){ var blocksNeedToReward = []; util.executeForEach(results, function(blockHashWithTs, callback){ @@ -166,10 +222,17 @@ var ShareProcessor = module.exports = function ShareProcessor(config, logger){ } if (!result){ - logger.error('Block is not in mainchain, remove block and shares, hash: ' + blockHash); - var [fromGroup, toGroup] = util.blockChainIndex(Buffer.from(blockHash, 'hex')); - removeBlockAndShares(fromGroup, toGroup, blockHash, blockHashWithTs); - callback(); + tryHandleUncleBlock(blockHash, blockHashWithTs, function (uncleBlockData) { + if (uncleBlockData) { + var block = { + pendingBlockValue: blockHashWithTs, + ...uncleBlockData + }; + blocksNeedToReward.push(block); + } + callback(); + return; + }); return; } @@ -183,7 +246,7 @@ var ShareProcessor = module.exports = function ShareProcessor(config, logger){ handleBlock(result, function(blockData){ var block = { pendingBlockValue: blockHashWithTs, - data: blockData + ...blockData }; blocksNeedToReward.push(block); callback(); @@ -215,8 +278,7 @@ var ShareProcessor = module.exports = function ShareProcessor(config, logger){ } function allocateRewardForBlock(block, redisTx, workerRewards, callback){ - var blockData = block.data; - var round = _this.roundKey(blockData.fromGroup, blockData.toGroup, blockData.hash); + var round = _this.roundKey(block.fromGroup, block.toGroup, block.hash); _this.redisClient.hgetall(round, function(error, shares){ if (error) { logger.error('Get shares failed, error: ' + error + ', round: ' + round); @@ -224,14 +286,14 @@ var ShareProcessor = module.exports = function ShareProcessor(config, logger){ return; } - var totalReward = Math.floor(parseInt(blockData.rewardAmount) * rewardPercent); - logger.info('Reward miners for block: ' + blockData.hash + ', total reward: ' + totalReward); - logger.debug('Block hash: ' + blockData.hash + ', shares: ' + JSON.stringify(shares)); + var totalReward = Math.floor(parseInt(block.rewardAmount) * rewardPercent); + logger.info('Reward miners for block: ' + block.hash + ', total reward: ' + totalReward); + logger.debug('Block hash: ' + block.hash + ', shares: ' + JSON.stringify(shares)); _this.allocateReward(totalReward, workerRewards, shares); redisTx.del(round); redisTx.srem(pendingBlocksKey, block.pendingBlockValue); - logger.info('Remove shares for block: ' + blockData.hash); + logger.info('Remove shares for block: ' + block.hash); callback(); }); } diff --git a/test/shareProcessorTest.js b/test/shareProcessorTest.js index 3435a5b..dd13f3f 100644 --- a/test/shareProcessorTest.js +++ b/test/shareProcessorTest.js @@ -84,8 +84,7 @@ describe('test share processor', function(){ shareProcessor.redisClient = redisClient; var shares = {'miner0': '4', 'miner1': '2', 'miner2': '2'}; - var blockData = {hash: '0011', fromGroup: 0, toGroup: 1, height: 1, rewardAmount: '40000000000000000000'}; - var block = {pendingBlockValue: blockData.hash + ':' + '0', data: blockData}; + var block = {pendingBlockValue: '0011' + ':' + '0', hash: '0011', fromGroup: 0, toGroup: 1, height: 1, rewardAmount: '40000000000000000000'}; var checkState = function(){ redisClient @@ -106,9 +105,9 @@ describe('test share processor', function(){ } var roundKey = shareProcessor.roundKey( - blockData.fromGroup, - blockData.toGroup, - blockData.hash + block.fromGroup, + block.toGroup, + block.hash ); redisClient @@ -123,6 +122,69 @@ describe('test share processor', function(){ }); }) + it('should reward uncle miners with correct reward amount', function(done){ + var config = { ...test.config, confirmationTime: 0 } + var shareProcessor = new ShareProcessor(config, test.logger); + shareProcessor.redisClient = redisClient; + + var currentMs = Date.now(); + var rewardAmount = '4000000000000000000'; + var ghostUncleRewardAmount = '2000000000000000000'; + var ghostUncleCoinbaseTx = [{unsigned:{fixedOutputs:[{attoAlphAmount: rewardAmount}]}}]; + var ghostUncleBlock = {hash: 'block1', height: 1, chainFrom: 0, chainTo: 0, transactions: ghostUncleCoinbaseTx, inMainChain: false, submittedMs: currentMs, ghostUncles: []} + + var mainChainCoinbaseTx = [{unsigned:{fixedOutputs:[{attoAlphAmount: rewardAmount},{attoAlphAmount: ghostUncleRewardAmount}]}}]; + var mainChainBlock = {hash: 'block2', height: 2, chainFrom: 0, chainTo: 0, transactions: mainChainCoinbaseTx, inMainChain: true, submittedMs: currentMs, ghostUncles: [{blockHash:ghostUncleBlock.hash}]} + var blocks = [ghostUncleBlock, mainChainBlock] + + function prepare(blocks, callback){ + var restServer = nock('http://127.0.0.1:12973'); + var redisTx = redisClient.multi(); + restServer.persist().get('/blockflow/main-chain-block-by-ghost-uncle/' + ghostUncleBlock.hash).reply(200, mainChainBlock) + for (var block of blocks){ + restServer.persist().get('/blockflow/blocks/' + block.hash).reply(200, block); + var isInMainChainPath = '/blockflow/is-block-in-main-chain?blockHash=' + block.hash; + restServer.persist().get(isInMainChainPath).reply(200, block.inMainChain ? true : false); + + var blockWithTs = block.hash + ':' + block.submittedMs; + redisTx.sadd('pendingBlocks', blockWithTs); + } + + redisTx.exec(function(error, _){ + if (error) assert.fail('Test failed: ' + error); + callback(restServer); + }); + } + + prepare(blocks, _ => { + shareProcessor.getPendingBlocks( + blocks.map(block => block.hash + ':' + block.submittedMs), + function(pendingBlocks){ + expect(pendingBlocks).to.deep.equal([ + { + fromGroup: 0, + hash: "block1", + height: 1, + pendingBlockValue: blocks[0].hash + ':' + blocks[0].submittedMs, + rewardAmount: "2000000000000000000", + toGroup: 0, + }, + { + fromGroup: 0, + hash: "block2", + height: 2, + pendingBlockValue: blocks[1].hash + ':' + blocks[1].submittedMs, + rewardAmount: "4000000000000000000", + toGroup: 0 + } + ]); + nock.cleanAll(); + done(); + } + ); + }); + }) + it('should remove orphan block and shares', function(done){ var shareProcessor = new ShareProcessor(test.config, test.logger); shareProcessor.redisClient = redisClient; @@ -137,6 +199,7 @@ describe('test share processor', function(){ {hash: 'block3', height: 3, chainFrom: 0, chainTo: 0, transactions: transactions, inMainChain: true, submittedMs: currentMs - confirmationTime}, {hash: 'block4', height: 4, chainFrom: 0, chainTo: 0, transactions: transactions, inMainChain: true, submittedMs: currentMs - confirmationTime}, ]; + var orphanBlock = blocks[1]; var shares = {}; for (var block of blocks){ @@ -146,10 +209,13 @@ describe('test share processor', function(){ function prepare(blocks, shares, callback){ var restServer = nock('http://127.0.0.1:12973'); var redisTx = redisClient.multi(); + restServer.persist() + .get('/blockflow/main-chain-block-by-ghost-uncle/' + orphanBlock.hash) + .reply(404, { detail: `Mainchain block by ghost uncle hash ${orphanBlock.hash} not found` }); for (var block of blocks){ - restServer.get('/blockflow/blocks/' + block.hash).reply(200, block); + restServer.persist().get('/blockflow/blocks/' + block.hash).reply(200, block); var path = '/blockflow/is-block-in-main-chain?blockHash=' + block.hash; - restServer.get(path).reply(200, block.inMainChain ? true : false); + restServer.persist().get(path).reply(200, block.inMainChain ? true : false); var blockWithTs = block.hash + ':' + block.submittedMs; redisTx.sadd('pendingBlocks', blockWithTs); @@ -171,7 +237,6 @@ describe('test share processor', function(){ } var checkState = function(){ - var orphanBlock = blocks[1]; var orphanBlockWithTs = orphanBlock.hash + ':' + orphanBlock.submittedMs; var roundKey = shareProcessor.roundKey(orphanBlock.chainFrom, orphanBlock.chainTo, orphanBlock.hash); @@ -193,11 +258,11 @@ describe('test share processor', function(){ var expected = [ { pendingBlockValue: blocks[2].hash + ':' + blocks[2].submittedMs, - data: blockData(blocks[2]) + ...blockData(blocks[2]) }, { pendingBlockValue: blocks[3].hash + ':' + blocks[3].submittedMs, - data: blockData(blocks[3]) + ...blockData(blocks[3]) } ];