lisk-framework
Version:
Lisk blockchain application platform
452 lines • 21.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Generator = void 0;
const fs = require("fs");
const events_1 = require("events");
const lisk_chain_1 = require("@liskhq/lisk-chain");
const lisk_codec_1 = require("@liskhq/lisk-codec");
const lisk_cryptography_1 = require("@liskhq/lisk-cryptography");
const lisk_db_1 = require("@liskhq/lisk-db");
const lisk_transaction_pool_1 = require("@liskhq/lisk-transaction-pool");
const lisk_tree_1 = require("@liskhq/lisk-tree");
const lisk_utils_1 = require("@liskhq/lisk-utils");
const lisk_validator_1 = require("@liskhq/lisk-validator");
const events_2 = require("../events");
const broadcaster_1 = require("./broadcaster");
const constants_1 = require("./constants");
const endpoint_1 = require("./endpoint");
const generator_store_1 = require("./generator_store");
const network_endpoint_1 = require("./network_endpoint");
const schemas_1 = require("./schemas");
const strategies_1 = require("./strategies");
const generated_info_1 = require("./generated_info");
const constants_2 = require("../consensus/constants");
const abi_1 = require("../../abi");
const consensus_1 = require("../consensus");
const path_1 = require("../../utils/path");
const metrics_1 = require("../metrics/metrics");
const single_commit_handler_1 = require("./single_commit_handler");
const BLOCK_VERSION = 2;
class Generator {
constructor(args) {
this.events = new events_1.EventEmitter();
this._metrics = {
blockGeneration: metrics_1.defaultMetrics.counter('generator_blockGeneration'),
};
this._abi = args.abi;
this._keypairs = new lisk_utils_1.dataStructures.BufferMap();
this._pool = new lisk_transaction_pool_1.TransactionPool({
maxPayloadLength: args.config.genesis.maxTransactionsSize,
verifyTransaction: async (transaction) => this._verifyTransaction(transaction),
});
this._config = args.config;
this._blockTime = args.config.genesis.blockTime;
this._chain = args.chain;
this._bft = args.bft;
this._consensus = args.consensus;
this._network = args.network;
this._broadcaster = new broadcaster_1.Broadcaster({
network: this._network,
transactionPool: this._pool,
interval: constants_1.DEFAULT_RELEASE_INTERVAL,
limit: constants_1.DEFAULT_RELEASE_LIMIT,
});
this._endpoint = new endpoint_1.Endpoint({
abi: this._abi,
keypair: this._keypairs,
consensus: this._consensus,
blockTime: this._blockTime,
chain: this._chain,
});
this._networkEndpoint = new network_endpoint_1.NetworkEndpoint({
abi: this._abi,
broadcaster: this._broadcaster,
chain: this._chain,
network: this._network,
pool: this._pool,
});
this._forgingStrategy = new strategies_1.HighFeeGenerationStrategy({
maxTransactionsSize: this._chain.constants.maxTransactionsSize,
abi: this._abi,
pool: this._pool,
});
this._generationJob = new lisk_utils_1.jobHandlers.Scheduler(async () => this._generateLoop().catch(err => {
this._logger.error({ err: err }, 'Failed to generate a block');
}), constants_1.FORGE_INTERVAL);
}
async init(args) {
this._logger = args.logger;
this._generatorDB = args.generatorDB;
this._blockchainDB = args.blockchainDB;
this._genesisHeight = args.genesisHeight;
this._singleCommitHandler = new single_commit_handler_1.SingleCommitHandler(this._logger, this._chain, this._consensus, this._bft, this._keypairs, this._blockchainDB);
this._broadcaster.init({
logger: this._logger,
});
this._endpoint.init({
generatorDB: this._generatorDB,
genesisHeight: this._genesisHeight,
singleCommitHandler: this._singleCommitHandler,
});
this._networkEndpoint.init({
logger: this._logger,
});
await this._saveKeysFromFile();
await this._loadGenerators();
this._network.registerHandler(constants_1.NETWORK_EVENT_POST_TRANSACTIONS_ANNOUNCEMENT, ({ data, peerId }) => {
this._networkEndpoint
.handleEventPostTransactionsAnnouncement(data, peerId)
.catch(err => this._logger.error({ err: err, peerId }, 'Fail to handle transaction announcement'));
});
this._network.registerEndpoint(constants_1.NETWORK_RPC_GET_TRANSACTIONS, async ({ data, peerId }) => this._networkEndpoint.handleRPCGetTransactions(data, peerId));
this._networkEndpoint.event.on(constants_1.GENERATOR_EVENT_NEW_TRANSACTION_ANNOUNCEMENT, e => {
this.events.emit(constants_1.GENERATOR_EVENT_NEW_TRANSACTION_ANNOUNCEMENT, e);
});
this._networkEndpoint.event.on(constants_1.GENERATOR_EVENT_NEW_TRANSACTION, e => {
this.events.emit(constants_1.GENERATOR_EVENT_NEW_TRANSACTION, e);
});
await this._singleCommitHandler.initAllSingleCommits();
}
get endpoint() {
return this._endpoint;
}
get txpool() {
return this._pool;
}
get broadcaster() {
return this._broadcaster;
}
async start() {
this._networkEndpoint.start();
this._pool.events.on(lisk_transaction_pool_1.events.EVENT_TRANSACTION_ADDED, (event) => {
this.events.emit(constants_1.GENERATOR_EVENT_NEW_TRANSACTION, {
transaction: event.transaction.toJSON(),
});
});
this._pool.events.on(lisk_transaction_pool_1.events.EVENT_TRANSACTION_REMOVED, (event) => {
this._logger.debug(event, 'Transaction was removed from the pool.');
});
this._broadcaster.start();
await this._pool.start();
this._generationJob.start();
this._network.events.on(events_2.EVENT_NETWORK_READY, () => {
this._loadTransactionsFromNetwork().catch(err => this._logger.error({ err: err }, 'Failed to load unconfirmed transactions from the network'));
});
this._consensus.events.on(constants_2.CONSENSUS_EVENT_FINALIZED_HEIGHT_CHANGED, ({ from, to }) => {
this._singleCommitHandler
.handleFinalizedHeightChanged(from, to)
.catch((err) => this._logger.error({ err }, 'Fail to certify single commit'));
});
}
async stop() {
this._pool.events.removeAllListeners(lisk_transaction_pool_1.events.EVENT_TRANSACTION_REMOVED);
this._broadcaster.stop();
this._pool.stop();
this._generationJob.stop();
this._networkEndpoint.stop();
}
onNewBlock(block) {
if (block.transactions.length) {
for (const transaction of block.transactions) {
this._pool.remove(transaction);
}
}
}
getPooledTransactions() {
return this._pool.getAll();
}
onDeleteBlock(block) {
if (block.transactions.length) {
for (const transaction of block.transactions) {
this._pool.add(transaction).catch((err) => {
this._logger.error({ err }, 'Failed to add transaction back to the pool');
});
}
}
}
async generateBlock(input) {
const block = await this._generateBlock({
...input,
}).catch(async (err) => {
await this._abi.clear({});
throw err;
});
return block;
}
async _loadTransactionsFromNetwork() {
for (let retry = 0; retry < constants_1.LOAD_TRANSACTION_RETRIES; retry += 1) {
try {
await this._getUnconfirmedTransactionsFromNetwork();
return;
}
catch (err) {
if (err && retry === constants_1.LOAD_TRANSACTION_RETRIES - 1) {
this._logger.error({ err: err }, `Failed to get transactions from network after ${constants_1.LOAD_TRANSACTION_RETRIES} retries`);
}
}
}
}
async _verifyTransaction(transaction) {
const { result } = await this._abi.verifyTransaction({
contextID: Buffer.alloc(0),
transaction,
header: this._chain.lastBlock.header.toObject(),
onlyCommand: false,
});
return result;
}
async _saveKeysFromFile() {
if (!this._config.generator.keys.fromFile) {
return;
}
const filePath = (0, path_1.getPathFromDataPath)(this._config.generator.keys.fromFile, this._config.system.dataPath);
this._logger.debug({ filePath }, 'Reading validator keys from a file');
const keysFile = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
lisk_validator_1.validator.validate(schemas_1.keysFileSchema, keysFile);
const generatorStore = new generator_store_1.GeneratorStore(this._generatorDB);
const batch = new lisk_db_1.Batch();
const subStore = generatorStore.getGeneratorStore(constants_1.GENERATOR_STORE_KEY_PREFIX);
for (const key of keysFile.keys) {
this._logger.info({ address: key.address }, 'saving generator from file');
if (key.encrypted && Object.keys(key.encrypted).length) {
await subStore.set(lisk_cryptography_1.address.getAddressFromLisk32Address(key.address), lisk_codec_1.codec.encode(schemas_1.generatorKeysSchema, {
type: 'encrypted',
data: lisk_codec_1.codec.encode(schemas_1.encryptedMessageSchema, key.encrypted),
}));
}
else if (key.plain) {
await subStore.set(lisk_cryptography_1.address.getAddressFromLisk32Address(key.address), lisk_codec_1.codec.encode(schemas_1.generatorKeysSchema, {
type: 'plain',
data: lisk_codec_1.codec.encode(schemas_1.plainGeneratorKeysSchema, {
blsKey: Buffer.from(key.plain.blsKey, 'hex'),
blsPrivateKey: Buffer.from(key.plain.blsPrivateKey, 'hex'),
generatorKey: Buffer.from(key.plain.generatorKey, 'hex'),
generatorPrivateKey: Buffer.from(key.plain.generatorPrivateKey, 'hex'),
}),
}));
}
}
generatorStore.finalize(batch);
await this._generatorDB.write(batch);
}
async _loadGenerators() {
const generatorStore = new generator_store_1.GeneratorStore(this._generatorDB);
const subStore = generatorStore.getGeneratorStore(constants_1.GENERATOR_STORE_KEY_PREFIX);
const encodedGeneratorKeysList = await subStore.iterate({
gte: Buffer.alloc(20, 0),
lte: Buffer.alloc(20, 255),
});
for (const { key, value } of encodedGeneratorKeysList) {
const encodedGeneratorKeys = lisk_codec_1.codec.decode(schemas_1.generatorKeysSchema, value);
if (encodedGeneratorKeys.type === 'plain') {
const keys = lisk_codec_1.codec.decode(schemas_1.plainGeneratorKeysSchema, encodedGeneratorKeys.data);
this._keypairs.set(key, {
publicKey: keys.generatorKey,
privateKey: keys.generatorPrivateKey,
blsPublicKey: keys.blsKey,
blsSecretKey: keys.blsPrivateKey,
});
this._logger.info(`Block generation enabled for address: ${lisk_cryptography_1.address.getLisk32AddressFromAddress(key)}`);
}
}
}
async _getUnconfirmedTransactionsFromNetwork() {
this._logger.info('Loading transactions from the network');
const { data } = (await this._network.request({
procedure: constants_1.NETWORK_RPC_GET_TRANSACTIONS,
}));
const transactionResponse = lisk_codec_1.codec.decode(schemas_1.getTransactionsResponseSchema, data);
lisk_validator_1.validator.validate(schemas_1.getTransactionsResponseSchema, transactionResponse);
const transactions = transactionResponse.transactions.map(transaction => lisk_chain_1.Transaction.fromBytes(transaction));
for (const transaction of transactions) {
const { error } = await this._pool.add(transaction);
if (error) {
this._logger.error({ err: error }, 'Failed to add transaction to pool.');
throw error;
}
}
}
async _generateLoop() {
if (this._consensus.syncing()) {
return;
}
const stateStore = new lisk_chain_1.StateStore(this._blockchainDB);
const MS_IN_A_SEC = 1000;
const currentTime = Math.floor(new Date().getTime() / MS_IN_A_SEC);
const currentSlot = this._bft.method.getSlotNumber(currentTime);
const currentSlotTime = this._bft.method.getSlotTime(currentSlot);
const waitThreshold = this._blockTime / 5;
const lastBlockSlot = this._bft.method.getSlotNumber(this._chain.lastBlock.header.timestamp);
if (currentSlot === lastBlockSlot) {
this._logger.trace({ slot: currentSlot }, 'Block already generated for the current slot');
return;
}
const nextHeight = this._chain.lastBlock.header.height + 1;
const generator = await this._bft.method.getGeneratorAtTimestamp(stateStore, nextHeight, currentTime);
const validatorKeypair = this._keypairs.get(generator.address);
if (validatorKeypair === undefined) {
this._logger.debug({ currentSlot }, 'Waiting for validator slot');
return;
}
if (lastBlockSlot < currentSlot - 1 && currentTime <= currentSlotTime + waitThreshold) {
this._logger.info('Skipping forging to wait for last block');
this._logger.debug({
currentSlot,
lastBlockSlot,
waitThreshold,
}, 'Slot information');
return;
}
const generatedBlock = await this._generateBlock({
height: nextHeight,
generatorAddress: generator.address,
privateKey: validatorKeypair.privateKey,
timestamp: currentTime,
});
this._logger.info({
id: generatedBlock.header.id,
height: generatedBlock.header.height,
generatorAddress: lisk_cryptography_1.address.getLisk32AddressFromAddress(generator.address),
}, 'Generated new block');
await this._consensus.execute(generatedBlock);
}
async _generateBlock(input) {
var _a;
const { generatorAddress, timestamp, privateKey, height } = input;
const stateStore = new lisk_chain_1.StateStore(this._blockchainDB);
const generatorStore = new generator_store_1.GeneratorStore((_a = input.db) !== null && _a !== void 0 ? _a : this._generatorDB);
const { maxHeightPrevoted } = await this._bft.method.getBFTHeights(stateStore);
const { height: maxHeightGenerated } = await (0, generated_info_1.getOrDefaultLastGeneratedInfo)(generatorStore, generatorAddress);
const aggregateCommit = await this._consensus.getAggregateCommit(stateStore);
const impliesMaxPrevotes = await this._bft.method.impliesMaximalPrevotes(stateStore, {
height,
maxHeightGenerated,
generatorAddress,
});
const blockHeader = new lisk_chain_1.BlockHeader({
generatorAddress,
height,
previousBlockID: this._chain.lastBlock.header.id,
version: BLOCK_VERSION,
maxHeightPrevoted,
maxHeightGenerated,
impliesMaxPrevotes,
aggregateCommit,
assetRoot: Buffer.alloc(0),
stateRoot: Buffer.alloc(0),
eventRoot: Buffer.alloc(0),
transactionRoot: Buffer.alloc(0),
validatorsHash: Buffer.alloc(0),
signature: Buffer.alloc(0),
timestamp,
});
const blockEvents = [];
let transactions;
const { contextID } = await this._abi.initStateMachine({
header: blockHeader.toObject(),
});
const { assets } = await this._abi.insertAssets({
contextID,
finalizedHeight: this._chain.finalizedHeight,
});
const blockAssets = new lisk_chain_1.BlockAssets(assets);
const maxRemovalHeight = await this._consensus.getMaxRemovalHeight();
await this._bft.beforeTransactionsExecute(stateStore, blockHeader, maxRemovalHeight);
const { events: beforeTxsEvents } = await this._abi.beforeTransactionsExecute({
contextID,
assets: blockAssets.getAll(),
});
blockEvents.push(...beforeTxsEvents.map(e => new lisk_chain_1.Event(e)));
if (input.transactions) {
const { transactions: executedTxs, events: txEvents } = await this._executeTransactions(contextID, blockHeader, blockAssets, input.transactions);
blockEvents.push(...txEvents);
transactions = executedTxs;
}
else {
const { transactions: executedTxs, events: txEvents } = await this._forgingStrategy.getTransactionsForBlock(contextID, blockHeader, blockAssets);
blockEvents.push(...txEvents);
transactions = executedTxs;
}
const afterResult = await this._abi.afterTransactionsExecute({
contextID,
assets: blockAssets.getAll(),
transactions: transactions.map(tx => tx.toObject()),
});
blockEvents.push(...afterResult.events.map(e => new lisk_chain_1.Event(e)));
if (!(0, consensus_1.isEmptyConsensusUpdate)(afterResult.preCommitThreshold, afterResult.certificateThreshold, afterResult.nextValidators)) {
await this._bft.method.setBFTParameters(stateStore, afterResult.preCommitThreshold, afterResult.certificateThreshold, afterResult.nextValidators);
}
stateStore.finalize(new lisk_db_1.Batch());
const txTree = new lisk_tree_1.MerkleTree();
await txTree.init(transactions.map(tx => tx.id));
const transactionRoot = txTree.root;
blockHeader.transactionRoot = transactionRoot;
blockHeader.assetRoot = await blockAssets.getRoot();
const keypairs = [];
for (let index = 0; index < blockEvents.length; index += 1) {
const e = blockEvents[index];
e.setIndex(index);
const pairs = e.keyPair();
for (const pair of pairs) {
keypairs.push(pair);
}
}
const smt = new lisk_db_1.SparseMerkleTree(lisk_chain_1.EVENT_KEY_LENGTH);
const eventRoot = await smt.update(constants_1.EMPTY_HASH, keypairs);
blockHeader.eventRoot = eventRoot;
const { stateRoot } = await this._abi.commit({
contextID,
dryRun: true,
expectedStateRoot: Buffer.alloc(0),
stateRoot: this._chain.lastBlock.header.stateRoot,
});
blockHeader.stateRoot = stateRoot;
const { validatorsHash } = await this._bft.method.getBFTParameters(stateStore, height + 1);
blockHeader.validatorsHash = validatorsHash;
blockHeader.sign(this._chain.chainID, privateKey);
const generatedBlock = new lisk_chain_1.Block(blockHeader, transactions, blockAssets);
await (0, generated_info_1.setLastGeneratedInfo)(generatorStore, blockHeader.generatorAddress, blockHeader);
const batch = new lisk_db_1.Batch();
generatorStore.finalize(batch);
await this._generatorDB.write(batch);
await this._abi.clear({});
this._metrics.blockGeneration.inc(1);
return generatedBlock;
}
async _executeTransactions(contextID, header, assets, transactions) {
const executedTransactions = [];
const executedEvents = [];
for (const transaction of transactions) {
try {
const { result: verifyResult } = await this._abi.verifyTransaction({
contextID,
transaction,
header: header.toObject(),
onlyCommand: false,
});
if (verifyResult !== abi_1.TransactionVerifyResult.OK) {
throw new Error('Transaction is not valid');
}
const { events: txEvents, result: executeResult } = await this._abi.executeTransaction({
contextID,
header: header.toObject(),
transaction,
assets: assets.getAll(),
dryRun: false,
});
if (executeResult === abi_1.TransactionExecutionResult.INVALID) {
this._pool.remove(transaction);
throw new Error('Transaction is not valid');
}
executedTransactions.push(transaction);
executedEvents.push(...txEvents.map(e => new lisk_chain_1.Event(e)));
}
catch (error) {
continue;
}
}
return { transactions: executedTransactions, events: executedEvents };
}
}
exports.Generator = Generator;
//# sourceMappingURL=generator.js.map