Hello everyone,
I would like to propose a new LIP for the roadmap objective “Improve consensus protocol”. This LIP introduces the “Dynamic Block Rewards module” that is responsible for providing the base reward system for the Lisk blockchain according to the delegate weight.
I’m looking forward to your feedback.
Here is the complete LIP draft:
LIP: <LIP number>
Title: Introduce dynamic block rewards module
Author: Vikas Jaiman <vikas.jaiman@lightcurve.io>
Type: Standards Track
Created: <YYYY-MM-DD>
Updated: <YYYY-MM-DD>
Required: LIP 0044, LIP 0051, LIP 0057, LIP 0058
Abstract
This LIP introduces the Dynamic Block Rewards module that is responsible for providing the base reward system for the Lisk blockchain according to the delegate weight. In this LIP, we specify the properties of the Dynamic Block Rewards module, along with their default values. Furthermore, we specify the protocol logic injected during the block lifecycle, and the functions that can be called from other modules or off-chain services. This module is mainly used for Delegate Proof-of-Stake (DPoS) in the Lisk ecosystem.
Copyright
This LIP is licensed under the Creative Commons Zero 1.0 Universal.
Motivation
In the current Lisk protocol, the rewards are the same for all active and standby delegates (selected according to the selection mechanism described in LIP 0022 and LIP 0023) who forge a block in a round, irrespective of their delegate weight. In the current scenario, a delegate with a large delegate weight does not have incentives to lock further tokens via self-votes as the rewards are fixed. Moreover, delegates with a large number of tokens are not motivated enough to lock more tokens unless they run multiple delegates which is not desirable for them due to operational costs and maintenance efforts. Thus, the LIP aims to simplify DPoS participation for delegates to lock the large number of tokens. By more participation, we expect to increase the Total Value Locked (TVL) and therefore, the security of the Lisk Mainchain. To achieve this, we assign block rewards to also depend on the weight of the delegate generating the block.
Rationale
The main purpose of Dynamic Block Rewards module is to compute the block rewards according to the delegate weight for active delegates and induces specific logic after the application of a block:
- This module assigns a minimum reward i.e.
MINIMUM_REWARD_ACTIVE_DELEGATES
per round to all active delegates to cover the minimal operational cost to run a delegate. It is calculated asFACTOR_MINIMUM_REWARD_ACTIVE_DELEGATES * getDefaultRewardAtHeight(blockHeader.height)
whereFACTOR_MINIMUM_REWARD_ACTIVE_DELEGATES
is the fractional parameter for calculating minimum rewards of all active delegates. TheFACTOR_MINIMUM_REWARD_ACTIVE_DELEGATES
is chosen to1000
i.e.10%
for the Lisk mainchain and can be configurable for sidechains. For technical convenience, the fractionFACTOR_MINIMUM_REWARD_ACTIVE_DELEGATES
is represented as an integer that takes values from 0 to 10000 and we use two decimals precision, i.e.,FACTOR_MINIMUM_REWARD_ACTIVE_DELEGATES/100
determines the percentage of minimum rewards assigned equally to all active delegates. In addition, the module distributes the remaining rewards i.e.STAKE_REWARD_ACTIVE_DELEGATES
proportional to the delegate weights. This way, the module achieves fairness (higher weight implies higher rewards) while ensuring that rewards of low-weight delegates do not go below the threshold of profitability. - In principle, if
FACTOR_MINIMUM_REWARD_ACTIVE_DELEGATES == 0
i.e.0%
, it will be a proof-of-stake system where rewards are proportional to the delegate weight. Moreover, ifFACTOR_MINIMUM_REWARD_ACTIVE_DELEGATES == 10000
i.e.100%
, it will be the current delegate proof-of-stake (DPoS) system where each delegate will receive the same reward for forging a block. - This module assigns
DEFAULT_REWARD_STANDBY_DELEGATES
to all stand-by delegates i.e. there is a fixed reward irrespective of the delegate weight. This will help stand-by delegates to cover the costs of running a node even though the probability of the selection is low in a round. - Currently, in the Lisk protocol if a delegate misses a block, some other delegates forge the block and the delegate gets
getDefaultRewardAtHeight(blockHeader.height)
(1 LSK
for mainchain) as a reward. The reason is, the total rewards shouldn’t exceed in a round it if a delegate misses a block. Now, in the dynamic block rewards, delegates get rewards proportional to the delegate weight. If a delegate misses a block then a delegate forging the block second time will get the reward proportional to their weight. This results in imbalancing the total reward in a round. In terms of inflation, currently in the Lisk mainchain, there is a maximum minting of6*60*24*365 = 3153600
LSK per year. Every time a delegate misses their block, 1 LSK less is minted per year. So the important aspect that we want to guarantee here is that in the dynamic block rewards also never more than3153600
LSK are minted per year so that there is a reliable upper bound on the inflation. In terms of incentives, it is also important that it should never be profitable for a delegate if someone misses a block. Therefore, for missed blocks, delegates forging blocks for the second time in the round, getsMINIMUM_REWARD_ACTIVE_DELEGATES
. Since delegates are already running full nodes and there is no additional effort for them to forge a missed block. There is a possibility that the minting of the tokens will be less if there are missed blocks. - After applying the block, a certain quantity of the native token of the blockchain is minted. The exact amount assigned to the block generator, i.e. the reward is calculated depending on the rules of the Random module and the Lisk-BFT protocol. Specifically, the reward is reduced for not participating in BFT block finalization or failure to properly reveal the random seed. This reduction happens exactly as before mentioned in the previous Reward module. After minting the block rewards,
updateSharedRewards
function is called from the reward sharing mechanism of the DPoS module which locks the amount of the rewards which will be shared with the voters.
Specification
In this section, we specify the protocol logic in the lifecycle of a block injected by the Dynamic Block Rewards module as well as the functions that can be called from off-chain services. It depends on the Token, Random module, and Lisk-BFT protocol. (see the table below).
Notation and Constants
We define the following constants:
Name | Type | Value | Description |
---|---|---|---|
MODULE_NAME_DYNAMIC_BLOCK_REWARDS |
string | “dynamicBlockRewards” | The module name of the Dynamic Block Rewards module. |
EVENT_NAME_REWARD_MINTED |
string | “rewardMinted” | Name of the event during minting of the rewards. |
REWARD_NO_REDUCTION |
uint32 | 0 | Return code for no block reward reduction. |
REWARD_REDUCTION_SEED_REVEAL |
uint32 | 1 | Return code for block reward reduction because of the failed seed reveal. |
REWARD_REDUCTION_MAX_PREVOTES |
uint32 | 2 | Return code for block reward reduction because the block header does not imply the maximal number of prevotes. |
REWARD_REDUCTION_FACTOR_BFT |
uint32 | 4 | The reduction factor for validator block reward in case when the block header does not imply the maximal number of prevotes. |
SUBSTORE_PREFIX_END_OF_ROUND_TIMESTAMP |
bytes | 0xe000 | The substore prefix of the End-Of-Round timestamp substore. |
SUBSTORE_PREFIX_BLOCK_REWARD |
bytes | 0xf000 | The substore prefix of the block reward substore. |
Configurable Constants | Mainchain Value | ||
FACTOR_MINIMUM_REWARD_ACTIVE_DELEGATES |
uint32 | 1000 |
It determines the percentage of rewards assigned equally to all active delegates. The percentage can be obtained by dividing the value by 100 , i.e., a value of 1000 corresponds to 10% . |
MINIMUM_REWARD_ACTIVE_DELEGATES |
uint64 | 10^7 |
The minimum reward (in beddows) per round assigned to all active delegates. It is calculated as FACTOR_MINIMUM_REWARD_ACTIVE_DELEGATES * getDefaultRewardAtHeight(blockHeader.height)//10000 where blockHeader.height is the current block height. |
DEFAULT_REWARD_STANDBY_DELEGATES |
uint64 | 10^8 |
The default reward (in beddows) per round assigned to all stand-by delegates. It is calculated as getDefaultRewardAtHeight(blockHeader.height) where blockHeader.height is the current block height. |
TOTAL_REWARD_ACTIVE_DELEGATES |
uint64 | 101*10^8 |
The total reward of the active delegates (per round) is calculated as NUMBER_ACTIVE_DELEGATES * getDefaultRewardAtHeight(blockHeader.height) where blockHeader.height is the current block height. |
STAKE_REWARD_ACTIVE_DELEGATES |
uint64 | 90.9*10^8 |
The remaining stake rewards for the active delegates (per round) after giving the minimum reward. It is calculated as TOTAL_REWARD_ACTIVE_DELEGATES - NUMBER_ACTIVE_DELEGATES * MINIMUM_REWARD_ACTIVE_DELEGATES
|
TOKEN_ID_DYNAMIC_BLOCK_REWARD |
bytes | TOKEN_ID_LSK = 0x 00 00 00 00 00 00 00 00 |
The token ID of the token used for the reward system of the Blockchain. |
NUMBER_ACTIVE_DELEGATES |
uint32 | 101 |
The number of active delegates (as defined in DPoS module). |
ROUND_LENGTH |
uint32 | 103 |
The round length (as defined in DPoS module). |
Functions from Other Modules
Calling a function fct
from another module (named module
) is represented by module.fct(required inputs)
.
Dynamic Block Rewards Module Store
The key-value pairs in the module store are organized as follows:
End-Of-Round Timestamp Substore
Substore Prefix, Store Key, and Store Value
The entry in the end-of-round timestamp substore is as follows:
- The substore prefix is set to
SUBSTORE_PREFIX_END_OF_ROUND_TIMESTAMP
. - The store key is set to empty bytes.
- The store value is the serialization of an object following
endOfRoundTimestampStoreSchema
- Notation: For the rest of this proposal, let
endOfRoundTimestamp
be thetimestamp
property of the entry in the end-of-round timestamp substore.
JSON Schema
endOfRoundTimestampStoreSchema = {
"type": "object",
"required": ["timestamp"],
"properties": {
"timestamp": {
"dataType": "uint32",
"fieldNumber": 1
}
}
}
Properties
-
timestamp
: The timestamp of the last block in a round.
Block Reward Substore
Substore Prefix, Store Key, and Store Value
The entry in the block reward substore is as follows:
- The substore prefix is set to
SUBSTORE_PREFIX_BLOCK_REWARD
. - The store key is set to empty bytes.
- The store value is the serialization of an object following
blockRewardStoreSchema
- Notation: For the rest of this proposal, let
blockReward
be thereward
property of the entry in the block reward substore.
JSON Schema
blockRewardSchema = {
"type": "object",
"required": ["reward"],
"properties": {
"reward": {
"dataType": "uint64",
"fieldNumber": 1
}
}
}
Properties
-
reward
: The reward of a block in a round.
Token for Rewards
The Dynamic Block Rewards module triggers the minting of rewards in the fungible token identified by the value of TOKEN_ID_DYNAMIC_BLOCK_REWARD
, which denotes a token ID. The value of TOKEN_ID_DYNAMIC_BLOCK_REWARD
is set according to the initial configuration of the Dynamic Block Rewards module.
Reward Brackets
As part of the Dynamic Block Rewards module configuration, the module has to define certain reward brackets, i.e., the values of the default block reward depending on the height of the block. For this LIP, we assume the reward brackets are given by the function getDefaultRewardAtHeight
, which returns a 64-bit unsigned integer value, the default block reward, given the block height as input.
Lisk Mainchain Configuration
Mainchain Rewards Token
The token for rewards on the Lisk mainchain is the LSK token.
Mainchain Reward Brackets
The reward brackets for the Lisk Mainchain are as follows:
Height | Default Reward |
---|---|
From 1,451,520 to 4,451,519 | 5 × 108 |
From 4,451,520 to 7,451,519 | 4 × 108 |
From 7,451,520 to 10,451,519 | 3 × 108 |
From 10,451,520 to 13,451,519 | 2 × 108 |
From 13,451,520 onwards | 1 × 108 |
This corresponds to default rewards of 5 LSK, 4 LSK, 3 LSK, 2 LSK, and 1 LSK respectively.
Commands
This module does not define any command.
Events
rewardMinted
The event is emitted when the reward is minted. In case of a reward reduction, it provides information about the reason for the reduction. This event has the name = EVENT_NAME_REWARD_MINTED
.
Topics
-
generatorAddress
: the address of the block generator that obtains the reward.
Data
rewardMintedDataSchema = {
"type": "object",
"required": ["amount", "reduction"],
"properties": {
"amount": {
"dataType": "uint64",
"fieldNumber": 1
},
"reduction": {
"dataType": "uint32",
"fieldNumber": 2
}
}
}
-
amount
: the amount of rewards minted. -
reduction
: an integer indicating whether the reward was reduced and for which reason. Allowed values are:REWARD_NO_REDUCTION
,REWARD_REDUCTION_SEED_REVEAL
,REWARD_REDUCTION_MAX_PREVOTES
.
Internal Functions
The Dynamic Block Rewards module has the following internal functions.
getDynamicBlockReward
This function is used to retrieve the reward of a block for a delegate.
Returns
-
amount
: amount of block reward to be minted. -
reduction
: an integer indicating whether the reward is reduced. Possible values areREWARD_NO_REDUCTION, REWARD_REDUCTION_SEED_REVEAL, REWARD_REDUCTION_MAX_PREVOTES.
def getDynamicBlockReward(blockHeader: BlockHeader)-> tuple[uint64, uint32]:
if Random.isSeedRevealValid(blockHeader.generatorAddress, blockHeader.seedReveal) == False:
return (0, REWARD_REDUCTION_SEED_REVEAL)
defaultReward = getDefaultBlockReward(blockHeader)
if blockHeader.impliesMaximalPrevotes == False:
return (defaultReward // REWARD_REDUCTION_FACTOR_BFT, REWARD_REDUCTION_MAX_PREVOTES)
return (defaultReward, REWARD_NO_REDUCTION)
Here //
represents integer division.
getDefaultBlockReward
This function is used to retrieve the default block reward for a delegate.
Returns
-
reward:
amount of default block reward.
def getDefaultBlockReward(blockHeader: BlockHeader) -> uint64:
if len(Validators.getGeneratorsBetweenTimestamps(endOfRoundTimestamp, blockHeader.timestamp)) >= ROUND_LENGTH:
return MINIMUM_REWARD_ACTIVE_DELEGATES
validatorParams = Validators.getValidatorParams()
totalBftWeight = sum(validator.bftWeight for validator in validatorParams.validators)
let validator be the item of validatorParams.validators with item.address == blockHeader.generatorAddress
if validator.bftWeight > 0:
reward = MINIMUM_REWARD_ACTIVE_DELEGATES + (validator.bftWeight*STAKE_REWARD_ACTIVE_DELEGATES)//totalBftWeight
return reward
else:
return DEFAULT_REWARD_STANDBY_DELEGATES
Protocol Logic for Other Modules
The Dynamic Block Rewards module exposes the following functions for other modules.
getDefaultRewardAtHeight
This function is used to retrieve the expected default reward at a given height.
Parameters
The height of a block as a 32-bit unsigned integer.
Returns
The default reward of the block as a 64-bit unsigned integer.
Endpoints for Off-Chain Services
TBD.
Genesis Block Processing
Genesis State Initialization
After the genesis block b
is executed, the following logic is executed:
- Create the entry in the end-of-round timestamp substore, setting
endOfRoundTimestamp.timestamp
tob.header.timestamp
.
Genesis State Finalization
The Dynamic Block Rewards module does not execute any logic during the genesis state finalization.
Block Processing
Before Transactions Execution
Before the transactions of a block b
are executed, the following logic is executed:
def beforeTransactionsExecute(b: Block) -> None:
#update blockReward value of the block reward substore
(blockReward, reduction) = getDynamicBlockReward(b.header)
After Transactions Execution
The following function assigns block rewards after all transactions in the block are executed.
def afterTransactionsExecute(b: Block) -> None:
if blockReward > 0:
Token.mint(b.header.generatorAddress, TOKEN_ID_DYNAMIC_BLOCK_REWARD, blockReward)
DPoS.updateSharedRewards(b.header.generatorAddress, TOKEN_ID_DYNAMIC_BLOCK_REWARD, blockReward)
if DPoS.isEndOfRound(b.header.height):
#update the entry in the End-Of-Round timestamp substore
endOfRoundTimestamp = b.header.timestamp
emitEvent(
module = MODULE_NAME_DYNAMIC_BLOCK_REWARDS,
name = EVENT_NAME_REWARD_MINTED,
data = {
"amount": blockReward,
"reduction": reduction
},
topics = [b.header.generatorAddress]
)
Backwards Compatibility
This LIP defines the interface for the Dynamic Block Rewards module but does not introduce any change to the protocol, hence it is a backward compatible change.