Fix legacy querying tx ("unregistered type" bug) (#7992)

* Add tests for query txs

* Add test for IBC

* Refactor checkSignModeError

* Fix lint

* Add successful IBC test

* Remove logs

* break

* Remove known errors

* Update error message

* Better comments

* Revert variable renaming

* Fix lint

* Add mention of gRPC as TODO
This commit is contained in:
Amaury 2020-11-25 13:02:31 +01:00 committed by GitHub
parent 9c47612b5d
commit a9dd334c34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 211 additions and 91 deletions

View File

@ -16,6 +16,7 @@ Some important information concerning all legacy REST endpoints:
| Legacy REST Endpoint | Description | Breaking Change |
| ------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `POST /txs` | Query tx by hash | Endpoint will error when trying to broadcast transactions that don't support Amino serialization (e.g. IBC txs)<sup>1</sup>. |
| `GET /txs/{hash}` | Query tx by hash | Endpoint will error when trying to output transactions that don't support Amino serialization (e.g. IBC txs)<sup>1</sup>. |
| `GET /txs` | Query tx by events | Endpoint will error when trying to output transactions that don't support Amino serialization (e.g. IBC txs)<sup>1</sup>. |
| `GET /staking/validators` | Get all validators | BondStatus is now a protobuf enum instead of an int32, and JSON serialized using its protobuf name, so expect query parameters like `?status=BOND_STATUS_{BONDED,UNBONDED,UNBONDING}` as opposed to `?status={bonded,unbonded,unbonding}`. |
@ -87,3 +88,7 @@ Some modules expose legacy `POST` endpoints to generate unsigned transactions fo
| `POST /upgrade/*` | Create unsigned `Msg`s for upgrade | N/A, use Protobuf directly |
| `GET /upgrade/current` | Get the current plan | `GET /cosmos/upgrade/v1beta1/current_plan` |
| `GET /upgrade/applied_plan/{name}` | Get a previously applied plan | `GET /cosmos/upgrade/v1beta1/applied/{name}` |
## Migrating to gRPC
Instead of hitting REST endpoints as described in the previous paragraph, the SDK also exposes a gRPC server. Any client can use gRPC instead of REST to interact with the node. An overview of different ways to communicate with a node can be found [here (TODO)](https://github.com/cosmos/cosmos-sdk/issues/7657), and a concrete tutorial for setting up a gRPC client [here (TODO)](https://github.com/cosmos/cosmos-sdk/issues/7657).

View File

@ -55,9 +55,10 @@ func DecodeTxRequestHandlerFn(clientCtx client.Context) http.HandlerFunc {
response := DecodeResp(stdTx)
err = checkSignModeError(w, clientCtx, response, "/cosmos/tx/v1beta1/txs/decode")
err = checkSignModeError(clientCtx, response, "/cosmos/tx/v1beta1/txs/decode")
if err != nil {
// Error is already returned by checkSignModeError.
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}

View File

@ -18,8 +18,6 @@ import (
genutilrest "github.com/cosmos/cosmos-sdk/x/genutil/client/rest"
)
const unRegisteredConcreteTypeErr = "unregistered concrete type"
// QueryAccountRequestHandlerFn is the query accountREST Handler.
func QueryAccountRequestHandlerFn(storeName string, clientCtx client.Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
@ -110,9 +108,10 @@ func QueryTxsRequestHandlerFn(clientCtx client.Context) http.HandlerFunc {
packStdTxResponse(w, clientCtx, txRes)
}
err = checkSignModeError(w, clientCtx, searchResult, "/cosmos/tx/v1beta1/txs")
err = checkSignModeError(clientCtx, searchResult, "/cosmos/tx/v1beta1/txs")
if err != nil {
// Error is already returned by checkSignModeError.
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
@ -152,9 +151,10 @@ func QueryTxRequestHandlerFn(clientCtx client.Context) http.HandlerFunc {
rest.WriteErrorResponse(w, http.StatusNotFound, fmt.Sprintf("no transaction found with hash %s", hashHexStr))
}
err = checkSignModeError(w, clientCtx, output, "/cosmos/tx/v1beta1/tx/{txhash}")
err = checkSignModeError(clientCtx, output, "/cosmos/tx/v1beta1/tx/{txhash}")
if err != nil {
// Error is already returned by checkSignModeError.
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
@ -198,18 +198,22 @@ func packStdTxResponse(w http.ResponseWriter, clientCtx client.Context, txRes *s
return nil
}
func checkSignModeError(w http.ResponseWriter, ctx client.Context, resp interface{}, grpcEndPoint string) error {
// checkSignModeError checks if there are errors with marshalling non-amino
// txs with amino.
func checkSignModeError(ctx client.Context, resp interface{}, grpcEndPoint string) error {
// LegacyAmino used intentionally here to handle the SignMode errors
marshaler := ctx.LegacyAmino
_, err := marshaler.MarshalJSON(resp)
if err != nil && strings.Contains(err.Error(), unRegisteredConcreteTypeErr) {
rest.WriteErrorResponse(w, http.StatusInternalServerError,
"This transaction was created with the new SIGN_MODE_DIRECT signing method, and therefore cannot be displayed"+
" via legacy REST handlers, please use CLI or directly query the Tendermint RPC endpoint to query"+
" this transaction. gRPC gateway endpoint is "+grpcEndPoint+". Please also see the REST endpoints migration"+
" guide at "+clientrest.DeprecationURL+".")
return err
if err != nil {
// If there's an unmarshalling error, we assume that it's because we're
// using amino to unmarshal a non-amino tx.
return fmt.Errorf("this transaction cannot be displayed via legacy REST endpoints, because it does not support"+
" Amino serialization. Please either use CLI, gRPC, gRPC-gateway, or directly query the Tendermint RPC"+
" endpoint to query this transaction. The new REST endpoint (via gRPC-gateway) is %s. Please also see the"+
"REST endpoints migration guide at %s for more info", grpcEndPoint, clientrest.DeprecationURL)
}
return nil

View File

@ -4,6 +4,7 @@ import (
"fmt"
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/suite"
"github.com/cosmos/cosmos-sdk/client/flags"
@ -16,16 +17,16 @@ import (
"github.com/cosmos/cosmos-sdk/testutil/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
txtypes "github.com/cosmos/cosmos-sdk/types/tx"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
authclient "github.com/cosmos/cosmos-sdk/x/auth/client"
authcli "github.com/cosmos/cosmos-sdk/x/auth/client/cli"
bankcli "github.com/cosmos/cosmos-sdk/x/bank/client/testutil"
ibccli "github.com/cosmos/cosmos-sdk/x/ibc/core/04-channel/client/cli"
txtypes "github.com/cosmos/cosmos-sdk/types/tx"
rest2 "github.com/cosmos/cosmos-sdk/x/auth/client/rest"
"github.com/cosmos/cosmos-sdk/x/auth/legacy/legacytx"
bankcli "github.com/cosmos/cosmos-sdk/x/bank/client/testutil"
"github.com/cosmos/cosmos-sdk/x/bank/types"
ibccli "github.com/cosmos/cosmos-sdk/x/ibc/core/04-channel/client/cli"
ibcsolomachinecli "github.com/cosmos/cosmos-sdk/x/ibc/light-clients/06-solomachine/client/cli"
)
type IntegrationTestSuite struct {
@ -129,51 +130,99 @@ func (s *IntegrationTestSuite) TestBroadcastTxRequest() {
s.Require().NotEmpty(txRes.TxHash)
}
func (s *IntegrationTestSuite) TestQueryTxByHash() {
// Helper function to test querying txs. We will use it to query StdTx and service `Msg`s.
func (s *IntegrationTestSuite) testQueryTx(txHeight int64, txHash, txRecipient string) {
val0 := s.network.Validators[0]
testCases := []struct {
desc string
malleate func() *sdk.TxResponse
}{
{
"Query by hash",
func() *sdk.TxResponse {
txJSON, err := rest.GetRequest(fmt.Sprintf("%s/txs/%s", val0.APIAddress, txHash))
s.Require().NoError(err)
var txResAmino sdk.TxResponse
s.Require().NoError(val0.ClientCtx.LegacyAmino.UnmarshalJSON(txJSON, &txResAmino))
return &txResAmino
},
},
{
"Query by height",
func() *sdk.TxResponse {
txJSON, err := rest.GetRequest(fmt.Sprintf("%s/txs?limit=10&page=1&tx.height=%d", val0.APIAddress, txHeight))
s.Require().NoError(err)
var searchtxResult sdk.SearchTxsResult
s.Require().NoError(val0.ClientCtx.LegacyAmino.UnmarshalJSON(txJSON, &searchtxResult))
s.Require().Len(searchtxResult.Txs, 1)
return searchtxResult.Txs[0]
},
},
{
"Query by event (transfer.recipient)",
func() *sdk.TxResponse {
txJSON, err := rest.GetRequest(fmt.Sprintf("%s/txs?transfer.recipient=%s", val0.APIAddress, txRecipient))
s.Require().NoError(err)
var searchtxResult sdk.SearchTxsResult
s.Require().NoError(val0.ClientCtx.LegacyAmino.UnmarshalJSON(txJSON, &searchtxResult))
s.Require().Len(searchtxResult.Txs, 1)
return searchtxResult.Txs[0]
},
},
}
for _, tc := range testCases {
s.Run(fmt.Sprintf("Case %s", tc.desc), func() {
txResponse := tc.malleate()
// Check that the height is correct.
s.Require().Equal(txHeight, txResponse.Height)
// Check that the events are correct.
s.Require().Contains(
txResponse.RawLog,
fmt.Sprintf("{\"key\":\"recipient\",\"value\":\"%s\"}", txRecipient),
)
// Check that the Msg is correct.
stdTx, ok := txResponse.Tx.GetCachedValue().(legacytx.StdTx)
s.Require().True(ok)
msgs := stdTx.GetMsgs()
s.Require().Equal(len(msgs), 1)
msg, ok := msgs[0].(*types.MsgSend)
s.Require().True(ok)
s.Require().Equal(txRecipient, msg.ToAddress)
})
}
}
func (s *IntegrationTestSuite) TestQueryTxWithStdTx() {
val0 := s.network.Validators[0]
// We broadcasted a StdTx in SetupSuite.
// we just check for a non-empty TxHash here, the actual hash will depend on the underlying tx configuration
// We just check for a non-empty TxHash here, the actual hash will depend on the underlying tx configuration
s.Require().NotEmpty(s.stdTxRes.TxHash)
// We now fetch the tx by hash on the `/tx/{hash}` route.
txJSON, err := rest.GetRequest(fmt.Sprintf("%s/txs/%s", val0.APIAddress, s.stdTxRes.TxHash))
s.Require().NoError(err)
// txJSON should contain the whole tx, we just make sure that our custom
// memo is there.
s.Require().Contains(string(txJSON), s.stdTx.Memo)
s.testQueryTx(s.stdTxRes.Height, s.stdTxRes.TxHash, val0.Address.String())
}
func (s *IntegrationTestSuite) TestQueryTxByHeight() {
val0 := s.network.Validators[0]
// We broadcasted a StdTx in SetupSuite.
// we just check for a non-empty height here, as we'll need to for querying.
s.Require().NotEmpty(s.stdTxRes.Height)
// We now fetch the tx on `/txs` route, filtering by `tx.height`
txJSON, err := rest.GetRequest(fmt.Sprintf("%s/txs?limit=10&page=1&tx.height=%d", val0.APIAddress, s.stdTxRes.Height))
s.Require().NoError(err)
// txJSON should contain the whole tx, we just make sure that our custom
// memo is there.
s.Require().Contains(string(txJSON), s.stdTx.Memo)
}
func (s *IntegrationTestSuite) TestQueryTxByHashWithServiceMessage() {
func (s *IntegrationTestSuite) TestQueryTxWithServiceMessage() {
val := s.network.Validators[0]
sendTokens := sdk.NewInt64Coin(s.cfg.BondDenom, 10)
_, _, addr := testdata.KeyTestPubAddr()
// Right after this line, we're sending a tx. Might need to wait a block
// to refresh sequences.
// Might need to wait a block to refresh sequences from previous setups.
s.Require().NoError(s.network.WaitForNextBlock())
out, err := bankcli.ServiceMsgSendExec(
val.ClientCtx,
val.Address,
val.Address,
addr,
sdk.NewCoins(sendTokens),
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
@ -188,17 +237,7 @@ func (s *IntegrationTestSuite) TestQueryTxByHashWithServiceMessage() {
s.Require().NoError(s.network.WaitForNextBlock())
txJSON, err := rest.GetRequest(fmt.Sprintf("%s/txs/%s", val.APIAddress, txRes.TxHash))
s.Require().NoError(err)
var txResAmino sdk.TxResponse
s.Require().NoError(val.ClientCtx.LegacyAmino.UnmarshalJSON(txJSON, &txResAmino))
stdTx, ok := txResAmino.Tx.GetCachedValue().(legacytx.StdTx)
s.Require().True(ok)
msgs := stdTx.GetMsgs()
s.Require().Equal(len(msgs), 1)
_, ok = msgs[0].(*types.MsgSend)
s.Require().True(ok)
s.testQueryTx(txRes.Height, txRes.TxHash, addr.String())
}
func (s *IntegrationTestSuite) TestMultipleSyncBroadcastTxRequests() {
@ -303,42 +342,43 @@ func (s *IntegrationTestSuite) broadcastReq(stdTx legacytx.StdTx, mode string) (
return rest.PostRequest(fmt.Sprintf("%s/txs", val.APIAddress), "application/json", bz)
}
func (s *IntegrationTestSuite) TestLegacyRestErrMessages() {
// testQueryIBCTx is a helper function to test querying txs which:
// - show an error message on legacy REST endpoints
// - succeed using gRPC
// In practise, we call this function on IBC txs.
func (s *IntegrationTestSuite) testQueryIBCTx(txRes sdk.TxResponse, cmd *cobra.Command, args []string) {
val := s.network.Validators[0]
args := []string{
"121", // dummy port-id
"channel-0", // dummy channel-id
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
fmt.Sprintf("--gas=%d", flags.DefaultGasLimit),
fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()),
fmt.Sprintf("--%s=foobar", flags.FlagMemo),
errMsg := "this transaction cannot be displayed via legacy REST endpoints, because it does not support" +
" Amino serialization. Please either use CLI, gRPC, gRPC-gateway, or directly query the Tendermint RPC" +
" endpoint to query this transaction. The new REST endpoint (via gRPC-gateway) is "
// Test that legacy endpoint return the above error message on IBC txs.
testCases := []struct {
desc string
url string
}{
{
"Query by hash",
fmt.Sprintf("%s/txs/%s", val.APIAddress, txRes.TxHash),
},
{
"Query by height",
fmt.Sprintf("%s/txs?tx.height=%d", val.APIAddress, txRes.Height),
},
}
// created a dummy txn for IBC, eventually it fails. Our intension is to test the error message of querying a
// message which is signed with proto, since IBC won't support legacy amino at all we are considering a message from IBC module.
out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, ibccli.NewChannelCloseInitCmd(), args)
s.Require().NoError(err)
var txRes sdk.TxResponse
s.Require().NoError(val.ClientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &txRes))
s.Require().NoError(s.network.WaitForNextBlock())
// try to fetch the txn using legacy rest, this won't work since the ibc module doesn't support amino.
txJSON, err := rest.GetRequest(fmt.Sprintf("%s/txs/%s", val.APIAddress, txRes.TxHash))
for _, tc := range testCases {
s.Run(fmt.Sprintf("Case %s", tc.desc), func() {
txJSON, err := rest.GetRequest(tc.url)
s.Require().NoError(err)
var errResp rest.ErrorResponse
s.Require().NoError(val.ClientCtx.LegacyAmino.UnmarshalJSON(txJSON, &errResp))
errMsg := "This transaction was created with the new SIGN_MODE_DIRECT signing method, " +
"and therefore cannot be displayed via legacy REST handlers, please use CLI or directly query the Tendermint " +
"RPC endpoint to query this transaction."
s.Require().Contains(errResp.Error, errMsg)
})
}
// try fetching the txn using gRPC req, it will fetch info since it has proto codec.
grpcJSON, err := rest.GetRequest(fmt.Sprintf("%s/cosmos/tx/v1beta1/tx/%s", val.APIAddress, txRes.TxHash))
@ -350,7 +390,7 @@ func (s *IntegrationTestSuite) TestLegacyRestErrMessages() {
// generate broadcast only txn.
args = append(args, fmt.Sprintf("--%s=true", flags.FlagGenerateOnly))
out, err = clitestutil.ExecTestCLICmd(val.ClientCtx, ibccli.NewChannelCloseInitCmd(), args)
out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, args)
s.Require().NoError(err)
txFile, cleanup := testutil.WriteToNewTempFile(s.T(), string(out.Bytes()))
@ -368,10 +408,80 @@ func (s *IntegrationTestSuite) TestLegacyRestErrMessages() {
res, err := rest.PostRequest(fmt.Sprintf("%s/txs/decode", val.APIAddress), "application/json", bz)
s.Require().NoError(err)
var errResp rest.ErrorResponse
s.Require().NoError(val.ClientCtx.LegacyAmino.UnmarshalJSON(res, &errResp))
s.Require().Contains(errResp.Error, errMsg)
}
// TestLegacyRestErrMessages creates two IBC txs, one that fails, one that
// succeeds, and make sure we cannot query any of them (with pretty error msg).
// Our intension is to test the error message of querying a message which is
// signed with proto, since IBC won't support legacy amino at all we are
// considering a message from IBC module.
func (s *IntegrationTestSuite) TestLegacyRestErrMessages() {
val := s.network.Validators[0]
// Write consensus json to temp file, used for an IBC message.
consensusJSON, cleanup := testutil.WriteToNewTempFile(
s.T(),
`{"public_key":{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"A/3SXL2ONYaOkxpdR5P8tHTlSlPv1AwQwSFxKRee5JQW"},"diversifier":"diversifier","timestamp":"10"}`,
)
defer cleanup()
testCases := []struct {
desc string
cmd *cobra.Command
args []string
code uint32
}{
{
"Failing IBC message",
ibccli.NewChannelCloseInitCmd(),
[]string{
"121", // dummy port-id
"channel-0", // dummy channel-id
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
fmt.Sprintf("--gas=%d", flags.DefaultGasLimit),
fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()),
fmt.Sprintf("--%s=foobar", flags.FlagMemo),
},
uint32(7),
},
{
"Successful IBC message",
ibcsolomachinecli.NewCreateClientCmd(),
[]string{
"21212121212", // dummy client-id
"1", // dummy sequence
consensusJSON.Name(), // path to consensus json,
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
fmt.Sprintf("--gas=%d", flags.DefaultGasLimit),
fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()),
fmt.Sprintf("--%s=foobar", flags.FlagMemo),
},
uint32(0),
},
}
for _, tc := range testCases {
s.Run(fmt.Sprintf("Case %s", tc.desc), func() {
out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, tc.cmd, tc.args)
s.Require().NoError(err)
var txRes sdk.TxResponse
s.Require().NoError(val.ClientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &txRes))
s.Require().Equal(tc.code, txRes.Code)
s.Require().NoError(s.network.WaitForNextBlock())
s.testQueryIBCTx(txRes, tc.cmd, tc.args)
})
}
}
func TestIntegrationTestSuite(t *testing.T) {
suite.Run(t, new(IntegrationTestSuite))
}