Skip to content
This repository has been archived by the owner on Jun 11, 2024. It is now read-only.

Latest commit

Β 

History

History
403 lines (271 loc) Β· 21.1 KB

lip-0071.md

File metadata and controls

403 lines (271 loc) Β· 21.1 KB
LIP: 0071
Title: Introduce dynamic reward module
Author: Vikas Jaiman <[email protected]>
Discussions-To: https://research.lisk.com/t/introduce-dynamic-block-rewards-module/387
Status: Active
Type: Standards Track
Created: 2022-10-28
Updated: 2024-01-04
Required: LIP 0044, LIP 0051, LIP 0057, LIP 0058

Abstract

This LIP introduces the Dynamic Reward module that is responsible for providing the base reward system for the Lisk blockchain according to the validator weight. In this LIP, we specify the properties of the Dynamic Reward 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 Proof-of-Stake (PoS) 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 validators (selected according to the selection mechanism described in LIP 0022 and LIP 0023) who generate a block in a round, irrespective of their validator weight. In the current scenario, the rewards for generating a block are fixed and therefore a validator with a large validator weight does not have incentives to lock further tokens via self-stakes unless they run multiple validators, which is not desirable for them due to operational costs and maintenance efforts. Thus, this LIP aims to simplify PoS participation for validators that are interested in staking large amounts 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 validator generating the block.

Rationale

​​The main purpose of the Dynamic Reward module is to assign block rewards to active validators according to their weight and induces specific logic after the application of a block:

Minimum Reward for Active Validators

This module assigns a minimum reward to all active validators that generate a block to cover the minimal operational cost to run a validator. The minimum reward is a certain fraction (specified by the configurable constant FACTOR_MINIMUM_REWARD_ACTIVE_VALIDATORS) of the total reward in a round. For the Lisk Mainchain, this fraction is set to 10%. Therefore, for height with getDefaultRewardAtHeight(height) set to 1 LSK, the minimum reward is 107 beddows or 0.1 LSK.

Rewards Proportional to the Validators Weight

  • This module distributes the remaining staking rewards proportional to the validators weight. It is given to the active validators that generate a block, on top of the minimum reward. For the Lisk mainchain, the rewards distributed according to stake are 90% of total active validator rewards, assuming 101 active validators. Therefore 90.9*10^8 beddows or 90.9 LSK per round are normally distributed to active validators. This way, the module achieves fairness (higher weight implies higher rewards) while ensuring that rewards of low-weight validators do not go below the threshold of profitability.
  • Technically, each time an active validator generates a block they get a fraction of stake reward (depending on the current number of active validators), proportionally to their BFT weight. Note that an active validator might generate multiple blocks in a round (assuming no missed blocks): this could happen in case the total number of validators of a round are less than the expected one (which is roundLength). In such cases, the total reward paid out in a round could slightly exceed the estimate of defaultReward * roundLength, in case validators with higher than average weight generate more blocks than the others. However, since in our PoS systems the validators are randomly shuffled, the expected value of the total reward given in a round is not higher than defaultReward * roundLength.
  • In principle, if the percentage of rewards shared equally among active validators is set to 0%, i.e.FACTOR_MINIMUM_REWARD_ACTIVE_VALIDATORS == 0, then it will be a proof-of-stake system where rewards are proportional to the validator weight. On the other hand, if the percentage of rewards shared equally among active validators is set to 100% i.e. FACTOR_MINIMUM_REWARD_ACTIVE_VALIDATORS == 10000, it will be the previous proof-of-stake (PoS) system where each validator received the same reward for forging a block irrespective of their weights. Therefore, this reward system can be seen as a generalization of the previous one and allows flexibility to developers to use the variant of Proof-of-Stake they find more appropriate for their use case.

Default Reward for StandBy Validators

This module assigns the default reward getDefaultRewardAtHeight(blockHeader.height) at current block height to all stand-by validators, i.e. there is a fixed reward irrespective of the validator weight. For the Lisk mainchain, the default reward for standby validator is 108 beddows or 1 LSK at current block height. This will help stand-by validators to cover the costs of running a node even though the probability of the selection is low in a round.

Reward for Missed blocks

In the new protocol proposed in this LIP, if some validator misses a block and another validator generates the block, then the generator gets a minimal reward at current block height (0.1 LSK for the mainchain). This is done to limit the total rewards in a round if an active validator misses a block. Also it should never be profitable for a validator if someone misses a block.

Minting of the Rewards

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 Proof-of-Stake (PoS) module which locks the amount of the rewards which will be shared with the stakers.

Specification

In this section, we specify the protocol logic in the lifecycle of a block injected by the Dynamic Reward 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
Global Constants
ADDRESS_LENGTH uint32 20 Length in bytes of type Address.
Dynamic Reward Module Constants
MODULE_NAME_DYNAMIC_REWARD string "dynamicReward" The module name of the Dynamic Reward 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_NO_ACCOUNT uint32 3 Return code for block reward reduction because the TOKEN_ID_DYNAMIC_BLOCK_REWARD account is not initialized.
SUBSTORE_PREFIX_END_OF_ROUND_TIMESTAMP bytes 0x0000 The substore prefix of the End-Of-Round timestamp substore.
Configurable Constants Mainchain Value
BLOCK_TIME integer 10 Block time (in seconds) set in the chain configuration.
FACTOR_MINIMUM_REWARD_ACTIVE_VALIDATORS uint32 1000 It determines the percentage of rewards assigned equally to all active validators. The percentage can be obtained by dividing the value by 100, i.e., a value of 1000 corresponds to 10% .
TOKEN_ID_DYNAMIC_BLOCK_REWARD bytes TOKEN_ID_LSK The token ID of the token used for the reward system of the blockchain.
REWARD_REDUCTION_FACTOR_BFT uint32 4 The reduction factor for the block reward in case the block header does not imply the maximal number of prevotes.

Furthermore, for the rest of this LIP we indicate with ctx the execution context which is passed as extra input to each method call.

Type Definitions

Name Type Validation Description
BlockAssets object see LIP 0055 a block property
Address bytes Must be of length ADDRESS_LENGTH. Address of an account.

Functions from Other Modules

Calling a function fct from another module (named module) is represented by module.fct(required inputs).

Dynamic Reward 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 the timestamp property of the entry in the end-of-round timestamp substore, deserialized using the endOfRoundTimestampStoreSchema schema.
JSON Schema
endOfRoundTimestampStoreSchema = {
    "type": "object",
    "required": ["timestamp"],
    "properties": {
        "timestamp": {
            "dataType": "uint32",
            "fieldNumber": 1
        }
    }
}
Properties
  • timestamp: The timestamp of the last block in a round.

Token for Rewards

The Dynamic Reward 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 Reward module.

Reward Brackets

As part of the Dynamic Reward 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. The value of TOKEN_ID_DYNAMIC_BLOCK_REWARD is defined in LIP 0063.

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, REWARD_REDUCTION_NO_ACCOUNT.

Internal Functions

The Dynamic Reward module has the following internal functions.

getDynamicBlockReward

This function is used to retrieve the reward of a block for a validator.

Returns
  • amount: amount of block reward to be minted.
  • reduction: an integer indicating whether the reward is reduced and if yes, for which reason. Possible values are REWARD_NO_REDUCTION, and REWARD_REDUCTION_SEED_REVEAL.
Execution
def getDynamicBlockReward(blockHeader: BlockHeader, blockAssets: BlockAssets)-> tuple[uint64, uint32]:
    if Random.isSeedRevealValid(blockHeader.generatorAddress, blockAssets) == False:
        return (0, REWARD_REDUCTION_SEED_REVEAL)

    validatorReward = getValidatorBlockReward(blockHeader)

    if blockHeader.impliesMaxPrevotes == False:
        return (validatorReward // REWARD_REDUCTION_FACTOR_BFT, REWARD_REDUCTION_MAX_PREVOTES)

    return (validatorReward, REWARD_NO_REDUCTION)

Here // represents integer division.

getNumberActiveValidators

This function is used to return the number of active validators.

Execution
def getNumberActiveValidators() -> uint32:
        return len([item for item in Validators.getValidatorParams().validators if item.bftWeight > 0])

getMinimumRewardActiveValidators

This function is used to return the minimum reward per round for an active validator.

Execution
def getMinimumRewardActiveValidators(height: uint32) -> uint64:
        return (FACTOR_MINIMUM_REWARD_ACTIVE_VALIDATORS * getDefaultRewardAtHeight(height))// 10000

getStakeRewardActiveValidators

This function is used to return the remaining rewards for active validators (per round) after giving the minimum reward.

Execution
def getStakeRewardActiveValidators(height: uint32) -> uint64:
        return getNumberActiveValidators() * (getDefaultRewardAtHeight(height) -  getMinimumRewardActiveValidators(height))

getValidatorBlockReward

This function is used to retrieve the block reward for a validator.

Returns
  • reward: amount of block reward for a validator.
Execution
def getValidatorBlockReward(blockHeader: BlockHeader) -> uint64:
    height = blockHeader.height
    minimumRewardActiveValidators = getMinimumRewardActiveValidators(height)
    if len(Validators.getGeneratorsBetweenTimestamps(endOfRoundTimestamp, blockHeader.timestamp)) >= Pos.getRoundLength():
        return minimumRewardActiveValidators

    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: #active validator
        reward = minimumRewardActiveValidators + (validator.bftWeight * getStakeRewardActiveValidators(height))//totalBftWeight
        return reward
    else: # standby validator
        return getDefaultRewardAtHeight(height)

Protocol Logic for Other Modules

The Dynamic Reward 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

This section specifies the non-trivial or recommended endpoints of the module and does not include all endpoints.

getExpectedValidatorRewards

This function is used to estimate the expected rewards for a validator assuming the current BFT weight distribution. It provides an estimate of rewards for one block, one day, one month and one year.

Execution
def getExpectedValidatorRewards(validatorAddress: Address) -> tuple[uint64, uint64, uint64, uint64]:
    validatorParams = Validators.getValidatorParams()
    height = current height of the last block
    totalBftWeight = sum(validator.bftWeight for validator in validatorParams.validators)

    let validator be the item of validatorParams.validators with item.address == validatorAddress
    if not validator:
        return (0, 0, 0, 0)

    if validator.bftWeight > 0: #active validator
        blockReward = getMinimumRewardActiveValidators(height) + (validator.bftWeight * getStakeRewardActiveValidators(height))//totalBftWeight
    else: # standby validator
        blockReward = getDefaultRewardAtHeight(height)

    dailyReward =   86400 * blockReward // (Pos.getRoundLength * BLOCK_TIME)
    monthlyReward = 2592000 * blockReward // (Pos.getRoundLength * BLOCK_TIME)
    yearlyReward = 31536000 * blockReward // (Pos.getRoundLength * BLOCK_TIME)

    return (blockReward, dailyReward, monthlyReward, yearlyReward)

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 to b.header.timestamp.

Genesis State Finalization

The Dynamic Reward 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.
    (ctx.blockReward, ctx.reduction) = getDynamicBlockReward(b.header, b.blockAssets)

Here we calculate the reward for the block generator; then this value will be stored in the context in memory for the afterTransactionsExecute in order to mint this reward and assign it to the generator. The reason for calculating the reward here is to make sure that during the call to getDynamicBlockReward function, the Validators module takes the BFT weight from the PoS module. Therefore, if we calculate the reward in afterTransactionsExecute, there might be a case where BFT weights are being updated by PoS module before calculating the reward then the reward could be from the next/future round.

After Transactions Execution

The following function assigns block rewards after all transactions in the block are executed.

def afterTransactionsExecute(b: Block) -> None:
    if ctx.blockReward > 0:
        if Token.userSubstoreExists(b.header.generatorAddress, TOKEN_ID_DYNAMIC_BLOCK_REWARD):
            Token.mint(b.header.generatorAddress, TOKEN_ID_DYNAMIC_BLOCK_REWARD, ctx.blockReward)
            Pos.updateSharedRewards(b.header.generatorAddress, TOKEN_ID_DYNAMIC_BLOCK_REWARD, ctx.blockReward)
        else:
            ctx.blockReward = 0
            ctx.reduction = REWARD_REDUCTION_NO_ACCOUNT

    if Pos.isEndOfRound(b.header.height):
        # Update the entry in the End-Of-Round timestamp substore.
        endOfRoundTimestamp = b.header.timestamp

    emitEvent(
        module = MODULE_NAME_DYNAMIC_REWARD,
        name = EVENT_NAME_REWARD_MINTED,
        data = {
            "amount": ctx.blockReward,
            "reduction": ctx.reduction
        },
        topics = [b.header.generatorAddress]
    )

Backwards Compatibility

This LIP defines the interface for the Dynamic Reward module but does not introduce any change to the protocol, hence it is a backward compatible change.

Reference Implementation

Add dynamic reward module #7717