diff --git a/CHANGELOG.md b/CHANGELOG.md index 08a7139458d..17cea4b72d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,6 +115,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features +* (light-clients/07-tendermint) [\#2800](https://github.com/cosmos/ibc-go/pull/2800) Add optional in-place store migration function to prune all expired tendermint consensus states. * (core/24-host) [\#2820](https://github.com/cosmos/ibc-go/pull/2820) Add `MustParseClientStatePath` which parses the clientID from a client state key path. * (apps/27-interchain-accounts) [\#2147](https://github.com/cosmos/ibc-go/pull/2147) Adding a `SubmitTx` gRPC endpoint for the ICS27 Controller module which allows owners of interchain accounts to submit transactions. This replaces the previously existing need for authentication modules to implement this standard functionality. * (testing/simapp) [\#2190](https://github.com/cosmos/ibc-go/pull/2190) Adding the new `x/group` cosmos-sdk module to simapp. diff --git a/docs/migrations/v6-to-v7.md b/docs/migrations/v6-to-v7.md index c43bddd8dcf..77c9268644e 100644 --- a/docs/migrations/v6-to-v7.md +++ b/docs/migrations/v6-to-v7.md @@ -1,4 +1,4 @@ -# Migrating from ibc-go v5 to v6 +# Migrating from ibc-go v6 to v7 This document is intended to highlight significant changes which may require more information than presented in the CHANGELOG. Any changes that must be done by a user of ibc-go should be documented here. @@ -13,7 +13,31 @@ There are four sections based on the four potential user groups of this document ## Chains -- No relevant changes were made in this release. +Chains will perform automatic migrations to remove existing localhost clients and to migrate the solomachine to v3 of the protobuf definition. + +An optional upgrade handler has been added to prune expired tendermint consensus states. It may be used during any upgrade (from v7 onwards). +Add the following to the function call to the upgrade handler in `app/app.go`, to perform the optional state pruning. + +```go +import ( + // ... + ibctm "github.com/cosmos/ibc-go/v6/modules/light-clients/07-tendermint" +) + +// ... + +app.UpgradeKeeper.SetUpgradeHandler( + upgradeName, + func(ctx sdk.Context, _ upgradetypes.Plan, _ module.VersionMap) (module.VersionMap, error) { + // prune expired tendermint consensus states to save storage space + ibctm.PruneTendermintConsensusStates(ctx, app.Codec, appCodec, keys[ibchost.StoreKey]) + + return app.mm.RunMigrations(ctx, app.configurator, fromVM) + }, +) +``` + +Checkout the logs to see how many consensus states are pruned. ## IBC Apps @@ -57,41 +81,6 @@ A zero proof height is now allowed by core IBC and may be passed into `VerifyMem The `GetRoot` function has been removed from consensus state interface since it was not used by core IBC. -### Light client implementations - -The `09-localhost` light client implementation has been removed because it is currently non-functional. - -An upgrade handler has been added to supply chain developers with the logic needed to prune the ibc client store and successfully complete the removal of `09-localhost`. -Add the following to the application upgrade handler in `app/app.go`, calling `MigrateToV6` to perform store migration logic. - -```go -import ( - // ... - ibcv6 "github.com/cosmos/ibc-go/v6/modules/core/migrations/v6" -) - -// ... - -app.UpgradeKeeper.SetUpgradeHandler( - upgradeName, - func(ctx sdk.Context, _ upgradetypes.Plan, _ module.VersionMap) (module.VersionMap, error) { - // prune the 09-localhost client from the ibc client store - ibcv6.MigrateToV6(ctx, app.IBCKeeper.ClientKeeper) - - return app.mm.RunMigrations(ctx, app.configurator, fromVM) - }, -) -``` - -Please note the above upgrade handler is optional and should only be run if chains have an existing `09-localhost` client stored in state. -A simple query can be performed to check for a `09-localhost` client on chain. - -For example: - -``` -simd query ibc client states | grep 09-localhost -``` - ### Client Keeper Keeper function `CheckMisbehaviourAndUpdateState` has been removed since function `UpdateClient` can now handle updating `ClientState` on `ClientMessage` type which can be any `Misbehaviour` implementations. diff --git a/modules/light-clients/07-tendermint/migrations.go b/modules/light-clients/07-tendermint/migrations.go new file mode 100644 index 00000000000..c369e6b6836 --- /dev/null +++ b/modules/light-clients/07-tendermint/migrations.go @@ -0,0 +1,72 @@ +package tendermint + +import ( + "fmt" + "strings" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store/prefix" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + clienttypes "github.com/cosmos/ibc-go/v6/modules/core/02-client/types" + host "github.com/cosmos/ibc-go/v6/modules/core/24-host" + "github.com/cosmos/ibc-go/v6/modules/core/exported" +) + +// PruneTendermintConsensusStates prunes all expired tendermint consensus states. This function +// may optionally be called during in-place store migrations. The ibc store key must be provided. +func PruneTendermintConsensusStates(ctx sdk.Context, cdc codec.BinaryCodec, storeKey storetypes.StoreKey) error { + store := ctx.KVStore(storeKey) + + // iterate over ibc store with prefix: clients/07-tendermint, + tendermintClientPrefix := []byte(fmt.Sprintf("%s/%s", host.KeyClientStorePrefix, exported.Tendermint)) + iterator := sdk.KVStorePrefixIterator(store, tendermintClientPrefix) + + var clientIDs []string + + // collect all clients to avoid performing store state changes during iteration + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + path := string(iterator.Key()) + if !strings.Contains(path, host.KeyClientState) { + // skip non client state keys + continue + } + + clientID := host.MustParseClientStatePath(path) + clientIDs = append(clientIDs, clientID) + } + + // keep track of the total consensus states pruned so chains can + // understand how much space is saved when the migration is run + var totalPruned int + + for _, clientID := range clientIDs { + clientPrefix := []byte(fmt.Sprintf("%s/%s/", host.KeyClientStorePrefix, clientID)) + clientStore := prefix.NewStore(ctx.KVStore(storeKey), clientPrefix) + + bz := clientStore.Get(host.ClientStateKey()) + if bz == nil { + return clienttypes.ErrClientNotFound + } + + var clientState exported.ClientState + if err := cdc.UnmarshalInterface(bz, &clientState); err != nil { + return sdkerrors.Wrap(err, "failed to unmarshal client state bytes into tendermint client state") + } + + tmClientState, ok := clientState.(*ClientState) + if !ok { + return sdkerrors.Wrap(clienttypes.ErrInvalidClient, "client state is not tendermint even though client id contains 07-tendermint") + } + + totalPruned += PruneAllExpiredConsensusStates(ctx, clientStore, cdc, tmClientState) + } + + clientLogger := ctx.Logger().With("module", "x/"+host.ModuleName+"/"+clienttypes.SubModuleName) + clientLogger.Info("pruned expired tendermint consensus states", "total", totalPruned) + + return nil +} diff --git a/modules/light-clients/07-tendermint/migrations_test.go b/modules/light-clients/07-tendermint/migrations_test.go new file mode 100644 index 00000000000..c7fe56af5db --- /dev/null +++ b/modules/light-clients/07-tendermint/migrations_test.go @@ -0,0 +1,153 @@ +package tendermint_test + +import ( + "time" + + clienttypes "github.com/cosmos/ibc-go/v6/modules/core/02-client/types" + host "github.com/cosmos/ibc-go/v6/modules/core/24-host" + "github.com/cosmos/ibc-go/v6/modules/core/exported" + ibctm "github.com/cosmos/ibc-go/v6/modules/light-clients/07-tendermint" + ibctesting "github.com/cosmos/ibc-go/v6/testing" +) + +// test pruning of multiple expired tendermint consensus states +func (suite *TendermintTestSuite) TestPruneTendermintConsensusStates() { + // create multiple tendermint clients and a solo machine client + // the solo machine is used to verify this pruning function only modifies + // the tendermint store. + + numTMClients := 3 + paths := make([]*ibctesting.Path, numTMClients) + + for i := 0; i < numTMClients; i++ { + path := ibctesting.NewPath(suite.chainA, suite.chainB) + suite.coordinator.SetupClients(path) + + paths[i] = path + } + + solomachine := ibctesting.NewSolomachine(suite.T(), suite.chainA.Codec, "06-solomachine-0", "testing", 1) + smClientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), solomachine.ClientID) + + // set client state + bz, err := suite.chainA.App.AppCodec().MarshalInterface(solomachine.ClientState()) + suite.Require().NoError(err) + smClientStore.Set(host.ClientStateKey(), bz) + + bz, err = suite.chainA.App.AppCodec().MarshalInterface(solomachine.ConsensusState()) + suite.Require().NoError(err) + smHeight := clienttypes.NewHeight(0, 1) + smClientStore.Set(host.ConsensusStateKey(smHeight), bz) + + pruneHeightMap := make(map[*ibctesting.Path][]exported.Height) + unexpiredHeightMap := make(map[*ibctesting.Path][]exported.Height) + + for _, path := range paths { + // collect all heights expected to be pruned + var pruneHeights []exported.Height + pruneHeights = append(pruneHeights, path.EndpointA.GetClientState().GetLatestHeight()) + + // these heights will be expired and also pruned + for i := 0; i < 3; i++ { + err := path.EndpointA.UpdateClient() + suite.Require().NoError(err) + + pruneHeights = append(pruneHeights, path.EndpointA.GetClientState().GetLatestHeight()) + } + + // double chedck all information is currently stored + for _, pruneHeight := range pruneHeights { + consState, ok := suite.chainA.GetConsensusState(path.EndpointA.ClientID, pruneHeight) + suite.Require().True(ok) + suite.Require().NotNil(consState) + + ctx := suite.chainA.GetContext() + clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(ctx, path.EndpointA.ClientID) + + processedTime, ok := ibctm.GetProcessedTime(clientStore, pruneHeight) + suite.Require().True(ok) + suite.Require().NotNil(processedTime) + + processedHeight, ok := ibctm.GetProcessedHeight(clientStore, pruneHeight) + suite.Require().True(ok) + suite.Require().NotNil(processedHeight) + + expectedConsKey := ibctm.GetIterationKey(clientStore, pruneHeight) + suite.Require().NotNil(expectedConsKey) + } + pruneHeightMap[path] = pruneHeights + } + + // Increment the time by a week + suite.coordinator.IncrementTimeBy(7 * 24 * time.Hour) + + for _, path := range paths { + // create the consensus state that can be used as trusted height for next update + var unexpiredHeights []exported.Height + err := path.EndpointA.UpdateClient() + suite.Require().NoError(err) + unexpiredHeights = append(unexpiredHeights, path.EndpointA.GetClientState().GetLatestHeight()) + + err = path.EndpointA.UpdateClient() + suite.Require().NoError(err) + unexpiredHeights = append(unexpiredHeights, path.EndpointA.GetClientState().GetLatestHeight()) + + unexpiredHeightMap[path] = unexpiredHeights + } + + // Increment the time by another week, then update the client. + // This will cause the consensus states created before the first time increment + // to be expired + suite.coordinator.IncrementTimeBy(7 * 24 * time.Hour) + err = ibctm.PruneTendermintConsensusStates(suite.chainA.GetContext(), suite.chainA.App.AppCodec(), suite.chainA.GetSimApp().GetKey(host.StoreKey)) + suite.Require().NoError(err) + + for _, path := range paths { + ctx := suite.chainA.GetContext() + clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(ctx, path.EndpointA.ClientID) + + // ensure everything has been pruned + for i, pruneHeight := range pruneHeightMap[path] { + consState, ok := suite.chainA.GetConsensusState(path.EndpointA.ClientID, pruneHeight) + suite.Require().False(ok, i) + suite.Require().Nil(consState, i) + + processedTime, ok := ibctm.GetProcessedTime(clientStore, pruneHeight) + suite.Require().False(ok, i) + suite.Require().Equal(uint64(0), processedTime, i) + + processedHeight, ok := ibctm.GetProcessedHeight(clientStore, pruneHeight) + suite.Require().False(ok, i) + suite.Require().Nil(processedHeight, i) + + expectedConsKey := ibctm.GetIterationKey(clientStore, pruneHeight) + suite.Require().Nil(expectedConsKey, i) + } + + // ensure metadata is set for unexpired consensus state + for _, height := range unexpiredHeightMap[path] { + consState, ok := suite.chainA.GetConsensusState(path.EndpointA.ClientID, height) + suite.Require().True(ok) + suite.Require().NotNil(consState) + + processedTime, ok := ibctm.GetProcessedTime(clientStore, height) + suite.Require().True(ok) + suite.Require().NotEqual(uint64(0), processedTime) + + processedHeight, ok := ibctm.GetProcessedHeight(clientStore, height) + suite.Require().True(ok) + suite.Require().NotEqual(clienttypes.ZeroHeight(), processedHeight) + + consKey := ibctm.GetIterationKey(clientStore, height) + suite.Require().Equal(host.ConsensusStateKey(height), consKey) + } + } + + // verify that solomachine client and consensus state were not removed + smClientStore = suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), solomachine.ClientID) + bz = smClientStore.Get(host.ClientStateKey()) + suite.Require().NotEmpty(bz) + + bz = smClientStore.Get(host.ConsensusStateKey(smHeight)) + suite.Require().NotEmpty(bz) +} diff --git a/modules/light-clients/07-tendermint/store.go b/modules/light-clients/07-tendermint/store.go index d46d9b01796..ce513c2cd68 100644 --- a/modules/light-clients/07-tendermint/store.go +++ b/modules/light-clients/07-tendermint/store.go @@ -273,11 +273,11 @@ func GetPreviousConsensusState(clientStore sdk.KVStore, cdc codec.BinaryCodec, h // PruneAllExpiredConsensusStates iterates over all consensus states for a given // client store. If a consensus state is expired, it is deleted and its metadata -// is deleted. +// is deleted. The number of consensus states pruned is returned. func PruneAllExpiredConsensusStates( ctx sdk.Context, clientStore sdk.KVStore, cdc codec.BinaryCodec, clientState *ClientState, -) { +) int { var heights []exported.Height pruneCb := func(height exported.Height) bool { @@ -299,6 +299,8 @@ func PruneAllExpiredConsensusStates( deleteConsensusState(clientStore, height) deleteConsensusMetadata(clientStore, height) } + + return len(heights) } // Helper function for GetNextConsensusState and GetPreviousConsensusState