diff --git a/testutil/keeper/mocks.go b/testutil/keeper/mocks.go index e588f51e30..7abadd63d5 100644 --- a/testutil/keeper/mocks.go +++ b/testutil/keeper/mocks.go @@ -49,6 +49,20 @@ func (m *MockStakingKeeper) EXPECT() *MockStakingKeeperMockRecorder { return m.recorder } +// BlockValidatorUpdates mocks base method. +func (m *MockStakingKeeper) BlockValidatorUpdates(ctx types0.Context) []types.ValidatorUpdate { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BlockValidatorUpdates", ctx) + ret0, _ := ret[0].([]types.ValidatorUpdate) + return ret0 +} + +// BlockValidatorUpdates indicates an expected call of BlockValidatorUpdates. +func (mr *MockStakingKeeperMockRecorder) BlockValidatorUpdates(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlockValidatorUpdates", reflect.TypeOf((*MockStakingKeeper)(nil).BlockValidatorUpdates), ctx) +} + // BondDenom mocks base method. func (m *MockStakingKeeper) BondDenom(ctx types0.Context) string { m.ctrl.T.Helper() @@ -119,6 +133,21 @@ func (mr *MockStakingKeeperMockRecorder) GetLastValidators(ctx interface{}) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastValidators", reflect.TypeOf((*MockStakingKeeper)(nil).GetLastValidators), ctx) } +// GetRedelegationByUnbondingID mocks base method. +func (m *MockStakingKeeper) GetRedelegationByUnbondingID(ctx types0.Context, id uint64) (types5.Redelegation, bool) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRedelegationByUnbondingID", ctx, id) + ret0, _ := ret[0].(types5.Redelegation) + ret1, _ := ret[1].(bool) + return ret0, ret1 +} + +// GetRedelegationByUnbondingID indicates an expected call of GetRedelegationByUnbondingID. +func (mr *MockStakingKeeperMockRecorder) GetRedelegationByUnbondingID(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRedelegationByUnbondingID", reflect.TypeOf((*MockStakingKeeper)(nil).GetRedelegationByUnbondingID), ctx, id) +} + // GetRedelegationsFromSrcValidator mocks base method. func (m *MockStakingKeeper) GetRedelegationsFromSrcValidator(ctx types0.Context, valAddr types0.ValAddress) []types5.Redelegation { m.ctrl.T.Helper() @@ -133,6 +162,21 @@ func (mr *MockStakingKeeperMockRecorder) GetRedelegationsFromSrcValidator(ctx, v return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRedelegationsFromSrcValidator", reflect.TypeOf((*MockStakingKeeper)(nil).GetRedelegationsFromSrcValidator), ctx, valAddr) } +// GetUnbondingDelegationByUnbondingID mocks base method. +func (m *MockStakingKeeper) GetUnbondingDelegationByUnbondingID(ctx types0.Context, id uint64) (types5.UnbondingDelegation, bool) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUnbondingDelegationByUnbondingID", ctx, id) + ret0, _ := ret[0].(types5.UnbondingDelegation) + ret1, _ := ret[1].(bool) + return ret0, ret1 +} + +// GetUnbondingDelegationByUnbondingID indicates an expected call of GetUnbondingDelegationByUnbondingID. +func (mr *MockStakingKeeperMockRecorder) GetUnbondingDelegationByUnbondingID(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUnbondingDelegationByUnbondingID", reflect.TypeOf((*MockStakingKeeper)(nil).GetUnbondingDelegationByUnbondingID), ctx, id) +} + // GetUnbondingDelegationsFromValidator mocks base method. func (m *MockStakingKeeper) GetUnbondingDelegationsFromValidator(ctx types0.Context, valAddr types0.ValAddress) []types5.UnbondingDelegation { m.ctrl.T.Helper() @@ -192,6 +236,21 @@ func (mr *MockStakingKeeperMockRecorder) GetValidatorByConsAddr(ctx, consAddr in return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidatorByConsAddr", reflect.TypeOf((*MockStakingKeeper)(nil).GetValidatorByConsAddr), ctx, consAddr) } +// GetValidatorByUnbondingID mocks base method. +func (m *MockStakingKeeper) GetValidatorByUnbondingID(ctx types0.Context, id uint64) (types5.Validator, bool) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetValidatorByUnbondingID", ctx, id) + ret0, _ := ret[0].(types5.Validator) + ret1, _ := ret[1].(bool) + return ret0, ret1 +} + +// GetValidatorByUnbondingID indicates an expected call of GetValidatorByUnbondingID. +func (mr *MockStakingKeeperMockRecorder) GetValidatorByUnbondingID(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidatorByUnbondingID", reflect.TypeOf((*MockStakingKeeper)(nil).GetValidatorByUnbondingID), ctx, id) +} + // GetValidatorUpdates mocks base method. func (m *MockStakingKeeper) GetValidatorUpdates(ctx types0.Context) []types.ValidatorUpdate { m.ctrl.T.Helper() diff --git a/x/ccv/provider/keeper/hooks.go b/x/ccv/provider/keeper/hooks.go index 88590a9875..ed926f16c7 100644 --- a/x/ccv/provider/keeper/hooks.go +++ b/x/ccv/provider/keeper/hooks.go @@ -8,6 +8,7 @@ import ( v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/cosmos/interchain-security/v4/x/ccv/provider/types" providertypes "github.com/cosmos/interchain-security/v4/x/ccv/provider/types" ccvtypes "github.com/cosmos/interchain-security/v4/x/ccv/types" ) @@ -35,8 +36,64 @@ func (k *Keeper) Hooks() Hooks { func (h Hooks) AfterUnbondingInitiated(ctx sdk.Context, id uint64) error { var consumerChainIDS []string + // get validator address from unbonding operation + unbondingType, found := h.k.stakingKeeper.GetUnbondingType(ctx, id) + vadAddrBech32 := "" + if !found { + ctx.Logger().Error("undefined type for unbonding operation id: %d", id) + return nil + } + + switch unbondingType { + case stakingtypes.UnbondingType_UnbondingDelegation: + ubd, found := h.k.stakingKeeper.GetUnbondingDelegationByUnbondingID(ctx, id) + if !found { + ctx.Logger().Error("unfound ubonding delegation for unbonding id: %d", id) + return nil + } + vadAddrBech32 = ubd.ValidatorAddress + case stakingtypes.UnbondingType_Redelegation: + red, found := h.k.stakingKeeper.GetRedelegationByUnbondingID(ctx, id) + if !found { + ctx.Logger().Error("unfound relegation for unbonding operation id: %d", id) + return nil + } + vadAddrBech32 = red.ValidatorSrcAddress + case stakingtypes.UnbondingType_ValidatorUnbonding: + val, found := h.k.stakingKeeper.GetValidatorByUnbondingID(ctx, id) + if !found { + ctx.Logger().Error("unfound validator for unbonding operation id: %d", id) + return nil + } + vadAddrBech32 = val.OperatorAddress + default: + ctx.Logger().Error("invalid unbonding operation type: %s", unbondingType) + return nil + } + + valAddr, err := sdk.ValAddressFromBech32(vadAddrBech32) + if err != nil { + ctx.Logger().Error(err.Error()) + return nil + } + + validator, found := h.k.stakingKeeper.GetValidator(ctx, valAddr) + if !found { + ctx.Logger().Error("unfound validator for validator address %s", vadAddrBech32) + return nil + } + + consAddr, err := validator.GetConsAddr() + if err != nil { + ctx.Logger().Error(err.Error()) + return nil + } + + // get all consumers where the validator is in the validator set for _, chain := range h.k.GetAllConsumerChains(ctx) { - consumerChainIDS = append(consumerChainIDS, chain.ChainId) + if h.k.IsConsumerValidator(ctx, chain.ChainId, types.NewProviderConsAddress(consAddr)) { + consumerChainIDS = append(consumerChainIDS, chain.ChainId) + } } if len(consumerChainIDS) == 0 { diff --git a/x/ccv/provider/keeper/relay_test.go b/x/ccv/provider/keeper/relay_test.go index 594783a9a1..118d481063 100644 --- a/x/ccv/provider/keeper/relay_test.go +++ b/x/ccv/provider/keeper/relay_test.go @@ -12,7 +12,9 @@ import ( "cosmossdk.io/math" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" @@ -468,6 +470,10 @@ func TestHandleVSCMaturedPacket(t *testing.T) { // Start first unbonding without any consumers registered var unbondingOpId uint64 = 1 + gomock.InOrder( + mocks.MockStakingKeeper.EXPECT().GetUnbondingType(ctx, unbondingOpId).Return(stakingtypes.UnbondingType_Undefined, false), + ) + err := pk.Hooks().AfterUnbondingInitiated(ctx, unbondingOpId) require.NoError(t, err) // Check that no unbonding op was stored @@ -478,12 +484,34 @@ func TestHandleVSCMaturedPacket(t *testing.T) { pk.IncrementValidatorSetUpdateId(ctx) require.Equal(t, uint64(2), pk.GetValidatorSetUpdateId(ctx)) - // Registered first consumer + // Register first consumer pk.SetConsumerClientId(ctx, "chain-1", "client-1") + // Create 2 validators + vals := []stakingtypes.Validator{} + valsPk := []cryptotypes.PubKey{} + for i := 0; i < 2; i++ { + pubkey, err := cryptocodec.FromTmPubKeyInterface(cryptotestutil.NewCryptoIdentityFromIntSeed(54321 + i).TMCryptoPubKey()) + require.NoError(t, err) + valsPk = append(valsPk, pubkey) + pkAny, err := codectypes.NewAnyWithValue(pubkey) + require.NoError(t, err) + vals = append(vals, stakingtypes.Validator{ConsensusPubkey: pkAny}) + } + + // Opt-in one validator to consumer + pk.SetConsumerValidator(ctx, "chain-1", types.ConsumerValidator{ProviderConsAddr: valsPk[0].Address()}) + // Start second unbonding unbondingOpId = 2 gomock.InOrder( + mocks.MockStakingKeeper.EXPECT().GetUnbondingType(ctx, unbondingOpId).Return(stakingtypes.UnbondingType_UnbondingDelegation, true), + mocks.MockStakingKeeper.EXPECT().GetUnbondingDelegationByUnbondingID(ctx, unbondingOpId).Return( + stakingtypes.UnbondingDelegation{ + ValidatorAddress: sdk.ValAddress([]byte{1}).String(), + }, true), + mocks.MockStakingKeeper.EXPECT().GetValidator(ctx, sdk.ValAddress([]byte{1})). + Return(vals[0], true), mocks.MockStakingKeeper.EXPECT().PutUnbondingOnHold(ctx, unbondingOpId).Return(nil), ) err = pk.Hooks().AfterUnbondingInitiated(ctx, unbondingOpId) @@ -507,10 +535,21 @@ func TestHandleVSCMaturedPacket(t *testing.T) { // Registered second consumer pk.SetConsumerClientId(ctx, "chain-2", "client-2") + // Opt-in both validators to second consumer + pk.SetConsumerValidator(ctx, "chain-2", types.ConsumerValidator{ProviderConsAddr: valsPk[0].Address()}) + pk.SetConsumerValidator(ctx, "chain-2", types.ConsumerValidator{ProviderConsAddr: valsPk[1].Address()}) + // Start third and fourth unbonding unbondingOpIds := []uint64{3, 4} for _, id := range unbondingOpIds { gomock.InOrder( + mocks.MockStakingKeeper.EXPECT().GetUnbondingType(ctx, id).Return(stakingtypes.UnbondingType_Redelegation, true), + mocks.MockStakingKeeper.EXPECT().GetRedelegationByUnbondingID(ctx, id).Return( + stakingtypes.Redelegation{ + ValidatorSrcAddress: sdk.ValAddress([]byte{1}).String(), + }, true), + mocks.MockStakingKeeper.EXPECT().GetValidator(ctx, sdk.ValAddress([]byte{1})). + Return(vals[0], true), mocks.MockStakingKeeper.EXPECT().PutUnbondingOnHold(ctx, id).Return(nil), ) err = pk.Hooks().AfterUnbondingInitiated(ctx, id) @@ -531,6 +570,33 @@ func TestHandleVSCMaturedPacket(t *testing.T) { require.Equal(t, unbondingOpIds, ids) } + // Increment vscID + pk.IncrementValidatorSetUpdateId(ctx) + require.Equal(t, uint64(4), pk.GetValidatorSetUpdateId(ctx)) + + // Start fith unbonding + unbondingOpId = 5 + gomock.InOrder( + mocks.MockStakingKeeper.EXPECT().GetUnbondingType(ctx, unbondingOpId).Return(stakingtypes.UnbondingType_ValidatorUnbonding, true), + mocks.MockStakingKeeper.EXPECT().GetValidatorByUnbondingID(ctx, unbondingOpId).Return( + stakingtypes.Validator{ + OperatorAddress: sdk.ValAddress([]byte{1}).String(), + }, true), + mocks.MockStakingKeeper.EXPECT().GetValidator(ctx, sdk.ValAddress([]byte{1})). + Return(vals[1], true), + mocks.MockStakingKeeper.EXPECT().PutUnbondingOnHold(ctx, unbondingOpId).Return(nil), + ) + err = pk.Hooks().AfterUnbondingInitiated(ctx, unbondingOpId) + require.NoError(t, err) + + // Check that an unbonding op was stored for chain-2 only + // since it's the only consumer the unbonding validator has opted-in to + expectedChains = []string{"chain-2"} + unbondingOp, found = pk.GetUnbondingOp(ctx, unbondingOpId) + require.True(t, found) + require.Equal(t, unbondingOpId, unbondingOp.Id) + require.Equal(t, expectedChains, unbondingOp.UnbondingConsumerChains) + // Handle VSCMatured packet from chain-1 for vscID 1. // Note that no VSCPacket was sent as the chain was not yet registered, // but the code should still work diff --git a/x/ccv/types/expected_keepers.go b/x/ccv/types/expected_keepers.go index 73566926ec..7b77ebd095 100644 --- a/x/ccv/types/expected_keepers.go +++ b/x/ccv/types/expected_keepers.go @@ -42,6 +42,9 @@ type StakingKeeper interface { IterateLastValidatorPowers(ctx sdk.Context, cb func(addr sdk.ValAddress, power int64) (stop bool)) PowerReduction(ctx sdk.Context) math.Int PutUnbondingOnHold(ctx sdk.Context, id uint64) error + GetUnbondingDelegationByUnbondingID(ctx sdk.Context, id uint64) (ubd stakingtypes.UnbondingDelegation, found bool) + GetRedelegationByUnbondingID(ctx sdk.Context, id uint64) (red stakingtypes.Redelegation, found bool) + GetValidatorByUnbondingID(ctx sdk.Context, id uint64) (val stakingtypes.Validator, found bool) IterateValidators(ctx sdk.Context, f func(index int64, validator stakingtypes.ValidatorI) (stop bool)) Validator(ctx sdk.Context, addr sdk.ValAddress) stakingtypes.ValidatorI IsValidatorJailed(ctx sdk.Context, addr sdk.ConsAddress) bool @@ -54,6 +57,7 @@ type StakingKeeper interface { GetUnbondingDelegationsFromValidator(ctx sdk.Context, valAddr sdk.ValAddress) (ubds []stakingtypes.UnbondingDelegation) GetRedelegationsFromSrcValidator(ctx sdk.Context, valAddr sdk.ValAddress) (reds []stakingtypes.Redelegation) GetUnbondingType(ctx sdk.Context, id uint64) (unbondingType stakingtypes.UnbondingType, found bool) + BlockValidatorUpdates(ctx sdk.Context) []abci.ValidatorUpdate } // SlashingKeeper defines the contract expected to perform ccv slashing