diff --git a/CHANGELOG.md b/CHANGELOG.md index 66296a985a8..929183fb2fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,8 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (testing) [\#892](https://github.com/cosmos/ibc-go/pull/892) IBC Mock modules store the scoped keeper and portID within the IBCMockApp. They also maintain reference to the AppModule to update the AppModule's list of IBC applications it references. Allows for the mock module to be reused as a base application in middleware stacks. * (channel) [\#882](https://github.com/cosmos/ibc-go/pull/882) The `WriteAcknowledgement` API now takes `exported.Acknowledgement` instead of a byte array * (modules/core/ante) [\#950](https://github.com/cosmos/ibc-go/pull/950) Replaces the channel keeper with the IBC keeper in the IBC `AnteDecorator` in order to execute the entire message and be able to reject redundant messages that are in the same block as the non-redundant messages. +* (modules/core/exported) [\#1107](https://github.com/cosmos/ibc-go/pull/1107) Merging the `Header` and `Misbehaviour` interfaces into a single `ClientMessage` type + ### State Machine Breaking diff --git a/modules/core/02-client/client/cli/tx.go b/modules/core/02-client/client/cli/tx.go index 65703fb1f4c..097baaa5f82 100644 --- a/modules/core/02-client/client/cli/tx.go +++ b/modules/core/02-client/client/cli/tx.go @@ -100,7 +100,7 @@ func NewUpdateClientCmd() *cobra.Command { cdc := codec.NewProtoCodec(clientCtx.InterfaceRegistry) - var header exported.Header + var header exported.ClientMessage headerContentOrFileName := args[1] if err := cdc.UnmarshalInterfaceJSON([]byte(headerContentOrFileName), &header); err != nil { @@ -141,7 +141,7 @@ func NewSubmitMisbehaviourCmd() *cobra.Command { } cdc := codec.NewProtoCodec(clientCtx.InterfaceRegistry) - var misbehaviour exported.Misbehaviour + var misbehaviour exported.ClientMessage clientID := args[0] misbehaviourContentOrFileName := args[1] if err := cdc.UnmarshalInterfaceJSON([]byte(misbehaviourContentOrFileName), &misbehaviour); err != nil { diff --git a/modules/core/02-client/keeper/client.go b/modules/core/02-client/keeper/client.go index 600519bf5f4..78086ddb4d6 100644 --- a/modules/core/02-client/keeper/client.go +++ b/modules/core/02-client/keeper/client.go @@ -57,7 +57,7 @@ func (k Keeper) CreateClient( } // UpdateClient updates the consensus state and the state root from a provided header. -func (k Keeper) UpdateClient(ctx sdk.Context, clientID string, header exported.Header) error { +func (k Keeper) UpdateClient(ctx sdk.Context, clientID string, header exported.ClientMessage) error { clientState, found := k.GetClientState(ctx, clientID) if !found { return sdkerrors.Wrapf(types.ErrClientNotFound, "cannot update client with ID %s", clientID) @@ -85,7 +85,7 @@ func (k Keeper) UpdateClient(ctx sdk.Context, clientID string, header exported.H // Marshal the Header as an Any and encode the resulting bytes to hex. // This prevents the event value from containing invalid UTF-8 characters // which may cause data to be lost when JSON encoding/decoding. - headerStr = hex.EncodeToString(types.MustMarshalHeader(k.cdc, header)) + headerStr = hex.EncodeToString(types.MustMarshalClientMessage(k.cdc, header)) // set default consensus height with header height consensusHeight = header.GetHeight() @@ -188,7 +188,7 @@ func (k Keeper) UpgradeClient(ctx sdk.Context, clientID string, upgradedClient e // CheckMisbehaviourAndUpdateState checks for client misbehaviour and freezes the // client if so. -func (k Keeper) CheckMisbehaviourAndUpdateState(ctx sdk.Context, clientID string, misbehaviour exported.Misbehaviour) error { +func (k Keeper) CheckMisbehaviourAndUpdateState(ctx sdk.Context, clientID string, misbehaviour exported.ClientMessage) error { clientState, found := k.GetClientState(ctx, clientID) if !found { return sdkerrors.Wrapf(types.ErrClientNotFound, "cannot check misbehaviour for client with ID %s", clientID) diff --git a/modules/core/02-client/keeper/client_test.go b/modules/core/02-client/keeper/client_test.go index dad38787c47..8d70a1816e0 100644 --- a/modules/core/02-client/keeper/client_test.go +++ b/modules/core/02-client/keeper/client_test.go @@ -691,7 +691,7 @@ func (suite *KeeperTestSuite) TestUpdateClientEventEmission() { bz, err := hex.DecodeString(string(attr.Value)) suite.Require().NoError(err) - emittedHeader, err := types.UnmarshalHeader(suite.chainA.App.AppCodec(), bz) + emittedHeader, err := types.UnmarshalClientMessage(suite.chainA.App.AppCodec(), bz) suite.Require().NoError(err) suite.Require().Equal(header, emittedHeader) } diff --git a/modules/core/02-client/types/msgs.go b/modules/core/02-client/types/msgs.go index 8e339199923..f1b8076c5b9 100644 --- a/modules/core/02-client/types/msgs.go +++ b/modules/core/02-client/types/msgs.go @@ -5,8 +5,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - host "github.com/cosmos/ibc-go/v5/modules/core/24-host" - "github.com/cosmos/ibc-go/v5/modules/core/exported" + host "github.com/cosmos/ibc-go/v3/modules/core/24-host" + "github.com/cosmos/ibc-go/v3/modules/core/exported" ) // message types for the IBC client @@ -30,11 +30,11 @@ var ( ) // NewMsgCreateClient creates a new MsgCreateClient instance -// //nolint:interfacer func NewMsgCreateClient( clientState exported.ClientState, consensusState exported.ConsensusState, signer string, ) (*MsgCreateClient, error) { + anyClientState, err := PackClientState(clientState) if err != nil { return nil, err @@ -65,6 +65,9 @@ func (msg MsgCreateClient) ValidateBasic() error { if err := clientState.Validate(); err != nil { return err } + if clientState.ClientType() == exported.Localhost { + return sdkerrors.Wrap(ErrInvalidClient, "localhost client can only be created on chain initialization") + } consensusState, err := UnpackConsensusState(msg.ConsensusState) if err != nil { return err @@ -100,18 +103,17 @@ func (msg MsgCreateClient) UnpackInterfaces(unpacker codectypes.AnyUnpacker) err } // NewMsgUpdateClient creates a new MsgUpdateClient instance -// //nolint:interfacer -func NewMsgUpdateClient(id string, clientMsg exported.ClientMessage, signer string) (*MsgUpdateClient, error) { - anyClientMsg, err := PackClientMessage(clientMsg) +func NewMsgUpdateClient(id string, header exported.ClientMessage, signer string) (*MsgUpdateClient, error) { + anyHeader, err := PackClientMessage(header) if err != nil { return nil, err } return &MsgUpdateClient{ - ClientId: id, - ClientMessage: anyClientMsg, - Signer: signer, + ClientId: id, + Header: anyHeader, + Signer: signer, }, nil } @@ -121,13 +123,16 @@ func (msg MsgUpdateClient) ValidateBasic() error { if err != nil { return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "string could not be parsed as address: %v", err) } - clientMsg, err := UnpackClientMessage(msg.ClientMessage) + header, err := UnpackClientMessage(msg.Header) if err != nil { return err } - if err := clientMsg.ValidateBasic(); err != nil { + if err := header.ValidateBasic(); err != nil { return err } + if msg.ClientId == exported.Localhost { + return sdkerrors.Wrap(ErrInvalidClient, "localhost client is only updated on ABCI BeginBlock") + } return host.ClientIdentifierValidator(msg.ClientId) } @@ -142,16 +147,14 @@ func (msg MsgUpdateClient) GetSigners() []sdk.AccAddress { // UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces func (msg MsgUpdateClient) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error { - var clientMsg exported.ClientMessage - return unpacker.UnpackAny(msg.ClientMessage, &clientMsg) + var header exported.ClientMessage + return unpacker.UnpackAny(msg.Header, &header) } // NewMsgUpgradeClient creates a new MsgUpgradeClient instance -// -//nolint:interfacer +// nolint: interfacer func NewMsgUpgradeClient(clientID string, clientState exported.ClientState, consState exported.ConsensusState, - proofUpgradeClient, proofUpgradeConsState []byte, signer string, -) (*MsgUpgradeClient, error) { + proofUpgradeClient, proofUpgradeConsState []byte, signer string) (*MsgUpgradeClient, error) { anyClient, err := PackClientState(clientState) if err != nil { return nil, err @@ -225,7 +228,6 @@ func (msg MsgUpgradeClient) UnpackInterfaces(unpacker codectypes.AnyUnpacker) er } // NewMsgSubmitMisbehaviour creates a new MsgSubmitMisbehaviour instance. -// //nolint:interfacer func NewMsgSubmitMisbehaviour(clientID string, misbehaviour exported.ClientMessage, signer string) (*MsgSubmitMisbehaviour, error) { anyMisbehaviour, err := PackClientMessage(misbehaviour) diff --git a/modules/core/ante/ante_test.go b/modules/core/ante/ante_test.go index c04f6483f74..737b5f97b7b 100644 --- a/modules/core/ante/ante_test.go +++ b/modules/core/ante/ante_test.go @@ -153,7 +153,7 @@ func (suite *AnteTestSuite) createUpdateClientMessage() sdk.Msg { // ensure counterparty has committed state endpoint.Chain.Coordinator.CommitBlock(endpoint.Counterparty.Chain) - var header exported.Header + var header exported.ClientMessage switch endpoint.ClientConfig.GetClientType() { case exported.Tendermint: diff --git a/modules/core/exported/client.go b/modules/core/exported/client.go index a4839bdbf97..bb473468041 100644 --- a/modules/core/exported/client.go +++ b/modules/core/exported/client.go @@ -58,8 +58,8 @@ type ClientState interface { // Update and Misbehaviour functions - CheckHeaderAndUpdateState(sdk.Context, codec.BinaryCodec, sdk.KVStore, Header) (ClientState, ConsensusState, error) - CheckMisbehaviourAndUpdateState(sdk.Context, codec.BinaryCodec, sdk.KVStore, Misbehaviour) (ClientState, error) + CheckHeaderAndUpdateState(sdk.Context, codec.BinaryCodec, sdk.KVStore, ClientMessage) (ClientState, ConsensusState, error) + CheckMisbehaviourAndUpdateState(sdk.Context, codec.BinaryCodec, sdk.KVStore, ClientMessage) (ClientState, error) CheckSubstituteAndUpdateState(ctx sdk.Context, cdc codec.BinaryCodec, subjectClientStore, substituteClientStore sdk.KVStore, substituteClient ClientState) (ClientState, error) // Upgrade functions @@ -194,20 +194,14 @@ type ConsensusState interface { ValidateBasic() error } -// Misbehaviour defines counterparty misbehaviour for a specific consensus type -type Misbehaviour interface { +// ClientMessage is an interface used to update an IBC client. +// The update may be done by a single header, a batch of headers, misbehaviour, or any type which when verified produces +// a change to state of the IBC client +type ClientMessage interface { proto.Message - ClientType() string - ValidateBasic() error -} - -// Header is the consensus state update information -type Header interface { - proto.Message - - ClientType() string GetHeight() Height + ClientType() string ValidateBasic() error } diff --git a/modules/core/keeper/msg_server.go b/modules/core/keeper/msg_server.go index 74e7cc19ae4..673978e053b 100644 --- a/modules/core/keeper/msg_server.go +++ b/modules/core/keeper/msg_server.go @@ -45,7 +45,7 @@ func (k Keeper) CreateClient(goCtx context.Context, msg *clienttypes.MsgCreateCl func (k Keeper) UpdateClient(goCtx context.Context, msg *clienttypes.MsgUpdateClient) (*clienttypes.MsgUpdateClientResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - header, err := clienttypes.UnpackHeader(msg.Header) + header, err := clienttypes.UnpackClientMessage(msg.Header) if err != nil { return nil, err } @@ -82,7 +82,7 @@ func (k Keeper) UpgradeClient(goCtx context.Context, msg *clienttypes.MsgUpgrade func (k Keeper) SubmitMisbehaviour(goCtx context.Context, msg *clienttypes.MsgSubmitMisbehaviour) (*clienttypes.MsgSubmitMisbehaviourResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - misbehaviour, err := clienttypes.UnpackMisbehaviour(msg.Misbehaviour) + misbehaviour, err := clienttypes.UnpackClientMessage(msg.Misbehaviour) if err != nil { return nil, err } diff --git a/modules/light-clients/06-solomachine/misbehaviour.go b/modules/light-clients/06-solomachine/misbehaviour.go index 31b9b1dc97c..0b4c2f3bcd0 100644 --- a/modules/light-clients/06-solomachine/misbehaviour.go +++ b/modules/light-clients/06-solomachine/misbehaviour.go @@ -10,7 +10,7 @@ import ( "github.com/cosmos/ibc-go/v3/modules/core/exported" ) -var _ exported.Misbehaviour = &Misbehaviour{} +var _ exported.ClientMessage = &Misbehaviour{} // ClientType is a Solo Machine light client. func (misbehaviour Misbehaviour) ClientType() string { @@ -70,3 +70,9 @@ func (sd SignatureAndData) ValidateBasic() error { return nil } + +// TODO: Remove GetHeight() +// GetHeight implements the curret exported.Header interface, to be updated +func (misbehaviour Misbehaviour) GetHeight() exported.Height { + return nil +} diff --git a/modules/light-clients/06-solomachine/types/codec.go b/modules/light-clients/06-solomachine/types/codec.go index 1db36165157..9ceccaef3cb 100644 --- a/modules/light-clients/06-solomachine/types/codec.go +++ b/modules/light-clients/06-solomachine/types/codec.go @@ -22,11 +22,11 @@ func RegisterInterfaces(registry codectypes.InterfaceRegistry) { &ConsensusState{}, ) registry.RegisterImplementations( - (*exported.Header)(nil), + (*exported.ClientMessage)(nil), &Header{}, ) registry.RegisterImplementations( - (*exported.Misbehaviour)(nil), + (*exported.ClientMessage)(nil), &Misbehaviour{}, ) } diff --git a/modules/light-clients/06-solomachine/types/misbehaviour_handle.go b/modules/light-clients/06-solomachine/types/misbehaviour_handle.go index d5a1d57cb57..171ad08e5f0 100644 --- a/modules/light-clients/06-solomachine/types/misbehaviour_handle.go +++ b/modules/light-clients/06-solomachine/types/misbehaviour_handle.go @@ -19,7 +19,7 @@ func (cs ClientState) CheckMisbehaviourAndUpdateState( ctx sdk.Context, cdc codec.BinaryCodec, clientStore sdk.KVStore, - misbehaviour exported.Misbehaviour, + misbehaviour exported.ClientMessage, ) (exported.ClientState, error) { soloMisbehaviour, ok := misbehaviour.(*Misbehaviour) diff --git a/modules/light-clients/06-solomachine/types/misbehaviour_handle_test.go b/modules/light-clients/06-solomachine/types/misbehaviour_handle_test.go new file mode 100644 index 00000000000..0fd4aa0edd9 --- /dev/null +++ b/modules/light-clients/06-solomachine/types/misbehaviour_handle_test.go @@ -0,0 +1,265 @@ +package types_test + +import ( + "github.com/cosmos/ibc-go/v3/modules/core/exported" + "github.com/cosmos/ibc-go/v3/modules/light-clients/06-solomachine/types" + ibctmtypes "github.com/cosmos/ibc-go/v3/modules/light-clients/07-tendermint/types" + ibctesting "github.com/cosmos/ibc-go/v3/testing" +) + +func (suite *SoloMachineTestSuite) TestCheckMisbehaviourAndUpdateState() { + var ( + clientState exported.ClientState + misbehaviour exported.ClientMessage + ) + + // test singlesig and multisig public keys + for _, solomachine := range []*ibctesting.Solomachine{suite.solomachine, suite.solomachineMulti} { + + testCases := []struct { + name string + setup func() + expPass bool + }{ + { + "valid misbehaviour", + func() { + clientState = solomachine.ClientState() + misbehaviour = solomachine.CreateMisbehaviour() + }, + true, + }, + { + "old misbehaviour is successful (timestamp is less than current consensus state)", + func() { + clientState = solomachine.ClientState() + solomachine.Time = solomachine.Time - 5 + misbehaviour = solomachine.CreateMisbehaviour() + }, true, + }, + { + "wrong client state type", + func() { + clientState = &ibctmtypes.ClientState{} + misbehaviour = solomachine.CreateMisbehaviour() + }, + false, + }, + { + "invalid misbehaviour type", + func() { + clientState = solomachine.ClientState() + misbehaviour = &ibctmtypes.Misbehaviour{} + }, + false, + }, + { + "invalid SignatureOne SignatureData", + func() { + clientState = solomachine.ClientState() + m := solomachine.CreateMisbehaviour() + + m.SignatureOne.Signature = suite.GetInvalidProof() + misbehaviour = m + }, false, + }, + { + "invalid SignatureTwo SignatureData", + func() { + clientState = solomachine.ClientState() + m := solomachine.CreateMisbehaviour() + + m.SignatureTwo.Signature = suite.GetInvalidProof() + misbehaviour = m + }, false, + }, + { + "invalid SignatureOne timestamp", + func() { + clientState = solomachine.ClientState() + m := solomachine.CreateMisbehaviour() + + m.SignatureOne.Timestamp = 1000000000000 + misbehaviour = m + }, false, + }, + { + "invalid SignatureTwo timestamp", + func() { + clientState = solomachine.ClientState() + m := solomachine.CreateMisbehaviour() + + m.SignatureTwo.Timestamp = 1000000000000 + misbehaviour = m + }, false, + }, + { + "invalid first signature data", + func() { + clientState = solomachine.ClientState() + + // store in temp before assigning to interface type + m := solomachine.CreateMisbehaviour() + + msg := []byte("DATA ONE") + signBytes := &types.SignBytes{ + Sequence: solomachine.Sequence + 1, + Timestamp: solomachine.Time, + Diversifier: solomachine.Diversifier, + DataType: types.CLIENT, + Data: msg, + } + + data, err := suite.chainA.Codec.Marshal(signBytes) + suite.Require().NoError(err) + + sig := solomachine.GenerateSignature(data) + + m.SignatureOne.Signature = sig + m.SignatureOne.Data = msg + misbehaviour = m + }, + false, + }, + { + "invalid second signature data", + func() { + clientState = solomachine.ClientState() + + // store in temp before assigning to interface type + m := solomachine.CreateMisbehaviour() + + msg := []byte("DATA TWO") + signBytes := &types.SignBytes{ + Sequence: solomachine.Sequence + 1, + Timestamp: solomachine.Time, + Diversifier: solomachine.Diversifier, + DataType: types.CLIENT, + Data: msg, + } + + data, err := suite.chainA.Codec.Marshal(signBytes) + suite.Require().NoError(err) + + sig := solomachine.GenerateSignature(data) + + m.SignatureTwo.Signature = sig + m.SignatureTwo.Data = msg + misbehaviour = m + }, + false, + }, + { + "wrong pubkey generates first signature", + func() { + clientState = solomachine.ClientState() + badMisbehaviour := solomachine.CreateMisbehaviour() + + // update public key to a new one + solomachine.CreateHeader() + m := solomachine.CreateMisbehaviour() + + // set SignatureOne to use the wrong signature + m.SignatureOne = badMisbehaviour.SignatureOne + misbehaviour = m + }, false, + }, + { + "wrong pubkey generates second signature", + func() { + clientState = solomachine.ClientState() + badMisbehaviour := solomachine.CreateMisbehaviour() + + // update public key to a new one + solomachine.CreateHeader() + m := solomachine.CreateMisbehaviour() + + // set SignatureTwo to use the wrong signature + m.SignatureTwo = badMisbehaviour.SignatureTwo + misbehaviour = m + }, false, + }, + + { + "signatures sign over different sequence", + func() { + clientState = solomachine.ClientState() + + // store in temp before assigning to interface type + m := solomachine.CreateMisbehaviour() + + // Signature One + msg := []byte("DATA ONE") + // sequence used is plus 1 + signBytes := &types.SignBytes{ + Sequence: solomachine.Sequence + 1, + Timestamp: solomachine.Time, + Diversifier: solomachine.Diversifier, + DataType: types.CLIENT, + Data: msg, + } + + data, err := suite.chainA.Codec.Marshal(signBytes) + suite.Require().NoError(err) + + sig := solomachine.GenerateSignature(data) + + m.SignatureOne.Signature = sig + m.SignatureOne.Data = msg + + // Signature Two + msg = []byte("DATA TWO") + // sequence used is minus 1 + + signBytes = &types.SignBytes{ + Sequence: solomachine.Sequence - 1, + Timestamp: solomachine.Time, + Diversifier: solomachine.Diversifier, + DataType: types.CLIENT, + Data: msg, + } + data, err = suite.chainA.Codec.Marshal(signBytes) + suite.Require().NoError(err) + + sig = solomachine.GenerateSignature(data) + + m.SignatureTwo.Signature = sig + m.SignatureTwo.Data = msg + + misbehaviour = m + + }, + false, + }, + { + "consensus state pubkey is nil", + func() { + cs := solomachine.ClientState() + cs.ConsensusState.PublicKey = nil + clientState = cs + misbehaviour = solomachine.CreateMisbehaviour() + }, + false, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + // setup test + tc.setup() + + clientState, err := clientState.CheckMisbehaviourAndUpdateState(suite.chainA.GetContext(), suite.chainA.App.AppCodec(), suite.store, misbehaviour) + + if tc.expPass { + suite.Require().NoError(err) + suite.Require().True(clientState.(*types.ClientState).IsFrozen, "client not frozen") + } else { + suite.Require().Error(err) + suite.Require().Nil(clientState) + } + }) + } + } +} diff --git a/modules/light-clients/06-solomachine/types/update.go b/modules/light-clients/06-solomachine/types/update.go new file mode 100644 index 00000000000..881a8d3a5d5 --- /dev/null +++ b/modules/light-clients/06-solomachine/types/update.go @@ -0,0 +1,90 @@ +package types + +import ( + "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/v3/modules/core/02-client/types" + "github.com/cosmos/ibc-go/v3/modules/core/exported" +) + +// CheckHeaderAndUpdateState checks if the provided header is valid and updates +// the consensus state if appropriate. It returns an error if: +// - the header provided is not parseable to a solo machine header +// - the header sequence does not match the current sequence +// - the header timestamp is less than the consensus state timestamp +// - the currently registered public key did not provide the update signature +func (cs ClientState) CheckHeaderAndUpdateState( + ctx sdk.Context, cdc codec.BinaryCodec, clientStore sdk.KVStore, + header exported.ClientMessage, +) (exported.ClientState, exported.ConsensusState, error) { + smHeader, ok := header.(*Header) + if !ok { + return nil, nil, sdkerrors.Wrapf( + clienttypes.ErrInvalidHeader, "header type %T, expected %T", header, &Header{}, + ) + } + + if err := checkHeader(cdc, &cs, smHeader); err != nil { + return nil, nil, err + } + + clientState, consensusState := update(&cs, smHeader) + return clientState, consensusState, nil +} + +// checkHeader checks if the Solo Machine update signature is valid. +func checkHeader(cdc codec.BinaryCodec, clientState *ClientState, header *Header) error { + // assert update sequence is current sequence + if header.Sequence != clientState.Sequence { + return sdkerrors.Wrapf( + clienttypes.ErrInvalidHeader, + "header sequence does not match the client state sequence (%d != %d)", header.Sequence, clientState.Sequence, + ) + } + + // assert update timestamp is not less than current consensus state timestamp + if header.Timestamp < clientState.ConsensusState.Timestamp { + return sdkerrors.Wrapf( + clienttypes.ErrInvalidHeader, + "header timestamp is less than to the consensus state timestamp (%d < %d)", header.Timestamp, clientState.ConsensusState.Timestamp, + ) + } + + // assert currently registered public key signed over the new public key with correct sequence + data, err := HeaderSignBytes(cdc, header) + if err != nil { + return err + } + + sigData, err := UnmarshalSignatureData(cdc, header.Signature) + if err != nil { + return err + } + + publicKey, err := clientState.ConsensusState.GetPubKey() + if err != nil { + return err + } + + if err := VerifySignature(publicKey, data, sigData); err != nil { + return sdkerrors.Wrap(ErrInvalidHeader, err.Error()) + } + + return nil +} + +// update the consensus state to the new public key and an incremented sequence +func update(clientState *ClientState, header *Header) (*ClientState, *ConsensusState) { + consensusState := &ConsensusState{ + PublicKey: header.NewPublicKey, + Diversifier: header.NewDiversifier, + Timestamp: header.Timestamp, + } + + // increment sequence number + clientState.Sequence++ + clientState.ConsensusState = consensusState + return clientState, consensusState +} diff --git a/modules/light-clients/06-solomachine/types/update_test.go b/modules/light-clients/06-solomachine/types/update_test.go new file mode 100644 index 00000000000..6c8d34486e6 --- /dev/null +++ b/modules/light-clients/06-solomachine/types/update_test.go @@ -0,0 +1,182 @@ +package types_test + +import ( + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/ibc-go/v3/modules/core/exported" + "github.com/cosmos/ibc-go/v3/modules/light-clients/06-solomachine/types" + ibctmtypes "github.com/cosmos/ibc-go/v3/modules/light-clients/07-tendermint/types" + ibctesting "github.com/cosmos/ibc-go/v3/testing" +) + +func (suite *SoloMachineTestSuite) TestCheckHeaderAndUpdateState() { + var ( + clientState exported.ClientState + header exported.ClientMessage + ) + + // test singlesig and multisig public keys + for _, solomachine := range []*ibctesting.Solomachine{suite.solomachine, suite.solomachineMulti} { + + testCases := []struct { + name string + setup func() + expPass bool + }{ + { + "successful update", + func() { + clientState = solomachine.ClientState() + header = solomachine.CreateHeader() + }, + true, + }, + { + "wrong client state type", + func() { + clientState = &ibctmtypes.ClientState{} + header = solomachine.CreateHeader() + }, + false, + }, + { + "invalid header type", + func() { + clientState = solomachine.ClientState() + header = &ibctmtypes.Header{} + }, + false, + }, + { + "wrong sequence in header", + func() { + clientState = solomachine.ClientState() + // store in temp before assigning to interface type + h := solomachine.CreateHeader() + h.Sequence++ + header = h + }, + false, + }, + { + "invalid header Signature", + func() { + clientState = solomachine.ClientState() + h := solomachine.CreateHeader() + h.Signature = suite.GetInvalidProof() + header = h + }, false, + }, + { + "invalid timestamp in header", + func() { + clientState = solomachine.ClientState() + h := solomachine.CreateHeader() + h.Timestamp-- + header = h + }, false, + }, + { + "signature uses wrong sequence", + func() { + clientState = solomachine.ClientState() + solomachine.Sequence++ + header = solomachine.CreateHeader() + }, + false, + }, + { + "signature uses new pubkey to sign", + func() { + // store in temp before assinging to interface type + cs := solomachine.ClientState() + h := solomachine.CreateHeader() + + publicKey, err := codectypes.NewAnyWithValue(solomachine.PublicKey) + suite.NoError(err) + + data := &types.HeaderData{ + NewPubKey: publicKey, + NewDiversifier: h.NewDiversifier, + } + + dataBz, err := suite.chainA.Codec.Marshal(data) + suite.Require().NoError(err) + + // generate invalid signature + signBytes := &types.SignBytes{ + Sequence: cs.Sequence, + Timestamp: solomachine.Time, + Diversifier: solomachine.Diversifier, + DataType: types.CLIENT, + Data: dataBz, + } + + signBz, err := suite.chainA.Codec.Marshal(signBytes) + suite.Require().NoError(err) + + sig := solomachine.GenerateSignature(signBz) + suite.Require().NoError(err) + h.Signature = sig + + clientState = cs + header = h + + }, + false, + }, + { + "signature signs over old pubkey", + func() { + // store in temp before assinging to interface type + cs := solomachine.ClientState() + oldPubKey := solomachine.PublicKey + h := solomachine.CreateHeader() + + // generate invalid signature + data := append(sdk.Uint64ToBigEndian(cs.Sequence), oldPubKey.Bytes()...) + sig := solomachine.GenerateSignature(data) + h.Signature = sig + + clientState = cs + header = h + }, + false, + }, + { + "consensus state public key is nil", + func() { + cs := solomachine.ClientState() + cs.ConsensusState.PublicKey = nil + clientState = cs + header = solomachine.CreateHeader() + }, + false, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + // setup test + tc.setup() + + clientState, consensusState, err := clientState.CheckHeaderAndUpdateState(suite.chainA.GetContext(), suite.chainA.Codec, suite.store, header) + + if tc.expPass { + suite.Require().NoError(err) + suite.Require().Equal(header.(*types.Header).NewPublicKey, clientState.(*types.ClientState).ConsensusState.PublicKey) + suite.Require().Equal(false, clientState.(*types.ClientState).IsFrozen) + suite.Require().Equal(header.(*types.Header).Sequence+1, clientState.(*types.ClientState).Sequence) + suite.Require().Equal(consensusState, clientState.(*types.ClientState).ConsensusState) + } else { + suite.Require().Error(err) + suite.Require().Nil(clientState) + suite.Require().Nil(consensusState) + } + }) + } + } +} diff --git a/modules/light-clients/07-tendermint/misbehaviour_handle.go b/modules/light-clients/07-tendermint/misbehaviour_handle.go index 0417dd4da17..98878fbbbe8 100644 --- a/modules/light-clients/07-tendermint/misbehaviour_handle.go +++ b/modules/light-clients/07-tendermint/misbehaviour_handle.go @@ -1,4 +1,4 @@ -package tendermint +package types import ( "bytes" @@ -9,10 +9,11 @@ import ( sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" tmtypes "github.com/tendermint/tendermint/types" - clienttypes "github.com/cosmos/ibc-go/v5/modules/core/02-client/types" + clienttypes "github.com/cosmos/ibc-go/v3/modules/core/02-client/types" + "github.com/cosmos/ibc-go/v3/modules/core/exported" ) -// verifyMisbehaviour determines whether or not two conflicting +// CheckMisbehaviourAndUpdateState determines whether or not two conflicting // headers at the same height would have convinced the light client. // // NOTE: consensusState1 is the trusted consensus state that corresponds to the TrustedHeight @@ -20,42 +21,58 @@ import ( // Similarly, consensusState2 is the trusted consensus state that corresponds // to misbehaviour.Header2 // Misbehaviour sets frozen height to {0, 1} since it is only used as a boolean value (zero or non-zero). -func (cs *ClientState) verifyMisbehaviour(ctx sdk.Context, clientStore sdk.KVStore, cdc codec.BinaryCodec, misbehaviour *Misbehaviour) error { +func (cs ClientState) CheckMisbehaviourAndUpdateState( + ctx sdk.Context, + cdc codec.BinaryCodec, + clientStore sdk.KVStore, + misbehaviour exported.ClientMessage, +) (exported.ClientState, error) { + tmMisbehaviour, ok := misbehaviour.(*Misbehaviour) + if !ok { + return nil, sdkerrors.Wrapf(clienttypes.ErrInvalidClientType, "expected type %T, got %T", misbehaviour, &Misbehaviour{}) + } + + // The status of the client is checked in 02-client + // if heights are equal check that this is valid misbehaviour of a fork // otherwise if heights are unequal check that this is valid misbehavior of BFT time violation - if misbehaviour.Header1.GetHeight().EQ(misbehaviour.Header2.GetHeight()) { - blockID1, err := tmtypes.BlockIDFromProto(&misbehaviour.Header1.SignedHeader.Commit.BlockID) + if tmMisbehaviour.Header1.GetHeight().EQ(tmMisbehaviour.Header2.GetHeight()) { + blockID1, err := tmtypes.BlockIDFromProto(&tmMisbehaviour.Header1.SignedHeader.Commit.BlockID) if err != nil { - return sdkerrors.Wrap(err, "invalid block ID from header 1 in misbehaviour") + return nil, sdkerrors.Wrap(err, "invalid block ID from header 1 in misbehaviour") } - - blockID2, err := tmtypes.BlockIDFromProto(&misbehaviour.Header2.SignedHeader.Commit.BlockID) + blockID2, err := tmtypes.BlockIDFromProto(&tmMisbehaviour.Header2.SignedHeader.Commit.BlockID) if err != nil { - return sdkerrors.Wrap(err, "invalid block ID from header 2 in misbehaviour") + return nil, sdkerrors.Wrap(err, "invalid block ID from header 2 in misbehaviour") } // Ensure that Commit Hashes are different if bytes.Equal(blockID1.Hash, blockID2.Hash) { - return sdkerrors.Wrap(clienttypes.ErrInvalidMisbehaviour, "headers block hashes are equal") + return nil, sdkerrors.Wrap(clienttypes.ErrInvalidMisbehaviour, "headers block hashes are equal") } - - } else if misbehaviour.Header1.SignedHeader.Header.Time.After(misbehaviour.Header2.SignedHeader.Header.Time) { + } else { // Header1 is at greater height than Header2, therefore Header1 time must be less than or equal to // Header2 time in order to be valid misbehaviour (violation of monotonic time). - return sdkerrors.Wrap(clienttypes.ErrInvalidMisbehaviour, "headers are not at same height and are monotonically increasing") + if tmMisbehaviour.Header1.SignedHeader.Header.Time.After(tmMisbehaviour.Header2.SignedHeader.Header.Time) { + return nil, sdkerrors.Wrap(clienttypes.ErrInvalidMisbehaviour, "headers are not at same height and are monotonically increasing") + } } // Regardless of the type of misbehaviour, ensure that both headers are valid and would have been accepted by light-client // Retrieve trusted consensus states for each Header in misbehaviour - tmConsensusState1, found := GetConsensusState(clientStore, cdc, misbehaviour.Header1.TrustedHeight) - if !found { - return sdkerrors.Wrapf(clienttypes.ErrConsensusStateNotFound, "could not get trusted consensus state from clientStore for Header1 at TrustedHeight: %s", misbehaviour.Header1.TrustedHeight) + // and unmarshal from clientStore + + // Get consensus bytes from clientStore + tmConsensusState1, err := GetConsensusState(clientStore, cdc, tmMisbehaviour.Header1.TrustedHeight) + if err != nil { + return nil, sdkerrors.Wrapf(err, "could not get trusted consensus state from clientStore for Header1 at TrustedHeight: %s", tmMisbehaviour.Header1) } - tmConsensusState2, found := GetConsensusState(clientStore, cdc, misbehaviour.Header2.TrustedHeight) - if !found { - return sdkerrors.Wrapf(clienttypes.ErrConsensusStateNotFound, "could not get trusted consensus state from clientStore for Header2 at TrustedHeight: %s", misbehaviour.Header2.TrustedHeight) + // Get consensus bytes from clientStore + tmConsensusState2, err := GetConsensusState(clientStore, cdc, tmMisbehaviour.Header2.TrustedHeight) + if err != nil { + return nil, sdkerrors.Wrapf(err, "could not get trusted consensus state from clientStore for Header2 at TrustedHeight: %s", tmMisbehaviour.Header2) } // Check the validity of the two conflicting headers against their respective @@ -64,17 +81,19 @@ func (cs *ClientState) verifyMisbehaviour(ctx sdk.Context, clientStore sdk.KVSto // misbehaviour.ValidateBasic by the client keeper and msg.ValidateBasic // by the base application. if err := checkMisbehaviourHeader( - cs, tmConsensusState1, misbehaviour.Header1, ctx.BlockTime(), + &cs, tmConsensusState1, tmMisbehaviour.Header1, ctx.BlockTime(), ); err != nil { - return sdkerrors.Wrap(err, "verifying Header1 in Misbehaviour failed") + return nil, sdkerrors.Wrap(err, "verifying Header1 in Misbehaviour failed") } if err := checkMisbehaviourHeader( - cs, tmConsensusState2, misbehaviour.Header2, ctx.BlockTime(), + &cs, tmConsensusState2, tmMisbehaviour.Header2, ctx.BlockTime(), ); err != nil { - return sdkerrors.Wrap(err, "verifying Header2 in Misbehaviour failed") + return nil, sdkerrors.Wrap(err, "verifying Header2 in Misbehaviour failed") } - return nil + cs.FrozenHeight = FrozenHeight + + return &cs, nil } // checkMisbehaviourHeader checks that a Header in Misbehaviour is valid misbehaviour given @@ -82,6 +101,7 @@ func (cs *ClientState) verifyMisbehaviour(ctx sdk.Context, clientStore sdk.KVSto func checkMisbehaviourHeader( clientState *ClientState, consState *ConsensusState, header *Header, currentTimestamp time.Time, ) error { + tmTrustedValset, err := tmtypes.ValidatorSetFromProto(header.TrustedValidators) if err != nil { return sdkerrors.Wrap(err, "trusted validator set is not tendermint validator set type") @@ -109,8 +129,6 @@ func checkMisbehaviourHeader( chainID := clientState.GetChainID() // If chainID is in revision format, then set revision number of chainID with the revision number // of the misbehaviour header - // NOTE: misbehaviour verification is not supported for chains which upgrade to a new chainID without - // strictly following the chainID revision format if clienttypes.IsRevisionFormat(chainID) { chainID, _ = clienttypes.SetRevisionNumber(chainID, header.GetHeight().GetRevisionNumber()) } diff --git a/modules/light-clients/07-tendermint/types/misbehaviour_handle_test.go b/modules/light-clients/07-tendermint/types/misbehaviour_handle_test.go new file mode 100644 index 00000000000..8efe54c7fba --- /dev/null +++ b/modules/light-clients/07-tendermint/types/misbehaviour_handle_test.go @@ -0,0 +1,430 @@ +package types_test + +import ( + "fmt" + "time" + + "github.com/tendermint/tendermint/crypto/tmhash" + tmtypes "github.com/tendermint/tendermint/types" + + clienttypes "github.com/cosmos/ibc-go/v3/modules/core/02-client/types" + commitmenttypes "github.com/cosmos/ibc-go/v3/modules/core/23-commitment/types" + "github.com/cosmos/ibc-go/v3/modules/core/exported" + "github.com/cosmos/ibc-go/v3/modules/light-clients/07-tendermint/types" + ibctesting "github.com/cosmos/ibc-go/v3/testing" + ibctestingmock "github.com/cosmos/ibc-go/v3/testing/mock" +) + +func (suite *TendermintTestSuite) TestCheckMisbehaviourAndUpdateState() { + altPrivVal := ibctestingmock.NewPV() + altPubKey, err := altPrivVal.GetPubKey() + suite.Require().NoError(err) + + altVal := tmtypes.NewValidator(altPubKey, 4) + + // Create bothValSet with both suite validator and altVal + bothValSet := tmtypes.NewValidatorSet(append(suite.valSet.Validators, altVal)) + bothValsHash := bothValSet.Hash() + // Create alternative validator set with only altVal + altValSet := tmtypes.NewValidatorSet([]*tmtypes.Validator{altVal}) + + _, suiteVal := suite.valSet.GetByIndex(0) + + // Create signer array and ensure it is in same order as bothValSet + bothSigners := ibctesting.CreateSortedSignerArray(altPrivVal, suite.privVal, altVal, suiteVal) + + altSigners := []tmtypes.PrivValidator{altPrivVal} + + heightMinus1 := clienttypes.NewHeight(height.RevisionNumber, height.RevisionHeight-1) + heightMinus3 := clienttypes.NewHeight(height.RevisionNumber, height.RevisionHeight-3) + + testCases := []struct { + name string + clientState exported.ClientState + consensusState1 exported.ConsensusState + height1 clienttypes.Height + consensusState2 exported.ConsensusState + height2 clienttypes.Height + misbehaviour exported.ClientMessage + timestamp time.Time + expPass bool + }{ + { + "valid fork misbehaviour", + types.NewClientState(chainID, types.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, height, commitmenttypes.GetSDKSpecs(), upgradePath, false, false), + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + &types.Misbehaviour{ + Header1: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), height, suite.now, bothValSet, bothValSet, bothSigners), + Header2: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), height, suite.now.Add(time.Minute), bothValSet, bothValSet, bothSigners), + ClientId: chainID, + }, + suite.now, + true, + }, + { + "valid time misbehaviour", + types.NewClientState(chainID, types.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, height, commitmenttypes.GetSDKSpecs(), upgradePath, false, false), + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + &types.Misbehaviour{ + Header1: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+3), height, suite.now, bothValSet, bothValSet, bothSigners), + Header2: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), height, suite.now, bothValSet, bothValSet, bothSigners), + ClientId: chainID, + }, + suite.now, + true, + }, + { + "valid time misbehaviour header 1 stricly less than header 2", + types.NewClientState(chainID, types.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, height, commitmenttypes.GetSDKSpecs(), upgradePath, false, false), + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + &types.Misbehaviour{ + Header1: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+3), height, suite.now, bothValSet, bothValSet, bothSigners), + Header2: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), height, suite.now.Add(time.Hour), bothValSet, bothValSet, bothSigners), + ClientId: chainID, + }, + suite.now, + true, + }, + { + "valid misbehavior at height greater than last consensusState", + types.NewClientState(chainID, types.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, height, commitmenttypes.GetSDKSpecs(), upgradePath, false, false), + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + heightMinus1, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + heightMinus1, + &types.Misbehaviour{ + Header1: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), heightMinus1, suite.now, bothValSet, bothValSet, bothSigners), + Header2: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), heightMinus1, suite.now.Add(time.Minute), bothValSet, bothValSet, bothSigners), + ClientId: chainID, + }, + suite.now, + true, + }, + { + "valid misbehaviour with different trusted heights", + types.NewClientState(chainID, types.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, height, commitmenttypes.GetSDKSpecs(), upgradePath, false, false), + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + heightMinus1, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), suite.valsHash), + heightMinus3, + &types.Misbehaviour{ + Header1: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), heightMinus1, suite.now, bothValSet, bothValSet, bothSigners), + Header2: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), heightMinus3, suite.now.Add(time.Minute), bothValSet, suite.valSet, bothSigners), + ClientId: chainID, + }, + suite.now, + true, + }, + { + "valid misbehaviour at a previous revision", + types.NewClientState(chainIDRevision1, types.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, clienttypes.NewHeight(1, 1), commitmenttypes.GetSDKSpecs(), upgradePath, false, false), + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + heightMinus1, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), suite.valsHash), + heightMinus3, + &types.Misbehaviour{ + Header1: suite.chainA.CreateTMClientHeader(chainIDRevision0, int64(height.RevisionHeight+1), heightMinus1, suite.now, bothValSet, bothValSet, bothSigners), + Header2: suite.chainA.CreateTMClientHeader(chainIDRevision0, int64(height.RevisionHeight+1), heightMinus3, suite.now.Add(time.Minute), bothValSet, suite.valSet, bothSigners), + ClientId: chainID, + }, + suite.now, + true, + }, + { + "valid misbehaviour at a future revision", + types.NewClientState(chainIDRevision0, types.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, height, commitmenttypes.GetSDKSpecs(), upgradePath, false, false), + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + heightMinus1, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), suite.valsHash), + heightMinus3, + &types.Misbehaviour{ + Header1: suite.chainA.CreateTMClientHeader(chainIDRevision0, 3, heightMinus1, suite.now, bothValSet, bothValSet, bothSigners), + Header2: suite.chainA.CreateTMClientHeader(chainIDRevision0, 3, heightMinus3, suite.now.Add(time.Minute), bothValSet, suite.valSet, bothSigners), + ClientId: chainID, + }, + suite.now, + true, + }, + { + "valid misbehaviour with trusted heights at a previous revision", + types.NewClientState(chainIDRevision1, types.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, clienttypes.NewHeight(1, 1), commitmenttypes.GetSDKSpecs(), upgradePath, false, false), + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + heightMinus1, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), suite.valsHash), + heightMinus3, + &types.Misbehaviour{ + Header1: suite.chainA.CreateTMClientHeader(chainIDRevision1, 1, heightMinus1, suite.now, bothValSet, bothValSet, bothSigners), + Header2: suite.chainA.CreateTMClientHeader(chainIDRevision1, 1, heightMinus3, suite.now.Add(time.Minute), bothValSet, suite.valSet, bothSigners), + ClientId: chainID, + }, + suite.now, + true, + }, + { + "consensus state's valset hash different from misbehaviour should still pass", + types.NewClientState(chainID, types.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, height, commitmenttypes.GetSDKSpecs(), upgradePath, false, false), + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), suite.valsHash), + height, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), suite.valsHash), + height, + &types.Misbehaviour{ + Header1: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), height, suite.now, bothValSet, suite.valSet, bothSigners), + Header2: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), height, suite.now.Add(time.Minute), bothValSet, suite.valSet, bothSigners), + ClientId: chainID, + }, + suite.now, + true, + }, + { + "invalid fork misbehaviour: identical headers", + types.NewClientState(chainID, types.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, height, commitmenttypes.GetSDKSpecs(), upgradePath, false, false), + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + &types.Misbehaviour{ + Header1: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), height, suite.now, bothValSet, bothValSet, bothSigners), + Header2: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), height, suite.now, bothValSet, bothValSet, bothSigners), + ClientId: chainID, + }, + suite.now, + false, + }, + { + "invalid time misbehaviour: monotonically increasing time", + types.NewClientState(chainID, types.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, height, commitmenttypes.GetSDKSpecs(), upgradePath, false, false), + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + &types.Misbehaviour{ + Header1: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+3), height, suite.now.Add(time.Minute), bothValSet, bothValSet, bothSigners), + Header2: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), height, suite.now, bothValSet, bothValSet, bothSigners), + ClientId: chainID, + }, + suite.now, + false, + }, + { + "invalid misbehavior misbehaviour from different chain", + types.NewClientState(chainID, types.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, height, commitmenttypes.GetSDKSpecs(), upgradePath, false, false), + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + &types.Misbehaviour{ + Header1: suite.chainA.CreateTMClientHeader("ethermint", int64(height.RevisionHeight+1), height, suite.now, bothValSet, bothValSet, bothSigners), + Header2: suite.chainA.CreateTMClientHeader("ethermint", int64(height.RevisionHeight+1), height, suite.now.Add(time.Minute), bothValSet, bothValSet, bothSigners), + ClientId: chainID, + }, + suite.now, + false, + }, + { + "invalid misbehavior misbehaviour with trusted height different from trusted consensus state", + types.NewClientState(chainID, types.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, height, commitmenttypes.GetSDKSpecs(), upgradePath, false, false), + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + heightMinus1, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), suite.valsHash), + heightMinus3, + &types.Misbehaviour{ + Header1: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), heightMinus1, suite.now, bothValSet, bothValSet, bothSigners), + Header2: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), height, suite.now.Add(time.Minute), bothValSet, suite.valSet, bothSigners), + ClientId: chainID, + }, + suite.now, + false, + }, + { + "invalid misbehavior misbehaviour with trusted validators different from trusted consensus state", + types.NewClientState(chainID, types.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, height, commitmenttypes.GetSDKSpecs(), upgradePath, false, false), + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + heightMinus1, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), suite.valsHash), + heightMinus3, + &types.Misbehaviour{ + Header1: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), heightMinus1, suite.now, bothValSet, bothValSet, bothSigners), + Header2: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), heightMinus3, suite.now.Add(time.Minute), bothValSet, bothValSet, bothSigners), + ClientId: chainID, + }, + suite.now, + false, + }, + { + "already frozen client state", + &types.ClientState{FrozenHeight: clienttypes.NewHeight(0, 1)}, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + &types.Misbehaviour{ + Header1: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), height, suite.now, bothValSet, bothValSet, bothSigners), + Header2: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), height, suite.now.Add(time.Minute), bothValSet, bothValSet, bothSigners), + ClientId: chainID, + }, + suite.now, + false, + }, + { + "trusted consensus state does not exist", + types.NewClientState(chainID, types.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, height, commitmenttypes.GetSDKSpecs(), upgradePath, false, false), + nil, // consensus state for trusted height - 1 does not exist in store + clienttypes.Height{}, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + &types.Misbehaviour{ + Header1: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), heightMinus1, suite.now, bothValSet, bothValSet, bothSigners), + Header2: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), height, suite.now.Add(time.Minute), bothValSet, bothValSet, bothSigners), + ClientId: chainID, + }, + suite.now, + false, + }, + { + "invalid tendermint misbehaviour", + types.NewClientState(chainID, types.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, height, commitmenttypes.GetSDKSpecs(), upgradePath, false, false), + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + nil, + suite.now, + false, + }, + { + "provided height > header height", + types.NewClientState(chainID, types.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, height, commitmenttypes.GetSDKSpecs(), upgradePath, false, false), + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + &types.Misbehaviour{ + Header1: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), heightMinus1, suite.now, bothValSet, bothValSet, bothSigners), + Header2: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), heightMinus1, suite.now.Add(time.Minute), bothValSet, bothValSet, bothSigners), + ClientId: chainID, + }, + suite.now, + false, + }, + { + "trusting period expired", + types.NewClientState(chainID, types.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, height, commitmenttypes.GetSDKSpecs(), upgradePath, false, false), + types.NewConsensusState(time.Time{}, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + heightMinus1, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + &types.Misbehaviour{ + Header1: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), heightMinus1, suite.now, bothValSet, bothValSet, bothSigners), + Header2: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), height, suite.now.Add(time.Minute), bothValSet, bothValSet, bothSigners), + ClientId: chainID, + }, + suite.now.Add(trustingPeriod), + false, + }, + { + "trusted validators is incorrect for given consensus state", + types.NewClientState(chainID, types.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, height, commitmenttypes.GetSDKSpecs(), upgradePath, false, false), + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + &types.Misbehaviour{ + Header1: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), height, suite.now, bothValSet, suite.valSet, bothSigners), + Header2: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), height, suite.now.Add(time.Minute), bothValSet, suite.valSet, bothSigners), + ClientId: chainID, + }, + suite.now, + false, + }, + { + "first valset has too much change", + types.NewClientState(chainID, types.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, height, commitmenttypes.GetSDKSpecs(), upgradePath, false, false), + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + &types.Misbehaviour{ + Header1: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), height, suite.now, altValSet, bothValSet, altSigners), + Header2: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), height, suite.now.Add(time.Minute), bothValSet, bothValSet, bothSigners), + ClientId: chainID, + }, + suite.now, + false, + }, + { + "second valset has too much change", + types.NewClientState(chainID, types.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, height, commitmenttypes.GetSDKSpecs(), upgradePath, false, false), + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + &types.Misbehaviour{ + Header1: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), height, suite.now, bothValSet, bothValSet, bothSigners), + Header2: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), height, suite.now.Add(time.Minute), altValSet, bothValSet, altSigners), + ClientId: chainID, + }, + suite.now, + false, + }, + { + "both valsets have too much change", + types.NewClientState(chainID, types.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, height, commitmenttypes.GetSDKSpecs(), upgradePath, false, false), + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + types.NewConsensusState(suite.now, commitmenttypes.NewMerkleRoot(tmhash.Sum([]byte("app_hash"))), bothValsHash), + height, + &types.Misbehaviour{ + Header1: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), height, suite.now, altValSet, bothValSet, altSigners), + Header2: suite.chainA.CreateTMClientHeader(chainID, int64(height.RevisionHeight+1), height, suite.now.Add(time.Minute), altValSet, bothValSet, altSigners), + ClientId: chainID, + }, + suite.now, + false, + }, + } + + for i, tc := range testCases { + tc := tc + suite.Run(fmt.Sprintf("Case: %s", tc.name), func() { + // reset suite to create fresh application state + suite.SetupTest() + + // Set current timestamp in context + ctx := suite.chainA.GetContext().WithBlockTime(tc.timestamp) + + // Set trusted consensus states in client store + + if tc.consensusState1 != nil { + suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientConsensusState(ctx, clientID, tc.height1, tc.consensusState1) + } + if tc.consensusState2 != nil { + suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientConsensusState(ctx, clientID, tc.height2, tc.consensusState2) + } + + clientState, err := tc.clientState.CheckMisbehaviourAndUpdateState( + ctx, + suite.chainA.App.AppCodec(), + suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(ctx, clientID), // pass in clientID prefixed clientStore + tc.misbehaviour, + ) + + if tc.expPass { + suite.Require().NoError(err, "valid test case %d failed: %s", i, tc.name) + suite.Require().NotNil(clientState, "valid test case %d failed: %s", i, tc.name) + suite.Require().True(!clientState.(*types.ClientState).FrozenHeight.IsZero(), "valid test case %d failed: %s", i, tc.name) + } else { + suite.Require().Error(err, "invalid test case %d passed: %s", i, tc.name) + suite.Require().Nil(clientState, "invalid test case %d passed: %s", i, tc.name) + } + }) + } +} diff --git a/modules/light-clients/07-tendermint/types/update.go b/modules/light-clients/07-tendermint/types/update.go new file mode 100644 index 00000000000..2699ebc2cc9 --- /dev/null +++ b/modules/light-clients/07-tendermint/types/update.go @@ -0,0 +1,263 @@ +package types + +import ( + "bytes" + "reflect" + "time" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/tendermint/tendermint/light" + tmtypes "github.com/tendermint/tendermint/types" + + clienttypes "github.com/cosmos/ibc-go/v3/modules/core/02-client/types" + commitmenttypes "github.com/cosmos/ibc-go/v3/modules/core/23-commitment/types" + "github.com/cosmos/ibc-go/v3/modules/core/exported" +) + +// CheckHeaderAndUpdateState checks if the provided header is valid, and if valid it will: +// create the consensus state for the header.Height +// and update the client state if the header height is greater than the latest client state height +// It returns an error if: +// - the client or header provided are not parseable to tendermint types +// - the header is invalid +// - header height is less than or equal to the trusted header height +// - header revision is not equal to trusted header revision +// - header valset commit verification fails +// - header timestamp is past the trusting period in relation to the consensus state +// - header timestamp is less than or equal to the consensus state timestamp +// +// UpdateClient may be used to either create a consensus state for: +// - a future height greater than the latest client state height +// - a past height that was skipped during bisection +// If we are updating to a past height, a consensus state is created for that height to be persisted in client store +// If we are updating to a future height, the consensus state is created and the client state is updated to reflect +// the new latest height +// UpdateClient must only be used to update within a single revision, thus header revision number and trusted height's revision +// number must be the same. To update to a new revision, use a separate upgrade path +// Tendermint client validity checking uses the bisection algorithm described +// in the [Tendermint spec](https://github.com/tendermint/spec/blob/master/spec/consensus/light-client.md). +// +// Misbehaviour Detection: +// UpdateClient will detect implicit misbehaviour by enforcing certain invariants on any new update call and will return a frozen client. +// 1. Any valid update that creates a different consensus state for an already existing height is evidence of misbehaviour and will freeze client. +// 2. Any valid update that breaks time monotonicity with respect to its neighboring consensus states is evidence of misbehaviour and will freeze client. +// Misbehaviour sets frozen height to {0, 1} since it is only used as a boolean value (zero or non-zero). +// +// Pruning: +// UpdateClient will additionally retrieve the earliest consensus state for this clientID and check if it is expired. If it is, +// that consensus state will be pruned from store along with all associated metadata. This will prevent the client store from +// becoming bloated with expired consensus states that can no longer be used for updates and packet verification. +func (cs ClientState) CheckHeaderAndUpdateState( + ctx sdk.Context, cdc codec.BinaryCodec, clientStore sdk.KVStore, + header exported.ClientMessage, +) (exported.ClientState, exported.ConsensusState, error) { + tmHeader, ok := header.(*Header) + if !ok { + return nil, nil, sdkerrors.Wrapf( + clienttypes.ErrInvalidHeader, "expected type %T, got %T", &Header{}, header, + ) + } + + // Check if the Client store already has a consensus state for the header's height + // If the consensus state exists, and it matches the header then we return early + // since header has already been submitted in a previous UpdateClient. + var conflictingHeader bool + prevConsState, _ := GetConsensusState(clientStore, cdc, header.GetHeight()) + if prevConsState != nil { + // This header has already been submitted and the necessary state is already stored + // in client store, thus we can return early without further validation. + if reflect.DeepEqual(prevConsState, tmHeader.ConsensusState()) { + return &cs, prevConsState, nil + } + // A consensus state already exists for this height, but it does not match the provided header. + // Thus, we must check that this header is valid, and if so we will freeze the client. + conflictingHeader = true + } + + // get consensus state from clientStore + trustedConsState, err := GetConsensusState(clientStore, cdc, tmHeader.TrustedHeight) + if err != nil { + return nil, nil, sdkerrors.Wrapf( + err, "could not get consensus state from clientstore at TrustedHeight: %s", tmHeader.TrustedHeight, + ) + } + + if err := checkValidity(&cs, trustedConsState, tmHeader, ctx.BlockTime()); err != nil { + return nil, nil, err + } + + consState := tmHeader.ConsensusState() + // Header is different from existing consensus state and also valid, so freeze the client and return + if conflictingHeader { + cs.FrozenHeight = FrozenHeight + return &cs, consState, nil + } + // Check that consensus state timestamps are monotonic + prevCons, prevOk := GetPreviousConsensusState(clientStore, cdc, header.GetHeight()) + nextCons, nextOk := GetNextConsensusState(clientStore, cdc, header.GetHeight()) + // if previous consensus state exists, check consensus state time is greater than previous consensus state time + // if previous consensus state is not before current consensus state, freeze the client and return. + if prevOk && !prevCons.Timestamp.Before(consState.Timestamp) { + cs.FrozenHeight = FrozenHeight + return &cs, consState, nil + } + // if next consensus state exists, check consensus state time is less than next consensus state time + // if next consensus state is not after current consensus state, freeze the client and return. + if nextOk && !nextCons.Timestamp.After(consState.Timestamp) { + cs.FrozenHeight = FrozenHeight + return &cs, consState, nil + } + + // Check the earliest consensus state to see if it is expired, if so then set the prune height + // so that we can delete consensus state and all associated metadata. + var ( + pruneHeight exported.Height + pruneError error + ) + pruneCb := func(height exported.Height) bool { + consState, err := GetConsensusState(clientStore, cdc, height) + // this error should never occur + if err != nil { + pruneError = err + return true + } + if cs.IsExpired(consState.Timestamp, ctx.BlockTime()) { + pruneHeight = height + } + return true + } + IterateConsensusStateAscending(clientStore, pruneCb) + if pruneError != nil { + return nil, nil, pruneError + } + // if pruneHeight is set, delete consensus state and metadata + if pruneHeight != nil { + deleteConsensusState(clientStore, pruneHeight) + deleteConsensusMetadata(clientStore, pruneHeight) + } + + newClientState, consensusState := update(ctx, clientStore, &cs, tmHeader) + return newClientState, consensusState, nil +} + +// checkTrustedHeader checks that consensus state matches trusted fields of Header +func checkTrustedHeader(header *Header, consState *ConsensusState) error { + tmTrustedValidators, err := tmtypes.ValidatorSetFromProto(header.TrustedValidators) + if err != nil { + return sdkerrors.Wrap(err, "trusted validator set in not tendermint validator set type") + } + + // assert that trustedVals is NextValidators of last trusted header + // to do this, we check that trustedVals.Hash() == consState.NextValidatorsHash + tvalHash := tmTrustedValidators.Hash() + if !bytes.Equal(consState.NextValidatorsHash, tvalHash) { + return sdkerrors.Wrapf( + ErrInvalidValidatorSet, + "trusted validators %s, does not hash to latest trusted validators. Expected: %X, got: %X", + header.TrustedValidators, consState.NextValidatorsHash, tvalHash, + ) + } + return nil +} + +// checkValidity checks if the Tendermint header is valid. +// CONTRACT: consState.Height == header.TrustedHeight +func checkValidity( + clientState *ClientState, consState *ConsensusState, + header *Header, currentTimestamp time.Time, +) error { + if err := checkTrustedHeader(header, consState); err != nil { + return err + } + + // UpdateClient only accepts updates with a header at the same revision + // as the trusted consensus state + if header.GetHeight().GetRevisionNumber() != header.TrustedHeight.RevisionNumber { + return sdkerrors.Wrapf( + ErrInvalidHeaderHeight, + "header height revision %d does not match trusted header revision %d", + header.GetHeight().GetRevisionNumber(), header.TrustedHeight.RevisionNumber, + ) + } + + tmTrustedValidators, err := tmtypes.ValidatorSetFromProto(header.TrustedValidators) + if err != nil { + return sdkerrors.Wrap(err, "trusted validator set in not tendermint validator set type") + } + + tmSignedHeader, err := tmtypes.SignedHeaderFromProto(header.SignedHeader) + if err != nil { + return sdkerrors.Wrap(err, "signed header in not tendermint signed header type") + } + + tmValidatorSet, err := tmtypes.ValidatorSetFromProto(header.ValidatorSet) + if err != nil { + return sdkerrors.Wrap(err, "validator set in not tendermint validator set type") + } + + // assert header height is newer than consensus state + if header.GetHeight().LTE(header.TrustedHeight) { + return sdkerrors.Wrapf( + clienttypes.ErrInvalidHeader, + "header height ≤ consensus state height (%s ≤ %s)", header.GetHeight(), header.TrustedHeight, + ) + } + + chainID := clientState.GetChainID() + // If chainID is in revision format, then set revision number of chainID with the revision number + // of the header we are verifying + // This is useful if the update is at a previous revision rather than an update to the latest revision + // of the client. + // The chainID must be set correctly for the previous revision before attempting verification. + // Updates for previous revisions are not supported if the chainID is not in revision format. + if clienttypes.IsRevisionFormat(chainID) { + chainID, _ = clienttypes.SetRevisionNumber(chainID, header.GetHeight().GetRevisionNumber()) + } + + // Construct a trusted header using the fields in consensus state + // Only Height, Time, and NextValidatorsHash are necessary for verification + trustedHeader := tmtypes.Header{ + ChainID: chainID, + Height: int64(header.TrustedHeight.RevisionHeight), + Time: consState.Timestamp, + NextValidatorsHash: consState.NextValidatorsHash, + } + signedHeader := tmtypes.SignedHeader{ + Header: &trustedHeader, + } + + // Verify next header with the passed-in trustedVals + // - asserts trusting period not passed + // - assert header timestamp is not past the trusting period + // - assert header timestamp is past latest stored consensus state timestamp + // - assert that a TrustLevel proportion of TrustedValidators signed new Commit + err = light.Verify( + &signedHeader, + tmTrustedValidators, tmSignedHeader, tmValidatorSet, + clientState.TrustingPeriod, currentTimestamp, clientState.MaxClockDrift, clientState.TrustLevel.ToTendermint(), + ) + if err != nil { + return sdkerrors.Wrap(err, "failed to verify header") + } + return nil +} + +// update the consensus state from a new header and set processed time metadata +func update(ctx sdk.Context, clientStore sdk.KVStore, clientState *ClientState, header *Header) (*ClientState, *ConsensusState) { + height := header.GetHeight().(clienttypes.Height) + if height.GT(clientState.LatestHeight) { + clientState.LatestHeight = height + } + consensusState := &ConsensusState{ + Timestamp: header.GetTime(), + Root: commitmenttypes.NewMerkleRoot(header.Header.GetAppHash()), + NextValidatorsHash: header.Header.NextValidatorsHash, + } + + // set metadata for this consensus state + setConsensusMetadata(ctx, clientStore, header.GetHeight()) + + return clientState, consensusState +} diff --git a/modules/light-clients/09-localhost/types/client_state.go b/modules/light-clients/09-localhost/types/client_state.go index d8f0af38f00..e818f9c8d7e 100644 --- a/modules/light-clients/09-localhost/types/client_state.go +++ b/modules/light-clients/09-localhost/types/client_state.go @@ -79,28 +79,28 @@ func (cs ClientState) ExportMetadata(_ sdk.KVStore) []exported.GenesisMetadata { // CheckHeaderAndUpdateState updates the localhost client. It only needs access to the context func (cs *ClientState) CheckHeaderAndUpdateState( - ctx sdk.Context, cdc codec.BinaryCodec, clientStore sdk.KVStore, header exported.Header, + ctx sdk.Context, cdc codec.BinaryCodec, clientStore sdk.KVStore, header exported.ClientMessage, ) (exported.ClientState, exported.ConsensusState, error) { return cs.UpdateState(ctx, cdc, clientStore, header) } // VerifyHeader is a no-op. func (cs *ClientState) VerifyHeader( - _ sdk.Context, _ codec.BinaryCodec, _ sdk.KVStore, _ exported.Header, + _ sdk.Context, _ codec.BinaryCodec, _ sdk.KVStore, _ exported.ClientMessage, ) (exported.ClientState, exported.ConsensusState, error) { return cs, nil, nil } // CheckForMisbehaviour returns false. func (cs *ClientState) CheckForMisbehaviour( - _ sdk.Context, _ codec.BinaryCodec, _ sdk.KVStore, _ exported.Header, + _ sdk.Context, _ codec.BinaryCodec, _ sdk.KVStore, _ exported.ClientMessage, ) (bool, error) { return false, nil } // UpdateState updates the localhost client. It only needs access to the context func (cs *ClientState) UpdateState( - ctx sdk.Context, _ codec.BinaryCodec, _ sdk.KVStore, _ exported.Header, + ctx sdk.Context, _ codec.BinaryCodec, _ sdk.KVStore, _ exported.ClientMessage, ) (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() @@ -111,7 +111,7 @@ func (cs *ClientState) UpdateState( // UpdateStateOnMisbehaviour returns an error (no misbehaviour case). func (cs *ClientState) UpdateStateOnMisbehaviour( - _ sdk.Context, _ codec.BinaryCodec, _ sdk.KVStore, _ exported.Header, + _ sdk.Context, _ codec.BinaryCodec, _ sdk.KVStore, _ exported.ClientMessage, ) (*ClientState, error) { return nil, sdkerrors.Wrapf(clienttypes.ErrUpdateClientFailed, "cannot update localhost client on misbehaviour") } @@ -120,7 +120,7 @@ func (cs *ClientState) UpdateStateOnMisbehaviour( // 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, + _ sdk.Context, _ codec.BinaryCodec, _ sdk.KVStore, _ exported.ClientMessage, ) (exported.ClientState, error) { return nil, sdkerrors.Wrap(clienttypes.ErrInvalidMisbehaviour, "cannot submit misbehaviour to localhost client") } diff --git a/testing/endpoint.go b/testing/endpoint.go index 607f7a16843..e466bd781e9 100644 --- a/testing/endpoint.go +++ b/testing/endpoint.go @@ -131,7 +131,7 @@ func (endpoint *Endpoint) UpdateClient() (err error) { // ensure counterparty has committed state endpoint.Chain.Coordinator.CommitBlock(endpoint.Counterparty.Chain) - var header exported.Header + var header exported.ClientMessage switch endpoint.ClientConfig.GetClientType() { case exported.Tendermint: