-
Notifications
You must be signed in to change notification settings - Fork 3.8k
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
Fixes staking hooks safety issues #12578
Changes from all commits
1dbf873
135a3fb
8f21c56
bcd789a
7c48c6c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -323,22 +323,33 @@ func (k Keeper) redelegationEntryCanComplete(ctx sdk.Context, id uint64) (found | |
return true, nil | ||
} | ||
|
||
// WARNING: precondition: | ||
// Safety only guaranteed if this method is called AFTER staking.EndBlock | ||
func (k Keeper) validatorUnbondingCanComplete(ctx sdk.Context, id uint64) (found bool, err error) { | ||
val, found := k.GetValidatorByUnbondingId(ctx, id) | ||
|
||
if !found { | ||
// validator can never be deleted before unbonding | ||
// even if it is slashed to 0, so we always expect | ||
// to find it. | ||
return false, nil | ||
} | ||
|
||
if !val.IsMature(ctx.BlockTime(), ctx.BlockHeight()) { | ||
if val.UnbondingId != id { | ||
// validator already rebonded | ||
return true, nil | ||
} | ||
|
||
if val.UnbondingTime.After(ctx.BlockTime()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with removing the |
||
// validator cannot have already been dequeued by EndBlock | ||
val.UnbondingOnHold = false | ||
k.SetValidator(ctx, val) | ||
} else { | ||
// If unbonding is mature complete it | ||
val = k.UnbondingToUnbonded(ctx, val) | ||
// Validator is mature. Unbond it. | ||
if val.GetDelegatorShares().IsZero() { | ||
k.RemoveValidator(ctx, val.GetOperator()) | ||
} | ||
|
||
val = k.UnbondingToUnbonded(ctx, val) | ||
k.DeleteUnbondingIndex(ctx, id) | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ import ( | |
"bytes" | ||
"fmt" | ||
"sort" | ||
"time" | ||
|
||
gogotypes "github.com/gogo/protobuf/types" | ||
abci "github.com/tendermint/tendermint/abci/types" | ||
|
@@ -273,7 +274,7 @@ func (k Keeper) unbondedToBonded(ctx sdk.Context, validator types.Validator) (ty | |
// UnbondingToUnbonded switches a validator from unbonding state to unbonded state | ||
func (k Keeper) UnbondingToUnbonded(ctx sdk.Context, validator types.Validator) types.Validator { | ||
if !validator.IsUnbonding() { | ||
panic(fmt.Sprintf("bad state transition unbondingToBonded, validator: %v\n", validator)) | ||
panic(fmt.Sprintf("bad state transition unbondingToUnbonded, validator: %v\n", validator)) | ||
} | ||
|
||
return k.CompleteUnbondingValidator(ctx, validator) | ||
|
@@ -306,15 +307,19 @@ func (k Keeper) bondValidator(ctx sdk.Context, validator types.Validator) (types | |
// delete the validator by power index, as the key will change | ||
k.DeleteValidatorByPowerIndex(ctx, validator) | ||
|
||
// delete from queue if present | ||
k.DeleteValidatorQueue(ctx, validator, validator.UnbondingTime, validator.UnbondingHeight) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why pass |
||
|
||
validator = validator.UpdateStatus(types.Bonded) | ||
validator.UnbondingHeight = 0 | ||
validator.UnbondingTime = time.Time{} | ||
Comment on lines
+314
to
+315
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are these needed now and they were not needed before? |
||
validator.UnbondingOnHold = false | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be moved to |
||
validator.UnbondingId = 0 | ||
|
||
// save the now bonded validator record to the two referenced stores | ||
k.SetValidator(ctx, validator) | ||
k.SetValidatorByPowerIndex(ctx, validator) | ||
|
||
// delete from queue if present | ||
k.DeleteValidatorQueue(ctx, validator) | ||
|
||
// trigger hook | ||
consAddr, err := validator.GetConsAddr() | ||
if err != nil { | ||
|
@@ -337,27 +342,29 @@ func (k Keeper) BeginUnbondingValidator(ctx sdk.Context, validator types.Validat | |
panic(fmt.Sprintf("should not already be unbonded or unbonding, validator: %v\n", validator)) | ||
} | ||
|
||
consAddr, err := validator.GetConsAddr() | ||
if err != nil { | ||
return validator, err | ||
} | ||
|
||
validator = validator.UpdateStatus(types.Unbonding) | ||
|
||
// set the unbonding completion time and completion height appropriately | ||
validator.UnbondingTime = ctx.BlockHeader().Time.Add(params.UnbondingTime) | ||
validator.UnbondingHeight = ctx.BlockHeader().Height | ||
|
||
id := k.IncrementUnbondingId(ctx) | ||
validator.UnbondingId = id | ||
|
||
// save the now unbonded validator record and power index | ||
k.SetValidator(ctx, validator) | ||
k.SetValidatorByPowerIndex(ctx, validator) | ||
|
||
// Adds to unbonding validator queue | ||
k.InsertUnbondingValidatorQueue(ctx, validator) | ||
|
||
// trigger hook | ||
consAddr, err := validator.GetConsAddr() | ||
if err != nil { | ||
return validator, err | ||
} | ||
Comment on lines
-353
to
-357
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why move this block? |
||
k.AfterValidatorBeginUnbonding(ctx, consAddr, validator.GetOperator()) | ||
|
||
id := k.IncrementUnbondingId(ctx) | ||
k.SetValidatorByUnbondingIndex(ctx, validator, id) | ||
|
||
k.AfterUnbondingInitiated(ctx, id) | ||
|
@@ -368,6 +375,10 @@ func (k Keeper) BeginUnbondingValidator(ctx sdk.Context, validator types.Validat | |
// perform all the store operations for when a validator status becomes unbonded | ||
func (k Keeper) CompleteUnbondingValidator(ctx sdk.Context, validator types.Validator) types.Validator { | ||
validator = validator.UpdateStatus(types.Unbonded) | ||
validator.UnbondingHeight = 0 | ||
validator.UnbondingTime = time.Time{} | ||
validator.UnbondingOnHold = false | ||
validator.UnbondingId = 0 | ||
Comment on lines
+378
to
+381
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are these needed? For example, a validator unbonding cannot complete without |
||
k.SetValidator(ctx, validator) | ||
|
||
return validator | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -367,8 +367,9 @@ func (k Keeper) DeleteValidatorQueueTimeSlice(ctx sdk.Context, endTime time.Time | |
|
||
// DeleteValidatorQueue removes a validator by address from the unbonding queue | ||
// indexed by a given height and time. | ||
func (k Keeper) DeleteValidatorQueue(ctx sdk.Context, val types.Validator) { | ||
addrs := k.GetUnbondingValidators(ctx, val.UnbondingTime, val.UnbondingHeight) | ||
func (k Keeper) DeleteValidatorQueue(ctx sdk.Context, val types.Validator, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to change the signature of this function. |
||
unbondingTime time.Time, unbondingHeight int64) { | ||
addrs := k.GetUnbondingValidators(ctx, unbondingTime, unbondingHeight) | ||
newAddrs := []string{} | ||
|
||
for _, addr := range addrs { | ||
|
@@ -378,14 +379,14 @@ func (k Keeper) DeleteValidatorQueue(ctx sdk.Context, val types.Validator) { | |
} | ||
|
||
if len(newAddrs) == 0 { | ||
k.DeleteValidatorQueueTimeSlice(ctx, val.UnbondingTime, val.UnbondingHeight) | ||
k.DeleteValidatorQueueTimeSlice(ctx, unbondingTime, unbondingHeight) | ||
} else { | ||
k.SetUnbondingValidatorsQueue(ctx, val.UnbondingTime, val.UnbondingHeight, newAddrs) | ||
k.SetUnbondingValidatorsQueue(ctx, unbondingTime, unbondingHeight, newAddrs) | ||
} | ||
} | ||
|
||
// ValidatorQueueIterator returns an interator ranging over validators that are | ||
// unbonding whose unbonding completion occurs at the given height and time. | ||
// unbonding whose unbonding completion occurs not before a given time. | ||
func (k Keeper) ValidatorQueueIterator(ctx sdk.Context, endTime time.Time, endHeight int64) sdk.Iterator { | ||
store := ctx.KVStore(k.storeKey) | ||
return store.Iterator(types.ValidatorQueueKey, sdk.InclusiveEndBytes(types.GetValidatorQueueKey(endTime, endHeight))) | ||
|
@@ -409,15 +410,15 @@ func (k Keeper) UnbondAllMatureValidators(ctx sdk.Context) { | |
|
||
for ; unbondingValIterator.Valid(); unbondingValIterator.Next() { | ||
key := unbondingValIterator.Key() | ||
keyTime, keyHeight, err := types.ParseValidatorQueueKey(key) | ||
keyTime, _, err := types.ParseValidatorQueueKey(key) | ||
if err != nil { | ||
panic(fmt.Errorf("failed to parse unbonding key: %w", err)) | ||
} | ||
|
||
// All addresses for the given key have the same unbonding height and time. | ||
// We only unbond if the height and time are less than the current height | ||
// and time. | ||
if keyHeight <= blockHeight && (keyTime.Before(blockTime) || keyTime.Equal(blockTime)) { | ||
if keyTime.Before(blockTime) || keyTime.Equal(blockTime) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removing |
||
addrs := types.ValAddresses{} | ||
k.cdc.MustUnmarshal(unbondingValIterator.Value(), &addrs) | ||
|
||
|
@@ -432,6 +433,8 @@ func (k Keeper) UnbondAllMatureValidators(ctx sdk.Context) { | |
} | ||
|
||
if !val.IsUnbonding() { | ||
fmt.Println("status is ", val.Status) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove. |
||
|
||
panic("unexpected validator in unbonding queue; status was not unbonding") | ||
} | ||
if !val.UnbondingOnHold { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the solution described here would be cleaner without adding an extra field to the
Validator
struct, i.e.,validatorUnbondingCanComplete
just setsval.UnbondingOnHold
tofalse
and let the logic inUnbondAllMatureValidators
move validators toUNBONDED
. @jtremback what do you think?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually it is not sufficient, since a validator that goes through unbond-rebond-unbond, can have its
val.UnbondingOnHold
set totrue
by the CCV module (on the second unbond) and then have this flag reset tofalse
by a call tovalidatorUnbondingCanComplete
for the first unbond.A solution could be for
val.UnbondingOnHold
to be a counter instead of a bool, i.e.,PutUnbondingOnHold
increments it;UnbondingCanComplete
decrements it;UNBONDED
only ifval.UnbondingOnHold == 0
Like this we allow for these functions to be used by multiple modules (in case it's needed in the future).