From da5312472520d6b6e98fad079aed5b843377d1c7 Mon Sep 17 00:00:00 2001 From: marbar3778 Date: Thu, 1 Sep 2022 12:18:27 +0200 Subject: [PATCH] IS up port --- proto/cosmos/staking/v1beta1/staking.proto | 10 +- proto/cosmos/staking/v1beta1/tx.proto | 17 + x/distribution/keeper/hooks.go | 4 + x/slashing/keeper/hooks.go | 4 + x/staking/keeper/delegation.go | 27 +- x/staking/keeper/hooks.go | 8 + x/staking/keeper/keeper.go | 17 + x/staking/keeper/slash.go | 6 +- x/staking/keeper/unbonding.go | 419 +++++++++++++++++++++ x/staking/keeper/unbonding_test.go | 385 +++++++++++++++++++ x/staking/keeper/val_state_change.go | 11 + x/staking/keeper/validator.go | 28 +- x/staking/simulation/decoder_test.go | 4 +- x/staking/types/delegation.go | 75 +++- x/staking/types/delegation_test.go | 14 +- x/staking/types/errors.go | 2 + x/staking/types/expected_keepers.go | 3 +- x/staking/types/hooks.go | 9 + x/staking/types/keys.go | 13 +- x/staking/types/validator.go | 23 +- 20 files changed, 1020 insertions(+), 59 deletions(-) create mode 100644 x/staking/keeper/unbonding.go create mode 100644 x/staking/keeper/unbonding_test.go diff --git a/proto/cosmos/staking/v1beta1/staking.proto b/proto/cosmos/staking/v1beta1/staking.proto index 193ff3c0def4..f8ccd7939b4f 100644 --- a/proto/cosmos/staking/v1beta1/staking.proto +++ b/proto/cosmos/staking/v1beta1/staking.proto @@ -124,6 +124,12 @@ message Validator { (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int", (gogoproto.nullable) = false ]; + + // strictly positive if this validator's unbonding has been stopped by external modules + int64 unbonding_on_hold_ref_count = 12; + + // list of unbonding ids, each uniquely identifing an unbonding of this validator + repeated uint64 unbonding_ids = 13; } // BondStatus is the status of a validator. @@ -216,7 +222,7 @@ message UnbondingDelegation { // validator_address is the bech32-encoded address of the validator. string validator_address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // entries are the unbonding delegation entries. - repeated UnbondingDelegationEntry entries = 3 [(gogoproto.nullable) = false]; // unbonding delegation entries + repeated UnbondingDelegationEntry entries = 3 [(gogoproto.nullable) = false]; // unbonding delegation entries } // UnbondingDelegationEntry defines an unbonding object with relevant metadata. @@ -279,7 +285,7 @@ message Redelegation { // validator_dst_address is the validator redelegation destination operator address. string validator_dst_address = 3 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // entries are the redelegation entries. - repeated RedelegationEntry entries = 4 [(gogoproto.nullable) = false]; // redelegation entries + repeated RedelegationEntry entries = 4 [(gogoproto.nullable) = false]; // redelegation entries } // Params defines the parameters for the x/staking module. diff --git a/proto/cosmos/staking/v1beta1/tx.proto b/proto/cosmos/staking/v1beta1/tx.proto index dec72dfe87ff..0f1f7af305a2 100644 --- a/proto/cosmos/staking/v1beta1/tx.proto +++ b/proto/cosmos/staking/v1beta1/tx.proto @@ -8,6 +8,7 @@ import "gogoproto/gogo.proto"; import "cosmos_proto/cosmos.proto"; import "cosmos/base/v1beta1/coin.proto"; import "cosmos/staking/v1beta1/staking.proto"; +import "tendermint/abci/types.proto"; import "cosmos/msg/v1/msg.proto"; @@ -188,3 +189,19 @@ message MsgUpdateParams { // // Since: cosmos-sdk 0.47 message MsgUpdateParamsResponse {}; + +// InfractionType indicates the infraction type a validator commited. +enum InfractionType { + option (gogoproto.goproto_enum_prefix) = false; + + // UNSPECIFIED defines an empty infraction type. + INFRACTION_TYPE_UNSPECIFIED = 0 [(gogoproto.enumvalue_customname) = "InfractionEmpty"]; + // DOUBLE_SIGN defines a validator that double-signs a block. + INFRACTION_TYPE_DOUBLE_SIGN = 1 [(gogoproto.enumvalue_customname) = "DoubleSign"]; + // DOWNTIME defines a validator that missed signing too many blocks. + INFRACTION_TYPE_DOWNTIME = 2 [(gogoproto.enumvalue_customname) = "Downtime"]; +} + +message ValidatorUpdates { + repeated tendermint.abci.ValidatorUpdate updates = 1 [(gogoproto.nullable) = false]; +} diff --git a/x/distribution/keeper/hooks.go b/x/distribution/keeper/hooks.go index bfae8482da5e..54bcd22c3a3b 100644 --- a/x/distribution/keeper/hooks.go +++ b/x/distribution/keeper/hooks.go @@ -121,3 +121,7 @@ func (h Hooks) AfterValidatorBeginUnbonding(_ sdk.Context, _ sdk.ConsAddress, _ func (h Hooks) BeforeDelegationRemoved(_ sdk.Context, _ sdk.AccAddress, _ sdk.ValAddress) error { return nil } + +func (h Hooks) AfterUnbondingInitiated(_ sdk.Context, _ uint64) error { + return nil +} diff --git a/x/slashing/keeper/hooks.go b/x/slashing/keeper/hooks.go index a306f76c210d..2b8c9fa5c9fe 100644 --- a/x/slashing/keeper/hooks.go +++ b/x/slashing/keeper/hooks.go @@ -101,3 +101,7 @@ func (h Hooks) AfterDelegationModified(_ sdk.Context, _ sdk.AccAddress, _ sdk.Va func (h Hooks) BeforeValidatorSlashed(_ sdk.Context, _ sdk.ValAddress, _ sdk.Dec) error { return nil } + +func (h Hooks) AfterUnbondingInitiated(_ sdk.Context, _ uint64) error { + return nil +} diff --git a/x/staking/keeper/delegation.go b/x/staking/keeper/delegation.go index c63286e2769e..fcebe740d4e4 100644 --- a/x/staking/keeper/delegation.go +++ b/x/staking/keeper/delegation.go @@ -305,14 +305,21 @@ func (k Keeper) SetUnbondingDelegationEntry( creationHeight int64, minTime time.Time, balance math.Int, ) types.UnbondingDelegation { ubd, found := k.GetUnbondingDelegation(ctx, delegatorAddr, validatorAddr) + id := k.IncrementUnbondingId(ctx) if found { - ubd.AddEntry(creationHeight, minTime, balance) + ubd.AddEntry(creationHeight, minTime, balance, id) } else { - ubd = types.NewUnbondingDelegation(delegatorAddr, validatorAddr, creationHeight, minTime, balance) + ubd = types.NewUnbondingDelegation(delegatorAddr, validatorAddr, creationHeight, minTime, balance, id) } k.SetUnbondingDelegation(ctx, ubd) + // Add to the UBDByUnbondingOp index to look up the UBD by the UBDE ID + k.SetUnbondingDelegationByUnbondingId(ctx, ubd, id) + + // Call hook + k.AfterUnbondingInitiated(ctx, id) + return ubd } @@ -488,15 +495,22 @@ func (k Keeper) SetRedelegationEntry(ctx sdk.Context, sharesSrc, sharesDst sdk.Dec, ) types.Redelegation { red, found := k.GetRedelegation(ctx, delegatorAddr, validatorSrcAddr, validatorDstAddr) + id := k.IncrementUnbondingId(ctx) if found { - red.AddEntry(creationHeight, minTime, balance, sharesDst) + red.AddEntry(creationHeight, minTime, balance, sharesDst, id) } else { red = types.NewRedelegation(delegatorAddr, validatorSrcAddr, - validatorDstAddr, creationHeight, minTime, balance, sharesDst) + validatorDstAddr, creationHeight, minTime, balance, sharesDst, id) } k.SetRedelegation(ctx, red) + // Add to the UBDByEntry index to look up the UBD by the UBDE ID + k.SetRedelegationByUnbondingId(ctx, red, id) + + // Call hook + k.AfterUnbondingInitiated(ctx, id) + return red } @@ -846,7 +860,7 @@ func (k Keeper) CompleteUnbonding(ctx sdk.Context, delAddr sdk.AccAddress, valAd // loop through all the entries and complete unbonding mature entries for i := 0; i < len(ubd.Entries); i++ { entry := ubd.Entries[i] - if entry.IsMature(ctxTime) { + if entry.IsMature(ctxTime) && !entry.OnHold() { ubd.RemoveEntry(int64(i)) i-- @@ -950,9 +964,10 @@ func (k Keeper) CompleteRedelegation( // loop through all the entries and complete mature redelegation entries for i := 0; i < len(red.Entries); i++ { entry := red.Entries[i] - if entry.IsMature(ctxTime) { + if entry.IsMature(ctxTime) && !entry.OnHold() { red.RemoveEntry(int64(i)) i-- + k.DeleteUnbondingIndex(ctx, entry.UnbondingId) if !entry.InitialBalance.IsZero() { balances = balances.Add(sdk.NewCoin(bondDenom, entry.InitialBalance)) diff --git a/x/staking/keeper/hooks.go b/x/staking/keeper/hooks.go index 91375c9e3881..8afc09a2db5b 100644 --- a/x/staking/keeper/hooks.go +++ b/x/staking/keeper/hooks.go @@ -87,3 +87,11 @@ func (k Keeper) BeforeValidatorSlashed(ctx sdk.Context, valAddr sdk.ValAddress, } return nil } + +// This is called when an UnbondingDelegationEntry is first created +func (k Keeper) AfterUnbondingInitiated(ctx sdk.Context, id uint64) error { + if k.hooks != nil { + return k.hooks.AfterUnbondingInitiated(ctx, id) + } + return nil +} diff --git a/x/staking/keeper/keeper.go b/x/staking/keeper/keeper.go index 975ee64f9a1b..8a324ea20951 100644 --- a/x/staking/keeper/keeper.go +++ b/x/staking/keeper/keeper.go @@ -6,6 +6,7 @@ import ( "cosmossdk.io/math" storetypes "github.com/cosmos/cosmos-sdk/store/types" + abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/libs/log" "github.com/cosmos/cosmos-sdk/codec" @@ -101,3 +102,19 @@ func (k Keeper) SetLastTotalPower(ctx sdk.Context, power math.Int) { func (k Keeper) GetAuthority() string { return k.authority } + +// SetValidatorUpdates sets the ABCI validator power updates for the current block. +func (k Keeper) SetValidatorUpdates(ctx sdk.Context, valUpdates []abci.ValidatorUpdate) { + store := ctx.KVStore(k.storeKey) + bz := k.cdc.MustMarshal(&types.ValidatorUpdates{Updates: valUpdates}) + store.Set(types.ValidatorUpdatesKey, bz) +} + +// GetValidatorUpdates returns the ABCI validator power updates within the current block. +func (k Keeper) GetValidatorUpdates(ctx sdk.Context) []abci.ValidatorUpdate { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.ValidatorUpdatesKey) + var valUpdates types.ValidatorUpdates + k.cdc.MustUnmarshal(bz, &valUpdates) + return valUpdates.Updates +} diff --git a/x/staking/keeper/slash.go b/x/staking/keeper/slash.go index c0a147510963..cb6af2c3746f 100644 --- a/x/staking/keeper/slash.go +++ b/x/staking/keeper/slash.go @@ -30,7 +30,7 @@ import ( // // Infraction was committed at the current height or at a past height, // not at a height in the future -func (k Keeper) Slash(ctx sdk.Context, consAddr sdk.ConsAddress, infractionHeight int64, power int64, slashFactor sdk.Dec) math.Int { +func (k Keeper) Slash(ctx sdk.Context, consAddr sdk.ConsAddress, infractionHeight int64, power int64, slashFactor sdk.Dec, _ types.InfractionType) math.Int { logger := k.Logger(ctx) if slashFactor.IsNegative() { @@ -187,7 +187,7 @@ func (k Keeper) SlashUnbondingDelegation(ctx sdk.Context, unbondingDelegation ty continue } - if entry.IsMature(now) { + if entry.IsMature(now) && !entry.OnHold() { // Unbonding delegation no longer eligible for slashing, skip it continue } @@ -241,7 +241,7 @@ func (k Keeper) SlashRedelegation(ctx sdk.Context, srcValidator types.Validator, continue } - if entry.IsMature(now) { + if entry.IsMature(now) && !entry.OnHold() { // Redelegation no longer eligible for slashing, skip it continue } diff --git a/x/staking/keeper/unbonding.go b/x/staking/keeper/unbonding.go new file mode 100644 index 000000000000..099950e6030d --- /dev/null +++ b/x/staking/keeper/unbonding.go @@ -0,0 +1,419 @@ +package keeper + +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" +) + +// Increments and returns a unique ID for an unbonding operation +func (k Keeper) IncrementUnbondingId(ctx sdk.Context) (unbondingId uint64) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.UnbondingIdKey) + + if bz == nil { + unbondingId = 0 + } else { + unbondingId = binary.BigEndian.Uint64(bz) + } + + unbondingId = unbondingId + 1 + + // Convert back into bytes for storage + bz = make([]byte, 8) + binary.BigEndian.PutUint64(bz, unbondingId) + + store.Set(types.UnbondingIdKey, bz) + + return unbondingId +} + +// Remove a mapping from UnbondingId to unbonding operation +func (k Keeper) DeleteUnbondingIndex(ctx sdk.Context, id uint64) { + store := ctx.KVStore(k.storeKey) + store.Delete(types.GetUnbondingIndexKey(id)) +} + +// return a unbonding delegation that has an unbonding delegation entry with a certain ID +func (k Keeper) GetUnbondingDelegationByUnbondingId( + ctx sdk.Context, id uint64, +) (ubd types.UnbondingDelegation, found bool) { + store := ctx.KVStore(k.storeKey) + + ubdeKey := store.Get(types.GetUnbondingIndexKey(id)) + if ubdeKey == nil { + return types.UnbondingDelegation{}, false + } + + value := store.Get(ubdeKey) + if value == nil { + return types.UnbondingDelegation{}, false + } + + ubd, err := types.UnmarshalUBD(k.cdc, value) + // An error here means that what we got wasn't the right type + if err != nil { + return types.UnbondingDelegation{}, false + } + + return ubd, true +} + +// return a unbonding delegation that has an unbonding delegation entry with a certain ID +func (k Keeper) GetRedelegationByUnbondingId( + ctx sdk.Context, id uint64, +) (red types.Redelegation, found bool) { + store := ctx.KVStore(k.storeKey) + + redKey := store.Get(types.GetUnbondingIndexKey(id)) + if redKey == nil { + return types.Redelegation{}, false + } + + value := store.Get(redKey) + if value == nil { + return types.Redelegation{}, false + } + + red, err := types.UnmarshalRED(k.cdc, value) + // An error here means that what we got wasn't the right type + if err != nil { + return types.Redelegation{}, false + } + + return red, true +} + +// return the validator that is unbonding with a certain unbonding op ID +func (k Keeper) GetValidatorByUnbondingId( + ctx sdk.Context, id uint64, +) (val types.Validator, found bool) { + store := ctx.KVStore(k.storeKey) + + valKey := store.Get(types.GetUnbondingIndexKey(id)) + if valKey == nil { + return types.Validator{}, false + } + + value := store.Get(valKey) + if value == nil { + return types.Validator{}, false + } + + val, err := types.UnmarshalValidator(k.cdc, value) + // An error here means that what we got wasn't the right type + if err != nil { + return types.Validator{}, false + } + + return val, true +} + +// Set an index to look up an UnbondingDelegation by the unbondingId of an UnbondingDelegationEntry that it contains +func (k Keeper) SetUnbondingDelegationByUnbondingId(ctx sdk.Context, ubd types.UnbondingDelegation, id uint64) { + store := ctx.KVStore(k.storeKey) + + delAddr, err := sdk.AccAddressFromBech32(ubd.DelegatorAddress) + if err != nil { + panic(err) + } + + valAddr, err := sdk.ValAddressFromBech32(ubd.ValidatorAddress) + if err != nil { + panic(err) + } + + ubdKey := types.GetUBDKey(delAddr, valAddr) + store.Set(types.GetUnbondingIndexKey(id), ubdKey) +} + +// Set an index to look up an Redelegation by the unbondingId of an RedelegationEntry that it contains +func (k Keeper) SetRedelegationByUnbondingId(ctx sdk.Context, red types.Redelegation, id uint64) { + store := ctx.KVStore(k.storeKey) + + delAddr, err := sdk.AccAddressFromBech32(red.DelegatorAddress) + if err != nil { + panic(err) + } + + valSrcAddr, err := sdk.ValAddressFromBech32(red.ValidatorSrcAddress) + if err != nil { + panic(err) + } + + valDstAddr, err := sdk.ValAddressFromBech32(red.ValidatorDstAddress) + if err != nil { + panic(err) + } + + redKey := types.GetREDKey(delAddr, valSrcAddr, valDstAddr) + store.Set(types.GetUnbondingIndexKey(id), redKey) +} + +// Set an index to look up a Validator by the unbondingId corresponding to its current unbonding +func (k Keeper) SetValidatorByUnbondingId(ctx sdk.Context, val types.Validator, id uint64) { + store := ctx.KVStore(k.storeKey) + + valAddr, err := sdk.ValAddressFromBech32(val.OperatorAddress) + if err != nil { + panic(err) + } + + valKey := types.GetValidatorKey(valAddr) + store.Set(types.GetUnbondingIndexKey(id), valKey) +} + +// unbondingDelegationEntryArrayIndex and redelegationEntryArrayIndex are utilities to find +// at which position in the Entries array the entry with a given id is +// ---------------------------------------------------------------------------------------- + +func unbondingDelegationEntryArrayIndex(ubd types.UnbondingDelegation, id uint64) (index int, found bool) { + for i, entry := range ubd.Entries { + // we find the entry with the right ID + if entry.UnbondingId == id { + return i, true + } + } + + return 0, false +} + +func redelegationEntryArrayIndex(red types.Redelegation, id uint64) (index int, found bool) { + for i, entry := range red.Entries { + // we find the entry with the right ID + if entry.UnbondingId == id { + return i, true + } + } + + return 0, false +} + +// 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 { + found, err := k.unbondingDelegationEntryCanComplete(ctx, id) + if err != nil { + return err + } + if found { + return nil + } + + found, err = k.redelegationEntryCanComplete(ctx, id) + if err != nil { + return err + } + if found { + return nil + } + + found, err = k.validatorUnbondingCanComplete(ctx, id) + if err != nil { + return err + } + if found { + return nil + } + + // If an entry was not found + return types.ErrUnbondingNotFound +} + +func (k Keeper) unbondingDelegationEntryCanComplete(ctx sdk.Context, id uint64) (found bool, err error) { + ubd, found := k.GetUnbondingDelegationByUnbondingId(ctx, id) + if !found { + return false, nil + } + + i, found := unbondingDelegationEntryArrayIndex(ubd, id) + + if !found { + return false, nil + } + + // The entry must be on hold + if !ubd.Entries[i].OnHold() { + return true, + sdkerrors.Wrapf( + types.ErrUnbondingOnHoldRefCountNegative, + "undelegation unbondingId(%d), expecting UnbondingOnHoldRefCount > 0, got %T", + id, ubd.Entries[i].UnbondingOnHoldRefCount, + ) + } + ubd.Entries[i].UnbondingOnHoldRefCount-- + + // Check if entry is matured. + 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 { + return false, err + } + + bondDenom := k.GetParams(ctx).BondDenom + + // track undelegation only when remaining or truncated shares are non-zero + if !ubd.Entries[i].Balance.IsZero() { + amt := sdk.NewCoin(bondDenom, ubd.Entries[i].Balance) + if err := k.bankKeeper.UndelegateCoinsFromModuleToAccount( + ctx, types.NotBondedPoolName, delegatorAddress, sdk.NewCoins(amt), + ); err != nil { + return false, err + } + } + + // Remove entry + ubd.RemoveEntry(int64(i)) + // Remove from the UnbondingIndex + k.DeleteUnbondingIndex(ctx, id) + } + + // set the unbonding delegation or remove it if there are no more entries + if len(ubd.Entries) == 0 { + k.RemoveUnbondingDelegation(ctx, ubd) + } else { + k.SetUnbondingDelegation(ctx, ubd) + } + + // Successfully completed unbonding + return true, nil +} + +func (k Keeper) redelegationEntryCanComplete(ctx sdk.Context, id uint64) (found bool, err error) { + red, found := k.GetRedelegationByUnbondingId(ctx, id) + if !found { + return false, nil + } + + i, found := redelegationEntryArrayIndex(red, id) + if !found { + return false, nil + } + + // The entry must be on hold + if !red.Entries[i].OnHold() { + 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)) + // Remove from the Unbonding index + k.DeleteUnbondingIndex(ctx, id) + } + + // set the redelegation or remove it if there are no more entries + if len(red.Entries) == 0 { + k.RemoveRedelegation(ctx, red) + } else { + k.SetRedelegation(ctx, red) + } + + // Successfully completed unbonding + return true, nil +} + +func (k Keeper) validatorUnbondingCanComplete(ctx sdk.Context, id uint64) (found bool, err error) { + val, found := k.GetValidatorByUnbondingId(ctx, id) + if !found { + return false, nil + } + + 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. +// 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) + if found { + return nil + } + + found = k.putRedelegationEntryOnHold(ctx, id) + if found { + return nil + } + + found = k.putValidatorOnHold(ctx, id) + if found { + return nil + } + + // If an entry was not found + return types.ErrUnbondingNotFound +} + +func (k Keeper) putUnbondingDelegationEntryOnHold(ctx sdk.Context, id uint64) (found bool) { + ubd, found := k.GetUnbondingDelegationByUnbondingId(ctx, id) + if !found { + return false + } + + i, found := unbondingDelegationEntryArrayIndex(ubd, id) + if !found { + return false + } + + ubd.Entries[i].UnbondingOnHoldRefCount++ + k.SetUnbondingDelegation(ctx, ubd) + + return true +} + +func (k Keeper) putRedelegationEntryOnHold(ctx sdk.Context, id uint64) (found bool) { + red, found := k.GetRedelegationByUnbondingId(ctx, id) + if !found { + return false + } + + i, found := redelegationEntryArrayIndex(red, id) + if !found { + return false + } + + red.Entries[i].UnbondingOnHoldRefCount++ + k.SetRedelegation(ctx, red) + + return true +} + +func (k Keeper) putValidatorOnHold(ctx sdk.Context, id uint64) (found bool) { + val, found := k.GetValidatorByUnbondingId(ctx, id) + if !found { + return false + } + + val.UnbondingOnHoldRefCount++ + k.SetValidator(ctx, val) + + return true +} diff --git a/x/staking/keeper/unbonding_test.go b/x/staking/keeper/unbonding_test.go new file mode 100644 index 000000000000..1a06ea451837 --- /dev/null +++ b/x/staking/keeper/unbonding_test.go @@ -0,0 +1,385 @@ +package keeper_test + +import ( + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/staking/keeper" + "github.com/cosmos/cosmos-sdk/x/staking/teststaking" + "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/stretchr/testify/require" +) + +type MockStakingHooks struct { + types.StakingHooksTemplate + afterUnbondingInitiated func(uint64) +} + +func (h MockStakingHooks) AfterUnbondingInitiated(_ sdk.Context, id uint64) { + h.afterUnbondingInitiated(id) +} + +func setup(t *testing.T, hookCalled *bool, ubdeID *uint64) ( + app *simapp.SimApp, ctx sdk.Context, bondDenom string, addrDels []sdk.AccAddress, addrVals []sdk.ValAddress, +) { + _, app, ctx = createTestInput() + + stakingKeeper := keeper.NewKeeper( + app.AppCodec(), + app.GetKey(types.StoreKey), + app.AccountKeeper, + app.BankKeeper, + app.GetSubspace(types.ModuleName), + ) + + myHooks := MockStakingHooks{ + afterUnbondingInitiated: func(id uint64) { + *hookCalled = true + // save id + *ubdeID = id + // call back to stop unbonding + err := app.StakingKeeper.PutUnbondingOnHold(ctx, id) + require.NoError(t, err) + }, + } + + stakingKeeper.SetHooks( + types.NewMultiStakingHooks(myHooks), + ) + + app.StakingKeeper = stakingKeeper + + addrDels = simapp.AddTestAddrsIncremental(app, ctx, 2, sdk.NewInt(10000)) + addrVals = simapp.ConvertAddrsToValAddrs(addrDels) + + valTokens := app.StakingKeeper.TokensFromConsensusPower(ctx, 10) + startTokens := app.StakingKeeper.TokensFromConsensusPower(ctx, 20) + + bondDenom = app.StakingKeeper.BondDenom(ctx) + notBondedPool := app.StakingKeeper.GetNotBondedPool(ctx) + + require.NoError(t, simapp.FundModuleAccount(app.BankKeeper, ctx, notBondedPool.GetName(), sdk.NewCoins(sdk.NewCoin(bondDenom, startTokens)))) + app.AccountKeeper.SetModuleAccount(ctx, notBondedPool) + + // Create a validator + validator1 := teststaking.NewValidator(t, addrVals[0], PKs[0]) + validator1, issuedShares1 := validator1.AddTokensFromDel(valTokens) + require.Equal(t, valTokens, issuedShares1.RoundInt()) + + validator1 = keeper.TestingUpdateValidator(app.StakingKeeper, ctx, validator1, true) + require.True(sdk.IntEq(t, valTokens, validator1.BondedTokens())) + require.True(t, validator1.IsBonded()) + + // Create a delegator + delegation := types.NewDelegation(addrDels[0], addrVals[0], issuedShares1) + app.StakingKeeper.SetDelegation(ctx, delegation) + + // Create a validator to redelegate to + validator2 := teststaking.NewValidator(t, addrVals[1], PKs[1]) + validator2, issuedShares2 := validator2.AddTokensFromDel(valTokens) + require.Equal(t, valTokens, issuedShares2.RoundInt()) + + validator2 = keeper.TestingUpdateValidator(app.StakingKeeper, ctx, validator2, true) + require.Equal(t, types.Bonded, validator2.Status) + require.True(t, validator2.IsBonded()) + + return +} + +func doUnbondingDelegation( + t *testing.T, app *simapp.SimApp, ctx sdk.Context, bondDenom string, addrDels []sdk.AccAddress, addrVals []sdk.ValAddress, hookCalled *bool, +) (completionTime time.Time, bondedAmt sdk.Int, notBondedAmt sdk.Int) { + // UNDELEGATE + // Save original bonded and unbonded amounts + bondedAmt1 := app.BankKeeper.GetBalance(ctx, app.StakingKeeper.GetBondedPool(ctx).GetAddress(), bondDenom).Amount + notBondedAmt1 := app.BankKeeper.GetBalance(ctx, app.StakingKeeper.GetNotBondedPool(ctx).GetAddress(), bondDenom).Amount + + var err error + completionTime, err = app.StakingKeeper.Undelegate(ctx, addrDels[0], addrVals[0], sdk.NewDec(1)) + require.NoError(t, err) + + // check that the unbonding actually happened + bondedAmt2 := app.BankKeeper.GetBalance(ctx, app.StakingKeeper.GetBondedPool(ctx).GetAddress(), bondDenom).Amount + notBondedAmt2 := app.BankKeeper.GetBalance(ctx, app.StakingKeeper.GetNotBondedPool(ctx).GetAddress(), bondDenom).Amount + // Bonded amount is less + require.True(sdk.IntEq(t, bondedAmt1.SubRaw(1), bondedAmt2)) + // Unbonded amount is more + require.True(sdk.IntEq(t, notBondedAmt1.AddRaw(1), notBondedAmt2)) + + // Check that the unbonding happened- we look up the entry and see that it has the correct number of shares + unbondingDelegations := app.StakingKeeper.GetUnbondingDelegationsFromValidator(ctx, addrVals[0]) + require.Equal(t, sdk.NewInt(1), unbondingDelegations[0].Entries[0].Balance) + + // check that our hook was called + require.True(t, *hookCalled) + + return completionTime, bondedAmt2, notBondedAmt2 +} + +func doRedelegation( + t *testing.T, app *simapp.SimApp, ctx sdk.Context, addrDels []sdk.AccAddress, addrVals []sdk.ValAddress, hookCalled *bool, +) (completionTime time.Time) { + var err error + completionTime, err = app.StakingKeeper.BeginRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1], sdk.NewDec(1)) + require.NoError(t, err) + + // Check that the redelegation happened- we look up the entry and see that it has the correct number of shares + redelegations := app.StakingKeeper.GetRedelegationsFromSrcValidator(ctx, addrVals[0]) + require.Equal(t, 1, len(redelegations)) + require.Equal(t, sdk.NewDec(1), redelegations[0].Entries[0].SharesDst) + + // check that our hook was called + require.True(t, *hookCalled) + + return completionTime +} + +func doValidatorUnbonding( + t *testing.T, app *simapp.SimApp, ctx sdk.Context, addrVal sdk.ValAddress, hookCalled *bool, +) (validator types.Validator) { + validator, found := app.StakingKeeper.GetValidator(ctx, addrVal) + require.True(t, found) + // Check that status is bonded + require.Equal(t, types.BondStatus(3), validator.Status) + + validator, err := app.StakingKeeper.BeginUnbondingValidator(ctx, validator) + require.NoError(t, err) + + // Check that status is unbonding + require.Equal(t, types.BondStatus(2), validator.Status) + + // check that our hook was called + require.True(t, *hookCalled) + + return validator +} + +func TestValidatorUnbondingOnHold1(t *testing.T) { + var hookCalled bool + var ubdeID uint64 + + app, ctx, _, _, addrVals := setup(t, &hookCalled, &ubdeID) + + // Start unbonding first validator + validator := doValidatorUnbonding(t, app, ctx, addrVals[0], &hookCalled) + + completionTime := validator.UnbondingTime + completionHeight := validator.UnbondingHeight + + // CONSUMER CHAIN'S UNBONDING PERIOD ENDS - STOPPED UNBONDING CAN NOW COMPLETE + err := app.StakingKeeper.UnbondingCanComplete(ctx, ubdeID) + require.NoError(t, err) + + // Try to unbond validator + app.StakingKeeper.UnbondAllMatureValidators(ctx) + + // Check that validator unbonding is not complete (is not mature yet) + validator, found := app.StakingKeeper.GetValidator(ctx, addrVals[0]) + require.True(t, found) + require.Equal(t, types.Unbonding, validator.Status) + unbondingVals := app.StakingKeeper.GetUnbondingValidators(ctx, completionTime, completionHeight) + require.Equal(t, 1, len(unbondingVals)) + require.Equal(t, validator.OperatorAddress, unbondingVals[0]) + + // 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 validator unbonding is complete + validator, found = app.StakingKeeper.GetValidator(ctx, addrVals[0]) + require.True(t, found) + require.Equal(t, types.Unbonded, validator.Status) + unbondingVals = app.StakingKeeper.GetUnbondingValidators(ctx, completionTime, completionHeight) + require.Equal(t, 0, len(unbondingVals)) +} + +func TestValidatorUnbondingOnHold2(t *testing.T) { + var hookCalled bool + var ubdeID uint64 + var ubdeIDs []uint64 + app, ctx, _, _, addrVals := setup(t, &hookCalled, &ubdeID) + + // Start unbonding first validator + validator1 := doValidatorUnbonding(t, app, ctx, addrVals[0], &hookCalled) + ubdeIDs = append(ubdeIDs, ubdeID) + + // Reset hookCalled flag + hookCalled = false + + // Start unbonding second validator + validator2 := doValidatorUnbonding(t, app, ctx, addrVals[1], &hookCalled) + ubdeIDs = append(ubdeIDs, ubdeID) + + // Check that there are two unbonding operations + require.Equal(t, 2, len(ubdeIDs)) + + // Check that both validators have same unbonding time + require.Equal(t, validator1.UnbondingTime, validator2.UnbondingTime) + + 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 for both validators + validator1, found := app.StakingKeeper.GetValidator(ctx, addrVals[0]) + require.True(t, found) + 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, ubdeIDs[0]) + require.NoError(t, err) + + // 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) + + // Check that unbonding is complete for validator2 + validator2, found = app.StakingKeeper.GetValidator(ctx, addrVals[1]) + require.True(t, found) + 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) { + var hookCalled bool + var ubdeID uint64 + app, ctx, _, addrDels, addrVals := setup(t, &hookCalled, &ubdeID) + completionTime := doRedelegation(t, app, ctx, addrDels, addrVals, &hookCalled) + + // CONSUMER CHAIN'S UNBONDING PERIOD ENDS - BUT UNBONDING CANNOT COMPLETE + err := app.StakingKeeper.UnbondingCanComplete(ctx, ubdeID) + require.NoError(t, err) + + // Redelegation is not complete - still exists + redelegations := app.StakingKeeper.GetRedelegationsFromSrcValidator(ctx, addrVals[0]) + require.Equal(t, 1, len(redelegations)) + + // PROVIDER CHAIN'S UNBONDING PERIOD ENDS - STOPPED UNBONDING CAN NOW COMPLETE + ctx = ctx.WithBlockTime(completionTime) + _, err = app.StakingKeeper.CompleteRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1]) + require.NoError(t, err) + + // Redelegation is complete and record is gone + redelegations = app.StakingKeeper.GetRedelegationsFromSrcValidator(ctx, addrVals[0]) + require.Equal(t, 0, len(redelegations)) +} + +func TestRedelegationOnHold2(t *testing.T) { + var hookCalled bool + var ubdeID uint64 + app, ctx, _, addrDels, addrVals := setup(t, &hookCalled, &ubdeID) + completionTime := doRedelegation(t, app, ctx, addrDels, addrVals, &hookCalled) + + // PROVIDER CHAIN'S UNBONDING PERIOD ENDS - BUT UNBONDING CANNOT COMPLETE + ctx = ctx.WithBlockTime(completionTime) + _, err := app.StakingKeeper.CompleteRedelegation(ctx, addrDels[0], addrVals[0], addrVals[1]) + require.NoError(t, err) + + // Redelegation is not complete - still exists + redelegations := app.StakingKeeper.GetRedelegationsFromSrcValidator(ctx, addrVals[0]) + require.Equal(t, 1, len(redelegations)) + + // CONSUMER CHAIN'S UNBONDING PERIOD ENDS - STOPPED UNBONDING CAN NOW COMPLETE + err = app.StakingKeeper.UnbondingCanComplete(ctx, ubdeID) + require.NoError(t, err) + + // Redelegation is complete and record is gone + redelegations = app.StakingKeeper.GetRedelegationsFromSrcValidator(ctx, addrVals[0]) + require.Equal(t, 0, len(redelegations)) +} + +func TestUnbondingDelegationOnHold1(t *testing.T) { + var hookCalled bool + var ubdeID uint64 + app, ctx, bondDenom, addrDels, addrVals := setup(t, &hookCalled, &ubdeID) + completionTime, bondedAmt1, notBondedAmt1 := doUnbondingDelegation(t, app, ctx, bondDenom, addrDels, addrVals, &hookCalled) + + // CONSUMER CHAIN'S UNBONDING PERIOD ENDS - BUT UNBONDING CANNOT COMPLETE + err := app.StakingKeeper.UnbondingCanComplete(ctx, ubdeID) + require.NoError(t, err) + + bondedAmt3 := app.BankKeeper.GetBalance(ctx, app.StakingKeeper.GetBondedPool(ctx).GetAddress(), bondDenom).Amount + notBondedAmt3 := app.BankKeeper.GetBalance(ctx, app.StakingKeeper.GetNotBondedPool(ctx).GetAddress(), bondDenom).Amount + + // Bonded and unbonded amounts are the same as before because the completionTime has not yet passed and so the + // unbondingDelegation has not completed + require.True(sdk.IntEq(t, bondedAmt1, bondedAmt3)) + require.True(sdk.IntEq(t, notBondedAmt1, notBondedAmt3)) + + // PROVIDER CHAIN'S UNBONDING PERIOD ENDS - STOPPED UNBONDING CAN NOW COMPLETE + ctx = ctx.WithBlockTime(completionTime) + _, err = app.StakingKeeper.CompleteUnbonding(ctx, addrDels[0], addrVals[0]) + require.NoError(t, err) + + // Check that the unbonding was finally completed + bondedAmt5 := app.BankKeeper.GetBalance(ctx, app.StakingKeeper.GetBondedPool(ctx).GetAddress(), bondDenom).Amount + notBondedAmt5 := app.BankKeeper.GetBalance(ctx, app.StakingKeeper.GetNotBondedPool(ctx).GetAddress(), bondDenom).Amount + + require.True(sdk.IntEq(t, bondedAmt1, bondedAmt5)) + // Not bonded amount back to what it was originaly + require.True(sdk.IntEq(t, notBondedAmt1.SubRaw(1), notBondedAmt5)) +} + +func TestUnbondingDelegationOnHold2(t *testing.T) { + var hookCalled bool + var ubdeID uint64 + app, ctx, bondDenom, addrDels, addrVals := setup(t, &hookCalled, &ubdeID) + completionTime, bondedAmt1, notBondedAmt1 := doUnbondingDelegation(t, app, ctx, bondDenom, addrDels, addrVals, &hookCalled) + + // PROVIDER CHAIN'S UNBONDING PERIOD ENDS - BUT UNBONDING CANNOT COMPLETE + ctx = ctx.WithBlockTime(completionTime) + _, err := app.StakingKeeper.CompleteUnbonding(ctx, addrDels[0], addrVals[0]) + require.NoError(t, err) + + bondedAmt3 := app.BankKeeper.GetBalance(ctx, app.StakingKeeper.GetBondedPool(ctx).GetAddress(), bondDenom).Amount + notBondedAmt3 := app.BankKeeper.GetBalance(ctx, app.StakingKeeper.GetNotBondedPool(ctx).GetAddress(), bondDenom).Amount + + // Bonded and unbonded amounts are the same as before because the completionTime has not yet passed and so the + // unbondingDelegation has not completed + require.True(sdk.IntEq(t, bondedAmt1, bondedAmt3)) + require.True(sdk.IntEq(t, notBondedAmt1, notBondedAmt3)) + + // CONSUMER CHAIN'S UNBONDING PERIOD ENDS - STOPPED UNBONDING CAN NOW COMPLETE + err = app.StakingKeeper.UnbondingCanComplete(ctx, ubdeID) + require.NoError(t, err) + + // Check that the unbonding was finally completed + bondedAmt5 := app.BankKeeper.GetBalance(ctx, app.StakingKeeper.GetBondedPool(ctx).GetAddress(), bondDenom).Amount + notBondedAmt5 := app.BankKeeper.GetBalance(ctx, app.StakingKeeper.GetNotBondedPool(ctx).GetAddress(), bondDenom).Amount + + require.True(sdk.IntEq(t, bondedAmt1, bondedAmt5)) + // Not bonded amount back to what it was originaly + require.True(sdk.IntEq(t, notBondedAmt1.SubRaw(1), notBondedAmt5)) +} diff --git a/x/staking/keeper/val_state_change.go b/x/staking/keeper/val_state_change.go index db63c03ebf56..a55f5a0d67ba 100644 --- a/x/staking/keeper/val_state_change.go +++ b/x/staking/keeper/val_state_change.go @@ -219,6 +219,9 @@ func (k Keeper) ApplyAndReturnValidatorSetUpdates(ctx sdk.Context) (updates []ab k.SetLastTotalPower(ctx, totalPower) } + // set the list of validator updates + k.SetValidatorUpdates(ctx, updates) + return updates, err } @@ -315,12 +318,16 @@ func (k Keeper) beginUnbondingValidator(ctx sdk.Context, validator types.Validat panic(fmt.Sprintf("should not already be unbonded or unbonding, validator: %v\n", validator)) } + id := k.IncrementUnbondingId(ctx) + 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 + validator.UnbondingIds = append(validator.UnbondingIds, id) + // save the now unbonded validator record and power index k.SetValidator(ctx, validator) k.SetValidatorByPowerIndex(ctx, validator) @@ -335,6 +342,10 @@ func (k Keeper) beginUnbondingValidator(ctx sdk.Context, validator types.Validat } k.AfterValidatorBeginUnbonding(ctx, consAddr, validator.GetOperator()) + k.SetValidatorByUnbondingId(ctx, validator, id) + + k.AfterUnbondingInitiated(ctx, id) + return validator, nil } diff --git a/x/staking/keeper/validator.go b/x/staking/keeper/validator.go index 2a55ef43e571..58ec8bc68440 100644 --- a/x/staking/keeper/validator.go +++ b/x/staking/keeper/validator.go @@ -415,7 +415,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() @@ -456,13 +455,30 @@ func (k Keeper) UnbondAllMatureValidators(ctx sdk.Context) { panic("unexpected validator in unbonding queue; status was not unbonding") } - val = k.UnbondingToUnbonded(ctx, val) - if val.GetDelegatorShares().IsZero() { - k.RemoveValidator(ctx, val.GetOperator()) + if val.UnbondingOnHoldRefCount == 0 { + for _, id := range val.UnbondingIds { + k.DeleteUnbondingIndex(ctx, id) + } + val = k.UnbondingToUnbonded(ctx, val) + if val.GetDelegatorShares().IsZero() { + k.RemoveValidator(ctx, val.GetOperator()) + } else { + // remove unbonding ids + val.UnbondingIds = []uint64{} + } + // remove validator from queue + k.DeleteValidatorQueue(ctx, val) } } - - store.Delete(key) } } } + +func (k Keeper) IsValidatorJailed(ctx sdk.Context, addr sdk.ConsAddress) bool { + v, f := k.GetValidatorByConsAddr(ctx, addr) + if !f { + return false + } + + return v.Jailed +} diff --git a/x/staking/simulation/decoder_test.go b/x/staking/simulation/decoder_test.go index 06033a4cdb86..67fbd65760c5 100644 --- a/x/staking/simulation/decoder_test.go +++ b/x/staking/simulation/decoder_test.go @@ -40,8 +40,8 @@ func TestDecodeStore(t *testing.T) { val, err := types.NewValidator(valAddr1, delPk1, types.NewDescription("test", "test", "test", "test", "test")) require.NoError(t, err) del := types.NewDelegation(delAddr1, valAddr1, math.LegacyOneDec()) - ubd := types.NewUnbondingDelegation(delAddr1, valAddr1, 15, bondTime, math.OneInt()) - red := types.NewRedelegation(delAddr1, valAddr1, valAddr1, 12, bondTime, math.OneInt(), math.LegacyOneDec()) + ubd := types.NewUnbondingDelegation(delAddr1, valAddr1, 15, bondTime, math.OneInt(), 1) + red := types.NewRedelegation(delAddr1, valAddr1, valAddr1, 12, bondTime, math.OneInt(), math.LegacyOneDec(), 0) kvPairs := kv.Pairs{ Pairs: []kv.Pair{ diff --git a/x/staking/types/delegation.go b/x/staking/types/delegation.go index 51df1eb78b68..7b577e875c06 100644 --- a/x/staking/types/delegation.go +++ b/x/staking/types/delegation.go @@ -92,12 +92,14 @@ func (d Delegations) String() (out string) { return strings.TrimSpace(out) } -func NewUnbondingDelegationEntry(creationHeight int64, completionTime time.Time, balance math.Int) UnbondingDelegationEntry { +func NewUnbondingDelegationEntry(creationHeight int64, completionTime time.Time, balance math.Int, id uint64) UnbondingDelegationEntry { return UnbondingDelegationEntry{ - CreationHeight: creationHeight, - CompletionTime: completionTime, - InitialBalance: balance, - Balance: balance, + CreationHeight: creationHeight, + CompletionTime: completionTime, + InitialBalance: balance, + Balance: balance, + UnbondingId: id, + UnbondingOnHoldRefCount: 0, } } @@ -112,24 +114,50 @@ func (e UnbondingDelegationEntry) IsMature(currentTime time.Time) bool { return !e.CompletionTime.After(currentTime) } +// OnHold - is the current entry on hold due to external modules +func (e UnbondingDelegationEntry) OnHold() bool { + return e.UnbondingOnHoldRefCount > 0 +} + +// return the unbonding delegation entry +func MustMarshalUBDE(cdc codec.BinaryCodec, ubd UnbondingDelegationEntry) []byte { + return cdc.MustMarshal(&ubd) +} + +// unmarshal a unbonding delegation entry from a store value +func MustUnmarshalUBDE(cdc codec.BinaryCodec, value []byte) UnbondingDelegationEntry { + ubd, err := UnmarshalUBDE(cdc, value) + if err != nil { + panic(err) + } + + return ubd +} + +// unmarshal a unbonding delegation entry from a store value +func UnmarshalUBDE(cdc codec.BinaryCodec, value []byte) (ubd UnbondingDelegationEntry, err error) { + err = cdc.Unmarshal(value, &ubd) + return ubd, err +} + // NewUnbondingDelegation - create a new unbonding delegation object // //nolint:interfacer func NewUnbondingDelegation( delegatorAddr sdk.AccAddress, validatorAddr sdk.ValAddress, - creationHeight int64, minTime time.Time, balance math.Int, + creationHeight int64, minTime time.Time, balance math.Int, id uint64, ) UnbondingDelegation { return UnbondingDelegation{ DelegatorAddress: delegatorAddr.String(), ValidatorAddress: validatorAddr.String(), Entries: []UnbondingDelegationEntry{ - NewUnbondingDelegationEntry(creationHeight, minTime, balance), + NewUnbondingDelegationEntry(creationHeight, minTime, balance, id), }, } } // AddEntry - append entry to the unbonding delegation -func (ubd *UnbondingDelegation) AddEntry(creationHeight int64, minTime time.Time, balance math.Int) { +func (ubd *UnbondingDelegation) AddEntry(creationHeight int64, minTime time.Time, balance math.Int, id uint64) { // Check the entries exists with creation_height and complete_time entryIndex := -1 for index, ubdEntry := range ubd.Entries { @@ -148,7 +176,7 @@ func (ubd *UnbondingDelegation) AddEntry(creationHeight int64, minTime time.Time ubd.Entries[entryIndex] = ubdEntry } else { // append the new unbond delegation entry - entry := NewUnbondingDelegationEntry(creationHeight, minTime, balance) + entry := NewUnbondingDelegationEntry(creationHeight, minTime, balance, id) ubd.Entries = append(ubd.Entries, entry) } } @@ -207,12 +235,14 @@ func (ubds UnbondingDelegations) String() (out string) { return strings.TrimSpace(out) } -func NewRedelegationEntry(creationHeight int64, completionTime time.Time, balance math.Int, sharesDst sdk.Dec) RedelegationEntry { +func NewRedelegationEntry(creationHeight int64, completionTime time.Time, balance math.Int, sharesDst sdk.Dec, id uint64) RedelegationEntry { return RedelegationEntry{ - CreationHeight: creationHeight, - CompletionTime: completionTime, - InitialBalance: balance, - SharesDst: sharesDst, + CreationHeight: creationHeight, + CompletionTime: completionTime, + InitialBalance: balance, + SharesDst: sharesDst, + UnbondingId: id, + UnbondingOnHoldRefCount: 0, } } @@ -227,24 +257,29 @@ func (e RedelegationEntry) IsMature(currentTime time.Time) bool { return !e.CompletionTime.After(currentTime) } +// OnHold - is the current entry on hold due to external modules +func (e RedelegationEntry) OnHold() bool { + return e.UnbondingOnHoldRefCount > 0 +} + //nolint:interfacer func NewRedelegation( delegatorAddr sdk.AccAddress, validatorSrcAddr, validatorDstAddr sdk.ValAddress, - creationHeight int64, minTime time.Time, balance math.Int, sharesDst sdk.Dec, + creationHeight int64, minTime time.Time, balance math.Int, sharesDst sdk.Dec, id uint64, ) Redelegation { return Redelegation{ DelegatorAddress: delegatorAddr.String(), ValidatorSrcAddress: validatorSrcAddr.String(), ValidatorDstAddress: validatorDstAddr.String(), Entries: []RedelegationEntry{ - NewRedelegationEntry(creationHeight, minTime, balance, sharesDst), + NewRedelegationEntry(creationHeight, minTime, balance, sharesDst, id), }, } } // AddEntry - append entry to the unbonding delegation -func (red *Redelegation) AddEntry(creationHeight int64, minTime time.Time, balance math.Int, sharesDst sdk.Dec) { - entry := NewRedelegationEntry(creationHeight, minTime, balance, sharesDst) +func (red *Redelegation) AddEntry(creationHeight int64, minTime time.Time, balance math.Int, sharesDst sdk.Dec, id uint64) { + entry := NewRedelegationEntry(creationHeight, minTime, balance, sharesDst, id) red.Entries = append(red.Entries, entry) } @@ -371,10 +406,10 @@ func NewRedelegationResponse( // NewRedelegationEntryResponse creates a new RedelegationEntryResponse instance. func NewRedelegationEntryResponse( - creationHeight int64, completionTime time.Time, sharesDst sdk.Dec, initialBalance, balance math.Int, + creationHeight int64, completionTime time.Time, sharesDst sdk.Dec, initialBalance, balance math.Int, id uint64, ) RedelegationEntryResponse { return RedelegationEntryResponse{ - RedelegationEntry: NewRedelegationEntry(creationHeight, completionTime, initialBalance, sharesDst), + RedelegationEntry: NewRedelegationEntry(creationHeight, completionTime, initialBalance, sharesDst, id), Balance: balance, } } diff --git a/x/staking/types/delegation_test.go b/x/staking/types/delegation_test.go index 5cd9caca23b8..b259f95f45d9 100644 --- a/x/staking/types/delegation_test.go +++ b/x/staking/types/delegation_test.go @@ -34,7 +34,7 @@ func TestDelegationString(t *testing.T) { func TestUnbondingDelegationEqual(t *testing.T) { ubd1 := types.NewUnbondingDelegation(sdk.AccAddress(valAddr1), valAddr2, 0, - time.Unix(0, 0), sdk.NewInt(0)) + time.Unix(0, 0), sdk.NewInt(0), 1) ubd2 := ubd1 ok := ubd1.String() == ubd2.String() @@ -49,7 +49,7 @@ func TestUnbondingDelegationEqual(t *testing.T) { func TestUnbondingDelegationString(t *testing.T) { ubd := types.NewUnbondingDelegation(sdk.AccAddress(valAddr1), valAddr2, 0, - time.Unix(0, 0), sdk.NewInt(0)) + time.Unix(0, 0), sdk.NewInt(0), 1) require.NotEmpty(t, ubd.String()) } @@ -57,10 +57,10 @@ func TestUnbondingDelegationString(t *testing.T) { func TestRedelegationEqual(t *testing.T) { r1 := types.NewRedelegation(sdk.AccAddress(valAddr1), valAddr2, valAddr3, 0, time.Unix(0, 0), sdk.NewInt(0), - math.LegacyNewDec(0)) + math.LegacyNewDec(0), 1) r2 := types.NewRedelegation(sdk.AccAddress(valAddr1), valAddr2, valAddr3, 0, time.Unix(0, 0), sdk.NewInt(0), - math.LegacyNewDec(0)) + math.LegacyNewDec(0), 2) ok := r1.String() == r2.String() require.True(t, ok) @@ -75,7 +75,7 @@ func TestRedelegationEqual(t *testing.T) { func TestRedelegationString(t *testing.T) { r := types.NewRedelegation(sdk.AccAddress(valAddr1), valAddr2, valAddr3, 0, time.Unix(0, 0), sdk.NewInt(0), - math.LegacyNewDec(10)) + math.LegacyNewDec(10), 1) require.NotEmpty(t, r.String()) } @@ -112,8 +112,8 @@ func TestDelegationResponses(t *testing.T) { func TestRedelegationResponses(t *testing.T) { cdc := codec.NewLegacyAmino() entries := []types.RedelegationEntryResponse{ - types.NewRedelegationEntryResponse(0, time.Unix(0, 0), math.LegacyNewDec(5), sdk.NewInt(5), sdk.NewInt(5)), - types.NewRedelegationEntryResponse(0, time.Unix(0, 0), math.LegacyNewDec(5), sdk.NewInt(5), sdk.NewInt(5)), + types.NewRedelegationEntryResponse(0, time.Unix(0, 0), math.LegacyNewDec(5), sdk.NewInt(5), sdk.NewInt(5), 0), + types.NewRedelegationEntryResponse(0, time.Unix(0, 0), math.LegacyNewDec(5), sdk.NewInt(5), sdk.NewInt(5), 0), } rdr1 := types.NewRedelegationResponse(sdk.AccAddress(valAddr1), valAddr2, valAddr3, entries) rdr2 := types.NewRedelegationResponse(sdk.AccAddress(valAddr2), valAddr1, valAddr3, entries) diff --git a/x/staking/types/errors.go b/x/staking/types/errors.go index a9a6e43e35b6..5cf482984dc5 100644 --- a/x/staking/types/errors.go +++ b/x/staking/types/errors.go @@ -50,4 +50,6 @@ var ( ErrNoHistoricalInfo = sdkerrors.Register(ModuleName, 38, "no historical info found") ErrEmptyValidatorPubKey = sdkerrors.Register(ModuleName, 39, "empty validator public key") ErrCommissionLTMinRate = sdkerrors.Register(ModuleName, 40, "commission cannot be less than min rate") + ErrUnbondingNotFound = sdkerrors.Register(ModuleName, 41, "unbonding operation not found") + ErrUnbondingOnHoldRefCountNegative = sdkerrors.Register(ModuleName, 42, "cannot un-hold unbonding operation that is not on hold") ) diff --git a/x/staking/types/expected_keepers.go b/x/staking/types/expected_keepers.go index 871adcba9202..5d370cd405b5 100644 --- a/x/staking/types/expected_keepers.go +++ b/x/staking/types/expected_keepers.go @@ -61,7 +61,7 @@ type ValidatorSet interface { StakingTokenSupply(sdk.Context) math.Int // total staking token supply // slash the validator and delegators of the validator, specifying offence height, offence power, and slash fraction - Slash(sdk.Context, sdk.ConsAddress, int64, int64, sdk.Dec) math.Int + Slash(sdk.Context, sdk.ConsAddress, int64, int64, sdk.Dec, infraction InfractionType) math.Int Jail(sdk.Context, sdk.ConsAddress) // jail a validator Unjail(sdk.Context, sdk.ConsAddress) // unjail a validator @@ -103,6 +103,7 @@ type StakingHooks interface { BeforeDelegationRemoved(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) error // Must be called when a delegation is removed AfterDelegationModified(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) error BeforeValidatorSlashed(ctx sdk.Context, valAddr sdk.ValAddress, fraction sdk.Dec) error + AfterUnbondingInitiated(ctx sdk.Context, id uint64) error } // StakingHooksWrapper is a wrapper for modules to inject StakingHooks using depinject. diff --git a/x/staking/types/hooks.go b/x/staking/types/hooks.go index 93e0d21cc085..6fad1df77d6b 100644 --- a/x/staking/types/hooks.go +++ b/x/staking/types/hooks.go @@ -103,3 +103,12 @@ func (h MultiStakingHooks) BeforeValidatorSlashed(ctx sdk.Context, valAddr sdk.V } return nil } + +func (h MultiStakingHooks) AfterUnbondingInitiated(ctx sdk.Context, id uint64) error { + for i := range h { + if err := h[i].AfterUnbondingInitiated(ctx, id); err != nil { + return err + } + } + return nil +} diff --git a/x/staking/types/keys.go b/x/staking/types/keys.go index c56348789a66..4cba8d662014 100644 --- a/x/staking/types/keys.go +++ b/x/staking/types/keys.go @@ -42,15 +42,26 @@ var ( RedelegationByValSrcIndexKey = []byte{0x35} // prefix for each key for an redelegation, by source validator operator RedelegationByValDstIndexKey = []byte{0x36} // prefix for each key for an redelegation, by destination validator operator + UnbondingIdKey = []byte{0x37} // key for the counter for the incrementing id for UnbondingOperations + UnbondingIndexKey = []byte{0x38} // prefix for an index for looking up unbonding operations by their IDs + UnbondingQueueKey = []byte{0x41} // prefix for the timestamps in unbonding queue RedelegationQueueKey = []byte{0x42} // prefix for the timestamps in redelegations queue ValidatorQueueKey = []byte{0x43} // prefix for the timestamps in validator queue - HistoricalInfoKey = []byte{0x50} // prefix for the historical info + HistoricalInfoKey = []byte{0x50} // prefix for the historical info + ValidatorUpdatesKey = []byte{0x51} // prefix for the end block validator updates key ParamsKey = []byte{0x51} // prefix for parameters for module x/staking ) +// Returns a key for the index for looking up UnbondingDelegations by the UnbondingDelegationEntries they contain +func GetUnbondingIndexKey(id uint64) []byte { + bz := make([]byte, 8) + binary.BigEndian.PutUint64(bz, id) + return append(UnbondingIndexKey, bz...) +} + // GetValidatorKey creates the key for the validator with address // VALUE: staking/Validator func GetValidatorKey(operatorAddr sdk.ValAddress) []byte { diff --git a/x/staking/types/validator.go b/x/staking/types/validator.go index 434589554b91..933d3bf1b162 100644 --- a/x/staking/types/validator.go +++ b/x/staking/types/validator.go @@ -48,17 +48,18 @@ func NewValidator(operator sdk.ValAddress, pubKey cryptotypes.PubKey, descriptio } return Validator{ - OperatorAddress: operator.String(), - ConsensusPubkey: pkAny, - Jailed: false, - Status: Unbonded, - Tokens: math.ZeroInt(), - DelegatorShares: math.LegacyZeroDec(), - Description: description, - UnbondingHeight: int64(0), - UnbondingTime: time.Unix(0, 0).UTC(), - Commission: NewCommission(math.LegacyZeroDec(), math.LegacyZeroDec(), math.LegacyZeroDec()), - MinSelfDelegation: math.OneInt(), + OperatorAddress: operator.String(), + ConsensusPubkey: pkAny, + Jailed: false, + Status: Unbonded, + Tokens: math.ZeroInt(), + DelegatorShares: math.LegacyZeroDec(), + Description: description, + UnbondingHeight: int64(0), + UnbondingTime: time.Unix(0, 0).UTC(), + Commission: NewCommission(math.LegacyZeroDec(), math.LegacyZeroDec(), math.LegacyZeroDec()), + MinSelfDelegation: math.OneInt(), + UnbondingOnHoldRefCount: 0, }, nil }