Skip to content
This repository has been archived by the owner on Jun 11, 2024. It is now read-only.

Implement BFT fork choice rules #3725

Merged
Merged
Show file tree
Hide file tree
Changes from 10 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
10 changes: 10 additions & 0 deletions framework/src/modules/chain/blocks/block_slots.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,16 @@ class BlockSlots {
return nextSlot + this.blocksPerRound;
}

/**
* Check if timestamp is contained within a slot time window
* @param slot
* @param time
* @returns {boolean}
*/
timeFallsInSlot(slot, time) {
2snEM6 marked this conversation as resolved.
Show resolved Hide resolved
return this.getSlotNumber(time) === slot;
}

/**
* Calculates round number from the given height.
*
Expand Down
287 changes: 266 additions & 21 deletions framework/src/modules/chain/blocks/blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
const EventEmitter = require('events');
const { cloneDeep } = require('lodash');
const blocksUtils = require('./utils');
const { convertErrorsToString } = require('../helpers/error_handlers');
2snEM6 marked this conversation as resolved.
Show resolved Hide resolved
const { BlocksProcess } = require('./process');
const { BlocksVerify } = require('./verify');
const { BlocksChain } = require('./chain');
const { BlockReward } = require('./block_reward');
const forkChoiceRule = require('./fork_choice_rule');

const EVENT_NEW_BLOCK = 'EVENT_NEW_BLOCK';
const EVENT_DELETE_BLOCK = 'EVENT_DELETE_BLOCK';
Expand Down Expand Up @@ -302,6 +304,26 @@ class Blocks extends EventEmitter {

// Process a block from the P2P
async receiveBlockFromNetwork(block) {
this.logger.info(
2snEM6 marked this conversation as resolved.
Show resolved Hide resolved
`Received new block from network with id: ${block.id} height: ${
block.height
} round: ${this.slots.calcRound(
block.height
)} slot: ${this.slots.getSlotNumber(block.timestamp)} reward: ${
block.reward
} version: ${block.version}`
);

const receiveBlockImplementations = {
1: this.receiveBlockFromNetworkV1,
2snEM6 marked this conversation as resolved.
Show resolved Hide resolved
2: this.receiveBlockFromNetworkV2,
};
2snEM6 marked this conversation as resolved.
Show resolved Hide resolved

// TODO: Use block_version.js#getBlockVersion when https://github.com/LiskHQ/lisk-sdk/pull/3722 is merged
return receiveBlockImplementations[block.version](block);
}

async receiveBlockFromNetworkV1(block) {
return this.sequence.add(async cb => {
try {
this._shouldNotBeActive();
Expand All @@ -312,22 +334,13 @@ class Blocks extends EventEmitter {
this._isActive = true;
// set active to true
if (this.blocksVerify.isSaneBlock(block, this._lastBlock)) {
this._updateLastReceipt();
try {
const newBlock = await this.blocksProcess.processBlock(
block,
this._lastBlock,
validBlock => this.broadcast(validBlock)
);
await this._updateBroadhash();
this._lastBlock = newBlock;
this._isActive = false;
setImmediate(cb);
await this._processReceivedBlock(block);
} catch (error) {
this._isActive = false;
this.logger.error(error);
setImmediate(cb, error);
return;
}
setImmediate(cb);
2snEM6 marked this conversation as resolved.
Show resolved Hide resolved
return;
}
if (this.blocksVerify.isForkOne(block, this._lastBlock)) {
Expand Down Expand Up @@ -391,7 +404,6 @@ class Blocks extends EventEmitter {
this._isActive = false;
return;
}
this._updateLastReceipt();
try {
const {
verified,
Expand All @@ -412,14 +424,9 @@ class Blocks extends EventEmitter {
block: deletingBlock,
newLastBlock: cloneDeep(this._lastBlock),
});
// emit event
this._lastBlock = await this.blocksProcess.processBlock(
block,
this._lastBlock,
validBlock => this.broadcast(validBlock)
);
await this._updateBroadhash();
this._isActive = false;

await this._processReceivedBlock(block);

setImmediate(cb);
return;
} catch (error) {
Expand Down Expand Up @@ -449,6 +456,120 @@ class Blocks extends EventEmitter {
});
}

async receiveBlockFromNetworkV2(block) {
// Current time since Lisk Epoch
// Better to do it here rather than in the Sequence so receiving time is more accurate
const newBlockReceivedAt = this.slots.getTime();

// Execute in sequence via sequence
2snEM6 marked this conversation as resolved.
Show resolved Hide resolved
return this.sequence.add(callback => {
try {
this._shouldNotBeActive();
} catch (error) {
callback(error);
return;
}
this._isActive = true;
this._forkChoiceTask(block, newBlockReceivedAt)
.then(result => callback(null, result))
.catch(error => callback(error))
.finally(() => {
this._isActive = false;
});
});
}

// PRE: Block has been validated and verified before
async _processReceivedBlock(block) {
this._updateLastReceipt();
try {
const newBlock = await this.blocksProcess.processBlock(
block,
this._lastBlock,
validBlock => this.broadcast(validBlock)
);

this.logger.info(
`Successfully applied new received block id: ${block.id} height: ${
block.height
} round: ${this.slots.calcRound(
block.height
)} slot: ${this.slots.getSlotNumber(block.timestamp)} reward: ${
block.reward
} version: ${block.version}`
);

await this._updateBroadhash();
this._lastBlock = newBlock;
this._isActive = false;
} catch (error) {
this.logger.error(
`Failed to apply new received block id: ${block.id} height: ${
block.height
} round: ${this.slots.calcRound(
block.height
)} slot: ${this.slots.getSlotNumber(block.timestamp)} reward: ${
block.reward
} version: ${block.version} with error: ${error}`
);
this._isActive = false;
throw error;
}
2snEM6 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Wrap of fork choice rule logic so it can be added to Sequence and properly tested
* @param block
* @param newBlockReceivedAt - Time when the new block was received since Lisk Epoch
* @return {Promise}
* @private
*/
async _forkChoiceTask(block, newBlockReceivedAt) {
// Cases are numbered following LIP-0014 Fork choice rule.
// See: https://github.com/LiskHQ/lips/blob/master/proposals/lip-0014.md#applying-blocks-according-to-fork-choice-rule
// Case 2 and 1 have flipped execution order for better readability. Behavior is still the same

if (forkChoiceRule.isValidBlock(this._lastBlock, block)) {
// Case 2: correct block received
return this._handleGoodBlock(block);
2snEM6 marked this conversation as resolved.
Show resolved Hide resolved
}

if (forkChoiceRule.isIdenticalBlock(this._lastBlock, block)) {
// Case 1: same block received twice
return this._handleSameBlockReceived(block);
}

if (forkChoiceRule.isDoubleForging(this._lastBlock, block)) {
// Delegates are the same
// Case 3: double forging different blocks in the same slot.
// Last Block stands.
return this._handleDoubleForging(block, this._lastBlock);
}

if (
forkChoiceRule.isTieBreak({
slots: this.slots,
lastBlock: this._lastBlock,
currentBlock: block,
lastReceivedAt: this._lastReceipt || this._lastBlock.timestamp,
currentReceivedAt: newBlockReceivedAt,
})
) {
// Two competing blocks by different delegates at the same height.
// Case 4: Tie break
return this._handleDoubleForgingTieBreak(block, this._lastBlock);
}

if (forkChoiceRule.isDifferentChain(this._lastBlock, block)) {
// Case 5: received block has priority. Move to a different chain.
// TODO: Move to a different chain
return this._handleMovingToDifferentChain();
}

// Discard newly received block
return this._handleDiscardedBlock(block);
}

// Process a block from syncing
async loadBlocksFromNetwork(blocks) {
this._shouldNotBeActive();
Expand Down Expand Up @@ -584,6 +705,130 @@ class Blocks extends EventEmitter {
}
);
}

/**
* Block IDs are the same ~ Blocks are equal
* @param block
* @returns {*}
* @private
*/
// eslint-disable-next-line class-methods-use-this
_handleSameBlockReceived(block) {
this.logger.debug('Block already processed', block.id);
2snEM6 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Block received is correct
* @param block
* @returns {Promise}
* @private
*/
_handleGoodBlock(block) {
2snEM6 marked this conversation as resolved.
Show resolved Hide resolved
return this._processReceivedBlock(block);
}

/**
* Double forging. Last block stands
* @param block
* @param lastBlock
* @returns {*}
* @private
*/
// eslint-disable-next-line class-methods-use-this
_handleDoubleForging(block, lastBlock) {
this.logger.warn(
'Delegate forging on multiple nodes',
block.generatorPublicKey
);
this.logger.info(
`Last block ${lastBlock.id} stands, new block ${block.id} is discarded`
);
// TODO: Implement Proof of Misbehavior
}

/**
* Tie break: two competing blocks by different delegates at the same height.
* @param lastBlock
* @param newBlock
* @returns {Promise}
* @private
*/
async _handleDoubleForgingTieBreak(newBlock, lastBlock) {
const block = cloneDeep(newBlock);
// It mutates the argument
const check = await this.blocksVerify.normalizeAndVerify(
block,
lastBlock,
this._lastNBlockIds
);

if (!check.verified) {
const errors = convertErrorsToString(check.errors);

this.logger.error(
2snEM6 marked this conversation as resolved.
Show resolved Hide resolved
`Fork Choice Case 4 recovery failed because block ${
block.id
} verification and normalization failed`,
errors
);
// Return first error from checks
throw errors;
}

// If the new block is correctly validated and verified,
// last block is deleted and new block is added to the tip of the chain
this.logger.info(
`Deleting last block with id: ${
lastBlock.id
} due to Fork Choice Rule Case 4`
);
const previousLastBlock = cloneDeep(this._lastBlock);

// Deletes last block and assigns new received block to this._lastBlock
await this.recoverChain();
shuse2 marked this conversation as resolved.
Show resolved Hide resolved

try {
await this._processReceivedBlock(block);
} catch (error) {
this.logger.warn(
`Failed to apply newly received block with id: ${
block.id
}, restoring previous block ${previousLastBlock.id}`
);

await this._processReceivedBlock(previousLastBlock);
}
}

/**
* Move to a different chain
* @private
*/
// eslint-disable-next-line class-methods-use-this
_handleMovingToDifferentChain() {
// TODO: Move to a different chain.
// Determine which method to use to move to a different chain: Block Sync Mechanism or Fast Chain Switching Mechanism
}

/**
* Handle discarded block determined by the fork choice rule
* @param block
* @return {*}
* @private
*/
// eslint-disable-next-line class-methods-use-this
_handleDiscardedBlock(block) {
// Discard newly received block
return this.logger.warn(
2snEM6 marked this conversation as resolved.
Show resolved Hide resolved
`Discarded block that does not match with current chain: ${
block.id
} height: ${block.height} round: ${this.slots.calcRound(
block.height
)} slot: ${this.slots.getSlotNumber(block.timestamp)} generator: ${
block.generatorPublicKey
}`
);
}
}

module.exports = {
Expand Down
Loading