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

486 lines
19 KiB
Markdown
Raw Normal View History

2018-02-11 09:15:15 -08:00
# 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 for it's service of securing blockchain network rewarded by the inflationary
provisions and transactions fees. This incentivizes correct behavior of the validators and provide 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 holder 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 securing network (and taking a risk of being slashed in case the
validator misbehaves), 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
- `map[PubKey]Candidate`, a map of validator candidates (including current validators), indexed by public key
- `map[rational.Rat]Candidate`, an ordered map of validator candidates (including current validators), indexed by
shares in the global pool (bonded or unbonded depending on candidate status)
- `map[[]byte]DelegatorBond`, a map of DelegatorBonds (for each delegation to a candidate by a delegator), indexed by
the delegator address and the candidate public key
- `queue[QueueElemUnbondDelegation]`, a queue of unbonding delegations
- `queue[QueueElemReDelegate]`, a queue of re-delegations
### Global State
GlobalState data structure contains total Atoms 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 feel accum (?).
``` golang
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 unbonded 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
}
```
### Candidate
The `Candidate` data structure 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
}
```
CandidateStatus can be VyingUnbonded, VyingUnbonding, Bonded, KickUnbonding and KickUnbonded.
``` golang
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` 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 the candidate is a validator.
- 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 validators; 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 validator. The sender of the transaction is
considered the owner of the bond.
``` golang
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
Unbonding and re-delegation process is implemented using the ordered queue data structure.
All queue elements used share a common structure:
``` golang
type QueueElem struct {
Candidate crypto.PubKey
InitHeight int64 // when the queue was initiated
}
```
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`, the final settlement of the unbonding is started or re-delegation is executed, and the element is
pop from the queue. Each `QueueElem` is persisted in the store until it is popped from the queue.
### QueueElemUnbondDelegation
``` golang
type QueueElemUnbondDelegation struct {
QueueElem
Payout Address // account to pay out to
Shares rational.Rat // amount of delegator 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.
### QueueElemReDelegate
``` 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
}
```
### Transaction Overview
Available Transactions:
- TxDeclareCandidacy
- TxEditCandidacy
- TxLivelinessCheck
- TxProveLive
- TxDelegate
- TxUnbond
- TxRedelegate
## Transaction processing
In this section we describe the processing of the transactions and the corresponding updates to the global state.
For the following text we will use gs to refer to the GlobalState data structure, candidateMap is a reference to the
map[PubKey]Candidate, delegatorBonds is a reference to map[[]byte]DelegatorBond, unbondDelegationQueue is a
reference to the queue[QueueElemUnbondDelegation] and redelegationQueue is the reference for the
queue[QueueElemReDelegate]. We use tx to denote reference to a transaction that is being processed.
### TxDeclareCandidacy
A 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 (TODO: What does this mean?).
``` golang
type TxDeclareCandidacy struct {
PubKey crypto.PubKey
Amount coin.Coin
GovernancePubKey crypto.PubKey
Commission rational.Rat
CommissionMax int64
CommissionMaxChange int64
Description Description
}
```
```
declareCandidacy(tx TxDeclareCandidacy):
// create and save the empty candidate
candidate = loadCandidate(store, tx.PubKey)
if candidate != nil then return
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)
// move coins from the sender account to a (self-bond) delegator account
// the candidate account and global shares are updated within here
txDelegate = TxDelegate{tx.BondUpdate}
return delegateWithCandidate(txDelegate, candidate)
```
### 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
}
```
```
editCandidacy(tx TxEditCandidacy):
candidate = loadCandidate(store, tx.PubKey)
if candidate == nil or candidate.Status == Unbonded return
if tx.GovernancePubKey != nil then candidate.GovernancePubKey = tx.GovernancePubKey
if tx.Commission >= 0 then candidate.Commission = tx.Commission
if tx.Description != nil then candidate.Description = tx.Description
saveCandidate(store, candidate)
return
```
### TxDelegate
All bonding, whether self-bonding or delegation, is done via `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`. The amount of created delegator shares depends on the candidate's
delegator-shares-to-atoms exchange rate and is computed as
`delegator-shares = delegator-coins / delegator-shares-to-atom-ex-rate`.
``` golang
type TxDelegate struct {
PubKey crypto.PubKey
Amount coin.Coin
}
```
```
delegate(tx TxDelegate):
candidate = loadCandidate(store, tx.PubKey)
if candidate == nil then return
return delegateWithCandidate(tx, candidate)
delegateWithCandidate(tx TxDelegate, candidate Candidate):
if candidate.Status == Revoked then return
if candidate.Status == Bonded then
poolAccount = address of the bonded pool
else
poolAccount = address of the unbonded pool
// Move coins from the delegator account to the bonded pool account
err = transfer(sender, poolAccount, tx.Amount)
if err != nil then return
// Get or create the delegator bond
bond = loadDelegatorBond(store, sender, tx.PubKey)
if bond == nil then
bond = DelegatorBond{tx.PubKey,rational.Zero, Coin(0), Coin(0)}
issuedDelegatorShares = candidate.addTokens(tx.Amount, gs)
bond.Shares = bond.Shares.Add(issuedDelegatorShares)
saveCandidate(store, candidate)
store.Set(GetDelegatorBondKey(sender, bond.PubKey), bond)
saveGlobalState(store, gs)
return
addTokens(amount int64, gs GlobalState, candidate Candidate):
// get the exchange rate of global pool shares over delegator shares
if candidate.IssuedDelegatorShares.IsZero() then
exRate = rational.One
else
exRate = candiate.GlobalStakeShares.Quo(candidate.IssuedDelegatorShares)
if candidate.Status == Bonded then
gs.BondedPool += amount
issuedShares = exchangeRate(gs.BondedShares, gs.BondedPool).Inv().Mul(amount) // (tokens/shares)^-1 * tokens
gs.BondedShares = gs.BondedShares.Add(issuedShares)
else
gs.UnbondedPool += amount
issuedShares = exchangeRate(gs.UnbondedShares, gs.UnbondedPool).Inv().Mul(amount) // (tokens/shares)^-1 * tokens
gs.UnbondedShares = gs.UnbondedShares.Add(issuedShares)
candidate.GlobalStakeShares = candidate.GlobalStakeShares.Add(issuedShares)
issuedDelegatorShares = exRate.Mul(receivedGlobalShares)
candidate.IssuedDelegatorShares = candidate.IssuedDelegatorShares.Add(issuedDelegatorShares)
return
exchangeRate(shares rational.Rat, tokenAmount int64):
if shares.IsZero() then return rational.One
return shares.Inv().Mul(tokenAmount)
```
### TxUnbond
Delegator unbonding is defined with the following transaction:
``` golang
type TxUnbond struct {
PubKey crypto.PubKey
Shares rational.Rat
}
```
```
unbond(tx TxUnbond):
// get delegator bond
bond = loadDelegatorBond(store, sender, tx.PubKey)
if bond == nil then return
// subtract bond tokens from delegator bond
if bond.Shares.LT(tx.Shares) return // bond shares < tx shares
bond.Shares = bond.Shares.Sub(ts.Shares)
candidate = loadCandidate(store, tx.PubKey)
if candidate == nil return
revokeCandidacy = false
if bond.Shares.IsZero() {
// if the bond is the owner of the candidate then trigger a revoke candidacy
if sender.Equals(candidate.Owner) and candidate.Status != Revoked then
revokeCandidacy = true
// remove the bond
removeDelegatorBond(store, sender, tx.PubKey)
else
saveDelegatorBond(store, sender, bond)
// transfer coins back to account
if candidate.Status == Bonded then
poolAccount = address of the bonded pool
else
poolAccount = address of the unbonded pool
returnCoins = candidate.removeShares(shares, gs)
// TODO: Shouldn't it be created a queue element in this case?
transfer(poolAccount, sender, returnCoins)
if revokeCandidacy then
// change the share types to unbonded if they were not already
if candidate.Status == Bonded then
// replace bonded shares with unbonded shares
tokens = gs.removeSharesBonded(candidate.GlobalStakeShares)
candidate.GlobalStakeShares = gs.addTokensUnbonded(tokens)
candidate.Status = Unbonded
transfer(address of the bonded pool, address of the unbonded pool, tokens)
// lastly update the status
candidate.Status = Revoked
// deduct shares from the candidate and save
if candidate.GlobalStakeShares.IsZero() then
removeCandidate(store, tx.PubKey)
else
saveCandidate(store, candidate)
saveGlobalState(store, gs)
return
removeDelegatorBond(candidate Candidate):
// first remove from the list of bonds
pks = loadDelegatorCandidates(store, sender)
for i, pk := range pks {
if candidate.Equals(pk) {
pks = append(pks[:i], pks[i+1:]...)
}
}
b := wire.BinaryBytes(pks)
store.Set(GetDelegatorBondsKey(delegator), b)
// now remove the actual bond
store.Remove(GetDelegatorBondKey(delegator, candidate))
//updateDelegatorBonds(store, delegator)
}
```
### Inflation provisions
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
GlobalState.Inflation(0) = 0.07
bondedRatio = GlobalState.BondedPool / GlobalState.TotalSupply
AnnualInflationRateChange = (1 - bondedRatio / 0.67) * 0.13
annualInflation += AnnualInflationRateChange
if annualInflation > 0.20 then GlobalState.Inflation = 0.20
if annualInflation < 0.07 then GlobalState.Inflation = 0.07
provisionTokensHourly = GlobalState.TotalSupply * GlobalState.Inflation / (365.25*24)
```
Because the validators hold a relative bonded share (`GlobalStakeShares`), when
more bonded tokens are added proportionally to all validators, the only term
which needs to be updated is the `GlobalState.BondedPool`. So for each previsions
cycle:
```
GlobalState.BondedPool += provisionTokensHourly
```