// Copyright 2020 Parity Technologies (UK) Ltd.
// This file is part of Substrate.
// Substrate is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Substrate is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Substrate. If not, see .
//! Rust implementation of the Phragmén reduce algorithm. This can be used by any off chain
//! application to reduce cycles from the edge assignment, which will result in smaller size.
//!
//! ### Notions
//! - `m`: size of the committee to elect.
//! - `k`: maximum allowed votes (16 as of this writing).
//! - `nv ∈ E` means that nominator `n ∈ N` supports the election of candidate `v ∈ V`.
//! - A valid solution consists of a tuple `(S, W)` , where `S ⊆ V` is a committee of m validators,
//! and `W : E → R ≥ 0` is an edge weight vector which describes how the budget of each nominator
//! n is fractionally assigned to n 's elected neighbors.
//! - `E_w := { e ∈ E : w_e > 0 }`.
//!
//! ### Algorithm overview
//!
//! > We consider the input edge weight vector `w` as a directed flow over `E_w` , where the flow in
//! > each edge is directed from the nominator to the validator. We build `w′` from `w` by removing
//! > **circulations** to cancel out the flow over as many edges as possible, while preserving flow
//! > demands over all vertices and without reverting the flow direction over any edge. As long as
//! > there is a cycle, we can remove an additional circulation and eliminate at least one new edge
//! > from `E_w′` . This shows that the algorithm always progresses and will eventually finish with
//! > an acyclic edge support. We keep a data structure that represents a forest of rooted trees,
//! > which is initialized as a collection of singletons – one per vertex – and to which edges in
//! > `E_w` are added one by one, causing the trees to merge. Whenever a new edge creates a cycle,
//! > we detect it and destroy it by removing a circulation. We also run a pre-computation which is
//! > designed to detect and remove cycles of length four exclusively. This pre-computation is
//! > optional, and if we skip it then the running time becomes `O (|E_w| ⋅ m), so the
//! > pre-computation makes sense only if `m >> k` and `|E_w| >> m^2`.
//!
//! ### Resources:
//!
//! 1. https://hackmd.io/JOn9x98iS0e0DPWQ87zGWg?view
use crate::node::{Node, NodeId, NodeRef, NodeRole};
use crate::{ExtendedBalance, IdentifierT, StakedAssignment};
use sp_runtime::traits::{Bounded, Zero};
use sp_std::{
collections::btree_map::{BTreeMap, Entry::*},
prelude::*,
};
/// Map type used for reduce_4. Can be easily swapped with HashMap.
type Map = BTreeMap<(A, A), A>;
/// Returns all combinations of size two in the collection `input` with no repetition.
fn combinations_2(input: &[T]) -> Vec<(T, T)> {
let n = input.len();
if n < 2 {
return Default::default();
}
let mut comb = Vec::with_capacity(n * (n - 1) / 2);
for i in 0..n {
for j in i + 1..n {
comb.push((input[i].clone(), input[j].clone()))
}
}
comb
}
/// Returns the count of trailing common elements in two slices.
pub(crate) fn trailing_common(t1: &[T], t2: &[T]) -> usize {
t1.iter().rev().zip(t2.iter().rev()).take_while(|e| e.0 == e.1).count()
}
/// Merges two parent roots as described by the reduce algorithm.
fn merge(voter_root_path: Vec>, target_root_path: Vec>) {
let (shorter_path, longer_path) = if voter_root_path.len() <= target_root_path.len() {
(voter_root_path, target_root_path)
} else {
(target_root_path, voter_root_path)
};
// iterate from last to beginning, skipping the first one. This asserts that
// indexing is always correct.
shorter_path
.iter()
.zip(shorter_path.iter().skip(1))
.for_each(|(voter, next)| Node::set_parent_of(&next, &voter));
Node::set_parent_of(&shorter_path[0], &longer_path[0]);
}
/// Reduce only redundant edges with cycle length of 4.
///
/// Returns the number of edges removed.
///
/// It is strictly assumed that the `who` attribute of all provided assignments are unique. The
/// result will most likely be corrupt otherwise.
///
/// O(|E_w| ⋅ k).
fn reduce_4(assignments: &mut Vec>) -> u32 {
let mut combination_map: Map = Map::new();
let mut num_changed: u32 = Zero::zero();
// we have to use the old fashioned loops here with manual indexing. Borrowing assignments will
// not work since then there is NO way to mutate it inside.
for assignment_index in 0..assignments.len() {
let who = assignments[assignment_index].who.clone();
// all combinations for this particular voter
let distribution_ids = &assignments[assignment_index]
.distribution
.iter()
.map(|(t, _p)| t.clone())
.collect::>();
let candidate_combinations = combinations_2(distribution_ids);
for (v1, v2) in candidate_combinations {
match combination_map.entry((v1.clone(), v2.clone())) {
Vacant(entry) => {
entry.insert(who.clone());
}
Occupied(mut entry) => {
let other_who = entry.get_mut();
// double check if who is still voting for this pair. If not, it means that this
// pair is no longer valid and must have been removed in previous rounds. The
// reason for this is subtle; candidate_combinations is created once while the
// inner loop might remove some edges. Note that if count() > 2, the we have
// duplicates.
if assignments[assignment_index]
.distribution
.iter()
.filter(|(t, _)| *t == v1 || *t == v2)
.count() != 2
{
continue;
}
// check if other_who voted for the same pair v1, v2.
let maybe_other_assignments = assignments.iter().find(|a| a.who == *other_who);
if maybe_other_assignments.is_none() {
continue;
}
let other_assignment =
maybe_other_assignments.expect("value is checked to be 'Some'");
// Collect potential cycle votes
let mut other_cycle_votes = other_assignment
.distribution
.iter()
.filter_map(|(t, w)| {
if *t == v1 || *t == v2 {
Some((t.clone(), *w))
} else {
None
}
})
.collect::>();
let other_votes_count = other_cycle_votes.len();
// If the length is more than 2, then we have identified duplicates. For now, we
// just skip. Later on we can early exit and stop processing this data since it
// is corrupt anyhow.
debug_assert!(other_votes_count <= 2);
if other_votes_count < 2 {
// This is not a cycle. Replace and continue.
*other_who = who.clone();
continue;
} else if other_votes_count == 2 {
// This is a cycle.
let mut who_cycle_votes: Vec<(A, ExtendedBalance)> = Vec::with_capacity(2);
assignments[assignment_index]
.distribution
.iter()
.for_each(|(t, w)| {
if *t == v1 || *t == v2 {
who_cycle_votes.push((t.clone(), *w));
}
});
if who_cycle_votes.len() != 2 {
continue;
}
// Align the targets similarly. This helps with the circulation below.
if other_cycle_votes[0].0 != who_cycle_votes[0].0 {
other_cycle_votes.swap(0, 1);
}
// Find min
let mut min_value: ExtendedBalance = Bounded::max_value();
let mut min_index: usize = 0;
let cycle = who_cycle_votes
.iter()
.chain(other_cycle_votes.iter())
.enumerate()
.map(|(index, (t, w))| {
if *w <= min_value {
min_value = *w;
min_index = index;
}
(t.clone(), *w)
})
.collect::>();
// min was in the first part of the chained iters
let mut increase_indices: Vec = Vec::new();
let mut decrease_indices: Vec = Vec::new();
decrease_indices.push(min_index);
if min_index < 2 {
// min_index == 0 => sibling_index <- 1
// min_index == 1 => sibling_index <- 0
let sibling_index = 1 - min_index;
increase_indices.push(sibling_index);
// valid because the two chained sections of `cycle` are aligned;
// index [0, 2] are both voting for v1 or both v2. Same goes for [1, 3].
decrease_indices.push(sibling_index + 2);
increase_indices.push(min_index + 2);
} else {
// min_index == 2 => sibling_index <- 3
// min_index == 3 => sibling_index <- 2
let sibling_index = 3 - min_index % 2;
increase_indices.push(sibling_index);
// valid because the two chained sections of `cycle` are aligned;
// index [0, 2] are both voting for v1 or both v2. Same goes for [1, 3].
decrease_indices.push(sibling_index - 2);
increase_indices.push(min_index - 2);
}
// apply changes
let mut remove_indices: Vec = Vec::with_capacity(1);
increase_indices.into_iter().for_each(|i| {
let voter = if i < 2 {
who.clone()
} else {
other_who.clone()
};
// Note: so this is pretty ambiguous. We should only look for one
// assignment that meets this criteria and if we find multiple then that
// is a corrupt input. Same goes for the next block.
assignments
.iter_mut()
.filter(|a| a.who == voter)
.for_each(|ass| {
ass.distribution
.iter_mut()
.position(|(t, _)| *t == cycle[i].0)
.map(|idx| {
let next_value =
ass.distribution[idx].1.saturating_add(min_value);
ass.distribution[idx].1 = next_value;
});
});
});
decrease_indices.into_iter().for_each(|i| {
let voter = if i < 2 {
who.clone()
} else {
other_who.clone()
};
assignments
.iter_mut()
.filter(|a| a.who == voter)
.for_each(|ass| {
ass.distribution
.iter_mut()
.position(|(t, _)| *t == cycle[i].0)
.map(|idx| {
let next_value =
ass.distribution[idx].1.saturating_sub(min_value);
if next_value.is_zero() {
ass.distribution.remove(idx);
remove_indices.push(i);
num_changed += 1;
} else {
ass.distribution[idx].1 = next_value;
}
});
});
});
// remove either one of them.
let who_removed = remove_indices.iter().find(|i| **i < 2usize).is_some();
let other_removed =
remove_indices.into_iter().find(|i| *i >= 2usize).is_some();
match (who_removed, other_removed) {
(false, true) => {
*other_who = who.clone();
}
(true, false) => {
// nothing, other_who can stay there.
}
(true, true) => {
// remove and don't replace
entry.remove();
}
(false, false) => {
// Neither of the edges was removed? impossible.
debug_assert!(false, "Duplicate voter (or other corrupt input).");
}
}
}
}
}
}
}
num_changed
}
/// Reduce redundant edges from the edge weight graph, with all possible length.
///
/// To get the best performance, this should be called after `reduce_4()`.
///
/// Returns the number of edges removed.
///
/// It is strictly assumed that the `who` attribute of all provided assignments are unique. The
/// result will most likely be corrupt otherwise.
///
/// O(|Ew| ⋅ m)
fn reduce_all(assignments: &mut Vec>) -> u32 {
let mut num_changed: u32 = Zero::zero();
let mut tree: BTreeMap, NodeRef> = BTreeMap::new();
// NOTE: This code can heavily use an index cache. Looking up a pair of (voter, target) in the
// assignments happens numerous times and and we can save time. For now it is written as such
// because abstracting some of this code into a function/closure is super hard due to borrow
// checks (and most likely needs unsafe code at the end). For now I will keep it as it and
// refactor later.
// a flat iterator of (voter, target) over all pairs of votes. Similar to reduce_4, we loop
// without borrowing.
for assignment_index in 0..assignments.len() {
let voter = assignments[assignment_index].who.clone();
let mut dist_index = 0;
loop {
// A distribution could have been removed. We don't know for sure. Hence, we check.
let maybe_dist = assignments[assignment_index].distribution.get(dist_index);
if maybe_dist.is_none() {
// The rest of this loop is moot.
break;
}
let (target, _) = maybe_dist.expect("Value checked to be some").clone();
// store if they existed already.
let voter_id = NodeId::from(voter.clone(), NodeRole::Voter);
let target_id = NodeId::from(target.clone(), NodeRole::Target);
let voter_exists = tree.contains_key(&voter_id);
let target_exists = tree.contains_key(&target_id);
// create both.
let voter_node = tree
.entry(voter_id.clone())
.or_insert(Node::new(voter_id).into_ref())
.clone();
let target_node = tree
.entry(target_id.clone())
.or_insert(Node::new(target_id).into_ref())
.clone();
// If one exists but the other one doesn't, or if both does not, then set the existing
// one as the parent of the non-existing one and move on. Else, continue with the rest
// of the code.
match (voter_exists, target_exists) {
(false, false) => {
Node::set_parent_of(&target_node, &voter_node);
dist_index += 1;
continue;
}
(false, true) => {
Node::set_parent_of(&voter_node, &target_node);
dist_index += 1;
continue;
}
(true, false) => {
Node::set_parent_of(&target_node, &voter_node);
dist_index += 1;
continue;
}
(true, true) => { /* don't continue and execute the rest */ }
};
let (voter_root, voter_root_path) = Node::root(&voter_node);
let (target_root, target_root_path) = Node::root(&target_node);
if voter_root != target_root {
// swap
merge(voter_root_path, target_root_path);
dist_index += 1;
} else {
// find common and cycle.
let common_count = trailing_common(&voter_root_path, &target_root_path);
// because roots are the same.
#[cfg(feature = "std")]
debug_assert_eq!(
target_root_path.last().unwrap(),
voter_root_path.last().unwrap()
);
debug_assert!(common_count > 0);
// cycle part of each path will be `path[path.len() - common_count - 1 : 0]`
// NOTE: the order of chaining is important! it is always build from [target, ...,
// voter]
let cycle = target_root_path
.iter()
.take(target_root_path.len() - common_count + 1)
.cloned()
.chain(
voter_root_path
.iter()
.take(voter_root_path.len() - common_count)
.rev()
.cloned(),
)
.collect::>>();
// a cycle's length shall always be multiple of two.
#[cfg(feature = "std")]
debug_assert_eq!(cycle.len() % 2, 0);
// find minimum of cycle.
let mut min_value: ExtendedBalance = Bounded::max_value();
// The voter and the target pair that create the min edge.
let mut min_target: A = Default::default();
let mut min_voter: A = Default::default();
// The index of the min in opaque cycle list.
let mut min_index = 0usize;
// 1 -> next // 0 -> prev
let mut min_direction = 0u32;
// helpers
let next_index = |i| {
if i < (cycle.len() - 1) {
i + 1
} else {
0
}
};
let prev_index = |i| {
if i > 0 {
i - 1
} else {
cycle.len() - 1
}
};
for i in 0..cycle.len() {
if cycle[i].borrow().id.role == NodeRole::Voter {
// NOTE: sadly way too many clones since I don't want to make A: Copy
let current = cycle[i].borrow().id.who.clone();
let next = cycle[next_index(i)].borrow().id.who.clone();
let prev = cycle[prev_index(i)].borrow().id.who.clone();
assignments.iter().find(|a| a.who == current).map(|ass| {
ass.distribution.iter().find(|d| d.0 == next).map(|(_, w)| {
if *w < min_value {
min_value = *w;
min_target = next.clone();
min_voter = current.clone();
min_index = i;
min_direction = 1;
}
})
});
assignments.iter().find(|a| a.who == current).map(|ass| {
ass.distribution.iter().find(|d| d.0 == prev).map(|(_, w)| {
if *w < min_value {
min_value = *w;
min_target = prev.clone();
min_voter = current.clone();
min_index = i;
min_direction = 0;
}
})
});
}
}
// if the min edge is in the voter's sub-chain.
// [target, ..., X, Y, ... voter]
let target_chunk = target_root_path.len() - common_count;
let min_chain_in_voter = (min_index + min_direction as usize) > target_chunk;
// walk over the cycle and update the weights
let mut should_inc_counter = true;
let start_operation_add = ((min_index % 2) + min_direction as usize) % 2 == 1;
let mut additional_removed = Vec::new();
for i in 0..cycle.len() {
let current = cycle[i].borrow();
if current.id.role == NodeRole::Voter {
let prev = cycle[prev_index(i)].borrow();
assignments
.iter_mut()
.enumerate()
.filter(|(_, a)| a.who == current.id.who)
.for_each(|(target_ass_index, ass)| {
ass.distribution
.iter_mut()
.position(|(t, _)| *t == prev.id.who)
.map(|idx| {
let next_value = if i % 2 == 0 {
if start_operation_add {
ass.distribution[idx].1.saturating_add(min_value)
} else {
ass.distribution[idx].1.saturating_sub(min_value)
}
} else {
if start_operation_add {
ass.distribution[idx].1.saturating_sub(min_value)
} else {
ass.distribution[idx].1.saturating_add(min_value)
}
};
if next_value.is_zero() {
// if the removed edge is from the current assignment, dis_index
// should NOT be increased.
if target_ass_index == assignment_index {
should_inc_counter = false
}
ass.distribution.remove(idx);
num_changed += 1;
// only add if this is not the min itself.
if !(i == min_index && min_direction == 0) {
additional_removed.push((
cycle[i].clone(),
cycle[prev_index(i)].clone(),
));
}
} else {
ass.distribution[idx].1 = next_value;
}
});
});
let next = cycle[next_index(i)].borrow();
assignments
.iter_mut()
.enumerate()
.filter(|(_, a)| a.who == current.id.who)
.for_each(|(target_ass_index, ass)| {
ass.distribution
.iter_mut()
.position(|(t, _)| *t == next.id.who)
.map(|idx| {
let next_value = if i % 2 == 0 {
if start_operation_add {
ass.distribution[idx].1.saturating_sub(min_value)
} else {
ass.distribution[idx].1.saturating_add(min_value)
}
} else {
if start_operation_add {
ass.distribution[idx].1.saturating_add(min_value)
} else {
ass.distribution[idx].1.saturating_sub(min_value)
}
};
if next_value.is_zero() {
// if the removed edge is from the current assignment, dis_index
// should NOT be increased.
if target_ass_index == assignment_index {
should_inc_counter = false
}
ass.distribution.remove(idx);
num_changed += 1;
if !(i == min_index && min_direction == 1) {
additional_removed.push((
cycle[i].clone(),
cycle[next_index(i)].clone(),
));
}
} else {
ass.distribution[idx].1 = next_value;
}
});
});
}
}
// don't do anything if the edge removed itself. This is always the first and last
// element
let should_reorg = !(min_index == (cycle.len() - 1) && min_direction == 1);
// re-org.
if should_reorg {
let min_edge = vec![min_voter, min_target];
if min_chain_in_voter {
// NOTE: safe; voter_root_path is always bigger than 1 element.
for i in 0..voter_root_path.len() - 1 {
let current = voter_root_path[i].clone().borrow().id.who.clone();
let next = voter_root_path[i + 1].clone().borrow().id.who.clone();
if min_edge.contains(¤t) && min_edge.contains(&next) {
break;
}
Node::set_parent_of(&voter_root_path[i + 1], &voter_root_path[i]);
}
Node::set_parent_of(&voter_node, &target_node);
} else {
// NOTE: safe; target_root_path is always bigger than 1 element.
for i in 0..target_root_path.len() - 1 {
let current = target_root_path[i].clone().borrow().id.who.clone();
let next = target_root_path[i + 1].clone().borrow().id.who.clone();
if min_edge.contains(¤t) && min_edge.contains(&next) {
break;
}
Node::set_parent_of(&target_root_path[i + 1], &target_root_path[i]);
}
Node::set_parent_of(&target_node, &voter_node);
}
}
// remove every other node which has collapsed to zero
for (r1, r2) in additional_removed {
if Node::is_parent_of(&r1, &r2) {
Node::remove_parent(&r1);
} else if Node::is_parent_of(&r2, &r1) {
Node::remove_parent(&r2);
}
}
// increment the counter if needed.
if should_inc_counter {
dist_index += 1;
}
}
}
}
num_changed
}
/// Reduce the given [`PhragmenResult`]. This removes redundant edges from without changing the
/// overall backing of any of the elected candidates.
///
/// Returns the number of edges removed.
///
/// IMPORTANT: It is strictly assumed that the `who` attribute of all provided assignments are
/// unique. The result will most likely be corrupt otherwise. Furthermore, if the _distribution
/// vector_ contains duplicate ids, only the first instance is ever updates.
///
/// O(min{ |Ew| ⋅ k + m3 , |Ew| ⋅ m })
pub fn reduce(assignments: &mut Vec>) -> u32 where {
let mut num_changed = reduce_4(assignments);
num_changed += reduce_all(assignments);
num_changed
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn merging_works() {
// D <-- A <-- B <-- C
//
// F <-- E
let d = Node::new(NodeId::from(1, NodeRole::Target)).into_ref();
let a = Node::new(NodeId::from(2, NodeRole::Target)).into_ref();
let b = Node::new(NodeId::from(3, NodeRole::Target)).into_ref();
let c = Node::new(NodeId::from(4, NodeRole::Target)).into_ref();
let e = Node::new(NodeId::from(5, NodeRole::Target)).into_ref();
let f = Node::new(NodeId::from(6, NodeRole::Target)).into_ref();
Node::set_parent_of(&c, &b);
Node::set_parent_of(&b, &a);
Node::set_parent_of(&a, &d);
Node::set_parent_of(&e, &f);
let path1 = vec![c.clone(), b.clone(), a.clone(), d.clone()];
let path2 = vec![e.clone(), f.clone()];
merge(path1, path2);
// D <-- A <-- B <-- C
// |
// F --> E --> -->
assert_eq!(e.borrow().clone().parent.unwrap().borrow().id.who, 4u32); // c
}
#[test]
fn merge_with_len_one() {
// D <-- A <-- B <-- C
//
// F <-- E
let d = Node::new(NodeId::from(1, NodeRole::Target)).into_ref();
let a = Node::new(NodeId::from(2, NodeRole::Target)).into_ref();
let b = Node::new(NodeId::from(3, NodeRole::Target)).into_ref();
let c = Node::new(NodeId::from(4, NodeRole::Target)).into_ref();
let f = Node::new(NodeId::from(6, NodeRole::Target)).into_ref();
Node::set_parent_of(&c, &b);
Node::set_parent_of(&b, &a);
Node::set_parent_of(&a, &d);
let path1 = vec![c.clone(), b.clone(), a.clone(), d.clone()];
let path2 = vec![f.clone()];
merge(path1, path2);
// D <-- A <-- B <-- C
// |
// F --> -->
assert_eq!(f.borrow().clone().parent.unwrap().borrow().id.who, 4u32); // c
}
#[test]
fn basic_reduce_4_cycle_works() {
use super::*;
let assignments = vec![
StakedAssignment {
who: 1,
distribution: vec![(10, 25), (20, 75)],
},
StakedAssignment {
who: 2,
distribution: vec![(10, 50), (20, 50)],
},
];
let mut new_assignments = assignments.clone();
let num_reduced = reduce_4(&mut new_assignments);
assert_eq!(num_reduced, 1);
assert_eq!(
new_assignments,
vec![
StakedAssignment {
who: 1,
distribution: vec![(20, 100),],
},
StakedAssignment {
who: 2,
distribution: vec![(10, 75), (20, 25),],
},
],
);
}
#[test]
fn basic_reduce_all_cycles_works() {
let mut assignments = vec![
StakedAssignment {
who: 1,
distribution: vec![(10, 10)],
},
StakedAssignment {
who: 2,
distribution: vec![(10, 15), (20, 5)],
},
StakedAssignment {
who: 3,
distribution: vec![(20, 15), (40, 15)],
},
StakedAssignment {
who: 4,
distribution: vec![(20, 10), (30, 10), (40, 20)],
},
StakedAssignment {
who: 5,
distribution: vec![(20, 20), (30, 10), (40, 20)],
},
];
assert_eq!(3, reduce_all(&mut assignments));
assert_eq!(
assignments,
vec![
StakedAssignment {
who: 1,
distribution: vec![(10, 10),]
},
StakedAssignment {
who: 2,
distribution: vec![(10, 15), (20, 5),],
},
StakedAssignment {
who: 3,
distribution: vec![(20, 30),],
},
StakedAssignment {
who: 4,
distribution: vec![(40, 40),]
},
StakedAssignment {
who: 5,
distribution: vec![(20, 15), (30, 20), (40, 15),],
},
],
)
}
#[test]
fn basic_reduce_works() {
let mut assignments = vec![
StakedAssignment {
who: 1,
distribution: vec![(10, 10)],
},
StakedAssignment {
who: 2,
distribution: vec![(10, 15), (20, 5)],
},
StakedAssignment {
who: 3,
distribution: vec![(20, 15), (40, 15)],
},
StakedAssignment {
who: 4,
distribution: vec![(20, 10), (30, 10), (40, 20)],
},
StakedAssignment {
who: 5,
distribution: vec![(20, 20), (30, 10), (40, 20)],
},
];
assert_eq!(3, reduce(&mut assignments));
assert_eq!(
assignments,
vec![
StakedAssignment {
who: 1,
distribution: vec![(10, 10),]
},
StakedAssignment {
who: 2,
distribution: vec![(10, 15), (20, 5),],
},
StakedAssignment {
who: 3,
distribution: vec![(20, 30),],
},
StakedAssignment {
who: 4,
distribution: vec![(40, 40),]
},
StakedAssignment {
who: 5,
distribution: vec![(20, 15), (30, 20), (40, 15),],
},
],
)
}
#[test]
fn should_deal_with_self_vote() {
let mut assignments = vec![
StakedAssignment {
who: 1,
distribution: vec![(10, 10)],
},
StakedAssignment {
who: 2,
distribution: vec![(10, 15), (20, 5)],
},
StakedAssignment {
who: 3,
distribution: vec![(20, 15), (40, 15)],
},
StakedAssignment {
who: 4,
distribution: vec![(20, 10), (30, 10), (40, 20)],
},
StakedAssignment {
who: 5,
distribution: vec![(20, 20), (30, 10), (40, 20)],
},
// self vote from 10 and 20 to itself.
StakedAssignment {
who: 10,
distribution: vec![(10, 100)],
},
StakedAssignment {
who: 20,
distribution: vec![(20, 200)],
},
];
assert_eq!(3, reduce(&mut assignments));
assert_eq!(
assignments,
vec![
StakedAssignment {
who: 1,
distribution: vec![(10, 10),]
},
StakedAssignment {
who: 2,
distribution: vec![(10, 15), (20, 5),],
},
StakedAssignment {
who: 3,
distribution: vec![(20, 30),],
},
StakedAssignment {
who: 4,
distribution: vec![(40, 40),]
},
StakedAssignment {
who: 5,
distribution: vec![(20, 15), (30, 20), (40, 15),],
},
// should stay untouched.
StakedAssignment {
who: 10,
distribution: vec![(10, 100)]
},
StakedAssignment {
who: 20,
distribution: vec![(20, 200)]
},
],
)
}
#[test]
fn reduce_3_common_votes_same_weight() {
let mut assignments = vec![
StakedAssignment {
who: 4,
distribution: vec![
(
1000000,
100,
),
(
1000002,
100,
),
(
1000004,
100,
),
],
},
StakedAssignment {
who: 5,
distribution: vec![
(
1000000,
100,
),
(
1000002,
100,
),
(
1000004,
100,
),
],
},
];
reduce_4(&mut assignments);
assert_eq!(
assignments,
vec![
StakedAssignment {
who: 4,
distribution: vec![(1000000, 200,), (1000004, 100,),],
},
StakedAssignment {
who: 5,
distribution: vec![(1000002, 200,), (1000004, 100,),],
},
],
)
}
#[test]
#[should_panic]
fn reduce_panics_on_duplicate_voter() {
let mut assignments = vec![
StakedAssignment {
who: 1,
distribution: vec![(10, 10), (20, 10)],
},
StakedAssignment {
who: 1,
distribution: vec![(10, 15), (20, 5)],
},
StakedAssignment {
who: 2,
distribution: vec![(10, 15), (20, 15)],
},
];
reduce(&mut assignments);
}
#[test]
fn should_deal_with_duplicates_target() {
let mut assignments = vec![
StakedAssignment {
who: 1,
distribution: vec![(10, 15), (20, 5)],
},
StakedAssignment {
who: 2,
distribution: vec![
(10, 15),
(20, 15),
// duplicate
(10, 1),
// duplicate
(20, 1),
],
},
];
reduce(&mut assignments);
assert_eq!(
assignments,
vec![
StakedAssignment {
who: 1,
distribution: vec![(10, 20),],
},
StakedAssignment {
who: 2,
distribution: vec![
(10, 10),
(20, 20),
// duplicate votes are silently ignored.
(10, 1),
(20, 1),
],
},
],
)
}
#[test]
fn bound_should_be_kept() {
let mut assignments = vec![
StakedAssignment {
who: 1,
distribution: vec![(103, 72), (101, 53), (100, 83), (102, 38)],
},
StakedAssignment {
who: 2,
distribution: vec![(103, 18), (101, 36), (102, 54), (100, 94)],
},
StakedAssignment {
who: 3,
distribution: vec![(100, 96), (101, 35), (102, 52), (103, 69)],
},
StakedAssignment {
who: 4,
distribution: vec![(102, 34), (100, 47), (103, 91), (101, 73)],
},
];
let winners = vec![103, 101, 100, 102];
let n = 4;
let m = winners.len() as u32;
let num_reduced = reduce_all(&mut assignments);
assert!(16 - num_reduced <= n + m);
}
}