diff --git a/x/ccv/provider/keeper/consumer_lifecycle.go b/x/ccv/provider/keeper/consumer_lifecycle.go index 609cca108e..adda4902d1 100644 --- a/x/ccv/provider/keeper/consumer_lifecycle.go +++ b/x/ccv/provider/keeper/consumer_lifecycle.go @@ -25,7 +25,7 @@ import ( // PrepareConsumerForLaunch prepares to move the launch of a consumer chain from the previous spawn time to spawn time. // Previous spawn time can correspond to its zero value if the validator was not previously set for launch. func (k Keeper) PrepareConsumerForLaunch(ctx sdk.Context, consumerId string, previousSpawnTime, spawnTime time.Time) error { - if !previousSpawnTime.Equal(time.Time{}) { + if !previousSpawnTime.IsZero() { // if this is not the first initialization and hence `previousSpawnTime` does not contain the zero value of `Time` // remove the consumer id from the previous spawn time err := k.RemoveConsumerToBeLaunched(ctx, consumerId, previousSpawnTime) diff --git a/x/ccv/provider/keeper/msg_server.go b/x/ccv/provider/keeper/msg_server.go index 45fad0f7d7..ddce3d117e 100644 --- a/x/ccv/provider/keeper/msg_server.go +++ b/x/ccv/provider/keeper/msg_server.go @@ -494,7 +494,7 @@ func (k msgServer) UpdateConsumer(goCtx context.Context, msg *types.MsgUpdateCon eventAttributes = append(eventAttributes, sdk.NewAttribute(types.AttributeConsumerName, msg.Metadata.Name)) } - // get the previous spawn time so that we can use it in `PrepareConsumerForLaunch` + // get the previous spawn time so that we can remove its previously planned spawn time if a new spawn time is provided previousInitializationParameters, err := k.Keeper.GetConsumerInitializationParameters(ctx, consumerId) if err != nil { return &resp, errorsmod.Wrapf(ccvtypes.ErrInvalidConsumerState, @@ -503,6 +503,31 @@ func (k msgServer) UpdateConsumer(goCtx context.Context, msg *types.MsgUpdateCon previousSpawnTime := previousInitializationParameters.SpawnTime if msg.InitializationParameters != nil { + phase := k.GetConsumerPhase(ctx, consumerId) + + if phase == types.CONSUMER_PHASE_LAUNCHED { + return &resp, errorsmod.Wrap(types.ErrInvalidMsgUpdateConsumer, + "cannot update the initialization parameters of an an already launched chain; "+ + "do not provide any initialization parameters when updating a launched chain") + } + + if msg.InitializationParameters.SpawnTime.IsZero() { + if phase == types.CONSUMER_PHASE_INITIALIZED { + // chain was previously ready to launch at `previousSpawnTime` so we remove the + // consumer from getting launched and move it back to the Registered phase + err = k.RemoveConsumerToBeLaunched(ctx, consumerId, previousSpawnTime) + if err != nil { + return &resp, errorsmod.Wrapf(types.ErrInvalidMsgUpdateConsumer, + "cannot remove the consumer from being launched: %s", err.Error()) + } + k.SetConsumerPhase(ctx, consumerId, types.CONSUMER_PHASE_REGISTERED) + } + } else { + // add SpawnTime event attribute + eventAttributes = append(eventAttributes, + sdk.NewAttribute(types.AttributeConsumerSpawnTime, msg.InitializationParameters.SpawnTime.String())) + } + if err = k.Keeper.SetConsumerInitializationParameters(ctx, msg.ConsumerId, *msg.InitializationParameters); err != nil { return &resp, errorsmod.Wrapf(types.ErrInvalidConsumerInitializationParameters, "cannot set consumer initialization parameters: %s", err.Error()) diff --git a/x/ccv/provider/keeper/msg_server_test.go b/x/ccv/provider/keeper/msg_server_test.go index a2f9cb3aab..e9689eb943 100644 --- a/x/ccv/provider/keeper/msg_server_test.go +++ b/x/ccv/provider/keeper/msg_server_test.go @@ -1,6 +1,7 @@ package keeper_test import ( + "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" "testing" "time" @@ -176,4 +177,84 @@ func TestUpdateConsumer(t *testing.T) { require.Equal(t, providertypes.ConsumerIds{ Ids: []string{consumerId}, }, consumerIds) + + // assert that we CANNOT update the initialization parameters of a launched chain + providerKeeper.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_LAUNCHED) + _, err = msgServer.UpdateConsumer(ctx, + &providertypes.MsgUpdateConsumer{ + Owner: expectedOwnerAddress, ConsumerId: consumerId, + Metadata: nil, + InitializationParameters: &expectedInitializationParameters, + PowerShapingParameters: nil, + }) + require.ErrorContains(t, err, "cannot update the initialization parameters of an an already launched chain") + + // assert that we can update the consumer metadata of a launched chain + providerKeeper.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_LAUNCHED) + expectedConsumerMetadata.Name = "name of a launched chain" + _, err = msgServer.UpdateConsumer(ctx, + &providertypes.MsgUpdateConsumer{ + Owner: expectedOwnerAddress, ConsumerId: consumerId, + Metadata: &expectedConsumerMetadata, + InitializationParameters: nil, + PowerShapingParameters: nil, + }) + require.NoError(t, err) + actualConsumerMetadata, err = providerKeeper.GetConsumerMetadata(ctx, consumerId) + require.NoError(t, err) + require.Equal(t, expectedConsumerMetadata, actualConsumerMetadata) + + // assert that we can update the power-shaping parameters of a launched chain + providerKeeper.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_LAUNCHED) + expectedPowerShapingParameters.ValidatorSetCap = 123 + _, err = msgServer.UpdateConsumer(ctx, + &providertypes.MsgUpdateConsumer{ + Owner: expectedOwnerAddress, ConsumerId: consumerId, + Metadata: nil, + InitializationParameters: nil, + PowerShapingParameters: &expectedPowerShapingParameters, + }) + require.NoError(t, err) + actualPowerShapingParameters, err = providerKeeper.GetConsumerPowerShapingParameters(ctx, consumerId) + require.NoError(t, err) + require.Equal(t, expectedPowerShapingParameters, actualPowerShapingParameters) + + // assert that if we call `MsgUpdateConsumer` with a spawn time of zero on an initialized chain, the chain + // will not be scheduled to launch and will move back to its Registered phase + providerKeeper.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_INITIALIZED) + // first assert that the chain is scheduled to launch + previousSpawnTime = expectedInitializationParameters.SpawnTime + consumerIds, err = providerKeeper.GetConsumersToBeLaunched(ctx, previousSpawnTime) + require.NoError(t, err) + require.Equal(t, providertypes.ConsumerIds{ + Ids: []string{consumerId}, + }, consumerIds) + + // then, update with a spawn time of zero to prevent the chain from launching + expectedInitializationParameters.SpawnTime = time.Time{} + // also update an arbitrary field of the initialization parameters + // to verify that the parameters of the chain get updated + expectedInitializationParameters.InitialHeight = types.NewHeight(1, 123456) + _, err = msgServer.UpdateConsumer(ctx, + &providertypes.MsgUpdateConsumer{ + Owner: expectedOwnerAddress, ConsumerId: consumerId, + Metadata: nil, + InitializationParameters: &expectedInitializationParameters, + PowerShapingParameters: nil, + }) + // assert the chain is not scheduled to launch + consumerIds, err = providerKeeper.GetConsumersToBeLaunched(ctx, previousSpawnTime) + require.NoError(t, err) + require.Empty(t, consumerIds) + // also assert that no chain is scheduled to launch at zero time + consumerIds, err = providerKeeper.GetConsumersToBeLaunched(ctx, time.Time{}) + require.NoError(t, err) + require.Empty(t, consumerIds) + // assert that the chain has moved to the registered phase because it is not ready to launch + phase = providerKeeper.GetConsumerPhase(ctx, consumerId) + require.Equal(t, providertypes.CONSUMER_PHASE_REGISTERED, phase) + // assert that the initialization parameters of the chain were nevertheless updated + actualInitializationParameters, err = providerKeeper.GetConsumerInitializationParameters(ctx, consumerId) + require.NoError(t, err) + require.Equal(t, expectedInitializationParameters, actualInitializationParameters) }