diff --git a/x/ccv/provider/keeper/consumer_lifecycle.go b/x/ccv/provider/keeper/consumer_lifecycle.go index 51d2120f89..4eb2c7be0a 100644 --- a/x/ccv/provider/keeper/consumer_lifecycle.go +++ b/x/ccv/provider/keeper/consumer_lifecycle.go @@ -190,6 +190,33 @@ func (k Keeper) ConsumeIdsFromTimeQueue( return result, nil } +// HasActiveValidatorOptedIn checks whether at least one active validator is opted in to chain with `consumerId` +func (k Keeper) HasActiveValidatorOptedIn(ctx sdk.Context, consumerId string, activeValidators []stakingtypes.Validator) (bool, error) { + currentValidatorSet, err := k.GetConsumerValSet(ctx, consumerId) + if err != nil { + return false, err + } + + isActiveValidator := make(map[string]bool) + for _, val := range activeValidators { + consAddr, err := val.GetConsAddr() + if err != nil { + return false, fmt.Errorf("creating consumer genesis state, consumerId(%s): %w", consumerId, err) + } + providerConsAddr := types.NewProviderConsAddress(consAddr) + isActiveValidator[providerConsAddr.String()] = true + } + + for _, val := range currentValidatorSet { + providerConsAddr := types.NewProviderConsAddress(val.ProviderConsAddr) + if isActiveValidator[providerConsAddr.String()] { + return true, nil + } + } + + return false, nil +} + // LaunchConsumer launches the chain with the provided consumer id by creating the consumer client and the respective // consumer genesis file // @@ -205,8 +232,17 @@ func (k Keeper) LaunchConsumer( if err != nil { return fmt.Errorf("computing consumer next validator set, consumerId(%s): %w", consumerId, err) } + if len(initialValUpdates) == 0 { - return fmt.Errorf("cannot launch consumer with no validator opted in, consumerId(%s)", consumerId) + return fmt.Errorf("cannot launch consumer with no active validator opted in, consumerId(%s)", consumerId) + } + + hasActiveValidatorOptedIn, err := k.HasActiveValidatorOptedIn(ctx, consumerId, activeValidators) + if err != nil { + return fmt.Errorf("cannot check if an active validator has opted in, consumerId(%s): %w", consumerId, err) + } + if !hasActiveValidatorOptedIn { + return fmt.Errorf("cannot launch consumer with no active validator opted in, consumerId(%s)", consumerId) } // create consumer genesis diff --git a/x/ccv/provider/keeper/consumer_lifecycle_test.go b/x/ccv/provider/keeper/consumer_lifecycle_test.go index f90c359e4c..166f458301 100644 --- a/x/ccv/provider/keeper/consumer_lifecycle_test.go +++ b/x/ccv/provider/keeper/consumer_lifecycle_test.go @@ -472,6 +472,67 @@ func TestConsumeIdsFromTimeQueue(t *testing.T) { } } +func TestHasActiveValidatorOptedIn(t *testing.T) { + keeperParams := testkeeper.NewInMemKeeperParams(t) + providerKeeper, ctx, _, mocks := testkeeper.GetProviderKeeperAndCtx(t, keeperParams) + + // set 5 bonded validators with powers 5, 4, 3, 2, and 1 + NumberOfBondedValidators := 5 + var bondedValidators []stakingtypes.Validator + for i := 0; i < NumberOfBondedValidators; i++ { + power := int64(NumberOfBondedValidators - i) + bondedValidators = append(bondedValidators, createStakingValidator(ctx, mocks, power, i)) + } + mocks.MockStakingKeeper.EXPECT().GetBondedValidatorsByPower(gomock.Any()).Return(bondedValidators, nil).AnyTimes() + + // get the consensus addresses of the previously-set bonded validators + var consensusAddresses [][]byte + for i := 0; i < NumberOfBondedValidators; i++ { + consAddr, _ := bondedValidators[i].GetConsAddr() + consensusAddresses = append(consensusAddresses, consAddr) + } + + // Set the maximum number of provider consensus active validators (i.e., active validators) to 3. As a result + // `bondedValidators[4]` (with power of 4), `bondedValidators[3]` (with power of 3), `bondedValidators[2]` (with power of 2) + // are the active validators, and `bondedValidators[1]` (with power of 1) and `bondedValidators[0]` (with power of 0) + // are non-active validators. + maxProviderConsensusValidators := int64(3) + params := providerKeeper.GetParams(ctx) + params.MaxProviderConsensusValidators = maxProviderConsensusValidators + providerKeeper.SetParams(ctx, params) + + activeValidators, _ := providerKeeper.GetLastProviderConsensusActiveValidators(ctx) + + consumerId := "0" + + // consumer chain has only non-active validators + err := providerKeeper.SetConsumerValSet(ctx, consumerId, []providertypes.ConsensusValidator{ + {ProviderConsAddr: consensusAddresses[3]}, + {ProviderConsAddr: consensusAddresses[4]}}) + require.NoError(t, err) + hasActiveValidatorOptedIn, err := providerKeeper.HasActiveValidatorOptedIn(ctx, consumerId, activeValidators) + require.NoError(t, err) + require.False(t, hasActiveValidatorOptedIn) + + // consumer chain has one active validator + err = providerKeeper.SetConsumerValSet(ctx, consumerId, []providertypes.ConsensusValidator{ + {ProviderConsAddr: consensusAddresses[2]}}) + require.NoError(t, err) + hasActiveValidatorOptedIn, err = providerKeeper.HasActiveValidatorOptedIn(ctx, consumerId, activeValidators) + require.NoError(t, err) + require.True(t, hasActiveValidatorOptedIn) + + // consumer chain has one active and two non-active validators + err = providerKeeper.SetConsumerValSet(ctx, consumerId, []providertypes.ConsensusValidator{ + {ProviderConsAddr: consensusAddresses[3]}, + {ProviderConsAddr: consensusAddresses[4]}, + {ProviderConsAddr: consensusAddresses[1]}}) + require.NoError(t, err) + hasActiveValidatorOptedIn, err = providerKeeper.HasActiveValidatorOptedIn(ctx, consumerId, activeValidators) + require.NoError(t, err) + require.True(t, hasActiveValidatorOptedIn) +} + func TestCreateConsumerClient(t *testing.T) { type testCase struct { description string