Skip to content

Commit

Permalink
Merge pull request #1604 from lavanet/CNS-1003-reputation-proto-defin…
Browse files Browse the repository at this point in the history
…itions

feat: Reputation: CNS-1003: reputation proto definitions
  • Loading branch information
Yaroms authored Jan 19, 2025
2 parents 344ad89 + 349311a commit 8943739
Show file tree
Hide file tree
Showing 50 changed files with 6,544 additions and 587 deletions.
15 changes: 11 additions & 4 deletions proto/lavanet/lava/pairing/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "lavanet/lava/pairing/params.proto";
import "lavanet/lava/pairing/epoch_cu.proto";
import "lavanet/lava/fixationstore/fixation.proto";
import "lavanet/lava/timerstore/timer.proto";
import "lavanet/lava/pairing/reputation.proto";

// this line is used by starport scaffolding # genesis/proto/import

Expand All @@ -18,17 +19,16 @@ message BadgeUsedCu {

// GenesisState defines the pairing module's genesis state.
message GenesisState {
reserved 2,3,4,7;
Params params = 1 [(gogoproto.nullable) = false];
reserved 2;
reserved 3;
reserved 4;
repeated BadgeUsedCu badgeUsedCuList = 5 [(gogoproto.nullable) = false];
lavanet.lava.timerstore.GenesisState badgesTS = 6 [(gogoproto.nullable) = false];
lavanet.lava.fixationstore.GenesisState providerQosFS = 7 [(gogoproto.nullable) = false];
repeated UniqueEpochSessionGenesis unique_epoch_sessions = 8 [(gogoproto.nullable) = false];
repeated ProviderEpochCuGenesis provider_epoch_cus = 9 [(gogoproto.nullable) = false];
repeated ProviderEpochComplainerCuGenesis provider_epoch_complained_cus = 10 [(gogoproto.nullable) = false];
repeated ProviderConsumerEpochCuGenesis provider_consumer_epoch_cus = 11 [(gogoproto.nullable) = false];
repeated ReputationGenesis reputations = 12 [(gogoproto.nullable) = false];
lavanet.lava.fixationstore.GenesisState reputation_scores = 13 [(gogoproto.nullable) = false];
// this line is used by starport scaffolding # genesis/proto/state
}

Expand Down Expand Up @@ -60,4 +60,11 @@ message ProviderConsumerEpochCuGenesis {
string project = 3;
string chain_id = 4;
ProviderConsumerEpochCu provider_consumer_epoch_cu = 5 [(gogoproto.nullable) = false];
}

message ReputationGenesis {
string chain_id = 1;
string cluster = 2;
string provider = 3;
Reputation reputation = 4 [(gogoproto.nullable) = false];
}
2 changes: 1 addition & 1 deletion proto/lavanet/lava/pairing/params.proto
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ message Params {
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
int64 reputation_half_life_factor = 17 [(gogoproto.moretags) = "yaml:\"reputation_half_life_factor\""];
uint64 reputation_half_life_factor = 17 [(gogoproto.moretags) = "yaml:\"reputation_half_life_factor\""];
uint64 reputation_relay_failure_cost = 18 [(gogoproto.moretags) = "yaml:\"reputation_relay_failure_cost\""];
}
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/{provider}/{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 provider = 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];
}
7 changes: 7 additions & 0 deletions proto/lavanet/lava/pairing/relay.proto
Original file line number Diff line number Diff line change
Expand Up @@ -95,16 +95,23 @@ message RelayReply {
}

message QualityOfServiceReport{
// Latency of provider answers in milliseconds, range 0-inf, lower is better
string latency = 1 [
(gogoproto.moretags) = "yaml:\"Latency\"",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];

// Percentage of times the provider returned a non-error response, range 0-1, higher is better
string availability = 2 [
(gogoproto.moretags) = "yaml:\"availability\"",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];

// Amount of time the provider is not synced (have the latest block) in milliseconds, range 0-inf, lower is better.
// Example: in ETH we have 15sec block time. So sync = 15000 means that the provider is one block
// behind the actual latest block.
string sync = 3 [
(gogoproto.moretags) = "yaml:\"sync\"",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
Expand Down
44 changes: 44 additions & 0 deletions proto/lavanet/lava/pairing/reputation.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
syntax = "proto3";
package lavanet.lava.pairing;

option go_package = "github.com/lavanet/lava/v4/x/pairing/types";
import "gogoproto/gogo.proto";
import "cosmos/base/v1beta1/coin.proto";

// Frac is a fracture struct that helps calculating and updating weighted average on the go
message Frac {
string num = 1 [(gogoproto.moretags) = "yaml:\"num\"", (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false]; // weighted average numerator (w1*s1+w2*s2+...)
string denom = 2 [(gogoproto.moretags) = "yaml:\"denom\"", (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false]; // weighted denominator (w1+w2+...)
}

// QosScore holds the QoS score from a QoS excellence report. The score and its variance are updated over time using a weighted average.
// Currently, the weight is the amount of relays that are associated with the QoS report.
message QosScore {
Frac score = 1 [(gogoproto.nullable) = false];
Frac variance = 2 [(gogoproto.nullable) = false];
}

// Reputation keeps the QosScore of a provider for a specific cluster for in the provider's geolocation.
// The store key is provider+chain+cluster.
// The epoch_score is a QosScore object that is aggregated over an epoch. When an epoch ends, the "score" field is updated
// with the epoch_score and the epoch_score is reset.
// The time_last_updated is used to calculate the appropriate time decay upon update.
// The creation_time is used to determine if the variance stabilization period has passed and score can be truncated.
// The stake is used when converting the reputation QoS scores to repuatation pairing score.
message Reputation {
QosScore score = 1 [(gogoproto.nullable) = false];
QosScore epoch_score = 2 [(gogoproto.nullable) = false];
int64 time_last_updated = 3;
int64 creation_time = 4;
cosmos.base.v1beta1.Coin stake = 5 [(gogoproto.nullable) = false];
}

// ReputationPairingScore holds the reputation pairing score used by the reputation pairing requirement.
// The score is ranged between [0.5-2]. It's kept in the reputations fixation store with a provider+chain+cluster key.
message ReputationPairingScore {
string score = 1 [(gogoproto.moretags) = "yaml:\"score\"", (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false];
}

3 changes: 3 additions & 0 deletions protocol/lavaprotocol/request_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ func ConstructRelaySession(lavaChainID string, relayRequestData *pairingtypes.Re
copiedQOS := copyQoSServiceReport(singleConsumerSession.QoSInfo.LastQoSReport)
copiedExcellenceQOS := copyQoSServiceReport(singleConsumerSession.QoSInfo.LastExcellenceQoSReportRaw) // copy raw report for the node

// validate and fix QoS excellence report before sending it to the node
copiedExcellenceQOS.ValidateAndFixQoSExcellence()

return &pairingtypes.RelaySession{
SpecId: chainID,
ContentHash: sigs.HashMsg(relayRequestData.GetContentHashData()),
Expand Down
2 changes: 2 additions & 0 deletions scripts/test/cli_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,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
29 changes: 29 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{
Provider: 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 Expand Up @@ -1091,6 +1111,15 @@ func (ts *Tester) GetNextMonth(from time.Time) int64 {
return utils.NextMonth(from).UTC().Unix()
}

func (ts *Tester) BlockTimeDefault() time.Duration {
return ts.Keepers.Downtime.GetParams(ts.Ctx).DowntimeDuration
}

func (ts *Tester) EpochTimeDefault() time.Duration {
epochBlocks := ts.Keepers.Epochstorage.GetParams(ts.Ctx).EpochBlocks
return ts.BlockTimeDefault() * time.Duration(epochBlocks)
}

func (ts *Tester) AdvanceToBlock(block uint64) {
if block < ts.BlockHeight() {
panic("AdvanceToBlock: block in the past: " +
Expand Down
9 changes: 9 additions & 0 deletions utils/convert.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
package utils

import "math"

func SafeUint64ToInt64Convert(val uint64) int64 {
if val > math.MaxInt64 {
val = math.MaxInt64
}
return int64(val)
}

func Btof(b bool) float64 {
if b {
return 1
Expand Down
40 changes: 39 additions & 1 deletion utils/math.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
package utils

import "golang.org/x/exp/constraints"
import (
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"golang.org/x/exp/constraints"
)

const (
DecayFactorNaturalBaseString = "2.718281828459045235"
)

func Min[T constraints.Ordered](x, y T) T {
if x < y {
Expand All @@ -15,3 +23,33 @@ func Max[T constraints.Ordered](x, y T) T {
}
return y
}

// NaturalBaseExponentFraction calculates an exponent of the
// natural base e using a sdk.Dec. using the formula: e^(numerator / denominator)
// since it is not possible to directly calculate a power of a fraction,
// we're doing it in three steps:
// 1. Calculate e^numerator
// 2. Take the denominatorth root
// 3. Take the reciprocal (if negative=true)
func NaturalBaseExponentFraction(numerator, denominator int64, negative bool) math.LegacyDec {
numeratorUint64 := uint64(numerator)
denominatorUint64 := uint64(denominator)

e := sdk.MustNewDecFromStr(DecayFactorNaturalBaseString)

// Step 1: Take the bth root of e
eRoot, err := e.ApproxRoot(denominatorUint64)
if err != nil {
panic(err)
}

// Step 2: Calculate (e^(1/b))^a
result := eRoot.Power(numeratorUint64)

if negative {
// Step 3: Take the reciprocal
result = sdk.OneDec().Quo(result)
}

return result
}
17 changes: 12 additions & 5 deletions x/pairing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ The pairing module is one of Lava's core modules and is closely connected to the
* [Filters](#filters)
* [Scores](#scores)
* [Quality Of Service](#quality-of-service)
* [Reputation](#reputation)
* [Passable QoS](#passable-qos)
* [Pairing Verification](#pairing-verification)
* [Unresponsiveness](#unresponsiveness)
* [Static Providers](#static-providers)
Expand Down Expand Up @@ -197,17 +199,17 @@ Finally, we calculate the score of each provider for a specific slot and select

#### Quality Of Service

##### Excellence QoS
##### Reputation

The Lava Network places a strong emphasis on delivering exceptional Quality of Service (QoS) to its consumers. To ensure this, consumers actively participate in monitoring and customizing their QoS metrics. They gauge provider performance by measuring latency in provider responses relative to a benchmark, assessing data freshness in comparison to the fastest provider, and evaluating the percentage of error or timeout responses in the availability metric. These scores are diligently recorded and sent on-chain alongside the relay proofs of service, creating a transparent and accountable system.
The Lava Network places a strong emphasis on delivering exceptional Quality of Service (QoS) to its consumers. To ensure this, consumers actively participate in monitoring and customizing their QoS excellence metrics. They gauge provider performance by measuring latency in provider responses relative to a benchmark, assessing data freshness in comparison to the fastest provider, and evaluating the percentage of error or timeout responses in the availability metric. These scores are diligently recorded and sent on-chain alongside the relay proofs of service, creating a transparent and accountable system. The provider's performance metric is called "Reputation". Higher reputation indicates higher QoS scores.

To further enhance the integrity of the QoS scores, updates are aggregated across all consumers in a manner that safeguards against false reports. Negative reports are weighted by usage, meaning that a consumer must actively use and pay a provider to diminish their QoS score. This mechanism discourages users from artificially lowering a provider's score.

These QoS excellence metrics only affect pairings and are aggregated over time with a decay function that favors the latest data, meaning providers can improve, and those providers that their service fails will be impacted to affect fewer users. This approach ensures that the QoS system remains dynamic and responsive, benefiting providers striving to enhance their services while minimizing the impact of service failures on a broader scale.
The Reputation metric only affect pairings and is aggregated over time with a decay function that favors the latest data, meaning providers can improve, and those providers that their service fails will be impacted to affect fewer users. This approach ensures that the reputation system remains dynamic and responsive, benefiting providers striving to enhance their services while minimizing the impact of service failures on a broader scale.

##### QoS
##### Passable QoS

In the Lava Network, alongside the comprehensive Quality of Service of Excellence metrics, there exists an additional metric known as Passable QoS. Unlike Excellence QoS, which offers a broad range of values, Passable QoS operates on a binary scale, either assigning a value of 0 or 1, averaged over relays. This metric simplifies the evaluation of service quality to a binary determination, indicating whether a relay meets the Passable QoS threshold, meaning it provides a level of service deemed acceptable for use.
In the Lava Network, alongside the comprehensive Reputation metric (which is calculated using QoS excellence reports), there exists an additional metric known as Passable QoS. Unlike Reputation, which offers a broad range of values, Passable QoS operates on a binary scale, either assigning a value of 0 or 1, averaged over relays. This metric simplifies the evaluation of service quality to a binary determination, indicating whether a relay meets the Passable QoS threshold, meaning it provides a level of service deemed acceptable for use.

The Passable QoS score directly influences the total payout for a specific payment; however, it's important to note that only 50% of the payout is exposed to this metric (can be changed via governance). This allocation ensures a balance between incentivizing excellent service and discouraging poor performance.

Expand Down Expand Up @@ -350,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
Loading

0 comments on commit 8943739

Please sign in to comment.