mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-11 15:21:08 +00:00
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:
Generated
+2
-2
@@ -2205,9 +2205,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "honggfuzz"
|
||||
version = "0.5.49"
|
||||
version = "0.5.51"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "832bac18a82ec7d6c21887daa8616b238fe90d5d5e762d0d4b9372cdaa9e097f"
|
||||
checksum = "6f085725a5828d7e959f014f624773094dfe20acc91be310ef106923c30594bc"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"lazy_static",
|
||||
|
||||
@@ -110,7 +110,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
|
||||
// implementation changes and behavior does not, then leave spec_version as
|
||||
// is and increment impl_version.
|
||||
spec_version: 259,
|
||||
impl_version: 0,
|
||||
impl_version: 1,
|
||||
apis: RUNTIME_API_VERSIONS,
|
||||
transaction_version: 1,
|
||||
};
|
||||
|
||||
@@ -295,10 +295,10 @@ mod tests {
|
||||
#[test]
|
||||
fn claim_secondary_plain_slot_works() {
|
||||
let keystore = sc_keystore::Store::new_in_memory();
|
||||
let valid_public_key = dbg!(keystore.write().sr25519_generate_new(
|
||||
let valid_public_key = keystore.write().sr25519_generate_new(
|
||||
AuthorityId::ID,
|
||||
Some(sp_core::crypto::DEV_PHRASE),
|
||||
).unwrap());
|
||||
).unwrap();
|
||||
|
||||
let authorities = vec![
|
||||
(AuthorityId::from(Pair::generate().0.public()), 5),
|
||||
|
||||
@@ -100,7 +100,7 @@ use frame_support::{
|
||||
ContainsLengthBound,
|
||||
}
|
||||
};
|
||||
use sp_npos_elections::{build_support_map, ExtendedBalance, VoteWeight, ElectionResult};
|
||||
use sp_npos_elections::{ExtendedBalance, VoteWeight, ElectionResult};
|
||||
use frame_system::{ensure_signed, ensure_root};
|
||||
|
||||
mod benchmarking;
|
||||
@@ -209,7 +209,7 @@ decl_storage! {
|
||||
// ---- State
|
||||
/// The current elected membership. Sorted based on account id.
|
||||
pub Members get(fn members): Vec<(T::AccountId, BalanceOf<T>)>;
|
||||
/// The current runners_up. Sorted based on low to high merit (worse to best runner).
|
||||
/// The current runners_up. Sorted based on low to high merit (worse to best).
|
||||
pub RunnersUp get(fn runners_up): Vec<(T::AccountId, BalanceOf<T>)>;
|
||||
/// The total number of vote rounds that have happened, excluding the upcoming one.
|
||||
pub ElectionRounds get(fn election_rounds): u32 = Zero::zero();
|
||||
@@ -689,7 +689,9 @@ decl_event!(
|
||||
/// No (or not enough) candidates existed for this round. This is different from
|
||||
/// `NewTerm(\[\])`. See the description of `NewTerm`.
|
||||
EmptyTerm,
|
||||
/// A \[member\] has been removed. This should always be followed by either `NewTerm` ot
|
||||
/// Internal error happened while trying to perform election.
|
||||
ElectionError,
|
||||
/// A \[member\] has been removed. This should always be followed by either `NewTerm` or
|
||||
/// `EmptyTerm`.
|
||||
MemberKicked(AccountId),
|
||||
/// A \[member\] has renounced their candidacy.
|
||||
@@ -827,11 +829,6 @@ impl<T: Trait> Module<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The locked stake of a voter.
|
||||
fn locked_stake_of(who: &T::AccountId) -> BalanceOf<T> {
|
||||
Voting::<T>::get(who).0
|
||||
}
|
||||
|
||||
/// Check there's nothing to do this block.
|
||||
///
|
||||
/// Runs phragmen election and cleans all the previous candidate state. The voter state is NOT
|
||||
@@ -846,7 +843,8 @@ impl<T: Trait> Module<T> {
|
||||
0
|
||||
}
|
||||
|
||||
/// Run the phragmen election with all required side processes and state updates.
|
||||
/// Run the phragmen election with all required side processes and state updates, if election
|
||||
/// succeeds. Else, it will emit an `ElectionError` event.
|
||||
///
|
||||
/// Calls the appropriate [`ChangeMembers`] function variant internally.
|
||||
///
|
||||
@@ -867,6 +865,11 @@ impl<T: Trait> Module<T> {
|
||||
// previous runners_up are also always candidates for the next round.
|
||||
candidates.append(&mut Self::runners_up_ids());
|
||||
|
||||
if candidates.len().is_zero() {
|
||||
Self::deposit_event(RawEvent::EmptyTerm);
|
||||
return;
|
||||
}
|
||||
|
||||
// helper closures to deal with balance/stake.
|
||||
let to_votes = |b: BalanceOf<T>| -> VoteWeight {
|
||||
<T::CurrencyToVote as Convert<BalanceOf<T>, VoteWeight>>::convert(b)
|
||||
@@ -874,9 +877,6 @@ impl<T: Trait> Module<T> {
|
||||
let to_balance = |e: ExtendedBalance| -> BalanceOf<T> {
|
||||
<T::CurrencyToVote as Convert<ExtendedBalance, BalanceOf<T>>>::convert(e)
|
||||
};
|
||||
let stake_of = |who: &T::AccountId| -> VoteWeight {
|
||||
to_votes(Self::locked_stake_of(who))
|
||||
};
|
||||
|
||||
// used for prime election.
|
||||
let voters_and_stakes = Voting::<T>::iter()
|
||||
@@ -887,14 +887,13 @@ impl<T: Trait> Module<T> {
|
||||
.cloned()
|
||||
.map(|(voter, stake, votes)| { (voter, to_votes(stake), votes)} )
|
||||
.collect::<Vec<_>>();
|
||||
let maybe_phragmen_result = sp_npos_elections::seq_phragmen::<T::AccountId, Perbill>(
|
||||
num_to_elect,
|
||||
0,
|
||||
candidates,
|
||||
voters_and_votes,
|
||||
);
|
||||
|
||||
if let Some(ElectionResult { winners, assignments }) = maybe_phragmen_result {
|
||||
let _ = sp_npos_elections::seq_phragmen::<T::AccountId, Perbill>(
|
||||
num_to_elect,
|
||||
candidates,
|
||||
voters_and_votes.clone(),
|
||||
None,
|
||||
).map(|ElectionResult { winners, assignments: _ }| {
|
||||
let old_members_ids = <Members<T>>::take().into_iter()
|
||||
.map(|(m, _)| m)
|
||||
.collect::<Vec<T::AccountId>>();
|
||||
@@ -902,41 +901,17 @@ impl<T: Trait> Module<T> {
|
||||
.map(|(r, _)| r)
|
||||
.collect::<Vec<T::AccountId>>();
|
||||
|
||||
// filter out those who had literally no votes at all.
|
||||
// NOTE: the need to do this is because all candidates, even those who have no
|
||||
// vote are still considered by phragmen and when good candidates are scarce, then these
|
||||
// cheap ones might get elected. We might actually want to remove the filter and allow
|
||||
// zero-voted candidates to also make it to the membership set.
|
||||
let new_set_with_approval = winners;
|
||||
let new_set = new_set_with_approval
|
||||
// filter out those who end up with no backing stake.
|
||||
let new_set_with_stake = winners
|
||||
.into_iter()
|
||||
.filter_map(|(m, a)| if a.is_zero() { None } else { Some(m) } )
|
||||
.collect::<Vec<T::AccountId>>();
|
||||
.filter_map(|(m, b)| if b.is_zero() { None } else { Some((m, to_balance(b))) })
|
||||
.collect::<Vec<(T::AccountId, BalanceOf<T>)>>();
|
||||
|
||||
// OPTIMISATION NOTE: we could bail out here if `new_set.len() == 0`. There isn't much
|
||||
// left to do. Yet, re-arranging the code would require duplicating the slashing of
|
||||
// exposed candidates, cleaning any previous members, and so on. For now, in favour of
|
||||
// readability and veracity, we keep it simple.
|
||||
|
||||
let staked_assignments = sp_npos_elections::assignment_ratio_to_staked(
|
||||
assignments,
|
||||
stake_of,
|
||||
);
|
||||
|
||||
let (support_map, _) = build_support_map::<T::AccountId>(&new_set, &staked_assignments);
|
||||
|
||||
let new_set_with_stake = new_set
|
||||
.into_iter()
|
||||
.map(|ref m| {
|
||||
let support = support_map.get(m)
|
||||
.expect(
|
||||
"entire new_set was given to build_support_map; en entry must be \
|
||||
created for each item; qed"
|
||||
);
|
||||
(m.clone(), to_balance(support.total))
|
||||
})
|
||||
.collect::<Vec<(T::AccountId, BalanceOf<T>)>>();
|
||||
|
||||
// split new set into winners and runners up.
|
||||
let split_point = desired_seats.min(new_set_with_stake.len());
|
||||
let mut new_members = (&new_set_with_stake[..split_point]).to_vec();
|
||||
@@ -1031,14 +1006,15 @@ impl<T: Trait> Module<T> {
|
||||
<RunnersUp<T>>::put(new_runners_up);
|
||||
|
||||
Self::deposit_event(RawEvent::NewTerm(new_members.clone().to_vec()));
|
||||
} else {
|
||||
Self::deposit_event(RawEvent::EmptyTerm);
|
||||
}
|
||||
|
||||
// clean candidates.
|
||||
<Candidates<T>>::kill();
|
||||
// clean candidates.
|
||||
<Candidates<T>>::kill();
|
||||
|
||||
ElectionRounds::mutate(|v| *v += 1);
|
||||
ElectionRounds::mutate(|v| *v += 1);
|
||||
}).map_err(|e| {
|
||||
frame_support::debug::error!("elections-phragmen: failed to run election [{:?}].", e);
|
||||
Self::deposit_event(RawEvent::ElectionError);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1366,6 +1342,10 @@ mod tests {
|
||||
assert_eq!(Elections::candidates(), candidates);
|
||||
}
|
||||
|
||||
fn locked_stake_of(who: &u64) -> u64 {
|
||||
Voting::<Test>::get(who).0
|
||||
}
|
||||
|
||||
fn ensure_members_has_approval_stake() {
|
||||
// we filter members that have no approval state. This means that even we have more seats
|
||||
// than candidates, we will never ever chose a member with no votes.
|
||||
@@ -1684,13 +1664,13 @@ mod tests {
|
||||
|
||||
assert_eq!(balances(&2), (18, 2));
|
||||
assert_eq!(has_lock(&2), 20);
|
||||
assert_eq!(Elections::locked_stake_of(&2), 20);
|
||||
assert_eq!(locked_stake_of(&2), 20);
|
||||
|
||||
// can update; different stake; different lock and reserve.
|
||||
assert_ok!(vote(Origin::signed(2), vec![5, 4], 15));
|
||||
assert_eq!(balances(&2), (18, 2));
|
||||
assert_eq!(has_lock(&2), 15);
|
||||
assert_eq!(Elections::locked_stake_of(&2), 15);
|
||||
assert_eq!(locked_stake_of(&2), 15);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1828,7 +1808,7 @@ mod tests {
|
||||
|
||||
assert_ok!(vote(Origin::signed(2), vec![4, 5], 30));
|
||||
// you can lie but won't get away with it.
|
||||
assert_eq!(Elections::locked_stake_of(&2), 20);
|
||||
assert_eq!(locked_stake_of(&2), 20);
|
||||
assert_eq!(has_lock(&2), 20);
|
||||
});
|
||||
}
|
||||
@@ -1842,8 +1822,8 @@ mod tests {
|
||||
assert_ok!(vote(Origin::signed(3), vec![5], 30));
|
||||
|
||||
assert_eq_uvec!(all_voters(), vec![2, 3]);
|
||||
assert_eq!(Elections::locked_stake_of(&2), 20);
|
||||
assert_eq!(Elections::locked_stake_of(&3), 30);
|
||||
assert_eq!(locked_stake_of(&2), 20);
|
||||
assert_eq!(locked_stake_of(&3), 30);
|
||||
assert_eq!(votes_of(&2), vec![5]);
|
||||
assert_eq!(votes_of(&3), vec![5]);
|
||||
|
||||
@@ -1851,7 +1831,7 @@ mod tests {
|
||||
|
||||
assert_eq_uvec!(all_voters(), vec![3]);
|
||||
assert!(votes_of(&2).is_empty());
|
||||
assert_eq!(Elections::locked_stake_of(&2), 0);
|
||||
assert_eq!(locked_stake_of(&2), 0);
|
||||
|
||||
assert_eq!(balances(&2), (20, 0));
|
||||
assert_eq!(Balances::locks(&2).len(), 0);
|
||||
@@ -2096,6 +2076,57 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_term() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
// no candidates, no nothing.
|
||||
System::set_block_number(5);
|
||||
Elections::end_block(System::block_number());
|
||||
|
||||
assert_eq!(
|
||||
System::events().iter().last().unwrap().event,
|
||||
Event::elections_phragmen(RawEvent::EmptyTerm),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_outgoing() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
assert_ok!(submit_candidacy(Origin::signed(5)));
|
||||
assert_ok!(submit_candidacy(Origin::signed(4)));
|
||||
|
||||
assert_ok!(vote(Origin::signed(5), vec![5], 50));
|
||||
assert_ok!(vote(Origin::signed(4), vec![4], 40));
|
||||
|
||||
System::set_block_number(5);
|
||||
Elections::end_block(System::block_number());
|
||||
|
||||
assert_eq!(
|
||||
System::events().iter().last().unwrap().event,
|
||||
Event::elections_phragmen(RawEvent::NewTerm(vec![(4, 40), (5, 50)])),
|
||||
);
|
||||
|
||||
assert_eq!(Elections::members(), vec![(4, 40), (5, 50)]);
|
||||
assert_eq!(Elections::runners_up(), vec![]);
|
||||
|
||||
assert_ok!(Elections::remove_voter(Origin::signed(5)));
|
||||
assert_ok!(Elections::remove_voter(Origin::signed(4)));
|
||||
|
||||
System::set_block_number(10);
|
||||
Elections::end_block(System::block_number());
|
||||
|
||||
assert_eq!(
|
||||
System::events().iter().last().unwrap().event,
|
||||
Event::elections_phragmen(RawEvent::NewTerm(vec![])),
|
||||
);
|
||||
|
||||
// outgoing have lost their bond.
|
||||
assert_eq!(balances(&4), (37, 0));
|
||||
assert_eq!(balances(&5), (47, 0));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defunct_voter_will_be_counted() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
@@ -2670,29 +2701,29 @@ mod tests {
|
||||
})
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn runner_up_replacement_works_when_out_of_order() {
|
||||
// ExtBuilder::default().desired_runners_up(2).build_and_execute(|| {
|
||||
// assert_ok!(submit_candidacy(Origin::signed(5)));
|
||||
// assert_ok!(submit_candidacy(Origin::signed(4)));
|
||||
// assert_ok!(submit_candidacy(Origin::signed(3)));
|
||||
// assert_ok!(submit_candidacy(Origin::signed(2)));
|
||||
#[test]
|
||||
fn runner_up_replacement_works_when_out_of_order() {
|
||||
ExtBuilder::default().desired_runners_up(2).build_and_execute(|| {
|
||||
assert_ok!(submit_candidacy(Origin::signed(5)));
|
||||
assert_ok!(submit_candidacy(Origin::signed(4)));
|
||||
assert_ok!(submit_candidacy(Origin::signed(3)));
|
||||
assert_ok!(submit_candidacy(Origin::signed(2)));
|
||||
|
||||
// assert_ok!(vote(Origin::signed(2), vec![5], 20));
|
||||
// assert_ok!(vote(Origin::signed(3), vec![3], 30));
|
||||
// assert_ok!(vote(Origin::signed(4), vec![4], 40));
|
||||
// assert_ok!(vote(Origin::signed(5), vec![2], 50));
|
||||
assert_ok!(vote(Origin::signed(2), vec![5], 20));
|
||||
assert_ok!(vote(Origin::signed(3), vec![3], 30));
|
||||
assert_ok!(vote(Origin::signed(4), vec![4], 40));
|
||||
assert_ok!(vote(Origin::signed(5), vec![2], 50));
|
||||
|
||||
// System::set_block_number(5);
|
||||
// Elections::end_block(System::block_number());
|
||||
System::set_block_number(5);
|
||||
Elections::end_block(System::block_number());
|
||||
|
||||
// assert_eq!(Elections::members_ids(), vec![2, 4]);
|
||||
// assert_eq!(ELections::runners_up_ids(), vec![3, 5]);
|
||||
// assert_ok!(Elections::renounce_candidacy(Origin::signed(3), Renouncing::RunnerUp));
|
||||
// assert_eq!(Elections::members_ids(), vec![2, 4]);
|
||||
// assert_eq!(ELections::runners_up_ids(), vec![5]);
|
||||
// });
|
||||
// }
|
||||
assert_eq!(Elections::members_ids(), vec![2, 4]);
|
||||
assert_eq!(Elections::runners_up_ids(), vec![5, 3]);
|
||||
assert_ok!(Elections::renounce_candidacy(Origin::signed(3), Renouncing::RunnerUp));
|
||||
assert_eq!(Elections::members_ids(), vec![2, 4]);
|
||||
assert_eq!(Elections::runners_up_ids(), vec![5]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_renounce_candidacy_candidate() {
|
||||
|
||||
@@ -111,7 +111,7 @@ fn main() {
|
||||
// stuff to submit
|
||||
let (winners, compact, score, size) = match mode {
|
||||
Mode::InitialSubmission => {
|
||||
/* No need to setup anything */
|
||||
// No need to setup anything
|
||||
get_seq_phragmen_solution::<Test>(do_reduce)
|
||||
},
|
||||
Mode::StrongerSubmission => {
|
||||
|
||||
@@ -304,7 +304,7 @@ use frame_support::{
|
||||
};
|
||||
use pallet_session::historical;
|
||||
use sp_runtime::{
|
||||
Percent, Perbill, PerU16, PerThing, RuntimeDebug, DispatchError,
|
||||
Percent, Perbill, PerU16, PerThing, InnerOf, RuntimeDebug, DispatchError,
|
||||
curve::PiecewiseLinear,
|
||||
traits::{
|
||||
Convert, Zero, StaticLookup, CheckedSub, Saturating, SaturatedConversion,
|
||||
@@ -707,18 +707,18 @@ pub struct ElectionSize {
|
||||
|
||||
|
||||
impl<BlockNumber: PartialEq> ElectionStatus<BlockNumber> {
|
||||
fn is_open_at(&self, n: BlockNumber) -> bool {
|
||||
pub fn is_open_at(&self, n: BlockNumber) -> bool {
|
||||
*self == Self::Open(n)
|
||||
}
|
||||
|
||||
fn is_closed(&self) -> bool {
|
||||
pub fn is_closed(&self) -> bool {
|
||||
match self {
|
||||
Self::Closed => true,
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
|
||||
fn is_open(&self) -> bool {
|
||||
pub fn is_open(&self) -> bool {
|
||||
!self.is_closed()
|
||||
}
|
||||
}
|
||||
@@ -1372,6 +1372,25 @@ decl_module! {
|
||||
T::BondingDuration::get(),
|
||||
)
|
||||
);
|
||||
|
||||
use sp_runtime::UpperOf;
|
||||
// see the documentation of `Assignment::try_normalize`. Now we can ensure that this
|
||||
// will always return `Ok`.
|
||||
// 1. Maximum sum of Vec<ChainAccuracy> must fit into `UpperOf<ChainAccuracy>`.
|
||||
assert!(
|
||||
<usize as TryInto<UpperOf<ChainAccuracy>>>::try_into(MAX_NOMINATIONS)
|
||||
.unwrap()
|
||||
.checked_mul(<ChainAccuracy>::one().deconstruct().try_into().unwrap())
|
||||
.is_some()
|
||||
);
|
||||
|
||||
// 2. Maximum sum of Vec<OffchainAccuracy> must fit into `UpperOf<OffchainAccuracy>`.
|
||||
assert!(
|
||||
<usize as TryInto<UpperOf<OffchainAccuracy>>>::try_into(MAX_NOMINATIONS)
|
||||
.unwrap()
|
||||
.checked_mul(<OffchainAccuracy>::one().deconstruct().try_into().unwrap())
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
/// Take the origin account as a stash and lock up `value` of its balance. `controller` will
|
||||
@@ -2165,7 +2184,7 @@ impl<T: Trait> Module<T> {
|
||||
}
|
||||
|
||||
/// internal impl of [`slashable_balance_of`] that returns [`VoteWeight`].
|
||||
fn slashable_balance_of_vote_weight(stash: &T::AccountId) -> VoteWeight {
|
||||
pub fn slashable_balance_of_vote_weight(stash: &T::AccountId) -> VoteWeight {
|
||||
<T::CurrencyToVote as Convert<BalanceOf<T>, VoteWeight>>::convert(
|
||||
Self::slashable_balance_of(stash)
|
||||
)
|
||||
@@ -2577,14 +2596,10 @@ impl<T: Trait> Module<T> {
|
||||
);
|
||||
|
||||
// build the support map thereof in order to evaluate.
|
||||
// OPTIMIZATION: loop to create the staked assignments but it would bloat the code. Okay for
|
||||
// now as it does not add to the complexity order.
|
||||
let (supports, num_error) = build_support_map::<T::AccountId>(
|
||||
let supports = build_support_map::<T::AccountId>(
|
||||
&winners,
|
||||
&staked_assignments,
|
||||
);
|
||||
// This technically checks that all targets in all nominators were among the winners.
|
||||
ensure!(num_error == 0, Error::<T>::OffchainElectionBogusEdge);
|
||||
).map_err(|_| Error::<T>::OffchainElectionBogusEdge)?;
|
||||
|
||||
// Check if the score is the same as the claimed one.
|
||||
let submitted_score = evaluate_support(&supports);
|
||||
@@ -2811,7 +2826,7 @@ impl<T: Trait> Module<T> {
|
||||
fn try_do_election() -> Option<ElectionResult<T::AccountId, BalanceOf<T>>> {
|
||||
// an election result from either a stored submission or locally executed one.
|
||||
let next_result = <QueuedElected<T>>::take().or_else(||
|
||||
Self::do_phragmen_with_post_processing::<ChainAccuracy>(ElectionCompute::OnChain)
|
||||
Self::do_on_chain_phragmen()
|
||||
);
|
||||
|
||||
// either way, kill this. We remove it here to make sure it always has the exact same
|
||||
@@ -2828,13 +2843,8 @@ impl<T: Trait> Module<T> {
|
||||
/// `PrimitiveElectionResult` into `ElectionResult`.
|
||||
///
|
||||
/// No storage item is updated.
|
||||
fn do_phragmen_with_post_processing<Accuracy: PerThing>(compute: ElectionCompute)
|
||||
-> Option<ElectionResult<T::AccountId, BalanceOf<T>>>
|
||||
where
|
||||
Accuracy: sp_std::ops::Mul<ExtendedBalance, Output=ExtendedBalance>,
|
||||
ExtendedBalance: From<<Accuracy as PerThing>::Inner>,
|
||||
{
|
||||
if let Some(phragmen_result) = Self::do_phragmen::<Accuracy>() {
|
||||
fn do_on_chain_phragmen() -> Option<ElectionResult<T::AccountId, BalanceOf<T>>> {
|
||||
if let Some(phragmen_result) = Self::do_phragmen::<ChainAccuracy>(0) {
|
||||
let elected_stashes = phragmen_result.winners.iter()
|
||||
.map(|(s, _)| s.clone())
|
||||
.collect::<Vec<T::AccountId>>();
|
||||
@@ -2845,10 +2855,17 @@ impl<T: Trait> Module<T> {
|
||||
Self::slashable_balance_of_vote_weight,
|
||||
);
|
||||
|
||||
let (supports, _) = build_support_map::<T::AccountId>(
|
||||
let supports = build_support_map::<T::AccountId>(
|
||||
&elected_stashes,
|
||||
&staked_assignments,
|
||||
);
|
||||
)
|
||||
.map_err(|_|
|
||||
log!(
|
||||
error,
|
||||
"💸 on-chain phragmen is failing due to a problem in the result. This must be a bug."
|
||||
)
|
||||
)
|
||||
.ok()?;
|
||||
|
||||
// collect exposures
|
||||
let exposures = Self::collect_exposure(supports);
|
||||
@@ -2860,7 +2877,7 @@ impl<T: Trait> Module<T> {
|
||||
Some(ElectionResult::<T::AccountId, BalanceOf<T>> {
|
||||
elected_stashes,
|
||||
exposures,
|
||||
compute,
|
||||
compute: ElectionCompute::OnChain,
|
||||
})
|
||||
} else {
|
||||
// There were not enough candidates for even our minimal level of functionality. This is
|
||||
@@ -2874,10 +2891,14 @@ impl<T: Trait> Module<T> {
|
||||
/// Execute phragmen election and return the new results. No post-processing is applied and the
|
||||
/// raw edge weights are returned.
|
||||
///
|
||||
/// Self votes are added and nominations before the most recent slashing span are reaped.
|
||||
/// Self votes are added and nominations before the most recent slashing span are ignored.
|
||||
///
|
||||
/// No storage item is updated.
|
||||
fn do_phragmen<Accuracy: PerThing>() -> Option<PrimitiveElectionResult<T::AccountId, Accuracy>> {
|
||||
pub fn do_phragmen<Accuracy: PerThing>(
|
||||
iterations: usize,
|
||||
) -> Option<PrimitiveElectionResult<T::AccountId, Accuracy>>
|
||||
where ExtendedBalance: From<InnerOf<Accuracy>>
|
||||
{
|
||||
let mut all_nominators: Vec<(T::AccountId, VoteWeight, Vec<T::AccountId>)> = Vec::new();
|
||||
let mut all_validators = Vec::new();
|
||||
for (validator, _) in <Validators<T>>::iter() {
|
||||
@@ -2906,16 +2927,26 @@ impl<T: Trait> Module<T> {
|
||||
(n, s, ns)
|
||||
}));
|
||||
|
||||
seq_phragmen::<_, Accuracy>(
|
||||
Self::validator_count() as usize,
|
||||
Self::minimum_validator_count().max(1) as usize,
|
||||
all_validators,
|
||||
all_nominators,
|
||||
)
|
||||
if all_validators.len() < Self::minimum_validator_count().max(1) as usize {
|
||||
// If we don't have enough candidates, nothing to do.
|
||||
log!(error, "💸 Chain does not have enough staking candidates to operate. Era {:?}.", Self::current_era());
|
||||
None
|
||||
} else {
|
||||
seq_phragmen::<_, Accuracy>(
|
||||
Self::validator_count() as usize,
|
||||
all_validators,
|
||||
all_nominators,
|
||||
Some((iterations, 0)), // exactly run `iterations` rounds.
|
||||
)
|
||||
.map_err(|err| log!(error, "Call to seq-phragmen failed due to {}", err))
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume a set of [`Supports`] from [`sp_npos_elections`] and collect them into a [`Exposure`]
|
||||
fn collect_exposure(supports: SupportMap<T::AccountId>) -> Vec<(T::AccountId, Exposure<T::AccountId, BalanceOf<T>>)> {
|
||||
fn collect_exposure(
|
||||
supports: SupportMap<T::AccountId>,
|
||||
) -> Vec<(T::AccountId, Exposure<T::AccountId, BalanceOf<T>>)> {
|
||||
let to_balance = |e: ExtendedBalance|
|
||||
<T::CurrencyToVote as Convert<ExtendedBalance, BalanceOf<T>>>::convert(e);
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ use frame_support::{
|
||||
use sp_io;
|
||||
use sp_npos_elections::{
|
||||
build_support_map, evaluate_support, reduce, ExtendedBalance, StakedAssignment, ElectionScore,
|
||||
VoteWeight,
|
||||
};
|
||||
use crate::*;
|
||||
|
||||
@@ -198,6 +197,7 @@ parameter_types! {
|
||||
pub const MaximumBlockWeight: Weight = 1024;
|
||||
pub const MaximumBlockLength: u32 = 2 * 1024;
|
||||
pub const AvailableBlockRatio: Perbill = Perbill::one();
|
||||
pub const MaxLocks: u32 = 1024;
|
||||
}
|
||||
impl frame_system::Trait for Test {
|
||||
type BaseCallFilter = ();
|
||||
@@ -227,7 +227,7 @@ impl frame_system::Trait for Test {
|
||||
type SystemWeightInfo = ();
|
||||
}
|
||||
impl pallet_balances::Trait for Test {
|
||||
type MaxLocks = ();
|
||||
type MaxLocks = MaxLocks;
|
||||
type Balance = Balance;
|
||||
type Event = MetaEvent;
|
||||
type DustRemoval = ();
|
||||
@@ -857,9 +857,9 @@ pub(crate) fn horrible_npos_solution(
|
||||
// Ensure that this result is worse than seq-phragmen. Otherwise, it should not have been used
|
||||
// for testing.
|
||||
let score = {
|
||||
let (_, _, better_score) = prepare_submission_with(true, 0, |_| {});
|
||||
let (_, _, better_score) = prepare_submission_with(true, true, 0, |_| {});
|
||||
|
||||
let support = build_support_map::<AccountId>(&winners, &staked_assignment).0;
|
||||
let support = build_support_map::<AccountId>(&winners, &staked_assignment).unwrap();
|
||||
let score = evaluate_support(&support);
|
||||
|
||||
assert!(sp_npos_elections::is_score_better::<Perbill>(
|
||||
@@ -898,9 +898,13 @@ pub(crate) fn horrible_npos_solution(
|
||||
(compact, winners, score)
|
||||
}
|
||||
|
||||
// Note: this should always logically reproduce [`offchain_election::prepare_submission`], yet we
|
||||
// cannot do it since we want to have `tweak` injected into the process.
|
||||
/// Note: this should always logically reproduce [`offchain_election::prepare_submission`], yet we
|
||||
/// cannot do it since we want to have `tweak` injected into the process.
|
||||
///
|
||||
/// If the input is being tweaked in a way that the score cannot be compute accurately,
|
||||
/// `compute_real_score` can be set to true. In this case a `Default` score is returned.
|
||||
pub(crate) fn prepare_submission_with(
|
||||
compute_real_score: bool,
|
||||
do_reduce: bool,
|
||||
iterations: usize,
|
||||
tweak: impl FnOnce(&mut Vec<StakedAssignment<AccountId>>),
|
||||
@@ -909,26 +913,13 @@ pub(crate) fn prepare_submission_with(
|
||||
let sp_npos_elections::ElectionResult {
|
||||
winners,
|
||||
assignments,
|
||||
} = Staking::do_phragmen::<OffchainAccuracy>().unwrap();
|
||||
} = Staking::do_phragmen::<OffchainAccuracy>(iterations).unwrap();
|
||||
let winners = sp_npos_elections::to_without_backing(winners);
|
||||
|
||||
let stake_of = |who: &AccountId| -> VoteWeight {
|
||||
<CurrencyToVoteHandler as Convert<Balance, VoteWeight>>::convert(
|
||||
Staking::slashable_balance_of(&who)
|
||||
)
|
||||
};
|
||||
|
||||
let mut staked = sp_npos_elections::assignment_ratio_to_staked(assignments, stake_of);
|
||||
let (mut support_map, _) = build_support_map::<AccountId>(&winners, &staked);
|
||||
|
||||
if iterations > 0 {
|
||||
sp_npos_elections::balance_solution(
|
||||
&mut staked,
|
||||
&mut support_map,
|
||||
Zero::zero(),
|
||||
iterations,
|
||||
);
|
||||
}
|
||||
let mut staked = sp_npos_elections::assignment_ratio_to_staked(
|
||||
assignments,
|
||||
Staking::slashable_balance_of_vote_weight,
|
||||
);
|
||||
|
||||
// apply custom tweaks. awesome for testing.
|
||||
tweak(&mut staked);
|
||||
@@ -962,17 +953,19 @@ pub(crate) fn prepare_submission_with(
|
||||
let assignments_reduced = sp_npos_elections::assignment_staked_to_ratio(staked);
|
||||
|
||||
// re-compute score by converting, yet again, into staked type
|
||||
let score = {
|
||||
let score = if compute_real_score {
|
||||
let staked = sp_npos_elections::assignment_ratio_to_staked(
|
||||
assignments_reduced.clone(),
|
||||
Staking::slashable_balance_of_vote_weight,
|
||||
);
|
||||
|
||||
let (support_map, _) = build_support_map::<AccountId>(
|
||||
let support_map = build_support_map::<AccountId>(
|
||||
winners.as_slice(),
|
||||
staked.as_slice(),
|
||||
);
|
||||
).unwrap();
|
||||
evaluate_support::<AccountId>(&support_map)
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let compact =
|
||||
|
||||
@@ -25,10 +25,10 @@ use crate::{
|
||||
use frame_system::offchain::SubmitTransaction;
|
||||
use sp_npos_elections::{
|
||||
build_support_map, evaluate_support, reduce, Assignment, ExtendedBalance, ElectionResult,
|
||||
ElectionScore, balance_solution,
|
||||
ElectionScore,
|
||||
};
|
||||
use sp_runtime::offchain::storage::StorageValueRef;
|
||||
use sp_runtime::{PerThing, RuntimeDebug, traits::{TrailingZeroInput, Zero}};
|
||||
use sp_runtime::{PerThing, RuntimeDebug, traits::TrailingZeroInput};
|
||||
use frame_support::traits::Get;
|
||||
use sp_std::{convert::TryInto, prelude::*};
|
||||
|
||||
@@ -106,16 +106,24 @@ pub(crate) fn set_check_offchain_execution_status<T: Trait>(
|
||||
/// compacts and reduces the solution, computes the score and submits it back to the chain as an
|
||||
/// unsigned transaction, without any signature.
|
||||
pub(crate) fn compute_offchain_election<T: Trait>() -> Result<(), OffchainElectionError> {
|
||||
let iters = get_balancing_iters::<T>();
|
||||
// compute raw solution. Note that we use `OffchainAccuracy`.
|
||||
let ElectionResult {
|
||||
winners,
|
||||
assignments,
|
||||
} = <Module<T>>::do_phragmen::<OffchainAccuracy>()
|
||||
} = <Module<T>>::do_phragmen::<OffchainAccuracy>(iters)
|
||||
.ok_or(OffchainElectionError::ElectionFailed)?;
|
||||
|
||||
// process and prepare it for submission.
|
||||
let (winners, compact, score, size) = prepare_submission::<T>(assignments, winners, true)?;
|
||||
|
||||
crate::log!(
|
||||
info,
|
||||
"prepared a seq-phragmen solution with {} balancing iterations and score {:?}",
|
||||
iters,
|
||||
score,
|
||||
);
|
||||
|
||||
// defensive-only: current era can never be none except genesis.
|
||||
let current_era = <Module<T>>::current_era().unwrap_or_default();
|
||||
|
||||
@@ -132,6 +140,20 @@ pub(crate) fn compute_offchain_election<T: Trait>() -> Result<(), OffchainElecti
|
||||
.map_err(|_| OffchainElectionError::PoolSubmissionFailed)
|
||||
}
|
||||
|
||||
/// Get a random number of iterations to run the balancing.
|
||||
///
|
||||
/// Uses the offchain seed to generate a random number.
|
||||
pub fn get_balancing_iters<T: Trait>() -> usize {
|
||||
match T::MaxIterations::get() {
|
||||
0 => 0,
|
||||
max @ _ => {
|
||||
let seed = sp_io::offchain::random_seed();
|
||||
let random = <u32>::decode(&mut TrailingZeroInput::new(seed.as_ref()))
|
||||
.expect("input is padded with zeroes; qed") % max.saturating_add(1);
|
||||
random as usize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes an election result and spits out some data that can be submitted to the chain.
|
||||
///
|
||||
@@ -177,26 +199,6 @@ pub fn prepare_submission<T: Trait>(
|
||||
<Module<T>>::slashable_balance_of_vote_weight,
|
||||
);
|
||||
|
||||
let (mut support_map, _) = build_support_map::<T::AccountId>(&winners, &staked);
|
||||
// balance a random number of times.
|
||||
let iterations_executed = match T::MaxIterations::get() {
|
||||
0 => {
|
||||
// Don't run balance_solution at all
|
||||
0
|
||||
}
|
||||
iterations @ _ => {
|
||||
let seed = sp_io::offchain::random_seed();
|
||||
let iterations = <u32>::decode(&mut TrailingZeroInput::new(seed.as_ref()))
|
||||
.expect("input is padded with zeroes; qed") % iterations.saturating_add(1);
|
||||
balance_solution(
|
||||
&mut staked,
|
||||
&mut support_map,
|
||||
Zero::zero(),
|
||||
iterations as usize,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// reduce
|
||||
if do_reduce {
|
||||
reduce(&mut staked);
|
||||
@@ -220,7 +222,8 @@ pub fn prepare_submission<T: Trait>(
|
||||
<Module<T>>::slashable_balance_of_vote_weight,
|
||||
);
|
||||
|
||||
let (support_map, _) = build_support_map::<T::AccountId>(&winners, &staked);
|
||||
let support_map = build_support_map::<T::AccountId>(&winners, &staked)
|
||||
.map_err(|_| OffchainElectionError::ElectionFailed)?;
|
||||
evaluate_support::<T::AccountId>(&support_map)
|
||||
};
|
||||
|
||||
@@ -250,12 +253,5 @@ pub fn prepare_submission<T: Trait>(
|
||||
nominators: snapshot_nominators.len() as NominatorIndex,
|
||||
};
|
||||
|
||||
crate::log!(
|
||||
info,
|
||||
"prepared solution after {} equalization iterations with score {:?}",
|
||||
iterations_executed,
|
||||
score,
|
||||
);
|
||||
|
||||
Ok((winners_indexed, compact, score, size))
|
||||
}
|
||||
|
||||
@@ -237,8 +237,10 @@ pub fn get_weak_solution<T: Trait>(
|
||||
stake_of
|
||||
);
|
||||
|
||||
let (support_map, _) =
|
||||
build_support_map::<T::AccountId>(winners.as_slice(), staked.as_slice());
|
||||
let support_map = build_support_map::<T::AccountId>(
|
||||
winners.as_slice(),
|
||||
staked.as_slice(),
|
||||
).unwrap();
|
||||
evaluate_support::<T::AccountId>(&support_map)
|
||||
};
|
||||
|
||||
@@ -276,10 +278,12 @@ pub fn get_weak_solution<T: Trait>(
|
||||
pub fn get_seq_phragmen_solution<T: Trait>(
|
||||
do_reduce: bool,
|
||||
) -> (Vec<ValidatorIndex>, CompactAssignments, ElectionScore, ElectionSize) {
|
||||
let iters = offchain_election::get_balancing_iters::<T>();
|
||||
|
||||
let sp_npos_elections::ElectionResult {
|
||||
winners,
|
||||
assignments,
|
||||
} = <Module<T>>::do_phragmen::<OffchainAccuracy>().unwrap();
|
||||
} = <Module<T>>::do_phragmen::<OffchainAccuracy>(iters).unwrap();
|
||||
|
||||
offchain_election::prepare_submission::<T>(assignments, winners, do_reduce).unwrap()
|
||||
}
|
||||
|
||||
@@ -1749,11 +1749,10 @@ fn bond_with_duplicate_vote_should_be_ignored_by_npos_election() {
|
||||
assert_ok!(Staking::nominate(Origin::signed(4), vec![21, 31]));
|
||||
|
||||
// winners should be 21 and 31. Otherwise this election is taking duplicates into account.
|
||||
|
||||
let sp_npos_elections::ElectionResult {
|
||||
winners,
|
||||
assignments,
|
||||
} = Staking::do_phragmen::<Perbill>().unwrap();
|
||||
} = Staking::do_phragmen::<Perbill>(0).unwrap();
|
||||
let winners = sp_npos_elections::to_without_backing(winners);
|
||||
|
||||
assert_eq!(winners, vec![31, 21]);
|
||||
@@ -1801,7 +1800,7 @@ fn bond_with_duplicate_vote_should_be_ignored_by_npos_election_elected() {
|
||||
let sp_npos_elections::ElectionResult {
|
||||
winners,
|
||||
assignments,
|
||||
} = Staking::do_phragmen::<Perbill>().unwrap();
|
||||
} = Staking::do_phragmen::<Perbill>(0).unwrap();
|
||||
|
||||
let winners = sp_npos_elections::to_without_backing(winners);
|
||||
assert_eq!(winners, vec![21, 11]);
|
||||
@@ -3157,7 +3156,7 @@ mod offchain_election {
|
||||
assert_eq!(Staking::era_election_status(), ElectionStatus::Open(12));
|
||||
assert!(Staking::snapshot_validators().is_some());
|
||||
|
||||
let (compact, winners, score) = prepare_submission_with(true, 2, |_| {});
|
||||
let (compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});
|
||||
assert_ok!(submit_solution(
|
||||
Origin::signed(10),
|
||||
winners,
|
||||
@@ -3214,7 +3213,7 @@ mod offchain_election {
|
||||
run_to_block(14);
|
||||
assert_eq!(Staking::era_election_status(), ElectionStatus::Open(12));
|
||||
|
||||
let (compact, winners, score) = prepare_submission_with(true, 2, |_| {});
|
||||
let (compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});
|
||||
assert_ok!(submit_solution(Origin::signed(10), winners, compact, score));
|
||||
|
||||
let queued_result = Staking::queued_elected().unwrap();
|
||||
@@ -3255,7 +3254,7 @@ mod offchain_election {
|
||||
|
||||
// create all the indices just to build the solution.
|
||||
Staking::create_stakers_snapshot();
|
||||
let (compact, winners, score) = prepare_submission_with(true, 2, |_| {});
|
||||
let (compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});
|
||||
Staking::kill_stakers_snapshot();
|
||||
|
||||
assert_err_with_weight!(
|
||||
@@ -3286,7 +3285,7 @@ mod offchain_election {
|
||||
run_to_block(12);
|
||||
|
||||
// a good solution
|
||||
let (compact, winners, score) = prepare_submission_with(true, 2, |_| {});
|
||||
let (compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});
|
||||
assert_ok!(submit_solution(
|
||||
Origin::signed(10),
|
||||
winners,
|
||||
@@ -3331,7 +3330,7 @@ mod offchain_election {
|
||||
));
|
||||
|
||||
// a better solution
|
||||
let (compact, winners, score) = prepare_submission_with(true, 2, |_| {});
|
||||
let (compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});
|
||||
assert_ok!(submit_solution(
|
||||
Origin::signed(10),
|
||||
winners,
|
||||
@@ -3436,7 +3435,7 @@ mod offchain_election {
|
||||
ext.execute_with(|| {
|
||||
run_to_block(12);
|
||||
// put a good solution on-chain
|
||||
let (compact, winners, score) = prepare_submission_with(true, 2, |_| {});
|
||||
let (compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});
|
||||
assert_ok!(submit_solution(
|
||||
Origin::signed(10),
|
||||
winners,
|
||||
@@ -3481,7 +3480,7 @@ mod offchain_election {
|
||||
run_to_block(12);
|
||||
|
||||
ValidatorCount::put(3);
|
||||
let (compact, winners, score) = prepare_submission_with(true, 2, |_| {});
|
||||
let (compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});
|
||||
ValidatorCount::put(4);
|
||||
|
||||
assert_eq!(winners.len(), 3);
|
||||
@@ -3506,7 +3505,7 @@ mod offchain_election {
|
||||
.execute_with(|| {
|
||||
run_to_block(12);
|
||||
|
||||
let (compact, winners, score) = prepare_submission_with(true, 2, |_| {});
|
||||
let (compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});
|
||||
|
||||
assert_noop!(
|
||||
Staking::submit_election_solution(
|
||||
@@ -3535,7 +3534,7 @@ mod offchain_election {
|
||||
run_to_block(12);
|
||||
|
||||
ValidatorCount::put(3);
|
||||
let (compact, winners, score) = prepare_submission_with(true, 2, |_| {});
|
||||
let (compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});
|
||||
ValidatorCount::put(4);
|
||||
|
||||
assert_eq!(winners.len(), 3);
|
||||
@@ -3564,7 +3563,7 @@ mod offchain_election {
|
||||
build_offchain_election_test_ext();
|
||||
run_to_block(12);
|
||||
|
||||
let (compact, winners, score) = prepare_submission_with(true, 2, |_| {});
|
||||
let (compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});
|
||||
|
||||
assert_eq!(winners.len(), 4);
|
||||
|
||||
@@ -3592,7 +3591,7 @@ mod offchain_election {
|
||||
|
||||
assert_eq!(Staking::snapshot_nominators().unwrap().len(), 5 + 4);
|
||||
assert_eq!(Staking::snapshot_validators().unwrap().len(), 4);
|
||||
let (mut compact, winners, score) = prepare_submission_with(true, 2, |_| {});
|
||||
let (mut compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});
|
||||
|
||||
// index 9 doesn't exist.
|
||||
compact.votes1.push((9, 2));
|
||||
@@ -3624,7 +3623,7 @@ mod offchain_election {
|
||||
|
||||
assert_eq!(Staking::snapshot_nominators().unwrap().len(), 5 + 4);
|
||||
assert_eq!(Staking::snapshot_validators().unwrap().len(), 4);
|
||||
let (mut compact, winners, score) = prepare_submission_with(true, 2, |_| {});
|
||||
let (mut compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});
|
||||
|
||||
// index 4 doesn't exist.
|
||||
compact.votes1.iter_mut().for_each(|(_, vidx)| if *vidx == 1 { *vidx = 4 });
|
||||
@@ -3656,7 +3655,7 @@ mod offchain_election {
|
||||
|
||||
assert_eq!(Staking::snapshot_nominators().unwrap().len(), 5 + 4);
|
||||
assert_eq!(Staking::snapshot_validators().unwrap().len(), 4);
|
||||
let (compact, _, score) = prepare_submission_with(true, 2, |_| {});
|
||||
let (compact, _, score) = prepare_submission_with(true, true, 2, |_| {});
|
||||
|
||||
// index 4 doesn't exist.
|
||||
let winners = vec![0, 1, 2, 4];
|
||||
@@ -3688,7 +3687,7 @@ mod offchain_election {
|
||||
|
||||
assert_eq!(Staking::snapshot_nominators().unwrap().len(), 5 + 4);
|
||||
assert_eq!(Staking::snapshot_validators().unwrap().len(), 4);
|
||||
let (compact, winners, score) = prepare_submission_with(true, 2, |a| {
|
||||
let (compact, winners, score) = prepare_submission_with(false, true, 2, |a| {
|
||||
// swap all 11 and 41s in the distribution with non-winners. Note that it is
|
||||
// important that the count of winners and the count of unique targets remain
|
||||
// valid.
|
||||
@@ -3729,7 +3728,7 @@ mod offchain_election {
|
||||
assert_eq!(Staking::snapshot_nominators().unwrap().len(), 5 + 4);
|
||||
assert_eq!(Staking::snapshot_validators().unwrap().len(), 4);
|
||||
|
||||
let (compact, winners, score) = prepare_submission_with(true, 2, |a| {
|
||||
let (compact, winners, score) = prepare_submission_with(false, true, 2, |a| {
|
||||
a.iter_mut()
|
||||
.find(|x| x.who == 5)
|
||||
// just add any new target.
|
||||
@@ -3765,7 +3764,7 @@ mod offchain_election {
|
||||
build_offchain_election_test_ext();
|
||||
run_to_block(12);
|
||||
|
||||
let (compact, winners, score) = prepare_submission_with(true, 2, |a| {
|
||||
let (compact, winners, score) = prepare_submission_with(true, true, 2, |a| {
|
||||
// mutate a self vote to target someone else. That someone else is still among the
|
||||
// winners
|
||||
a.iter_mut().find(|x| x.who == 11).map(|x| {
|
||||
@@ -3800,7 +3799,7 @@ mod offchain_election {
|
||||
build_offchain_election_test_ext();
|
||||
run_to_block(12);
|
||||
|
||||
let (compact, winners, score) = prepare_submission_with(true, 2, |a| {
|
||||
let (compact, winners, score) = prepare_submission_with(true, true, 2, |a| {
|
||||
// Remove the self vote.
|
||||
a.retain(|x| x.who != 11);
|
||||
// add is as a new double vote
|
||||
@@ -3837,7 +3836,7 @@ mod offchain_election {
|
||||
|
||||
// Note: we don't reduce here to be able to tweak votes3. votes3 will vanish if you
|
||||
// reduce.
|
||||
let (mut compact, winners, score) = prepare_submission_with(false, 0, |_| {});
|
||||
let (mut compact, winners, score) = prepare_submission_with(true, false, 0, |_| {});
|
||||
|
||||
if let Some(c) = compact.votes3.iter_mut().find(|x| x.0 == 0) {
|
||||
// by default it should have been (0, [(2, 33%), (1, 33%)], 0)
|
||||
@@ -3878,7 +3877,7 @@ mod offchain_election {
|
||||
build_offchain_election_test_ext();
|
||||
run_to_block(12);
|
||||
|
||||
let (compact, winners, score) = prepare_submission_with(false, 0, |a| {
|
||||
let (compact, winners, score) = prepare_submission_with(true, false, 0, |a| {
|
||||
// 3 only voted for 20 and 40. We add a fake vote to 30. The stake sum is still
|
||||
// correctly 100.
|
||||
a.iter_mut()
|
||||
@@ -3939,7 +3938,7 @@ mod offchain_election {
|
||||
run_to_block(32);
|
||||
|
||||
// a solution that has been prepared after the slash.
|
||||
let (compact, winners, score) = prepare_submission_with(false, 0, |a| {
|
||||
let (compact, winners, score) = prepare_submission_with(true, false, 0, |a| {
|
||||
// no one is allowed to vote for 10, except for itself.
|
||||
a.into_iter()
|
||||
.filter(|s| s.who != 11)
|
||||
@@ -3957,7 +3956,7 @@ mod offchain_election {
|
||||
));
|
||||
|
||||
// a wrong solution.
|
||||
let (compact, winners, score) = prepare_submission_with(false, 0, |a| {
|
||||
let (compact, winners, score) = prepare_submission_with(true, false, 0, |a| {
|
||||
// add back the vote that has been filtered out.
|
||||
a.push(StakedAssignment {
|
||||
who: 1,
|
||||
@@ -3990,7 +3989,7 @@ mod offchain_election {
|
||||
build_offchain_election_test_ext();
|
||||
run_to_block(12);
|
||||
|
||||
let (compact, winners, mut score) = prepare_submission_with(true, 2, |_| {});
|
||||
let (compact, winners, mut score) = prepare_submission_with(true, true, 2, |_| {});
|
||||
score[0] += 1;
|
||||
|
||||
assert_noop!(
|
||||
|
||||
@@ -33,8 +33,8 @@ name = "per_thing_rational"
|
||||
path = "src/per_thing_rational.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "rational128"
|
||||
path = "src/rational128.rs"
|
||||
name = "multiply_by_rational"
|
||||
path = "src/multiply_by_rational.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "fixed_point"
|
||||
|
||||
@@ -149,7 +149,7 @@ fn main() {
|
||||
let w = u.div_unit(v.get(0));
|
||||
let num_w = num_u / &num_v;
|
||||
assert_biguints_eq(&w, &num_w);
|
||||
} else if u.len() > v.len() && v.len() > 0 {
|
||||
} else if u.len() > v.len() && v.len() > 1 {
|
||||
let num_remainder = num_u.clone() % num_v.clone();
|
||||
|
||||
let (w, remainder) = u.div(&v, return_remainder).unwrap();
|
||||
|
||||
+2
-2
@@ -16,12 +16,12 @@
|
||||
// limitations under the License.
|
||||
|
||||
//! # Running
|
||||
//! Running this fuzzer can be done with `cargo hfuzz run rational128`. `honggfuzz` CLI options can
|
||||
//! Running this fuzzer can be done with `cargo hfuzz run multiply_by_rational`. `honggfuzz` CLI options can
|
||||
//! be used by setting `HFUZZ_RUN_ARGS`, such as `-n 4` to use 4 threads.
|
||||
//!
|
||||
//! # Debugging a panic
|
||||
//! Once a panic is found, it can be debugged with
|
||||
//! `cargo hfuzz run-debug rational128 hfuzz_workspace/rational128/*.fuzz`.
|
||||
//! `cargo hfuzz run-debug multiply_by_rational hfuzz_workspace/multiply_by_rational/*.fuzz`.
|
||||
//!
|
||||
//! # More information
|
||||
//! More information about `honggfuzz` can be found
|
||||
@@ -28,12 +28,14 @@ use honggfuzz::fuzz;
|
||||
use sp_arithmetic::Normalizable;
|
||||
use std::convert::TryInto;
|
||||
|
||||
type Ty = u64;
|
||||
|
||||
fn main() {
|
||||
let sum_limit = u32::max_value() as u128;
|
||||
let len_limit: usize = u32::max_value().try_into().unwrap();
|
||||
let sum_limit = Ty::max_value() as u128;
|
||||
let len_limit: usize = Ty::max_value().try_into().unwrap();
|
||||
|
||||
loop {
|
||||
fuzz!(|data: (Vec<u32>, u32)| {
|
||||
fuzz!(|data: (Vec<Ty>, Ty)| {
|
||||
let (data, norm) = data;
|
||||
if data.len() == 0 { return; }
|
||||
let pre_sum: u128 = data.iter().map(|x| *x as u128).sum();
|
||||
@@ -55,6 +57,8 @@ fn main() {
|
||||
normalized,
|
||||
norm,
|
||||
);
|
||||
} else {
|
||||
panic!("Should have returned Ok for input = {:?}, target = {:?}", data, norm);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -17,12 +17,13 @@
|
||||
|
||||
//! Infinite precision unsigned integer for substrate runtime.
|
||||
|
||||
use num_traits::Zero;
|
||||
use num_traits::{Zero, One};
|
||||
use sp_std::{cmp::Ordering, ops, prelude::*, vec, cell::RefCell, convert::TryFrom};
|
||||
|
||||
// A sensible value for this would be half of the dword size of the host machine. Since the
|
||||
// runtime is compiled to 32bit webassembly, using 32 and 64 for single and double respectively
|
||||
// should yield the most performance.
|
||||
|
||||
/// Representation of a single limb.
|
||||
pub type Single = u32;
|
||||
/// Representation of two limbs.
|
||||
@@ -75,7 +76,7 @@ fn div_single(a: Double, b: Single) -> (Double, Single) {
|
||||
/// Simple wrapper around an infinitely large integer, represented as limbs of [`Single`].
|
||||
#[derive(Clone, Default)]
|
||||
pub struct BigUint {
|
||||
/// digits (limbs) of this number (sorted as msb -> lsd).
|
||||
/// digits (limbs) of this number (sorted as msb -> lsb).
|
||||
pub(crate) digits: Vec<Single>,
|
||||
}
|
||||
|
||||
@@ -515,6 +516,12 @@ impl Zero for BigUint {
|
||||
}
|
||||
}
|
||||
|
||||
impl One for BigUint {
|
||||
fn one() -> Self {
|
||||
Self { digits: vec![Single::one()] }
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_try_from_number_for {
|
||||
($([$type:ty, $len:expr]),+) => {
|
||||
$(
|
||||
@@ -550,15 +557,21 @@ macro_rules! impl_from_for_smaller_than_word {
|
||||
})*
|
||||
}
|
||||
}
|
||||
impl_from_for_smaller_than_word!(u8, u16, Single);
|
||||
impl_from_for_smaller_than_word!(u8, u16, u32);
|
||||
|
||||
impl From<Double> for BigUint {
|
||||
impl From<u64> for BigUint {
|
||||
fn from(a: Double) -> Self {
|
||||
let (ah, al) = split(a);
|
||||
Self { digits: vec![ah, al] }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u128> for BigUint {
|
||||
fn from(a: u128) -> Self {
|
||||
crate::helpers_128bit::to_big_uint(a)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -36,13 +36,13 @@ macro_rules! assert_eq_error_rate {
|
||||
pub mod biguint;
|
||||
pub mod helpers_128bit;
|
||||
pub mod traits;
|
||||
mod per_things;
|
||||
mod fixed_point;
|
||||
mod rational128;
|
||||
pub mod per_things;
|
||||
pub mod fixed_point;
|
||||
pub mod rational;
|
||||
|
||||
pub use fixed_point::{FixedPointNumber, FixedPointOperand, FixedI64, FixedI128, FixedU128};
|
||||
pub use per_things::{PerThing, InnerOf, UpperOf, Percent, PerU16, Permill, Perbill, Perquintill};
|
||||
pub use rational128::Rational128;
|
||||
pub use rational::{Rational128, RationalInfinite};
|
||||
|
||||
use sp_std::{prelude::*, cmp::Ordering, fmt::Debug, convert::TryInto};
|
||||
use traits::{BaseArithmetic, One, Zero, SaturatedConversion, Unsigned};
|
||||
@@ -114,13 +114,22 @@ impl_normalize_for_numeric!(u8, u16, u32, u64, u128);
|
||||
|
||||
impl<P: PerThing> Normalizable<P> for Vec<P> {
|
||||
fn normalize(&self, targeted_sum: P) -> Result<Vec<P>, &'static str> {
|
||||
let inners = self.iter().map(|p| p.clone().deconstruct().into()).collect::<Vec<_>>();
|
||||
let inners = self
|
||||
.iter()
|
||||
.map(|p| p.clone().deconstruct().into())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let normalized = normalize(inners.as_ref(), targeted_sum.deconstruct().into())?;
|
||||
Ok(normalized.into_iter().map(|i: UpperOf<P>| P::from_parts(i.saturated_into())).collect())
|
||||
|
||||
Ok(
|
||||
normalized
|
||||
.into_iter()
|
||||
.map(|i: UpperOf<P>| P::from_parts(i.saturated_into()))
|
||||
.collect()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Normalize `input` so that the sum of all elements reaches `targeted_sum`.
|
||||
///
|
||||
/// This implementation is currently in a balanced position between being performant and accurate.
|
||||
@@ -143,8 +152,8 @@ impl<P: PerThing> Normalizable<P> for Vec<P> {
|
||||
/// `leftover` value. This ensures that the result will always stay accurate, yet it might cause the
|
||||
/// execution to become increasingly slow, since leftovers are applied one by one.
|
||||
///
|
||||
/// All in all, the complicated case above is rare to happen in all substrate use cases, hence we
|
||||
/// opt for it due to its simplicity.
|
||||
/// All in all, the complicated case above is rare to happen in most use cases within this repo ,
|
||||
/// hence we opt for it due to its simplicity.
|
||||
///
|
||||
/// This function will return an error is if length of `input` cannot fit in `T`, or if `sum(input)`
|
||||
/// cannot fit inside `T`.
|
||||
|
||||
+107
-4
@@ -17,19 +17,106 @@
|
||||
|
||||
use sp_std::{cmp::Ordering, prelude::*};
|
||||
use crate::helpers_128bit;
|
||||
use num_traits::Zero;
|
||||
use sp_debug_derive::RuntimeDebug;
|
||||
use num_traits::{Zero, One, Bounded};
|
||||
use crate::biguint::BigUint;
|
||||
|
||||
/// A wrapper for any rational number with infinitely large numerator and denominator.
|
||||
///
|
||||
/// This type exists to facilitate `cmp` operation
|
||||
/// on values like `a/b < c/d` where `a, b, c, d` are all `BigUint`.
|
||||
#[derive(Clone, Default, Eq)]
|
||||
pub struct RationalInfinite(BigUint, BigUint);
|
||||
|
||||
impl RationalInfinite {
|
||||
/// Return the numerator reference.
|
||||
pub fn n(&self) -> &BigUint {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Return the denominator reference.
|
||||
pub fn d(&self) -> &BigUint {
|
||||
&self.1
|
||||
}
|
||||
|
||||
/// Build from a raw `n/d`.
|
||||
pub fn from(n: BigUint, d: BigUint) -> Self {
|
||||
Self(n, d.max(BigUint::one()))
|
||||
}
|
||||
|
||||
/// Zero.
|
||||
pub fn zero() -> Self {
|
||||
Self(BigUint::zero(), BigUint::one())
|
||||
}
|
||||
|
||||
/// One.
|
||||
pub fn one() -> Self {
|
||||
Self(BigUint::one(), BigUint::one())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for RationalInfinite {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for RationalInfinite {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
// handle some edge cases.
|
||||
if self.d() == other.d() {
|
||||
self.n().cmp(&other.n())
|
||||
} else if self.d().is_zero() {
|
||||
Ordering::Greater
|
||||
} else if other.d().is_zero() {
|
||||
Ordering::Less
|
||||
} else {
|
||||
// (a/b) cmp (c/d) => (a*d) cmp (c*b)
|
||||
self.n().clone().mul(&other.d()).cmp(&other.n().clone().mul(&self.d()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for RationalInfinite {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.cmp(other) == Ordering::Equal
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rational128> for RationalInfinite {
|
||||
fn from(t: Rational128) -> Self {
|
||||
Self(t.0.into(), t.1.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper for any rational number with a 128 bit numerator and denominator.
|
||||
#[derive(Clone, Copy, Default, Eq, RuntimeDebug)]
|
||||
#[derive(Clone, Copy, Default, Eq)]
|
||||
pub struct Rational128(u128, u128);
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl sp_std::fmt::Debug for Rational128 {
|
||||
fn fmt(&self, f: &mut sp_std::fmt::Formatter<'_>) -> sp_std::fmt::Result {
|
||||
write!(f, "Rational128({:.4})", self.0 as f32 / self.1 as f32)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
impl sp_std::fmt::Debug for Rational128 {
|
||||
fn fmt(&self, f: &mut sp_std::fmt::Formatter<'_>) -> sp_std::fmt::Result {
|
||||
write!(f, "Rational128(..)")
|
||||
}
|
||||
}
|
||||
|
||||
impl Rational128 {
|
||||
/// Nothing.
|
||||
/// Zero.
|
||||
pub fn zero() -> Self {
|
||||
Self(0, 1)
|
||||
}
|
||||
|
||||
/// One
|
||||
pub fn one() -> Self {
|
||||
Self(1, 1)
|
||||
}
|
||||
|
||||
/// If it is zero or not
|
||||
pub fn is_zero(&self) -> bool {
|
||||
self.0.is_zero()
|
||||
@@ -122,6 +209,22 @@ impl Rational128 {
|
||||
}
|
||||
}
|
||||
|
||||
impl Bounded for Rational128 {
|
||||
fn min_value() -> Self {
|
||||
Self(0, 1)
|
||||
}
|
||||
|
||||
fn max_value() -> Self {
|
||||
Self(Bounded::max_value(), 1)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<u128>> From<T> for Rational128 {
|
||||
fn from(t: T) -> Self {
|
||||
Self::from(t.into(), 1)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Rational128 {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
@@ -149,7 +149,10 @@ fn do_phragmen(
|
||||
if eq_iters > 0 {
|
||||
let staked = assignment_ratio_to_staked(assignments, &stake_of);
|
||||
let winners = to_without_backing(winners);
|
||||
let mut support = build_support_map(winners.as_ref(), staked.as_ref()).0;
|
||||
let mut support = build_support_map(
|
||||
winners.as_ref(),
|
||||
staked.as_ref(),
|
||||
).unwrap();
|
||||
|
||||
balance_solution(
|
||||
staked.into_iter().map(|a| (a.clone(), stake_of(&a.who))).collect(),
|
||||
|
||||
@@ -26,8 +26,12 @@ name = "reduce"
|
||||
path = "src/reduce.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "balance_solution"
|
||||
path = "src/balance_solution.rs"
|
||||
name = "phragmen_balancing"
|
||||
path = "src/phragmen_balancing.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "phragmms_balancing"
|
||||
path = "src/phragmms_balancing.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "compact"
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
// 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.
|
||||
|
||||
//! Fuzzing fro the balance_solution algorithm
|
||||
//!
|
||||
//! It ensures that any solution which gets equalized will lead into a better or equally scored
|
||||
//! one.
|
||||
|
||||
mod common;
|
||||
use common::to_range;
|
||||
use honggfuzz::fuzz;
|
||||
use sp_npos_elections::{
|
||||
balance_solution, assignment_ratio_to_staked, build_support_map, to_without_backing, seq_phragmen,
|
||||
ElectionResult, VoteWeight, evaluate_support, is_score_better,
|
||||
};
|
||||
use sp_std::collections::btree_map::BTreeMap;
|
||||
use sp_runtime::Perbill;
|
||||
use rand::{self, Rng, SeedableRng, RngCore};
|
||||
|
||||
type AccountId = u64;
|
||||
|
||||
fn generate_random_phragmen_result(
|
||||
voter_count: u64,
|
||||
target_count: u64,
|
||||
to_elect: usize,
|
||||
edge_per_voter: u64,
|
||||
mut rng: impl RngCore,
|
||||
) -> (ElectionResult<AccountId, Perbill>, BTreeMap<AccountId, VoteWeight>) {
|
||||
let prefix = 100_000;
|
||||
// Note, it is important that stakes are always bigger than ed and
|
||||
let base_stake: u64 = 1_000_000_000;
|
||||
let ed: u64 = base_stake;
|
||||
|
||||
let mut candidates = Vec::with_capacity(target_count as usize);
|
||||
let mut stake_of_tree: BTreeMap<AccountId, VoteWeight> = BTreeMap::new();
|
||||
|
||||
(1..=target_count).for_each(|acc| {
|
||||
candidates.push(acc);
|
||||
let stake_var = rng.gen_range(ed, 100 * ed);
|
||||
stake_of_tree.insert(acc, base_stake + stake_var);
|
||||
});
|
||||
|
||||
let mut voters = Vec::with_capacity(voter_count as usize);
|
||||
(prefix ..= (prefix + voter_count)).for_each(|acc| {
|
||||
// all possible targets
|
||||
let mut all_targets = candidates.clone();
|
||||
// we remove and pop into `targets` `edge_per_voter` times.
|
||||
let targets = (0..edge_per_voter).map(|_| {
|
||||
let upper = all_targets.len() - 1;
|
||||
let idx = rng.gen_range(0, upper);
|
||||
all_targets.remove(idx)
|
||||
})
|
||||
.collect::<Vec<AccountId>>();
|
||||
|
||||
let stake_var = rng.gen_range(ed, 100 * ed) ;
|
||||
let stake = base_stake + stake_var;
|
||||
stake_of_tree.insert(acc, stake);
|
||||
voters.push((acc, stake, targets));
|
||||
});
|
||||
|
||||
(
|
||||
seq_phragmen::<AccountId, sp_runtime::Perbill>(
|
||||
to_elect,
|
||||
0,
|
||||
candidates,
|
||||
voters,
|
||||
).unwrap(),
|
||||
stake_of_tree,
|
||||
)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
loop {
|
||||
fuzz!(|data: (usize, usize, usize, usize, usize, u64)| {
|
||||
let (
|
||||
mut target_count,
|
||||
mut voter_count,
|
||||
mut iterations,
|
||||
mut edge_per_voter,
|
||||
mut to_elect,
|
||||
seed,
|
||||
) = data;
|
||||
let rng = rand::rngs::SmallRng::seed_from_u64(seed);
|
||||
target_count = to_range(target_count, 50, 2000);
|
||||
voter_count = to_range(voter_count, 50, 1000);
|
||||
iterations = to_range(iterations, 1, 20);
|
||||
to_elect = to_range(to_elect, 25, target_count);
|
||||
edge_per_voter = to_range(edge_per_voter, 1, target_count);
|
||||
|
||||
println!("++ [{} / {} / {} / {}]", voter_count, target_count, to_elect, iterations);
|
||||
let (ElectionResult { winners, assignments }, stake_of_tree) = generate_random_phragmen_result(
|
||||
voter_count as u64,
|
||||
target_count as u64,
|
||||
to_elect,
|
||||
edge_per_voter as u64,
|
||||
rng,
|
||||
);
|
||||
|
||||
let stake_of = |who: &AccountId| -> VoteWeight {
|
||||
*stake_of_tree.get(who).unwrap()
|
||||
};
|
||||
|
||||
let mut staked = assignment_ratio_to_staked(assignments, &stake_of);
|
||||
let winners = to_without_backing(winners);
|
||||
let mut support = build_support_map(winners.as_ref(), staked.as_ref()).0;
|
||||
|
||||
let initial_score = evaluate_support(&support);
|
||||
if initial_score[0] == 0 {
|
||||
// such cases cannot be improved by reduce.
|
||||
return;
|
||||
}
|
||||
|
||||
let i = balance_solution(
|
||||
&mut staked,
|
||||
&mut support,
|
||||
10,
|
||||
iterations,
|
||||
);
|
||||
|
||||
let final_score = evaluate_support(&support);
|
||||
if final_score[0] == initial_score[0] {
|
||||
// such solutions can only be improved by such a tiny fiction that it is most often
|
||||
// wrong due to rounding errors.
|
||||
return;
|
||||
}
|
||||
|
||||
let enhance = is_score_better(final_score, initial_score, Perbill::zero());
|
||||
|
||||
println!(
|
||||
"iter = {} // {:?} -> {:?} [{}]",
|
||||
i,
|
||||
initial_score,
|
||||
final_score,
|
||||
enhance,
|
||||
);
|
||||
|
||||
// if more than one iteration has been done, or they must be equal.
|
||||
assert!(enhance || initial_score == final_score || i == 0)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,14 @@
|
||||
|
||||
//! Common fuzzing utils.
|
||||
|
||||
// Each function will be used based on which fuzzer binary is being used.
|
||||
#![allow(dead_code)]
|
||||
|
||||
use sp_npos_elections::{ElectionResult, VoteWeight, phragmms, seq_phragmen};
|
||||
use sp_std::collections::btree_map::BTreeMap;
|
||||
use sp_runtime::Perbill;
|
||||
use rand::{self, Rng, RngCore};
|
||||
|
||||
/// converts x into the range [a, b] in a pseudo-fair way.
|
||||
pub fn to_range(x: usize, a: usize, b: usize) -> usize {
|
||||
// does not work correctly if b < 2 * a
|
||||
@@ -28,3 +36,78 @@ pub fn to_range(x: usize, a: usize, b: usize) -> usize {
|
||||
collapsed + a
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ElectionType {
|
||||
Phragmen(Option<(usize, u128)>),
|
||||
Phragmms(Option<(usize, u128)>)
|
||||
}
|
||||
|
||||
pub type AccountId = u64;
|
||||
|
||||
pub fn generate_random_npos_result(
|
||||
voter_count: u64,
|
||||
target_count: u64,
|
||||
to_elect: usize,
|
||||
mut rng: impl RngCore,
|
||||
election_type: ElectionType,
|
||||
) -> (
|
||||
ElectionResult<AccountId, Perbill>,
|
||||
Vec<AccountId>,
|
||||
Vec<(AccountId, VoteWeight, Vec<AccountId>)>,
|
||||
BTreeMap<AccountId, VoteWeight>,
|
||||
) {
|
||||
let prefix = 100_000;
|
||||
// Note, it is important that stakes are always bigger than ed.
|
||||
let base_stake: u64 = 1_000_000_000_000;
|
||||
let ed: u64 = base_stake;
|
||||
|
||||
let mut candidates = Vec::with_capacity(target_count as usize);
|
||||
let mut stake_of: BTreeMap<AccountId, VoteWeight> = BTreeMap::new();
|
||||
|
||||
(1..=target_count).for_each(|acc| {
|
||||
candidates.push(acc);
|
||||
let stake_var = rng.gen_range(ed, 100 * ed);
|
||||
stake_of.insert(acc, base_stake + stake_var);
|
||||
});
|
||||
|
||||
let mut voters = Vec::with_capacity(voter_count as usize);
|
||||
(prefix ..= (prefix + voter_count)).for_each(|acc| {
|
||||
let edge_per_this_voter = rng.gen_range(1, candidates.len());
|
||||
// all possible targets
|
||||
let mut all_targets = candidates.clone();
|
||||
// we remove and pop into `targets` `edge_per_this_voter` times.
|
||||
let targets = (0..edge_per_this_voter).map(|_| {
|
||||
let upper = all_targets.len() - 1;
|
||||
let idx = rng.gen_range(0, upper);
|
||||
all_targets.remove(idx)
|
||||
})
|
||||
.collect::<Vec<AccountId>>();
|
||||
|
||||
let stake_var = rng.gen_range(ed, 100 * ed) ;
|
||||
let stake = base_stake + stake_var;
|
||||
stake_of.insert(acc, stake);
|
||||
voters.push((acc, stake, targets));
|
||||
});
|
||||
|
||||
(
|
||||
match election_type {
|
||||
ElectionType::Phragmen(conf) =>
|
||||
seq_phragmen::<AccountId, sp_runtime::Perbill>(
|
||||
to_elect,
|
||||
candidates.clone(),
|
||||
voters.clone(),
|
||||
conf,
|
||||
).unwrap(),
|
||||
ElectionType::Phragmms(conf) =>
|
||||
phragmms::<AccountId, sp_runtime::Perbill>(
|
||||
to_elect,
|
||||
candidates.clone(),
|
||||
voters.clone(),
|
||||
conf,
|
||||
).unwrap(),
|
||||
},
|
||||
candidates,
|
||||
voters,
|
||||
stake_of,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// 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.
|
||||
|
||||
//! Fuzzing for sequential phragmen with potential balancing.
|
||||
|
||||
mod common;
|
||||
|
||||
use common::*;
|
||||
use honggfuzz::fuzz;
|
||||
use sp_npos_elections::{
|
||||
assignment_ratio_to_staked_normalized, build_support_map, to_without_backing, VoteWeight,
|
||||
evaluate_support, is_score_better, seq_phragmen,
|
||||
};
|
||||
use sp_runtime::Perbill;
|
||||
use rand::{self, SeedableRng};
|
||||
|
||||
fn main() {
|
||||
loop {
|
||||
fuzz!(|data: (usize, usize, usize, usize, u64)| {
|
||||
let (
|
||||
mut target_count,
|
||||
mut voter_count,
|
||||
mut iterations,
|
||||
mut to_elect,
|
||||
seed,
|
||||
) = data;
|
||||
let rng = rand::rngs::SmallRng::seed_from_u64(seed);
|
||||
target_count = to_range(target_count, 100, 200);
|
||||
voter_count = to_range(voter_count, 100, 200);
|
||||
iterations = to_range(iterations, 0, 30);
|
||||
to_elect = to_range(to_elect, 25, target_count);
|
||||
|
||||
println!(
|
||||
"++ [voter_count: {} / target_count:{} / to_elect:{} / iterations:{}]",
|
||||
voter_count, target_count, to_elect, iterations,
|
||||
);
|
||||
let (
|
||||
unbalanced,
|
||||
candidates,
|
||||
voters,
|
||||
stake_of_tree,
|
||||
) = generate_random_npos_result(
|
||||
voter_count as u64,
|
||||
target_count as u64,
|
||||
to_elect,
|
||||
rng,
|
||||
ElectionType::Phragmen(None),
|
||||
);
|
||||
|
||||
let stake_of = |who: &AccountId| -> VoteWeight {
|
||||
*stake_of_tree.get(who).unwrap()
|
||||
};
|
||||
|
||||
let unbalanced_score = {
|
||||
let staked = assignment_ratio_to_staked_normalized(unbalanced.assignments.clone(), &stake_of).unwrap();
|
||||
let winners = to_without_backing(unbalanced.winners.clone());
|
||||
let support = build_support_map(winners.as_ref(), staked.as_ref()).unwrap();
|
||||
|
||||
let score = evaluate_support(&support);
|
||||
if score[0] == 0 {
|
||||
// such cases cannot be improved by balancing.
|
||||
return;
|
||||
}
|
||||
score
|
||||
};
|
||||
|
||||
if iterations > 0 {
|
||||
let balanced = seq_phragmen::<AccountId, sp_runtime::Perbill>(
|
||||
to_elect,
|
||||
candidates,
|
||||
voters,
|
||||
Some((iterations, 0)),
|
||||
).unwrap();
|
||||
|
||||
let balanced_score = {
|
||||
let staked = assignment_ratio_to_staked_normalized(balanced.assignments.clone(), &stake_of).unwrap();
|
||||
let winners = to_without_backing(balanced.winners);
|
||||
let support = build_support_map(winners.as_ref(), staked.as_ref()).unwrap();
|
||||
|
||||
evaluate_support(&support)
|
||||
};
|
||||
|
||||
let enhance = is_score_better(balanced_score, unbalanced_score, Perbill::zero());
|
||||
|
||||
println!(
|
||||
"iter = {} // {:?} -> {:?} [{}]",
|
||||
iterations,
|
||||
unbalanced_score,
|
||||
balanced_score,
|
||||
enhance,
|
||||
);
|
||||
|
||||
// The only guarantee of balancing is such that the first and third element of the score
|
||||
// cannot decrease.
|
||||
assert!(
|
||||
balanced_score[0] >= unbalanced_score[0] &&
|
||||
balanced_score[1] == unbalanced_score[1] &&
|
||||
balanced_score[2] <= unbalanced_score[2]
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// 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.
|
||||
|
||||
//! Fuzzing for phragmms.
|
||||
|
||||
mod common;
|
||||
|
||||
use common::*;
|
||||
use honggfuzz::fuzz;
|
||||
use sp_npos_elections::{
|
||||
assignment_ratio_to_staked_normalized, build_support_map, to_without_backing, VoteWeight,
|
||||
evaluate_support, is_score_better, phragmms,
|
||||
};
|
||||
use sp_runtime::Perbill;
|
||||
use rand::{self, SeedableRng};
|
||||
|
||||
fn main() {
|
||||
loop {
|
||||
fuzz!(|data: (usize, usize, usize, usize, u64)| {
|
||||
let (
|
||||
mut target_count,
|
||||
mut voter_count,
|
||||
mut iterations,
|
||||
mut to_elect,
|
||||
seed,
|
||||
) = data;
|
||||
let rng = rand::rngs::SmallRng::seed_from_u64(seed);
|
||||
target_count = to_range(target_count, 100, 200);
|
||||
voter_count = to_range(voter_count, 100, 200);
|
||||
iterations = to_range(iterations, 5, 30);
|
||||
to_elect = to_range(to_elect, 25, target_count);
|
||||
|
||||
println!(
|
||||
"++ [voter_count: {} / target_count:{} / to_elect:{} / iterations:{}]",
|
||||
voter_count, target_count, to_elect, iterations,
|
||||
);
|
||||
let (
|
||||
unbalanced,
|
||||
candidates,
|
||||
voters,
|
||||
stake_of_tree,
|
||||
) = generate_random_npos_result(
|
||||
voter_count as u64,
|
||||
target_count as u64,
|
||||
to_elect,
|
||||
rng,
|
||||
ElectionType::Phragmms(None),
|
||||
);
|
||||
|
||||
let stake_of = |who: &AccountId| -> VoteWeight {
|
||||
*stake_of_tree.get(who).unwrap()
|
||||
};
|
||||
|
||||
let unbalanced_score = {
|
||||
let staked = assignment_ratio_to_staked_normalized(unbalanced.assignments.clone(), &stake_of).unwrap();
|
||||
let winners = to_without_backing(unbalanced.winners.clone());
|
||||
let support = build_support_map(winners.as_ref(), staked.as_ref()).unwrap();
|
||||
|
||||
let score = evaluate_support(&support);
|
||||
if score[0] == 0 {
|
||||
// such cases cannot be improved by balancing.
|
||||
return;
|
||||
}
|
||||
score
|
||||
};
|
||||
|
||||
let balanced = phragmms::<AccountId, sp_runtime::Perbill>(
|
||||
to_elect,
|
||||
candidates,
|
||||
voters,
|
||||
Some((iterations, 0)),
|
||||
).unwrap();
|
||||
|
||||
let balanced_score = {
|
||||
let staked = assignment_ratio_to_staked_normalized(balanced.assignments.clone(), &stake_of).unwrap();
|
||||
let winners = to_without_backing(balanced.winners);
|
||||
let support = build_support_map(winners.as_ref(), staked.as_ref()).unwrap();
|
||||
|
||||
evaluate_support(&support)
|
||||
};
|
||||
|
||||
let enhance = is_score_better(balanced_score, unbalanced_score, Perbill::zero());
|
||||
|
||||
println!(
|
||||
"iter = {} // {:?} -> {:?} [{}]",
|
||||
iterations,
|
||||
unbalanced_score,
|
||||
balanced_score,
|
||||
enhance,
|
||||
);
|
||||
|
||||
// The only guarantee of balancing is such that the first and third element of the score
|
||||
// cannot decrease.
|
||||
assert!(
|
||||
balanced_score[0] >= unbalanced_score[0] &&
|
||||
balanced_score[1] == unbalanced_score[1] &&
|
||||
balanced_score[2] <= unbalanced_score[2]
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -110,8 +110,8 @@ fn assert_assignments_equal(
|
||||
ass2: &Vec<StakedAssignment<AccountId>>,
|
||||
) {
|
||||
|
||||
let (support_1, _) = build_support_map::<AccountId>(winners, ass1);
|
||||
let (support_2, _) = build_support_map::<AccountId>(winners, ass2);
|
||||
let support_1 = build_support_map::<AccountId>(winners, ass1).unwrap();
|
||||
let support_2 = build_support_map::<AccountId>(winners, ass2).unwrap();
|
||||
|
||||
for (who, support) in support_1.iter() {
|
||||
assert_eq!(support.total, support_2.get(who).unwrap().total);
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
|
||||
@@ -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,)
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -71,8 +71,9 @@ pub use sp_core::RuntimeDebug;
|
||||
|
||||
/// Re-export top-level arithmetic stuff.
|
||||
pub use sp_arithmetic::{
|
||||
PerThing, traits::SaturatedConversion, Perquintill, Perbill, Permill, Percent, PerU16, InnerOf,
|
||||
PerThing, Perquintill, Perbill, Permill, Percent, PerU16, InnerOf, UpperOf,
|
||||
Rational128, FixedI64, FixedI128, FixedU128, FixedPointNumber, FixedPointOperand,
|
||||
traits::SaturatedConversion,
|
||||
};
|
||||
/// Re-export 128 bit helpers.
|
||||
pub use sp_arithmetic::helpers_128bit;
|
||||
|
||||
Reference in New Issue
Block a user