Skip to content

Commit c4e6dfc

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 c4e6dfc

File tree

2 files changed

+98
-14
lines changed

2 files changed

+98
-14
lines changed

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

Lines changed: 14 additions & 0 deletions
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

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,35 @@ 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), ranks.get(&Good)) {
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+
(_, Some(excellent), Some(good)) => {
92+
(Decimal::from(*excellent) + Decimal::from(*good)) / Decimal::from(rankings.len())
93+
}
94+
(_, _, Some(good)) => {
95+
Decimal::from(*good) / Decimal::from(rankings.len())
96+
}
97+
_ => Decimal::ONE,
98+
}
99+
}
100+
80101
pub fn calculate_veteran_advisors_incentives(
81102
veteran_rankings: &[VeteranRankingRow],
82103
total_rewards: Rewards,
83104
rewards_thresholds: EligibilityThresholds,
84105
reputation_thresholds: EligibilityThresholds,
85106
rewards_mod_args: Vec<(Decimal, Decimal)>,
86107
reputation_mod_args: Vec<(Decimal, Decimal)>,
108+
minimum_consensus: Decimal,
87109
) -> HashMap<VeteranAdvisorId, VeteranAdvisorIncentive> {
88110
let final_rankings_per_review = veteran_rankings
89111
.iter()
@@ -92,6 +114,13 @@ pub fn calculate_veteran_advisors_incentives(
92114
.map(|(review, rankings)| (review, calc_final_ranking_per_review(&rankings)))
93115
.collect::<BTreeMap<_, _>>();
94116

117+
let final_rankings_consensus_per_review = veteran_rankings
118+
.iter()
119+
.into_group_map_by(|ranking| ranking.review_id())
120+
.into_iter()
121+
.map(|(review, rankings)| (review, calc_final_ranking_consensus_per_review(&rankings)))
122+
.collect::<BTreeMap<_, _>>();
123+
95124
let rankings_per_vca = veteran_rankings
96125
.iter()
97126
.counts_by(|ranking| ranking.vca.clone());
@@ -103,7 +132,7 @@ pub fn calculate_veteran_advisors_incentives(
103132
.get(&ranking.review_id())
104133
.unwrap()
105134
.is_positive()
106-
== ranking.score().is_positive()
135+
== ranking.score().is_positive() || *final_rankings_consensus_per_review.get(&ranking.review_id()).unwrap() < minimum_consensus
107136
})
108137
.counts_by(|ranking| ranking.vca.clone());
109138

@@ -156,6 +185,8 @@ mod tests {
156185
const VCA_1: &str = "vca1";
157186
const VCA_2: &str = "vca2";
158187
const VCA_3: &str = "vca3";
188+
const SIMPLE_MINIMUM_CONSENSUS: Decimal = dec!(.5);
189+
const QUALIFIED_MINIMUM_CONSENSUS: Decimal = dec!(.7);
159190

160191
struct RandomIterator;
161192
impl Iterator for RandomIterator {
@@ -231,6 +262,7 @@ mod tests {
231262
.into_iter()
232263
.zip(REPUTATION_DISAGREEMENT_MODIFIERS.into_iter())
233264
.collect(),
265+
SIMPLE_MINIMUM_CONSENSUS,
234266
);
235267
assert!(results.get(VCA_1).is_none());
236268
let res = results.get(VCA_2).unwrap();
@@ -260,6 +292,7 @@ mod tests {
260292
.into_iter()
261293
.zip(REPUTATION_DISAGREEMENT_MODIFIERS.into_iter())
262294
.collect(),
295+
SIMPLE_MINIMUM_CONSENSUS,
263296
);
264297
let res1 = results.get(VCA_1).unwrap();
265298
assert_eq!(res1.reputation, 1);
@@ -283,12 +316,12 @@ mod tests {
283316
(Rewards::new(8, 1), Rewards::ONE, Rewards::ONE),
284317
(Rewards::new(9, 1), Rewards::new(125, 2), Rewards::ONE),
285318
];
286-
for (agreement, reward_modifier, reputation_modifier) in inputs {
319+
for (vca3_agreement, reward_modifier, reputation_modifier) in inputs {
287320
let rankings = (0..100)
288321
.flat_map(|i| {
289322
let vcas =
290323
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)
324+
let (good, filtered_out) = if Rewards::from(i) < vca3_agreement * Rewards::from(100)
292325
{
293326
(3, 0)
294327
} else {
@@ -297,7 +330,7 @@ mod tests {
297330
gen_dummy_rankings(i.to_string(), 0, good, filtered_out, vcas).into_iter()
298331
})
299332
.collect::<Vec<_>>();
300-
let results = calculate_veteran_advisors_incentives(
333+
let results_simple_consensus = calculate_veteran_advisors_incentives(
301334
&rankings,
302335
total_rewards,
303336
1..=200,
@@ -310,21 +343,58 @@ mod tests {
310343
.into_iter()
311344
.zip(REPUTATION_DISAGREEMENT_MODIFIERS.into_iter())
312345
.collect(),
346+
SIMPLE_MINIMUM_CONSENSUS,
313347
);
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();
348+
let vca3_expected_reward_portion_simple_consensus = vca3_agreement * Rewards::from(100) * reward_modifier;
349+
dbg!(vca3_expected_reward_portion_simple_consensus);
350+
dbg!(vca3_agreement, reward_modifier, reputation_modifier);
351+
let vca3_expected_rewards_simple_consensus = total_rewards
352+
/ (Rewards::from(125 * 2) + vca3_expected_reward_portion_simple_consensus)
353+
* vca3_expected_reward_portion_simple_consensus;
354+
let res_vca3_simple_consensus = results_simple_consensus.get(VCA_3).unwrap();
355+
assert_eq!(
356+
res_vca3_simple_consensus.reputation,
357+
(Rewards::from(100) * vca3_agreement * reputation_modifier)
358+
.to_u64()
359+
.unwrap()
360+
);
361+
assert!(are_close(res_vca3_simple_consensus.rewards, vca3_expected_rewards_simple_consensus));
362+
363+
364+
let results_qualified_consensus = calculate_veteran_advisors_incentives(
365+
&rankings,
366+
total_rewards,
367+
1..=200,
368+
1..=200,
369+
THRESHOLDS
370+
.into_iter()
371+
.zip(REWARDS_DISAGREEMENT_MODIFIERS.into_iter())
372+
.collect(),
373+
THRESHOLDS
374+
.into_iter()
375+
.zip(REPUTATION_DISAGREEMENT_MODIFIERS.into_iter())
376+
.collect(),
377+
QUALIFIED_MINIMUM_CONSENSUS,
378+
);
379+
380+
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
381+
dbg!(vca3_expected_reward_portion_qualified_consensus);
382+
dbg!(vca3_agreement, reward_modifier, reputation_modifier);
383+
384+
let vca3_expected_rewards_qualified_consensus = total_rewards
385+
/ (Rewards::from(125 * 2) + vca3_expected_reward_portion_qualified_consensus)
386+
* vca3_expected_reward_portion_qualified_consensus; // 1/3 of the reward
387+
388+
let res_vca3_qualified_consensus = results_qualified_consensus.get(VCA_3).unwrap();
389+
390+
321391
assert_eq!(
322-
res.reputation,
323-
(Rewards::from(100) * agreement * reputation_modifier)
392+
res_vca3_qualified_consensus.reputation,
393+
(Rewards::from(100)) // all assessment are valid since consensus is low (2/3 < 0.7)
324394
.to_u64()
325395
.unwrap()
326396
);
327-
assert!(are_close(res.rewards, expected_rewards));
397+
assert!(are_close(res_vca3_qualified_consensus.rewards, vca3_expected_rewards_qualified_consensus));
328398
}
329399
}
330400
}

0 commit comments

Comments
 (0)