-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
fix: [Interchain Security] validatorUnbondingCanComplete
must take into account (re)bonded validators
#12796
Changes from 5 commits
b352143
ac12d67
04dc36b
af6ee93
db20718
c33200b
4f7e70e
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 |
---|---|---|
|
@@ -4,6 +4,7 @@ import ( | |
"encoding/binary" | ||
|
||
sdk "github.com/cosmos/cosmos-sdk/types" | ||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" | ||
"github.com/cosmos/cosmos-sdk/x/staking/types" | ||
) | ||
|
||
|
@@ -205,8 +206,10 @@ func redelegationEntryArrayIndex(red types.Redelegation, id uint64) (index int, | |
return 0, false | ||
} | ||
|
||
// UnbondingCanComplete allows a stopped unbonding operation such as an | ||
// unbonding delegation, a redelegation, or a validator unbonding to complete | ||
// UnbondingCanComplete allows a stopped unbonding operation, such as an | ||
// unbonding delegation, a redelegation, or a validator unbonding to complete. | ||
// In order for the unbonding operation with `id` to eventually complete, every call | ||
// to PutUnbondingOnHold(id) must be matched by a call to UnbondingCanComplete(id). | ||
// ---------------------------------------------------------------------------------------- | ||
|
||
func (k Keeper) UnbondingCanComplete(ctx sdk.Context, id uint64) error { | ||
|
@@ -250,11 +253,18 @@ func (k Keeper) unbondingDelegationEntryCanComplete(ctx sdk.Context, id uint64) | |
return false, nil | ||
} | ||
|
||
if ubd.Entries[i].UnbondingOnHoldRefCount <= 0 { | ||
return true, | ||
sdkerrors.Wrapf( | ||
types.ErrUnbondingOnHoldRefCountNegative, | ||
"undelegation unbondingId(%d), expecting UnbondingOnHoldRefCount > 0, got %T", | ||
id, ubd.Entries[i].UnbondingOnHoldRefCount, | ||
) | ||
} | ||
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. Isn't this really a panic situation? 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 think it's better to return an error that will be handled at an upper layer, e.g., in the provider CCV module where we actually panic (see https://github.com/cosmos/interchain-security/blob/3c647c2db5a733737b27ccf9da02bb4e5db18114/x/ccv/provider/keeper/relay.go#L82). |
||
ubd.Entries[i].UnbondingOnHoldRefCount-- | ||
|
||
// Check if entry is matured. | ||
if !ubd.Entries[i].IsMature(ctx.BlockHeader().Time) { | ||
// If not matured, set onHold to false | ||
ubd.Entries[i].UnbondingOnHold = false | ||
} else { | ||
if !ubd.Entries[i].OnHold() && ubd.Entries[i].IsMature(ctx.BlockHeader().Time) { | ||
// If matured, complete it. | ||
delegatorAddress, err := sdk.AccAddressFromBech32(ubd.DelegatorAddress) | ||
if err != nil { | ||
|
@@ -301,10 +311,17 @@ func (k Keeper) redelegationEntryCanComplete(ctx sdk.Context, id uint64) (found | |
return false, nil | ||
} | ||
|
||
if !red.Entries[i].IsMature(ctx.BlockHeader().Time) { | ||
// If not matured, set onHold to false | ||
red.Entries[i].UnbondingOnHold = false | ||
} else { | ||
if red.Entries[i].UnbondingOnHoldRefCount <= 0 { | ||
return true, | ||
sdkerrors.Wrapf( | ||
types.ErrUnbondingOnHoldRefCountNegative, | ||
"redelegation unbondingId(%d), expecting UnbondingOnHoldRefCount > 0, got %T", | ||
id, red.Entries[i].UnbondingOnHoldRefCount, | ||
) | ||
} | ||
red.Entries[i].UnbondingOnHoldRefCount-- | ||
|
||
if !red.Entries[i].OnHold() && red.Entries[i].IsMature(ctx.BlockHeader().Time) { | ||
// If matured, complete it. | ||
// Remove entry | ||
red.RemoveEntry(int64(i)) | ||
|
@@ -329,24 +346,24 @@ func (k Keeper) validatorUnbondingCanComplete(ctx sdk.Context, id uint64) (found | |
return false, nil | ||
} | ||
|
||
if !val.IsMature(ctx.BlockTime(), ctx.BlockHeight()) { | ||
val.UnbondingOnHold = false | ||
k.SetValidator(ctx, val) | ||
} else { | ||
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 did this get removed? Doesn't it remove functionality from this function? Where does
happen now? 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 see it is also happening in 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. Seems like the logic might now be broken if unbonding completes on the provider chain first, but I think the unit tests test for that so I'm confused. 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. Huh, looks like they don't. I don't remember why not. 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. @jtremback 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. Notice that for UBDEs and REDEs, we cannot remove the code from |
||
// If unbonding is mature complete it | ||
val = k.UnbondingToUnbonded(ctx, val) | ||
if val.GetDelegatorShares().IsZero() { | ||
k.RemoveValidator(ctx, val.GetOperator()) | ||
} | ||
|
||
k.DeleteUnbondingIndex(ctx, id) | ||
if val.UnbondingOnHoldRefCount <= 0 { | ||
return true, | ||
sdkerrors.Wrapf( | ||
types.ErrUnbondingOnHoldRefCountNegative, | ||
"val(%s), expecting UnbondingOnHoldRefCount > 0, got %T", | ||
val.OperatorAddress, val.UnbondingOnHoldRefCount, | ||
) | ||
} | ||
val.UnbondingOnHoldRefCount-- | ||
k.SetValidator(ctx, val) | ||
|
||
return true, nil | ||
} | ||
|
||
// PutUnbondingOnHold allows an external module to stop an unbonding operation such as an | ||
// unbonding delegation, a redelegation, or a validator unbonding | ||
// PutUnbondingOnHold allows an external module to stop an unbonding operation, | ||
// such as an unbonding delegation, a redelegation, or a validator unbonding. | ||
// In order for the unbonding operation with `id` to eventually complete, every call | ||
// to PutUnbondingOnHold(id) must be matched by a call to UnbondingCanComplete(id). | ||
// ---------------------------------------------------------------------------------------- | ||
func (k Keeper) PutUnbondingOnHold(ctx sdk.Context, id uint64) error { | ||
found := k.putUnbondingDelegationEntryOnHold(ctx, id) | ||
|
@@ -379,8 +396,7 @@ func (k Keeper) putUnbondingDelegationEntryOnHold(ctx sdk.Context, id uint64) (f | |
return false | ||
} | ||
|
||
ubd.Entries[i].UnbondingOnHold = true | ||
|
||
ubd.Entries[i].UnbondingOnHoldRefCount++ | ||
k.SetUnbondingDelegation(ctx, ubd) | ||
|
||
return true | ||
|
@@ -397,8 +413,7 @@ func (k Keeper) putRedelegationEntryOnHold(ctx sdk.Context, id uint64) (found bo | |
return false | ||
} | ||
|
||
red.Entries[i].UnbondingOnHold = true | ||
|
||
red.Entries[i].UnbondingOnHoldRefCount++ | ||
k.SetRedelegation(ctx, red) | ||
|
||
return true | ||
|
@@ -410,8 +425,7 @@ func (k Keeper) putValidatorOnHold(ctx sdk.Context, id uint64) (found bool) { | |
return false | ||
} | ||
|
||
val.UnbondingOnHold = true | ||
|
||
val.UnbondingOnHoldRefCount++ | ||
k.SetValidator(ctx, val) | ||
|
||
return true | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -138,9 +138,9 @@ func doRedelegation( | |
} | ||
|
||
func doValidatorUnbonding( | ||
t *testing.T, app *simapp.SimApp, ctx sdk.Context, addrDels []sdk.AccAddress, addrVals []sdk.ValAddress, hookCalled *bool, | ||
t *testing.T, app *simapp.SimApp, ctx sdk.Context, addrVal sdk.ValAddress, hookCalled *bool, | ||
) (validator types.Validator) { | ||
validator, found := app.StakingKeeper.GetValidator(ctx, addrVals[0]) | ||
validator, found := app.StakingKeeper.GetValidator(ctx, addrVal) | ||
require.True(t, found) | ||
// Check that status is bonded | ||
require.Equal(t, types.BondStatus(3), validator.Status) | ||
|
@@ -157,62 +157,80 @@ func doValidatorUnbonding( | |
return validator | ||
} | ||
|
||
func TestValidatorUnbondingOnHold1(t *testing.T) { | ||
func TestValidatorUnbondingOnHold(t *testing.T) { | ||
var hookCalled bool | ||
var ubdeID uint64 | ||
app, ctx, _, addrDels, addrVals := setup(t, &hookCalled, &ubdeID) | ||
validator := doValidatorUnbonding(t, app, ctx, addrDels, addrVals, &hookCalled) | ||
var ubdeIDs []uint64 | ||
app, ctx, _, _, addrVals := setup(t, &hookCalled, &ubdeID) | ||
|
||
completionTime := validator.UnbondingTime | ||
// Start unbonding first validator | ||
validator1 := doValidatorUnbonding(t, app, ctx, addrVals[0], &hookCalled) | ||
ubdeIDs = append(ubdeIDs, ubdeID) | ||
|
||
// CONSUMER CHAIN'S UNBONDING PERIOD ENDS - BUT UNBONDING CANNOT COMPLETE | ||
err := app.StakingKeeper.UnbondingCanComplete(ctx, ubdeID) | ||
require.NoError(t, err) | ||
// Reset hookCalled flag | ||
hookCalled = false | ||
|
||
// Check that unbonding is not complete | ||
validator, found := app.StakingKeeper.GetValidator(ctx, addrVals[0]) | ||
require.True(t, found) | ||
// Check that status is unbonding | ||
require.Equal(t, types.BondStatus(2), validator.Status) | ||
// Start unbonding second validator | ||
validator2 := doValidatorUnbonding(t, app, ctx, addrVals[1], &hookCalled) | ||
ubdeIDs = append(ubdeIDs, ubdeID) | ||
|
||
// PROVIDER CHAIN'S UNBONDING PERIOD ENDS - STOPPED UNBONDING CAN NOW COMPLETE | ||
ctx = ctx.WithBlockTime(completionTime) | ||
app.StakingKeeper.UnbondAllMatureValidators(ctx) | ||
// Check that there are two unbonding operations | ||
require.Equal(t, 2, len(ubdeIDs)) | ||
|
||
validator, found = app.StakingKeeper.GetValidator(ctx, addrVals[0]) | ||
require.True(t, found) | ||
// Check that status is unbonded | ||
require.Equal(t, types.BondStatus(1), validator.Status) | ||
} | ||
|
||
func TestValidatorUnbondingOnHold2(t *testing.T) { | ||
var hookCalled bool | ||
var ubdeID uint64 | ||
app, ctx, _, addrDels, addrVals := setup(t, &hookCalled, &ubdeID) | ||
validator := doValidatorUnbonding(t, app, ctx, addrDels, addrVals, &hookCalled) | ||
// Check that both validators have same unbonding time | ||
require.Equal(t, validator1.UnbondingTime, validator2.UnbondingTime) | ||
|
||
completionTime := validator.UnbondingTime | ||
completionHeight := validator.UnbondingHeight | ||
completionTime := validator1.UnbondingTime | ||
completionHeight := validator1.UnbondingHeight | ||
|
||
// PROVIDER CHAIN'S UNBONDING PERIOD ENDS - BUT UNBONDING CANNOT COMPLETE | ||
ctx = ctx.WithBlockTime(completionTime.Add(time.Duration(1))) | ||
ctx = ctx.WithBlockHeight(completionHeight + 1) | ||
app.StakingKeeper.UnbondAllMatureValidators(ctx) | ||
|
||
// Check that unbonding is not complete | ||
validator, found := app.StakingKeeper.GetValidator(ctx, addrVals[0]) | ||
// Check that unbonding is not complete for both validators | ||
validator1, found := app.StakingKeeper.GetValidator(ctx, addrVals[0]) | ||
require.True(t, found) | ||
// Check that status is unbonding | ||
require.Equal(t, types.BondStatus(2), validator.Status) | ||
require.Equal(t, types.Unbonding, validator1.Status) | ||
validator2, found = app.StakingKeeper.GetValidator(ctx, addrVals[1]) | ||
require.True(t, found) | ||
require.Equal(t, types.Unbonding, validator2.Status) | ||
unbondingVals := app.StakingKeeper.GetUnbondingValidators(ctx, completionTime, completionHeight) | ||
require.Equal(t, 2, len(unbondingVals)) | ||
require.Equal(t, validator1.OperatorAddress, unbondingVals[0]) | ||
require.Equal(t, validator2.OperatorAddress, unbondingVals[1]) | ||
|
||
// CONSUMER CHAIN'S UNBONDING PERIOD ENDS - STOPPED UNBONDING CAN NOW COMPLETE | ||
err := app.StakingKeeper.UnbondingCanComplete(ctx, ubdeID) | ||
err := app.StakingKeeper.UnbondingCanComplete(ctx, ubdeIDs[0]) | ||
require.NoError(t, err) | ||
|
||
validator, found = app.StakingKeeper.GetValidator(ctx, addrVals[0]) | ||
// Try again to unbond validators | ||
app.StakingKeeper.UnbondAllMatureValidators(ctx) | ||
|
||
// Check that unbonding is complete for validator1, but not for validator2 | ||
validator1, found = app.StakingKeeper.GetValidator(ctx, addrVals[0]) | ||
require.True(t, found) | ||
require.Equal(t, types.Unbonded, validator1.Status) | ||
validator2, found = app.StakingKeeper.GetValidator(ctx, addrVals[1]) | ||
require.True(t, found) | ||
require.Equal(t, types.Unbonding, validator2.Status) | ||
unbondingVals = app.StakingKeeper.GetUnbondingValidators(ctx, completionTime, completionHeight) | ||
require.Equal(t, 1, len(unbondingVals)) | ||
require.Equal(t, validator2.OperatorAddress, unbondingVals[0]) | ||
|
||
// Unbonding for validator2 can complete | ||
err = app.StakingKeeper.UnbondingCanComplete(ctx, ubdeIDs[1]) | ||
require.NoError(t, err) | ||
|
||
// Try again to unbond validators | ||
app.StakingKeeper.UnbondAllMatureValidators(ctx) | ||
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. What triggers this to be called IRL? 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. The next block. At any given time, |
||
|
||
// Check that unbonding is complete for validator2 | ||
validator2, found = app.StakingKeeper.GetValidator(ctx, addrVals[1]) | ||
require.True(t, found) | ||
// Check that status is unbonded | ||
require.Equal(t, types.BondStatus(1), validator.Status) | ||
require.Equal(t, types.Unbonded, validator2.Status) | ||
unbondingVals = app.StakingKeeper.GetUnbondingValidators(ctx, completionTime, completionHeight) | ||
require.Equal(t, 0, len(unbondingVals)) | ||
} | ||
|
||
func TestRedelegationOnHold1(t *testing.T) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -394,8 +394,6 @@ func (k Keeper) ValidatorQueueIterator(ctx sdk.Context, endTime time.Time, endHe | |
// UnbondAllMatureValidators unbonds all the mature unbonding validators that | ||
// have finished their unbonding period. | ||
func (k Keeper) UnbondAllMatureValidators(ctx sdk.Context) { | ||
store := ctx.KVStore(k.storeKey) | ||
|
||
blockTime := ctx.BlockTime() | ||
blockHeight := ctx.BlockHeight() | ||
|
||
|
@@ -434,15 +432,15 @@ func (k Keeper) UnbondAllMatureValidators(ctx sdk.Context) { | |
if !val.IsUnbonding() { | ||
panic("unexpected validator in unbonding queue; status was not unbonding") | ||
} | ||
if !val.UnbondingOnHold { | ||
if val.UnbondingOnHoldRefCount == 0 { | ||
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. Isn't .OnHold() doing this elsewhere? 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.
|
||
val = k.UnbondingToUnbonded(ctx, val) | ||
if val.GetDelegatorShares().IsZero() { | ||
k.RemoveValidator(ctx, val.GetOperator()) | ||
} | ||
// remove validator from queue | ||
k.DeleteValidatorQueue(ctx, val) | ||
} | ||
} | ||
|
||
store.Delete(key) | ||
} | ||
} | ||
} | ||
|
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.
Minor nitpick- why not call it
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.
My idea was to keep it consistent with
IsMature
(it is notUnbondingIsMature
). The Unbonding Delegation Entry is matured or on hold. But I don't have a strong position about it. @jtremback Let me know if I should change toUnbondingOnHold()
.