Skip to content

Commit ace5917

Browse files
authored
Merge pull request #63 from alephium/reward-ghost-uncle-block-miners
Reward ghost uncle block miners
2 parents 5f1cc5c + 6a6e1b6 commit ace5917

File tree

3 files changed

+153
-21
lines changed

3 files changed

+153
-21
lines changed

lib/httpClient.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,11 @@ var HttpClient = module.exports = function HttpClient(host, port, apiKey){
136136
this.get(path, callback);
137137
}
138138

139+
this.getMainChainBlockByGhostUncle = function(ghostUncleHash, callback) {
140+
var path = '/blockflow/main-chain-block-by-ghost-uncle/' + ghostUncleHash
141+
this.get(path, callback)
142+
}
143+
139144
this.blockHashesAtHeight = function(height, fromGroup, toGroup, callback){
140145
var path = '/blockflow/hashes?fromGroup=' + fromGroup + '&toGroup=' + toGroup + '&height=' + height;
141146
this.get(path, callback);

lib/shareProcessor.js

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ const HttpClient = require('./httpClient');
33
const util = require('./util');
44
const { Pool } = require('pg');
55

6+
const MaxGhostUncleAge = 7;
7+
68
var ShareProcessor = module.exports = function ShareProcessor(config, logger){
79
var confirmationTime = config.confirmationTime * 1000;
810
var rewardPercent = 1 - config.withholdPercent;
@@ -128,6 +130,7 @@ var ShareProcessor = module.exports = function ShareProcessor(config, logger){
128130
height: block.height,
129131
rewardAmount: rewardOutput.attoAlphAmount // string
130132
};
133+
logger.debug('Main chain block: ' + JSON.stringify(blockData));
131134
callback(blockData);
132135
}
133136

@@ -143,6 +146,59 @@ var ShareProcessor = module.exports = function ShareProcessor(config, logger){
143146
})
144147
}
145148

149+
function getUncleReward(ghostUncleHash, ghostUncleHashWithTs, callback) {
150+
_this.httpClient.getMainChainBlockByGhostUncle(ghostUncleHash, function (block) {
151+
if (block.error) {
152+
var errorMsg = `${block.error}`
153+
if (errorMsg.includes(`Mainchain block by ghost uncle hash ${ghostUncleHash} not found`)) {
154+
logger.warn(`Block ${ghostUncleHash} is not a ghost uncle block`);
155+
var [fromGroup, toGroup] = util.blockChainIndex(Buffer.from(ghostUncleHash, 'hex'));
156+
removeBlockAndShares(fromGroup, toGroup, ghostUncleHash, ghostUncleHashWithTs);
157+
} else {
158+
logger.error('Get main chain block error: ' + block.error + ', ghost uncle hash: ' + ghostUncleHash);
159+
}
160+
callback(null);
161+
return;
162+
}
163+
164+
var transactions = block.transactions;
165+
var coinbaseTx = transactions[transactions.length - 1];
166+
var index = block.ghostUncles.findIndex((u) => u.blockHash === ghostUncleHash)
167+
var rewardOutput = coinbaseTx.unsigned.fixedOutputs[index + 1];
168+
var rewardAmount = rewardOutput.attoAlphAmount;
169+
logger.info('Found main chain block ' + block.hash + ', uncle reward: ' + rewardAmount);
170+
callback(rewardAmount);
171+
})
172+
}
173+
174+
function tryHandleUncleBlock(ghostUncleHash, ghostUncleHashWithTs, callback) {
175+
logger.info('Try handling uncle block: ' + ghostUncleHash)
176+
_this.httpClient.getBlock(ghostUncleHash, function(ghostUncleBlock){
177+
if (ghostUncleBlock.error){
178+
logger.error('Get uncle block error: ' + ghostUncleBlock.error + ', hash: ' + ghostUncleHash);
179+
callback(null);
180+
return;
181+
}
182+
183+
getUncleReward(ghostUncleHash, ghostUncleHashWithTs, function (uncleReward) {
184+
if (uncleReward) {
185+
var blockData = {
186+
hash: ghostUncleHash,
187+
fromGroup: ghostUncleBlock.chainFrom,
188+
toGroup: ghostUncleBlock.chainTo,
189+
height: ghostUncleBlock.height,
190+
rewardAmount: uncleReward // string
191+
};
192+
logger.debug('Ghost uncle block: ' + JSON.stringify(blockData));
193+
callback(blockData);
194+
return;
195+
}
196+
callback(null);
197+
return;
198+
})
199+
});
200+
}
201+
146202
this.getPendingBlocks = function(results, callback){
147203
var blocksNeedToReward = [];
148204
util.executeForEach(results, function(blockHashWithTs, callback){
@@ -166,10 +222,17 @@ var ShareProcessor = module.exports = function ShareProcessor(config, logger){
166222
}
167223

168224
if (!result){
169-
logger.error('Block is not in mainchain, remove block and shares, hash: ' + blockHash);
170-
var [fromGroup, toGroup] = util.blockChainIndex(Buffer.from(blockHash, 'hex'));
171-
removeBlockAndShares(fromGroup, toGroup, blockHash, blockHashWithTs);
172-
callback();
225+
tryHandleUncleBlock(blockHash, blockHashWithTs, function (uncleBlockData) {
226+
if (uncleBlockData) {
227+
var block = {
228+
pendingBlockValue: blockHashWithTs,
229+
...uncleBlockData
230+
};
231+
blocksNeedToReward.push(block);
232+
}
233+
callback();
234+
return;
235+
});
173236
return;
174237
}
175238

@@ -183,7 +246,7 @@ var ShareProcessor = module.exports = function ShareProcessor(config, logger){
183246
handleBlock(result, function(blockData){
184247
var block = {
185248
pendingBlockValue: blockHashWithTs,
186-
data: blockData
249+
...blockData
187250
};
188251
blocksNeedToReward.push(block);
189252
callback();
@@ -215,23 +278,22 @@ var ShareProcessor = module.exports = function ShareProcessor(config, logger){
215278
}
216279

217280
function allocateRewardForBlock(block, redisTx, workerRewards, callback){
218-
var blockData = block.data;
219-
var round = _this.roundKey(blockData.fromGroup, blockData.toGroup, blockData.hash);
281+
var round = _this.roundKey(block.fromGroup, block.toGroup, block.hash);
220282
_this.redisClient.hgetall(round, function(error, shares){
221283
if (error) {
222284
logger.error('Get shares failed, error: ' + error + ', round: ' + round);
223285
callback();
224286
return;
225287
}
226288

227-
var totalReward = Math.floor(parseInt(blockData.rewardAmount) * rewardPercent);
228-
logger.info('Reward miners for block: ' + blockData.hash + ', total reward: ' + totalReward);
229-
logger.debug('Block hash: ' + blockData.hash + ', shares: ' + JSON.stringify(shares));
289+
var totalReward = Math.floor(parseInt(block.rewardAmount) * rewardPercent);
290+
logger.info('Reward miners for block: ' + block.hash + ', total reward: ' + totalReward);
291+
logger.debug('Block hash: ' + block.hash + ', shares: ' + JSON.stringify(shares));
230292
_this.allocateReward(totalReward, workerRewards, shares);
231293

232294
redisTx.del(round);
233295
redisTx.srem(pendingBlocksKey, block.pendingBlockValue);
234-
logger.info('Remove shares for block: ' + blockData.hash);
296+
logger.info('Remove shares for block: ' + block.hash);
235297
callback();
236298
});
237299
}

test/shareProcessorTest.js

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,7 @@ describe('test share processor', function(){
8484
shareProcessor.redisClient = redisClient;
8585

8686
var shares = {'miner0': '4', 'miner1': '2', 'miner2': '2'};
87-
var blockData = {hash: '0011', fromGroup: 0, toGroup: 1, height: 1, rewardAmount: '40000000000000000000'};
88-
var block = {pendingBlockValue: blockData.hash + ':' + '0', data: blockData};
87+
var block = {pendingBlockValue: '0011' + ':' + '0', hash: '0011', fromGroup: 0, toGroup: 1, height: 1, rewardAmount: '40000000000000000000'};
8988

9089
var checkState = function(){
9190
redisClient
@@ -106,9 +105,9 @@ describe('test share processor', function(){
106105
}
107106

108107
var roundKey = shareProcessor.roundKey(
109-
blockData.fromGroup,
110-
blockData.toGroup,
111-
blockData.hash
108+
block.fromGroup,
109+
block.toGroup,
110+
block.hash
112111
);
113112

114113
redisClient
@@ -123,6 +122,69 @@ describe('test share processor', function(){
123122
});
124123
})
125124

125+
it('should reward uncle miners with correct reward amount', function(done){
126+
var config = { ...test.config, confirmationTime: 0 }
127+
var shareProcessor = new ShareProcessor(config, test.logger);
128+
shareProcessor.redisClient = redisClient;
129+
130+
var currentMs = Date.now();
131+
var rewardAmount = '4000000000000000000';
132+
var ghostUncleRewardAmount = '2000000000000000000';
133+
var ghostUncleCoinbaseTx = [{unsigned:{fixedOutputs:[{attoAlphAmount: rewardAmount}]}}];
134+
var ghostUncleBlock = {hash: 'block1', height: 1, chainFrom: 0, chainTo: 0, transactions: ghostUncleCoinbaseTx, inMainChain: false, submittedMs: currentMs, ghostUncles: []}
135+
136+
var mainChainCoinbaseTx = [{unsigned:{fixedOutputs:[{attoAlphAmount: rewardAmount},{attoAlphAmount: ghostUncleRewardAmount}]}}];
137+
var mainChainBlock = {hash: 'block2', height: 2, chainFrom: 0, chainTo: 0, transactions: mainChainCoinbaseTx, inMainChain: true, submittedMs: currentMs, ghostUncles: [{blockHash:ghostUncleBlock.hash}]}
138+
var blocks = [ghostUncleBlock, mainChainBlock]
139+
140+
function prepare(blocks, callback){
141+
var restServer = nock('http://127.0.0.1:12973');
142+
var redisTx = redisClient.multi();
143+
restServer.persist().get('/blockflow/main-chain-block-by-ghost-uncle/' + ghostUncleBlock.hash).reply(200, mainChainBlock)
144+
for (var block of blocks){
145+
restServer.persist().get('/blockflow/blocks/' + block.hash).reply(200, block);
146+
var isInMainChainPath = '/blockflow/is-block-in-main-chain?blockHash=' + block.hash;
147+
restServer.persist().get(isInMainChainPath).reply(200, block.inMainChain ? true : false);
148+
149+
var blockWithTs = block.hash + ':' + block.submittedMs;
150+
redisTx.sadd('pendingBlocks', blockWithTs);
151+
}
152+
153+
redisTx.exec(function(error, _){
154+
if (error) assert.fail('Test failed: ' + error);
155+
callback(restServer);
156+
});
157+
}
158+
159+
prepare(blocks, _ => {
160+
shareProcessor.getPendingBlocks(
161+
blocks.map(block => block.hash + ':' + block.submittedMs),
162+
function(pendingBlocks){
163+
expect(pendingBlocks).to.deep.equal([
164+
{
165+
fromGroup: 0,
166+
hash: "block1",
167+
height: 1,
168+
pendingBlockValue: blocks[0].hash + ':' + blocks[0].submittedMs,
169+
rewardAmount: "2000000000000000000",
170+
toGroup: 0,
171+
},
172+
{
173+
fromGroup: 0,
174+
hash: "block2",
175+
height: 2,
176+
pendingBlockValue: blocks[1].hash + ':' + blocks[1].submittedMs,
177+
rewardAmount: "4000000000000000000",
178+
toGroup: 0
179+
}
180+
]);
181+
nock.cleanAll();
182+
done();
183+
}
184+
);
185+
});
186+
})
187+
126188
it('should remove orphan block and shares', function(done){
127189
var shareProcessor = new ShareProcessor(test.config, test.logger);
128190
shareProcessor.redisClient = redisClient;
@@ -137,6 +199,7 @@ describe('test share processor', function(){
137199
{hash: 'block3', height: 3, chainFrom: 0, chainTo: 0, transactions: transactions, inMainChain: true, submittedMs: currentMs - confirmationTime},
138200
{hash: 'block4', height: 4, chainFrom: 0, chainTo: 0, transactions: transactions, inMainChain: true, submittedMs: currentMs - confirmationTime},
139201
];
202+
var orphanBlock = blocks[1];
140203

141204
var shares = {};
142205
for (var block of blocks){
@@ -146,10 +209,13 @@ describe('test share processor', function(){
146209
function prepare(blocks, shares, callback){
147210
var restServer = nock('http://127.0.0.1:12973');
148211
var redisTx = redisClient.multi();
212+
restServer.persist()
213+
.get('/blockflow/main-chain-block-by-ghost-uncle/' + orphanBlock.hash)
214+
.reply(404, { detail: `Mainchain block by ghost uncle hash ${orphanBlock.hash} not found` });
149215
for (var block of blocks){
150-
restServer.get('/blockflow/blocks/' + block.hash).reply(200, block);
216+
restServer.persist().get('/blockflow/blocks/' + block.hash).reply(200, block);
151217
var path = '/blockflow/is-block-in-main-chain?blockHash=' + block.hash;
152-
restServer.get(path).reply(200, block.inMainChain ? true : false);
218+
restServer.persist().get(path).reply(200, block.inMainChain ? true : false);
153219

154220
var blockWithTs = block.hash + ':' + block.submittedMs;
155221
redisTx.sadd('pendingBlocks', blockWithTs);
@@ -171,7 +237,6 @@ describe('test share processor', function(){
171237
}
172238

173239
var checkState = function(){
174-
var orphanBlock = blocks[1];
175240
var orphanBlockWithTs = orphanBlock.hash + ':' + orphanBlock.submittedMs;
176241
var roundKey = shareProcessor.roundKey(orphanBlock.chainFrom, orphanBlock.chainTo, orphanBlock.hash);
177242

@@ -193,11 +258,11 @@ describe('test share processor', function(){
193258
var expected = [
194259
{
195260
pendingBlockValue: blocks[2].hash + ':' + blocks[2].submittedMs,
196-
data: blockData(blocks[2])
261+
...blockData(blocks[2])
197262
},
198263
{
199264
pendingBlockValue: blocks[3].hash + ':' + blocks[3].submittedMs,
200-
data: blockData(blocks[3])
265+
...blockData(blocks[3])
201266
}
202267
];
203268

0 commit comments

Comments
 (0)