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

Remove consumer chain from provider #124

Merged
merged 11 commits into from
Jun 24, 2022
2 changes: 1 addition & 1 deletion app/provider/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ func New(
AddRoute(distrtypes.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.DistrKeeper)).
AddRoute(upgradetypes.RouterKey, upgrade.NewSoftwareUpgradeProposalHandler(app.UpgradeKeeper)).
AddRoute(ibchost.RouterKey, ibcclient.NewClientProposalHandler(app.IBCKeeper.ClientKeeper)).
AddRoute(ibcprovidertypes.RouterKey, ibcprovider.NewCreateConsumerChainHandler(app.ProviderKeeper)).
AddRoute(ibcprovidertypes.RouterKey, ibcprovider.NewConsumerChainProposalHandler(app.ProviderKeeper)).
AddRoute(ibcclienttypes.RouterKey, ibcclient.NewClientProposalHandler(app.IBCKeeper.ClientKeeper))

app.GovKeeper = govkeeper.NewKeeper(
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ require (
github.com/gorilla/handlers v1.5.1 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.2 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/gtank/merlin v0.1.1 // indirect
github.com/gtank/ristretto255 v0.1.2 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -552,8 +552,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.14.7/go.mod h1:oYZKL012gGh6LMyg/xA7Q2y
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.0.1/go.mod h1:oVMjMN64nzEcepv1kdZKgx1qNYt4Ro0Gqefiq2JWdis=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.1 h1:Y7pyy1viWfoKMUVxmjfI5X6fVLlen75kdYjeIwl9CKc=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.1/go.mod h1:chrfS3YoLAlKTRE5cFWvCbt8uGAjshktT4PveTUpsFQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.2 h1:ERKrevVTnCw3Wu4I3mtR15QU3gtWy86cBo6De0jEohg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.2/go.mod h1:chrfS3YoLAlKTRE5cFWvCbt8uGAjshktT4PveTUpsFQ=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0=
github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=
Expand Down
63 changes: 41 additions & 22 deletions proto/interchain_security/ccv/provider/v1/provider.proto
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,47 @@ import "ibc/lightclients/tendermint/v1/tendermint.proto";
// If it passes, then all validators on the provider chain are expected to validate the consumer chain at spawn time
// or get slashed. It is recommended that spawn time occurs after the proposal end time.
message CreateConsumerChainProposal {
option (gogoproto.goproto_getters) = false;
option (gogoproto.goproto_stringer) = false;

// the title of the proposal
string title = 1;
// the description of the proposal
string description = 2;
// the proposed chain-id of the new consumer chain, must be different from all other consumer chain ids of the executing
// provider chain.
string chain_id = 3;
// the proposed initial height of new consumer chain.
// For a completely new chain, this will be {0,1}. However, it may be different if this is a chain that is converting to a consumer chain.
ibc.core.client.v1.Height initial_height = 4 [(gogoproto.nullable) = false];
// genesis hash with no staking information included.
bytes genesis_hash = 5;
// binary hash is the hash of the binary that should be used by validators on chain initialization.
bytes binary_hash = 6;
// spawn time is the time on the provider chain at which the consumer chain genesis is finalized and all validators
// will be responsible for starting their consumer chain validator node.
google.protobuf.Timestamp spawn_time = 7
[(gogoproto.stdtime) = true, (gogoproto.nullable) = false];
}
option (gogoproto.goproto_getters) = false;
option (gogoproto.goproto_stringer) = false;

// the title of the proposal
string title = 1;
// the description of the proposal
string description = 2;
// the proposed chain-id of the new consumer chain, must be different from all other consumer chain ids of the executing
// provider chain.
string chain_id = 3 ;
// the proposed initial height of new consumer chain.
// For a completely new chain, this will be {0,1}. However, it may be different if this is a chain that is converting to a consumer chain.
ibc.core.client.v1.Height initial_height = 4 [(gogoproto.nullable) = false];
// genesis hash with no staking information included.
bytes genesis_hash = 5 ;
// binary hash is the hash of the binary that should be used by validators on chain initialization.
bytes binary_hash = 6 ;
// spawn time is the time on the provider chain at which the consumer chain genesis is finalized and all validators
// will be responsible for starting their consumer chain validator node.
google.protobuf.Timestamp spawn_time = 7
[(gogoproto.stdtime) = true, (gogoproto.nullable) = false];
// Indicates whether the outstanding unbonding operations should be released
// in case of a channel time-outs. When set to true, a governance proposal
// on the provider chain would be necessary to release the locked funds.
bool lock_unbonding_on_timeout = 8;
}

// StopConsumerProposal is a governance proposal on the provider chain to stop a consumer chain.
// If it passes, all the consumer chain's state is removed from the provider chain. The outstanding unbonding
// operation funds are released if the LockUnbondingOnTimeout parameter is set to false for the consumer chain ID.
message StopConsumerChainProposal {
// the title of the proposal
string title = 1;
// the description of the proposal
string description = 2;
// the chain-id of the consumer chain to be stopped
string chain_id = 3;
// the time on the provider chain at which all validators are responsible to stop their consumer chain validator node
google.protobuf.Timestamp stop_time = 4
[(gogoproto.stdtime) = true, (gogoproto.nullable) = false];
}

// Params defines the parameters for CCV Provider module
message Params {
Expand Down
6 changes: 6 additions & 0 deletions x/ccv/consumer/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@ func (k Keeper) GetProviderChannel(ctx sdk.Context) (string, bool) {
return string(channelIdBytes), true
}

// DeleteProviderChannel deletes the provider channel ID that is validating the chain.
func (k Keeper) DeleteProviderChannel(ctx sdk.Context) {
store := ctx.KVStore(k.storeKey)
store.Delete(types.ProviderChannelKey())
}

// SetPendingChanges sets the pending validator set change packet that haven't been flushed to ABCI
func (k Keeper) SetPendingChanges(ctx sdk.Context, updates ccv.ValidatorSetChangePacketData) error {
store := ctx.KVStore(k.storeKey)
Expand Down
1 change: 1 addition & 0 deletions x/ccv/consumer/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ func (suite *KeeperTestSuite) SetupTest() {
suite.providerChain.GetContext(),
suite.consumerChain.ChainID,
suite.consumerChain.LastHeader.GetHeight().(clienttypes.Height),
false,
)
// move provider to next block to commit the state
suite.providerChain.NextBlock()
Expand Down
9 changes: 9 additions & 0 deletions x/ccv/consumer/keeper/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,12 @@ func (k Keeper) OnAcknowledgementPacket(ctx sdk.Context, packet channeltypes.Pac
func (k Keeper) OnTimeoutPacket(ctx sdk.Context, packet channeltypes.Packet, data ccv.SlashPacketData) error {
return nil
}

// IsChannelClosed returns a boolean whether a given channel is in the CLOSED state
func (k Keeper) IsChannelClosed(ctx sdk.Context, channelID string) bool {
channel, found := k.channelKeeper.GetChannel(ctx, types.PortID, channelID)
if !found || channel.State == channeltypes.CLOSED {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it okay here to return true if the channel ID is not found?

@AdityaSripal is channel state pruned? As long as the consumer chain has the channedID, then channelKeeper.GetChannel() should always find the channel, right? If this is true, we should panic in case !found.

Copy link
Member

Choose a reason for hiding this comment

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

Channels are never pruned automatically. Of course, chains could choose to prune closed channels during a state migration.

Copy link
Member

Choose a reason for hiding this comment

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

Where and why would this be used?

Copy link
Contributor

Choose a reason for hiding this comment

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

Where and why would this be used?

In BeginBlock to check whether the CCV channel was closed.

return true
}
return false
}
14 changes: 14 additions & 0 deletions x/ccv/consumer/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,21 @@ func (AppModule) ConsensusVersion() uint64 { return 1 }

// BeginBlock implements the AppModule interface
// Set the VSC ID for the subsequent block to the same value as the current block
// Panic if the provider's channel was established and then closed
func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) {
mpoke marked this conversation as resolved.
Show resolved Hide resolved
channelID, found := am.keeper.GetProviderChannel(ctx)
if found && am.keeper.IsChannelClosed(ctx, channelID) {
// the CCV channel was established, but it was then closed;
// the consumer chain is no longer safe

// cleanup state
am.keeper.DeleteProviderChannel(ctx)
Copy link
Contributor

Choose a reason for hiding this comment

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

This may require cleaning more of the state, i.e., see all the setters in keeper/keeper.go. I'll open a new issue to deal with this. Cleaning up the UnbondingPackets require first solving #39.

Copy link
Contributor

Choose a reason for hiding this comment

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

@jtremback Do we want to be able to restart the consumer chain afterwards? This will impact how we cleanup the state.

Copy link
Member

Choose a reason for hiding this comment

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

It seems unnecessary here if we're just going to stop the consumer chain?

It will require off-chain coordination to restart the chain anyway. So we may as well leave it to off-chain coordination to decide what the new genesis should look like

Copy link
Contributor

Choose a reason for hiding this comment

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

@AdityaSripal Yeah, but we should be cleaning the state as well as we can to facilitate the restart. Is there a reason not to do it?


channelClosedMsg := fmt.Sprintf("CCV channel %q was closed - shutdown consumer chain since it is not secured anymore", channelID)
ctx.Logger().Error(channelClosedMsg)
panic(channelClosedMsg)
sainoe marked this conversation as resolved.
Show resolved Hide resolved
}

blockHeight := uint64(ctx.BlockHeight())
vID := am.keeper.GetHeightValsetUpdateID(ctx, blockHeight)
am.keeper.SetHeightValsetUpdateID(ctx, blockHeight+1, vID)
Expand Down
1 change: 1 addition & 0 deletions x/ccv/consumer/module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func (suite *ConsumerTestSuite) SetupTest() {
suite.providerChain.GetContext(),
suite.consumerChain.ChainID,
suite.consumerChain.LastHeader.GetHeight().(clienttypes.Height),
false,
)
// move provider to next block to commit the state
suite.providerChain.NextBlock()
Expand Down
92 changes: 88 additions & 4 deletions x/ccv/provider/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ func (k Keeper) GetChainToChannel(ctx sdk.Context, chainID string) (string, bool
return string(bz), true
}

// DeleteChainToChannel deletes the CCV channel ID for the given consumer chain ID
func (k Keeper) DeleteChainToChannel(ctx sdk.Context, chainID string) {
store := ctx.KVStore(k.storeKey)
store.Delete(types.ChainToChannelKey(chainID))
}

// IterateConsumerChains iterates over all of the consumer chains that the provider module controls.
// It calls the provided callback function which takes in a chainID and channelID and returns
// a stop boolean which will stop the iteration.
Expand Down Expand Up @@ -168,6 +174,12 @@ func (k Keeper) GetChannelToChain(ctx sdk.Context, channelID string) (string, bo
return string(bz), true
}

// DeleteChannelToChain deletes the consumer chain ID for a given CCV channe lID
func (k Keeper) DeleteChannelToChain(ctx sdk.Context, channelID string) {
store := ctx.KVStore(k.storeKey)
store.Delete(types.ChannelToChainKey(channelID))
}

// IterateChannelToChain iterates over the channel to chain mappings and calls the provided callback until the iteration ends
// or the callback returns stop=true
func (k Keeper) IterateChannelToChain(ctx sdk.Context, cb func(ctx sdk.Context, channelID, chainID string) (stop bool)) {
Expand Down Expand Up @@ -309,8 +321,36 @@ func (k Keeper) SetUnbondingOpIndex(ctx sdk.Context, chainID string, valsetUpdat
store.Set(types.UnbondingOpIndexKey(chainID, valsetUpdateID), bz)
}

// This index allows retreiving UnbondingDelegationEntries by chainID and valsetUpdateID
func (k Keeper) GetUnbodingOpIndex(ctx sdk.Context, chainID string, valsetUpdateID uint64) ([]uint64, bool) {
// IterateOverUnbondingOpIndex iterates over the unbonding indexes for a given chain id.
func (k Keeper) IterateOverUnbondingOpIndex(ctx sdk.Context, chainID string, cb func(vscID uint64, ubdIndex []uint64) bool) {
store := ctx.KVStore(k.storeKey)
prefix := append(types.HashString(types.UnbondingOpIndexPrefix), types.HashString(chainID)...)
iterator := sdk.KVStorePrefixIterator(store, prefix)

defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
// parse key to get the current VSC ID
var vscID uint64
vscBytes, err := types.ParseUnbondingOpIndexKey(iterator.Key())
if err != nil {
panic(err)
}
vscID = binary.BigEndian.Uint64(vscBytes)

var ids []uint64
err = json.Unmarshal(iterator.Value(), &ids)
if err != nil {
panic("Failed to unmarshal JSON")
}

if !cb(vscID, ids) {
return
}
}
}

// This index allows retrieving UnbondingDelegationEntries by chainID and valsetUpdateID
func (k Keeper) GetUnbondingOpIndex(ctx sdk.Context, chainID string, valsetUpdateID uint64) ([]uint64, bool) {
store := ctx.KVStore(k.storeKey)

bz := store.Get(types.UnbondingOpIndexKey(chainID, valsetUpdateID))
Expand All @@ -335,7 +375,7 @@ func (k Keeper) DeleteUnbondingOpIndex(ctx sdk.Context, chainID string, valsetUp

// Retrieve UnbondingDelegationEntries by chainID and valsetUpdateID
func (k Keeper) GetUnbondingOpsFromIndex(ctx sdk.Context, chainID string, valsetUpdateID uint64) (entries []ccv.UnbondingOp, found bool) {
ids, found := k.GetUnbodingOpIndex(ctx, chainID, valsetUpdateID)
ids, found := k.GetUnbondingOpIndex(ctx, chainID, valsetUpdateID)
if !found {
return entries, false
}
Expand Down Expand Up @@ -448,7 +488,7 @@ func (h StakingHooks) AfterUnbondingInitiated(ctx sdk.Context, ID uint64) {

// Add to indexes
for _, consumerChainID := range consumerChainIDS {
index, _ := h.k.GetUnbodingOpIndex(ctx, consumerChainID, valsetUpdateID)
index, _ := h.k.GetUnbondingOpIndex(ctx, consumerChainID, valsetUpdateID)
index = append(index, ID)
h.k.SetUnbondingOpIndex(ctx, consumerChainID, valsetUpdateID, index)
}
Expand Down Expand Up @@ -574,3 +614,47 @@ func (k Keeper) GetInitChainHeight(ctx sdk.Context, chainID string) uint64 {

return binary.BigEndian.Uint64(bz)
}

// DeleteInitChainHeight deletes the block height value for which the given consumer chain's channel was established
func (k Keeper) DeleteInitChainHeight(ctx sdk.Context, chainID string) {
store := ctx.KVStore(k.storeKey)
store.Delete(types.InitChainHeightKey(chainID))
}

// GetLockUnbondingOnTimeout returns the mapping from the given consumer chain ID to a boolean value indicating whether
// the unbonding operation funds should be locked on CCV channel timeout
func (k Keeper) GetLockUnbondingOnTimeout(ctx sdk.Context, chainID string) bool {
store := ctx.KVStore(k.storeKey)
bz := store.Get(types.LockUnbondingOnTimeoutKey(chainID))
return bz != nil
}

// SetLockUnbondingOnTimeout locks the unbonding operation funds in case of a CCV channel timeouts for the given consumer chain ID
func (k Keeper) SetLockUnbondingOnTimeout(ctx sdk.Context, chainID string) {
store := ctx.KVStore(k.storeKey)
store.Set(types.LockUnbondingOnTimeoutKey(chainID), []byte{})
}

// DeleteLockUnbondingOnTimeout deletes the unbonding operation lock in case of a CCV channel timeouts for the given consumer chain ID
func (k Keeper) DeleteLockUnbondingOnTimeout(ctx sdk.Context, chainID string) {
store := ctx.KVStore(k.storeKey)
store.Delete(types.LockUnbondingOnTimeoutKey(chainID))
}

// SetConsumerClient sets the client ID for the given chain ID
func (k Keeper) SetConsumerClient(ctx sdk.Context, chainID, clientID string) {
store := ctx.KVStore(k.storeKey)
store.Set(types.ChainToClientKey(chainID), []byte(clientID))
}

// GetConsumerClient returns the clientID for the given chain ID
func (k Keeper) GetConsumerClient(ctx sdk.Context, chainID string) string {
store := ctx.KVStore(k.storeKey)
return string(store.Get(types.ChainToClientKey(chainID)))
}

// DeleteConsumerClient deletes the client ID for the given chain ID
func (k Keeper) DeleteConsumerClient(ctx sdk.Context, chainID string) {
store := ctx.KVStore(k.storeKey)
store.Delete(types.ChainToClientKey(chainID))
}
24 changes: 24 additions & 0 deletions x/ccv/provider/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ func (suite *KeeperTestSuite) SetupTest() {
suite.providerChain.GetContext(),
suite.consumerChain.ChainID,
suite.consumerChain.LastHeader.GetHeight().(clienttypes.Height),
false,
)
// move provider to next block to commit the state
suite.providerChain.NextBlock()
Expand Down Expand Up @@ -447,3 +448,26 @@ func (suite *KeeperTestSuite) TestHandleSlashPacketDistribution() {
ubdBalance = ubd.Entries[0].Balance
}
}

func (suite *KeeperTestSuite) TestIterateOverUnbondingOpIndex() {
providerKeeper := suite.providerChain.App.(*appProvider.App).ProviderKeeper
chainID := suite.consumerChain.ChainID

// mock an unbonding index
unbondingOpIndex := []uint64{0, 1, 2, 3, 4, 5, 6}

// set ubd ops by varying vsc ids and index slices
for i := 1; i < len(unbondingOpIndex); i++ {
providerKeeper.SetUnbondingOpIndex(suite.providerChain.GetContext(), chainID, uint64(i), unbondingOpIndex[:i])
}

// check iterator returns expected entries
i := 1
providerKeeper.IterateOverUnbondingOpIndex(suite.providerChain.GetContext(), chainID, func(vscID uint64, ubdIndex []uint64) bool {
suite.Require().Equal(uint64(i), vscID)
suite.Require().EqualValues(unbondingOpIndex[:i], ubdIndex)
i++
return true
})
suite.Require().Equal(len(unbondingOpIndex), i)
}
Loading