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

feat: Reputation: CNS-1006 - Reputation queries #1631

Open
wants to merge 7 commits into
base: CNS-1005-reputation-pairing-score
Choose a base branch
from
52 changes: 49 additions & 3 deletions proto/lavanet/lava/pairing/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "gogoproto/gogo.proto";
import "google/api/annotations.proto";
import "cosmos/base/query/v1beta1/pagination.proto";
import "lavanet/lava/pairing/params.proto";
import "lavanet/lava/pairing/reputation.proto";
import "lavanet/lava/spec/spec.proto";


Expand Down Expand Up @@ -80,9 +81,19 @@ service Query {
}

// Queries a for the aggregated CU of all ProviderEpochCu objects all the providers.
rpc ProvidersEpochCu(QueryProvidersEpochCuRequest) returns (QueryProvidersEpochCuResponse) {
option (google.api.http).get = "/lavanet/lava/pairing/providers_epoch_cu";
}
rpc ProvidersEpochCu(QueryProvidersEpochCuRequest) returns (QueryProvidersEpochCuResponse) {
option (google.api.http).get = "/lavanet/lava/pairing/providers_epoch_cu";
}

// Queries a for a provider reputation.
rpc ProviderReputation(QueryProviderReputationRequest) returns (QueryProviderReputationResponse) {
option (google.api.http).get = "/lavanet/lava/pairing/provider_reputation/{address}/{chainID}/{cluster}";
}

// Queries a for a provider reputation's details (mainly for developers).
rpc ProviderReputationDetails(QueryProviderReputationDetailsRequest) returns (QueryProviderReputationDetailsResponse) {
option (google.api.http).get = "/lavanet/lava/pairing/provider_reputation_details/{address}/{chainID}/{cluster}";
}

// this line is used by starport scaffolding # 2

Expand Down Expand Up @@ -238,4 +249,39 @@ message QueryProvidersEpochCuResponse {
message ProviderCuInfo {
string provider = 1;
uint64 cu = 2;
}

message QueryProviderReputationRequest {
string address = 1;
string chainID = 2;
string cluster = 3;
}

message ReputationData {
uint64 rank = 1; // rank compared to other providers
uint64 providers = 2; // amount of providers with the same chainID+cluster
string overall_performance = 3; // overall performance metric which can be "good", "bad", or "low variance"
string chainID = 4;
string cluster = 5;
}

message QueryProviderReputationResponse {
repeated ReputationData data = 1 [(gogoproto.nullable) = false];
}

message QueryProviderReputationDetailsRequest {
string address = 1;
string chainID = 2;
string cluster = 3;
}

message ReputationDevData {
Reputation reputation = 1 [(gogoproto.nullable) = false];
ReputationPairingScore reputation_pairing_score = 2 [(gogoproto.nullable) = false];
string chainID = 4;
string cluster = 5;
}

message QueryProviderReputationDetailsResponse {
repeated ReputationDevData data = 1 [(gogoproto.nullable) = false];
}
2 changes: 2 additions & 0 deletions scripts/test/cli_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ trace lavad q pairing static-providers-list LAV1 >/dev/null
trace lavad q pairing user-entry $(lavad keys show alice -a) ETH1 20 >/dev/null
trace lavad q pairing verify-pairing STRK $(lavad keys show alice -a) $(lavad keys show alice -a) 60 >/dev/null
trace lavad q pairing provider-pairing-chance $(lavad keys show servicer1 -a) STRK 1 "" >/dev/null
trace lavad q pairing provider-reputation $(lavad keys show servicer1 -a) ETH1 free >/dev/null
trace lavad q pairing provider-reputation-details $(lavad keys show servicer1 -a) ETH1 free >/dev/null

echo "Testing dualstaking tx commands"
wait_count_blocks 1 >/dev/null
Expand Down
20 changes: 20 additions & 0 deletions testutil/common/tester.go
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,26 @@ func (ts *Tester) QueryPairingProviderEpochCu(provider string, project string, c
return ts.Keepers.Pairing.ProvidersEpochCu(ts.GoCtx, msg)
}

// QueryPairingProviderReputation implements 'q pairing provider-reputation'
func (ts *Tester) QueryPairingProviderReputation(provider string, chainID string, cluster string) (*pairingtypes.QueryProviderReputationResponse, error) {
msg := &pairingtypes.QueryProviderReputationRequest{
Address: provider,
ChainID: chainID,
Cluster: cluster,
}
return ts.Keepers.Pairing.ProviderReputation(ts.GoCtx, msg)
}

// QueryPairingProviderReputationDetails implements 'q pairing provider-reputation-details'
func (ts *Tester) QueryPairingProviderReputationDetails(provider string, chainID string, cluster string) (*pairingtypes.QueryProviderReputationDetailsResponse, error) {
msg := &pairingtypes.QueryProviderReputationDetailsRequest{
Address: provider,
ChainID: chainID,
Cluster: cluster,
}
return ts.Keepers.Pairing.ProviderReputationDetails(ts.GoCtx, msg)
}

// QueryPairingSubscriptionMonthlyPayout implements 'q pairing subscription-monthly-payout'
func (ts *Tester) QueryPairingSubscriptionMonthlyPayout(consumer string) (*pairingtypes.QuerySubscriptionMonthlyPayoutResponse, error) {
msg := &pairingtypes.QuerySubscriptionMonthlyPayoutRequest{
Expand Down
5 changes: 5 additions & 0 deletions x/pairing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,8 +352,13 @@ The pairing module supports the following queries:
| `list-epoch-payments` | none | show all epochPayment objects |
| `list-provider-payment-storage` | none | show all providerPaymentStorage objects |
| `list-unique-payment-storage-client-provider` | none | show all uniquePaymentStorageClientProvider objects |
| `provider` | chain-id (string) | show a provider staked on a specific chain |
| `provider-monthly-payout` | provider (string) | show the current monthly payout for a specific provider |
| `provider-pairing-chance` | provider (string), chain-id (string) | show the chance of a provider has to be part of the pairing list for a specific chain |
| `provider-reputation` | provider (string), chain-id (string), cluster (string) | show the provider's rank compared to other provider with the same chain-id and cluster by their reputation score |
| `provider-reputation-details` | provider (string), chain-id (string), cluster (string) | developer query to show the provider's reputation score raw data |
| `providers` | chain-id (string) | show all the providers staked on a specific chain |
| `providers-epoch-cu` | | developer query to list the amount of CU serviced by all the providers every epoch |
| `sdk-pairing` | none | query used by Lava-SDK to get all the required pairing info |
| `show-epoch-payments` | index (string) | show an epochPayment object by index |
| `show-provider-payment-storage` | index (string) | show a providerPaymentStorage object by index |
Expand Down
2 changes: 2 additions & 0 deletions x/pairing/client/cli/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ func GetQueryCmd(queryRoute string) *cobra.Command {
cmd.AddCommand(CmdSubscriptionMonthlyPayout())

cmd.AddCommand(CmdProvidersEpochCu())
cmd.AddCommand(CmdProviderReputation())
cmd.AddCommand(CmdProviderReputationDetails())

cmd.AddCommand(CmdDebugQuery())

Expand Down
65 changes: 65 additions & 0 deletions x/pairing/client/cli/query_provider_reputation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package cli

import (
"strconv"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/lavanet/lava/v4/utils"
"github.com/lavanet/lava/v4/x/pairing/types"
"github.com/spf13/cobra"
)

var _ = strconv.Itoa(0)

func CmdProviderReputation() *cobra.Command {
cmd := &cobra.Command{
Use: "provider-reputation [address] [chain-id] [cluster]",
Short: "Query for a provider's reputation. Use \"*\" for specify all for chain/cluster.",
Args: cobra.ExactArgs(3),
Example: `
Reputation of alice for chain ETH1 and the cluster "free":
lavad q pairing provider-reputation alice ETH1 free

Reputation of alice for all chains and the cluster "free":
lavad q pairing provider-reputation alice * free

Reputation of alice for ETH1 and for all clusters:
lavad q pairing provider-reputation alice ETH1 *

Reputation of alice for all chains and for all clusters:
lavad q pairing provider-reputation alice * *`,
RunE: func(cmd *cobra.Command, args []string) (err error) {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}

address, err := utils.ParseCLIAddress(clientCtx, args[0])
if err != nil {
return err
}
chainID := args[1]
cluster := args[2]

queryClient := types.NewQueryClient(clientCtx)

params := &types.QueryProviderReputationRequest{
Address: address,
ChainID: chainID,
Cluster: cluster,
}

res, err := queryClient.ProviderReputation(cmd.Context(), params)
if err != nil {
return err
}

return clientCtx.PrintProto(res)
},
}

flags.AddQueryFlagsToCmd(cmd)

return cmd
}
65 changes: 65 additions & 0 deletions x/pairing/client/cli/query_provider_reputation_details.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package cli

import (
"strconv"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/lavanet/lava/v4/utils"
"github.com/lavanet/lava/v4/x/pairing/types"
"github.com/spf13/cobra"
)

var _ = strconv.Itoa(0)

func CmdProviderReputationDetails() *cobra.Command {
cmd := &cobra.Command{
Use: "provider-reputation-details [address] [chain-id] [cluster]",
Short: "Query for a provider's reputation details. Mainly used by developers. Use \"*\" for specify all for chain/cluster.",
Args: cobra.ExactArgs(3),
Example: `
Reputation details of alice for chain ETH1 and the cluster "free":
lavad q pairing provider-reputation-details alice ETH1 free

Reputation details of alice for all chains and the cluster "free":
lavad q pairing provider-reputation-details alice * free

Reputation details of alice for ETH1 and for all clusters:
lavad q pairing provider-reputation-details alice ETH1 *

Reputation details of alice for all chains and for all clusters:
lavad q pairing provider-reputation-details alice * *`,
RunE: func(cmd *cobra.Command, args []string) (err error) {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}

address, err := utils.ParseCLIAddress(clientCtx, args[0])
if err != nil {
return err
}
chainID := args[1]
cluster := args[2]

queryClient := types.NewQueryClient(clientCtx)

params := &types.QueryProviderReputationDetailsRequest{
Address: address,
ChainID: chainID,
Cluster: cluster,
}

res, err := queryClient.ProviderReputationDetails(cmd.Context(), params)
if err != nil {
return err
}

return clientCtx.PrintProto(res)
},
}

flags.AddQueryFlagsToCmd(cmd)

return cmd
}
4 changes: 2 additions & 2 deletions x/pairing/client/cli/query_providers_epoch_cu.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import (
func CmdProvidersEpochCu() *cobra.Command {
cmd := &cobra.Command{
Use: "providers-epoch-cu",
Short: "Query to show the amount of CU serviced by all provider in a specific epoch",
Short: "Query to list the amount of CU serviced by all the providers every epoch",
Example: `
lavad q pairing providers-epoch-cu`,
Args: cobra.ExactArgs(1),
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) (err error) {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
Expand Down
113 changes: 113 additions & 0 deletions x/pairing/keeper/grpc_query_provider_reputation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package keeper

import (
"context"
"fmt"
"sort"

"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/lavanet/lava/v4/utils"
"github.com/lavanet/lava/v4/utils/lavaslices"
"github.com/lavanet/lava/v4/x/pairing/types"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

const (
varianceThreshold = float64(1) // decides if the overall_performance field can be calculated
percentileRank = float64(0.8) // rank for percentile to decide whether the overall_performance is "good" or "bad"
goodScore = "good"
badScore = "bad"
lowVariance = "low_variance"
)

func (k Keeper) ProviderReputation(goCtx context.Context, req *types.QueryProviderReputationRequest) (*types.QueryProviderReputationResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}

ctx := sdk.UnwrapSDKContext(goCtx)

chains := []string{req.ChainID}
if req.ChainID == "*" {
chains = k.specKeeper.GetAllChainIDs(ctx)
}

clusters := []string{req.Cluster}
if req.Cluster == "*" {
clusters = k.subscriptionKeeper.GetAllClusters(ctx)
}

// get all the reputation scores of the requested provider and gather valid chainID+cluster pairs
type chainClusterScore struct {
chainID string
cluster string
score math.LegacyDec
}
requestedProviderData := []chainClusterScore{}
for _, chainID := range chains {
for _, cluster := range clusters {
score, found := k.GetReputationScore(ctx, chainID, cluster, req.Address)
if !found {
continue
}
requestedProviderData = append(requestedProviderData, chainClusterScore{chainID: chainID, cluster: cluster, score: score})
}
}

// get scores from other providers for the relevant chains and clusters
res := []types.ReputationData{}
for _, data := range requestedProviderData {
chainClusterRes := types.ReputationData{ChainID: data.chainID, Cluster: data.cluster}

// get all reputation pairing score indices for a chainID+cluster pair
inds := k.reputationsFS.GetAllEntryIndicesWithPrefix(ctx, types.ReputationScoreKey(data.chainID, data.cluster, ""))

// collect all pairing scores with indices and sort in ascending order
pairingScores := []float64{}
for _, ind := range inds {
var score types.ReputationPairingScore
found := k.reputationsFS.FindEntry(ctx, ind, uint64(ctx.BlockHeight()), &score)
if !found {
return nil, utils.LavaFormatError("invalid reputationFS state", fmt.Errorf("reputation pairing score not found"),
utils.LogAttr("index", ind),
utils.LogAttr("block", ctx.BlockHeight()),
)
}
pairingScores = append(pairingScores, score.Score.MustFloat64())
}
sort.Slice(pairingScores, func(i, j int) bool {
return pairingScores[i] < pairingScores[j]
})

// find the provider's rank
rank := len(pairingScores)
for i, score := range pairingScores {
if data.score.MustFloat64() <= score {
rank -= i
break
}
}

// calculate the pairing scores variance
mean := lavaslices.Average(pairingScores)
variance := lavaslices.Variance(pairingScores, mean)

// create the reputation data and append
chainClusterRes.Rank = uint64(rank)
chainClusterRes.Providers = uint64(len(pairingScores))
if variance < varianceThreshold {
chainClusterRes.OverallPerformance = lowVariance
} else {
if pairingScores[len(pairingScores)-rank] > lavaslices.Percentile(pairingScores, percentileRank) {
chainClusterRes.OverallPerformance = goodScore
} else {
chainClusterRes.OverallPerformance = badScore
}
}
res = append(res, chainClusterRes)
}

return &types.QueryProviderReputationResponse{Data: res}, nil
}
Loading
Loading