mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-07-09 21:07:31 +00:00
c786fb21a0
* Refactor election solution trimming for efficiency
The previous version always trimmed the `CompactOf<T>` instance,
which was intrinsically inefficient: that's a packed data structure,
which is naturally expensive to edit. It's much easier to edit
the unpacked data structures: the `voters` and `assignments` lists.
* rework length-trim tests to work with the new interface
Test suite now compiles. Tests still don't pass because the macro
generating the compact structure still generates `unimplemented!()`
for the actual `compact_length_of` implementation.
* simplify
* add a fuzzer which can validate `Compact::encoded_size_for`
The `Compact` solution type is generated distinctly for each runtime,
and has both three type parameters and a built-in limit to the number
of candidates that each voter can vote for. Finally, they have an
optional `#[compact]` attribute which changes the encoding behavior.
The assignment truncation algorithm we're using depends on the ability
to efficiently and accurately determine how much space a `Compact`
solution will take once encoded.
Together, these two facts imply that simple unit tests are not
sufficient to validate the behavior of `Compact::encoded_size_for`.
This commit adds such a fuzzer. It is designed such that it is possible
to add a new fuzzer to the family by simply adjusting the
`generate_solution_type` macro invocation as desired, and making a
few minor documentation edits.
Of course, the fuzzer still fails for now: the generated implementation
for `encoded_size_for` is still `unimplemented!()`. However, once
the macro is updated appropriately, this fuzzer family should allow
us to gain confidence in the correctness of the generated code.
* Revert "add a fuzzer which can validate `Compact::encoded_size_for`"
This reverts commit 916038790887e64217c6a46e9a6d281386762bfb.
The design of `Compact::encoded_size_for` is flawed. When `#[compact]`
mode is enabled, every integer in the dataset is encoded using run-
length encoding. This means that it is impossible to compute the final
length faster than actually encoding the data structure, because the
encoded length of every field varies with the actual value stored.
Given that we won't be adding that method to the trait, we won't be
needing a fuzzer to validate its performance.
* revert changes to `trait CompactSolution`
If `CompactSolution::encoded_size_for` can't be implemented in the
way that we wanted, there's no point in adding it.
* WIP: restructure trim_assignments_length by actually encoding
This is not as efficient as what we'd hoped for, but it should still
be better than what it's replacing. Overall efficiency of
`fn trim_assignments_length` is now `O(edges * lg assignments.len())`.
* fix compiler errors
* don't sort voters, just assignments
Sorting the `voters` list causes lots of problems; an invariant that
we need to maintain is that an index into the voters list has a stable
meaning.
Luckily, it turns out that there is no need for the assignments list
to correspond to the voters list. That isn't an invariant, though previously
I'd thought that it was.
This simplifies things; we can just leave the voters list alone,
and sort the assignments list the way that is convenient.
* WIP: add `IndexAssignment` type to speed up repeatedly creating `Compact`
Next up: `impl<'a, T> From<&'a [IndexAssignmentOf<T>]> for Compact`,
in the proc-macro which makes `Compact`. Should be a pretty straightforward
adaptation of `from_assignment`.
* Add IndexAssignment and conversion method to CompactSolution
This involves a bit of duplication of types from
`election-provider-multi-phase`; we'll clean those up shortly.
I'm not entirely happy that we had to add a `from_index_assignments`
method to `CompactSolution`, but we couldn't define
`trait CompactSolution: TryFrom<&'a [Self::IndexAssignment]` because
that made trait lookup recursive, and I didn't want to propagate
`CompactSolutionOf<T> + TryFrom<&[IndexAssignmentOf<T>]>` everywhere
that compact solutions are specified.
* use `CompactSolution::from_index_assignment` and clean up dead code
* get rid of `from_index_assignments` in favor of `TryFrom`
* cause `pallet-election-provider-multi-phase` tests to compile successfully
Mostly that's just updating the various test functions to keep track of
refactorings elsewhere, though in a few places we needed to refactor some
test-only helpers as well.
* fix infinite binary search loop
Turns out that moving `low` and `high` into an averager function is a
bad idea, because the averager gets copies of those values, which
of course are never updated. Can't use mutable references, because
we want to read them elsewhere in the code. Just compute the average
directly; life is better that way.
* fix a test failure
* fix the rest of test failures
* remove unguarded subtraction
* fix npos-elections tests compilation
* ensure we use sp_std::vec::Vec in assignments
* add IndexAssignmentOf to sp_npos_elections
* move miner types to `unsigned`
* use stable sort
* rewrap some long comments
* use existing cache instead of building a dedicated stake map
* generalize the TryFrom bound on CompactSolution
* undo adding sp-core dependency
* consume assignments to produce index_assignments
* Add a test of Assignment -> IndexAssignment -> Compact
* fix `IndexAssignmentOf` doc
* move compact test from sp-npos-elections-compact to sp-npos-elections
This means that we can put the mocking parts of that into a proper
mock package, put the test into a test package among other tests.
Having the mocking parts in a mock package enables us to create a
benchmark (which is treated as a separate crate) import them.
* rename assignments -> sorted_assignments
* sort after reducing to avoid potential re-sort issues
* add runtime benchmark, fix critical binary search error
"Why don't you add a benchmark?", he said. "It'll be good practice,
and can help demonstrate that this isn't blowing up the runtime."
He was absolutely right.
The biggest discovery is that adding a parametric benchmark means that
you get a bunch of new test cases, for free. This is excellent, because
those test cases uncovered a binary search bug. Fixing that simplified
that part of the code nicely.
The other nice thing you get from a parametric benchmark is data about
what each parameter does. In this case, `f` is the size factor: what
percent of the votes (by size) should be removed. 0 means that we should
keep everything, 95 means that we should trim down to 5% of original size
or less.
```
Median Slopes Analysis
========
-- Extrinsic Time --
Model:
Time ~= 3846
+ v 0.015
+ t 0
+ a 0.192
+ d 0
+ f 0
µs
Min Squares Analysis
========
-- Extrinsic Time --
Data points distribution:
v t a d f mean µs sigma µs %
<snip>
6000 1600 3000 800 0 4385 75.87 1.7%
6000 1600 3000 800 9 4089 46.28 1.1%
6000 1600 3000 800 18 3793 36.45 0.9%
6000 1600 3000 800 27 3365 41.13 1.2%
6000 1600 3000 800 36 3096 7.498 0.2%
6000 1600 3000 800 45 2774 17.96 0.6%
6000 1600 3000 800 54 2057 37.94 1.8%
6000 1600 3000 800 63 1885 2.515 0.1%
6000 1600 3000 800 72 1591 3.203 0.2%
6000 1600 3000 800 81 1219 25.72 2.1%
6000 1600 3000 800 90 859 5.295 0.6%
6000 1600 3000 800 95 684.6 2.969 0.4%
Quality and confidence:
param error
v 0.008
t 0.029
a 0.008
d 0.044
f 0.185
Model:
Time ~= 3957
+ v 0.009
+ t 0
+ a 0.185
+ d 0
+ f 0
µs
```
What's nice about this is the clear negative correlation between
amount removed and total time. The more we remove, the less total
time things take.
539 lines
16 KiB
Rust
539 lines
16 KiB
Rust
// This file is part of Substrate.
|
|
|
|
// Copyright (C) 2019-2021 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.
|
|
|
|
//! Mock file for npos-elections.
|
|
|
|
#![cfg(any(test, mocks))]
|
|
|
|
use std::{
|
|
collections::{HashSet, HashMap},
|
|
convert::TryInto,
|
|
hash::Hash,
|
|
};
|
|
|
|
use rand::{self, Rng, seq::SliceRandom};
|
|
use sp_arithmetic::{
|
|
traits::{One, SaturatedConversion, Zero},
|
|
PerThing,
|
|
};
|
|
use sp_runtime::assert_eq_error_rate;
|
|
use sp_std::collections::btree_map::BTreeMap;
|
|
|
|
use crate::{Assignment, ElectionResult, ExtendedBalance, PerThing128, VoteWeight, seq_phragmen};
|
|
|
|
sp_npos_elections_compact::generate_solution_type!(
|
|
#[compact]
|
|
pub struct Compact::<VoterIndex = u32, TargetIndex = u16, Accuracy = Accuracy>(16)
|
|
);
|
|
|
|
pub type AccountId = u64;
|
|
/// The candidate mask allows easy disambiguation between voters and candidates: accounts
|
|
/// for which this bit is set are candidates, and without it, are voters.
|
|
pub const CANDIDATE_MASK: AccountId = 1 << ((std::mem::size_of::<AccountId>() * 8) - 1);
|
|
pub type CandidateId = AccountId;
|
|
|
|
pub type Accuracy = sp_runtime::Perbill;
|
|
|
|
pub type MockAssignment = crate::Assignment<AccountId, Accuracy>;
|
|
pub type Voter = (AccountId, VoteWeight, Vec<AccountId>);
|
|
|
|
#[derive(Default, Debug)]
|
|
pub(crate) struct _Candidate<A> {
|
|
who: A,
|
|
score: f64,
|
|
approval_stake: f64,
|
|
elected: bool,
|
|
}
|
|
|
|
#[derive(Default, Debug)]
|
|
pub(crate) struct _Voter<A> {
|
|
who: A,
|
|
edges: Vec<_Edge<A>>,
|
|
budget: f64,
|
|
load: f64,
|
|
}
|
|
|
|
#[derive(Default, Debug)]
|
|
pub(crate) struct _Edge<A> {
|
|
who: A,
|
|
load: f64,
|
|
candidate_index: usize,
|
|
}
|
|
|
|
#[derive(Default, Debug, PartialEq)]
|
|
pub(crate) struct _Support<A> {
|
|
pub own: f64,
|
|
pub total: f64,
|
|
pub others: Vec<_Assignment<A>>,
|
|
}
|
|
|
|
pub(crate) type _Assignment<A> = (A, f64);
|
|
pub(crate) type _SupportMap<A> = BTreeMap<A, _Support<A>>;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub(crate) struct _ElectionResult<A: Clone> {
|
|
pub winners: Vec<(A, ExtendedBalance)>,
|
|
pub assignments: Vec<(A, Vec<_Assignment<A>>)>
|
|
}
|
|
|
|
pub(crate) fn auto_generate_self_voters<A: Clone>(candidates: &[A]) -> Vec<(A, Vec<A>)> {
|
|
candidates.iter().map(|c| (c.clone(), vec![c.clone()])).collect()
|
|
}
|
|
|
|
pub(crate) fn elect_float<A>(
|
|
candidate_count: usize,
|
|
initial_candidates: Vec<A>,
|
|
initial_voters: Vec<(A, Vec<A>)>,
|
|
stake_of: impl Fn(&A) -> VoteWeight,
|
|
) -> Option<_ElectionResult<A>> where
|
|
A: Default + Ord + Copy,
|
|
{
|
|
let mut elected_candidates: Vec<(A, ExtendedBalance)>;
|
|
let mut assigned: Vec<(A, Vec<_Assignment<A>>)>;
|
|
let mut c_idx_cache = BTreeMap::<A, usize>::new();
|
|
let num_voters = initial_candidates.len() + initial_voters.len();
|
|
let mut voters: Vec<_Voter<A>> = Vec::with_capacity(num_voters);
|
|
|
|
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<A>>>();
|
|
|
|
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());
|
|
for v in votes {
|
|
if let Some(idx) = c_idx_cache.get(&v) {
|
|
candidates[*idx].approval_stake = candidates[*idx].approval_stake + voter_stake;
|
|
edges.push(
|
|
_Edge { who: v.clone(), candidate_index: *idx, ..Default::default() }
|
|
);
|
|
}
|
|
}
|
|
_Voter {
|
|
who,
|
|
edges: edges,
|
|
budget: voter_stake,
|
|
load: 0f64,
|
|
}
|
|
}));
|
|
|
|
let to_elect = candidate_count.min(candidates.len());
|
|
elected_candidates = Vec::with_capacity(candidate_count);
|
|
assigned = Vec::with_capacity(candidate_count);
|
|
|
|
for _round in 0..to_elect {
|
|
for c in &mut candidates {
|
|
if !c.elected {
|
|
c.score = 1.0 / c.approval_stake;
|
|
}
|
|
}
|
|
for n in &voters {
|
|
for e in &n.edges {
|
|
let c = &mut candidates[e.candidate_index];
|
|
if !c.elected && !(c.approval_stake == 0f64) {
|
|
c.score += n.budget * n.load / c.approval_stake;
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(winner) = candidates
|
|
.iter_mut()
|
|
.filter(|c| !c.elected)
|
|
.min_by(|x, y| x.score.partial_cmp(&y.score).unwrap_or(sp_std::cmp::Ordering::Equal))
|
|
{
|
|
winner.elected = true;
|
|
for n in &mut voters {
|
|
for e in &mut n.edges {
|
|
if e.who == winner.who {
|
|
e.load = winner.score - n.load;
|
|
n.load = winner.score;
|
|
}
|
|
}
|
|
}
|
|
|
|
elected_candidates.push((winner.who.clone(), winner.approval_stake as ExtendedBalance));
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
for n in &mut voters {
|
|
let mut assignment = (n.who.clone(), vec![]);
|
|
for e in &mut n.edges {
|
|
if let Some(c) = elected_candidates.iter().cloned().map(|(c, _)| c).find(|c| *c == e.who) {
|
|
if c != n.who {
|
|
let ratio = e.load / n.load;
|
|
assignment.1.push((e.who.clone(), ratio));
|
|
}
|
|
}
|
|
}
|
|
if assignment.1.len() > 0 {
|
|
assigned.push(assignment);
|
|
}
|
|
}
|
|
|
|
Some(_ElectionResult {
|
|
winners: elected_candidates,
|
|
assignments: assigned,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn equalize_float<A, FS>(
|
|
mut assignments: Vec<(A, Vec<_Assignment<A>>)>,
|
|
supports: &mut _SupportMap<A>,
|
|
tolerance: f64,
|
|
iterations: usize,
|
|
stake_of: FS,
|
|
) where
|
|
for<'r> FS: Fn(&'r A) -> VoteWeight,
|
|
A: Ord + Clone + std::fmt::Debug,
|
|
{
|
|
for _i in 0..iterations {
|
|
let mut max_diff = 0.0;
|
|
for (voter, assignment) in assignments.iter_mut() {
|
|
let voter_budget = stake_of(&voter);
|
|
let diff = do_equalize_float(
|
|
voter,
|
|
voter_budget,
|
|
assignment,
|
|
supports,
|
|
tolerance,
|
|
);
|
|
if diff > max_diff { max_diff = diff; }
|
|
}
|
|
|
|
if max_diff < tolerance {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn do_equalize_float<A>(
|
|
voter: &A,
|
|
budget_balance: VoteWeight,
|
|
elected_edges: &mut Vec<_Assignment<A>>,
|
|
support_map: &mut _SupportMap<A>,
|
|
tolerance: f64
|
|
) -> f64 where
|
|
A: Ord + Clone,
|
|
{
|
|
let budget = budget_balance as f64;
|
|
if elected_edges.is_empty() { return 0.0; }
|
|
|
|
let stake_used = elected_edges
|
|
.iter()
|
|
.fold(0.0, |s, e| s + 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.0)
|
|
.filter_map(|e| support_map.get(&e.0))
|
|
.map(|e| e.total)
|
|
.collect::<Vec<f64>>();
|
|
|
|
let mut difference;
|
|
if backing_backed_stake.len() > 0 {
|
|
let max_stake = backing_backed_stake
|
|
.iter()
|
|
.max_by(|x, y| x.partial_cmp(&y).unwrap_or(sp_std::cmp::Ordering::Equal))
|
|
.expect("vector with positive length will have a max; qed");
|
|
let min_stake = backed_stakes_iter
|
|
.min_by(|x, y| x.partial_cmp(&y).unwrap_or(sp_std::cmp::Ordering::Equal))
|
|
.expect("iterator with positive length will have a min; qed");
|
|
|
|
difference = max_stake - min_stake;
|
|
difference = difference + budget - 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 - e.1;
|
|
support.others.retain(|i_support| i_support.0 != *voter);
|
|
}
|
|
e.1 = 0.0;
|
|
});
|
|
|
|
elected_edges.sort_by(|x, y|
|
|
support_map.get(&x.0)
|
|
.and_then(|x| support_map.get(&y.0).and_then(|y| x.total.partial_cmp(&y.total)))
|
|
.unwrap_or(sp_std::cmp::Ordering::Equal)
|
|
);
|
|
|
|
let mut cumulative_stake = 0.0;
|
|
let mut last_index = elected_edges.len() - 1;
|
|
elected_edges.iter_mut().enumerate().for_each(|(idx, e)| {
|
|
if let Some(support) = support_map.get_mut(&e.0) {
|
|
let stake = support.total;
|
|
let stake_mul = stake * (idx as f64);
|
|
let stake_sub = stake_mul - cumulative_stake;
|
|
if stake_sub > budget {
|
|
last_index = idx.checked_sub(1).unwrap_or(0);
|
|
return
|
|
}
|
|
cumulative_stake = cumulative_stake + stake;
|
|
}
|
|
});
|
|
|
|
let last_stake = elected_edges[last_index].1;
|
|
let split_ways = last_index + 1;
|
|
let excess = budget + cumulative_stake - last_stake * (split_ways as f64);
|
|
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 f64) + last_stake - support.total;
|
|
support.total = support.total + e.1;
|
|
support.others.push((voter.clone(), e.1));
|
|
}
|
|
});
|
|
|
|
difference
|
|
}
|
|
|
|
|
|
pub(crate) fn create_stake_of(stakes: &[(AccountId, VoteWeight)])
|
|
-> impl Fn(&AccountId) -> VoteWeight
|
|
{
|
|
let mut storage = BTreeMap::<AccountId, VoteWeight>::new();
|
|
stakes.iter().for_each(|s| { storage.insert(s.0, s.1); });
|
|
move |who: &AccountId| -> VoteWeight { storage.get(who).unwrap().to_owned() }
|
|
}
|
|
|
|
|
|
pub fn check_assignments_sum<T: PerThing>(assignments: &[Assignment<AccountId, T>]) {
|
|
for Assignment { distribution, .. } in assignments {
|
|
let mut sum: u128 = Zero::zero();
|
|
distribution.iter().for_each(|(_, p)| sum += p.deconstruct().saturated_into::<u128>());
|
|
assert_eq!(sum, T::ACCURACY.saturated_into(), "Assignment ratio sum is not 100%");
|
|
}
|
|
}
|
|
|
|
pub(crate) fn run_and_compare<Output: PerThing128, FS>(
|
|
candidates: Vec<AccountId>,
|
|
voters: Vec<(AccountId, Vec<AccountId>)>,
|
|
stake_of: FS,
|
|
to_elect: usize,
|
|
)
|
|
where
|
|
Output: PerThing128,
|
|
FS: Fn(&AccountId) -> VoteWeight,
|
|
{
|
|
// run fixed point code.
|
|
let ElectionResult { winners, assignments } = seq_phragmen::<_, Output>(
|
|
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,
|
|
candidates,
|
|
voters,
|
|
&stake_of,
|
|
).unwrap();
|
|
|
|
assert_eq!(winners.iter().map(|(x, _)| x).collect::<Vec<_>>(), truth_value.winners.iter().map(|(x, _)| x).collect::<Vec<_>>());
|
|
|
|
for Assignment { who, distribution } in assignments.iter() {
|
|
if let Some(float_assignments) = truth_value.assignments.iter().find(|x| x.0 == *who) {
|
|
for (candidate, per_thingy) in distribution {
|
|
if let Some(float_assignment) = float_assignments.1.iter().find(|x| x.0 == *candidate ) {
|
|
assert_eq_error_rate!(
|
|
Output::from_float(float_assignment.1).deconstruct(),
|
|
per_thingy.deconstruct(),
|
|
Output::Inner::one(),
|
|
);
|
|
} else {
|
|
panic!(
|
|
"candidate mismatch. This should never happen. could not find ({:?}, {:?})",
|
|
candidate,
|
|
per_thingy,
|
|
)
|
|
}
|
|
}
|
|
} else {
|
|
panic!("nominator mismatch. This should never happen.")
|
|
}
|
|
}
|
|
|
|
check_assignments_sum(&assignments);
|
|
}
|
|
|
|
pub(crate) fn build_support_map_float(
|
|
result: &mut _ElectionResult<AccountId>,
|
|
stake_of: impl Fn(&AccountId) -> VoteWeight,
|
|
) -> _SupportMap<AccountId> {
|
|
let mut supports = <_SupportMap<AccountId>>::new();
|
|
result.winners
|
|
.iter()
|
|
.map(|(e, _)| (e, stake_of(e) as f64))
|
|
.for_each(|(e, s)| {
|
|
let item = _Support { own: s, total: s, ..Default::default() };
|
|
supports.insert(e.clone(), item);
|
|
});
|
|
|
|
for (n, assignment) in result.assignments.iter_mut() {
|
|
for (c, r) in assignment.iter_mut() {
|
|
let nominator_stake = stake_of(n) as f64;
|
|
let other_stake = nominator_stake * *r;
|
|
if let Some(support) = supports.get_mut(c) {
|
|
support.total = support.total + other_stake;
|
|
support.others.push((n.clone(), other_stake));
|
|
}
|
|
*r = other_stake;
|
|
}
|
|
}
|
|
supports
|
|
}
|
|
|
|
/// Generate voter and assignment lists. Makes no attempt to be realistic about winner or assignment fairness.
|
|
///
|
|
/// Maintains these invariants:
|
|
///
|
|
/// - candidate ids have `CANDIDATE_MASK` bit set
|
|
/// - voter ids do not have `CANDIDATE_MASK` bit set
|
|
/// - assignments have the same ordering as voters
|
|
/// - `assignments.distribution.iter().map(|(_, frac)| frac).sum() == One::one()`
|
|
/// - a coherent set of winners is chosen.
|
|
/// - the winner set is a subset of the candidate set.
|
|
/// - `assignments.distribution.iter().all(|(who, _)| winners.contains(who))`
|
|
pub fn generate_random_votes(
|
|
candidate_count: usize,
|
|
voter_count: usize,
|
|
mut rng: impl Rng,
|
|
) -> (Vec<Voter>, Vec<MockAssignment>, Vec<CandidateId>) {
|
|
// cache for fast generation of unique candidate and voter ids
|
|
let mut used_ids = HashSet::with_capacity(candidate_count + voter_count);
|
|
|
|
// candidates are easy: just a completely random set of IDs
|
|
let mut candidates: Vec<AccountId> = Vec::with_capacity(candidate_count);
|
|
while candidates.len() < candidate_count {
|
|
let mut new = || rng.gen::<AccountId>() | CANDIDATE_MASK;
|
|
let mut id = new();
|
|
// insert returns `false` when the value was already present
|
|
while !used_ids.insert(id) {
|
|
id = new();
|
|
}
|
|
candidates.push(id);
|
|
}
|
|
|
|
// voters are random ids, random weights, random selection from the candidates
|
|
let mut voters = Vec::with_capacity(voter_count);
|
|
while voters.len() < voter_count {
|
|
let mut new = || rng.gen::<AccountId>() & !CANDIDATE_MASK;
|
|
let mut id = new();
|
|
// insert returns `false` when the value was already present
|
|
while !used_ids.insert(id) {
|
|
id = new();
|
|
}
|
|
|
|
let vote_weight = rng.gen();
|
|
|
|
// it's not interesting if a voter chooses 0 or all candidates, so rule those cases out.
|
|
// also, let's not generate any cases which result in a compact overflow.
|
|
let n_candidates_chosen = rng.gen_range(1, candidates.len().min(16));
|
|
|
|
let mut chosen_candidates = Vec::with_capacity(n_candidates_chosen);
|
|
chosen_candidates.extend(candidates.choose_multiple(&mut rng, n_candidates_chosen));
|
|
voters.push((id, vote_weight, chosen_candidates));
|
|
}
|
|
|
|
// always generate a sensible number of winners: elections are uninteresting if nobody wins,
|
|
// or everybody wins
|
|
let num_winners = rng.gen_range(1, candidate_count);
|
|
let mut winners: HashSet<AccountId> = HashSet::with_capacity(num_winners);
|
|
winners.extend(candidates.choose_multiple(&mut rng, num_winners));
|
|
assert_eq!(winners.len(), num_winners);
|
|
|
|
let mut assignments = Vec::with_capacity(voters.len());
|
|
for (voter_id, _, votes) in voters.iter() {
|
|
let chosen_winners = votes.iter().filter(|vote| winners.contains(vote)).cloned();
|
|
let num_chosen_winners = chosen_winners.clone().count();
|
|
|
|
// distribute the available stake randomly
|
|
let stake_distribution = if num_chosen_winners == 0 {
|
|
Vec::new()
|
|
} else {
|
|
let mut available_stake = 1000;
|
|
let mut stake_distribution = Vec::with_capacity(num_chosen_winners);
|
|
for _ in 0..num_chosen_winners - 1 {
|
|
let stake = rng.gen_range(0, available_stake);
|
|
stake_distribution.push(Accuracy::from_perthousand(stake));
|
|
available_stake -= stake;
|
|
}
|
|
stake_distribution.push(Accuracy::from_perthousand(available_stake));
|
|
stake_distribution.shuffle(&mut rng);
|
|
stake_distribution
|
|
};
|
|
|
|
assignments.push(MockAssignment {
|
|
who: *voter_id,
|
|
distribution: chosen_winners.zip(stake_distribution).collect(),
|
|
});
|
|
}
|
|
|
|
(voters, assignments, candidates)
|
|
}
|
|
|
|
fn generate_cache<Voters, Item>(voters: Voters) -> HashMap<Item, usize>
|
|
where
|
|
Voters: Iterator<Item = Item>,
|
|
Item: Hash + Eq + Copy,
|
|
{
|
|
let mut cache = HashMap::new();
|
|
for (idx, voter_id) in voters.enumerate() {
|
|
cache.insert(voter_id, idx);
|
|
}
|
|
cache
|
|
}
|
|
|
|
/// Create a function that returns the index of a voter in the voters list.
|
|
pub fn make_voter_fn<VoterIndex>(voters: &[Voter]) -> impl Fn(&AccountId) -> Option<VoterIndex>
|
|
where
|
|
usize: TryInto<VoterIndex>,
|
|
{
|
|
let cache = generate_cache(voters.iter().map(|(id, _, _)| *id));
|
|
move |who| cache.get(who).cloned().and_then(|i| i.try_into().ok())
|
|
}
|
|
|
|
/// Create a function that returns the index of a candidate in the candidates list.
|
|
pub fn make_target_fn<TargetIndex>(
|
|
candidates: &[CandidateId],
|
|
) -> impl Fn(&CandidateId) -> Option<TargetIndex>
|
|
where
|
|
usize: TryInto<TargetIndex>,
|
|
{
|
|
let cache = generate_cache(candidates.iter().cloned());
|
|
move |who| cache.get(who).cloned().and_then(|i| i.try_into().ok())
|
|
}
|