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 6 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 Fixed bug where the cliff validator was not be updated correctly
80 changes: 66 additions & 14 deletions x/stake/keeper/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,13 +191,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 +210,26 @@ 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 {
k.updateCliffValidator(ctx, validator)
Copy link
Contributor

Choose a reason for hiding this comment

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

from the godoc of k.updateCliffVlaidator, it seems that we should have the cliffPower != nil check in there?

Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure I totally agree, I think have the explicit check here makes the context more clear opposed to always calling updateCliffValidator.

}

// 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 +239,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 +252,54 @@ 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.
func (k Keeper) updateCliffValidator(ctx sdk.Context, affectedVal types.Validator) {
cliffAddr := sdk.AccAddress(k.GetCliffValidator(ctx))
if !bytes.Equal(cliffAddr, affectedVal.Owner) {
return
}

store := ctx.KVStore(k.storeKey)
pool := k.GetPool(ctx)

// 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 in power
// where only the smallest by power is needed.
iterator := sdk.KVStorePrefixIterator(store, ValidatorsByPowerIndexKey)
Copy link
Contributor Author

@cwgoes cwgoes Aug 5, 2018

Choose a reason for hiding this comment

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

Hmm I don't think this is the correct logic. The power store includes (a) many validator candidates which are not validators (not in the top hundred by stake) and (b) revoked validators, neither of which should be set as the cliff validator. The cliff validator is simply the "lowest-ranked bonded validator by stake" - but this logic reads the "lowest-ranked validator candidate", which is not necessarily the same.

For the case of "same cliff validator, increased power" this rough logic is fine. For the case of "cliff validator increased power, so now no longer cliff validator" I think we need to seek from the old cliff validator power store key and find the next-highest stake validator (which might become the cliff validator).

Copy link
Contributor

Choose a reason for hiding this comment

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

I see. It did not occur to me that the power store held those validators as well. I'll need to refactor this and add test cases where we also have revoked validators and candidates which are not validators.

if !iterator.Valid() {
panic("invalid iterator for validator power store")
}

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

iterator.Close()

if bytes.Equal(affectedVal.Owner, cliffValByPower.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(cliffValByPower.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, cliffValByPower, 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 +364,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 +376,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 +403,7 @@ func (k Keeper) UpdateBondedValidators(ctx sdk.Context,
validatorToBond = validator
newValidatorBonded = true
}

bondedValidatorsCount++

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

iterator.Next()
}

iterator.Close()

// clear or set the cliff validator
Expand All @@ -372,17 +423,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 +442,7 @@ func (k Keeper) UpdateBondedValidators(ctx sdk.Context,
return validator, true
}
}

return types.Validator{}, false
}

Expand Down
168 changes: 168 additions & 0 deletions x/stake/keeper/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,174 @@ func TestUpdateValidatorByPowerIndex(t *testing.T) {
require.True(t, keeper.validatorByPowerIndexExists(ctx, power))
}

func TestCliffValidatorPowerIncrease(t *testing.T) {
// 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 = 2
keeper.SetParams(ctx, params)

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

// add validators
validatorA := types.NewValidator(addrVals[0], PKs[0], types.Description{Moniker: "A"})
validatorA, pool, _ = validatorA.AddTokensFromDel(pool, 200)

keeper.SetPool(ctx, pool)
validatorA = keeper.UpdateValidator(ctx, validatorA)

validatorB := types.NewValidator(addrVals[1], PKs[1], types.Description{Moniker: "B"})
validatorB, pool, _ = validatorB.AddTokensFromDel(pool, 99)

keeper.SetPool(ctx, pool)
validatorB = keeper.UpdateValidator(ctx, validatorB)

// assert correct cliff validator & cliff power
require.Equal(t, validatorB.Owner, sdk.AccAddress(keeper.GetCliffValidator(ctx)))
cliffPower := keeper.GetCliffValidatorPower(ctx)
require.Equal(t, GetValidatorsByPowerIndexKey(validatorB, pool), cliffPower)

// add some tokens
validatorB, pool, _ = validatorB.AddTokensFromDel(pool, 2)
keeper.SetPool(ctx, pool)
validatorB = keeper.UpdateValidator(ctx, validatorB)

// assert validator B has new power
validatorB, found := keeper.GetValidator(ctx, validatorB.Owner)
require.True(t, found)
require.Equal(t, sdk.NewRat(101), validatorB.GetPower())

// assert cliff validator should not have switched
require.Equal(t, validatorB.Owner, sdk.AccAddress(keeper.GetCliffValidator(ctx)))

// assert cliff validator power has been updated for the expected validator
cliffPower = keeper.GetCliffValidatorPower(ctx)
require.Equal(t, GetValidatorsByPowerIndexKey(validatorB, pool), cliffPower)
}

func TestCliffValidatorSwitch(t *testing.T) {
// 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 = 3
keeper.SetParams(ctx, params)

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

// add validators
validatorA := types.NewValidator(addrVals[0], PKs[0], types.Description{Moniker: "A"})
validatorA, pool, _ = validatorA.AddTokensFromDel(pool, 100)

keeper.SetPool(ctx, pool)
validatorA = keeper.UpdateValidator(ctx, validatorA)

validatorB := types.NewValidator(addrVals[1], PKs[1], types.Description{Moniker: "B"})
validatorB, pool, _ = validatorB.AddTokensFromDel(pool, 99)

keeper.SetPool(ctx, pool)
validatorB = keeper.UpdateValidator(ctx, validatorB)

validatorC := types.NewValidator(addrVals[2], PKs[2], types.Description{Moniker: "C"})
validatorC, pool, _ = validatorC.AddTokensFromDel(pool, 200)

keeper.SetPool(ctx, pool)
validatorC = keeper.UpdateValidator(ctx, validatorC)

// assert correct cliff validator & cliff power
require.Equal(t, validatorB.Owner, sdk.AccAddress(keeper.GetCliffValidator(ctx)))
cliffPower := keeper.GetCliffValidatorPower(ctx)
require.Equal(t, GetValidatorsByPowerIndexKey(validatorB, pool), cliffPower)

// add a lot of tokens
validatorB, pool, _ = validatorB.AddTokensFromDel(pool, 200)
keeper.SetPool(ctx, pool)
validatorB = keeper.UpdateValidator(ctx, validatorB)

validatorA, found := keeper.GetValidator(ctx, validatorA.Owner)
require.True(t, found)
require.Equal(t, sdk.NewRat(100), validatorA.GetPower())

// assert validator B has higher power now
validatorB, found = keeper.GetValidator(ctx, validatorB.Owner)
require.True(t, found)
require.Equal(t, sdk.NewRat(299), validatorB.GetPower())

// assert cliff validator should have switched
require.Equal(t, validatorA.Owner, sdk.AccAddress(keeper.GetCliffValidator(ctx)))

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

func TestCliffValidatorNoSwitch(t *testing.T) {
// 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 = 2
keeper.SetParams(ctx, params)

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

// add validators
validatorA := types.NewValidator(addrVals[0], PKs[0], types.Description{Moniker: "A"})
validatorA, pool, _ = validatorA.AddTokensFromDel(pool, 200)

keeper.SetPool(ctx, pool)
validatorA = keeper.UpdateValidator(ctx, validatorA)

validatorB := types.NewValidator(addrVals[1], PKs[1], types.Description{Moniker: "B"})
validatorB, pool, _ = validatorB.AddTokensFromDel(pool, 100)

keeper.SetPool(ctx, pool)
validatorB = keeper.UpdateValidator(ctx, validatorB)

// assert correct cliff validator & cliff power
require.Equal(t, validatorB.Owner, sdk.AccAddress(keeper.GetCliffValidator(ctx)))
cliffPower := keeper.GetCliffValidatorPower(ctx)
require.Equal(t, GetValidatorsByPowerIndexKey(validatorB, pool), cliffPower)

// add a lot of tokens
validatorB, pool, _ = validatorB.AddTokensFromDel(pool, 100)
keeper.SetPool(ctx, pool)
validatorB = keeper.UpdateValidator(ctx, validatorB)

// assert validator B has higher power now
validatorB, found := keeper.GetValidator(ctx, validatorB.Owner)
require.True(t, found)
require.Equal(t, sdk.NewRat(200), validatorB.GetPower())

// assert the two validators have equal power
validatorA, found = keeper.GetValidator(ctx, validatorA.Owner)
require.True(t, found)
require.Equal(t, validatorB.GetPower(), validatorA.GetPower())

// assert cliff validator should not have been switched as their power is equal
require.Equal(t, validatorB.Owner, sdk.AccAddress(keeper.GetCliffValidator(ctx)))

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

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