Rename all the election operations (#6245)

* Rename and move sp-phragmen

* More renames for equalise

* Update main module doc

* Fix line width

* Line width
This commit is contained in:
Kian Paimani
2020-06-05 17:33:13 +02:00
committed by GitHub
parent d63b8e0da0
commit 8a8b4f99c3
35 changed files with 296 additions and 322 deletions
@@ -0,0 +1,99 @@
// 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.
//! Helper methods for npos-elections.
use crate::{Assignment, ExtendedBalance, VoteWeight, IdentifierT, StakedAssignment, WithApprovalOf};
use sp_arithmetic::PerThing;
use sp_std::prelude::*;
/// Converts a vector of ratio assignments into ones with absolute budget value.
pub fn assignment_ratio_to_staked<A: IdentifierT, T: PerThing, FS>(
ratio: Vec<Assignment<A, T>>,
stake_of: FS,
) -> Vec<StakedAssignment<A>>
where
for<'r> FS: Fn(&'r A) -> VoteWeight,
T: sp_std::ops::Mul<ExtendedBalance, Output = ExtendedBalance>,
ExtendedBalance: From<<T as PerThing>::Inner>,
{
ratio
.into_iter()
.map(|a| {
let stake = stake_of(&a.who);
a.into_staked(stake.into(), true)
})
.collect()
}
/// Converts a vector of staked assignments into ones with ratio values.
pub fn assignment_staked_to_ratio<A: IdentifierT, T: PerThing>(
staked: Vec<StakedAssignment<A>>,
) -> Vec<Assignment<A, T>>
where
ExtendedBalance: From<<T as PerThing>::Inner>,
{
staked.into_iter().map(|a| a.into_assignment(true)).collect()
}
/// consumes a vector of winners with backing stake to just winners.
pub fn to_without_backing<A: IdentifierT>(winners: Vec<WithApprovalOf<A>>) -> Vec<A> {
winners.into_iter().map(|(who, _)| who).collect::<Vec<A>>()
}
#[cfg(test)]
mod tests {
use super::*;
use sp_arithmetic::Perbill;
#[test]
fn into_staked_works() {
let assignments = vec![
Assignment {
who: 1u32,
distribution: vec![
(10u32, Perbill::from_fraction(0.5)),
(20, Perbill::from_fraction(0.5)),
],
},
Assignment {
who: 2u32,
distribution: vec![
(10, Perbill::from_fraction(0.33)),
(20, Perbill::from_fraction(0.67)),
],
},
];
let stake_of = |_: &u32| -> VoteWeight { 100 };
let staked = assignment_ratio_to_staked(assignments, stake_of);
assert_eq!(
staked,
vec![
StakedAssignment {
who: 1u32,
distribution: vec![(10u32, 50), (20, 50),]
},
StakedAssignment {
who: 2u32,
distribution: vec![(10u32, 33), (20, 67),]
}
]
);
}
}
@@ -0,0 +1,779 @@
// This file is part of Substrate.
// 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
//
// 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.
//! A set of election algorithms to be used with a substrate runtime, typically within the staking
//! 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`].
//!
//! 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_arithmetic::{
PerThing, Rational128, ThresholdOrd,
helpers_128bit::multiply_by_rational,
traits::{Zero, Saturating, Bounded, SaturatedConversion},
};
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
#[cfg(feature = "std")]
use serde::{Serialize, Deserialize};
#[cfg(feature = "std")]
use codec::{Encode, Decode};
mod node;
mod reduce;
mod helpers;
// re-export reduce stuff.
pub use reduce::reduce;
// re-export the helpers.
pub use helpers::*;
// re-export the compact macro, with the dependencies of the macro.
#[doc(hidden)]
pub use codec;
#[doc(hidden)]
pub use sp_arithmetic;
// re-export the compact solution type.
pub use sp_npos_elections_compact::generate_compact_solution_type;
/// A trait to limit the number of votes per voter. The generated compact type will implement this.
pub trait VotingLimit {
const LIMIT: usize;
}
/// an aggregator trait for a generic type of a voter/target identifier. This usually maps to
/// substrate's account id.
pub trait IdentifierT: Clone + Eq + Default + Ord + Debug + codec::Codec {}
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.
CompactStakeOverflow,
/// The compact type has a voter who's number of targets is out of bound.
CompactTargetOverflow,
/// One of the index functions returned none.
CompactInvalidIndex,
}
/// A type which is used in the API of this crate as a numeric weight of a vote, most often the
/// stake of the voter. It is always converted to [`ExtendedBalance`] for computation.
pub type VoteWeight = u64;
/// A type in which performing operations on vote weights are safe.
pub type ExtendedBalance = u128;
/// The score of an assignment. This can be computed from the support map via [`evaluate_support`].
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 candidate entity for the election.
#[derive(Clone, Default, Debug)]
struct Candidate<AccountId> {
/// Identifier.
who: AccountId,
/// Intermediary value used to sort candidates.
score: Rational128,
/// Sum of the stake of this candidate based on received votes.
approval_stake: ExtendedBalance,
/// Flag for being elected.
elected: bool,
}
/// A voter entity.
#[derive(Clone, Default, Debug)]
struct Voter<AccountId> {
/// Identifier.
who: AccountId,
/// List of candidates proposed 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: 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,
}
/// Final result of the election.
#[derive(Debug)]
pub struct ElectionResult<AccountId, T: 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>>,
}
/// A voter's stake assignment among a set of targets, represented as ratios.
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "std", derive(PartialEq, Eq, Encode, Decode))]
pub struct Assignment<AccountId, T: PerThing> {
/// Voter's identifier.
pub who: AccountId,
/// The distribution of the voter's stake.
pub distribution: Vec<(AccountId, T)>,
}
impl<AccountId, T: PerThing> Assignment<AccountId, T>
where
ExtendedBalance: From<<T as PerThing>::Inner>,
{
/// 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
/// distribution's sum is exactly equal to the total budget, by adding or subtracting the
/// remainder from the last distribution.
///
/// If an edge ratio is [`Bounded::max_value()`], it is dropped. This edge can never mean
/// anything useful.
pub fn into_staked(self, stake: ExtendedBalance, fill: bool) -> StakedAssignment<AccountId>
where
T: sp_std::ops::Mul<ExtendedBalance, Output = ExtendedBalance>,
{
let mut sum: ExtendedBalance = Bounded::min_value();
let mut distribution = self
.distribution
.into_iter()
.filter_map(|(target, p)| {
// if this ratio is zero, then skip it.
if p == Bounded::min_value() {
None
} else {
// NOTE: this mul impl will always round to the nearest number, so we might both
// overflow and underflow.
let distribution_stake = p * stake;
// defensive only. We assume that balance cannot exceed extended balance.
sum = sum.saturating_add(distribution_stake);
Some((target, distribution_stake))
}
})
.collect::<Vec<(AccountId, ExtendedBalance)>>();
if fill {
// NOTE: we can do this better.
// https://revs.runtime-revolution.com/getting-100-with-rounded-percentages-273ffa70252b
if let Some(leftover) = stake.checked_sub(sum) {
if let Some(last) = distribution.last_mut() {
last.1 = last.1.saturating_add(leftover);
}
} else if let Some(excess) = sum.checked_sub(stake) {
if let Some(last) = distribution.last_mut() {
last.1 = last.1.saturating_sub(excess);
}
}
}
StakedAssignment {
who: self.who,
distribution,
}
}
}
/// A voter's stake assignment among a set of targets, represented as absolute values in the scale
/// of [`ExtendedBalance`].
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "std", derive(PartialEq, Eq, Encode, Decode))]
pub struct StakedAssignment<AccountId> {
/// Voter's identifier
pub who: AccountId,
/// The distribution of the voter's stake.
pub distribution: Vec<(AccountId, ExtendedBalance)>,
}
impl<AccountId> StakedAssignment<AccountId> {
/// Converts self into the normal [`Assignment`] type.
///
/// 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 100%, by adding or subtracting
/// the remainder from the last distribution.
///
/// NOTE: it is quite critical that this attempt always works. The data type returned here will
/// potentially get used to create a compact type; a compact type requires sum of ratios to be
/// less than 100% upon un-compacting.
///
/// If an edge stake is so small that it cannot be represented in `T`, it is ignored. This edge
/// can never be re-created and does not mean anything useful anymore.
pub fn into_assignment<T: PerThing>(self, fill: bool) -> Assignment<AccountId, T>
where
ExtendedBalance: From<<T as PerThing>::Inner>,
{
let accuracy: u128 = T::ACCURACY.saturated_into();
let mut sum: u128 = Zero::zero();
let stake = self.distribution.iter().map(|x| x.1).sum();
let mut distribution = self
.distribution
.into_iter()
.filter_map(|(target, w)| {
let per_thing = T::from_rational_approximation(w, stake);
if per_thing == Bounded::min_value() {
None
} else {
sum += per_thing.clone().deconstruct().saturated_into();
Some((target, per_thing))
}
})
.collect::<Vec<(AccountId, T)>>();
if fill {
if let Some(leftover) = accuracy.checked_sub(sum) {
if let Some(last) = distribution.last_mut() {
last.1 = last.1.saturating_add(
T::from_parts(leftover.saturated_into())
);
}
} else if let Some(excess) = sum.checked_sub(accuracy) {
if let Some(last) = distribution.last_mut() {
last.1 = last.1.saturating_sub(
T::from_parts(excess.saturated_into())
);
}
}
}
Assignment {
who: self.who,
distribution,
}
}
/// Get the total stake of this assignment (aka voter budget).
pub fn total(&self) -> ExtendedBalance {
self.distribution.iter().fold(Zero::zero(), |a, b| a.saturating_add(b.1))
}
}
/// A structure to demonstrate the election result from the perspective of the candidate, i.e. how
/// much support each candidate is receiving.
///
/// 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.
#[derive(Default, Debug)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Eq, PartialEq))]
pub struct Support<AccountId> {
/// Total support.
pub total: ExtendedBalance,
/// Support from voters.
pub voters: Vec<(AccountId, ExtendedBalance)>,
}
/// 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 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(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(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(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)]
/// ]
/// ```
///
/// into a mapping of candidates and their respective support:
///
/// ```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)]
/// },
/// }
/// ```
///
/// The second returned flag indicates the number of edges who didn't corresponded to an actual
/// winner from the given winner set. A value in this place larger than 0 indicates a potentially
/// faulty assignment.
///
/// `O(E)` where `E` is the total number of edges.
pub fn build_support_map<AccountId>(
winners: &[AccountId],
assignments: &[StakedAssignment<AccountId>],
) -> (SupportMap<AccountId>, u32) where
AccountId: Default + Ord + Clone,
{
let mut errors = 0;
// Initialize the support of each candidate.
let mut supports = <SupportMap<AccountId>>::new();
winners
.iter()
.for_each(|e| { supports.insert(e.clone(), Default::default()); });
// build support struct.
for StakedAssignment { who, distribution } in assignments.iter() {
for (c, weight_extended) in distribution.iter() {
if let Some(support) = supports.get_mut(c) {
support.total = support.total.saturating_add(*weight_extended);
support.voters.push((who.clone(), *weight_extended));
} else {
errors = errors.saturating_add(1);
}
}
}
(supports, errors)
}
/// Evaluate a support map. The returned tuple contains:
///
/// - Minimum support. This value must be **maximized**.
/// - Sum of all supports. This value must be **maximized**.
/// - Sum of all supports squared. This value must be **minimized**.
///
/// `O(E)` where `E` is the total number of edges.
pub fn evaluate_support<AccountId>(
support: &SupportMap<AccountId>,
) -> ElectionScore {
let mut min_support = ExtendedBalance::max_value();
let mut sum: ExtendedBalance = Zero::zero();
// NOTE: The third element might saturate but fine for now since this will run on-chain and need
// to be fast.
let mut sum_squared: ExtendedBalance = Zero::zero();
for (_, support) in support.iter() {
sum = sum.saturating_add(support.total);
let squared = support.total.saturating_mul(support.total);
sum_squared = sum_squared.saturating_add(squared);
if support.total < min_support {
min_support = support.total;
}
}
[min_support, sum, sum_squared]
}
/// 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`.
///
/// Note that the third component should be minimized.
pub fn is_score_better<P: PerThing>(this: ElectionScore, that: ElectionScore, epsilon: P) -> bool
where ExtendedBalance: From<sp_arithmetic::InnerOf<P>>
{
match this
.iter()
.enumerate()
.map(|(i, e)| (
e.ge(&that[i]),
e.tcmp(&that[i], epsilon.mul_ceil(that[i])),
))
.collect::<Vec<(bool, Ordering)>>()
.as_slice()
{
// epsilon better in the score[0], accept.
[(_, Ordering::Greater), _, _] => true,
// less than epsilon better in score[0], but more than epsilon better in the second.
[(true, Ordering::Equal), (_, Ordering::Greater), _] => true,
// less than epsilon better in score[0, 1], but more than epsilon better in the third
[(true, Ordering::Equal), (true, Ordering::Equal), (_, Ordering::Less)] => true,
// anything else is not a good score.
_ => false,
}
}
/// 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.
///
/// 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; }
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; }
}
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_unstable_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;
}
cumulative_stake = cumulative_stake.saturating_add(stake);
}
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));
}
});
difference
}
@@ -0,0 +1,395 @@
// This file is part of Substrate.
// 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
//
// 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(test)]
use crate::{seq_phragmen, ElectionResult, Assignment, VoteWeight, ExtendedBalance};
use sp_arithmetic::{PerThing, traits::{SaturatedConversion, Zero, One}};
use sp_std::collections::btree_map::BTreeMap;
use sp_runtime::assert_eq_error_rate;
#[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<_PhragmenAssignment<A>>,
}
pub(crate) type _PhragmenAssignment<A> = (A, f64);
pub(crate) type _SupportMap<A> = BTreeMap<A, _Support<A>>;
pub(crate) type AccountId = u64;
#[derive(Debug, Clone)]
pub(crate) struct _PhragmenResult<A: Clone> {
pub winners: Vec<(A, ExtendedBalance)>,
pub assignments: Vec<(A, Vec<_PhragmenAssignment<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, FS>(
candidate_count: usize,
minimum_candidate_count: usize,
initial_candidates: Vec<A>,
initial_voters: Vec<(A, Vec<A>)>,
stake_of: FS,
) -> Option<_PhragmenResult<A>> where
A: Default + Ord + Copy,
for<'r> FS: Fn(&'r A) -> VoteWeight,
{
let mut elected_candidates: Vec<(A, ExtendedBalance)>;
let mut assigned: Vec<(A, Vec<_PhragmenAssignment<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>>>();
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());
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(_PhragmenResult {
winners: elected_candidates,
assignments: assigned,
})
}
pub(crate) fn equalize_float<A, FS>(
mut assignments: Vec<(A, Vec<_PhragmenAssignment<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<_PhragmenAssignment<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_unstable_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)])
-> Box<dyn Fn(&AccountId) -> VoteWeight>
{
let mut storage = BTreeMap::<AccountId, VoteWeight>::new();
stakes.iter().for_each(|s| { storage.insert(s.0, s.1); });
let stake_of = move |who: &AccountId| -> VoteWeight { storage.get(who).unwrap().to_owned() };
Box::new(stake_of)
}
pub fn check_assignments_sum<T: PerThing>(assignments: Vec<Assignment<AccountId, T>>) {
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);
}
}
pub(crate) fn run_and_compare<Output: PerThing>(
candidates: Vec<AccountId>,
voters: Vec<(AccountId, Vec<AccountId>)>,
stake_of: &Box<dyn Fn(&AccountId) -> VoteWeight>,
to_elect: usize,
min_to_elect: usize,
) {
// 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<_>>(),
).unwrap();
// run float poc code.
let truth_value = elect_float(
to_elect,
min_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.clone() {
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_fraction(float_assignment.1).deconstruct(),
per_thingy.deconstruct(),
Output::Inner::one(),
);
} else {
panic!("candidate mismatch. This should never happen.")
}
}
} else {
panic!("nominator mismatch. This should never happen.")
}
}
check_assignments_sum(assignments);
}
pub(crate) fn build_support_map_float<FS>(
result: &mut _PhragmenResult<AccountId>,
stake_of: FS,
) -> _SupportMap<AccountId>
where for<'r> FS: Fn(&'r AccountId) -> VoteWeight
{
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
}
@@ -0,0 +1,287 @@
// 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.
//! (very) Basic implementation of a graph node used in the reduce algorithm.
use sp_std::{cell::RefCell, fmt, prelude::*, rc::Rc};
/// The role that a node can accept.
#[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Debug)]
pub(crate) enum NodeRole {
/// A voter. This is synonym to a nominator in a staking context.
Voter,
/// A target. This is synonym to a candidate/validator in a staking context.
Target,
}
pub(crate) type RefCellOf<T> = Rc<RefCell<T>>;
pub(crate) type NodeRef<A> = RefCellOf<Node<A>>;
/// Identifier of a node. This is particularly handy to have a proper `PartialEq` implementation.
/// Otherwise, self votes wouldn't have been indistinguishable.
#[derive(PartialOrd, Ord, Clone, PartialEq, Eq)]
pub(crate) struct NodeId<A> {
/// An account-like identifier representing the node.
pub who: A,
/// The role of the node.
pub role: NodeRole,
}
impl<A> NodeId<A> {
/// Create a new [`NodeId`].
pub fn from(who: A, role: NodeRole) -> Self {
Self { who, role }
}
}
#[cfg(feature = "std")]
impl<A: fmt::Debug> sp_std::fmt::Debug for NodeId<A> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> sp_std::fmt::Result {
write!(
f,
"Node({:?}, {:?})",
self.who,
if self.role == NodeRole::Voter {
"V"
} else {
"T"
}
)
}
}
/// A one-way graph note. This can only store a pointer to its parent.
#[derive(Clone)]
pub(crate) struct Node<A> {
/// The identifier of the note.
pub(crate) id: NodeId<A>,
/// The parent pointer.
pub(crate) parent: Option<NodeRef<A>>,
}
impl<A: PartialEq> PartialEq for Node<A> {
fn eq(&self, other: &Node<A>) -> bool {
self.id == other.id
}
}
impl<A: PartialEq> Eq for Node<A> {}
#[cfg(feature = "std")]
impl<A: fmt::Debug + Clone> fmt::Debug for Node<A> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"({:?} --> {:?})",
self.id,
self.parent.as_ref().map(|p| p.borrow().id.clone())
)
}
}
impl<A: PartialEq + Eq + Clone + fmt::Debug> Node<A> {
/// Create a new [`Node`]
pub fn new(id: NodeId<A>) -> Node<A> {
Self { id, parent: None }
}
/// Returns true if `other` is the parent of `who`.
pub fn is_parent_of(who: &NodeRef<A>, other: &NodeRef<A>) -> bool {
if who.borrow().parent.is_none() {
return false;
}
who.borrow().parent.as_ref() == Some(other)
}
/// Removes the parent of `who`.
pub fn remove_parent(who: &NodeRef<A>) {
who.borrow_mut().parent = None;
}
/// Sets `who`'s parent to be `parent`.
pub fn set_parent_of(who: &NodeRef<A>, parent: &NodeRef<A>) {
who.borrow_mut().parent = Some(parent.clone());
}
/// Finds the root of `start`. It return a tuple of `(root, root_vec)` where `root_vec` is the
/// vector of Nodes leading to the root. Hence the first element is the start itself and the
/// last one is the root. As convenient, the root itself is also returned as the first element
/// of the tuple.
///
/// This function detects cycles and breaks as soon a duplicate node is visited, returning the
/// cycle up to but not including the duplicate node.
///
/// If you are certain that no cycles exist, you can use [`root_unchecked`].
pub fn root(start: &NodeRef<A>) -> (NodeRef<A>, Vec<NodeRef<A>>) {
let mut parent_path: Vec<NodeRef<A>> = Vec::new();
let mut visited: Vec<NodeRef<A>> = Vec::new();
parent_path.push(start.clone());
visited.push(start.clone());
let mut current = start.clone();
while let Some(ref next_parent) = current.clone().borrow().parent {
if visited.contains(next_parent) {
break;
}
parent_path.push(next_parent.clone());
current = next_parent.clone();
visited.push(current.clone());
}
(current, parent_path)
}
/// Consumes self and wraps it in a `Rc<RefCell<T>>`. This type can be used as the pointer type
/// to a parent node.
pub fn into_ref(self) -> NodeRef<A> {
Rc::from(RefCell::from(self))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn id(i: u32) -> NodeId<u32> {
NodeId::from(i, NodeRole::Target)
}
#[test]
fn basic_create_works() {
let node = Node::new(id(10));
assert_eq!(
node,
Node {
id: NodeId {
who: 10,
role: NodeRole::Target
},
parent: None
}
);
}
#[test]
fn set_parent_works() {
let a = Node::new(id(10)).into_ref();
let b = Node::new(id(20)).into_ref();
assert_eq!(a.borrow().parent, None);
Node::set_parent_of(&a, &b);
assert_eq!(*a.borrow().parent.as_ref().unwrap(), b);
}
#[test]
fn get_root_singular() {
let a = Node::new(id(1)).into_ref();
assert_eq!(Node::root(&a), (a.clone(), vec![a.clone()]));
}
#[test]
fn get_root_works() {
// D <-- A <-- B <-- C
// \
// <-- E
let a = Node::new(id(1)).into_ref();
let b = Node::new(id(2)).into_ref();
let c = Node::new(id(3)).into_ref();
let d = Node::new(id(4)).into_ref();
let e = Node::new(id(5)).into_ref();
let f = Node::new(id(6)).into_ref();
Node::set_parent_of(&c, &b);
Node::set_parent_of(&b, &a);
Node::set_parent_of(&e, &a);
Node::set_parent_of(&a, &d);
assert_eq!(
Node::root(&e),
(d.clone(), vec![e.clone(), a.clone(), d.clone()]),
);
assert_eq!(Node::root(&a), (d.clone(), vec![a.clone(), d.clone()]),);
assert_eq!(
Node::root(&c),
(d.clone(), vec![c.clone(), b.clone(), a.clone(), d.clone()]),
);
// D A <-- B <-- C
// F <-- / \
// <-- E
Node::set_parent_of(&a, &f);
assert_eq!(Node::root(&a), (f.clone(), vec![a.clone(), f.clone()]),);
assert_eq!(
Node::root(&c),
(f.clone(), vec![c.clone(), b.clone(), a.clone(), f.clone()]),
);
}
#[test]
fn get_root_on_cycle() {
// A ---> B
// | |
// <---- C
let a = Node::new(id(1)).into_ref();
let b = Node::new(id(2)).into_ref();
let c = Node::new(id(3)).into_ref();
Node::set_parent_of(&a, &b);
Node::set_parent_of(&b, &c);
Node::set_parent_of(&c, &a);
let (root, path) = Node::root(&a);
assert_eq!(root, c);
assert_eq!(path.clone(), vec![a.clone(), b.clone(), c.clone()]);
}
#[test]
fn get_root_on_cycle_2() {
// A ---> B
// | | |
// - C
let a = Node::new(id(1)).into_ref();
let b = Node::new(id(2)).into_ref();
let c = Node::new(id(3)).into_ref();
Node::set_parent_of(&a, &b);
Node::set_parent_of(&b, &c);
Node::set_parent_of(&c, &b);
let (root, path) = Node::root(&a);
assert_eq!(root, c);
assert_eq!(path.clone(), vec![a.clone(), b.clone(), c.clone()]);
}
#[test]
fn node_cmp_stack_overflows_on_non_unique_elements() {
// To make sure we don't stack overflow on duplicate who. This needs manual impl of
// PartialEq.
let a = Node::new(id(1)).into_ref();
let b = Node::new(id(2)).into_ref();
let c = Node::new(id(3)).into_ref();
Node::set_parent_of(&a, &b);
Node::set_parent_of(&b, &c);
Node::set_parent_of(&c, &a);
Node::root(&a);
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff