diff --git a/client/flags/flags.go b/client/flags/flags.go index 62f2a8d012f..0dbaa4c68d1 100644 --- a/client/flags/flags.go +++ b/client/flags/flags.go @@ -54,6 +54,7 @@ const ( FlagRPCWriteTimeout = "write-timeout" FlagOutputDocument = "output-document" // inspired by wget -O FlagSkipConfirmation = "yes" + FlagProve = "prove" ) // LineBreak can be included in a command list to provide a blank line diff --git a/x/ibc/03-connection/alias.go b/x/ibc/03-connection/alias.go new file mode 100644 index 00000000000..27757d8163e --- /dev/null +++ b/x/ibc/03-connection/alias.go @@ -0,0 +1,102 @@ +package connection + +// nolint +// autogenerated code using github.com/rigelrozanski/multitool +// aliases generated for the following subdirectories: +// ALIASGEN: github.com/cosmos/cosmos-sdk/x/ibc/03-connection/keeper +// ALIASGEN: github.com/cosmos/cosmos-sdk/x/ibc/03-connection/types + +import ( + "github.com/cosmos/cosmos-sdk/x/ibc/03-connection/keeper" + "github.com/cosmos/cosmos-sdk/x/ibc/03-connection/types" +) + +const ( + NONE = types.NONE + INIT = types.INIT + TRYOPEN = types.TRYOPEN + OPEN = types.OPEN + StateNone = types.StateNone + StateInit = types.StateInit + StateTryOpen = types.StateTryOpen + StateOpen = types.StateOpen + DefaultCodespace = types.DefaultCodespace + CodeConnectionExists = types.CodeConnectionExists + CodeConnectionNotFound = types.CodeConnectionNotFound + CodeClientConnectionPathsNotFound = types.CodeClientConnectionPathsNotFound + CodeConnectionPath = types.CodeConnectionPath + CodeInvalidCounterpartyConnection = types.CodeInvalidCounterpartyConnection + CodeInvalidVersion = types.CodeInvalidVersion + CodeInvalidHeight = types.CodeInvalidHeight + CodeInvalidConnectionState = types.CodeInvalidConnectionState + CodeInvalidProof = types.CodeInvalidProof + CodeInvalidCounterparty = types.CodeInvalidCounterparty + AttributeKeyConnectionID = types.AttributeKeyConnectionID + AttributeKeyCounterpartyClientID = types.AttributeKeyCounterpartyClientID + SubModuleName = types.SubModuleName + StoreKey = types.StoreKey + RouterKey = types.RouterKey + QuerierRoute = types.QuerierRoute + QueryConnection = types.QueryConnection + QueryClientConnections = types.QueryClientConnections +) + +var ( + // functions aliases + NewKeeper = keeper.NewKeeper + QuerierConnection = keeper.QuerierConnection + QuerierClientConnections = keeper.QuerierClientConnections + RegisterCodec = types.RegisterCodec + NewConnectionEnd = types.NewConnectionEnd + NewCounterparty = types.NewCounterparty + StateFromString = types.StateFromString + ErrConnectionExists = types.ErrConnectionExists + ErrConnectionNotFound = types.ErrConnectionNotFound + ErrClientConnectionPathsNotFound = types.ErrClientConnectionPathsNotFound + ErrConnectionPath = types.ErrConnectionPath + ErrInvalidCounterpartyConnection = types.ErrInvalidCounterpartyConnection + ErrInvalidVersion = types.ErrInvalidVersion + ErrInvalidHeight = types.ErrInvalidHeight + ErrInvalidConnectionState = types.ErrInvalidConnectionState + ErrInvalidConnectionProof = types.ErrInvalidConnectionProof + ErrInvalidCounterparty = types.ErrInvalidCounterparty + ConnectionPath = types.ConnectionPath + ClientConnectionsPath = types.ClientConnectionsPath + KeyConnection = types.KeyConnection + KeyClientConnections = types.KeyClientConnections + NewMsgConnectionOpenInit = types.NewMsgConnectionOpenInit + NewMsgConnectionOpenTry = types.NewMsgConnectionOpenTry + NewMsgConnectionOpenAck = types.NewMsgConnectionOpenAck + NewMsgConnectionOpenConfirm = types.NewMsgConnectionOpenConfirm + NewConnectionResponse = types.NewConnectionResponse + NewQueryConnectionParams = types.NewQueryConnectionParams + NewClientConnectionsResponse = types.NewClientConnectionsResponse + NewQueryClientConnectionsParams = types.NewQueryClientConnectionsParams + GetCompatibleVersions = types.GetCompatibleVersions + LatestVersion = types.LatestVersion + PickVersion = types.PickVersion + + // variable aliases + SubModuleCdc = types.SubModuleCdc + EventTypeConnectionOpenInit = types.EventTypeConnectionOpenInit + EventTypeConnectionOpenTry = types.EventTypeConnectionOpenTry + EventTypeConnectionOpenAck = types.EventTypeConnectionOpenAck + EventTypeConnectionOpenConfirm = types.EventTypeConnectionOpenConfirm + AttributeValueCategory = types.AttributeValueCategory +) + +type ( + Keeper = keeper.Keeper + ConnectionEnd = types.ConnectionEnd + Counterparty = types.Counterparty + State = types.State + ClientKeeper = types.ClientKeeper + MsgConnectionOpenInit = types.MsgConnectionOpenInit + MsgConnectionOpenTry = types.MsgConnectionOpenTry + MsgConnectionOpenAck = types.MsgConnectionOpenAck + MsgConnectionOpenConfirm = types.MsgConnectionOpenConfirm + ConnectionResponse = types.ConnectionResponse + QueryConnectionParams = types.QueryConnectionParams + ClientConnectionsResponse = types.ClientConnectionsResponse + QueryClientConnectionsParams = types.QueryClientConnectionsParams +) diff --git a/x/ibc/03-connection/client/cli/query.go b/x/ibc/03-connection/client/cli/query.go new file mode 100644 index 00000000000..266ac4bf02d --- /dev/null +++ b/x/ibc/03-connection/client/cli/query.go @@ -0,0 +1,130 @@ +package cli + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/version" + "github.com/cosmos/cosmos-sdk/x/ibc/03-connection/types" +) + +// GetQueryCmd returns the query commands for IBC connections +func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { + ics03ConnectionQueryCmd := &cobra.Command{ + Use: "connection", + Short: "IBC connection query subcommands", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + } + + ics03ConnectionQueryCmd.AddCommand(client.GetCommands( + GetCmdQueryConnection(queryRoute, cdc), + )...) + return ics03ConnectionQueryCmd +} + +// GetCmdQueryConnection defines the command to query a connection end +func GetCmdQueryConnection(queryRoute string, cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "end [connection-id]", + Short: "Query stored connection end", + Long: strings.TrimSpace(fmt.Sprintf(`Query stored connection end + +Example: +$ %s query ibc connection end [connection-id] + `, version.ClientName), + ), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + connectionID := args[0] + + bz, err := cdc.MarshalJSON(types.NewQueryConnectionParams(connectionID)) + if err != nil { + return err + } + + req := abci.RequestQuery{ + Path: fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryConnection), + Data: bz, + Prove: viper.GetBool(flags.FlagProve), + } + + res, err := cliCtx.QueryABCI(req) + if err != nil { + return err + } + + var connection types.ConnectionEnd + if err := cdc.UnmarshalJSON(res.Value, &connection); err != nil { + return err + } + + if res.Proof == nil { + return cliCtx.PrintOutput(connection) + } + + connRes := types.NewConnectionResponse(connectionID, connection, res.Proof, res.Height) + return cliCtx.PrintOutput(connRes) + }, + } + cmd.Flags().Bool(flags.FlagProve, true, "show proofs for the query results") + + return cmd +} + +// GetCmdQueryClientConnections defines the command to query a client connections +func GetCmdQueryClientConnections(queryRoute string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "client [client-id]", + Short: "Query stored client connection paths", + Long: strings.TrimSpace(fmt.Sprintf(`Query stored client connection paths + +Example: +$ %s query ibc connection client [client-id] + `, version.ClientName), + ), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + clientID := args[0] + + bz, err := cdc.MarshalJSON(types.NewQueryClientConnectionsParams(clientID)) + if err != nil { + return err + } + + req := abci.RequestQuery{ + Path: fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryClientConnections), + Data: bz, + Prove: viper.GetBool(flags.FlagProve), + } + + res, err := cliCtx.QueryABCI(req) + if err != nil { + return err + } + + var connectionPaths []string + if err := cdc.UnmarshalJSON(res.Value, &connectionPaths); err != nil { + return err + } + + if res.Proof == nil { + return cliCtx.PrintOutput(connectionPaths) + } + + connPathsRes := types.NewClientConnectionsResponse(clientID, connectionPaths, res.Proof, res.Height) + return cliCtx.PrintOutput(connPathsRes) + }, + } +} diff --git a/x/ibc/03-connection/client/cli/tx.go b/x/ibc/03-connection/client/cli/tx.go new file mode 100644 index 00000000000..22ccf7777d3 --- /dev/null +++ b/x/ibc/03-connection/client/cli/tx.go @@ -0,0 +1,569 @@ +package cli + +import ( + "fmt" + "io/ioutil" + "os" + "strings" + "time" + + "github.com/pkg/errors" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/cosmos/cosmos-sdk/client" + "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" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + clientutils "github.com/cosmos/cosmos-sdk/x/ibc/02-client/client/utils" + clienttypes "github.com/cosmos/cosmos-sdk/x/ibc/02-client/types" + "github.com/cosmos/cosmos-sdk/x/ibc/03-connection/types" + commitment "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment" + abci "github.com/tendermint/tendermint/abci/types" +) + +// Connection Handshake flags +const ( + FlagNode1 = "node1" + FlagNode2 = "node2" + FlagFrom1 = "from1" + FlagFrom2 = "from2" + FlagChainID2 = "chain-id2" +) + +// GetTxCmd returns the transaction commands for IBC Connections +func GetTxCmd(storeKey string, cdc *codec.Codec) *cobra.Command { + ics03ConnectionTxCmd := &cobra.Command{ + Use: "connection", + Short: "IBC connection transaction subcommands", + } + + ics03ConnectionTxCmd.AddCommand(client.PostCommands( + GetCmdConnectionOpenInit(storeKey, cdc), + GetCmdConnectionOpenTry(storeKey, cdc), + GetCmdConnectionOpenAck(storeKey, cdc), + GetCmdConnectionOpenConfirm(storeKey, cdc), + GetCmdHandshakeState(storeKey, cdc), + )...) + + return ics03ConnectionTxCmd +} + +// GetCmdConnectionOpenInit defines the command to initialize a connection on +// chain A with a given counterparty chain B +func GetCmdConnectionOpenInit(storeKey string, cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: strings.TrimSpace(`open-init [connection-id] [client-id] [counterparty-connection-id] + [counterparty-client-id] [path/to/counterparty_prefix.json]`), + Short: "initialize connection on chain A", + Long: strings.TrimSpace( + fmt.Sprintf(`initialize a connection on chain A with a given counterparty chain B: + +Example: +$ %s tx ibc connection open-init [connection-id] [client-id] [counterparty-connection-id] +[counterparty-client-id] [path/to/counterparty_prefix.json] + `, version.ClientName), + ), + Args: cobra.ExactArgs(6), + RunE: func(cmd *cobra.Command, args []string) error { + txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContext().WithCodec(cdc) + + connectionID := args[0] + clientID := args[1] + counterpartyConnectionID := args[2] + counterpartyClientID := args[3] + + bz, err := ioutil.ReadFile(args[4]) + if err != nil { + return err + } + + var counterpartyPrefix commitment.Prefix + if err := cdc.UnmarshalJSON(bz, &counterpartyPrefix); err != nil { + return err + } + + msg := types.NewMsgConnectionOpenInit( + connectionID, clientID, counterpartyConnectionID, counterpartyClientID, + counterpartyPrefix, cliCtx.GetFromAddress(), + ) + + if err := msg.ValidateBasic(); err != nil { + return err + } + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + return cmd +} + +// GetCmdConnectionOpenTry defines the command to relay a try open a connection on +// chain B +func GetCmdConnectionOpenTry(storeKey string, cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: strings.TrimSpace(`open-try [connection-id] [client-id] +[counterparty-connection-id] [counterparty-client-id] [path/to/counterparty_prefix.json] +[counterparty-versions] [path/to/proof_init.json]`), + Short: "initiate connection handshake between two chains", + Long: strings.TrimSpace( + fmt.Sprintf(`initialize a connection on chain A with a given counterparty chain B: + +Example: +$ %s tx ibc connection open-try connection-id] [client-id] +[counterparty-connection-id] [counterparty-client-id] [path/to/counterparty_prefix.json] +[counterparty-versions] [path/to/proof_init.json] + `, version.ClientName), + ), + Args: cobra.ExactArgs(6), + RunE: func(cmd *cobra.Command, args []string) error { + txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContext(). + WithCodec(cdc). + WithHeight(viper.GetInt64(flags.FlagHeight)) + + connectionID := args[0] + clientID := args[1] + counterpartyConnectionID := args[2] + counterpartyClientID := args[3] + + prefixBz, err := ioutil.ReadFile(args[4]) + if err != nil { + return err + } + + var counterpartyPrefix commitment.Prefix + if err := cdc.UnmarshalJSON(prefixBz, &counterpartyPrefix); err != nil { + return err + } + + // TODO: parse strings? + counterpartyVersions := args[5] + + proofBz, err := ioutil.ReadFile(args[6]) + if err != nil { + return err + } + + var proofInit commitment.Proof + if err := cdc.UnmarshalJSON(proofBz, &proofInit); err != nil { + return err + } + + proofHeight := uint64(cliCtx.Height) + consensusHeight, err := lastHeight(cliCtx) + if err != nil { + return err + } + + msg := types.NewMsgConnectionOpenTry( + connectionID, clientID, counterpartyConnectionID, counterpartyClientID, + counterpartyPrefix, []string{counterpartyVersions}, proofInit, proofInit, proofHeight, + consensusHeight, cliCtx.GetFromAddress(), + ) + + if err := msg.ValidateBasic(); err != nil { + return err + } + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + return cmd +} + +// GetCmdConnectionOpenAck defines the command to relay the acceptance of a +// connection open attempt from chain B to chain A +func GetCmdConnectionOpenAck(storeKey string, cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "open-ack [connection-id] [path/to/proof_try.json] [version]", + Short: "relay the acceptance of a connection open attempt from chain B to chain A", + Long: strings.TrimSpace( + fmt.Sprintf(`relay the acceptance of a connection open attempt from chain B to chain A: + +Example: +$ %s tx ibc connection open-ack [connection-id] [path/to/proof_try.json] [version] + `, version.ClientName), + ), + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContext().WithCodec(cdc) + + connectionID := args[0] + proofBz, err := ioutil.ReadFile(args[1]) + if err != nil { + return err + } + + var proofTry commitment.Proof + if err := cdc.UnmarshalJSON(proofBz, &proofTry); err != nil { + return err + } + + proofHeight := uint64(cliCtx.Height) + consensusHeight, err := lastHeight(cliCtx) + if err != nil { + return err + } + + version := args[4] + + msg := types.NewMsgConnectionOpenAck( + connectionID, proofTry, proofTry, proofHeight, + consensusHeight, version, cliCtx.GetFromAddress(), + ) + + if err := msg.ValidateBasic(); err != nil { + return err + } + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + return cmd +} + +// GetCmdConnectionOpenConfirm defines the command to initialize a connection on +// chain A with a given counterparty chain B +func GetCmdConnectionOpenConfirm(storeKey string, cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "open-confirm [connection-id] [path/to/proof_ack.json]", + Short: "confirm to chain B that connection is open on chain A", + Long: strings.TrimSpace( + fmt.Sprintf(`confirm to chain B that connection is open on chain A: + +Example: +$ %s tx ibc connection open-confirm [connection-id] [path/to/proof_ack.json] + `, version.ClientName), + ), + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContext(). + WithCodec(cdc). + WithHeight(viper.GetInt64(flags.FlagHeight)) + + connectionID := args[0] + + proofBz, err := ioutil.ReadFile(args[1]) + if err != nil { + return err + } + + var proofAck commitment.Proof + if err := cdc.UnmarshalJSON(proofBz, &proofAck); err != nil { + return err + } + + proofHeight := uint64(cliCtx.Height) + + msg := types.NewMsgConnectionOpenConfirm( + connectionID, proofAck, proofHeight, cliCtx.GetFromAddress(), + ) + + if err := msg.ValidateBasic(); err != nil { + return err + } + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + return cmd +} + +// lastHeight util function to get the consensus height from the node +func lastHeight(cliCtx context.CLIContext) (uint64, error) { + node, err := cliCtx.GetNode() + if err != nil { + return 0, err + } + + info, err := node.ABCIInfo() + if err != nil { + return 0, err + } + + return uint64(info.Response.LastBlockHeight), nil +} + +func GetCmdHandshakeState(storeKey string, cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "handshake [conn-id-chain-1] [client-id-chain-1] [path-chain-1] [conn-id-chain-2] [client-id-chain-2] [path-chain-2] ", + Short: "initiate connection handshake between two chains", + Args: cobra.ExactArgs(6), + RunE: func(cmd *cobra.Command, args []string) error { + + viper.Set(flags.FlagProve, true) + + // --chain-id values for each chain + cid1 := viper.GetString(flags.FlagChainID) + cid2 := viper.GetString(FlagChainID2) + + // --from values for each wallet + from1 := viper.GetString(FlagFrom1) + from2 := viper.GetString(FlagFrom2) + + // --node values for each RPC + rpc1 := viper.GetString(FlagNode1) + rpc2 := viper.GetString(FlagNode2) + + // ibc connection-id for each chain + connID1 := args[0] + connID2 := args[3] + + // ibc client-id for each chain + clientID1 := args[1] + clientID2 := args[4] + + // Get default version + version := types.GetCompatibleVersions()[0] + + // Create txbldr, clictx, querier for cid1 + viper.Set(flags.FlagChainID, cid1) + txBldr1 := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + ctx1 := context.NewCLIContextIBC(from1, cid1, rpc1).WithCodec(cdc). + WithBroadcastMode(flags.BroadcastBlock) + + // Create txbldr, clictx, querier for cid1 + viper.Set(flags.FlagChainID, cid2) + txBldr2 := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + ctx2 := context.NewCLIContextIBC(from2, cid2, rpc2).WithCodec(cdc). + WithBroadcastMode(flags.BroadcastBlock) + + // read in path for cid1 + path1, err := parsePath(ctx1.Codec, args[2]) + if err != nil { + return err + } + + // read in path for cid2 + path2, err := parsePath(ctx1.Codec, args[5]) + if err != nil { + return err + } + + viper.Set(flags.FlagChainID, cid1) + msgOpenInit := types.NewMsgConnectionOpenInit( + connID1, clientID1, connID2, clientID2, + path2, ctx1.GetFromAddress(), + ) + + if err := msgOpenInit.ValidateBasic(); err != nil { + return err + } + + err = utils.CompleteAndBroadcastTxCLI(txBldr1, ctx1, []sdk.Msg{msgOpenInit}) + if err != nil { + return err + } + + // Another block has to be passed after msgOpenInit is committed + // to retrieve the correct proofs + // TODO: Modify this to actually check two blocks being processed, and + // remove hardcoding this to 8 seconds. + time.Sleep(8 * time.Second) + + header, err := clientutils.GetTendermintHeader(ctx1) + if err != nil { + return err + } + + // Create and send msgUpdateClient + viper.Set(flags.FlagChainID, cid2) + msgUpdateClient := clienttypes.NewMsgUpdateClient(clientID2, header, ctx2.GetFromAddress()) + + if err := msgUpdateClient.ValidateBasic(); err != nil { + return err + } + + err = utils.CompleteAndBroadcastTxCLI(txBldr2, ctx2, []sdk.Msg{msgUpdateClient}) + if err != nil { + return err + } + fmt.Printf(" [OK] client(%v)\n", clientID1) + + // Fetch proofs from cid1 + viper.Set(flags.FlagChainID, cid1) + proofs, err := queryProofs(ctx1.WithHeight(header.Height-1), connID1, storeKey) + if err != nil { + return err + } + + csProof, err := clientutils.QueryConsensusStateProof(ctx1.WithHeight(header.Height-1), clientID1) + if err != nil { + return err + } + + // Create and send msgOpenTry + viper.Set(flags.FlagChainID, cid2) + msgOpenTry := types.NewMsgConnectionOpenTry(connID2, clientID2, connID1, clientID1, path1, []string{version}, proofs.Proof, csProof.Proof, uint64(header.Height), uint64(header.Height), ctx2.GetFromAddress()) + + if err := msgOpenTry.ValidateBasic(); err != nil { + return err + } + + err = utils.CompleteAndBroadcastTxCLI(txBldr2, ctx2, []sdk.Msg{msgOpenTry}) + if err != nil { + return err + } + + fmt.Printf(" [OK] client(%v) connection(%v)\n", clientID2, connID2) + + // Another block has to be passed after msgOpenInit is committed + // to retrieve the correct proofs + // TODO: Modify this to actually check two blocks being processed, and + // remove hardcoding this to 8 seconds. + time.Sleep(8 * time.Second) + + header, err = clientutils.GetTendermintHeader(ctx2) + if err != nil { + return err + } + + // Update the client for cid2 on cid1 + viper.Set(flags.FlagChainID, cid1) + msgUpdateClient = clienttypes.NewMsgUpdateClient(clientID1, header, ctx1.GetFromAddress()) + + if err := msgUpdateClient.ValidateBasic(); err != nil { + return err + } + + err = utils.CompleteAndBroadcastTxCLI(txBldr1, ctx1, []sdk.Msg{msgUpdateClient}) + if err != nil { + return err + } + fmt.Printf(" [OK] client(%v)\n", clientID2) + + // Fetch proofs from cid2 + viper.Set(flags.FlagChainID, cid2) + proofs, err = queryProofs(ctx2.WithHeight(header.Height-1), connID2, storeKey) + if err != nil { + return err + } + + csProof, err = clientutils.QueryConsensusStateProof(ctx2.WithHeight(header.Height-1), clientID2) + if err != nil { + return err + } + + // Create and send msgOpenAck + viper.Set(flags.FlagChainID, cid1) + msgOpenAck := types.NewMsgConnectionOpenAck(connID1, proofs.Proof, csProof.Proof, uint64(header.Height), uint64(header.Height), version, ctx1.GetFromAddress()) + + if err := msgOpenAck.ValidateBasic(); err != nil { + return err + } + + err = utils.CompleteAndBroadcastTxCLI(txBldr1, ctx1, []sdk.Msg{msgOpenAck}) + if err != nil { + return err + } + fmt.Printf(" [OK] connection(%v)\n", connID1) + + // Another block has to be passed after msgOpenInit is committed + // to retrieve the correct proofs + // TODO: Modify this to actually check two blocks being processed, and + // remove hardcoding this to 8 seconds. + time.Sleep(8 * time.Second) + + header, err = clientutils.GetTendermintHeader(ctx1) + if err != nil { + return err + } + + // Update client for cid1 on cid2 + viper.Set(flags.FlagChainID, cid2) + msgUpdateClient = clienttypes.NewMsgUpdateClient(clientID2, header, ctx2.GetFromAddress()) + + if err := msgUpdateClient.ValidateBasic(); err != nil { + return err + } + + err = utils.CompleteAndBroadcastTxCLI(txBldr2, ctx2, []sdk.Msg{msgUpdateClient}) + if err != nil { + return err + } + fmt.Printf(" [OK] client(%v)\n", clientID1) + + // Fetch proof from cid1 + viper.Set(flags.FlagChainID, cid1) + proofs, err = queryProofs(ctx1.WithHeight(header.Height-1), connID1, storeKey) + if err != nil { + return err + } + + // Create and send msgOpenConfirm + viper.Set(flags.FlagChainID, cid2) + msgOpenConfirm := types.NewMsgConnectionOpenConfirm(connID2, proofs.Proof, uint64(header.Height), ctx2.GetFromAddress()) + + if err := msgOpenConfirm.ValidateBasic(); err != nil { + return err + } + + err = utils.CompleteAndBroadcastTxCLI(txBldr2, ctx2, []sdk.Msg{msgOpenConfirm}) + if err != nil { + return err + } + fmt.Printf(" [OK] connection(%v)\n", connID2) + + return nil + }, + } + + cmd.Flags().String(FlagNode1, "tcp://localhost:26657", "RPC port for the first chain") + cmd.Flags().String(FlagNode2, "tcp://localhost:26657", "RPC port for the second chain") + cmd.Flags().String(FlagFrom1, "", "key in local keystore for first chain") + cmd.Flags().String(FlagFrom2, "", "key in local keystore for second chain") + cmd.Flags().String(FlagChainID2, "", "chain-id for the second chain") + + cmd.MarkFlagRequired(FlagNode1) + cmd.MarkFlagRequired(FlagNode2) + cmd.MarkFlagRequired(FlagFrom1) + cmd.MarkFlagRequired(FlagFrom2) + cmd.MarkFlagRequired(FlagChainID2) + + return cmd +} + +func queryProofs(ctx client.CLIContext, connectionID string, queryRoute string) (types.ConnectionResponse, error) { + + var connRes types.ConnectionResponse + + req := abci.RequestQuery{ + Path: "store/ibc/key", + Data: []byte(fmt.Sprintf("connections/%s", connectionID)), + Prove: true, + } + res, err := ctx.QueryABCI(req) + if err != nil { + return connRes, err + } + + var connection types.ConnectionEnd + if err := ctx.Codec.UnmarshalBinaryLengthPrefixed(res.Value, &connection); err != nil { + return connRes, err + } + return types.NewConnectionResponse(connectionID, connection, res.Proof, res.Height), nil +} + +func parsePath(cdc *codec.Codec, arg string) (commitment.Prefix, error) { + var path commitment.Prefix + if err := cdc.UnmarshalJSON([]byte(arg), &path); err != nil { + fmt.Fprintf(os.Stderr, "failed to unmarshall input into struct, checking for file...") + contents, err := ioutil.ReadFile(arg) + if err != nil { + return path, errors.Wrap(err, "error opening path file") + } + if err := cdc.UnmarshalJSON(contents, &path); err != nil { + return path, errors.Wrap(err, "error unmarshalling path file") + } + } + return path, nil +} diff --git a/x/ibc/03-connection/handler.go b/x/ibc/03-connection/handler.go new file mode 100644 index 00000000000..854436eadd6 --- /dev/null +++ b/x/ibc/03-connection/handler.go @@ -0,0 +1,102 @@ +package connection + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/ibc/03-connection/keeper" + "github.com/cosmos/cosmos-sdk/x/ibc/03-connection/types" +) + +// HandleMsgConnectionOpenInit defines the sdk.Handler for MsgConnectionOpenInit +func HandleMsgConnectionOpenInit(ctx sdk.Context, k keeper.Keeper, msg types.MsgConnectionOpenInit) sdk.Result { + err := k.ConnOpenInit(ctx, msg.ConnectionID, msg.ClientID, msg.Counterparty) + if err != nil { + return sdk.ResultFromError(err) + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeConnectionOpenInit, + sdk.NewAttribute(types.AttributeKeyConnectionID, msg.ConnectionID), + sdk.NewAttribute(types.AttributeKeyCounterpartyClientID, msg.Counterparty.ClientID), + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Signer.String()), + ), + }) + + return sdk.Result{Events: ctx.EventManager().Events()} +} + +// HandleMsgConnectionOpenTry defines the sdk.Handler for MsgConnectionOpenTry +func HandleMsgConnectionOpenTry(ctx sdk.Context, k keeper.Keeper, msg types.MsgConnectionOpenTry) sdk.Result { + err := k.ConnOpenTry( + ctx, msg.ConnectionID, msg.Counterparty, msg.ClientID, + msg.CounterpartyVersions, msg.ProofInit, msg.ProofConsensus, msg.ProofHeight, msg.ConsensusHeight) + if err != nil { + return sdk.ResultFromError(err) + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeConnectionOpenTry, + sdk.NewAttribute(types.AttributeKeyConnectionID, msg.ConnectionID), + sdk.NewAttribute(types.AttributeKeyCounterpartyClientID, msg.Counterparty.ClientID), + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Signer.String()), + ), + }) + + return sdk.Result{Events: ctx.EventManager().Events()} +} + +// HandleMsgConnectionOpenAck defines the sdk.Handler for MsgConnectionOpenAck +func HandleMsgConnectionOpenAck(ctx sdk.Context, k keeper.Keeper, msg types.MsgConnectionOpenAck) sdk.Result { + err := k.ConnOpenAck( + ctx, msg.ConnectionID, msg.Version, msg.ProofTry, msg.ProofConsensus, + msg.ProofHeight, msg.ConsensusHeight, + ) + if err != nil { + return sdk.ResultFromError(err) + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeConnectionOpenAck, + sdk.NewAttribute(types.AttributeKeyConnectionID, msg.ConnectionID), + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Signer.String()), + ), + }) + + return sdk.Result{Events: ctx.EventManager().Events()} +} + +// HandleMsgConnectionOpenConfirm defines the sdk.Handler for MsgConnectionOpenConfirm +func HandleMsgConnectionOpenConfirm(ctx sdk.Context, k keeper.Keeper, msg types.MsgConnectionOpenConfirm) sdk.Result { + err := k.ConnOpenConfirm(ctx, msg.ConnectionID, msg.ProofAck, msg.ProofHeight) + if err != nil { + return sdk.ResultFromError(err) + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeConnectionOpenConfirm, + sdk.NewAttribute(types.AttributeKeyConnectionID, msg.ConnectionID), + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Signer.String()), + ), + }) + + return sdk.Result{Events: ctx.EventManager().Events()} +} diff --git a/x/ibc/03-connection/keeper/handshake.go b/x/ibc/03-connection/keeper/handshake.go new file mode 100644 index 00000000000..21a005b12c9 --- /dev/null +++ b/x/ibc/03-connection/keeper/handshake.go @@ -0,0 +1,256 @@ +package keeper + +import ( + "errors" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/ibc/03-connection/types" + commitment "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment" +) + +// ConnOpenInit initialises a connection attempt on chain A. +// +// NOTE: Identifiers are checked on msg validation. +func (k Keeper) ConnOpenInit( + ctx sdk.Context, + connectionID, // identifier + clientID string, + counterparty types.Counterparty, // desiredCounterpartyConnectionIdentifier, counterpartyPrefix, counterpartyClientIdentifier +) error { + _, found := k.GetConnection(ctx, connectionID) + if found { + return sdkerrors.Wrap(types.ErrConnectionExists(k.codespace, connectionID), "cannot initialize connection") + } + + // connection defines chain A's ConnectionEnd + connection := types.NewConnectionEnd(types.INIT, clientID, counterparty, types.GetCompatibleVersions()) + k.SetConnection(ctx, connectionID, connection) + + err := k.addConnectionToClient(ctx, clientID, connectionID) + if err != nil { + sdkerrors.Wrap(err, "cannot initialize connection") + } + + k.Logger(ctx).Info(fmt.Sprintf("connection %s state updated: NONE -> INIT", connectionID)) + return nil +} + +// ConnOpenTry relays notice of a connection attempt on chain A to chain B (this +// code is executed on chain B). +// +// NOTE: +// - Here chain A acts as the counterparty +// - Identifiers are checked on msg validation +func (k Keeper) ConnOpenTry( + ctx sdk.Context, + connectionID string, // desiredIdentifier + counterparty types.Counterparty, // counterpartyConnectionIdentifier, counterpartyPrefix and counterpartyClientIdentifier + clientID string, + counterpartyVersions []string, + proofInit commitment.ProofI, + proofConsensus commitment.ProofI, + proofHeight uint64, + consensusHeight uint64, +) error { + // XXX: blocked by #5078 + /* + if consensusHeight > uint64(ctx.BlockHeight()) { + return errors.New("invalid consensus height") // TODO: sdk.Error + } + + expectedConsensusState, found := k.clientKeeper.GetConsensusState(ctx, clientID) + if !found { + return errors.New("client consensus state not found") // TODO: use ICS02 error + } + */ + + // expectedConn defines Chain A's ConnectionEnd + // NOTE: chain A's counterparty is chain B (i.e where this code is executed) + prefix := k.GetCommitmentPrefix() + expectedCounterparty := types.NewCounterparty(clientID, connectionID, prefix) + expectedConn := types.NewConnectionEnd(types.INIT, counterparty.ClientID, expectedCounterparty, counterpartyVersions) + + // chain B picks a version from Chain A's available versions that is compatible + // with the supported IBC versions + version := types.PickVersion(counterpartyVersions, types.GetCompatibleVersions()) + + // connection defines chain B's ConnectionEnd + connection := types.NewConnectionEnd(types.NONE, clientID, counterparty, []string{version}) + expConnBz, err := k.cdc.MarshalBinaryLengthPrefixed(expectedConn) + if err != nil { + return err + } + + ok := k.VerifyMembership( + ctx, connection, proofHeight, proofInit, + types.ConnectionPath(counterparty.ConnectionID), expConnBz, + ) + if !ok { + return errors.New("couldn't verify connection membership on counterparty's client") // TODO: sdk.Error + } + + // XXX: blocked by #5078 + /* + expConsStateBz, err := k.cdc.MarshalBinaryLengthPrefixed(expectedConsensusState) + if err != nil { + return err + } + + ok = k.VerifyMembership( + ctx, connection, proofHeight, proofConsensus, + clienttypes.ConsensusStatePath(counterparty.ClientID), expConsStateBz, + ) + if !ok { + fmt.Sprintf("couldn't verify consensus state membership on counterparty's client\n") + return errors.New("couldn't verify consensus state membership on counterparty's client") // TODO: sdk.Error + } + + */ + + _, found := k.GetConnection(ctx, connectionID) + if found { + return sdkerrors.Wrap(types.ErrConnectionExists(k.codespace, connectionID), "cannot relay connection attempt") + } + + connection.State = types.TRYOPEN + err = k.addConnectionToClient(ctx, clientID, connectionID) + if err != nil { + return sdkerrors.Wrap(err, "cannot relay connection attempt") + } + + k.SetConnection(ctx, connectionID, connection) + k.Logger(ctx).Info(fmt.Sprintf("connection %s state updated: NONE -> TRYOPEN ", connectionID)) + return nil +} + +// ConnOpenAck relays acceptance of a connection open attempt from chain B back +// to chain A (this code is executed on chain A). +// +// NOTE: Identifiers are checked on msg validation. +func (k Keeper) ConnOpenAck( + ctx sdk.Context, + connectionID string, + version string, + proofTry commitment.ProofI, + proofConsensus commitment.ProofI, + proofHeight uint64, + consensusHeight uint64, +) error { + // XXX: blocked by #5078 + /* + if consensusHeight > uint64(ctx.BlockHeight()) { + return errors.New("invalid consensus height") // TODO: sdk.Error + } + */ + connection, found := k.GetConnection(ctx, connectionID) + if !found { + return sdkerrors.Wrap(types.ErrConnectionNotFound(k.codespace, connectionID), "cannot relay ACK of open attempt") + } + + if connection.State != types.INIT { + return types.ErrInvalidConnectionState( + k.codespace, + fmt.Sprintf("connection state is not INIT (got %s)", connection.State.String()), + ) + } + + if types.LatestVersion(connection.Versions) != version { + return types.ErrInvalidVersion( + k.codespace, + fmt.Sprintf("connection version does't match provided one (%s ≠ %s)", types.LatestVersion(connection.Versions), version), + ) + } + + // XXX: blocked by #5078 + /* + expectedConsensusState, found := k.clientKeeper.GetConsensusState(ctx, connection.ClientID) + if !found { + return errors.New("client consensus state not found") // TODO: use ICS02 error + } + */ + prefix := k.GetCommitmentPrefix() + expectedCounterparty := types.NewCounterparty(connection.ClientID, connectionID, prefix) + expectedConn := types.NewConnectionEnd(types.TRYOPEN, connection.Counterparty.ClientID, expectedCounterparty, []string{version}) + + expConnBz, err := k.cdc.MarshalBinaryLengthPrefixed(expectedConn) + if err != nil { + return err + } + + ok := k.VerifyMembership( + ctx, connection, proofHeight, proofTry, + types.ConnectionPath(connection.Counterparty.ConnectionID), expConnBz, + ) + if !ok { + return errors.New("couldn't verify connection membership on counterparty's client") // TODO: sdk.Error + } + + // XXX: blocked by #5078 + /* + expConsStateBz, err := k.cdc.MarshalBinaryLengthPrefixed(expectedConsensusState) + if err != nil { + return err + } + + ok = k.VerifyMembership( + ctx, connection, proofHeight, proofConsensus, + clienttypes.ConsensusStatePath(connection.Counterparty.ClientID), expConsStateBz, + ) + if !ok { + return errors.New("couldn't verify consensus state membership on counterparty's client") // TODO: sdk.Error + } + + */ + connection.State = types.OPEN + connection.Versions = []string{version} + k.SetConnection(ctx, connectionID, connection) + k.Logger(ctx).Info(fmt.Sprintf("connection %s state updated: INIT -> OPEN ", connectionID)) + return nil +} + +// ConnOpenConfirm confirms opening of a connection on chain A to chain B, after +// which the connection is open on both chains (this code is executed on chain B). +// +// NOTE: Identifiers are checked on msg validation. +func (k Keeper) ConnOpenConfirm( + ctx sdk.Context, + connectionID string, + proofAck commitment.ProofI, + proofHeight uint64, +) error { + connection, found := k.GetConnection(ctx, connectionID) + if !found { + return sdkerrors.Wrap(types.ErrConnectionNotFound(k.codespace, connectionID), "cannot relay ACK of open attempt") + } + + if connection.State != types.TRYOPEN { + return types.ErrInvalidConnectionState( + k.codespace, + fmt.Sprintf("connection state is not TRYOPEN (got %s)", connection.State.String()), + ) + } + + prefix := k.GetCommitmentPrefix() + expectedCounterparty := types.NewCounterparty(connection.ClientID, connectionID, prefix) + expectedConn := types.NewConnectionEnd(types.OPEN, connection.Counterparty.ClientID, expectedCounterparty, connection.Versions) + + expConnBz, err := k.cdc.MarshalBinaryLengthPrefixed(expectedConn) + if err != nil { + return err + } + + ok := k.VerifyMembership( + ctx, connection, proofHeight, proofAck, + types.ConnectionPath(connection.Counterparty.ConnectionID), expConnBz, + ) + if !ok { + return types.ErrInvalidCounterpartyConnection(k.codespace) + } + + connection.State = types.OPEN + k.SetConnection(ctx, connectionID, connection) + k.Logger(ctx).Info(fmt.Sprintf("connection %s state updated: TRYOPEN -> OPEN ", connectionID)) + return nil +} diff --git a/x/ibc/03-connection/keeper/keeper.go b/x/ibc/03-connection/keeper/keeper.go new file mode 100644 index 00000000000..1488c6c03ec --- /dev/null +++ b/x/ibc/03-connection/keeper/keeper.go @@ -0,0 +1,171 @@ +package keeper + +import ( + "fmt" + + "github.com/tendermint/tendermint/libs/log" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/ibc/03-connection/types" + commitment "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment" + host "github.com/cosmos/cosmos-sdk/x/ibc/24-host" + ibctypes "github.com/cosmos/cosmos-sdk/x/ibc/types" +) + +// Keeper defines the IBC connection keeper +type Keeper struct { + storeKey sdk.StoreKey + cdc *codec.Codec + codespace sdk.CodespaceType + prefix []byte // prefix bytes for accessing the store + + clientKeeper types.ClientKeeper +} + +// NewKeeper creates a new IBC connection Keeper instance +func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, codespace sdk.CodespaceType, ck types.ClientKeeper) Keeper { + return Keeper{ + storeKey: key, + cdc: cdc, + codespace: sdk.CodespaceType(fmt.Sprintf("%s/%s", codespace, types.DefaultCodespace)), // "ibc/connection", + prefix: []byte{}, + // prefix: []byte(types.SubModuleName + "/"), // "connection/" + clientKeeper: ck, + } +} + +// Logger returns a module-specific logger. +func (k Keeper) Logger(ctx sdk.Context) log.Logger { + return ctx.Logger().With("module", fmt.Sprintf("x/%s/%s", ibctypes.ModuleName, types.SubModuleName)) +} + +// GetCommitmentPrefix returns the IBC connection store prefix as a commitment +// Prefix +func (k Keeper) GetCommitmentPrefix() commitment.PrefixI { + return commitment.NewPrefix([]byte(k.storeKey.Name())) +} + +// GetConnection returns a connection with a particular identifier +func (k Keeper) GetConnection(ctx sdk.Context, connectionID string) (types.ConnectionEnd, bool) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), k.prefix) + bz := store.Get(types.KeyConnection(connectionID)) + if bz == nil { + return types.ConnectionEnd{}, false + } + + var connection types.ConnectionEnd + k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &connection) + return connection, true +} + +// SetConnection sets a connection to the store +func (k Keeper) SetConnection(ctx sdk.Context, connectionID string, connection types.ConnectionEnd) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), k.prefix) + bz := k.cdc.MustMarshalBinaryLengthPrefixed(connection) + store.Set(types.KeyConnection(connectionID), bz) +} + +// GetClientConnectionPaths returns all the connection paths stored under a +// particular client +func (k Keeper) GetClientConnectionPaths(ctx sdk.Context, clientID string) ([]string, bool) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), k.prefix) + bz := store.Get(types.KeyClientConnections(clientID)) + if bz == nil { + return nil, false + } + + var paths []string + k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &paths) + return paths, true +} + +// SetClientConnectionPaths sets the connections paths for client +func (k Keeper) SetClientConnectionPaths(ctx sdk.Context, clientID string, paths []string) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), k.prefix) + bz := k.cdc.MustMarshalBinaryLengthPrefixed(paths) + store.Set(types.KeyClientConnections(clientID), bz) +} + +// addConnectionToClient is used to add a connection identifier to the set of +// connections associated with a client. +// +// CONTRACT: client must already exist +func (k Keeper) addConnectionToClient(ctx sdk.Context, clientID, connectionID string) sdk.Error { + conns, found := k.GetClientConnectionPaths(ctx, clientID) + if !found { + conns = []string{} + } + + conns = append(conns, connectionID) + k.SetClientConnectionPaths(ctx, clientID, conns) + return nil +} + +// removeConnectionFromClient is used to remove a connection identifier from the +// set of connections associated with a client. +// +// CONTRACT: client must already exist +func (k Keeper) removeConnectionFromClient(ctx sdk.Context, clientID, connectionID string) sdk.Error { + conns, found := k.GetClientConnectionPaths(ctx, clientID) + if !found { + return types.ErrClientConnectionPathsNotFound(k.codespace, clientID) + } + + conns, ok := host.RemovePath(conns, connectionID) + if !ok { + return types.ErrConnectionPath(k.codespace) + } + + k.SetClientConnectionPaths(ctx, clientID, conns) + return nil +} + +// VerifyMembership helper function for state membership verification +func (k Keeper) VerifyMembership( + ctx sdk.Context, + connection types.ConnectionEnd, + height uint64, + proof commitment.ProofI, + pathStr string, + value []byte, +) bool { + // FIXME: commented out for demo + /* + clientState, found := k.clientKeeper.GetClientState(ctx, connection.ClientID) + if !found { + return false + } + */ + path, err := commitment.ApplyPrefix(connection.Counterparty.Prefix, pathStr) + if err != nil { + return false + } + + return k.clientKeeper.VerifyMembership(ctx, connection.ClientID, height, proof, path, value) +} + +// VerifyNonMembership helper function for state non-membership verification +func (k Keeper) VerifyNonMembership( + ctx sdk.Context, + connection types.ConnectionEnd, + height uint64, + proof commitment.ProofI, + pathStr string, +) bool { + // FIXME: commented out for demo + /* + clientState, found := k.clientKeeper.GetClientState(ctx, connection.ClientID) + if !found { + return false + } + */ + + path, err := commitment.ApplyPrefix(connection.Counterparty.Prefix, pathStr) + if err != nil { + return false + } + + return k.clientKeeper.VerifyNonMembership(ctx, connection.ClientID, height, proof, path) +} diff --git a/x/ibc/03-connection/keeper/querier.go b/x/ibc/03-connection/keeper/querier.go new file mode 100644 index 00000000000..85bb02559e7 --- /dev/null +++ b/x/ibc/03-connection/keeper/querier.go @@ -0,0 +1,54 @@ +package keeper + +import ( + "fmt" + + abci "github.com/tendermint/tendermint/abci/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/ibc/03-connection/types" +) + +// QuerierConnection defines the sdk.Querier to query a connection end +func QuerierConnection(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, sdk.Error) { + var params types.QueryConnectionParams + + err := types.SubModuleCdc.UnmarshalJSON(req.Data, ¶ms) + if err != nil { + return nil, sdk.ErrInternal(fmt.Sprintf("failed to parse params: %s", err)) + } + + connection, found := k.GetConnection(ctx, params.ConnectionID) + if !found { + return nil, types.ErrConnectionNotFound(k.codespace, params.ConnectionID) + } + + bz, err := types.SubModuleCdc.MarshalJSON(connection) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + } + + return bz, nil +} + +// QuerierClientConnections defines the sdk.Querier to query the client connections +func QuerierClientConnections(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, sdk.Error) { + var params types.QueryClientConnectionsParams + + err := types.SubModuleCdc.UnmarshalJSON(req.Data, ¶ms) + if err != nil { + return nil, sdk.ErrInternal(fmt.Sprintf("failed to parse params: %s", err)) + } + + clientConnectionPaths, found := k.GetClientConnectionPaths(ctx, params.ClientID) + if !found { + return nil, types.ErrClientConnectionPathsNotFound(k.codespace, params.ClientID) + } + + bz, err := types.SubModuleCdc.MarshalJSON(clientConnectionPaths) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + } + + return bz, nil +} diff --git a/x/ibc/03-connection/module.go b/x/ibc/03-connection/module.go new file mode 100644 index 00000000000..713ed04d6c6 --- /dev/null +++ b/x/ibc/03-connection/module.go @@ -0,0 +1,25 @@ +package connection + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/x/ibc/03-connection/client/cli" +) + +// Name returns the IBC connection ICS name +func Name() string { + return SubModuleName +} + +// GetTxCmd returns the root tx command for the IBC connections. +func GetTxCmd(cdc *codec.Codec, storeKey string) *cobra.Command { + return cli.GetTxCmd(fmt.Sprintf("%s/%s", storeKey, SubModuleName), cdc) +} + +// GetQueryCmd returns no root query command for the IBC connections. +func GetQueryCmd(cdc *codec.Codec, queryRoute string) *cobra.Command { + return cli.GetQueryCmd(fmt.Sprintf("%s/%s", queryRoute, SubModuleName), cdc) +} diff --git a/x/ibc/03-connection/types/codec.go b/x/ibc/03-connection/types/codec.go new file mode 100644 index 00000000000..23fb04e37a8 --- /dev/null +++ b/x/ibc/03-connection/types/codec.go @@ -0,0 +1,21 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/codec" +) + +// SubModuleCdc defines the IBC connection codec. +var SubModuleCdc = codec.New() + +// RegisterCodec registers the IBC connection types +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterConcrete(MsgConnectionOpenInit{}, "ibc/connection/MsgConnectionOpenInit", nil) + cdc.RegisterConcrete(MsgConnectionOpenTry{}, "ibc/connection/MsgConnectionOpenTry", nil) + cdc.RegisterConcrete(MsgConnectionOpenAck{}, "ibc/connection/MsgConnectionOpenAck", nil) + cdc.RegisterConcrete(MsgConnectionOpenConfirm{}, "ibc/connection/MsgConnectionOpenConfirm", nil) + cdc.RegisterConcrete(ConnectionEnd{}, "ibc/connection/ConnectionEnd", nil) +} + +func init() { + RegisterCodec(SubModuleCdc) +} diff --git a/x/ibc/03-connection/types/connection.go b/x/ibc/03-connection/types/connection.go new file mode 100644 index 00000000000..07eb0a25c6a --- /dev/null +++ b/x/ibc/03-connection/types/connection.go @@ -0,0 +1,143 @@ +package types + +import ( + "encoding/json" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + commitment "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment" + host "github.com/cosmos/cosmos-sdk/x/ibc/24-host" +) + +// ICS03 - Connection Data Structures as defined in https://github.com/cosmos/ics/tree/master/spec/ics-003-connection-semantics#data-structures + +// ConnectionEnd defines a stateful object on a chain connected to another separate +// one. +// NOTE: there must only be 2 defined ConnectionEnds to stablish a connection +// between two chains. +type ConnectionEnd struct { + State State `json:"state" yaml:"state"` + ClientID string `json:"client_id" yaml:"client_id"` + + // Counterparty chain associated with this connection. + Counterparty Counterparty `json:"counterparty" yaml:"counterparty"` + // Version is utilised to determine encodings or protocols for channels or + // packets utilising this connection. + Versions []string `json:"versions" yaml:"versions"` +} + +// NewConnectionEnd creates a new ConnectionEnd instance. +func NewConnectionEnd(state State, clientID string, counterparty Counterparty, versions []string) ConnectionEnd { + return ConnectionEnd{ + State: state, + ClientID: clientID, + Counterparty: counterparty, + Versions: versions, + } +} + +// TODO: create a custom JSON marshaler + +// Counterparty defines the counterparty chain associated with a connection end. +type Counterparty struct { + ClientID string `json:"client_id" yaml:"client_id"` + ConnectionID string `json:"connection_id" yaml:"connection_id"` + Prefix commitment.PrefixI `json:"prefix" yaml:"prefix"` +} + +// NewCounterparty creates a new Counterparty instance. +func NewCounterparty(clientID, connectionID string, prefix commitment.PrefixI) Counterparty { + return Counterparty{ + ClientID: clientID, + ConnectionID: connectionID, + Prefix: prefix, + } +} + +// ValidateBasic performs a basic validation check of the identifiers and prefix +func (c Counterparty) ValidateBasic() sdk.Error { + if err := host.DefaultConnectionIdentifierValidator(c.ConnectionID); err != nil { + return sdk.NewError(host.IBCCodeSpace, 1, fmt.Sprintf("invalid counterparty connection ID: %s", err.Error())) + } + if err := host.DefaultClientIdentifierValidator(c.ClientID); err != nil { + return sdk.NewError(host.IBCCodeSpace, 1, fmt.Sprintf("invalid counterparty client ID: %s", err.Error())) + } + if c.Prefix == nil || len(c.Prefix.Bytes()) == 0 { + return ErrInvalidCounterparty(DefaultCodespace, "invalid counterparty prefix") + } + return nil +} + +// State defines the state of a connection between two disctinct +// chains +type State byte + +// available connection states +const ( + NONE State = iota // default State + INIT + TRYOPEN + OPEN +) + +// string representation of the connection states +const ( + StateNone string = "NONE" + StateInit string = "INIT" + StateTryOpen string = "TRYOPEN" + StateOpen string = "OPEN" +) + +// String implements the Stringer interface +func (cs State) String() string { + switch cs { + case NONE: + return StateNone + case INIT: + return StateInit + case TRYOPEN: + return StateTryOpen + case OPEN: + return StateOpen + default: + return "" + } +} + +// StateFromString parses a string into a connection state +func StateFromString(state string) (State, error) { + switch state { + case StateNone: + return NONE, nil + case StateInit: + return INIT, nil + case StateTryOpen: + return TRYOPEN, nil + case StateOpen: + return OPEN, nil + default: + return NONE, fmt.Errorf("'%s' is not a valid connection state", state) + } +} + +// MarshalJSON marshal to JSON using string. +func (cs State) MarshalJSON() ([]byte, error) { + return json.Marshal(cs.String()) +} + +// UnmarshalJSON decodes from JSON assuming Bech32 encoding. +func (cs *State) UnmarshalJSON(data []byte) error { + var s string + err := json.Unmarshal(data, &s) + if err != nil { + return err + } + + bz2, err := StateFromString(s) + if err != nil { + return err + } + + *cs = bz2 + return nil +} diff --git a/x/ibc/03-connection/types/errors.go b/x/ibc/03-connection/types/errors.go new file mode 100644 index 00000000000..b2a2cbe2ad5 --- /dev/null +++ b/x/ibc/03-connection/types/errors.go @@ -0,0 +1,73 @@ +package types + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// connection error codes +const ( + DefaultCodespace sdk.CodespaceType = SubModuleName + + CodeConnectionExists sdk.CodeType = 101 + CodeConnectionNotFound sdk.CodeType = 102 + CodeClientConnectionPathsNotFound sdk.CodeType = 103 + CodeConnectionPath sdk.CodeType = 104 + CodeInvalidCounterpartyConnection sdk.CodeType = 105 + CodeInvalidVersion sdk.CodeType = 106 + CodeInvalidHeight sdk.CodeType = 107 + CodeInvalidConnectionState sdk.CodeType = 108 + CodeInvalidProof sdk.CodeType = 109 + CodeInvalidCounterparty sdk.CodeType = 110 +) + +// ErrConnectionExists implements sdk.Error +func ErrConnectionExists(codespace sdk.CodespaceType, connectionID string) sdk.Error { + return sdk.NewError(codespace, CodeConnectionExists, fmt.Sprintf("connection with ID %s already exists", connectionID)) +} + +// ErrConnectionNotFound implements sdk.Error +func ErrConnectionNotFound(codespace sdk.CodespaceType, connectionID string) sdk.Error { + return sdk.NewError(codespace, CodeConnectionNotFound, fmt.Sprintf("connection with ID %s not found", connectionID)) +} + +// ErrClientConnectionPathsNotFound implements sdk.Error +func ErrClientConnectionPathsNotFound(codespace sdk.CodespaceType, clientID string) sdk.Error { + return sdk.NewError(codespace, CodeClientConnectionPathsNotFound, fmt.Sprintf("client connection paths not found for ID %s", clientID)) +} + +// ErrConnectionPath implements sdk.Error +func ErrConnectionPath(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeConnectionPath, "connection path is not associated to the client") +} + +// ErrInvalidCounterpartyConnection implements sdk.Error +func ErrInvalidCounterpartyConnection(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidCounterpartyConnection, "couldn't verify connection membership on counterparty's client") +} + +// ErrInvalidVersion implements sdk.Error +func ErrInvalidVersion(codespace sdk.CodespaceType, msg string) sdk.Error { + return sdk.NewError(codespace, CodeInvalidVersion, msg) +} + +// ErrInvalidHeight implements sdk.Error +func ErrInvalidHeight(codespace sdk.CodespaceType, msg string) sdk.Error { + return sdk.NewError(codespace, CodeInvalidHeight, msg) +} + +// ErrInvalidConnectionState implements sdk.Error +func ErrInvalidConnectionState(codespace sdk.CodespaceType, msg string) sdk.Error { + return sdk.NewError(codespace, CodeInvalidConnectionState, msg) +} + +// ErrInvalidConnectionProof implements sdk.Error +func ErrInvalidConnectionProof(codespace sdk.CodespaceType, msg string) sdk.Error { + return sdk.NewError(codespace, CodeInvalidProof, msg) +} + +// ErrInvalidCounterparty implements sdk.Error +func ErrInvalidCounterparty(codespace sdk.CodespaceType, msg string) sdk.Error { + return sdk.NewError(codespace, CodeInvalidCounterparty, msg) +} diff --git a/x/ibc/03-connection/types/events.go b/x/ibc/03-connection/types/events.go new file mode 100644 index 00000000000..2ede46db907 --- /dev/null +++ b/x/ibc/03-connection/types/events.go @@ -0,0 +1,23 @@ +package types + +import ( + "fmt" + + ibctypes "github.com/cosmos/cosmos-sdk/x/ibc/types" +) + +// IBC connection events +const ( + AttributeKeyConnectionID = "connection_id" + AttributeKeyCounterpartyClientID = "counterparty_client_id" +) + +// IBC connection events vars +var ( + EventTypeConnectionOpenInit = MsgConnectionOpenInit{}.Type() + EventTypeConnectionOpenTry = MsgConnectionOpenTry{}.Type() + EventTypeConnectionOpenAck = MsgConnectionOpenAck{}.Type() + EventTypeConnectionOpenConfirm = MsgConnectionOpenConfirm{}.Type() + + AttributeValueCategory = fmt.Sprintf("%s_%s", ibctypes.ModuleName, SubModuleName) +) diff --git a/x/ibc/03-connection/types/expected_keepers.go b/x/ibc/03-connection/types/expected_keepers.go new file mode 100644 index 00000000000..1a114f6f837 --- /dev/null +++ b/x/ibc/03-connection/types/expected_keepers.go @@ -0,0 +1,22 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + client "github.com/cosmos/cosmos-sdk/x/ibc/02-client" + clientexported "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported" + commitment "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment" +) + +// ClientKeeper expected account IBC client keeper +type ClientKeeper interface { + GetConsensusState(ctx sdk.Context, clientID string) (clientexported.ConsensusState, bool) + GetClientState(ctx sdk.Context, clientID string) (client.State, bool) + VerifyMembership( + ctx sdk.Context, clientID string, height uint64, + proof commitment.ProofI, path commitment.PathI, value []byte, + ) bool + VerifyNonMembership( + ctx sdk.Context, clientID string, height uint64, + proof commitment.ProofI, path commitment.PathI, + ) bool +} diff --git a/x/ibc/03-connection/types/keys.go b/x/ibc/03-connection/types/keys.go new file mode 100644 index 00000000000..7dd82ae80a2 --- /dev/null +++ b/x/ibc/03-connection/types/keys.go @@ -0,0 +1,41 @@ +package types + +import ( + "fmt" +) + +const ( + // SubModuleName defines the IBC connection name + SubModuleName = "connection" + + // StoreKey is the store key string for IBC connections + StoreKey = SubModuleName + + // RouterKey is the message route for IBC connections + RouterKey = SubModuleName + + // QuerierRoute is the querier route for IBC connections + QuerierRoute = SubModuleName +) + +// The following paths are the keys to the store as defined in https://github.com/cosmos/ics/tree/master/spec/ics-003-connection-semantics#store-paths + +// ConnectionPath defines the path under which connection paths are stored +func ConnectionPath(connectionID string) string { + return fmt.Sprintf("connections/%s", connectionID) +} + +// ClientConnectionsPath defines a reverse mapping from clients to a set of connections +func ClientConnectionsPath(clientID string) string { + return fmt.Sprintf("clients/%s/connections", clientID) +} + +// KeyConnection returns the store key for a particular connection +func KeyConnection(connectionID string) []byte { + return []byte(ConnectionPath(connectionID)) +} + +// KeyClientConnections returns the store key for the connectios of a given client +func KeyClientConnections(clientID string) []byte { + return []byte(ClientConnectionsPath(clientID)) +} diff --git a/x/ibc/03-connection/types/msgs.go b/x/ibc/03-connection/types/msgs.go new file mode 100644 index 00000000000..10ede21b112 --- /dev/null +++ b/x/ibc/03-connection/types/msgs.go @@ -0,0 +1,300 @@ +package types + +import ( + "fmt" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + commitment "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment" + host "github.com/cosmos/cosmos-sdk/x/ibc/24-host" + ibctypes "github.com/cosmos/cosmos-sdk/x/ibc/types" +) + +var _ sdk.Msg = MsgConnectionOpenInit{} + +// MsgConnectionOpenInit defines the msg sent by an account on Chain A to +// initialize a connection with Chain B. +type MsgConnectionOpenInit struct { + ConnectionID string `json:"connection_id"` + ClientID string `json:"client_id"` + Counterparty Counterparty `json:"counterparty"` + Signer sdk.AccAddress `json:"signer"` +} + +// NewMsgConnectionOpenInit creates a new MsgConnectionOpenInit instance +func NewMsgConnectionOpenInit( + connectionID, clientID, counterpartyConnectionID, + counterpartyClientID string, counterpartyPrefix commitment.PrefixI, + signer sdk.AccAddress, +) MsgConnectionOpenInit { + counterparty := NewCounterparty(counterpartyClientID, counterpartyConnectionID, counterpartyPrefix) + return MsgConnectionOpenInit{ + ConnectionID: connectionID, + ClientID: clientID, + Counterparty: counterparty, + Signer: signer, + } +} + +// Route implements sdk.Msg +func (msg MsgConnectionOpenInit) Route() string { + return ibctypes.RouterKey +} + +// Type implements sdk.Msg +func (msg MsgConnectionOpenInit) Type() string { + return "connection_open_init" +} + +// ValidateBasic implements sdk.Msg +func (msg MsgConnectionOpenInit) ValidateBasic() sdk.Error { + if err := host.DefaultConnectionIdentifierValidator(msg.ConnectionID); err != nil { + return sdk.NewError(host.IBCCodeSpace, 1, fmt.Sprintf("invalid connection ID: %s", err.Error())) + } + if err := host.DefaultClientIdentifierValidator(msg.ClientID); err != nil { + return sdk.NewError(host.IBCCodeSpace, 1, fmt.Sprintf("invalid client ID: %s", err.Error())) + } + if msg.Signer.Empty() { + return sdk.ErrInvalidAddress("missing signer address") + } + return msg.Counterparty.ValidateBasic() +} + +// GetSignBytes implements sdk.Msg +func (msg MsgConnectionOpenInit) GetSignBytes() []byte { + return sdk.MustSortJSON(SubModuleCdc.MustMarshalJSON(msg)) +} + +// GetSigners implements sdk.Msg +func (msg MsgConnectionOpenInit) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Signer} +} + +var _ sdk.Msg = MsgConnectionOpenTry{} + +// MsgConnectionOpenTry defines a msg sent by a Relayer to try to open a connection +// on Chain B. +type MsgConnectionOpenTry struct { + ConnectionID string `json:"connection_id"` + ClientID string `json:"client_id"` + Counterparty Counterparty `json:"counterparty"` + CounterpartyVersions []string `json:"counterparty_versions"` + ProofInit commitment.ProofI `json:"proof_init"` // proof of the initialization the connection on Chain A: `none -> INIT` + ProofConsensus commitment.ProofI `json:"proof_consensus"` // proof of client consensus state + ProofHeight uint64 `json:"proof_height"` + ConsensusHeight uint64 `json:"consensus_height"` + Signer sdk.AccAddress `json:"signer"` +} + +// NewMsgConnectionOpenTry creates a new MsgConnectionOpenTry instance +func NewMsgConnectionOpenTry( + connectionID, clientID, counterpartyConnectionID, + counterpartyClientID string, counterpartyPrefix commitment.PrefixI, + counterpartyVersions []string, proofInit, proofConsensus commitment.ProofI, + proofHeight, consensusHeight uint64, signer sdk.AccAddress, +) MsgConnectionOpenTry { + counterparty := NewCounterparty(counterpartyClientID, counterpartyConnectionID, counterpartyPrefix) + return MsgConnectionOpenTry{ + ConnectionID: connectionID, + ClientID: clientID, + Counterparty: counterparty, + CounterpartyVersions: counterpartyVersions, + ProofInit: proofInit, + ProofConsensus: proofConsensus, + ProofHeight: proofHeight, + ConsensusHeight: consensusHeight, + Signer: signer, + } +} + +// Route implements sdk.Msg +func (msg MsgConnectionOpenTry) Route() string { + return ibctypes.RouterKey +} + +// Type implements sdk.Msg +func (msg MsgConnectionOpenTry) Type() string { + return "connection_open_try" +} + +// ValidateBasic implements sdk.Msg +func (msg MsgConnectionOpenTry) ValidateBasic() sdk.Error { + if err := host.DefaultConnectionIdentifierValidator(msg.ConnectionID); err != nil { + return sdk.NewError(host.IBCCodeSpace, 1, fmt.Sprintf("invalid connection ID: %s", err.Error())) + } + if err := host.DefaultClientIdentifierValidator(msg.ClientID); err != nil { + return sdk.NewError(host.IBCCodeSpace, 1, fmt.Sprintf("invalid client ID: %s", err.Error())) + } + if len(msg.CounterpartyVersions) == 0 { + return ErrInvalidVersion(DefaultCodespace, "missing counterparty versions") + } + for _, version := range msg.CounterpartyVersions { + if strings.TrimSpace(version) == "" { + return ErrInvalidVersion(DefaultCodespace, "version can't be blank") + } + } + if msg.ProofInit == nil { + return ErrInvalidConnectionProof(DefaultCodespace, "proof init cannot be nil") + } + if msg.ProofConsensus == nil { + return ErrInvalidConnectionProof(DefaultCodespace, "proof consensus cannot be nil") + } + if msg.ProofHeight == 0 { + return ErrInvalidHeight(DefaultCodespace, "proof height must be > 0") + } + if msg.ConsensusHeight == 0 { + return ErrInvalidHeight(DefaultCodespace, "consensus height must be > 0") + } + if msg.Signer.Empty() { + return sdk.ErrInvalidAddress("missing signer address") + } + return msg.Counterparty.ValidateBasic() +} + +// GetSignBytes implements sdk.Msg +func (msg MsgConnectionOpenTry) GetSignBytes() []byte { + return sdk.MustSortJSON(SubModuleCdc.MustMarshalJSON(msg)) +} + +// GetSigners implements sdk.Msg +func (msg MsgConnectionOpenTry) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Signer} +} + +var _ sdk.Msg = MsgConnectionOpenAck{} + +// MsgConnectionOpenAck defines a msg sent by a Relayer to Chain A to acknowledge +// the change of connection state to TRYOPEN on Chain B. +type MsgConnectionOpenAck struct { + ConnectionID string `json:"connection_id"` + ProofTry commitment.ProofI `json:"proof_try"` // proof for the change of the connection state on Chain B: `none -> TRYOPEN` + ProofConsensus commitment.ProofI `json:"proof_consensus"` // proof of client consensus state + ProofHeight uint64 `json:"proof_height"` + ConsensusHeight uint64 `json:"consensus_height"` + Version string `json:"version"` + Signer sdk.AccAddress `json:"signer"` +} + +// NewMsgConnectionOpenAck creates a new MsgConnectionOpenAck instance +func NewMsgConnectionOpenAck( + connectionID string, proofTry, proofConsensus commitment.ProofI, + proofHeight, consensusHeight uint64, version string, + signer sdk.AccAddress, +) MsgConnectionOpenAck { + return MsgConnectionOpenAck{ + ConnectionID: connectionID, + ProofTry: proofTry, + ProofConsensus: proofConsensus, + ProofHeight: proofHeight, + ConsensusHeight: consensusHeight, + Version: version, + Signer: signer, + } +} + +// Route implements sdk.Msg +func (msg MsgConnectionOpenAck) Route() string { + return ibctypes.RouterKey +} + +// Type implements sdk.Msg +func (msg MsgConnectionOpenAck) Type() string { + return "connection_open_ack" +} + +// ValidateBasic implements sdk.Msg +func (msg MsgConnectionOpenAck) ValidateBasic() sdk.Error { + if err := host.DefaultConnectionIdentifierValidator(msg.ConnectionID); err != nil { + return sdk.NewError(host.IBCCodeSpace, 1, fmt.Sprintf("invalid connection ID: %s", err.Error())) + } + if strings.TrimSpace(msg.Version) == "" { + return ErrInvalidVersion(DefaultCodespace, "version can't be blank") + } + if msg.ProofTry == nil { + return ErrInvalidConnectionProof(DefaultCodespace, "proof try cannot be nil") + } + if msg.ProofConsensus == nil { + return ErrInvalidConnectionProof(DefaultCodespace, "proof consensus cannot be nil") + } + if msg.ProofHeight == 0 { + return ErrInvalidHeight(DefaultCodespace, "proof height must be > 0") + } + if msg.ConsensusHeight == 0 { + return ErrInvalidHeight(DefaultCodespace, "consensus height must be > 0") + } + if msg.Signer.Empty() { + return sdk.ErrInvalidAddress("missing signer address") + } + return nil +} + +// GetSignBytes implements sdk.Msg +func (msg MsgConnectionOpenAck) GetSignBytes() []byte { + return sdk.MustSortJSON(SubModuleCdc.MustMarshalJSON(msg)) +} + +// GetSigners implements sdk.Msg +func (msg MsgConnectionOpenAck) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Signer} +} + +var _ sdk.Msg = MsgConnectionOpenConfirm{} + +// MsgConnectionOpenConfirm defines a msg sent by a Relayer to Chain B to acknowledge +// the change of connection state to OPEN on Chain A. +type MsgConnectionOpenConfirm struct { + ConnectionID string `json:"connection_id"` + ProofAck commitment.ProofI `json:"proof_ack"` // proof for the change of the connection state on Chain A: `INIT -> OPEN` + ProofHeight uint64 `json:"proof_height"` + Signer sdk.AccAddress `json:"signer"` +} + +// NewMsgConnectionOpenConfirm creates a new MsgConnectionOpenConfirm instance +func NewMsgConnectionOpenConfirm( + connectionID string, proofAck commitment.ProofI, proofHeight uint64, signer sdk.AccAddress, +) MsgConnectionOpenConfirm { + return MsgConnectionOpenConfirm{ + ConnectionID: connectionID, + ProofAck: proofAck, + ProofHeight: proofHeight, + Signer: signer, + } +} + +// Route implements sdk.Msg +func (msg MsgConnectionOpenConfirm) Route() string { + return ibctypes.RouterKey +} + +// Type implements sdk.Msg +func (msg MsgConnectionOpenConfirm) Type() string { + return "connection_open_confirm" +} + +// ValidateBasic implements sdk.Msg +func (msg MsgConnectionOpenConfirm) ValidateBasic() sdk.Error { + if err := host.DefaultConnectionIdentifierValidator(msg.ConnectionID); err != nil { + return sdk.NewError(host.IBCCodeSpace, 1, fmt.Sprintf("invalid connection ID: %s", err.Error())) + } + if msg.ProofAck == nil { + return ErrInvalidConnectionProof(DefaultCodespace, "proof ack cannot be nil") + } + if msg.ProofHeight == 0 { + return ErrInvalidHeight(DefaultCodespace, "proof height must be > 0") + } + + if msg.Signer.Empty() { + return sdk.ErrInvalidAddress("missing signer address") + } + return nil +} + +// GetSignBytes implements sdk.Msg +func (msg MsgConnectionOpenConfirm) GetSignBytes() []byte { + return sdk.MustSortJSON(SubModuleCdc.MustMarshalJSON(msg)) +} + +// GetSigners implements sdk.Msg +func (msg MsgConnectionOpenConfirm) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Signer} +} diff --git a/x/ibc/03-connection/types/querier.go b/x/ibc/03-connection/types/querier.go new file mode 100644 index 00000000000..6fbb151342f --- /dev/null +++ b/x/ibc/03-connection/types/querier.go @@ -0,0 +1,83 @@ +package types + +import ( + "strings" + + commitment "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment" + "github.com/tendermint/tendermint/crypto/merkle" +) + +// query routes supported by the IBC connection Querier +const ( + QueryConnection = "connection" + QueryClientConnections = "client_connections" +) + +// ConnectionResponse defines the client query response for a connection which +// also includes a proof and the height from which the proof was retrieved. +type ConnectionResponse struct { + Connection ConnectionEnd `json:"connection" yaml:"connection"` + Proof commitment.Proof `json:"proof,omitempty" yaml:"proof,omitempty"` + ProofPath commitment.Path `json:"proof_path,omitempty" yaml:"proof_path,omitempty"` + ProofHeight uint64 `json:"proof_height,omitempty" yaml:"proof_height,omitempty"` +} + +// NewConnectionResponse creates a new ConnectionResponse instance +func NewConnectionResponse( + connectionID string, connection ConnectionEnd, proof *merkle.Proof, height int64, +) ConnectionResponse { + return ConnectionResponse{ + Connection: connection, + Proof: commitment.Proof{Proof: proof}, + ProofPath: commitment.NewPath(strings.Split(ConnectionPath(connectionID), "/")), + ProofHeight: uint64(height), + } +} + +// QueryConnectionParams defines the params for the following queries: +// - 'custom/ibc/connections/' +type QueryConnectionParams struct { + ConnectionID string +} + +// NewQueryConnectionParams creates a new QueryConnectionParams instance +func NewQueryConnectionParams(clientID string) QueryConnectionParams { + return QueryConnectionParams{ + ConnectionID: clientID, + } +} + +// ClientConnectionsResponse defines the client query response for a client +// connection paths which also includes a proof and the height from which the +// proof was retrieved. +type ClientConnectionsResponse struct { + ConnectionPaths []string `json:"connection_paths" yaml:"connection_paths"` + Proof commitment.Proof `json:"proof,omitempty" yaml:"proof,omitempty"` + ProofPath commitment.Path `json:"proof_path,omitempty" yaml:"proof_path,omitempty"` + ProofHeight uint64 `json:"proof_height,omitempty" yaml:"proof_height,omitempty"` +} + +// NewClientConnectionsResponse creates a new ConnectionPaths instance +func NewClientConnectionsResponse( + clientID string, connectionPaths []string, proof *merkle.Proof, height int64, +) ClientConnectionsResponse { + return ClientConnectionsResponse{ + ConnectionPaths: connectionPaths, + Proof: commitment.Proof{Proof: proof}, + ProofPath: commitment.NewPath(strings.Split(ClientConnectionsPath(clientID), "/")), + ProofHeight: uint64(height), + } +} + +// QueryClientConnectionsParams defines the params for the following queries: +// - 'custom/ibc/client//connections' +type QueryClientConnectionsParams struct { + ClientID string +} + +// NewQueryClientConnectionsParams creates a new QueryClientConnectionsParams instance +func NewQueryClientConnectionsParams(clientID string) QueryClientConnectionsParams { + return QueryClientConnectionsParams{ + ClientID: clientID, + } +} diff --git a/x/ibc/03-connection/types/version.go b/x/ibc/03-connection/types/version.go new file mode 100644 index 00000000000..bf74a52dae9 --- /dev/null +++ b/x/ibc/03-connection/types/version.go @@ -0,0 +1,96 @@ +package types + +// GetCompatibleVersions returns an ordered set of compatible IBC versions for the +// caller chain's connection end. +func GetCompatibleVersions() []string { + return []string{"1.0.0"} +} + +// LatestVersion gets the latest version of a connection protocol +// +// CONTRACT: version array MUST be already sorted. +func LatestVersion(versions []string) string { + if len(versions) == 0 { + return "" + } + return versions[len(versions)-1] +} + +// PickVersion picks the counterparty latest version that is matches the list +// of compatible versions for the connection. +func PickVersion(counterpartyVersions, compatibleVersions []string) string { + + n := len(counterpartyVersions) + m := len(compatibleVersions) + + // aux hash maps to lookup already seen versions + counterpartyVerLookup := make(map[string]bool) + compatibleVerLookup := make(map[string]bool) + + // versions suported + var supportedVersions []string + + switch { + case n == 0 || m == 0: + return "" + case n == m: + for i := n - 1; i >= 0; i-- { + counterpartyVerLookup[counterpartyVersions[i]] = true + compatibleVerLookup[compatibleVersions[i]] = true + + // check if we've seen any of the versions + if _, ok := compatibleVerLookup[counterpartyVersions[i]]; ok { + supportedVersions = append(supportedVersions, counterpartyVersions[i]) + } + + if _, ok := counterpartyVerLookup[compatibleVersions[i]]; ok { + // TODO: check if the version is already in the array + supportedVersions = append(supportedVersions, compatibleVersions[i]) + } + } + case n > m: + for i := n - 1; i >= m; i-- { + counterpartyVerLookup[compatibleVersions[i]] = true + } + + for i := m - 1; i >= 0; i-- { + counterpartyVerLookup[counterpartyVersions[i]] = true + compatibleVerLookup[compatibleVersions[i]] = true + + // check if we've seen any of the versions + if _, ok := compatibleVerLookup[counterpartyVersions[i]]; ok { + supportedVersions = append(supportedVersions, counterpartyVersions[i]) + } + + if _, ok := counterpartyVerLookup[compatibleVersions[i]]; ok { + supportedVersions = append(supportedVersions, compatibleVersions[i]) + } + } + + case n < m: + for i := m - 1; i >= n; i-- { + compatibleVerLookup[compatibleVersions[i]] = true + } + + for i := n - 1; i >= 0; i-- { + counterpartyVerLookup[counterpartyVersions[i]] = true + compatibleVerLookup[compatibleVersions[i]] = true + + // check if we've seen any of the versions + if _, ok := compatibleVerLookup[counterpartyVersions[i]]; ok { + supportedVersions = append(supportedVersions, counterpartyVersions[i]) + } + + if _, ok := counterpartyVerLookup[compatibleVersions[i]]; ok { + supportedVersions = append(supportedVersions, compatibleVersions[i]) + } + } + } + + if len(supportedVersions) == 0 { + return "" + } + + // TODO: compare latest version before appending + return supportedVersions[len(supportedVersions)-1] +} diff --git a/x/ibc/client/cli/cli.go b/x/ibc/client/cli/cli.go index 5657d2aad3c..04289628f64 100644 --- a/x/ibc/client/cli/cli.go +++ b/x/ibc/client/cli/cli.go @@ -6,6 +6,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" ibcclient "github.com/cosmos/cosmos-sdk/x/ibc/02-client" + connection "github.com/cosmos/cosmos-sdk/x/ibc/03-connection" "github.com/cosmos/cosmos-sdk/x/ibc/types" ) @@ -21,6 +22,7 @@ func GetTxCmd(storeKey string, cdc *codec.Codec) *cobra.Command { ibcTxCmd.AddCommand(client.PostCommands( ibcclient.GetTxCmd(cdc, storeKey), + connection.GetTxCmd(cdc, storeKey), )...) return ibcTxCmd } @@ -38,6 +40,7 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { ibcQueryCmd.AddCommand(client.GetCommands( ibcclient.GetQueryCmd(cdc, queryRoute), + connection.GetQueryCmd(cdc, queryRoute), )...) return ibcQueryCmd } diff --git a/x/ibc/handler.go b/x/ibc/handler.go index 5dc802b6f06..ed740f39183 100644 --- a/x/ibc/handler.go +++ b/x/ibc/handler.go @@ -5,6 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" client "github.com/cosmos/cosmos-sdk/x/ibc/02-client" + connection "github.com/cosmos/cosmos-sdk/x/ibc/03-connection" ) // NewHandler defines the IBC handler @@ -22,8 +23,20 @@ func NewHandler(k Keeper) sdk.Handler { case client.MsgSubmitMisbehaviour: return client.HandleMsgSubmitMisbehaviour(ctx, k.ClientKeeper, msg) + case connection.MsgConnectionOpenInit: + return connection.HandleMsgConnectionOpenInit(ctx, k.ConnectionKeeper, msg) + + case connection.MsgConnectionOpenTry: + return connection.HandleMsgConnectionOpenTry(ctx, k.ConnectionKeeper, msg) + + case connection.MsgConnectionOpenAck: + return connection.HandleMsgConnectionOpenAck(ctx, k.ConnectionKeeper, msg) + + case connection.MsgConnectionOpenConfirm: + return connection.HandleMsgConnectionOpenConfirm(ctx, k.ConnectionKeeper, msg) + default: - errMsg := fmt.Sprintf("unrecognized IBC Client message type: %T", msg) + errMsg := fmt.Sprintf("unrecognized IBC message type: %T", msg) return sdk.ErrUnknownRequest(errMsg).Result() } } diff --git a/x/ibc/keeper/keeper.go b/x/ibc/keeper/keeper.go index fce91fb353b..dafe8892d16 100644 --- a/x/ibc/keeper/keeper.go +++ b/x/ibc/keeper/keeper.go @@ -4,16 +4,22 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" client "github.com/cosmos/cosmos-sdk/x/ibc/02-client" + connection "github.com/cosmos/cosmos-sdk/x/ibc/03-connection" ) // Keeper defines each ICS keeper for IBC type Keeper struct { - ClientKeeper client.Keeper + ClientKeeper client.Keeper + ConnectionKeeper connection.Keeper } // NewKeeper creates a new ibc Keeper func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, codespace sdk.CodespaceType) Keeper { + clientKeeper := client.NewKeeper(cdc, key, codespace) + connectionKeeper := connection.NewKeeper(cdc, key, codespace, clientKeeper) + return Keeper{ - ClientKeeper: client.NewKeeper(cdc, key, codespace), + ClientKeeper: clientKeeper, + ConnectionKeeper: connectionKeeper, } } diff --git a/x/ibc/keeper/querier.go b/x/ibc/keeper/querier.go index 7cbd4ec9c49..43db6f574dc 100644 --- a/x/ibc/keeper/querier.go +++ b/x/ibc/keeper/querier.go @@ -1,24 +1,43 @@ package keeper import ( + "fmt" + abci "github.com/tendermint/tendermint/abci/types" sdk "github.com/cosmos/cosmos-sdk/types" client "github.com/cosmos/cosmos-sdk/x/ibc/02-client" + connection "github.com/cosmos/cosmos-sdk/x/ibc/03-connection" ) // NewQuerier creates a querier for the IBC module func NewQuerier(k Keeper) sdk.Querier { return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) { switch path[0] { - case client.QueryClientState: - return client.QuerierClientState(ctx, req, k.ClientKeeper) - case client.QueryConsensusState: - return client.QuerierConsensusState(ctx, req, k.ClientKeeper) - case client.QueryVerifiedRoot: - return client.QuerierVerifiedRoot(ctx, req, k.ClientKeeper) + case client.SubModuleName: + switch path[1] { + case client.QueryClientState: + return client.QuerierClientState(ctx, req, k.ClientKeeper) + case client.QueryConsensusState: + return client.QuerierConsensusState(ctx, req, k.ClientKeeper) + case client.QueryVerifiedRoot: + return client.QuerierVerifiedRoot(ctx, req, k.ClientKeeper) + default: + return nil, sdk.ErrUnknownRequest(fmt.Sprintf("unknown IBC %s query endpoint", client.SubModuleName)) + } + + case connection.SubModuleName: + switch path[1] { + case connection.QueryConnection: + return connection.QuerierConnection(ctx, req, k.ConnectionKeeper) + case connection.QueryClientConnections: + return connection.QuerierClientConnections(ctx, req, k.ConnectionKeeper) + default: + return nil, sdk.ErrUnknownRequest(fmt.Sprintf("unknown IBC %s query endpoint", connection.SubModuleName)) + } + default: - return nil, sdk.ErrUnknownRequest("unknown IBC client query endpoint") + return nil, sdk.ErrUnknownRequest("unknown IBC query endpoint") } } } diff --git a/x/ibc/module.go b/x/ibc/module.go index 675b18aa1dd..21ba001210d 100644 --- a/x/ibc/module.go +++ b/x/ibc/module.go @@ -13,6 +13,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" client "github.com/cosmos/cosmos-sdk/x/ibc/02-client" + connection "github.com/cosmos/cosmos-sdk/x/ibc/03-connection" commitment "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment" "github.com/cosmos/cosmos-sdk/x/ibc/client/cli" "github.com/cosmos/cosmos-sdk/x/ibc/types" @@ -37,6 +38,7 @@ func (AppModuleBasic) Name() string { // RegisterCodec registers the staking module's types for the given codec. func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { client.RegisterCodec(cdc) + connection.RegisterCodec(cdc) commitment.RegisterCodec(cdc) }