diff --git a/testutil/keeper/expectations.go b/testutil/keeper/expectations.go index 014c0d72a5..12e3c338e1 100644 --- a/testutil/keeper/expectations.go +++ b/testutil/keeper/expectations.go @@ -31,21 +31,18 @@ import ( func GetMocksForCreateConsumerClient(ctx sdk.Context, mocks *MockedKeepers, expectedChainID string, expectedLatestHeight clienttypes.Height, ) []*gomock.Call { - // append MakeConsumerGenesis and CreateClient expectations - expectations := GetMocksForMakeConsumerGenesis(ctx, mocks, time.Hour) - createClientExp := mocks.MockClientKeeper.EXPECT().CreateClient( - gomock.Any(), - // Allows us to expect a match by field. These are the only two client state values - // that are dependent on parameters passed to CreateConsumerClient. - extra.StructMatcher().Field( - "ChainId", expectedChainID).Field( - "LatestHeight", expectedLatestHeight, - ), - gomock.Any(), - ).Return("clientID", nil).Times(1) - expectations = append(expectations, createClientExp) - - return expectations + return []*gomock.Call{ + mocks.MockClientKeeper.EXPECT().CreateClient( + gomock.Any(), + // Allows us to expect a match by field. These are the only two client state values + // that are dependent on parameters passed to CreateConsumerClient. + extra.StructMatcher().Field( + "ChainId", expectedChainID).Field( + "LatestHeight", expectedLatestHeight, + ), + gomock.Any(), + ).Return("clientID", nil).Times(1), + } } // GetMocksForMakeConsumerGenesis returns mock expectations needed to call MakeConsumerGenesis(). diff --git a/testutil/keeper/unit_test_helpers.go b/testutil/keeper/unit_test_helpers.go index df7fa80920..d70e2b5316 100644 --- a/testutil/keeper/unit_test_helpers.go +++ b/testutil/keeper/unit_test_helpers.go @@ -222,8 +222,6 @@ func SetupForDeleteConsumerChain(t *testing.T, ctx sdk.Context, ) { t.Helper() - SetupMocksForLastBondedValidatorsExpectation(mocks.MockStakingKeeper, 1, []stakingtypes.Validator{}, 1) - expectations := GetMocksForCreateConsumerClient(ctx, &mocks, "chainID", clienttypes.NewHeight(4, 5)) expectations = append(expectations, GetMocksForSetConsumerChain(ctx, &mocks, "chainID")...) @@ -241,7 +239,7 @@ func SetupForDeleteConsumerChain(t *testing.T, ctx sdk.Context, // set the chain to initialized so that we can create a consumer client providerKeeper.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_INITIALIZED) - err = providerKeeper.CreateConsumerClient(ctx, consumerId) + err = providerKeeper.CreateConsumerClient(ctx, consumerId, []byte{}) require.NoError(t, err) // set the mapping consumer ID <> client ID for the consumer chain providerKeeper.SetConsumerClientId(ctx, consumerId, "clientID") diff --git a/x/ccv/provider/keeper/consumer_lifecycle.go b/x/ccv/provider/keeper/consumer_lifecycle.go index 9c9e14f59b..5f806b0e6d 100644 --- a/x/ccv/provider/keeper/consumer_lifecycle.go +++ b/x/ccv/provider/keeper/consumer_lifecycle.go @@ -16,6 +16,7 @@ import ( sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + abci "github.com/cometbft/cometbft/abci/types" tmtypes "github.com/cometbft/cometbft/types" "github.com/cosmos/interchain-security/v6/x/ccv/provider/types" @@ -62,6 +63,9 @@ func (k Keeper) InitializeConsumer(ctx sdk.Context, consumerId string) (time.Tim // BeginBlockLaunchConsumers launches initialized consumers chains for which the spawn time has passed func (k Keeper) BeginBlockLaunchConsumers(ctx sdk.Context) error { + bondedValidators := []stakingtypes.Validator{} + activeValidators := []stakingtypes.Validator{} + consumerIds, err := k.ConsumeIdsFromTimeQueue( ctx, types.SpawnTimeToConsumerIdsKeyPrefix(), @@ -73,9 +77,22 @@ func (k Keeper) BeginBlockLaunchConsumers(ctx sdk.Context) error { if err != nil { return errorsmod.Wrapf(ccv.ErrInvalidConsumerState, "getting consumers ready to laumch: %s", err.Error()) } + if len(consumerIds) > 0 { + // get the bonded validators from the staking module + bondedValidators, err = k.GetLastBondedValidators(ctx) + if err != nil { + return fmt.Errorf("getting last bonded validators: %w", err) + } + // get the provider active validators + activeValidators, err = k.GetLastProviderConsensusActiveValidators(ctx) + if err != nil { + return fmt.Errorf("getting last provider active validators: %w", err) + } + } + for _, consumerId := range consumerIds { cachedCtx, writeFn := ctx.CacheContext() - err = k.LaunchConsumer(cachedCtx, consumerId) + err = k.LaunchConsumer(cachedCtx, bondedValidators, activeValidators, consumerId) if err != nil { ctx.Logger().Error("could not launch chain", "consumerId", consumerId, @@ -162,29 +179,64 @@ func (k Keeper) ConsumeIdsFromTimeQueue( // LaunchConsumer launches the chain with the provided consumer id by creating the consumer client and the respective // consumer genesis file -func (k Keeper) LaunchConsumer(ctx sdk.Context, consumerId string) error { - err := k.CreateConsumerClient(ctx, consumerId) +// +// TODO add unit test for LaunchConsumer +func (k Keeper) LaunchConsumer( + ctx sdk.Context, + bondedValidators []stakingtypes.Validator, + activeValidators []stakingtypes.Validator, + consumerId string, +) error { + // compute consumer initial validator set + initialValUpdates, err := k.ComputeConsumerNextValSet(ctx, bondedValidators, activeValidators, consumerId, []types.ConsensusValidator{}) if err != nil { - return err + 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) } - consumerGenesis, found := k.GetConsumerGenesis(ctx, consumerId) - if !found { - return errorsmod.Wrapf(types.ErrNoConsumerGenesis, "consumer genesis could not be found for consumer id: %s", consumerId) + // create consumer genesis + genesisState, err := k.MakeConsumerGenesis(ctx, consumerId, initialValUpdates) + if err != nil { + return fmt.Errorf("creating consumer genesis state, consumerId(%s): %w", consumerId, err) + } + err = k.SetConsumerGenesis(ctx, consumerId, genesisState) + if err != nil { + return fmt.Errorf("setting consumer genesis state, consumerId(%s): %w", consumerId, err) } - if len(consumerGenesis.Provider.InitialValSet) == 0 { - return errorsmod.Wrapf(types.ErrInvalidConsumerGenesis, "consumer genesis initial validator set is empty - no validators opted in consumer id: %s", consumerId) + // compute the hash of the consumer initial validator updates + updatesAsValSet, err := tmtypes.PB2TM.ValidatorUpdates(initialValUpdates) + if err != nil { + return fmt.Errorf("unable to create initial validator set from initial validator updates: %w", err) + } + valsetHash := tmtypes.NewValidatorSet(updatesAsValSet).Hash() + + // create the consumer client and the genesis + err = k.CreateConsumerClient(ctx, consumerId, valsetHash) + if err != nil { + return fmt.Errorf("crating consumer client, consumerId(%s): %w", consumerId, err) } k.SetConsumerPhase(ctx, consumerId, types.CONSUMER_PHASE_LAUNCHED) + k.Logger(ctx).Info("consumer successfully launched", + "consumerId", consumerId, + "valset size", len(initialValUpdates), + "valsetHash", string(valsetHash), + ) + return nil } // CreateConsumerClient will create the CCV client for the given consumer chain. The CCV channel must be built // on top of the CCV client to ensure connection with the right consumer chain. -func (k Keeper) CreateConsumerClient(ctx sdk.Context, consumerId string) error { +func (k Keeper) CreateConsumerClient( + ctx sdk.Context, + consumerId string, + valsetHash []byte, +) error { initializationRecord, err := k.GetConsumerInitializationParameters(ctx, consumerId) if err != nil { return err @@ -219,20 +271,11 @@ func (k Keeper) CreateConsumerClient(ctx sdk.Context, consumerId string) error { clientState.TrustingPeriod = trustPeriod clientState.UnbondingPeriod = consumerUnbondingPeriod - consumerGen, validatorSetHash, err := k.MakeConsumerGenesis(ctx, consumerId) - if err != nil { - return err - } - err = k.SetConsumerGenesis(ctx, consumerId, consumerGen) - if err != nil { - return err - } - // Create consensus state consensusState := ibctmtypes.NewConsensusState( ctx.BlockTime(), commitmenttypes.NewMerkleRoot([]byte(ibctmtypes.SentinelRoot)), - validatorSetHash, // use the hash of the updated initial valset + valsetHash, ) clientID, err := k.clientKeeper.CreateClient(ctx, clientState, consensusState) @@ -241,7 +284,7 @@ func (k Keeper) CreateConsumerClient(ctx sdk.Context, consumerId string) error { } k.SetConsumerClientId(ctx, consumerId, clientID) - k.Logger(ctx).Info("consumer chain launched (client created)", + k.Logger(ctx).Info("consumer client created", "consumer id", consumerId, "client id", clientID, ) @@ -256,6 +299,7 @@ func (k Keeper) CreateConsumerClient(ctx sdk.Context, consumerId string) error { sdk.NewAttribute(types.AttributeInitialHeight, initializationRecord.InitialHeight.String()), sdk.NewAttribute(types.AttributeTrustingPeriod, clientState.TrustingPeriod.String()), sdk.NewAttribute(types.AttributeUnbondingPeriod, clientState.UnbondingPeriod.String()), + sdk.NewAttribute(types.AttributeValsetHash, string(valsetHash)), ), ) @@ -267,21 +311,36 @@ func (k Keeper) CreateConsumerClient(ctx sdk.Context, consumerId string) error { func (k Keeper) MakeConsumerGenesis( ctx sdk.Context, consumerId string, -) (gen ccv.ConsumerGenesisState, nextValidatorsHash []byte, err error) { + initialValidatorUpdates []abci.ValidatorUpdate, +) (gen ccv.ConsumerGenesisState, err error) { initializationRecord, err := k.GetConsumerInitializationParameters(ctx, consumerId) if err != nil { - return gen, nil, errorsmod.Wrapf(ccv.ErrInvalidConsumerState, - "cannot retrieve initialization parameters: %s", err.Error()) - } - powerShapingParameters, err := k.GetConsumerPowerShapingParameters(ctx, consumerId) - if err != nil { - return gen, nil, errorsmod.Wrapf(ccv.ErrInvalidConsumerState, - "cannot retrieve power shaping parameters: %s", err.Error()) + return gen, errorsmod.Wrapf(ccv.ErrInvalidConsumerState, + "getting initialization parameters, consumerId(%s): %s", consumerId, err.Error()) } + // note that providerFeePoolAddrStr is sent to the consumer during the IBC Channel handshake; + // see HandshakeMetadata in OnChanOpenTry on the provider-side, and OnChanOpenAck on the consumer-side + consumerGenesisParams := ccv.NewParams( + true, + initializationRecord.BlocksPerDistributionTransmission, + initializationRecord.DistributionTransmissionChannel, + "", // providerFeePoolAddrStr, + initializationRecord.CcvTimeoutPeriod, + initializationRecord.TransferTimeoutPeriod, + initializationRecord.ConsumerRedistributionFraction, + initializationRecord.HistoricalEntries, + initializationRecord.UnbondingPeriod, + []string{}, + []string{}, + ccv.DefaultRetryDelayPeriod, + ) + + // create provider client state and consensus state for the consumer to be able + // to create a provider client providerUnbondingPeriod, err := k.stakingKeeper.UnbondingTime(ctx) if err != nil { - return gen, nil, errorsmod.Wrapf(types.ErrNoUnbondingTime, "unbonding time not found: %s", err) + return gen, errorsmod.Wrapf(types.ErrNoUnbondingTime, "unbonding time not found: %s", err) } height := clienttypes.GetSelfHeight(ctx) @@ -293,97 +352,23 @@ func (k Keeper) MakeConsumerGenesis( clientState.LatestHeight = height trustPeriod, err := ccv.CalculateTrustPeriod(providerUnbondingPeriod, k.GetTrustingPeriodFraction(ctx)) if err != nil { - return gen, nil, errorsmod.Wrapf(sdkerrors.ErrInvalidHeight, "error %s calculating trusting_period for: %s", err, height) + return gen, errorsmod.Wrapf(sdkerrors.ErrInvalidHeight, "error %s calculating trusting_period for: %s", err, height) } clientState.TrustingPeriod = trustPeriod clientState.UnbondingPeriod = providerUnbondingPeriod consState, err := k.clientKeeper.GetSelfConsensusState(ctx, height) if err != nil { - return gen, nil, errorsmod.Wrapf(clienttypes.ErrConsensusStateNotFound, "error %s getting self consensus state for: %s", err, height) - } - - // get the bonded validators from the staking module - bondedValidators, err := k.GetLastBondedValidators(ctx) - if err != nil { - return gen, nil, errorsmod.Wrapf(stakingtypes.ErrNoValidatorFound, "error getting last bonded validators: %s", err) - } - - minPower := int64(0) - if powerShapingParameters.Top_N > 0 { - // get the consensus active validators - // we do not want to base the power calculation for the top N - // on inactive validators, too, since the top N will be a percentage of the active set power - // otherwise, it could be that inactive validators are forced to validate - activeValidators, err := k.GetLastProviderConsensusActiveValidators(ctx) - if err != nil { - return gen, nil, errorsmod.Wrapf(stakingtypes.ErrNoValidatorFound, "error getting last active bonded validators: %s", err) - } - - // in a Top-N chain, we automatically opt in all validators that belong to the top N - minPower, err = k.ComputeMinPowerInTopN(ctx, activeValidators, powerShapingParameters.Top_N) - if err != nil { - return gen, nil, err - } - // log the minimum power in top N - k.Logger(ctx).Info("minimum power in top N at consumer genesis", - "consumerId", consumerId, - "minPower", minPower, - ) - - // set the minimal power of validators in the top N in the store - k.SetMinimumPowerInTopN(ctx, consumerId, minPower) - - err = k.OptInTopNValidators(ctx, consumerId, activeValidators, minPower) - if err != nil { - return gen, nil, fmt.Errorf("unable to opt in topN validators in MakeConsumerGenesis, consumerId(%s): %w", consumerId, err) - } - } - - // need to use the bondedValidators, not activeValidators, here since the chain might be opt-in and allow inactive vals - nextValidators, err := k.ComputeNextValidators(ctx, consumerId, bondedValidators, powerShapingParameters, minPower) - if err != nil { - return gen, nil, fmt.Errorf("unable to compute the next validators in MakeConsumerGenesis, consumerId(%s): %w", consumerId, err) - } - err = k.SetConsumerValSet(ctx, consumerId, nextValidators) - if err != nil { - return gen, nil, fmt.Errorf("unable to set consumer validator set in MakeConsumerGenesis, consumerId(%s): %w", consumerId, err) - } - - // get the initial updates with the latest set consumer public keys - initialUpdatesWithConsumerKeys := DiffValidators([]types.ConsensusValidator{}, nextValidators) - - // Get a hash of the consumer validator set from the update with applied consumer assigned keys - updatesAsValSet, err := tmtypes.PB2TM.ValidatorUpdates(initialUpdatesWithConsumerKeys) - if err != nil { - return gen, nil, fmt.Errorf("unable to create validator set from updates computed from key assignment in MakeConsumerGenesis: %s", err) + return gen, errorsmod.Wrapf(clienttypes.ErrConsensusStateNotFound, "error %s getting self consensus state for: %s", err, height) } - hash := tmtypes.NewValidatorSet(updatesAsValSet).Hash() - - // note that providerFeePoolAddrStr is sent to the consumer during the IBC Channel handshake; - // see HandshakeMetadata in OnChanOpenTry on the provider-side, and OnChanOpenAck on the consumer-side - consumerGenesisParams := ccv.NewParams( - true, - initializationRecord.BlocksPerDistributionTransmission, - initializationRecord.DistributionTransmissionChannel, - "", // providerFeePoolAddrStr, - initializationRecord.CcvTimeoutPeriod, - initializationRecord.TransferTimeoutPeriod, - initializationRecord.ConsumerRedistributionFraction, - initializationRecord.HistoricalEntries, - initializationRecord.UnbondingPeriod, - []string{}, - []string{}, - ccv.DefaultRetryDelayPeriod, - ) gen = *ccv.NewInitialConsumerGenesisState( clientState, consState.(*ibctmtypes.ConsensusState), - initialUpdatesWithConsumerKeys, + initialValidatorUpdates, consumerGenesisParams, ) - return gen, hash, nil + return gen, nil } // StopAndPrepareForConsumerRemoval sets the phase of the chain to stopped and prepares to get the state of the diff --git a/x/ccv/provider/keeper/consumer_lifecycle_test.go b/x/ccv/provider/keeper/consumer_lifecycle_test.go index a412b8826c..9f7eec3d69 100644 --- a/x/ccv/provider/keeper/consumer_lifecycle_test.go +++ b/x/ccv/provider/keeper/consumer_lifecycle_test.go @@ -8,16 +8,19 @@ import ( clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" ibctmtypes "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint" + ibctesting "github.com/cosmos/ibc-go/v8/testing" _go "github.com/cosmos/ics23/go" "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" "cosmossdk.io/math" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" sdk "github.com/cosmos/cosmos-sdk/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" abci "github.com/cometbft/cometbft/abci/types" + tmprotocrypto "github.com/cometbft/cometbft/proto/tendermint/crypto" cryptotestutil "github.com/cosmos/interchain-security/v6/testutil/crypto" testkeeper "github.com/cosmos/interchain-security/v6/testutil/keeper" @@ -143,28 +146,6 @@ func TestBeginBlockLaunchConsumers(t *testing.T) { ctx = ctx.WithBlockTime(now) // initialize registration, initialization, and update records - consumerMetadata := []providertypes.ConsumerMetadata{ - { - Name: "name", - Description: "spawn time passed", - }, - { - Name: "title", - Description: "spawn time passed", - }, - { - Name: "title", - Description: "spawn time not passed", - }, - { - Name: "title", - Description: "opt-in chain with at least one validator opted in", - }, - { - Name: "title", - Description: "opt-in chain with no validator opted in", - }, - } chainIds := []string{"chain0", "chain1", "chain2", "chain3", "chain4"} initializationParameters := []providertypes.ConsumerInitializationParameters{ @@ -272,25 +253,11 @@ func TestBeginBlockLaunchConsumers(t *testing.T) { }, } - // Expect client creation for only the first, second, and fifth proposals (spawn time already passed and valid) - expectedCalls := testkeeper.GetMocksForCreateConsumerClient(ctx, &mocks, "chain0", clienttypes.NewHeight(3, 4)) - expectedCalls = append(expectedCalls, testkeeper.GetMocksForCreateConsumerClient(ctx, &mocks, "chain1", clienttypes.NewHeight(3, 4))...) - expectedCalls = append(expectedCalls, testkeeper.GetMocksForCreateConsumerClient(ctx, &mocks, "chain3", clienttypes.NewHeight(3, 4))...) - - // The fifth chain would have spawn time passed and hence needs the mocks but the client will not be - // created because `chain4` is an Opt In chain and has no validator opted in - expectedCalls = append(expectedCalls, testkeeper.GetMocksForCreateConsumerClient(ctx, &mocks, "chain4", clienttypes.NewHeight(3, 4))...) - - gomock.InOrder(expectedCalls...) - // set up all the records for i, chainId := range chainIds { providerKeeper.SetConsumerChainId(ctx, fmt.Sprintf("%d", i), chainId) } - for i, r := range consumerMetadata { - providerKeeper.SetConsumerMetadata(ctx, fmt.Sprintf("%d", i), r) - } for i, r := range initializationParameters { err := providerKeeper.SetConsumerInitializationParameters(ctx, fmt.Sprintf("%d", i), r) require.NoError(t, err) @@ -311,11 +278,19 @@ func TestBeginBlockLaunchConsumers(t *testing.T) { valAddr, _ := sdk.ValAddressFromBech32(validator.GetOperator()) mocks.MockStakingKeeper.EXPECT().GetLastValidatorPower(gomock.Any(), valAddr).Return(int64(1), nil).AnyTimes() - // for the validator, expect a call to GetValidatorByConsAddr with its consensus address - mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr(gomock.Any(), consAddr).Return(validator, nil).AnyTimes() providerKeeper.SetOptedIn(ctx, "3", providertypes.NewProviderConsAddress(consAddr)) + // Expect genesis and client creation for only the first, second, and fifth chains (spawn time already passed and valid) + expectedCalls := testkeeper.GetMocksForMakeConsumerGenesis(ctx, &mocks, time.Hour) + expectedCalls = append(expectedCalls, testkeeper.GetMocksForCreateConsumerClient(ctx, &mocks, "chain0", clienttypes.NewHeight(3, 4))...) + expectedCalls = append(expectedCalls, testkeeper.GetMocksForMakeConsumerGenesis(ctx, &mocks, time.Hour)...) + expectedCalls = append(expectedCalls, testkeeper.GetMocksForCreateConsumerClient(ctx, &mocks, "chain1", clienttypes.NewHeight(3, 4))...) + expectedCalls = append(expectedCalls, testkeeper.GetMocksForMakeConsumerGenesis(ctx, &mocks, time.Hour)...) + expectedCalls = append(expectedCalls, testkeeper.GetMocksForCreateConsumerClient(ctx, &mocks, "chain3", clienttypes.NewHeight(3, 4))...) + + gomock.InOrder(expectedCalls...) + err := providerKeeper.BeginBlockLaunchConsumers(ctx) require.NoError(t, err) @@ -488,8 +463,6 @@ func TestConsumeIdsFromTimeQueue(t *testing.T) { } } -// Tests the CreateConsumerClient method against the spec, -// with more granularity than what's covered in TestHandleCreateConsumerChainProposal. func TestCreateConsumerClient(t *testing.T) { type testCase struct { description string @@ -502,12 +475,11 @@ func TestCreateConsumerClient(t *testing.T) { { description: "No state mutation, new client should be created", setup: func(providerKeeper *providerkeeper.Keeper, ctx sdk.Context, mocks *testkeeper.MockedKeepers) { - providerKeeper.SetConsumerPhase(ctx, "0", providertypes.CONSUMER_PHASE_INITIALIZED) + providerKeeper.SetConsumerPhase(ctx, CONSUMER_ID, providertypes.CONSUMER_PHASE_INITIALIZED) // Valid client creation is asserted with mock expectations here - testkeeper.SetupMocksForLastBondedValidatorsExpectation(mocks.MockStakingKeeper, 0, []stakingtypes.Validator{}, 1) // returns empty validator set gomock.InOrder( - testkeeper.GetMocksForCreateConsumerClient(ctx, mocks, "chainID", clienttypes.NewHeight(4, 5))..., + testkeeper.GetMocksForCreateConsumerClient(ctx, mocks, CONSUMER_CHAIN_ID, clienttypes.NewHeight(4, 5))..., ) }, expClientCreated: true, @@ -515,13 +487,10 @@ func TestCreateConsumerClient(t *testing.T) { { description: "chain for this consumer id has already launched, and hence client was created, NO new one is created", setup: func(providerKeeper *providerkeeper.Keeper, ctx sdk.Context, mocks *testkeeper.MockedKeepers) { - providerKeeper.SetConsumerPhase(ctx, "0", providertypes.CONSUMER_PHASE_LAUNCHED) + providerKeeper.SetConsumerPhase(ctx, CONSUMER_ID, providertypes.CONSUMER_PHASE_LAUNCHED) // Expect none of the client creation related calls to happen - mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Times(0) mocks.MockClientKeeper.EXPECT().CreateClient(gomock.Any(), gomock.Any(), gomock.Any()).Times(0) - mocks.MockClientKeeper.EXPECT().GetSelfConsensusState(gomock.Any(), gomock.Any()).Times(0) - testkeeper.SetupMocksForLastBondedValidatorsExpectation(mocks.MockStakingKeeper, 0, []stakingtypes.Validator{}, 0) // returns empty validator set }, expClientCreated: false, }, @@ -537,18 +506,16 @@ func TestCreateConsumerClient(t *testing.T) { tc.setup(&providerKeeper, ctx, &mocks) // Call method with same arbitrary values as defined above in mock expectations. - providerKeeper.SetConsumerChainId(ctx, "0", "chainID") - err := providerKeeper.SetConsumerMetadata(ctx, "0", testkeeper.GetTestConsumerMetadata()) - require.NoError(t, err) - err = providerKeeper.SetConsumerInitializationParameters(ctx, "0", testkeeper.GetTestInitializationParameters()) - require.NoError(t, err) - err = providerKeeper.SetConsumerPowerShapingParameters(ctx, "0", testkeeper.GetTestPowerShapingParameters()) + providerKeeper.SetConsumerChainId(ctx, CONSUMER_ID, CONSUMER_CHAIN_ID) + err := providerKeeper.SetConsumerInitializationParameters(ctx, CONSUMER_ID, testkeeper.GetTestInitializationParameters()) require.NoError(t, err) - err = providerKeeper.CreateConsumerClient(ctx, "0") + err = providerKeeper.CreateConsumerClient(ctx, CONSUMER_ID, []byte{}) if tc.expClientCreated { require.NoError(t, err) - testCreatedConsumerClient(t, ctx, providerKeeper, "0", "clientID") + clientId, found := providerKeeper.GetConsumerClientId(ctx, CONSUMER_ID) + require.True(t, found) + require.Equal(t, "clientID", clientId) } else { require.Error(t, err) } @@ -558,30 +525,14 @@ func TestCreateConsumerClient(t *testing.T) { } } -// Executes test assertions for a created consumer client. -// -// Note: Separated from TestCreateConsumerClient to also be called from TestCreateConsumerChainProposal. -func testCreatedConsumerClient(t *testing.T, - ctx sdk.Context, providerKeeper providerkeeper.Keeper, consumerId, expectedClientID string, -) { - t.Helper() - // ClientID should be stored. - clientId, found := providerKeeper.GetConsumerClientId(ctx, consumerId) - require.True(t, found, "consumer client not found") - require.Equal(t, expectedClientID, clientId) - - // Only assert that consumer genesis was set, - // more granular tests on consumer genesis should be defined in TestMakeConsumerGenesis - _, ok := providerKeeper.GetConsumerGenesis(ctx, consumerId) - require.True(t, ok) -} - // TestMakeConsumerGenesis tests the MakeConsumerGenesis keeper method. // An expected genesis state is hardcoded in json, unmarshaled, and compared // against an actual consumer genesis state constructed by a provider keeper. func TestMakeConsumerGenesis(t *testing.T) { keeperParams := testkeeper.NewInMemKeeperParams(t) providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, keeperParams) + defer ctrl.Finish() + moduleParams := providertypes.Params{ TemplateClient: &ibctmtypes.ClientState{ TrustLevel: ibctmtypes.DefaultTrustLevel, @@ -641,73 +592,80 @@ func TestMakeConsumerGenesis(t *testing.T) { NumberOfEpochsToStartReceivingRewards: 24, } providerKeeper.SetParams(ctx, moduleParams) - defer ctrl.Finish() - - // - // Other setup not covered by custom template client state - // - ctx = ctx.WithChainID("testchain1") // consumerId is obtained from ctx - ctx = ctx.WithBlockHeight(5) // RevisionHeight obtained from ctx - testkeeper.SetupMocksForLastBondedValidatorsExpectation(mocks.MockStakingKeeper, 0, []stakingtypes.Validator{}, 1) - gomock.InOrder(testkeeper.GetMocksForMakeConsumerGenesis(ctx, &mocks, 1814400000000000)...) // matches params from jsonString - consumerMetadata := providertypes.ConsumerMetadata{ - Name: "name", - Description: "description", - } - ccvTimeoutPeriod := time.Duration(2419200000000000) transferTimeoutPeriod := time.Duration(3600000000000) - unbondingPeriod := time.Duration(1728000000000000) + consumerUnbondingPeriod := time.Duration(1728000000000000) + providerUnbondingPeriod := time.Duration(1814400000000000) + trustingPeriod := time.Duration(1197504000000000) + providerChainId := "provider-1" + providerRevisionNumber := uint64(1) + providerRevisionHeight := int64(5) + initializationParameters := providertypes.ConsumerInitializationParameters{ BlocksPerDistributionTransmission: 1000, CcvTimeoutPeriod: ccvTimeoutPeriod, TransferTimeoutPeriod: transferTimeoutPeriod, ConsumerRedistributionFraction: "0.75", HistoricalEntries: 10000, - UnbondingPeriod: unbondingPeriod, + UnbondingPeriod: consumerUnbondingPeriod, } - providerKeeper.SetConsumerChainId(ctx, "0", "testchain1") - err := providerKeeper.SetConsumerMetadata(ctx, "0", consumerMetadata) - require.NoError(t, err) - err = providerKeeper.SetConsumerInitializationParameters(ctx, "0", initializationParameters) - require.NoError(t, err) - err = providerKeeper.SetConsumerPowerShapingParameters(ctx, "0", providertypes.PowerShapingParameters{}) + + // + // Other setup not covered by custom template client state + // + ctx = ctx.WithChainID(providerChainId) // consumerId is obtained from ctx + ctx = ctx.WithBlockHeight(providerRevisionHeight) // RevisionHeight obtained from ctx + gomock.InOrder(testkeeper.GetMocksForMakeConsumerGenesis(ctx, &mocks, providerUnbondingPeriod)...) + + providerKeeper.SetConsumerChainId(ctx, CONSUMER_ID, CONSUMER_CHAIN_ID) + err := providerKeeper.SetConsumerInitializationParameters(ctx, CONSUMER_ID, initializationParameters) require.NoError(t, err) - actualGenesis, _, err := providerKeeper.MakeConsumerGenesis(ctx, "0") + _, pks, _ := ibctesting.GenerateKeys(t, 2) + var ppks [2]tmprotocrypto.PublicKey + for i, pk := range pks { + ppks[i], _ = cryptocodec.ToCmtProtoPublicKey(pk) + } + initialValUpdates := []abci.ValidatorUpdate{ + {PubKey: ppks[0], Power: 1}, + {PubKey: ppks[1], Power: 2}, + } + + actualGenesis, err := providerKeeper.MakeConsumerGenesis(ctx, CONSUMER_ID, initialValUpdates) require.NoError(t, err) // JSON string with tabs, newlines and spaces for readability - jsonString := `{ + jsonString := fmt.Sprintf(`{ "params": { "enabled": true, - "blocks_per_distribution_transmission": 1000, - "ccv_timeout_period": 2419200000000000, - "transfer_timeout_period": 3600000000000, - "consumer_redistribution_fraction": "0.75", - "historical_entries": 10000, - "unbonding_period": 1728000000000000, + "blocks_per_distribution_transmission": %d, + "ccv_timeout_period": %d, + "transfer_timeout_period": %d, + "consumer_redistribution_fraction": "%s", + "historical_entries": %d, + "unbonding_period": %d, "soft_opt_out_threshold": "0", "reward_denoms": [], "provider_reward_denoms": [], - "retry_delay_period": 3600000000000 + "retry_delay_period": %d }, "new_chain": true, "provider" : { "client_state": { - "chain_id": "testchain1", + "chain_id": "%s", "trust_level": { "numerator": 1, "denominator": 3 }, - "trusting_period": 1197504000000000, - "unbonding_period": 1814400000000000, - "max_clock_drift": 10000000000, + "trusting_period": %d, + "unbonding_period": %d, + "max_clock_drift": %d, "frozen_height": {}, "latest_height": { - "revision_height": 5 + "revision_number": %d, + "revision_height": %d }, "proof_specs": [ { @@ -752,25 +710,30 @@ func TestMakeConsumerGenesis(t *testing.T) { }, "next_validators_hash": "E30CE736441FB9101FADDAF7E578ABBE6DFDB67207112350A9A904D554E1F5BE" }, - "initial_val_set": [ - { - "pub_key": { - "type": "tendermint/PubKeyEd25519", - "value": "dcASx5/LIKZqagJWN0frOlFtcvz91frYmj/zmoZRWro=" - }, - "power": 1 - } - ] + "initial_val_set": [{}] } - }` + }`, + initializationParameters.BlocksPerDistributionTransmission, + ccvTimeoutPeriod.Nanoseconds(), + transferTimeoutPeriod.Nanoseconds(), + initializationParameters.ConsumerRedistributionFraction, + initializationParameters.HistoricalEntries, + consumerUnbondingPeriod.Nanoseconds(), + ccvtypes.DefaultRetryDelayPeriod.Nanoseconds(), + providerChainId, + trustingPeriod.Nanoseconds(), + providerUnbondingPeriod.Nanoseconds(), + providertypes.DefaultMaxClockDrift.Nanoseconds(), + providerRevisionNumber, + providerRevisionHeight, + ) var expectedGenesis ccvtypes.ConsumerGenesisState err = json.Unmarshal([]byte(jsonString), &expectedGenesis) // ignores tabs, newlines and spaces require.NoError(t, err) + expectedGenesis.Provider.InitialValSet = initialValUpdates // Zeroing out different fields that are challenging to mock - actualGenesis.Provider.InitialValSet = []abci.ValidatorUpdate{} - expectedGenesis.Provider.InitialValSet = []abci.ValidatorUpdate{} actualGenesis.Provider.ConsensusState = &ibctmtypes.ConsensusState{} expectedGenesis.Provider.ConsensusState = &ibctmtypes.ConsensusState{} @@ -808,7 +771,6 @@ func TestBeginBlockStopConsumers(t *testing.T) { for i := range consumerIds { chainId := chainIds[i] // A consumer chain is setup corresponding to each consumerId, making these mocks necessary - testkeeper.SetupMocksForLastBondedValidatorsExpectation(mocks.MockStakingKeeper, 0, []stakingtypes.Validator{}, 1) expectations = append(expectations, testkeeper.GetMocksForCreateConsumerClient(ctx, &mocks, chainId, clienttypes.NewHeight(2, 3))...) expectations = append(expectations, testkeeper.GetMocksForSetConsumerChain(ctx, &mocks, chainId)...) @@ -838,7 +800,7 @@ func TestBeginBlockStopConsumers(t *testing.T) { providerKeeper.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_INITIALIZED) providerKeeper.SetConsumerClientId(ctx, consumerId, "clientID") - err = providerKeeper.CreateConsumerClient(ctx, consumerId) + err = providerKeeper.CreateConsumerClient(ctx, consumerId, []byte{}) require.NoError(t, err) err = providerKeeper.SetConsumerChain(ctx, "channelID") require.NoError(t, err) diff --git a/x/ccv/provider/keeper/keeper_test.go b/x/ccv/provider/keeper/keeper_test.go index 68a6381421..6b3e9ba8a3 100644 --- a/x/ccv/provider/keeper/keeper_test.go +++ b/x/ccv/provider/keeper/keeper_test.go @@ -23,7 +23,7 @@ import ( const ( CONSUMER_CHAIN_ID = "chain-id" - CONSUMER_ID = "13" + CONSUMER_ID = "0" ) // TestValsetUpdateBlockHeight tests the getter, setter, and deletion methods for valset updates mapped to block height diff --git a/x/ccv/provider/keeper/relay.go b/x/ccv/provider/keeper/relay.go index cf8a3de7cf..6af8aa23ec 100644 --- a/x/ccv/provider/keeper/relay.go +++ b/x/ccv/provider/keeper/relay.go @@ -227,42 +227,15 @@ func (k Keeper) QueueVSCPackets(ctx sdk.Context) error { continue } - currentValidators, err := k.GetConsumerValSet(ctx, consumerId) + currentValSet, err := k.GetConsumerValSet(ctx, consumerId) if err != nil { - return fmt.Errorf("getting consumer validators, consumerId(%s): %w", consumerId, err) - } - powerShapingParameters, err := k.GetConsumerPowerShapingParameters(ctx, consumerId) - if err != nil { - return fmt.Errorf("getting consumer power shaping parameters, consumerId(%s): %w", consumerId, err) - } - - minPower := int64(0) - if powerShapingParameters.Top_N > 0 { - // in a Top-N chain, we automatically opt in all validators that belong to the top N - // of the active validators - minPower, err = k.ComputeMinPowerInTopN(ctx, activeValidators, powerShapingParameters.Top_N) - if err != nil { - return fmt.Errorf("computing min power to opt in, consumerId(%s): %w", consumerId, err) - } - - // set the minimal power of validators in the top N in the store - k.SetMinimumPowerInTopN(ctx, consumerId, minPower) - - err := k.OptInTopNValidators(ctx, consumerId, activeValidators, minPower) - if err != nil { - return fmt.Errorf("opting in topN validators, consumerId(%s), minPower(%d): %w", consumerId, minPower, err) - } - } - - nextValidators, err := k.ComputeNextValidators(ctx, consumerId, bondedValidators, powerShapingParameters, minPower) - if err != nil { - return fmt.Errorf("computing next validators, consumerId(%s), minPower(%d): %w", consumerId, minPower, err) + return fmt.Errorf("getting consumer current validator set, consumerId(%s): %w", consumerId, err) } - valUpdates := DiffValidators(currentValidators, nextValidators) - err = k.SetConsumerValSet(ctx, consumerId, nextValidators) + // compute consumer next validator set + valUpdates, err := k.ComputeConsumerNextValSet(ctx, bondedValidators, activeValidators, consumerId, currentValSet) if err != nil { - return fmt.Errorf("setting consumer validator set, consumerId(%s): %w", consumerId, err) + return fmt.Errorf("computing consumer next validator set, consumerId(%s): %w", consumerId, err) } // check whether there are changes in the validator set diff --git a/x/ccv/provider/keeper/validator_set_update.go b/x/ccv/provider/keeper/validator_set_update.go index 2366568e2e..d375d42d8f 100644 --- a/x/ccv/provider/keeper/validator_set_update.go +++ b/x/ccv/provider/keeper/validator_set_update.go @@ -4,6 +4,7 @@ import ( "fmt" "sort" + errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" @@ -255,3 +256,61 @@ func (k Keeper) GetLastProviderConsensusActiveValidators(ctx sdk.Context) ([]sta maxVals := k.GetMaxProviderConsensusValidators(ctx) return ccv.GetLastBondedValidatorsUtil(ctx, k.stakingKeeper, uint32(maxVals)) } + +// ComputeConsumerNextValSet computes the consumer next validator set and returns +// the validator updates to be sent to the consumer chain. +// For TopN consumer chains, it automatically opts in all validators that +// belong to the top N of the active validators. +// +// TODO add unit test for ComputeConsumerNextValSet +func (k Keeper) ComputeConsumerNextValSet( + ctx sdk.Context, + bondedValidators []stakingtypes.Validator, + activeValidators []stakingtypes.Validator, + consumerId string, + currentConsumerValSet []types.ConsensusValidator, +) ([]abci.ValidatorUpdate, error) { + powerShapingParameters, err := k.GetConsumerPowerShapingParameters(ctx, consumerId) + if err != nil { + return []abci.ValidatorUpdate{}, + errorsmod.Wrapf(ccv.ErrInvalidConsumerState, "getting power shaping parameters: %s", err.Error()) + } + + minPower := int64(0) + if powerShapingParameters.Top_N > 0 { + minPower, err = k.ComputeMinPowerInTopN(ctx, activeValidators, powerShapingParameters.Top_N) + if err != nil { + return []abci.ValidatorUpdate{}, + fmt.Errorf("computing min power to opt in, consumerId(%s): %w", consumerId, err) + } + + // set the minimal power of validators in the top N in the store + k.SetMinimumPowerInTopN(ctx, consumerId, minPower) + + // in a Top-N chain, we automatically opt in all validators that belong to the top N + // of the active validators + err = k.OptInTopNValidators(ctx, consumerId, activeValidators, minPower) + if err != nil { + return []abci.ValidatorUpdate{}, + fmt.Errorf("opting in topN validators, consumerId(%s), minPower(%d): %w", consumerId, minPower, err) + } + } + + // need to use the bondedValidators, not activeValidators, here since the chain might be opt-in and allow inactive vals + nextValidators, err := k.ComputeNextValidators(ctx, consumerId, bondedValidators, powerShapingParameters, minPower) + if err != nil { + return []abci.ValidatorUpdate{}, + fmt.Errorf("computing next validators, consumerId(%s), minPower(%d): %w", consumerId, minPower, err) + } + + err = k.SetConsumerValSet(ctx, consumerId, nextValidators) + if err != nil { + return []abci.ValidatorUpdate{}, + fmt.Errorf("setting consumer validator set, consumerId(%s): %w", consumerId, err) + } + + // get the initial updates with the latest set consumer public keys + valUpdates := DiffValidators(currentConsumerValSet, nextValidators) + + return valUpdates, nil +} diff --git a/x/ccv/provider/types/events.go b/x/ccv/provider/types/events.go index 77992a7e02..5583174e42 100644 --- a/x/ccv/provider/types/events.go +++ b/x/ccv/provider/types/events.go @@ -17,6 +17,7 @@ const ( AttributeInitialHeight = "initial_height" AttributeTrustingPeriod = "trusting_period" AttributeUnbondingPeriod = "unbonding_period" + AttributeValsetHash = "valset_hash" AttributeProviderValidatorAddress = "provider_validator_address" AttributeConsumerConsensusPubKey = "consumer_consensus_pub_key" AttributeAddConsumerRewardDenom = "add_consumer_reward_denom"