Skip to content

Commit

Permalink
Tendermint URI RPC (cosmos#224)
Browse files Browse the repository at this point in the history
  • Loading branch information
tzdybal authored Jan 13, 2022
1 parent a649b55 commit 7c42e2d
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 31 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG-PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion da/grpc/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
147 changes: 137 additions & 10 deletions rpc/json/handler.go
Original file line number Diff line number Diff line change
@@ -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
}

Expand Down Expand Up @@ -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
}
5 changes: 3 additions & 2 deletions rpc/json/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
84 changes: 79 additions & 5 deletions rpc/json/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ import (
"bytes"
"context"
"crypto/rand"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"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"
Expand All @@ -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))
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
Loading

0 comments on commit 7c42e2d

Please sign in to comment.