# Staking

Crypto.com Chain is based on Tendermint Core's consensus engine, it relies on a set of validators (Council Node) to participate in the proof of stake (PoS) consensus protocol, and they are responsible for committing new blocks in the blockchain. This document contains the specification of the initial staking, slashing, and rewards mechanisms which establish the foundation of the token ecosystem of CRO.

# Staked state / Account

StakedState is a data structure that holds state about staking:

pub struct StakedState {
    pub nonce: Nonce, // transaction / update counter
    pub bonded: Coin, // bonded stake amount
    pub unbonded: Coin, // unbonded stake amount
    pub unbonded_from: Timespec, // when the unboded stake amount can be withdrawn
    pub address: StakedStateAddress, // ETH-style address of staking account
    pub punishment: Option<Punishment>, // punishment data structure -- FIXME: details
    pub council_node: Option<CouncilNode>, // associated council node metadata
}

TODO: should it have a minimum bonded amount?

# Council Node

Council Node is a data structure that holds state about a node responsible for transaction validation:

pub struct CouncilNode {
    pub name: String, // human-readable name
    pub security_contact: Option<String>, // optional email for security contact
    pub staking_address: RedeemAddress, // account with the required staked amount
    pub consensus_pubkey: crypto.PubKey, // Tendermint consensus validator-associated public key: note that Tendermint supports different signature schemes
    pub council_pubkey: PublicKey, // key for council node-related transaction types
    // FIXME: bootstrapping
}

# EFFECTIVE_MIN_STAKE

Effective minimum stake (EFFECTIVE_MIN_STAKE) is defined as follows at any time:

  1. if the number of validators has not reached MAX_VALIDATORS, it is COUNCIL_NODE_MIN_STAKE (the network parameter)
  2. if the number of validators has reached MAX_VALIDATORS, it is equal to the lowest bonded amount of the staked states of the current validators plus 1.0 Coin.

# Joining the network

Anyone who wishes to become a council node can submit a NodeJoinTx; this transaction is considered to be valid as long as:

  1. The associated staking account has bonded amount >= EFFECTIVE_MIN_STAKE and is not punished;
  2. There is no other validator with the same staking_address or the consensus_pubkey

# Leaving the network

Anyone who wishes to leave the network, provided their associated staked state does not have any punishments, can submit UnbondStakeTx with the amount that will reduce the bonded amount to be lower than COUNCIL_NODE_MIN_STAKE.

# Voting power and proposer selection

At the beginning of each round, a council node will be chosen deterministically to be the block proposer.

The chance of being a proposer is directly proportional to their voting power at that time, which, in general, is equal to the bonded amount (rounded to the whole unit) in the associated staking address of the council node.

The top MAX_VALIDATORS with the most bonded amount would put to the active validator set in END_BLOCK. If a validator's bonded amount is below the top MAX_VALIDATORS, It will be considered as non-active and would not be able to take part in the consensus. For example,

  • If the number of active validators < MAX_VALIDATORS:

    bonded amount < COUNCIL_NODE_MIN_STAKE bonded amount => COUNCIL_NODE_MIN_STAKE
    Validator's voting power Set to 0 Set to the bonded amount
  • On the other hand, If the number of active validators = MAX_VALIDATORS:

    bonded amount is below the top MAX_VALIDATORS Otherwise
    Validator's voting power Set to 0 Set to the bonded amount

# Rewards

To incentivise validators to run the network, rewards are accumulated and distributed to the validators. There are three sources for the rewards:

  1. Monetary expansion with a fixed total supply
  2. Transaction Fees
  3. Slashing of byzantine and non-live nodes

The RewardsPoolState data structure stores all the information about remaining funds and distribution states:

pub struct RewardsPoolState {
    pub period_bonus: Coin, // rewards accumulated from fees and slashing during current period
    pub last_block_height: BlockHeight, // when was the pool last updated
    pub last_distribution_time: Timespec, // when was the pool last distributed
    pub minted: Coin,  // record the number of new coins ever minted, can't exceeds max supply
    pub tau: u64,  // a decaying parameter in monetery expansion process
}

# Reward distribution

FIXME: fix fairness; define rewarding in terms of LastCommitInfo instead of block proposer

Rewards are distributed periodically (e.g. daily), rewards are accumulated during each period, and block proposers are recorded. At the end of each period, validators will receive a portion of the "reward pool" as a reward for participating in the consensus process. Specifically, the reward is proportional to the number of blocks that were successfully proposed by the validator; it is calculated as follows:

rewards of validator = total rewards * number of blocks proposed by the validator / total number of blocks

The remainder of division will become rewards of next period.

The recording of block proposer is done in BeginBlock right before rewards distribution.

# Monetary expansion

Monetary expansion is designed to release tokens from the reserve account to the reward pool, while keeping a fixed max total supply.

Configuration section in genesis.json for this:

{
  "app_state": {
    ...
    "network_params": {
      ...
      "rewards_config": {
        "reward_period_seconds": 86400,  # range: [0, 365 * 86400]
        "monetary_expansion_r0": 450,  # range: [0, 1000]
        "monetary_expansion_tau": 14500000000000000,  # range: [1, 2**64 - 1]
        "monetary_expansion_decay": 999860,  # range: [0, 1000000]
        "monetary_expansion_cap": "6250000000000000000" # range: [0, 100_00000000_00000000]
      }
    }
  }
}

The number of tokens being released at each period is defined as:

R0 = rewards_config["monetary_expansion_r0"]
tau = rewards_config["monetary_expansion_tau"]
P = rewards_config["reward_period_seconds"]

S = total_staking  # example: 1500000000_00000000
Y = 365 * 24 * 60 * 60  # seconds of a year

R = (R0 / 1000) * exp(-S / tau)
N = floor(S * (pow(1 + R, P / Y) - 1))
N = N - N % 10000

N: released coins
S: total stakings at current block when reward distribution happens
R0: upper bound for the reward rate p.a.

Note that this is an exponential decay function, where the index of it controls the “steepness” of the curve. Precisely, this damping factor controls the exponential decay rate of the reward function.

The parameter tau will decay each time rewards get distributed,:

tau(n) = tau(n-1) * rewards_config["monetary_expansion_decay"]

The rewards validator received goes to the bonded balance of their staking account, and results in validator voting power change accordingly.

# Fixed-point arithmetic

First we transform the power function into exponencial and natural logarithm functions:

pow(x, y) = exp(y * log(x))

The exp and log are computed with continued fractions representation, using a form with better convergence:

exp2(x, y) = exp(x / y)
log2(x, y) = log(1 + x / y)

MainEq1

MainEq2

With above substitutes, we can transform the formula like this:

R = (R0 / 1000) * exp(-S/tau)
  = R0 * exp2(-S, tau) / 1000
N = S * (pow(1 + R, P / Y) - 1)
  = S * (exp(P * log(1 + R) / Y) - 1)
  = S * (exp2(P * log(1 + R), Y) - 1)
  = S * (exp2(P * log(1 + R0 * exp2(-S, tau) / 1000), Y) - 1)
  = S * (exp2(P * log2(R0 * exp2(-S, tau), 1000), Y) - 1)

Break it down into simpler computation steps:

# To keep the intermidiate numbers smaller
S' = S / 10000000_00000000
tau' = tau / 10000000_00000000

n0 = exp2(-S', tau')
n1 = log2(R0 * n0, 1000)
n2 = exp2(P * n1, Y)
n3 = floor(S * (n2 - 1))
N  = n3 - n3 % 10000

Fixed point number format: I65F63.

exp2 runs 25 iterations.

log2 runs 10 iterations.

TODO, how to compute the continued fractions form of exp2 and log2.

# Punishments

This part describes functionality that aims to disincentivize network-observable actions, such as faulty validations, of participants with values at stake by penalizing/slashing and jailing them. The penalties may include losing some amount of their stake (surrendered to the rewards pool), losing their ability to perform the network functionality for a period of time, collect rewards etc.

Punishments for a validator are triggered when they either make a byzantine fault or become non-live:

  • Liveness Faults (Low availability)

    A validator is said to be non-live when they fail to sign at least MISSED_BLOCK_THRESHOLD blocks in last BLOCK_SIGNING_WINDOW blocks successfully. BLOCK_SIGNING_WINDOW and MISSED_BLOCK_THRESHOLD are network parameters and can be configured during genesis (currently, changing these network parameters at runtime is not supported). Tendermint passes signing information to ABCI application as last_commit_info in BeginBlock request.

Example:

For example, if `BLOCK_SIGNING_WINDOW` is `100` blocks and `MISSED_BLOCK_THRESHOLD` is `50` blocks, a validator will be
marked as **non-live** if they fail to successfully sign at least `50` blocks in last `100` blocks.
  • Byzantine Faults (Double signing)

    A validator is said to make a byzantine fault when they sign conflicting transactions at the same height and round. Tendermint has mechanisms to publish evidence of validators that signed conflicting votes (it passes this information to ABCI application in BeginBlock request), so they can be punished by the application.

# Jailing

A validator is jailed if:

  1. They are not live, i.e., they failed to sign missed_block_threshold blocks in last block_signing_window blocks successfully.
  2. They make a byzantine fault, e.g., they sign messages at same height and round.

When a validator gets jailed, they cannot perform any operations relating to their account, for example, withdraw_stake, deposit_stake, unbond_stake, etc., until they are un-jailed. Also, a validator cannot be un-jailed before account.jailed_until which is set to block_time + jail_duration while jailing. jail_duration is a network parameter which can be configured during genesis.

# Un-jailing

When a jailed validator wishes to resume normal operations (after account.jailed_until has passed), they can create UnjailTx which marks them as un-jailed and adds them back to validator set.

# Slashing

Validators are responsible for signing or proposing block at each consensus round. It is important that they maintain excellent availability and network connectivity to perform these tasks. A penalty performed by the slashing module should be imposed on validators' misbehaviour or unavailability to reinforce this. Similar to jailing, a validator is slashed if:

  1. They are not live, i.e., they failed to sign MISSED_BLOCK_THRESHOLD blocks in last BLOCK_SIGNING_WINDOW blocks successfully.
  2. They make a byzantine fault, e.g., they sign messages at same height and round.

Unlike jailing, which happens immediately after punishments are triggered, slashing happens after slash_wait_period. slash_wait_period is a network parameter and can be configured during genesis. Validators are not immediately slashed because faulty behavior may be non-malicious, e.g. due to misconfiguration. slash_wait_period is introduced to create a tolerant punishment setup.

Besides this, if a validator makes multiple faults in slash_wait_period, they'll only be slashed once for the worst fault in that time period.

# Slashing Rate

Whenever a validator is slashed, a percentage of their bonded and unbonded amount is transferred to rewards_pool. There are many factors involved in determining the slashing rate for a validator:

  1. MAX_LIVENESS_SLASH_RATIO and MAX_BYZANTINE_SLASH_RATIO are the two network parameters used while calculating the slashing rate. Both of these can be configured in the genesis.
  2. validator_voting_percent is the voting percent of faulty validator in the network.
  3. List of all the faulty validators in that period.

The algorithm for calculating slashing_rate is as follows:

validator_slash_percent = max(slash percent of individual faults)

For example, if a validator made a byzantine fault as well as they are non-live, then,

validator_slash_percent = max(liveness_slash_percent, byzantine_slash_percent)

And if there are n faulty validators in this period, then,

slashing_rate = 
    validator_slash_percent * 
    (
        sqrt(validator_voting_percent_1) +
        sqrt(validator_voting_percent_2) +
        .. + 
        sqrt(validator_voting_percent_n)
    )^2

So, if one validator of 10% voting power faults, it gets a 10% slash (assuming liveness_slash_percent and byzantine_slash_percent are both 100%). While, if two validators of 5% voting power each fault together, they both get a 20% slash.

slashing_rate = max(liveness_slash_percent, byzantine_slash_percent) * (sqrt(validator_voting_percent_1) + sqrt(validator_voting_percent_2))^2
              = max(1, 1) * (sqrt(0.05) + sqrt(0.05))^2                // assuming liveness_slash_percent and byzantine_slash_percent are both 100%
              = 0.2

# Removed validators / council nodes

A Validator is removed when its voting power is set to 0. This can happen in the following scenarios:

  1. The Validator is being punished for infraction(s).
  2. Its operator submitted UnbondStakeTx with sufficient stake to leave the network.
  3. When the bonded amount <= EFFECTIVE_MIN_STAKE

When this happens, the following metadata is cleaned up as follows:

  • its associated reward tracking-related data is cleared:
    • (a) immediately (if the validator is being punished)
    • (b) in the block that triggers next reward distribution (otherwise)
  • its liveness tracking information is cleared in the block when this occurs
  • its slashing related information (mapping Tendermint address / pubkey => staked state address) is scheduled to be cleared in a block after the current block time + MAX_EVIDENCE_AGE (SLASH_MAP_DELETE)

Note this inequality must be checked in network parameters: UNBOND_PERIOD >= JAIL_DURATION >= MAX_EVIDENCE_AGE

If the validator wishes to re-join the validator set, they can (unjail if necessary and) submit a NodeJoinTx (see the Joining the Network section).

If the NodeJoinTx transaction is valid AND this happens before SLASH_MAP_DELETE AND the consensus pubkey (or associated Tendermint address) from NodeJoinTx transaction is the same, the previous slashing information is preserved (i.e. the "delete schedule" is cancelled).

If the NodeJoinTx transaction is valid AND this happens before next reward distribution AND the consensus pubkey from NodeJoinTx transaction is different, the associated staked state is then rewarded for interactions with both consensus pubkeys (as reward tracking is per staked state).

# Validators / Council Nodes

Each BeginBlock contains two fields that will determine penalties:

  • LastCommitInfo: this contains information which validators signed the last block
  • ByzantineValidators: evidence of validators that acted maliciously

Their processing is the following:

FIXME: this looks out of date?

for each evidence in ByzantineValidators:
    if evidence.Timestamp >= BeginBlock.Timestamp - MAX_EVIDENCE_AGE:
        account = (... get corresponding account ...)
        if account.slashing_period.end is set and it's before BeginBlock.Timestamp:
            account.slashing_period.slashed_ratio = max(account.slashing_period.slashed_ratio, BYZANTINE_SLASH_RATIO)
        else:
            if account.slashing_period.end is set and it's after BeginBlock.Timestamp:
                END/COMMIT_BLOCK_STATE_UPDATE(deduct(slashing_period, account))

            account.slashing_period = Some(SlashingPeriod(start = Some(BeginBlock.Timestamp),
                end = Some(BeginBlock.Timestamp + SLASHING_PERIOD_DURATION, slashed_ratio = BYZANTINE_SLASH_RATIO)))
            account.jailed_until = Some(BeginBlock.Timestamp + JAIL_DURATION)


for each signer in LastCommitInfo:
    council_node = (... lookup by signer / tendermint validator key ...)
    update(council_node, BLOCK_SIGNING_WINDOW)

for each council_node:
    missed_blocks = get_missed_signed_blocks(council_node)
    if missed_blocks / BLOCK_SIGNING_WINDOW >= MISSED_BLOCK_THRESHOLD:
        account = (... lookup corresponding account ...)
        if account.slashing_period.end is set and it's before BeginBlock.Timestamp:
            account.slashing_period.slashed_ratio = max(account.slashing_period.slashed_ratio, LIVENESS_SLASH_RATIO)
        else:
            if account.slashing_period.end is set and it's after BeginBlock.Timestamp:
                END/COMMIT_BLOCK_STATE_UPDATE(deduct(slashing_period, account))

            account.slashing_period = Some(SlashingPeriod(start = Some(BeginBlock.Timestamp),
                end = Some(BeginBlock.Timestamp + SLASHING_PERIOD_DURATION, slashed_ratio = LIVENESS_SLASH_RATIO)))
            account.jailed_until = Some(BeginBlock.Timestamp + JAIL_DURATION)

# “Global state” / APP_HASH

Tendermint expects a single compact value, APP_HASH, after each BlockCommit that represents the state of the application. In the early Chain prototype, this was constructed as a root of a Merkle tree from IDs of valid transactions.

In this staking scenario, some form of “chimeric ledger” needs to be employed, as staking-related functionality is represented with accounts. In Ethereum, Merkle Patricia Trees are used: (The alternative in Tendermint: depends on the order of transactions though)

They could possibly be used to represent an UTXO set: https://medium.com/codechain/codechains-merkleized-utxo-set-c76c9376fd4f

The overall “global state” then consists of the following items:

  • UTXO set
  • Account
  • RewardsPool
  • CouncilNode

So each component could possibly be represented as MPT and these MPTs would then be combined together to form a single APP_HASH.

# END/COMMIT_BLOCK_STATE_UPDATE

FIXME: scheduling slashing cleanup

Besides committing all the relevant changes and computing the resulting APP_HASH in BlockCommit; for all changes in Accounts, the implementation needs to signal ValidatorUpdate in EndBlock.

For example, when the number of active validators is less then MAX_VALIDATORS:

  • When the changes are relevant to the bonded amount of the council node’s staking address and the validator is not jailed:

    • If the bonded amount changes and < COUNCIL_NODE_MIN_STAKE, then the validator’s voting power should be set to 0;
    • If the bonded amount changes and >= COUNCIL_NODE_MIN_STAKE, then the validator’s voting power should be set to that amount (rounded to the whole unit).
  • When the changes are relevant to the jailing condition of the council node’s staking address:

    • If the jailed_until changes to Some(...) (i.e. the node is being jailed), then the validator’s power should be set to 0;
    • If the jailed_until changes to None (i.e. the node was un-jailed) and bonded amount >= COUNCIL_NODE_MIN_STAKE, then the validator’s power should be set to the bonded amount (rounded to the whole unit).

    It can be summarized in the following table:

    bonded < COUNCIL_NODE_MIN_STAKE bonded >= COUNCIL_NODE_MIN_STAKE but Jailed bonded >= COUNCIL_NODE_MIN_STAKE and NOT jailed
    Validator's voting power Set to 0 Set to 0 Set to the bonded amount

On the other hand, If the number of the current active validators is equal to MAX_VALIDATORS, the validator's voting power will also be depended on whether its bonded amount is at the top MAX_VALIDATORS, please refer to the previous section

# InitChain

The initial prototype’s configuration will need to contain the following elements:

  • Network parameters
  • ERC20 snapshot state / holder mapping initial_holders: Vec<(address: RedeemAddress, is_contract: bool, amount: Coin)> (or Map<RedeemAddress, (bool, Coin)>)
  • Network long-term incentive address: nlt_address
  • Secondary distribution and launch incentive addresses: dist_address1, dist_address2
  • Initial validators: Vec<CouncilNode>
  • Bootstrap nodes / initially bonded: Vec<RedeemAddress>

The validation of the configuration is the following:

  1. validate parameters format (e.g. CUSTOMER_ACQUIRER_SHARE + MERCHANT_ACQUIRER_SHARE + COUNCIL_NODE_SHARE = 1.0)
  2. check that sum of amounts in initial_holders == MAX_COIN (network constant)
  3. check there are no duplicate addresses (if Vec) in initial_holders
  4. for each council_node in Vec<CouncilNode>: check staking_address is in initial_holders and the amount >= COUNCIL_NODE_MIN_STAKE
  5. for each address in initially bonded Vec<RedeemAddress>: check address is initial_holders and the amount >= COMMUNITY_NODE_MIN_STAKE
  6. size(Validators in InitChainRequest) == size(Vec<CouncilNode>) and for every CouncilNode, its consensus_pubkey appears in Validators and power in Validators corresponds to staking address’ amount

If valid, the genesis state is then initialized as follows:

  1. initialize RewardsPool’s amount with amounts corresponding to: nlt_address + dist_address1 + dist_address2
  2. for every council node, create a CouncilNode structure
  3. for every council node’s staking address and every address in initially bonded: create Account where all of the corresponding amount is set as bonded
  4. for every address in initial_holders except for nlt_address, dist_address1, dist_address2, council nodes’ staking addresses, initially bonded addresses: if is_contract then add the amount to RewardsPool else create Account where the amount is all in unbonded and unbonded_from is set to genesis block’s time (in InitChain)