Merge pull request #971 from cosmos/bucky/staking-spec

Clean up staking spec structure
This commit is contained in:
Ethan Buchman 2018-05-09 09:54:25 -04:00 committed by GitHub
commit e462e1216d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 743 additions and 705 deletions

View File

@ -0,0 +1,33 @@
# Staking module specification
## Abstract
This paper specifies the Staking module of the Cosmos-SDK, which was first described in the [Cosmos Whitepaper](https://cosmos.network/about/whitepaper) in June 2016.
The module enables Cosmos-SDK based blockchain to support an advanced Proof-of-Stake system. In this system, holders of the native staking token of the chain can become candidate validators and can delegate tokens to candidate validators, ultimately determining the effective validator set for the system.
This module will be used in the Cosmos Hub, the first Hub in the Cosmos network.
## Contents
The following specification uses *Atom* as the native staking token. The module can be adapted to any Proof-Of-Stake blockchain by replacing *Atom* with the native staking token of the chain.
1. **[Design overview](overview.md)**
2. **Implementation**
1. **[State](state.md)**
1. Global State
2. Validator Candidates
3. Delegator Bonds
4. Unbond and Rebond Queue
2. **[Transactions](transactions.md)**
1. Declare Candidacy
2. Edit Candidacy
3. Delegate
4. Unbond
5. Redelegate
6. ProveLive
3. **[Validator Set Changes](valset-changes.md)**
1. Validator set updates
2. Slashing
3. Automatic Unbonding
3. **[Future improvements](future_improvements.md)**

View File

@ -1,5 +1,38 @@
# 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.
## Basic Terms and Definitions
* Cosmsos Hub - a Tendermint-based Proof of Stake blockchain system
@ -179,7 +212,3 @@ provisions cycle:
```go
GlobalState.BondedPool += provisionTokensHourly
```

View File

@ -1,79 +0,0 @@
# Slashing
A validator bond is an economic commitment made by a validator signing key to both the safety and liveness of
the consensus. Validator keys must not sign invalid messages which could
violate consensus safety, and their signed precommit messages must be regularly included in
block commits.
The incentivization of these two goals are treated separately.
## Safety
Messges which may compromise the safety of the underlying consensus protocol ("equivocations")
result in some amount of the offending validator's shares being removed ("slashed").
Currently, such messages include only the following:
- prevotes by the same validator for more than one BlockID at the same
Height and Round
- precommits by the same validator for more than one BlockID at the same
Height and Round
We call any such pair of conflicting votes `Evidence`. Full nodes in the network prioritize the
detection and gossipping of `Evidence` so that it may be rapidly included in blocks and the offending
validators punished.
For some `evidence` to be valid, it must satisfy:
`evidence.Height >= block.Height - MAX_EVIDENCE_AGE`
If valid evidence is included in a block, the offending validator loses
a constant `SLASH_PROPORTION` of their current stake:
```
oldShares = validator.shares
validator.shares = oldShares * (1 - SLASH_PROPORTION)
```
This ensures that offending validators are punished the same amount whether they
act as a single validator with X stake or as N validators with collectively X
stake.
## Liveness
Every block includes a set of precommits by the validators for the previous block,
known as the LastCommit. A LastCommit is valid so long as it contains precommits from +2/3 of voting power.
Proposers are incentivized to include precommits from all
validators in the LastCommit by receiving additional fees
proportional to the difference between the voting power included in the
LastCommit and +2/3 (see [TODO](https://github.com/cosmos/cosmos-sdk/issues/967)).
Validators are penalized for failing to be included in the LastCommit for some
number of blocks by being automatically unbonded.
TODO: do we do this by trying to track absence directly in the state, using
something like the below, or do we let users notify the app when a validator has
been absent using the
[TxLivenessCheck](https://github.com/cosmos/cosmos-sdk/blob/develop/docs/spec/staking/spec-technical.md#txlivelinesscheck).
A list, `ValidatorAbsenceInfos`, is stored in the state and used to track how often
validators were included in a LastCommit.
```go
// Ordered by ValidatorAddress.
// One entry for each validator.
type ValidatorAbsenceInfos []ValidatorAbsenceInfo
type ValidatorAbsenceInfo struct {
ValidatorAddress []byte // address of the validator
FirstHeight int64 // first height the validator was absent
Count int64 // number of heights validator was absent since (and including) first
}
```

View File

@ -1,622 +0,0 @@
# 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`, a struct describing the global pools, inflation, and
fees
* `ValidatorCandidates: <pubkey | shares> => <candidate>`, a map of all candidates (including current validators) in the store,
indexed by their public key and shares in the global pool.
* `DelegatorBonds: < delegator-address | candidate-pubkey > => <delegator-bond>`. a map of all delegations by a delegator to a candidate,
indexed by delegator address and candidate pubkey.
public key
* `UnbondQueue`, the queue of unbonding delegations
* `RedelegateQueue`, the queue of re-delegations
### Global State
The GlobalState contains information about the total amount of Atoms, the
global bonded/unbonded position, the Atom inflation rate, and the fees.
`Params` is global data structure that stores system parameters and defines overall functioning of the
module.
``` go
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` holds the current state and some historical
actions of validators or candidate-validators.
``` go
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.
``` go
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:
```golang
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.
```golang
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.
```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
* 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.
```golang
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:
```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 == 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`.
```golang
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:
```golang
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.
```golang
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.
```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 liveliness is demonstrated
by sending in a `TxProveLive` transaction:
```golang
type TxProveLive struct {
PubKey crypto.PubKey
}
```
### End of block handling
```golang
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)
```

204
docs/spec/staking/state.md Normal file
View File

@ -0,0 +1,204 @@
## State
The staking module persists the following information to the store:
* `GlobalState`, a struct describing the global pools, inflation, and
fees
* `ValidatorCandidates: <pubkey | shares> => <candidate>`, a map of all candidates (including current validators) in the store,
indexed by their public key and shares in the global pool.
* `DelegatorBonds: < delegator-address | candidate-pubkey > => <delegator-bond>`. a map of all delegations by a delegator to a candidate,
indexed by delegator address and candidate pubkey.
public key
* `UnbondQueue`, the queue of unbonding delegations
* `RedelegateQueue`, the queue of re-delegations
### Global State
The GlobalState contains information about the total amount of Atoms, the
global bonded/unbonded position, the Atom inflation rate, and the fees.
`Params` is global data structure that stores system parameters and defines overall functioning of the
module.
``` go
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` holds the current state and some historical
actions of validators or candidate-validators.
``` go
type CandidateStatus byte
const (
Bonded CandidateStatus = 0x01
Unbonded CandidateStatus = 0x02
Revoked CandidateStatus = 0x03
)
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.
``` go
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:
```golang
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.
```golang
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.
```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
}
```

View File

@ -0,0 +1,283 @@
### Transaction Overview
Available Transactions:
* TxDeclareCandidacy
* TxEditCandidacy
* TxDelegate
* TxUnbond
* TxRedelegate
* 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.
```golang
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:
```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 == 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`.
```golang
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:
```golang
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.
```golang
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
```
### TxProveLive
If a validator was automatically unbonded due to liveness issues and wishes to
assert it is still online, it can send `TxProveLive`:
```golang
type TxProveLive struct {
PubKey crypto.PubKey
}
```
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.
```
TODO: pseudo-code
```

View File

@ -0,0 +1,190 @@
# Validator Set Changes
The validator set may be updated by state transitions that run at the beginning and
end of every block. This can happen one of three ways:
- voting power of a validator changes due to bonding and unbonding
- voting power of validator is "slashed" due to conflicting signed messages
- validator is automatically unbonded due to inactivity
## Voting Power Changes
At the end of every block, we run the following:
(TODO remove inflation from here)
```golang
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)
```
## Slashing
Messges which may compromise the safety of the underlying consensus protocol ("equivocations")
result in some amount of the offending validator's shares being removed ("slashed").
Currently, such messages include only the following:
- prevotes by the same validator for more than one BlockID at the same
Height and Round
- precommits by the same validator for more than one BlockID at the same
Height and Round
We call any such pair of conflicting votes `Evidence`. Full nodes in the network prioritize the
detection and gossipping of `Evidence` so that it may be rapidly included in blocks and the offending
validators punished.
For some `evidence` to be valid, it must satisfy:
`evidence.Timestamp >= block.Timestamp - MAX_EVIDENCE_AGE`
where `evidence.Timestamp` is the timestamp in the block at height
`evidence.Height` and `block.Timestamp` is the current block timestamp.
If valid evidence is included in a block, the offending validator loses
a constant `SLASH_PROPORTION` of their current stake at the beginning of the block:
```
oldShares = validator.shares
validator.shares = oldShares * (1 - SLASH_PROPORTION)
```
This ensures that offending validators are punished the same amount whether they
act as a single validator with X stake or as N validators with collectively X
stake.
## Automatic Unbonding
Every block includes a set of precommits by the validators for the previous block,
known as the LastCommit. A LastCommit is valid so long as it contains precommits from +2/3 of voting power.
Proposers are incentivized to include precommits from all
validators in the LastCommit by receiving additional fees
proportional to the difference between the voting power included in the
LastCommit and +2/3 (see [TODO](https://github.com/cosmos/cosmos-sdk/issues/967)).
Validators are penalized for failing to be included in the LastCommit for some
number of blocks by being automatically unbonded.
The following information is stored with each validator candidate, and is only non-zero if the candidate becomes an active validator:
```go
type ValidatorSigningInfo struct {
StartHeight int64
SignedBlocksBitArray BitArray
}
```
Where:
* `StartHeight` is set to the height that the candidate became an active validator (with non-zero voting power).
* `SignedBlocksBitArray` is a bit-array of size `SIGNED_BLOCKS_WINDOW` that records, for each of the last `SIGNED_BLOCKS_WINDOW` blocks,
whether or not this validator was included in the LastCommit. It uses a `0` if the validator was included, and a `1` if it was not.
Note it is initialized with all 0s.
At the beginning of each block, we update the signing info for each validator and check if they should be automatically unbonded:
```
h = block.Height
index = h % SIGNED_BLOCKS_WINDOW
for val in block.Validators:
signInfo = val.SignInfo
if val in block.LastCommit:
signInfo.SignedBlocksBitArray.Set(index, 0)
else
signInfo.SignedBlocksBitArray.Set(index, 1)
// validator must be active for at least SIGNED_BLOCKS_WINDOW
// before they can be automatically unbonded for failing to be
// included in 50% of the recent LastCommits
minHeight = signInfo.StartHeight + SIGNED_BLOCKS_WINDOW
minSigned = SIGNED_BLOCKS_WINDOW / 2
blocksSigned = signInfo.SignedBlocksBitArray.Sum()
if h > minHeight AND blocksSigned < minSigned:
unbond the validator
```