Skip to content

Commit

Permalink
fix: make ModuleAccountInvariants pass for IS SDK changes (#12554)
Browse files Browse the repository at this point in the history
* fix bug breaking ModuleAccountInvariants

* set UnbondingOnHold to false explicitly

* Fixes staking hooks safety issues (#12578)

Co-authored-by: Daniel T <30197399+danwt@users.noreply.github.com>
  • Loading branch information
mpoke and danwt authored Jul 19, 2022
1 parent c783aea commit 67c8163
Show file tree
Hide file tree
Showing 14 changed files with 864 additions and 810 deletions.
3 changes: 2 additions & 1 deletion docs/core/proto-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -6613,7 +6613,8 @@ multiplied by exchange rate.
| `unbonding_time` | [google.protobuf.Timestamp](#google.protobuf.Timestamp) | | unbonding_time defines, if unbonding, the min time for the validator to complete unbonding. |
| `commission` | [Commission](#cosmos.staking.v1beta1.Commission) | | commission defines the commission parameters. |
| `min_self_delegation` | [string](#string) | | min_self_delegation is the validator's self declared minimum self delegation. |
| `unbonding_on_hold` | [bool](#bool) | | True if this validator's unbonding has been stopped by an external module |
| `unbonding_on_hold` | [bool](#bool) | | false iff unbonding is allowed to complete in staking EndBlock |
| `unbonding_id` | [uint64](#uint64) | | unique id, used to distinguish unbond->rebond->unbond executions |



Expand Down
5 changes: 3 additions & 2 deletions proto/cosmos/staking/v1beta1/staking.proto
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,10 @@ message Validator {
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];

// True if this validator's unbonding has been stopped by an external module
// false iff unbonding is allowed to complete in staking EndBlock
bool unbonding_on_hold = 12;
// unique id, used to distinguish unbond->rebond->unbond executions
uint64 unbonding_id = 13;
}

// BondStatus is the status of a validator.
Expand Down
14 changes: 7 additions & 7 deletions x/staking/keeper/delegation.go
Original file line number Diff line number Diff line change
Expand Up @@ -432,10 +432,10 @@ func (k Keeper) SetRedelegationEntry(ctx sdk.Context,
red, found := k.GetRedelegation(ctx, delegatorAddr, validatorSrcAddr, validatorDstAddr)
id := k.IncrementUnbondingId(ctx)
if found {
red.AddEntry(creationHeight, minTime, balance, sharesDst, false, id)
red.AddEntry(creationHeight, minTime, balance, sharesDst, id)
} else {
red = types.NewRedelegation(delegatorAddr, validatorSrcAddr,
validatorDstAddr, creationHeight, minTime, balance, sharesDst, false, id)
validatorDstAddr, creationHeight, minTime, balance, sharesDst, id)
}

k.SetRedelegation(ctx, red)
Expand Down Expand Up @@ -703,9 +703,9 @@ func (k Keeper) Unbond(
return amount, nil
}

// getBeginInfo returns the completion time and height of a redelegation, along
// with a boolean signaling if the redelegation is complete based on the source
// validator.
// getBeginInfo returns the completion time and creation height of a
// redelegation, along with a boolean signaling if the redelegation is
// complete based on the source validator.
func (k Keeper) getBeginInfo(
ctx sdk.Context, valSrcAddr sdk.ValAddress,
) (completionTime time.Time, height int64, completeNow bool) {
Expand Down Expand Up @@ -785,7 +785,7 @@ func (k Keeper) CompleteUnbonding(ctx sdk.Context, delAddr sdk.AccAddress, valAd

// loop through all the entries and try to complete unbonding mature entries
for i := 0; i < len(ubd.Entries); i++ {
entry := &ubd.Entries[i]
entry := ubd.Entries[i]
if entry.IsMature(ctxTime) && !entry.UnbondingOnHold {
// Proceed with unbonding
ubd.RemoveEntry(int64(i))
Expand Down Expand Up @@ -889,7 +889,7 @@ func (k Keeper) CompleteRedelegation(

// loop through all the entries and try to complete mature redelegation entries
for i := 0; i < len(red.Entries); i++ {
entry := &red.Entries[i]
entry := red.Entries[i]
if entry.IsMature(ctxTime) && !entry.UnbondingOnHold {
red.RemoveEntry(int64(i))
i--
Expand Down
4 changes: 2 additions & 2 deletions x/staking/keeper/delegation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ func TestGetRedelegationsFromSrcValidator(t *testing.T) {

rd := types.NewRedelegation(addrDels[0], addrVals[0], addrVals[1], 0,
time.Unix(0, 0), sdk.NewInt(5),
sdk.NewDec(5), false, 1)
sdk.NewDec(5), 1)

// set and retrieve a record
app.StakingKeeper.SetRedelegation(ctx, rd)
Expand All @@ -634,7 +634,7 @@ func TestRedelegation(t *testing.T) {

rd := types.NewRedelegation(addrDels[0], addrVals[0], addrVals[1], 0,
time.Unix(0, 0).UTC(), sdk.NewInt(5),
sdk.NewDec(5), false, 1)
sdk.NewDec(5), 1)

// test shouldn't have and redelegations
has := app.StakingKeeper.HasReceivingRedelegation(ctx, addrDels[0], addrVals[1])
Expand Down
1 change: 0 additions & 1 deletion x/staking/keeper/querier.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,6 @@ func RedelegationsToRedelegationResponses(
entry.SharesDst,
entry.InitialBalance,
val.TokensFromShares(entry.SharesDst).TruncateInt(),
entry.UnbondingOnHold,
entry.UnbondingId,
)
}
Expand Down
6 changes: 3 additions & 3 deletions x/staking/keeper/slash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func TestSlashRedelegation(t *testing.T) {
// set a redelegation with an expiration timestamp beyond which the
// redelegation shouldn't be slashed
rd := types.NewRedelegation(addrDels[0], addrVals[0], addrVals[1], 0,
time.Unix(5, 0), sdk.NewInt(10), sdk.NewDec(10), false, 1)
time.Unix(5, 0), sdk.NewInt(10), sdk.NewDec(10), 1)

app.StakingKeeper.SetRedelegation(ctx, rd)

Expand Down Expand Up @@ -390,7 +390,7 @@ func TestSlashWithRedelegation(t *testing.T) {
// set a redelegation
rdTokens := app.StakingKeeper.TokensFromConsensusPower(ctx, 6)
rd := types.NewRedelegation(addrDels[0], addrVals[0], addrVals[1], 11,
time.Unix(0, 0), rdTokens, rdTokens.ToDec(), false, 1)
time.Unix(0, 0), rdTokens, rdTokens.ToDec(), 1)
app.StakingKeeper.SetRedelegation(ctx, rd)

// set the associated delegation
Expand Down Expand Up @@ -544,7 +544,7 @@ func TestSlashBoth(t *testing.T) {
rdATokens := app.StakingKeeper.TokensFromConsensusPower(ctx, 6)
rdA := types.NewRedelegation(addrDels[0], addrVals[0], addrVals[1], 11,
time.Unix(0, 0), rdATokens,
rdATokens.ToDec(), false, 1)
rdATokens.ToDec(), 1)
app.StakingKeeper.SetRedelegation(ctx, rdA)

// set the associated delegation
Expand Down
19 changes: 15 additions & 4 deletions x/staking/keeper/unbonding.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
// 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)
}

Expand Down
31 changes: 21 additions & 10 deletions x/staking/keeper/val_state_change.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"sort"
"time"

gogotypes "github.com/gogo/protobuf/types"
abci "github.com/tendermint/tendermint/abci/types"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

validator = validator.UpdateStatus(types.Bonded)
validator.UnbondingHeight = 0
validator.UnbondingTime = time.Time{}
validator.UnbondingOnHold = false
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 {
Expand All @@ -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
}
k.AfterValidatorBeginUnbonding(ctx, consAddr, validator.GetOperator())

id := k.IncrementUnbondingId(ctx)
k.SetValidatorByUnbondingIndex(ctx, validator, id)

k.AfterUnbondingInitiated(ctx, id)
Expand All @@ -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
k.SetValidator(ctx, validator)

return validator
Expand Down
17 changes: 10 additions & 7 deletions x/staking/keeper/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
unbondingTime time.Time, unbondingHeight int64) {
addrs := k.GetUnbondingValidators(ctx, unbondingTime, unbondingHeight)
newAddrs := []string{}

for _, addr := range addrs {
Expand All @@ -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)))
Expand All @@ -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) {
addrs := types.ValAddresses{}
k.cdc.MustUnmarshal(unbondingValIterator.Value(), &addrs)

Expand All @@ -432,6 +433,8 @@ func (k Keeper) UnbondAllMatureValidators(ctx sdk.Context) {
}

if !val.IsUnbonding() {
fmt.Println("status is ", val.Status)

panic("unexpected validator in unbonding queue; status was not unbonding")
}
if !val.UnbondingOnHold {
Expand Down
2 changes: 1 addition & 1 deletion x/staking/simulation/decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func TestDecodeStore(t *testing.T) {
require.NoError(t, err)
del := types.NewDelegation(delAddr1, valAddr1, sdk.OneDec())
ubd := types.NewUnbondingDelegation(delAddr1, valAddr1, 15, bondTime, sdk.OneInt(), 1)
red := types.NewRedelegation(delAddr1, valAddr1, valAddr1, 12, bondTime, sdk.OneInt(), sdk.OneDec(), false, 0)
red := types.NewRedelegation(delAddr1, valAddr1, valAddr1, 12, bondTime, sdk.OneInt(), sdk.OneDec(), 0)

kvPairs := kv.Pairs{
Pairs: []kv.Pair{
Expand Down
27 changes: 14 additions & 13 deletions x/staking/types/delegation.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,12 @@ func (d Delegations) String() (out string) {

func NewUnbondingDelegationEntry(creationHeight int64, completionTime time.Time, balance sdk.Int, id uint64) UnbondingDelegationEntry {
return UnbondingDelegationEntry{
CreationHeight: creationHeight,
CompletionTime: completionTime,
InitialBalance: balance,
Balance: balance,
UnbondingId: id,
CreationHeight: creationHeight,
CompletionTime: completionTime,
InitialBalance: balance,
Balance: balance,
UnbondingId: id,
UnbondingOnHold: false,
}
}

Expand Down Expand Up @@ -209,14 +210,14 @@ func (ubds UnbondingDelegations) String() (out string) {
return strings.TrimSpace(out)
}

func NewRedelegationEntry(creationHeight int64, completionTime time.Time, balance sdk.Int, sharesDst sdk.Dec, onHold bool, id uint64) RedelegationEntry {
func NewRedelegationEntry(creationHeight int64, completionTime time.Time, balance sdk.Int, sharesDst sdk.Dec, id uint64) RedelegationEntry {
return RedelegationEntry{
CreationHeight: creationHeight,
CompletionTime: completionTime,
InitialBalance: balance,
SharesDst: sharesDst,
UnbondingOnHold: onHold,
UnbondingId: id,
UnbondingOnHold: false,
}
}

Expand All @@ -234,21 +235,21 @@ func (e RedelegationEntry) IsMature(currentTime time.Time) bool {
//nolint:interfacer
func NewRedelegation(
delegatorAddr sdk.AccAddress, validatorSrcAddr, validatorDstAddr sdk.ValAddress,
creationHeight int64, minTime time.Time, balance sdk.Int, sharesDst sdk.Dec, onHold bool, unbondingId uint64,
creationHeight int64, minTime time.Time, balance sdk.Int, sharesDst sdk.Dec, unbondingId uint64,
) Redelegation {
return Redelegation{
DelegatorAddress: delegatorAddr.String(),
ValidatorSrcAddress: validatorSrcAddr.String(),
ValidatorDstAddress: validatorDstAddr.String(),
Entries: []RedelegationEntry{
NewRedelegationEntry(creationHeight, minTime, balance, sharesDst, onHold, unbondingId),
NewRedelegationEntry(creationHeight, minTime, balance, sharesDst, unbondingId),
},
}
}

// AddEntry - append entry to the unbonding delegation
func (red *Redelegation) AddEntry(creationHeight int64, minTime time.Time, balance sdk.Int, sharesDst sdk.Dec, onHold bool, unbondingId uint64) {
entry := NewRedelegationEntry(creationHeight, minTime, balance, sharesDst, onHold, unbondingId)
func (red *Redelegation) AddEntry(creationHeight int64, minTime time.Time, balance sdk.Int, sharesDst sdk.Dec, unbondingId uint64) {
entry := NewRedelegationEntry(creationHeight, minTime, balance, sharesDst, unbondingId)
red.Entries = append(red.Entries, entry)
}

Expand Down Expand Up @@ -374,9 +375,9 @@ func NewRedelegationResponse(

// NewRedelegationEntryResponse creates a new RedelegationEntryResponse instance.
func NewRedelegationEntryResponse(
creationHeight int64, completionTime time.Time, sharesDst sdk.Dec, initialBalance, balance sdk.Int, onHold bool, id uint64) RedelegationEntryResponse {
creationHeight int64, completionTime time.Time, sharesDst sdk.Dec, initialBalance, balance sdk.Int, id uint64) RedelegationEntryResponse {
return RedelegationEntryResponse{
RedelegationEntry: NewRedelegationEntry(creationHeight, completionTime, initialBalance, sharesDst, onHold, id),
RedelegationEntry: NewRedelegationEntry(creationHeight, completionTime, initialBalance, sharesDst, id),
Balance: balance,
}
}
Expand Down
Loading

0 comments on commit 67c8163

Please sign in to comment.