lisk-framework
Version:
Lisk blockchain application platform
273 lines • 14 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BlockSynchronizationMechanism = void 0;
const lisk_utils_1 = require("@liskhq/lisk-utils");
const base_synchronizer_1 = require("./base_synchronizer");
const utils_1 = require("./utils");
const errors_1 = require("./errors");
const fork_choice_rule_1 = require("../fork_choice/fork_choice_rule");
const groupByPeer = (peers) => {
const groupedPeers = new lisk_utils_1.dataStructures.BufferMap();
for (const peer of peers) {
let grouped = groupedPeers.get(peer.options.lastBlockID);
if (grouped === undefined) {
grouped = [];
}
grouped.push(peer);
groupedPeers.set(peer.options.lastBlockID, grouped);
}
return groupedPeers;
};
class BlockSynchronizationMechanism extends base_synchronizer_1.BaseSynchronizer {
constructor({ logger, chain, blockExecutor, network: networkModule, }) {
super(logger, chain, networkModule);
this._chain = chain;
this.blockExecutor = blockExecutor;
}
async run(receivedBlock) {
const bestPeer = this._computeBestPeer();
await this._requestAndValidateLastBlock(bestPeer.peerId);
const lastCommonBlock = await this._revertToLastCommonBlock(bestPeer.peerId);
await this._requestAndApplyBlocksToCurrentChain(receivedBlock, lastCommonBlock, bestPeer.peerId);
}
async isValidFor() {
const finalizedBlock = await this._chain.dataAccess.getBlockHeaderByHeight(this.blockExecutor.getFinalizedHeight());
const validators = await this.blockExecutor.getCurrentValidators();
const finalizedBlockSlot = this.blockExecutor.getSlotNumber(finalizedBlock.timestamp);
const currentBlockSlot = this.blockExecutor.getSlotNumber(Math.floor(Date.now() / 1000));
const threeRounds = validators.length * 3;
return currentBlockSlot - finalizedBlockSlot > threeRounds;
}
async _requestAndApplyBlocksWithinIDs(peerId, fromId, toId) {
const maxFailedAttempts = 10;
let failedAttempts = 0;
let lastFetchedID = fromId;
let finished = false;
while (!finished && failedAttempts < maxFailedAttempts) {
let blocks = [];
try {
blocks = await this._getBlocksFromNetwork(peerId, lastFetchedID);
}
catch (error) {
failedAttempts += 1;
continue;
}
blocks.sort((a, b) => a.header.height - b.header.height);
[
{
header: { id: lastFetchedID },
},
] = blocks.slice(-1);
const index = blocks.findIndex(block => block.header.id.equals(toId));
if (index > -1) {
blocks.splice(index + 1);
}
this._logger.debug({
fromId: blocks[0].header.id,
toId: blocks[blocks.length - 1].header.id,
}, 'Applying obtained blocks from peer');
try {
for (const block of blocks) {
this.blockExecutor.validate(block);
}
}
catch (err) {
this._logger.error({ err: err }, 'Block validation failed');
throw new errors_1.BlockProcessingError();
}
try {
for (const block of blocks) {
if (this._stop) {
return;
}
await this.blockExecutor.verify(block);
await this.blockExecutor.executeValidated(block, { skipBroadcast: true });
}
}
catch (err) {
this._logger.error({ err: err }, 'Block processing failed');
throw new errors_1.BlockProcessingError();
}
finished = this._chain.lastBlock.header.id.equals(toId);
}
if (failedAttempts === maxFailedAttempts) {
throw new errors_1.ApplyPenaltyAndRestartError(peerId, "Peer didn't return any block after requesting blocks");
}
}
async _handleBlockProcessingError(lastCommonBlock, peerId) {
this._logger.debug('Failed to apply obtained blocks from peer');
const tempBlocks = await this._chain.dataAccess.getTempBlocks();
const [tipBeforeApplying] = [...tempBlocks].sort((a, b) => b.header.height - a.header.height);
if (!tipBeforeApplying) {
this._logger.error('Blocks temp table should not be empty');
throw new errors_1.RestartError('Blocks temp table should not be empty');
}
const newTipHasPreference = (0, fork_choice_rule_1.isDifferentChain)(tipBeforeApplying.header, this._chain.lastBlock.header);
if (!newTipHasPreference) {
this._logger.debug({
currentTip: this._chain.lastBlock.header.id,
previousTip: tipBeforeApplying.header.id,
}, 'Previous tip of the chain has preference over current tip. Restoring chain from temp table');
try {
this._logger.debug({ height: lastCommonBlock.height }, 'Deleting blocks after height');
await (0, utils_1.deleteBlocksAfterHeight)(this.blockExecutor, this._chain, this._logger, lastCommonBlock.height);
this._logger.debug('Restoring blocks from temporary table');
await (0, utils_1.restoreBlocks)(this._chain, this.blockExecutor);
this._logger.debug('Cleaning blocks temp table');
await (0, utils_1.clearBlocksTempTable)(this._chain);
}
catch (error) {
this._logger.error({ err: error }, 'Failed to restore blocks from blocks temp table');
}
throw new errors_1.ApplyPenaltyAndRestartError(peerId, 'New tip of the chain has no preference over the previous tip before synchronizing');
}
this._logger.debug({
currentTip: this._chain.lastBlock.header.id,
previousTip: tipBeforeApplying.header.id,
}, 'Current tip of the chain has preference over previous tip');
this._logger.debug('Cleaning blocks temporary table');
await (0, utils_1.clearBlocksTempTable)(this._chain);
this._logger.info('Restarting block synchronization');
throw new errors_1.RestartError('The list of blocks has not been fully applied. Trying again');
}
async _requestAndApplyBlocksToCurrentChain(receivedBlock, lastCommonBlock, peerId) {
this._logger.debug({
peerId,
from: {
blockId: lastCommonBlock.id,
height: lastCommonBlock.height,
},
to: {
blockId: receivedBlock.header.id,
height: receivedBlock.header.height,
},
}, 'Requesting blocks within ID range from peer');
try {
await this._requestAndApplyBlocksWithinIDs(peerId, lastCommonBlock.id, receivedBlock.header.id);
}
catch (err) {
if (!(err instanceof errors_1.BlockProcessingError)) {
throw err;
}
await this._handleBlockProcessingError(lastCommonBlock, peerId);
}
this._logger.debug('Cleaning up blocks temporary table');
await (0, utils_1.clearBlocksTempTable)(this._chain);
this._logger.debug({ peerId }, 'Successfully requested and applied blocks from peer');
return true;
}
async _revertToLastCommonBlock(peerId) {
this._logger.debug({ peerId }, 'Reverting chain to the last common block with peer');
this._logger.debug({ peerId }, 'Requesting the last common block from peer');
const lastCommonBlock = await this._requestLastCommonBlock(peerId);
if (!lastCommonBlock) {
throw new errors_1.ApplyPenaltyAndRestartError(peerId, 'No common block has been found between the chain and the targeted peer');
}
this._logger.debug({
blockId: lastCommonBlock.id,
height: lastCommonBlock.height,
}, 'Found common block');
if (lastCommonBlock.height < this.blockExecutor.getFinalizedHeight()) {
throw new errors_1.ApplyPenaltyAndRestartError(peerId, 'The last common block height is less than the finalized height of the current chain');
}
this._logger.debug({
blockId: lastCommonBlock.id,
height: lastCommonBlock.height,
}, 'Deleting blocks after common block');
await (0, utils_1.deleteBlocksAfterHeight)(this.blockExecutor, this._chain, this._logger, lastCommonBlock.height, true);
this._logger.debug({ lastBlockID: this._chain.lastBlock.header.id }, 'Successfully deleted blocks');
return lastCommonBlock;
}
async _requestLastCommonBlock(peerId) {
const blocksPerRequestLimit = 10;
const requestLimit = 3;
let numberOfRequests = 1;
let highestCommonBlock;
const validators = await this.blockExecutor.getCurrentValidators();
let currentRound = Math.ceil(this._chain.lastBlock.header.height / validators.length);
let currentHeight = currentRound * validators.length;
while (!highestCommonBlock &&
numberOfRequests < requestLimit &&
currentHeight >= this.blockExecutor.getFinalizedHeight()) {
const heightList = (0, utils_1.computeBlockHeightsList)(this.blockExecutor.getFinalizedHeight(), validators.length, blocksPerRequestLimit, currentRound);
const blockHeaders = await this._chain.dataAccess.getBlockHeadersWithHeights(heightList);
let data;
try {
data = await this._getHighestCommonBlockFromNetwork(peerId, blockHeaders.map(block => block.id));
}
catch (e) {
numberOfRequests += 1;
continue;
}
highestCommonBlock = data;
currentRound -= blocksPerRequestLimit;
currentHeight = currentRound * validators.length;
}
return highestCommonBlock;
}
async _requestAndValidateLastBlock(peerId) {
this._logger.debug({ peerId }, 'Requesting tip of the chain from peer');
const networkLastBlock = await this._getLastBlockFromNetwork(peerId);
this._logger.debug({ peerId, blockId: networkLastBlock.header.id }, 'Received tip of the chain from peer');
const { valid: validBlock } = this._blockDetachedStatus(networkLastBlock);
const inDifferentChain = (0, fork_choice_rule_1.isDifferentChain)(this._chain.lastBlock.header, networkLastBlock.header) ||
networkLastBlock.header.id.equals(this._chain.lastBlock.header.id);
if (!validBlock || !inDifferentChain) {
throw new errors_1.ApplyPenaltyAndRestartError(peerId, 'The tip of the chain of the peer is not valid or is not in a different chain');
}
}
_blockDetachedStatus(networkLastBlock) {
try {
this.blockExecutor.validate(networkLastBlock);
return { valid: true, err: null };
}
catch (err) {
return { valid: false, err: err };
}
}
_computeBestPeer() {
const peers = this._network.getConnectedPeers();
if (!peers.length) {
throw new Error('List of connected peers is empty');
}
this._logger.trace({ peers: peers.map(peer => peer.peerId) }, 'List of connected peers');
const requiredProps = ['blockVersion', 'maxHeightPrevoted', 'height'];
const compatiblePeers = peers.filter(p => requiredProps.every(prop => Object.keys(p.options).includes(prop)));
if (!compatiblePeers.length) {
throw new Error('Connected compatible peers list is empty');
}
this._logger.trace({ peers: compatiblePeers.map(peer => peer.peerId) }, 'List of compatible peers connected peers');
this._logger.debug('Computing the best peer to synchronize from');
const largestSubsetBymaxHeightPrevoted = (0, utils_1.computeLargestSubsetMaxBy)(compatiblePeers, peer => peer.options.maxHeightPrevoted);
const largestSubsetByHeight = (0, utils_1.computeLargestSubsetMaxBy)(largestSubsetBymaxHeightPrevoted, peer => peer.options.height);
const peersGroupedByBlockId = groupByPeer(largestSubsetByHeight);
const blockIds = peersGroupedByBlockId.entries();
let maxNumberOfPeersInSet = 0;
let selectedPeers = [];
let selectedBlockId = blockIds[0][0];
for (const [blockId, peersByBlockId] of blockIds) {
const numberOfPeersInSet = peersByBlockId.length;
if (numberOfPeersInSet > maxNumberOfPeersInSet ||
(numberOfPeersInSet === maxNumberOfPeersInSet && selectedBlockId.compare(blockId) > 0)) {
maxNumberOfPeersInSet = numberOfPeersInSet;
selectedPeers = peersByBlockId;
selectedBlockId = blockId;
}
}
const randomPeerIndex = Math.floor(Math.random() * selectedPeers.length);
const peersTip = {
height: selectedPeers[randomPeerIndex].options.height,
version: selectedPeers[randomPeerIndex].options.blockVersion,
maxHeightPrevoted: selectedPeers[randomPeerIndex].options.maxHeightPrevoted,
};
const tipHasPreference = (0, fork_choice_rule_1.isDifferentChain)(this._chain.lastBlock.header, peersTip);
if (!tipHasPreference) {
throw new errors_1.AbortError('Peer tip does not have preference over current tip.');
}
const bestPeer = selectedPeers[Math.floor(Math.random() * selectedPeers.length)];
this._logger.debug({ peer: bestPeer }, 'Successfully computed the best peer');
return bestPeer;
}
}
exports.BlockSynchronizationMechanism = BlockSynchronizationMechanism;
//# sourceMappingURL=block_synchronization_mechanism.js.map