diff --git a/CHANGELOG.md b/CHANGELOG.md index a07ffd1cb..d37557e44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,11 @@ longer panics if the store to load contains substores that we didn't explicitly * [\#4979](https://github.com/cosmos/cosmos-sdk/issues/4979) Introduce a new `halt-time` config and CLI option to the `start` command. When provided, an application will halt during `Commit` when the block time is >= the `halt-time`. +* [\#4972](https://github.com/cosmos/cosmos-sdk/issues/4972) A `TxResponse` with a corresponding code +and tx hash will be returned for specific Tendermint errors: + * `CodeTxInMempoolCache` + * `CodeMempoolIsFull` + * `CodeTxTooLarge` ### Improvements diff --git a/client/context/broadcast.go b/client/context/broadcast.go index 9c1a720ae..1996c47d9 100644 --- a/client/context/broadcast.go +++ b/client/context/broadcast.go @@ -2,6 +2,10 @@ package context import ( "fmt" + "strings" + + "github.com/tendermint/tendermint/crypto/tmhash" + "github.com/tendermint/tendermint/mempool" "github.com/cosmos/cosmos-sdk/client/flags" sdk "github.com/cosmos/cosmos-sdk/types" @@ -29,6 +33,46 @@ func (ctx CLIContext) BroadcastTx(txBytes []byte) (res sdk.TxResponse, err error return res, err } +// CheckTendermintError checks if the error returned from BroadcastTx is a +// Tendermint error that is returned before the tx is submitted due to +// precondition checks that failed. If an Tendermint error is detected, this +// function returns the correct code back in TxResponse. +// +// TODO: Avoid brittle string matching in favor of error matching. This requires +// a change to Tendermint's RPCError type to allow retrieval or matching against +// a concrete error type. +func CheckTendermintError(err error, txBytes []byte) *sdk.TxResponse { + if err == nil { + return nil + } + + errStr := strings.ToLower(err.Error()) + txHash := fmt.Sprintf("%X", tmhash.Sum(txBytes)) + + switch { + case strings.Contains(errStr, strings.ToLower(mempool.ErrTxInCache.Error())): + return &sdk.TxResponse{ + Code: uint32(sdk.CodeTxInMempoolCache), + TxHash: txHash, + } + + case strings.Contains(errStr, "mempool is full"): + return &sdk.TxResponse{ + Code: uint32(sdk.CodeMempoolIsFull), + TxHash: txHash, + } + + case strings.Contains(errStr, "tx too large"): + return &sdk.TxResponse{ + Code: uint32(sdk.CodeTxTooLarge), + TxHash: txHash, + } + + default: + return nil + } +} + // BroadcastTxCommit broadcasts transaction bytes to a Tendermint node and // waits for a commit. An error is only returned if there is no RPC node // connection or if broadcasting fails. @@ -44,6 +88,10 @@ func (ctx CLIContext) BroadcastTxCommit(txBytes []byte) (sdk.TxResponse, error) res, err := node.BroadcastTxCommit(txBytes) if err != nil { + if errRes := CheckTendermintError(err, txBytes); errRes != nil { + return *errRes, nil + } + return sdk.NewResponseFormatBroadcastTxCommit(res), err } @@ -67,6 +115,10 @@ func (ctx CLIContext) BroadcastTxSync(txBytes []byte) (sdk.TxResponse, error) { } res, err := node.BroadcastTxSync(txBytes) + if errRes := CheckTendermintError(err, txBytes); errRes != nil { + return *errRes, nil + } + return sdk.NewResponseFormatBroadcastTx(res), err } @@ -79,5 +131,9 @@ func (ctx CLIContext) BroadcastTxAsync(txBytes []byte) (sdk.TxResponse, error) { } res, err := node.BroadcastTxAsync(txBytes) + if errRes := CheckTendermintError(err, txBytes); errRes != nil { + return *errRes, nil + } + return sdk.NewResponseFormatBroadcastTx(res), err } diff --git a/client/context/broadcast_test.go b/client/context/broadcast_test.go new file mode 100644 index 000000000..2afddb8f3 --- /dev/null +++ b/client/context/broadcast_test.go @@ -0,0 +1,70 @@ +package context + +import ( + "fmt" + "testing" + + "github.com/tendermint/tendermint/crypto/tmhash" + + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/mempool" + "github.com/tendermint/tendermint/rpc/client/mock" + ctypes "github.com/tendermint/tendermint/rpc/core/types" + tmtypes "github.com/tendermint/tendermint/types" + + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/types" +) + +type MockClient struct { + mock.Client + err error +} + +func (c MockClient) BroadcastTxCommit(tx tmtypes.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + return nil, c.err +} + +func (c MockClient) BroadcastTxAsync(tx tmtypes.Tx) (*ctypes.ResultBroadcastTx, error) { + return nil, c.err +} + +func (c MockClient) BroadcastTxSync(tx tmtypes.Tx) (*ctypes.ResultBroadcastTx, error) { + return nil, c.err +} + +func CreateContextWithErrorAndMode(err error, mode string) CLIContext { + return CLIContext{ + Client: MockClient{err: err}, + BroadcastMode: mode, + } +} + +// Test the correct code is returned when +func TestBroadcastError(t *testing.T) { + errors := map[error]uint32{ + mempool.ErrTxInCache: uint32(types.CodeTxInMempoolCache), + mempool.ErrTxTooLarge{}: uint32(types.CodeTxTooLarge), + mempool.ErrMempoolIsFull{}: uint32(types.CodeMempoolIsFull), + } + + modes := []string{ + flags.BroadcastAsync, + flags.BroadcastBlock, + flags.BroadcastSync, + } + + txBytes := []byte{0xA, 0xB} + txHash := fmt.Sprintf("%X", tmhash.Sum(txBytes)) + + for _, mode := range modes { + for err, code := range errors { + ctx := CreateContextWithErrorAndMode(err, mode) + resp, returnedErr := ctx.BroadcastTx(txBytes) + require.NoError(t, returnedErr) + require.Equal(t, code, resp.Code) + require.Equal(t, txHash, resp.TxHash) + } + } + +} diff --git a/go.mod b/go.mod index b31c642c7..c74a8e996 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/tendermint/iavl v0.12.4 github.com/tendermint/tendermint v0.32.3 github.com/tendermint/tm-db v0.1.1 + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 gopkg.in/yaml.v2 v2.2.2 ) diff --git a/go.sum b/go.sum index 3d6455d4e..951d80213 100644 --- a/go.sum +++ b/go.sum @@ -87,8 +87,6 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U= -github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= @@ -98,8 +96,6 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk= github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -214,8 +210,6 @@ github.com/tendermint/go-amino v0.15.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoM github.com/tendermint/iavl v0.12.4 h1:hd1woxUGISKkfUWBA4mmmTwOua6PQZTJM/F0FDrmMV8= github.com/tendermint/iavl v0.12.4/go.mod h1:8LHakzt8/0G3/I8FUU0ReNx98S/EP6eyPJkAUvEXT/o= github.com/tendermint/tendermint v0.32.1/go.mod h1:jmPDAKuNkev9793/ivn/fTBnfpA9mGBww8MPRNPNxnU= -github.com/tendermint/tendermint v0.32.2 h1:FvZWdksfDg/65vKKr5Lgo57keARFnmhrUEXHwyrV1QY= -github.com/tendermint/tendermint v0.32.2/go.mod h1:NwMyx58S8VJ7tEpFKqRVlVWKO9N9zjTHu+Dx96VsnOE= github.com/tendermint/tendermint v0.32.3 h1:GEnWpGQ795h5oTFNbfBLsY0LW/CW2j6p6HtiYNfxsgg= github.com/tendermint/tendermint v0.32.3/go.mod h1:ZK2c29jl1QRYznIRyRWRDsmm1yvtPzBRT00x4t1JToY= github.com/tendermint/tm-db v0.1.1 h1:G3Xezy3sOk9+ekhjZ/kjArYIs1SmwV+1OUgNkj7RgV0= @@ -274,6 +268,8 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= diff --git a/types/errors.go b/types/errors.go index 7a7a0d4a9..f59a4126c 100644 --- a/types/errors.go +++ b/types/errors.go @@ -46,6 +46,9 @@ const ( CodeTooManySignatures CodeType = 15 CodeGasOverflow CodeType = 16 CodeNoSignatures CodeType = 17 + CodeTxInMempoolCache CodeType = 18 + CodeMempoolIsFull CodeType = 19 + CodeTxTooLarge CodeType = 20 // CodespaceRoot is a codespace for error codes in this file only. // Notice that 0 is an "unset" codespace, which can be overridden with