Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: enable standalone consumers to reuse existing clients for ICS #2400

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
12 changes: 12 additions & 0 deletions .changelog/unreleased/api-breaking/2400-preccv.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
- Enable existing (standalone) chains to use the existing client (and connection)
to the provider chain when becoming a consumer chain. This feature introduces
the following API-breaking changes.
([\#2400](https://github.com/cosmos/interchain-security/pull/2400))

- Add `connection_id` and `preCCV` to `ConsumerGenesisState`, the consumer
genesis state created by the provider chain. If the `connection_id` is not empty,
`preCCV` is set to true and both `provider.client_state` and `provider.consensus_state`
are set to nil (as the consumer doesn't need to create a new provider client).
As a result, for older versions of consumers, the `connection_id` in
`ConsumerInitializationParameters` must be empty and the resulting `ConsumerGenesisState`
needs to be adapted, i.e., both `connection_id` and `preCCV` need to be removed.
13 changes: 13 additions & 0 deletions .changelog/unreleased/features/2400-preccv.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
- Enable existing (standalone) chains to use the existing client (and connection)
to the provider chain when becoming a consumer chain. This feature introduces
the following changes.
([\#2400](https://github.com/cosmos/interchain-security/pull/2400))

- Add `connection_id` to `ConsumerInitializationParameters`, the ID of
the connection end _on the provider chain_ on top of which the CCV channel will
be established. Consumer chain owners can set `connection_id` to a valid ID in
order to reuse the underlying clients.
- Add `connection_id` to the consumer genesis state, the ID of the connection
end _on the consumer chain_ on top of which the CCV channel will be established.
If `connection_id` is a valid ID, then the consumer chain will use the underlying
client as the provider client and it will initiate the channel handshake.
3 changes: 3 additions & 0 deletions .changelog/unreleased/state-breaking/2400-preccv.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- Enable existing (standalone) chains to use the existing client (and connection)
to the provider chain when becoming a consumer chain.
([\#2400](https://github.com/cosmos/interchain-security/pull/2400))
1 change: 1 addition & 0 deletions app/consumer-democracy/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@ func New(
}
}

// TODO: remove this code
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: is any of the code below needed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's how the changeover is done. The chain needs to read the new validator set from somewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand. Why are we calling InitGenesis. This should be automatically called when adding a new module to the chain.

// For a new consumer chain, this code (together with the entire SetUpgradeHandler) is not needed at all,
// upgrade handler code is application specific. However, as an example, standalone to consumer
// changeover chains should utilize customized upgrade handler code similar to below.
Expand Down
4 changes: 3 additions & 1 deletion docs/docs/build/modules/02-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -1553,6 +1553,7 @@ init_params:
spawn_time: "2024-09-26T06:55:14.616054Z"
transfer_timeout_period: 3600s
unbonding_period: 1209600s
connection_id: ""
metadata:
description: description of your chain and all other relevant information
metadata: some metadata about your chain
Expand Down Expand Up @@ -1708,7 +1709,8 @@ where `update-consumer-msg.json` contains:
"consumer_redistribution_fraction": "0.75",
"blocks_per_distribution_transmission": "1500",
"historical_entries": "1000",
"distribution_transmission_channel": ""
"distribution_transmission_channel": "",
"connection_id": ""
},
"power_shaping_parameters":{
"top_N": 0,
Expand Down
7 changes: 6 additions & 1 deletion proto/interchain_security/ccv/consumer/v1/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,15 @@ message GenesisState {
// LastTransmissionBlockHeight nil on new chain, filled in on restart.
LastTransmissionBlockHeight last_transmission_block_height = 12
[ (gogoproto.nullable) = false ];
// flag indicating whether the consumer CCV module starts in pre-CCV state
// Flag indicating whether the consumer CCV module starts in pre-CCV state
bool preCCV = 13;
interchain_security.ccv.v1.ProviderInfo provider = 14
[ (gogoproto.nullable) = false ];
// The ID of the connection end on the consumer chain on top of which the
// CCV channel will be established. If connection_id == "", a new client of
// the provider chain and a new connection on top of this client are created.
// The new client is initialized using provider.client_state and provider.consensus_state.
string connection_id = 15;
}

// HeightValsetUpdateID represents a mapping internal to the consumer CCV module
Expand Down
10 changes: 8 additions & 2 deletions proto/interchain_security/ccv/provider/v1/provider.proto
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ message ConsumerAdditionProposal {
// sub-protocol. If DistributionTransmissionChannel == "", a new transfer
// channel is created on top of the same connection as the CCV channel.
// Note that transfer_channel_id is the ID of the channel end on the consumer
// chain. it is most relevant for chains performing a sovereign to consumer
// chain. It is most relevant for chains performing a standalone to consumer
// changeover in order to maintain the existing ibc transfer channel
string distribution_transmission_channel = 14;
// Corresponds to the percentage of validators that have to validate the chain under the Top N case.
Expand Down Expand Up @@ -466,9 +466,15 @@ message ConsumerInitializationParameters {
// sub-protocol. If DistributionTransmissionChannel == "", a new transfer
// channel is created on top of the same connection as the CCV channel.
// Note that transfer_channel_id is the ID of the channel end on the consumer
// chain. it is most relevant for chains performing a sovereign to consumer
// chain. It is most relevant for chains performing a standalone to consumer
// changeover in order to maintain the existing ibc transfer channel
string distribution_transmission_channel = 11;
// The ID of the connection end on the provider chain on top of which the CCV
// channel will be established. If connection_id == "", a new client of the
// consumer chain and a new connection on top of this client are created.
// Note that a standalone chain can transition to a consumer chain while
// maintaining existing IBC channels to other chains by providing a valid connection_id.
string connection_id = 12;
}

// PowerShapingParameters contains parameters that shape the validator set that we send to the consumer chain
Expand Down
19 changes: 15 additions & 4 deletions proto/interchain_security/ccv/v1/shared_consumer.proto
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,27 @@ message ConsumerParams {
message ConsumerGenesisState {
ConsumerParams params = 1 [ (gogoproto.nullable) = false ];
ProviderInfo provider = 2 [ (gogoproto.nullable) = false ];
// true for new chain, false for chain restart.
bool new_chain = 3; // TODO:Check if this is really needed
// True for new chain, false for chain restart.
// This is needed and always set to true; otherwise, new_chain in the consumer
// genesis state will default to false
bool new_chain = 3;
// Flag indicating whether the consumer CCV module starts in pre-CCV state
bool preCCV = 4;
Copy link
Contributor

@MSalopek MSalopek Nov 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little problematic to understand why there's new_chain and preCCV, but I get it.

Not: Maybe consider uniform snakecase/camelcase usage. (I don't have strong feelings, feel free to ignore).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

preCCV is already a field in the consumer genesis, so I cannot change the name without breaking compatibility.

// The ID of the connection end on the consumer chain on top of which the
// CCV channel will be established. If connection_id == "", a new client of
// the provider chain and a new connection on top of this client are created.
// The new client is initialized using client_state and consensus_state.
string connection_id = 5;
}

// ProviderInfo defines all information a consumer needs from a provider
// Shared data type between provider and consumer
message ProviderInfo {
// ProviderClientState filled in on new chain, nil on restart.
// The client state for the provider client filled in on new chain, nil on restart.
// If connection_id != "", then client_state is ignored.
ibc.lightclients.tendermint.v1.ClientState client_state = 1;
// ProviderConsensusState filled in on new chain, nil on restart.
// The consensus state for the provider client filled in on new chain, nil on restart.
// If connection_id != "", then consensus_state is ignored.
ibc.lightclients.tendermint.v1.ConsensusState consensus_state = 2;
// InitialValset filled in on new chain and on restart.
repeated .tendermint.abci.ValidatorUpdate initial_val_set = 3
Expand Down
53 changes: 48 additions & 5 deletions x/ccv/consumer/keeper/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package keeper
import (
"fmt"

errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
conntypes "github.com/cosmos/ibc-go/v8/modules/core/03-connection/types"
channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types"

abci "github.com/cometbft/cometbft/abci/types"

Expand Down Expand Up @@ -55,11 +58,31 @@ func (k Keeper) InitGenesis(ctx sdk.Context, state *types.GenesisState) []abci.V
// initialValSet is checked in NewChain case by ValidateGenesis
// start a new chain
if state.NewChain {
// create the provider client in InitGenesis for new consumer chain. CCV Handshake must be established with this client id.
clientID, err := k.clientKeeper.CreateClient(ctx, state.Provider.ClientState, state.Provider.ConsensusState)
if err != nil {
// If the client creation fails, the chain MUST NOT start
panic(err)
var clientID string
if state.ConnectionId == "" {
// create the provider client in InitGenesis for new consumer chain. CCV Handshake must be established with this client id.
cid, err := k.clientKeeper.CreateClient(ctx, state.Provider.ClientState, state.Provider.ConsensusState)
if err != nil {
// If the client creation fails, the chain MUST NOT start
panic(err)
}
clientID = cid

k.Logger(ctx).Info("create new provider chain client",
"client id", clientID,
)
} else {
// if connection id is provided, then the client is already created
connectionEnd, found := k.connectionKeeper.GetConnection(ctx, state.ConnectionId)
if !found {
panic(errorsmod.Wrapf(conntypes.ErrConnectionNotFound, "could not find connection(%s)", state.ConnectionId))
}
clientID = connectionEnd.ClientId

k.Logger(ctx).Info("use existing client and connection to provider chain",
"client id", clientID,
"connection id", state.ConnectionId,
)
}

// set provider client id.
Expand All @@ -68,6 +91,26 @@ func (k Keeper) InitGenesis(ctx sdk.Context, state *types.GenesisState) []abci.V
// set default value for valset update ID
k.SetHeightValsetUpdateID(ctx, uint64(ctx.BlockHeight()), uint64(0))

if state.ConnectionId != "" {
// initiate CCV channel handshake
ccvChannelOpenInitMsg := channeltypes.NewMsgChannelOpenInit(
ccv.ConsumerPortID,
ccv.Version,
channeltypes.ORDERED,
[]string{state.ConnectionId},
ccv.ProviderPortID,
"", // signer unused
)
_, err := k.ChannelOpenInit(ctx, ccvChannelOpenInitMsg)
if err != nil {
panic(err)
}

// Note that if the connection ID is not provider, we cannot initiate
// the connection handshake as the counterparty client ID is unknown
// at this point. The connection handshake must be initiated by a relayer.
}

} else {
// chain restarts with the CCV channel established
if state.ProviderChannelId != "" {
Expand Down
57 changes: 41 additions & 16 deletions x/ccv/consumer/types/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,13 @@ func NewInitialGenesisState(cs *ibctmtypes.ClientState, consState *ibctmtypes.Co
// expect the following optional and mandatory genesis states:
//
// 1. New chain starts:
// - Params, InitialValset, provider client state, provider consensus state // mandatory
// - Params, InitialValset // mandatory
//
// 1a. ConnectionId is empty
// - provider client state, provider consensus state // mandatory
//
// 1b. ConnectionId is not empty
// - provider client state, provider consensus state // nil
//
// 2. Chain restarts with CCV handshake still in progress:
// - Params, InitialValset, ProviderID, HeightToValidatorSetUpdateID // mandatory
Expand All @@ -73,8 +79,6 @@ func NewInitialGenesisState(cs *ibctmtypes.ClientState, consState *ibctmtypes.Co
// 3. Chain restarts with CCV handshake completed:
// - Params, InitialValset, ProviderID, channelID, HeightToValidatorSetUpdateID // mandatory
// - MaturingVSCPackets, OutstandingDowntime, PendingConsumerPacket, LastTransmissionBlockHeight // optional
//

func (gs GenesisState) Validate() error {
if !gs.Params.Enabled {
return nil
Expand All @@ -87,24 +91,45 @@ func (gs GenesisState) Validate() error {
}

if gs.NewChain {
if gs.Provider.ClientState == nil {
return errorsmod.Wrap(ccv.ErrInvalidGenesis, "provider client state cannot be nil for new chain")
}
if err := gs.Provider.ClientState.Validate(); err != nil {
return errorsmod.Wrapf(ccv.ErrInvalidGenesis, "provider client state invalid for new chain %s", err.Error())
}
if gs.Provider.ConsensusState == nil {
return errorsmod.Wrap(ccv.ErrInvalidGenesis, "provider consensus state cannot be nil for new chain")
}
if err := gs.Provider.ConsensusState.ValidateBasic(); err != nil {
return errorsmod.Wrapf(ccv.ErrInvalidGenesis, "provider consensus state invalid for new chain %s", err.Error())
if gs.ConnectionId == "" {
// connection ID is not provided
if gs.Provider.ClientState == nil {
return errorsmod.Wrap(ccv.ErrInvalidGenesis, "provider client state cannot be nil for new chain")
}
if err := gs.Provider.ClientState.Validate(); err != nil {
return errorsmod.Wrapf(ccv.ErrInvalidGenesis, "provider client state invalid for new chain %s", err.Error())
}
if gs.Provider.ConsensusState == nil {
return errorsmod.Wrap(ccv.ErrInvalidGenesis, "provider consensus state cannot be nil for new chain")
}
if err := gs.Provider.ConsensusState.ValidateBasic(); err != nil {
return errorsmod.Wrapf(ccv.ErrInvalidGenesis, "provider consensus state invalid for new chain %s", err.Error())
}
} else {
// connection ID is provided
if gs.Provider.ClientState != nil {
return errorsmod.Wrap(ccv.ErrInvalidGenesis, "provider client state must be nil when connection id is provided")
}
if gs.Provider.ConsensusState != nil {
return errorsmod.Wrap(ccv.ErrInvalidGenesis, "provider consensus state must be nil when connection id is provided")
}
if err := ccv.ValidateConnectionIdentifier(gs.ConnectionId); err != nil {
return errorsmod.Wrapf(ccv.ErrInvalidGenesis, "invalid connection id: %s", err.Error())
}
}

if gs.ProviderClientId != "" {
return errorsmod.Wrap(ccv.ErrInvalidGenesis, "provider client id cannot be set for new chain. It must be established on handshake")
}
if gs.ProviderChannelId != "" {
return errorsmod.Wrap(ccv.ErrInvalidGenesis, "provider channel id cannot be set for new chain. It must be established on handshake")
}
if len(gs.HeightToValsetUpdateId) != 0 {
return errorsmod.Wrap(ccv.ErrInvalidGenesis, "HeightToValsetUpdateId must be nil for new chain")
}
if len(gs.OutstandingDowntimeSlashing) != 0 {
return errorsmod.Wrap(ccv.ErrInvalidGenesis, "OutstandingDowntimeSlashing must be nil for new chain")
}
if len(gs.PendingConsumerPackets.List) != 0 {
return errorsmod.Wrap(ccv.ErrInvalidGenesis, "pending consumer packets must be empty for new chain")
}
Expand All @@ -121,11 +146,11 @@ func (gs GenesisState) Validate() error {
if handshakeInProgress {
if len(gs.OutstandingDowntimeSlashing) != 0 {
return errorsmod.Wrap(
ccv.ErrInvalidGenesis, "outstanding downtime must be empty when handshake isn't completed")
ccv.ErrInvalidGenesis, "outstanding downtime must be empty when handshake in progress")
}
if gs.LastTransmissionBlockHeight.Height != 0 {
return errorsmod.Wrap(
ccv.ErrInvalidGenesis, "last transmission block height must be zero when handshake isn't completed")
ccv.ErrInvalidGenesis, "last transmission block height must be zero when handshake in progress")
}
if len(gs.PendingConsumerPackets.List) != 0 {
for _, packet := range gs.PendingConsumerPackets.List {
Expand Down
Loading
Loading