use super::{
Candidate, CandidatePtr, Edge, ExtendedBalance, IdentifierT, Support, SupportMap, Supports,
VoteWeight, Voter,
};
use crate::arithmetic::{traits::Zero, Perbill};
use alloc::{collections::btree_map::BTreeMap, rc::Rc, vec::Vec};
type Threshold = ExtendedBalance;
pub fn standard_threshold(
committee_size: usize,
weights: impl IntoIterator<Item = ExtendedBalance>,
) -> Threshold {
weights
.into_iter()
.fold(Threshold::zero(), |acc, elem| acc.saturating_add(elem))
/ committee_size.max(1) as Threshold
}
pub fn pjr_check<AccountId: IdentifierT>(
supports: &Supports<AccountId>,
all_candidates: Vec<AccountId>,
all_voters: Vec<(AccountId, VoteWeight, Vec<AccountId>)>,
) -> Result<(), AccountId> {
let t = standard_threshold(
supports.len(),
all_voters.iter().map(|voter| voter.1 as ExtendedBalance),
);
t_pjr_check(supports, all_candidates, all_voters, t)
}
pub fn t_pjr_check<AccountId: IdentifierT>(
supports: &Supports<AccountId>,
all_candidates: Vec<AccountId>,
all_voters: Vec<(AccountId, VoteWeight, Vec<AccountId>)>,
t: Threshold,
) -> Result<(), AccountId> {
let (candidates, voters) = prepare_pjr_input(supports, all_candidates, all_voters);
pjr_check_core(candidates.as_ref(), voters.as_ref(), t)
}
pub fn pjr_check_core<AccountId: IdentifierT>(
candidates: &[CandidatePtr<AccountId>],
voters: &[Voter<AccountId>],
t: Threshold,
) -> Result<(), AccountId> {
let unelected = candidates.iter().filter(|c| !c.borrow().elected);
let maybe_max_pre_score = unelected
.map(|c| (pre_score(Rc::clone(c), voters, t), c.borrow().who.clone()))
.max();
match maybe_max_pre_score {
Some((max_pre_score, counter_example)) if max_pre_score >= t => Err(counter_example),
_ => Ok(()),
}
}
pub fn validate_pjr_challenge<AccountId: IdentifierT>(
counter_example: AccountId,
supports: &Supports<AccountId>,
all_candidates: Vec<AccountId>,
all_voters: Vec<(AccountId, VoteWeight, Vec<AccountId>)>,
) -> bool {
let threshold = standard_threshold(
supports.len(),
all_voters.iter().map(|voter| voter.1 as ExtendedBalance),
);
validate_t_pjr_challenge(counter_example, supports, all_candidates, all_voters, threshold)
}
pub fn validate_t_pjr_challenge<AccountId: IdentifierT>(
counter_example: AccountId,
supports: &Supports<AccountId>,
all_candidates: Vec<AccountId>,
all_voters: Vec<(AccountId, VoteWeight, Vec<AccountId>)>,
threshold: Threshold,
) -> bool {
let (candidates, voters) = prepare_pjr_input(supports, all_candidates, all_voters);
validate_pjr_challenge_core(counter_example, &candidates, &voters, threshold)
}
fn validate_pjr_challenge_core<AccountId: IdentifierT>(
counter_example: AccountId,
candidates: &[CandidatePtr<AccountId>],
voters: &[Voter<AccountId>],
threshold: Threshold,
) -> bool {
let candidate =
match candidates.iter().find(|candidate| candidate.borrow().who == counter_example) {
None => return false,
Some(candidate) => candidate.clone(),
};
pre_score(candidate, voters, threshold) >= threshold
}
fn prepare_pjr_input<AccountId: IdentifierT>(
supports: &Supports<AccountId>,
all_candidates: Vec<AccountId>,
all_voters: Vec<(AccountId, VoteWeight, Vec<AccountId>)>,
) -> (Vec<CandidatePtr<AccountId>>, Vec<Voter<AccountId>>) {
let mut candidates_index: BTreeMap<AccountId, usize> = BTreeMap::new();
let mut assignment_map: BTreeMap<AccountId, Vec<(AccountId, ExtendedBalance)>> =
BTreeMap::new();
for (winner_id, Support { voters, .. }) in supports.iter() {
for (voter_id, support) in voters.iter() {
assignment_map
.entry(voter_id.clone())
.or_default()
.push((winner_id.clone(), *support));
}
}
let supports: SupportMap<AccountId> = supports.iter().cloned().collect();
let candidates = all_candidates
.into_iter()
.enumerate()
.map(|(i, c)| {
candidates_index.insert(c.clone(), i);
let who = c;
let maybe_support = supports.get(&who);
let elected = maybe_support.is_some();
let backed_stake = maybe_support.map(|support| support.total).unwrap_or_default();
Candidate {
who,
elected,
backed_stake,
score: Default::default(),
approval_stake: Default::default(),
round: Default::default(),
}
.to_ptr()
})
.collect::<Vec<_>>();
let voters = all_voters
.into_iter()
.map(|(v, w, ts)| {
let mut edges: Vec<Edge<AccountId>> = Vec::with_capacity(ts.len());
for t in ts {
if edges.iter().any(|e| e.who == t) {
continue;
}
if let Some(idx) = candidates_index.get(&t) {
let weight = assignment_map
.get(&v)
.and_then(|d| {
d.iter().find_map(|(x, y)| if x == &t { Some(y) } else { None })
})
.cloned()
.unwrap_or_default();
edges.push(Edge {
who: t,
candidate: Rc::clone(&candidates[*idx]),
weight,
load: Default::default(),
});
}
}
let who = v;
let budget: ExtendedBalance = w.into();
Voter { who, budget, edges, load: Default::default() }
})
.collect::<Vec<_>>();
(candidates, voters)
}
fn pre_score<AccountId: IdentifierT>(
unelected: CandidatePtr<AccountId>,
voters: &[Voter<AccountId>],
t: Threshold,
) -> ExtendedBalance {
debug_assert!(!unelected.borrow().elected);
voters
.iter()
.filter(|v| v.votes_for(&unelected.borrow().who))
.fold(Zero::zero(), |acc: ExtendedBalance, voter| acc.saturating_add(slack(voter, t)))
}
fn slack<AccountId: IdentifierT>(voter: &Voter<AccountId>, t: Threshold) -> ExtendedBalance {
let budget = voter.budget;
let leftover = voter.edges.iter().fold(Zero::zero(), |acc: ExtendedBalance, edge| {
let candidate = edge.candidate.borrow();
if candidate.elected {
let extra =
Perbill::one().min(Perbill::from_rational(t, candidate.backed_stake)) * edge.weight;
acc.saturating_add(extra)
} else {
acc
}
});
budget.saturating_sub(leftover)
}
#[cfg(test)]
mod tests {
use super::*;
fn setup_voter(who: u32, votes: Vec<(u32, u128, bool)>) -> Voter<u32> {
let mut voter = Voter::new(who);
let mut budget = 0u128;
let candidates = votes
.into_iter()
.map(|(t, w, e)| {
budget += w;
Candidate {
who: t,
elected: e,
backed_stake: w,
score: Default::default(),
approval_stake: Default::default(),
round: Default::default(),
}
})
.collect::<Vec<_>>();
let edges = candidates
.into_iter()
.map(|c| Edge {
who: c.who,
weight: c.backed_stake,
candidate: c.to_ptr(),
load: Default::default(),
})
.collect::<Vec<_>>();
voter.edges = edges;
voter.budget = budget;
voter
}
fn assert_core_failure<AccountId: IdentifierT>(
candidates: &[CandidatePtr<AccountId>],
voters: &[Voter<AccountId>],
t: Threshold,
) {
let counter_example = pjr_check_core(candidates, voters, t).unwrap_err();
assert!(validate_pjr_challenge_core(counter_example, candidates, voters, t));
}
#[test]
fn slack_works() {
let voter = setup_voter(10, vec![(1, 10, true), (2, 20, true)]);
assert_eq!(slack(&voter, 15), 5);
assert_eq!(slack(&voter, 17), 3);
assert_eq!(slack(&voter, 10), 10);
assert_eq!(slack(&voter, 5), 20);
}
#[test]
fn pre_score_works() {
let v1 = setup_voter(10, vec![(1, 10, true), (2, 20, true), (3, 0, false)]);
let v2 = setup_voter(20, vec![(1, 5, true), (2, 5, true)]);
let v3 = setup_voter(30, vec![(1, 20, true), (2, 20, true), (3, 0, false)]);
let unelected = Candidate {
who: 3u32,
elected: false,
score: Default::default(),
approval_stake: Default::default(),
backed_stake: Default::default(),
round: Default::default(),
}
.to_ptr();
let score = pre_score(unelected, &vec![v1, v2, v3], 15);
assert_eq!(score, 15);
}
#[test]
fn can_convert_data_from_external_api() {
let all_candidates = vec![10, 20, 30, 40];
let all_voters = vec![
(1, 10, vec![10, 20, 30, 40]),
(2, 20, vec![10, 20, 30, 40]),
(3, 30, vec![10, 30]),
];
let supports: Supports<u32> = vec![
(20, Support { total: 15, voters: vec![(1, 5), (2, 10)] }),
(40, Support { total: 15, voters: vec![(1, 5), (2, 10)] }),
];
let (candidates, voters) = prepare_pjr_input(&supports, all_candidates, all_voters);
assert_eq!(
candidates
.iter()
.map(|c| (c.borrow().who, c.borrow().elected, c.borrow().backed_stake))
.collect::<Vec<_>>(),
vec![(10, false, 0), (20, true, 15), (30, false, 0), (40, true, 15)],
);
assert_eq!(
voters
.iter()
.map(|v| (
v.who,
v.budget,
v.edges.iter().map(|e| (e.who, e.weight)).collect::<Vec<_>>(),
))
.collect::<Vec<_>>(),
vec![
(1, 10, vec![(10, 0), (20, 5), (30, 0), (40, 5)]),
(2, 20, vec![(10, 0), (20, 10), (30, 0), (40, 10)]),
(3, 30, vec![(10, 0), (30, 0)]),
],
);
assert_core_failure(&candidates, &voters, 1);
assert_core_failure(&candidates, &voters, 10);
assert_core_failure(&candidates, &voters, 20);
}
#[test]
fn find_upper_bound_for_threshold_scenario_1() {
let all_candidates = vec![10, 20, 30, 40];
let all_voters = vec![
(1, 10, vec![10, 20, 30, 40]),
(2, 20, vec![10, 20, 30, 40]),
(3, 30, vec![10, 30]),
];
let supports: Supports<u32> = vec![
(20, Support { total: 15, voters: vec![(1, 5), (2, 10)] }),
(40, Support { total: 15, voters: vec![(1, 5), (2, 10)] }),
];
let (candidates, voters) = prepare_pjr_input(&supports, all_candidates, all_voters);
find_threshold_phase_change_for_scenario(candidates, voters);
}
#[test]
fn find_upper_bound_for_threshold_scenario_2() {
let all_candidates = vec![10, 20, 30, 40];
let all_voters = vec![
(1, 10, vec![10, 20, 30, 40]),
(2, 20, vec![10, 20, 30, 40]),
(3, 25, vec![10, 30]),
];
let supports: Supports<u32> = vec![
(20, Support { total: 15, voters: vec![(1, 5), (2, 10)] }),
(40, Support { total: 15, voters: vec![(1, 5), (2, 10)] }),
];
let (candidates, voters) = prepare_pjr_input(&supports, all_candidates, all_voters);
find_threshold_phase_change_for_scenario(candidates, voters);
}
#[test]
fn find_upper_bound_for_threshold_scenario_3() {
let all_candidates = vec![10, 20, 30, 40];
let all_voters = vec![
(1, 10, vec![10, 20, 30, 40]),
(2, 20, vec![10, 20, 30, 40]),
(3, 35, vec![10, 30]),
];
let supports: Supports<u32> = vec![
(20, Support { total: 15, voters: vec![(1, 5), (2, 10)] }),
(40, Support { total: 15, voters: vec![(1, 5), (2, 10)] }),
];
let (candidates, voters) = prepare_pjr_input(&supports, all_candidates, all_voters);
find_threshold_phase_change_for_scenario(candidates, voters);
}
fn find_threshold_phase_change_for_scenario<AccountId: IdentifierT>(
candidates: Vec<CandidatePtr<AccountId>>,
voters: Vec<Voter<AccountId>>,
) -> Threshold {
let mut threshold = 1;
let mut prev_threshold = 0;
while pjr_check_core(&candidates, &voters, threshold).is_err() {
prev_threshold = threshold;
threshold = threshold
.checked_mul(2)
.expect("pjr check must fail before we run out of capacity in u128");
}
let mut high_bound = threshold;
let mut low_bound = prev_threshold;
while high_bound - low_bound > 1 {
let test = low_bound + ((high_bound - low_bound) / 2);
if pjr_check_core(&candidates, &voters, test).is_ok() {
high_bound = test;
} else {
low_bound = test;
}
}
println!("highest failing check: {}", low_bound);
println!("lowest succeeding check: {}", high_bound);
let mut unexpected_failures = Vec::new();
let mut unexpected_successes = Vec::new();
for t in 0..=low_bound {
if pjr_check_core(&candidates, &voters, t).is_ok() {
unexpected_successes.push(t);
}
}
for t in high_bound..(high_bound * 2) {
if pjr_check_core(&candidates, &voters, t).is_err() {
unexpected_failures.push(t);
}
}
dbg!(&unexpected_successes, &unexpected_failures);
assert!(unexpected_failures.is_empty() && unexpected_successes.is_empty());
high_bound
}
}