Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

R4R: Fix Cliff Validator Update Bugs #1858

Merged
merged 23 commits into from
Aug 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,16 @@ BUG FIXES
* \#1799 Fix `gaiad export`
* \#1828 Force user to specify amount on create-validator command by removing default
* \#1839 Fixed bug where intra-tx counter wasn't set correctly for genesis validators
* [staking] [#1858](https://github.com/cosmos/cosmos-sdk/pull/1858) Fixed bug where the cliff validator was not be updated correctly
* [tests] \#1675 Fix non-deterministic `test_cover`
* [client] \#1551: Refactored `CoreContext`
* Renamed `CoreContext` to `QueryContext`
* Removed all tx related fields and logic (building & signing) to separate
structure `TxContext` in `x/auth/client/context`
* Cleaned up documentation and API of what used to be `CoreContext`
* Implemented `KeyType` enum for key info

BUG FIXES
* \#1666 Add intra-tx counter to the genesis validators
* [tests] \#1551: Fixed invalid LCD test JSON payload in `doIBCTransfer`
* \#1787 Fixed bug where Tally fails due to revoked/unbonding validator
* [basecoin] Fixes coin transaction failure and account query [discussion](https://forum.cosmos.network/t/unmarshalbinarybare-expected-to-read-prefix-bytes-75fbfab8-since-it-is-registered-concrete-but-got-0a141dfa/664/6)
* \#1787 Fixed bug where Tally fails due to revoked/unbonding validator
* [basecoin] Fixes coin transaction failure and account query [discussion](https://forum.cosmos.network/t/unmarshalbinarybare-expected-to-read-prefix-bytes-75fbfab8-since-it-is-registered-concrete-but-got-0a141dfa/664/6)
100 changes: 86 additions & 14 deletions x/stake/keeper/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ func (k Keeper) GetValidatorsBonded(ctx sdk.Context) (validators []types.Validat
}

// get the group of bonded validators sorted by power-rank
//
// TODO: Rename to GetBondedValidatorsByPower or GetValidatorsByPower(ctx, status)
func (k Keeper) GetValidatorsByPower(ctx sdk.Context) []types.Validator {
store := ctx.KVStore(k.storeKey)
maxValidators := k.GetParams(ctx).MaxValidators
Expand Down Expand Up @@ -191,13 +193,13 @@ func (k Keeper) ClearTendermintUpdates(ctx sdk.Context) {

//___________________________________________________________________________

// Perfom all the necessary steps for when a validator changes its power. This
// Perform all the necessary steps for when a validator changes its power. This
// function updates all validator stores as well as tendermint update store.
// It may kick out validators if a new validator is entering the bonded validator
// group.
//
// nolint: gocyclo
// TODO: Remove above nolint, function needs to be simplified
// TODO: Remove above nolint, function needs to be simplified!
func (k Keeper) UpdateValidator(ctx sdk.Context, validator types.Validator) types.Validator {
store := ctx.KVStore(k.storeKey)
pool := k.GetPool(ctx)
Expand All @@ -210,19 +212,29 @@ func (k Keeper) UpdateValidator(ctx sdk.Context, validator types.Validator) type
cliffPower := k.GetCliffValidatorPower(ctx)

switch {
// if already bonded and power increasing only need to update tendermint
// if the validator is already bonded and the power is increasing, we need
// perform the following:
// a) update Tendermint
// b) check if the cliff validator needs to be updated
case powerIncreasing && !validator.Revoked &&
(oldFound && oldValidator.Status == sdk.Bonded):

bz := k.cdc.MustMarshalBinary(validator.ABCIValidator())
store.Set(GetTendermintUpdatesKey(validator.Owner), bz)

if cliffPower != nil {
cliffAddr := sdk.AccAddress(k.GetCliffValidator(ctx))
if bytes.Equal(cliffAddr, validator.Owner) {
k.updateCliffValidator(ctx, validator)
}
}

// if is a new validator and the new power is less than the cliff validator
case cliffPower != nil && !oldFound &&
bytes.Compare(valPower, cliffPower) == -1: //(valPower < cliffPower
// skip to completion

// if was unbonded and the new power is less than the cliff validator
// if was unbonded and the new power is less than the cliff validator
case cliffPower != nil &&
(oldFound && oldValidator.Status == sdk.Unbonded) &&
bytes.Compare(valPower, cliffPower) == -1: //(valPower < cliffPower
Expand All @@ -232,11 +244,10 @@ func (k Keeper) UpdateValidator(ctx sdk.Context, validator types.Validator) type
// a) not-bonded and now has power-rank greater than cliff validator
// b) bonded and now has decreased in power
default:

// update the validator set for this validator
// if updated, the validator has changed bonding status
updatedVal, updated := k.UpdateBondedValidators(ctx, validator)
if updated { // updates to validator occurred to be updated
if updated {
// the validator has changed bonding status
validator = updatedVal
break
}
Expand All @@ -246,13 +257,69 @@ func (k Keeper) UpdateValidator(ctx sdk.Context, validator types.Validator) type
bz := k.cdc.MustMarshalBinary(validator.ABCIValidator())
store.Set(GetTendermintUpdatesKey(validator.Owner), bz)
}

}

k.SetValidator(ctx, validator)
return validator
}

// updateCliffValidator determines if the current cliff validator needs to be
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like to call this, "maybeUpdateCliffValidator", which implies that it only happens conditionally.
The comment could be better, as it does more than determine the conditional.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ehhhh I'm not totally sure about maybeUpdateCliffValidator, but I will certainly update the godoc 👍

Copy link
Contributor

@rigelrozanski rigelrozanski Aug 7, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not of the idea of putting maybe in the func name. Something we've used for a similar idea for the sequence number is Ensure, -> I'd be okay with something more like EnsureCliffValidator or UpdateEnsureCliffValidator or UpdateOrEnsureCliffValidator

Use of this type of language for func naming would be cool to document in the Tenderming/coding - opened an issue here tendermint/coding#71

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed more appropriate nomenclature could be defined and standardized, but I'm not convinced Ensure* is it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ensure implies a post-condition for the function, which is accurate

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(then we should comment explaining what the postcondition is)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first it seemed to me, ensure implies something must be done (in this, now old, case would imply that cliff must be updated, even though it's possible for it not to).

Now that I think about it more, it kinda makes sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexanderbez Did we reach consensus on updating the name to ensureCliffValidator?

// updated or swapped. If the provided affected validator is the current cliff
// validator before it's power was increased, either the cliff power key will
// be updated or if it's power is greater than the next bonded validator by
// power, it'll be swapped.
func (k Keeper) updateCliffValidator(ctx sdk.Context, affectedVal types.Validator) {
var newCliffVal types.Validator

store := ctx.KVStore(k.storeKey)
pool := k.GetPool(ctx)
cliffAddr := sdk.AccAddress(k.GetCliffValidator(ctx))

oldCliffVal, found := k.GetValidator(ctx, cliffAddr)
if !found {
panic(fmt.Sprintf("cliff validator record not found for address: %v\n", cliffAddr))
}

// NOTE: We get the power via affectedVal since the store (by power key)
// has yet to be updated.
affectedValPower := affectedVal.GetPower()

// Create a validator iterator ranging from smallest to largest by power
// starting the current cliff validator's power.
start := GetValidatorsByPowerIndexKey(oldCliffVal, pool)
end := sdk.PrefixEndBytes(ValidatorsByPowerIndexKey)
iterator := store.Iterator(start, end)

if iterator.Valid() {
ownerAddr := iterator.Value()
currVal, found := k.GetValidator(ctx, ownerAddr)
if !found {
panic(fmt.Sprintf("validator record not found for address: %v\n", ownerAddr))
}

if currVal.Status != sdk.Bonded || currVal.Revoked {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good defensive coding!

panic(fmt.Sprintf("unexpected revoked or unbonded validator for address: %s\n", ownerAddr))
}

newCliffVal = currVal
iterator.Close()
} else {
panic("failed to create valid validator power iterator")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this might crash if the user is running only one validator. Not sure if that is a usecase we care about or not - but we could check it with params.MaxVals

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably want to support that for convenience in testing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not crash, but let me check.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it works with one validator 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sweet! Lets merge this then!

}

if bytes.Equal(affectedVal.Owner, newCliffVal.Owner) {
// The affected validator remains the cliff validator, however, since
// the store does not contain the new power, set the new cliff
// validator to the affected validator.
bz := GetValidatorsByPowerIndexKey(affectedVal, pool)
store.Set(ValidatorPowerCliffKey, bz)
} else if affectedValPower.GT(newCliffVal.GetPower()) {
// The affected validator no longer remains the cliff validator as it's
// power is greater than the new current cliff validator.
k.setCliffValidator(ctx, newCliffVal, pool)
}
}

func (k Keeper) updateForRevoking(ctx sdk.Context, oldFound bool, oldValidator, newValidator types.Validator) types.Validator {
if newValidator.Revoked && oldFound && oldValidator.Status == sdk.Bonded {
newValidator = k.unbondValidator(ctx, newValidator)
Expand Down Expand Up @@ -317,8 +384,9 @@ func (k Keeper) updateValidatorPower(ctx sdk.Context, oldFound bool, oldValidato
//
// nolint: gocyclo
// TODO: Remove the above golint
func (k Keeper) UpdateBondedValidators(ctx sdk.Context,
affectedValidator types.Validator) (updatedVal types.Validator, updated bool) {
func (k Keeper) UpdateBondedValidators(
ctx sdk.Context, affectedValidator types.Validator) (
updatedVal types.Validator, updated bool) {

store := ctx.KVStore(k.storeKey)

Expand All @@ -328,7 +396,8 @@ func (k Keeper) UpdateBondedValidators(ctx sdk.Context,
var validator, validatorToBond types.Validator
newValidatorBonded := false

iterator := sdk.KVStoreReversePrefixIterator(store, ValidatorsByPowerIndexKey) // largest to smallest
// create a validator iterator ranging from largest to smallest by power
iterator := sdk.KVStoreReversePrefixIterator(store, ValidatorsByPowerIndexKey)
for {
if !iterator.Valid() || bondedValidatorsCount > int(maxValidators-1) {
break
Expand All @@ -354,6 +423,7 @@ func (k Keeper) UpdateBondedValidators(ctx sdk.Context,
validatorToBond = validator
newValidatorBonded = true
}

bondedValidatorsCount++

// sanity check
Expand All @@ -363,6 +433,7 @@ func (k Keeper) UpdateBondedValidators(ctx sdk.Context,

iterator.Next()
}

iterator.Close()

// clear or set the cliff validator
Expand All @@ -372,17 +443,17 @@ func (k Keeper) UpdateBondedValidators(ctx sdk.Context,
k.clearCliffValidator(ctx)
}

// swap the cliff validator for a new validator if the affected validator was bonded
// swap the cliff validator for a new validator if the affected validator
// was bonded
if newValidatorBonded {

// unbond the cliff validator
if oldCliffValidatorAddr != nil {
cliffVal, found := k.GetValidator(ctx, oldCliffValidatorAddr)
if !found {
panic(fmt.Sprintf("validator record not found for address: %v\n", oldCliffValidatorAddr))
}
k.unbondValidator(ctx, cliffVal)

k.unbondValidator(ctx, cliffVal)
}

// bond the new validator
Expand All @@ -391,6 +462,7 @@ func (k Keeper) UpdateBondedValidators(ctx sdk.Context,
return validator, true
}
}

return types.Validator{}, false
}

Expand Down
57 changes: 57 additions & 0 deletions x/stake/keeper/validator_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package keeper

import (
"fmt"
"testing"

sdk "github.com/cosmos/cosmos-sdk/types"
Expand Down Expand Up @@ -88,6 +89,62 @@ func TestUpdateValidatorByPowerIndex(t *testing.T) {
require.True(t, keeper.validatorByPowerIndexExists(ctx, power))
}

func TestCliffValidatorChange(t *testing.T) {
numVals := 10
maxVals := 5

// create context, keeper, and pool for tests
ctx, _, keeper := CreateTestInput(t, false, 0)
pool := keeper.GetPool(ctx)

// create keeper parameters
params := keeper.GetParams(ctx)
params.MaxValidators = uint16(maxVals)
keeper.SetParams(ctx, params)

// create a random pool
pool.LooseTokens = sdk.NewRat(10000)
pool.BondedTokens = sdk.NewRat(1234)
keeper.SetPool(ctx, pool)

validators := make([]types.Validator, numVals)
for i := 0; i < len(validators); i++ {
moniker := fmt.Sprintf("val#%d", int64(i))
val := types.NewValidator(Addrs[i], PKs[i], types.Description{Moniker: moniker})
val.BondHeight = int64(i)
val.BondIntraTxCounter = int16(i)
val, pool, _ = val.AddTokensFromDel(pool, int64((i+1)*10))

keeper.SetPool(ctx, pool)
val = keeper.UpdateValidator(ctx, val)
validators[i] = val
}

// add a large amount of tokens to current cliff validator
currCliffVal := validators[numVals-maxVals]
currCliffVal, pool, _ = currCliffVal.AddTokensFromDel(pool, 200)
keeper.SetPool(ctx, pool)
currCliffVal = keeper.UpdateValidator(ctx, currCliffVal)

// assert new cliff validator to be set to the second lowest bonded validator by power
newCliffVal := validators[numVals-maxVals+1]
require.Equal(t, newCliffVal.Owner, sdk.AccAddress(keeper.GetCliffValidator(ctx)))

// assert cliff validator power should have been updated
cliffPower := keeper.GetCliffValidatorPower(ctx)
require.Equal(t, GetValidatorsByPowerIndexKey(newCliffVal, pool), cliffPower)

// add small amount of tokens to new current cliff validator
newCliffVal, pool, _ = newCliffVal.AddTokensFromDel(pool, 1)
keeper.SetPool(ctx, pool)
newCliffVal = keeper.UpdateValidator(ctx, newCliffVal)

// assert cliff validator has not change but increased in power
cliffPower = keeper.GetCliffValidatorPower(ctx)
require.Equal(t, newCliffVal.Owner, sdk.AccAddress(keeper.GetCliffValidator(ctx)))
require.Equal(t, GetValidatorsByPowerIndexKey(newCliffVal, pool), cliffPower)
}

func TestSlashToZeroPowerRemoved(t *testing.T) {
// initialize setup
ctx, _, keeper := CreateTestInput(t, false, 100)
Expand Down