leasehold-chain
Version:
Leasehold sidechain
540 lines (486 loc) • 13.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 Promise = require('bluebird');
const { getAddressFromPublicKey } = require('@liskhq/lisk-cryptography');
const BigNum = require('@liskhq/bignum');
/**
* Validates required scope properties.
*
* @class
* @memberof rounds
* @see Parent: {@link rounds}
* @requires bluebird
* @param {Object} scope
* @param {Task} t
* @todo Add description for the params
*/
class Round {
constructor(scope, t) {
this.scope = {
backwards: scope.backwards,
round: scope.round,
roundOutsiders: scope.roundOutsiders,
roundDelegates: scope.roundDelegates,
roundFees: scope.roundFees,
roundRewards: scope.roundRewards,
library: {
account: scope.library.account,
logger: scope.library.logger,
storage: scope.library.storage,
},
block: {
generatorPublicKey: scope.block.generatorPublicKey,
id: scope.block.id,
height: scope.block.height,
timestamp: scope.block.timestamp,
},
constants: scope.library.constants,
exceptions: scope.library.exceptions,
};
this.t = t;
// List of required scope properties
let requiredProperties = ['library', 'block', 'round', 'backwards'];
// Require extra scope properties when finishing round
if (scope.finishRound) {
requiredProperties = requiredProperties.concat([
'roundFees',
'roundRewards',
'roundDelegates',
'roundOutsiders',
]);
}
// Iterate over requiredProperties, checking for undefined scope properties
requiredProperties.forEach(property => {
if (scope[property] === undefined) {
throw new Error(`Missing required scope property: ${property}`);
}
});
}
/**
* Returns result from call to logic.account.merge.
*
* @returns {function} Promise
* @todo Check type and description of the return value
*/
mergeBlockGenerator() {
const self = this;
return new Promise((resolve, reject) => {
const data = {
publicKey: self.scope.block.generatorPublicKey,
producedBlocks: self.scope.backwards ? -1 : 1,
round: self.scope.round,
};
const address = getAddressFromPublicKey(data.publicKey);
self.scope.library.account.merge(
address,
data,
(err, account) => {
if (err) {
return reject(err);
}
return resolve(account);
},
self.t,
);
});
}
/**
* If outsiders content, calls sql updateMissedBlocks.
*
* @todo Add @returns tag
*/
updateMissedBlocks() {
if (this.scope.roundOutsiders.length === 0) {
return this.t;
}
const filters = { address_in: this.scope.roundOutsiders };
const field = 'missedBlocks';
const value = '1';
if (this.scope.backwards) {
return this.scope.library.storage.entities.Account.decreaseFieldBy(
filters,
field,
value,
this.t,
);
}
return this.scope.library.storage.entities.Account.increaseFieldBy(
filters,
field,
value,
this.t,
);
}
/**
* Calls sql getVotes from `mem_round` table.
*
* @todo Round must be a param option
* @todo Add @returns tag
*/
getVotes() {
return this.scope.library.storage.entities.Round.getTotalVotedAmount(
{ round: this.scope.round },
{},
this.t,
);
}
/**
* Calls getVotes with round.
*
* @returns {function} Promise
* @todo Check type and description of the return value
*/
updateVotes() {
const self = this;
return self.getVotes(self.scope.round).then(votes => {
const queries = votes.map(vote =>
self.scope.library.storage.entities.Account.increaseFieldBy(
{
address: getAddressFromPublicKey(vote.delegate),
},
'vote',
// Have to revert the logic to not use bignumber. it was causing change
// in vote amount. More details can be found on the issue.
// new Bignum(vote.amount).integerValue(Bignum.ROUND_FLOOR)
// TODO: https://github.com/LiskHQ/lisk/issues/2423
Math.floor(vote.amount),
this.t,
),
);
if (queries.length > 0) {
return self.t.batch(queries);
}
return self.t;
});
}
/**
* Calls sql flush:
* - Deletes round from `mem_round` table.
*
* @returns {function} Promise
* @todo Check type and description of the return value
*/
flushRound() {
return this.scope.library.storage.entities.Round.delete(
{
round: this.scope.round,
},
{},
this.t,
);
}
/**
* Calls sql restoreRoundSnapshot:
* - Restores mem_round table snapshot.
* - Performed only when rollback last block of round.
*
* @returns {function} Promise
* @todo Check type and description of the return value
*/
restoreRoundSnapshot() {
this.scope.library.logger.debug('Restoring mem_round snapshot...');
return this.scope.library.storage.entities.Round.restoreRoundSnapshot(
this.t,
);
}
/**
* Calls sql restoreVotesSnapshot:
* - Restores mem_accounts.votes snapshot.
* - Performed only when rollback last block of round.
*
* @returns {function} Promise
* @todo Check type and description of the return value
*/
restoreVotesSnapshot() {
this.scope.library.logger.debug('Restoring mem_accounts.vote snapshot...');
return this.scope.library.storage.entities.Round.restoreVotesSnapshot(
this.t,
);
}
/**
* Checks round snapshot availability for current round.
*
* @returns {Promise}
*/
checkSnapshotAvailability() {
return this.scope.library.storage.entities.Round.checkSnapshotAvailability(
this.scope.round,
this.t,
).then(isAvailable => {
if (!isAvailable) {
// Snapshot for current round is not available, check if round snapshot table is empty,
// because we need to allow to restore snapshot in that case (no transactions during entire round)
return this.scope.library.storage.entities.Round.countRoundSnapshot(
this.t,
).then(count => {
// Throw an error when round snapshot table is not empty
if (count) {
throw new Error(
`Snapshot for round ${this.scope.round} not available`,
);
}
});
}
return null;
});
}
/**
* Calls sql updateDelegatesRanks: Update current ranks of all delegates
*
* @returns {Promise}
*/
updateDelegatesRanks() {
this.scope.library.logger.debug('Updating ranks of all delegates...');
return this.scope.library.storage.entities.Account.syncDelegatesRanks(
this.t,
);
}
/**
* Calls sql deleteRoundRewards:
* - Removes rewards for entire round from round_rewards table.
* - Performed only when rollback last block of round.
* @returns {function} Promise
*/
deleteRoundRewards() {
this.scope.library.logger.debug(
`Deleting rewards for round ${this.scope.round}`,
);
return this.scope.library.storage.entities.Round.deleteRoundRewards(
this.scope.round,
this.t,
);
}
/**
* Calculates rewards at round position.
* Fees and feesRemaining based on slots.
*
* @param {number} index
* @returns {Object} With fees, feesRemaining, rewards, balance
*/
rewardsAtRound(index) {
let roundFees = Math.floor(this.scope.roundFees) || 0;
const roundRewards = [...this.scope.roundRewards] || [];
// Apply exception for round if required
if (this.scope.exceptions.rounds[this.scope.round.toString()]) {
// Apply rewards factor
roundRewards.forEach((reward, subIndex) => {
roundRewards[subIndex] = new BigNum(reward.toPrecision(15))
.times(
this.scope.exceptions.rounds[this.scope.round.toString()]
.rewards_factor,
)
.floor();
});
// Apply fees factor and bonus
roundFees = new BigNum(roundFees.toPrecision(15))
.times(
this.scope.exceptions.rounds[this.scope.round.toString()].fees_factor,
)
.plus(
this.scope.exceptions.rounds[this.scope.round.toString()].fees_bonus,
)
.floor();
}
const fees = new BigNum(roundFees.toPrecision(15))
.dividedBy(this.scope.constants.activeDelegates)
.floor();
const feesRemaining = new BigNum(roundFees.toPrecision(15)).minus(
fees.times(this.scope.constants.activeDelegates),
);
const rewards =
new BigNum(roundRewards[index].toPrecision(15)).floor() || 0;
return {
fees: Number(fees.toFixed()),
feesRemaining: Number(feesRemaining.toFixed()),
rewards: Number(rewards.toFixed()),
balance: Number(fees.plus(rewards).toFixed()),
};
}
/**
* For each delegate calls logic.account.merge and creates an address array.
*
* @returns {function} Promise with address array
*/
applyRound() {
const queries = [];
const self = this;
let changes;
let delegate;
let p;
const roundRewards = [];
// Reverse delegates if going backwards
const delegates = self.scope.backwards
? self.scope.roundDelegates.reverse()
: self.scope.roundDelegates;
// Reverse rewards if going backwards
if (self.scope.backwards) {
self.scope.roundRewards.reverse();
}
// Apply round changes to each delegate
// eslint-disable-next-line no-plusplus
for (let i = 0; i < self.scope.roundDelegates.length; i++) {
delegate = self.scope.roundDelegates[i];
changes = this.rewardsAtRound(i);
this.scope.library.logger.trace('Delegate changes', {
delegate,
changes,
});
const accountData = {
publicKey: delegate,
balance: self.scope.backwards ? -changes.balance : changes.balance,
round: self.scope.round,
fees: self.scope.backwards ? -changes.fees : changes.fees,
rewards: self.scope.backwards ? -changes.rewards : changes.rewards,
};
const address = getAddressFromPublicKey(accountData.publicKey);
p = new Promise((resolve, reject) => {
self.scope.library.account.merge(
address,
accountData,
(err, account) => {
if (err) {
return reject(err);
}
return resolve(account);
},
self.t,
);
});
queries.push(p);
// Aggregate round rewards data - when going forward
if (!self.scope.backwards) {
roundRewards.push({
timestamp: self.scope.block.timestamp,
fees: new BigNum(changes.fees).toString(),
reward: new BigNum(changes.rewards).toString(),
round: self.scope.round,
publicKey: delegate,
});
}
}
// Decide which delegate receives fees remainder
const remainderIndex = this.scope.backwards ? 0 : delegates.length - 1;
const remainderDelegate = delegates[remainderIndex];
// Get round changes for chosen delegate
changes = this.rewardsAtRound(remainderIndex);
// Apply fees remaining to chosen delegate
if (changes.feesRemaining > 0) {
const feesRemaining = this.scope.backwards
? -changes.feesRemaining
: changes.feesRemaining;
this.scope.library.logger.trace('Fees remaining', {
index: remainderIndex,
delegate: remainderDelegate,
fees: feesRemaining,
});
p = new Promise((resolve, reject) => {
const data = {
publicKey: remainderDelegate,
balance: feesRemaining,
round: self.scope.round,
fees: feesRemaining,
};
const address = getAddressFromPublicKey(data.publicKey);
self.scope.library.account.merge(
address,
data,
(err, account) => {
if (err) {
return reject(err);
}
return resolve(account);
},
self.t,
);
});
// Aggregate round rewards data (remaining fees) - when going forward
if (!self.scope.backwards) {
roundRewards[roundRewards.length - 1].fees = new BigNum(
roundRewards[roundRewards.length - 1].fees,
)
.plus(feesRemaining)
.toString();
}
queries.push(p);
}
// Prepare queries for inserting round rewards
roundRewards.forEach(item => {
queries.push(
self.scope.library.storage.entities.Round.createRoundRewards(
{
timestamp: item.timestamp,
fees: item.fees,
reward: item.reward,
round: item.round,
publicKey: item.publicKey,
},
self.t,
),
);
});
self.scope.library.logger.trace('Applying round', {
queries_count: queries.length,
rewards: roundRewards,
});
if (queries.length > 0) {
return this.t.batch(queries);
}
return this.t;
}
/**
* Calls:
* - updateVotes
* - updateMissedBlocks
* - flushRound
* - applyRound
* - updateVotes
* - flushRound
*
* @returns {function} Call result
*/
land() {
return this.updateVotes()
.then(this.updateMissedBlocks.bind(this))
.then(this.flushRound.bind(this))
.then(this.applyRound.bind(this))
.then(this.updateVotes.bind(this))
.then(this.flushRound.bind(this))
.then(this.updateDelegatesRanks.bind(this))
.then(() => this.t);
}
/**
* Calls:
* - applyRound
* - flushRound
* - checkSnapshotAvailability
* - restoreRoundSnapshot
* - restoreVotesSnapshot
* - deleteRoundRewards
*
* @returns {function} Call result
*/
backwardLand() {
return Promise.resolve()
.then(this.applyRound.bind(this))
.then(this.flushRound.bind(this))
.then(this.checkSnapshotAvailability.bind(this))
.then(this.restoreRoundSnapshot.bind(this))
.then(this.restoreVotesSnapshot.bind(this))
.then(this.deleteRoundRewards.bind(this))
.then(this.updateDelegatesRanks.bind(this))
.then(() => this.t);
}
}
module.exports = Round;