From 63df4554c1686fb59eb3a6c47fddb6efd2a4e1ab Mon Sep 17 00:00:00 2001 From: Bo Du Date: Tue, 8 Nov 2022 03:45:51 -0500 Subject: [PATCH] feat: add localhost client + testing --- modules/core/02-client/genesis.go | 7 + modules/core/02-client/keeper/keeper.go | 5 + modules/core/02-client/types/genesis.go | 2 +- modules/core/02-client/types/keys.go | 6 + modules/core/02-client/types/params.go | 2 +- .../core/03-connection/keeper/handshake.go | 36 +- modules/core/03-connection/keeper/keeper.go | 5 + modules/core/03-connection/keeper/verify.go | 134 ++-- modules/core/exported/client.go | 4 + .../09-localhost/types/client_state.go | 341 ++++++++++ .../09-localhost/types/client_state_test.go | 634 ++++++++++++++++++ .../09-localhost/types/localhost_test.go | 27 + testing/app.go | 1 + testing/config.go | 10 + testing/coordinator.go | 8 + testing/endpoint.go | 21 +- testing/path.go | 14 + 17 files changed, 1167 insertions(+), 90 deletions(-) create mode 100644 modules/light-clients/09-localhost/types/client_state.go create mode 100644 modules/light-clients/09-localhost/types/client_state_test.go create mode 100644 modules/light-clients/09-localhost/types/localhost_test.go diff --git a/modules/core/02-client/genesis.go b/modules/core/02-client/genesis.go index 9f49c288e5a..08624d44006 100644 --- a/modules/core/02-client/genesis.go +++ b/modules/core/02-client/genesis.go @@ -8,6 +8,7 @@ import ( "github.com/cosmos/ibc-go/v6/modules/core/02-client/keeper" "github.com/cosmos/ibc-go/v6/modules/core/02-client/types" "github.com/cosmos/ibc-go/v6/modules/core/exported" + localhosttypes "github.com/cosmos/ibc-go/v6/modules/light-clients/09-localhost/types" ) // InitGenesis initializes the ibc client submodule's state from a provided genesis @@ -46,6 +47,12 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, gs types.GenesisState) { } k.SetNextClientSequence(ctx, gs.NextClientSequence) + + if gs.CreateLocalhost { + revision := uint64(0) + clientState := localhosttypes.NewClientState("", types.NewHeight(revision, 1)) // height start at block 1 + k.SetClientState(ctx, exported.Localhost, clientState) + } } // ExportGenesis returns the ibc client submodule's exported genesis. diff --git a/modules/core/02-client/keeper/keeper.go b/modules/core/02-client/keeper/keeper.go index 79a33c1ca05..2882c039897 100644 --- a/modules/core/02-client/keeper/keeper.go +++ b/modules/core/02-client/keeper/keeper.go @@ -270,6 +270,11 @@ func (k Keeper) GetSelfConsensusState(ctx sdk.Context, height exported.Height) ( // Client must be in same revision as the executing chain func (k Keeper) ValidateSelfClient(ctx sdk.Context, clientState exported.ClientState) error { tmClient, ok := clientState.(*ibctm.ClientState) + + if clientState.ClientType() == exported.Localhost { + return nil + } + if !ok { return sdkerrors.Wrapf(types.ErrInvalidClient, "client must be a Tendermint client, expected: %T, got: %T", &ibctm.ClientState{}, tmClient) diff --git a/modules/core/02-client/types/genesis.go b/modules/core/02-client/types/genesis.go index c1c30666639..a5190c12534 100644 --- a/modules/core/02-client/types/genesis.go +++ b/modules/core/02-client/types/genesis.go @@ -89,7 +89,7 @@ func DefaultGenesisState() GenesisState { Clients: []IdentifiedClientState{}, ClientsConsensus: ClientsConsensusStates{}, Params: DefaultParams(), - CreateLocalhost: false, + CreateLocalhost: true, NextClientSequence: 0, } } diff --git a/modules/core/02-client/types/keys.go b/modules/core/02-client/types/keys.go index 74597232d7c..29aff1dc5f4 100644 --- a/modules/core/02-client/types/keys.go +++ b/modules/core/02-client/types/keys.go @@ -9,6 +9,7 @@ import ( sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" host "github.com/cosmos/ibc-go/v6/modules/core/24-host" + "github.com/cosmos/ibc-go/v6/modules/core/exported" ) const ( @@ -45,6 +46,11 @@ func IsValidClientID(clientID string) bool { // ParseClientIdentifier parses the client type and sequence from the client identifier. func ParseClientIdentifier(clientID string) (string, uint64, error) { + // Localhost client ID == client type + if clientID == exported.Localhost { + return clientID, 0, nil + } + if !IsClientIDFormat(clientID) { return "", 0, sdkerrors.Wrapf(host.ErrInvalidID, "invalid client identifier %s is not in format: `{client-type}-{N}`", clientID) } diff --git a/modules/core/02-client/types/params.go b/modules/core/02-client/types/params.go index 38a341f8e38..3033c86a822 100644 --- a/modules/core/02-client/types/params.go +++ b/modules/core/02-client/types/params.go @@ -11,7 +11,7 @@ import ( var ( // DefaultAllowedClients are "06-solomachine" and "07-tendermint" - DefaultAllowedClients = []string{exported.Solomachine, exported.Tendermint} + DefaultAllowedClients = []string{exported.Solomachine, exported.Tendermint, exported.Localhost} // KeyAllowedClients is store's key for AllowedClients Params KeyAllowedClients = []byte("AllowedClients") diff --git a/modules/core/03-connection/keeper/handshake.go b/modules/core/03-connection/keeper/handshake.go index cb4b4240a41..4abccc72c94 100644 --- a/modules/core/03-connection/keeper/handshake.go +++ b/modules/core/03-connection/keeper/handshake.go @@ -74,12 +74,8 @@ func (k Keeper) ConnOpenTry( // generate a new connection connectionID := k.GenerateConnectionIdentifier(ctx) - selfHeight := clienttypes.GetSelfHeight(ctx) - if consensusHeight.GTE(selfHeight) { - return "", sdkerrors.Wrapf( - sdkerrors.ErrInvalidHeight, - "consensus height is greater than or equal to the current block height (%s >= %s)", consensusHeight, selfHeight, - ) + if err := k.validateHeight(ctx, clientState.ClientType(), consensusHeight); err != nil { + return "", err } // validate client parameters of a chainB client stored on chainA @@ -163,13 +159,8 @@ func (k Keeper) ConnOpenAck( proofHeight exported.Height, // height that relayer constructed proofTry consensusHeight exported.Height, // latest height of chainA that chainB has stored on its chainA client ) error { - // Check that chainB client hasn't stored invalid height - selfHeight := clienttypes.GetSelfHeight(ctx) - if consensusHeight.GTE(selfHeight) { - return sdkerrors.Wrapf( - sdkerrors.ErrInvalidHeight, - "consensus height is greater than or equal to the current block height (%s >= %s)", consensusHeight, selfHeight, - ) + if err := k.validateHeight(ctx, clientState.ClientType(), consensusHeight); err != nil { + return err } // Retrieve connection @@ -295,3 +286,22 @@ func (k Keeper) ConnOpenConfirm( return nil } + +func (k Keeper) validateHeight( + ctx sdk.Context, + clientType string, + consensusHeight exported.Height, // latest height of chain B which chain A has stored in its chain B client +) error { + if clientType == exported.Localhost { + return nil + } + + selfHeight := clienttypes.GetSelfHeight(ctx) + if consensusHeight.GTE(selfHeight) { + return sdkerrors.Wrapf( + sdkerrors.ErrInvalidHeight, + "consensus height is greater than or equal to the current block height (%s >= %s)", consensusHeight, selfHeight, + ) + } + return nil +} diff --git a/modules/core/03-connection/keeper/keeper.go b/modules/core/03-connection/keeper/keeper.go index 35a1a764319..e10d01c1094 100644 --- a/modules/core/03-connection/keeper/keeper.go +++ b/modules/core/03-connection/keeper/keeper.go @@ -86,6 +86,11 @@ func (k Keeper) SetConnection(ctx sdk.Context, connectionID string, connection t // GetTimestampAtHeight returns the timestamp in nanoseconds of the consensus state at the // given height. func (k Keeper) GetTimestampAtHeight(ctx sdk.Context, connection types.ConnectionEnd, height exported.Height) (uint64, error) { + // Localhost client does not have a consensus state, use current block time. + if connection.ClientId == exported.Localhost { + return uint64(ctx.BlockTime().UnixNano()), nil + } + clientState, found := k.clientKeeper.GetClientState(ctx, connection.GetClientID()) if !found { return 0, sdkerrors.Wrapf( diff --git a/modules/core/03-connection/keeper/verify.go b/modules/core/03-connection/keeper/verify.go index 64c6cdb43cf..83774a2733c 100644 --- a/modules/core/03-connection/keeper/verify.go +++ b/modules/core/03-connection/keeper/verify.go @@ -12,6 +12,7 @@ import ( commitmenttypes "github.com/cosmos/ibc-go/v6/modules/core/23-commitment/types" host "github.com/cosmos/ibc-go/v6/modules/core/24-host" "github.com/cosmos/ibc-go/v6/modules/core/exported" + localhosttypes "github.com/cosmos/ibc-go/v6/modules/light-clients/09-localhost/types" ) // VerifyClientState verifies a proof of a client state of the running machine @@ -73,19 +74,13 @@ func (k Keeper) VerifyClientConsensusState( consensusState exported.ConsensusState, ) error { clientID := connection.GetClientID() - clientStore := k.clientKeeper.ClientStore(ctx, clientID) - - clientState, found := k.clientKeeper.GetClientState(ctx, clientID) - if !found { - return sdkerrors.Wrap(clienttypes.ErrClientNotFound, clientID) - } - - if status := clientState.Status(ctx, clientStore, k.cdc); status != exported.Active { - return sdkerrors.Wrapf(clienttypes.ErrClientNotActive, "client (%s) status is %s", clientID, status) + clientState, store, err := k.getClientStateAndKVStore(ctx, clientID) + if err != nil { + return err } merklePath := commitmenttypes.NewMerklePath(host.FullConsensusStatePath(connection.GetCounterparty().GetClientID(), consensusHeight)) - merklePath, err := commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) + merklePath, err = commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) if err != nil { return err } @@ -101,7 +96,7 @@ func (k Keeper) VerifyClientConsensusState( } if err := clientState.VerifyMembership( - ctx, clientStore, k.cdc, height, + ctx, store, k.cdc, height, 0, 0, // skip delay period checks for non-packet processing verification proof, path, bz, ); err != nil { @@ -122,19 +117,13 @@ func (k Keeper) VerifyConnectionState( counterpartyConnection exported.ConnectionI, // opposite connection ) error { clientID := connection.GetClientID() - clientStore := k.clientKeeper.ClientStore(ctx, clientID) - - clientState, found := k.clientKeeper.GetClientState(ctx, clientID) - if !found { - return sdkerrors.Wrap(clienttypes.ErrClientNotFound, clientID) - } - - if status := clientState.Status(ctx, clientStore, k.cdc); status != exported.Active { - return sdkerrors.Wrapf(clienttypes.ErrClientNotActive, "client (%s) status is %s", clientID, status) + clientState, store, err := k.getClientStateAndKVStore(ctx, clientID) + if err != nil { + return err } merklePath := commitmenttypes.NewMerklePath(host.ConnectionPath(connectionID)) - merklePath, err := commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) + merklePath, err = commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) if err != nil { return err } @@ -155,7 +144,7 @@ func (k Keeper) VerifyConnectionState( } if err := clientState.VerifyMembership( - ctx, clientStore, k.cdc, height, + ctx, store, k.cdc, height, 0, 0, // skip delay period checks for non-packet processing verification proof, path, bz, ); err != nil { @@ -177,19 +166,17 @@ func (k Keeper) VerifyChannelState( channel exported.ChannelI, ) error { clientID := connection.GetClientID() - clientStore := k.clientKeeper.ClientStore(ctx, clientID) - - clientState, found := k.clientKeeper.GetClientState(ctx, clientID) - if !found { - return sdkerrors.Wrap(clienttypes.ErrClientNotFound, clientID) + clientState, store, err := k.getClientStateAndKVStore(ctx, clientID) + if err != nil { + return err } - if status := clientState.Status(ctx, clientStore, k.cdc); status != exported.Active { + if status := clientState.Status(ctx, store, k.cdc); status != exported.Active { return sdkerrors.Wrapf(clienttypes.ErrClientNotActive, "client (%s) status is %s", clientID, status) } merklePath := commitmenttypes.NewMerklePath(host.ChannelPath(portID, channelID)) - merklePath, err := commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) + merklePath, err = commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) if err != nil { return err } @@ -210,7 +197,7 @@ func (k Keeper) VerifyChannelState( } if err := clientState.VerifyMembership( - ctx, clientStore, k.cdc, height, + ctx, store, k.cdc, height, 0, 0, // skip delay period checks for non-packet processing verification proof, path, bz, ); err != nil { @@ -233,15 +220,9 @@ func (k Keeper) VerifyPacketCommitment( commitmentBytes []byte, ) error { clientID := connection.GetClientID() - clientStore := k.clientKeeper.ClientStore(ctx, clientID) - - clientState, found := k.clientKeeper.GetClientState(ctx, clientID) - if !found { - return sdkerrors.Wrap(clienttypes.ErrClientNotFound, clientID) - } - - if status := clientState.Status(ctx, clientStore, k.cdc); status != exported.Active { - return sdkerrors.Wrapf(clienttypes.ErrClientNotActive, "client (%s) status is %s", clientID, status) + clientState, store, err := k.getClientStateAndKVStore(ctx, clientID) + if err != nil { + return err } // get time and block delays @@ -249,7 +230,7 @@ func (k Keeper) VerifyPacketCommitment( blockDelay := k.getBlockDelay(ctx, connection) merklePath := commitmenttypes.NewMerklePath(host.PacketCommitmentPath(portID, channelID, sequence)) - merklePath, err := commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) + merklePath, err = commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) if err != nil { return err } @@ -260,7 +241,7 @@ func (k Keeper) VerifyPacketCommitment( } if err := clientState.VerifyMembership( - ctx, clientStore, k.cdc, height, + ctx, store, k.cdc, height, timeDelay, blockDelay, proof, path, commitmentBytes, ); err != nil { @@ -283,15 +264,9 @@ func (k Keeper) VerifyPacketAcknowledgement( acknowledgement []byte, ) error { clientID := connection.GetClientID() - clientStore := k.clientKeeper.ClientStore(ctx, clientID) - - clientState, found := k.clientKeeper.GetClientState(ctx, clientID) - if !found { - return sdkerrors.Wrap(clienttypes.ErrClientNotFound, clientID) - } - - if status := clientState.Status(ctx, clientStore, k.cdc); status != exported.Active { - return sdkerrors.Wrapf(clienttypes.ErrClientNotActive, "client (%s) status is %s", clientID, status) + clientState, store, err := k.getClientStateAndKVStore(ctx, clientID) + if err != nil { + return err } // get time and block delays @@ -299,7 +274,7 @@ func (k Keeper) VerifyPacketAcknowledgement( blockDelay := k.getBlockDelay(ctx, connection) merklePath := commitmenttypes.NewMerklePath(host.PacketAcknowledgementPath(portID, channelID, sequence)) - merklePath, err := commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) + merklePath, err = commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) if err != nil { return err } @@ -310,7 +285,7 @@ func (k Keeper) VerifyPacketAcknowledgement( } if err := clientState.VerifyMembership( - ctx, clientStore, k.cdc, height, + ctx, store, k.cdc, height, timeDelay, blockDelay, proof, path, channeltypes.CommitAcknowledgement(acknowledgement), ); err != nil { @@ -333,15 +308,9 @@ func (k Keeper) VerifyPacketReceiptAbsence( sequence uint64, ) error { clientID := connection.GetClientID() - clientStore := k.clientKeeper.ClientStore(ctx, clientID) - - clientState, found := k.clientKeeper.GetClientState(ctx, clientID) - if !found { - return sdkerrors.Wrap(clienttypes.ErrClientNotFound, clientID) - } - - if status := clientState.Status(ctx, clientStore, k.cdc); status != exported.Active { - return sdkerrors.Wrapf(clienttypes.ErrClientNotActive, "client (%s) status is %s", clientID, status) + clientState, store, err := k.getClientStateAndKVStore(ctx, clientID) + if err != nil { + return err } // get time and block delays @@ -349,7 +318,7 @@ func (k Keeper) VerifyPacketReceiptAbsence( blockDelay := k.getBlockDelay(ctx, connection) merklePath := commitmenttypes.NewMerklePath(host.PacketReceiptPath(portID, channelID, sequence)) - merklePath, err := commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) + merklePath, err = commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) if err != nil { return err } @@ -360,7 +329,7 @@ func (k Keeper) VerifyPacketReceiptAbsence( } if err := clientState.VerifyNonMembership( - ctx, clientStore, k.cdc, height, + ctx, store, k.cdc, height, timeDelay, blockDelay, proof, path, ); err != nil { @@ -382,15 +351,9 @@ func (k Keeper) VerifyNextSequenceRecv( nextSequenceRecv uint64, ) error { clientID := connection.GetClientID() - clientStore := k.clientKeeper.ClientStore(ctx, clientID) - - clientState, found := k.clientKeeper.GetClientState(ctx, clientID) - if !found { - return sdkerrors.Wrap(clienttypes.ErrClientNotFound, clientID) - } - - if status := clientState.Status(ctx, clientStore, k.cdc); status != exported.Active { - return sdkerrors.Wrapf(clienttypes.ErrClientNotActive, "client (%s) status is %s", clientID, status) + clientState, store, err := k.getClientStateAndKVStore(ctx, clientID) + if err != nil { + return err } // get time and block delays @@ -398,7 +361,7 @@ func (k Keeper) VerifyNextSequenceRecv( blockDelay := k.getBlockDelay(ctx, connection) merklePath := commitmenttypes.NewMerklePath(host.NextSequenceRecvPath(portID, channelID)) - merklePath, err := commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) + merklePath, err = commitmenttypes.ApplyPrefix(connection.GetCounterparty().GetPrefix(), merklePath) if err != nil { return err } @@ -409,7 +372,7 @@ func (k Keeper) VerifyNextSequenceRecv( } if err := clientState.VerifyMembership( - ctx, clientStore, k.cdc, height, + ctx, store, k.cdc, height, timeDelay, blockDelay, proof, path, sdk.Uint64ToBigEndian(nextSequenceRecv), ); err != nil { @@ -433,3 +396,26 @@ func (k Keeper) getBlockDelay(ctx sdk.Context, connection exported.ConnectionI) timeDelay := connection.GetDelayPeriod() return uint64(math.Ceil(float64(timeDelay) / float64(expectedTimePerBlock))) } + +func (k Keeper) getClientStateAndKVStore(ctx sdk.Context, clientID string) (exported.ClientState, sdk.KVStore, error) { + clientStore := k.clientKeeper.ClientStore(ctx, clientID) + + clientState, found := k.clientKeeper.GetClientState(ctx, clientID) + if !found { + return nil, nil, sdkerrors.Wrap(clienttypes.ErrClientNotFound, clientID) + } + + var store sdk.KVStore + switch clientState.(type) { + case *localhosttypes.ClientState: + store = ctx.KVStore(k.storeKey) + default: + store = clientStore + } + + if status := clientState.Status(ctx, store, k.cdc); status != exported.Active { + return nil, nil, sdkerrors.Wrapf(clienttypes.ErrClientNotActive, "client (%s) status is %s", clientID, status) + } + + return clientState, store, nil +} diff --git a/modules/core/exported/client.go b/modules/core/exported/client.go index ac76a647dc5..ebcf1af2cfb 100644 --- a/modules/core/exported/client.go +++ b/modules/core/exported/client.go @@ -19,6 +19,10 @@ const ( // Tendermint is used to indicate that the client uses the Tendermint Consensus Algorithm. Tendermint string = "07-tendermint" + // Localhost is the client type for a localhost client. It is also used as the clientID + // for the localhost client. + Localhost string = "09-localhost" + // Active is a status type of a client. An active client is allowed to be used. Active Status = "Active" diff --git a/modules/light-clients/09-localhost/types/client_state.go b/modules/light-clients/09-localhost/types/client_state.go new file mode 100644 index 00000000000..ed91f70d410 --- /dev/null +++ b/modules/light-clients/09-localhost/types/client_state.go @@ -0,0 +1,341 @@ +package types + +import ( + "bytes" + "encoding/binary" + "reflect" + "strings" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + clienttypes "github.com/cosmos/ibc-go/v5/modules/core/02-client/types" + connectiontypes "github.com/cosmos/ibc-go/v5/modules/core/03-connection/types" + channeltypes "github.com/cosmos/ibc-go/v5/modules/core/04-channel/types" + host "github.com/cosmos/ibc-go/v5/modules/core/24-host" + "github.com/cosmos/ibc-go/v5/modules/core/exported" +) + +var _ exported.ClientState = (*ClientState)(nil) + +// NewClientState creates a new ClientState instance +func NewClientState(chainID string, height clienttypes.Height) *ClientState { + return &ClientState{ + ChainId: chainID, + Height: height, + } +} + +// GetChainID returns an empty string +func (cs ClientState) GetChainID() string { + return cs.ChainId +} + +// ClientType is localhost. +func (cs ClientState) ClientType() string { + return exported.Localhost +} + +// GetLatestHeight returns the latest height stored. +func (cs ClientState) GetLatestHeight() exported.Height { + return cs.Height +} + +// Status always returns Active. The localhost status cannot be changed. +func (cs ClientState) Status(_ sdk.Context, _ sdk.KVStore, _ codec.BinaryCodec, +) exported.Status { + return exported.Active +} + +// Validate performs a basic validation of the client state fields. +func (cs ClientState) Validate() error { + if strings.TrimSpace(cs.ChainId) == "" { + return sdkerrors.Wrap(sdkerrors.ErrInvalidChainID, "chain id cannot be blank") + } + if cs.Height.RevisionHeight == 0 { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidHeight, "local revision height cannot be zero") + } + return nil +} + +// ZeroCustomFields returns the same client state since there are no custom fields in localhost +func (cs ClientState) ZeroCustomFields() exported.ClientState { + return &cs +} + +// Initialize ensures that initial consensus state for localhost is nil +func (cs ClientState) Initialize(_ sdk.Context, _ codec.BinaryCodec, _ sdk.KVStore, consState exported.ConsensusState) error { + if consState != nil { + return sdkerrors.Wrap(clienttypes.ErrInvalidConsensus, "initial consensus state for localhost must be nil.") + } + return nil +} + +// ExportMetadata is a no-op for localhost client +func (cs ClientState) ExportMetadata(_ sdk.KVStore) []exported.GenesisMetadata { + return nil +} + +// CheckHeaderAndUpdateState updates the localhost client. It only needs access to the context +func (cs *ClientState) CheckHeaderAndUpdateState( + ctx sdk.Context, _ codec.BinaryCodec, _ sdk.KVStore, _ exported.Header, +) (exported.ClientState, exported.ConsensusState, error) { + // use the chain ID from context since the localhost client is from the running chain (i.e self). + cs.ChainId = ctx.ChainID() + revision := clienttypes.ParseChainID(cs.ChainId) + cs.Height = clienttypes.NewHeight(revision, uint64(ctx.BlockHeight())) + return cs, nil, nil +} + +// CheckMisbehaviourAndUpdateState implements ClientState +// Since localhost is the client of the running chain, misbehaviour cannot be submitted to it +// Thus, CheckMisbehaviourAndUpdateState returns an error for localhost +func (cs ClientState) CheckMisbehaviourAndUpdateState( + _ sdk.Context, _ codec.BinaryCodec, _ sdk.KVStore, _ exported.Misbehaviour, +) (exported.ClientState, error) { + return nil, sdkerrors.Wrap(clienttypes.ErrInvalidMisbehaviour, "cannot submit misbehaviour to localhost client") +} + +// CheckSubstituteAndUpdateState returns an error. The localhost cannot be modified by +// proposals. +func (cs ClientState) CheckSubstituteAndUpdateState( + ctx sdk.Context, _ codec.BinaryCodec, _, _ sdk.KVStore, + _ exported.ClientState, +) (exported.ClientState, error) { + return nil, sdkerrors.Wrap(clienttypes.ErrUpdateClientFailed, "cannot update localhost client with a proposal") +} + +// VerifyUpgradeAndUpdateState returns an error since localhost cannot be upgraded +func (cs ClientState) VerifyUpgradeAndUpdateState( + _ sdk.Context, _ codec.BinaryCodec, _ sdk.KVStore, + _ exported.ClientState, _ exported.ConsensusState, _, _ []byte, +) (exported.ClientState, exported.ConsensusState, error) { + return nil, nil, sdkerrors.Wrap(clienttypes.ErrInvalidUpgradeClient, "cannot upgrade localhost client") +} + +// VerifyClientState verifies that the localhost client state is stored locally +func (cs ClientState) VerifyClientState( + store sdk.KVStore, cdc codec.BinaryCodec, + _ exported.Height, _ exported.Prefix, _ string, _ []byte, clientState exported.ClientState, +) error { + path := host.ClientStateKey() + bz := store.Get(path) + if bz == nil { + return sdkerrors.Wrapf(clienttypes.ErrFailedClientStateVerification, + "not found for path: %s", path) + } + + selfClient := clienttypes.MustUnmarshalClientState(cdc, bz) + + if !reflect.DeepEqual(selfClient, clientState) { + return sdkerrors.Wrapf(clienttypes.ErrFailedClientStateVerification, + "stored clientState != provided clientState: \n%v\n≠\n%v", + selfClient, clientState, + ) + } + return nil +} + +// VerifyClientConsensusState returns nil since a local host client does not store consensus +// states. +func (cs ClientState) VerifyClientConsensusState( + sdk.KVStore, codec.BinaryCodec, + exported.Height, string, exported.Height, exported.Prefix, + []byte, exported.ConsensusState, +) error { + return nil +} + +// VerifyConnectionState verifies a proof of the connection state of the +// specified connection end stored locally. +func (cs ClientState) VerifyConnectionState( + store sdk.KVStore, + cdc codec.BinaryCodec, + _ exported.Height, + _ exported.Prefix, + _ []byte, + connectionID string, + connectionEnd exported.ConnectionI, +) error { + path := host.ConnectionKey(connectionID) + bz := store.Get(path) + if bz == nil { + return sdkerrors.Wrapf(clienttypes.ErrFailedConnectionStateVerification, "not found for path %s", path) + } + + var prevConnection connectiontypes.ConnectionEnd + err := cdc.Unmarshal(bz, &prevConnection) + if err != nil { + return err + } + + if !reflect.DeepEqual(prevConnection, connectionEnd) { + return sdkerrors.Wrapf( + clienttypes.ErrFailedConnectionStateVerification, + "connection end ≠ previous stored connection: \n%v\n≠\n%v", connectionEnd, prevConnection, + ) + } + + return nil +} + +// VerifyChannelState verifies a proof of the channel state of the specified +// channel end, under the specified port, stored on the local machine. +func (cs ClientState) VerifyChannelState( + store sdk.KVStore, + cdc codec.BinaryCodec, + _ exported.Height, + _ exported.Prefix, + _ []byte, + portID, + channelID string, + channel exported.ChannelI, +) error { + path := host.ChannelKey(portID, channelID) + bz := store.Get(path) + if bz == nil { + return sdkerrors.Wrapf(clienttypes.ErrFailedChannelStateVerification, "not found for path %s", path) + } + + var prevChannel channeltypes.Channel + err := cdc.Unmarshal(bz, &prevChannel) + if err != nil { + return err + } + + if !reflect.DeepEqual(prevChannel, channel) { + return sdkerrors.Wrapf( + clienttypes.ErrFailedChannelStateVerification, + "channel end ≠ previous stored channel: \n%v\n≠\n%v", channel, prevChannel, + ) + } + + return nil +} + +// VerifyPacketCommitment verifies a proof of an outgoing packet commitment at +// the specified port, specified channel, and specified sequence. +func (cs ClientState) VerifyPacketCommitment( + ctx sdk.Context, + store sdk.KVStore, + _ codec.BinaryCodec, + _ exported.Height, + _ uint64, + _ uint64, + _ exported.Prefix, + _ []byte, + portID, + channelID string, + sequence uint64, + commitmentBytes []byte, +) error { + path := host.PacketCommitmentKey(portID, channelID, sequence) + + data := store.Get(path) + if len(data) == 0 { + return sdkerrors.Wrapf(clienttypes.ErrFailedPacketCommitmentVerification, "not found for path %s", path) + } + + if !bytes.Equal(data, commitmentBytes) { + return sdkerrors.Wrapf( + clienttypes.ErrFailedPacketCommitmentVerification, + "commitment ≠ previous commitment: \n%X\n≠\n%X", commitmentBytes, data, + ) + } + + return nil +} + +// VerifyPacketAcknowledgement verifies a proof of an incoming packet +// acknowledgement at the specified port, specified channel, and specified sequence. +func (cs ClientState) VerifyPacketAcknowledgement( + ctx sdk.Context, + store sdk.KVStore, + _ codec.BinaryCodec, + _ exported.Height, + _ uint64, + _ uint64, + _ exported.Prefix, + _ []byte, + portID, + channelID string, + sequence uint64, + acknowledgement []byte, +) error { + path := host.PacketAcknowledgementKey(portID, channelID, sequence) + + data := store.Get(path) + if len(data) == 0 { + return sdkerrors.Wrapf(clienttypes.ErrFailedPacketAckVerification, "not found for path %s", path) + } + + commit := channeltypes.CommitAcknowledgement(acknowledgement) + if !bytes.Equal(data, commit) { + return sdkerrors.Wrapf( + clienttypes.ErrFailedPacketAckVerification, + "ack bytes ≠ previous ack: \n%X\n≠\n%X", acknowledgement, data, + ) + } + + return nil +} + +// VerifyPacketReceiptAbsence verifies a proof of the absence of an +// incoming packet receipt at the specified port, specified channel, and +// specified sequence. +func (cs ClientState) VerifyPacketReceiptAbsence( + ctx sdk.Context, + store sdk.KVStore, + _ codec.BinaryCodec, + _ exported.Height, + _ uint64, + _ uint64, + _ exported.Prefix, + _ []byte, + portID, + channelID string, + sequence uint64, +) error { + path := host.PacketReceiptKey(portID, channelID, sequence) + + data := store.Get(path) + if data != nil { + return sdkerrors.Wrap(clienttypes.ErrFailedPacketReceiptVerification, "expected no packet receipt") + } + + return nil +} + +// VerifyNextSequenceRecv verifies a proof of the next sequence number to be +// received of the specified channel at the specified port. +func (cs ClientState) VerifyNextSequenceRecv( + ctx sdk.Context, + store sdk.KVStore, + _ codec.BinaryCodec, + _ exported.Height, + _ uint64, + _ uint64, + _ exported.Prefix, + _ []byte, + portID, + channelID string, + nextSequenceRecv uint64, +) error { + path := host.NextSequenceRecvKey(portID, channelID) + + data := store.Get(path) + if len(data) == 0 { + return sdkerrors.Wrapf(clienttypes.ErrFailedNextSeqRecvVerification, "not found for path %s", path) + } + + prevSequenceRecv := binary.BigEndian.Uint64(data) + if prevSequenceRecv+1 != nextSequenceRecv { + return sdkerrors.Wrapf( + clienttypes.ErrFailedNextSeqRecvVerification, + "next sequence receive ≠ previous stored sequence (%d ≠ %d)", nextSequenceRecv, prevSequenceRecv, + ) + } + + return nil +} diff --git a/modules/light-clients/09-localhost/types/client_state_test.go b/modules/light-clients/09-localhost/types/client_state_test.go new file mode 100644 index 00000000000..68ec77155e9 --- /dev/null +++ b/modules/light-clients/09-localhost/types/client_state_test.go @@ -0,0 +1,634 @@ +package types_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/cosmos-sdk/codec" + clienttypes "github.com/cosmos/ibc-go/v5/modules/core/02-client/types" + connectiontypes "github.com/cosmos/ibc-go/v5/modules/core/03-connection/types" + + channeltypes "github.com/cosmos/ibc-go/v5/modules/core/04-channel/types" + commitmenttypes "github.com/cosmos/ibc-go/v5/modules/core/23-commitment/types" + host "github.com/cosmos/ibc-go/v5/modules/core/24-host" + "github.com/cosmos/ibc-go/v5/modules/core/exported" + ibctmtypes "github.com/cosmos/ibc-go/v5/modules/light-clients/07-tendermint/types" + "github.com/cosmos/ibc-go/v5/modules/light-clients/09-localhost/types" + ibctesting "github.com/cosmos/ibc-go/v5/testing" + ibcmock "github.com/cosmos/ibc-go/v5/testing/mock" +) + +const ( + testConnectionID = "connectionid" + testPortID = "testportid" + testChannelID = "testchannelid" + testSequence = 1 +) + +func (suite *LocalhostTestSuite) TestStatus() { + ctx := suite.chain.GetContext() + clientState := types.NewClientState("chainID", clienttypes.NewHeight(3, 10)) + + // localhost should always return active + status := clientState.Status(ctx, nil, nil) + suite.Require().Equal(exported.Active, status) +} + +func (suite *LocalhostTestSuite) TestValidate() { + testCases := []struct { + name string + clientState *types.ClientState + expPass bool + }{ + { + name: "valid client", + clientState: types.NewClientState("chainID", clienttypes.NewHeight(3, 10)), + expPass: true, + }, + { + name: "invalid chain id", + clientState: types.NewClientState(" ", clienttypes.NewHeight(3, 10)), + expPass: false, + }, + { + name: "invalid height", + clientState: types.NewClientState("chainID", clienttypes.ZeroHeight()), + expPass: false, + }, + } + + for _, tc := range testCases { + err := tc.clientState.Validate() + if tc.expPass { + suite.Require().NoError(err, tc.name) + } else { + suite.Require().Error(err, tc.name) + } + } +} + +func (suite *LocalhostTestSuite) TestInitialize() { + testCases := []struct { + name string + consState exported.ConsensusState + expPass bool + }{ + { + "valid initialization", + nil, + true, + }, + { + "invalid consenus state", + &ibctmtypes.ConsensusState{}, + false, + }, + } + + clientState := types.NewClientState("chainID", clienttypes.NewHeight(3, 10)) + + for _, tc := range testCases { + err := clientState.Initialize(suite.chain.GetContext(), suite.chain.Codec, nil, tc.consState) + + if tc.expPass { + suite.Require().NoError(err, "valid testcase: %s failed", tc.name) + } else { + suite.Require().Error(err, "invalid testcase: %s passed", tc.name) + } + } +} + +func (suite *LocalhostTestSuite) TestVerifyClientState() { + clientState := types.NewClientState("chainID", clienttypes.Height{}) + invalidClient := types.NewClientState("chainID", clienttypes.NewHeight(0, 12)) + testCases := []struct { + name string + clientState *types.ClientState + malleate func(codec.BinaryCodec, sdk.KVStore) + counterparty *types.ClientState + expPass bool + }{ + { + name: "proof verification success", + clientState: clientState, + malleate: func(cdc codec.BinaryCodec, store sdk.KVStore) { + bz := clienttypes.MustMarshalClientState(cdc, clientState) + store.Set(host.ClientStateKey(), bz) + }, + counterparty: clientState, + expPass: true, + }, + { + name: "proof verification failed: invalid client", + clientState: clientState, + malleate: func(cdc codec.BinaryCodec, store sdk.KVStore) { + bz := clienttypes.MustMarshalClientState(cdc, clientState) + store.Set(host.ClientStateKey(), bz) + }, + counterparty: invalidClient, + expPass: false, + }, + { + name: "proof verification failed: client not stored", + clientState: clientState, + malleate: func(cdc codec.BinaryCodec, store sdk.KVStore) {}, + counterparty: clientState, + expPass: false, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.SetupTest() + cdc := suite.chain.Codec + store := suite.chain.GetContext().KVStore(suite.chain.App.GetKey(host.StoreKey)) + tc.malleate(cdc, store) + + err := tc.clientState.VerifyClientState( + store, cdc, clienttypes.NewHeight(0, 10), nil, "", []byte{}, tc.counterparty, + ) + + if tc.expPass { + suite.Require().NoError(err) + } else { + suite.Require().Error(err) + } + }) + } +} + +func (suite *LocalhostTestSuite) TestVerifyClientConsensusState() { + clientState := types.NewClientState("chainID", clienttypes.Height{}) + err := clientState.VerifyClientConsensusState( + nil, nil, nil, "", nil, nil, nil, nil, + ) + suite.Require().NoError(err) +} + +func (suite *LocalhostTestSuite) TestCheckHeaderAndUpdateState() { + ctx := suite.chain.GetContext() + clientState := types.NewClientState("chainID", clienttypes.Height{}) + cs, _, err := clientState.CheckHeaderAndUpdateState(ctx, nil, nil, nil) + suite.Require().NoError(err) + suite.Require().Equal(uint64(0), cs.GetLatestHeight().GetRevisionNumber()) + suite.Require().Equal(ctx.BlockHeight(), int64(cs.GetLatestHeight().GetRevisionHeight())) + suite.Require().Equal(ctx.BlockHeader().ChainID, clientState.ChainId) +} + +func (suite *LocalhostTestSuite) TestMisbehaviourAndUpdateState() { + ctx := suite.chain.GetContext() + clientState := types.NewClientState("chainID", clienttypes.Height{}) + cs, err := clientState.CheckMisbehaviourAndUpdateState(ctx, nil, nil, nil) + suite.Require().Error(err) + suite.Require().Nil(cs) +} + +func (suite *LocalhostTestSuite) TestProposedHeaderAndUpdateState() { + ctx := suite.chain.GetContext() + clientState := types.NewClientState("chainID", clienttypes.Height{}) + cs, err := clientState.CheckSubstituteAndUpdateState(ctx, nil, nil, nil, nil) + suite.Require().Error(err) + suite.Require().Nil(cs) +} + +func (suite *LocalhostTestSuite) TestVerifyConnectionState() { + var ( + path *ibctesting.Path + connID string + conn connectiontypes.ConnectionEnd + ) + + testCases := []struct { + name string + malleate func() + expPass bool + }{ + { + name: "proof verification success", + malleate: func() { + conn = path.EndpointB.GetConnection() + connID = path.EndpointB.ConnectionID + }, + expPass: true, + }, + { + name: "proof verification failed: connection not stored", + malleate: func() { + connID = testConnectionID + }, + expPass: false, + }, + { + name: "proof verification failed: unmarshal failed", + malleate: func() { + connID = testConnectionID + store := suite.chain.GetContext().KVStore(suite.chain.App.GetKey(host.StoreKey)) + store.Set(host.ConnectionKey(connID), []byte("connection")) + }, + expPass: false, + }, + { + name: "proof verification failed: different connection stored", + malleate: func() { + counterparty := connectiontypes.NewCounterparty(path.EndpointB.ClientID, path.EndpointB.ConnectionID, commitmenttypes.NewMerklePrefix([]byte("ibc"))) + conn = connectiontypes.NewConnectionEnd(connectiontypes.OPEN, path.EndpointA.ClientID, counterparty, []*connectiontypes.Version{connectiontypes.NewVersion("2", nil)}, 0) + connID = conn.Counterparty.ConnectionId + }, + expPass: false, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.SetupTest() + path = ibctesting.NewLocalPath(suite.chain) + suite.coordinator.Setup(path) + tc.malleate() + + clientStateI := suite.chain.GetClientState(path.EndpointA.ClientID) + clientState, ok := clientStateI.(*types.ClientState) + suite.Require().True(ok) + + store := suite.chain.GetContext().KVStore(suite.chain.App.GetKey(host.StoreKey)) + err := clientState.VerifyConnectionState( + store, suite.chain.Codec, clienttypes.Height{}, nil, []byte{}, connID, conn, + ) + + if tc.expPass { + suite.Require().NoError(err) + } else { + suite.Require().Error(err) + } + }) + } +} + +func (suite *LocalhostTestSuite) TestVerifyChannelState() { + + var ( + path *ibctesting.Path + channelID string + portID string + channel channeltypes.Channel + ) + testCases := []struct { + name string + malleate func() + expPass bool + }{ + { + name: "proof verification success", + malleate: func() { + channelB := path.EndpointB.GetChannel() + channelID = channelB.Counterparty.ChannelId + portID = channelB.Counterparty.PortId + channel = path.EndpointA.GetChannel() + }, + expPass: true, + }, + { + name: "proof verification failed: channel not stored", + malleate: func() { + channelID = testChannelID + portID = testPortID + }, + expPass: false, + }, + { + name: "proof verification failed: unmarshal failed", + malleate: func() { + channelID = testChannelID + portID = testPortID + store := suite.chain.GetContext().KVStore(suite.chain.App.GetKey(host.StoreKey)) + store.Set(host.ChannelKey(testPortID, testChannelID), []byte("channel")) + }, + expPass: false, + }, + { + name: "proof verification failed: different channel stored", + malleate: func() { + activeChannel := path.EndpointB.GetChannel() + channelID = activeChannel.Counterparty.ChannelId + portID = activeChannel.Counterparty.PortId + + counterparty := channeltypes.NewCounterparty(testPortID, testChannelID) + channel = channeltypes.NewChannel(channeltypes.OPEN, channeltypes.ORDERED, counterparty, []string{testConnectionID}, "1.0.0") + }, + expPass: false, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.SetupTest() + path = ibctesting.NewLocalPath(suite.chain) + suite.coordinator.Setup(path) + tc.malleate() + + clientStateI := suite.chain.GetClientState(path.EndpointA.ClientID) + clientState, ok := clientStateI.(*types.ClientState) + suite.Require().True(ok) + + store := suite.chain.GetContext().KVStore(suite.chain.App.GetKey(host.StoreKey)) + err := clientState.VerifyChannelState( + store, suite.chain.Codec, clienttypes.Height{}, nil, []byte{}, portID, channelID, channel, + ) + + if tc.expPass { + suite.Require().NoError(err) + } else { + suite.Require().Error(err) + } + }) + } +} + +func (suite *LocalhostTestSuite) TestVerifyPacketCommitment() { + var ( + packet channeltypes.Packet + portID string + channelID string + sequence uint64 + commitment []byte + ) + + testCases := []struct { + name string + malleate func() + expPass bool + }{ + { + name: "proof verification success", + malleate: func() { + portID = packet.GetSourcePort() + channelID = packet.GetSourceChannel() + sequence = packet.GetSequence() + commitment = channeltypes.CommitPacket(suite.chain.Codec, packet) + }, + expPass: true, + }, + { + name: "proof verification failed: different commitment stored", + malleate: func() { + portID = packet.GetSourcePort() + channelID = packet.GetSourceChannel() + sequence = packet.GetSequence() + commitment = []byte("commitment") + }, + expPass: false, + }, + { + name: "proof verification failed: no commitment stored", + malleate: func() { + portID = testPortID + channelID = testChannelID + sequence = testSequence + }, + expPass: false, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.SetupTest() + path := ibctesting.NewLocalPath(suite.chain) + suite.coordinator.Setup(path) + + // send packet + packet = channeltypes.NewPacket(ibctesting.MockPacketData, 1, path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, clienttypes.NewHeight(0, 100), 0) + err := path.EndpointB.SendPacket(packet) + suite.Require().NoError(err) + + tc.malleate() + + clientStateI := suite.chain.GetClientState(path.EndpointA.ClientID) + clientState, ok := clientStateI.(*types.ClientState) + suite.Require().True(ok) + + ctx := suite.chain.GetContext() + store := ctx.KVStore(suite.chain.App.GetKey(host.StoreKey)) + err = clientState.VerifyPacketCommitment( + ctx, store, suite.chain.Codec, clienttypes.Height{}, 0, 0, nil, []byte{}, portID, channelID, sequence, commitment, + ) + + if tc.expPass { + suite.Require().NoError(err) + } else { + suite.Require().Error(err) + } + }) + } +} + +func (suite *LocalhostTestSuite) TestVerifyPacketAcknowledgement() { + var ( + packet channeltypes.Packet + portID string + channelID string + sequence uint64 + ack []byte + ) + + testCases := []struct { + name string + malleate func() + expPass bool + }{ + { + name: "proof verification success", + malleate: func() { + portID = packet.GetDestPort() + channelID = packet.GetDestChannel() + sequence = packet.GetSequence() + ack = ibcmock.MockAcknowledgement.Acknowledgement() + }, + expPass: true, + }, + { + name: "proof verification failed: different ack stored", + malleate: func() { + portID = packet.GetDestPort() + channelID = packet.GetDestChannel() + sequence = packet.GetSequence() + ack = channeltypes.NewResultAcknowledgement([]byte("different acknowledgement")).Acknowledgement() + }, + expPass: false, + }, + { + name: "proof verification failed: no ack stored", + malleate: func() { + portID = testPortID + channelID = testChannelID + sequence = testSequence + }, + expPass: false, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.SetupTest() + path := ibctesting.NewLocalPath(suite.chain) + suite.coordinator.Setup(path) + + // send packet + packet = channeltypes.NewPacket(ibctesting.MockPacketData, 1, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, clienttypes.NewHeight(0, 100), 0) + err := path.EndpointA.SendPacket(packet) + suite.Require().NoError(err) + + // write receipt and ack + err = path.EndpointB.RecvPacket(packet) + suite.Require().NoError(err) + + tc.malleate() + + clientStateI := suite.chain.GetClientState(path.EndpointA.ClientID) + clientState, ok := clientStateI.(*types.ClientState) + suite.Require().True(ok) + + ctx := suite.chain.GetContext() + store := ctx.KVStore(suite.chain.App.GetKey(host.StoreKey)) + err = clientState.VerifyPacketAcknowledgement( + ctx, store, suite.chain.Codec, clienttypes.Height{}, 0, 0, nil, []byte{}, portID, channelID, sequence, ack, + ) + + if tc.expPass { + suite.Require().NoError(err) + } else { + suite.Require().Error(err) + } + }) + } +} + +func (suite *LocalhostTestSuite) TestVerifyPacketReceiptAbsence() { + suite.SetupTest() + path := ibctesting.NewLocalPath(suite.chain) + suite.coordinator.Setup(path) + + // send packet + packet := channeltypes.NewPacket(ibctesting.MockPacketData, 1, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, clienttypes.NewHeight(0, 100), 0) + err := path.EndpointA.SendPacket(packet) + suite.Require().NoError(err) + + clientStateI := suite.chain.GetClientState(path.EndpointA.ClientID) + clientState, ok := clientStateI.(*types.ClientState) + suite.Require().True(ok) + + ctx := suite.chain.GetContext() + store := ctx.KVStore(suite.chain.App.GetKey(host.StoreKey)) + portID := packet.GetDestPort() + channelID := packet.GetDestChannel() + sequence := packet.GetSequence() + err = clientState.VerifyPacketReceiptAbsence( + ctx, store, suite.chain.Codec, clienttypes.Height{}, 0, 0, nil, nil, portID, channelID, sequence, + ) + suite.Require().NoError(err, "receipt absence failed") + + // write receipt and ack + err = path.EndpointB.RecvPacket(packet) + suite.Require().NoError(err) + + err = clientState.VerifyPacketReceiptAbsence( + ctx, store, suite.chain.Codec, clienttypes.Height{}, 0, 0, nil, nil, portID, channelID, sequence, + ) + suite.Require().Error(err, "receipt exists in store") +} + +func (suite *LocalhostTestSuite) TestVerifyNextSeqRecv() { + var ( + path *ibctesting.Path + packet channeltypes.Packet + portID string + channelID string + nextSequence uint64 + ) + + testCases := []struct { + name string + malleate func() + expPass bool + }{ + { + name: "proof verification success", + malleate: func() { + // send packet + packet = channeltypes.NewPacket(ibctesting.MockPacketData, 1, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, clienttypes.NewHeight(0, 100), 0) + err := path.EndpointA.SendPacket(packet) + suite.Require().NoError(err) + + // write receipt and ack + err = path.EndpointB.RecvPacket(packet) + suite.Require().NoError(err) + + portID = packet.GetDestPort() + channelID = packet.GetDestChannel() + nextSequence = packet.GetSequence() + 1 + }, + expPass: true, + }, + { + name: "proof verification failed: different nextSequence stored", + malleate: func() { + // send packet + packet = channeltypes.NewPacket(ibctesting.MockPacketData, 1, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, clienttypes.NewHeight(0, 100), 0) + err := path.EndpointA.SendPacket(packet) + suite.Require().NoError(err) + + // write receipt and ack + err = path.EndpointB.RecvPacket(packet) + suite.Require().NoError(err) + + portID = packet.GetDestPort() + channelID = packet.GetDestChannel() + nextSequence = packet.GetSequence() + }, + expPass: false, + }, + { + name: "proof verification failed: no nextSequence stored", + malleate: func() { + portID = testPortID + channelID = testChannelID + }, + expPass: false, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.SetupTest() + path = ibctesting.NewLocalPath(suite.chain) + suite.coordinator.Setup(path) + + tc.malleate() + + clientStateI := suite.chain.GetClientState(path.EndpointA.ClientID) + clientState, ok := clientStateI.(*types.ClientState) + suite.Require().True(ok) + + ctx := suite.chain.GetContext() + store := ctx.KVStore(suite.chain.App.GetKey(host.StoreKey)) + err := clientState.VerifyNextSequenceRecv( + ctx, store, suite.chain.Codec, clienttypes.Height{}, 0, 0, nil, nil, portID, channelID, nextSequence, + ) + + if tc.expPass { + suite.Require().NoError(err) + } else { + suite.Require().Error(err) + } + }) + } +} diff --git a/modules/light-clients/09-localhost/types/localhost_test.go b/modules/light-clients/09-localhost/types/localhost_test.go new file mode 100644 index 00000000000..560d2e05c85 --- /dev/null +++ b/modules/light-clients/09-localhost/types/localhost_test.go @@ -0,0 +1,27 @@ +package types_test + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + ibctesting "github.com/cosmos/ibc-go/v5/testing" +) + +type LocalhostTestSuite struct { + suite.Suite + + coordinator ibctesting.Coordinator + chain *ibctesting.TestChain +} + +func (suite *LocalhostTestSuite) SetupTest() { + suite.coordinator = *ibctesting.NewCoordinator(suite.T(), 1) + suite.chain = suite.coordinator.GetChain(ibctesting.GetChainID(1)) + // commit some blocks so that QueryProof returns valid proof (cannot return valid query if height <= 1) + suite.coordinator.CommitNBlocks(suite.chain, 2) +} + +func TestLocalhostTestSuite(t *testing.T) { + suite.Run(t, new(LocalhostTestSuite)) +} diff --git a/testing/app.go b/testing/app.go index f09d3e7499f..abe67410af7 100644 --- a/testing/app.go +++ b/testing/app.go @@ -43,6 +43,7 @@ type TestingApp interface { // Implemented by SimApp AppCodec() codec.Codec + GetKey(string) *storetypes.KVStoreKey // Implemented by BaseApp LastCommitID() storetypes.CommitID diff --git a/testing/config.go b/testing/config.go index 7473f839859..9898f91c64d 100644 --- a/testing/config.go +++ b/testing/config.go @@ -34,6 +34,16 @@ func (tmcfg *TendermintConfig) GetClientType() string { return exported.Tendermint } +type LocalhostConfig struct{} + +func NewLocalhostConfig() *LocalhostConfig { + return &LocalhostConfig{} +} + +func (l *LocalhostConfig) GetClientType() string { + return exported.Localhost +} + type ConnectionConfig struct { DelayPeriod uint64 Version *connectiontypes.Version diff --git a/testing/coordinator.go b/testing/coordinator.go index 6374a17b170..930193d53c2 100644 --- a/testing/coordinator.go +++ b/testing/coordinator.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/cosmos/ibc-go/v5/modules/core/exported" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" ) @@ -85,6 +86,13 @@ func (coord *Coordinator) Setup(path *Path) { // SetupClients is a helper function to create clients on both chains. It assumes the // caller does not anticipate any errors. func (coord *Coordinator) SetupClients(path *Path) { + // Localhost client needs to be created on chain initialization. + if path.EndpointA.ClientConfig.GetClientType() == exported.Localhost { + // Localhost client ID == client type + path.EndpointA.ClientID = exported.Localhost + path.EndpointB.ClientID = exported.Localhost + return + } err := path.EndpointA.CreateClient() require.NoError(coord.T, err) diff --git a/testing/endpoint.go b/testing/endpoint.go index 419a68b6d8d..6503dd9a83e 100644 --- a/testing/endpoint.go +++ b/testing/endpoint.go @@ -46,6 +46,16 @@ func NewEndpoint( } } +// NewLocalEndpoint constructs a new endpoint using default values. +func NewLocalEndpoint(chain *TestChain) *Endpoint { + return &Endpoint{ + Chain: chain, + ClientConfig: NewLocalhostConfig(), + ConnectionConfig: NewConnectionConfig(), + ChannelConfig: NewChannelConfig(), + } +} + // NewDefaultEndpoint constructs a new endpoint using default values. // CONTRACT: the counterparty endpoitn must be set by the caller. func NewDefaultEndpoint(chain *TestChain) *Endpoint { @@ -70,6 +80,11 @@ func (endpoint *Endpoint) QueryProof(key []byte) ([]byte, clienttypes.Height) { // QueryProofAtHeight queries proof associated with this endpoint using the proof height // provided func (endpoint *Endpoint) QueryProofAtHeight(key []byte, height uint64) ([]byte, clienttypes.Height) { + if endpoint.ClientConfig.GetClientType() == exported.Localhost { + revision := clienttypes.ParseChainID(endpoint.Chain.ChainID) + // Return an empty invalid proof to pass validation logic (proof is unused by localhost client). + return []byte("empty"), clienttypes.NewHeight(revision, height) + } // query proof on the counterparty using the latest height of the IBC client return endpoint.Chain.QueryProofAtHeight(key, int64(height)) } @@ -101,7 +116,6 @@ func (endpoint *Endpoint) CreateClient() (err error) { // solo := NewSolomachine(endpoint.Chain.T, endpoint.Chain.Codec, clientID, "", 1) // clientState = solo.ClientState() // consensusState = solo.ConsensusState() - default: err = fmt.Errorf("client type %s is not supported", endpoint.ClientConfig.GetClientType()) } @@ -128,6 +142,11 @@ func (endpoint *Endpoint) CreateClient() (err error) { // UpdateClient updates the IBC client associated with the endpoint. func (endpoint *Endpoint) UpdateClient() (err error) { + // Localhost clients get updated at the beginning of each block. + if endpoint.ClientConfig.GetClientType() == exported.Localhost { + return nil + } + // ensure counterparty has committed state endpoint.Chain.Coordinator.CommitBlock(endpoint.Counterparty.Chain) diff --git a/testing/path.go b/testing/path.go index ca81912c50a..8289b880e63 100644 --- a/testing/path.go +++ b/testing/path.go @@ -29,6 +29,20 @@ func NewPath(chainA, chainB *TestChain) *Path { } } +// NewLocalPath constructs a local endpoint for the same chain. +func NewLocalPath(chain *TestChain) *Path { + endpointA := NewLocalEndpoint(chain) + endpointB := NewLocalEndpoint(chain) + + endpointA.Counterparty = endpointB + endpointB.Counterparty = endpointA + + return &Path{ + EndpointA: endpointA, + EndpointB: endpointB, + } +} + // SetChannelOrdered sets the channel order for both endpoints to ORDERED. func (path *Path) SetChannelOrdered() { path.EndpointA.ChannelConfig.Order = channeltypes.ORDERED