leasehold-chain
Version:
Leasehold sidechain
598 lines (565 loc) • 15.9 kB
JavaScript
/*
* Copyright © 2019 Lisk Foundation
*
* See the LICENSE file at the top-level directory of this distribution
* for licensing information.
*
* Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation,
* no part of this software, including this file, may be copied, modified,
* propagated, or distributed except according to the terms contained in the
* LICENSE file.
*
* Removal or modification of this copyright notice is prohibited.
*/
'use strict';
const EventEmitter = require('events');
const { cloneDeep } = require('lodash');
const blocksUtils = require('./utils');
const { BlocksProcess } = require('./process');
const { BlocksVerify } = require('./verify');
const { BlocksChain } = require('./chain');
const {
calculateSupply,
calculateReward,
calculateMilestone,
} = require('./block_reward');
const EVENT_NEW_BLOCK = 'EVENT_NEW_BLOCK';
const EVENT_DELETE_BLOCK = 'EVENT_DELETE_BLOCK';
const EVENT_BROADCAST_BLOCK = 'EVENT_BROADCAST_BLOCK';
const EVENT_NEW_BROADHASH = 'EVENT_NEW_BROADHASH';
class Blocks extends EventEmitter {
constructor({
// components
logger,
storage,
sequence,
// Unique requirements
genesisBlock,
slots,
exceptions,
// Modules
roundsModule,
interfaceAdapters,
// constants
blockReceiptTimeout, // set default
loadPerIteration,
maxPayloadLength,
maxTransactionsPerBlock,
activeDelegates,
rewardDistance,
rewardOffset,
rewardMileStones,
totalAmount,
blockSlotWindow,
}) {
super();
this._broadhash = genesisBlock.payloadHash;
this._lastNBlockIds = [];
this._lastBlock = {};
this._isActive = false;
this._lastReceipt = null;
this._cleaning = false;
this.logger = logger;
this.storage = storage;
this.roundsModule = roundsModule;
this.exceptions = exceptions;
this.genesisBlock = genesisBlock;
this.interfaceAdapters = interfaceAdapters;
this.slots = slots;
this.sequence = sequence;
this.blockRewardArgs = {
distance: rewardDistance,
rewardOffset,
milestones: rewardMileStones,
totalAmount,
};
this.blockReward = {
calculateMilestone: height =>
calculateMilestone(height, this.blockRewardArgs),
calculateReward: height => calculateReward(height, this.blockRewardArgs),
calculateSupply: height => calculateSupply(height, this.blockRewardArgs),
};
this.constants = {
blockReceiptTimeout,
maxPayloadLength,
maxTransactionsPerBlock,
loadPerIteration,
activeDelegates,
blockSlotWindow,
};
this.blocksChain = new BlocksChain({
storage: this.storage,
interfaceAdapters: this.interfaceAdapters,
roundsModule: this.roundsModule,
slots: this.slots,
exceptions: this.exceptions,
genesisBlock: this.genesisBlock,
});
this.blocksVerify = new BlocksVerify({
storage: this.storage,
exceptions: this.exceptions,
slots: this.slots,
genesisBlock: this.genesisBlock,
roundsModule: this.roundsModule,
blockReward: this.blockReward,
constants: this.constants,
interfaceAdapters: this.interfaceAdapters,
});
this.blocksProcess = new BlocksProcess({
blocksChain: this.blocksChain,
blocksVerify: this.blocksVerify,
storage: this.storage,
exceptions: this.exceptions,
slots: this.slots,
interfaceAdapters: this.interfaceAdapters,
genesisBlock: this.genesisBlock,
blockReward: this.blockReward,
constants: this.constants,
});
}
get lastBlock() {
return this._lastBlock;
}
get isActive() {
return this._isActive;
}
get lastReceipt() {
return this._lastReceipt;
}
get broadhash() {
return this._broadhash;
}
/**
* Returns status of last receipt - if it stale or not.
*
* @returns {boolean} Stale status of last receipt
*/
isStale() {
if (!this._lastReceipt) {
return true;
}
// Current time in seconds - lastReceipt (seconds)
const secondsAgo = Math.floor(Date.now() / 1000) - this._lastReceipt;
return secondsAgo > this.constants.blockReceiptTimeout;
}
async init() {
try {
const rows = await this.storage.entities.Block.get(
{},
{ limit: this.constants.blockSlotWindow, sort: 'height:desc' },
);
this._lastNBlockIds = rows.map(row => row.id);
} catch (error) {
this.logger.error(
error,
`Unable to load last ${this.constants.blockSlotWindow} block ids`,
);
}
}
broadcast(block) {
// emit event
const cloned = cloneDeep(block);
this.emit(EVENT_BROADCAST_BLOCK, { block: cloned });
}
/**
* Handle node shutdown request.
*
* @listens module:app~event:cleanup
* @param {function} cb - Callback function
* @returns {setImmediateCallback} cb
*/
async cleanup() {
this._cleaning = true;
if (!this._isActive) {
// Module ready for shutdown
return;
}
const waitFor = () =>
new Promise(resolve => {
setTimeout(resolve, 10000);
});
// Module is not ready, repeat
const nextWatch = async () => {
if (this._isActive) {
this.logger.info('Waiting for block processing to finish...');
await waitFor();
await nextWatch();
}
return null;
};
await nextWatch();
}
/**
* Loads blockchain upon application start:
* 1. Checks mem tables:
* - count blocks from `blocks` table
* - get genesis block from `blocks` table
* - count accounts from `mem_accounts` table by block id
* - get rounds from `mem_round`
* 2. Matches genesis block with database.
* 3. Verifies rebuild mode.
* 4. Recreates memory tables when neccesary:
* - Calls block to load block. When blockchain ready emits a bus message.
* 5. Detects orphaned blocks in `mem_accounts` and gets delegates.
* 6. Loads last block and emits a bus message blockchain is ready.
*
* @todo Add @returns tag
*/
async loadBlockChain(rebuildUpToRound) {
this._shouldNotBeActive();
this._isActive = true;
await this.blocksChain.saveGenesisBlock();
// check mem tables
const { blocksCount, genesisBlock, memRounds } = await new Promise(
(resolve, reject) => {
this.storage.entities.Block.begin('loader:checkMemTables', async tx => {
try {
const result = await blocksUtils.loadMemTables(this.storage, tx);
resolve(result);
} catch (error) {
reject(error);
}
});
},
);
if (blocksCount === 1) {
this.logger.info('Applying genesis block');
this._lastBlock = await this._reload(blocksCount);
this._isActive = false;
return;
}
// check genesisBlock
this.blocksVerify.matchGenesisBlock(genesisBlock);
// rebuild accounts if it's rebuild
if (rebuildUpToRound !== null && rebuildUpToRound !== undefined) {
try {
await this._rebuildMode(rebuildUpToRound, blocksCount);
this._isActive = false;
} catch (errors) {
this._isActive = false;
throw errors;
}
return;
}
// check reload condition, true then reload
try {
await this.blocksVerify.reloadRequired(blocksCount, memRounds);
} catch (error) {
this.logger.error(error, 'Reload of blockchain is required');
this._lastBlock = await this._reload(blocksCount);
this._isActive = false;
return;
}
try {
this._lastBlock = await blocksUtils.loadLastBlock(
this.storage,
this.interfaceAdapters,
this.genesisBlock,
);
} catch (error) {
this.logger.error(error, 'Failed to fetch last block');
// This is last attempt
this._lastBlock = await this._reload(blocksCount);
this._isActive = false;
return;
}
const recoverRequired = await this.blocksVerify.requireBlockRewind(
this._lastBlock,
);
if (recoverRequired) {
this.logger.error('Invalid own blockchain');
this._lastBlock = await this.blocksProcess.recoverInvalidOwnChain(
this._lastBlock,
(lastBlock, newLastBlock) => {
this.logger.info({ lastBlock, newLastBlock }, 'Deleted block');
this.emit(EVENT_DELETE_BLOCK, {
block: cloneDeep(lastBlock),
newLastBlock: cloneDeep(newLastBlock),
});
},
);
}
this._isActive = false;
this.logger.info('Blockchain ready');
}
async recoverChain() {
const originalLastBlock = cloneDeep(this._lastBlock);
this._lastBlock = await this.blocksChain.deleteLastBlock(this._lastBlock);
this.emit(EVENT_DELETE_BLOCK, {
block: originalLastBlock,
newLastBlock: cloneDeep(this._lastBlock),
});
return this._lastBlock;
}
async loadBlocksDataWS(filter, tx) {
return blocksUtils.loadBlocksDataWS(this.storage, filter, tx);
}
// Process a block from the P2P
async receiveBlockFromNetwork(block) {
return this.sequence.add(async () => {
this._shouldNotBeActive();
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;
this.emit(EVENT_NEW_BLOCK, { block: cloneDeep(this._lastBlock) });
} catch (error) {
this._isActive = false;
this.logger.error(error);
}
return;
}
if (this.blocksVerify.isForkOne(block, this._lastBlock)) {
this.roundsModule.fork(block, 1);
if (this.blocksVerify.shouldDiscardForkOne(block, this._lastBlock)) {
this.logger.info('Last block stands');
this._isActive = false;
return;
}
try {
const {
verified,
errors,
} = await this.blocksVerify.normalizeAndVerify(
block,
this._lastBlock,
this._lastNBlockIds,
);
if (!verified) {
throw errors;
}
const originalLastBlock = cloneDeep(this._lastBlock);
this._lastBlock = await this.blocksChain.deleteLastBlock(
this._lastBlock,
);
this.emit(EVENT_DELETE_BLOCK, {
block: originalLastBlock,
newLastBlock: cloneDeep(this._lastBlock),
});
// emit event
const secondLastBlock = cloneDeep(this._lastBlock);
this._lastBlock = await this.blocksChain.deleteLastBlock(
this._lastBlock,
);
this.emit(EVENT_DELETE_BLOCK, {
block: secondLastBlock,
newLastBlock: cloneDeep(this._lastBlock),
});
this._isActive = false;
} catch (error) {
this._isActive = false;
this.logger.error(error);
}
return;
}
if (this.blocksVerify.isForkFive(block, this._lastBlock)) {
this.roundsModule.fork(block, 5);
if (this.blocksVerify.isDoubleForge(block, this._lastBlock)) {
this.logger.warn(
'Delegate forging on multiple nodes',
block.generatorPublicKey,
);
}
if (this.blocksVerify.shouldDiscardForkFive(block, this._lastBlock)) {
this.logger.info('Last block stands');
this._isActive = false;
return;
}
this._updateLastReceipt();
try {
const {
verified,
errors,
} = await this.blocksVerify.normalizeAndVerify(
block,
this._lastBlock,
this._lastNBlockIds,
);
if (!verified) {
throw errors;
}
const deletingBlock = cloneDeep(this._lastBlock);
this._lastBlock = await this.blocksChain.deleteLastBlock(
this._lastBlock,
);
this.emit(EVENT_DELETE_BLOCK, {
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.emit(EVENT_NEW_BLOCK, { block: cloneDeep(this._lastBlock) });
this._isActive = false;
} catch (error) {
this.logger.error(error);
this._isActive = false;
}
return;
}
if (block.id === this._lastBlock.id) {
this.logger.debug({ blockId: block.id }, 'Block already processed');
} else {
this.logger.warn(
{
blockId: block.id,
height: block.height,
round: this.slots.calcRound(block.height),
generatorPublicKey: block.generatorPublicKey,
slot: this.slots.getSlotNumber(block.timestamp),
},
'Discarded block that does not match with current chain',
);
}
// Discard received block
this._isActive = false;
});
}
// Process a block from syncing
async loadBlocksFromNetwork(blocks) {
this._shouldNotBeActive();
this._isActive = true;
try {
const normalizedBlocks = blocksUtils.readDbRows(
blocks,
this.interfaceAdapters,
this.genesisBlock,
);
// eslint-disable-next-line no-restricted-syntax
for (const block of normalizedBlocks) {
// check if it's cleaning
if (this._cleaning) {
break;
}
// eslint-disable-next-line no-await-in-loop
this._lastBlock = await this.blocksProcess.processBlock(
block,
this._lastBlock,
);
// emit event
this._updateLastNBlocks(block);
this.emit(EVENT_NEW_BLOCK, { block: cloneDeep(block) });
}
this._isActive = false;
return this._lastBlock;
} catch (error) {
this._isActive = false;
throw error;
}
}
// Generate a block for forging
async generateBlock(keypair, timestamp, transactions = []) {
this._shouldNotBeActive();
this._isActive = true;
try {
const block = await this.blocksProcess.generateBlock(
this._lastBlock,
keypair,
timestamp,
transactions,
);
this._lastBlock = await this.blocksProcess.processBlock(
block,
this._lastBlock,
validBlock => this.broadcast(validBlock),
);
} catch (error) {
this._isActive = false;
throw error;
}
await this._updateBroadhash();
this._updateLastReceipt();
this._updateLastNBlocks(this._lastBlock);
this.emit(EVENT_NEW_BLOCK, { block: cloneDeep(this._lastBlock) });
this._isActive = false;
return this._lastBlock;
}
async _rebuildMode(rebuildUpToRound, blocksCount) {
this.logger.info(
{ rebuildUpToRound, blocksCount },
'Rebuild process started',
);
if (blocksCount < this.constants.activeDelegates) {
throw new Error(
'Unable to rebuild, blockchain should contain at least one round of blocks',
);
}
if (
Number.isNaN(parseInt(rebuildUpToRound, 10)) ||
parseInt(rebuildUpToRound, 10) < 0
) {
throw new Error(
'Unable to rebuild, "--rebuild" parameter should be an integer equal to or greater than zero',
);
}
const totalRounds = Math.floor(
blocksCount / this.constants.activeDelegates,
);
const targetRound =
parseInt(rebuildUpToRound, 10) === 0
? totalRounds
: Math.min(totalRounds, parseInt(rebuildUpToRound, 10));
const targetHeight = targetRound * this.constants.activeDelegates;
this._lastBlock = await this._reload(targetHeight);
// Remove remaining
await this.storage.entities.Block.delete({ height_gt: targetHeight });
this.logger.info({ targetHeight, totalRounds }, 'Rebuilding finished');
}
_updateLastNBlocks(block) {
this._lastNBlockIds.push(block.id);
if (this._lastNBlockIds.length > this.constants.blockSlotWindow) {
this._lastNBlockIds.shift();
}
}
_updateLastReceipt() {
this._lastReceipt = Math.floor(Date.now() / 1000);
return this._lastReceipt;
}
async _updateBroadhash() {
const { broadhash, height } = await blocksUtils.calculateNewBroadhash(
this.storage,
this._broadhash,
this._lastBlock.height,
);
this._broadhash = broadhash;
this.emit(EVENT_NEW_BROADHASH, { broadhash, height });
}
_shouldNotBeActive() {
if (this._isActive) {
throw new Error('Block process cannot be executed in parallel');
}
}
async _reload(blocksCount) {
return this.blocksProcess.reload(
blocksCount,
() => this._cleaning,
block => {
this._lastBlock = block;
this.logger.info(
{ blockId: block.id, height: block.height },
'Reloaded block',
);
},
);
}
}
module.exports = {
Blocks,
EVENT_NEW_BLOCK,
EVENT_DELETE_BLOCK,
EVENT_BROADCAST_BLOCK,
EVENT_NEW_BROADHASH,
};