cosmos-sdk/docs/spec/staking/old/spec2.md

699 lines
26 KiB
Markdown

# Stake Module
## Overview
The stake module is tasked with various core staking functionality,
including validator set rotation, unbonding periods, and the
distribution of inflationary provisions and transaction fees.
It is designed to efficiently facilitate small numbers of
validators (hundreds), and large numbers of delegators (tens of thousands).
Bonded Atoms are pooled globally and for each validator.
Validators have shares in the global pool, and delegators
have shares in the pool of every validator they delegate to.
Atom provisions simply accumulate in the global pool, making
each share worth proportionally more.
Validator shares can be redeemed for Atoms, but the Atoms will be locked in a queue
for an unbonding period before they can be withdrawn to an account.
Delegators can exchange one validator's shares for another immediately
(ie. they can re-delegate to another validator), but must then wait the
unbonding period before they can do it again.
Fees are pooled separately and withdrawn lazily, at any time.
They are not bonded, and can be paid in multiple tokens.
An adjustment factor is maintained for each validator
and delegator to determine the true proportion of fees in the pool they are entitled too.
Adjustment factors are updated every time a validator or delegator's voting power changes.
Validators and delegators must withdraw all fees they are entitled too before they can bond or
unbond Atoms.
## State
The staking module persists the following to the store:
- `GlobalState`, describing the global pools
- a `Candidate` for each candidate validator, indexed by public key
- a `Candidate` for each candidate validator, indexed by shares in the global pool (ie. ordered)
- a `DelegatorBond` for each delegation to a candidate by a delegator, indexed by delegator and candidate
public keys
- a `Queue` of unbonding delegations (TODO)
### Global State
``` golang
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
}
```
### Candidate
The `Candidate` struct holds the current state and some historical actions of
validators or candidate-validators.
``` golang
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
Candidates are indexed by their `Candidate.PubKey`.
Additionally, we index empty values by the candidates global stake shares concatenated with the public key.
TODO: be more precise.
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.
### DelegatorBond
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,
``` golang
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
### Unbonding Queue
- main unbonding queue contains both UnbondElem and RedelegateElem
- "queue" + <i>
- new unbonding queue every time a val leaves the validator set
- "queue"+ <candidate.pubkey > + <i>
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:
``` golang
type QueueElem struct {
Candidate crypto.PubKey
InitHeight int64 // when the queue was initiated
}
```
``` golang
type QueueElemUnbondCandidate struct {
QueueElem
}
```
``` golang
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
}
```
``` golang
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
}
```
Each `QueueElem` is persisted in the store until it is popped from the queue.
## Transactions
### TxDeclareCandidacy
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.
``` golang
type TxDeclareCandidacy struct {
PubKey crypto.PubKey
Amount coin.Coin
GovernancePubKey crypto.PubKey
Commission rational.Rat
CommissionMax int64
CommissionMaxChange int64
Description Description
}
```
### 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:
``` golang
type TxEditCandidacy struct {
GovernancePubKey crypto.PubKey
Commission int64
Description Description
}
```
### TxLivelinessCheck
Liveliness kicks are only checked when a `TxLivelinessCheck` transaction is
submitted.
``` golang
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 livliness is demonstrated
by sending in a `TxProveLive` transaction:
``` golang
type TxProveLive struct {
PubKey crypto.PubKey
}
```
### TxDelegate
All bonding, whether self-bonding or delegation, is done via
`TxDelegate`.
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`.
``` golang
type TxDelegate struct {
PubKey crypto.PubKey
Amount coin.Coin
}
```
### TxUnbond
In this context `TxUnbond` is used to
unbond either delegation bonds or validator self-bonds.
Delegator unbonding is defined by the following transaction type:
``` golang
type TxUnbond struct {
PubKey crypto.PubKey
Shares rational.Rat
}
```
### TxRedelegate
The re-delegation command allows delegators to switch validators while still
receiving equal reward to as if you had never unbonded.
``` golang
type TxRedelegate struct {
PubKeyFrom crypto.PubKey
PubKeyTo crypto.PubKey
Shares rational.Rat
}
```
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.
### TxWithdraw
....
## EndBlock
### Update 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.
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.
### Unbonding
When unbonding is initiated, delegator shares are immediately removed from the
candidate and added to a queue object.
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.
#### 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
## Invariants
-----------------------------
------------
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.
### Re-Delegation
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.
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.
----------------------------------------------
## 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.
```golang
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.