Skip to content

Commit

Permalink
Merge pull request #63 from alephium/reward-ghost-uncle-block-miners
Browse files Browse the repository at this point in the history
Reward ghost uncle block miners
  • Loading branch information
Lbqds authored May 17, 2024
2 parents 5f1cc5c + 6a6e1b6 commit ace5917
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 21 deletions.
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

0 comments on commit ace5917

Please sign in to comment.