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 16 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
1 change: 1 addition & 0 deletions PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,4 @@ 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
104 changes: 90 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 nessisary 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 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,73 @@ 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) {
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)

offset := 0
var newCliffVal types.Validator

for ; iterator.Valid() && offset < 1; iterator.Next() {
Copy link
Contributor Author

@cwgoes cwgoes Aug 8, 2018

Choose a reason for hiding this comment

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

Why a loop and an offset - we should only need to scan one record?

Copy link
Contributor

Choose a reason for hiding this comment

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

I believe Rige and I discussed to use an offset to be a bit cautious (offset should only be 1).

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it'll be cleaner w/o one. I'll remove it.

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
offset++
}

iterator.Close()
if offset == 0 {
panic("unable to find cliff validator")
}

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.
k.setCliffValidator(ctx, affectedVal, pool)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We just need to update the power, right? I think setCliffValidator will unnecessarily re-set the cliff validator address.

Copy link
Contributor

Choose a reason for hiding this comment

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

Correct! I suppose we can save a write, huh?

} 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 +388,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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

better indentation:

func (k Keeper) UpdateBondedValidators(
    ctx sdk.Context, affectedValidator types.Validator) (
    updatedVal types.Validator, updated bool) {

Copy link
Contributor

@alexanderbez alexanderbez Aug 8, 2018

Choose a reason for hiding this comment

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

Tbh, I don't necessarily agree here because it's hard for me to immediately see the return values (and their potential names) due to the location of the opening parenthesis. (e.g. immediate glance seems to show it has no returns).

But this is your baby and I'd like to keep in-line with what you've written and the team's standards, so I can update it 😄


store := ctx.KVStore(k.storeKey)

Expand All @@ -328,7 +400,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 +427,7 @@ func (k Keeper) UpdateBondedValidators(ctx sdk.Context,
validatorToBond = validator
newValidatorBonded = true
}

bondedValidatorsCount++

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

iterator.Next()
}

iterator.Close()

// clear or set the cliff validator
Expand All @@ -372,17 +447,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 +466,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