Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reward ghost uncle block miners #63

Merged
merged 3 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/httpClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
84 changes: 73 additions & 11 deletions lib/shareProcessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

Expand All @@ -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){
Expand All @@ -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;
}

Expand All @@ -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();
Expand Down Expand Up @@ -215,23 +278,22 @@ 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);
callback();
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();
});
}
Expand Down
85 changes: 75 additions & 10 deletions test/shareProcessorTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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;
Expand All @@ -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){
Expand All @@ -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);
Expand All @@ -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);

Expand All @@ -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])
}
];

Expand Down
Loading