Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG-PENDING.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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