Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Utility] Support RESTful relays (need to merge #960 first) #961

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
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
37 changes: 26 additions & 11 deletions app/client/cli/servicer.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,16 +243,35 @@ func sendTrustlessRelay(ctx context.Context, servicerUrl string, relay *rpc.Rela
return client.PostV1ClientRelayWithResponse(ctx, *relay)
}

func buildRelay(payload string, appPrivateKey crypto.PrivateKey, session *rpc.Session, servicer *rpc.ProtocolActor) (*rpc.RelayRequest, error) {
// IMPROVE(#958) configure acceptable payload type for each service
// unmarshalPayload attempts to deserialize the provided relay payload into one of the supported types.
//
// It returns an error if none of the supported types is a valid match for the provided payload.
func unmarshalRelayPayload(payload string) (*rpc.RelayRequest, error) {
// TECHDEBT: This is mostly COPIED from pocket-go: we should refactor pocket-go code and import this functionality from there instead.
relayPayload := rpc.Payload{
// INCOMPLETE(#803): need to unmarshal into JSONRPC and other supported relay formats once proto-generated custom types are added.
Jsonrpc: "2.0",
Method: payload,
// INCOMPLETE: set Headers for HTTP relays
var jsonRpcPayload rpc.JSONRPCPayload
// INCOMPLETE: set Headers for HTTP relays
if err := json.Unmarshal([]byte(payload), &jsonRpcPayload); err == nil && jsonRpcPayload.Validate() == nil {
return &rpc.RelayRequest{Payload: &jsonRpcPayload}, nil
}

// JSONRPC deserialization failed, assume the relay to be in REST format, i.e. any valid JSON is accepted.
var restPayload json.RawMessage
if err := json.Unmarshal([]byte(payload), &restPayload); err == nil {
bz := []byte(restPayload)
return &rpc.RelayRequest{Payload: &bz}, nil
}

return nil, fmt.Errorf("error unmarshalling relay payload %s", payload)
}

func buildRelay(payload string, appPrivateKey crypto.PrivateKey, session *rpc.Session, servicer *rpc.ProtocolActor) (*rpc.RelayRequest, error) {
relay, err := unmarshalRelayPayload(payload)
if err != nil {
return nil, fmt.Errorf("error unmarshalling relay payload %s", payload)
}

relayMeta := rpc.RelayRequestMeta{
relay.Meta = rpc.RelayRequestMeta{
BlockHeight: session.SessionHeight,
// TODO: Make Chain Identifier type consistent in Session and Meta use Identifiable for Chain in Session (or string for Chain in Relay Meta)
Chain: rpc.Identifiable{
Expand All @@ -262,10 +281,6 @@ func buildRelay(payload string, appPrivateKey crypto.PrivateKey, session *rpc.Se
// TODO(#697): Geozone
}

relay := &rpc.RelayRequest{
Payload: relayPayload,
Meta: relayMeta,
}
// TECHDEBT: Evaluate which fields we should and shouldn't marshall when signing the payload
reqBytes, err := json.Marshal(relay)
if err != nil {
Expand Down
47 changes: 47 additions & 0 deletions app/client/cli/servicer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,53 @@ func TestGetSessionFromCache(t *testing.T) {
}
}

func TestUnmarshalRelay(t *testing.T) {
restPayload := rpc.RESTPayload(`{"field1":"value1"}`)

testCases := []struct {
name string
payload string
expected *rpc.RelayRequest
expectErr bool
}{
{
name: "JSONRPC payload",
payload: `{"jsonrpc": "2.0", "id": "1", "method": "eth_blockNumber"}`,
expected: &rpc.RelayRequest{
Payload: &rpc.JSONRPCPayload{
Jsonrpc: "2.0",
Method: "eth_blockNumber",
Id: &rpc.JsonRpcId{Id: []byte("1")},
},
},
},
{
name: "REST payload",
payload: `{"field1":"value1"}`,
expected: &rpc.RelayRequest{
Payload: &restPayload,
},
},
{
name: "Payload with invalid format is rejected",
payload: "foo",
expectErr: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got, err := unmarshalRelayPayload(tc.payload)
if tc.expectErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.EqualValues(t, *tc.expected, *got)
})
}
}

func testSession(appAddr string, height int64) *rpc.Session {
const numSessionBlocks = 4

Expand Down
115 changes: 101 additions & 14 deletions rpc/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ package rpc

import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"

"github.com/labstack/echo/v4"
"github.com/pokt-network/pocket/app"
coreTypes "github.com/pokt-network/pocket/shared/core/types"
)

var errInvalidJsonRpc = errors.New("JSONRPC validation failed")

// CONSIDER: Remove all the V1 prefixes from the RPC module

func (s *rpcServer) GetV1Health(ctx echo.Context) error {
Expand Down Expand Up @@ -83,9 +88,7 @@ func (s *rpcServer) PostV1ClientGetSession(ctx echo.Context) error {
}

func (s *rpcServer) PostV1ClientRelay(ctx echo.Context) error {
utility := s.GetBus().GetUtilityModule()
_, err := utility.GetServicerModule()

servicer, err := s.GetBus().GetUtilityModule().GetServicerModule()
if err != nil {
return ctx.String(http.StatusInternalServerError, "node is not a servicer")
}
Expand All @@ -112,10 +115,18 @@ func (s *rpcServer) PostV1ClientRelay(ctx echo.Context) error {
Signature: body.Meta.Signature,
}

relayRequest := buildJsonRPCRelayPayload(&body)
var relayRequest *coreTypes.Relay
switch p := body.Payload.(type) {
case JSONRPCPayload:
relayRequest = buildJsonRPCRelayPayload(&p)
case RESTPayload:
relayRequest = buildRestRelayPayload(&p)
default:
return ctx.String(http.StatusBadRequest, "unsupported relay type")
}
relayRequest.Meta = relayMeta

relayResponse, err := utility.HandleRelay(relayRequest)
relayResponse, err := servicer.HandleRelay(relayRequest)
if err != nil {
return ctx.String(http.StatusInternalServerError, err.Error())
}
Expand Down Expand Up @@ -217,25 +228,25 @@ func (s *rpcServer) GetV1P2pStakedActorsAddressBook(ctx echo.Context, params Get
}

// TECHDEBT: handle other relay payload types, e.g. JSON, GRPC, etc.
func buildJsonRPCRelayPayload(body *RelayRequest) *coreTypes.Relay {
func buildJsonRPCRelayPayload(src *JSONRPCPayload) *coreTypes.Relay {
payload := &coreTypes.Relay_JsonRpcPayload{
JsonRpcPayload: &coreTypes.JSONRPCPayload{
JsonRpc: body.Payload.Jsonrpc,
Method: body.Payload.Method,
JsonRpc: src.Jsonrpc,
Method: src.Method,
},
}

if body.Payload.Id != nil {
payload.JsonRpcPayload.Id = []byte(*body.Payload.Id)
if src.Id != nil {
payload.JsonRpcPayload.Id = src.Id.Id
}

if body.Payload.Parameters != nil {
payload.JsonRpcPayload.Parameters = *body.Payload.Parameters
if src.Parameters != nil {
payload.JsonRpcPayload.Parameters = *src.Parameters
}

if body.Payload.Headers != nil {
if src.Headers != nil {
headers := make(map[string]string)
for _, header := range *body.Payload.Headers {
for _, header := range *src.Headers {
headers[header.Name] = header.Value
}
payload.JsonRpcPayload.Headers = headers
Expand All @@ -245,3 +256,79 @@ func buildJsonRPCRelayPayload(body *RelayRequest) *coreTypes.Relay {
RelayPayload: payload,
}
}

// DISCUSS: Path and Method requirements of relays in REST format.
func buildRestRelayPayload(src *RESTPayload) *coreTypes.Relay {
return &coreTypes.Relay{
RelayPayload: &coreTypes.Relay_RestPayload{
RestPayload: &coreTypes.RESTPayload{
Contents: *src,
},
},
}
}

// UnmarshalJSON is the custom unmarshaller for RelayRequest type. This is needed because the payload could be JSONRPC or REST.
func (r *RelayRequest) UnmarshalJSON(data []byte) error {
type relayWithJsonRpcPayload struct {
Meta RelayRequestMeta `json:"meta"`
Payload JSONRPCPayload `json:"payload"`
}
var jsonRpcRelay relayWithJsonRpcPayload
if err := json.Unmarshal(data, &jsonRpcRelay); err == nil && jsonRpcRelay.Payload.Validate() == nil {
r.Meta = jsonRpcRelay.Meta
r.Payload = jsonRpcRelay.Payload
return nil
}

type relayWithRestPayload struct {
Meta RelayRequestMeta `json:"meta"`
Payload RESTPayload `json:"payload"`
}
var restRelay relayWithRestPayload
if err := json.Unmarshal(data, &restRelay); err == nil {
r.Meta = restRelay.Meta
r.Payload = restRelay.Payload
return nil
}

return fmt.Errorf("invalid relay: %s", string(data))
}

// Validate returns an error if the payload struct is not valid JSONRPC
func (p *JSONRPCPayload) Validate() error {
if p.Method == "" {
return fmt.Errorf("%w: missing method field", errInvalidJsonRpc)
}

if p.Jsonrpc != "2.0" {
return fmt.Errorf("%w: invalid JSONRPC field value: %q", errInvalidJsonRpc, p.Jsonrpc)
}

return nil
}

// UnmarshalJSON is the custom unmarshaller for JsonRpcId type. It is needed because JSONRPC spec allows the "id" field to be nil, an integer, or a string.
//
// See the following link for more details:
// https://www.jsonrpc.org/specification#request_object
func (i *JsonRpcId) UnmarshalJSON(data []byte) error {
var v int64
if err := json.Unmarshal(data, &v); err == nil {
i.Id = []byte(fmt.Sprintf("%d", v))
return nil
}

var s string
if err := json.Unmarshal(data, &s); err == nil {
i.Id = []byte(s)
return nil
}

return fmt.Errorf("invalid JSONRPC ID value: %v", data)
}

// MarshalJSON is the custom marshaller for JsonRpcId type. It is needed to flatten the struct as a single value, i.e. mask the "Id" field.
func (i *JsonRpcId) MarshalJSON() ([]byte, error) {
return i.Id, nil
}
114 changes: 114 additions & 0 deletions rpc/handlers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package rpc

import (
"bytes"
"encoding/json"
"io"
"net/http/httptest"
"strings"
"testing"

"github.com/golang/mock/gomock"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/require"

coreTypes "github.com/pokt-network/pocket/shared/core/types"
mockModules "github.com/pokt-network/pocket/shared/modules/mocks"
)

type testRelayHandler func(relay *coreTypes.Relay) (*coreTypes.RelayResponse, error)

func TestRPCServer_PostV1Client(t *testing.T) {
testCases := []struct {
name string
relay RelayRequest
handler testRelayHandler
expectedResponse string
}{
{
name: "JSONRPC payload is processed correctly",
relay: RelayRequest{
Payload: JSONRPCPayload{
Jsonrpc: "2.0",
Method: "eth_blockNumber",
Id: &JsonRpcId{Id: []byte("1")},
},
},
handler: func(relay *coreTypes.Relay) (*coreTypes.RelayResponse, error) {
payload := relay.GetJsonRpcPayload()
require.EqualValues(t, payload.Id, []byte("1"))
require.Equal(t, payload.JsonRpc, "2.0")
require.Equal(t, payload.Method, "eth_blockNumber")
return &coreTypes.RelayResponse{Payload: "JSONRPC Relay Response"}, nil
},
expectedResponse: `{"payload":"JSONRPC Relay Response","servicer_signature":""}`,
},
{
name: "REST payload is processed correctly",
relay: RelayRequest{
Payload: RESTPayload(`{"field1":"value1"}`),
},
handler: func(relay *coreTypes.Relay) (*coreTypes.RelayResponse, error) {
payload := relay.GetRestPayload()
require.Equal(t, &coreTypes.RESTPayload{Contents: []byte(`{"field1":"value1"}`)}, payload)
return &coreTypes.RelayResponse{Payload: "REST Relay Response"}, nil
},
expectedResponse: `{"payload":"REST Relay Response","servicer_signature":""}`,
},
{
name: "Invalid payload is rejected",
relay: RelayRequest{
Payload: "foo",
},
handler: func(_ *coreTypes.Relay) (*coreTypes.RelayResponse, error) { return nil, nil },
expectedResponse: "bad request",
},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
bz, err := json.Marshal(testCase.relay)
require.NoError(t, err)
req := httptest.NewRequest("POST", "/v1/relay", bytes.NewReader(bz))
req.Header.Add("Content-Type", "application/json")

responseRecorder := httptest.NewRecorder()
ctx := echo.New().NewContext(req, responseRecorder)

mockBus := mockBus(t, testCase.handler)
rpcServer := NewRPCServer(mockBus)

err = rpcServer.PostV1ClientRelay(ctx)
require.NoError(t, err)

resp := responseRecorder.Result()
defer resp.Body.Close()
responseBody, err := io.ReadAll(resp.Body)

require.EqualValues(t, testCase.expectedResponse, strings.TrimRight(string(responseBody), "\n"))
})
}
}

// Create a mockBus with mock implementations of the utility module
func mockBus(t *testing.T, handler testRelayHandler) *mockModules.MockBus {
ctrl := gomock.NewController(t)
busMock := mockModules.NewMockBus(ctrl)
busMock.EXPECT().GetUtilityModule().Return(baseUtilityMock(ctrl, handler)).AnyTimes()
return busMock
}

// Creates utility and servicer modules mocks with mock implementations of some basic functionality
func baseUtilityMock(ctrl *gomock.Controller, handler testRelayHandler) *mockModules.MockUtilityModule {
servicerMock := mockModules.NewMockServicerModule(ctrl)
servicerMock.EXPECT().
HandleRelay(gomock.Any()).
DoAndReturn(
func(relay *coreTypes.Relay) (*coreTypes.RelayResponse, error) {
return handler(relay)
}).AnyTimes()

utilityMock := mockModules.NewMockUtilityModule(ctrl)
utilityMock.EXPECT().GetServicerModule().Return(servicerMock, nil).AnyTimes()
return utilityMock
}
Loading