PhragMMS election. (#6685)

* Revamp npos-elections and implement phragmms

* Update primitives/npos-elections/src/phragmms.rs

* Fix build

* Some review grumbles

* Add some stuff for remote testing

* fix some of the grumbles.

* Add remote testing stuff.

* Cleanup

* fix docs

* Update primitives/arithmetic/src/rational.rs

Co-authored-by: Dan Forbes <dan@danforbes.dev>

* Small config change

* Better handling of approval_stake == 0

* Final touhces.

* Clean fuzzer a bit

* Clean fuzzer a bit

* Update primitives/npos-elections/src/balancing.rs

Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>

* Fix fuzzer.

* Better api for normalize

* Add noramlize_up

* A large number of small fixes.

* make it merge ready

* Fix warns

* bump

* Fix fuzzers a bit.

* Fix warns as well.

* Fix more tests.

Co-authored-by: Dan Forbes <dan@danforbes.dev>
Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>
This commit is contained in:
Kian Paimani
2020-09-23 10:16:10 +02:00
committed by GitHub
parent ecdc94420e
commit 313f86ec23
32 changed files with 2074 additions and 914 deletions
@@ -0,0 +1,193 @@
// This file is part of Substrate.
// Copyright (C) 2020 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Balancing algorithm implementation.
//!
//! Given a committee `A` and an edge weight vector `w`, a balanced solution is one that
//!
//! 1. it maximizes the sum of member supports, i.e `Argmax { sum(support(c)) }`. for all `c` in
//! `A`.
//! 2. it minimizes the sum of supports squared, i.e `Argmin { sum(support(c).pow(2)) }` for all `c`
//! in `A`.
//!
//! See [`balance`] for more information.
use crate::{IdentifierT, Voter, ExtendedBalance, Edge};
use sp_arithmetic::traits::Zero;
use sp_std::prelude::*;
/// Balance the weight distribution of a given `voters` at most `iterations` times, or up until the
/// point where the biggest difference created per iteration of all stakes is `tolerance`. If this
/// is called with `tolerance = 0`, then exactly `iterations` rounds will be executed, except if no
/// change has been made (`difference = 0`).
///
/// In almost all cases, a balanced solution will have a better score than an unbalanced solution,
/// yet this is not 100% guaranteed because the first element of a [`ElectionScore`] does not
/// directly related to balancing.
///
/// Note that some reference implementation adopt an approach in which voters are balanced randomly
/// per round. To advocate determinism, we don't do this. In each round, all voters are exactly
/// balanced once, in the same order.
///
/// Also, note that due to re-distribution of weights, the outcome of this function might contain
/// edges with weight zero. The call site should filter such weight if desirable. Moreover, the
/// outcome might need balance re-normalization, see `Voter::try_normalize`.
///
/// ### References
///
/// - [A new approach to the maximum flow problem](https://dl.acm.org/doi/10.1145/48014.61051).
/// - [Validator election in nominated proof-of-stake](https://arxiv.org/abs/2004.12990) (Appendix
/// A.)
pub fn balance<AccountId: IdentifierT>(
voters: &mut Vec<Voter<AccountId>>,
iterations: usize,
tolerance: ExtendedBalance,
) -> usize {
if iterations == 0 { return 0; }
let mut iter = 0;
loop {
let mut max_diff = 0;
for voter in voters.iter_mut() {
let diff = balance_voter(voter, tolerance);
if diff > max_diff { max_diff = diff; }
}
iter += 1;
if max_diff <= tolerance || iter >= iterations {
break iter;
}
}
}
/// Internal implementation of balancing for one voter.
pub(crate) fn balance_voter<AccountId: IdentifierT>(
voter: &mut Voter<AccountId>,
tolerance: ExtendedBalance,
) -> ExtendedBalance {
// create a shallow copy of the elected ones. The original one will not be used henceforth.
let mut elected_edges = voter.edges
.iter_mut()
.filter(|e| e.candidate.borrow().elected)
.collect::<Vec<&mut Edge<AccountId>>>();
// Either empty, or a self vote. Not much to do in either case.
if elected_edges.len() <= 1 {
return Zero::zero()
}
// amount of stake from this voter that is used in edges.
let stake_used = elected_edges
.iter()
.fold(0, |a: ExtendedBalance, e| a.saturating_add(e.weight));
// backed stake of each of the elected edges.
let backed_stakes = elected_edges
.iter()
.map(|e| e.candidate.borrow().backed_stake)
.collect::<Vec<_>>();
// backed stake of all the edges for whom we've spent some stake.
let backing_backed_stake = elected_edges
.iter()
.filter_map(|e|
if e.weight > 0 {
Some(e.candidate.borrow().backed_stake)
} else {
None
}
)
.collect::<Vec<_>>();
let difference = if backing_backed_stake.len() > 0 {
let max_stake = backing_backed_stake
.iter()
.max()
.expect("vector with positive length will have a max; qed");
let min_stake = backed_stakes
.iter()
.min()
.expect("iterator with positive length will have a min; qed");
let mut difference = max_stake.saturating_sub(*min_stake);
difference = difference.saturating_add(voter.budget.saturating_sub(stake_used));
if difference < tolerance {
return difference;
}
difference
} else {
voter.budget
};
// remove all backings.
for edge in elected_edges.iter_mut() {
let mut candidate = edge.candidate.borrow_mut();
candidate.backed_stake = candidate.backed_stake.saturating_sub(edge.weight);
edge.weight = 0;
}
elected_edges.sort_by_key(|e| e.candidate.borrow().backed_stake);
let mut cumulative_backed_stake = Zero::zero();
let mut last_index = elected_edges.len() - 1;
for (index, edge) in elected_edges.iter().enumerate() {
let index = index as ExtendedBalance;
let backed_stake = edge.candidate.borrow().backed_stake;
let temp = backed_stake.saturating_mul(index);
if temp.saturating_sub(cumulative_backed_stake) > voter.budget {
// defensive only. length of elected_edges is checked to be above 1.
last_index = index.saturating_sub(1) as usize;
break
}
cumulative_backed_stake = cumulative_backed_stake.saturating_add(backed_stake);
}
let last_stake = elected_edges.get(last_index).expect(
"length of elected_edges is greater than or equal 2; last_index index is at \
the minimum elected_edges.len() - 1; index is within range; qed"
).candidate.borrow().backed_stake;
let ways_to_split = last_index + 1;
let excess = voter.budget
.saturating_add(cumulative_backed_stake)
.saturating_sub(last_stake.saturating_mul(ways_to_split as ExtendedBalance));
// Do the final update.
for edge in elected_edges.into_iter().take(ways_to_split) {
// first, do one scoped borrow to get the previous candidate stake.
let candidate_backed_stake = {
let candidate = edge.candidate.borrow();
candidate.backed_stake
};
let new_edge_weight = (excess / ways_to_split as ExtendedBalance)
.saturating_add(last_stake)
.saturating_sub(candidate_backed_stake);
// write the new edge weight
edge.weight = new_edge_weight;
// write the new candidate stake
let mut candidate = edge.candidate.borrow_mut();
candidate.backed_stake = candidate.backed_stake.saturating_add(new_edge_weight);
}
// excess / ways_to_split can cause a small un-normalized voters to be created.
// We won't `expect` here because even a result which is not normalized is not corrupt;
let _ = voter.try_normalize_elected();
difference
}
@@ -17,7 +17,9 @@
//! Helper methods for npos-elections.
use crate::{Assignment, ExtendedBalance, VoteWeight, IdentifierT, StakedAssignment, WithApprovalOf, Error};
use crate::{
Assignment, ExtendedBalance, VoteWeight, IdentifierT, StakedAssignment, WithApprovalOf, Error,
};
use sp_arithmetic::{PerThing, InnerOf};
use sp_std::prelude::*;
+281 -422
View File
@@ -1,58 +1,109 @@
// This file is part of Substrate.
// Copyright (C) 2019-2020 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2019-2020 Parity Technologies (UK) Ltd. SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
// in compliance with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Unless required by applicable law or agreed to in writing, software distributed under the License
// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
// or implied. See the License for the specific language governing permissions and limitations under
// the License.
//! A set of election algorithms to be used with a substrate runtime, typically within the staking
//! sub-system. Notable implementation include
//! sub-system. Notable implementation include:
//!
//! - [`seq_phragmen`]: Implements the Phragmén Sequential Method. An un-ranked, relatively fast
//! election method that ensures PJR, but does not provide a constant factor approximation of the
//! maximin problem.
//! - [`balance_solution`]: Implements the star balancing algorithm. This iterative process can
//! increase a solutions score, as described in [`evaluate_support`].
//! - [`phragmms`]: Implements a hybrid approach inspired by Phragmén which is executed faster but
//! it can achieve a constant factor approximation of the maximin problem, similar to that of the
//! MMS algorithm.
//! - [`balance_solution`]: Implements the star balancing algorithm. This iterative process can push
//! a solution toward being more `balances`, which in turn can increase its score.
//!
//! ### Terminology
//!
//! This crate uses context-independent words, not to be confused with staking. This is because the
//! election algorithms of this crate, while designed for staking, can be used in other contexts as
//! well.
//!
//! `Voter`: The entity casting some votes to a number of `Targets`. This is the same as `Nominator`
//! in the context of staking. `Target`: The entities eligible to be voted upon. This is the same as
//! `Validator` in the context of staking. `Edge`: A mapping from a `Voter` to a `Target`.
//!
//! The goal of an election algorithm is to provide an `ElectionResult`. A data composed of:
//! - `winners`: A flat list of identifiers belonging to those who have won the election, usually
//! ordered in some meaningful way. They are zipped with their total backing stake.
//! - `assignment`: A mapping from each voter to their winner-only targets, zipped with a ration
//! denoting the amount of support given to that particular target.
//!
//! ```rust
//! # use sp_npos_elections::*;
//! # use sp_runtime::Perbill;
//! // the winners.
//! let winners = vec![(1, 100), (2, 50)];
//! let assignments = vec![
//! // A voter, giving equal backing to both 1 and 2.
//! Assignment {
//! who: 10,
//! distribution: vec![(1, Perbill::from_percent(50)), (2, Perbill::from_percent(50))],
//! },
//! // A voter, Only backing 1.
//! Assignment { who: 20, distribution: vec![(1, Perbill::from_percent(100))] },
//! ];
//!
//! // the combination of the two makes the election result.
//! let election_result = ElectionResult { winners, assignments };
//!
//! ```
//!
//! The `Assignment` field of the election result is voter-major, i.e. it is from the perspective of
//! the voter. The struct that represents the opposite is called a `Support`. This struct is usually
//! accessed in a map-like manner, i.e. keyed vy voters, therefor it is stored as a mapping called
//! `SupportMap`.
//!
//! Moreover, the support is built from absolute backing values, not ratios like the example above.
//! A struct similar to `Assignment` that has stake value instead of ratios is called an
//! `StakedAssignment`.
//!
//!
//! More information can be found at: https://arxiv.org/abs/2004.12990
#![cfg_attr(not(feature = "std"), no_std)]
use sp_std::{prelude::*, collections::btree_map::BTreeMap, fmt::Debug, cmp::Ordering, convert::TryFrom};
use sp_std::{
prelude::*, collections::btree_map::BTreeMap, fmt::Debug, cmp::Ordering, rc::Rc, cell::RefCell,
};
use sp_arithmetic::{
PerThing, Rational128, ThresholdOrd, InnerOf, Normalizable,
helpers_128bit::multiply_by_rational,
traits::{Zero, Saturating, Bounded, SaturatedConversion},
traits::{Zero, Bounded},
};
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
#[cfg(feature = "std")]
use serde::{Serialize, Deserialize};
#[cfg(feature = "std")]
use codec::{Encode, Decode};
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
mod phragmen;
mod balancing;
mod phragmms;
mod node;
mod reduce;
mod helpers;
// re-export reduce stuff.
pub use reduce::reduce;
// re-export the helpers.
pub use helpers::*;
pub use phragmen::*;
pub use phragmms::*;
pub use balancing::*;
// re-export the compact macro, with the dependencies of the macro.
#[doc(hidden)]
@@ -91,8 +142,8 @@ impl<T: Clone + Eq + Default + Ord + Debug + codec::Codec> IdentifierT for T {}
/// The errors that might occur in the this crate and compact.
#[derive(Debug, Eq, PartialEq)]
pub enum Error {
/// While going from compact to staked, the stake of all the edges has gone above the
/// total and the last stake cannot be assigned.
/// While going from compact to staked, the stake of all the edges has gone above the total and
/// the last stake cannot be assigned.
CompactStakeOverflow,
/// The compact type has a voter who's number of targets is out of bound.
CompactTargetOverflow,
@@ -115,57 +166,159 @@ pub type ElectionScore = [ExtendedBalance; 3];
/// A winner, with their respective approval stake.
pub type WithApprovalOf<A> = (A, ExtendedBalance);
/// The denominator used for loads. Since votes are collected as u64, the smallest ratio that we
/// might collect is `1/approval_stake` where approval stake is the sum of votes. Hence, some number
/// bigger than u64::max_value() is needed. For maximum accuracy we simply use u128;
const DEN: u128 = u128::max_value();
/// A pointer to a candidate struct with interior mutability.
pub type CandidatePtr<A> = Rc<RefCell<Candidate<A>>>;
/// A candidate entity for the election.
#[derive(Clone, Default, Debug)]
struct Candidate<AccountId> {
#[derive(Debug, Clone, Default)]
pub struct Candidate<AccountId> {
/// Identifier.
who: AccountId,
/// Intermediary value used to sort candidates.
/// Score of the candidate.
///
/// Used differently in seq-phragmen and max-score.
score: Rational128,
/// Sum of the stake of this candidate based on received votes.
/// Approval stake of the candidate. Merely the sum of all the voter's stake who approve this
/// candidate.
approval_stake: ExtendedBalance,
/// Flag for being elected.
/// The final stake of this candidate. Will be equal to a subset of approval stake.
backed_stake: ExtendedBalance,
/// True if this candidate is already elected in the current election.
elected: bool,
/// The round index at which this candidate was elected.
round: usize,
}
/// A vote being casted by a [`Voter`] to a [`Candidate`] is an `Edge`.
#[derive(Clone, Default)]
pub struct Edge<AccountId> {
/// Identifier of the target.
///
/// This is equivalent of `self.candidate.borrow().who`, yet it helps to avoid double borrow
/// errors of the candidate pointer.
who: AccountId,
/// Load of this edge.
load: Rational128,
/// Pointer to the candidate.
candidate: CandidatePtr<AccountId>,
/// The weight (i.e. stake given to `who`) of this edge.
weight: ExtendedBalance,
}
#[cfg(feature = "std")]
impl<A: IdentifierT> sp_std::fmt::Debug for Edge<A> {
fn fmt(&self, f: &mut sp_std::fmt::Formatter<'_>) -> sp_std::fmt::Result {
write!(f, "Edge({:?}, weight = {:?})", self.who, self.weight)
}
}
/// A voter entity.
#[derive(Clone, Default, Debug)]
struct Voter<AccountId> {
#[derive(Clone, Default)]
pub struct Voter<AccountId> {
/// Identifier.
who: AccountId,
/// List of candidates proposed by this voter.
/// List of candidates approved by this voter.
edges: Vec<Edge<AccountId>>,
/// The stake of this voter.
budget: ExtendedBalance,
/// Incremented each time a candidate that this voter voted for has been elected.
/// Load of the voter.
load: Rational128,
}
/// A candidate being backed by a voter.
#[derive(Clone, Default, Debug)]
struct Edge<AccountId> {
/// Identifier.
who: AccountId,
/// Load of this vote.
load: Rational128,
/// Index of the candidate stored in the 'candidates' vector.
candidate_index: usize,
#[cfg(feature = "std")]
impl<A: IdentifierT> std::fmt::Debug for Voter<A> {
fn fmt(&self, f: &mut sp_std::fmt::Formatter<'_>) -> sp_std::fmt::Result {
write!(f, "Voter({:?}, budget = {}, edges = {:?})", self.who, self.budget, self.edges)
}
}
impl<AccountId: IdentifierT> Voter<AccountId> {
/// Returns none if this voter does not have any non-zero distributions.
///
/// Note that this might create _un-normalized_ assignments, due to accuracy loss of `P`. Call
/// site might compensate by calling `normalize()` on the returned `Assignment` as a
/// post-precessing.
pub fn into_assignment<P: PerThing>(self) -> Option<Assignment<AccountId, P>>
where
ExtendedBalance: From<InnerOf<P>>,
{
let who = self.who;
let budget = self.budget;
let distribution = self.edges.into_iter().filter_map(|e| {
let per_thing = P::from_rational_approximation(e.weight, budget);
// trim zero edges.
if per_thing.is_zero() { None } else { Some((e.who, per_thing)) }
}).collect::<Vec<_>>();
if distribution.len() > 0 {
Some(Assignment { who, distribution })
} else {
None
}
}
/// Try and normalize the votes of self.
///
/// If the normalization is successful then `Ok(())` is returned.
///
/// Note that this will not distinguish between elected and unelected edges. Thus, it should
/// only be called on a voter who has already been reduced to only elected edges.
///
/// ### Errors
///
/// This will return only if the internal `normalize` fails. This can happen if the sum of the
/// weights exceeds `ExtendedBalance::max_value()`.
pub fn try_normalize(&mut self) -> Result<(), &'static str> {
let edge_weights = self.edges.iter().map(|e| e.weight).collect::<Vec<_>>();
edge_weights.normalize(self.budget).map(|normalized| {
// here we count on the fact that normalize does not change the order.
for (edge, corrected) in self.edges.iter_mut().zip(normalized.into_iter()) {
let mut candidate = edge.candidate.borrow_mut();
// first, subtract the incorrect weight
candidate.backed_stake = candidate.backed_stake.saturating_sub(edge.weight);
edge.weight = corrected;
// Then add the correct one again.
candidate.backed_stake = candidate.backed_stake.saturating_add(edge.weight);
}
})
}
/// Same as [`try_normalize`] but the normalization is only limited between elected edges.
pub fn try_normalize_elected(&mut self) -> Result<(), &'static str> {
let elected_edge_weights = self
.edges
.iter()
.filter_map(|e| if e.candidate.borrow().elected { Some(e.weight) } else { None })
.collect::<Vec<_>>();
elected_edge_weights.normalize(self.budget).map(|normalized| {
// here we count on the fact that normalize does not change the order, and that vector
// iteration is deterministic.
for (edge, corrected) in self
.edges
.iter_mut()
.filter(|e| e.candidate.borrow().elected)
.zip(normalized.into_iter())
{
let mut candidate = edge.candidate.borrow_mut();
// first, subtract the incorrect weight
candidate.backed_stake = candidate.backed_stake.saturating_sub(edge.weight);
edge.weight = corrected;
// Then add the correct one again.
candidate.backed_stake = candidate.backed_stake.saturating_add(edge.weight);
}
})
}
}
/// Final result of the election.
#[derive(Debug)]
pub struct ElectionResult<AccountId, T: PerThing> {
pub struct ElectionResult<AccountId, P: PerThing> {
/// Just winners zipped with their approval stake. Note that the approval stake is merely the
/// sub of their received stake and could be used for very basic sorting and approval voting.
pub winners: Vec<WithApprovalOf<AccountId>>,
/// Individual assignments. for each tuple, the first elements is a voter and the second
/// is the list of candidates that it supports.
pub assignments: Vec<Assignment<AccountId, T>>,
/// Individual assignments. for each tuple, the first elements is a voter and the second is the
/// list of candidates that it supports.
pub assignments: Vec<Assignment<AccountId, P>>,
}
/// A voter's stake assignment among a set of targets, represented as ratios.
@@ -184,8 +337,8 @@ where
{
/// Convert from a ratio assignment into one with absolute values aka. [`StakedAssignment`].
///
/// It needs `stake` which is the total budget of the voter. If `fill` is set to true,
/// it _tries_ to ensure that all the potential rounding errors are compensated and the
/// It needs `stake` which is the total budget of the voter. If `fill` is set to true, it
/// _tries_ to ensure that all the potential rounding errors are compensated and the
/// distribution's sum is exactly equal to the total budget, by adding or subtracting the
/// remainder from the last distribution.
///
@@ -219,6 +372,13 @@ where
/// Try and normalize this assignment.
///
/// If `Ok(())` is returned, then the assignment MUST have been successfully normalized to 100%.
///
/// ### Errors
///
/// This will return only if the internal `normalize` fails. This can happen if sum of
/// `self.distribution.map(|p| p.deconstruct())` fails to fit inside `UpperOf<P>`. A user of
/// this crate may statically assert that this can never happen and safely `expect` this to
/// return `Ok`.
pub fn try_normalize(&mut self) -> Result<(), &'static str> {
self.distribution
.iter()
@@ -289,9 +449,9 @@ impl<AccountId> StakedAssignment<AccountId> {
///
/// NOTE: current implementation of `.normalize` is almost safe to `expect()` upon. The only
/// error case is when the input cannot fit in `T`, or the sum of input cannot fit in `T`.
/// Sadly, both of these are dependent upon the implementation of `VoteLimit`, i.e. the limit
/// of edges per voter which is enforced from upstream. Hence, at this crate, we prefer
/// returning a result and a use the name prefix `try_`.
/// Sadly, both of these are dependent upon the implementation of `VoteLimit`, i.e. the limit of
/// edges per voter which is enforced from upstream. Hence, at this crate, we prefer returning a
/// result and a use the name prefix `try_`.
pub fn try_normalize(&mut self, stake: ExtendedBalance) -> Result<(), &'static str> {
self.distribution
.iter()
@@ -317,8 +477,8 @@ impl<AccountId> StakedAssignment<AccountId> {
///
/// This complements the [`ElectionResult`] and is needed to run the balancing post-processing.
///
/// This, at the current version, resembles the `Exposure` defined in the Staking pallet, yet
/// they do not necessarily have to be the same.
/// This, at the current version, resembles the `Exposure` defined in the Staking pallet, yet they
/// do not necessarily have to be the same.
#[derive(Default, Debug)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Eq, PartialEq))]
pub struct Support<AccountId> {
@@ -331,228 +491,12 @@ pub struct Support<AccountId> {
/// A linkage from a candidate and its [`Support`].
pub type SupportMap<A> = BTreeMap<A, Support<A>>;
/// Perform election based on Phragmén algorithm.
///
/// Returns an `Option` the set of winners and their detailed support ratio from each voter if
/// enough candidates are provided. Returns `None` otherwise.
///
/// * `candidate_count`: number of candidates to elect.
/// * `minimum_candidate_count`: minimum number of candidates to elect. If less candidates exist,
/// `None` is returned.
/// * `initial_candidates`: candidates list to be elected from.
/// * `initial_voters`: voters list.
///
/// This function does not strip out candidates who do not have any backing stake. It is the
/// responsibility of the caller to make sure only those candidates who have a sensible economic
/// value are passed in. From the perspective of this function, a candidate can easily be among the
/// winner with no backing stake.
pub fn seq_phragmen<AccountId, R>(
candidate_count: usize,
minimum_candidate_count: usize,
initial_candidates: Vec<AccountId>,
initial_voters: Vec<(AccountId, VoteWeight, Vec<AccountId>)>,
) -> Option<ElectionResult<AccountId, R>> where
AccountId: Default + Ord + Clone,
R: PerThing,
{
// return structures
let mut elected_candidates: Vec<(AccountId, ExtendedBalance)>;
let mut assigned: Vec<Assignment<AccountId, R>>;
// used to cache and access candidates index.
let mut c_idx_cache = BTreeMap::<AccountId, usize>::new();
// voters list.
let num_voters = initial_candidates.len() + initial_voters.len();
let mut voters: Vec<Voter<AccountId>> = Vec::with_capacity(num_voters);
// Iterate once to create a cache of candidates indexes. This could be optimized by being
// provided by the call site.
let mut candidates = initial_candidates
.into_iter()
.enumerate()
.map(|(idx, who)| {
c_idx_cache.insert(who.clone(), idx);
Candidate { who, ..Default::default() }
})
.collect::<Vec<Candidate<AccountId>>>();
// early return if we don't have enough candidates
if candidates.len() < minimum_candidate_count { return None; }
// collect voters. use `c_idx_cache` for fast access and aggregate `approval_stake` of
// candidates.
voters.extend(initial_voters.into_iter().map(|(who, voter_stake, votes)| {
let mut edges: Vec<Edge<AccountId>> = Vec::with_capacity(votes.len());
for v in votes {
if edges.iter().any(|e| e.who == v) {
// duplicate edge.
continue;
}
if let Some(idx) = c_idx_cache.get(&v) {
// This candidate is valid + already cached.
candidates[*idx].approval_stake = candidates[*idx].approval_stake
.saturating_add(voter_stake.into());
edges.push(Edge { who: v.clone(), candidate_index: *idx, ..Default::default() });
} // else {} would be wrong votes. We don't really care about it.
}
Voter {
who,
edges: edges,
budget: voter_stake.into(),
load: Rational128::zero(),
}
}));
// we have already checked that we have more candidates than minimum_candidate_count.
let to_elect = candidate_count.min(candidates.len());
elected_candidates = Vec::with_capacity(candidate_count);
assigned = Vec::with_capacity(candidate_count);
// main election loop
for _round in 0..to_elect {
// loop 1: initialize score
for c in &mut candidates {
if !c.elected {
// 1 / approval_stake == (DEN / approval_stake) / DEN. If approval_stake is zero,
// then the ratio should be as large as possible, essentially `infinity`.
if c.approval_stake.is_zero() {
c.score = Rational128::from_unchecked(DEN, 0);
} else {
c.score = Rational128::from(DEN / c.approval_stake, DEN);
}
}
}
// loop 2: increment score
for n in &voters {
for e in &n.edges {
let c = &mut candidates[e.candidate_index];
if !c.elected && !c.approval_stake.is_zero() {
let temp_n = multiply_by_rational(
n.load.n(),
n.budget,
c.approval_stake,
).unwrap_or_else(|_| Bounded::max_value());
let temp_d = n.load.d();
let temp = Rational128::from(temp_n, temp_d);
c.score = c.score.lazy_saturating_add(temp);
}
}
}
// loop 3: find the best
if let Some(winner) = candidates
.iter_mut()
.filter(|c| !c.elected)
.min_by_key(|c| c.score)
{
// loop 3: update voter and edge load
winner.elected = true;
for n in &mut voters {
for e in &mut n.edges {
if e.who == winner.who {
e.load = winner.score.lazy_saturating_sub(n.load);
n.load = winner.score;
}
}
}
elected_candidates.push((winner.who.clone(), winner.approval_stake));
} else {
break
}
} // end of all rounds
// update backing stake of candidates and voters
for n in &mut voters {
let mut assignment = Assignment {
who: n.who.clone(),
..Default::default()
};
for e in &mut n.edges {
if elected_candidates.iter().position(|(ref c, _)| *c == e.who).is_some() {
let per_bill_parts: R::Inner =
{
if n.load == e.load {
// Full support. No need to calculate.
R::ACCURACY
} else {
if e.load.d() == n.load.d() {
// return e.load / n.load.
let desired_scale: u128 = R::ACCURACY.saturated_into();
let parts = multiply_by_rational(
desired_scale,
e.load.n(),
n.load.n(),
)
// If result cannot fit in u128. Not much we can do about it.
.unwrap_or_else(|_| Bounded::max_value());
TryFrom::try_from(parts)
// If the result cannot fit into R::Inner. Defensive only. This can
// never happen. `desired_scale * e / n`, where `e / n < 1` always
// yields a value smaller than `desired_scale`, which will fit into
// R::Inner.
.unwrap_or_else(|_| Bounded::max_value())
} else {
// defensive only. Both edge and voter loads are built from
// scores, hence MUST have the same denominator.
Zero::zero()
}
}
};
let per_thing = R::from_parts(per_bill_parts);
assignment.distribution.push((e.who.clone(), per_thing));
}
}
let len = assignment.distribution.len();
if len > 0 {
// To ensure an assertion indicating: no stake from the voter going to waste,
// we add a minimal post-processing to equally assign all of the leftover stake ratios.
let vote_count: R::Inner = len.saturated_into();
let accuracy = R::ACCURACY;
let mut sum: R::Inner = Zero::zero();
assignment.distribution.iter().for_each(|a| sum = sum.saturating_add(a.1.deconstruct()));
let diff = accuracy.saturating_sub(sum);
let diff_per_vote = (diff / vote_count).min(accuracy);
if !diff_per_vote.is_zero() {
for i in 0..len {
let current_ratio = assignment.distribution[i % len].1;
let next_ratio = current_ratio
.saturating_add(R::from_parts(diff_per_vote));
assignment.distribution[i % len].1 = next_ratio;
}
}
// `remainder` is set to be less than maximum votes of a voter (currently 16).
// safe to cast it to usize.
let remainder = diff - diff_per_vote * vote_count;
for i in 0..remainder.saturated_into::<usize>() {
let current_ratio = assignment.distribution[i % len].1;
let next_ratio = current_ratio.saturating_add(R::from_parts(1u8.into()));
assignment.distribution[i % len].1 = next_ratio;
}
assigned.push(assignment);
}
}
Some(ElectionResult {
winners: elected_candidates,
assignments: assigned,
})
}
/// Build the support map from the given election result. It maps a flat structure like
///
/// ```nocompile
/// assignments: vec![
/// voter1, vec![(candidate1, w11), (candidate2, w12)],
/// voter2, vec![(candidate1, w21), (candidate2, w22)]
/// voter1, vec![(candidate1, w11), (candidate2, w12)],
/// voter2, vec![(candidate1, w21), (candidate2, w22)]
/// ]
/// ```
///
@@ -560,16 +504,16 @@ pub fn seq_phragmen<AccountId, R>(
///
/// ```nocompile
/// SupportMap {
/// candidate1: Support {
/// own:0,
/// total: w11 + w21,
/// others: vec![(candidate1, w11), (candidate2, w21)]
/// },
/// candidate2: Support {
/// own:0,
/// total: w12 + w22,
/// others: vec![(candidate1, w12), (candidate2, w22)]
/// },
/// candidate1: Support {
/// own:0,
/// total: w11 + w21,
/// others: vec![(candidate1, w11), (candidate2, w21)]
/// },
/// candidate2: Support {
/// own:0,
/// total: w12 + w22,
/// others: vec![(candidate1, w12), (candidate2, w22)]
/// },
/// }
/// ```
///
@@ -581,10 +525,9 @@ pub fn seq_phragmen<AccountId, R>(
pub fn build_support_map<AccountId>(
winners: &[AccountId],
assignments: &[StakedAssignment<AccountId>],
) -> (SupportMap<AccountId>, u32) where
AccountId: Default + Ord + Clone,
) -> Result<SupportMap<AccountId>, AccountId> where
AccountId: IdentifierT,
{
let mut errors = 0;
// Initialize the support of each candidate.
let mut supports = <SupportMap<AccountId>>::new();
winners
@@ -598,11 +541,11 @@ pub fn build_support_map<AccountId>(
support.total = support.total.saturating_add(*weight_extended);
support.voters.push((who.clone(), *weight_extended));
} else {
errors = errors.saturating_add(1);
return Err(c.clone())
}
}
}
(supports, errors)
Ok(supports)
}
/// Evaluate a support map. The returned tuple contains:
@@ -631,8 +574,8 @@ pub fn evaluate_support<AccountId>(
[min_support, sum, sum_squared]
}
/// Compares two sets of election scores based on desirability and returns true if `this` is
/// better than `that`.
/// Compares two sets of election scores based on desirability and returns true if `this` is better
/// than `that`.
///
/// Evaluation is done in a lexicographic manner, and if each element of `this` is `that * epsilon`
/// greater or less than `that`.
@@ -665,139 +608,55 @@ pub fn is_score_better<P: PerThing>(this: ElectionScore, that: ElectionScore, ep
}
}
/// Performs balancing post-processing to the output of the election algorithm. This happens in
/// rounds. The number of rounds and the maximum diff-per-round tolerance can be tuned through input
/// parameters.
/// Converts raw inputs to types used in this crate.
///
/// Returns the number of iterations that were preformed.
///
/// - `assignments`: exactly the same as the output of [`seq_phragmen`].
/// - `supports`: mutable reference to s `SupportMap`. This parameter is updated.
/// - `tolerance`: maximum difference that can occur before an early quite happens.
/// - `iterations`: maximum number of iterations that will be processed.
pub fn balance_solution<AccountId>(
assignments: &mut Vec<StakedAssignment<AccountId>>,
supports: &mut SupportMap<AccountId>,
tolerance: ExtendedBalance,
iterations: usize,
) -> usize where AccountId: Ord + Clone {
if iterations == 0 { return 0; }
/// This will perform some cleanup that are most often important:
/// - It drops any votes that are pointing to non-candidates.
/// - It drops duplicate targets within a voter.
pub(crate) fn setup_inputs<AccountId: IdentifierT>(
initial_candidates: Vec<AccountId>,
initial_voters: Vec<(AccountId, VoteWeight, Vec<AccountId>)>,
) -> (Vec<CandidatePtr<AccountId>>, Vec<Voter<AccountId>>) {
// used to cache and access candidates index.
let mut c_idx_cache = BTreeMap::<AccountId, usize>::new();
let mut i = 0 ;
loop {
let mut max_diff = 0;
for assignment in assignments.iter_mut() {
let voter_budget = assignment.total();
let StakedAssignment { who, distribution } = assignment;
let diff = do_balancing(
who,
voter_budget,
distribution,
supports,
tolerance,
);
if diff > max_diff { max_diff = diff; }
}
let candidates = initial_candidates
.into_iter()
.enumerate()
.map(|(idx, who)| {
c_idx_cache.insert(who.clone(), idx);
Rc::new(RefCell::new(Candidate { who, ..Default::default() }))
})
.collect::<Vec<CandidatePtr<AccountId>>>();
i += 1;
if max_diff <= tolerance || i >= iterations {
break i;
}
}
}
/// actually perform balancing. same interface is `balance_solution`. Just called in loops with a check for
/// maximum difference.
fn do_balancing<AccountId>(
voter: &AccountId,
budget: ExtendedBalance,
elected_edges: &mut Vec<(AccountId, ExtendedBalance)>,
support_map: &mut SupportMap<AccountId>,
tolerance: ExtendedBalance
) -> ExtendedBalance where AccountId: Ord + Clone {
// Nothing to do. This voter had nothing useful.
// Defensive only. Assignment list should always be populated. 1 might happen for self vote.
if elected_edges.is_empty() || elected_edges.len() == 1 { return 0; }
let stake_used = elected_edges
.iter()
.fold(0 as ExtendedBalance, |s, e| s.saturating_add(e.1));
let backed_stakes_iter = elected_edges
.iter()
.filter_map(|e| support_map.get(&e.0))
.map(|e| e.total);
let backing_backed_stake = elected_edges
.iter()
.filter(|e| e.1 > 0)
.filter_map(|e| support_map.get(&e.0))
.map(|e| e.total)
.collect::<Vec<ExtendedBalance>>();
let mut difference;
if backing_backed_stake.len() > 0 {
let max_stake = backing_backed_stake
.iter()
.max()
.expect("vector with positive length will have a max; qed");
let min_stake = backed_stakes_iter
.min()
.expect("iterator with positive length will have a min; qed");
difference = max_stake.saturating_sub(min_stake);
difference = difference.saturating_add(budget.saturating_sub(stake_used));
if difference < tolerance {
return difference;
}
} else {
difference = budget;
}
// Undo updates to support
elected_edges.iter_mut().for_each(|e| {
if let Some(support) = support_map.get_mut(&e.0) {
support.total = support.total.saturating_sub(e.1);
support.voters.retain(|i_support| i_support.0 != *voter);
}
e.1 = 0;
});
elected_edges.sort_by_key(|e|
if let Some(e) = support_map.get(&e.0) { e.total } else { Zero::zero() }
);
let mut cumulative_stake: ExtendedBalance = 0;
let mut last_index = elected_edges.len() - 1;
let mut idx = 0usize;
for e in &mut elected_edges[..] {
if let Some(support) = support_map.get_mut(&e.0) {
let stake = support.total;
let stake_mul = stake.saturating_mul(idx as ExtendedBalance);
let stake_sub = stake_mul.saturating_sub(cumulative_stake);
if stake_sub > budget {
last_index = idx.checked_sub(1).unwrap_or(0);
break;
let voters = initial_voters.into_iter().map(|(who, voter_stake, votes)| {
let mut edges: Vec<Edge<AccountId>> = Vec::with_capacity(votes.len());
for v in votes {
if edges.iter().any(|e| e.who == v) {
// duplicate edge.
continue;
}
cumulative_stake = cumulative_stake.saturating_add(stake);
if let Some(idx) = c_idx_cache.get(&v) {
// This candidate is valid + already cached.
let mut candidate = candidates[*idx].borrow_mut();
candidate.approval_stake =
candidate.approval_stake.saturating_add(voter_stake.into());
edges.push(
Edge {
who: v.clone(),
candidate: Rc::clone(&candidates[*idx]),
..Default::default()
}
);
} // else {} would be wrong votes. We don't really care about it.
}
idx += 1;
}
let last_stake = elected_edges[last_index].1;
let split_ways = last_index + 1;
let excess = budget
.saturating_add(cumulative_stake)
.saturating_sub(last_stake.saturating_mul(split_ways as ExtendedBalance));
elected_edges.iter_mut().take(split_ways).for_each(|e| {
if let Some(support) = support_map.get_mut(&e.0) {
e.1 = (excess / split_ways as ExtendedBalance)
.saturating_add(last_stake)
.saturating_sub(support.total);
support.total = support.total.saturating_add(e.1);
support.voters.push((voter.clone(), e.1));
Voter {
who,
edges: edges,
budget: voter_stake.into(),
load: Rational128::zero(),
}
});
}).collect::<Vec<_>>();
difference
(candidates, voters,)
}
+12 -12
View File
@@ -20,7 +20,7 @@
#![cfg(test)]
use crate::{seq_phragmen, ElectionResult, Assignment, VoteWeight, ExtendedBalance};
use sp_arithmetic::{PerThing, traits::{SaturatedConversion, Zero, One}};
use sp_arithmetic::{PerThing, InnerOf, traits::{SaturatedConversion, Zero, One}};
use sp_std::collections::btree_map::BTreeMap;
use sp_runtime::assert_eq_error_rate;
@@ -71,7 +71,6 @@ pub(crate) fn auto_generate_self_voters<A: Clone>(candidates: &[A]) -> Vec<(A, V
pub(crate) fn elect_float<A, FS>(
candidate_count: usize,
minimum_candidate_count: usize,
initial_candidates: Vec<A>,
initial_voters: Vec<(A, Vec<A>)>,
stake_of: FS,
@@ -94,10 +93,6 @@ pub(crate) fn elect_float<A, FS>(
})
.collect::<Vec<_Candidate<A>>>();
if candidates.len() < minimum_candidate_count {
return None;
}
voters.extend(initial_voters.into_iter().map(|(who, votes)| {
let voter_stake = stake_of(&who) as f64;
let mut edges: Vec<_Edge<A>> = Vec::with_capacity(votes.len());
@@ -314,7 +309,7 @@ pub fn check_assignments_sum<T: PerThing>(assignments: Vec<Assignment<AccountId,
for Assignment { distribution, .. } in assignments {
let mut sum: u128 = Zero::zero();
distribution.iter().for_each(|(_, p)| sum += p.deconstruct().saturated_into());
assert_eq_error_rate!(sum, T::ACCURACY.saturated_into(), 1);
assert_eq!(sum, T::ACCURACY.saturated_into(), "Assignment ratio sum is not 100%");
}
}
@@ -323,20 +318,21 @@ pub(crate) fn run_and_compare<Output: PerThing>(
voters: Vec<(AccountId, Vec<AccountId>)>,
stake_of: &Box<dyn Fn(&AccountId) -> VoteWeight>,
to_elect: usize,
min_to_elect: usize,
) {
) where
ExtendedBalance: From<InnerOf<Output>>,
Output: sp_std::ops::Mul<ExtendedBalance, Output = ExtendedBalance>,
{
// run fixed point code.
let ElectionResult { winners, assignments } = seq_phragmen::<_, Output>(
to_elect,
min_to_elect,
candidates.clone(),
voters.iter().map(|(ref v, ref vs)| (v.clone(), stake_of(v), vs.clone())).collect::<Vec<_>>(),
None
).unwrap();
// run float poc code.
let truth_value = elect_float(
to_elect,
min_to_elect,
candidates,
voters,
&stake_of,
@@ -354,7 +350,11 @@ pub(crate) fn run_and_compare<Output: PerThing>(
Output::Inner::one(),
);
} else {
panic!("candidate mismatch. This should never happen.")
panic!(
"candidate mismatch. This should never happen. could not find ({:?}, {:?})",
candidate,
per_thingy,
)
}
}
} else {
@@ -0,0 +1,206 @@
// This file is part of Substrate.
// Copyright (C) 2020 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Implementation of the sequential-phragmen election method.
//!
//! This method is ensured to achieve PJR, yet, it does not achieve a constant factor approximation
//! to the Maximin problem.
use crate::{
IdentifierT, VoteWeight, Voter, CandidatePtr, ExtendedBalance, setup_inputs, ElectionResult,
};
use sp_std::prelude::*;
use sp_arithmetic::{
PerThing, InnerOf, Rational128,
helpers_128bit::multiply_by_rational,
traits::{Zero, Bounded},
};
use crate::balancing;
/// The denominator used for loads. Since votes are collected as u64, the smallest ratio that we
/// might collect is `1/approval_stake` where approval stake is the sum of votes. Hence, some number
/// bigger than u64::max_value() is needed. For maximum accuracy we simply use u128;
const DEN: ExtendedBalance = ExtendedBalance::max_value();
/// Execute sequential phragmen with potentially some rounds of `balancing`. The return type is list
/// of winners and a weight distribution vector of all voters who contribute to the winners.
///
/// - This function is a best effort to elect `rounds` members. Nonetheless, if less candidates are
/// available, it will only return what is available. It is the responsibility of the call site to
/// ensure they have provided enough members.
/// - If `balance` parameter is `Some(i, t)`, `i` iterations of balancing is with tolerance `t` is
/// performed.
/// - Returning winners are sorted based on desirability. Voters are unsorted. Nonetheless,
/// seq-phragmen is in general an un-ranked election and the desirability should not be
/// interpreted with any significance.
/// - The returning winners are zipped with their final backing stake. Yet, to get the exact final
/// weight distribution from the winner's point of view, one needs to build a support map. See
/// [`crate::SupportMap`] for more info. Note that this backing stake is computed in
/// ExtendedBalance and may be slightly different that what will be computed from the support map,
/// due to accuracy loss.
/// - The accuracy of the returning edge weight ratios can be configured via the `P` generic
/// argument.
/// - The returning weight distribution is _normalized_, meaning that it is guaranteed that the sum
/// of the ratios in each voter's distribution sums up to exactly `P::one()`.
///
/// This can only fail of the normalization fails. This can happen if for any of the resulting
/// assignments, `assignment.distribution.map(|p| p.deconstruct()).sum()` fails to fit inside
/// `UpperOf<P>`. A user of this crate may statically assert that this can never happen and safely
/// `expect` this to return `Ok`.
///
/// This can only fail if the normalization fails.
pub fn seq_phragmen<AccountId: IdentifierT, P: PerThing>(
rounds: usize,
initial_candidates: Vec<AccountId>,
initial_voters: Vec<(AccountId, VoteWeight, Vec<AccountId>)>,
balance: Option<(usize, ExtendedBalance)>,
) -> Result<ElectionResult<AccountId, P>, &'static str> where ExtendedBalance: From<InnerOf<P>> {
let (candidates, voters) = setup_inputs(initial_candidates, initial_voters);
let (candidates, mut voters) = seq_phragmen_core::<AccountId>(
rounds,
candidates,
voters,
)?;
if let Some((iterations, tolerance)) = balance {
// NOTE: might create zero-edges, but we will strip them again when we convert voter into
// assignment.
let _iters = balancing::balance::<AccountId>(&mut voters, iterations, tolerance);
}
let mut winners = candidates
.into_iter()
.filter(|c_ptr| c_ptr.borrow().elected)
// defensive only: seq-phragmen-core returns only up to rounds.
.take(rounds)
.collect::<Vec<_>>();
// sort winners based on desirability.
winners.sort_by_key(|c_ptr| c_ptr.borrow().round);
let mut assignments = voters.into_iter().filter_map(|v| v.into_assignment()).collect::<Vec<_>>();
let _ = assignments.iter_mut().map(|a| a.try_normalize()).collect::<Result<(), _>>()?;
let winners = winners.into_iter().map(|w_ptr|
(w_ptr.borrow().who.clone(), w_ptr.borrow().backed_stake)
).collect();
Ok(ElectionResult { winners, assignments })
}
/// Core implementation of seq-phragmen.
///
/// This is the internal implementation that works with the types defined in this crate. see
/// `seq_phragmen` for more information. This function is left public in case a crate needs to use
/// the implementation in a custom way.
///
/// To create th inputs needed for this function, see [`crate::setup_inputs`].
///
/// This can only fail if the normalization fails.
pub fn seq_phragmen_core<AccountId: IdentifierT>(
rounds: usize,
candidates: Vec<CandidatePtr<AccountId>>,
mut voters: Vec<Voter<AccountId>>,
) -> Result<(Vec<CandidatePtr<AccountId>>, Vec<Voter<AccountId>>), &'static str> {
// we have already checked that we have more candidates than minimum_candidate_count.
let to_elect = rounds.min(candidates.len());
// main election loop
for round in 0..to_elect {
// loop 1: initialize score
for c_ptr in &candidates {
let mut candidate = c_ptr.borrow_mut();
if !candidate.elected {
// 1 / approval_stake == (DEN / approval_stake) / DEN. If approval_stake is zero,
// then the ratio should be as large as possible, essentially `infinity`.
if candidate.approval_stake.is_zero() {
candidate.score = Bounded::max_value();
} else {
candidate.score = Rational128::from(DEN / candidate.approval_stake, DEN);
}
}
}
// loop 2: increment score
for voter in &voters {
for edge in &voter.edges {
let mut candidate = edge.candidate.borrow_mut();
if !candidate.elected && !candidate.approval_stake.is_zero() {
let temp_n = multiply_by_rational(
voter.load.n(),
voter.budget,
candidate.approval_stake,
).unwrap_or(Bounded::max_value());
let temp_d = voter.load.d();
let temp = Rational128::from(temp_n, temp_d);
candidate.score = candidate.score.lazy_saturating_add(temp);
}
}
}
// loop 3: find the best
if let Some(winner_ptr) = candidates
.iter()
.filter(|c| !c.borrow().elected)
.min_by_key(|c| c.borrow().score)
{
let mut winner = winner_ptr.borrow_mut();
// loop 3: update voter and edge load
winner.elected = true;
winner.round = round;
for voter in &mut voters {
for edge in &mut voter.edges {
if edge.who == winner.who {
edge.load = winner.score.lazy_saturating_sub(voter.load);
voter.load = winner.score;
}
}
}
} else {
break
}
}
// update backing stake of candidates and voters
for voter in &mut voters {
for edge in &mut voter.edges {
if edge.candidate.borrow().elected {
// update internal state.
edge.weight = multiply_by_rational(
voter.budget,
edge.load.n(),
voter.load.n(),
)
// If result cannot fit in u128. Not much we can do about it.
.unwrap_or(Bounded::max_value());
} else {
edge.weight = 0
}
let mut candidate = edge.candidate.borrow_mut();
candidate.backed_stake = candidate.backed_stake.saturating_add(edge.weight);
}
// remove all zero edges. These can become phantom edges during normalization.
voter.edges.retain(|e| e.weight > 0);
// edge of all candidates that eventually have a non-zero weight must be elected.
debug_assert!(voter.edges.iter().all(|e| e.candidate.borrow().elected));
// inc budget to sum the budget.
voter.try_normalize_elected()?;
}
Ok((candidates, voters))
}
@@ -0,0 +1,399 @@
// This file is part of Substrate.
// Copyright (C) 2020 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Implementation of the PhragMMS method.
//!
//! The naming comes from the fact that this method is highly inspired by Phragmen's method, yet it
//! _also_ provides a constant factor approximation of the Maximin problem, similar to that of the
//! MMS algorithm.
use crate::{
IdentifierT, ElectionResult, ExtendedBalance, setup_inputs, VoteWeight, Voter, CandidatePtr,
balance,
};
use sp_arithmetic::{PerThing, InnerOf, Rational128, traits::Bounded};
use sp_std::{prelude::*, rc::Rc};
/// Execute the phragmms method.
///
/// This can be used interchangeably with [`seq-phragmen`] and offers a similar API, namely:
///
/// - The resulting edge weight distribution is normalized (thus, safe to use for submission).
/// - The accuracy can be configured via the generic type `P`.
/// - The algorithm is a _best-effort_ to elect `to_elect`. If less candidates are provided, less
/// winners are returned, without an error.
///
/// This can only fail of the normalization fails. This can happen if for any of the resulting
/// assignments, `assignment.distribution.map(|p| p.deconstruct()).sum()` fails to fit inside
/// `UpperOf<P>`. A user of this crate may statically assert that this can never happen and safely
/// `expect` this to return `Ok`.
pub fn phragmms<AccountId: IdentifierT, P: PerThing>(
to_elect: usize,
initial_candidates: Vec<AccountId>,
initial_voters: Vec<(AccountId, VoteWeight, Vec<AccountId>)>,
balancing_config: Option<(usize, ExtendedBalance)>,
) -> Result<ElectionResult<AccountId, P>, &'static str>
where ExtendedBalance: From<InnerOf<P>>
{
let (candidates, mut voters) = setup_inputs(initial_candidates, initial_voters);
let mut winners = vec![];
for round in 0..to_elect {
if let Some(round_winner) = calculate_max_score::<AccountId, P>(&candidates, &voters) {
apply_elected::<AccountId>(&mut voters, Rc::clone(&round_winner));
round_winner.borrow_mut().round = round;
round_winner.borrow_mut().elected = true;
winners.push(round_winner);
if let Some((iterations, tolerance)) = balancing_config {
balance(&mut voters, iterations, tolerance);
}
} else {
break;
}
}
let mut assignments = voters.into_iter().filter_map(|v| v.into_assignment()).collect::<Vec<_>>();
let _ = assignments.iter_mut().map(|a| a.try_normalize()).collect::<Result<(), _>>()?;
let winners = winners.into_iter().map(|w_ptr|
(w_ptr.borrow().who.clone(), w_ptr.borrow().backed_stake)
).collect();
Ok(ElectionResult { winners, assignments })
}
/// Find the candidate that can yield the maximum score for this round.
///
/// Returns a new `Some(CandidatePtr)` to the winner candidate. The score of the candidate is
/// updated and can be read from the returned pointer.
///
/// If no winner can be determined (i.e. everyone is already elected), then `None` is returned.
///
/// This is an internal part of the [`phragmms`].
pub(crate) fn calculate_max_score<AccountId: IdentifierT, P: PerThing>(
candidates: &[CandidatePtr<AccountId>],
voters: &[Voter<AccountId>],
) -> Option<CandidatePtr<AccountId>> where ExtendedBalance: From<InnerOf<P>> {
for c_ptr in candidates.iter() {
let mut candidate = c_ptr.borrow_mut();
if !candidate.elected {
candidate.score = Rational128::from(1, P::ACCURACY.into());
}
}
for voter in voters.iter() {
let mut denominator_contribution: ExtendedBalance = 0;
// gather contribution from all elected edges.
for edge in voter.edges.iter() {
let edge_candidate = edge.candidate.borrow();
if edge_candidate.elected {
let edge_contribution: ExtendedBalance = P::from_rational_approximation(
edge.weight,
edge_candidate.backed_stake,
).deconstruct().into();
denominator_contribution += edge_contribution;
}
}
// distribute to all _unelected_ edges.
for edge in voter.edges.iter() {
let mut edge_candidate = edge.candidate.borrow_mut();
if !edge_candidate.elected {
let prev_d = edge_candidate.score.d();
edge_candidate.score = Rational128::from(1, denominator_contribution + prev_d);
}
}
}
// finalise the score value, and find the best.
let mut best_score = Rational128::zero();
let mut best_candidate = None;
for c_ptr in candidates.iter() {
let mut candidate = c_ptr.borrow_mut();
if candidate.approval_stake > 0 {
// finalise the score value.
let score_d = candidate.score.d();
let one: ExtendedBalance = P::ACCURACY.into();
// Note: the accuracy here is questionable.
// First, let's consider what will happen if this saturates. In this case, two very
// whale-like validators will be effectively the same and their score will be equal.
// This is, more or less fine if the threshold of saturation is high and only a small
// subset or ever likely to become saturated. Once saturated, the score of these whales
// are effectively the same.
// Let's consider when this will happen. The approval stake of a target is the sum of
// stake of all the voter who have backed this target. Given the fact that the total
// issuance of a sane chain will fit in u128, it is safe to also assume that the
// approval stake will, since it is a subset of the total issuance at most.
// Finally, the only chance of overflow is multiplication by `one`. This highly depends
// on the `P` generic argument. With a PerBill and a 12 decimal token the maximum value
// that `candidate.approval_stake` can have is:
// (2 ** 128 - 1) / 10**9 / 10**12 = 340,282,366,920,938,463
// Assuming that each target will have 200,000 voters, then each voter's stake can be
// roughly:
// (2 ** 128 - 1) / 10**9 / 10**12 / 200000 = 1,701,411,834,604
//
// It is worth noting that these value would be _very_ different if one were to use
// `PerQuintill` as `P`. For now, we prefer the performance of using `Rational128` here.
// For the future, a properly benchmarked pull request can prove that using
// `RationalInfinite` as the score type does not introduce significant overhead. Then we
// can switch the score type to `RationalInfinite` and ensure compatibility with any
// crazy token scale.
let score_n = candidate.approval_stake.checked_mul(one).unwrap_or_else(|| Bounded::max_value());
candidate.score = Rational128::from(score_n, score_d);
// check if we have a new winner.
if !candidate.elected && candidate.score > best_score {
best_score = candidate.score;
best_candidate = Some(Rc::clone(&c_ptr));
}
} else {
candidate.score = Rational128::zero();
}
}
best_candidate
}
/// Update the weights of `voters` given that `elected_ptr` has been elected in the previous round.
///
/// Updates `voters` in place.
///
/// This is an internal part of the [`phragmms`] and should be called after
/// [`calculate_max_score`].
pub(crate) fn apply_elected<AccountId: IdentifierT>(
voters: &mut Vec<Voter<AccountId>>,
elected_ptr: CandidatePtr<AccountId>,
) {
let elected_who = elected_ptr.borrow().who.clone();
let cutoff = elected_ptr.borrow().score.to_den(1)
.expect("(n / d) < u128::max() and (n' / 1) == (n / d), thus n' < u128::max()'; qed.")
.n();
let mut elected_backed_stake = elected_ptr.borrow().backed_stake;
for voter in voters {
if let Some(new_edge_index) = voter.edges.iter().position(|e| e.who == elected_who) {
let used_budget: ExtendedBalance = voter.edges.iter().map(|e| e.weight).sum();
let mut new_edge_weight = voter.budget.saturating_sub(used_budget);
elected_backed_stake = elected_backed_stake.saturating_add(new_edge_weight);
// Iterate over all other edges.
for (_, edge) in voter.edges
.iter_mut()
.enumerate()
.filter(|(edge_index, edge_inner)| *edge_index != new_edge_index && edge_inner.weight > 0)
{
let mut edge_candidate = edge.candidate.borrow_mut();
if edge_candidate.backed_stake > cutoff {
let stake_to_take = edge.weight.saturating_mul(cutoff) / edge_candidate.backed_stake.max(1);
// subtract this amount from this edge.
edge.weight = edge.weight.saturating_sub(stake_to_take);
edge_candidate.backed_stake = edge_candidate.backed_stake.saturating_sub(stake_to_take);
// inject it into the outer loop's edge.
elected_backed_stake = elected_backed_stake.saturating_add(stake_to_take);
new_edge_weight = new_edge_weight.saturating_add(stake_to_take);
}
}
voter.edges[new_edge_index].weight = new_edge_weight;
}
}
// final update.
elected_ptr.borrow_mut().backed_stake = elected_backed_stake;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ElectionResult, Assignment};
use sp_runtime::{Perbill, Percent};
use sp_std::rc::Rc;
#[test]
fn basic_election_manual_works() {
//! Manually run the internal steps of phragmms. In each round we select a new winner by
//! `max_score`, then apply this change by `apply_elected`, and finally do a `balance` round.
let candidates = vec![1, 2, 3];
let voters = vec![
(10, 10, vec![1, 2]),
(20, 20, vec![1, 3]),
(30, 30, vec![2, 3]),
];
let (candidates, mut voters) = setup_inputs(candidates, voters);
// Round 1
let winner = calculate_max_score::<u32, Percent>(candidates.as_ref(), voters.as_ref()).unwrap();
assert_eq!(winner.borrow().who, 3);
assert_eq!(winner.borrow().score, 50u32.into());
apply_elected(&mut voters, Rc::clone(&winner));
assert_eq!(
voters.iter().find(|x| x.who == 30).map(|v| (
v.who,
v.edges.iter().map(|e| (e.who, e.weight)).collect::<Vec<_>>()
)).unwrap(),
(30, vec![(2, 0), (3, 30)]),
);
assert_eq!(
voters.iter().find(|x| x.who == 20).map(|v| (
v.who,
v.edges.iter().map(|e| (e.who, e.weight)).collect::<Vec<_>>()
)).unwrap(),
(20, vec![(1, 0), (3, 20)]),
);
// finish the round.
winner.borrow_mut().elected = true;
winner.borrow_mut().round = 0;
drop(winner);
// balancing makes no difference here but anyhow.
balance(&mut voters, 10, 0);
// round 2
let winner = calculate_max_score::<u32, Percent>(candidates.as_ref(), voters.as_ref()).unwrap();
assert_eq!(winner.borrow().who, 2);
assert_eq!(winner.borrow().score, 25u32.into());
apply_elected(&mut voters, Rc::clone(&winner));
assert_eq!(
voters.iter().find(|x| x.who == 30).map(|v| (
v.who,
v.edges.iter().map(|e| (e.who, e.weight)).collect::<Vec<_>>()
)).unwrap(),
(30, vec![(2, 15), (3, 15)]),
);
assert_eq!(
voters.iter().find(|x| x.who == 20).map(|v| (
v.who,
v.edges.iter().map(|e| (e.who, e.weight)).collect::<Vec<_>>()
)).unwrap(),
(20, vec![(1, 0), (3, 20)]),
);
assert_eq!(
voters.iter().find(|x| x.who == 10).map(|v| (
v.who,
v.edges.iter().map(|e| (e.who, e.weight)).collect::<Vec<_>>()
)).unwrap(),
(10, vec![(1, 0), (2, 10)]),
);
// finish the round.
winner.borrow_mut().elected = true;
winner.borrow_mut().round = 0;
drop(winner);
// balancing will improve stuff here.
balance(&mut voters, 10, 0);
assert_eq!(
voters.iter().find(|x| x.who == 30).map(|v| (
v.who,
v.edges.iter().map(|e| (e.who, e.weight)).collect::<Vec<_>>()
)).unwrap(),
(30, vec![(2, 20), (3, 10)]),
);
assert_eq!(
voters.iter().find(|x| x.who == 20).map(|v| (
v.who,
v.edges.iter().map(|e| (e.who, e.weight)).collect::<Vec<_>>()
)).unwrap(),
(20, vec![(1, 0), (3, 20)]),
);
assert_eq!(
voters.iter().find(|x| x.who == 10).map(|v| (
v.who,
v.edges.iter().map(|e| (e.who, e.weight)).collect::<Vec<_>>()
)).unwrap(),
(10, vec![(1, 0), (2, 10)]),
);
}
#[test]
fn basic_election_works() {
let candidates = vec![1, 2, 3];
let voters = vec![
(10, 10, vec![1, 2]),
(20, 20, vec![1, 3]),
(30, 30, vec![2, 3]),
];
let ElectionResult { winners, assignments } = phragmms::<_, Perbill>(2, candidates, voters, Some((2, 0))).unwrap();
assert_eq!(winners, vec![(3, 30), (2, 30)]);
assert_eq!(
assignments,
vec![
Assignment {
who: 10u64,
distribution: vec![(2, Perbill::one())],
},
Assignment {
who: 20,
distribution: vec![(3, Perbill::one())],
},
Assignment {
who: 30,
distribution: vec![
(2, Perbill::from_parts(666666666)),
(3, Perbill::from_parts(333333334)),
],
},
]
)
}
#[test]
fn linear_voting_example_works() {
let candidates = vec![11, 21, 31, 41, 51, 61, 71];
let voters = vec![
(2, 2000, vec![11]),
(4, 1000, vec![11, 21]),
(6, 1000, vec![21, 31]),
(8, 1000, vec![31, 41]),
(110, 1000, vec![41, 51]),
(120, 1000, vec![51, 61]),
(130, 1000, vec![61, 71]),
];
let ElectionResult { winners, assignments: _ } = phragmms::<_, Perbill>(4, candidates, voters, Some((2, 0))).unwrap();
assert_eq!(winners, vec![
(11, 3000),
(31, 2000),
(51, 1500),
(61, 1500),
]);
}
#[test]
fn large_balance_wont_overflow() {
let candidates = vec![1u32, 2, 3];
let mut voters = (0..1000).map(|i| (10 + i, u64::max_value(), vec![1, 2, 3])).collect::<Vec<_>>();
// give a bit more to 1 and 3.
voters.push((2, u64::max_value(), vec![1, 3]));
let ElectionResult { winners, assignments: _ } = phragmms::<_, Perbill>(2, candidates, voters, Some((2, 0))).unwrap();
assert_eq!(winners.into_iter().map(|(w, _)| w).collect::<Vec<_>>(), vec![1u32, 3]);
}
}
+241 -91
View File
@@ -19,8 +19,9 @@
use crate::mock::*;
use crate::{
seq_phragmen, balance_solution, build_support_map, is_score_better, helpers::*,
Support, StakedAssignment, Assignment, ElectionResult, ExtendedBalance,
seq_phragmen, balancing, build_support_map, is_score_better, helpers::*,
Support, StakedAssignment, Assignment, ElectionResult, ExtendedBalance, setup_inputs,
seq_phragmen_core, Voter,
};
use substrate_test_utils::assert_eq_uvec;
use sp_arithmetic::{Perbill, Permill, Percent, PerU16};
@@ -34,7 +35,7 @@ fn float_phragmen_poc_works() {
(30, vec![2, 3]),
];
let stake_of = create_stake_of(&[(10, 10), (20, 20), (30, 30), (1, 0), (2, 0), (3, 0)]);
let mut phragmen_result = elect_float(2, 2, candidates, voters, &stake_of).unwrap();
let mut phragmen_result = elect_float(2, candidates, voters, &stake_of).unwrap();
let winners = phragmen_result.clone().winners;
let assignments = phragmen_result.clone().assignments;
@@ -71,6 +72,153 @@ fn float_phragmen_poc_works() {
);
}
#[test]
fn phragmen_core_poc_works() {
let candidates = vec![1, 2, 3];
let voters = vec![
(10, 10, vec![1, 2]),
(20, 20, vec![1, 3]),
(30, 30, vec![2, 3]),
];
let (candidates, voters) = setup_inputs(candidates, voters);
let (candidates, voters) = seq_phragmen_core(2, candidates, voters).unwrap();
assert_eq!(
voters
.iter()
.map(|v| (
v.who,
v.budget,
(v.edges.iter().map(|e| (e.who, e.weight)).collect::<Vec<_>>()),
))
.collect::<Vec<_>>(),
vec![
(10, 10, vec![(2, 10)]),
(20, 20, vec![(3, 20)]),
(30, 30, vec![(2, 15), (3, 15)]),
]
);
assert_eq!(
candidates
.iter()
.map(|c_ptr| (
c_ptr.borrow().who,
c_ptr.borrow().elected,
c_ptr.borrow().round,
c_ptr.borrow().backed_stake,
)).collect::<Vec<_>>(),
vec![
(1, false, 0, 0),
(2, true, 1, 25),
(3, true, 0, 35),
]
);
}
#[test]
fn balancing_core_works() {
let candidates = vec![1, 2, 3, 4, 5];
let voters = vec![
(10, 10, vec![1, 2]),
(20, 20, vec![1, 3]),
(30, 30, vec![1, 2, 3, 4]),
(40, 40, vec![1, 3, 4, 5]),
(50, 50, vec![2, 4, 5]),
];
let (candidates, voters) = setup_inputs(candidates, voters);
let (candidates, mut voters) = seq_phragmen_core(4, candidates, voters).unwrap();
let iters = balancing::balance::<AccountId>(&mut voters, 4, 0);
assert!(iters > 0);
assert_eq!(
voters
.iter()
.map(|v| (
v.who,
v.budget,
(v.edges.iter().map(|e| (e.who, e.weight)).collect::<Vec<_>>()),
))
.collect::<Vec<_>>(),
vec![
// note the 0 edge. This is know and not an issue per se. Also note that the stakes are
// normalized.
(10, 10, vec![(1, 9), (2, 1)]),
(20, 20, vec![(1, 9), (3, 11)]),
(30, 30, vec![(1, 8), (2, 7), (3, 8), (4, 7)]),
(40, 40, vec![(1, 11), (3, 18), (4, 11)]),
(50, 50, vec![(2, 30), (4, 20)]),
]
);
assert_eq!(
candidates
.iter()
.map(|c_ptr| (
c_ptr.borrow().who,
c_ptr.borrow().elected,
c_ptr.borrow().round,
c_ptr.borrow().backed_stake,
)).collect::<Vec<_>>(),
vec![
(1, true, 1, 37),
(2, true, 2, 38),
(3, true, 3, 37),
(4, true, 0, 38),
(5, false, 0, 0),
]
);
}
#[test]
fn voter_normalize_ops_works() {
use crate::{Candidate, Edge};
use sp_std::{cell::RefCell, rc::Rc};
// normalize
{
let c1 = Candidate { who: 10, elected: false ,..Default::default() };
let c2 = Candidate { who: 20, elected: false ,..Default::default() };
let c3 = Candidate { who: 30, elected: false ,..Default::default() };
let e1 = Edge { candidate: Rc::new(RefCell::new(c1)), weight: 30, ..Default::default() };
let e2 = Edge { candidate: Rc::new(RefCell::new(c2)), weight: 33, ..Default::default() };
let e3 = Edge { candidate: Rc::new(RefCell::new(c3)), weight: 30, ..Default::default() };
let mut v = Voter {
who: 1,
budget: 100,
edges: vec![e1, e2, e3],
..Default::default()
};
v.try_normalize().unwrap();
assert_eq!(v.edges.iter().map(|e| e.weight).collect::<Vec<_>>(), vec![34, 33, 33]);
}
// // normalize_elected
{
let c1 = Candidate { who: 10, elected: false ,..Default::default() };
let c2 = Candidate { who: 20, elected: true ,..Default::default() };
let c3 = Candidate { who: 30, elected: true ,..Default::default() };
let e1 = Edge { candidate: Rc::new(RefCell::new(c1)), weight: 30, ..Default::default() };
let e2 = Edge { candidate: Rc::new(RefCell::new(c2)), weight: 33, ..Default::default() };
let e3 = Edge { candidate: Rc::new(RefCell::new(c3)), weight: 30, ..Default::default() };
let mut v = Voter {
who: 1,
budget: 100,
edges: vec![e1, e2, e3],
..Default::default()
};
v.try_normalize_elected().unwrap();
assert_eq!(v.edges.iter().map(|e| e.weight).collect::<Vec<_>>(), vec![30, 34, 66]);
}
}
#[test]
fn phragmen_poc_works() {
let candidates = vec![1, 2, 3];
@@ -82,13 +230,13 @@ fn phragmen_poc_works() {
let stake_of = create_stake_of(&[(10, 10), (20, 20), (30, 30)]);
let ElectionResult { winners, assignments } = seq_phragmen::<_, Perbill>(
2,
2,
candidates,
voters.iter().map(|(ref v, ref vs)| (v.clone(), stake_of(v), vs.clone())).collect::<Vec<_>>(),
None,
).unwrap();
assert_eq_uvec!(winners, vec![(2, 40), (3, 50)]);
assert_eq_uvec!(winners, vec![(2, 25), (3, 35)]);
assert_eq_uvec!(
assignments,
vec![
@@ -110,9 +258,9 @@ fn phragmen_poc_works() {
]
);
let mut staked = assignment_ratio_to_staked(assignments, &stake_of);
let staked = assignment_ratio_to_staked(assignments, &stake_of);
let winners = to_without_backing(winners);
let mut support_map = build_support_map::<AccountId>(&winners, &staked).0;
let support_map = build_support_map::<AccountId>(&winners, &staked).unwrap();
assert_eq_uvec!(
staked,
@@ -143,14 +291,51 @@ fn phragmen_poc_works() {
*support_map.get(&3).unwrap(),
Support::<AccountId> { total: 35, voters: vec![(20, 20), (30, 15)] },
);
}
balance_solution(
&mut staked,
&mut support_map,
0,
#[test]
fn phragmen_poc_works_with_balancing() {
let candidates = vec![1, 2, 3];
let voters = vec![
(10, vec![1, 2]),
(20, vec![1, 3]),
(30, vec![2, 3]),
];
let stake_of = create_stake_of(&[(10, 10), (20, 20), (30, 30)]);
let ElectionResult { winners, assignments } = seq_phragmen::<_, Perbill>(
2,
candidates,
voters.iter().map(|(ref v, ref vs)| (v.clone(), stake_of(v), vs.clone())).collect::<Vec<_>>(),
Some((4, 0)),
).unwrap();
assert_eq_uvec!(winners, vec![(2, 30), (3, 30)]);
assert_eq_uvec!(
assignments,
vec![
Assignment {
who: 10u64,
distribution: vec![(2, Perbill::from_percent(100))],
},
Assignment {
who: 20,
distribution: vec![(3, Perbill::from_percent(100))],
},
Assignment {
who: 30,
distribution: vec![
(2, Perbill::from_parts(666666666)),
(3, Perbill::from_parts(333333334)),
],
},
]
);
let staked = assignment_ratio_to_staked(assignments, &stake_of);
let winners = to_without_backing(winners);
let support_map = build_support_map::<AccountId>(&winners, &staked).unwrap();
assert_eq_uvec!(
staked,
vec![
@@ -182,6 +367,7 @@ fn phragmen_poc_works() {
);
}
#[test]
fn phragmen_poc_2_works() {
let candidates = vec![10, 20, 30];
@@ -198,10 +384,10 @@ fn phragmen_poc_2_works() {
(4, 500),
]);
run_and_compare::<Perbill>(candidates.clone(), voters.clone(), &stake_of, 2, 2);
run_and_compare::<Permill>(candidates.clone(), voters.clone(), &stake_of, 2, 2);
run_and_compare::<Percent>(candidates.clone(), voters.clone(), &stake_of, 2, 2);
run_and_compare::<PerU16>(candidates, voters, &stake_of, 2, 2);
run_and_compare::<Perbill>(candidates.clone(), voters.clone(), &stake_of, 2);
run_and_compare::<Permill>(candidates.clone(), voters.clone(), &stake_of, 2);
run_and_compare::<Percent>(candidates.clone(), voters.clone(), &stake_of, 2);
run_and_compare::<PerU16>(candidates, voters, &stake_of, 2);
}
#[test]
@@ -219,14 +405,14 @@ fn phragmen_poc_3_works() {
(4, 1000),
]);
run_and_compare::<Perbill>(candidates.clone(), voters.clone(), &stake_of, 2, 2);
run_and_compare::<Permill>(candidates.clone(), voters.clone(), &stake_of, 2, 2);
run_and_compare::<Percent>(candidates.clone(), voters.clone(), &stake_of, 2, 2);
run_and_compare::<PerU16>(candidates, voters, &stake_of, 2, 2);
run_and_compare::<Perbill>(candidates.clone(), voters.clone(), &stake_of, 2);
run_and_compare::<Permill>(candidates.clone(), voters.clone(), &stake_of, 2);
run_and_compare::<Percent>(candidates.clone(), voters.clone(), &stake_of, 2);
run_and_compare::<PerU16>(candidates, voters, &stake_of, 2);
}
#[test]
fn phragmen_accuracy_on_large_scale_only_validators() {
fn phragmen_accuracy_on_large_scale_only_candidates() {
// because of this particular situation we had per_u128 and now rational128. In practice, a
// candidate can have the maximum amount of tokens, and also supported by the maximum.
let candidates = vec![1, 2, 3, 4, 5];
@@ -239,13 +425,13 @@ fn phragmen_accuracy_on_large_scale_only_validators() {
]);
let ElectionResult { winners, assignments } = seq_phragmen::<_, Perbill>(
2,
2,
candidates.clone(),
auto_generate_self_voters(&candidates)
.iter()
.map(|(ref v, ref vs)| (v.clone(), stake_of(v), vs.clone()))
.collect::<Vec<_>>(),
None,
).unwrap();
assert_eq_uvec!(winners, vec![(1, 18446744073709551614u128), (5, 18446744073709551613u128)]);
@@ -254,7 +440,7 @@ fn phragmen_accuracy_on_large_scale_only_validators() {
}
#[test]
fn phragmen_accuracy_on_large_scale_validators_and_nominators() {
fn phragmen_accuracy_on_large_scale_voters_and_candidates() {
let candidates = vec![1, 2, 3, 4, 5];
let mut voters = vec![
(13, vec![1, 3, 5]),
@@ -272,13 +458,14 @@ fn phragmen_accuracy_on_large_scale_validators_and_nominators() {
]);
let ElectionResult { winners, assignments } = seq_phragmen::<_, Perbill>(
2,
2,
candidates,
voters.iter().map(|(ref v, ref vs)| (v.clone(), stake_of(v), vs.clone())).collect::<Vec<_>>(),
None,
).unwrap();
assert_eq_uvec!(winners, vec![(2, 36893488147419103226u128), (1, 36893488147419103219u128)]);
assert_eq!(
assignments,
vec![
@@ -300,6 +487,7 @@ fn phragmen_accuracy_on_large_scale_validators_and_nominators() {
},
]
);
check_assignments_sum(assignments);
}
@@ -314,14 +502,15 @@ fn phragmen_accuracy_on_small_scale_self_vote() {
(30, 1),
]);
let ElectionResult { winners, assignments: _ } = seq_phragmen::<_, Perbill>(
3,
let ElectionResult { winners, assignments } = seq_phragmen::<_, Perbill>(
3,
candidates,
voters.iter().map(|(ref v, ref vs)| (v.clone(), stake_of(v), vs.clone())).collect::<Vec<_>>(),
None,
).unwrap();
assert_eq_uvec!(winners, vec![(20, 2), (10, 1), (30, 1)]);
check_assignments_sum(assignments);
}
#[test]
@@ -344,14 +533,16 @@ fn phragmen_accuracy_on_small_scale_no_self_vote() {
(3, 1),
]);
let ElectionResult { winners, assignments: _ } = seq_phragmen::<_, Perbill>(
3,
let ElectionResult { winners, assignments } = seq_phragmen::<_, Perbill>(
3,
candidates,
voters.iter().map(|(ref v, ref vs)| (v.clone(), stake_of(v), vs.clone())).collect::<Vec<_>>(),
None,
).unwrap();
assert_eq_uvec!(winners, vec![(20, 2), (10, 1), (30, 1)]);
check_assignments_sum(assignments);
}
#[test]
@@ -378,13 +569,13 @@ fn phragmen_large_scale_test() {
]);
let ElectionResult { winners, assignments } = seq_phragmen::<_, Perbill>(
2,
2,
candidates,
voters.iter().map(|(ref v, ref vs)| (v.clone(), stake_of(v), vs.clone())).collect::<Vec<_>>(),
None,
).unwrap();
assert_eq_uvec!(winners, vec![(24, 1490000000000200000u128), (22, 1490000000000100000u128)]);
assert_eq_uvec!(to_without_backing(winners.clone()), vec![24, 22]);
check_assignments_sum(assignments);
}
@@ -404,21 +595,22 @@ fn phragmen_large_scale_test_2() {
]);
let ElectionResult { winners, assignments } = seq_phragmen::<_, Perbill>(
2,
2,
candidates,
voters.iter().map(|(ref v, ref vs)| (v.clone(), stake_of(v), vs.clone())).collect::<Vec<_>>(),
None,
).unwrap();
assert_eq_uvec!(winners, vec![(2, 1000000000004000000u128), (4, 1000000000004000000u128)]);
assert_eq!(
assert_eq_uvec!(winners, vec![(2, 500000000005000000u128), (4, 500000000003000000)]);
assert_eq_uvec!(
assignments,
vec![
Assignment {
who: 50u64,
distribution: vec![
(2, Perbill::from_parts(500000001)),
(4, Perbill::from_parts(499999999))
(2, Perbill::from_parts(500000000)),
(4, Perbill::from_parts(500000000)),
],
},
Assignment {
@@ -431,6 +623,7 @@ fn phragmen_large_scale_test_2() {
},
],
);
check_assignments_sum(assignments);
}
@@ -464,7 +657,7 @@ fn phragmen_linear_equalize() {
(130, 1000),
]);
run_and_compare::<Perbill>(candidates, voters, &stake_of, 2, 2);
run_and_compare::<Perbill>(candidates, voters, &stake_of, 2);
}
#[test]
@@ -480,10 +673,10 @@ fn elect_has_no_entry_barrier() {
]);
let ElectionResult { winners, assignments: _ } = seq_phragmen::<_, Perbill>(
3,
3,
candidates,
voters.iter().map(|(ref v, ref vs)| (v.clone(), stake_of(v), vs.clone())).collect::<Vec<_>>(),
None,
).unwrap();
// 30 is elected with stake 0. The caller is responsible for stripping this.
@@ -495,29 +688,7 @@ fn elect_has_no_entry_barrier() {
}
#[test]
fn minimum_to_elect_is_respected() {
let candidates = vec![10, 20, 30];
let voters = vec![
(1, vec![10]),
(2, vec![20]),
];
let stake_of = create_stake_of(&[
(1, 10),
(2, 10),
]);
let maybe_result = seq_phragmen::<_, Perbill>(
10,
10,
candidates,
voters.iter().map(|(ref v, ref vs)| (v.clone(), stake_of(v), vs.clone())).collect::<Vec<_>>(),
);
assert!(maybe_result.is_none());
}
#[test]
fn self_votes_should_be_kept() {
fn phragmen_self_votes_should_be_kept() {
let candidates = vec![5, 10, 20, 30];
let voters = vec![
(5, vec![5]),
@@ -533,33 +704,29 @@ fn self_votes_should_be_kept() {
]);
let result = seq_phragmen::<_, Perbill>(
2,
2,
candidates,
voters.iter().map(|(ref v, ref vs)| (v.clone(), stake_of(v), vs.clone())).collect::<Vec<_>>(),
None,
).unwrap();
assert_eq!(result.winners, vec![(20, 28), (10, 18)]);
assert_eq!(
assert_eq!(result.winners, vec![(20, 24), (10, 14)]);
assert_eq_uvec!(
result.assignments,
vec![
Assignment { who: 10, distribution: vec![(10, Perbill::from_percent(100))] },
Assignment { who: 20, distribution: vec![(20, Perbill::from_percent(100))] },
Assignment { who: 1, distribution: vec![
(10, Perbill::from_percent(50)),
(20, Perbill::from_percent(50))
(20, Perbill::from_percent(50)),
]
},
],
Assignment { who: 10, distribution: vec![(10, Perbill::from_percent(100))] },
Assignment { who: 20, distribution: vec![(20, Perbill::from_percent(100))] },
]
);
let mut staked_assignments = assignment_ratio_to_staked(result.assignments, &stake_of);
let staked_assignments = assignment_ratio_to_staked(result.assignments, &stake_of);
let winners = to_without_backing(result.winners);
let (mut supports, _) = build_support_map::<AccountId>(
&winners,
&staked_assignments,
);
let supports = build_support_map::<AccountId>(&winners, &staked_assignments).unwrap();
assert_eq!(supports.get(&5u64), None);
assert_eq!(
@@ -570,22 +737,6 @@ fn self_votes_should_be_kept() {
supports.get(&20u64).unwrap(),
&Support { total: 24u128, voters: vec![(20u64, 20u128), (1u64, 4u128)] },
);
balance_solution(
&mut staked_assignments,
&mut supports,
0,
2usize,
);
assert_eq!(
supports.get(&10u64).unwrap(),
&Support { total: 18u128, voters: vec![(10u64, 10u128), (1u64, 8u128)] },
);
assert_eq!(
supports.get(&20u64).unwrap(),
&Support { total: 20u128, voters: vec![(20u64, 20u128)] },
);
}
#[test]
@@ -598,10 +749,10 @@ fn duplicate_target_is_ignored() {
];
let ElectionResult { winners, assignments } = seq_phragmen::<_, Perbill>(
2,
2,
candidates,
voters,
None,
).unwrap();
let winners = to_without_backing(winners);
@@ -628,10 +779,10 @@ fn duplicate_target_is_ignored_when_winner() {
];
let ElectionResult { winners, assignments } = seq_phragmen::<_, Perbill>(
2,
2,
candidates,
voters,
None,
).unwrap();
let winners = to_without_backing(winners);
@@ -979,7 +1130,6 @@ mod solution_type {
compact.encode().len()
};
dbg!(with_compact, without_compact);
assert!(with_compact < without_compact);
}