leasehold-chain
Version:
Leasehold sidechain
450 lines (410 loc) • 12 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.
*/
;
const { cloneDeep } = require('lodash');
const { Status: TransactionStatus } = require('@liskhq/lisk-transactions');
const transactionsModule = require('../transactions');
const { storageRead } = require('./block');
const TRANSACTION_TYPES_VOTE = 3;
const saveBlockBatch = async (storage, parsedBlock, saveBlockBatchTx) => {
const promises = [
storage.entities.Block.create(parsedBlock, {}, saveBlockBatchTx),
];
if (parsedBlock.transactions.length) {
promises.push(
storage.entities.Transaction.create(
parsedBlock.transactions.map(transaction => transaction.toJSON()),
{},
saveBlockBatchTx,
),
);
}
return saveBlockBatchTx.batch(promises);
};
/**
* Save block with transactions to database.
*
* @param {Object} block - Full normalized block
* @param {function} cb - Callback function
* @returns {Function|afterSave} cb - If SQL transaction was OK - returns safterSave execution, if not returns callback function from params (through setImmediate)
* @returns {string} cb.err - Error if occurred
*/
const saveBlock = async (storage, block, tx) => {
// Parse block data to storage module
const parsedBlock = cloneDeep(block);
if (parsedBlock.reward) {
parsedBlock.reward = parsedBlock.reward.toString();
}
if (parsedBlock.totalAmount) {
parsedBlock.totalAmount = parsedBlock.totalAmount.toString();
}
if (parsedBlock.totalFee) {
parsedBlock.totalFee = parsedBlock.totalFee.toString();
}
parsedBlock.previousBlockId = parsedBlock.previousBlock;
delete parsedBlock.previousBlock;
parsedBlock.transactions.map(transaction => {
transaction.blockId = parsedBlock.id;
return transaction;
});
// If there is already a running transaction use it
if (tx) {
return saveBlockBatch(storage, parsedBlock, tx);
}
// Prepare and execute SQL transaction
// WARNING: DB_WRITE
return storage.entities.Block.begin('Chain:saveBlock', async t => {
await saveBlockBatch(storage, parsedBlock, t);
});
};
/**
* Deletes block from blocks table.
*
* @param {number} blockId - ID of block to delete
* @param {function} cb - Callback function
* @param {Object} tx - Database transaction
* @returns {function} cb - Callback function from params (through setImmediate)
* @returns {Object} cb.err - String if SQL error occurred, null if success
*/
const deleteBlock = async (storage, blockId, tx) =>
// Delete block with ID from blocks table
// WARNING: DB_WRITE
storage.entities.Block.delete({ id: blockId }, {}, tx);
/**
* Deletes all blocks with height >= supplied block ID.
*
* @param {number} blockId - ID of block to begin with
* @param {function} cb - Callback function
* @returns {function} cb - Callback function from params (through setImmediate)
* @returns {Object} cb.err - SQL error
* @returns {Object} cb.res - SQL response
*/
const deleteFromBlockId = async (storage, blockId) => {
const block = await storage.entities.Block.getOne({
id: blockId,
});
return storage.entities.Block.delete({
height_gte: block.height,
});
};
/**
* Applies transactions to the confirmed state.
*
* @private
* @param {Object} block - Block object
* @param {Object} transactions - Transaction object
* @param {Object} sender - Sender account
* @param {function} cb - Callback function
* @returns {function} cb - Callback function from params (through setImmediate)
* @returns {Object} cb.err - Error if occurred
*/
const applyGenesisBlockTransactions = async (storage, slots, transactions) => {
const { stateStore } = await transactionsModule.applyGenesisTransactions(
storage,
)(transactions);
await stateStore.account.finalize();
stateStore.round.setRoundForData(slots.calcRound(1));
await stateStore.round.finalize();
};
/**
* Calls applyConfirmed from transactions module for each transaction in block
*
* @private
* @param {Object} block - Block object
* @param {function} tx - Database transaction
* @returns {Promise<reject|resolve>}
*/
const applyConfirmedStep = async (storage, slots, block, exceptions, tx) => {
if (block.transactions.length <= 0) {
return;
}
const nonInertTransactions = block.transactions.filter(
transaction =>
!transactionsModule.checkIfTransactionIsInert(transaction, exceptions),
);
const {
stateStore,
transactionsResponses,
} = await transactionsModule.applyTransactions(storage, exceptions)(
nonInertTransactions,
tx,
);
const unappliableTransactionsResponse = transactionsResponses.filter(
transactionResponse => transactionResponse.status !== TransactionStatus.OK,
);
if (unappliableTransactionsResponse.length > 0) {
throw unappliableTransactionsResponse[0].errors;
}
await stateStore.account.finalize();
stateStore.round.setRoundForData(slots.calcRound(block.height));
await stateStore.round.finalize();
};
/**
* Calls saveBlock for the block and performs round tick
*
* @private
* @param {Object} block - Block object
* @param {boolean} saveBlock - Flag to save block into database
* @param {function} tx - Database transaction
* @returns {Promise<reject|resolve>}
*/
const saveBlockStep = async (storage, roundsModule, block, shouldSave, tx) => {
if (shouldSave) {
await saveBlock(storage, block, tx);
}
await new Promise((resolve, reject) => {
roundsModule.tick(
block,
tickErr => {
if (tickErr) {
return reject(tickErr);
}
return resolve();
},
tx,
);
});
};
/**
* Reverts confirmed transactions due to block deletion
* @param {Object} block - secondLastBlock
* @param {Object} tx - database transaction
*/
const undoConfirmedStep = async (storage, slots, block, exceptions, tx) => {
if (block.transactions.length === 0) {
return;
}
const nonInertTransactions = block.transactions.filter(
transaction =>
!exceptions.inertTransactions ||
!exceptions.inertTransactions.includes(transaction.id),
);
const {
stateStore,
transactionsResponses,
} = await transactionsModule.undoTransactions(storage, exceptions)(
nonInertTransactions,
tx,
);
const unappliedTransactionResponse = transactionsResponses.find(
transactionResponse => transactionResponse.status !== TransactionStatus.OK,
);
if (unappliedTransactionResponse) {
throw unappliedTransactionResponse.errors;
}
await stateStore.account.finalize();
stateStore.round.setRoundForData(slots.calcRound(block.height));
await stateStore.round.finalize();
};
/**
* Performs backward tick
* @param {Object} oldLastBlock - secondLastBlock
* @param {Object} previousBlock - block to delete
* @param {Object} tx - database transaction
*/
const backwardTickStep = async (
roundsModule,
oldLastBlock,
previousBlock,
tx,
) =>
new Promise((resolve, reject) => {
// Perform backward tick on rounds
// WARNING: DB_WRITE
roundsModule.backwardTick(
oldLastBlock,
previousBlock,
backwardTickErr => {
if (backwardTickErr) {
return reject(backwardTickErr);
}
return resolve();
},
tx,
);
});
/**
* Deletes last block, undo transactions, recalculate round.
*
* @param {function} cb - Callback function
* @returns {function} cb - Callback function from params (through setImmediate)
* @returns {Object} cb.err - Error
* @returns {Object} cb.obj - New last block
*/
const popLastBlock = async (
storage,
interfaceAdapters,
genesisBlock,
roundsModule,
slots,
oldLastBlock,
exceptions,
) =>
storage.entities.Block.begin('Chain:deleteBlock', async tx => {
const [storageResult] = await storage.entities.Block.get(
{ id: oldLastBlock.previousBlock },
{ extended: true },
tx,
);
if (!storageResult) {
throw new Error('PreviousBlock is null');
}
const secondLastBlock = storageRead(storageResult);
secondLastBlock.transactions = interfaceAdapters.transactions.fromBlock(
secondLastBlock,
);
await undoConfirmedStep(storage, slots, oldLastBlock, exceptions, tx);
await backwardTickStep(roundsModule, oldLastBlock, secondLastBlock, tx);
await deleteBlock(storage, oldLastBlock.id, tx);
return secondLastBlock;
});
class BlocksChain {
constructor({
storage,
interfaceAdapters,
roundsModule,
slots,
exceptions,
genesisBlock,
}) {
this.storage = storage;
this.interfaceAdapters = interfaceAdapters;
this.roundsModule = roundsModule;
this.slots = slots;
this.exceptions = exceptions;
this.genesisBlock = genesisBlock;
}
/**
* Save genesis block to database.
*
* @returns {Object} Block genesis block
*/
async saveGenesisBlock() {
// Check if genesis block ID already exists in the database
const isPersisted = await this.storage.entities.Block.isPersisted({
id: this.genesisBlock.id,
});
if (isPersisted) {
return;
}
// If there is no block with genesis ID - save to database
// WARNING: DB_WRITE
// FIXME: This will fail if we already have genesis block in database, but with different ID
const block = {
...this.genesisBlock,
transactions: this.interfaceAdapters.transactions.fromBlock(
this.genesisBlock,
),
};
await saveBlock(this.storage, block);
}
/**
* Description of the function.
*
* @param {Object} block - Full normalized genesis block
* @param {function} cb - Callback function
* @returns {function} cb - Callback function from params (through setImmediate)
* @returns {Object} cb.err - Error if occurred
* @todo Add description for the function
*/
async applyBlock(block, shouldSave = true) {
await this.storage.entities.Block.begin('Chain:applyBlock', async tx => {
await applyConfirmedStep(
this.storage,
this.slots,
block,
this.exceptions,
tx,
);
await saveBlockStep(
this.storage,
this.roundsModule,
block,
shouldSave,
tx,
);
});
}
/**
* Apply genesis block's transactions to blockchain.
*
* @param {Object} block - Full normalized genesis block
* @param {function} cb - Callback function
* @returns {function} cb - Callback function from params (through setImmediate)
* @returns {Object} cb.err - Error if occurred
*/
async applyGenesisBlock(block) {
// Sort transactions included in block
block.transactions = block.transactions.sort(a => {
if (a.type === TRANSACTION_TYPES_VOTE) {
return 1;
}
return 0;
});
await applyGenesisBlockTransactions(
this.storage,
this.slots,
block.transactions,
this.exceptions,
);
await new Promise((resolve, reject) => {
this.roundsModule.tick(block, tickErr => {
if (tickErr) {
return reject(tickErr);
}
return resolve();
});
});
return block;
}
/**
* Deletes last block.
* - Apply the block to database if both verifications are ok
* - Update headers: broadhash and height
* - Put transactions from deleted block back into transaction pool
*
* @param {function} cb - Callback function
* @returns {function} cb - Callback function from params (through setImmediate)
* @returns {Object} cb.err - Error if occurred
* @returns {Object} cb.obj - New last block
*/
async deleteLastBlock(lastBlock) {
if (lastBlock.height === 1) {
throw new Error('Cannot delete genesis block');
}
const previousBlock = await popLastBlock(
this.storage,
this.interfaceAdapters,
this.genesisBlock,
this.roundsModule,
this.slots,
lastBlock,
this.exceptions,
);
return previousBlock;
}
}
module.exports = {
BlocksChain,
saveBlock,
applyGenesisBlockTransactions,
backwardTickStep,
saveBlockBatch,
deleteBlock,
deleteFromBlockId,
saveBlockStep,
applyConfirmedStep,
undoConfirmedStep,
popLastBlock,
};