Skip to content

Commit b13a7a6

Browse files
committed
feat(provider): add support for updating consumer chain-id prelaunch
- Add NewChainId field to MsgConsumerModification - Implement renameConsumerChain to migrate all chain-scoped state - Extend HandleConsumerModificationProposal with prelaunch rename logic - Add comprehensive tests covering all control-flow paths - Update CLI and tx.proto accordingly
1 parent e614a55 commit b13a7a6

File tree

7 files changed

+641
-118
lines changed

7 files changed

+641
-118
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Allow the chain id of a consumer chain to be updated before the chain
2+
launches. ([\#2378](https://github.com/cosmos/interchain-security/pull/2378))

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,16 @@ message MsgConsumerModification {
320320
repeated string denylist = 8;
321321
// signer address
322322
string authority = 9 [ (cosmos_proto.scalar) = "cosmos.AddressString" ];
323+
// (optional) If the consumer chain has NOT yet launched, the chain id can be updated.
324+
// After a chain has launched the chain id CANNOT be updated.
325+
//
326+
// This field is optional and can remain empty (i.e., `new_chain_id = ""`) or
327+
// correspond to the chain id the chain already has. Renaming is only allowed
328+
// in the chain’s pre-launch phase (registered/initialized in v6 terms). In this
329+
// v5 fork we treat “pre-launch” as “no IBC client exists yet for the current chain id”.
330+
//
331+
// Example use case: correcting a typo before launch.
332+
string new_chain_id = 10;
323333
}
324334

325335
message MsgConsumerModificationResponse {}

x/ccv/provider/client/cli/tx.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func GetTxCmd() *cobra.Command {
3434
cmd.AddCommand(NewAssignConsumerKeyCmd())
3535
cmd.AddCommand(NewSubmitConsumerMisbehaviourCmd())
3636
cmd.AddCommand(NewSubmitConsumerDoubleVotingCmd())
37+
cmd.AddCommand(NewConsumerModificationCmd())
3738

3839
return cmd
3940
}
@@ -203,3 +204,96 @@ Example:
203204

204205
return cmd
205206
}
207+
208+
func NewConsumerModificationCmd() *cobra.Command {
209+
cmd := &cobra.Command{
210+
Use: "consumer-modification [path/to/modification.json]",
211+
Short: "submit a governance consumer-modification (supports pre-launch rename via new_chain_id)",
212+
Long: strings.TrimSpace(fmt.Sprintf(`
213+
Submit a governance proposal-like message to modify a consumer chain (TopN, caps, allow/deny lists),
214+
and optionally rename its chain-id *before launch* using "new_chain_id".
215+
216+
Example:
217+
%s tx provider consumer-modification ./mod.json --from <key> --chain-id <CID>
218+
219+
mod.json schema (all fields optional except authority, chain_id):
220+
{
221+
"title": "Update consumer params",
222+
"description": "Adjust caps; optionally rename pre-launch",
223+
"chain_id": "consumer-1",
224+
"top_N": 53,
225+
"validators_power_cap": 32,
226+
"validator_set_cap": 0,
227+
"allowlist": ["cosmosvalcons1..."],
228+
"denylist": ["cosmosvalcons1..."],
229+
"authority": "cosmos1govacct...", // governance authority address (required)
230+
"new_chain_id": "consumer-mainnet" // optional; ignored after launch
231+
}
232+
`, version.AppName)),
233+
Args: cobra.ExactArgs(1),
234+
RunE: func(cmd *cobra.Command, args []string) error {
235+
clientCtx, err := client.GetClientTxContext(cmd)
236+
if err != nil {
237+
return err
238+
}
239+
240+
txf, err := tx.NewFactoryCLI(clientCtx, cmd.Flags())
241+
if err != nil {
242+
return err
243+
}
244+
txf = txf.WithTxConfig(clientCtx.TxConfig).WithAccountRetriever(clientCtx.AccountRetriever)
245+
246+
// Read user JSON
247+
raw, err := os.ReadFile(args[0])
248+
if err != nil {
249+
return err
250+
}
251+
252+
var in struct {
253+
Title string `json:"title"`
254+
Description string `json:"description"`
255+
ChainID string `json:"chain_id"`
256+
Top_N uint32 `json:"top_N"`
257+
ValidatorsPowerCap uint32 `json:"validators_power_cap"`
258+
ValidatorSetCap uint32 `json:"validator_set_cap"`
259+
Allowlist []string `json:"allowlist"`
260+
Denylist []string `json:"denylist"`
261+
Authority string `json:"authority"`
262+
NewChainID string `json:"new_chain_id"`
263+
}
264+
if err := json.Unmarshal(raw, &in); err != nil {
265+
return fmt.Errorf("modification data unmarshalling failed: %w", err)
266+
}
267+
268+
// Basic checks that mirror your proto expectations
269+
if strings.TrimSpace(in.ChainID) == "" {
270+
return fmt.Errorf("chain_id cannot be empty")
271+
}
272+
if strings.TrimSpace(in.Authority) == "" {
273+
return fmt.Errorf("authority cannot be empty")
274+
}
275+
276+
msg := &types.MsgConsumerModification{
277+
Title: in.Title,
278+
Description: in.Description,
279+
ChainId: in.ChainID,
280+
Top_N: in.Top_N,
281+
ValidatorsPowerCap: in.ValidatorsPowerCap,
282+
ValidatorSetCap: in.ValidatorSetCap,
283+
Allowlist: in.Allowlist,
284+
Denylist: in.Denylist,
285+
Authority: in.Authority,
286+
NewChainId: in.NewChainID, // <-- your new field
287+
}
288+
289+
if err := msg.ValidateBasic(); err != nil {
290+
return err
291+
}
292+
return tx.GenerateOrBroadcastTxWithFactory(clientCtx, txf, msg)
293+
},
294+
}
295+
296+
flags.AddTxFlagsToCmd(cmd)
297+
_ = cmd.MarkFlagRequired(flags.FlagFrom)
298+
return cmd
299+
}

x/ccv/provider/keeper/proposal.go

Lines changed: 122 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package keeper
22

33
import (
44
"fmt"
5+
"strings"
56
"time"
67

78
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
@@ -72,20 +73,137 @@ func (k Keeper) HandleConsumerRewardDenomProposal(ctx sdk.Context, proposal *typ
7273
return k.HandleLegacyConsumerRewardDenomProposal(ctx, &p)
7374
}
7475

75-
// HandleConsumerModificationProposal modifies a running consumer chain
76+
// HandleConsumerModificationProposal modifies a consumer chain.
77+
// If proposal.NewChainId is present and different, we validate it and (only if pre-launch)
78+
// rename the chain-id by migrating all chain-scoped keys. Then we hand off to the legacy path.
7679
func (k Keeper) HandleConsumerModificationProposal(ctx sdk.Context, proposal *types.MsgConsumerModification) error {
77-
p := types.ConsumerModificationProposal{
80+
chainID := proposal.ChainId
81+
82+
// Optional pre-launch rename
83+
if s := strings.TrimSpace(proposal.NewChainId); s != "" && s != chainID {
84+
if err := types.ValidateChainId("NewChainId", s); err != nil {
85+
return errorsmod.Wrapf(types.ErrInvalidConsumerModificationProposal,
86+
"invalid new chain id: %s", err.Error())
87+
}
88+
89+
// launched == has client-id
90+
if _, launched := k.GetConsumerClientId(ctx, chainID); launched {
91+
return errorsmod.Wrapf(types.ErrInvalidConsumerModificationProposal,
92+
"cannot update chain id of a launched chain: %s", chainID)
93+
}
94+
95+
// ensure target not taken (taken == has client-id)
96+
if _, taken := k.GetConsumerClientId(ctx, s); taken {
97+
return errorsmod.Wrapf(types.ErrInvalidConsumerModificationProposal,
98+
"target chain id already exists: %s", s)
99+
}
100+
101+
// Move any chain-scoped state you’re tracking prelaunch
102+
if err := k.renameConsumerChain(ctx, chainID, s); err != nil {
103+
return err
104+
}
105+
106+
ctx.EventManager().EmitEvent(
107+
sdk.NewEvent("consumer_chain_renamed",
108+
sdk.NewAttribute("old_chain_id", chainID),
109+
sdk.NewAttribute("new_chain_id", s),
110+
),
111+
)
112+
113+
chainID = s
114+
}
115+
116+
// Only call legacy path if the chain is actually running (has client-id).
117+
if _, running := k.GetConsumerClientId(ctx, chainID); !running {
118+
return nil
119+
}
120+
121+
legacy := types.ConsumerModificationProposal{
78122
Title: proposal.Title,
79123
Description: proposal.Description,
80-
ChainId: proposal.ChainId,
124+
ChainId: chainID,
81125
Top_N: proposal.Top_N,
82126
ValidatorsPowerCap: proposal.ValidatorsPowerCap,
83127
ValidatorSetCap: proposal.ValidatorSetCap,
84128
Allowlist: proposal.Allowlist,
85129
Denylist: proposal.Denylist,
86130
}
131+
return k.HandleLegacyConsumerModificationProposal(ctx, &legacy)
132+
}
133+
134+
// renameConsumerChain moves all chain-scoped state from oldID -> newID.
135+
// It handles exact keys (singletons) and prefixed collections that include the chain-id in the key.
136+
func (k Keeper) renameConsumerChain(ctx sdk.Context, oldID, newID string) error {
137+
kv := ctx.KVStore(k.storeKey)
138+
139+
// --- basic helpers ---
140+
141+
// copy+delete for a single exact key
142+
moveIf := func(oldKey, newKey []byte) {
143+
if bz := kv.Get(oldKey); bz != nil {
144+
kv.Set(newKey, bz)
145+
kv.Delete(oldKey)
146+
}
147+
}
87148

88-
return k.HandleLegacyConsumerModificationProposal(ctx, &p)
149+
// copy+delete for all entries under prefix(oldID)+suffix -> prefix(newID)+suffix
150+
migratePrefix := func(prefixFor func(string) []byte) {
151+
oldPref := prefixFor(oldID)
152+
it := storetypes.KVStorePrefixIterator(kv, oldPref)
153+
defer it.Close()
154+
for ; it.Valid(); it.Next() {
155+
key := it.Key()
156+
val := it.Value()
157+
suffix := key[len(oldPref):]
158+
newKey := append(prefixFor(newID), suffix...)
159+
kv.Set(newKey, val)
160+
kv.Delete(key)
161+
}
162+
}
163+
164+
// convenience for prefixes created via ChainIdWithLenKey(prefixByte, chainID)
165+
migrateByPrefixByte := func(b byte) {
166+
migratePrefix(func(id string) []byte { return types.ChainIdWithLenKey(b, id) })
167+
}
168+
169+
// --- singletons keyed by chain-id ---
170+
moveIf(types.ChainToClientKey(oldID), types.ChainToClientKey(newID))
171+
if ch := kv.Get(types.ChainToChannelKey(oldID)); ch != nil {
172+
// forward map: chainID -> channelID
173+
moveIf(types.ChainToChannelKey(oldID), types.ChainToChannelKey(newID))
174+
// reverse map: channelID -> chainID (rewrite the VALUE to newID)
175+
kv.Set(types.ChannelToChainKey(string(ch)), []byte(newID))
176+
}
177+
moveIf(types.ConsumerGenesisKey(oldID), types.ConsumerGenesisKey(newID))
178+
moveIf(types.SlashAcksKey(oldID), types.SlashAcksKey(newID))
179+
moveIf(types.InitChainHeightKey(oldID), types.InitChainHeightKey(newID))
180+
moveIf(types.PendingVSCsKey(oldID), types.PendingVSCsKey(newID))
181+
moveIf(types.ConsumerRewardsAllocationKey(oldID), types.ConsumerRewardsAllocationKey(newID))
182+
moveIf(types.TopNKey(oldID), types.TopNKey(newID))
183+
moveIf(types.MinimumPowerInTopNKey(oldID), types.MinimumPowerInTopNKey(newID))
184+
moveIf(types.ValidatorSetCapKey(oldID), types.ValidatorSetCapKey(newID))
185+
moveIf(types.ValidatorsPowerCapKey(oldID), types.ValidatorsPowerCapKey(newID))
186+
187+
// --- collections prefixed by (prefixByte + chain-id + suffix) ---
188+
migrateByPrefixByte(types.ConsumerValidatorBytePrefix)
189+
migrateByPrefixByte(types.ConsumerValidatorsBytePrefix)
190+
migrateByPrefixByte(types.ValidatorsByConsumerAddrBytePrefix)
191+
migrateByPrefixByte(types.OptedInBytePrefix)
192+
migrateByPrefixByte(types.AllowlistPrefix)
193+
migrateByPrefixByte(types.DenylistPrefix)
194+
migrateByPrefixByte(types.ConsumerAddrsToPruneV2BytePrefix)
195+
migrateByPrefixByte(types.ThrottledPacketDataBytePrefix)
196+
197+
// --- proposal side-table where VALUE == chain-id (rewrite values) ---
198+
it := storetypes.KVStorePrefixIterator(kv, []byte{types.ProposedConsumerChainByteKey})
199+
defer it.Close()
200+
for ; it.Valid(); it.Next() {
201+
if string(it.Value()) == oldID {
202+
kv.Set(it.Key(), []byte(newID))
203+
}
204+
}
205+
206+
return nil
89207
}
90208

91209
// CreateConsumerClient will create the CCV client for the given consumer chain. The CCV channel must be built

0 commit comments

Comments
 (0)