leasehold-chain
Version:
Leasehold sidechain
500 lines (453 loc) • 11.7 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 { Status: TransactionStatus } = require('@liskhq/lisk-transactions');
const {
BIG_ENDIAN,
getAddressFromPublicKey,
hash,
hexToBuffer,
intToBuffer,
LITTLE_ENDIAN,
signDataWithPrivateKey,
verifyData,
} = require('@liskhq/lisk-cryptography');
const _ = require('lodash');
const BigNum = require('@liskhq/bignum');
const { validator } = require('@liskhq/lisk-validator');
const { validateTransactions } = require('../transactions');
const blockVersion = require('./block_version');
// TODO: remove type constraints
const TRANSACTION_TYPES_MULTI = 4;
const blockSchema = {
type: 'object',
properties: {
id: {
type: 'string',
format: 'id',
minLength: 1,
maxLength: 20,
},
height: {
type: 'integer',
},
blockSignature: {
type: 'string',
format: 'signature',
},
generatorPublicKey: {
type: 'string',
format: 'publicKey',
},
numberOfTransactions: {
type: 'integer',
},
payloadHash: {
type: 'string',
format: 'hex',
},
payloadLength: {
type: 'integer',
},
previousBlock: {
type: 'string',
format: 'id',
minLength: 1,
maxLength: 20,
},
timestamp: {
type: 'integer',
},
totalAmount: {
type: 'object',
format: 'amount',
},
totalFee: {
type: 'object',
format: 'amount',
},
reward: {
type: 'object',
format: 'amount',
},
transactions: {
type: 'array',
uniqueItems: true,
},
version: {
type: 'integer',
minimum: 0,
},
},
required: [
'blockSignature',
'generatorPublicKey',
'numberOfTransactions',
'payloadHash',
'payloadLength',
'timestamp',
'totalAmount',
'totalFee',
'reward',
'transactions',
'version',
],
};
/**
* Block headers buffer size and endianness
*
* BLOCK_VERSION_LENGTH = 4 (LITTLE_ENDIAN);
* TIMESTAMP_LENGTH = 4 (LITTLE_ENDIAN);
* PREVIOUS_BLOCK_LENGTH = 8 (BIG_ENDIAN);
* NUMBERS_OF_TRANSACTIONS_LENGTH = 4 (LITTLE_ENDIAN);
* TOTAL_AMOUNT_LENGTH = 8 (LITTLE_ENDIAN);
* TOTAL_FEE_LENGTH = 8 (LITTLE_ENDIAN);
* REWARD_LENGTH = 8 (LITTLE_ENDIAN);
* PAYLOAD_LENGTH_LENGTH = 4 (LITTLE_ENDIAN);
* PAYLOAD_HASH_LENGTH = 32 (BIG_ENDIAN);
* GENERATOR_PUBLIC_KEY_LENGTH = 32 (BIG_ENDIAN);
* BLOCK_SIGNATURE_LENGTH = 64 (BIG_ENDIAN);
* UNUSED_LENGTH = 4;
*/
const SIZE_INT32 = 4;
const SIZE_INT64 = 8;
/**
* Description of the function.
*
* @param {block} block
* @throws {Error}
* @returns {!Array} Contents as an ArrayBuffer
* @todo Add description for the function and the params
*/
const getBytes = block => {
const blockVersionBuffer = intToBuffer(
block.version,
SIZE_INT32,
LITTLE_ENDIAN,
);
const timestampBuffer = intToBuffer(
block.timestamp,
SIZE_INT32,
LITTLE_ENDIAN,
);
const previousBlockBuffer = block.previousBlock
? intToBuffer(block.previousBlock, SIZE_INT64, BIG_ENDIAN)
: Buffer.alloc(SIZE_INT64);
const numTransactionsBuffer = intToBuffer(
block.numberOfTransactions,
SIZE_INT32,
LITTLE_ENDIAN,
);
const totalAmountBuffer = intToBuffer(
block.totalAmount.toString(),
SIZE_INT64,
LITTLE_ENDIAN,
);
const totalFeeBuffer = intToBuffer(
block.totalFee.toString(),
SIZE_INT64,
LITTLE_ENDIAN,
);
const rewardBuffer = intToBuffer(
block.reward.toString(),
SIZE_INT64,
LITTLE_ENDIAN,
);
const payloadLengthBuffer = intToBuffer(
block.payloadLength,
SIZE_INT32,
LITTLE_ENDIAN,
);
const payloadHashBuffer = hexToBuffer(block.payloadHash);
const generatorPublicKeyBuffer = hexToBuffer(block.generatorPublicKey);
const blockSignatureBuffer = block.blockSignature
? hexToBuffer(block.blockSignature)
: Buffer.alloc(0);
return Buffer.concat([
blockVersionBuffer,
timestampBuffer,
previousBlockBuffer,
numTransactionsBuffer,
totalAmountBuffer,
totalFeeBuffer,
rewardBuffer,
payloadLengthBuffer,
payloadHashBuffer,
generatorPublicKeyBuffer,
blockSignatureBuffer,
]);
};
/**
* Creates a block signature.
*
* @param {block} block
* @param {Object} keypair
* @returns {signature} Block signature
* @todo Add description for the params
*/
const sign = (block, keypair) =>
signDataWithPrivateKey(hash(getBytes(block)), keypair.privateKey);
/**
* Creates hash based on block bytes.
*
* @param {block} block
* @returns {Buffer} SHA256 hash
* @todo Add description for the params
*/
const getHash = block => hash(getBytes(block));
/**
* Description of the function.
*
* @param {block} block
* @throws {string|Error}
* @returns {Object} Normalized block
* @todo Add description for the function and the params
*/
const objectNormalize = (block, exceptions = {}) => {
Object.keys(block).forEach(key => {
if (block[key] === null || typeof block[key] === 'undefined') {
delete block[key];
}
});
const errors = validator.validate(blockSchema, block);
if (errors.length) {
throw errors;
}
const { transactionsResponses } = validateTransactions(exceptions)(
block.transactions,
);
const invalidTransactionResponse = transactionsResponses.find(
transactionResponse => transactionResponse.status !== TransactionStatus.OK,
);
if (invalidTransactionResponse) {
throw invalidTransactionResponse.errors;
}
return block;
};
/**
* Sorts transactions for later including in the block.
*
* @param {Array} transactions Unsorted collection of transactions
* @returns {Array} transactions Sorted collection of transactions
* @static
*/
const sortTransactions = transactions =>
transactions.sort((a, b) => {
// Place MULTI transaction after all other transaction types
if (
a.type === TRANSACTION_TYPES_MULTI &&
b.type !== TRANSACTION_TYPES_MULTI
) {
return 1;
}
// Place all other transaction types before MULTI transaction
if (
a.type !== TRANSACTION_TYPES_MULTI &&
b.type === TRANSACTION_TYPES_MULTI
) {
return -1;
}
// Place depending on type (lower first)
if (a.type < b.type) {
return -1;
}
if (a.type > b.type) {
return 1;
}
// Place depending on amount (lower first)
if (a.amount.lt(b.amount)) {
return -1;
}
if (a.amount.gt(b.amount)) {
return 1;
}
return 0;
});
/**
* Sorts input data transactions.
* Calculates reward based on previous block data.
* Generates new block.
*
* @param {Object} data
* @returns {block} block
* @todo Add description for the params
*/
const create = ({
blockReward,
transactions,
previousBlock,
keypair,
timestamp,
maxPayloadLength,
exceptions,
}) => {
// TODO: move to transactions module logic
const sortedTransactions = sortTransactions(transactions);
const nextHeight = previousBlock ? previousBlock.height + 1 : 1;
const reward = blockReward.calculateReward(nextHeight);
let totalFee = new BigNum(0);
let totalAmount = new BigNum(0);
let size = 0;
const blockTransactions = [];
const transactionsBytesArray = [];
// eslint-disable-next-line no-plusplus
for (let i = 0; i < sortedTransactions.length; i++) {
const transaction = sortedTransactions[i];
const transactionBytes = transaction.getBytes(transaction);
if (size + transactionBytes.length > maxPayloadLength) {
break;
}
size += transactionBytes.length;
totalFee = totalFee.plus(transaction.fee);
totalAmount = totalAmount.plus(transaction.amount);
blockTransactions.push(transaction);
transactionsBytesArray.push(transactionBytes);
}
const transactionsBuffer = Buffer.concat(transactionsBytesArray);
const payloadHash = hash(transactionsBuffer).toString('hex');
const block = {
version: blockVersion.currentBlockVersion,
totalAmount,
totalFee,
reward,
payloadHash,
timestamp,
numberOfTransactions: blockTransactions.length,
payloadLength: size,
previousBlock: previousBlock.id,
generatorPublicKey: keypair.publicKey.toString('hex'),
transactions: blockTransactions,
};
block.blockSignature = sign(block, keypair);
return objectNormalize(block, exceptions);
};
/**
* Verifies block hash, generator block publicKey and block signature.
*
* @param {block} block
* @throws {Error}
* @returns {boolean} Verified hash, signature and publicKey
* @todo Add description for the params
*/
const verifySignature = block => {
const signatureLength = 64;
const data = getBytes(block);
const dataWithoutSignature = Buffer.alloc(data.length - signatureLength);
// eslint-disable-next-line no-plusplus
for (let i = 0; i < dataWithoutSignature.length; i++) {
dataWithoutSignature[i] = data[i];
}
const hashedBlock = hash(dataWithoutSignature);
return verifyData(
hashedBlock,
block.blockSignature,
block.generatorPublicKey,
);
};
/**
* Calculates block id based on block.
*
* @param {block} block
* @returns {string} Block id
* @todo Add description for the params
*/
const getId = block => {
const hashedBlock = hash(getBytes(block));
const temp = Buffer.alloc(8);
// eslint-disable-next-line no-plusplus
for (let i = 0; i < 8; i++) {
temp[i] = hashedBlock[7 - i];
}
// eslint-disable-next-line new-cap
const id = new BigNum.fromBuffer(temp).toString();
return id;
};
/**
* Creates block object based on raw data.
*
* @param {Object} raw
* @returns {null|block} Block object
* @todo Add description for the params
*/
const dbRead = raw => {
if (!raw.b_id) {
return null;
}
const block = {
id: raw.b_id,
version: parseInt(raw.b_version, 10),
timestamp: parseInt(raw.b_timestamp, 10),
height: parseInt(raw.b_height, 10),
previousBlock: raw.b_previousBlock,
numberOfTransactions: parseInt(raw.b_numberOfTransactions, 10),
totalAmount: new BigNum(raw.b_totalAmount),
totalFee: new BigNum(raw.b_totalFee),
reward: new BigNum(raw.b_reward),
payloadLength: parseInt(raw.b_payloadLength, 10),
payloadHash: raw.b_payloadHash,
generatorPublicKey: raw.b_generatorPublicKey,
generatorId: getAddressFromPublicKey(raw.b_generatorPublicKey),
blockSignature: raw.b_blockSignature,
confirmations: parseInt(raw.b_confirmations, 10),
};
block.totalForged = block.totalFee.plus(block.reward).toString();
return block;
};
/**
* Creates block object based on raw database block data.
*
* @param {Object} raw Raw database data block object
* @returns {null|block} Block object
*/
const storageRead = raw => {
if (!raw.id) {
return null;
}
const block = {
id: raw.id,
version: parseInt(raw.version, 10),
timestamp: parseInt(raw.timestamp, 10),
height: parseInt(raw.height, 10),
previousBlock: raw.previousBlockId,
numberOfTransactions: parseInt(raw.numberOfTransactions, 10),
totalAmount: new BigNum(raw.totalAmount),
totalFee: new BigNum(raw.totalFee),
reward: new BigNum(raw.reward),
payloadLength: parseInt(raw.payloadLength, 10),
payloadHash: raw.payloadHash,
generatorPublicKey: raw.generatorPublicKey,
generatorId: getAddressFromPublicKey(raw.generatorPublicKey),
blockSignature: raw.blockSignature,
confirmations: parseInt(raw.confirmations, 10),
};
if (raw.transactions) {
block.transactions = raw.transactions
.filter(tx => !!tx.id)
.map(tx => _.omitBy(tx, _.isNull));
}
block.totalForged = block.totalFee.plus(block.reward).toString();
return block;
};
module.exports = {
sign,
getHash,
getId,
create,
dbRead,
storageRead,
sortTransactions,
getBytes,
verifySignature,
objectNormalize,
};