Skip to content

Commit 1ae670b

Browse files
committed
feat: adding base implementation with unit and integration tests
1 parent 33dbf16 commit 1ae670b

File tree

13 files changed

+980
-332
lines changed

13 files changed

+980
-332
lines changed

proto/interchain_security/ccv/consumer/v1/genesis.proto

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ message GenesisState {
5454
bool preCCV = 13;
5555
interchain_security.ccv.v1.ProviderInfo provider = 14
5656
[ (gogoproto.nullable) = false ];
57+
// The ID of the connection end on the consumer chain on top of which the CCV channel will be established
58+
string connection_id = 15;
5759
}
5860

5961
// HeightValsetUpdateID represents a mapping internal to the consumer CCV module

proto/interchain_security/ccv/provider/v1/provider.proto

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,15 @@ message ConsumerAdditionProposal {
114114
// Corresponds to a list of provider consensus addresses of validators that
115115
// CANNOT validate the consumer chain.
116116
repeated string denylist = 19;
117+
// ICS1_DEVIATION: v6.4.0 structure adaptation
118+
// Upstream v6.4.0 moved connection_id to ConsumerInitializationParameters message.
119+
// Since we're based on v5.2.0 which uses ConsumerAdditionProposal for all init params,
120+
// we add connection_id here to maintain the same functionality.
121+
//
122+
// The ID of the connection end on the provider chain on top of which the CCV channel will be established.
123+
// If connection_id is empty, a new client will be created and a new connection on top of this client will be
124+
// established for the consumer chain.
125+
string connection_id = 20;
117126
}
118127

119128
// ConsumerRemovalProposal is a governance proposal on the provider chain to

proto/interchain_security/ccv/provider/v1/tx.proto

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,15 @@ message MsgConsumerAddition {
191191
repeated string denylist = 17;
192192
// signer address
193193
string authority = 18 [ (cosmos_proto.scalar) = "cosmos.AddressString" ];
194+
// ICS1_DEVIATION: v6.4.0 structure adaptation
195+
// Upstream v6.4.0 moved connection_id to ConsumerInitializationParameters message.
196+
// Since we're based on v5.2.0 which uses MsgConsumerAddition for all init params,
197+
// we add connection_id here to maintain the same functionality.
198+
//
199+
// The ID of the connection end on the provider chain on top of which the CCV channel will be established.
200+
// If connection_id is empty, a new client will be created and a new connection on top of this client will be
201+
// established for the consumer chain.
202+
string connection_id = 19;
194203
}
195204

196205
// MsgConsumerAdditionResponse defines response type for MsgConsumerAddition

proto/interchain_security/ccv/v1/shared_consumer.proto

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ message ConsumerGenesisState {
8585
ProviderInfo provider = 2 [ (gogoproto.nullable) = false ];
8686
// true for new chain, false for chain restart.
8787
bool new_chain = 3; // TODO:Check if this is really needed
88+
// Flag indicating whether the consumer CCV module starts in pre-CCV state
89+
bool preCCV = 4;
90+
// The ID of the connection end on the consumer chain on top of which the CCV channel will be established
91+
string connection_id = 5;
8892
}
8993

9094
// ProviderInfo defines all information a consumer needs from a provider

tests/integration/changeover.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ package integration
33
import (
44
transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types"
55
channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types"
6+
7+
consumertypes "github.com/cosmos/interchain-security/v5/x/ccv/consumer/types"
8+
providertypes "github.com/cosmos/interchain-security/v5/x/ccv/provider/types"
9+
ccv "github.com/cosmos/interchain-security/v5/x/ccv/types"
610
)
711

812
func (suite *CCVTestSuite) TestRecycleTransferChannel() {
@@ -53,3 +57,129 @@ func (suite *CCVTestSuite) TestRecycleTransferChannel() {
5357
channels := suite.consumerApp.GetIBCKeeper().ChannelKeeper.GetAllChannels(suite.consumerCtx())
5458
suite.Require().Len(channels, 2)
5559
}
60+
61+
// TestChangeoverWithConnectionReuse tests the standalone-to-consumer changeover
62+
// when reusing an existing IBC connection (ICS1 feature).
63+
// This validates that a consumer chain can reuse an existing connection during changeover.
64+
func (suite *CCVTestSuite) TestChangeoverWithConnectionReuse() {
65+
// Step 1: Create connection between consumer and provider (simulating existing connection)
66+
suite.coordinator.CreateConnections(suite.path)
67+
68+
consumerKeeper := suite.consumerApp.GetConsumerKeeper()
69+
providerKeeper := suite.providerApp.GetProviderKeeper()
70+
71+
// Get the connection IDs from the created connection
72+
providerConnectionID := suite.path.EndpointA.ConnectionID
73+
consumerConnectionID := suite.path.EndpointB.ConnectionID
74+
75+
suite.Require().NotEmpty(providerConnectionID, "provider connection ID should be set")
76+
suite.Require().NotEmpty(consumerConnectionID, "consumer connection ID should be set")
77+
78+
// Commit blocks on provider chain to ensure historical info is saved
79+
// MakeConsumerGenesis needs historical info at the current height
80+
suite.coordinator.CommitBlock(suite.providerChain)
81+
82+
// Step 2: Create a consumer addition proposal with connection_id set
83+
prop := providertypes.ConsumerAdditionProposal{
84+
ChainId: suite.consumerChain.ChainID,
85+
UnbondingPeriod: ccv.DefaultConsumerUnbondingPeriod,
86+
CcvTimeoutPeriod: ccv.DefaultCCVTimeoutPeriod,
87+
TransferTimeoutPeriod: ccv.DefaultTransferTimeoutPeriod,
88+
ConsumerRedistributionFraction: "0.75",
89+
BlocksPerDistributionTransmission: 1000,
90+
HistoricalEntries: 10000,
91+
ConnectionId: providerConnectionID, // ICS1: Reuse existing connection
92+
}
93+
94+
// Step 3: Generate consumer genesis with connection reuse
95+
consumerGenesis, _, err := providerKeeper.MakeConsumerGenesis(suite.providerCtx(), &prop)
96+
suite.Require().NoError(err, "MakeConsumerGenesis should succeed")
97+
98+
// Step 4: Verify the generated genesis has connection reuse fields set correctly
99+
suite.Require().Equal(consumerConnectionID, consumerGenesis.ConnectionId,
100+
"consumer genesis should have connection_id set to consumer-side connection")
101+
suite.Require().True(consumerGenesis.PreCCV,
102+
"consumer genesis should have preCCV=true for connection reuse")
103+
suite.Require().Nil(consumerGenesis.Provider.ClientState,
104+
"client_state should be nil when reusing connection")
105+
suite.Require().Nil(consumerGenesis.Provider.ConsensusState,
106+
"consensus_state should be nil when reusing connection")
107+
108+
// For integration testing purposes, override preCCV to false since our test consumer
109+
// is not a real standalone chain with a standalone staking keeper.
110+
// In a real changeover scenario, preCCV would be true and the consumer would have
111+
// a standalone staking keeper. This test focuses on verifying the connection reuse
112+
// mechanism (provider genesis generation and consumer initialization with existing client).
113+
consumerGenesis.PreCCV = false
114+
115+
// Step 5: Get the existing client ID from the connection (before InitGenesis)
116+
consumerConn, found := suite.consumerApp.GetIBCKeeper().ConnectionKeeper.GetConnection(
117+
suite.consumerCtx(), consumerConnectionID,
118+
)
119+
suite.Require().True(found, "consumer connection should exist")
120+
existingClientID := consumerConn.ClientId
121+
122+
// Step 6: Initialize consumer with the generated genesis (with connection reuse)
123+
// Construct consumer GenesisState from the shared ConsumerGenesisState
124+
consumerGenesisState := &consumertypes.GenesisState{
125+
Params: consumerGenesis.Params,
126+
NewChain: consumerGenesis.NewChain,
127+
Provider: consumerGenesis.Provider,
128+
PreCCV: consumerGenesis.PreCCV,
129+
ConnectionId: consumerGenesis.ConnectionId,
130+
}
131+
consumerKeeper.InitGenesis(suite.consumerCtx(), consumerGenesisState)
132+
133+
// Step 7: Verify consumer initialized correctly using existing connection's client
134+
providerClientID, found := consumerKeeper.GetProviderClientID(suite.consumerCtx())
135+
suite.Require().True(found, "provider client ID should be set")
136+
suite.Require().Equal(existingClientID, providerClientID,
137+
"consumer should use existing client from connection")
138+
139+
// Step 8: Complete CCV channel handshake on top of existing connection
140+
suite.ExecuteCCVChannelHandshake(suite.path)
141+
142+
// Step 9: Verify CCV channel was established on the existing connection
143+
// Note: In a real scenario, the provider channel ID would be set when the first VSC packet
144+
// is received. For this test, we verify the channel exists and manually set it since
145+
// we're testing connection reuse, not the full packet relay flow.
146+
channels := suite.consumerApp.GetIBCKeeper().ChannelKeeper.GetAllChannels(suite.consumerCtx())
147+
var ccvChannelID string
148+
for _, ch := range channels {
149+
if ch.PortId == ccv.ConsumerPortID && ch.State == channeltypes.OPEN {
150+
ccvChannelID = ch.ChannelId
151+
break
152+
}
153+
}
154+
suite.Require().NotEmpty(ccvChannelID, "CCV channel should exist")
155+
156+
// Set the provider channel manually for testing purposes
157+
consumerKeeper.SetProviderChannel(suite.consumerCtx(), ccvChannelID)
158+
159+
// Verify we can now get the provider channel
160+
retrievedChannelID, found := consumerKeeper.GetProviderChannel(suite.consumerCtx())
161+
suite.Require().True(found, "provider channel should be set")
162+
suite.Require().Equal(ccvChannelID, retrievedChannelID, "provider channel ID should match")
163+
164+
ccvChannel, found := suite.consumerApp.GetIBCKeeper().ChannelKeeper.GetChannel(
165+
suite.consumerCtx(), ccv.ConsumerPortID, ccvChannelID,
166+
)
167+
suite.Require().True(found, "CCV channel should exist")
168+
suite.Require().Equal(consumerConnectionID, ccvChannel.ConnectionHops[0],
169+
"CCV channel should be on the existing connection")
170+
171+
// Step 10: Verify the connection is shared between CCV and any other channels
172+
allChannels := suite.consumerApp.GetIBCKeeper().ChannelKeeper.GetAllChannels(suite.consumerCtx())
173+
174+
// Count how many channels use the existing connection
175+
channelsOnConnection := 0
176+
for _, ch := range allChannels {
177+
if len(ch.ConnectionHops) > 0 && ch.ConnectionHops[0] == consumerConnectionID {
178+
channelsOnConnection++
179+
}
180+
}
181+
182+
// At minimum, the CCV channel should be on the existing connection
183+
suite.Require().GreaterOrEqual(channelsOnConnection, 1,
184+
"at least CCV channel should use the existing connection")
185+
}

x/ccv/consumer/keeper/genesis.go

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,29 @@ func (k Keeper) InitGenesis(ctx sdk.Context, state *types.GenesisState) []abci.V
4545
// initialValSet is checked in NewChain case by ValidateGenesis
4646
// start a new chain
4747
if state.NewChain {
48-
// create the provider client in InitGenesis for new consumer chain. CCV Handshake must be established with this client id.
49-
// IBC v10: CreateClient now requires clientType string and marshaled states
50-
clientStateBz := k.cdc.MustMarshal(state.Provider.ClientState)
51-
consensusStateBz := k.cdc.MustMarshal(state.Provider.ConsensusState)
52-
clientID, err := k.clientKeeper.CreateClient(ctx, ibcexported.Tendermint, clientStateBz, consensusStateBz)
53-
if err != nil {
54-
// If the client creation fails, the chain MUST NOT start
55-
panic(err)
48+
// ICS1_DEVIATION: Connection reuse logic
49+
// Upstream v6.4.0 has this logic but in a different code structure.
50+
// When connection_id is set, we reuse the existing client instead of creating a new one.
51+
52+
var clientID string
53+
if state.ConnectionId != "" {
54+
// Reuse existing client from the connection
55+
connection, found := k.connectionKeeper.GetConnection(ctx, state.ConnectionId)
56+
if !found {
57+
panic("connection " + state.ConnectionId + " not found during consumer genesis initialization")
58+
}
59+
clientID = connection.ClientId
60+
} else {
61+
// create the provider client in InitGenesis for new consumer chain. CCV Handshake must be established with this client id.
62+
// IBC v10: CreateClient now requires clientType string and marshaled states
63+
clientStateBz := k.cdc.MustMarshal(state.Provider.ClientState)
64+
consensusStateBz := k.cdc.MustMarshal(state.Provider.ConsensusState)
65+
var err error
66+
clientID, err = k.clientKeeper.CreateClient(ctx, ibcexported.Tendermint, clientStateBz, consensusStateBz)
67+
if err != nil {
68+
// If the client creation fails, the chain MUST NOT start
69+
panic(err)
70+
}
5671
}
5772

5873
// set provider client id.

x/ccv/consumer/keeper/genesis_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"time"
66

77
clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types"
8+
conntypes "github.com/cosmos/ibc-go/v10/modules/core/03-connection/types"
89
commitmenttypes "github.com/cosmos/ibc-go/v10/modules/core/23-commitment/types"
910
ibctmtypes "github.com/cosmos/ibc-go/v10/modules/light-clients/07-tendermint"
1011
"github.com/golang/mock/gomock"
@@ -205,6 +206,47 @@ func TestInitGenesis(t *testing.T) {
205206
require.Equal(t, gs.Params, ck.GetConsumerParams(ctx))
206207
},
207208
},
209+
{
210+
"start a new chain with connection reuse",
211+
func(ctx sdk.Context, mocks testkeeper.MockedKeepers) {
212+
// ICS1_DEVIATION: Test connection reuse path
213+
// Mock the connection keeper to return an existing connection
214+
connectionID := "connection-0"
215+
clientID := "07-tendermint-existing"
216+
gomock.InOrder(
217+
mocks.MockConnectionKeeper.EXPECT().GetConnection(ctx, connectionID).Return(
218+
conntypes.ConnectionEnd{
219+
ClientId: clientID,
220+
},
221+
true, // connection found
222+
).Times(1),
223+
)
224+
},
225+
func() *consumertypes.GenesisState {
226+
// Create genesis with connection_id set for reuse
227+
gs := consumertypes.NewInitialGenesisState(
228+
provClientState,
229+
provConsState,
230+
valset,
231+
params,
232+
)
233+
gs.ConnectionId = "connection-0" // Set connection_id for reuse
234+
return gs
235+
}(),
236+
func(ctx sdk.Context, ck consumerkeeper.Keeper, gs *consumertypes.GenesisState) {
237+
assertConsumerPortIsBound(t, ctx, &ck)
238+
239+
// Verify the provider client ID was set from the existing connection
240+
providerClientID, ok := ck.GetProviderClientID(ctx)
241+
require.True(t, ok, "provider client ID should be set")
242+
require.Equal(t, "07-tendermint-existing", providerClientID, "should use existing client from connection")
243+
244+
assertHeightValsetUpdateIDs(t, ctx, &ck, defaultHeightValsetUpdateIDs)
245+
246+
require.Equal(t, validator.Address.Bytes(), ck.GetAllCCValidator(ctx)[0].Address)
247+
require.Equal(t, gs.Params, ck.GetConsumerParams(ctx))
248+
},
249+
},
208250
}
209251

210252
for _, tc := range testCases {

0 commit comments

Comments
 (0)