diff --git a/app/provider/app.go b/app/provider/app.go index 659114fa36..77d40fc313 100644 --- a/app/provider/app.go +++ b/app/provider/app.go @@ -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( diff --git a/go.mod b/go.mod index e006b7cb43..ef0b737330 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index f0faa2d459..c0890d024b 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/proto/interchain_security/ccv/provider/v1/provider.proto b/proto/interchain_security/ccv/provider/v1/provider.proto index 9b3affc570..0d21a6a10b 100644 --- a/proto/interchain_security/ccv/provider/v1/provider.proto +++ b/proto/interchain_security/ccv/provider/v1/provider.proto @@ -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 { diff --git a/x/ccv/consumer/keeper/keeper.go b/x/ccv/consumer/keeper/keeper.go index 663e103366..ee2c3d19ec 100644 --- a/x/ccv/consumer/keeper/keeper.go +++ b/x/ccv/consumer/keeper/keeper.go @@ -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) diff --git a/x/ccv/consumer/keeper/keeper_test.go b/x/ccv/consumer/keeper/keeper_test.go index 35b8b47951..e5628ebd8f 100644 --- a/x/ccv/consumer/keeper/keeper_test.go +++ b/x/ccv/consumer/keeper/keeper_test.go @@ -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() diff --git a/x/ccv/consumer/keeper/relay.go b/x/ccv/consumer/keeper/relay.go index 0a897d6a93..7585dfc8f4 100644 --- a/x/ccv/consumer/keeper/relay.go +++ b/x/ccv/consumer/keeper/relay.go @@ -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 { + return true + } + return false +} diff --git a/x/ccv/consumer/module.go b/x/ccv/consumer/module.go index a7f21a98d3..fa82545db5 100644 --- a/x/ccv/consumer/module.go +++ b/x/ccv/consumer/module.go @@ -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) { + 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) + + channelClosedMsg := fmt.Sprintf("CCV channel %q was closed - shutdown consumer chain since it is not secured anymore", channelID) + ctx.Logger().Error(channelClosedMsg) + panic(channelClosedMsg) + } + blockHeight := uint64(ctx.BlockHeight()) vID := am.keeper.GetHeightValsetUpdateID(ctx, blockHeight) am.keeper.SetHeightValsetUpdateID(ctx, blockHeight+1, vID) diff --git a/x/ccv/consumer/module_test.go b/x/ccv/consumer/module_test.go index d836634455..abffc9f305 100644 --- a/x/ccv/consumer/module_test.go +++ b/x/ccv/consumer/module_test.go @@ -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() diff --git a/x/ccv/provider/keeper/keeper.go b/x/ccv/provider/keeper/keeper.go index 34714ca1a4..2911c71a28 100644 --- a/x/ccv/provider/keeper/keeper.go +++ b/x/ccv/provider/keeper/keeper.go @@ -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. @@ -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)) { @@ -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)) @@ -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 } @@ -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) } @@ -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)) +} diff --git a/x/ccv/provider/keeper/keeper_test.go b/x/ccv/provider/keeper/keeper_test.go index 0065a095aa..d125d87b30 100644 --- a/x/ccv/provider/keeper/keeper_test.go +++ b/x/ccv/provider/keeper/keeper_test.go @@ -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() @@ -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) +} diff --git a/x/ccv/provider/keeper/proposal.go b/x/ccv/provider/keeper/proposal.go index 83cbc06cb4..d085c90189 100644 --- a/x/ccv/provider/keeper/proposal.go +++ b/x/ccv/provider/keeper/proposal.go @@ -2,9 +2,12 @@ package keeper import ( "encoding/binary" + "fmt" "strings" "time" + channeltypes "github.com/cosmos/ibc-go/v3/modules/core/04-channel/types" + sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" @@ -22,17 +25,89 @@ import ( // If the spawn time has already passed, then set the consumer chain. Otherwise store the client // as a pending client, and set once spawn time has passed. func (k Keeper) CreateConsumerChainProposal(ctx sdk.Context, p *types.CreateConsumerChainProposal) error { - if ctx.BlockTime().After(p.SpawnTime) { - return k.CreateConsumerClient(ctx, p.ChainId, p.InitialHeight) + if !ctx.BlockTime().Before(p.SpawnTime) { + return k.CreateConsumerClient(ctx, p.ChainId, p.InitialHeight, p.LockUnbondingOnTimeout) + } + + k.SetPendingClientInfo(ctx, p) + return nil +} + +// StopConsumerChainProposal stops a consumer chain and released the outstanding unbonding operations. +// If the stop time hasn't already passed, it stores the proposal as a pending proposal. +func (k Keeper) StopConsumerChainProposal(ctx sdk.Context, p *types.StopConsumerChainProposal) error { + + if !ctx.BlockTime().Before(p.StopTime) { + return k.StopConsumerChain(ctx, p.ChainId, false, true) + } + + k.SetPendingStopProposal(ctx, p.ChainId, p.StopTime) + return nil +} + +// StopConsumerChain cleans up the states for the given consumer chain ID and, if the given lockUbd is false, +// it completes the outstanding unbonding operations lock by the consumer chain. +func (k Keeper) StopConsumerChain(ctx sdk.Context, chainID string, lockUbd, closeChan bool) (err error) { + + // clean up states + k.DeleteConsumerClient(ctx, chainID) + k.DeleteLockUnbondingOnTimeout(ctx, chainID) + + // close channel and delete the mappings between chain ID and channel ID + if channelID, found := k.GetChainToChannel(ctx, chainID); found { + if closeChan { + k.CloseChannel(ctx, channelID) + } + k.DeleteChainToChannel(ctx, chainID) + k.DeleteChannelToChain(ctx, channelID) + } + + // TODO remove pending VSC packets once https://github.com/cosmos/interchain-security/issues/27 is fixed + k.DeleteInitChainHeight(ctx, chainID) + k.EmptySlashAcks(ctx, chainID) + + // release unbonding operations if they aren't locked + if !lockUbd { + // iterate over the consumer chain's unbonding operation VSC ids + k.IterateOverUnbondingOpIndex(ctx, chainID, func(vscID uint64, ids []uint64) bool { + // range over the unbonding operations for the current VSC ID + for _, id := range ids { + unbondingOp, found := k.GetUnbondingOp(ctx, id) + if !found { + err = fmt.Errorf("could not find UnbondingOp according to index - id: %d", id) + return false + } + // remove consumer chain ID from unbonding op record + unbondingOp.UnbondingConsumerChains, _ = removeStringFromSlice(unbondingOp.UnbondingConsumerChains, chainID) + + // If unbonding op is completely unbonded from all relevant consumer chains + if len(unbondingOp.UnbondingConsumerChains) == 0 { + // Attempt to complete unbonding in staking module + err = k.stakingKeeper.UnbondingCanComplete(ctx, unbondingOp.Id) + if err != nil { + return false + } + // Delete unbonding op + k.DeleteUnbondingOp(ctx, unbondingOp.Id) + } else { + k.SetUnbondingOp(ctx, unbondingOp) + } + } + // clean up index + k.DeleteUnbondingOpIndex(ctx, chainID, vscID) + return true + }) + } + if err != nil { + return err } - k.SetPendingClientInfo(ctx, p.SpawnTime, p.ChainId, p.InitialHeight) 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. -func (k Keeper) CreateConsumerClient(ctx sdk.Context, chainID string, initialHeight clienttypes.Height) error { +func (k Keeper) CreateConsumerClient(ctx sdk.Context, chainID string, initialHeight clienttypes.Height, lockUbdOnTimeout bool) error { // Use the unbonding period on the provider to // compute the unbonding period on the consumer unbondingTime := utils.ComputeConsumerUnbondingPeriod(k.stakingKeeper.UnbondingTime(ctx)) @@ -58,6 +133,11 @@ func (k Keeper) CreateConsumerClient(ctx sdk.Context, chainID string, initialHei } k.SetConsumerGenesis(ctx, chainID, consumerGen) + + // store LockUnbondingOnTimeout flag + if lockUbdOnTimeout { + k.SetLockUnbondingOnTimeout(ctx, chainID) + } return nil } @@ -119,13 +199,13 @@ func (k Keeper) MakeConsumerGenesis(ctx sdk.Context) (gen consumertypes.GenesisS return gen, nil } -// SetConsumerClientId sets the clientID for the given chainID +// SetConsumerClientId sets the client ID for the given chain ID func (k Keeper) SetConsumerClientId(ctx sdk.Context, chainID, clientID string) { store := ctx.KVStore(k.storeKey) store.Set(types.ChainToClientKey(chainID), []byte(clientID)) } -// GetConsumerClientId returns the clientID for the given chainID. +// GetConsumerClientId returns the client ID for the given chain ID. func (k Keeper) GetConsumerClientId(ctx sdk.Context, chainID string) (string, bool) { store := ctx.KVStore(k.storeKey) clientIdBytes := store.Get(types.ChainToClientKey(chainID)) @@ -135,27 +215,29 @@ func (k Keeper) GetConsumerClientId(ctx sdk.Context, chainID string) (string, bo return string(clientIdBytes), true } -// SetPendingClientInfo sets the initial height for the given timestamp and chainID -func (k Keeper) SetPendingClientInfo(ctx sdk.Context, timestamp time.Time, chainID string, initialHeight clienttypes.Height) error { +// SetPendingClientInfo sets the initial height for the given timestamp and chain ID +func (k Keeper) SetPendingClientInfo(ctx sdk.Context, clientInfo *types.CreateConsumerChainProposal) error { store := ctx.KVStore(k.storeKey) - bz, err := k.cdc.Marshal(&initialHeight) + bz, err := k.cdc.Marshal(clientInfo) if err != nil { return err } - store.Set(types.PendingClientKey(timestamp, chainID), bz) + + store.Set(types.PendingClientKey(clientInfo.SpawnTime, clientInfo.ChainId), bz) return nil } -// GetPendingClient gets the initial height for the given timestamp and chainID -func (k Keeper) GetPendingClientInfo(ctx sdk.Context, timestamp time.Time, chainID string) clienttypes.Height { +// GetPendingClient gets the client pending info for the given timestamp and chain ID +func (k Keeper) GetPendingClientInfo(ctx sdk.Context, timestamp time.Time, chainID string) types.CreateConsumerChainProposal { store := ctx.KVStore(k.storeKey) bz := store.Get(types.PendingClientKey(timestamp, chainID)) if len(bz) == 0 { - return clienttypes.Height{} + return types.CreateConsumerChainProposal{} } - var initialHeight clienttypes.Height - k.cdc.MustUnmarshal(bz, &initialHeight) - return initialHeight + var clientInfo types.CreateConsumerChainProposal + k.cdc.MustUnmarshal(bz, &clientInfo) + + return clientInfo } // IteratePendingClientInfo iterates over the pending client info in order and creates the consumer client if the spawn time has passed, @@ -168,21 +250,107 @@ func (k Keeper) IteratePendingClientInfo(ctx sdk.Context) { if !iterator.Valid() { return } + // store the executed proposals in order + execProposals := []types.CreateConsumerChainProposal{} for ; iterator.Valid(); iterator.Next() { suffixKey := iterator.Key() - // splitKey contains the bigendian time in the first element and the chainID in the second element/ + // splitKey contains the bigendian time in the second element and the chainID in the third element splitKey := strings.Split(string(suffixKey), "/") - timeNano := binary.BigEndian.Uint64([]byte(splitKey[0])) + timeNano := binary.BigEndian.Uint64([]byte(splitKey[1])) spawnTime := time.Unix(0, int64(timeNano)) - var initialHeight clienttypes.Height - k.cdc.MustUnmarshal(iterator.Value(), &initialHeight) + chainID := string([]byte(splitKey[2])) + + var clientInfo types.CreateConsumerChainProposal + k.cdc.MustUnmarshal(iterator.Value(), &clientInfo) + + if !ctx.BlockTime().Before(spawnTime) { + k.CreateConsumerClient(ctx, chainID, clientInfo.InitialHeight, clientInfo.LockUnbondingOnTimeout) + execProposals = append(execProposals, + types.CreateConsumerChainProposal{ChainId: chainID, SpawnTime: spawnTime}) + } else { + break + } + } + + // delete the proposals executed + k.DeletePendingClientInfo(ctx, execProposals...) +} + +// DeletePendingClientInfo deletes the given create consumer proposals +func (k Keeper) DeletePendingClientInfo(ctx sdk.Context, proposals ...types.CreateConsumerChainProposal) { + store := ctx.KVStore(k.storeKey) + + for _, p := range proposals { + store.Delete(types.PendingClientKey(p.SpawnTime, p.ChainId)) + } +} + +// SetPendingStopProposal sets the consumer chain ID for the given timestamp +func (k Keeper) SetPendingStopProposal(ctx sdk.Context, chainID string, timestamp time.Time) { + store := ctx.KVStore(k.storeKey) + store.Set(types.PendingStopProposalKey(timestamp, chainID), []byte{}) +} + +// GetPendingStopProposal returns a boolean if a pending stop proposal exists for the given consumer chain ID and the timestamp +func (k Keeper) GetPendingStopProposal(ctx sdk.Context, chainID string, timestamp time.Time) bool { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.PendingStopProposalKey(timestamp, chainID)) + + return bz != nil +} + +// DeletePendingStopProposals deletes the given stop proposals +func (k Keeper) DeletePendingStopProposals(ctx sdk.Context, proposals ...types.StopConsumerChainProposal) { + store := ctx.KVStore(k.storeKey) + + for _, p := range proposals { + store.Delete(types.PendingStopProposalKey(p.StopTime, p.ChainId)) + } +} + +// IteratePendingStopProposal iterates over the pending stop proposals in order and stop the chain if the stop time has passed, +// otherwise it will break out of loop and return. +func (k Keeper) IteratePendingStopProposal(ctx sdk.Context) { + store := ctx.KVStore(k.storeKey) + iterator := sdk.KVStorePrefixIterator(store, []byte(types.PendingStopProposalKeyPrefix+"/")) + defer iterator.Close() - if ctx.BlockTime().After(spawnTime) { - k.CreateConsumerClient(ctx, splitKey[1], initialHeight) + if !iterator.Valid() { + return + } + + // store the executed proposals in order + execProposals := []types.StopConsumerChainProposal{} + + for ; iterator.Valid(); iterator.Next() { + suffixKey := iterator.Key() + // splitKey contains the bigendian time in the second element and the chainID in the third element + splitKey := strings.Split(string(suffixKey), "/") + + timeNano := binary.BigEndian.Uint64([]byte(splitKey[1])) + stopTime := time.Unix(0, int64(timeNano)) + chainID := string([]byte(splitKey[2])) + + if !ctx.BlockTime().Before(stopTime) { + k.StopConsumerChain(ctx, chainID, false, true) + execProposals = append(execProposals, + types.StopConsumerChainProposal{ChainId: chainID, StopTime: stopTime}) } else { break } } + + // delete the proposals executed + k.DeletePendingStopProposals(ctx, execProposals...) +} + +// CloseChannel closes the channel for the given channel ID on the condition +// that the channel exists and isn't already in the CLOSED state +func (k Keeper) CloseChannel(ctx sdk.Context, channelID string) { + channel, found := k.channelKeeper.GetChannel(ctx, types.PortID, channelID) + if found && channel.State != channeltypes.CLOSED { + k.chanCloseInit(ctx, channelID) + } } diff --git a/x/ccv/provider/keeper/proposal_test.go b/x/ccv/provider/keeper/proposal_test.go index 75a03a82fb..5e6da7ee69 100644 --- a/x/ccv/provider/keeper/proposal_test.go +++ b/x/ccv/provider/keeper/proposal_test.go @@ -20,7 +20,7 @@ func (suite *KeeperTestSuite) TestMakeConsumerGenesis() { actualGenesis, err := suite.providerChain.App.(*appProvider.App).ProviderKeeper.MakeConsumerGenesis(suite.providerChain.GetContext()) suite.Require().NoError(err) - jsonString := `{"params":{"enabled":true, "blocks_per_distribution_transmission":1000},"new_chain":true,"provider_client_state":{"chain_id":"testchain1","trust_level":{"numerator":1,"denominator":3},"trusting_period":907200000000000,"unbonding_period":1814400000000000,"max_clock_drift":10000000000,"frozen_height":{},"latest_height":{"revision_height":5},"proof_specs":[{"leaf_spec":{"hash":1,"prehash_value":1,"length":1,"prefix":"AA=="},"inner_spec":{"child_order":[0,1],"child_size":33,"min_prefix_length":4,"max_prefix_length":12,"hash":1}},{"leaf_spec":{"hash":1,"prehash_value":1,"length":1,"prefix":"AA=="},"inner_spec":{"child_order":[0,1],"child_size":32,"min_prefix_length":1,"max_prefix_length":1,"hash":1}}],"upgrade_path":["upgrade","upgradedIBCState"],"allow_update_after_expiry":true,"allow_update_after_misbehaviour":true},"provider_consensus_state":{"timestamp":"2020-01-02T00:00:10Z","root":{"hash":"LpGpeyQVLUo9HpdsgJr12NP2eCICspcULiWa5u9udOA="},"next_validators_hash":"E30CE736441FB9101FADDAF7E578ABBE6DFDB67207112350A9A904D554E1F5BE"},"unbonding_sequences":null,"initial_val_set":[{"pub_key":{"Sum":{"ed25519":"dcASx5/LIKZqagJWN0frOlFtcvz91frYmj/zmoZRWro="}},"power":1}]}` + jsonString := `{"params":{"enabled":true, "blocks_per_distribution_transmission":1000, "lock_unbonding_on_timeout": false},"new_chain":true,"provider_client_state":{"chain_id":"testchain1","trust_level":{"numerator":1,"denominator":3},"trusting_period":907200000000000,"unbonding_period":1814400000000000,"max_clock_drift":10000000000,"frozen_height":{},"latest_height":{"revision_height":5},"proof_specs":[{"leaf_spec":{"hash":1,"prehash_value":1,"length":1,"prefix":"AA=="},"inner_spec":{"child_order":[0,1],"child_size":33,"min_prefix_length":4,"max_prefix_length":12,"hash":1}},{"leaf_spec":{"hash":1,"prehash_value":1,"length":1,"prefix":"AA=="},"inner_spec":{"child_order":[0,1],"child_size":32,"min_prefix_length":1,"max_prefix_length":1,"hash":1}}],"upgrade_path":["upgrade","upgradedIBCState"],"allow_update_after_expiry":true,"allow_update_after_misbehaviour":true},"provider_consensus_state":{"timestamp":"2020-01-02T00:00:10Z","root":{"hash":"LpGpeyQVLUo9HpdsgJr12NP2eCICspcULiWa5u9udOA="},"next_validators_hash":"E30CE736441FB9101FADDAF7E578ABBE6DFDB67207112350A9A904D554E1F5BE"},"unbonding_sequences":null,"initial_val_set":[{"pub_key":{"Sum":{"ed25519":"dcASx5/LIKZqagJWN0frOlFtcvz91frYmj/zmoZRWro="}},"power":1}]}` var expectedGenesis consumertypes.GenesisState json.Unmarshal([]byte(jsonString), &expectedGenesis) @@ -49,6 +49,7 @@ func (suite *KeeperTestSuite) TestCreateConsumerChainProposal() { chainID := "chainID" initialHeight := clienttypes.NewHeight(2, 3) + lockUbdOnTimeout := false testCases := []struct { name string @@ -101,8 +102,9 @@ func (suite *KeeperTestSuite) TestCreateConsumerChainProposal() { suite.Require().Equal(expectedGenesis, consumerGenesis) suite.Require().NotEqual("", clientId, "consumer client was not created after spawn time reached") } else { - gotHeight := suite.providerChain.App.(*appProvider.App).ProviderKeeper.GetPendingClientInfo(ctx, proposal.SpawnTime, chainID) - suite.Require().Equal(initialHeight, gotHeight, "pending client not equal to clientstate in proposal") + gotClient := suite.providerChain.App.(*appProvider.App).ProviderKeeper.GetPendingClientInfo(ctx, proposal.SpawnTime, chainID) + suite.Require().Equal(initialHeight, gotClient.InitialHeight, "pending client not equal to clientstate in proposal") + suite.Require().Equal(lockUbdOnTimeout, gotClient.LockUnbondingOnTimeout, "pending client not equal to clientstate in proposal") } } else { suite.Require().Error(err, "did not return error on invalid case") @@ -110,3 +112,72 @@ func (suite *KeeperTestSuite) TestCreateConsumerChainProposal() { }) } } + +func (suite *KeeperTestSuite) TestIteratePendingStopProposal() { + + chainID := suite.consumerChain.ChainID + + testCases := []struct { + types.StopConsumerChainProposal + ExpDeleted bool + }{ + { + StopConsumerChainProposal: types.StopConsumerChainProposal{ChainId: chainID, StopTime: time.Now().UTC()}, + ExpDeleted: true, + }, + { + StopConsumerChainProposal: types.StopConsumerChainProposal{ChainId: chainID, StopTime: time.Now().UTC().Add(time.Hour)}, + ExpDeleted: false, + }, + } + + for _, tc := range testCases { + suite.providerChain.App.(*appProvider.App).ProviderKeeper.SetPendingStopProposal( + suite.providerChain.GetContext(), tc.ChainId, tc.StopTime) + } + + ctx := suite.providerChain.GetContext().WithBlockTime(testCases[0].StopTime) + suite.providerChain.App.(*appProvider.App).ProviderKeeper.IteratePendingStopProposal(ctx) + + for _, tc := range testCases { + found := suite.providerChain.App.(*appProvider.App).ProviderKeeper.GetPendingStopProposal(ctx, tc.ChainId, tc.StopTime) + suite.Require().NotEqual(tc.ExpDeleted, found, "stop proposal was not deleted %s %v", tc.ChainId, tc.StopTime) + } +} + +func (suite *KeeperTestSuite) TestIteratePendingClientInfo() { + + chainID := suite.consumerChain.ChainID + + testCases := []struct { + types.CreateConsumerChainProposal + ExpDeleted bool + }{ + { + CreateConsumerChainProposal: types.CreateConsumerChainProposal{ChainId: chainID, SpawnTime: time.Now().UTC()}, + ExpDeleted: true, + }, + { + CreateConsumerChainProposal: types.CreateConsumerChainProposal{ChainId: chainID, SpawnTime: time.Now().UTC().Add(time.Hour)}, + ExpDeleted: false, + }, + } + + for _, tc := range testCases { + suite.providerChain.App.(*appProvider.App).ProviderKeeper.SetPendingClientInfo( + suite.providerChain.GetContext(), &tc.CreateConsumerChainProposal) + } + + ctx := suite.providerChain.GetContext().WithBlockTime(testCases[0].SpawnTime) + + suite.providerChain.App.(*appProvider.App).ProviderKeeper.IteratePendingClientInfo(ctx) + + for _, tc := range testCases { + res := suite.providerChain.App.(*appProvider.App).ProviderKeeper.GetPendingClientInfo(ctx, tc.SpawnTime, tc.ChainId) + if !tc.ExpDeleted { + suite.Require().NotEmpty(res, "stop proposal was not deleted: %s %s", tc.ChainId, tc.SpawnTime.String()) + continue + } + suite.Require().Empty(res, "stop proposal was not deleted %s %s", tc.ChainId, tc.SpawnTime.String()) + } +} diff --git a/x/ccv/provider/keeper/relay.go b/x/ccv/provider/keeper/relay.go index 3811155f86..5853aac15a 100644 --- a/x/ccv/provider/keeper/relay.go +++ b/x/ccv/provider/keeper/relay.go @@ -87,8 +87,20 @@ func (k Keeper) OnAcknowledgementPacket(ctx sdk.Context, packet channeltypes.Pac return nil } -func (k Keeper) OnTimeoutPacket(ctx sdk.Context, packet channeltypes.Packet, data ccv.ValidatorSetChangePacketData) error { - // TODO: Unbonding everything? +// OnTimeoutPacket aborts the transaction if no chain exists for the destination channel, +// otherwise it stops the chain +func (k Keeper) OnTimeoutPacket(ctx sdk.Context, packet channeltypes.Packet) error { + chainID, found := k.GetChannelToChain(ctx, packet.DestinationChannel) + if !found { + // abort transaction + return sdkerrors.Wrap( + channeltypes.ErrInvalidChannelState, + packet.DestinationChannel, + ) + } + // stop consumer chain and uses the LockUnbondingOnTimeout flag + // to decide whether the unbonding operations should be released + k.StopConsumerChain(ctx, chainID, k.GetLockUnbondingOnTimeout(ctx, chainID), false) return nil } diff --git a/x/ccv/provider/module.go b/x/ccv/provider/module.go index 6949d19734..472ce74c97 100644 --- a/x/ccv/provider/module.go +++ b/x/ccv/provider/module.go @@ -157,6 +157,8 @@ func (AppModule) ConsensusVersion() uint64 { return 1 } func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { // Check if there are any consumer chains that are due to be started am.keeper.IteratePendingClientInfo(ctx) + // Check if there are any consumer chains that are due to be stopped + am.keeper.IteratePendingStopProposal(ctx) } // EndBlock implements the AppModule interface @@ -415,12 +417,8 @@ func (am AppModule) OnTimeoutPacket( packet channeltypes.Packet, _ sdk.AccAddress, ) error { - var data ccv.ValidatorSetChangePacketData - if err := ccv.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil { - return sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "cannot unmarshal provider packet data: %s", err.Error()) - } - // refund tokens - if err := am.keeper.OnTimeoutPacket(ctx, packet, data); err != nil { + + if err := am.keeper.OnTimeoutPacket(ctx, packet); err != nil { return err } diff --git a/x/ccv/provider/proposal_handler.go b/x/ccv/provider/proposal_handler.go index 26768ffe5b..fb8491ce3d 100644 --- a/x/ccv/provider/proposal_handler.go +++ b/x/ccv/provider/proposal_handler.go @@ -8,12 +8,14 @@ import ( "github.com/cosmos/interchain-security/x/ccv/provider/types" ) -// NewCreateConsumerChainHandler defines the CCV provider proposal handler -func NewCreateConsumerChainHandler(k keeper.Keeper) govtypes.Handler { +// NewConsumerChainProposalHandler defines the CCV provider proposal handler +func NewConsumerChainProposalHandler(k keeper.Keeper) govtypes.Handler { return func(ctx sdk.Context, content govtypes.Content) error { switch c := content.(type) { case *types.CreateConsumerChainProposal: return k.CreateConsumerChainProposal(ctx, c) + case *types.StopConsumerChainProposal: + return k.StopConsumerChainProposal(ctx, c) default: return sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized ccv proposal content type: %T", c) } diff --git a/x/ccv/provider/proposal_handler_test.go b/x/ccv/provider/proposal_handler_test.go index 9cc550b3c1..e60e1082d1 100644 --- a/x/ccv/provider/proposal_handler_test.go +++ b/x/ccv/provider/proposal_handler_test.go @@ -34,6 +34,13 @@ func (suite *ProviderTestSuite) TestCreateConsumerChainProposalHandler() { suite.Require().NoError(err) }, true, }, + { + "valid stop consumerchain proposal", func(suite *ProviderTestSuite) { + ctx = suite.providerChain.GetContext().WithBlockTime(time.Now().Add(time.Hour)) + content, err = types.NewStopConsumerChainProposal("title", "description", "chainID", time.Now()) + suite.Require().NoError(err) + }, true, + }, { "nil proposal", func(suite *ProviderTestSuite) { ctx = suite.providerChain.GetContext() @@ -56,7 +63,7 @@ func (suite *ProviderTestSuite) TestCreateConsumerChainProposalHandler() { tc.malleate(suite) - proposalHandler := provider.NewCreateConsumerChainHandler(suite.providerChain.App.(*appProvider.App).ProviderKeeper) + proposalHandler := provider.NewConsumerChainProposalHandler(suite.providerChain.App.(*appProvider.App).ProviderKeeper) err = proposalHandler(ctx, content) diff --git a/x/ccv/provider/provider_test.go b/x/ccv/provider/provider_test.go index 3e384f8803..84bc04a778 100644 --- a/x/ccv/provider/provider_test.go +++ b/x/ccv/provider/provider_test.go @@ -71,6 +71,7 @@ func (suite *ProviderTestSuite) SetupTest() { suite.providerCtx(), suite.consumerChain.ChainID, suite.consumerChain.LastHeader.GetHeight().(clienttypes.Height), + false, ) // move provider to next block to commit the state suite.providerChain.NextBlock() diff --git a/x/ccv/provider/stop_consumer_test.go b/x/ccv/provider/stop_consumer_test.go new file mode 100644 index 0000000000..b25ca6d962 --- /dev/null +++ b/x/ccv/provider/stop_consumer_test.go @@ -0,0 +1,295 @@ +package provider_test + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + clienttypes "github.com/cosmos/ibc-go/v3/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v3/modules/core/04-channel/types" + appProvider "github.com/cosmos/interchain-security/app/provider" + consumertypes "github.com/cosmos/interchain-security/x/ccv/consumer/types" + providertypes "github.com/cosmos/interchain-security/x/ccv/provider/types" + ccv "github.com/cosmos/interchain-security/x/ccv/types" + abci "github.com/tendermint/tendermint/abci/types" +) + +func (s *ProviderTestSuite) TestStopConsumerChain() { + + // default consumer chain ID + consumerChainID := s.consumerChain.ChainID + + // choose a validator + tmValidator := s.providerChain.Vals.Validators[0] + valAddr, err := sdk.ValAddressFromHex(tmValidator.Address.String()) + s.Require().NoError(err) + + validator, found := s.providerChain.App.(*appProvider.App).StakingKeeper.GetValidator(s.providerCtx(), valAddr) + s.Require().True(found) + + // get delegator address + delAddr := s.providerChain.SenderAccount.GetAddress() + + // define variables required for test setup + var ( + // bond amount + bondAmt = sdk.NewInt(1000000) + // number of unbonding operations performed + ubdOpsNum = 4 + // store new shares created + testShares sdk.Dec + ) + + // populate the provider chain states to setup the test using the following operations: + // - setup CCV channel; establish CCV channel and set channelToChain, chainToChannel and initHeight mapping for the consumer chain ID + // - delegate the total bond amount to the chosed validator + // - undelegate the shares in four consecutive blocks evenly; create UnbondigOp and UnbondingOpIndex entries for the consumer chain ID + // - set SlashAck and LockUnbondingOnTimeout states for the consumer chain ID + setupOperations := []struct { + fn func(suite *ProviderTestSuite) error + }{ + { + func(suite *ProviderTestSuite) error { + suite.SetupCCVChannel() + return nil + }, + }, + { + func(suite *ProviderTestSuite) error { + testShares, err = s.providerChain.App.(*appProvider.App).StakingKeeper.Delegate(s.providerCtx(), delAddr, bondAmt, stakingtypes.Unbonded, stakingtypes.Validator(validator), true) + return err + }, + }, + { + func(suite *ProviderTestSuite) error { + for i := 0; i < ubdOpsNum; i++ { + // undelegate one quarter of the shares + _, err := s.providerChain.App.(*appProvider.App).StakingKeeper.Undelegate(s.providerCtx(), delAddr, valAddr, testShares.QuoInt64(int64(ubdOpsNum))) + if err != nil { + return err + } + // increment block + s.providerChain.NextBlock() + } + return nil + }, + }, + { + func(suite *ProviderTestSuite) error { + s.providerChain.App.(*appProvider.App).ProviderKeeper.SetSlashAcks(s.providerCtx(), consumerChainID, []string{"validator-1", "validator-2", "validator-3"}) + s.providerChain.App.(*appProvider.App).ProviderKeeper.SetLockUnbondingOnTimeout(s.providerCtx(), consumerChainID) + return nil + }, + }, + } + + for _, so := range setupOperations { + err := so.fn(s) + s.Require().NoError(err) + } + + // stop the consumer chain + err = s.providerChain.App.(*appProvider.App).ProviderKeeper.StopConsumerChain(s.providerCtx(), consumerChainID, false, true) + s.Require().NoError(err) + + // check all states are removed and the unbonding operation released + s.checkConsumerChainIsRemoved(consumerChainID, false) +} + +func (s *ProviderTestSuite) TestStopConsumerChainProposal() { + var ( + ctx sdk.Context + proposal *providertypes.StopConsumerChainProposal + ok bool + ) + + chainID := s.consumerChain.ChainID + + testCases := []struct { + name string + malleate func(*ProviderTestSuite) + expPass bool + stopReached bool + }{ + { + "valid stop consumer chain proposal: stop time reached", func(suite *ProviderTestSuite) { + + // ctx blocktime is after proposal's stop time + ctx = s.providerCtx().WithBlockTime(time.Now().Add(time.Hour)) + content, err := providertypes.NewStopConsumerChainProposal("title", "description", chainID, time.Now()) + s.Require().NoError(err) + proposal, ok = content.(*providertypes.StopConsumerChainProposal) + s.Require().True(ok) + }, true, true, + }, + { + "valid proposal: stop time has not yet been reached", func(suite *ProviderTestSuite) { + + // ctx blocktime is before proposal's stop time + ctx = s.providerCtx().WithBlockTime(time.Now()) + content, err := providertypes.NewStopConsumerChainProposal("title", "description", chainID, time.Now().Add(time.Hour)) + s.Require().NoError(err) + proposal, ok = content.(*providertypes.StopConsumerChainProposal) + s.Require().True(ok) + }, true, false, + }, + { + "valid proposal: fail due to an invalid unbonding index", func(suite *ProviderTestSuite) { + + // ctx blocktime is after proposal's stop time + ctx = s.providerCtx().WithBlockTime(time.Now().Add(time.Hour)) + + // set invalid unbonding op index + s.providerChain.App.(*appProvider.App).ProviderKeeper.SetUnbondingOpIndex(ctx, chainID, 0, []uint64{0}) + + content, err := providertypes.NewStopConsumerChainProposal("title", "description", chainID, time.Now()) + s.Require().NoError(err) + proposal, ok = content.(*providertypes.StopConsumerChainProposal) + s.Require().True(ok) + }, false, true, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + s.SetupTest() + s.SetupCCVChannel() + + tc.malleate(s) + + err := s.providerChain.App.(*appProvider.App).ProviderKeeper.StopConsumerChainProposal(ctx, proposal) + if tc.expPass { + s.Require().NoError(err, "error returned on valid case") + if tc.stopReached { + // check that the pending stop consumer chain proposal is deleted + found := s.providerChain.App.(*appProvider.App).ProviderKeeper.GetPendingStopProposal(ctx, chainID, proposal.StopTime) + s.Require().False(found, "pending stop consumer proposal wasn't deleted") + + // check that the consumer chain is removed + s.checkConsumerChainIsRemoved(chainID, false) + + } else { + found := s.providerChain.App.(*appProvider.App).ProviderKeeper.GetPendingStopProposal(ctx, chainID, proposal.StopTime) + s.Require().True(found, "pending stop consumer was not found for chain ID %s", chainID) + + // check that the consumer chain client exists + s.Require().NotZero(s.providerChain.App.(*appProvider.App).ProviderKeeper.GetConsumerClient(s.providerCtx(), chainID)) + + // check that the chainToChannel and channelToChain exist for the consumer chain ID + _, found = s.providerChain.App.(*appProvider.App).ProviderKeeper.GetChainToChannel(s.providerCtx(), chainID) + s.Require().True(found) + + _, found = s.providerChain.App.(*appProvider.App).ProviderKeeper.GetChannelToChain(s.providerCtx(), s.path.EndpointB.ChannelID) + s.Require().True(found) + + // check that channel is in OPEN state + s.Require().Equal(channeltypes.OPEN, s.path.EndpointB.GetChannel().State) + } + } else { + s.Require().Error(err, "did not return error on invalid case") + } + }) + } +} + +// TODO Simon: implement OnChanCloseConfirm in IBC-GO testing to close the consumer chain's channel end +func (s *ProviderTestSuite) TestStopConsumerOnChannelClosed() { + // init the CCV channel states + s.SetupCCVChannel() + s.SendEmptyVSCPacket() + + // stop the consumer chain + err := s.providerChain.App.(*appProvider.App).ProviderKeeper.StopConsumerChain(s.providerCtx(), s.consumerChain.ChainID, true, true) + s.Require().NoError(err) + + err = s.path.EndpointA.UpdateClient() + s.Require().NoError(err) + + // check that provider chain's channel end is closed + s.Require().Equal(channeltypes.CLOSED, s.path.EndpointB.GetChannel().State) + + // simulate a relayer behaviour + // err = s.path.EndpointA.OnChanCloseConfirm() + // s.Require().NoError(err) + + // expect to panic in consumer chain's BeginBlock due to the above + //s.consumerChain.NextBlock() + + // check that the provider's channel is removed + // _, found := s.consumerChain.App.(*appConsumer.App).ConsumerKeeper.GetProviderChannel(s.consumerCtx()) + // s.Require().False(found) +} + +func (s *ProviderTestSuite) checkConsumerChainIsRemoved(chainID string, lockUbd bool) { + channelID := s.path.EndpointB.ChannelID + providerKeeper := s.providerChain.App.(*appProvider.App).ProviderKeeper + + // check channel's state is closed + s.Require().Equal(channeltypes.CLOSED, s.path.EndpointB.GetChannel().State) + + // check UnbondingOps were deleted and undelegation entries aren't onHold + if !lockUbd { + s.providerChain.App.(*appProvider.App).ProviderKeeper.IterateOverUnbondingOpIndex( + s.providerCtx(), + chainID, + func(vscID uint64, ubdIndex []uint64) bool { + _, found := providerKeeper.GetUnbondingOpIndex(s.providerCtx(), chainID, uint64(vscID)) + s.Require().False(found) + for _, ubdID := range ubdIndex { + _, found = providerKeeper.GetUnbondingOp(s.providerCtx(), ubdIndex[ubdID]) + s.Require().False(found) + ubd, _ := s.providerChain.App.(*appProvider.App).StakingKeeper.GetUnbondingDelegationByUnbondingId(s.providerCtx(), ubdIndex[ubdID]) + s.Require().False(ubd.Entries[ubdID].UnbondingOnHold) + } + return true + }, + ) + + } + + // verify consumer chain's states are removed + s.Require().False(providerKeeper.GetLockUnbondingOnTimeout(s.providerCtx(), chainID)) + s.Require().Zero(providerKeeper.GetConsumerClient(s.providerCtx(), chainID)) + + _, found := providerKeeper.GetChainToChannel(s.providerCtx(), chainID) + s.Require().False(found) + + _, found = providerKeeper.GetChannelToChain(s.providerCtx(), channelID) + s.Require().False(found) + + s.Require().Nil(providerKeeper.GetSlashAcks(s.providerCtx(), chainID)) + s.Require().Zero(providerKeeper.GetInitChainHeight(s.providerCtx(), chainID)) + // TODO Simon: check that pendingVSCPacket are emptied - once + // https://github.com/cosmos/interchain-security/issues/27 is implemented +} + +// TODO Simon: duplicated from consumer/keeper_test.go; figure out how it can be refactored +// SendEmptyVSCPacket sends a VSC packet without any changes +// to ensure that the CCV channel gets established +func (s *ProviderTestSuite) SendEmptyVSCPacket() { + providerKeeper := s.providerChain.App.(*appProvider.App).ProviderKeeper + + oldBlockTime := s.providerChain.GetContext().BlockTime() + timeout := uint64(ccv.GetTimeoutTimestamp(oldBlockTime).UnixNano()) + + valUpdateID := providerKeeper.GetValidatorSetUpdateId(s.providerChain.GetContext()) + + pd := ccv.NewValidatorSetChangePacketData( + []abci.ValidatorUpdate{}, + valUpdateID, + nil, + ) + + seq, ok := s.providerChain.App.(*appProvider.App).GetIBCKeeper().ChannelKeeper.GetNextSequenceSend( + s.providerChain.GetContext(), providertypes.PortID, s.path.EndpointB.ChannelID) + s.Require().True(ok) + + packet := channeltypes.NewPacket(pd.GetBytes(), seq, providertypes.PortID, s.path.EndpointB.ChannelID, + consumertypes.PortID, s.path.EndpointA.ChannelID, clienttypes.Height{}, timeout) + + s.path.EndpointB.SendPacket(packet) + err := s.path.EndpointA.RecvPacket(packet) + s.Require().NoError(err) +} diff --git a/x/ccv/provider/types/keys.go b/x/ccv/provider/types/keys.go index 0519f05646..0209b59983 100644 --- a/x/ccv/provider/types/keys.go +++ b/x/ccv/provider/types/keys.go @@ -1,10 +1,13 @@ package types import ( + "bytes" "crypto/sha256" "encoding/binary" "fmt" "time" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) type Status int @@ -39,6 +42,11 @@ const ( // PendingClientKeyPrefix is the key prefix for storing the pending identified consumer chain client before the spawn time occurs. // The key includes the BigEndian timestamp to allow for efficient chronological iteration PendingClientKeyPrefix = "pendingclient" + + //PendingStopProposalKeyPrefix is the key prefix for storing the pending identified consumer chain before the stop time occurs. + // The key includes the BigEndian timestamp to allow for efficient chronological iteration + PendingStopProposalKeyPrefix = "pendingstopproposal" + // UnbondingOpPrefix is the key prefix that stores a record of all the ids of consumer chains that // need to unbond before a given delegation can unbond on this chain. UnbondingOpPrefix = "unbondingops" @@ -62,6 +70,9 @@ const ( // InitChainHeightPrefix is the key prefix that will store the mapping from a chain id to the corresponding block height on the provider // this consumer chain was initialized InitChainHeightPrefix = "initchainheight" + + // LockUnbondingOnTimeout is the key prefix that will store the consumer chain id which unbonding operations are locked on CCV channel timeout + LockUnbondingOnTimeoutPrefix = "LockUnbondingOnTimeout" ) var ( @@ -106,15 +117,24 @@ func ChainToClientKey(chainID string) []byte { return []byte(fmt.Sprintf("%s/%s", ChainToClientKeyPrefix, chainID)) } -// PendingClientKey returns the key under which a pending identified client is store +// PendingClientKey returns the key under which a pending identified client is stored func PendingClientKey(timestamp time.Time, chainID string) []byte { timeBytes := make([]byte, 8) binary.BigEndian.PutUint64(timeBytes, uint64(timestamp.UnixNano())) + return []byte(fmt.Sprintf("%s/%s/%s", PendingClientKeyPrefix, timeBytes, chainID)) } +// PendingStopProposalKey returns the key under which pending consumer chain stop proposals are stored +func PendingStopProposalKey(timestamp time.Time, chainID string) []byte { + timeBytes := make([]byte, 8) + binary.BigEndian.PutUint64(timeBytes, uint64(timestamp.UnixNano())) + + return []byte(fmt.Sprintf("%s/%s/%s", PendingStopProposalKeyPrefix, timeBytes, chainID)) +} + func UnbondingOpIndexKey(chainID string, valsetUpdateID uint64) []byte { - return AppendMany(HashString(UnbondingOpIndexPrefix), HashString(chainID), Uint64ToBytes(valsetUpdateID)) + return AppendMany(HashString(UnbondingOpIndexPrefix), HashString(chainID), HashString("/"), Uint64ToBytes(valsetUpdateID)) } func UnbondingOpKey(id uint64) []byte { @@ -143,3 +163,18 @@ func SlashAcksKey(chainID string) []byte { func InitChainHeightKey(chainID string) []byte { return []byte(fmt.Sprintf("%s/%s", InitChainHeightPrefix, chainID)) } + +func LockUnbondingOnTimeoutKey(chainID string) []byte { + return []byte(fmt.Sprintf("%s/%s", LockUnbondingOnTimeoutPrefix, chainID)) +} + +func ParseUnbondingOpIndexKey(key []byte) (vscID []byte, err error) { + keySplit := bytes.Split(key, HashString("/")) + if len(keySplit) != 2 { + return nil, sdkerrors.Wrapf( + sdkerrors.ErrLogic, "key provided is incorrect: the key split has incorrect length, expected %d, got %d", 2, len(keySplit), + ) + } + + return keySplit[1], nil +} diff --git a/x/ccv/provider/types/proposal.go b/x/ccv/provider/types/proposal.go index 9ef1b4fef5..bc5a11f3c7 100644 --- a/x/ccv/provider/types/proposal.go +++ b/x/ccv/provider/types/proposal.go @@ -12,6 +12,7 @@ import ( const ( ProposalTypeCreateConsumerChain = "CreateConsumerChain" + ProposalTypeStopConsumerChain = "StopConsumerChain" ) var ( @@ -45,7 +46,9 @@ func (cccp *CreateConsumerChainProposal) GetDescription() string { return cccp.D func (cccp *CreateConsumerChainProposal) ProposalRoute() string { return RouterKey } // ProposalType returns the type of a create consumerchain proposal. -func (cccp *CreateConsumerChainProposal) ProposalType() string { return ProposalTypeCreateConsumerChain } +func (cccp *CreateConsumerChainProposal) ProposalType() string { + return ProposalTypeCreateConsumerChain +} // ValidateBasic runs basic stateless validity checks func (cccp *CreateConsumerChainProposal) ValidateBasic() error { @@ -85,3 +88,35 @@ func (cccp *CreateConsumerChainProposal) String() string { BinaryHash: %s SpawnTime: %s`, cccp.Title, cccp.Description, cccp.ChainId, cccp.InitialHeight, cccp.GenesisHash, cccp.BinaryHash, cccp.SpawnTime) } + +// NewStopConsumerChainProposal creates a new stop consumer chain proposal. +func NewStopConsumerChainProposal(title, description, chainID string, stopTime time.Time) (govtypes.Content, error) { + return &StopConsumerChainProposal{ + Title: title, + Description: description, + ChainId: chainID, + StopTime: stopTime, + }, nil +} + +// ProposalRoute returns the routing key of a stop consumer chain proposal. +func (sccp *StopConsumerChainProposal) ProposalRoute() string { return RouterKey } + +// ProposalType returns the type of a stop consumer chain proposal. +func (sccp *StopConsumerChainProposal) ProposalType() string { return ProposalTypeStopConsumerChain } + +// ValidateBasic runs basic stateless validity checks +func (sccp *StopConsumerChainProposal) ValidateBasic() error { + if err := govtypes.ValidateAbstract(sccp); err != nil { + return err + } + + if strings.TrimSpace(sccp.ChainId) == "" { + return sdkerrors.Wrap(ErrInvalidProposal, "consumer chain id must not be blank") + } + + if sccp.StopTime.IsZero() { + return sdkerrors.Wrap(ErrInvalidProposal, "spawn time cannot be zero") + } + return nil +} diff --git a/x/ccv/provider/types/provider.pb.go b/x/ccv/provider/types/provider.pb.go index 22d6014688..87d5250359 100644 --- a/x/ccv/provider/types/provider.pb.go +++ b/x/ccv/provider/types/provider.pb.go @@ -50,6 +50,10 @@ type CreateConsumerChainProposal struct { // 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. SpawnTime time.Time `protobuf:"bytes,7,opt,name=spawn_time,json=spawnTime,proto3,stdtime" json:"spawn_time"` + // 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. + LockUnbondingOnTimeout bool `protobuf:"varint,8,opt,name=lock_unbonding_on_timeout,json=lockUnbondingOnTimeout,proto3" json:"lock_unbonding_on_timeout,omitempty"` } func (m *CreateConsumerChainProposal) Reset() { *m = CreateConsumerChainProposal{} } @@ -84,6 +88,81 @@ func (m *CreateConsumerChainProposal) XXX_DiscardUnknown() { var xxx_messageInfo_CreateConsumerChainProposal proto.InternalMessageInfo +// 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. +type StopConsumerChainProposal struct { + // the title of the proposal + Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` + // the description of the proposal + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + // the chain-id of the consumer chain to be stopped + ChainId string `protobuf:"bytes,3,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"` + // the time on the provider chain at which all validators are responsible to stop their consumer chain validator node + StopTime time.Time `protobuf:"bytes,4,opt,name=stop_time,json=stopTime,proto3,stdtime" json:"stop_time"` +} + +func (m *StopConsumerChainProposal) Reset() { *m = StopConsumerChainProposal{} } +func (m *StopConsumerChainProposal) String() string { return proto.CompactTextString(m) } +func (*StopConsumerChainProposal) ProtoMessage() {} +func (*StopConsumerChainProposal) Descriptor() ([]byte, []int) { + return fileDescriptor_f22ec409a72b7b72, []int{1} +} +func (m *StopConsumerChainProposal) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *StopConsumerChainProposal) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_StopConsumerChainProposal.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *StopConsumerChainProposal) XXX_Merge(src proto.Message) { + xxx_messageInfo_StopConsumerChainProposal.Merge(m, src) +} +func (m *StopConsumerChainProposal) XXX_Size() int { + return m.Size() +} +func (m *StopConsumerChainProposal) XXX_DiscardUnknown() { + xxx_messageInfo_StopConsumerChainProposal.DiscardUnknown(m) +} + +var xxx_messageInfo_StopConsumerChainProposal proto.InternalMessageInfo + +func (m *StopConsumerChainProposal) GetTitle() string { + if m != nil { + return m.Title + } + return "" +} + +func (m *StopConsumerChainProposal) GetDescription() string { + if m != nil { + return m.Description + } + return "" +} + +func (m *StopConsumerChainProposal) GetChainId() string { + if m != nil { + return m.ChainId + } + return "" +} + +func (m *StopConsumerChainProposal) GetStopTime() time.Time { + if m != nil { + return m.StopTime + } + return time.Time{} +} + // Params defines the parameters for CCV Provider module type Params struct { TemplateClient *types1.ClientState `protobuf:"bytes,1,opt,name=template_client,json=templateClient,proto3" json:"template_client,omitempty"` @@ -93,7 +172,7 @@ func (m *Params) Reset() { *m = Params{} } func (m *Params) String() string { return proto.CompactTextString(m) } func (*Params) ProtoMessage() {} func (*Params) Descriptor() ([]byte, []int) { - return fileDescriptor_f22ec409a72b7b72, []int{1} + return fileDescriptor_f22ec409a72b7b72, []int{2} } func (m *Params) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -138,7 +217,7 @@ func (m *HandshakeMetadata) Reset() { *m = HandshakeMetadata{} } func (m *HandshakeMetadata) String() string { return proto.CompactTextString(m) } func (*HandshakeMetadata) ProtoMessage() {} func (*HandshakeMetadata) Descriptor() ([]byte, []int) { - return fileDescriptor_f22ec409a72b7b72, []int{2} + return fileDescriptor_f22ec409a72b7b72, []int{3} } func (m *HandshakeMetadata) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -183,6 +262,7 @@ func (m *HandshakeMetadata) GetVersion() string { func init() { proto.RegisterType((*CreateConsumerChainProposal)(nil), "interchain_security.ccv.provider.v1.CreateConsumerChainProposal") + proto.RegisterType((*StopConsumerChainProposal)(nil), "interchain_security.ccv.provider.v1.StopConsumerChainProposal") proto.RegisterType((*Params)(nil), "interchain_security.ccv.provider.v1.Params") proto.RegisterType((*HandshakeMetadata)(nil), "interchain_security.ccv.provider.v1.HandshakeMetadata") } @@ -192,42 +272,46 @@ func init() { } var fileDescriptor_f22ec409a72b7b72 = []byte{ - // 545 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x93, 0x4f, 0x6f, 0xd3, 0x4c, - 0x10, 0xc6, 0xed, 0xbe, 0xfd, 0xbb, 0xe9, 0x5b, 0x84, 0xa9, 0x90, 0x29, 0x92, 0x1d, 0xca, 0xa5, - 0x12, 0x62, 0x57, 0x69, 0x6f, 0xbd, 0xd1, 0x48, 0x10, 0x0e, 0x48, 0x51, 0xc8, 0x89, 0x03, 0xd6, - 0x7a, 0x3d, 0xb5, 0x57, 0xd8, 0x5e, 0x6b, 0x77, 0x63, 0xc8, 0x37, 0xe0, 0xd8, 0x23, 0xc7, 0x7e, - 0x9c, 0x1e, 0x38, 0xf4, 0xc8, 0x09, 0x50, 0xf2, 0x45, 0x90, 0x77, 0xed, 0x24, 0x48, 0xdc, 0x76, - 0x9e, 0xf9, 0x8d, 0x3d, 0x33, 0xcf, 0x2e, 0x3a, 0xe7, 0xa5, 0x06, 0xc9, 0x32, 0xca, 0xcb, 0x48, - 0x01, 0x9b, 0x49, 0xae, 0xe7, 0x84, 0xb1, 0x9a, 0x54, 0x52, 0xd4, 0x3c, 0x01, 0x49, 0xea, 0xc1, - 0xea, 0x8c, 0x2b, 0x29, 0xb4, 0xf0, 0x9e, 0xff, 0xa3, 0x06, 0x33, 0x56, 0xe3, 0x15, 0x57, 0x0f, - 0x4e, 0x8e, 0x53, 0x91, 0x0a, 0xc3, 0x93, 0xe6, 0x64, 0x4b, 0x4f, 0xc2, 0x54, 0x88, 0x34, 0x07, - 0x62, 0xa2, 0x78, 0x76, 0x4d, 0x34, 0x2f, 0x40, 0x69, 0x5a, 0x54, 0x1d, 0xc0, 0x63, 0x46, 0x98, - 0x90, 0x40, 0x58, 0xce, 0xa1, 0xd4, 0xcd, 0xef, 0xed, 0xa9, 0x05, 0x48, 0x03, 0xe4, 0x3c, 0xcd, - 0xb4, 0x95, 0x15, 0xd1, 0x50, 0x26, 0x20, 0x0b, 0x6e, 0xe1, 0x75, 0x64, 0x0b, 0x4e, 0xbf, 0x6f, - 0xa1, 0xa7, 0x43, 0x09, 0x54, 0xc3, 0x50, 0x94, 0x6a, 0x56, 0x80, 0x1c, 0x36, 0x9d, 0x8f, 0xa5, - 0xa8, 0x84, 0xa2, 0xb9, 0x77, 0x8c, 0x76, 0x34, 0xd7, 0x39, 0xf8, 0x6e, 0xdf, 0x3d, 0x3b, 0x98, - 0xd8, 0xc0, 0xeb, 0xa3, 0x5e, 0x02, 0x8a, 0x49, 0x5e, 0x69, 0x2e, 0x4a, 0x7f, 0xcb, 0xe4, 0x36, - 0x25, 0xef, 0x09, 0xda, 0xb7, 0x2b, 0xe0, 0x89, 0xff, 0x9f, 0x49, 0xef, 0x99, 0xf8, 0x6d, 0xe2, - 0xbd, 0x41, 0x47, 0xbc, 0xe4, 0x9a, 0xd3, 0x3c, 0xca, 0xa0, 0x69, 0xd5, 0xdf, 0xee, 0xbb, 0x67, - 0xbd, 0xf3, 0x13, 0xcc, 0x63, 0x86, 0x9b, 0xe9, 0x70, 0x3b, 0x53, 0x3d, 0xc0, 0x23, 0x43, 0x5c, - 0x6d, 0xdf, 0xfd, 0x0c, 0x9d, 0xc9, 0xff, 0x6d, 0x9d, 0x15, 0xbd, 0x67, 0xe8, 0x30, 0x85, 0x12, - 0x14, 0x57, 0x51, 0x46, 0x55, 0xe6, 0xef, 0xf4, 0xdd, 0xb3, 0xc3, 0x49, 0xaf, 0xd5, 0x46, 0x54, - 0x65, 0x5e, 0x88, 0x7a, 0x31, 0x2f, 0xa9, 0x9c, 0x5b, 0x62, 0xd7, 0x10, 0xc8, 0x4a, 0x06, 0x18, - 0x22, 0xa4, 0x2a, 0xfa, 0xb9, 0x8c, 0x9a, 0x55, 0xfb, 0x7b, 0x6d, 0x23, 0xd6, 0x07, 0xdc, 0xf9, - 0x80, 0xa7, 0x9d, 0x0f, 0x57, 0xfb, 0x4d, 0x23, 0x37, 0xbf, 0x42, 0x77, 0x72, 0x60, 0xea, 0x9a, - 0xcc, 0xe5, 0xfe, 0xd7, 0xdb, 0xd0, 0xf9, 0x76, 0x1b, 0x3a, 0xa7, 0x1f, 0xd1, 0xee, 0x98, 0x4a, - 0x5a, 0x28, 0x6f, 0x8a, 0x1e, 0x68, 0x28, 0xaa, 0x9c, 0x6a, 0x88, 0xec, 0x38, 0x66, 0x85, 0xbd, - 0xf3, 0x17, 0x66, 0xcc, 0x4d, 0x8f, 0xf0, 0x86, 0x2b, 0xf5, 0x00, 0x0f, 0x8d, 0xfa, 0x5e, 0x53, - 0x0d, 0x93, 0xa3, 0xee, 0x1b, 0x56, 0x3c, 0x8d, 0xd1, 0xc3, 0x11, 0x2d, 0x13, 0x95, 0xd1, 0x4f, - 0xf0, 0x0e, 0x34, 0x4d, 0xa8, 0xa6, 0xde, 0x05, 0x7a, 0xdc, 0xdd, 0xad, 0xe8, 0x1a, 0x20, 0xaa, - 0x84, 0xc8, 0x23, 0x9a, 0x24, 0xb2, 0x35, 0xed, 0x51, 0x97, 0x7d, 0x0d, 0x30, 0x16, 0x22, 0x7f, - 0x95, 0x24, 0xd2, 0xf3, 0xd1, 0x5e, 0x0d, 0x52, 0xad, 0xed, 0xeb, 0xc2, 0xab, 0xe9, 0xdd, 0x22, - 0x70, 0xef, 0x17, 0x81, 0xfb, 0x7b, 0x11, 0xb8, 0x37, 0xcb, 0xc0, 0xb9, 0x5f, 0x06, 0xce, 0x8f, - 0x65, 0xe0, 0x7c, 0xb8, 0x4c, 0xb9, 0xce, 0x66, 0x31, 0x66, 0xa2, 0x20, 0x4c, 0xa8, 0x42, 0x28, - 0xb2, 0xbe, 0xec, 0x2f, 0x57, 0x0f, 0xe4, 0xcb, 0xdf, 0x4f, 0x44, 0xcf, 0x2b, 0x50, 0xf1, 0xae, - 0x59, 0xe6, 0xc5, 0x9f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x06, 0xcf, 0x1a, 0x97, 0x53, 0x03, 0x00, - 0x00, + // 610 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x54, 0xbd, 0x6e, 0xd4, 0x40, + 0x10, 0x3e, 0x93, 0xbf, 0xcb, 0x5e, 0x08, 0xc2, 0x44, 0x91, 0x13, 0xa4, 0xbb, 0xe3, 0x68, 0x4e, + 0x42, 0xd8, 0xba, 0xa4, 0x22, 0x5d, 0x72, 0x12, 0x84, 0x02, 0x11, 0x5d, 0x42, 0x43, 0x81, 0xb5, + 0x5e, 0x4f, 0xec, 0x55, 0xec, 0x1d, 0x6b, 0x77, 0x6d, 0xc8, 0x13, 0x40, 0x99, 0x92, 0x32, 0xaf, + 0xc0, 0x5b, 0xa4, 0x4c, 0x49, 0x05, 0x28, 0x79, 0x11, 0xe4, 0x5d, 0x3b, 0x09, 0x12, 0x0d, 0x0d, + 0xdd, 0xcc, 0x37, 0xdf, 0x67, 0xcf, 0xcc, 0xb7, 0xbb, 0x64, 0x8b, 0x0b, 0x0d, 0x92, 0xa5, 0x94, + 0x8b, 0x50, 0x01, 0x2b, 0x25, 0xd7, 0xa7, 0x01, 0x63, 0x55, 0x50, 0x48, 0xac, 0x78, 0x0c, 0x32, + 0xa8, 0x26, 0x37, 0xb1, 0x5f, 0x48, 0xd4, 0xe8, 0x3e, 0xfd, 0x8b, 0xc6, 0x67, 0xac, 0xf2, 0x6f, + 0x78, 0xd5, 0x64, 0x73, 0x2d, 0xc1, 0x04, 0x0d, 0x3f, 0xa8, 0x23, 0x2b, 0xdd, 0x1c, 0x24, 0x88, + 0x49, 0x06, 0x81, 0xc9, 0xa2, 0xf2, 0x38, 0xd0, 0x3c, 0x07, 0xa5, 0x69, 0x5e, 0xb4, 0x04, 0x1e, + 0xb1, 0x80, 0xa1, 0x84, 0x80, 0x65, 0x1c, 0x84, 0xae, 0x7f, 0x6f, 0xa3, 0x86, 0x10, 0xd4, 0x84, + 0x8c, 0x27, 0xa9, 0xb6, 0xb0, 0x0a, 0x34, 0x88, 0x18, 0x64, 0xce, 0x2d, 0xf9, 0x36, 0xb3, 0x82, + 0xd1, 0xe7, 0x39, 0xf2, 0x78, 0x2a, 0x81, 0x6a, 0x98, 0xa2, 0x50, 0x65, 0x0e, 0x72, 0x5a, 0x77, + 0x7e, 0x20, 0xb1, 0x40, 0x45, 0x33, 0x77, 0x8d, 0x2c, 0x68, 0xae, 0x33, 0xf0, 0x9c, 0xa1, 0x33, + 0x5e, 0x9e, 0xd9, 0xc4, 0x1d, 0x92, 0x5e, 0x0c, 0x8a, 0x49, 0x5e, 0x68, 0x8e, 0xc2, 0xbb, 0x67, + 0x6a, 0x77, 0x21, 0x77, 0x83, 0x74, 0xed, 0x0a, 0x78, 0xec, 0xcd, 0x99, 0xf2, 0x92, 0xc9, 0x5f, + 0xc7, 0xee, 0x2b, 0xb2, 0xca, 0x05, 0xd7, 0x9c, 0x66, 0x61, 0x0a, 0x75, 0xab, 0xde, 0xfc, 0xd0, + 0x19, 0xf7, 0xb6, 0x36, 0x7d, 0x1e, 0x31, 0xbf, 0x9e, 0xce, 0x6f, 0x66, 0xaa, 0x26, 0xfe, 0xbe, + 0x61, 0xec, 0xcd, 0x5f, 0xfc, 0x18, 0x74, 0x66, 0xf7, 0x1b, 0x9d, 0x05, 0xdd, 0x27, 0x64, 0x25, + 0x01, 0x01, 0x8a, 0xab, 0x30, 0xa5, 0x2a, 0xf5, 0x16, 0x86, 0xce, 0x78, 0x65, 0xd6, 0x6b, 0xb0, + 0x7d, 0xaa, 0x52, 0x77, 0x40, 0x7a, 0x11, 0x17, 0x54, 0x9e, 0x5a, 0xc6, 0xa2, 0x61, 0x10, 0x0b, + 0x19, 0xc2, 0x94, 0x10, 0x55, 0xd0, 0x8f, 0x22, 0xac, 0x57, 0xed, 0x2d, 0x35, 0x8d, 0x58, 0x1f, + 0xfc, 0xd6, 0x07, 0xff, 0xa8, 0xf5, 0x61, 0xaf, 0x5b, 0x37, 0x72, 0xf6, 0x73, 0xe0, 0xcc, 0x96, + 0x8d, 0xae, 0xae, 0xb8, 0x2f, 0xc8, 0x46, 0x86, 0xec, 0x24, 0x2c, 0x45, 0x84, 0x22, 0xe6, 0x22, + 0x09, 0xd1, 0x7e, 0x10, 0x4b, 0xed, 0x75, 0x87, 0xce, 0xb8, 0x3b, 0x5b, 0xaf, 0x09, 0xef, 0xda, + 0xfa, 0x5b, 0xa3, 0xc3, 0x52, 0xef, 0x74, 0xbf, 0x9c, 0x0f, 0x3a, 0x5f, 0xcf, 0x07, 0x9d, 0xd1, + 0x37, 0x87, 0x6c, 0x1c, 0x6a, 0x2c, 0xfe, 0x9b, 0x0f, 0xbb, 0x64, 0x59, 0x69, 0x2c, 0xec, 0xe4, + 0xf3, 0xff, 0x30, 0x79, 0xb7, 0x96, 0xd5, 0x85, 0xd1, 0x07, 0xb2, 0x78, 0x40, 0x25, 0xcd, 0x95, + 0x7b, 0x44, 0x1e, 0x68, 0xc8, 0x8b, 0x8c, 0x6a, 0x08, 0xad, 0x7b, 0xa6, 0xd3, 0xde, 0xd6, 0x33, + 0xe3, 0xea, 0xdd, 0x23, 0xe9, 0xdf, 0x39, 0x84, 0xd5, 0xc4, 0x9f, 0x1a, 0xf4, 0x50, 0x53, 0x0d, + 0xb3, 0xd5, 0xf6, 0x1b, 0x16, 0x1c, 0x45, 0xe4, 0xe1, 0x3e, 0x15, 0xb1, 0x4a, 0xe9, 0x09, 0xbc, + 0x01, 0x4d, 0x63, 0xaa, 0xa9, 0xbb, 0x4d, 0xd6, 0xdb, 0xab, 0x14, 0x1e, 0x03, 0x84, 0x05, 0x62, + 0x16, 0xd2, 0x38, 0x96, 0xcd, 0x6e, 0x1e, 0xb5, 0xd5, 0x97, 0x00, 0x07, 0x88, 0xd9, 0x6e, 0x1c, + 0x4b, 0xd7, 0x23, 0x4b, 0x15, 0x48, 0x75, 0xbb, 0xa5, 0x36, 0xdd, 0x3b, 0xba, 0xb8, 0xea, 0x3b, + 0x97, 0x57, 0x7d, 0xe7, 0xd7, 0x55, 0xdf, 0x39, 0xbb, 0xee, 0x77, 0x2e, 0xaf, 0xfb, 0x9d, 0xef, + 0xd7, 0xfd, 0xce, 0xfb, 0x9d, 0x84, 0xeb, 0xb4, 0x8c, 0x7c, 0x86, 0x79, 0xc0, 0x50, 0xe5, 0xa8, + 0x82, 0xdb, 0xbb, 0xfd, 0xfc, 0xe6, 0x3d, 0xf8, 0xf4, 0xe7, 0x8b, 0xa0, 0x4f, 0x0b, 0x50, 0xd1, + 0xa2, 0xd9, 0xe0, 0xf6, 0xef, 0x00, 0x00, 0x00, 0xff, 0xff, 0xc5, 0x6c, 0x6d, 0x4d, 0x42, 0x04, + 0x00, 0x00, } func (m *CreateConsumerChainProposal) Marshal() (dAtA []byte, err error) { @@ -250,6 +334,16 @@ func (m *CreateConsumerChainProposal) MarshalToSizedBuffer(dAtA []byte) (int, er _ = i var l int _ = l + if m.LockUnbondingOnTimeout { + i-- + if m.LockUnbondingOnTimeout { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x40 + } n1, err1 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.SpawnTime, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.SpawnTime):]) if err1 != nil { return 0, err1 @@ -306,6 +400,58 @@ func (m *CreateConsumerChainProposal) MarshalToSizedBuffer(dAtA []byte) (int, er return len(dAtA) - i, nil } +func (m *StopConsumerChainProposal) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *StopConsumerChainProposal) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *StopConsumerChainProposal) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + n3, err3 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.StopTime, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.StopTime):]) + if err3 != nil { + return 0, err3 + } + i -= n3 + i = encodeVarintProvider(dAtA, i, uint64(n3)) + i-- + dAtA[i] = 0x22 + if len(m.ChainId) > 0 { + i -= len(m.ChainId) + copy(dAtA[i:], m.ChainId) + i = encodeVarintProvider(dAtA, i, uint64(len(m.ChainId))) + i-- + dAtA[i] = 0x1a + } + if len(m.Description) > 0 { + i -= len(m.Description) + copy(dAtA[i:], m.Description) + i = encodeVarintProvider(dAtA, i, uint64(len(m.Description))) + i-- + dAtA[i] = 0x12 + } + if len(m.Title) > 0 { + i -= len(m.Title) + copy(dAtA[i:], m.Title) + i = encodeVarintProvider(dAtA, i, uint64(len(m.Title))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func (m *Params) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -419,6 +565,32 @@ func (m *CreateConsumerChainProposal) Size() (n int) { } l = github_com_gogo_protobuf_types.SizeOfStdTime(m.SpawnTime) n += 1 + l + sovProvider(uint64(l)) + if m.LockUnbondingOnTimeout { + n += 2 + } + return n +} + +func (m *StopConsumerChainProposal) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Title) + if l > 0 { + n += 1 + l + sovProvider(uint64(l)) + } + l = len(m.Description) + if l > 0 { + n += 1 + l + sovProvider(uint64(l)) + } + l = len(m.ChainId) + if l > 0 { + n += 1 + l + sovProvider(uint64(l)) + } + l = github_com_gogo_protobuf_types.SizeOfStdTime(m.StopTime) + n += 1 + l + sovProvider(uint64(l)) return n } @@ -717,6 +889,205 @@ func (m *CreateConsumerChainProposal) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 8: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field LockUnbondingOnTimeout", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProvider + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.LockUnbondingOnTimeout = bool(v != 0) + default: + iNdEx = preIndex + skippy, err := skipProvider(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthProvider + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *StopConsumerChainProposal) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProvider + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: StopConsumerChainProposal: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: StopConsumerChainProposal: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Title", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProvider + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthProvider + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthProvider + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Title = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Description", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProvider + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthProvider + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthProvider + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Description = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ChainId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProvider + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthProvider + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthProvider + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ChainId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field StopTime", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProvider + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthProvider + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthProvider + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.StopTime, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipProvider(dAtA[iNdEx:])