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

[AppGate] Implement the MVP AppGateServer #108

Merged
merged 42 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
cec3ef4
feat: implement app client
red-0ne Oct 27, 2023
d312423
Merge remote-tracking branch 'origin/main' into feat/app-client
red-0ne Nov 6, 2023
c4fe2da
chore: address review comments
red-0ne Nov 7, 2023
a6edf61
Merge remote-tracking branch 'origin/main' into feat/app-client
red-0ne Nov 7, 2023
aab9e17
fix: remove signature field before signing
red-0ne Nov 7, 2023
0f2a53f
chore: go.mod
h5law Nov 7, 2023
65c524f
Merge branch 'main' into feat/app-client
h5law Nov 7, 2023
6e265c2
feat: add ring signatures
h5law Nov 7, 2023
a966b66
Merge branch 'main' into feat/app-client
h5law Nov 7, 2023
80af5fb
Merge branch 'main' into feat/app-client
h5law Nov 9, 2023
c1f6115
chore: remove mock files
h5law Nov 9, 2023
c681484
chore: fix spelling errors
h5law Nov 9, 2023
c787d80
fixup: spelling mistake
h5law Nov 9, 2023
65e9bee
feat: add command to start the appgateserver
h5law Nov 9, 2023
c22dc9c
chore: add debug lines
h5law Nov 9, 2023
c14a8b6
Merge branch 'main' into feat/app-client
h5law Nov 9, 2023
ec57a8e
chore: debugging
h5law Nov 9, 2023
ba3bd8f
Merge branch 'main' into feat/app-client
h5law Nov 9, 2023
781ee13
chore: go.mod
h5law Nov 9, 2023
078be29
chore: close websocket connections
h5law Nov 9, 2023
00211a9
Merge branch 'main' into feat/app-client
h5law Nov 9, 2023
12d97d6
chore: add ws todo
h5law Nov 9, 2023
2591e19
Merge remote-tracking branch 'origin/main' into feat/app-client
red-0ne Nov 10, 2023
3f2cb90
chore: Use depinject for AppGateServer
red-0ne Nov 10, 2023
a857e5f
fix: Get appAddress from url query when appAddress is empty
red-0ne Nov 10, 2023
bc2efb3
feat: address comments
h5law Nov 10, 2023
76a31c0
chore: fix signing key field
h5law Nov 10, 2023
09c843c
chore: defer cancelling ctx
h5law Nov 10, 2023
151313e
chore: cleanup log lines
h5law Nov 10, 2023
2cd04e7
chore: address comments
h5law Nov 10, 2023
5daed4a
chore: address comments
h5law Nov 10, 2023
d8c668a
Merge branch 'main' into feat/app-client
h5law Nov 10, 2023
118d26d
chore: update comments and naming
h5law Nov 10, 2023
ce91371
chore: fix missing if
h5law Nov 10, 2023
969bf0f
chore: add signed relay received debug log
h5law Nov 10, 2023
211df7f
chore: cleanup comments
h5law Nov 10, 2023
89786c3
chore: comments comments comments
h5law Nov 10, 2023
b6f7ef5
chore: comments comments comments
h5law Nov 10, 2023
1d9c233
chore: update ring comments
h5law Nov 10, 2023
89688ab
feat: refactor appgateserver creation with depinject supplier functio…
h5law Nov 10, 2023
683b285
chore: re-add missing signing information check
h5law Nov 10, 2023
35936ac
Merge branch 'main' into feat/app-client
h5law Nov 10, 2023
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
159 changes: 159 additions & 0 deletions pkg/appclient/appclient.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package appclient
red-0ne marked this conversation as resolved.
Show resolved Hide resolved

import (
"context"
"log"
"net/http"
"net/url"
"strings"

sdkclient "github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
accounttypes "github.com/cosmos/cosmos-sdk/x/auth/types"

blocktypes "pocket/pkg/client"
"pocket/x/service/types"
sessiontypes "pocket/x/session/types"
)

type appClient struct {
red-0ne marked this conversation as resolved.
Show resolved Hide resolved
// keyName is the name of the key in the keyring that will be used to sign relay requests.
red-0ne marked this conversation as resolved.
Show resolved Hide resolved
keyName string
keyring keyring.Keyring

// clientCtx is the client context for the application.
// It is used to query for the application's account to unmarshal the supplier's account
// and get the public key to verify the relay response signature.
clientCtx sdkclient.Context

// appAddress is the address of the application that this app client is for.
appAddress string

// sessionQuerier is the querier for the session module.
// It used to get the current session for the application given a requested service.
sessionQuerier sessiontypes.QueryClient

// accountQuerier is the querier for the account module.
// It is used to get the the supplier's public key to verify the relay response signature.
accountQuerier accounttypes.QueryClient

// blockClient is the client for the block module.
// It is used to get the current block height to query for the current session.
blockClient blocktypes.BlockClient
red-0ne marked this conversation as resolved.
Show resolved Hide resolved

// server is the HTTP server that will be used capture application requests
h5law marked this conversation as resolved.
Show resolved Hide resolved
// so that they can be signed and relayed to the supplier.
server *http.Server
}

func NewAppClient(
red-0ne marked this conversation as resolved.
Show resolved Hide resolved
clientCtx sdkclient.Context,
keyName string,
keyring keyring.Keyring,
applicationEndpoint *url.URL,
blockClient blocktypes.BlockClient,
) *appClient {
sessionQuerier := sessiontypes.NewQueryClient(clientCtx)
accountQuerier := accounttypes.NewQueryClient(clientCtx)

return &appClient{
clientCtx: clientCtx,
keyName: keyName,
keyring: keyring,
sessionQuerier: sessionQuerier,
accountQuerier: accountQuerier,
blockClient: blockClient,
server: &http.Server{Addr: applicationEndpoint.Host},
}
}

// Start starts the application server and blocks until the context is done
// or the server returns an error.
func (app *appClient) Start(ctx context.Context) error {
// Get and populate the application address from the keyring.
keyRecord, err := app.keyring.Key(app.keyName)
if err != nil {
return err
}

accAddress, err := keyRecord.GetAddress()
if err != nil {
return err
}

app.appAddress = accAddress.String()

// Shutdown the HTTP server when the context is done.
go func() {
<-ctx.Done()
app.server.Shutdown(ctx)
}()

// Start the HTTP server.
return app.server.ListenAndServe()
}

// Stop stops the application server and returns any error that occurred.
func (app *appClient) Stop(ctx context.Context) error {
return app.server.Shutdown(ctx)
}

// ServeHTTP is the HTTP handler for the application server.
h5law marked this conversation as resolved.
Show resolved Hide resolved
// It captures the application request, signs it, and sends it to the supplier.
// After receiving the response from the supplier, it verifies the response signature
// before returning the response to the application.
// The serviceId is extracted from the request path.
h5law marked this conversation as resolved.
Show resolved Hide resolved
func (app *appClient) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
red-0ne marked this conversation as resolved.
Show resolved Hide resolved
ctx := request.Context()

// Extract the serviceId from the request path.
path := request.URL.Path
serviceId := strings.Split(path, "/")[1]

// Currently only JSON RPC requests are supported.
red-0ne marked this conversation as resolved.
Show resolved Hide resolved
relayReponse, err := app.handleJSONRPCRelays(ctx, serviceId, request)
if err != nil {
// Reply with an error response if there was an error handling the relay.
app.replyWithError(writer, err)
log.Printf("ERROR: failed handling relay: %s", err)
return
}

// Reply with the RelayResponse payload.
if _, err := writer.Write(relayReponse); err != nil {
app.replyWithError(writer, err)
return
}
log.Print("INFO: request serviced successfully")
}

// replyWithError replies to the application with an error response.
// TODO_TECHDEBT: This method should be aware of the nature of the error to use the appropriate JSONRPC
// Code, Message and Data. Possibly by augmenting the passed in error with the adequate information.
func (app *appClient) replyWithError(writer http.ResponseWriter, err error) {
relayResponse := &types.RelayResponse{
Payload: &types.RelayResponse_JsonRpcPayload{
JsonRpcPayload: &types.JSONRPCResponsePayload{
Id: make([]byte, 0),
Jsonrpc: "2.0",
Error: &types.JSONRPCResponseError{
// Using conventional error code indicating internal server error.
Code: -32000,
Message: err.Error(),
Data: nil,
},
},
},
}

relayResponseBz, err := relayResponse.Marshal()
if err != nil {
log.Printf("ERROR: failed marshaling relay response: %s", err)
return
}

if _, err = writer.Write(relayResponseBz); err != nil {
log.Printf("ERROR: failed writing relay response: %s", err)
return
}
}
38 changes: 38 additions & 0 deletions pkg/appclient/endpoint_selector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package appclient

import (
"context"
"net/url"

sessiontypes "pocket/x/session/types"
sharedtypes "pocket/x/shared/types"
)

// getRelayerUrl gets the URL of the relayer for the given service.
red-0ne marked this conversation as resolved.
Show resolved Hide resolved
// It gets the suppliers list from the current session and returns
// the first relayer URL that matches the JSON RPC RpcType.
red-0ne marked this conversation as resolved.
Show resolved Hide resolved
red-0ne marked this conversation as resolved.
Show resolved Hide resolved
func (app *appClient) getRelayerUrl(
ctx context.Context,
serviceId string,
session *sessiontypes.Session,
) (supplierUrl *url.URL, supplierAddress string, err error) {
for _, supplier := range session.Suppliers {
for _, service := range supplier.Services {
// Skip services that don't match the requested serviceId.
if service.ServiceId.Id != serviceId {
continue
}

for _, endpoint := range service.Endpoints {
// Return the first endpoint url that matches the JSON RPC RpcType.
if endpoint.RpcType == sharedtypes.RPCType_JSON_RPC {
supplierUrl, err := url.Parse(endpoint.Url)
red-0ne marked this conversation as resolved.
Show resolved Hide resolved
return supplierUrl, supplier.Address, err
}
}
}
}

// Return an error if no relayer endpoints were found.
return nil, "", ErrNoRelayEndpoints
}
9 changes: 9 additions & 0 deletions pkg/appclient/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package appclient

import sdkerrors "cosmossdk.io/errors"

var (
codespace = "appclient"
ErrInvalidRelayResponseSignature = sdkerrors.Register(codespace, 1, "invalid relay response signature")
ErrNoRelayEndpoints = sdkerrors.Register(codespace, 2, "no relay endpoints found")
)
116 changes: 116 additions & 0 deletions pkg/appclient/jsonrpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package appclient

import (
"bytes"
"context"
"io"
"net/http"

"github.com/cometbft/cometbft/crypto"

"pocket/x/service/types"
sessiontypes "pocket/x/session/types"
sharedtypes "pocket/x/shared/types"
)

// handleJSONRPCRelays handles JSON RPC relay requests.
func (app *appClient) handleJSONRPCRelays(
red-0ne marked this conversation as resolved.
Show resolved Hide resolved
ctx context.Context,
serviceId string,
request *http.Request,
) (responseBz []byte, err error) {
// Read the request body bytes.
payloadBz, err := io.ReadAll(request.Body)
if err != nil {
return nil, err
}

// Hash and sign the request payload.
hash := crypto.Sha256(payloadBz)
signature, _, err := app.keyring.Sign(app.keyName, hash)
h5law marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}

// Create the relay request payload.
relayRequestPayload := &types.RelayRequest_JsonRpcPayload{}
relayRequestPayload.JsonRpcPayload.Unmarshal(payloadBz)

// Get the current block height to query for the current session.
currentBlock := app.blockClient.LatestBlock(ctx)

// Query for the current session.
sessionQueryReq := sessiontypes.QueryGetSessionRequest{
ApplicationAddress: app.appAddress,
ServiceId: &sharedtypes.ServiceId{Id: serviceId},
BlockHeight: currentBlock.Height(),
}
sessionQueryRes, err := app.sessionQuerier.GetSession(ctx, &sessionQueryReq)
h5law marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}

session := sessionQueryRes.Session

// Get a supplier URL and address for the given service and session.
supplierUrl, supplierAddress, err := app.getRelayerUrl(ctx, serviceId, session)
if err != nil {
return nil, err
}

// Create the relay request.
relayRequest := &types.RelayRequest{
Meta: &types.RelayRequestMetadata{
SessionHeader: session.Header,
Signature: signature,
},
Payload: relayRequestPayload,
}

// Marshal the relay request to bytes and create a reader to be used as an HTTP request body.
relayRequestBz, err := relayRequest.Marshal()
if err != nil {
return nil, err
}
relayRequestReader := io.NopCloser(bytes.NewReader(relayRequestBz))

// Create the HTTP request to send the request to the relayer.
relayHTTPRequest := &http.Request{
Method: request.Method,
Header: request.Header,
URL: supplierUrl,
Body: relayRequestReader,
}

// Perform the HTTP request to the relayer.
relayHTTPResponse, err := http.DefaultClient.Do(relayHTTPRequest)
if err != nil {
return nil, err
}

// Read the response body bytes.
relayResponseBz, err := io.ReadAll(relayHTTPResponse.Body)
if err != nil {
return nil, err
}

// Unmarshal the response bytes into a RelayResponse.
relayResponse := &types.RelayResponse{}
if err := relayResponse.Unmarshal(relayResponseBz); err != nil {
return nil, err
}

// Verify the response signature. We use the supplier address that we got from
// the getRelayerUrl function since this is the address we are expecting to sign the response.
// TODO_TECHDEBT: if the RelayResponse is an internal error response, we should not verify the signature
// as in some relayer early failures, it may not be signed by the supplier.
h5law marked this conversation as resolved.
Show resolved Hide resolved
if err := app.verifyResponse(ctx, supplierAddress, relayResponse); err != nil {
return nil, err
}

// Marshal the response payload to bytes to be sent back to the application.
var responsePayloadBz []byte
_, err = relayResponse.Payload.MarshalTo(responsePayloadBz)

return responsePayloadBz, err
red-0ne marked this conversation as resolved.
Show resolved Hide resolved
}
47 changes: 47 additions & 0 deletions pkg/appclient/relay_verifier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package appclient

import (
"context"

"github.com/cometbft/cometbft/crypto"
accounttypes "github.com/cosmos/cosmos-sdk/x/auth/types"

"pocket/x/service/types"
)

// verifyResponse verifies the relay response signature.
func (app *appClient) verifyResponse(
ctx context.Context,
supplierAddress string,
relayResponse *types.RelayResponse,
) error {
// Query for the supplier account to get the application's public key to verify the relay request signature.
accQueryReq := &accounttypes.QueryAccountRequest{Address: supplierAddress}
accQueryRes, err := app.accountQuerier.Account(ctx, accQueryReq)
red-0ne marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}

// Marshal the relay response payload to bytes and get the hash.
var payloadBz []byte
_, err = relayResponse.Payload.MarshalTo(payloadBz)
if err != nil {
red-0ne marked this conversation as resolved.
Show resolved Hide resolved
return err
}
hash := crypto.Sha256(payloadBz)
h5law marked this conversation as resolved.
Show resolved Hide resolved

// accQueryRes.Account.Value is a protobuf Any type that should be unmarshaled into an AccountI interface.
// TODO_TECHDEBT: Make sure our `AccountI`/`any` unmarshalling is correct.
// See https://github.com/pokt-network/poktroll/pull/101/files/edbd628e9146e232ef58c71cfa8f4be2135cdb50..fbba10626df79f6bf6e2218513dfdeb40a629790#r1372464439
red-0ne marked this conversation as resolved.
Show resolved Hide resolved
var account accounttypes.AccountI
if err := app.clientCtx.Codec.UnmarshalJSON(accQueryRes.Account.Value, account); err != nil {
return err
}

// Verify the relay response signature.
if !account.GetPubKey().VerifySignature(hash, relayResponse.Meta.SupplierSignature) {
return ErrInvalidRelayResponseSignature
}

return nil
}