KIP 82: A new GC reward structure due to abolition of the Gini coefficient Source

AuthorYeri, Daniel, Aidan, Ollie, Sam, Uno, Eddie
StatusDraft
TypeStandards Track
CategoryCore
Created2022-09-21

Simple Summary

A renewal of the Governance Council(GC) reward structure due to abolition of the Gini coefficient.

Abstract

In addition to traditional enterprises, Klaytn is expanding the Governance Council (GC) by bringing DAOs in response to the growth of nontraditional entities. Such a change in the GC ecosystem will restructure the entire Klaytn governance structure. Abolishing the current block proposal selection method based on the Gini coefficient and staking quantity, the new reward program alleviates the problem of providing insufficient compensation for the network contribution and block verification due to lack of staking. The instability of certain nodes with high staking amounts can result in poor network stability as a whole. The renewed structure provides 20% of GC rewards to block proposers and the rest to the stakers. This form is to reward the participants for providing network stability with node operation and financial contribution.

Motivation

Klaytn has been compensating GC with block proposal rewards based on the quantity of staking and the Gini coefficient. This included rewards incurred by minting and gas fee generated by transactions. As mentioned in the Klaytn 2.0 Lightpaper, Klaytn is abolishing the Gini coefficient since this measure demotivates members from staking. Discontinuation of the Gini coefficient increases the risk to network stability due to the expanding reliance on certain nodes. This change ultimately calls for the revision of block proposer selection and reward structure. In an effort to enhance individual voting power and promote ultimate decentralization, we aim to accomplish the following changes:

  • Ameliorate the risk that block creation depends on larger staker nodes
  • Associate GC reward with the growth of the Klaytn ecosystem

Specification

Block generation is rewarded for the node operations that are serving as public goods in the Klaytn network. CNs and PNs are in charge of block proposal and propagation while ENs are in charge of blockchain data inquiry based on an individual’s needs. The infrastructure cost remains similar, enabling some fixed benefits. Therefore, fixed compensation will be equivalent to the sum of the transaction fee and an additional block compensation, which can be both inflationary and deflationary.

The block proposal method will be based on equal selection regardless of the staking amount. The current model provides a block generation reward based on staking amount and the Gini-coefficient. Until now, the Gini coefficient was utilized to alleviate the chance of an entity with more staking amount being selected as a block proposer. As this model provided a fairly equal chance to every participant, each node was promised a certain amount of rewards.

In the new method, the reward for node operation is required since the staking amount is not considered in the block proposer selection. Therefore, a certain percentage of the total reward amount will be placed as a node operation reward.

Node operators will also be compensated with the staking for enhancing network stability from an economic standpoint. The opportunity cost incurred from staking is rewarded based on inflation. In this system, inflation provides a tax-like common effect. Public users can also participate and receive rewards through the GC-run public staking program.

As of September 21st, 2022, of the 300 million KLAY that is minted annually, only 100 million KLAY goes to GC. The renewal separates GC reward into two components: block proposer reward and staking reward. The block proposer reward is compensating for node operation by providing 20% of the GC reward and 100% of gas fee resources from transactions. The staking reward is from 80% of the GC reward and is distributed based on GC staking amounts.

The relative ratio of 20% and 80% is a tentative value to be used in the Cypress mainnet. However, this ratio is defined as a governance paramter, so GC members can change the ratio through the governance process.

gc reward allocation

After the Magma hard fork, Klaytn has been burning the first half of the gas fee based on the KIP-71 proposal while the second half is distributed. The second half is distributed into GC reward, KGF and KIR.

In addition to changing the reward structure, this renewal will include burning the gas fee up to a threshold which is the size of block proposer reward. If the gas fee exceeds the threshold, remaining amount is rewarded to the block proposer. The KGF and KIR portion will be excluded from the gas fee.

Klaytn node update

The Klaytn node will be updated according to the new reward policy. The CN reward is further split into proposer reward and staking reward. The staking reward is distributed among CNs proportionally to their staking amounts minus the minimum staking amount.

A new governance parameter is introduced to tune the new reward distribution algorithm. The initial value below will be used in the Cypress mainnet when KIP-82 is first applied. The parameter can be updated using the existing governance mechanism.

Name Type Meaning Initial Value
reward.kip82ratio string The relative ratio between proposer and staking rewards “20/80”

During integer arithmetics, minuscule remaining amounts may be emitted as by-products. The proposer gets the remainder from the staking share disribution, and KGF gets the the remainder from the distribution among proposer, stakers, KIR, KGF.

Below pseudocode illustrates the new reward distribution algorithm. Note: // is round down integer division.

from collections import defaultdict

MAGMA_BLOCK_NUMBER = 99841497
KORE_BLOCK_NUMBER = 120000000 # TBD

# Block header. Only relevant fields are shown here.
class Header:
    Number: int = 0   # Block number
    GasUsed: int = 0  # Total gas spent in the block
    BaseFee: int = 0  # Base fee per gas at the block
    RewardBase: string = "" # Reward recipient address of the block proposer

# Network configurations related to reward distribution.
# Corresponds to Klaytn's reward.rewardConfig struct.
class RewardConfig:
    # "istanbul.policy" parameter
    RoundRobin = 0
    Sticky = 1
    WeightedRandom = 2
    ProposerPolicy: int = WeightedRandom

    UnitPrice: int = 25000000000              # "governance.unitprice" parameter (in peb)
    MintingAmount: int = 9600000000000000000  # "reward.mintingamount" parameter (in peb)
    MinimumStake: int = 5000000               # "reward.minimumstake" parameter (in KLAY)
    DeferredTxFee: bool = True                # "reward.deferredtxfee" parameter

    # "reward.ratio" parameter (e.g. "50/40/10")
    CnRatio: int = 50
    KgfRatio: int = 40
    KirRatio: int = 10
    TotalRatio: int = CnRatio + KgfRatio + KirRatio

    # "reward.kip82ratio" parameter (e.g. "20/80") (new)
    CnProposerRatio: int = 20
    CnStakingRatio: int = 80
    CnTotalRatio: int = CnProposerRatio + CnStakingRatio

# CN staking status and KGF/KIR addresses.
# Corresponds to Klaytn's reward.StakingInfo struct.
# Can be obtained from reward.GetStakingInfo(blockNum) function.
class StakingInfo:
    KIRAddr: string = ""
    KGFAddr: string = ""
    Nodes: List[ConsolidatedNode] = []

# Staking information merged under the same CN.
# Sometimes a node would register multiple NodeAddrs
# in which each entry has a different StakingAddr and the same RewardAddr.
# We treat those entries with common RewardAddr as one node.
# Corresponds to Klaytn's reward.ConsolidatedNode struct.
class ConsolidatedNode:
    NodeAddrs: List[string] = []
    StakingAddrs: List[string] = []
    RewardAddr: string = "" # The common reward address of the CN
    StakingAmount: int = 0  # Total staking amount across StakingAddrs (in KLAY)

# Reward distribution details.
class RewardSpec:
    Minted: int = 0   # The amount newly minted
    TotalFee: int = 0 # Total tx fee spent
    BurntFee: int = 0 # The amount burnt
    Proposer: int = 0 # The amount allocated to the block proposer
    Stakers: int = 0  # Total amount allocated to stakers
    Kgf: int = 0      # The amount allocated to KGF
    Kir: int = 0      # The amount allocated to KIR
    Rewards: Dict[string, int] = {}  # Mapping from reward recipient to amounts

# Perform post-transaction state modifications such as block rewards.
# Corresponds to Klaytn's consensus/istanbul/backend.Finalize()
#
# - state is the StateDB to apply the rewards.
# - header is a Header instance.
# - config is a RewardConfig instance.
def finalize_block(state, header, config):
    if config.ProposerPolicy in [RoundRobin, Sticky]:
        spec = calc_deferred_reward_simple(header, config)
    else:
        spec = calc_deferred_reward(header, config)

    for addr, amount in spec.Rewards:
        state.AddBalance(addr, amount)
    header.Root = state.Root()

def calc_deferred_reward_simple(header, config):
    minted = config.MintingAmount

    total_fee = get_total_fee(header)
    if header.Number >= KORE_BLOCK_NUMBER and not config.DeferredTxFee:
        total_fee = 0

    reward_fee = total_fee
    burnt_fee = 0

    if header.Number >= MAGMA_BLOCK_NUMBER:
        burn_amount = get_burn_amount_magma(reward_fee)
        reward_fee -= burn_amount
        burnt_fee += burn_amount

    proposer = minted + reward_fee

    spec = RewardSpec()
    spec.Minted = minted
    spec.TotalFee = total_fee
    spec.BurntFee = burnt_fee
    spec.Proposer = proposer
    spec.Rewards = {header.RewardBase: proposer}
    return spec

# Calculates the deferred rewards, which are determined at the end of block processing.
# Used in reward distribution.
# Returns a RewardSpec.
def calc_deferred_reward(header, config):
    staking_info = GetStakingInfo(header.Number)
    minted = config.MintingAmount
    total_fee, reward_fee, burnt_fee = calc_deferred_fee(header, config)

    proposer, stakers, kgf, kir, split_rem = calc_split(header, config, minted, reward_fee)
    shares, share_rem = calc_shares(config, staking_info, stakers)

    # Remainder from (CN, KGF, KIR) split goes to KGF
    kgf += split_rem
    # Remainder from staker shares goes to Proposer
    proposer += share_rem

    # If KGF or KIR address is not set, proposer gets the portion.
    if staking_info.KGFAddr is None:
        proposer += kgf
        kgf = 0
    if staking_info.KIRAddr is None:
        proposer += kir
        kir = 0

    spec = RewardSpec()
    spec.Minted = minted
    spec.TotalFee = total_fee
    spec.BurntFee = burnt_fee
    spec.Proposer = proposer
    spec.Stakers = stakers
    spec.Kgf = kgf
    spec.Kir = kir

    spec.Rewards = defaultdict(int)
    spec.Rewards[header.RewardBase] += proposer
    if staking_info.KGFAddr is not None:
        spec.Rewards[staking_info.KGFAddr] += kgf
    if staking_info.KIRAddr is not None:
        spec.Rewards[staking_info.KIRAddr] += kir
    for reward_addr, reward_amount in shares:
        spec.Rewards[reward_addr] += reward_amount

    return spec

# Returns (total_fee, reward_fee, burnt_fee)
def calc_deferred_fee(header, config):
    # If not DeferredTxFee, fees are already added to the proposer during TX execution.
    # Therefore, there are no fees to distribute here at the end of block processing.
    # However, the fees must be compensated to calculate actual rewards paid.
    if not config.DeferredTxFee:
        return (0, 0, 0)

    # Start with the total block gas fee
    total_fee = get_total_fee(header)
    reward_fee = total_fee
    burnt_fee = 0

    # Since Magma, burn half of gas
    if header.number >= MAGMA_BLOCK_NUMBER:
        burn_amount = get_burn_amount_magma(reward_fee)
        reward_fee -= burn_amount
        burnt_fee += burn_amount

    # If KIP-82 is enabled, burn fees up to proposer's minted reward
    if header.number >= KORE_BLOCK_NUMBER:
        burn_amount = get_burn_amount_kip82(config, reward_fee)
        reward_fee -= burn_amount
        burnt_fee += burn_amount

    return (total_fee, reward_fee, burnt_fee)

def get_total_fee(header):
    if header.number >= MAGMA_BLOCK_NUMBER:
        return header.GasUsed * header.BaseFee
    else:
        return header.GasUsed * config.UnitPrice

def get_burn_amount_magma(fee):
    return fee / 2

def get_burn_amount_kip82(config, fee):
    cn, _, _ = split_by_ratio(config, config.MintingAmount)
    proposer, _ = split_by_kip82_ratio(config, cn)
    if fee >= proposer:
        return proposer
    else:
        return fee

# Returns (proposer, stakers, kgf, kir, remaining) amounts
# The sum of output must be equal to (minted + reward_fee).
def calc_split(header, config, minted, reward_fee):
    total_resource = minted + reward_fee

    if header.number >= KORE_BLOCK_NUMBER:
        cn, kgf, kir = split_by_ratio(config, minted)
        proposer, stakers = split_by_kip82_ratio(config, cn)
        proposer += reward_fee

        remaining = total_resource - kgf - kir - proposer - stakers
        return (proposer, stakers, kgf, kir, remaining)
    else:
        cn, kgf, kir = split_by_ratio(config, minted + reward_fee)

        remaining = total_resource - kgf - kir - cn
        return (cn, 0, kgf, kir, remaining)

# Split by `ratio`. Ignore remaining amounts.
def split_by_ratio(config, source):
    cn = source * config.CnRatio // config.TotalRatio
    kgf = source * config.KgfRatio // config.TotalRatio
    kir = source * config.KirRatio // config.TotalRatio
    return (cn, kgf, kir)

# Split by `kip82ratio`. Ignore remaining amounts.
def split_by_kip82_ratio(config, source):
    proposer = source * config.CnProposerRatio // config.CnTotalRatio
    stakers = source * config.CnStakingRatio // config.CnTotalRatio
    return (proposer, stakers)

# Distribute stake_reward among staked CNs
# Returns a mapping from each reward address to their reward shares,
# and the remaining amount.
def calc_shares(config, staking_info, stake_reward):
    min_stake = config.MinimumStake
    total_stakes = 0
    for node in staking_info.Nodes:
        if node.StakingAmount > min_stake:
            total_stakes += (node.StakingAmount - min_stake)

    shares = {}
    remaining = stake_reward
    for node in staking_info.Nodes:
        if node.StakingAmount > min_stake:
            effective_stake = node.StakingAmount - min_stake
            reward_amount = stake_reward * effective_stake // total_stakes
            remaining -= reward_amount
            shares[node.RewardAddr] = reward_amount

    return (shares, remaining)

The updated algorithm increases the balance of every GC member’s reward account at every block. The performance impact should be reasonable since the number of GC members is significantly smaller than Klaytn’s transaction processing capability.

Reward JSON-RPC API

A new JSON-RPC method should be added to provide historic reward distribution details.

  • Name: klay_getRewards
  • Description: Returns allocation details of reward distribution at the specified block. If the parameter is not set, returns a breakdown of reward distribution at the latest block.
  • Parameters
    1. QUANTITY | TAG - (optional) integer or hexadecimal block number, or the string “earlist” or “latest”.
  • Returns
    • DATA
      • minted: The amount minted
      • totalFee: Total tx fee spent
      • burntFee: The amount burnt
      • proposer: The amount for the block proposer
      • stakers: Total amount for stakers
      • kgf: The amount for KGF
      • kir: The amount for KIR
      • rewards: A mapping from reward recipient addresses to reward amounts
  • Example
    // Request
    curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0", "method":"klay_getRewards", "params":["0x64fe7e0"],"id":1}' https://api.baobab.klaytn.net:8651
    // Response
    {
      "jsonrpc":"2.0",
      "id":1,
      "result":{
        "minted": "6400000000000000000",
        "totalFee": "1075975000000000",
        "burntFee": "537987500000000",
        "proposer": "3200268993750000000",
        "stakers": "0",
        "kgf": "2560215195000000000",
        "kir": "640053798750000000"
        "rewards": {
          "0xa86fd667c6a340c53cc5d796ba84dbe1f29cb2f7": "3200268993750000000",
          "0x2bcf9d3e4a846015e7e3152a614c684de16f37c6": "2560215195000000000",
          "0x716f89d9bc333286c79db4ebb05516897c8d208a": "640053798750000000"
        }
      }
    }
    

Expected Effect

The proposed GC reward mechanism is expected to produce the following changes:

  • GC members increase individual staking amount.
  • KLAY holders receive profit based on the staking amount of KLAY.
  • Total Value Locked (TVL) of Klaytn increases.
  • The total circulation reduces.

Backward Compatibility

Klaytn nodes must be upgraded before KORE_BLOCK_NUMBER to correctly produce or verify blocks.

Reference

n/a

Copyright and related rights waived via CC0.