From 7c42e2deb511a1baa9cc759037dcaeb57440a28b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zdyba=C5=82?= Date: Thu, 13 Jan 2022 15:59:48 +0100 Subject: [PATCH] Tendermint URI RPC (#224) --- CHANGELOG-PENDING.md | 3 +- da/grpc/grpc.go | 3 +- go.mod | 1 + go.sum | 2 + rpc/json/handler.go | 147 ++++++++++++++++++++++++++++++++++++--- rpc/json/service.go | 5 +- rpc/json/service_test.go | 84 ++++++++++++++++++++-- rpc/json/types.go | 20 +++--- rpc/server.go | 2 +- 9 files changed, 236 insertions(+), 31 deletions(-) diff --git a/CHANGELOG-PENDING.md b/CHANGELOG-PENDING.md index d83b603c42a..9e7afdda4f7 100644 --- a/CHANGELOG-PENDING.md +++ b/CHANGELOG-PENDING.md @@ -11,11 +11,12 @@ Month, DD, YYYY ### FEATURES - [indexer] [Implement block and transaction indexing, enable TxSearch RPC endpoint #202](https://github.com/celestiaorg/optimint/pull/202) [@mattdf](https://github.com/mattdf) +- [rpc] [Tendermint URI RPC](https://github.com/celestiaorg/optimint/pull/224) [@tzdybal](https://github.com/tzdybal/) ### IMPROVEMENTS -- [deps] [Update dependencies: grpc, cors, cobra, viper, tm-db](https://github.com/celestiaorg/optimint/pull/245) [@tzdybal](https://github.com/tzdybal/) - [ci] [Add more linters #219](https://github.com/celestiaorg/optimint/pull/219) [@tzdybal](https://github.com/tzdybal/) +- [deps] [Update dependencies: grpc, cors, cobra, viper, tm-db](https://github.com/celestiaorg/optimint/pull/245) [@tzdybal](https://github.com/tzdybal/) ### BUG FIXES diff --git a/da/grpc/grpc.go b/da/grpc/grpc.go index 21e0ff6ac34..1b555e2ad25 100644 --- a/da/grpc/grpc.go +++ b/da/grpc/grpc.go @@ -6,6 +6,7 @@ import ( "strconv" "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" "github.com/celestiaorg/optimint/da" "github.com/celestiaorg/optimint/log" @@ -51,7 +52,7 @@ func (d *DataAvailabilityLayerClient) Start() error { var err error var opts []grpc.DialOption // TODO(tzdybal): add more options - opts = append(opts, grpc.WithInsecure()) + opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) d.conn, err = grpc.Dial(d.config.Host+":"+strconv.Itoa(d.config.Port), opts...) if err != nil { return err diff --git a/go.mod b/go.mod index 52d5829a666..b19a8ae6c66 100644 --- a/go.mod +++ b/go.mod @@ -55,6 +55,7 @@ require ( github.com/google/gopacket v1.1.19 // indirect github.com/google/uuid v1.3.0 // indirect github.com/gopherjs/gopherjs v0.0.0-20190812055157-5d271430af9f // indirect + github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/gtank/merlin v0.1.1 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect diff --git a/go.sum b/go.sum index 3955c74dae6..a249e756680 100644 --- a/go.sum +++ b/go.sum @@ -419,6 +419,8 @@ github.com/gopherjs/gopherjs v0.0.0-20190812055157-5d271430af9f/go.mod h1:wJfORR github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk= github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= diff --git a/rpc/json/handler.go b/rpc/json/handler.go index 13c0ae1acb1..8b03b1314f2 100644 --- a/rpc/json/handler.go +++ b/rpc/json/handler.go @@ -1,44 +1,65 @@ package json import ( + "encoding/hex" + "encoding/json" "errors" - "io" + "fmt" "net/http" + "net/url" "reflect" + "strconv" "github.com/gorilla/rpc/v2" + "github.com/gorilla/rpc/v2/json2" + + "github.com/celestiaorg/optimint/log" ) type handler struct { s *service + m *http.ServeMux c rpc.Codec + l log.Logger } -func newHandler(s *service, codec rpc.Codec) *handler { - return &handler{ +func newHandler(s *service, codec rpc.Codec, logger log.Logger) *handler { + mux := http.NewServeMux() + h := &handler{ + m: mux, s: s, c: codec, + l: logger, + } + mux.HandleFunc("/", h.serveJSONRPC) + for name, method := range s.methods { + logger.Debug("registering method", "name", name) + mux.HandleFunc("/"+name, h.newHandler(method)) } + return h +} +func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.m.ServeHTTP(w, r) } -// ServeHTTP servces HTTP request +// serveJSONRPC serves HTTP request // implementation is highly inspired by Gorilla RPC v2 (but simplified a lot) -func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (h *handler) serveJSONRPC(w http.ResponseWriter, r *http.Request) { // Create a new c request. codecReq := h.c.NewRequest(r) // Get service method to be called. - method, errMethod := codecReq.Method() - if errMethod != nil { - if errors.Is(errMethod, io.EOF) && method == "" { + method, err := codecReq.Method() + if err != nil { + if e, ok := err.(*json2.Error); method == "" && ok && e.Message == "EOF" { // just serve empty page if request is empty return } - codecReq.WriteError(w, http.StatusBadRequest, errMethod) + codecReq.WriteError(w, http.StatusBadRequest, err) return } methodSpec, ok := h.s.methods[method] if !ok { - codecReq.WriteError(w, http.StatusBadRequest, errMethod) + codecReq.WriteError(w, int(json2.E_NO_METHOD), err) return } @@ -74,3 +95,109 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { codecReq.WriteError(w, statusCode, errResult) } } + +func (h *handler) newHandler(methodSpec *method) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + args := reflect.New(methodSpec.argsType) + values, err := url.ParseQuery(r.URL.RawQuery) + if err != nil { + h.encodeAndWriteResponse(w, nil, err, int(json2.E_PARSE)) + return + } + for i := 0; i < methodSpec.argsType.NumField(); i++ { + field := methodSpec.argsType.Field(i) + name := field.Tag.Get("json") + if !values.Has(name) { + h.encodeAndWriteResponse(w, nil, fmt.Errorf("missing param '%s'", name), int(json2.E_INVALID_REQ)) + return + } + rawVal := values.Get(name) + var err error + switch field.Type.Kind() { + case reflect.Bool: + err = setBoolParam(rawVal, &args, i) + case reflect.Int, reflect.Int64: + err = setIntParam(rawVal, &args, i) + case reflect.String: + args.Elem().Field(i).SetString(rawVal) + case reflect.Slice: + // []byte is a reflect.Slice of reflect.Uint8's + if field.Type.Elem().Kind() == reflect.Uint8 { + err = setByteSliceParam(rawVal, &args, i) + } + default: + err = errors.New("unknown type") + } + if err != nil { + err = fmt.Errorf("failed to parse param '%s': %w", name, err) + h.encodeAndWriteResponse(w, nil, err, int(json2.E_PARSE)) + return + } + } + rets := methodSpec.m.Call([]reflect.Value{ + reflect.ValueOf(r), + args, + }) + + // Extract the result to error if needed. + statusCode := http.StatusOK + errInter := rets[1].Interface() + if errInter != nil { + statusCode = int(json2.E_INTERNAL) + err = errInter.(error) + } + + h.encodeAndWriteResponse(w, rets[0].Interface(), err, statusCode) + } +} + +func (h *handler) encodeAndWriteResponse(w http.ResponseWriter, result interface{}, errResult error, statusCode int) { + // Prevents Internet Explorer from MIME-sniffing a response away + // from the declared content-type + w.Header().Set("x-content-type-options", "nosniff") + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + resp := response{ + Version: "2.0", + Id: []byte("-1"), + } + + if errResult != nil { + resp.Error = &json2.Error{Code: json2.ErrorCode(statusCode), Data: errResult.Error()} + } else { + resp.Result = result + } + + encoder := json.NewEncoder(w) + err := encoder.Encode(resp) + if err != nil { + h.l.Error("failed to encode RPC response", "error", err) + } +} + +func setBoolParam(rawVal string, args *reflect.Value, i int) error { + v, err := strconv.ParseBool(rawVal) + if err != nil { + return err + } + args.Elem().Field(i).SetBool(v) + return nil +} + +func setIntParam(rawVal string, args *reflect.Value, i int) error { + v, err := strconv.ParseInt(rawVal, 10, 64) + if err != nil { + return err + } + args.Elem().Field(i).SetInt(v) + return nil +} + +func setByteSliceParam(rawVal string, args *reflect.Value, i int) error { + b, err := hex.DecodeString(rawVal) + if err != nil { + return err + } + args.Elem().Field(i).SetBytes(b) + return nil +} diff --git a/rpc/json/service.go b/rpc/json/service.go index 043d8d94c58..0bef0797268 100644 --- a/rpc/json/service.go +++ b/rpc/json/service.go @@ -9,11 +9,12 @@ import ( rpcclient "github.com/tendermint/tendermint/rpc/client" ctypes "github.com/tendermint/tendermint/rpc/core/types" + "github.com/celestiaorg/optimint/log" "github.com/celestiaorg/optimint/rpc/client" ) -func GetHttpHandler(l *client.Client) (http.Handler, error) { - return newHandler(newService(l), json2.NewCodec()), nil +func GetHttpHandler(l *client.Client, logger log.Logger) (http.Handler, error) { + return newHandler(newService(l), json2.NewCodec(), logger), nil } type method struct { diff --git a/rpc/json/service_test.go b/rpc/json/service_test.go index 1883fab3bd4..afdb0034ae6 100644 --- a/rpc/json/service_test.go +++ b/rpc/json/service_test.go @@ -4,8 +4,10 @@ import ( "bytes" "context" "crypto/rand" + "encoding/json" "net/http" "net/http/httptest" + "net/url" "strings" "testing" @@ -13,7 +15,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - gorillajson "github.com/gorilla/rpc/v2/json" + "github.com/gorilla/rpc/v2/json2" "github.com/libp2p/go-libp2p-core/crypto" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/libs/log" @@ -31,10 +33,10 @@ func TestHandlerMapping(t *testing.T) { require := require.New(t) _, local := getRPC(t) - handler, err := GetHttpHandler(local) + handler, err := GetHttpHandler(local, log.TestingLogger()) require.NoError(err) - jsonReq, err := gorillajson.EncodeClientRequest("health", &HealthArgs{}) + jsonReq, err := json2.EncodeClientRequest("health", &HealthArgs{}) require.NoError(err) req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(jsonReq)) @@ -44,12 +46,73 @@ func TestHandlerMapping(t *testing.T) { assert.Equal(200, resp.Code) } +func TestREST(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + txSearchParams := url.Values{} + txSearchParams.Set("query", "message.sender='cosmos1njr26e02fjcq3schxstv458a3w5szp678h23dh'") + txSearchParams.Set("prove", "true") + txSearchParams.Set("page", "1") + txSearchParams.Set("per_page", "10") + txSearchParams.Set("order_by", "asc") + + cases := []struct { + name string + uri string + httpCode int + jsonrpcCode int + bodyContains string + }{ + + {"invalid/malformed request", "/block?so{}wrong!", 200, int(json2.E_INVALID_REQ), ``}, + {"invalid/missing param", "/block", 200, int(json2.E_INVALID_REQ), `missing param 'height'`}, + {"valid/no params", "/abci_info", 200, -1, `"last_block_height":345`}, + // to keep test simple, allow returning application error in following case + {"valid/int param", "/block?height=321", 200, int(json2.E_INTERNAL), `"key not found"`}, + {"invalid/int param", "/block?height=foo", 200, int(json2.E_PARSE), "failed to parse param 'height'"}, + {"valid/bool int string params", + "/tx_search?" + txSearchParams.Encode(), + 200, -1, `"total_count":0`}, + {"invalid/bool int string params", + "/tx_search?" + strings.Replace(txSearchParams.Encode(), "true", "blue", 1), + 200, int(json2.E_PARSE), "failed to parse param 'prove'"}, + {"valid/hex param", "/check_tx?tx=DEADBEEF", 200, -1, `"gas_used":"1000"`}, + {"invalid/hex param", "/check_tx?tx=QWERTY", 200, int(json2.E_PARSE), "failed to parse param 'tx'"}, + } + + _, local := getRPC(t) + handler, err := GetHttpHandler(local, log.TestingLogger()) + require.NoError(err) + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodPost, c.uri, nil) + resp := httptest.NewRecorder() + handler.ServeHTTP(resp, req) + + assert.Equal(c.httpCode, resp.Code) + s := resp.Body.String() + assert.NotEmpty(s) + assert.Contains(s, c.bodyContains) + var jsonResp response + assert.NoError(json.Unmarshal([]byte(s), &jsonResp)) + if c.jsonrpcCode != -1 { + require.NotNil(jsonResp.Error) + assert.EqualValues(c.jsonrpcCode, jsonResp.Error.Code) + } + t.Log(s) + }) + } + +} + func TestEmptyRequest(t *testing.T) { assert := assert.New(t) require := require.New(t) _, local := getRPC(t) - handler, err := GetHttpHandler(local) + handler, err := GetHttpHandler(local, log.TestingLogger()) require.NoError(err) req := httptest.NewRequest(http.MethodGet, "/", nil) @@ -64,7 +127,7 @@ func TestStringyRequest(t *testing.T) { require := require.New(t) _, local := getRPC(t) - handler, err := GetHttpHandler(local) + handler, err := GetHttpHandler(local, log.TestingLogger()) require.NoError(err) // `starport chain faucet ...` generates broken JSON (ints are "quoted" as strings) @@ -87,6 +150,17 @@ func getRPC(t *testing.T) (*mocks.Application, *client.Client) { require := require.New(t) app := &mocks.Application{} app.On("InitChain", mock.Anything).Return(abci.ResponseInitChain{}) + app.On("CheckTx", mock.Anything).Return(abci.ResponseCheckTx{ + GasWanted: 1000, + GasUsed: 1000, + }) + app.On("Info", mock.Anything).Return(abci.ResponseInfo{ + Data: "mock", + Version: "mock", + AppVersion: 123, + LastBlockHeight: 345, + LastBlockAppHash: nil, + }) key, _, _ := crypto.GenerateEd25519Key(rand.Reader) node, err := node.NewNode(context.Background(), config.NodeConfig{DALayer: "mock"}, key, proxy.NewLocalClientCreator(app), &types.GenesisDoc{ChainID: "test"}, log.TestingLogger()) require.NoError(err) diff --git a/rpc/json/types.go b/rpc/json/types.go index 10a588ec7c1..f5ec544c4b1 100644 --- a/rpc/json/types.go +++ b/rpc/json/types.go @@ -5,6 +5,7 @@ import ( "reflect" "strconv" + "github.com/gorilla/rpc/v2/json2" "github.com/tendermint/tendermint/libs/bytes" "github.com/tendermint/tendermint/types" ) @@ -30,7 +31,7 @@ type BlockchainInfoArgs struct { type GenesisArgs struct { } type GenesisChunkedArgs struct { - Id StrUint `json:"chunk"` + Id StrInt `json:"chunk"` } type BlockArgs struct { Height StrInt64 `json:"height"` @@ -115,9 +116,6 @@ type EmptyResult struct{} // StrInt is an proper int or quoted "int" type StrInt int -// StrUint is an proper uint or quoted "uint" -type StrUint uint - // StrInt64 is an proper int64 or quoted "int64" type StrInt64 int64 @@ -132,13 +130,6 @@ func (s *StrInt) UnmarshalJSON(b []byte) error { return err } -func (s *StrUint) UnmarshalJSON(b []byte) error { - var val StrInt64 - err := unmarshalStrInt64(b, &val) - *s = StrUint(val) - return err -} - func unmarshalStrInt64(b []byte, s *StrInt64) error { var i interface{} err := json.Unmarshal(b, &i) @@ -165,3 +156,10 @@ func unmarshalStrInt64(b []byte, s *StrInt64) error { } return nil } + +type response struct { + Version string `json:"jsonrpc"` + Result interface{} `json:"result,omitempty"` + Error *json2.Error `json:"error,omitempty"` + Id json.RawMessage `json:"id"` +} diff --git a/rpc/server.go b/rpc/server.go index d00b32c55ac..6a8913a7390 100644 --- a/rpc/server.go +++ b/rpc/server.go @@ -76,7 +76,7 @@ func (s *Server) startRPC() error { listener = netutil.LimitListener(listener, s.config.MaxOpenConnections) } - handler, err := json.GetHttpHandler(s.client) + handler, err := json.GetHttpHandler(s.client, s.Logger) if err != nil { return err }