cosmos-sdk/docs/_attic/stakingSpec1.md

26 KiB

Stake Module

Overview

The stake module is tasked with various core staking functionality. Through the stake module atoms may be bonded, delegated, and provisions/rewards are distributed. Atom provisions are distributed to validators and their delegators through share distribution of a collective pool of all staked atoms. As atoms are created they are added to the common pool and each share become proportionally worth more atoms. Fees are distributed through a similar pooling mechanism but where each validator and delegator maintains an adjustment factor to determine the true proportion of fees they are entitled too. This adjustment factor is updated for each delegator and validator for each block where changes to the voting power occurs in the network. Broken down, the stake module at a high level is responsible for:

  • Declaration of candidacy for becoming a validator
  • Updating Tendermint validating power to reflect slashable stake
  • Delegation and unbonding transactions
  • Implementing unbonding period
  • Provisioning Atoms
  • Managing and distributing transaction fees
  • Providing the framework for validator commission on delegators

Transaction Overview

Available Transactions:

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

Global State

Params and GlobalState represent the global persistent state of Gaia. Params is intended to remain static whereas GlobalState is anticipated to change each block.

type Params struct {
	HoldBonded   Address // account  where all bonded coins are held
	HoldUnbonded Address // account where all delegated but unbonded 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 
}
type GlobalState struct {
	TotalSupply              int64        // total supply of atom tokens
	BondedShares             rational.Rat // sum of all shares distributed for the BondedPool
	UnbondedShares           rational.Rat // sum of all shares distributed for the UnbondedPool
	BondedPool               int64        // reserve of bonded tokens
	UnbondedPool             int64        // reserve of unbonded tokens held with candidates
	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
}

The Queue

The queue is ordered so the next 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 InitHeight commence with final settlement of the unbonding and pop the queue. All queue elements used for unbonding share a common struct:

type QueueElem struct {
	Candidate   crypto.PubKey
	InitHeight  int64    // when the queue was initiated
}

Each QueueElem is persisted in the store until it is popped from the queue.

Validator-Candidate

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

type Candidate struct {
	Status                 CandidateStatus       
	PubKey                 crypto.PubKey
	GovernancePubKey       crypto.PubKey
	Owner                  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 CandidateStatus byte
const (
    VyingUnbonded  CandidateStatus = 0x00
    VyingUnbonding CandidateStatus = 0x01
    Bonded         CandidateStatus = 0x02
    KickUnbonding  CandidateStatus = 0x03
    KickUnbonded   CandidateStatus = 0x04
)

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

Candidate parameters are described:

  • Status: signal that the candidate is either vying for validator status either unbonded or unbonding, an active validator, or a kicked validator either unbonding or unbonded.
  • PubKey: separated key from the owner of the candidate as is used strictly for participating in consensus.
  • Owner: Address where coins are bonded from and unbonded to
  • GlobalStakeShares: Represents shares of GlobalState.BondedPool if Candidate.Status is Bonded; or shares of GlobalState.UnbondedPool if Candidate.Status is otherwise
  • IssuedDelegatorShares: Sum of all shares issued to delegators (which includes the candidate's self-bond) which represent each of 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 the validator is within the top 100 validators.
  • Commission: The commission rate of fees charged to any delegators
  • CommissionMax: The maximum commission rate which 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

validator candidacy can be declared using the TxDeclareCandidacy transaction. During this transaction a self-delegation transaction is executed to bond tokens which are sent in with the transaction.

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

For all subsequent self-bonding, whether self-bonding or delegation the TxDelegate function should be used. In this context TxUnbond is used to unbond either delegation bonds or validator self-bonds.

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
}

Persistent State

Within the store, each Candidate is stored by validator-pubkey.

  • key: validator-pubkey
  • value: Candidate object

A second key-value pair is also persisted in order to quickly sort though the group of all candidates, this second index is however not persisted through the merkle store.

  • key: Candidate.GlobalStakeShares
  • value: Candidate.PubKey

When the set of all validators needs to be determined from the group of all candidates, the top candidates, sorted by GlobalStakeShares can be retrieved from this sorting without the need to retrieve the entire group of candidates. When validators are kicked from the validator set they are removed from this list.

New Validators

The validator set is updated in the first block of every hour. Validators are taken as the first GlobalState.MaxValidators number of candidates with the greatest amount of staked atoms who have not been kicked from the validator set.

Kicked Validators

Unbonding of an entire validator-candidate to a temporary liquid account occurs under the scenarios:

  • not enough stake to be within the validator set
  • the owner unbonds all of their staked tokens
  • validator liveliness issues
  • crosses a self-imposed safety threshold
    • minimum number of tokens staked by owner
    • minimum ratio of tokens staked by owner to delegator tokens

When this occurs delegator's tokens do not unbond to their personal wallets but begin the unbonding process to a pool where they must then transact in order to withdraw to their respective wallets. The following unbonding will use the following queue element

type QueueElemUnbondCandidate struct {
	QueueElem
}

If a delegator chooses to initiate an unbond or re-delegation of their shares while a candidate-unbond is commencing, then that unbond/re-delegation is subject to a reduced unbonding period based on how much time those funds have already spent in the unbonding queue.

Liveliness issues

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`.

Validator Liveliness Proof

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 livliness is demonstrated by sending in a TxProveLive transaction:

type TxProveLive struct {
    PubKey crypto.PubKey
}

Delegator bond

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

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

Description:

  • Candidate: pubkey of the validator candidate: bonding too
  • Shares: the number of 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``

Each DelegatorBond is individually indexed within the store by delegator address and candidate pubkey.

  • key: Delegator and Candidate-Pubkey
  • value: DelegatorBond

Delegating

Delegator bonds are created using the TxDelegate transaction. Within this transaction the validator candidate queried with an amount of coins, whereby given the current exchange rate of candidate's delegator-shares-to-atoms the candidate will return shares which are assigned in DelegatorBond.Shares.

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

Unbonding

Delegator unbonding is defined by the following transaction type:

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

When unbonding is initiated, delegator shares are immediately removed from the candidate and added to a queue object.

type QueueElemUnbondDelegation struct {
	QueueElem
	Payout           Address  // account to pay out to
    Shares           rational.Rat  // amount of shares which are unbonding
    StartSlashRatio  rational.Rat  // candidate slash ratio at start of re-delegation
}

In the unbonding queue - the fraction of all historical slashings on that validator are recorded (StartSlashRatio). When this queue reaches maturity if that total slashing applied is greater on the validator then the difference (amount that should have been slashed from the first validator) is assigned to the amount being paid out.

Re-Delegation

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

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

When re-delegation is initiated, delegator shares remain accounted for within the Candidate.Shares, the term RedelegatingShares is incremented and a queue element is created.

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
}

During the unbonding period all unbonding shares do not count towards the voting power of a validator. Once the QueueElemReDelegation has reached maturity, the appropriate unbonding shares are removed from the Shares and RedelegatingShares term.

Note that with the current menchanism a delegator cannot redelegate funds which are currently redelegating.

Cancel Unbonding

A delegator who is in the process of unbonding from a validator may use the re-delegate transaction to bond back to the original validator they're currently unbonding from (and only that validator). If initiated, the delegator will immediately begin to one again collect rewards from their validator.

Provision Calculations

Every hour atom provisions are assigned proportionally to the each slashable bonded token which includes re-delegating atoms but not unbonding tokens.

Validation provisions are payed directly to a global hold account (BondedTokenPool) and proportions of that hold account owned by each validator is defined as the GlobalStakeBonded. The tokens are payed as bonded tokens.

Here, the bonded tokens that a candidate has can be calculated as:

globalStakeExRate = params.BondedTokenPool / params.IssuedGlobalStakeShares
candidateCoins = candidate.GlobalStakeShares * globalStakeExRate 

If a delegator chooses to add more tokens to a validator then the amount of validator shares distributed is calculated on exchange rate (aka every delegators shares do not change value at that moment. The validator's accounting of distributed shares to delegators must also increased at every deposit.

delegatorExRate = validatorCoins / candidate.IssuedDelegatorShares 
createShares = coinsDeposited / delegatorExRate 
candidate.IssuedDelegatorShares += createShares

Whenever a validator has new tokens added to it, the BondedTokenPool is increased and must be reflected in the global parameter as well as the validators GlobalStakeShares. This calculation ensures that the worth of the GlobalStakeShares of other validators remains worth a constant absolute amount of the BondedTokenPool

createdGlobalStakeShares = coinsDeposited / globalStakeExRate 
validator.GlobalStakeShares +=  createdGlobalStakeShares
params.IssuedGlobalStakeShares +=  createdGlobalStakeShares

params.BondedTokenPool += coinsDeposited

Similarly, if a delegator wanted to unbond coins:

coinsWithdrawn = withdrawlShares * delegatorExRate

destroyedGlobalStakeShares = coinsWithdrawn / globalStakeExRate 
validator.GlobalStakeShares -= destroyedGlobalStakeShares
params.IssuedGlobalStakeShares -= destroyedGlobalStakeShares
params.BondedTokenPool -= coinsWithdrawn

Note that when an re-delegation occurs the shares to move are placed in an re-delegation queue where they continue to collect validator provisions until queue element matures. Although provisions are collected during re-delegation, re-delegation tokens do not contribute to the voting power of a validator.

Validator provisions are minted on an hourly basis (the first block of a new hour). The annual target of between 7% and 20%. The long-term target ratio of bonded tokens to unbonded tokens is 67%.

The target annual inflation rate is recalculated for each previsions cycle. The inflation is also subject to a rate change (positive of negative) depending or the distance from the desired ratio (67%). The maximum rate change possible is defined to be 13% per year, however the annual inflation is capped as between 7% and 20%.

inflationRateChange(0) = 0
annualInflation(0) = 0.07

bondedRatio = bondedTokenPool / totalTokenSupply
AnnualInflationRateChange = (1 - bondedRatio / 0.67) * 0.13

annualInflation += AnnualInflationRateChange

if annualInflation > 0.20 then annualInflation = 0.20
if annualInflation < 0.07 then annualInflation = 0.07

provisionTokensHourly = totalTokenSupply * annualInflation / (365.25*24)

Because the validators hold a relative bonded share (GlobalStakeShare), when more bonded tokens are added proportionally to all validators the only term which needs to be updated is the BondedTokenPool. So for each previsions cycle:

params.BondedTokenPool += provisionTokensHourly

Fee Calculations

Collected fees are pooled globally and divided out passively to validators and delegators. Each validator has the opportunity to charge commission to the delegators on the fees collected on behalf of the delegators by the validators. Fees are paid directly into a global fee pool. Due to the nature of of passive accounting whenever changes to parameters which affect the rate of fee distribution occurs, withdrawal of fees must also occur.

  • when withdrawing one must withdrawal the maximum amount they are entitled too, leaving nothing in the pool,
  • when bonding, unbonding, or re-delegating tokens to an existing account a full withdrawal of the fees must occur (as the rules for lazy accounting change),
  • when a candidate chooses to change the commission on fees, all accumulated commission fees must be simultaneously withdrawn.

When the validator is the proposer of the round, that validator (and their delegators) receives between 1% and 5% of fee rewards, the reserve tax is then charged, then the remainder is distributed socially by voting power to all validators including the proposer validator. The amount of proposer reward is calculated from pre-commits Tendermint messages. All provision rewards are added to a provision reward pool which validator holds individually. Here note that BondedShares represents the sum of all voting power saved in the GlobalState (denoted gs).

proposerReward = feesCollected * (0.01 + 0.04 
                  * sumOfVotingPowerOfPrecommitValidators / gs.BondedShares)
candidate.ProposerRewardPool += proposerReward

reserveTaxed = feesCollected * params.ReserveTax
gs.ReservePool += reserveTaxed

distributedReward = feesCollected - proposerReward - reserveTaxed
gs.FeePool += distributedReward
gs.SumFeesReceived += distributedReward
gs.RecentFee = distributedReward

The entitlement to the fee pool held by the each validator can be accounted for lazily. First we must account for a candidate's count and adjustment. The count represents a lazy accounting of what that candidates entitlement to the fee pool would be if there VotingPower was to never change and they were to never withdraw fees.

candidate.count = candidate.VotingPower * BlockHeight

Similarly the GlobalState count can be passively calculated whenever needed, where BondedShares is the updated sum of voting powers from all validators.

gs.count = gs.BondedShares * BlockHeight

The adjustment term accounts for changes in voting power and withdrawals of fees. The adjustment factor must be persisted with the candidate and modified whenever fees are withdrawn from the candidate or the voting power of the candidate changes. When the voting power of the candidate changes the Adjustment factor is increased/decreased by the cumulative difference in the voting power if the voting power has been the new voting power as opposed to the old voting power for the entire duration of the blockchain up the previous block. Each time there is an adjustment change the GlobalState (denoted gs) Adjustment must also be updated.

simplePool = candidate.count / gs.count * gs.SumFeesReceived
projectedPool = candidate.PrevPower * (height-1) 
                / (gs.PrevPower * (height-1)) * gs.PrevFeesReceived
                + candidate.Power / gs.Power * gs.RecentFee

AdjustmentChange = simplePool - projectedPool
candidate.AdjustmentRewardPool += AdjustmentChange
gs.Adjustment += AdjustmentChange

Every instance that the voting power changes, information about the state of the validator set during the change must be recorded as a powerChange for other validators to run through. Before any validator modifies its voting power it must first run through the above calculation to determine the change in their caandidate.AdjustmentRewardPool for all historical changes in the set of powerChange which they have not yet synced to. The set of all powerChange may be trimmed from its oldest members once all validators have synced past the height of the oldest powerChange. This trim procedure will occur on an epoch basis.

type powerChange struct {
    height      int64        // block height at change
    power       rational.Rat // total power at change
    prevpower   rational.Rat // total power at previous height-1 
    feesin      coins.Coin   // fees in at block height
    prevFeePool coins.Coin   // total fees in at previous block height
}

Note that the adjustment factor may result as negative if the voting power of a different candidate has decreased.

candidate.AdjustmentRewardPool += withdrawn
gs.Adjustment += withdrawn

Now the entitled fee pool of each candidate can be lazily accounted for at any given block:

candidate.feePool = candidate.simplePool - candidate.Adjustment

So far we have covered two sources fees which can be withdrawn from: Fees from proposer rewards (candidate.ProposerRewardPool), and fees from the fee pool (candidate.feePool). However we should note that all fees from fee pool are subject to commission rate from the owner of the candidate. These next calculations outline the math behind withdrawing fee rewards as either a delegator to a candidate providing commission, or as the owner of a candidate who is receiving commission.

Calculations For Delegators and Candidates

The same mechanism described to calculate the fees which an entire validator is entitled to is be applied to delegator level to determine the entitled fees for each delegator and the candidates entitled commission from gs.FeesPool and candidate.ProposerRewardPool.

The calculations are identical with a few modifications to the parameters:

  • Delegator's entitlement to gs.FeePool:
    • entitled party voting power should be taken as the effective voting power after commission is retrieved, bond.Shares/candidate.TotalDelegatorShares * candidate.VotingPower * (1 - candidate.Commission)
  • Delegator's entitlement to candidate.ProposerFeePool
    • global power in this context is actually shares candidate.TotalDelegatorShares
    • entitled party voting power should be taken as the effective shares after commission is retrieved, bond.Shares * (1 - candidate.Commission)
  • Candidate's commission entitlement to gs.FeePool
    • entitled party voting power should be taken as the effective voting power of commission portion of total voting power, candidate.VotingPower * candidate.Commission
  • Candidate's commission entitlement to candidate.ProposerFeePool
    • global power in this context is actually shares candidate.TotalDelegatorShares
    • entitled party voting power should be taken as the of commission portion of total delegators shares, candidate.TotalDelegatorShares * candidate.Commission

For more implementation ideas see spreadsheet spec/AbsoluteFeeDistrModel.xlsx

As mentioned earlier, every time the voting power of a delegator bond is changing either by unbonding or further bonding, all fees must be simultaneously withdrawn. Similarly if the validator changes the commission rate, all commission on fees must be simultaneously withdrawn.

Other general notes on fees accounting

  • When a delegator chooses to re-delegate shares, fees continue to accumulate until the re-delegation queue reaches maturity. At the block which the queue reaches maturity and shares are re-delegated all available fees are simultaneously withdrawn.
  • Whenever a totally new validator is added to the validator set, the accum of the entire candidate must be 0, meaning that the initial value for candidate.Adjustment must be set to the value of canidate.Count for the height which the candidate is added on the validator set.
  • The feePool of a new delegator bond will be 0 for the height at which the bond was added. This is achieved by setting DelegatorBond.FeeWithdrawalHeight to the height which the bond was added.