Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions x/vaas/provider/keeper/consumer_equivocation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
}
Expand Down
135 changes: 8 additions & 127 deletions x/vaas/provider/keeper/consumer_lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand All @@ -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)

Expand All @@ -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(
Expand Down
23 changes: 13 additions & 10 deletions x/vaas/provider/keeper/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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,
Expand Down Expand Up @@ -98,34 +101,34 @@ 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)
}

// 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)...)
}

Expand Down
57 changes: 57 additions & 0 deletions x/vaas/provider/keeper/genesis_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
33 changes: 12 additions & 21 deletions x/vaas/provider/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down
22 changes: 0 additions & 22 deletions x/vaas/provider/keeper/keeper_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package keeper_test

import (
"fmt"
"slices"
"sort"
"testing"

Expand Down Expand Up @@ -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))
Expand Down
Loading
Loading