Skip to content

Commit 7c42e2d

Browse files
authored
Tendermint URI RPC (cosmos#224)
1 parent a649b55 commit 7c42e2d

9 files changed

+236
-31
lines changed

CHANGELOG-PENDING.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ Month, DD, YYYY
1111
### FEATURES
1212

1313
- [indexer] [Implement block and transaction indexing, enable TxSearch RPC endpoint #202](https://github.com/celestiaorg/optimint/pull/202) [@mattdf](https://github.com/mattdf)
14+
- [rpc] [Tendermint URI RPC](https://github.com/celestiaorg/optimint/pull/224) [@tzdybal](https://github.com/tzdybal/)
1415

1516
### IMPROVEMENTS
1617

17-
- [deps] [Update dependencies: grpc, cors, cobra, viper, tm-db](https://github.com/celestiaorg/optimint/pull/245) [@tzdybal](https://github.com/tzdybal/)
1818
- [ci] [Add more linters #219](https://github.com/celestiaorg/optimint/pull/219) [@tzdybal](https://github.com/tzdybal/)
19+
- [deps] [Update dependencies: grpc, cors, cobra, viper, tm-db](https://github.com/celestiaorg/optimint/pull/245) [@tzdybal](https://github.com/tzdybal/)
1920

2021
### BUG FIXES
2122

da/grpc/grpc.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strconv"
77

88
"google.golang.org/grpc"
9+
"google.golang.org/grpc/credentials/insecure"
910

1011
"github.com/celestiaorg/optimint/da"
1112
"github.com/celestiaorg/optimint/log"
@@ -51,7 +52,7 @@ func (d *DataAvailabilityLayerClient) Start() error {
5152
var err error
5253
var opts []grpc.DialOption
5354
// TODO(tzdybal): add more options
54-
opts = append(opts, grpc.WithInsecure())
55+
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
5556
d.conn, err = grpc.Dial(d.config.Host+":"+strconv.Itoa(d.config.Port), opts...)
5657
if err != nil {
5758
return err

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ require (
5555
github.com/google/gopacket v1.1.19 // indirect
5656
github.com/google/uuid v1.3.0 // indirect
5757
github.com/gopherjs/gopherjs v0.0.0-20190812055157-5d271430af9f // indirect
58+
github.com/gorilla/mux v1.8.0 // indirect
5859
github.com/gorilla/websocket v1.4.2 // indirect
5960
github.com/gtank/merlin v0.1.1 // indirect
6061
github.com/hashicorp/errwrap v1.0.0 // indirect

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,8 @@ github.com/gopherjs/gopherjs v0.0.0-20190812055157-5d271430af9f/go.mod h1:wJfORR
419419
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
420420
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
421421
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
422+
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
423+
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
422424
github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk=
423425
github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ=
424426
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=

rpc/json/handler.go

+137-10
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,65 @@
11
package json
22

33
import (
4+
"encoding/hex"
5+
"encoding/json"
46
"errors"
5-
"io"
7+
"fmt"
68
"net/http"
9+
"net/url"
710
"reflect"
11+
"strconv"
812

913
"github.com/gorilla/rpc/v2"
14+
"github.com/gorilla/rpc/v2/json2"
15+
16+
"github.com/celestiaorg/optimint/log"
1017
)
1118

1219
type handler struct {
1320
s *service
21+
m *http.ServeMux
1422
c rpc.Codec
23+
l log.Logger
1524
}
1625

17-
func newHandler(s *service, codec rpc.Codec) *handler {
18-
return &handler{
26+
func newHandler(s *service, codec rpc.Codec, logger log.Logger) *handler {
27+
mux := http.NewServeMux()
28+
h := &handler{
29+
m: mux,
1930
s: s,
2031
c: codec,
32+
l: logger,
33+
}
34+
mux.HandleFunc("/", h.serveJSONRPC)
35+
for name, method := range s.methods {
36+
logger.Debug("registering method", "name", name)
37+
mux.HandleFunc("/"+name, h.newHandler(method))
2138
}
39+
return h
40+
}
41+
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
42+
h.m.ServeHTTP(w, r)
2243
}
2344

24-
// ServeHTTP servces HTTP request
45+
// serveJSONRPC serves HTTP request
2546
// implementation is highly inspired by Gorilla RPC v2 (but simplified a lot)
26-
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
47+
func (h *handler) serveJSONRPC(w http.ResponseWriter, r *http.Request) {
2748
// Create a new c request.
2849
codecReq := h.c.NewRequest(r)
2950
// Get service method to be called.
30-
method, errMethod := codecReq.Method()
31-
if errMethod != nil {
32-
if errors.Is(errMethod, io.EOF) && method == "" {
51+
method, err := codecReq.Method()
52+
if err != nil {
53+
if e, ok := err.(*json2.Error); method == "" && ok && e.Message == "EOF" {
3354
// just serve empty page if request is empty
3455
return
3556
}
36-
codecReq.WriteError(w, http.StatusBadRequest, errMethod)
57+
codecReq.WriteError(w, http.StatusBadRequest, err)
3758
return
3859
}
3960
methodSpec, ok := h.s.methods[method]
4061
if !ok {
41-
codecReq.WriteError(w, http.StatusBadRequest, errMethod)
62+
codecReq.WriteError(w, int(json2.E_NO_METHOD), err)
4263
return
4364
}
4465

@@ -74,3 +95,109 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
7495
codecReq.WriteError(w, statusCode, errResult)
7596
}
7697
}
98+
99+
func (h *handler) newHandler(methodSpec *method) func(http.ResponseWriter, *http.Request) {
100+
return func(w http.ResponseWriter, r *http.Request) {
101+
args := reflect.New(methodSpec.argsType)
102+
values, err := url.ParseQuery(r.URL.RawQuery)
103+
if err != nil {
104+
h.encodeAndWriteResponse(w, nil, err, int(json2.E_PARSE))
105+
return
106+
}
107+
for i := 0; i < methodSpec.argsType.NumField(); i++ {
108+
field := methodSpec.argsType.Field(i)
109+
name := field.Tag.Get("json")
110+
if !values.Has(name) {
111+
h.encodeAndWriteResponse(w, nil, fmt.Errorf("missing param '%s'", name), int(json2.E_INVALID_REQ))
112+
return
113+
}
114+
rawVal := values.Get(name)
115+
var err error
116+
switch field.Type.Kind() {
117+
case reflect.Bool:
118+
err = setBoolParam(rawVal, &args, i)
119+
case reflect.Int, reflect.Int64:
120+
err = setIntParam(rawVal, &args, i)
121+
case reflect.String:
122+
args.Elem().Field(i).SetString(rawVal)
123+
case reflect.Slice:
124+
// []byte is a reflect.Slice of reflect.Uint8's
125+
if field.Type.Elem().Kind() == reflect.Uint8 {
126+
err = setByteSliceParam(rawVal, &args, i)
127+
}
128+
default:
129+
err = errors.New("unknown type")
130+
}
131+
if err != nil {
132+
err = fmt.Errorf("failed to parse param '%s': %w", name, err)
133+
h.encodeAndWriteResponse(w, nil, err, int(json2.E_PARSE))
134+
return
135+
}
136+
}
137+
rets := methodSpec.m.Call([]reflect.Value{
138+
reflect.ValueOf(r),
139+
args,
140+
})
141+
142+
// Extract the result to error if needed.
143+
statusCode := http.StatusOK
144+
errInter := rets[1].Interface()
145+
if errInter != nil {
146+
statusCode = int(json2.E_INTERNAL)
147+
err = errInter.(error)
148+
}
149+
150+
h.encodeAndWriteResponse(w, rets[0].Interface(), err, statusCode)
151+
}
152+
}
153+
154+
func (h *handler) encodeAndWriteResponse(w http.ResponseWriter, result interface{}, errResult error, statusCode int) {
155+
// Prevents Internet Explorer from MIME-sniffing a response away
156+
// from the declared content-type
157+
w.Header().Set("x-content-type-options", "nosniff")
158+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
159+
160+
resp := response{
161+
Version: "2.0",
162+
Id: []byte("-1"),
163+
}
164+
165+
if errResult != nil {
166+
resp.Error = &json2.Error{Code: json2.ErrorCode(statusCode), Data: errResult.Error()}
167+
} else {
168+
resp.Result = result
169+
}
170+
171+
encoder := json.NewEncoder(w)
172+
err := encoder.Encode(resp)
173+
if err != nil {
174+
h.l.Error("failed to encode RPC response", "error", err)
175+
}
176+
}
177+
178+
func setBoolParam(rawVal string, args *reflect.Value, i int) error {
179+
v, err := strconv.ParseBool(rawVal)
180+
if err != nil {
181+
return err
182+
}
183+
args.Elem().Field(i).SetBool(v)
184+
return nil
185+
}
186+
187+
func setIntParam(rawVal string, args *reflect.Value, i int) error {
188+
v, err := strconv.ParseInt(rawVal, 10, 64)
189+
if err != nil {
190+
return err
191+
}
192+
args.Elem().Field(i).SetInt(v)
193+
return nil
194+
}
195+
196+
func setByteSliceParam(rawVal string, args *reflect.Value, i int) error {
197+
b, err := hex.DecodeString(rawVal)
198+
if err != nil {
199+
return err
200+
}
201+
args.Elem().Field(i).SetBytes(b)
202+
return nil
203+
}

rpc/json/service.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import (
99
rpcclient "github.com/tendermint/tendermint/rpc/client"
1010
ctypes "github.com/tendermint/tendermint/rpc/core/types"
1111

12+
"github.com/celestiaorg/optimint/log"
1213
"github.com/celestiaorg/optimint/rpc/client"
1314
)
1415

15-
func GetHttpHandler(l *client.Client) (http.Handler, error) {
16-
return newHandler(newService(l), json2.NewCodec()), nil
16+
func GetHttpHandler(l *client.Client, logger log.Logger) (http.Handler, error) {
17+
return newHandler(newService(l), json2.NewCodec(), logger), nil
1718
}
1819

1920
type method struct {

rpc/json/service_test.go

+79-5
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@ import (
44
"bytes"
55
"context"
66
"crypto/rand"
7+
"encoding/json"
78
"net/http"
89
"net/http/httptest"
10+
"net/url"
911
"strings"
1012
"testing"
1113

1214
"github.com/stretchr/testify/assert"
1315
"github.com/stretchr/testify/mock"
1416
"github.com/stretchr/testify/require"
1517

16-
gorillajson "github.com/gorilla/rpc/v2/json"
18+
"github.com/gorilla/rpc/v2/json2"
1719
"github.com/libp2p/go-libp2p-core/crypto"
1820
abci "github.com/tendermint/tendermint/abci/types"
1921
"github.com/tendermint/tendermint/libs/log"
@@ -31,10 +33,10 @@ func TestHandlerMapping(t *testing.T) {
3133
require := require.New(t)
3234

3335
_, local := getRPC(t)
34-
handler, err := GetHttpHandler(local)
36+
handler, err := GetHttpHandler(local, log.TestingLogger())
3537
require.NoError(err)
3638

37-
jsonReq, err := gorillajson.EncodeClientRequest("health", &HealthArgs{})
39+
jsonReq, err := json2.EncodeClientRequest("health", &HealthArgs{})
3840
require.NoError(err)
3941

4042
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(jsonReq))
@@ -44,12 +46,73 @@ func TestHandlerMapping(t *testing.T) {
4446
assert.Equal(200, resp.Code)
4547
}
4648

49+
func TestREST(t *testing.T) {
50+
assert := assert.New(t)
51+
require := require.New(t)
52+
53+
txSearchParams := url.Values{}
54+
txSearchParams.Set("query", "message.sender='cosmos1njr26e02fjcq3schxstv458a3w5szp678h23dh'")
55+
txSearchParams.Set("prove", "true")
56+
txSearchParams.Set("page", "1")
57+
txSearchParams.Set("per_page", "10")
58+
txSearchParams.Set("order_by", "asc")
59+
60+
cases := []struct {
61+
name string
62+
uri string
63+
httpCode int
64+
jsonrpcCode int
65+
bodyContains string
66+
}{
67+
68+
{"invalid/malformed request", "/block?so{}wrong!", 200, int(json2.E_INVALID_REQ), ``},
69+
{"invalid/missing param", "/block", 200, int(json2.E_INVALID_REQ), `missing param 'height'`},
70+
{"valid/no params", "/abci_info", 200, -1, `"last_block_height":345`},
71+
// to keep test simple, allow returning application error in following case
72+
{"valid/int param", "/block?height=321", 200, int(json2.E_INTERNAL), `"key not found"`},
73+
{"invalid/int param", "/block?height=foo", 200, int(json2.E_PARSE), "failed to parse param 'height'"},
74+
{"valid/bool int string params",
75+
"/tx_search?" + txSearchParams.Encode(),
76+
200, -1, `"total_count":0`},
77+
{"invalid/bool int string params",
78+
"/tx_search?" + strings.Replace(txSearchParams.Encode(), "true", "blue", 1),
79+
200, int(json2.E_PARSE), "failed to parse param 'prove'"},
80+
{"valid/hex param", "/check_tx?tx=DEADBEEF", 200, -1, `"gas_used":"1000"`},
81+
{"invalid/hex param", "/check_tx?tx=QWERTY", 200, int(json2.E_PARSE), "failed to parse param 'tx'"},
82+
}
83+
84+
_, local := getRPC(t)
85+
handler, err := GetHttpHandler(local, log.TestingLogger())
86+
require.NoError(err)
87+
88+
for _, c := range cases {
89+
t.Run(c.name, func(t *testing.T) {
90+
req := httptest.NewRequest(http.MethodPost, c.uri, nil)
91+
resp := httptest.NewRecorder()
92+
handler.ServeHTTP(resp, req)
93+
94+
assert.Equal(c.httpCode, resp.Code)
95+
s := resp.Body.String()
96+
assert.NotEmpty(s)
97+
assert.Contains(s, c.bodyContains)
98+
var jsonResp response
99+
assert.NoError(json.Unmarshal([]byte(s), &jsonResp))
100+
if c.jsonrpcCode != -1 {
101+
require.NotNil(jsonResp.Error)
102+
assert.EqualValues(c.jsonrpcCode, jsonResp.Error.Code)
103+
}
104+
t.Log(s)
105+
})
106+
}
107+
108+
}
109+
47110
func TestEmptyRequest(t *testing.T) {
48111
assert := assert.New(t)
49112
require := require.New(t)
50113

51114
_, local := getRPC(t)
52-
handler, err := GetHttpHandler(local)
115+
handler, err := GetHttpHandler(local, log.TestingLogger())
53116
require.NoError(err)
54117

55118
req := httptest.NewRequest(http.MethodGet, "/", nil)
@@ -64,7 +127,7 @@ func TestStringyRequest(t *testing.T) {
64127
require := require.New(t)
65128

66129
_, local := getRPC(t)
67-
handler, err := GetHttpHandler(local)
130+
handler, err := GetHttpHandler(local, log.TestingLogger())
68131
require.NoError(err)
69132

70133
// `starport chain faucet ...` generates broken JSON (ints are "quoted" as strings)
@@ -87,6 +150,17 @@ func getRPC(t *testing.T) (*mocks.Application, *client.Client) {
87150
require := require.New(t)
88151
app := &mocks.Application{}
89152
app.On("InitChain", mock.Anything).Return(abci.ResponseInitChain{})
153+
app.On("CheckTx", mock.Anything).Return(abci.ResponseCheckTx{
154+
GasWanted: 1000,
155+
GasUsed: 1000,
156+
})
157+
app.On("Info", mock.Anything).Return(abci.ResponseInfo{
158+
Data: "mock",
159+
Version: "mock",
160+
AppVersion: 123,
161+
LastBlockHeight: 345,
162+
LastBlockAppHash: nil,
163+
})
90164
key, _, _ := crypto.GenerateEd25519Key(rand.Reader)
91165
node, err := node.NewNode(context.Background(), config.NodeConfig{DALayer: "mock"}, key, proxy.NewLocalClientCreator(app), &types.GenesisDoc{ChainID: "test"}, log.TestingLogger())
92166
require.NoError(err)

0 commit comments

Comments
 (0)