From 257a374b78251c6f88c76b2ff892c5cd30f304c8 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 19 Jun 2020 09:25:52 +0400 Subject: [PATCH] rpc: add /check_tx endpoint (#5017) Closes #4549 --- CHANGELOG_PENDING.md | 1 + light/rpc/client.go | 4 ++ mempool/clist_mempool.go | 4 +- node/node.go | 3 +- proxy/app_conn.go | 5 ++ rpc/client/http/http.go | 9 ++++ rpc/client/interface.go | 1 + rpc/client/local/local.go | 4 ++ rpc/client/mock/client.go | 4 ++ rpc/client/rpc_test.go | 14 ++++++ rpc/core/env.go | 3 +- rpc/core/mempool.go | 11 +++++ rpc/core/routes.go | 1 + rpc/core/types/responses.go | 5 ++ rpc/swagger/swagger.yaml | 99 +++++++++++++++++++++++++++++++++++++ 15 files changed, 164 insertions(+), 4 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index eba36075e..ac259c55f 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -87,6 +87,7 @@ Friendly reminder, we have a [bug bounty program](https://hackerone.com/tendermi - [rpc] \#4979 Support EXISTS operator in `/tx_search` query (@melekes) - [p2p] \#4981 Expose `SaveAs` func on NodeKey (@melekes) - [evidence] [#4821](https://github.com/tendermint/tendermint/pull/4821) Amnesia evidence can be detected, verified and committed (@cmwaters) +- [rpc] \#5017 Add `/check_tx` endpoint to check transactions without executing them or adding them to the mempool (@melekes) ### IMPROVEMENTS: diff --git a/light/rpc/client.go b/light/rpc/client.go index 59d2a3d62..deba891d6 100644 --- a/light/rpc/client.go +++ b/light/rpc/client.go @@ -144,6 +144,10 @@ func (c *Client) NumUnconfirmedTxs() (*ctypes.ResultUnconfirmedTxs, error) { return c.next.NumUnconfirmedTxs() } +func (c *Client) CheckTx(tx types.Tx) (*ctypes.ResultCheckTx, error) { + return c.next.CheckTx(tx) +} + func (c *Client) NetInfo() (*ctypes.ResultNetInfo, error) { return c.next.NetInfo() } diff --git a/mempool/clist_mempool.go b/mempool/clist_mempool.go index 36c26fa75..aa173d07c 100644 --- a/mempool/clist_mempool.go +++ b/mempool/clist_mempool.go @@ -706,9 +706,9 @@ func (cache *mapTxCache) Push(tx types.Tx) bool { if cache.list.Len() >= cache.size { popped := cache.list.Front() - poppedTxHash := popped.Value.([sha256.Size]byte) //nolint:staticcheck // SA5011: possible nil pointer dereference - delete(cache.cacheMap, poppedTxHash) if popped != nil { + poppedTxHash := popped.Value.([sha256.Size]byte) + delete(cache.cacheMap, poppedTxHash) cache.list.Remove(popped) } } diff --git a/node/node.go b/node/node.go index 97e6e37c0..711faf1eb 100644 --- a/node/node.go +++ b/node/node.go @@ -958,7 +958,8 @@ func (n *Node) ConfigureRPC() error { return fmt.Errorf("can't get pubkey: %w", err) } rpccore.SetEnvironment(&rpccore.Environment{ - ProxyAppQuery: n.proxyApp.Query(), + ProxyAppQuery: n.proxyApp.Query(), + ProxyAppMempool: n.proxyApp.Mempool(), StateDB: n.stateDB, BlockStore: n.blockStore, diff --git a/proxy/app_conn.go b/proxy/app_conn.go index 32e647cd6..376c19342 100644 --- a/proxy/app_conn.go +++ b/proxy/app_conn.go @@ -27,6 +27,7 @@ type AppConnMempool interface { Error() error CheckTxAsync(types.RequestCheckTx) *abcicli.ReqRes + CheckTxSync(types.RequestCheckTx) (*types.ResponseCheckTx, error) FlushAsync() *abcicli.ReqRes FlushSync() error @@ -125,6 +126,10 @@ func (app *appConnMempool) CheckTxAsync(req types.RequestCheckTx) *abcicli.ReqRe return app.appConn.CheckTxAsync(req) } +func (app *appConnMempool) CheckTxSync(req types.RequestCheckTx) (*types.ResponseCheckTx, error) { + return app.appConn.CheckTxSync(req) +} + //------------------------------------------------ // Implements AppConnQuery (subset of abcicli.Client) diff --git a/rpc/client/http/http.go b/rpc/client/http/http.go index ed6b6dbe3..e253080fc 100644 --- a/rpc/client/http/http.go +++ b/rpc/client/http/http.go @@ -288,6 +288,15 @@ func (c *baseRPCClient) NumUnconfirmedTxs() (*ctypes.ResultUnconfirmedTxs, error return result, nil } +func (c *baseRPCClient) CheckTx(tx types.Tx) (*ctypes.ResultCheckTx, error) { + result := new(ctypes.ResultCheckTx) + _, err := c.caller.Call("check_tx", map[string]interface{}{"tx": tx}, result) + if err != nil { + return nil, err + } + return result, nil +} + func (c *baseRPCClient) NetInfo() (*ctypes.ResultNetInfo, error) { result := new(ctypes.ResultNetInfo) _, err := c.caller.Call("net_info", map[string]interface{}{}, result) diff --git a/rpc/client/interface.go b/rpc/client/interface.go index b3ce6492e..340a483e1 100644 --- a/rpc/client/interface.go +++ b/rpc/client/interface.go @@ -115,6 +115,7 @@ type EventsClient interface { type MempoolClient interface { UnconfirmedTxs(limit *int) (*ctypes.ResultUnconfirmedTxs, error) NumUnconfirmedTxs() (*ctypes.ResultUnconfirmedTxs, error) + CheckTx(tx types.Tx) (*ctypes.ResultCheckTx, error) } // EvidenceClient is used for submitting an evidence of the malicious diff --git a/rpc/client/local/local.go b/rpc/client/local/local.go index 76209edcf..54a2041ea 100644 --- a/rpc/client/local/local.go +++ b/rpc/client/local/local.go @@ -104,6 +104,10 @@ func (c *Local) NumUnconfirmedTxs() (*ctypes.ResultUnconfirmedTxs, error) { return core.NumUnconfirmedTxs(c.ctx) } +func (c *Local) CheckTx(tx types.Tx) (*ctypes.ResultCheckTx, error) { + return core.CheckTx(c.ctx, tx) +} + func (c *Local) NetInfo() (*ctypes.ResultNetInfo, error) { return core.NetInfo(c.ctx) } diff --git a/rpc/client/mock/client.go b/rpc/client/mock/client.go index 16ce0c889..6fea29807 100644 --- a/rpc/client/mock/client.go +++ b/rpc/client/mock/client.go @@ -110,6 +110,10 @@ func (c Client) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) return core.BroadcastTxSync(&rpctypes.Context{}, tx) } +func (c Client) CheckTx(tx types.Tx) (*ctypes.ResultCheckTx, error) { + return core.CheckTx(&rpctypes.Context{}, tx) +} + func (c Client) NetInfo() (*ctypes.ResultNetInfo, error) { return core.NetInfo(&rpctypes.Context{}) } diff --git a/rpc/client/rpc_test.go b/rpc/client/rpc_test.go index 312e4c05a..83d075092 100644 --- a/rpc/client/rpc_test.go +++ b/rpc/client/rpc_test.go @@ -392,6 +392,20 @@ func TestNumUnconfirmedTxs(t *testing.T) { mempool.Flush() } +func TestCheckTx(t *testing.T) { + mempool := node.Mempool() + + for _, c := range GetClients() { + _, _, tx := MakeTxKV() + + res, err := c.CheckTx(tx) + require.NoError(t, err) + assert.Equal(t, abci.CodeTypeOK, res.Code) + + assert.Equal(t, 0, mempool.Size(), "mempool must be empty") + } +} + func TestTx(t *testing.T) { // first we broadcast a tx c := getHTTPClient() diff --git a/rpc/core/env.go b/rpc/core/env.go index 7d19f145e..47996bf4d 100644 --- a/rpc/core/env.go +++ b/rpc/core/env.go @@ -67,7 +67,8 @@ type peers interface { // to be setup once during startup. type Environment struct { // external, thread safe interfaces - ProxyAppQuery proxy.AppConnQuery + ProxyAppQuery proxy.AppConnQuery + ProxyAppMempool proxy.AppConnMempool // interfaces defined in types and above StateDB dbm.DB diff --git a/rpc/core/mempool.go b/rpc/core/mempool.go index 11e72cd3d..53bfcbdcd 100644 --- a/rpc/core/mempool.go +++ b/rpc/core/mempool.go @@ -150,3 +150,14 @@ func NumUnconfirmedTxs(ctx *rpctypes.Context) (*ctypes.ResultUnconfirmedTxs, err Total: env.Mempool.Size(), TotalBytes: env.Mempool.TxsBytes()}, nil } + +// CheckTx checks the transaction without executing it. The transaction won't +// be added to the mempool either. +// More: https://docs.tendermint.com/master/rpc/#/Tx/check_tx +func CheckTx(ctx *rpctypes.Context, tx types.Tx) (*ctypes.ResultCheckTx, error) { + res, err := env.ProxyAppMempool.CheckTxSync(abci.RequestCheckTx{Tx: tx}) + if err != nil { + return nil, err + } + return &ctypes.ResultCheckTx{ResponseCheckTx: *res}, nil +} diff --git a/rpc/core/routes.go b/rpc/core/routes.go index 626b8cca0..ddfb40d51 100644 --- a/rpc/core/routes.go +++ b/rpc/core/routes.go @@ -22,6 +22,7 @@ var Routes = map[string]*rpc.RPCFunc{ "block_by_hash": rpc.NewRPCFunc(BlockByHash, "hash"), "block_results": rpc.NewRPCFunc(BlockResults, "height"), "commit": rpc.NewRPCFunc(Commit, "height"), + "check_tx": rpc.NewRPCFunc(CheckTx, "tx"), "tx": rpc.NewRPCFunc(Tx, "hash,prove"), "tx_search": rpc.NewRPCFunc(TxSearch, "query,prove,page,per_page,order_by"), "validators": rpc.NewRPCFunc(Validators, "height,page,per_page"), diff --git a/rpc/core/types/responses.go b/rpc/core/types/responses.go index 7b5443776..7c63b3872 100644 --- a/rpc/core/types/responses.go +++ b/rpc/core/types/responses.go @@ -174,6 +174,11 @@ type ResultBroadcastTxCommit struct { Height int64 `json:"height"` } +// ResultCheckTx wraps abci.ResponseCheckTx. +type ResultCheckTx struct { + abci.ResponseCheckTx +} + // Result of querying for a tx type ResultTx struct { Hash bytes.HexBytes `json:"hash"` diff --git a/rpc/swagger/swagger.yaml b/rpc/swagger/swagger.yaml index b195ac935..b7c74b8e9 100644 --- a/rpc/swagger/swagger.yaml +++ b/rpc/swagger/swagger.yaml @@ -153,6 +153,39 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorResponse" + /check_tx: + get: + summary: Checks the transaction without executing it. + tags: + - Tx + operationId: broadcast_tx_commit + description: | + The transaction won't be added to the mempool. + + Please refer to + https://docs.tendermint.com/master/tendermint-core/using-tendermint.html#formatting + for formatting/encoding rules. + parameters: + - in: query + name: tx + required: true + schema: + type: string + example: "785" + description: The transaction + responses: + 200: + description: ABCI application's CheckTx response + content: + application/json: + schema: + $ref: "#/components/schemas/CheckTxResponse" + 500: + description: empty error + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /subscribe: get: summary: Subscribe for events via WebSocket. @@ -2923,6 +2956,72 @@ components: jsonrpc: type: "string" example: "2.0" + CheckTxResponse: + type: object + required: + - "error" + - "result" + - "id" + - "jsonrpc" + properties: + error: + type: "string" + example: "" + result: + required: + - "log" + - "data" + - "code" + properties: + code: + type: "string" + example: "0" + data: + type: "string" + example: "" + log: + type: "string" + example: "" + info: + type: "string" + example: "" + gas_wanted: + type: "string" + example: "1" + gas_used: + type: "string" + example: "0" + events: + type: "array" + x-nullable: true + items: + type: "object" + properties: + type: + type: "string" + example: "app" + attributes: + type: "array" + x-nullable: false + items: + type: "object" + properties: + key: + type: "string" + example: "Y3JlYXRvcg==" + value: + type: "string" + example: "Q29zbW9zaGkgTmV0b3dva28=" + codespace: + type: "string" + example: "bank" + type: "object" + id: + type: "number" + example: 0 + jsonrpc: + type: "string" + example: "2.0" BroadcastTxResponse: type: object required: