Skip to content

Commit 9720607

Browse files
authored
feat: add optional migration pruning for tendermint consensus states (#2800)
feat: add optional in-place store migration function to prune all expired tendermint consensus states
1 parent ddf9baf commit 9720607

File tree

5 files changed

+256
-39
lines changed

5 files changed

+256
-39
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
115115

116116
### Features
117117

118+
* (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.
118119
* (core/24-host) [\#2820](https://github.com/cosmos/ibc-go/pull/2820) Add `MustParseClientStatePath` which parses the clientID from a client state key path.
119120
* (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.
120121
* (testing/simapp) [\#2190](https://github.com/cosmos/ibc-go/pull/2190) Adding the new `x/group` cosmos-sdk module to simapp.

docs/migrations/v6-to-v7.md

Lines changed: 26 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Migrating from ibc-go v5 to v6
1+
# Migrating from ibc-go v6 to v7
22

33
This document is intended to highlight significant changes which may require more information than presented in the CHANGELOG.
44
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
1313

1414
## Chains
1515

16-
- No relevant changes were made in this release.
16+
Chains will perform automatic migrations to remove existing localhost clients and to migrate the solomachine to v3 of the protobuf definition.
17+
18+
An optional upgrade handler has been added to prune expired tendermint consensus states. It may be used during any upgrade (from v7 onwards).
19+
Add the following to the function call to the upgrade handler in `app/app.go`, to perform the optional state pruning.
20+
21+
```go
22+
import (
23+
// ...
24+
ibctm "github.com/cosmos/ibc-go/v6/modules/light-clients/07-tendermint"
25+
)
26+
27+
// ...
28+
29+
app.UpgradeKeeper.SetUpgradeHandler(
30+
upgradeName,
31+
func(ctx sdk.Context, _ upgradetypes.Plan, _ module.VersionMap) (module.VersionMap, error) {
32+
// prune expired tendermint consensus states to save storage space
33+
ibctm.PruneTendermintConsensusStates(ctx, app.Codec, appCodec, keys[ibchost.StoreKey])
34+
35+
return app.mm.RunMigrations(ctx, app.configurator, fromVM)
36+
},
37+
)
38+
```
39+
40+
Checkout the logs to see how many consensus states are pruned.
1741

1842
## IBC Apps
1943

@@ -57,41 +81,6 @@ A zero proof height is now allowed by core IBC and may be passed into `VerifyMem
5781

5882
The `GetRoot` function has been removed from consensus state interface since it was not used by core IBC.
5983

60-
### Light client implementations
61-
62-
The `09-localhost` light client implementation has been removed because it is currently non-functional.
63-
64-
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`.
65-
Add the following to the application upgrade handler in `app/app.go`, calling `MigrateToV6` to perform store migration logic.
66-
67-
```go
68-
import (
69-
// ...
70-
ibcv6 "github.com/cosmos/ibc-go/v6/modules/core/migrations/v6"
71-
)
72-
73-
// ...
74-
75-
app.UpgradeKeeper.SetUpgradeHandler(
76-
upgradeName,
77-
func(ctx sdk.Context, _ upgradetypes.Plan, _ module.VersionMap) (module.VersionMap, error) {
78-
// prune the 09-localhost client from the ibc client store
79-
ibcv6.MigrateToV6(ctx, app.IBCKeeper.ClientKeeper)
80-
81-
return app.mm.RunMigrations(ctx, app.configurator, fromVM)
82-
},
83-
)
84-
```
85-
86-
Please note the above upgrade handler is optional and should only be run if chains have an existing `09-localhost` client stored in state.
87-
A simple query can be performed to check for a `09-localhost` client on chain.
88-
89-
For example:
90-
91-
```
92-
simd query ibc client states | grep 09-localhost
93-
```
94-
9584
### Client Keeper
9685

9786
Keeper function `CheckMisbehaviourAndUpdateState` has been removed since function `UpdateClient` can now handle updating `ClientState` on `ClientMessage` type which can be any `Misbehaviour` implementations.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package tendermint
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/cosmos/cosmos-sdk/codec"
8+
"github.com/cosmos/cosmos-sdk/store/prefix"
9+
storetypes "github.com/cosmos/cosmos-sdk/store/types"
10+
sdk "github.com/cosmos/cosmos-sdk/types"
11+
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
12+
13+
clienttypes "github.com/cosmos/ibc-go/v6/modules/core/02-client/types"
14+
host "github.com/cosmos/ibc-go/v6/modules/core/24-host"
15+
"github.com/cosmos/ibc-go/v6/modules/core/exported"
16+
)
17+
18+
// PruneTendermintConsensusStates prunes all expired tendermint consensus states. This function
19+
// may optionally be called during in-place store migrations. The ibc store key must be provided.
20+
func PruneTendermintConsensusStates(ctx sdk.Context, cdc codec.BinaryCodec, storeKey storetypes.StoreKey) error {
21+
store := ctx.KVStore(storeKey)
22+
23+
// iterate over ibc store with prefix: clients/07-tendermint,
24+
tendermintClientPrefix := []byte(fmt.Sprintf("%s/%s", host.KeyClientStorePrefix, exported.Tendermint))
25+
iterator := sdk.KVStorePrefixIterator(store, tendermintClientPrefix)
26+
27+
var clientIDs []string
28+
29+
// collect all clients to avoid performing store state changes during iteration
30+
defer iterator.Close()
31+
for ; iterator.Valid(); iterator.Next() {
32+
path := string(iterator.Key())
33+
if !strings.Contains(path, host.KeyClientState) {
34+
// skip non client state keys
35+
continue
36+
}
37+
38+
clientID := host.MustParseClientStatePath(path)
39+
clientIDs = append(clientIDs, clientID)
40+
}
41+
42+
// keep track of the total consensus states pruned so chains can
43+
// understand how much space is saved when the migration is run
44+
var totalPruned int
45+
46+
for _, clientID := range clientIDs {
47+
clientPrefix := []byte(fmt.Sprintf("%s/%s/", host.KeyClientStorePrefix, clientID))
48+
clientStore := prefix.NewStore(ctx.KVStore(storeKey), clientPrefix)
49+
50+
bz := clientStore.Get(host.ClientStateKey())
51+
if bz == nil {
52+
return clienttypes.ErrClientNotFound
53+
}
54+
55+
var clientState exported.ClientState
56+
if err := cdc.UnmarshalInterface(bz, &clientState); err != nil {
57+
return sdkerrors.Wrap(err, "failed to unmarshal client state bytes into tendermint client state")
58+
}
59+
60+
tmClientState, ok := clientState.(*ClientState)
61+
if !ok {
62+
return sdkerrors.Wrap(clienttypes.ErrInvalidClient, "client state is not tendermint even though client id contains 07-tendermint")
63+
}
64+
65+
totalPruned += PruneAllExpiredConsensusStates(ctx, clientStore, cdc, tmClientState)
66+
}
67+
68+
clientLogger := ctx.Logger().With("module", "x/"+host.ModuleName+"/"+clienttypes.SubModuleName)
69+
clientLogger.Info("pruned expired tendermint consensus states", "total", totalPruned)
70+
71+
return nil
72+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package tendermint_test
2+
3+
import (
4+
"time"
5+
6+
clienttypes "github.com/cosmos/ibc-go/v6/modules/core/02-client/types"
7+
host "github.com/cosmos/ibc-go/v6/modules/core/24-host"
8+
"github.com/cosmos/ibc-go/v6/modules/core/exported"
9+
ibctm "github.com/cosmos/ibc-go/v6/modules/light-clients/07-tendermint"
10+
ibctesting "github.com/cosmos/ibc-go/v6/testing"
11+
)
12+
13+
// test pruning of multiple expired tendermint consensus states
14+
func (suite *TendermintTestSuite) TestPruneTendermintConsensusStates() {
15+
// create multiple tendermint clients and a solo machine client
16+
// the solo machine is used to verify this pruning function only modifies
17+
// the tendermint store.
18+
19+
numTMClients := 3
20+
paths := make([]*ibctesting.Path, numTMClients)
21+
22+
for i := 0; i < numTMClients; i++ {
23+
path := ibctesting.NewPath(suite.chainA, suite.chainB)
24+
suite.coordinator.SetupClients(path)
25+
26+
paths[i] = path
27+
}
28+
29+
solomachine := ibctesting.NewSolomachine(suite.T(), suite.chainA.Codec, "06-solomachine-0", "testing", 1)
30+
smClientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), solomachine.ClientID)
31+
32+
// set client state
33+
bz, err := suite.chainA.App.AppCodec().MarshalInterface(solomachine.ClientState())
34+
suite.Require().NoError(err)
35+
smClientStore.Set(host.ClientStateKey(), bz)
36+
37+
bz, err = suite.chainA.App.AppCodec().MarshalInterface(solomachine.ConsensusState())
38+
suite.Require().NoError(err)
39+
smHeight := clienttypes.NewHeight(0, 1)
40+
smClientStore.Set(host.ConsensusStateKey(smHeight), bz)
41+
42+
pruneHeightMap := make(map[*ibctesting.Path][]exported.Height)
43+
unexpiredHeightMap := make(map[*ibctesting.Path][]exported.Height)
44+
45+
for _, path := range paths {
46+
// collect all heights expected to be pruned
47+
var pruneHeights []exported.Height
48+
pruneHeights = append(pruneHeights, path.EndpointA.GetClientState().GetLatestHeight())
49+
50+
// these heights will be expired and also pruned
51+
for i := 0; i < 3; i++ {
52+
err := path.EndpointA.UpdateClient()
53+
suite.Require().NoError(err)
54+
55+
pruneHeights = append(pruneHeights, path.EndpointA.GetClientState().GetLatestHeight())
56+
}
57+
58+
// double chedck all information is currently stored
59+
for _, pruneHeight := range pruneHeights {
60+
consState, ok := suite.chainA.GetConsensusState(path.EndpointA.ClientID, pruneHeight)
61+
suite.Require().True(ok)
62+
suite.Require().NotNil(consState)
63+
64+
ctx := suite.chainA.GetContext()
65+
clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(ctx, path.EndpointA.ClientID)
66+
67+
processedTime, ok := ibctm.GetProcessedTime(clientStore, pruneHeight)
68+
suite.Require().True(ok)
69+
suite.Require().NotNil(processedTime)
70+
71+
processedHeight, ok := ibctm.GetProcessedHeight(clientStore, pruneHeight)
72+
suite.Require().True(ok)
73+
suite.Require().NotNil(processedHeight)
74+
75+
expectedConsKey := ibctm.GetIterationKey(clientStore, pruneHeight)
76+
suite.Require().NotNil(expectedConsKey)
77+
}
78+
pruneHeightMap[path] = pruneHeights
79+
}
80+
81+
// Increment the time by a week
82+
suite.coordinator.IncrementTimeBy(7 * 24 * time.Hour)
83+
84+
for _, path := range paths {
85+
// create the consensus state that can be used as trusted height for next update
86+
var unexpiredHeights []exported.Height
87+
err := path.EndpointA.UpdateClient()
88+
suite.Require().NoError(err)
89+
unexpiredHeights = append(unexpiredHeights, path.EndpointA.GetClientState().GetLatestHeight())
90+
91+
err = path.EndpointA.UpdateClient()
92+
suite.Require().NoError(err)
93+
unexpiredHeights = append(unexpiredHeights, path.EndpointA.GetClientState().GetLatestHeight())
94+
95+
unexpiredHeightMap[path] = unexpiredHeights
96+
}
97+
98+
// Increment the time by another week, then update the client.
99+
// This will cause the consensus states created before the first time increment
100+
// to be expired
101+
suite.coordinator.IncrementTimeBy(7 * 24 * time.Hour)
102+
err = ibctm.PruneTendermintConsensusStates(suite.chainA.GetContext(), suite.chainA.App.AppCodec(), suite.chainA.GetSimApp().GetKey(host.StoreKey))
103+
suite.Require().NoError(err)
104+
105+
for _, path := range paths {
106+
ctx := suite.chainA.GetContext()
107+
clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(ctx, path.EndpointA.ClientID)
108+
109+
// ensure everything has been pruned
110+
for i, pruneHeight := range pruneHeightMap[path] {
111+
consState, ok := suite.chainA.GetConsensusState(path.EndpointA.ClientID, pruneHeight)
112+
suite.Require().False(ok, i)
113+
suite.Require().Nil(consState, i)
114+
115+
processedTime, ok := ibctm.GetProcessedTime(clientStore, pruneHeight)
116+
suite.Require().False(ok, i)
117+
suite.Require().Equal(uint64(0), processedTime, i)
118+
119+
processedHeight, ok := ibctm.GetProcessedHeight(clientStore, pruneHeight)
120+
suite.Require().False(ok, i)
121+
suite.Require().Nil(processedHeight, i)
122+
123+
expectedConsKey := ibctm.GetIterationKey(clientStore, pruneHeight)
124+
suite.Require().Nil(expectedConsKey, i)
125+
}
126+
127+
// ensure metadata is set for unexpired consensus state
128+
for _, height := range unexpiredHeightMap[path] {
129+
consState, ok := suite.chainA.GetConsensusState(path.EndpointA.ClientID, height)
130+
suite.Require().True(ok)
131+
suite.Require().NotNil(consState)
132+
133+
processedTime, ok := ibctm.GetProcessedTime(clientStore, height)
134+
suite.Require().True(ok)
135+
suite.Require().NotEqual(uint64(0), processedTime)
136+
137+
processedHeight, ok := ibctm.GetProcessedHeight(clientStore, height)
138+
suite.Require().True(ok)
139+
suite.Require().NotEqual(clienttypes.ZeroHeight(), processedHeight)
140+
141+
consKey := ibctm.GetIterationKey(clientStore, height)
142+
suite.Require().Equal(host.ConsensusStateKey(height), consKey)
143+
}
144+
}
145+
146+
// verify that solomachine client and consensus state were not removed
147+
smClientStore = suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), solomachine.ClientID)
148+
bz = smClientStore.Get(host.ClientStateKey())
149+
suite.Require().NotEmpty(bz)
150+
151+
bz = smClientStore.Get(host.ConsensusStateKey(smHeight))
152+
suite.Require().NotEmpty(bz)
153+
}

modules/light-clients/07-tendermint/store.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,11 +273,11 @@ func GetPreviousConsensusState(clientStore sdk.KVStore, cdc codec.BinaryCodec, h
273273

274274
// PruneAllExpiredConsensusStates iterates over all consensus states for a given
275275
// client store. If a consensus state is expired, it is deleted and its metadata
276-
// is deleted.
276+
// is deleted. The number of consensus states pruned is returned.
277277
func PruneAllExpiredConsensusStates(
278278
ctx sdk.Context, clientStore sdk.KVStore,
279279
cdc codec.BinaryCodec, clientState *ClientState,
280-
) {
280+
) int {
281281
var heights []exported.Height
282282

283283
pruneCb := func(height exported.Height) bool {
@@ -299,6 +299,8 @@ func PruneAllExpiredConsensusStates(
299299
deleteConsensusState(clientStore, height)
300300
deleteConsensusMetadata(clientStore, height)
301301
}
302+
303+
return len(heights)
302304
}
303305

304306
// Helper function for GetNextConsensusState and GetPreviousConsensusState

0 commit comments

Comments
 (0)