transaction stake updates

This commit is contained in:
rigelrozanski 2018-05-25 18:52:34 -04:00
parent 35956c1c78
commit b8cf5f347e
4 changed files with 232 additions and 319 deletions

View File

@ -0,0 +1,88 @@
# Validator Set Changes
## 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, and is only non-zero if the validator becomes an active validator:
```go
type ValidatorSigningInfo struct {
StartHeight int64
SignedBlocksBitArray BitArray
}
```
Where:
* `StartHeight` is set to the height that the validator 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
```

View File

@ -1,16 +1,7 @@
## State
The staking module persists the following information to the store:
* `Params`, a struct describing the global pools, inflation, and fees
* `Pool`, a struct describing the global pools, inflation, and fees
* `ValidatorValidators: <pubkey | shares> => <validator>`, a map of all validators (including current validators) in the store,
indexed by their public key and shares in the global pool.
* `DelegatorBonds: < delegator-address | validator-pubkey > => <delegator-bond>`. a map of all delegations by a delegator to a validator,
indexed by delegator address and validator pubkey.
public key
### Pool
- index: n/a single-record
The pool is a space for all dynamic global state of the Cosmos Hub. It tracks
information about the total amounts of Atoms in all states, representative
@ -39,6 +30,7 @@ type PoolShares struct {
```
### Params
- index: n/a single-record
Params is global data structure that stores system parameters and defines
overall functioning of the stake module.
@ -56,6 +48,11 @@ type Params struct {
```
### Validator
- index 1: validator owner address
- index 2: validator Tendermint PubKey
- index 3: bonded validators only
- index 4: voting power
- index 5: Tendermint updates
The `Validator` holds the current state and some historical actions of the
validator.
@ -90,10 +87,8 @@ type Description struct {
}
```
* RedelegatingShares: The portion of `IssuedDelegatorShares` which are
currently re-delegating to a new validator
### Delegation
- index: delegation address
Atom holders may delegate coins to validators; under this circumstance their
funds are held in a `Delegation` data structure. It is owned by one
@ -110,10 +105,12 @@ type Delegation struct {
```
### UnbondingDelegation
- index: delegation address
A UnbondingDelegation object is created every time an unbonding is initiated.
It must be completed with a second transaction provided by the delegation owner
The unbond must be completed with a second transaction provided by the delegation owner
after the unbonding period has passed.
```golang
type UnbondingDelegation struct {
@ -126,17 +123,17 @@ type UnbondingDelegation struct {
```
### Redelegation
A redelegation object is created every time a redelegation occurs. It must be
completed with a second transaction provided by the delegation owner after the
unbonding period has passed. The destination delegation of a redelegation may
not itself undergo a new redelegation until the original redelegation has been
completed.
- index: delegation address
- index 1: delegation address
- index 2: source validator owner address
- index 3: destination validator owner address
A redelegation object is created every time a redelegation occurs. The
redelegation must be completed with a second transaction provided by the
delegation owner after the unbonding period has passed. The destination
delegation of a redelegation may not itself undergo a new redelegation until
the original redelegation has been completed.
```golang
type Redelegation struct {
SourceDelegation []byte // source delegation key

View File

@ -1,67 +1,58 @@
### Transaction Overview
In this section we describe the processing of the transactions and the
corresponding updates to the state.
Available Transactions:
* TxDeclareCandidacy
* TxEditCandidacy
* TxDelegate
* TxUnbond
* TxRedelegate
* TxProveLive
- TxCreateValidator
- TxEditValidator
- TxDelegation
- TxRedelegation
- TxUnbond
## 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
`loadValidator(store, PubKey)` to obtain a Validator structure from the store,
and `saveValidator(store, validator)` 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.
Other notes:
- `tx` denotes a reference to the transaction being processed
- `sender` denotes the address of the sender of the transaction
- `getXxx`, `setXxx`, and `removeXxx` functions are used to retrieve and
modify objects from the store
- `sdk.Rat` refers to a rational numeric type specified by the sdk.
### TxDeclareCandidacy
### TxCreateValidator
A validator candidacy is declared using the `TxDeclareCandidacy` transaction.
A validator is created using the `TxCreateValidator` transaction.
```golang
type TxDeclareCandidacy struct {
type TxCreateValidator struct {
OwnerAddr sdk.Address
ConsensusPubKey crypto.PubKey
Amount coin.Coin
GovernancePubKey crypto.PubKey
Commission rational.Rat
CommissionMax int64
CommissionMaxChange int64
SelfDelegation coin.Coin
Description Description
Commission sdk.Rat
CommissionMax sdk.Rat
CommissionMaxChange sdk.Rat
}
declareCandidacy(tx TxDeclareCandidacy):
validator = loadValidator(store, tx.PubKey)
if validator != nil return // validator with that public key already exists
createValidator(tx TxCreateValidator):
validator = getValidator(tx.OwnerAddr)
if validator != nil return // only one validator per address
validator = NewValidator(tx.PubKey)
validator.Status = Unbonded
validator.Owner = sender
init validator VotingPower, GlobalStakeShares, IssuedDelegatorShares, RedelegatingShares and Adjustment to rational.Zero
init commision related fields based on the values from tx
validator.ProposerRewardPool = Coin(0)
validator.Description = tx.Description
validator = NewValidator(OwnerAddr, ConsensusPubKey, GovernancePubKey, Description)
init validator poolShares, delegatorShares set to 0 //XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
init validator commision fields from tx
validator.PoolShares = 0
saveValidator(store, validator)
setValidator(validator)
txDelegate = TxDelegate(tx.PubKey, tx.Amount)
return delegateWithValidator(txDelegate, validator)
// see delegateWithValidator function in [TxDelegate](TxDelegate)
txDelegate = TxDelegate(tx.OwnerAddr, tx.OwnerAddr, tx.SelfDelegation)
delegate(txDelegate, validator) // see delegate function in [TxDelegate](TxDelegate)
return
```
### TxEditCandidacy
### TxEditValidator
If either the `Description` (excluding `DateBonded` which is constant),
`Commission`, or the `GovernancePubKey` need to be updated, the
@ -70,87 +61,51 @@ If either the `Description` (excluding `DateBonded` which is constant),
```golang
type TxEditCandidacy struct {
GovernancePubKey crypto.PubKey
Commission int64
Commission sdk.Rat
Description Description
}
editCandidacy(tx TxEditCandidacy):
validator = loadValidator(store, tx.PubKey)
if validator == nil or validator.Status == Revoked return
validator = getValidator(tx.ValidatorAddr)
if tx.Commission > CommissionMax || tx.Commission < 0 return halt tx
if rateChange(tx.Commission) > CommissionMaxChange return halt tx
validator.Commission = tx.Commission
if tx.GovernancePubKey != nil validator.GovernancePubKey = tx.GovernancePubKey
if tx.Commission >= 0 validator.Commission = tx.Commission
if tx.Description != nil validator.Description = tx.Description
saveValidator(store, validator)
setValidator(store, validator)
return
```
### TxDelegate
### TxDelegation
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 validator's delegator shares that are assigned to
`DelegatorBond.Shares`.
Within this transaction the delegator provides coins, and in return receives
some amount of their validator's delegator-shares that are assigned to
`Delegation.Shares`.
```golang
type TxDelegate struct {
PubKey crypto.PubKey
Amount coin.Coin
DelegatorAddr sdk.Address
ValidatorAddr sdk.Address
Amount sdk.Coin
}
delegate(tx TxDelegate):
validator = loadValidator(store, tx.PubKey)
if validator == nil return
return delegateWithValidator(tx, validator)
delegateWithValidator(tx TxDelegate, validator Validator):
pool = getPool()
if validator.Status == Revoked return
if validator.Status == Bonded
poolAccount = params.HoldBonded
else
poolAccount = params.HoldUnbonded
delegation = getDelegatorBond(DelegatorAddr, ValidatorAddr)
if delegation == nil then delegation = NewDelegation(DelegatorAddr, ValidatorAddr)
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, validator)
bond.Shares += issuedDelegatorShares
saveValidator(store, validator)
saveDelegatorBond(store, sender, bond)
saveGlobalState(store, gs)
return
addTokens(amount coin.Coin, validator Validator):
if validator.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
validator.GlobalStakeShares += issuedShares
validator, pool, issuedDelegatorShares = validator.addTokensFromDel(tx.Amount, pool)
delegation.Shares += issuedDelegatorShares
if validator.IssuedDelegatorShares.IsZero()
exRate = rational.One
else
exRate = validator.GlobalStakeShares / validator.IssuedDelegatorShares
issuedDelegatorShares = issuedShares / exRate
validator.IssuedDelegatorShares += issuedDelegatorShares
return issuedDelegatorShares
exchangeRate(shares rational.Rat, tokenAmount int64):
if shares.IsZero() then return rational.One
return tokenAmount / shares
setDelegation(delegation)
updateValidator(validator)
setPool(pool)
return
```
### TxUnbond
@ -159,125 +114,89 @@ Delegator unbonding is defined with the following transaction:
```golang
type TxUnbond struct {
PubKey crypto.PubKey
Shares rational.Rat
DelegatorAddr sdk.Address
ValidatorAddr sdk.Address
Shares string
}
unbond(tx TxUnbond):
bond = loadDelegatorBond(store, sender, tx.PubKey)
if bond == nil return
if bond.Shares < tx.Shares return
bond.Shares -= tx.Shares
validator = loadValidator(store, tx.PubKey)
revokeCandidacy = false
if bond.Shares.IsZero()
if sender == validator.Owner and validator.Status != Revoked then revokeCandidacy = true then removeDelegatorBond(store, sender, bond)
else
saveDelegatorBond(store, sender, bond)
if validator.Status == Bonded
poolAccount = params.HoldBonded
else
poolAccount = params.HoldUnbonded
returnedCoins = removeShares(validator, shares)
unbondDelegationElem = QueueElemUnbondDelegation(tx.PubKey, currentHeight(), sender, returnedCoins, startSlashRatio)
unbondDelegationQueue.add(unbondDelegationElem)
transfer(poolAccount, unbondingPoolAddress, returnCoins)
delegation, found = getDelegatorBond(store, sender, tx.PubKey)
if !found == nil return
if revokeCandidacy
if validator.Status == Bonded then bondedToUnbondedPool(validator)
validator.Status = Revoked
if validator.IssuedDelegatorShares.IsZero()
removeValidator(store, tx.PubKey)
if msg.Shares == "MAX" {
if !bond.Shares.GT(sdk.ZeroRat()) {
return ErrNotEnoughBondShares(k.codespace, msg.Shares).Result()
else
saveValidator(store, validator)
var err sdk.Error
delShares, err = sdk.NewRatFromDecimal(msg.Shares)
if err != nil
return err
if bond.Shares.LT(delShares)
return ErrNotEnoughBondShares(k.codespace, msg.Shares).Result()
saveGlobalState(store, gs)
return
validator, found := k.GetValidator(ctx, msg.ValidatorAddr)
if !found {
return err
removeShares(validator Validator, shares rational.Rat):
globalPoolSharesToRemove = delegatorShareExRate(validator) * shares
if msg.Shares == "MAX"
delShares = bond.Shares
if validator.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
validator.GlobalStakeShares -= removedTokens
validator.IssuedDelegatorShares -= shares
return returnedCoins
delegatorShareExRate(validator Validator):
if validator.IssuedDelegatorShares.IsZero() then return rational.One
return validator.GlobalStakeShares / validator.IssuedDelegatorShares
bondedToUnbondedPool(validator Validator):
removedTokens = exchangeRate(gs.BondedShares, gs.BondedPool) * validator.GlobalStakeShares
gs.BondedShares -= validator.GlobalStakeShares
gs.BondedPool -= removedTokens
gs.UnbondedPool += removedTokens
issuedShares = removedTokens / exchangeRate(gs.UnbondedShares, gs.UnbondedPool)
gs.UnbondedShares += issuedShares
bond.Shares -= delShares
validator.GlobalStakeShares = issuedShares
validator.Status = Unbonded
unbondingDelegation = NewUnbondingDelegation(sender, delShares, currentHeight/Time, startSlashRatio)
setUnbondingDelegation(unbondingDelegation)
return transfer(address of the bonded pool, address of the unbonded pool, removedTokens)
revokeCandidacy := false
if bond.Shares.IsZero() {
if bond.DelegatorAddr == validator.Owner && validator.Revoked == false
revokeCandidacy = true
k.removeDelegation(ctx, bond)
else
bond.Height = currentBlockHeight
setDelegation(bond)
pool := k.GetPool(ctx)
validator, pool, returnAmount := validator.removeDelShares(pool, delShares)
k.setPool(ctx, pool)
AddCoins(ctx, bond.DelegatorAddr, returnAmount)
if revokeCandidacy
validator.Revoked = true
validator = updateValidator(ctx, validator)
if validator.DelegatorShares == 0 {
removeValidator(ctx, validator.Owner)
return
```
### TxRedelegate
### TxRedelegation
The re-delegation command allows delegators to switch validators while still
receiving equal reward to as if they had never unbonded.
The redelegation command allows delegators to instantly switch validators.
```golang
type TxRedelegate struct {
PubKeyFrom crypto.PubKey
PubKeyTo crypto.PubKey
Shares rational.Rat
DelegatorAddr Address
ValidatorFrom Validator
ValidatorTo Validator
Shares sdk.Rat
}
redelegate(tx TxRedelegate):
bond = loadDelegatorBond(store, sender, tx.PubKey)
if bond == nil then return
pool = getPool()
delegation = getDelegatorBond(tx.DelegatorAddr, tx.ValidatorFrom.Owner)
if delegation == nil then return
if bond.Shares < tx.Shares return
validator = loadValidator(store, tx.PubKeyFrom)
if validator == nil return
if delegation.Shares < tx.Shares return
delegation.shares -= Tx.Shares
validator, pool, createdCoins = validator.RemoveShares(pool, tx.Shares)
setPool(pool)
validator.RedelegatingShares += tx.Shares
reDelegationElem = QueueElemReDelegate(tx.PubKeyFrom, currentHeight(), sender, tx.Shares, tx.PubKeyTo)
redelegationQueue.add(reDelegationElem)
redelegation = newRedelegation(validatorFrom, validatorTo, Shares, createdCoins)
setRedelegation(redelegation)
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

@ -1,13 +1,9 @@
# 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
The Tendermint validator set may be updated by state transitions that run at
the beginning and end of every block. The Tendermint validator set may be
changed by validators either being revoked due to inactivity/unexpected
behaviour (covered in slashing) or changed in validator power.
At the end of every block, we run the following:
@ -101,90 +97,3 @@ unbondedToBondedPool(validator Validator):
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, and is only non-zero if the validator becomes an active validator:
```go
type ValidatorSigningInfo struct {
StartHeight int64
SignedBlocksBitArray BitArray
}
```
Where:
* `StartHeight` is set to the height that the validator 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
```