From b8cf5f347ea55820b42c4a48d98e071ab2bbc38c Mon Sep 17 00:00:00 2001 From: rigelrozanski Date: Fri, 25 May 2018 18:52:34 -0400 Subject: [PATCH] transaction stake updates --- docs/spec/slashing/valset-changes.md | 88 ++++++++ docs/spec/staking/state.md | 41 ++-- docs/spec/staking/transactions.md | 323 ++++++++++----------------- docs/spec/staking/valset-changes.md | 99 +------- 4 files changed, 232 insertions(+), 319 deletions(-) create mode 100644 docs/spec/slashing/valset-changes.md diff --git a/docs/spec/slashing/valset-changes.md b/docs/spec/slashing/valset-changes.md new file mode 100644 index 0000000000..c403e5d4c0 --- /dev/null +++ b/docs/spec/slashing/valset-changes.md @@ -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 +``` diff --git a/docs/spec/staking/state.md b/docs/spec/staking/state.md index c5601d0445..fa235479f6 100644 --- a/docs/spec/staking/state.md +++ b/docs/spec/staking/state.md @@ -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: => `, 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 > => `. 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 diff --git a/docs/spec/staking/transactions.md b/docs/spec/staking/transactions.md index c80ddc7a9b..685883dd11 100644 --- a/docs/spec/staking/transactions.md +++ b/docs/spec/staking/transactions.md @@ -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 -``` diff --git a/docs/spec/staking/valset-changes.md b/docs/spec/staking/valset-changes.md index 0547b171f5..92efa293b7 100644 --- a/docs/spec/staking/valset-changes.md +++ b/docs/spec/staking/valset-changes.md @@ -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 -```