Skip to content

Commit 39389c3

Browse files
committed
Implement --minimum_consensus parameter for vca reward command
Value in range [0.5, 1] The minimum consensus for a vCA ranking to be excluded from eligible rankings when in disagreement from simple majority. Simple majority is 50%. Qualified majority is 70%. Using 70% avoids punishing vCAs where the consensus is not clear. 70% is because when #vca == 3 consensus is only 66% and thus in this case, where there is just 1 vote in disagreement, all 3 vCAs get rewarded.
1 parent fffdd57 commit 39389c3

File tree

2 files changed

+92
-14
lines changed

2 files changed

+92
-14
lines changed

catalyst-toolbox/src/bin/cli/rewards/veterans.rs

+14
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ pub struct VeteransRewards {
6262
/// if the first cutoff is selected then the first modifier is used.
6363
#[structopt(long, required = true)]
6464
reputation_agreement_rate_modifiers: Vec<Decimal>,
65+
66+
/// Value in range [0.5, 1]
67+
/// The minimum consensus for a vCA ranking to be excluded from eligible rankings when in disagreement from simple majority.
68+
/// Simple majority is 50%.
69+
/// Qualified majority is 70%. Using 70% avoids punishing vCAs where the consensus is not clear.
70+
/// 70% is because when #vca == 3 consensus is only 66% and thus in this case, where there is just 1 vote in disagreement, all 3 vCAs get rewarded.
71+
#[structopt(long = "minimum_consensus")]
72+
minimum_consensus: Decimal,
6573
}
6674

6775
impl VeteransRewards {
@@ -77,6 +85,7 @@ impl VeteransRewards {
7785
rewards_agreement_rate_modifiers,
7886
reputation_agreement_rate_cutoffs,
7987
reputation_agreement_rate_modifiers,
88+
minimum_consensus,
8089
} = self;
8190
let reviews: Vec<VeteranRankingRow> = csv::load_data_from_csv::<_, b','>(&from)?;
8291

@@ -100,6 +109,10 @@ impl VeteransRewards {
100109
bail!("Expected rewards_agreement_rate_cutoffs to be descending");
101110
}
102111

112+
if minimum_consensus < Decimal::new(5,1) || minimum_consensus > Decimal::ONE {
113+
bail!("Expected minimum_consensus to range between .5 and 1");
114+
}
115+
103116
let results = veterans::calculate_veteran_advisors_incentives(
104117
&reviews,
105118
total_rewards,
@@ -113,6 +126,7 @@ impl VeteransRewards {
113126
.into_iter()
114127
.zip(reputation_agreement_rate_modifiers.into_iter())
115128
.collect(),
129+
Decimal::from(minimum_consensus),
116130
);
117131

118132
csv::dump_data_to_csv(rewards_to_csv_data(results).iter(), &to).unwrap();

catalyst-toolbox/src/rewards/veterans.rs

+78-14
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,29 @@ fn calc_final_eligible_rankings(
7777
.collect()
7878
}
7979

80+
fn calc_final_ranking_consensus_per_review(rankings: &[impl Borrow<VeteranRankingRow>]) -> Decimal {
81+
let rankings_majority = Decimal::from(rankings.len()) / Decimal::from(2);
82+
let ranks = rankings.iter().counts_by(|r| r.borrow().score());
83+
84+
match (ranks.get(&FilteredOut), ranks.get(&Excellent)) {
85+
(Some(filtered_out), _) if Decimal::from(*filtered_out) >= rankings_majority => {
86+
Decimal::from(*filtered_out) / Decimal::from(rankings.len())
87+
}
88+
(_, Some(excellent)) if Decimal::from(*excellent) > rankings_majority => {
89+
Decimal::from(*excellent) / Decimal::from(rankings.len())
90+
}
91+
_ => Decimal::from(*ranks.get(&Good).unwrap()) / Decimal::from(rankings.len()),
92+
}
93+
}
94+
8095
pub fn calculate_veteran_advisors_incentives(
8196
veteran_rankings: &[VeteranRankingRow],
8297
total_rewards: Rewards,
8398
rewards_thresholds: EligibilityThresholds,
8499
reputation_thresholds: EligibilityThresholds,
85100
rewards_mod_args: Vec<(Decimal, Decimal)>,
86101
reputation_mod_args: Vec<(Decimal, Decimal)>,
102+
minimum_consensus: Decimal,
87103
) -> HashMap<VeteranAdvisorId, VeteranAdvisorIncentive> {
88104
let final_rankings_per_review = veteran_rankings
89105
.iter()
@@ -92,6 +108,13 @@ pub fn calculate_veteran_advisors_incentives(
92108
.map(|(review, rankings)| (review, calc_final_ranking_per_review(&rankings)))
93109
.collect::<BTreeMap<_, _>>();
94110

111+
let final_rankings_consensus_per_review = veteran_rankings
112+
.iter()
113+
.into_group_map_by(|ranking| ranking.review_id())
114+
.into_iter()
115+
.map(|(review, rankings)| (review, calc_final_ranking_consensus_per_review(&rankings)))
116+
.collect::<BTreeMap<_, _>>();
117+
95118
let rankings_per_vca = veteran_rankings
96119
.iter()
97120
.counts_by(|ranking| ranking.vca.clone());
@@ -103,7 +126,7 @@ pub fn calculate_veteran_advisors_incentives(
103126
.get(&ranking.review_id())
104127
.unwrap()
105128
.is_positive()
106-
== ranking.score().is_positive()
129+
== ranking.score().is_positive() || *final_rankings_consensus_per_review.get(&ranking.review_id()).unwrap() < minimum_consensus
107130
})
108131
.counts_by(|ranking| ranking.vca.clone());
109132

@@ -156,6 +179,8 @@ mod tests {
156179
const VCA_1: &str = "vca1";
157180
const VCA_2: &str = "vca2";
158181
const VCA_3: &str = "vca3";
182+
const SIMPLE_MINIMUM_CONSENSUS: Decimal = dec!(.5);
183+
const QUALIFIED_MINIMUM_CONSENSUS: Decimal = dec!(.7);
159184

160185
struct RandomIterator;
161186
impl Iterator for RandomIterator {
@@ -231,6 +256,7 @@ mod tests {
231256
.into_iter()
232257
.zip(REPUTATION_DISAGREEMENT_MODIFIERS.into_iter())
233258
.collect(),
259+
SIMPLE_MINIMUM_CONSENSUS,
234260
);
235261
assert!(results.get(VCA_1).is_none());
236262
let res = results.get(VCA_2).unwrap();
@@ -260,6 +286,7 @@ mod tests {
260286
.into_iter()
261287
.zip(REPUTATION_DISAGREEMENT_MODIFIERS.into_iter())
262288
.collect(),
289+
SIMPLE_MINIMUM_CONSENSUS,
263290
);
264291
let res1 = results.get(VCA_1).unwrap();
265292
assert_eq!(res1.reputation, 1);
@@ -283,12 +310,12 @@ mod tests {
283310
(Rewards::new(8, 1), Rewards::ONE, Rewards::ONE),
284311
(Rewards::new(9, 1), Rewards::new(125, 2), Rewards::ONE),
285312
];
286-
for (agreement, reward_modifier, reputation_modifier) in inputs {
313+
for (vca3_agreement, reward_modifier, reputation_modifier) in inputs {
287314
let rankings = (0..100)
288315
.flat_map(|i| {
289316
let vcas =
290317
vec![VCA_1.to_owned(), VCA_2.to_owned(), VCA_3.to_owned()].into_iter();
291-
let (good, filtered_out) = if Rewards::from(i) < agreement * Rewards::from(100)
318+
let (good, filtered_out) = if Rewards::from(i) < vca3_agreement * Rewards::from(100)
292319
{
293320
(3, 0)
294321
} else {
@@ -297,7 +324,38 @@ mod tests {
297324
gen_dummy_rankings(i.to_string(), 0, good, filtered_out, vcas).into_iter()
298325
})
299326
.collect::<Vec<_>>();
300-
let results = calculate_veteran_advisors_incentives(
327+
let results_simple_consensus = calculate_veteran_advisors_incentives(
328+
&rankings,
329+
total_rewards,
330+
1..=200,
331+
1..=200,
332+
THRESHOLDS
333+
.into_iter()
334+
.zip(REWARDS_DISAGREEMENT_MODIFIERS.into_iter())
335+
.collect(),
336+
THRESHOLDS
337+
.into_iter()
338+
.zip(REPUTATION_DISAGREEMENT_MODIFIERS.into_iter())
339+
.collect(),
340+
SIMPLE_MINIMUM_CONSENSUS,
341+
);
342+
let vca3_expected_reward_portion_simple_consensus = vca3_agreement * Rewards::from(100) * reward_modifier;
343+
dbg!(vca3_expected_reward_portion_simple_consensus);
344+
dbg!(vca3_agreement, reward_modifier, reputation_modifier);
345+
let vca3_expected_rewards_simple_consensus = total_rewards
346+
/ (Rewards::from(125 * 2) + vca3_expected_reward_portion_simple_consensus)
347+
* vca3_expected_reward_portion_simple_consensus;
348+
let res_vca3_simple_consensus = results_simple_consensus.get(VCA_3).unwrap();
349+
assert_eq!(
350+
res_vca3_simple_consensus.reputation,
351+
(Rewards::from(100) * vca3_agreement * reputation_modifier)
352+
.to_u64()
353+
.unwrap()
354+
);
355+
assert!(are_close(res_vca3_simple_consensus.rewards, vca3_expected_rewards_simple_consensus));
356+
357+
358+
let results_qualified_consensus = calculate_veteran_advisors_incentives(
301359
&rankings,
302360
total_rewards,
303361
1..=200,
@@ -310,21 +368,27 @@ mod tests {
310368
.into_iter()
311369
.zip(REPUTATION_DISAGREEMENT_MODIFIERS.into_iter())
312370
.collect(),
371+
QUALIFIED_MINIMUM_CONSENSUS,
313372
);
314-
let expected_reward_portion = agreement * Rewards::from(100) * reward_modifier;
315-
dbg!(expected_reward_portion);
316-
dbg!(agreement, reward_modifier, reputation_modifier);
317-
let expected_rewards = total_rewards
318-
/ (Rewards::from(125 * 2) + expected_reward_portion)
319-
* expected_reward_portion;
320-
let res = results.get(VCA_3).unwrap();
373+
374+
let vca3_expected_reward_portion_qualified_consensus = Rewards::from(100) * dec!(1.25); // low consensus so max reward modifier, agreement ratio doesn't count as all and rankings are all eligible
375+
dbg!(vca3_expected_reward_portion_qualified_consensus);
376+
dbg!(vca3_agreement, reward_modifier, reputation_modifier);
377+
378+
let vca3_expected_rewards_qualified_consensus = total_rewards
379+
/ (Rewards::from(125 * 2) + vca3_expected_reward_portion_qualified_consensus)
380+
* vca3_expected_reward_portion_qualified_consensus; // 1/3 of the reward
381+
382+
let res_vca3_qualified_consensus = results_qualified_consensus.get(VCA_3).unwrap();
383+
384+
321385
assert_eq!(
322-
res.reputation,
323-
(Rewards::from(100) * agreement * reputation_modifier)
386+
res_vca3_qualified_consensus.reputation,
387+
(Rewards::from(100)) // all assessment are valid since consensus is low (2/3 < 0.7)
324388
.to_u64()
325389
.unwrap()
326390
);
327-
assert!(are_close(res.rewards, expected_rewards));
391+
assert!(are_close(res_vca3_qualified_consensus.rewards, vca3_expected_rewards_qualified_consensus));
328392
}
329393
}
330394
}

0 commit comments

Comments
 (0)