cosmos-sdk/docs/spec/staking/spec-technical.md

23 KiB

Staking Module

Overview

The Cosmos Hub is a Tendermint-based Proof of Stake blockchain system that serves as a backbone of the Cosmos ecosystem. It is operated and secured by an open and globally decentralized set of validators. Tendermint consensus is a Byzantine fault-tolerant distributed protocol that involves all validators in the process of exchanging protocol messages in the production of each block. To avoid Nothing-at-Stake problem, a validator in Tendermint needs to lock up coins in a bond deposit. Tendermint protocol messages are signed by the validator's private key, and this is a basis for Tendermint strict accountability that allows punishing misbehaving validators by slashing (burning) their bonded Atoms. On the other hand, validators are rewarded for their service of securing blockchain network by the inflationary provisions and transactions fees. This incentives correct behavior of the validators and provides the economic security of the network.

The native token of the Cosmos Hub is called Atom; becoming a validator of the Cosmos Hub requires holding Atoms. However, not all Atom holders are validators of the Cosmos Hub. More precisely, there is a selection process that determines the validator set as a subset of all validator candidates (Atom holders that wants to become a validator). The other option for Atom holder is to delegate their atoms to validators, i.e., being a delegator. A delegator is an Atom holder that has bonded its Atoms by delegating it to a validator (or validator candidate). By bonding Atoms to secure the network (and taking a risk of being slashed in case of misbehaviour), a user is rewarded with inflationary provisions and transaction fees proportional to the amount of its bonded Atoms. The Cosmos Hub is designed to efficiently facilitate a small numbers of validators (hundreds), and large numbers of delegators (tens of thousands). More precisely, it is the role of the Staking module of the Cosmos Hub to support various staking functionality including validator set selection, delegating, bonding and withdrawing Atoms, and the distribution of inflationary provisions and transaction fees.

State

The staking module persists the following information to the store:

  • GlobalState, describing the global pools and the inflation related fields
  • validator candidates (including current validators), indexed by public key and shares in the global pool (bonded or unbonded depending on candidate status)
  • delegator bonds (for each delegation to a candidate by a delegator), indexed by the delegator address and the candidate public key
  • the queue of unbonding delegations
  • the queue of re-delegations

Global State

The GlobalState data structure contains total Atom supply, amount of Atoms in the bonded pool, sum of all shares distributed for the bonded pool, amount of Atoms in the unbonded pool, sum of all shares distributed for the unbonded pool, a timestamp of the last processing of inflation, the current annual inflation rate, a timestamp for the last comission accounting reset, the global fee pool, a pool of reserve taxes collected for the governance use and an adjustment factor for calculating global fee accum. Params is global data structure that stores system parameters and defines overall functioning of the module.

type GlobalState struct {
    TotalSupply              int64        // total supply of Atoms
    BondedPool               int64        // reserve of bonded tokens
    BondedShares             rational.Rat // sum of all shares distributed for the BondedPool
    UnbondedPool             int64        // reserve of unbonding tokens held with candidates
    UnbondedShares           rational.Rat // sum of all shares distributed for the UnbondedPool
    InflationLastTime        int64        // timestamp of last processing of inflation
    Inflation                rational.Rat // current annual inflation rate
    DateLastCommissionReset  int64        // unix timestamp for last commission accounting reset
    FeePool                  coin.Coins   // fee pool for all the fee shares which have already been distributed
    ReservePool              coin.Coins   // pool of reserve taxes collected on all fees for governance use
    Adjustment               rational.Rat // Adjustment factor for calculating global fee accum
}

type Params struct {
    HoldBonded   Address // account  where all bonded coins are held
    HoldUnbonding Address // account where all delegated but unbonding coins are held

    InflationRateChange rational.Rational // maximum annual change in inflation rate
    InflationMax        rational.Rational // maximum inflation rate
    InflationMin        rational.Rational // minimum inflation rate
    GoalBonded          rational.Rational // Goal of percent bonded atoms
    ReserveTax          rational.Rational // Tax collected on all fees

    MaxVals          uint16  // maximum number of validators
    AllowedBondDenom string  // bondable coin denomination

    // gas costs for txs
    GasDeclareCandidacy int64 
    GasEditCandidacy    int64 
    GasDelegate         int64 
    GasRedelegate       int64 
    GasUnbond           int64 
}

Candidate

The Candidate data structure holds the current state and some historical actions of validators or candidate-validators.

type Candidate struct {
    Status                 CandidateStatus       
    ConsensusPubKey        crypto.PubKey
    GovernancePubKey       crypto.PubKey
    Owner                  crypto.Address
    GlobalStakeShares      rational.Rat 
    IssuedDelegatorShares  rational.Rat
    RedelegatingShares     rational.Rat
    VotingPower            rational.Rat 
    Commission             rational.Rat
    CommissionMax          rational.Rat
    CommissionChangeRate   rational.Rat
    CommissionChangeToday  rational.Rat
    ProposerRewardPool     coin.Coins
    Adjustment             rational.Rat
    Description            Description 
}

type Description struct {
    Name       string 
    DateBonded string 
    Identity   string 
    Website    string 
    Details    string 
}

Candidate parameters are described:

  • Status: it can be Bonded (active validator), Unbonding (validator candidate) or Revoked
  • ConsensusPubKey: candidate public key that is used strictly for participating in consensus
  • GovernancePubKey: public key used by the validator for governance voting
  • Owner: Address that is allowed to unbond coins.
  • GlobalStakeShares: Represents shares of GlobalState.BondedPool if Candidate.Status is Bonded; or shares of GlobalState.Unbondingt Pool otherwise
  • IssuedDelegatorShares: Sum of all shares a candidate issued to delegators (which includes the candidate's self-bond); a delegator share represents their stake in the Candidate's GlobalStakeShares
  • RedelegatingShares: The portion of IssuedDelegatorShares which are currently re-delegating to a new validator
  • VotingPower: Proportional to the amount of bonded tokens which the validator has if Candidate.Status is Bonded; otherwise it is equal to 0
  • Commission: The commission rate of fees charged to any delegators
  • CommissionMax: The maximum commission rate this candidate can charge each day from the date GlobalState.DateLastCommissionReset
  • CommissionChangeRate: The maximum daily increase of the candidate commission
  • CommissionChangeToday: Counter for the amount of change to commission rate which has occurred today, reset on the first block of each day (UTC time)
  • ProposerRewardPool: reward pool for extra fees collected when this candidate is the proposer of a block
  • Adjustment factor used to passively calculate each validators entitled fees from GlobalState.FeePool
  • Description
    • Name: moniker
    • DateBonded: date determined which the validator was bonded
    • Identity: optional field to provide a signature which verifies the validators identity (ex. UPort or Keybase)
    • Website: optional website link
    • Details: optional details

DelegatorBond

Atom holders may delegate coins to candidates; under this circumstance their funds are held in a DelegatorBond data structure. It is owned by one delegator, and is associated with the shares for one candidate. The sender of the transaction is the owner of the bond.

type DelegatorBond struct {
    Candidate            crypto.PubKey
    Shares               rational.Rat
    AdjustmentFeePool    coin.Coins  
    AdjustmentRewardPool coin.Coins  
} 

Description:

  • Candidate: the public key of the validator candidate: bonding too
  • Shares: the number of delegator shares received from the validator candidate
  • AdjustmentFeePool: Adjustment factor used to passively calculate each bonds entitled fees from GlobalState.FeePool
  • AdjustmentRewardPool: Adjustment factor used to passively calculate each bonds entitled fees from Candidate.ProposerRewardPool

QueueElem

The Unbonding and re-delegation process is implemented using the ordered queue data structure. All queue elements share a common structure:

type QueueElem struct {
    Candidate   crypto.PubKey
    InitTime    int64    // when the element was added to the queue
}

The queue is ordered so the next element to unbond/re-delegate is at the head. Every tick the head of the queue is checked and if the unbonding period has passed since InitTime, the final settlement of the unbonding is started or re-delegation is executed, and the element is popped from the queue. Each QueueElem is persisted in the store until it is popped from the queue.

QueueElemUnbondDelegation

QueueElemUnbondDelegation structure is used in the unbonding queue.

type QueueElemUnbondDelegation struct {
    QueueElem
    Payout           Address       // account to pay out to
    Tokens           coin.Coins    // the value in Atoms of the amount of delegator shares which are unbonding
    StartSlashRatio  rational.Rat  // candidate slash ratio 
}

QueueElemReDelegate

QueueElemReDelegate structure is used in the re-delegation queue.

type QueueElemReDelegate struct {
    QueueElem
    Payout       Address       // account to pay out to
    Shares       rational.Rat  // amount of shares which are unbonding
    NewCandidate crypto.PubKey // validator to bond to after unbond
}

Transaction Overview

Available Transactions:

  • TxDeclareCandidacy
  • TxEditCandidacy
  • TxDelegate
  • TxUnbond
  • TxRedelegate
  • TxLivelinessCheck
  • TxProveLive

Transaction processing

In this section we describe the processing of the transactions and the corresponding updates to the global state. In the following text we will use gs to refer to the GlobalState data structure, unbondDelegationQueue is a reference to the queue of unbond delegations, reDelegationQueue is the reference for the queue of redelegations. We use tx to denote a reference to a transaction that is being processed, and sender to denote the address of the sender of the transaction. We use function loadCandidate(store, PubKey) to obtain a Candidate structure from the store, and saveCandidate(store, candidate) to save it. Similarly, we use loadDelegatorBond(store, sender, PubKey) to load a delegator bond with the key (sender and PubKey) from the store, and saveDelegatorBond(store, sender, bond) to save it. removeDelegatorBond(store, sender, bond) is used to remove the bond from the store.

TxDeclareCandidacy

A validator candidacy is declared using the TxDeclareCandidacy transaction.

type TxDeclareCandidacy struct {
    ConsensusPubKey     crypto.PubKey
    Amount              coin.Coin       
    GovernancePubKey    crypto.PubKey
    Commission          rational.Rat
    CommissionMax       int64 
    CommissionMaxChange int64 
    Description         Description
}

declareCandidacy(tx TxDeclareCandidacy):
    candidate = loadCandidate(store, tx.PubKey)
    if candidate != nil return // candidate with that public key already exists 
   	
    candidate = NewCandidate(tx.PubKey)
    candidate.Status = Unbonded
    candidate.Owner = sender
    init candidate VotingPower, GlobalStakeShares, IssuedDelegatorShares, RedelegatingShares and Adjustment to rational.Zero
    init commision related fields based on the values from tx
    candidate.ProposerRewardPool = Coin(0)  
    candidate.Description = tx.Description
   	
    saveCandidate(store, candidate)
   
    txDelegate = TxDelegate(tx.PubKey, tx.Amount)
    return delegateWithCandidate(txDelegate, candidate) 

// see delegateWithCandidate function in [TxDelegate](TxDelegate)

TxEditCandidacy

If either the Description (excluding DateBonded which is constant), Commission, or the GovernancePubKey need to be updated, the TxEditCandidacy transaction should be sent from the owner account:

type TxEditCandidacy struct {
    GovernancePubKey    crypto.PubKey
    Commission          int64  
    Description         Description
}
 
editCandidacy(tx TxEditCandidacy):
    candidate = loadCandidate(store, tx.PubKey)
    if candidate == nil or candidate.Status == Revoked return 
    
    if tx.GovernancePubKey != nil candidate.GovernancePubKey = tx.GovernancePubKey
    if tx.Commission >= 0 candidate.Commission = tx.Commission
    if tx.Description != nil candidate.Description = tx.Description
    
    saveCandidate(store, candidate)
    return

TxDelegate

Delegator bonds are created using the TxDelegate transaction. Within this transaction the delegator provides an amount of coins, and in return receives some amount of candidate's delegator shares that are assigned to DelegatorBond.Shares.

type TxDelegate struct {
    PubKey crypto.PubKey
    Amount coin.Coin       
}

delegate(tx TxDelegate):
    candidate = loadCandidate(store, tx.PubKey)
    if candidate == nil return
	return delegateWithCandidate(tx, candidate)

delegateWithCandidate(tx TxDelegate, candidate Candidate):
    if candidate.Status == Revoked return

    if candidate.Status == Bonded 
	    poolAccount = params.HoldBonded
    else 
	    poolAccount = params.HoldUnbonded
	
    err = transfer(sender, poolAccount, tx.Amount)
    if err != nil return 

    bond = loadDelegatorBond(store, sender, tx.PubKey)
    if bond == nil then bond = DelegatorBond(tx.PubKey, rational.Zero, Coin(0), Coin(0))
	
    issuedDelegatorShares = addTokens(tx.Amount, candidate)
    bond.Shares += issuedDelegatorShares
	
    saveCandidate(store, candidate)
    saveDelegatorBond(store, sender, bond)
    saveGlobalState(store, gs)
    return 

addTokens(amount coin.Coin, candidate Candidate):
    if candidate.Status == Bonded 
	    gs.BondedPool += amount
	    issuedShares = amount / exchangeRate(gs.BondedShares, gs.BondedPool)
	    gs.BondedShares += issuedShares
    else 
	    gs.UnbondedPool += amount
	    issuedShares = amount / exchangeRate(gs.UnbondedShares, gs.UnbondedPool)
	    gs.UnbondedShares += issuedShares
	
    candidate.GlobalStakeShares += issuedShares
    
    if candidate.IssuedDelegatorShares.IsZero() 
        exRate = rational.One
    else
        exRate = candidate.GlobalStakeShares / candidate.IssuedDelegatorShares
	
    issuedDelegatorShares = issuedShares / exRate
    candidate.IssuedDelegatorShares += issuedDelegatorShares
    return issuedDelegatorShares
	
exchangeRate(shares rational.Rat, tokenAmount int64):
    if shares.IsZero() then return rational.One
    return tokenAmount / shares
    	

TxUnbond

Delegator unbonding is defined with the following transaction:

type TxUnbond struct {
    PubKey crypto.PubKey
    Shares rational.Rat 
}

unbond(tx TxUnbond):    
    bond = loadDelegatorBond(store, sender, tx.PubKey)
    if bond == nil return 
    if bond.Shares < tx.Shares return 
	
    bond.Shares -= tx.Shares

    candidate = loadCandidate(store, tx.PubKey)
	
    revokeCandidacy = false
    if bond.Shares.IsZero() 
	    if sender == candidate.Owner and candidate.Status != Revoked then revokeCandidacy = true then removeDelegatorBond(store, sender, bond)
    else 
	    saveDelegatorBond(store, sender, bond)

    if candidate.Status == Bonded 
        poolAccount = params.HoldBonded
    else 
        poolAccount = params.HoldUnbonded

    returnedCoins = removeShares(candidate, shares)
	
    unbondDelegationElem = QueueElemUnbondDelegation(tx.PubKey, currentHeight(), sender, returnedCoins, startSlashRatio)
    unbondDelegationQueue.add(unbondDelegationElem)
	
    transfer(poolAccount, unbondingPoolAddress, returnCoins)  
    
    if revokeCandidacy 
	    if candidate.Status == Bonded then bondedToUnbondedPool(candidate)
	    candidate.Status = Revoked

    if candidate.IssuedDelegatorShares.IsZero() 
	    removeCandidate(store, tx.PubKey)
    else 
	    saveCandidate(store, candidate)

    saveGlobalState(store, gs)
    return 

removeShares(candidate Candidate, shares rational.Rat):
    globalPoolSharesToRemove = delegatorShareExRate(candidate) * shares

    if candidate.Status == Bonded 
	    gs.BondedShares -= globalPoolSharesToRemove
	    removedTokens = exchangeRate(gs.BondedShares, gs.BondedPool) * globalPoolSharesToRemove
	    gs.BondedPool -= removedTokens
    else 
	    gs.UnbondedShares -= globalPoolSharesToRemove
	    removedTokens = exchangeRate(gs.UnbondedShares, gs.UnbondedPool) * globalPoolSharesToRemove
	    gs.UnbondedPool -= removedTokens
	
    candidate.GlobalStakeShares -= removedTokens
    candidate.IssuedDelegatorShares -= shares
    return returnedCoins

delegatorShareExRate(candidate Candidate):
    if candidate.IssuedDelegatorShares.IsZero() then return rational.One
    return candidate.GlobalStakeShares / candidate.IssuedDelegatorShares
	
bondedToUnbondedPool(candidate Candidate):
    removedTokens = exchangeRate(gs.BondedShares, gs.BondedPool) * candidate.GlobalStakeShares 
    gs.BondedShares -= candidate.GlobalStakeShares
    gs.BondedPool -= removedTokens
	
    gs.UnbondedPool += removedTokens
    issuedShares = removedTokens / exchangeRate(gs.UnbondedShares, gs.UnbondedPool)
    gs.UnbondedShares += issuedShares
    
    candidate.GlobalStakeShares = issuedShares
    candidate.Status = Unbonded

    return transfer(address of the bonded pool, address of the unbonded pool, removedTokens)

TxRedelegate

The re-delegation command allows delegators to switch validators while still receiving equal reward to as if they had never unbonded.

type TxRedelegate struct {
    PubKeyFrom crypto.PubKey
    PubKeyTo   crypto.PubKey
    Shares     rational.Rat 
}

redelegate(tx TxRedelegate):
    bond = loadDelegatorBond(store, sender, tx.PubKey)
    if bond == nil then return 
    
    if bond.Shares < tx.Shares return 
    candidate = loadCandidate(store, tx.PubKeyFrom)
    if candidate == nil return
    
    candidate.RedelegatingShares += tx.Shares
    reDelegationElem = QueueElemReDelegate(tx.PubKeyFrom, currentHeight(), sender, tx.Shares, tx.PubKeyTo)
    redelegationQueue.add(reDelegationElem)
    return     

TxLivelinessCheck

Liveliness issues are calculated by keeping track of the block precommits in the block header. A queue is persisted which contains the block headers from all recent blocks for the duration of the unbonding period. A validator is defined as having livliness issues if they have not been included in more than 33% of the blocks over:

  • The most recent 24 Hours if they have >= 20% of global stake
  • The most recent week if they have = 0% of global stake
  • Linear interpolation of the above two scenarios

Liveliness kicks are only checked when a TxLivelinessCheck transaction is submitted.

type TxLivelinessCheck struct { 
    PubKey        crypto.PubKey
    RewardAccount Addresss
}

If the TxLivelinessCheck is successful in kicking a validator, 5% of the liveliness punishment is provided as a reward to RewardAccount.

TxProveLive

If the validator was kicked for liveliness issues and is able to regain liveliness then all delegators in the temporary unbonding pool which have not transacted to move will be bonded back to the now-live validator and begin to once again collect provisions and rewards. Regaining liveliness is demonstrated by sending in a TxProveLive transaction:

type TxProveLive struct {
    PubKey crypto.PubKey
}

End of block handling

tick(ctx Context):
    hrsPerYr = 8766   // as defined by a julian year of 365.25 days
    
    time = ctx.Time()
    if time > gs.InflationLastTime + ProvisionTimeout 
        gs.InflationLastTime = time
        gs.Inflation = nextInflation(hrsPerYr).Round(1000000000)
        
        provisions = gs.Inflation * (gs.TotalSupply / hrsPerYr)
        
        gs.BondedPool += provisions
        gs.TotalSupply += provisions
        
        saveGlobalState(store, gs)
    
    if time > unbondDelegationQueue.head().InitTime + UnbondingPeriod 
        for each element elem in the unbondDelegationQueue where time > elem.InitTime + UnbondingPeriod do
    	    transfer(unbondingQueueAddress, elem.Payout, elem.Tokens)
    	    unbondDelegationQueue.remove(elem)
    
    if time > reDelegationQueue.head().InitTime + UnbondingPeriod 
        for each element elem in the unbondDelegationQueue where time > elem.InitTime + UnbondingPeriod do
            candidate = getCandidate(store, elem.PubKey)
            returnedCoins = removeShares(candidate, elem.Shares)
            candidate.RedelegatingShares -= elem.Shares 
            delegateWithCandidate(TxDelegate(elem.NewCandidate, returnedCoins), candidate)
            reDelegationQueue.remove(elem)
            
    return UpdateValidatorSet()

nextInflation(hrsPerYr rational.Rat):
    if gs.TotalSupply > 0 
        bondedRatio = gs.BondedPool / gs.TotalSupply
    else 
        bondedRation = 0
   
    inflationRateChangePerYear = (1 - bondedRatio / params.GoalBonded) * params.InflationRateChange
    inflationRateChange = inflationRateChangePerYear / hrsPerYr

    inflation = gs.Inflation + inflationRateChange
    if inflation > params.InflationMax then inflation = params.InflationMax
	
    if inflation < params.InflationMin then inflation = params.InflationMin
	
    return inflation 

UpdateValidatorSet():
    candidates = loadCandidates(store)

    v1 = candidates.Validators()
    v2 = updateVotingPower(candidates).Validators()

    change = v1.validatorsUpdated(v2) // determine all updated validators between two validator sets
    return change

updateVotingPower(candidates Candidates):
    foreach candidate in candidates do
	    candidate.VotingPower = (candidate.IssuedDelegatorShares - candidate.RedelegatingShares) * delegatorShareExRate(candidate)	
	    
    candidates.Sort()
	
    foreach candidate in candidates do
	    if candidate is not in the first params.MaxVals  
	        candidate.VotingPower = rational.Zero
	        if candidate.Status == Bonded then bondedToUnbondedPool(candidate Candidate)
		
	    else if candidate.Status == UnBonded then unbondedToBondedPool(candidate)
                      
	saveCandidate(store, c)
	
    return candidates

unbondedToBondedPool(candidate Candidate):
    removedTokens = exchangeRate(gs.UnbondedShares, gs.UnbondedPool) * candidate.GlobalStakeShares 
    gs.UnbondedShares -= candidate.GlobalStakeShares
    gs.UnbondedPool -= removedTokens
	
    gs.BondedPool += removedTokens
    issuedShares = removedTokens / exchangeRate(gs.BondedShares, gs.BondedPool)
    gs.BondedShares += issuedShares
    
    candidate.GlobalStakeShares = issuedShares
    candidate.Status = Bonded

    return transfer(address of the unbonded pool, address of the bonded pool, removedTokens)