-
Notifications
You must be signed in to change notification settings - Fork 608
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
Changes from all commits
bd37d18
7ad76de
5e9f208
5cb1262
33c64d6
21ab154
f6bbe6f
f7d8871
348b21e
969cc0b
4399a42
78fd0a3
20e4d8d
b95f5f0
54334ce
69961f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can potentially ignore an error here, is this a big deal? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. example full key:
tendermintClientPrefix: Does that make sense? The tendermint client prefix is iterating on the tendermint client type, The next value in the key is |
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❤️ |
||
|
||
return nil | ||
} |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there a constant we can use for There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
@@ -299,6 +299,8 @@ func PruneAllExpiredConsensusStates( | |
deleteConsensusState(clientStore, height) | ||
deleteConsensusMetadata(clientStore, height) | ||
} | ||
|
||
return len(heights) | ||
} | ||
|
||
// Helper function for GetNextConsensusState and GetPreviousConsensusState | ||
|
There was a problem hiding this comment.
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
)