diff --git a/x/ibc/02-client/exported/exported.go b/x/ibc/02-client/exported/exported.go index 50f0183cba50..183294cae697 100644 --- a/x/ibc/02-client/exported/exported.go +++ b/x/ibc/02-client/exported/exported.go @@ -130,6 +130,13 @@ type Header interface { GetHeight() uint64 } +// message types for the IBC client +const ( + TypeMsgCreateClient string = "create_client" + TypeMsgUpdateClient string = "update_client" + TypeMsgSubmitClientMisbehaviour string = "submit_client_misbehaviour" +) + // MsgCreateClient defines the msg interface that the // CreateClient Handler expects type MsgCreateClient interface { @@ -154,12 +161,14 @@ type ClientType byte const ( Tendermint ClientType = iota + 1 // 1 Localhost + SoloMachine ) // string representation of the client types const ( - ClientTypeTendermint string = "tendermint" - ClientTypeLocalHost string = "localhost" + ClientTypeTendermint string = "tendermint" + ClientTypeLocalHost string = "localhost" + ClientTypeSoloMachine string = "solomachine" ) func (ct ClientType) String() string { diff --git a/x/ibc/02-client/keeper/client.go b/x/ibc/02-client/keeper/client.go index 993393f14093..625ba2496d83 100644 --- a/x/ibc/02-client/keeper/client.go +++ b/x/ibc/02-client/keeper/client.go @@ -70,7 +70,7 @@ func (k Keeper) UpdateClient(ctx sdk.Context, clientID string, header exported.H return nil, sdkerrors.Wrapf(types.ErrClientNotFound, "cannot update client with ID %s", clientID) } - // addittion to spec: prevent update if the client is frozen + // addition to spec: prevent update if the client is frozen if clientState.IsFrozen() { return nil, sdkerrors.Wrapf(types.ErrClientFrozen, "cannot update client with ID %s", clientID) } diff --git a/x/ibc/06-solomachine/alias.go b/x/ibc/06-solomachine/alias.go new file mode 100644 index 000000000000..e4e6bd2def02 --- /dev/null +++ b/x/ibc/06-solomachine/alias.go @@ -0,0 +1,41 @@ +package solomachine + +// nolint +// autogenerated code using github.com/rigelrozanski/multitool +// aliases generated for the following subdirectories: +// ALIASGEN: github.com/cosmos/cosmos-sdk/x/ibc/06-solomachine/types/ + +import ( + "github.com/cosmos/cosmos-sdk/x/ibc/06-solomachine/types" +) + +const ( + SubModuleName = types.SubModuleName +) + +var ( + // functions aliases + InitializeFromMsg = types.InitializeFromMsg + Initialize = types.Initialize + RegisterCodec = types.RegisterCodec + SetSubModuleCodec = types.SetSubModuleCodec + NewMsgCreateClient = types.NewMsgCreateClient + NewMsgUpdateClient = types.NewMsgUpdateClient + NewMsgSubmitClientMisbehaviour = types.NewMsgSubmitClientMisbehaviour + + // variable aliases + SubModuleCdc = types.SubModuleCdc + ErrInvalidHeader = types.ErrInvalidHeader + ErrInvalidSequence = types.ErrInvalidSequence +) + +type ( + ClientState = types.ClientState + ConsensusState = types.ConsensusState + Evidence = types.Evidence + SignatureAndData = types.SignatureAndData + Header = types.Header + MsgCreateClient = types.MsgCreateClient + MsgUpdateClient = types.MsgUpdateClient + MsgSubmitClientMisbehaviour = types.MsgSubmitClientMisbehaviour +) diff --git a/x/ibc/06-solomachine/client/cli/cli.go b/x/ibc/06-solomachine/client/cli/cli.go new file mode 100644 index 000000000000..dc8e652b40c9 --- /dev/null +++ b/x/ibc/06-solomachine/client/cli/cli.go @@ -0,0 +1,28 @@ +package cli + +import ( + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/codec" +) + +// GetTxCmd returns the transaction commands for IBC clients +func GetTxCmd(cdc *codec.Codec, storeKey string) *cobra.Command { + ics06SoloMachineTxCmd := &cobra.Command{ + Use: "solomachine", + Short: "Solo Machine transaction subcommands", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + ics06SoloMachineTxCmd.AddCommand(flags.PostCommands( + GetCmdCreateClient(cdc), + GetCmdUpdateClient(cdc), + GetCmdSubmitMisbehaviour(cdc), + )...) + + return ics06SoloMachineTxCmd +} diff --git a/x/ibc/06-solomachine/client/cli/tx.go b/x/ibc/06-solomachine/client/cli/tx.go new file mode 100644 index 000000000000..8983f8913eb6 --- /dev/null +++ b/x/ibc/06-solomachine/client/cli/tx.go @@ -0,0 +1,149 @@ +package cli + +import ( + "bufio" + "fmt" + "io/ioutil" + "strings" + + "github.com/pkg/errors" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/version" + authclient "github.com/cosmos/cosmos-sdk/x/auth/client" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + evidenceexported "github.com/cosmos/cosmos-sdk/x/evidence/exported" + ibcsmtypes "github.com/cosmos/cosmos-sdk/x/ibc/06-solomachine/types" +) + +// GetCmdCreateClient defines the command to create a new IBC Client. +func GetCmdCreateClient(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "create [client-id] [path/to/consensus_state.json]", + Short: "create new client with a consensus state", + Long: strings.TrimSpace(fmt.Sprintf(` create new client with specified identifier and consensus state: + +Example: +$ %s tx ibc client create [client-id] [path/to/consensus_state.json] --from node0 --home ../node0/cli --chain-id $CID + `, version.ClientName), + ), + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := authtypes.NewTxBuilderFromCLI(inBuf).WithTxEncoder(authclient.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInput(inBuf).WithCodec(cdc).WithBroadcastMode(flags.BroadcastBlock) + + clientID := args[0] + + var consensusState ibcsmtypes.ConsensusState + if err := cdc.UnmarshalJSON([]byte(args[1]), &consensusState); err != nil { + // check for file path if JSON input is not provided + contents, err := ioutil.ReadFile(args[1]) + if err != nil { + return errors.New("neither JSON input nor path to .json file were provided") + } + if err := cdc.UnmarshalJSON(contents, &consensusState); err != nil { + return errors.Wrap(err, "error unmarshalling consensus header file") + } + } + + msg := ibcsmtypes.NewMsgCreateClient(clientID, consensusState) + + if err := msg.ValidateBasic(); err != nil { + return err + } + + return authclient.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + return cmd +} + +// GetCmdUpdateClient defines the command to update a client. +func GetCmdUpdateClient(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "update [client-id] [path/to/header.json]", + Short: "update existing client with a header", + Long: strings.TrimSpace(fmt.Sprintf(`update existing client with a header: + +Example: +$ %s tx ibc client update [client-id] [path/to/header.json] --from node0 --home ../node0/cli --chain-id $CID + `, version.ClientName), + ), + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := authtypes.NewTxBuilderFromCLI(inBuf).WithTxEncoder(authclient.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInput(inBuf).WithCodec(cdc) + + clientID := args[0] + + var header ibcsmtypes.Header + if err := cdc.UnmarshalJSON([]byte(args[1]), &header); err != nil { + // check for file path if JSON input is not provided + contents, err := ioutil.ReadFile(args[1]) + if err != nil { + return errors.New("neither JSON input nor path to .json file were provided") + } + if err := cdc.UnmarshalJSON(contents, &header); err != nil { + return errors.Wrap(err, "error unmarshalling header file") + } + } + + msg := ibcsmtypes.NewMsgUpdateClient(clientID, header) + if err := msg.ValidateBasic(); err != nil { + return err + } + + return authclient.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + return cmd +} + +// GetCmdSubmitMisbehaviour defines the command to submit a misbehaviour to prevent +// future updates as defined in +// https://github.com/cosmos/ics/tree/master/spec/ics-002-client-semantics#misbehaviour +func GetCmdSubmitMisbehaviour(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "misbehaviour [path/to/evidence.json]", + Short: "submit a client misbehaviour", + Long: strings.TrimSpace(fmt.Sprintf(`submit a client misbehaviour to prevent future updates: + +Example: +$ %s tx ibc client misbehaviour [path/to/evidence.json] --from node0 --home ../node0/cli --chain-id $CID + `, version.ClientName), + ), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := authtypes.NewTxBuilderFromCLI(inBuf).WithTxEncoder(authclient.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInput(inBuf).WithCodec(cdc) + + var ev evidenceexported.Evidence + if err := cdc.UnmarshalJSON([]byte(args[0]), &ev); err != nil { + // check for file path if JSON input is not provided + contents, err := ioutil.ReadFile(args[0]) + if err != nil { + return errors.New("neither JSON input nor path to .json file were provided") + } + if err := cdc.UnmarshalJSON(contents, &ev); err != nil { + return errors.Wrap(err, "error unmarshalling evidence file") + } + } + + msg := ibcsmtypes.NewMsgSubmitClientMisbehaviour(ev, cliCtx.GetFromAddress()) + if err := msg.ValidateBasic(); err != nil { + return err + } + + return authclient.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + return cmd +} diff --git a/x/ibc/06-solomachine/client/rest/rest.go b/x/ibc/06-solomachine/client/rest/rest.go new file mode 100644 index 000000000000..7f731e121cb3 --- /dev/null +++ b/x/ibc/06-solomachine/client/rest/rest.go @@ -0,0 +1,40 @@ +package rest + +import ( + "github.com/gorilla/mux" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/types/rest" + evidenceexported "github.com/cosmos/cosmos-sdk/x/evidence/exported" + ibcsmtypes "github.com/cosmos/cosmos-sdk/x/ibc/06-solomachine/types" +) + +// REST client flags +const ( + RestClientID = "client-id" +) + +// RegisterRoutes - General function to define routes that get registered by the main application +func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, queryRoute string) { + registerTxRoutes(cliCtx, r) +} + +// CreateClientReq defines the properties of a create client request's body. +type CreateClientReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + ClientID string `json:"client_id" yaml:"client_id"` + ConsensusState ibcsmtypes.ConsensusState `json:"consensus_state" yaml:"consensus_state"` +} + +// UpdateClientReq defines the properties of an update client request's body. +type UpdateClientReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + ClientID string `json:"client_id" yaml:"client_id"` + Header ibcsmtypes.Header `json:"header" yaml:"header"` +} + +// SubmitMisbehaviourReq defines the properties of a submit misbehaviour request's body. +type SubmitMisbehaviourReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + Evidence evidenceexported.Evidence `json:"evidence" yaml:"evidence"` +} diff --git a/x/ibc/06-solomachine/client/rest/tx.go b/x/ibc/06-solomachine/client/rest/tx.go new file mode 100644 index 000000000000..4e5cd462fdd3 --- /dev/null +++ b/x/ibc/06-solomachine/client/rest/tx.go @@ -0,0 +1,139 @@ +package rest + +import ( + "fmt" + "net/http" + + "github.com/gorilla/mux" + + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" + authclient "github.com/cosmos/cosmos-sdk/x/auth/client" + ibcsmtypes "github.com/cosmos/cosmos-sdk/x/ibc/06-solomachine/types" +) + +// RegisterRoutes - Central function to define routes that get registered by the main application +func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) { + r.HandleFunc("/ibc/clients/solomachine", createClientHandlerFn(cliCtx)).Methods("POST") + r.HandleFunc(fmt.Sprintf("/ibc/clients/{%s}/update", RestClientID), updateClientHandlerFn(cliCtx)).Methods("POST") + r.HandleFunc(fmt.Sprintf("/ibc/clients/{%s}/misbehaviour", RestClientID), submitMisbehaviourHandlerFn(cliCtx)).Methods("POST") +} + +// createClientHandlerFn implements a create client handler +// +// @Summary Create client +// @Tags IBC +// @Accept json +// @Produce json +// @Param body body rest.CreateClientReq true "Create client request body" +// @Success 200 {object} PostCreateClient "OK" +// @Failure 500 {object} rest.ErrorResponse "Internal Server Error" +// @Router /ibc/clients/solomachine [post] +func createClientHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req CreateClientReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + // create the message + msg := ibcsmtypes.NewMsgCreateClient( + req.ClientID, + req.ConsensusState, + ) + + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + authclient.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} + +// updateClientHandlerFn implements an update client handler +// +// @Summary update client +// @Tags IBC +// @Accept json +// @Produce json +// @Param client-id path string true "Client ID" +// @Param body body rest.UpdateClientReq true "Update client request body" +// @Success 200 {object} PostUpdateClient "OK" +// @Failure 400 {object} rest.ErrorResponse "Invalid client id" +// @Failure 500 {object} rest.ErrorResponse "Internal Server Error" +// @Router /ibc/clients/{client-id}/update [post] +func updateClientHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + clientID := vars[RestClientID] + + var req UpdateClientReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + // create the message + msg := ibcsmtypes.NewMsgUpdateClient( + clientID, + req.Header, + ) + + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + } + + authclient.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} + +// submitMisbehaviourHandlerFn implements a submit misbehaviour handler +// +// @Summary Submit misbehaviour +// @Tags IBC +// @Accept json +// @Produce json +// @Param body body rest.SubmitMisbehaviourReq true "Submit misbehaviour request body" +// @Success 200 {object} PostSubmitMisbehaviour "OK" +// @Failure 400 {object} rest.ErrorResponse "Invalid client id" +// @Failure 500 {object} rest.ErrorResponse "Internal Server Error" +// @Router /ibc/clients/{client-id}/misbehaviour [post] +func submitMisbehaviourHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req SubmitMisbehaviourReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + fromAddr, err := sdk.AccAddressFromBech32(req.BaseReq.From) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + // create the message + msg := ibcsmtypes.NewMsgSubmitClientMisbehaviour(req.Evidence, fromAddr) + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + authclient.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} diff --git a/x/ibc/06-solomachine/doc.go b/x/ibc/06-solomachine/doc.go new file mode 100644 index 000000000000..e025a0f68911 --- /dev/null +++ b/x/ibc/06-solomachine/doc.go @@ -0,0 +1,5 @@ +/* +Package solomachine implements a concrete `ConsensusState`, `Header`, +`Misbehaviour` and `Equivocation` types for the Solo Machine light client. +*/ +package solomachine diff --git a/x/ibc/06-solomachine/misbehaviour.go b/x/ibc/06-solomachine/misbehaviour.go new file mode 100644 index 000000000000..5e3f2798d5c6 --- /dev/null +++ b/x/ibc/06-solomachine/misbehaviour.go @@ -0,0 +1,62 @@ +package solomachine + +import ( + "bytes" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + clientexported "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported" + clienttypes "github.com/cosmos/cosmos-sdk/x/ibc/02-client/types" +) + +// CheckMisbehaviourAndUpdateState determines whether or not the currently registered +// public key signed over two different messages with the same sequence. +func CheckMisbehaviourAndUpdateState( + clientState clientexported.ClientState, + consensusState clientexported.ConsensusState, + misbehaviour clientexported.Misbehaviour, +) (clientexported.ClientState, error) { + // cast the interface to specific types before checking for misbehaviour + smClientState, ok := clientState.(ClientState) + if !ok { + return nil, sdkerrors.Wrap(clienttypes.ErrInvalidClientType, "client state type is not solo machine") + } + + if smClientState.IsFrozen() { + return nil, sdkerrors.Wrapf(clienttypes.ErrInvalidEvidence, "client is already frozen") + } + + smEvidence, ok := misbehaviour.(Evidence) + if !ok { + return nil, sdkerrors.Wrap(clienttypes.ErrInvalidClientType, "evidence type is not solo machine") + } + + if err := checkMisbehaviour(smClientState, smEvidence); err != nil { + return nil, sdkerrors.Wrap(clienttypes.ErrInvalidEvidence, err.Error()) + } + + smClientState.Frozen = true + return smClientState, nil +} + +// checkMisbehaviour checks if the currently registered public key has signed +// over two different messages at the same sequence. +func checkMisbehaviour(clientState ClientState, evidence Evidence) error { + pubKey := clientState.ConsensusState.PubKey + + // assert that provided signature data are different + if bytes.Equal(evidence.SignatureOne.Data, evidence.SignatureTwo.Data) { + return sdkerrors.Wrap(clienttypes.ErrInvalidEvidence, "evidence signatures have identical data messages") + } + + // check first signature + if pubKey.VerifyBytes(evidence.SignatureOne.Data, evidence.SignatureOne.Signature) { + return sdkerrors.Wrap(clienttypes.ErrInvalidEvidence, "evidence signature one not signed by currently registered public key") + } + + // check second signature + if pubKey.VerifyBytes(evidence.SignatureTwo.Data, evidence.SignatureTwo.Signature) { + return sdkerrors.Wrap(clienttypes.ErrInvalidEvidence, "evidence signature two not signed by currently registered public key") + } + + return nil +} diff --git a/x/ibc/06-solomachine/misbehaviour_test.go b/x/ibc/06-solomachine/misbehaviour_test.go new file mode 100644 index 000000000000..cb1e204e8931 --- /dev/null +++ b/x/ibc/06-solomachine/misbehaviour_test.go @@ -0,0 +1 @@ +package solomachine_test diff --git a/x/ibc/06-solomachine/module.go b/x/ibc/06-solomachine/module.go new file mode 100644 index 000000000000..5d29e0575e5a --- /dev/null +++ b/x/ibc/06-solomachine/module.go @@ -0,0 +1,29 @@ +package solomachine + +import ( + "fmt" + + "github.com/gorilla/mux" + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/x/ibc/06-solomachine/client/cli" + "github.com/cosmos/cosmos-sdk/x/ibc/06-solomachine/client/rest" + "github.com/cosmos/cosmos-sdk/x/ibc/06-solomachine/types" +) + +// Name returns the IBC client name. +func Name() string { + return SubModuleName +} + +// RegisterRESTRoutes registers the REST routes for the IBC client +func RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router, queryRoute string) { + rest.RegisterRoutes(ctx, rtr, fmt.Sprintf("%s/%s", queryRoute, types.SubModuleName)) +} + +// GetTxCmd returns the root tx command for the IBC Client. +func GetTxCmd(cdc *codec.Codec, storeKey string) *cobra.Command { + return cli.GetTxCmd(cdc, fmt.Sprintf("%s/%s", storeKey, types.SubModuleName)) +} diff --git a/x/ibc/06-solomachine/types/client_state.go b/x/ibc/06-solomachine/types/client_state.go new file mode 100644 index 000000000000..7ffe23820489 --- /dev/null +++ b/x/ibc/06-solomachine/types/client_state.go @@ -0,0 +1,440 @@ +package types + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + 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" + connectiontypes "github.com/cosmos/cosmos-sdk/x/ibc/03-connection/types" + channelexported "github.com/cosmos/cosmos-sdk/x/ibc/04-channel/exported" + channeltypes "github.com/cosmos/cosmos-sdk/x/ibc/04-channel/types" + commitmentexported "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/exported" + commitmenttypes "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/types" + host "github.com/cosmos/cosmos-sdk/x/ibc/24-host" +) + +var _ clientexported.ClientState = ClientState{} + +// ClientState of a Solo Machine represents whether or not the client is frozen. +type ClientState struct { + // Client ID + ID string `json:"id" yaml:"id"` + + // Frozen status of the client + Frozen bool `json:"frozen" yaml:"frozen"` + + // Current consensus state of the client + ConsensusState ConsensusState `json:"consensus_state" yaml:"consensus_state"` +} + +// InitializeFromMsg creates a solo machine client from a MsgCreateClient +func InitializeFromMsg(msg MsgCreateClient) (ClientState, error) { + return Initialize(msg.GetClientID(), msg.ConsensusState) +} + +// Initialize creates an unfrozen client with the initial consensus state +func Initialize(id string, consensusState ConsensusState) (ClientState, error) { + return ClientState{ + ID: id, + Frozen: false, + ConsensusState: consensusState, + }, nil +} + +// NewClientState creates a new ClientState instance. +func NewClientState(id string, consensusState ConsensusState) ClientState { + return ClientState{ + ID: id, + Frozen: false, + ConsensusState: consensusState, + } +} + +// GetID returns the solo machine client state identifier. +func (cs ClientState) GetID() string { + return cs.ID +} + +// GetChainID returns an empty string. +func (cs ClientState) GetChainID() string { + return "" +} + +// ClientType is Solo Machine. +func (cs ClientState) ClientType() clientexported.ClientType { + return clientexported.SoloMachine +} + +// GetLatestHeight returns the latest sequence number. +func (cs ClientState) GetLatestHeight() uint64 { + return cs.ConsensusState.Sequence +} + +// IsFrozen returns true if the client is frozen +func (cs ClientState) IsFrozen() bool { + return cs.Frozen +} + +// Validate performs basic validation of the client state fields. +func (cs ClientState) Validate() error { + if err := host.ClientIdentifierValidator(cs.ID); err != nil { + return err + } + return cs.ConsensusState.ValidateBasic() +} + +// VerifyClientConsensusState verifies a proof of the consensus state of the +// Solo Machine client stored on the target machine. +func (cs ClientState) VerifyClientConsensusState( + store sdk.KVStore, + cdc *codec.Codec, + root commitmentexported.Root, + _ uint64, + counterpartyClientIdentifier string, + consensusHeight uint64, + prefix commitmentexported.Prefix, + proof commitmentexported.Proof, + consensusState clientexported.ConsensusState, +) error { + clientPrefixedPath := "clients/" + counterpartyClientIdentifier + "/" + host.ConsensusStatePath(consensusHeight) + path, err := commitmenttypes.ApplyPrefix(prefix, clientPrefixedPath) + if err != nil { + return err + } + + if cs.IsFrozen() { + return clienttypes.ErrClientFrozen + } + + // cast the proof to a signature proof + signatureProof, ok := proof.(commitmenttypes.SignatureProof) + if !ok { + return sdkerrors.Wrapf(clienttypes.ErrInvalidClientType, "proof type %T is not type SignatureProof", proof) + } + + bz, err := cdc.MarshalBinaryBare(consensusState) + if err != nil { + return err + } + + // value = sequence + path + consensus state + value := append( + combineSequenceAndPath(cs.ConsensusState.Sequence, path), + bz..., + ) + if !cs.ConsensusState.PubKey.VerifyBytes(value, signatureProof.Signature) { + return sdkerrors.Wrap(clienttypes.ErrFailedClientConsensusStateVerification, "failed to verify proof against current public key, sequence, and consensus state") + } + + cs.ConsensusState.Sequence++ + setClientState(store, cs) + return nil +} + +// VerifyConnectionState verifies a proof of the connection state of the +// specified connection end stored on the target machine. +func (cs ClientState) VerifyConnectionState( + store sdk.KVStore, + cdc codec.Marshaler, + _ uint64, + prefix commitmentexported.Prefix, + proof commitmentexported.Proof, + connectionID string, + connectionEnd connectionexported.ConnectionI, + consensusState clientexported.ConsensusState, +) error { + path, err := commitmenttypes.ApplyPrefix(prefix, host.ConnectionPath(connectionID)) + if err != nil { + return err + } + + if cs.IsFrozen() { + return clienttypes.ErrClientFrozen + } + + // cast the proof to a signature proof + signatureProof, ok := proof.(commitmenttypes.SignatureProof) + if !ok { + return sdkerrors.Wrapf(clienttypes.ErrInvalidClientType, "proof type %T is not type SignatureProof", proof) + } + + connection, ok := connectionEnd.(connectiontypes.ConnectionEnd) + if !ok { + return fmt.Errorf("invalid connection type %T", connectionEnd) + } + + bz, err := cdc.MarshalBinaryBare(&connection) + if err != nil { + return err + } + + // value = sequence + path + connection end + value := append( + combineSequenceAndPath(cs.ConsensusState.Sequence, path), + bz..., + ) + if !cs.ConsensusState.PubKey.VerifyBytes(value, signatureProof.Signature) { + return sdkerrors.Wrap( + clienttypes.ErrFailedConnectionStateVerification, + "failed to verify proof against current public key, sequence, and connection state", + ) + } + + cs.ConsensusState.Sequence++ + setClientState(store, cs) + return nil +} + +// VerifyChannelState verifies a proof of the channel state of the specified +// channel end, under the specified port, stored on the target machine. +func (cs ClientState) VerifyChannelState( + store sdk.KVStore, + cdc codec.Marshaler, + _ uint64, + prefix commitmentexported.Prefix, + proof commitmentexported.Proof, + portID, + channelID string, + channel channelexported.ChannelI, + consensusState clientexported.ConsensusState, +) error { + path, err := commitmenttypes.ApplyPrefix(prefix, host.ChannelPath(portID, channelID)) + if err != nil { + return err + } + + if cs.IsFrozen() { + return clienttypes.ErrClientFrozen + } + + // cast the proof to a signature proof + signatureProof, ok := proof.(commitmenttypes.SignatureProof) + if !ok { + return sdkerrors.Wrapf(clienttypes.ErrInvalidClientType, "proof type %T is not type SignatureProof", proof) + } + + channelEnd, ok := channel.(channeltypes.Channel) + if !ok { + return fmt.Errorf("invalid channel type %T", channel) + } + + bz, err := cdc.MarshalBinaryBare(&channelEnd) + if err != nil { + return err + } + + // value = sequence + path + channel + value := append( + combineSequenceAndPath(cs.ConsensusState.Sequence, path), + bz..., + ) + if !cs.ConsensusState.PubKey.VerifyBytes(value, signatureProof.Signature) { + return sdkerrors.Wrap( + clienttypes.ErrFailedChannelStateVerification, + "failed to verify proof against current public key, sequence, and channel state", + ) + } + + cs.ConsensusState.Sequence++ + setClientState(store, cs) + return nil +} + +// VerifyPacketCommitment verifies a proof of an outgoing packet commitment at +// the specified port, specified channel, and specified sequence. +func (cs ClientState) VerifyPacketCommitment( + store sdk.KVStore, + _ uint64, + prefix commitmentexported.Prefix, + proof commitmentexported.Proof, + portID, + channelID string, + sequence uint64, + commitmentBytes []byte, + consensusState clientexported.ConsensusState, +) error { + path, err := commitmenttypes.ApplyPrefix(prefix, host.PacketCommitmentPath(portID, channelID, sequence)) + if err != nil { + return err + } + + if cs.IsFrozen() { + return clienttypes.ErrClientFrozen + } + + // cast the proof to a signature proof + signatureProof, ok := proof.(commitmenttypes.SignatureProof) + if !ok { + return sdkerrors.Wrapf(clienttypes.ErrInvalidClientType, "proof type %T is not type SignatureProof", proof) + } + + // value = sequence + path + commitment bytes + value := append( + combineSequenceAndPath(cs.ConsensusState.Sequence, path), + commitmentBytes..., + ) + if !cs.ConsensusState.PubKey.VerifyBytes(value, signatureProof.Signature) { + return sdkerrors.Wrap( + clienttypes.ErrFailedPacketCommitmentVerification, + "failed to verify proof against current public key, sequence, and packet commitment", + ) + } + + cs.ConsensusState.Sequence++ + setClientState(store, cs) + return nil + +} + +// VerifyPacketAcknowledgement verifies a proof of an incoming packet +// acknowledgement at the specified port, specified channel, and specified sequence. +func (cs ClientState) VerifyPacketAcknowledgement( + store sdk.KVStore, + _ uint64, + prefix commitmentexported.Prefix, + proof commitmentexported.Proof, + portID, + channelID string, + sequence uint64, + acknowledgement []byte, + consensusState clientexported.ConsensusState, +) error { + path, err := commitmenttypes.ApplyPrefix(prefix, host.PacketAcknowledgementPath(portID, channelID, sequence)) + if err != nil { + return err + } + + if cs.IsFrozen() { + return clienttypes.ErrClientFrozen + } + + // cast the proof to a signature proof + signatureProof, ok := proof.(commitmenttypes.SignatureProof) + if !ok { + return sdkerrors.Wrap(clienttypes.ErrInvalidClientType, "proof type %T is not type SignatureProof") + } + + // value = sequence + path + acknowledgement + value := append( + combineSequenceAndPath(cs.ConsensusState.Sequence, path), + acknowledgement..., + ) + if !cs.ConsensusState.PubKey.VerifyBytes(value, signatureProof.Signature) { + return sdkerrors.Wrap( + clienttypes.ErrFailedPacketAckVerification, + "failed to verify proof against current public key, sequence, and acknowledgement", + ) + } + + cs.ConsensusState.Sequence++ + setClientState(store, cs) + 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( + store sdk.KVStore, + _ uint64, + prefix commitmentexported.Prefix, + proof commitmentexported.Proof, + portID, + channelID string, + sequence uint64, + consensusState clientexported.ConsensusState, +) error { + path, err := commitmenttypes.ApplyPrefix(prefix, host.PacketAcknowledgementPath(portID, channelID, sequence)) + if err != nil { + return err + } + + if cs.IsFrozen() { + return clienttypes.ErrClientFrozen + } + + // cast the proof to a signature proof + signatureProof, ok := proof.(commitmenttypes.SignatureProof) + if !ok { + return sdkerrors.Wrapf(clienttypes.ErrInvalidClientType, "proof type %T is not type SignatureProof", proof) + } + + // value = sequence + path + value := combineSequenceAndPath(cs.ConsensusState.Sequence, path) + + if !cs.ConsensusState.PubKey.VerifyBytes(value, signatureProof.Signature) { + return sdkerrors.Wrap( + clienttypes.ErrFailedPacketAckAbsenceVerification, + "failed to verify proof against current public key, sequence, and an absent acknowledgement", + ) + } + + cs.ConsensusState.Sequence++ + setClientState(store, cs) + 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( + store sdk.KVStore, + _ uint64, + prefix commitmentexported.Prefix, + proof commitmentexported.Proof, + portID, + channelID string, + nextSequenceRecv uint64, + consensusState clientexported.ConsensusState, +) error { + path, err := commitmenttypes.ApplyPrefix(prefix, host.NextSequenceRecvPath(portID, channelID)) + if err != nil { + return err + } + + if cs.IsFrozen() { + return clienttypes.ErrClientFrozen + } + + // cast the proof to a signature proof + signatureProof, ok := proof.(commitmenttypes.SignatureProof) + if !ok { + return sdkerrors.Wrap(clienttypes.ErrInvalidClientType, "proof type %T is not type SignatureProof") + } + + // value = sequence + path + nextSequenceRecv + value := append( + combineSequenceAndPath(cs.ConsensusState.Sequence, path), + sdk.Uint64ToBigEndian(nextSequenceRecv)..., + ) + + if !cs.ConsensusState.PubKey.VerifyBytes(value, signatureProof.Signature) { + return sdkerrors.Wrap( + clienttypes.ErrFailedNextSeqRecvVerification, + "failed to verify proof against current public key, sequence, and the next sequence number to be received", + ) + } + + cs.ConsensusState.Sequence++ + setClientState(store, cs) + return nil +} + +// combineSequenceAndPath appends the sequence and path represented as bytes. +func combineSequenceAndPath(sequence uint64, path commitmenttypes.MerklePath) []byte { + return append( + sdk.Uint64ToBigEndian(sequence), + []byte(path.String())..., + ) +} + +// sets the client state to the store +func setClientState(store sdk.KVStore, clientState clientexported.ClientState) { + bz := SubModuleCdc.MustMarshalBinaryBare(clientState) + store.Set(host.KeyClientState(), bz) +} diff --git a/x/ibc/06-solomachine/types/client_state_test.go b/x/ibc/06-solomachine/types/client_state_test.go new file mode 100644 index 000000000000..82378deb78f7 --- /dev/null +++ b/x/ibc/06-solomachine/types/client_state_test.go @@ -0,0 +1,589 @@ +package types_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + connection "github.com/cosmos/cosmos-sdk/x/ibc/03-connection" + "github.com/cosmos/cosmos-sdk/x/ibc/04-channel" + solomachinetypes "github.com/cosmos/cosmos-sdk/x/ibc/06-solomachine/types" + commitmentexported "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/exported" + commitmenttypes "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/types" + host "github.com/cosmos/cosmos-sdk/x/ibc/24-host" +) + +const ( + counterpartyClientIdentifier = "chainA" + consensusHeight = uint64(0) + testConnectionID = "connectionid" + testChannelID = "testchannelid" + testPortID = "testportid" +) + +var ( + prefix = commitmenttypes.NewSignaturePrefix([]byte("ibc")) +) + +func (suite *SoloMachineTestSuite) TestClientStateValidateBasic() { + testCases := []struct { + name string + clientState solomachinetypes.ClientState + expPass bool + }{ + { + "valid client state", + suite.ClientState(), + true, + }, + { + "invalid client id", + solomachinetypes.NewClientState("(testClientID)", suite.ConsensusState()), + false, + }, + { + "sequence is zero", + solomachinetypes.NewClientState(suite.clientID, solomachinetypes.ConsensusState{0, suite.privKey.PubKey()}), + false, + }, + { + "pubkey is empty", + solomachinetypes.NewClientState(suite.clientID, solomachinetypes.ConsensusState{suite.sequence, nil}), + false, + }, + } + + for i, tc := range testCases { + tc := tc + + err := tc.clientState.Validate() + + 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 *SoloMachineTestSuite) TestVerifyClientConsensusState() { + clientPrefixedPath := "clients/" + counterpartyClientIdentifier + "/" + host.ConsensusStatePath(consensusHeight) + path, err := commitmenttypes.ApplyPrefix(prefix, clientPrefixedPath) + suite.Require().NoError(err) + + value := append(sdk.Uint64ToBigEndian(suite.sequence), []byte(path.String())...) + bz, err := suite.aminoCdc.MarshalBinaryBare(suite.ClientState().ConsensusState) + suite.Require().NoError(err) + value = append(value, bz...) + + sig, err := suite.privKey.Sign(value) + suite.Require().NoError(err) + proof := commitmenttypes.SignatureProof{sig} + + testCases := []struct { + name string + clientState solomachinetypes.ClientState + prefix commitmentexported.Prefix + proof commitmentexported.Proof + expPass bool + }{ + { + "successful verification", + suite.ClientState(), + prefix, + proof, + true, + }, + { + "ApplyPrefix failed", + suite.ClientState(), + commitmenttypes.SignaturePrefix{}, + proof, + false, + }, + { + "client is frozen", + solomachinetypes.ClientState{suite.clientID, true, suite.ConsensusState()}, + prefix, + proof, + false, + }, + { + "invalid proof type", + suite.ClientState(), + prefix, + commitmenttypes.MerkleProof{}, + false, + }, + { + "proof verification failed", + suite.ClientState(), + prefix, + commitmenttypes.SignatureProof{}, + false, + }, + } + + for i, tc := range testCases { + tc := tc + + err := tc.clientState.VerifyClientConsensusState( + suite.store, suite.aminoCdc, nil, 0, counterpartyClientIdentifier, consensusHeight, tc.prefix, tc.proof, tc.clientState.ConsensusState, + ) + + if tc.expPass { + suite.Require().NoError(err, "valid test case %d failed: %s", i, tc.name) + expSeq := tc.clientState.ConsensusState.Sequence + 1 + suite.Require().Equal(expSeq, suite.GetSequenceFromStore(), "sequence not updated in the store (%d) on valid test case %d: %s", suite.GetSequenceFromStore(), i, tc.name) + } else { + suite.Require().Error(err, "invalid test case %d passed: %s", i, tc.name) + } + } +} + +func (suite *SoloMachineTestSuite) TestVerifyConnectionState() { + counterparty := connection.NewCounterparty("clientB", testConnectionID, prefix) + conn := connection.NewConnectionEnd(connection.OPEN, testConnectionID, "clientA", counterparty, []string{"1.0.0"}) + + path, err := commitmenttypes.ApplyPrefix(prefix, host.ConnectionPath(testConnectionID)) + suite.Require().NoError(err) + + value := append(sdk.Uint64ToBigEndian(suite.sequence), []byte(path.String())...) + bz, err := suite.cdc.MarshalBinaryBare(&conn) + suite.Require().NoError(err) + value = append(value, bz...) + + sig, err := suite.privKey.Sign(value) + suite.Require().NoError(err) + proof := commitmenttypes.SignatureProof{sig} + + testCases := []struct { + name string + clientState solomachinetypes.ClientState + prefix commitmentexported.Prefix + proof commitmentexported.Proof + expPass bool + }{ + { + "successful verification", + suite.ClientState(), + prefix, + proof, + true, + }, + { + "ApplyPrefix failed", + suite.ClientState(), + commitmenttypes.NewSignaturePrefix([]byte{}), + proof, + false, + }, + { + "client is frozen", + solomachinetypes.ClientState{suite.clientID, true, suite.ConsensusState()}, + prefix, + proof, + false, + }, + { + "invalid proof type", + suite.ClientState(), + prefix, + commitmenttypes.MerkleProof{}, + false, + }, + { + "proof verification failed", + suite.ClientState(), + prefix, + commitmenttypes.SignatureProof{}, + false, + }, + } + + for i, tc := range testCases { + tc := tc + + err := tc.clientState.VerifyConnectionState( + suite.store, suite.cdc, 0, tc.prefix, tc.proof, testConnectionID, conn, tc.clientState.ConsensusState, + ) + + if tc.expPass { + suite.Require().NoError(err, "valid test case %d failed: %s", i, tc.name) + + expSeq := tc.clientState.ConsensusState.Sequence + 1 + suite.Require().Equal(expSeq, suite.GetSequenceFromStore(), "sequence not updated in the store (%d) on valid test case %d: %s", suite.GetSequenceFromStore(), i, tc.name) + } else { + suite.Require().Error(err, "invalid test case %d passed: %s", i, tc.name) + } + } +} + +func (suite *SoloMachineTestSuite) TestVerifyChannelState() { + counterparty := channel.NewCounterparty(testPortID, testChannelID) + ch := channel.NewChannel(channel.OPEN, channel.ORDERED, counterparty, []string{testConnectionID}, "1.0.0") + + path, err := commitmenttypes.ApplyPrefix(prefix, host.ChannelPath(testPortID, testChannelID)) + suite.Require().NoError(err) + + value := append(sdk.Uint64ToBigEndian(suite.sequence), []byte(path.String())...) + bz, err := suite.cdc.MarshalBinaryBare(&ch) + suite.Require().NoError(err) + value = append(value, bz...) + + sig, err := suite.privKey.Sign(value) + suite.Require().NoError(err) + proof := commitmenttypes.SignatureProof{sig} + + testCases := []struct { + name string + clientState solomachinetypes.ClientState + prefix commitmentexported.Prefix + proof commitmentexported.Proof + expPass bool + }{ + { + "successful verification", + suite.ClientState(), + prefix, + proof, + true, + }, + { + "ApplyPrefix failed", + suite.ClientState(), + commitmenttypes.NewSignaturePrefix([]byte{}), + proof, + false, + }, + { + "client is frozen", + solomachinetypes.ClientState{suite.clientID, true, suite.ConsensusState()}, + prefix, + proof, + false, + }, + { + "invalid proof type", + suite.ClientState(), + prefix, + commitmenttypes.MerkleProof{}, + false, + }, + { + "proof verification failed", + suite.ClientState(), + prefix, + commitmenttypes.SignatureProof{}, + false, + }, + } + + for i, tc := range testCases { + tc := tc + + err := tc.clientState.VerifyChannelState( + suite.store, suite.cdc, 0, tc.prefix, tc.proof, testPortID, testChannelID, ch, tc.clientState.ConsensusState, + ) + + if tc.expPass { + suite.Require().NoError(err, "valid test case %d failed: %s", i, tc.name) + + expSeq := tc.clientState.ConsensusState.Sequence + 1 + suite.Require().Equal(expSeq, suite.GetSequenceFromStore(), "sequence not updated in the store (%d) on valid test case %d: %s", suite.GetSequenceFromStore(), i, tc.name) + } else { + suite.Require().Error(err, "invalid test case %d passed: %s", i, tc.name) + } + } +} + +func (suite *SoloMachineTestSuite) TestVerifyPacketCommitment() { + commitmentBytes := []byte("COMMITMENT BYTES") + path, err := commitmenttypes.ApplyPrefix(prefix, host.PacketCommitmentPath(testPortID, testChannelID, suite.sequence)) + suite.Require().NoError(err) + + value := append(sdk.Uint64ToBigEndian(suite.sequence), []byte(path.String())...) + value = append(value, commitmentBytes...) + + sig, err := suite.privKey.Sign(value) + suite.Require().NoError(err) + proof := commitmenttypes.SignatureProof{sig} + + testCases := []struct { + name string + clientState solomachinetypes.ClientState + prefix commitmentexported.Prefix + proof commitmentexported.Proof + expPass bool + }{ + { + "successful verification", + suite.ClientState(), + prefix, + proof, + true, + }, + { + "ApplyPrefix failed", + suite.ClientState(), + commitmenttypes.NewSignaturePrefix([]byte{}), + proof, + false, + }, + { + "client is frozen", + solomachinetypes.ClientState{suite.clientID, true, suite.ConsensusState()}, + prefix, + proof, + false, + }, + { + "invalid proof type", + suite.ClientState(), + prefix, + commitmenttypes.MerkleProof{}, + false, + }, + { + "proof verification failed", + suite.ClientState(), + prefix, + commitmenttypes.SignatureProof{}, + false, + }, + } + + for i, tc := range testCases { + tc := tc + + err := tc.clientState.VerifyPacketCommitment( + suite.store, 0, tc.prefix, tc.proof, testPortID, testChannelID, suite.sequence, commitmentBytes, tc.clientState.ConsensusState, + ) + + if tc.expPass { + suite.Require().NoError(err, "valid test case %d failed: %s", i, tc.name) + + expSeq := tc.clientState.ConsensusState.Sequence + 1 + suite.Require().Equal(expSeq, suite.GetSequenceFromStore(), "sequence not updated in the store (%d) on valid test case %d: %s", suite.GetSequenceFromStore(), i, tc.name) + } else { + suite.Require().Error(err, "invalid test case %d passed: %s", i, tc.name) + } + } +} + +func (suite *SoloMachineTestSuite) TestVerifyPacketAcknowledgement() { + ack := []byte("ACK") + path, err := commitmenttypes.ApplyPrefix(prefix, host.PacketAcknowledgementPath(testPortID, testChannelID, suite.sequence)) + suite.Require().NoError(err) + + value := append(sdk.Uint64ToBigEndian(suite.sequence), []byte(path.String())...) + value = append(value, ack...) + + sig, err := suite.privKey.Sign(value) + suite.Require().NoError(err) + proof := commitmenttypes.SignatureProof{sig} + + testCases := []struct { + name string + clientState solomachinetypes.ClientState + prefix commitmentexported.Prefix + proof commitmentexported.Proof + expPass bool + }{ + { + "successful verification", + suite.ClientState(), + prefix, + proof, + true, + }, + { + "ApplyPrefix failed", + suite.ClientState(), + commitmenttypes.NewSignaturePrefix([]byte{}), + proof, + false, + }, + { + "client is frozen", + solomachinetypes.ClientState{suite.clientID, true, suite.ConsensusState()}, + prefix, + proof, + false, + }, + { + "invalid proof type", + suite.ClientState(), + prefix, + commitmenttypes.MerkleProof{}, + false, + }, + { + "proof verification failed", + suite.ClientState(), + prefix, + commitmenttypes.SignatureProof{}, + false, + }, + } + + for i, tc := range testCases { + tc := tc + + err := tc.clientState.VerifyPacketAcknowledgement( + suite.store, 0, tc.prefix, tc.proof, testPortID, testChannelID, suite.sequence, ack, tc.clientState.ConsensusState, + ) + + if tc.expPass { + suite.Require().NoError(err, "valid test case %d failed: %s", i, tc.name) + + expSeq := tc.clientState.ConsensusState.Sequence + 1 + suite.Require().Equal(expSeq, suite.GetSequenceFromStore(), "sequence not updated in the store (%d) on valid test case %d: %s", suite.GetSequenceFromStore(), i, tc.name) + } else { + suite.Require().Error(err, "invalid test case %d passed: %s", i, tc.name) + } + } +} + +func (suite *SoloMachineTestSuite) TestVerifyPacketAcknowledgementAbsence() { + path, err := commitmenttypes.ApplyPrefix(prefix, host.PacketAcknowledgementPath(testPortID, testChannelID, suite.sequence)) + suite.Require().NoError(err) + + value := append(sdk.Uint64ToBigEndian(suite.sequence), []byte(path.String())...) + + sig, err := suite.privKey.Sign(value) + suite.Require().NoError(err) + proof := commitmenttypes.SignatureProof{sig} + + testCases := []struct { + name string + clientState solomachinetypes.ClientState + prefix commitmentexported.Prefix + proof commitmentexported.Proof + expPass bool + }{ + { + "successful verification", + suite.ClientState(), + prefix, + proof, + true, + }, + { + "ApplyPrefix failed", + suite.ClientState(), + commitmenttypes.NewSignaturePrefix([]byte{}), + proof, + false, + }, + { + "client is frozen", + solomachinetypes.ClientState{suite.clientID, true, suite.ConsensusState()}, + prefix, + proof, + false, + }, + { + "invalid proof type", + suite.ClientState(), + prefix, + commitmenttypes.MerkleProof{}, + false, + }, + { + "proof verification failed", + suite.ClientState(), + prefix, + commitmenttypes.SignatureProof{}, + false, + }, + } + + for i, tc := range testCases { + tc := tc + + err := tc.clientState.VerifyPacketAcknowledgementAbsence( + suite.store, 0, tc.prefix, tc.proof, testPortID, testChannelID, suite.sequence, tc.clientState.ConsensusState, + ) + + if tc.expPass { + suite.Require().NoError(err, "valid test case %d failed: %s", i, tc.name) + + expSeq := tc.clientState.ConsensusState.Sequence + 1 + suite.Require().Equal(expSeq, suite.GetSequenceFromStore(), "sequence not updated in the store (%d) on valid test case %d: %s", suite.GetSequenceFromStore(), i, tc.name) + } else { + suite.Require().Error(err, "invalid test case %d passed: %s", i, tc.name) + } + } +} + +func (suite *SoloMachineTestSuite) TestVerifyNextSeqRecv() { + nextSeqRecv := suite.sequence + 1 + path, err := commitmenttypes.ApplyPrefix(prefix, host.NextSequenceRecvPath(testPortID, testChannelID)) + suite.Require().NoError(err) + + value := append(sdk.Uint64ToBigEndian(suite.sequence), []byte(path.String())...) + value = append(value, sdk.Uint64ToBigEndian(nextSeqRecv)...) + + sig, err := suite.privKey.Sign(value) + suite.Require().NoError(err) + proof := commitmenttypes.SignatureProof{sig} + + testCases := []struct { + name string + clientState solomachinetypes.ClientState + prefix commitmentexported.Prefix + proof commitmentexported.Proof + expPass bool + }{ + { + "successful verification", + suite.ClientState(), + prefix, + proof, + true, + }, + { + "ApplyPrefix failed", + suite.ClientState(), + commitmenttypes.NewSignaturePrefix([]byte{}), + proof, + false, + }, + { + "client is frozen", + solomachinetypes.ClientState{suite.clientID, true, suite.ConsensusState()}, + prefix, + proof, + false, + }, + { + "invalid proof type", + suite.ClientState(), + prefix, + commitmenttypes.MerkleProof{}, + false, + }, + { + "proof verification failed", + suite.ClientState(), + prefix, + commitmenttypes.SignatureProof{}, + false, + }, + } + + for i, tc := range testCases { + tc := tc + + err := tc.clientState.VerifyNextSequenceRecv( + suite.store, 0, tc.prefix, tc.proof, testPortID, testChannelID, nextSeqRecv, tc.clientState.ConsensusState, + ) + + if tc.expPass { + suite.Require().NoError(err, "valid test case %d failed: %s", i, tc.name) + + expSeq := tc.clientState.ConsensusState.Sequence + 1 + suite.Require().Equal(expSeq, suite.GetSequenceFromStore(), "sequence not updated in the store (%d) on valid test case %d: %s", suite.GetSequenceFromStore(), i, tc.name) + } else { + suite.Require().Error(err, "invalid test case %d passed: %s", i, tc.name) + } + } +} diff --git a/x/ibc/06-solomachine/types/codec.go b/x/ibc/06-solomachine/types/codec.go new file mode 100644 index 000000000000..07063650e737 --- /dev/null +++ b/x/ibc/06-solomachine/types/codec.go @@ -0,0 +1,26 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/codec" +) + +// SubModuleCdc defines the IBC solo machine client codec. +var SubModuleCdc *codec.Codec + +// RegisterCodec registers the Solo Machine types. +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterConcrete(ClientState{}, "ibc/client/solomachine/ClientState", nil) + cdc.RegisterConcrete(ConsensusState{}, "ibc/client/solomachine/ConsensusState", nil) + cdc.RegisterConcrete(Header{}, "ibc/client/solomachine/Header", nil) + cdc.RegisterConcrete(Evidence{}, "ibc/client/solomachine/Evidence", nil) + cdc.RegisterConcrete(MsgCreateClient{}, "ibc/client/solomachine/MsgCreateClient", nil) + cdc.RegisterConcrete(MsgUpdateClient{}, "ibc/client/solomachine/MsgUpdateClient", nil) + cdc.RegisterConcrete(MsgSubmitClientMisbehaviour{}, "ibc/client/solomachine/MsgSubmitClientMisbehaviour", nil) + + SetSubModuleCodec(cdc) +} + +// SetSubModuleCodec sets the ibc solo machine client codec. +func SetSubModuleCodec(cdc *codec.Codec) { + SubModuleCdc = cdc +} diff --git a/x/ibc/06-solomachine/types/consensus_state.go b/x/ibc/06-solomachine/types/consensus_state.go new file mode 100644 index 000000000000..8b1c76789263 --- /dev/null +++ b/x/ibc/06-solomachine/types/consensus_state.go @@ -0,0 +1,52 @@ +package types + +import ( + "github.com/tendermint/tendermint/crypto" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + clientexported "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported" + clienttypes "github.com/cosmos/cosmos-sdk/x/ibc/02-client/types" + commitmentexported "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/exported" +) + +var _ clientexported.ConsensusState = ConsensusState{} + +// ConsensusState defines a Solo Machine consensus state +type ConsensusState struct { + Sequence uint64 `json:"sequence" yaml:"sequence"` + + PubKey crypto.PubKey `json:"pubkey" yaml: "pubkey"` +} + +// ClientType returns Solo Machine type +func (ConsensusState) ClientType() clientexported.ClientType { + return clientexported.SoloMachine +} + +// GetHeight returns the sequence number +func (cs ConsensusState) GetHeight() uint64 { + return cs.Sequence +} + +// GetTimestamp returns zero. +func (cs ConsensusState) GetTimestamp() uint64 { + return 0 +} + +// GetRoot returns nil as solo machines do not have roots +func (cs ConsensusState) GetRoot() commitmentexported.Root { + return nil +} + +// ValidateBasic defines basic validation for the solo machine consensus state. +func (cs ConsensusState) ValidateBasic() error { + if cs.Sequence == 0 { + return sdkerrors.Wrap(clienttypes.ErrInvalidConsensus, "sequence cannot be 0") + } + + if cs.PubKey == nil { + return sdkerrors.Wrap(clienttypes.ErrInvalidConsensus, "public key cannot be empty") + } + + return nil +} diff --git a/x/ibc/06-solomachine/types/consensus_state_test.go b/x/ibc/06-solomachine/types/consensus_state_test.go new file mode 100644 index 000000000000..e5be30ca9a7e --- /dev/null +++ b/x/ibc/06-solomachine/types/consensus_state_test.go @@ -0,0 +1,55 @@ +package types_test + +import ( + clientexported "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported" + solomachinetypes "github.com/cosmos/cosmos-sdk/x/ibc/06-solomachine/types" +) + +func (suite *SoloMachineTestSuite) TestConsensusState() { + consensusState := suite.ConsensusState() + + suite.Require().Equal(clientexported.SoloMachine, consensusState.ClientType()) + suite.Require().Equal(suite.sequence, consensusState.GetHeight()) + suite.Require().Equal(uint64(0), consensusState.GetTimestamp()) + suite.Require().Nil(consensusState.GetRoot()) +} + +func (suite *SoloMachineTestSuite) TestConsensusStateValidateBasic() { + testCases := []struct { + name string + consensusState solomachinetypes.ConsensusState + expPass bool + }{ + { + "valid consensus state", + suite.ConsensusState(), + true, + }, + { + "sequence is zero", + solomachinetypes.ConsensusState{ + Sequence: 0, + PubKey: suite.privKey.PubKey(), + }, + false, + }, + { + "pubkey is nil", + solomachinetypes.ConsensusState{ + Sequence: suite.sequence, + PubKey: nil, + }, + false, + }, + } + + for i, tc := range testCases { + err := tc.consensusState.ValidateBasic() + + 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/06-solomachine/types/errors.go b/x/ibc/06-solomachine/types/errors.go new file mode 100644 index 000000000000..7aa68f991915 --- /dev/null +++ b/x/ibc/06-solomachine/types/errors.go @@ -0,0 +1,15 @@ +package types + +import ( + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +const ( + SubModuleName = "solo machine" +) + +var ( + ErrInvalidHeader = sdkerrors.Register(SubModuleName, 2, "invalid header") + ErrInvalidSequence = sdkerrors.Register(SubModuleName, 3, "invalid sequence") + ErrInvalidSignatureAndData = sdkerrors.Register(SubModuleName, 4, "invalid signature and data") +) diff --git a/x/ibc/06-solomachine/types/evidence.go b/x/ibc/06-solomachine/types/evidence.go new file mode 100644 index 000000000000..98c9cd4b86ac --- /dev/null +++ b/x/ibc/06-solomachine/types/evidence.go @@ -0,0 +1,119 @@ +package types + +import ( + "bytes" + + yaml "gopkg.in/yaml.v2" + + "github.com/tendermint/tendermint/crypto/tmhash" + tmbytes "github.com/tendermint/tendermint/libs/bytes" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + 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" + host "github.com/cosmos/cosmos-sdk/x/ibc/24-host" +) + +var ( + _ evidenceexported.Evidence = Evidence{} + _ clientexported.Misbehaviour = Evidence{} +) + +// Evidence is proof of misbehaviour for a solo machine which consists of a sequence +// and two signatures over different messages at that sequence. +type Evidence struct { + ClientID string `json:"client_id" yaml:"client_id"` + Sequence uint64 `json:"sequence" yaml:"sequnce"` + SignatureOne SignatureAndData `json:"signature_one" yaml:"signature_one"` + SignatureTwo SignatureAndData `json:"signature_one" yam;:"signature_two"` +} + +// ClientType is a Solo Machine light client. +func (ev Evidence) ClientType() clientexported.ClientType { + return clientexported.SoloMachine +} + +// GetClientID returns the ID of the client that committed a misbehaviour. +func (ev Evidence) GetClientID() string { + return ev.ClientID +} + +// 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 { + 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 the sequence at which misbehaviour occurred. +func (ev Evidence) GetHeight() int64 { + return int64(ev.Sequence) +} + +// ValidateBasic implements Evidence interface. +func (ev Evidence) ValidateBasic() error { + if err := host.ClientIdentifierValidator(ev.ClientID); err != nil { + return sdkerrors.Wrap(clienttypes.ErrInvalidEvidence, err.Error()) + } + + if ev.Sequence == 0 { + return sdkerrors.Wrap(clienttypes.ErrInvalidEvidence, "sequence cannot be 0") + } + + if err := ev.SignatureOne.ValidateBasic(); err != nil { + return sdkerrors.Wrap(clienttypes.ErrInvalidEvidence, err.Error()) + } + + if err := ev.SignatureTwo.ValidateBasic(); err != nil { + return sdkerrors.Wrap(clienttypes.ErrInvalidEvidence, err.Error()) + } + + // evidence signatures cannot be identical + if bytes.Equal(ev.SignatureOne.Signature, ev.SignatureTwo.Signature) { + return sdkerrors.Wrap(clienttypes.ErrInvalidEvidence, "evidence signatures cannot be equal") + } + + // message data signed cannot be identical + if bytes.Equal(ev.SignatureOne.Data, ev.SignatureTwo.Data) { + return sdkerrors.Wrap(clienttypes.ErrInvalidEvidence, "evidence signatures must be signed over different messages") + } + + return nil +} + +// SignatureAndData is a signature and the data signed over to create the signature. +type SignatureAndData struct { + Signature []byte `json:"signature" yaml:"signature"` + Data []byte `json:"data" yaml:"data"` +} + +// ValidateBasic ensures that the signature and data fields are non-empty. +func (sd SignatureAndData) ValidateBasic() error { + if len(sd.Signature) == 0 { + return sdkerrors.Wrap(ErrInvalidSignatureAndData, "signature cannot be empty") + } + if len(sd.Data) == 0 { + return sdkerrors.Wrap(ErrInvalidSignatureAndData, "data for signature cannot be emtpy") + } + + return nil +} diff --git a/x/ibc/06-solomachine/types/evidence_test.go b/x/ibc/06-solomachine/types/evidence_test.go new file mode 100644 index 000000000000..08def5586b7d --- /dev/null +++ b/x/ibc/06-solomachine/types/evidence_test.go @@ -0,0 +1,103 @@ +package types_test + +import ( + "github.com/tendermint/tendermint/crypto/tmhash" + tmbytes "github.com/tendermint/tendermint/libs/bytes" + + clientexported "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported" + solomachinetypes "github.com/cosmos/cosmos-sdk/x/ibc/06-solomachine/types" +) + +func (suite *SoloMachineTestSuite) TestEvidence() { + ev := suite.Evidence() + + suite.Require().Equal(clientexported.SoloMachine, ev.ClientType()) + suite.Require().Equal(suite.clientID, ev.GetClientID()) + suite.Require().Equal("client", ev.Route()) + suite.Require().Equal("client_misbehaviour", ev.Type()) + suite.Require().Equal(tmbytes.HexBytes(tmhash.Sum(solomachinetypes.SubModuleCdc.MustMarshalBinaryBare(ev))), ev.Hash()) + suite.Require().Equal(int64(suite.sequence), ev.GetHeight()) +} + +func (suite *SoloMachineTestSuite) TestEvidenceValidateBasic() { + testCases := []struct { + name string + malleateEvidence func(ev *solomachinetypes.Evidence) + expPass bool + }{ + { + "valid evidence", + func(*solomachinetypes.Evidence) {}, + true, + }, + { + "invalid client ID", + func(ev *solomachinetypes.Evidence) { + ev.ClientID = "(badclientid)" + }, + false, + }, + { + "sequence is zero", + func(ev *solomachinetypes.Evidence) { + ev.Sequence = 0 + }, + false, + }, + { + "signature one sig is empty", + func(ev *solomachinetypes.Evidence) { + ev.SignatureOne.Signature = []byte{} + }, + false, + }, + { + "signature two sig is empty", + func(ev *solomachinetypes.Evidence) { + ev.SignatureTwo.Signature = []byte{} + }, + false, + }, + { + "signature one data is empty", + func(ev *solomachinetypes.Evidence) { + ev.SignatureOne.Data = nil + }, + false, + }, + { + "signature two data is empty", + func(ev *solomachinetypes.Evidence) { + ev.SignatureTwo.Data = []byte{} + }, + false, + }, + { + "signatures are identical", + func(ev *solomachinetypes.Evidence) { + ev.SignatureTwo.Signature = ev.SignatureOne.Signature + }, + false, + }, + { + "data signed is identical", + func(ev *solomachinetypes.Evidence) { + ev.SignatureTwo.Data = ev.SignatureOne.Data + }, + false, + }, + } + + for i, tc := range testCases { + ev := suite.Evidence() + tc.malleateEvidence(&ev) + + err := ev.ValidateBasic() + + 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/06-solomachine/types/header.go b/x/ibc/06-solomachine/types/header.go new file mode 100644 index 000000000000..ad88257218ce --- /dev/null +++ b/x/ibc/06-solomachine/types/header.go @@ -0,0 +1,46 @@ +package types + +import ( + "github.com/tendermint/tendermint/crypto" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + clientexported "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported" + clienttypes "github.com/cosmos/cosmos-sdk/x/ibc/02-client/types" +) + +var _ clientexported.Header = Header{} + +// Header defines the Solo Machine consensus Header +type Header struct { + Sequence uint64 `json:"sequence" yaml:"sequence"` + Signature []byte `json:"signature" yaml:"signature"` + NewPubKey crypto.PubKey `json:"new_pubkey" yaml:"new_pubkey"` +} + +// ClientType defines that the Header is a Solo Machine verficiation algorithm. +func (Header) ClientType() clientexported.ClientType { + return clientexported.SoloMachine +} + +// GetHeight returns the current sequence number as the height. +func (h Header) GetHeight() uint64 { + return h.Sequence +} + +// ValidateBasic ensures that the sequence, signature and public key have all +// been initialized. +func (h Header) ValidateBasic() error { + if h.Sequence == 0 { + return sdkerrors.Wrap(clienttypes.ErrInvalidHeader, "sequence number cannot be zero") + } + + if len(h.Signature) == 0 { + return sdkerrors.Wrap(clienttypes.ErrInvalidHeader, "signature cannot be empty") + } + + if h.NewPubKey == nil { + return sdkerrors.Wrap(clienttypes.ErrInvalidHeader, "new public key is nil") + } + + return nil +} diff --git a/x/ibc/06-solomachine/types/header_test.go b/x/ibc/06-solomachine/types/header_test.go new file mode 100644 index 000000000000..5473fc050ef3 --- /dev/null +++ b/x/ibc/06-solomachine/types/header_test.go @@ -0,0 +1,32 @@ +package types_test + +import ( + clientexported "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported" + solomachinetypes "github.com/cosmos/cosmos-sdk/x/ibc/06-solomachine/types" +) + +func (suite *SoloMachineTestSuite) TestHeaderValidateBasic() { + header := suite.CreateHeader() + + cases := []struct { + name string + header solomachinetypes.Header + expPass bool + }{ + {"valid header", header, true}, + {"sequence is zero", solomachinetypes.Header{zero, header.Signature, header.NewPubKey}, false}, + {"signature is empty", solomachinetypes.Header{header.Sequence, []byte{}, header.NewPubKey}, false}, + {"public key is nil", solomachinetypes.Header{header.Sequence, header.Signature, nil}, false}, + } + + suite.Require().Equal(clientexported.SoloMachine, header.ClientType()) + + for i, tc := range cases { + if tc.expPass { + suite.Require().NoError(tc.header.ValidateBasic(), "valid test case %d failed: %s", i, tc.name) + } else { + suite.Require().Error(tc.header.ValidateBasic(), "invalid test case %d passed: %s", i, tc.name) + } + + } +} diff --git a/x/ibc/06-solomachine/types/msgs.go b/x/ibc/06-solomachine/types/msgs.go new file mode 100644 index 000000000000..3b2b1f1557d7 --- /dev/null +++ b/x/ibc/06-solomachine/types/msgs.go @@ -0,0 +1,182 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + evidenceexported "github.com/cosmos/cosmos-sdk/x/evidence/exported" + evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types" + clientexported "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported" + clienttypes "github.com/cosmos/cosmos-sdk/x/ibc/02-client/types" + host "github.com/cosmos/cosmos-sdk/x/ibc/24-host" +) + +var ( + _ clientexported.MsgCreateClient = MsgCreateClient{} + _ clientexported.MsgUpdateClient = MsgUpdateClient{} + _ evidenceexported.MsgSubmitEvidence = MsgSubmitClientMisbehaviour{} +) + +// MsgCreateClient defines a message to create an IBC client +type MsgCreateClient struct { + ClientID string `json:"client_id" yaml:"client_id"` + ConsensusState ConsensusState `json:"consensus_state" yaml:"consensus_state"` +} + +// NewMsgCreateClient creates a new MsgCreateClient instance +func NewMsgCreateClient(id string, consensusState ConsensusState) MsgCreateClient { + return MsgCreateClient{ + ClientID: id, + ConsensusState: consensusState, + } +} + +// Route implements sdk.Msg +func (msg MsgCreateClient) Route() string { + return host.RouterKey +} + +// Type implements sdk.Msg +func (msg MsgCreateClient) Type() string { + return clientexported.TypeMsgCreateClient +} + +// ValidateBasic implements sdk.Msg +func (msg MsgCreateClient) ValidateBasic() error { + if err := msg.ConsensusState.ValidateBasic(); err != nil { + return sdkerrors.Wrapf(clienttypes.ErrInvalidConsensus, "consensus state failed validatebasic: %v", err) + } + + return host.ClientIdentifierValidator(msg.ClientID) +} + +// GetSignBytes implements sdk.Msg +func (msg MsgCreateClient) GetSignBytes() []byte { + return sdk.MustSortJSON(SubModuleCdc.MustMarshalJSON(msg)) +} + +// GetSigners implements sdk.Msg +func (msg MsgCreateClient) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{sdk.AccAddress(msg.ConsensusState.PubKey.Address())} +} + +// GetClientID implements clientexported.MsgCreateClient +func (msg MsgCreateClient) GetClientID() string { + return msg.ClientID +} + +// GetClientType implements clientexported.MsgCreateClient +func (msg MsgCreateClient) GetClientType() string { + return clientexported.ClientTypeSoloMachine +} + +// GetConsensusState implements clientexported.MsgCreateClient +func (msg MsgCreateClient) GetConsensusState() clientexported.ConsensusState { + return msg.ConsensusState +} + +// MsgUpdateClient defines a message to update an IBC client +type MsgUpdateClient struct { + ClientID string `json:"client_id" yaml:"client_id"` + Header Header `json:"header" yaml:"header"` +} + +// NewMsgUpdateClient creates a new MsgUpdateClient instance +func NewMsgUpdateClient(id string, header Header) MsgUpdateClient { + return MsgUpdateClient{ + ClientID: id, + Header: header, + } +} + +// Route implements sdk.Msg +func (msg MsgUpdateClient) Route() string { + return host.RouterKey +} + +// Type implements sdk.Msg +func (msg MsgUpdateClient) Type() string { + return clientexported.TypeMsgUpdateClient +} + +// ValidateBasic implements sdk.Msg +func (msg MsgUpdateClient) ValidateBasic() error { + if err := msg.Header.ValidateBasic(); err != nil { + return sdkerrors.Wrapf(ErrInvalidHeader, "header validatebasic failed: %v", err) + } + return host.ClientIdentifierValidator(msg.ClientID) +} + +// GetSignBytes implements sdk.Msg +func (msg MsgUpdateClient) GetSignBytes() []byte { + return sdk.MustSortJSON(SubModuleCdc.MustMarshalJSON(msg)) +} + +// GetSigners implements sdk.Msg +func (msg MsgUpdateClient) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{sdk.AccAddress(msg.Header.NewPubKey.Address())} +} + +// GetClientID implements clientexported.MsgUpdateClient +func (msg MsgUpdateClient) GetClientID() string { + return msg.ClientID +} + +// GetHeader implements clientexported.MsgUpdateClient +func (msg MsgUpdateClient) GetHeader() clientexported.Header { + return msg.Header +} + +// MsgSubmitClientMisbehaviour defines an sdk.Msg type that supports submitting +// Evidence for client misbehaviour. +type MsgSubmitClientMisbehaviour struct { + Evidence evidenceexported.Evidence `json:"evidence" yaml:"evidence"` + Submitter sdk.AccAddress `json:"submitter" yaml:"submitter"` +} + +// NewMsgSubmitClientMisbehaviour creates a new MsgSubmitClientMisbehaviour +// instance. +func NewMsgSubmitClientMisbehaviour(e evidenceexported.Evidence, s sdk.AccAddress) MsgSubmitClientMisbehaviour { + return MsgSubmitClientMisbehaviour{Evidence: e, Submitter: s} +} + +// Route returns the MsgSubmitClientMisbehaviour's route. +func (msg MsgSubmitClientMisbehaviour) Route() string { return host.RouterKey } + +// Type returns the MsgSubmitClientMisbehaviour's type. +func (msg MsgSubmitClientMisbehaviour) Type() string { + return clientexported.TypeMsgSubmitClientMisbehaviour +} + +// ValidateBasic performs basic (non-state-dependent) validation on a MsgSubmitClientMisbehaviour. +func (msg MsgSubmitClientMisbehaviour) ValidateBasic() error { + if msg.Evidence == nil { + return sdkerrors.Wrap(evidencetypes.ErrInvalidEvidence, "missing evidence") + } + if err := msg.Evidence.ValidateBasic(); err != nil { + return err + } + if msg.Submitter.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, msg.Submitter.String()) + } + + return nil +} + +// GetSignBytes returns the raw bytes a signer is expected to sign when submitting +// a MsgSubmitClientMisbehaviour message. +func (msg MsgSubmitClientMisbehaviour) GetSignBytes() []byte { + return sdk.MustSortJSON(SubModuleCdc.MustMarshalJSON(msg)) +} + +// GetSigners returns the single expected signer for a MsgSubmitClientMisbehaviour. +func (msg MsgSubmitClientMisbehaviour) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Submitter} +} + +func (msg MsgSubmitClientMisbehaviour) GetEvidence() evidenceexported.Evidence { + return msg.Evidence +} + +func (msg MsgSubmitClientMisbehaviour) GetSubmitter() sdk.AccAddress { + return msg.Submitter +} diff --git a/x/ibc/06-solomachine/types/msgs_test.go b/x/ibc/06-solomachine/types/msgs_test.go new file mode 100644 index 000000000000..f6e8ca998a49 --- /dev/null +++ b/x/ibc/06-solomachine/types/msgs_test.go @@ -0,0 +1,72 @@ +package types_test + +import ( + solomachinetypes "github.com/cosmos/cosmos-sdk/x/ibc/06-solomachine" +) + +func (suite *SoloMachineTestSuite) TestMsgCreateClientValidateBasic() { + cases := []struct { + name string + msg solomachinetypes.MsgCreateClient + expPass bool + }{ + {"valid msg", solomachinetypes.NewMsgCreateClient(suite.clientID, suite.ConsensusState()), true}, + {"invalid client id", solomachinetypes.NewMsgCreateClient("(BADCLIENTID)", suite.ConsensusState()), false}, + {"invalid consensus state with zero sequence", solomachinetypes.NewMsgCreateClient(suite.clientID, solomachinetypes.ConsensusState{0, suite.privKey.PubKey()}), false}, + {"invalid consensus state with nil pubkey", solomachinetypes.NewMsgCreateClient(suite.clientID, solomachinetypes.ConsensusState{suite.sequence, nil}), false}, + } + + for i, tc := range cases { + err := tc.msg.ValidateBasic() + if tc.expPass { + suite.Require().NoError(err, "Msg %d failed: %v", i, tc.name) + } else { + suite.Require().Error(err, "Invalid Msg %d passed: %s", i, tc.name) + } + } +} + +func (suite *SoloMachineTestSuite) TestMsgUpdateClientValidateBasic() { + header := suite.CreateHeader() + + cases := []struct { + name string + msg solomachinetypes.MsgUpdateClient + expPass bool + }{ + {"valid msg", solomachinetypes.NewMsgUpdateClient(suite.clientID, header), true}, + {"invalid client id", solomachinetypes.NewMsgUpdateClient("(BADCLIENTID)", header), false}, + {"invalid header - sequence is zero", solomachinetypes.NewMsgUpdateClient(suite.clientID, solomachinetypes.Header{0, header.Signature, header.NewPubKey}), false}, + {"invalid header - signature is empty", solomachinetypes.NewMsgUpdateClient(suite.clientID, solomachinetypes.Header{header.Sequence, []byte{}, header.NewPubKey}), false}, + {"invalid header - pubkey is empty", solomachinetypes.NewMsgUpdateClient(suite.clientID, solomachinetypes.Header{header.Sequence, header.Signature, nil}), false}, + } + + for i, tc := range cases { + err := tc.msg.ValidateBasic() + if tc.expPass { + suite.Require().NoError(err, "Msg %d failed: %v", i, tc.name) + } else { + suite.Require().Error(err, "Invalid Msg %d passed: %s", i, tc.name) + } + } + +} + +// TODO +func (suite *SoloMachineTestSuite) TestMsgSubmitClientMisbehaviourValidateBasic() { + cases := []struct { + name string + msg solomachinetypes.MsgSubmitClientMisbehaviour + expPass bool + }{} + + for i, tc := range cases { + err := tc.msg.ValidateBasic() + if tc.expPass { + suite.Require().NoError(err, "Msg %d failed: %v", i, tc.name) + } else { + suite.Require().Error(err, "Invalid Msg %d passed: %s", i, tc.name) + } + } + +} diff --git a/x/ibc/06-solomachine/types/solomachine_test.go b/x/ibc/06-solomachine/types/solomachine_test.go new file mode 100644 index 000000000000..7fbd35428c1b --- /dev/null +++ b/x/ibc/06-solomachine/types/solomachine_test.go @@ -0,0 +1,122 @@ +package types_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/suite" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + clientexported "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported" + solomachinetypes "github.com/cosmos/cosmos-sdk/x/ibc/06-solomachine/types" + host "github.com/cosmos/cosmos-sdk/x/ibc/24-host" +) + +const ( + zero = 0 +) + +type SoloMachineTestSuite struct { + suite.Suite + + ctx sdk.Context + aminoCdc *codec.Codec + cdc codec.Marshaler + store sdk.KVStore + privKey crypto.PrivKey + sequence uint64 + clientID string + now time.Time +} + +func (suite *SoloMachineTestSuite) SetupTest() { + checkTx := false + app := simapp.Setup(checkTx) + + suite.aminoCdc = app.Codec() + suite.cdc = app.AppCodec() + + suite.now = time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC) + suite.privKey = ed25519.GenPrivKey() + + suite.sequence = 1 + suite.clientID = "solomachineclient" + suite.ctx = app.BaseApp.NewContext(checkTx, abci.Header{Height: 1, Time: suite.now}) + suite.store = app.IBCKeeper.ClientKeeper.ClientStore(suite.ctx, clientexported.ClientTypeSoloMachine) + + bz := suite.aminoCdc.MustMarshalBinaryBare(suite.ClientState()) + suite.store.Set(host.KeyClientState(), bz) +} + +func TestSoloMachineTestSuite(t *testing.T) { + suite.Run(t, new(SoloMachineTestSuite)) +} + +func (suite *SoloMachineTestSuite) CreateHeader() solomachinetypes.Header { + newPrivKey := ed25519.GenPrivKey() + signature, err := suite.privKey.Sign(newPrivKey.PubKey().Bytes()) + suite.Require().NoError(err) + + suite.sequence++ + suite.privKey = newPrivKey + + return solomachinetypes.Header{ + Sequence: suite.sequence, + Signature: signature, + NewPubKey: suite.privKey.PubKey(), + } +} + +func (suite *SoloMachineTestSuite) ClientState() solomachinetypes.ClientState { + return solomachinetypes.NewClientState(suite.clientID, suite.ConsensusState()) +} + +func (suite *SoloMachineTestSuite) ConsensusState() solomachinetypes.ConsensusState { + return solomachinetypes.ConsensusState{ + Sequence: suite.sequence, + PubKey: suite.privKey.PubKey(), + } +} + +func (suite *SoloMachineTestSuite) Evidence() solomachinetypes.Evidence { + dataOne := []byte("DATA ONE") + dataTwo := []byte("DATA TWO") + + sig, err := suite.privKey.Sign(append(sdk.Uint64ToBigEndian(suite.sequence), dataOne...)) + suite.Require().NoError(err) + + signatureOne := solomachinetypes.SignatureAndData{ + Signature: sig, + Data: dataOne, + } + + sig, err = suite.privKey.Sign(append(sdk.Uint64ToBigEndian(suite.sequence), dataTwo...)) + suite.Require().NoError(err) + + signatureTwo := solomachinetypes.SignatureAndData{ + Signature: sig, + Data: dataTwo, + } + + return solomachinetypes.Evidence{ + ClientID: suite.clientID, + Sequence: suite.sequence, + SignatureOne: signatureOne, + SignatureTwo: signatureTwo, + } +} + +func (suite *SoloMachineTestSuite) GetSequenceFromStore() uint64 { + bz := suite.store.Get(host.KeyClientState()) + suite.Require().NotNil(bz) + + var clientState solomachinetypes.ClientState + suite.aminoCdc.MustUnmarshalBinaryBare(bz, &clientState) + return clientState.ConsensusState.Sequence +} diff --git a/x/ibc/06-solomachine/update.go b/x/ibc/06-solomachine/update.go new file mode 100644 index 000000000000..2d776da24d4c --- /dev/null +++ b/x/ibc/06-solomachine/update.go @@ -0,0 +1,75 @@ +package solomachine + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + clientexported "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported" + clienttypes "github.com/cosmos/cosmos-sdk/x/ibc/02-client/types" +) + +// CheckValidityAndUpdateState checks if the provided header is valid and updates +// the consensus state if appropriate. It returns an error if: +// - the client or header provided are not parseable to solo machine types +// - the currently registered public key did not provide the update signature +func CheckValidityAndUpdateState( + clientState clientexported.ClientState, header clientexported.Header, +) (clientexported.ClientState, clientexported.ConsensusState, error) { + // cast the client state to solo machine + smClientState, ok := clientState.(ClientState) + if !ok { + return nil, nil, sdkerrors.Wrap( + clienttypes.ErrInvalidClientType, "light client is not from solo machine", + ) + } + + smHeader, ok := header.(Header) + if !ok { + return nil, nil, sdkerrors.Wrap( + clienttypes.ErrInvalidHeader, "header is not from solo machine", + ) + } + + if err := checkValidity(smClientState, smHeader); err != nil { + return nil, nil, err + } + + smClientState, consensusState := update(smClientState, smHeader) + return smClientState, consensusState, nil +} + +// checkValidity checks if the Solo Machine update signature is valid. +func checkValidity(clientState ClientState, header Header) error { + // assert update sequence is current sequence + if header.Sequence != clientState.ConsensusState.Sequence { + return sdkerrors.Wrapf( + clienttypes.ErrInvalidHeader, + "sequence provided in the header does not match the client state sequence (%d != %d)", header.Sequence, clientState.ConsensusState.Sequence, + ) + } + + // assert currently registered public key signed over the new public key with correct sequence + value := append( + sdk.Uint64ToBigEndian(header.Sequence), + header.NewPubKey.Bytes()..., + ) + if clientState.ConsensusState.PubKey.VerifyBytes(value, header.Signature) { + return sdkerrors.Wrap( + clienttypes.ErrInvalidHeader, + "header signature verification failed", + ) + } + + 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{ + // increment sequence number + Sequence: header.Sequence + 1, + PubKey: header.NewPubKey, + } + + clientState.ConsensusState = consensusState + return clientState, consensusState +} diff --git a/x/ibc/06-solomachine/update_test.go b/x/ibc/06-solomachine/update_test.go new file mode 100644 index 000000000000..cb1e204e8931 --- /dev/null +++ b/x/ibc/06-solomachine/update_test.go @@ -0,0 +1 @@ +package solomachine_test diff --git a/x/ibc/07-tendermint/client/rest/tx.go b/x/ibc/07-tendermint/client/rest/tx.go index b0646a887d5c..7fc119b5b0bb 100644 --- a/x/ibc/07-tendermint/client/rest/tx.go +++ b/x/ibc/07-tendermint/client/rest/tx.go @@ -64,7 +64,7 @@ func createClientHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { } } -// updateClientHandlerFn implements a update client handler +// updateClientHandlerFn implements an update client handler // // @Summary update client // @Tags IBC diff --git a/x/ibc/07-tendermint/types/msgs.go b/x/ibc/07-tendermint/types/msgs.go index 3fcc15e1b662..f20e2d6bde43 100644 --- a/x/ibc/07-tendermint/types/msgs.go +++ b/x/ibc/07-tendermint/types/msgs.go @@ -15,13 +15,6 @@ import ( host "github.com/cosmos/cosmos-sdk/x/ibc/24-host" ) -// Message types for the IBC client -const ( - TypeMsgCreateClient string = "create_client" - TypeMsgUpdateClient string = "update_client" - TypeMsgSubmitClientMisbehaviour string = "submit_client_misbehaviour" -) - var ( _ clientexported.MsgCreateClient = MsgCreateClient{} _ clientexported.MsgUpdateClient = MsgUpdateClient{} @@ -63,7 +56,7 @@ func (msg MsgCreateClient) Route() string { // Type implements sdk.Msg func (msg MsgCreateClient) Type() string { - return TypeMsgCreateClient + return clientexported.TypeMsgCreateClient } // ValidateBasic implements sdk.Msg @@ -145,7 +138,7 @@ func (msg MsgUpdateClient) Route() string { // Type implements sdk.Msg func (msg MsgUpdateClient) Type() string { - return TypeMsgUpdateClient + return clientexported.TypeMsgUpdateClient } // ValidateBasic implements sdk.Msg @@ -193,7 +186,9 @@ func NewMsgSubmitClientMisbehaviour(e evidenceexported.Evidence, s sdk.AccAddres func (msg MsgSubmitClientMisbehaviour) Route() string { return host.RouterKey } // Type returns the MsgSubmitClientMisbehaviour's type. -func (msg MsgSubmitClientMisbehaviour) Type() string { return TypeMsgSubmitClientMisbehaviour } +func (msg MsgSubmitClientMisbehaviour) Type() string { + return clientexported.TypeMsgSubmitClientMisbehaviour +} // ValidateBasic performs basic (non-state-dependant) validation on a MsgSubmitClientMisbehaviour. func (msg MsgSubmitClientMisbehaviour) ValidateBasic() error { diff --git a/x/ibc/09-localhost/types/msgs.go b/x/ibc/09-localhost/types/msgs.go index 8b526f446a05..d44fe1945532 100644 --- a/x/ibc/09-localhost/types/msgs.go +++ b/x/ibc/09-localhost/types/msgs.go @@ -7,11 +7,6 @@ import ( host "github.com/cosmos/cosmos-sdk/x/ibc/24-host" ) -// Message types for the IBC client -const ( - TypeMsgCreateClient string = "create_client" -) - var ( _ clientexported.MsgCreateClient = MsgCreateClient{} ) @@ -35,7 +30,7 @@ func (msg MsgCreateClient) Route() string { // Type implements sdk.Msg func (msg MsgCreateClient) Type() string { - return TypeMsgCreateClient + return clientexported.TypeMsgCreateClient } // ValidateBasic implements sdk.Msg diff --git a/x/ibc/23-commitment/exported/exported.go b/x/ibc/23-commitment/exported/exported.go index c6fbbce3ada1..eedbbe399e55 100644 --- a/x/ibc/23-commitment/exported/exported.go +++ b/x/ibc/23-commitment/exported/exported.go @@ -52,11 +52,13 @@ type Type byte // Registered commitment types const ( Merkle Type = iota + 1 // 1 + Signature ) // string representation of the commitment types const ( - TypeMerkle string = "merkle" + TypeMerkle string = "merkle" + TypeSignature string = "signature" ) // String implements the Stringer interface diff --git a/x/ibc/23-commitment/types/signature.go b/x/ibc/23-commitment/types/signature.go new file mode 100644 index 000000000000..bbe02be342af --- /dev/null +++ b/x/ibc/23-commitment/types/signature.go @@ -0,0 +1,68 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/exported" +) + +var _ exported.Proof = SignatureProof{} +var _ exported.Prefix = (*SignaturePrefix)(nil) + +// TODO: change to proto +type SignaturePrefix struct { + KeyPrefix []byte +} + +// NewSignaturePrefix constructs a new SignaturePrefix instance. +func NewSignaturePrefix(keyPrefix []byte) SignaturePrefix { + return SignaturePrefix{ + KeyPrefix: keyPrefix, + } +} + +// GetCommitmentType implements Prefix interface. +func (sp SignaturePrefix) GetCommitmentType() exported.Type { + return exported.Signature +} + +// Bytes returns the key prefix bytes. +func (sp SignaturePrefix) Bytes() []byte { + return sp.KeyPrefix +} + +// IsEmpty returns true if the prefix is empty. +func (sp SignaturePrefix) IsEmpty() bool { + return len(sp.Bytes()) == 0 +} + +// SignatureProof is a signature used as proof for verification. +type SignatureProof struct { + Signature []byte +} + +// GetCommitmentType implements ProofI. +func (SignatureProof) GetCommitmentType() exported.Type { + return exported.Signature +} + +// VerifyMembership implements ProofI. +func (SignatureProof) VerifyMembership(exported.Root, exported.Path, []byte) error { + return nil +} + +// VerifyNonMembership implements ProofI. +func (SignatureProof) VerifyNonMembership(exported.Root, exported.Path) error { + return nil +} + +// IsEmpty returns trie if the signature is emtpy. +func (proof SignatureProof) IsEmpty() bool { + return len(proof.Signature) == 0 +} + +// ValidateBasic checks if the proof is empty. +func (proof SignatureProof) ValidateBasic() error { + if proof.IsEmpty() { + return ErrInvalidProof + } + return nil +} diff --git a/x/ibc/types/codec.go b/x/ibc/types/codec.go index c4949ca68dc8..10b5c61f73b0 100644 --- a/x/ibc/types/codec.go +++ b/x/ibc/types/codec.go @@ -6,6 +6,7 @@ import ( client "github.com/cosmos/cosmos-sdk/x/ibc/02-client" connection "github.com/cosmos/cosmos-sdk/x/ibc/03-connection" channel "github.com/cosmos/cosmos-sdk/x/ibc/04-channel" + solomachinetypes "github.com/cosmos/cosmos-sdk/x/ibc/06-solomachine/types" ibctmtypes "github.com/cosmos/cosmos-sdk/x/ibc/07-tendermint/types" localhosttypes "github.com/cosmos/cosmos-sdk/x/ibc/09-localhost/types" commitmenttypes "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/types" @@ -17,6 +18,7 @@ func RegisterCodec(cdc *codec.Codec) { client.RegisterCodec(cdc) connection.RegisterCodec(cdc) channel.RegisterCodec(cdc) + solomachinetypes.RegisterCodec(cdc) ibctmtypes.RegisterCodec(cdc) localhosttypes.RegisterCodec(cdc) commitmenttypes.RegisterCodec(cdc)