diff --git a/CHANGELOG.md b/CHANGELOG.md index 42057e1a1b0..08a7139458d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,6 +115,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features +* (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/modules/core/02-client/keeper/keeper.go b/modules/core/02-client/keeper/keeper.go index 79a33c1ca05..d98e523624b 100644 --- a/modules/core/02-client/keeper/keeper.go +++ b/modules/core/02-client/keeper/keeper.go @@ -359,15 +359,16 @@ func (k Keeper) IterateClients(ctx sdk.Context, cb func(clientID string, cs expo defer iterator.Close() for ; iterator.Valid(); iterator.Next() { - keySplit := strings.Split(string(iterator.Key()), "/") - if keySplit[len(keySplit)-1] != host.KeyClientState { + path := string(iterator.Key()) + if !strings.Contains(path, host.KeyClientState) { + // skip non client state keys continue } + + clientID := host.MustParseClientStatePath(path) clientState := k.MustUnmarshalClientState(iterator.Value()) - // key is ibc/{clientid}/clientState - // Thus, keySplit[1] is clientID - if cb(keySplit[1], clientState) { + if cb(clientID, clientState) { break } } diff --git a/modules/core/24-host/parse.go b/modules/core/24-host/parse.go index 8c3459500d9..ad8f8af90e9 100644 --- a/modules/core/24-host/parse.go +++ b/modules/core/24-host/parse.go @@ -32,6 +32,40 @@ func ParseIdentifier(identifier, prefix string) (uint64, error) { return sequence, nil } +// MustParseClientStatePath returns the client ID from a client state path. It panics +// if the provided path is invalid or if the clientID is empty. +func MustParseClientStatePath(path string) string { + clientID, err := parseClientStatePath(path) + if err != nil { + panic(err.Error()) + } + + return clientID +} + +// parseClientStatePath returns the client ID from a client state path. It returns +// an error if the provided path is invalid. +func parseClientStatePath(path string) (string, error) { + split := strings.Split(path, "/") + if len(split) != 3 { + return "", sdkerrors.Wrapf(ErrInvalidPath, "cannot parse client state path %s", path) + } + + if split[0] != string(KeyClientStorePrefix) { + return "", sdkerrors.Wrapf(ErrInvalidPath, "path does not begin with client store prefix: expected %s, got %s", KeyClientStorePrefix, split[0]) + } + + if split[2] != KeyClientState { + return "", sdkerrors.Wrapf(ErrInvalidPath, "path does not end with client state key: expected %s, got %s", KeyClientState, split[2]) + } + + if strings.TrimSpace(split[1]) == "" { + return "", sdkerrors.Wrap(ErrInvalidPath, "clientID is empty") + } + + return split[1], nil +} + // ParseConnectionPath returns the connection ID from a full path. It returns // an error if the provided path is invalid. func ParseConnectionPath(path string) (string, error) { diff --git a/modules/core/24-host/parse_test.go b/modules/core/24-host/parse_test.go index ea30f671cde..5b83c25ef11 100644 --- a/modules/core/24-host/parse_test.go +++ b/modules/core/24-host/parse_test.go @@ -1,6 +1,7 @@ package host_test import ( + "fmt" "math" "testing" @@ -8,6 +9,7 @@ import ( connectiontypes "github.com/cosmos/ibc-go/v6/modules/core/03-connection/types" host "github.com/cosmos/ibc-go/v6/modules/core/24-host" + ibctesting "github.com/cosmos/ibc-go/v6/testing" ) func TestParseIdentifier(t *testing.T) { @@ -46,3 +48,32 @@ func TestParseIdentifier(t *testing.T) { } } } + +func TestMustParseClientStatePath(t *testing.T) { + testCases := []struct { + name string + path string + expPass bool + }{ + {"valid", host.FullClientStatePath(ibctesting.FirstClientID), true}, + {"path too large", fmt.Sprintf("clients/clients/%s/clientState", ibctesting.FirstClientID), false}, + {"path too small", fmt.Sprintf("clients/%s", ibctesting.FirstClientID), false}, + {"path does not begin with client store", fmt.Sprintf("cli/%s/%s", ibctesting.FirstClientID, host.KeyClientState), false}, + {"path does not end with client state key", fmt.Sprintf("%s/%s/consensus", string(host.KeyClientStorePrefix), ibctesting.FirstClientID), false}, + {"client ID is empty", host.FullClientStatePath(""), false}, + {"client ID is only spaces", host.FullClientStatePath(" "), false}, + } + + for _, tc := range testCases { + if tc.expPass { + require.NotPanics(t, func() { + clientID := host.MustParseClientStatePath(tc.path) + require.Equal(t, ibctesting.FirstClientID, clientID) + }) + } else { + require.Panics(t, func() { + host.MustParseClientStatePath(tc.path) + }) + } + } +}