Skip to content

Commit

Permalink
feat: add optional migration pruning for tendermint consensus states (#…
Browse files Browse the repository at this point in the history
…2800)

feat: add optional in-place store migration function to prune all expired tendermint consensus states
  • Loading branch information
colin-axner authored Nov 29, 2022
1 parent ddf9baf commit 9720607
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 39 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
63 changes: 26 additions & 37 deletions docs/migrations/v6-to-v7.md
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand Down
72 changes: 72 additions & 0 deletions modules/light-clients/07-tendermint/migrations.go
Original file line number Diff line number Diff line change
@@ -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
}
153 changes: 153 additions & 0 deletions modules/light-clients/07-tendermint/migrations_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
6 changes: 4 additions & 2 deletions modules/light-clients/07-tendermint/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -299,6 +299,8 @@ func PruneAllExpiredConsensusStates(
deleteConsensusState(clientStore, height)
deleteConsensusMetadata(clientStore, height)
}

return len(heights)
}

// Helper function for GetNextConsensusState and GetPreviousConsensusState
Expand Down

0 comments on commit 9720607

Please sign in to comment.