From de00a7fed106ad9d6f37eef8d1225f5efe0ce445 Mon Sep 17 00:00:00 2001 From: Greg Hill Date: Fri, 17 Apr 2020 15:36:47 +0100 Subject: [PATCH] x/ibc: implement 09-localhost per specification (#5769) Signed-off-by: Gregory Hill --- CHANGELOG.md | 5 +- x/ibc/02-client/exported/exported.go | 1 + x/ibc/02-client/handler.go | 3 + x/ibc/02-client/keeper/keeper.go | 4 + x/ibc/09-localhost/client_state.go | 277 ++++++++++++++++ x/ibc/09-localhost/client_state_test.go | 410 ++++++++++++++++++++++++ x/ibc/09-localhost/codec.go | 23 ++ x/ibc/09-localhost/consensus_state.go | 31 ++ x/ibc/09-localhost/doc.go | 5 + x/ibc/09-localhost/evidence.go | 67 ++++ x/ibc/09-localhost/header.go | 26 ++ x/ibc/09-localhost/localhost_test.go | 37 +++ 12 files changed, 887 insertions(+), 2 deletions(-) create mode 100644 x/ibc/09-localhost/client_state.go create mode 100644 x/ibc/09-localhost/client_state_test.go create mode 100644 x/ibc/09-localhost/codec.go create mode 100644 x/ibc/09-localhost/consensus_state.go create mode 100644 x/ibc/09-localhost/doc.go create mode 100644 x/ibc/09-localhost/evidence.go create mode 100644 x/ibc/09-localhost/header.go create mode 100644 x/ibc/09-localhost/localhost_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index ac443ae1902d..77214b4b1a0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,8 +103,9 @@ information on how to implement the new `Keyring` interface. * [ICS 020 - Fungible Token Transfer](https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer) module * [ICS 023 - Vector Commitments](https://github.com/cosmos/ics/tree/master/spec/ics-023-vector-commitments) subpackage * (ibc/ante) Implement IBC `AnteHandler` as per [ADR 15 - IBC Packet Receiver](https://github.com/cosmos/tree/master/docs/architecture/adr-015-ibc-packet-receiver.md). -* (x/capability) [\#5828](https://github.com/cosmos/cosmos-sdk/pull/5828) Capability module integration as outlined in [ADR 3 - Dynamic Capability Store](https://github.com/cosmos/tree/master/docs/architecture/adr-003-dynamic-capability-store.md). -* (x/params) [\#6005](https://github.com/cosmos/cosmos-sdk/pull/6005) Add new CLI command for querying raw x/params parameters by subspace and key. + * (x/capability) [\#5828](https://github.com/cosmos/cosmos-sdk/pull/5828) Capability module integration as outlined in [ADR 3 - Dynamic Capability Store](https://github.com/cosmos/tree/master/docs/architecture/adr-003-dynamic-capability-store.md). + * (x/params) [\#6005](https://github.com/cosmos/cosmos-sdk/pull/6005) Add new CLI command for querying raw x/params parameters by subspace and key. + * (x/ibc) [\#5769] Implementation of localhost client. ### Bug Fixes diff --git a/x/ibc/02-client/exported/exported.go b/x/ibc/02-client/exported/exported.go index 83ca5be502c0..8b61453eec37 100644 --- a/x/ibc/02-client/exported/exported.go +++ b/x/ibc/02-client/exported/exported.go @@ -142,6 +142,7 @@ type ClientType byte // available client types const ( Tendermint ClientType = iota + 1 // 1 + Localhost ) // string representation of the client types diff --git a/x/ibc/02-client/handler.go b/x/ibc/02-client/handler.go index df6f41e3a72d..3c8d135e9383 100644 --- a/x/ibc/02-client/handler.go +++ b/x/ibc/02-client/handler.go @@ -8,6 +8,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported" "github.com/cosmos/cosmos-sdk/x/ibc/02-client/types" ibctmtypes "github.com/cosmos/cosmos-sdk/x/ibc/07-tendermint/types" + localhost "github.com/cosmos/cosmos-sdk/x/ibc/09-localhost" ) // HandleMsgCreateClient defines the sdk.Handler for MsgCreateClient @@ -27,6 +28,8 @@ func HandleMsgCreateClient(ctx sdk.Context, k Keeper, msg exported.MsgCreateClie if err != nil { return nil, err } + case exported.Localhost: + clientState = localhost.NewClientState(ctx.MultiStore().GetKVStore(k.GetStoreKey())) default: return nil, sdkerrors.Wrap(ErrInvalidClientType, msg.GetClientType()) } diff --git a/x/ibc/02-client/keeper/keeper.go b/x/ibc/02-client/keeper/keeper.go index 6876aeee1dcd..1f678c532de0 100644 --- a/x/ibc/02-client/keeper/keeper.go +++ b/x/ibc/02-client/keeper/keeper.go @@ -40,6 +40,10 @@ func (k Keeper) Logger(ctx sdk.Context) log.Logger { return ctx.Logger().With("module", fmt.Sprintf("x/%s/%s", ibctypes.ModuleName, types.SubModuleName)) } +func (k Keeper) GetStoreKey() sdk.StoreKey { + return k.storeKey +} + // GetClientState gets a particular client from the store func (k Keeper) GetClientState(ctx sdk.Context, clientID string) (exported.ClientState, bool) { store := k.clientStore(ctx, clientID) diff --git a/x/ibc/09-localhost/client_state.go b/x/ibc/09-localhost/client_state.go new file mode 100644 index 000000000000..e723eb8ca4c6 --- /dev/null +++ b/x/ibc/09-localhost/client_state.go @@ -0,0 +1,277 @@ +package localhost + +import ( + "bytes" + "encoding/binary" + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported" + clientexported "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported" + clienttypes "github.com/cosmos/cosmos-sdk/x/ibc/02-client/types" + connectionexported "github.com/cosmos/cosmos-sdk/x/ibc/03-connection/exported" + channelexported "github.com/cosmos/cosmos-sdk/x/ibc/04-channel/exported" + commitmentexported "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/exported" + commitmenttypes "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/types" + ibctypes "github.com/cosmos/cosmos-sdk/x/ibc/types" +) + +var _ clientexported.ClientState = ClientState{} + +// ClientState requires (read-only) access to keys outside the client prefix. +type ClientState struct { + ctx sdk.Context + store types.KVStore +} + +// NewClientState creates a new ClientState instance +func NewClientState(store types.KVStore) ClientState { + return ClientState{ + store: store, + } +} + +// WithContext updates the client state context to provide the chain ID and latest height +func (cs *ClientState) WithContext(ctx sdk.Context) { + cs.ctx = ctx +} + +// GetID returns the loop-back client state identifier. +func (cs ClientState) GetID() string { + return clientexported.Localhost.String() +} + +// GetChainID returns an empty string +func (cs ClientState) GetChainID() string { + return cs.ctx.ChainID() +} + +// ClientType is localhost. +func (cs ClientState) ClientType() clientexported.ClientType { + return clientexported.Localhost +} + +// GetLatestHeight returns the block height from the stored context. +func (cs ClientState) GetLatestHeight() uint64 { + return uint64(cs.ctx.BlockHeight()) +} + +// IsFrozen returns false. +func (cs ClientState) IsFrozen() bool { + return false +} + +// VerifyClientConsensusState verifies a proof of the consensus +// state of the loop-back client. +// VerifyClientConsensusState verifies a proof of the consensus state of the +// Tendermint client stored on the target machine. +func (cs ClientState) VerifyClientConsensusState( + cdc *codec.Codec, + _ commitmentexported.Root, + height uint64, + _ string, + consensusHeight uint64, + prefix commitmentexported.Prefix, + _ commitmentexported.Proof, + consensusState clientexported.ConsensusState, +) error { + path, err := commitmenttypes.ApplyPrefix(prefix, consensusStatePath(cs.GetID())) + if err != nil { + return err + } + + data := cs.store.Get([]byte(path.String())) + if len(data) == 0 { + return sdkerrors.Wrap(clienttypes.ErrFailedClientConsensusStateVerification, "not found") + } + + var prevConsensusState exported.ConsensusState + cdc.MustUnmarshalBinaryBare(data, &prevConsensusState) + if consensusState != prevConsensusState { + return sdkerrors.Wrap(clienttypes.ErrFailedClientConsensusStateVerification, "not equal") + } + + return nil +} + +// VerifyConnectionState verifies a proof of the connection state of the +// specified connection end stored locally. +func (cs ClientState) VerifyConnectionState( + cdc *codec.Codec, + _ uint64, + prefix commitmentexported.Prefix, + _ commitmentexported.Proof, + connectionID string, + connectionEnd connectionexported.ConnectionI, + _ clientexported.ConsensusState, +) error { + path, err := commitmenttypes.ApplyPrefix(prefix, ibctypes.ConnectionPath(connectionID)) + if err != nil { + return err + } + + bz := cs.store.Get([]byte(path.String())) + if bz == nil { + return sdkerrors.Wrap(clienttypes.ErrFailedConnectionStateVerification, "not found") + } + + var prevConnectionState connectionexported.ConnectionI + cdc.MustUnmarshalBinaryBare(bz, &prevConnectionState) + if connectionEnd != prevConnectionState { + return sdkerrors.Wrap(clienttypes.ErrFailedConnectionStateVerification, "not equal") + } + + 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( + cdc *codec.Codec, + _ uint64, + prefix commitmentexported.Prefix, + _ commitmentexported.Proof, + portID, + channelID string, + channel channelexported.ChannelI, + _ clientexported.ConsensusState, +) error { + path, err := commitmenttypes.ApplyPrefix(prefix, ibctypes.ChannelPath(portID, channelID)) + if err != nil { + return err + } + + bz := cs.store.Get([]byte(path.String())) + if bz == nil { + return sdkerrors.Wrap(clienttypes.ErrFailedChannelStateVerification, "not found") + } + + var prevChannelState channelexported.ChannelI + cdc.MustUnmarshalBinaryBare(bz, &prevChannelState) + if channel != prevChannelState { + return sdkerrors.Wrap(clienttypes.ErrFailedChannelStateVerification, "not equal") + } + + return nil +} + +// VerifyPacketCommitment verifies a proof of an outgoing packet commitment at +// the specified port, specified channel, and specified sequence. +func (cs ClientState) VerifyPacketCommitment( + _ uint64, + prefix commitmentexported.Prefix, + _ commitmentexported.Proof, + portID, + channelID string, + sequence uint64, + commitmentBytes []byte, + _ clientexported.ConsensusState, +) error { + path, err := commitmenttypes.ApplyPrefix(prefix, ibctypes.PacketCommitmentPath(portID, channelID, sequence)) + if err != nil { + return err + } + + data := cs.store.Get([]byte(path.String())) + if len(data) == 0 { + return sdkerrors.Wrap(clienttypes.ErrFailedPacketCommitmentVerification, "not found") + } + + if !bytes.Equal(data, commitmentBytes) { + return sdkerrors.Wrap(clienttypes.ErrFailedPacketCommitmentVerification, "not equal") + } + + return nil +} + +// VerifyPacketAcknowledgement verifies a proof of an incoming packet +// acknowledgement at the specified port, specified channel, and specified sequence. +func (cs ClientState) VerifyPacketAcknowledgement( + _ uint64, + prefix commitmentexported.Prefix, + _ commitmentexported.Proof, + portID, + channelID string, + sequence uint64, + acknowledgement []byte, + _ clientexported.ConsensusState, +) error { + path, err := commitmenttypes.ApplyPrefix(prefix, ibctypes.PacketAcknowledgementPath(portID, channelID, sequence)) + if err != nil { + return err + } + + data := cs.store.Get([]byte(path.String())) + if len(data) == 0 { + return sdkerrors.Wrap(clienttypes.ErrFailedPacketAckVerification, "not found") + } + + if !bytes.Equal(data, acknowledgement) { + return sdkerrors.Wrap(clienttypes.ErrFailedPacketAckVerification, "not equal") + } + + return nil +} + +// VerifyPacketAcknowledgementAbsence verifies a proof of the absence of an +// incoming packet acknowledgement at the specified port, specified channel, and +// specified sequence. +func (cs ClientState) VerifyPacketAcknowledgementAbsence( + _ uint64, + prefix commitmentexported.Prefix, + _ commitmentexported.Proof, + portID, + channelID string, + sequence uint64, + _ clientexported.ConsensusState, +) error { + path, err := commitmenttypes.ApplyPrefix(prefix, ibctypes.PacketAcknowledgementPath(portID, channelID, sequence)) + if err != nil { + return err + } + + data := cs.store.Get([]byte(path.String())) + if data != nil { + return sdkerrors.Wrap(clienttypes.ErrFailedPacketAckAbsenceVerification, "expected no ack absence") + } + + 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( + _ uint64, + prefix commitmentexported.Prefix, + _ commitmentexported.Proof, + portID, + channelID string, + nextSequenceRecv uint64, + _ clientexported.ConsensusState, +) error { + path, err := commitmenttypes.ApplyPrefix(prefix, ibctypes.NextSequenceRecvPath(portID, channelID)) + if err != nil { + return err + } + + data := cs.store.Get([]byte(path.String())) + if len(data) == 0 { + return sdkerrors.Wrap(clienttypes.ErrFailedNextSeqRecvVerification, "not found") + } + + prevSequenceRecv := binary.BigEndian.Uint64(data) + if prevSequenceRecv != nextSequenceRecv { + return sdkerrors.Wrap(clienttypes.ErrFailedNextSeqRecvVerification, "not equal") + } + + return nil +} + +// consensusStatePath takes an Identifier and returns a Path under which to +// store the consensus state of a client. +func consensusStatePath(clientID string) string { + return fmt.Sprintf("consensusState/%s", clientID) +} diff --git a/x/ibc/09-localhost/client_state_test.go b/x/ibc/09-localhost/client_state_test.go new file mode 100644 index 000000000000..8a2c7cc11e7d --- /dev/null +++ b/x/ibc/09-localhost/client_state_test.go @@ -0,0 +1,410 @@ +package localhost_test + +import ( + connection "github.com/cosmos/cosmos-sdk/x/ibc/03-connection" + connectionexported "github.com/cosmos/cosmos-sdk/x/ibc/03-connection/exported" + channel "github.com/cosmos/cosmos-sdk/x/ibc/04-channel" + channelexported "github.com/cosmos/cosmos-sdk/x/ibc/04-channel/exported" + localhost "github.com/cosmos/cosmos-sdk/x/ibc/09-localhost" + commitmenttypes "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/types" +) + +const ( + testConnectionID = "connectionid" + testPortID = "testportid" + testChannelID = "testchannelid" + testSequence = 1 +) + +func (suite *LocalhostTestSuite) TestVerifyClientConsensusState() { + testCases := []struct { + name string + clientState localhost.ClientState + consensusState localhost.ConsensusState + prefix commitmenttypes.MerklePrefix + proof commitmenttypes.MerkleProof + expPass bool + }{ + { + name: "ApplyPrefix failed", + clientState: localhost.NewClientState(suite.store), + consensusState: localhost.ConsensusState{ + Root: commitmenttypes.NewMerkleRoot([]byte{}), + }, + prefix: commitmenttypes.MerklePrefix{}, + expPass: false, + }, + { + name: "proof verification failed", + clientState: localhost.NewClientState(suite.store), + consensusState: localhost.ConsensusState{ + Root: commitmenttypes.NewMerkleRoot([]byte{}), + }, + prefix: commitmenttypes.NewMerklePrefix([]byte("ibc")), + proof: commitmenttypes.MerkleProof{}, + expPass: false, + }, + } + + for i, tc := range testCases { + tc := tc + + err := tc.clientState.VerifyClientConsensusState( + suite.cdc, tc.consensusState.Root, height, "chainA", tc.consensusState.GetHeight(), tc.prefix, tc.proof, tc.consensusState, + + // suite.cdc, height, tc.prefix, tc.proof, tc.consensusState, + ) + + if tc.expPass { + suite.Require().NoError(err, "valid test case %d failed: %s", i, tc.name) + } else { + suite.Require().Error(err, "invalid test case %d passed: %s", i, tc.name) + } + } +} + +func (suite *LocalhostTestSuite) TestVerifyConnectionState() { + counterparty := connection.NewCounterparty("clientB", testConnectionID, commitmenttypes.NewMerklePrefix([]byte("ibc"))) + conn := connection.NewConnectionEnd(connectionexported.OPEN, "clientA", counterparty, []string{"1.0.0"}) + + testCases := []struct { + name string + clientState localhost.ClientState + connection connection.ConnectionEnd + consensusState localhost.ConsensusState + prefix commitmenttypes.MerklePrefix + proof commitmenttypes.MerkleProof + expPass bool + }{ + { + name: "ApplyPrefix failed", + clientState: localhost.NewClientState(suite.store), + connection: conn, + consensusState: localhost.ConsensusState{ + Root: commitmenttypes.NewMerkleRoot([]byte{}), + }, + prefix: commitmenttypes.MerklePrefix{}, + expPass: false, + }, + { + name: "proof verification failed", + clientState: localhost.NewClientState(suite.store), + connection: conn, + consensusState: localhost.ConsensusState{ + Root: commitmenttypes.NewMerkleRoot([]byte{}), + }, + prefix: commitmenttypes.NewMerklePrefix([]byte("ibc")), + proof: commitmenttypes.MerkleProof{}, + expPass: false, + }, + } + + for i, tc := range testCases { + tc := tc + + err := tc.clientState.VerifyConnectionState( + suite.cdc, height, tc.prefix, tc.proof, testConnectionID, tc.connection, tc.consensusState, + ) + + if tc.expPass { + suite.Require().NoError(err, "valid test case %d failed: %s", i, tc.name) + } else { + suite.Require().Error(err, "invalid test case %d passed: %s", i, tc.name) + } + } +} + +func (suite *LocalhostTestSuite) TestVerifyChannelState() { + counterparty := channel.NewCounterparty(testPortID, testChannelID) + ch := channel.NewChannel(channelexported.OPEN, channelexported.ORDERED, counterparty, []string{testConnectionID}, "1.0.0") + + testCases := []struct { + name string + clientState localhost.ClientState + channel channel.Channel + consensusState localhost.ConsensusState + prefix commitmenttypes.MerklePrefix + proof commitmenttypes.MerkleProof + expPass bool + }{ + { + name: "ApplyPrefix failed", + clientState: localhost.NewClientState(suite.store), + channel: ch, + consensusState: localhost.ConsensusState{ + Root: commitmenttypes.NewMerkleRoot([]byte{}), + }, + prefix: commitmenttypes.MerklePrefix{}, + expPass: false, + }, + { + name: "latest client height < height", + clientState: localhost.NewClientState(suite.store), + channel: ch, + consensusState: localhost.ConsensusState{ + Root: commitmenttypes.NewMerkleRoot([]byte{}), + }, + prefix: commitmenttypes.NewMerklePrefix([]byte("ibc")), + expPass: false, + }, + { + name: "proof verification failed", + clientState: localhost.NewClientState(suite.store), + channel: ch, + consensusState: localhost.ConsensusState{ + Root: commitmenttypes.NewMerkleRoot([]byte{}), + }, + prefix: commitmenttypes.NewMerklePrefix([]byte("ibc")), + proof: commitmenttypes.MerkleProof{}, + expPass: false, + }, + } + + for i, tc := range testCases { + tc := tc + + err := tc.clientState.VerifyChannelState( + suite.cdc, height, tc.prefix, tc.proof, testPortID, testChannelID, tc.channel, tc.consensusState, + ) + + if tc.expPass { + suite.Require().NoError(err, "valid test case %d failed: %s", i, tc.name) + } else { + suite.Require().Error(err, "invalid test case %d passed: %s", i, tc.name) + } + } +} + +func (suite *LocalhostTestSuite) TestVerifyPacketCommitment() { + testCases := []struct { + name string + clientState localhost.ClientState + commitment []byte + consensusState localhost.ConsensusState + prefix commitmenttypes.MerklePrefix + proof commitmenttypes.MerkleProof + expPass bool + }{ + { + name: "ApplyPrefix failed", + clientState: localhost.NewClientState(suite.store), + commitment: []byte{}, + consensusState: localhost.ConsensusState{ + Root: commitmenttypes.NewMerkleRoot([]byte{}), + }, + prefix: commitmenttypes.MerklePrefix{}, + expPass: false, + }, + { + name: "latest client height < height", + clientState: localhost.NewClientState(suite.store), + commitment: []byte{}, + consensusState: localhost.ConsensusState{ + Root: commitmenttypes.NewMerkleRoot([]byte{}), + }, + prefix: commitmenttypes.NewMerklePrefix([]byte("ibc")), + expPass: false, + }, + { + name: "client is frozen", + clientState: localhost.NewClientState(suite.store), + commitment: []byte{}, + consensusState: localhost.ConsensusState{ + Root: commitmenttypes.NewMerkleRoot([]byte{}), + }, + prefix: commitmenttypes.NewMerklePrefix([]byte("ibc")), + expPass: false, + }, + { + name: "proof verification failed", + clientState: localhost.NewClientState(suite.store), + commitment: []byte{}, + consensusState: localhost.ConsensusState{ + Root: commitmenttypes.NewMerkleRoot([]byte{}), + }, + prefix: commitmenttypes.NewMerklePrefix([]byte("ibc")), + proof: commitmenttypes.MerkleProof{}, + expPass: false, + }, + } + + for i, tc := range testCases { + tc := tc + + err := tc.clientState.VerifyPacketCommitment( + height, tc.prefix, tc.proof, testPortID, testChannelID, testSequence, tc.commitment, tc.consensusState, + ) + + if tc.expPass { + suite.Require().NoError(err, "valid test case %d failed: %s", i, tc.name) + } else { + suite.Require().Error(err, "invalid test case %d passed: %s", i, tc.name) + } + } +} + +func (suite *LocalhostTestSuite) TestVerifyPacketAcknowledgement() { + testCases := []struct { + name string + clientState localhost.ClientState + ack []byte + consensusState localhost.ConsensusState + prefix commitmenttypes.MerklePrefix + proof commitmenttypes.MerkleProof + expPass bool + }{ + { + name: "ApplyPrefix failed", + clientState: localhost.NewClientState(suite.store), + ack: []byte{}, + consensusState: localhost.ConsensusState{ + Root: commitmenttypes.NewMerkleRoot([]byte{}), + }, + prefix: commitmenttypes.MerklePrefix{}, + expPass: false, + }, + { + name: "latest client height < height", + clientState: localhost.NewClientState(suite.store), + ack: []byte{}, + consensusState: localhost.ConsensusState{ + Root: commitmenttypes.NewMerkleRoot([]byte{}), + }, + prefix: commitmenttypes.NewMerklePrefix([]byte("ibc")), + expPass: false, + }, + { + name: "client is frozen", + clientState: localhost.NewClientState(suite.store), + ack: []byte{}, + consensusState: localhost.ConsensusState{ + Root: commitmenttypes.NewMerkleRoot([]byte{}), + }, + prefix: commitmenttypes.NewMerklePrefix([]byte("ibc")), + expPass: false, + }, + { + name: "proof verification failed", + clientState: localhost.NewClientState(suite.store), + ack: []byte{}, + consensusState: localhost.ConsensusState{ + Root: commitmenttypes.NewMerkleRoot([]byte{}), + }, + prefix: commitmenttypes.NewMerklePrefix([]byte("ibc")), + proof: commitmenttypes.MerkleProof{}, + expPass: false, + }, + } + + for i, tc := range testCases { + tc := tc + + err := tc.clientState.VerifyPacketAcknowledgement( + height, tc.prefix, tc.proof, testPortID, testChannelID, testSequence, tc.ack, tc.consensusState, + ) + + if tc.expPass { + suite.Require().NoError(err, "valid test case %d failed: %s", i, tc.name) + } else { + suite.Require().Error(err, "invalid test case %d passed: %s", i, tc.name) + } + } +} + +func (suite *LocalhostTestSuite) TestVerifyPacketAcknowledgementAbsence() { + testCases := []struct { + name string + clientState localhost.ClientState + consensusState localhost.ConsensusState + prefix commitmenttypes.MerklePrefix + proof commitmenttypes.MerkleProof + expPass bool + }{ + { + name: "ApplyPrefix failed", + clientState: localhost.NewClientState(suite.store), + consensusState: localhost.ConsensusState{ + Root: commitmenttypes.NewMerkleRoot([]byte{}), + }, + prefix: commitmenttypes.MerklePrefix{}, + expPass: false, + }, + } + + for i, tc := range testCases { + tc := tc + + err := tc.clientState.VerifyPacketAcknowledgementAbsence( + height, tc.prefix, tc.proof, testPortID, testChannelID, testSequence, tc.consensusState, + ) + + if tc.expPass { + suite.Require().NoError(err, "valid test case %d failed: %s", i, tc.name) + } else { + suite.Require().Error(err, "invalid test case %d passed: %s", i, tc.name) + } + } +} + +func (suite *LocalhostTestSuite) TestVerifyNextSeqRecv() { + testCases := []struct { + name string + clientState localhost.ClientState + consensusState localhost.ConsensusState + prefix commitmenttypes.MerklePrefix + proof commitmenttypes.MerkleProof + expPass bool + }{ + { + name: "ApplyPrefix failed", + clientState: localhost.NewClientState(suite.store), + consensusState: localhost.ConsensusState{ + Root: commitmenttypes.NewMerkleRoot([]byte{}), + }, + prefix: commitmenttypes.MerklePrefix{}, + expPass: false, + }, + { + name: "latest client height < height", + clientState: localhost.NewClientState(suite.store), + consensusState: localhost.ConsensusState{ + Root: commitmenttypes.NewMerkleRoot([]byte{}), + }, + prefix: commitmenttypes.NewMerklePrefix([]byte("ibc")), + expPass: false, + }, + { + name: "client is frozen", + clientState: localhost.NewClientState(suite.store), + consensusState: localhost.ConsensusState{ + Root: commitmenttypes.NewMerkleRoot([]byte{}), + }, + prefix: commitmenttypes.NewMerklePrefix([]byte("ibc")), + expPass: false, + }, + { + name: "proof verification failed", + clientState: localhost.NewClientState(suite.store), + consensusState: localhost.ConsensusState{ + Root: commitmenttypes.NewMerkleRoot([]byte{}), + }, + prefix: commitmenttypes.NewMerklePrefix([]byte("ibc")), + proof: commitmenttypes.MerkleProof{}, + expPass: false, + }, + } + + for i, tc := range testCases { + tc := tc + + err := tc.clientState.VerifyNextSequenceRecv( + height, tc.prefix, tc.proof, testPortID, testChannelID, testSequence, tc.consensusState, + ) + + if tc.expPass { + suite.Require().NoError(err, "valid test case %d failed: %s", i, tc.name) + } else { + suite.Require().Error(err, "invalid test case %d passed: %s", i, tc.name) + } + } +} diff --git a/x/ibc/09-localhost/codec.go b/x/ibc/09-localhost/codec.go new file mode 100644 index 000000000000..a1871448395a --- /dev/null +++ b/x/ibc/09-localhost/codec.go @@ -0,0 +1,23 @@ +package localhost + +import ( + "github.com/cosmos/cosmos-sdk/codec" +) + +// SubModuleCdc defines the IBC localhost client codec. +var SubModuleCdc *codec.Codec + +// RegisterCodec registers the localhost types +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterConcrete(ClientState{}, "ibc/client/localhost/ClientState", nil) + cdc.RegisterConcrete(ConsensusState{}, "ibc/client/localhost/ConsensusState", nil) + cdc.RegisterConcrete(Header{}, "ibc/client/localhost/Header", nil) + cdc.RegisterConcrete(Evidence{}, "ibc/client/localhost/Evidence", nil) + + SetSubModuleCodec(cdc) +} + +// SetSubModuleCodec sets the ibc localhost client codec +func SetSubModuleCodec(cdc *codec.Codec) { + SubModuleCdc = cdc +} diff --git a/x/ibc/09-localhost/consensus_state.go b/x/ibc/09-localhost/consensus_state.go new file mode 100644 index 000000000000..bc6c8a2d23d3 --- /dev/null +++ b/x/ibc/09-localhost/consensus_state.go @@ -0,0 +1,31 @@ +package localhost + +import ( + clientexported "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported" + commitmentexported "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/exported" +) + +// ConsensusState defines a Localhost consensus state +type ConsensusState struct { + Root commitmentexported.Root `json:"root" yaml:"root"` +} + +// ClientType returns Localhost +func (ConsensusState) ClientType() clientexported.ClientType { + return clientexported.Localhost +} + +// GetRoot returns the commitment Root for the specific +func (cs ConsensusState) GetRoot() commitmentexported.Root { + return cs.Root +} + +// GetHeight returns the height for the specific consensus state +func (cs ConsensusState) GetHeight() uint64 { + return 0 +} + +// ValidateBasic defines a basic validation for the localhost consensus state. +func (cs ConsensusState) ValidateBasic() error { + return nil +} diff --git a/x/ibc/09-localhost/doc.go b/x/ibc/09-localhost/doc.go new file mode 100644 index 000000000000..40a0f0608670 --- /dev/null +++ b/x/ibc/09-localhost/doc.go @@ -0,0 +1,5 @@ +/* +Package localhost implements a concrete `ConsensusState`, `Header`, +`Misbehaviour` and `Equivocation` types for the loop-back client. +*/ +package localhost diff --git a/x/ibc/09-localhost/evidence.go b/x/ibc/09-localhost/evidence.go new file mode 100644 index 000000000000..d56883ee5171 --- /dev/null +++ b/x/ibc/09-localhost/evidence.go @@ -0,0 +1,67 @@ +package localhost + +import ( + yaml "gopkg.in/yaml.v2" + + "github.com/tendermint/tendermint/crypto/tmhash" + tmbytes "github.com/tendermint/tendermint/libs/bytes" + + evidenceexported "github.com/cosmos/cosmos-sdk/x/evidence/exported" + clientexported "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported" + clienttypes "github.com/cosmos/cosmos-sdk/x/ibc/02-client/types" +) + +var ( + _ evidenceexported.Evidence = Evidence{} + _ clientexported.Misbehaviour = Evidence{} +) + +// Evidence is not required for a loop-back client +type Evidence struct { +} + +// ClientType is Localhost light client +func (ev Evidence) ClientType() clientexported.ClientType { + return clientexported.Localhost +} + +// GetClientID returns the ID of the client that committed a misbehaviour. +func (ev Evidence) GetClientID() string { + return clientexported.Localhost.String() +} + +// Route implements Evidence interface +func (ev Evidence) Route() string { + return clienttypes.SubModuleName +} + +// Type implements Evidence interface +func (ev Evidence) Type() string { + return "client_misbehaviour" +} + +// String implements Evidence interface +func (ev Evidence) String() string { + // FIXME: implement custom marshaller + bz, err := yaml.Marshal(ev) + if err != nil { + panic(err) + } + return string(bz) +} + +// Hash implements Evidence interface. +func (ev Evidence) Hash() tmbytes.HexBytes { + bz := SubModuleCdc.MustMarshalBinaryBare(ev) + return tmhash.Sum(bz) +} + +// GetHeight returns 0. +func (ev Evidence) GetHeight() int64 { + return 0 +} + +// ValidateBasic implements Evidence interface. +func (ev Evidence) ValidateBasic() error { + return nil +} diff --git a/x/ibc/09-localhost/header.go b/x/ibc/09-localhost/header.go new file mode 100644 index 000000000000..af3b7f1ac237 --- /dev/null +++ b/x/ibc/09-localhost/header.go @@ -0,0 +1,26 @@ +package localhost + +import ( + clientexported "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported" +) + +var _ clientexported.Header = Header{} + +// Header defines the Localhost consensus Header +type Header struct { +} + +// ClientType defines that the Header is in loop-back mode. +func (h Header) ClientType() clientexported.ClientType { + return clientexported.Localhost +} + +// ConsensusState returns an empty consensus state. +func (h Header) ConsensusState() ConsensusState { + return ConsensusState{} +} + +// GetHeight returns 0. +func (h Header) GetHeight() uint64 { + return 0 +} diff --git a/x/ibc/09-localhost/localhost_test.go b/x/ibc/09-localhost/localhost_test.go new file mode 100644 index 000000000000..5c74475c5a24 --- /dev/null +++ b/x/ibc/09-localhost/localhost_test.go @@ -0,0 +1,37 @@ +package localhost_test + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store/cachekv" + "github.com/cosmos/cosmos-sdk/store/dbadapter" + commitmenttypes "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/types" + dbm "github.com/tendermint/tm-db" +) + +const ( + height = 4 +) + +type LocalhostTestSuite struct { + suite.Suite + + cdc *codec.Codec + store *cachekv.Store +} + +func (suite *LocalhostTestSuite) SetupTest() { + suite.cdc = codec.New() + codec.RegisterCrypto(suite.cdc) + commitmenttypes.RegisterCodec(suite.cdc) + + mem := dbadapter.Store{DB: dbm.NewMemDB()} + suite.store = cachekv.NewStore(mem) +} + +func TestLocalhostTestSuite(t *testing.T) { + suite.Run(t, new(LocalhostTestSuite)) +}