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

Add an gRPC endpoint for request verification #45

Merged
merged 17 commits into from
May 6, 2021
Merged
20 changes: 20 additions & 0 deletions proto/oracle/v1/oracle.proto
Original file line number Diff line number Diff line change
Expand Up @@ -351,3 +351,23 @@ message ReportersPerValidator {
// validator
repeated string reporters = 2 [ (gogoproto.nullable) = false ];
}

// RequestVerification is a message that is constructed and signed by a reporter
// to be used as a part of verification of oracle request.
message RequestVerification {
option (gogoproto.equal) = true;
// ChainID is the ID of targeted chain
string chain_id = 1 [ (gogoproto.customname) = "ChainID" ];
// Validator is an validator address
string validator = 2;
// RequestID is the targeted request ID
int64 request_id = 3 [
(gogoproto.customname) = "RequestID",
(gogoproto.casttype) = "RequestID"
];
// ExternalID is the oracle's external ID of data source
int64 external_id = 4 [
(gogoproto.customname) = "ExternalID",
(gogoproto.casttype) = "ExternalID"
];
}
43 changes: 43 additions & 0 deletions proto/oracle/v1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ service Query {
option (google.api.http).post = "/oracle/request_prices";
}

// RequestVerification verifies a request to make sure that
// all information that will be used to report the data is valid
rpc RequestVerification(QueryRequestVerificationRequest)
returns (QueryRequestVerificationResponse) {
option (google.api.http).get = "/oracle/v1/verify_request";
}

// RequestPool queries the request pool information corresponding to the given
// port, channel, and request key.
rpc RequestPool(QueryRequestPoolRequest) returns (QueryRequestPoolResponse) {
Expand Down Expand Up @@ -235,6 +242,42 @@ message QueryRequestPriceResponse {
int64 min_count = 4;
}

// QueryRequestVerificationRequest is request type for the
// Query/RequestVerification RPC
message QueryRequestVerificationRequest {
// ChainID is the chain ID to identify which chain ID is used for the
// verification
string chain_id = 1;
// Validator is a validator address
string validator = 2;
// RequestID is oracle request ID
int64 request_id = 3;
// ExternalID is an oracle's external ID
int64 external_id = 4;
// Reporter is an bech32-encoded public key of the reporter authorized by the
// validator
string reporter = 5;
// Signature is a signature signed by the reporter using reporter's private
// key
bytes signature = 6;
}

// QueryRequestVerificationResponse is response type for the
// Query/RequestVerification RPC
message QueryRequestVerificationResponse {
// ChainID is the targeted chain ID
string chain_id = 1;
// Validator is the targeted validator address
string validator = 2;
// RequestID is the ID of targeted request
int64 request_id = 3;
// ExternalID is the ID of targeted oracle's external data source
int64 external_id = 4;
// DataSourceID is the ID of a data source that relates to the targeted
// external ID
int64 data_source_id = 5;
}

// QueryRequestPoolRequest is request type for the Query/RequestPool RPC method.
message QueryRequestPoolRequest {
// RequestKey is a user-generated key for each request pool
Expand Down
50 changes: 49 additions & 1 deletion x/oracle/client/cli/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
// "net/http"

"context"
"encoding/hex"
"fmt"
"strconv"

"github.com/cosmos/cosmos-sdk/client"
Expand Down Expand Up @@ -38,6 +40,7 @@ func GetQueryCmd() *cobra.Command {
GetQueryCmdReporters(),
GetQueryActiveValidators(),
// GetQueryPendingRequests(storeKey, cdc),
GetQueryRequestVerification(),
GetQueryRequestPool(),
)
return oracleCmd
Expand Down Expand Up @@ -224,7 +227,7 @@ func GetQueryCmdReporters() *cobra.Command {
return err
}
queryClient := types.NewQueryClient(clientCtx)
r, err := queryClient.Reporters(context.Background(), &types.QueryReportersRequest{ValidatorAddress: args[1]})
r, err := queryClient.Reporters(context.Background(), &types.QueryReportersRequest{ValidatorAddress: args[0]})
if err != nil {
return err
}
Expand Down Expand Up @@ -284,6 +287,51 @@ func GetQueryActiveValidators() *cobra.Command {
// }
// }

// GetQueryRequestVerification implements the query request verification command.
func GetQueryRequestVerification() *cobra.Command {
cmd := &cobra.Command{
Use: "verify-request [chain-id] [validator-addr] [request-id] [data-source-external-id] [reporter-pubkey] [reporter-signature-hex]",
Args: cobra.ExactArgs(6),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := types.NewQueryClient(clientCtx)
requestID, err := strconv.ParseInt(args[2], 10, 64)
if err != nil {
return fmt.Errorf("unable to parse request ID: %w", err)
}
externalID, err := strconv.ParseInt(args[3], 10, 64)
if err != nil {
return fmt.Errorf("unable to parse external ID: %w", err)
}

signature, err := hex.DecodeString(args[5])
if err != nil {
return fmt.Errorf("unable to parse signature: %w", err)
}

r, err := queryClient.RequestVerification(context.Background(), &types.QueryRequestVerificationRequest{
ChainId: args[0],
Validator: args[1],
RequestId: requestID,
ExternalId: externalID,
Reporter: args[4],
Signature: signature,
})
if err != nil {
return err
}

return clientCtx.PrintProto(r)
},
}
flags.AddQueryFlagsToCmd(cmd)

return cmd
}

// GetQueryRequestPool implements the query request pool command.
func GetQueryRequestPool() *cobra.Command {
cmd := &cobra.Command{
Expand Down
105 changes: 105 additions & 0 deletions x/oracle/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package keeper

import (
"context"
"fmt"

"github.com/bandprotocol/chain/x/oracle/types"
sdk "github.com/cosmos/cosmos-sdk/types"
Expand Down Expand Up @@ -156,6 +157,110 @@ func (k Querier) RequestPrice(c context.Context, req *types.QueryRequestPriceReq
return &types.QueryRequestPriceResponse{}, nil
}

// RequestVerification verifies oracle request for validation before executing data sources
func (k Querier) RequestVerification(c context.Context, req *types.QueryRequestVerificationRequest) (*types.QueryRequestVerificationResponse, error) {
// Request should not be empty
if req == nil {
return nil, status.Error(codes.InvalidArgument, "empty request")
}

ctx := sdk.UnwrapSDKContext(c)

// Provided chain ID should match current chain ID
if ctx.ChainID() != req.ChainId {
return nil, status.Error(codes.FailedPrecondition, fmt.Sprintf("provided chain ID does not match the validator's chain ID; expected %s, got %s", ctx.ChainID(), req.ChainId))
}

// Provided validator's address should be valid
validator, err := sdk.ValAddressFromBech32(req.Validator)
if err != nil {
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("unable to parse validator address: %s", err.Error()))
}

// Provided signature should be valid, which means this query request should be signed by the provided reporter
reporterPubKey, err := sdk.GetPubKeyFromBech32(sdk.Bech32PubKeyTypeAccPub, req.Reporter)
if err != nil {
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("unable to get reporter's public key: %s", err.Error()))
}
requestVerificationContent := types.NewRequestVerification(req.ChainId, validator, types.RequestID(req.RequestId), types.ExternalID(req.ExternalId))
signByte := requestVerificationContent.GetSignBytes()
if !reporterPubKey.VerifySignature(signByte, req.Signature) {
return nil, status.Error(codes.Unauthenticated, "invalid reporter's signature")
}

// Provided reporter should be authorized by the provided validator
reporters := k.GetReporters(ctx, validator)
reporter := sdk.AccAddress(reporterPubKey.Address().Bytes())
isReporterAuthorizedByValidator := false
for _, existingReporter := range reporters {
if reporter.Equals(existingReporter) {
isReporterAuthorizedByValidator = true
break
}
}
if !isReporterAuthorizedByValidator {
return nil, status.Error(codes.PermissionDenied, fmt.Sprintf("%s is not an authorized reporter of %s", reporter, req.Validator))
}

// Provided request should exist on chain
request, err := k.GetRequest(ctx, types.RequestID(req.RequestId))
if err != nil {
return nil, status.Error(codes.NotFound, fmt.Sprintf("unable to get request from chain: %s", err.Error()))
}

// Provided validator should be assigned to response to the request
isValidatorAssigned := false
for _, requestedValidator := range request.RequestedValidators {
v, _ := sdk.ValAddressFromBech32(requestedValidator)
if validator.Equals(v) {
isValidatorAssigned = true
break
}
}
if !isValidatorAssigned {
return nil, status.Error(codes.PermissionDenied, fmt.Sprintf("%s is not assigned for request ID %d", validator, req.RequestId))
}

// Provided external ID should be required by the request determined by oracle script
var dataSourceID *types.DataSourceID
for _, rawRequest := range request.RawRequests {
if rawRequest.ExternalID == types.ExternalID(req.ExternalId) {
dataSourceID = &rawRequest.DataSourceID
break
}
}
if dataSourceID == nil {
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("no data source required by the request %d found which relates to the external data source with ID %d.", req.RequestId, req.ExternalId))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should return 502 (Internal server error). If a request existed it should have a data source for every raw requests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is another possibility that a client sends some random external ID that does not exists in the request, which causes data source ID to be nil.

}

// Provided validator should not have reported data for the request
reports := k.GetReports(ctx, types.RequestID(req.RequestId))
isValidatorReported := false
for _, report := range reports {
reportVal, _ := sdk.ValAddressFromBech32(report.Validator)
if reportVal.Equals(validator) {
isValidatorReported = true
break
}
}
if isValidatorReported {
return nil, status.Error(codes.AlreadyExists, fmt.Sprintf("validator %s already submitted data report for this request", validator))
}

// The request should not be expired
if request.RequestHeight+int64(k.ExpirationBlockCount(ctx)) < ctx.BlockHeader().Height {
return nil, status.Error(codes.DeadlineExceeded, fmt.Sprintf("Request with ID %d is already expired", req.RequestId))
}

return &types.QueryRequestVerificationResponse{
ChainId: req.ChainId,
Validator: req.Validator,
RequestId: req.RequestId,
ExternalId: req.ExternalId,
DataSourceId: int64(*dataSourceID),
}, nil
}

// RequestPool queries the request pool information
func (k Querier) RequestPool(c context.Context, req *types.QueryRequestPoolRequest) (*types.QueryRequestPoolResponse, error) {
if req == nil {
Expand Down
Loading