Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add optional migration pruning for tendermint consensus states #2800

Merged
merged 16 commits into from
Nov 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

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

the headline above needs to be updated (currently it's Migrating from ibc-go v5 to v6)


- 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()
Copy link
Contributor

Choose a reason for hiding this comment

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

we can potentially ignore an error here, is this a big deal?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good question. This error is ignored in all our iterator functions. Looks like the SDK recently added a function to handle this cosmos/cosmos-sdk#11785. Looks like the change hasn't been released yet. Should I update this function to handle the error returned or are we comfortable adding the LogDeferred usage when it is made available to us?

Copy link
Contributor

Choose a reason for hiding this comment

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

Happy to wait for LogDeffered!

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))
Copy link
Contributor

Choose a reason for hiding this comment

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

qq: why does this prefix have a trailing slash while the tendermintClientPrefix above does not?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

example full key:

clients/07-tendermint-0/clientState

tendermintClientPrefix: clients/07-tendermint
clientStore: clients/07-tendermint-0/
clientState key: clientState

Does that make sense? The tendermint client prefix is iterating on the tendermint client type, The next value in the key is -{N}

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)
Comment on lines +68 to +69
Copy link
Contributor

Choose a reason for hiding this comment

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

❤️


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)
Copy link
Contributor

Choose a reason for hiding this comment

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

is there a constant we can use for "06-solomachine-0"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm going to open an issue. I think we might be defining this string in multiple places, so I think it makes sense to fix up in a separate pr

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This function is only used for migrations atm so I thought it'd be useful to return the amount of consensus states pruned for logging purposes

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