From 260b8235f2fa38a5d4c08741451c782b7a4f71b7 Mon Sep 17 00:00:00 2001 From: Redouane Lakrache Date: Thu, 9 Nov 2023 00:13:46 +0100 Subject: [PATCH] [Proxy] feat: implement relayer proxy full workflow (#101) * feat: add general proxy logic and server builder * fix: make var names and comments more consistent * feat: add relay verification and signature * fix: interface assignment * feat: implement relayer proxy full workflow * chore: improve code comments * chore: change native terms to proxied * chore: address change requests * chore: explain -32000 error code * chore: add log message on relaying success/failure * chore: Update AccountI/any unmarshaling comment Co-authored-by: Daniel Olshansky * chore: update import paths to use GitHub organization name * chore: address change requests * chore: add TODO for test implementation * chore: Refactor relay request & response methods in RelayerProxy * chore: Address request changes * chore: comment about current proxy signing approach * Merge remote-tracking branch 'origin/main' into feat/relayer-proxy * chore: Revert relay request & response signing and verification interface --------- Co-authored-by: Daniel Olshansky --- pkg/relayer/interface.go | 50 +++++++++++ pkg/relayer/proxy/error_reply.go | 41 +++++++++ pkg/relayer/proxy/errors.go | 7 +- pkg/relayer/proxy/interface.go | 46 ---------- pkg/relayer/proxy/jsonrpc.go | 132 ++++++++++++++++++++++++---- pkg/relayer/proxy/proxy.go | 42 +++++---- pkg/relayer/proxy/proxy_test.go | 3 + pkg/relayer/proxy/relay_builders.go | 55 ++++++++++++ pkg/relayer/proxy/relay_signer.go | 25 ++++++ pkg/relayer/proxy/relay_verifier.go | 76 ++++++++++++++++ pkg/relayer/proxy/server_builder.go | 6 +- proto/pocket/service/relay.proto | 9 +- 12 files changed, 399 insertions(+), 93 deletions(-) create mode 100644 pkg/relayer/proxy/error_reply.go delete mode 100644 pkg/relayer/proxy/interface.go create mode 100644 pkg/relayer/proxy/proxy_test.go create mode 100644 pkg/relayer/proxy/relay_builders.go create mode 100644 pkg/relayer/proxy/relay_signer.go create mode 100644 pkg/relayer/proxy/relay_verifier.go diff --git a/pkg/relayer/interface.go b/pkg/relayer/interface.go index 68714efd7..a6d4d293f 100644 --- a/pkg/relayer/interface.go +++ b/pkg/relayer/interface.go @@ -1,12 +1,62 @@ package relayer import ( + "context" + "github.com/pokt-network/smt" "github.com/pokt-network/poktroll/pkg/observable" + "github.com/pokt-network/poktroll/x/service/types" sessiontypes "github.com/pokt-network/poktroll/x/session/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) +// RelayerProxy is the interface for the proxy that serves relays to the application. +// It is responsible for starting and stopping all supported RelayServers. +// While handling requests and responding in a closed loop, it also notifies +// the miner about the relays that have been served. +type RelayerProxy interface { + + // Start starts all advertised relay servers and returns an error if any of them fail to start. + Start(ctx context.Context) error + + // Stop stops all advertised relay servers and returns an error if any of them fail. + Stop(ctx context.Context) error + + // ServedRelays returns an observable that notifies the miner about the relays that have been served. + // A served relay is one whose RelayRequest's signature and session have been verified, + // and its RelayResponse has been signed and successfully sent to the client. + ServedRelays() observable.Observable[*types.Relay] + + // VerifyRelayRequest is a shared method used by RelayServers to check the + // relay request signature and session validity. + // TODO_TECHDEBT(@red-0ne): This method should be moved out of the RelayerProxy interface + // that should not be responsible for verifying relay requests. + VerifyRelayRequest( + ctx context.Context, + relayRequest *types.RelayRequest, + service *sharedtypes.Service, + ) error + + // SignRelayResponse is a shared method used by RelayServers to sign + // and append the signature to the RelayResponse. + // TODO_TECHDEBT(@red-0ne): This method should be moved out of the RelayerProxy interface + // that should not be responsible for signing relay responses. + SignRelayResponse(relayResponse *types.RelayResponse) error +} + +// RelayServer is the interface of the advertised relay servers provided by the RelayerProxy. +type RelayServer interface { + // Start starts the service server and returns an error if it fails. + Start(ctx context.Context) error + + // Stop terminates the service server and returns an error if it fails. + Stop(ctx context.Context) error + + // Service returns the service to which the RelayServer relays. + Service() *sharedtypes.Service +} + // RelayerSessionsManager is an interface for managing the relayer's sessions and Sparse // Merkle Sum Trees (SMSTs). It provides notifications about closing sessions that are // ready to be claimed, and handles the creation and retrieval of SMSTs for a given session. diff --git a/pkg/relayer/proxy/error_reply.go b/pkg/relayer/proxy/error_reply.go new file mode 100644 index 000000000..d881057a0 --- /dev/null +++ b/pkg/relayer/proxy/error_reply.go @@ -0,0 +1,41 @@ +package proxy + +import ( + "log" + "net/http" + + "github.com/pokt-network/poktroll/x/service/types" +) + +// replyWithError builds a JSONRPCResponseError from the passed in error and writes it to the writer. +// TODO_TECHDEBT: This method should be aware of the request id and use it in the response by having +// the caller pass it along with the error if available. +// 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 (j *jsonRPCServer) 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 + } +} diff --git a/pkg/relayer/proxy/errors.go b/pkg/relayer/proxy/errors.go index 1aa42ab7e..4d2b56241 100644 --- a/pkg/relayer/proxy/errors.go +++ b/pkg/relayer/proxy/errors.go @@ -3,6 +3,9 @@ package proxy import sdkerrors "cosmossdk.io/errors" var ( - codespace = "relayer/proxy" - ErrUnsupportedRPCType = sdkerrors.Register(codespace, 1, "unsupported rpc type") + codespace = "relayer/proxy" + ErrUnsupportedRPCType = sdkerrors.Register(codespace, 1, "unsupported rpc type") + ErrInvalidRelayRequestSignature = sdkerrors.Register(codespace, 2, "invalid relay request signature") + ErrInvalidSession = sdkerrors.Register(codespace, 3, "invalid session") + ErrInvalidSupplier = sdkerrors.Register(codespace, 4, "invalid supplier") ) diff --git a/pkg/relayer/proxy/interface.go b/pkg/relayer/proxy/interface.go deleted file mode 100644 index df4232588..000000000 --- a/pkg/relayer/proxy/interface.go +++ /dev/null @@ -1,46 +0,0 @@ -package proxy - -import ( - "context" - - "github.com/pokt-network/poktroll/pkg/observable" - "github.com/pokt-network/poktroll/x/service/types" - sharedtypes "github.com/pokt-network/poktroll/x/shared/types" -) - -// RelayerProxy is the interface for the proxy that serves relays to the application. -// It is responsible for starting and stopping all supported RelayServers. -// While handling requests and responding in a closed loop, it also notifies -// the miner about the relays that have been served. -type RelayerProxy interface { - - // Start starts all advertised relay servers and returns an error if any of them fail to start. - Start(ctx context.Context) error - - // Stop stops all advertised relay servers and returns an error if any of them fail. - Stop(ctx context.Context) error - - // ServedRelays returns an observable that notifies the miner about the relays that have been served. - // A served relay is one whose RelayRequest's signature and session have been verified, - // and its RelayResponse has been signed and successfully sent to the client. - ServedRelays() observable.Observable[*types.Relay] - - // VerifyRelayRequest is a shared method used by RelayServers to check the - // relay request signature and session validity. - VerifyRelayRequest(relayRequest *types.RelayRequest) (isValid bool, err error) - - // SignRelayResponse is a shared method used by RelayServers to sign the relay response. - SignRelayResponse(relayResponse *types.RelayResponse) ([]byte, error) -} - -// RelayServer is the interface of the advertised relay servers provided by the RelayerProxy. -type RelayServer interface { - // Start starts the service server and returns an error if it fails. - Start(ctx context.Context) error - - // Stop terminates the service server and returns an error if it fails. - Stop(ctx context.Context) error - - // Service returns the service to which the RelayServer relays. - Service() *sharedtypes.Service -} diff --git a/pkg/relayer/proxy/jsonrpc.go b/pkg/relayer/proxy/jsonrpc.go index f6cc9cb22..8ea953d6f 100644 --- a/pkg/relayer/proxy/jsonrpc.go +++ b/pkg/relayer/proxy/jsonrpc.go @@ -1,15 +1,19 @@ package proxy import ( + "bytes" "context" + "io" + "log" "net/http" "net/url" + "github.com/pokt-network/poktroll/pkg/relayer" "github.com/pokt-network/poktroll/x/service/types" sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) -var _ RelayServer = (*jsonRPCServer)(nil) +var _ relayer.RelayServer = (*jsonRPCServer)(nil) type jsonRPCServer struct { // service is the service that the server is responsible for. @@ -22,28 +26,28 @@ type jsonRPCServer struct { // proxiedServiceEndpoint is the address of the proxied service that the server relays requests to. proxiedServiceEndpoint url.URL - // server is the http server that listens for incoming relay requests. + // server is the HTTP server that listens for incoming relay requests. server *http.Server // relayerProxy is the main relayer proxy that the server uses to perform its operations. - relayerProxy RelayerProxy + relayerProxy relayer.RelayerProxy - // servedRelaysProducer is a channel that emits the relays that have been served so that the - // servedRelays observable can fan out the notifications to its subscribers. + // servedRelaysProducer is a channel that emits the relays that have been served, allowing + // the servedRelays observable to fan-out notifications to its subscribers. servedRelaysProducer chan<- *types.Relay } // NewJSONRPCServer creates a new HTTP server that listens for incoming relay requests // and forwards them to the supported proxied service endpoint. // It takes the serviceId, endpointUrl, and the main RelayerProxy as arguments and returns -// a RelayServer that listens to incoming RelayRequests +// a RelayServer that listens to incoming RelayRequests. func NewJSONRPCServer( service *sharedtypes.Service, supplierEndpoint *sharedtypes.SupplierEndpoint, proxiedServiceEndpoint url.URL, servedRelaysProducer chan<- *types.Relay, - proxy RelayerProxy, -) RelayServer { + proxy relayer.RelayerProxy, +) relayer.RelayServer { return &jsonRPCServer{ service: service, serverEndpoint: supplierEndpoint, @@ -57,18 +61,18 @@ func NewJSONRPCServer( // Start starts the service server and returns an error if it fails. // It also waits for the passed in context to end before shutting down. // This method is blocking and should be called in a goroutine. -func (j *jsonRPCServer) Start(ctx context.Context) error { +func (jsrv *jsonRPCServer) Start(ctx context.Context) error { go func() { <-ctx.Done() - j.server.Shutdown(ctx) + jsrv.server.Shutdown(ctx) }() - return j.server.ListenAndServe() + return jsrv.server.ListenAndServe() } // Stop terminates the service server and returns an error if it fails. -func (j *jsonRPCServer) Stop(ctx context.Context) error { - return j.server.Shutdown(ctx) +func (jsrv *jsonRPCServer) Stop(ctx context.Context) error { + return jsrv.server.Shutdown(ctx) } // Service returns the JSON-RPC service. @@ -80,6 +84,104 @@ func (j *jsonRPCServer) Service() *sharedtypes.Service { // method of the http.Handler interface. It is called by http.ListenAndServe() // when jsonRPCServer is used as an http.Handler with an http.Server. // (see https://pkg.go.dev/net/http#Handler) -func (j *jsonRPCServer) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - panic("TODO: implement jsonRPCServer.ServeHTTP") +func (jsrv *jsonRPCServer) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + ctx := request.Context() + // Relay the request to the proxied service and build the response that will be sent back to the client. + relay, err := jsrv.serveHTTP(ctx, request) + if err != nil { + // Reply with an error if the relay could not be served. + jsrv.replyWithError(writer, err) + log.Printf("WARN: failed serving relay request: %s", err) + return + } + + // Send the relay response to the client. + if err := jsrv.sendRelayResponse(relay.Res, writer); err != nil { + jsrv.replyWithError(writer, err) + log.Printf("WARN: failed sending relay response: %s", err) + return + } + + log.Printf( + "INFO: relay request served successfully for application %s, service %s, session start block height %d, proxied service %s", + relay.Res.Meta.SessionHeader.ApplicationAddress, + relay.Res.Meta.SessionHeader.Service.Id, + relay.Res.Meta.SessionHeader.SessionStartBlockHeight, + jsrv.serverEndpoint.Url, + ) + + // Emit the relay to the servedRelays observable. + jsrv.servedRelaysProducer <- relay +} + +// serveHTTP holds the underlying logic of ServeHTTP. +func (jsrv *jsonRPCServer) serveHTTP(ctx context.Context, request *http.Request) (*types.Relay, error) { + // Extract the relay request from the request body. + relayRequest, err := jsrv.newRelayRequest(request) + if err != nil { + return nil, err + } + + // Verify the relay request signature and session. + // TODO_TECHDEBT(red-0ne): Currently, the relayer proxy is responsible for verifying + // the relay request signature. This responsibility should be shifted to the relayer itself. + // Consider using a middleware pattern to handle non-proxy specific logic, such as + // request signature verification, session verification, and response signature. + // This would help in separating concerns and improving code maintainability. + // See https://github.com/pokt-network/poktroll/issues/160 + if err = jsrv.relayerProxy.VerifyRelayRequest(ctx, relayRequest, jsrv.service); err != nil { + return nil, err + } + + // Get the relayRequest payload's `io.ReadCloser` to add it to the http.Request + // that will be sent to the proxied (i.e. staked for) service. + // (see https://pkg.go.dev/net/http#Request) Body field type. + var payloadBz []byte + if _, err = relayRequest.Payload.MarshalTo(payloadBz); err != nil { + return nil, err + } + requestBodyReader := io.NopCloser(bytes.NewBuffer(payloadBz)) + + // Build the request to be sent to the native service by substituting + // the destination URL's host with the native service's listen address. + destinationURL, err := url.Parse(request.URL.String()) + if err != nil { + return nil, err + } + destinationURL.Host = jsrv.proxiedServiceEndpoint.Host + + relayHTTPRequest := &http.Request{ + Method: request.Method, + Header: request.Header, + URL: destinationURL, + Host: destinationURL.Host, + Body: requestBodyReader, + } + + // Send the relay request to the native service. + httpResponse, err := http.DefaultClient.Do(relayHTTPRequest) + if err != nil { + return nil, err + } + + // Build the relay response from the native service response + // Use relayRequest.Meta.SessionHeader on the relayResponse session header since it was verified to be valid + // and has to be the same as the relayResponse session header. + relayResponse, err := jsrv.newRelayResponse(httpResponse, relayRequest.Meta.SessionHeader) + if err != nil { + return nil, err + } + + return &types.Relay{Req: relayRequest, Res: relayResponse}, nil +} + +// sendRelayResponse marshals the relay response and sends it to the client. +func (j *jsonRPCServer) sendRelayResponse(relayResponse *types.RelayResponse, writer http.ResponseWriter) error { + relayResponseBz, err := relayResponse.Marshal() + if err != nil { + return err + } + + _, err = writer.Write(relayResponseBz) + return err } diff --git a/pkg/relayer/proxy/proxy.go b/pkg/relayer/proxy/proxy.go index 033e9caaf..6c64516cd 100644 --- a/pkg/relayer/proxy/proxy.go +++ b/pkg/relayer/proxy/proxy.go @@ -9,23 +9,28 @@ import ( accounttypes "github.com/cosmos/cosmos-sdk/x/auth/types" "golang.org/x/sync/errgroup" - // TODO_INCOMPLETE(@red-0ne): Import the appropriate block client interface once available. - // blocktypes "github.com/pokt-network/poktroll/pkg/client" + blocktypes "github.com/pokt-network/poktroll/pkg/client" "github.com/pokt-network/poktroll/pkg/observable" "github.com/pokt-network/poktroll/pkg/observable/channel" + "github.com/pokt-network/poktroll/pkg/relayer" "github.com/pokt-network/poktroll/x/service/types" sessiontypes "github.com/pokt-network/poktroll/x/session/types" suppliertypes "github.com/pokt-network/poktroll/x/supplier/types" ) -var _ RelayerProxy = (*relayerProxy)(nil) +var _ relayer.RelayerProxy = (*relayerProxy)(nil) type ( serviceId = string - relayServersMap = map[serviceId][]RelayServer + relayServersMap = map[serviceId][]relayer.RelayServer servicesEndpointsMap = map[serviceId]url.URL ) +// relayerProxy is the main relayer proxy that takes relay requests of supported services from the client +// and proxies them to the supported proxied services. +// It is responsible for notifying the miner about the relays that have been served so they can be counted +// when the miner enters the claim/proof phase. +// TODO_TEST: Have tests for the relayer proxy. type relayerProxy struct { // keyName is the supplier's key name in the Cosmos's keybase. It is used along with the keyring to // get the supplier address and sign the relay responses. @@ -34,8 +39,7 @@ type relayerProxy struct { // blocksClient is the client used to get the block at the latest height from the blockchain // and be notified of new incoming blocks. It is used to update the current session data. - // TODO_INCOMPLETE(@red-0ne): Uncomment once the BlockClient interface is available. - // blockClient blocktypes.BlockClient + blockClient blocktypes.BlockClient // accountsQuerier is the querier used to get account data (e.g. app publicKey) from the blockchain, // which, in the context of the RelayerProxy, is used to verify the relay request signatures. @@ -63,25 +67,28 @@ type relayerProxy struct { // servedRelaysProducer is a channel that emits the relays that have been served so that the // servedRelays observable can fan out the notifications to its subscribers. servedRelaysProducer chan<- *types.Relay + + // clientCtx is the Cosmos' client context used to build the needed query clients and unmarshal their replies. + clientCtx sdkclient.Context + + // supplierAddress is the address of the supplier that the relayer proxy is running for. + supplierAddress string } func NewRelayerProxy( - ctx context.Context, clientCtx sdkclient.Context, keyName string, keyring keyring.Keyring, proxiedServicesEndpoints servicesEndpointsMap, - // TODO_INCOMPLETE(@red-0ne): Uncomment once the BlockClient interface is available. - // blockClient blocktypes.BlockClient, -) RelayerProxy { + blockClient blocktypes.BlockClient, +) relayer.RelayerProxy { accountQuerier := accounttypes.NewQueryClient(clientCtx) supplierQuerier := suppliertypes.NewQueryClient(clientCtx) sessionQuerier := sessiontypes.NewQueryClient(clientCtx) servedRelays, servedRelaysProducer := channel.NewObservable[*types.Relay]() return &relayerProxy{ - // TODO_INCOMPLETE(@red-0ne): Uncomment once the BlockClient interface is available. - // blockClient: blockClient, + blockClient: blockClient, keyName: keyName, keyring: keyring, accountsQuerier: accountQuerier, @@ -90,6 +97,7 @@ func NewRelayerProxy( proxiedServicesEndpoints: proxiedServicesEndpoints, servedRelays: servedRelays, servedRelaysProducer: servedRelaysProducer, + clientCtx: clientCtx, } } @@ -137,13 +145,3 @@ func (rp *relayerProxy) Stop(ctx context.Context) error { func (rp *relayerProxy) ServedRelays() observable.Observable[*types.Relay] { return rp.servedRelays } - -// VerifyRelayRequest is a shared method used by RelayServers to check the relay request signature and session validity. -func (rp *relayerProxy) VerifyRelayRequest(relayRequest *types.RelayRequest) (isValid bool, err error) { - panic("TODO: implement relayerProxy.VerifyRelayRequest") -} - -// SignRelayResponse is a shared method used by RelayServers to sign the relay response. -func (rp *relayerProxy) SignRelayResponse(relayResponse *types.RelayResponse) ([]byte, error) { - panic("TODO: implement relayerProxy.SignRelayResponse") -} diff --git a/pkg/relayer/proxy/proxy_test.go b/pkg/relayer/proxy/proxy_test.go new file mode 100644 index 000000000..1fd0237f5 --- /dev/null +++ b/pkg/relayer/proxy/proxy_test.go @@ -0,0 +1,3 @@ +package proxy_test + +// TODO: Add tests the the relayerProxy and its jsonRPC component. diff --git a/pkg/relayer/proxy/relay_builders.go b/pkg/relayer/proxy/relay_builders.go new file mode 100644 index 000000000..beb400cad --- /dev/null +++ b/pkg/relayer/proxy/relay_builders.go @@ -0,0 +1,55 @@ +package proxy + +import ( + "io" + "net/http" + + "github.com/pokt-network/poktroll/x/service/types" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" +) + +// newRelayRequest builds a RelayRequest from an http.Request. +func (j *jsonRPCServer) newRelayRequest(request *http.Request) (*types.RelayRequest, error) { + requestBz, err := io.ReadAll(request.Body) + if err != nil { + return nil, err + } + + var relayRequest types.RelayRequest + if err := relayRequest.Unmarshal(requestBz); err != nil { + return nil, err + } + + return &relayRequest, nil +} + +// newRelayResponse builds a RelayResponse from an http.Response and a SessionHeader. +// It also signs the RelayResponse and assigns it to RelayResponse.Meta.SupplierSignature. +// If the response has a non-nil body, it will be parsed as a JSONRPCResponsePayload. +func (j *jsonRPCServer) newRelayResponse( + response *http.Response, + sessionHeader *sessiontypes.SessionHeader, +) (*types.RelayResponse, error) { + relayResponse := &types.RelayResponse{ + Meta: &types.RelayResponseMetadata{SessionHeader: sessionHeader}, + } + + responseBz, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + + jsonRPCResponse := &types.JSONRPCResponsePayload{} + if err := jsonRPCResponse.Unmarshal(responseBz); err != nil { + return nil, err + } + + relayResponse.Payload = &types.RelayResponse_JsonRpcPayload{JsonRpcPayload: jsonRPCResponse} + + // Sign the relay response and add the signature to the relay response metadata + if err = j.relayerProxy.SignRelayResponse(relayResponse); err != nil { + return nil, err + } + + return relayResponse, nil +} diff --git a/pkg/relayer/proxy/relay_signer.go b/pkg/relayer/proxy/relay_signer.go new file mode 100644 index 000000000..bc172af00 --- /dev/null +++ b/pkg/relayer/proxy/relay_signer.go @@ -0,0 +1,25 @@ +package proxy + +import ( + "github.com/cometbft/cometbft/crypto" + + "github.com/pokt-network/poktroll/x/service/types" +) + +// SignRelayResponse is a shared method used by the RelayServers to sign the hash of the RelayResponse. +// It uses the keyring and keyName to sign the payload and returns the signature. +// TODO_TECHDEBT(@red-0ne): This method should be moved out of the RelayerProxy interface +// that should not be responsible for signing relay responses. +// See https://github.com/pokt-network/poktroll/issues/160 for a better design. +func (rp *relayerProxy) SignRelayResponse(relayResponse *types.RelayResponse) error { + var responseBz []byte + _, err := relayResponse.MarshalTo(responseBz) + if err != nil { + return err + } + + hash := crypto.Sha256(responseBz) + relayResponse.Meta.SupplierSignature, _, err = rp.keyring.Sign(rp.keyName, hash) + + return err +} diff --git a/pkg/relayer/proxy/relay_verifier.go b/pkg/relayer/proxy/relay_verifier.go new file mode 100644 index 000000000..54f1685a4 --- /dev/null +++ b/pkg/relayer/proxy/relay_verifier.go @@ -0,0 +1,76 @@ +package proxy + +import ( + "context" + + "github.com/cometbft/cometbft/crypto" + accounttypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + "github.com/pokt-network/poktroll/x/service/types" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" +) + +// VerifyRelayRequest is a shared method used by RelayServers to check the relay request signature and session validity. +func (rp *relayerProxy) VerifyRelayRequest( + ctx context.Context, + relayRequest *types.RelayRequest, + service *sharedtypes.Service, +) error { + // Query for the application account to get the application's public key to verify the relay request signature. + applicationAddress := relayRequest.Meta.SessionHeader.ApplicationAddress + accQueryReq := &accounttypes.QueryAccountRequest{Address: applicationAddress} + accQueryRes, err := rp.accountsQuerier.Account(ctx, accQueryReq) + if err != nil { + return err + } + + var payloadBz []byte + if _, err = relayRequest.Payload.MarshalTo(payloadBz); err != nil { + return err + } + hash := crypto.Sha256(payloadBz) + + account := new(accounttypes.BaseAccount) + if err := account.Unmarshal(accQueryRes.Account.Value); err != nil { + return err + } + + if !account.GetPubKey().VerifySignature(hash, relayRequest.Meta.Signature) { + return ErrInvalidRelayRequestSignature + } + + // Query for the current session to check if relayRequest sessionId matches the current session. + currentBlock := rp.blockClient.LatestBlock(ctx) + sessionQuery := &sessiontypes.QueryGetSessionRequest{ + ApplicationAddress: applicationAddress, + Service: service, + BlockHeight: currentBlock.Height(), + } + sessionResponse, err := rp.sessionQuerier.GetSession(ctx, sessionQuery) + if err != nil { + return err + } + + session := sessionResponse.Session + + // Since the retrieved sessionId was in terms of: + // - the current block height (which is not provided by the relayRequest) + // - serviceId (which is not provided by the relayRequest) + // - applicationAddress (which is used to to verify the relayRequest signature) + // we can reduce the session validity check to checking if the retrieved session's sessionId + // matches the relayRequest sessionId. + // TODO_INVESTIGATE: Revisit the assumptions above at some point in the future, but good enough for now. + if session.SessionId != relayRequest.Meta.SessionHeader.SessionId { + return ErrInvalidSession + } + + // Check if the relayRequest is allowed to be served by the relayer proxy. + for _, supplier := range session.Suppliers { + if supplier.Address == rp.supplierAddress { + return nil + } + } + + return ErrInvalidSupplier +} diff --git a/pkg/relayer/proxy/server_builder.go b/pkg/relayer/proxy/server_builder.go index 53e0c4d14..a821b5752 100644 --- a/pkg/relayer/proxy/server_builder.go +++ b/pkg/relayer/proxy/server_builder.go @@ -3,6 +3,7 @@ package proxy import ( "context" + "github.com/pokt-network/poktroll/pkg/relayer" sharedtypes "github.com/pokt-network/poktroll/x/shared/types" suppliertypes "github.com/pokt-network/poktroll/x/supplier/types" ) @@ -31,10 +32,10 @@ func (rp *relayerProxy) BuildProvidedServices(ctx context.Context) error { for _, serviceConfig := range services { service := serviceConfig.Service proxiedServicesEndpoints := rp.proxiedServicesEndpoints[service.Id] - serviceEndpoints := make([]RelayServer, len(serviceConfig.Endpoints)) + serviceEndpoints := make([]relayer.RelayServer, len(serviceConfig.Endpoints)) for _, endpoint := range serviceConfig.Endpoints { - var server RelayServer + var server relayer.RelayServer // Switch to the RPC type to create the appropriate RelayServer switch endpoint.RpcType { @@ -57,6 +58,7 @@ func (rp *relayerProxy) BuildProvidedServices(ctx context.Context) error { } rp.advertisedRelayServers = providedServices + rp.supplierAddress = supplierQueryResponse.Supplier.Address return nil } diff --git a/proto/pocket/service/relay.proto b/proto/pocket/service/relay.proto index 3450a1e40..7c9e888e4 100644 --- a/proto/pocket/service/relay.proto +++ b/proto/pocket/service/relay.proto @@ -4,9 +4,8 @@ package pocket.service; option go_package = "github.com/pokt-network/poktroll/x/service/types"; import "cosmos_proto/cosmos.proto"; -// TODO(@Olshansk): Uncomment the line below once the `service.proto` is added. -// import "pocket/service/service.proto"; import "pocket/application/application.proto"; +import "pocket/session/session.proto"; // Relay contains both the RelayRequest (signed by the Application) and the RelayResponse (signed by the Supplier). // The serialized tuple is inserted into the SMST leaves as values in the Claim/Proof lifecycle. @@ -17,8 +16,7 @@ message Relay { // RelayRequestMetadata contains the metadata for a RelayRequest. message RelayRequestMetadata { - // TODO(@Olshansk): Uncomment the line below once the `service.proto` is added. - // session.SessionHeader session_header = 1; // Session header associated with the relay. + session.SessionHeader session_header = 1; // Session header associated with the relay. // TODO_COMMENT(@h5law): Add link or more details to how this is related to ring signatures once implemented. bytes signature = 2; // The request signature. This may be the application signature, or any gateway it delegated to. } @@ -77,8 +75,7 @@ message RelayResponse { // RelayResponseMetadata contains the metadata for a RelayResponse. message RelayResponseMetadata { - // TODO(@Olshansk): Uncomment the line below once the `service.proto` is added. - // session.SessionHeader session_header = 1; // Session header associated with the relay. + session.SessionHeader session_header = 1; // Session header associated with the relay. bytes supplier_signature = 2; // Signature of the supplier on the response. }