leasehold-chain
Version:
Leasehold sidechain
565 lines (511 loc) • 16.5 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 _ = require('lodash');
const { hash } = require('@liskhq/lisk-cryptography');
const BigNum = require('@liskhq/bignum');
const blocksLogic = require('./block');
/**
* Generates a list of full blocks structured as full_blocks_list DB view
* db.blocks.loadBlocksDataWS used to return the raw full_blocks_list fields and peers expect to receive this schema
* After replacing db.blocks for storage.entities.Block, this parser was required to transfor storage object to the expected format.
* This should be removed along with https://github.com/LiskHQ/lisk/issues/2424 implementation
*
* @param {Object} ExtendedBlock - Storage ExtendedBlock object
* @returns {Array} Array of transactions with block data formated as full_blocks_list db view
*/
const parseStorageObjToLegacyObj = block => {
const parsedBlocks = [];
let transactions = [{}];
if (Array.isArray(block.transactions) && block.transactions.length > 0) {
({ transactions } = block);
}
/* eslint-disable no-restricted-globals */
transactions.forEach(t => {
parsedBlocks.push({
b_id: _.get(block, 'id', null),
b_version: isNaN(+block.version) ? null : +block.version,
b_timestamp: isNaN(+block.timestamp) ? null : +block.timestamp,
b_height: isNaN(+block.height) ? null : +block.height,
b_previousBlock: _.get(block, 'previousBlockId', null),
b_numberOfTransactions: isNaN(+block.numberOfTransactions)
? null
: +block.numberOfTransactions,
b_totalAmount: _.get(block, 'totalAmount', null),
b_totalFee: _.get(block, 'totalFee', null),
b_reward: _.get(block, 'reward', null),
b_payloadLength: isNaN(+block.payloadLength)
? null
: +block.payloadLength,
b_payloadHash: _.get(block, 'payloadHash', null),
b_generatorPublicKey: _.get(block, 'generatorPublicKey', null),
b_blockSignature: _.get(block, 'blockSignature', null),
t_id: _.get(t, 'id', null),
t_type: _.get(t, 'type', null),
t_timestamp: _.get(t, 'timestamp', null),
t_senderPublicKey: _.get(t, 'senderPublicKey', null),
t_senderId: _.get(t, 'senderId', null),
t_recipientId: _.get(t, 'recipientId', null),
t_amount: _.get(t, 'amount', null),
t_fee: _.get(t, 'fee', null),
t_signature: _.get(t, 'signature', null),
t_signSignature: _.get(t, 'signSignature', null),
t_requesterPublicKey: _.get(t, 'requesterPublicKey', null),
t_signatures: t.signatures ? t.signatures.join(',') : null,
tf_data: _.get(t, 'asset.data', null),
s_publicKey: _.get(t, 'asset.signature.publicKey', null),
d_username: _.get(t, 'asset.delegate.username', null),
v_votes: t.asset && t.asset.votes ? t.asset.votes.join(',') : null,
m_min: _.get(t, 'asset.multisignature.min', null),
m_lifetime: _.get(t, 'asset.multisignature.lifetime', null),
m_keysgroup:
t.asset && t.asset.multisignature && t.asset.multisignature.keysgroup
? t.asset.multisignature.keysgroup.join(',')
: null,
dapp_name: _.get(t, 'asset.dapp.name', null),
dapp_description: _.get(t, 'asset.dapp.description', null),
dapp_tags: _.get(t, 'asset.dapp.tags', null),
dapp_type: _.get(t, 'asset.dapp.type', null),
dapp_link: _.get(t, 'asset.dapp.link', null),
dapp_category: _.get(t, 'asset.dapp.category', null),
dapp_icon: _.get(t, 'asset.dapp.icon', null),
in_dappId: _.get(t, 'asset.inTransfer.dappId', null),
ot_dappId: _.get(t, 'asset.outTransfer.dappId', null),
ot_outTransactionId: _.get(t, 'asset.outTransfer.transactionId', null),
});
});
return parsedBlocks;
};
/**
* Generates a list of full blocks for another node upon sync request from that node, see: modules.transport.internal.blocks.
* NOTE: changing the original method loadBlocksDataWS() could potentially change behaviour (popLastBlock() uses it for instance)
* so that's why this new method was added
* @param {Object} filter - Filter options
* @param {Object} filter.limit - Limit blocks to amount
* @param {Object} filter.lastId - ID of block to begin with
* @param {function} cb - Callback function
* @param {Object} tx - database transaction
* @returns {function} cb - Callback function from params (through setImmediate)
* @returns {Object} cb.err - Error if occurred
* @returns {Object} cb.rows - List of blocks
*/
// eslint-disable-next-line class-methods-use-this
const loadBlocksDataWS = async (storage, filter, tx) => {
const params = { limit: filter.limit || 1 };
// FIXME: filter.id is not used
if (filter.id && filter.lastId) {
throw new Error('Invalid filter: Received both id and lastId');
}
if (filter.id) {
params.id = filter.id;
} else if (filter.lastId) {
params.lastId = filter.lastId;
}
// Get height of block with supplied ID
const rows = await storage.entities.Block.get(
{ id: filter.lastId || null },
{ limit: params.limit },
tx,
);
if (!rows.length) {
throw new Error('Invalid lastBlockId requested');
}
const height = rows.length ? rows[0].height : 0;
// Calculate max block height for database query
const realLimit = height + (parseInt(filter.limit, 10) || 1);
params.limit = realLimit;
params.height = height;
const mergedParams = Object.assign({}, filter, params);
const queryFilters = {};
if (!mergedParams.id && !mergedParams.lastId) {
queryFilters.height_lt = mergedParams.limit;
}
if (mergedParams.id) {
queryFilters.id = mergedParams.id;
}
if (mergedParams.lastId) {
queryFilters.height_gt = mergedParams.height;
queryFilters.height_lt = mergedParams.limit;
}
const blockRows = await storage.entities.Block.get(
queryFilters,
{
extended: true,
limit: null,
sort: ['height'],
},
tx,
);
let parsedBlocks = [];
blockRows.forEach(block => {
parsedBlocks = parsedBlocks.concat(parseStorageObjToLegacyObj(block));
});
return parsedBlocks;
};
/**
* Load full block with a particular height
*
* @param {number} height - Block height
* @param {function} cb - Callback function
* @param {object} tx - Database transaction object
* @returns {function} cb - Callback function from params (through setImmediate)
* @returns {Object} cb.err - Error if occurred
* @returns {Object} cb.block - Block with requested height
*/
/**
* Normalize blocks and their transactions.
*
* @param {Array} rows - Data from extended block entity
* @returns {Array} blocks - List of normalized blocks with transactions
*/
const readStorageRows = (rows, interfaceAdapters, genesisBlock) => {
const blocks = rows.map(block => {
// Normalize block
// FIXME: Can have poor performance because it performs SHA256 hash calculation for each block
block = blocksLogic.storageRead(block);
if (block) {
if (block.id === genesisBlock.id) {
// Generate fake signature for genesis block
block.generationSignature = new Array(65).join('0');
}
// Normalize transaction
if (block.transactions) {
block.transactions = interfaceAdapters.transactions.fromBlock(block);
}
}
return block;
});
return blocks;
};
/**
* Normalize blocks and their transactions.
*
* @param {Array} rows - Data from full_blocks_list view
* @returns {Array} blocks - List of normalized blocks with transactions
*/
const readDbRows = (rows, interfaceAdapters, genesisBlock) => {
let blocks = {};
const order = [];
// eslint-disable-next-line no-plusplus
for (let i = 0, { length } = rows; i < length; i++) {
// Normalize block
// FIXME: Can have poor performance because it performs SHA256 hash calculation for each block
const block = blocksLogic.dbRead(rows[i]);
if (block) {
// If block is not already in the list...
if (!blocks[block.id]) {
if (block.id === genesisBlock.id) {
// Generate fake signature for genesis block
block.generationSignature = new Array(65).join('0');
}
// Add block ID to order list
order.push(block.id);
// Add block to list
blocks[block.id] = block;
}
// Normalize transaction
const transaction = interfaceAdapters.transactions.dbRead(rows[i]);
// Set empty object if there are no transactions in block
blocks[block.id].transactions = blocks[block.id].transactions || {};
if (transaction) {
// Add transaction to block if not there already
if (!blocks[block.id].transactions[transaction.id]) {
blocks[block.id].transactions[transaction.id] = transaction;
}
}
}
}
// Reorganize list
blocks = order.map(v => {
blocks[v].transactions = Object.keys(blocks[v].transactions).map(
t => blocks[v].transactions[t],
);
return blocks[v];
});
return blocks;
};
/**
* Loads full normalized last block from database, see: loader.loadBlockChain (private).
*
* @param {function} cb - Callback function
* @returns {function} cb - Callback function from params (through setImmediate)
* @returns {Object} cb.err - Error message if error occurred
* @returns {Object} cb.block - Full normalized last block
*/
const loadLastBlock = async (storage, interfaceAdapters, genesisBlock) => {
// Get full last block from database
// FIXME: Review SQL order by clause
const rows = await storage.entities.Block.get(
{},
{ sort: 'height:desc', limit: 1, extended: true },
);
if (!rows) {
throw new Error('Failed to load last block');
}
// Normalize block
return readStorageRows(rows, interfaceAdapters, genesisBlock)[0];
// Update last block
// TODO: Update from callee
// modules.blocks.lastBlock.set(block);
};
/**
* Get blocks IDs sequence - last block ID, IDs of first blocks of last 5 rounds, genesis block ID.
*
* @param {number} height - Block height
* @param {function} cb - Callback function
* @returns {function} cb - Callback function from params (through setImmediate)
* @returns {Object} cb.err - Error if occurred
* @returns {Object} cb.res - Result
* @returns {string} cb.res.firstHeight - Height of last block
* @returns {string} cb.res.ids - Comma separated list of blocks IDs
*/
const getIdSequence = async (
storage,
height,
lastBlock,
genesisBlock,
numberOfDelegates,
) => {
// Get IDs of first blocks of (n) last rounds, descending order
// EXAMPLE: For height 2000000 (round 19802) we will get IDs of blocks at height: 1999902, 1999801, 1999700, 1999599, 1999498
const rows = await storage.entities.Block.getFirstBlockIdOfLastRounds({
height,
numberOfRounds: 5,
numberOfDelegates,
});
if (rows.length === 0) {
throw new Error(`Failed to get id sequence for height: ${height}`);
}
const ids = [];
// Add genesis block at the end if the set doesn't contain it already
if (genesisBlock) {
const __genesisBlock = {
id: genesisBlock.id,
height: genesisBlock.height,
};
if (!_.includes(rows, __genesisBlock.id)) {
rows.push(__genesisBlock);
}
}
// Add last block at the beginning if the set doesn't contain it already
if (lastBlock && !_.includes(rows, lastBlock.id)) {
rows.unshift({
id: lastBlock.id,
height: lastBlock.height,
});
}
// Extract blocks IDs
rows.forEach(row => {
// FIXME: Looks like double check
if (!_.includes(ids, row.id)) {
ids.push(row.id);
}
});
return {
firstHeight: rows[0].height,
ids: ids.join(),
};
};
/**
* Load full block with a particular height
*
* @param {number} height - Block height
* @param {object} tx - Database transaction object
* @returns {function} cb - Callback function from params (through setImmediate)
* @returns {Object} cb.err - Error if occurred
* @returns {Object} cb.block - Block with requested height
*/
const loadBlockByHeight = async (
storage,
height,
interfaceAdapters,
genesisBlock,
tx,
) => {
const row = await storage.entities.Block.getOne(
{ height },
{ extended: true },
tx,
);
return readStorageRows([row], interfaceAdapters, genesisBlock)[0];
};
/**
* Load blocks with offset
*
*
* @param {number} height - Block height
* @param {object} tx - Database transaction object
* @returns {function} cb - Callback function from params (through setImmediate)
* @returns {Object} cb.err - Error if occurred
* @returns {Object} cb.block - Block with requested height
*/
const loadBlocksWithOffset = async (
storage,
interfaceAdapters,
genesisBlock,
blocksAmount,
fromHeight = 0,
) => {
// Calculate toHeight
const toHeight = fromHeight + blocksAmount;
const filters = {
height_gte: fromHeight,
height_lt: toHeight,
};
const options = {
limit: null,
sort: ['height:asc', 'rowId:asc'],
extended: true,
};
// Loads extended blocks from storage
const rows = await storage.entities.Block.get(filters, options);
return readStorageRows(rows, interfaceAdapters, genesisBlock);
};
/**
* Calculate broadhash getting the last 5 blocks from the database
*
* @returns {height, broadhash} broadhash and height
*
*/
const calculateNewBroadhash = async (storage, nethash, height) => {
const blocks = await storage.entities.Block.get(
{},
{
limit: 5,
sort: 'height:desc',
},
);
if (blocks.length <= 1) {
return {
broadhash: nethash,
height,
};
}
const seed = blocks.map(row => row.id).join('');
const broadhash = hash(seed, 'utf8').toString('hex');
const blockHeight = blocks[0].height;
return { broadhash, height: blockHeight };
};
/**
* Adds default properties to block.
*
* @param {Object} block - Block object reduced
* @returns {Object} Block object completed
*/
const addBlockProperties = block => {
block.totalAmount = new BigNum(block.totalAmount || 0);
block.totalFee = new BigNum(block.totalFee || 0);
block.reward = new BigNum(block.reward || 0);
if (block.version === undefined) {
block.version = 0;
}
if (block.numberOfTransactions === undefined) {
if (block.transactions === undefined) {
block.numberOfTransactions = 0;
} else {
block.numberOfTransactions = block.transactions.length;
}
}
if (block.payloadLength === undefined) {
block.payloadLength = 0;
}
if (block.transactions === undefined) {
block.transactions = [];
}
return block;
};
/**
* Deletes default properties from block.
*
* @param {Object} block - Block object completed
* @returns {Object} Block object reduced
*/
const deleteBlockProperties = block => {
const reducedBlock = {
...block,
};
if (reducedBlock.version === 0) {
delete reducedBlock.version;
}
// verifyBlock ensures numberOfTransactions is transactions.length
if (typeof reducedBlock.numberOfTransactions === 'number') {
delete reducedBlock.numberOfTransactions;
}
if (reducedBlock.totalAmount.equals(0)) {
delete reducedBlock.totalAmount;
}
if (reducedBlock.totalFee.equals(0)) {
delete reducedBlock.totalFee;
}
if (reducedBlock.payloadLength === 0) {
delete reducedBlock.payloadLength;
}
if (reducedBlock.reward.equals(0)) {
delete reducedBlock.reward;
}
if (reducedBlock.transactions && reducedBlock.transactions.length === 0) {
delete reducedBlock.transactions;
}
return reducedBlock;
};
/**
* Set height according to the given last block.
*
* @private
* @func setHeight
* @param {Object} block - Target block
* @param {Object} lastBlock - Last block
* @returns {Object} block - Target block
*/
const setHeight = (block, lastBlock) => {
block.height = lastBlock.height + 1;
return block;
};
/**
* Get mem table status for state check
*
* @private
* @func setHeight
* @param {Object} storage - storage class
* @param {Object} tx - database transaction
* @returns {Object} blockcount, genesisBlock, memRounds
*/
const loadMemTables = async (storage, tx) => {
const promises = [
storage.entities.Block.count({}, {}, tx),
storage.entities.Block.getOne({ height: 1 }, {}, tx),
storage.entities.Round.getUniqueRounds(tx),
];
const [blocksCount, genesisBlock, memRounds] = await tx.batch(promises);
return {
blocksCount,
genesisBlock,
memRounds,
};
};
module.exports = {
parseStorageObjToLegacyObj,
getIdSequence,
readStorageRows,
readDbRows,
loadBlocksDataWS,
loadLastBlock,
loadBlockByHeight,
loadBlocksWithOffset,
loadMemTables,
calculateNewBroadhash,
setHeight,
addBlockProperties,
deleteBlockProperties,
};