diff --git a/x/vaas/provider/keeper/consumer_equivocation.go b/x/vaas/provider/keeper/consumer_equivocation.go index c2ecd60..f5846d0 100644 --- a/x/vaas/provider/keeper/consumer_equivocation.go +++ b/x/vaas/provider/keeper/consumer_equivocation.go @@ -35,10 +35,10 @@ func (k Keeper) HandleConsumerDoubleVoting( pubkey cryptotypes.PubKey, ) error { // check that the evidence is for an ICS consumer chain - if _, found := k.GetConsumerClientId(ctx, consumerId); !found { + if k.GetConsumerPhase(ctx, consumerId) != types.CONSUMER_PHASE_LAUNCHED { return errorsmod.Wrapf( vaastypes.ErrInvalidDoubleVotingEvidence, - "cannot find consumer chain %s", + "consumer chain %s is not launched", consumerId, ) } diff --git a/x/vaas/provider/keeper/consumer_lifecycle.go b/x/vaas/provider/keeper/consumer_lifecycle.go index 41f933f..7c29ff4 100644 --- a/x/vaas/provider/keeper/consumer_lifecycle.go +++ b/x/vaas/provider/keeper/consumer_lifecycle.go @@ -9,11 +9,9 @@ import ( vaastypes "github.com/allinbits/vaas/x/vaas/types" abci "github.com/cometbft/cometbft/abci/types" - tmtypes "github.com/cometbft/cometbft/types" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" commitmenttypes "github.com/cosmos/ibc-go/v10/modules/core/23-commitment/types" - ibchost "github.com/cosmos/ibc-go/v10/modules/core/exported" ibctmtypes "github.com/cosmos/ibc-go/v10/modules/light-clients/07-tendermint" "cosmossdk.io/collections" @@ -220,13 +218,18 @@ func (k Keeper) HasActiveConsumerValidator(ctx sdk.Context, consumerId string, a return false, nil } -// LaunchConsumer launches the chain with the provided consumer id by creating the consumer client and the respective -// consumer genesis file. +// LaunchConsumer launches the chain with the provided consumer id by creating the consumer genesis file. +// The IBC client is not created here; it is discovered later when the relayer creates one. func (k Keeper) LaunchConsumer( ctx sdk.Context, bondedValidators []stakingtypes.Validator, consumerId string, ) error { + initializationRecord, err := k.GetConsumerInitializationParameters(ctx, consumerId) + if err != nil { + return fmt.Errorf("getting initialization parameters, consumerId(%s): %w", consumerId, err) + } + // compute consumer initial validator set (all validators validate all consumers) initialValUpdates, err := k.ComputeConsumerNextValSet(ctx, bondedValidators, consumerId, []types.ConsensusValidator{}) if err != nil { @@ -247,16 +250,7 @@ func (k Keeper) LaunchConsumer( return fmt.Errorf("setting consumer genesis state, consumerId(%s): %w", consumerId, err) } - 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() - - err = k.CreateConsumerClient(ctx, consumerId, valsetHash) - if err != nil { - return fmt.Errorf("creating consumer client, consumerId(%s): %w", consumerId, err) - } + k.SetEquivocationEvidenceMinHeight(ctx, consumerId, initializationRecord.InitialHeight.RevisionHeight) k.SetConsumerPhase(ctx, consumerId, types.CONSUMER_PHASE_LAUNCHED) @@ -268,119 +262,6 @@ func (k Keeper) LaunchConsumer( 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. -// -// IMPORTANT - Timing Constraint (Option B: New Consumer Chain): -// The consensus state for the new client is created with the current provider block time -// (ctx.BlockTime()). This timestamp will be used by the consumer's IBC client to verify -// headers from the new consumer chain. -// -// For the IBC client to remain valid, the consumer chain's genesis_time must satisfy: -// -// genesis_time - client_creation_time < trusting_period -// -// Where: -// - client_creation_time = the block time when this function executes (LaunchConsumer is called) -// - genesis_time = the time at which the consumer chain starts producing blocks -// - trusting_period = calculated from unbonding_period * trusting_period_fraction -// -// If this constraint is violated, the client will appear expired before any blocks can be -// relayed, causing relayers to fail with "trusted state outside of trusting period" errors. -// -// Recommended approach: Set the consumer's genesis_time to be close to the expected -// client_creation_time (spawn_time on the provider), or query the actual client creation -// timestamp and use it directly as genesis_time. -func (k Keeper) CreateConsumerClient( - ctx sdk.Context, - consumerId string, - valsetHash []byte, -) error { - initializationRecord, err := k.GetConsumerInitializationParameters(ctx, consumerId) - if err != nil { - return err - } - - phase := k.GetConsumerPhase(ctx, consumerId) - if phase != types.CONSUMER_PHASE_INITIALIZED { - return errorsmod.Wrapf(types.ErrInvalidPhase, - "cannot create client for consumer chain that is not in the Initialized phase but in phase %d: %s", phase, consumerId) - } - - chainId, err := k.GetConsumerChainId(ctx, consumerId) - if err != nil { - return err - } - - // Set minimum height for equivocation evidence from this consumer chain - k.SetEquivocationEvidenceMinHeight(ctx, consumerId, initializationRecord.InitialHeight.RevisionHeight) - - // Consumers start out with the unbonding period from the initialization parameters - consumerUnbondingPeriod := initializationRecord.UnbondingPeriod - - // Create client state by getting template client from initialization parameters - clientState := k.GetTemplateClient(ctx) - clientState.ChainId = chainId - clientState.LatestHeight = initializationRecord.InitialHeight - - trustPeriod, err := vaastypes.CalculateTrustPeriod(consumerUnbondingPeriod, k.GetTrustingPeriodFraction(ctx)) - if err != nil { - return err - } - clientState.TrustingPeriod = trustPeriod - clientState.UnbondingPeriod = consumerUnbondingPeriod - - // Create consensus state for the new consumer chain. - // - Timestamp: Current block time. IMPORTANT: Consumer's genesis_time must be within - // trusting_period of this timestamp, otherwise the client will appear expired. - // - Root: SentinelRoot is used as a placeholder since the consumer hasn't produced - // blocks yet. The client will be updated with real app hashes once blocks are relayed. - // - NextValidatorsHash: Hash of the initial validator set. This binds the client to - // the expected validators, so the first consumer block must be signed by this set. - consensusState := ibctmtypes.NewConsensusState( - ctx.BlockTime(), - commitmenttypes.NewMerkleRoot([]byte(ibctmtypes.SentinelRoot)), - valsetHash, - ) - - clientStateBytes, err := clientState.Marshal() - if err != nil { - return err - } - consensusStateBytes, err := consensusState.Marshal() - if err != nil { - return err - } - - // this means the client must be tendermint - clientID, err := k.clientKeeper.CreateClient(ctx, ibchost.Tendermint, clientStateBytes, consensusStateBytes) - if err != nil { - return err - } - k.SetConsumerClientId(ctx, consumerId, clientID) - - k.Logger(ctx).Info("consumer client created", - "consumer id", consumerId, - "client id", clientID, - ) - - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.EventTypeConsumerClientCreated, - sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), - sdk.NewAttribute(types.AttributeConsumerId, consumerId), - sdk.NewAttribute(types.AttributeConsumerChainId, chainId), - sdk.NewAttribute(clienttypes.AttributeKeyClientID, clientID), - 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)), - ), - ) - - return nil -} - // MakeConsumerGenesis returns the created consumer genesis state for consumer chain `consumerId`, // as well as the validator hash of the initial validator set of the consumer chain func (k Keeper) MakeConsumerGenesis( diff --git a/x/vaas/provider/keeper/genesis.go b/x/vaas/provider/keeper/genesis.go index 87d2584..1cf8619 100644 --- a/x/vaas/provider/keeper/genesis.go +++ b/x/vaas/provider/keeper/genesis.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/allinbits/vaas/x/vaas/provider/types" + vaastypes "github.com/allinbits/vaas/x/vaas/types" abci "github.com/cometbft/cometbft/abci/types" @@ -20,7 +21,9 @@ func (k Keeper) InitGenesis(ctx sdk.Context, genState *types.GenesisState) []abc // Set initial state for each consumer chain for _, cs := range genState.ConsumerStates { chainID := cs.ChainId - k.SetConsumerClientId(ctx, chainID, cs.ClientId) + if cs.ClientId != "" { + k.SetConsumerClientId(ctx, chainID, cs.ClientId) + } k.SetConsumerPhase(ctx, chainID, cs.Phase) if err := k.SetConsumerGenesis(ctx, chainID, cs.ConsumerGenesis); err != nil { // An error here would indicate something is very wrong, @@ -98,26 +101,26 @@ func (k Keeper) InitGenesisValUpdates(ctx sdk.Context) []abci.ValidatorUpdate { // ExportGenesis returns the CCV provider module's exported genesis func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState { - launchedConsumerIds := k.GetAllConsumersWithIBCClients(ctx) + activeConsumerIds := k.GetAllActiveConsumerIds(ctx) // export states for each consumer chains var consumerStates []types.ConsumerState - for _, consumerId := range launchedConsumerIds { - // no need for the second return value of GetConsumerClientId - // as GetAllConsumersWithIBCClients already iterated through - // the entire prefix range + for _, consumerId := range activeConsumerIds { clientId, _ := k.GetConsumerClientId(ctx, consumerId) + phase := k.GetConsumerPhase(ctx, consumerId) gen, found := k.GetConsumerGenesis(ctx, consumerId) if !found { - panic(fmt.Errorf("cannot find genesis for consumer chain %s with client %s", consumerId, clientId)) + if phase != types.CONSUMER_PHASE_REGISTERED && phase != types.CONSUMER_PHASE_INITIALIZED { + panic(fmt.Errorf("cannot find genesis for consumer chain %s in phase %d", consumerId, phase)) + } + gen = *vaastypes.DefaultConsumerGenesisState() } - // initial consumer chain states cs := types.ConsumerState{ ChainId: consumerId, ClientId: clientId, ConsumerGenesis: gen, - Phase: k.GetConsumerPhase(ctx, consumerId), + Phase: phase, PendingValsetChanges: k.GetPendingVSCPackets(ctx, consumerId), } consumerStates = append(consumerStates, cs) @@ -125,7 +128,7 @@ func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState { // ConsumerAddrsToPrune are added only for registered consumer chains consumerAddrsToPrune := []types.ConsumerAddrsToPrune{} - for _, chainID := range launchedConsumerIds { + for _, chainID := range activeConsumerIds { consumerAddrsToPrune = append(consumerAddrsToPrune, k.GetAllConsumerAddrsToPrune(ctx, chainID)...) } diff --git a/x/vaas/provider/keeper/genesis_test.go b/x/vaas/provider/keeper/genesis_test.go new file mode 100644 index 0000000..4b17397 --- /dev/null +++ b/x/vaas/provider/keeper/genesis_test.go @@ -0,0 +1,57 @@ +package keeper_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + testkeeper "github.com/allinbits/vaas/testutil/keeper" + providertypes "github.com/allinbits/vaas/x/vaas/provider/types" +) + +func TestExportGenesis_ConsumerWithoutGenesis(t *testing.T) { + pk, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + pk.SetParams(ctx, providertypes.DefaultParams()) + + consumerId := pk.FetchAndIncrementConsumerId(ctx) + pk.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_REGISTERED) + + genState := pk.ExportGenesis(ctx) + require.NotNil(t, genState) + require.Len(t, genState.ConsumerStates, 1) + require.Equal(t, consumerId, genState.ConsumerStates[0].ChainId) + require.Equal(t, providertypes.CONSUMER_PHASE_REGISTERED, genState.ConsumerStates[0].Phase) + require.Equal(t, "", genState.ConsumerStates[0].ClientId) +} + +func TestExportGenesis_InitializedConsumerWithoutGenesis(t *testing.T) { + pk, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + pk.SetParams(ctx, providertypes.DefaultParams()) + + consumerId := pk.FetchAndIncrementConsumerId(ctx) + pk.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_INITIALIZED) + + genState := pk.ExportGenesis(ctx) + require.NotNil(t, genState) + require.Len(t, genState.ConsumerStates, 1) + require.Equal(t, consumerId, genState.ConsumerStates[0].ChainId) + require.Equal(t, providertypes.CONSUMER_PHASE_INITIALIZED, genState.ConsumerStates[0].Phase) +} + +func TestExportGenesis_LaunchedConsumerWithoutGenesisPanics(t *testing.T) { + pk, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + pk.SetParams(ctx, providertypes.DefaultParams()) + + consumerId := pk.FetchAndIncrementConsumerId(ctx) + pk.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_LAUNCHED) + + require.Panics(t, func() { + pk.ExportGenesis(ctx) + }) +} diff --git a/x/vaas/provider/keeper/keeper.go b/x/vaas/provider/keeper/keeper.go index 67ef46b..deb6fcc 100644 --- a/x/vaas/provider/keeper/keeper.go +++ b/x/vaas/provider/keeper/keeper.go @@ -193,27 +193,6 @@ func (k Keeper) Logger(ctx context.Context) log.Logger { return sdkCtx.Logger().With("module", "x/"+ibchost.ModuleName+"-"+types.ModuleName) } -// GetAllConsumersWithIBCClients returns the ids of all consumer chains that with IBC clients created. -func (k Keeper) GetAllConsumersWithIBCClients(ctx context.Context) []string { - consumerIds := []string{} - - iter, err := k.ConsumerClients.Iterate(ctx, nil) - if err != nil { - return consumerIds - } - defer iter.Close() - - for ; iter.Valid(); iter.Next() { - key, err := iter.Key() - if err != nil { - continue - } - consumerIds = append(consumerIds, key) - } - - return consumerIds -} - func (k Keeper) SetConsumerGenesis(ctx context.Context, consumerId string, gen vaastypes.ConsumerGenesisState) error { return k.ConsumerGenesis.Set(ctx, consumerId, gen) } @@ -412,6 +391,18 @@ func (k Keeper) GetAllActiveConsumerIds(ctx context.Context) []string { return consumerIds } +// GetAllLaunchedConsumerIds returns all consumer ids in the launched phase. +func (k Keeper) GetAllLaunchedConsumerIds(ctx context.Context) []string { + consumerIds := []string{} + for _, consumerId := range k.GetAllConsumerIds(ctx) { + if k.GetConsumerPhase(ctx, consumerId) != types.CONSUMER_PHASE_LAUNCHED { + continue + } + consumerIds = append(consumerIds, consumerId) + } + return consumerIds +} + func (k Keeper) UnbondingCanComplete(ctx sdk.Context, id uint64) error { return k.stakingKeeper.UnbondingCanComplete(ctx, id) } diff --git a/x/vaas/provider/keeper/keeper_test.go b/x/vaas/provider/keeper/keeper_test.go index c902432..1e0785c 100644 --- a/x/vaas/provider/keeper/keeper_test.go +++ b/x/vaas/provider/keeper/keeper_test.go @@ -1,8 +1,6 @@ package keeper_test import ( - "fmt" - "slices" "sort" "testing" @@ -167,26 +165,6 @@ func TestInitHeight(t *testing.T) { } } -func TestGetAllConsumersWithIBCClients(t *testing.T) { - pk, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) - defer ctrl.Finish() - - consumerIds := []string{"2", "1", "4", "3"} - for i, consumerId := range consumerIds { - clientId := fmt.Sprintf("client-%d", len(consumerIds)-i) - pk.SetConsumerClientId(ctx, consumerId, clientId) - pk.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_LAUNCHED) - } - - actualConsumerIds := pk.GetAllConsumersWithIBCClients(ctx) - require.Len(t, actualConsumerIds, len(consumerIds)) - - // sort the consumer ids before comparing they are equal - slices.Sort(consumerIds) - slices.Sort(actualConsumerIds) - require.Equal(t, consumerIds, actualConsumerIds) -} - // TestConsumerClientId tests the getter, setter, and deletion of the client id <> consumer id mappings func TestConsumerClientId(t *testing.T) { providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) diff --git a/x/vaas/provider/keeper/relay.go b/x/vaas/provider/keeper/relay.go index f3f54d9..75c107d 100644 --- a/x/vaas/provider/keeper/relay.go +++ b/x/vaas/provider/keeper/relay.go @@ -127,18 +127,13 @@ func (k Keeper) BlocksUntilNextEpoch(ctx sdk.Context) int64 { } func (k Keeper) SendVSCPackets(ctx sdk.Context) error { - for _, consumerId := range k.GetAllConsumersWithIBCClients(ctx) { - if k.GetConsumerPhase(ctx, consumerId) != providertypes.CONSUMER_PHASE_LAUNCHED { - continue - } - - clientID, found := k.GetConsumerClientId(ctx, consumerId) - if !found { + for _, consumerId := range k.GetAllLaunchedConsumerIds(ctx) { + clientID, _ := k.GetConsumerClientId(ctx, consumerId) + clientID = k.discoverActiveConsumerClient(ctx, consumerId, clientID) + if clientID == "" { continue } - clientID = k.discoverActiveConsumerClient(ctx, consumerId, clientID) - if err := k.SendVSCPacketsToChain(ctx, consumerId, clientID); err != nil { return fmt.Errorf("sending VSCPacket to consumer, consumerId(%s): %w", consumerId, err) } @@ -149,13 +144,15 @@ func (k Keeper) SendVSCPackets(ctx sdk.Context) error { // discoverActiveConsumerClient scans for IBC clients pointing to the consumer chain // and returns the one with the highest latest height that has a counterparty registered. // This allows the provider to use a client being actively updated by a relayer. -// The current client is only replaced if it is expired or frozen. +// The current client is only replaced if it is expired, frozen, or has no counterparty. func (k Keeper) discoverActiveConsumerClient(ctx sdk.Context, consumerId, currentClientID string) string { - currentStatus := k.clientKeeper.GetClientStatus(ctx, currentClientID) - if currentStatus == ibcexported.Active { - cp, found := k.clientV2Keeper.GetClientCounterparty(ctx, currentClientID) - if found && cp.ClientId != "" { - return currentClientID + if currentClientID != "" { + currentStatus := k.clientKeeper.GetClientStatus(ctx, currentClientID) + if currentStatus == ibcexported.Active { + cp, found := k.clientV2Keeper.GetClientCounterparty(ctx, currentClientID) + if found && cp.ClientId != "" { + return currentClientID + } } } @@ -188,10 +185,9 @@ func (k Keeper) discoverActiveConsumerClient(ctx sdk.Context, consumerId, curren }) if bestClient != "" { - k.Logger(ctx).Info("current client not active, switching to best active client", + k.Logger(ctx).Info("switching to discovered active client", "consumerId", consumerId, "oldClient", currentClientID, - "oldStatus", string(currentStatus), "newClient", bestClient, ) k.SetConsumerClientId(ctx, consumerId, bestClient) @@ -276,11 +272,7 @@ func (k Keeper) QueueVSCPackets(ctx sdk.Context) error { return fmt.Errorf("getting bonded validators: %w", err) } - for _, consumerId := range k.GetAllConsumersWithIBCClients(ctx) { - if k.GetConsumerPhase(ctx, consumerId) != providertypes.CONSUMER_PHASE_LAUNCHED { - // only queue VSCPackets to launched chains - continue - } + for _, consumerId := range k.GetAllLaunchedConsumerIds(ctx) { currentValSet, err := k.GetConsumerValSet(ctx, consumerId) if err != nil { @@ -327,7 +319,7 @@ func (k Keeper) EndBlockCIS(ctx sdk.Context) { k.Logger(ctx).Debug("vscID was mapped to block height", "vscID", valUpdateID, "height", blockHeight) // prune previous consumer validator addresses that are no longer needed - for _, consumerId := range k.GetAllConsumersWithIBCClients(ctx) { + for _, consumerId := range k.GetAllLaunchedConsumerIds(ctx) { k.PruneKeyAssignments(ctx, consumerId) } }