From a6b5d1d15512644d8fabe0174c2eb845c8b2b8aa Mon Sep 17 00:00:00 2001 From: Kian Paimani <5588131+kianenigma@users.noreply.github.com> Date: Fri, 13 Sep 2019 08:41:33 +0200 Subject: [PATCH] Move phragmen benchmarks out of Staking (#3588) * Move phragmen benches to.. phragmen. * Move some basic phragmen tests to.. phragmen. * Line-width * Add phragmen equ implementation as flot * Add phragmen equ implementation as flot * Add mock and test file. --- substrate/.gitlab-ci.yml | 2 +- substrate/Cargo.lock | 3 +- substrate/core/phragmen/Cargo.toml | 2 + .../phragmen/benches/phragmen.rs} | 178 ++++---- substrate/core/phragmen/src/lib.rs | 228 +--------- substrate/core/phragmen/src/mock.rs | 419 ++++++++++++++++++ substrate/core/phragmen/src/tests.rs | 135 ++++++ substrate/srml/staking/Cargo.toml | 2 - substrate/srml/staking/src/lib.rs | 12 +- substrate/srml/staking/src/tests.rs | 54 +-- 10 files changed, 661 insertions(+), 374 deletions(-) rename substrate/{srml/staking/src/benches.rs => core/phragmen/benches/phragmen.rs} (53%) create mode 100644 substrate/core/phragmen/src/mock.rs create mode 100644 substrate/core/phragmen/src/tests.rs diff --git a/substrate/.gitlab-ci.yml b/substrate/.gitlab-ci.yml index b67d5eb648..60a75d5db4 100644 --- a/substrate/.gitlab-ci.yml +++ b/substrate/.gitlab-ci.yml @@ -101,7 +101,7 @@ cargo-check-benches: stage: test <<: *docker-env script: - - BUILD_DUMMY_WASM_BINARY=1 time cargo check --benches --all + - BUILD_DUMMY_WASM_BINARY=1 time cargo +nightly check --benches --all - sccache -s diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock index c1c6311395..f53a4a1acc 100644 --- a/substrate/Cargo.lock +++ b/substrate/Cargo.lock @@ -4264,7 +4264,6 @@ name = "srml-staking" version = "2.0.0" dependencies = [ "parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "safe-mix 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", @@ -5101,6 +5100,8 @@ dependencies = [ name = "substrate-phragmen" version = "2.0.0" dependencies = [ + "rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "sr-io 2.0.0", "sr-primitives 2.0.0", "sr-std 2.0.0", "srml-support 2.0.0", diff --git a/substrate/core/phragmen/Cargo.toml b/substrate/core/phragmen/Cargo.toml index 335bddb401..52f061b862 100644 --- a/substrate/core/phragmen/Cargo.toml +++ b/substrate/core/phragmen/Cargo.toml @@ -9,7 +9,9 @@ sr-primitives = { path = "../sr-primitives", default-features = false } rstd = { package = "sr-std", path = "../sr-std", default-features = false } [dev-dependencies] +runtime-io ={ package = "sr-io", path = "../sr-io" } support = { package = "srml-support", path = "../../srml/support" } +rand = "0.7.0" [features] default = ["std"] diff --git a/substrate/srml/staking/src/benches.rs b/substrate/core/phragmen/benches/phragmen.rs similarity index 53% rename from substrate/srml/staking/src/benches.rs rename to substrate/core/phragmen/benches/phragmen.rs index e9c3667984..dfe56aafd8 100644 --- a/substrate/srml/staking/src/benches.rs +++ b/substrate/core/phragmen/benches/phragmen.rs @@ -16,27 +16,35 @@ //! Note that execution times will not be accurate in an absolute scale, since //! - Everything is executed in the context of `TestExternalities` //! - Everything is executed in native environment. -//! -//! Run using: -//! -//! ```zsh -//! cargo bench --features bench --color always -//! ``` +#![feature(test)] + +extern crate test; use test::Bencher; -use runtime_io::with_externalities; -use mock::*; -use super::*; -use phragmen; + use rand::{self, Rng}; +extern crate substrate_phragmen as phragmen; +use phragmen::{Support, SupportMap, ACCURACY}; + +use std::collections::BTreeMap; +use sr_primitives::traits::{Convert, SaturatedConversion}; const VALIDATORS: u64 = 1000; const NOMINATORS: u64 = 10_000; const EDGES: u64 = 2; const TO_ELECT: usize = 100; -const STAKE: u64 = 1000; +const STAKE: Balance = 1000; -type C = ::CurrencyToVote; +type Balance = u128; +type AccountId = u64; + +pub struct TestCurrencyToVote; +impl Convert for TestCurrencyToVote { + fn convert(x: Balance) -> u64 { x.saturated_into() } +} +impl Convert for TestCurrencyToVote { + fn convert(x: u128) -> Balance { x.saturated_into() } +} fn do_phragmen( b: &mut Bencher, @@ -47,84 +55,89 @@ fn do_phragmen( eq_iters: usize, _eq_tolerance: u128, ) { - with_externalities(&mut ExtBuilder::default().nominate(false).build(), || { - assert!(num_vals > votes_per); - let rr = |a, b| rand::thread_rng().gen_range(a as usize, b as usize) as u64; + assert!(num_vals > votes_per); + let rr = |a, b| rand::thread_rng().gen_range(a as usize, b as usize) as Balance; - // prefix to distinguish the validator and nominator account ranges. - let np = 10_000; + // prefix to distinguish the validator and nominator account ranges. + let np = 10_000; - (1 ..= 2*num_vals) - .step_by(2) - .for_each(|acc| bond_validator(acc, STAKE + rr(10, 50))); + let mut candidates = vec![]; + let mut voters = vec![]; + let mut slashable_balance_of: BTreeMap = BTreeMap::new(); - (np ..= (np + 2*num_noms)) - .step_by(2) - .for_each(|acc| { - let mut stashes_to_vote = (1 ..= 2*num_vals) - .step_by(2) - .map(|ctrl| ctrl + 1) - .collect::>(); - let votes = (0 .. votes_per) - .map(|_| { - stashes_to_vote.remove(rr(0, stashes_to_vote.len()) as usize) - }) - .collect::>(); - bond_nominator(acc, STAKE + rr(10, 50), votes); - }); + (1 ..= num_vals) + .for_each(|acc| { + candidates.push(acc); + slashable_balance_of.insert(acc, STAKE + rr(10, 50)); + }); - b.iter(|| { - let r = phragmen::elect::<_, _, _, ::CurrencyToVote>( - count, - 1_usize, - >::enumerate().map(|(who, _)| who).collect::>(), - >::enumerate().collect(), - Staking::slashable_balance_of, - true, - ).unwrap(); + (np ..= (np + num_noms)) + .for_each(|acc| { + let mut stashes_to_vote = candidates.clone(); + let votes = (0 .. votes_per) + .map(|_| { + stashes_to_vote.remove(rr(0, stashes_to_vote.len()) as usize) + }) + .collect::>(); + voters.push((acc, votes)); + slashable_balance_of.insert(acc, STAKE + rr(10, 50)); + }); - // Do the benchmarking with equalize. - if eq_iters > 0 { - let elected_stashes = r.winners; - let mut assignments = r.assignments; + let slashable_balance = |who: &AccountId| -> Balance { + *slashable_balance_of.get(who).unwrap() + }; - let to_votes = |b: Balance| - as Convert>::convert(b) as ExtendedBalance; - let ratio_of = |b, r: ExtendedBalance| r.saturating_mul(to_votes(b)) / ACCURACY; + b.iter(|| { + let r = phragmen::elect::( + count, + 1_usize, + candidates.clone(), + voters.clone(), + slashable_balance, + true, + ).unwrap(); - // Initialize the support of each candidate. - let mut supports = >::new(); - elected_stashes - .iter() - .map(|e| (e, to_votes(Staking::slashable_balance_of(e)))) - .for_each(|(e, s)| { - let item = Support { own: s, total: s, ..Default::default() }; - supports.insert(e.clone(), item); - }); + // Do the benchmarking with equalize. + if eq_iters > 0 { + let elected_stashes = r.winners; + let mut assignments = r.assignments; - for (n, assignment) in assignments.iter_mut() { - for (c, r) in assignment.iter_mut() { - let nominator_stake = Staking::slashable_balance_of(n); - let other_stake = ratio_of(nominator_stake, *r); - if let Some(support) = supports.get_mut(c) { - support.total = support.total.saturating_add(other_stake); - support.others.push((n.clone(), other_stake)); - } - *r = other_stake; + let to_votes = |b: Balance| + >::convert(b) as u128; + let ratio_of = |b, r: u128| r.saturating_mul(to_votes(b)) / ACCURACY; + + // Initialize the support of each candidate. + let mut supports = >::new(); + elected_stashes + .iter() + .map(|e| (e, to_votes(slashable_balance(e)))) + .for_each(|(e, s)| { + let item = Support { own: s, total: s, ..Default::default() }; + supports.insert(e.clone(), item); + }); + + for (n, assignment) in assignments.iter_mut() { + for (c, r) in assignment.iter_mut() { + let nominator_stake = slashable_balance(n); + let other_stake = ratio_of(nominator_stake, *r); + if let Some(support) = supports.get_mut(c) { + support.total = support.total.saturating_add(other_stake); + support.others.push((n.clone(), other_stake)); } + *r = other_stake; } - - let tolerance = 0_u128; - let iterations = 2_usize; - phragmen::equalize::<_, _, ::CurrencyToVote, _>( - assignments, - &mut supports, - tolerance, - iterations, - Staking::slashable_balance_of, - ); } - }) + + let tolerance = 0_u128; + let iterations = 2_usize; + phragmen::equalize::<_, _, _, TestCurrencyToVote>( + assignments, + &mut supports, + tolerance, + iterations, + slashable_balance, + ); + } }) } @@ -134,11 +147,10 @@ macro_rules! phragmen_benches { #[bench] fn $name(b: &mut Bencher) { let (v, n, t, e, eq_iter, eq_tol) = $tup; - println!(""); + println!("----------------------"); println!( - r#" -++ Benchmark: {} Validators // {} Nominators // {} Edges-per-nominator // {} total edges // -electing {} // Equalize: {} iterations -- {} tolerance"#, + "++ Benchmark: {} Validators // {} Nominators // {} Edges-per-nominator // {} \ + total edges // electing {} // Equalize: {} iterations -- {} tolerance", v, n, e, e * n, t, eq_iter, eq_tol, ); do_phragmen(b, v, n, t, e, eq_iter, eq_tol); diff --git a/substrate/core/phragmen/src/lib.rs b/substrate/core/phragmen/src/lib.rs index 459254370b..da3e923424 100644 --- a/substrate/core/phragmen/src/lib.rs +++ b/substrate/core/phragmen/src/lib.rs @@ -37,6 +37,9 @@ use rstd::{prelude::*, collections::btree_map::BTreeMap}; use sr_primitives::PerU128; use sr_primitives::traits::{Zero, Convert, Member, SimpleArithmetic}; +mod mock; +mod tests; + /// Type used as the fraction. type Fraction = PerU128; @@ -355,7 +358,7 @@ pub fn elect( /// * `tolerance`: maximum difference that can occur before an early quite happens. /// * `iterations`: maximum number of iterations that will be processed. /// * `stake_of`: something that can return the stake stake of a particular candidate or voter. -pub fn equalize( +pub fn equalize( mut assignments: Vec<(AccountId, Vec>)>, supports: &mut SupportMap, tolerance: ExtendedBalance, @@ -489,226 +492,3 @@ fn do_equalize( difference } - -#[cfg(test)] -mod tests { - use super::{elect, ACCURACY, PhragmenResult}; - use sr_primitives::traits::{Convert, Member, SaturatedConversion}; - use rstd::collections::btree_map::BTreeMap; - use support::assert_eq_uvec; - - pub struct C; - impl Convert for C { - fn convert(x: u64) -> u64 { x } - } - impl Convert for C { - fn convert(x: u128) -> u64 { x.saturated_into() } - } - - #[derive(Default, Debug)] - struct _Candidate { - who: AccountId, - score: f64, - approval_stake: f64, - elected: bool, - } - - #[derive(Default, Debug)] - struct _Voter { - who: AccountId, - edges: Vec<_Edge>, - budget: f64, - load: f64, - } - - #[derive(Default, Debug)] - struct _Edge { - who: AccountId, - load: f64, - candidate_index: usize, - } - - type _PhragmenAssignment = (AccountId, f64); - - #[derive(Debug)] - pub struct _PhragmenResult { - pub winners: Vec, - pub assignments: Vec<(AccountId, Vec<_PhragmenAssignment>)> - } - - pub fn elect_poc( - candidate_count: usize, - minimum_candidate_count: usize, - initial_candidates: Vec, - initial_voters: Vec<(AccountId, Vec)>, - stake_of: FS, - self_vote: bool, - ) -> Option<_PhragmenResult> where - AccountId: Default + Ord + Member + Copy, - for<'r> FS: Fn(&'r AccountId) -> u64, - { - let mut elected_candidates: Vec; - let mut assigned: Vec<(AccountId, Vec<_PhragmenAssignment>)>; - let mut c_idx_cache = BTreeMap::::new(); - let num_voters = initial_candidates.len() + initial_voters.len(); - let mut voters: Vec<_Voter> = Vec::with_capacity(num_voters); - - let mut candidates = if self_vote { - initial_candidates.into_iter().map(|who| { - let stake = stake_of(&who) as f64; - _Candidate { who, approval_stake: stake, ..Default::default() } - }) - .filter(|c| c.approval_stake != 0f64) - .enumerate() - .map(|(i, c)| { - let who = c.who; - voters.push(_Voter { - who: who.clone(), - edges: vec![ - _Edge { who: who.clone(), candidate_index: i, ..Default::default() } - ], - budget: c.approval_stake, - load: 0f64, - }); - c_idx_cache.insert(c.who.clone(), i); - c - }) - .collect::>>() - } else { - initial_candidates.into_iter() - .enumerate() - .map(|(idx, who)| { - c_idx_cache.insert(who.clone(), idx); - _Candidate { who, ..Default::default() } - }) - .collect::>>() - }; - - 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> = 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(rstd::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()); - } 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().find(|c| *c == e.who) { - if c != n.who { - let ratio = e.load / n.load; - assignment.1.push((e.who.clone(), ratio)); - } - } - } - assigned.push(assignment); - } - - Some(_PhragmenResult { - winners: elected_candidates, - assignments: assigned, - }) - } - - #[test] - fn float_poc_works() { - let candidates = vec![1, 2, 3]; - let voters = vec![ - (10, vec![1, 2]), - (20, vec![1, 3]), - (30, vec![2, 3]), - ]; - let stake_of = |x: &u64| { if *x >= 10 { *x } else { 0 }}; - let _PhragmenResult { winners, assignments } = - elect_poc(2, 2, candidates, voters, stake_of, false).unwrap(); - - assert_eq_uvec!(winners, vec![2, 3]); - assert_eq_uvec!( - assignments, - vec![ - (10, vec![(2, 1.0)]), - (20, vec![(3, 1.0)]), - (30, vec![(2, 0.5), (3, 0.5)]) - ] - ); - } - - #[test] - fn phragmen_works() { - let candidates = vec![1, 2, 3]; - let voters = vec![ - (10, vec![1, 2]), - (20, vec![1, 3]), - (30, vec![2, 3]), - ]; - let stake_of = |x: &u64| { if *x >= 10 { *x } else { 0 }}; - let PhragmenResult { winners, assignments } = - elect::<_, _, _, C>(2, 2, candidates, voters, stake_of, false).unwrap(); - - assert_eq_uvec!(winners, vec![2, 3]); - assert_eq_uvec!( - assignments, - vec![ - (10, vec![(2, ACCURACY)]), - (20, vec![(3, ACCURACY)]), - (30, vec![(2, ACCURACY/2), (3, ACCURACY/2)]) - ] - ); - } -} diff --git a/substrate/core/phragmen/src/mock.rs b/substrate/core/phragmen/src/mock.rs new file mode 100644 index 0000000000..659ff9dacb --- /dev/null +++ b/substrate/core/phragmen/src/mock.rs @@ -0,0 +1,419 @@ +// Copyright 2019 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 . + +//! Mock file for phragmen. + +#![cfg(test)] + +use crate::{elect, ACCURACY, PhragmenResult}; +use sr_primitives::traits::{Convert, Member, SaturatedConversion}; +use rstd::collections::btree_map::BTreeMap; +use support::assert_eq_error_rate; + +pub(crate) struct TestCurrencyToVote; +impl Convert for TestCurrencyToVote { + fn convert(x: Balance) -> u64 { x.saturated_into() } +} +impl Convert for TestCurrencyToVote { + fn convert(x: u128) -> Balance { x } +} + +#[derive(Default, Debug)] +pub(crate) struct _Candidate { + who: A, + score: f64, + approval_stake: f64, + elected: bool, +} + +#[derive(Default, Debug)] +pub(crate) struct _Voter { + who: A, + edges: Vec<_Edge>, + budget: f64, + load: f64, +} + +#[derive(Default, Debug)] +pub(crate) struct _Edge { + who: A, + load: f64, + candidate_index: usize, +} + +#[derive(Default, Debug, PartialEq)] +pub(crate) struct _Support { + pub own: f64, + pub total: f64, + pub others: Vec<_PhragmenAssignment>, +} + +pub(crate) type _PhragmenAssignment = (A, f64); +pub(crate) type _SupportMap = BTreeMap>; + +pub(crate) type Balance = u128; +pub(crate) type AccountId = u64; + +#[derive(Debug, Clone)] +pub(crate) struct _PhragmenResult { + pub winners: Vec, + pub assignments: Vec<(A, Vec<_PhragmenAssignment>)> +} + +pub(crate) fn elect_float( + candidate_count: usize, + minimum_candidate_count: usize, + initial_candidates: Vec, + initial_voters: Vec<(A, Vec)>, + stake_of: FS, + self_vote: bool, +) -> Option<_PhragmenResult> where + A: Default + Ord + Member + Copy, + for<'r> FS: Fn(&'r A) -> Balance, +{ + let mut elected_candidates: Vec; + let mut assigned: Vec<(A, Vec<_PhragmenAssignment>)>; + let mut c_idx_cache = BTreeMap::::new(); + let num_voters = initial_candidates.len() + initial_voters.len(); + let mut voters: Vec<_Voter> = Vec::with_capacity(num_voters); + + let mut candidates = if self_vote { + initial_candidates.into_iter().map(|who| { + let stake = stake_of(&who) as f64; + _Candidate { who, approval_stake: stake, ..Default::default() } + }) + .filter(|c| c.approval_stake != 0f64) + .enumerate() + .map(|(i, c)| { + let who = c.who; + voters.push(_Voter { + who: who.clone(), + edges: vec![ + _Edge { who: who.clone(), candidate_index: i, ..Default::default() } + ], + budget: c.approval_stake, + load: 0f64, + }); + c_idx_cache.insert(c.who.clone(), i); + c + }) + .collect::>>() + } else { + initial_candidates.into_iter() + .enumerate() + .map(|(idx, who)| { + c_idx_cache.insert(who.clone(), idx); + _Candidate { who, ..Default::default() } + }) + .collect::>>() + }; + + 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> = 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(rstd::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()); + } 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().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( + mut assignments: Vec<(A, Vec<_PhragmenAssignment>)>, + supports: &mut _SupportMap, + tolerance: f64, + iterations: usize, + stake_of: FS, +) where + for<'r> FS: Fn(&'r A) -> Balance, + 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( + voter: &A, + budget_balance: Balance, + elected_edges: &mut Vec<_PhragmenAssignment>, + support_map: &mut _SupportMap, + 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::>(); + + 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(rstd::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(rstd::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; + }); + + // todo: rewrite. + elected_edges.sort_unstable_by(|x, y| + if let Some(x) = support_map.get(&x.0) { + if let Some(y) = support_map.get(&y.0) { + x.total.partial_cmp(&y.total).unwrap_or(rstd::cmp::Ordering::Equal) + } else { + rstd::cmp::Ordering::Equal + } + } else { + rstd::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, Balance)]) + -> Box Balance> +{ + let mut storage = BTreeMap::::new(); + stakes.iter().for_each(|s| { storage.insert(s.0, s.1); }); + let stake_of = move |who: &AccountId| -> Balance { storage.get(who).unwrap().to_owned() }; + Box::new(stake_of) +} + +pub(crate) fn run_and_compare( + candidates: Vec, + voters: Vec<(AccountId, Vec)>, + stake_of: Box Balance>, + to_elect: usize, + min_to_elect: usize, + self_vote: bool, +) { + // run fixed point code. + let PhragmenResult { winners, assignments } = elect::<_, _, _, TestCurrencyToVote>( + to_elect, + min_to_elect, + candidates.clone(), + voters.clone(), + &stake_of, + self_vote, + ).unwrap(); + + // run float poc code. + let truth_value = elect_float( + to_elect, + min_to_elect, + candidates, + voters, + &stake_of, + self_vote, + ).unwrap(); + + assert_eq!(winners, truth_value.winners); + + for (nominator, assigned) in assignments.clone() { + if let Some(float_assignments) = truth_value.assignments.iter().find(|x| x.0 == nominator) { + for (candidate, ratio) in assigned { + if let Some(float_assignment) = float_assignments.1.iter().find(|x| x.0 == candidate ) { + assert_eq_error_rate!((float_assignment.1 * ACCURACY as f64).round() as u128, ratio, 1); + } else { + panic!("candidate mismatch. This should never happen.") + } + } + } else { + panic!("nominator mismatch. This should never happen.") + } + } +} + +pub(crate) fn build_support_map( + result: &mut _PhragmenResult, + stake_of: FS, +) -> _SupportMap + where for<'r> FS: Fn(&'r AccountId) -> Balance +{ + let mut supports = <_SupportMap>::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 +} diff --git a/substrate/core/phragmen/src/tests.rs b/substrate/core/phragmen/src/tests.rs new file mode 100644 index 0000000000..5a8cddb984 --- /dev/null +++ b/substrate/core/phragmen/src/tests.rs @@ -0,0 +1,135 @@ +// Copyright 2019 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 . + +//! Tests for phragmen. + +#![cfg(test)] + +use crate::mock::*; +use crate::{elect, ACCURACY, PhragmenResult}; +use support::assert_eq_uvec; + +#[test] +fn float_phragmen_poc_works() { + let candidates = vec![1, 2, 3]; + let voters = vec![ + (10, vec![1, 2]), + (20, vec![1, 3]), + (30, vec![2, 3]), + ]; + let stake_of = create_stake_of(&[(10, 10), (20, 20), (30, 30), (1, 0), (2, 0), (3, 0)]); + let mut phragmen_result = elect_float(2, 2, candidates, voters, &stake_of, false).unwrap(); + let winners = phragmen_result.clone().winners; + let assignments = phragmen_result.clone().assignments; + + assert_eq_uvec!(winners, vec![2, 3]); + assert_eq_uvec!( + assignments, + vec![ + (10, vec![(2, 1.0)]), + (20, vec![(3, 1.0)]), + (30, vec![(2, 0.5), (3, 0.5)]), + ] + ); + + let mut support_map = build_support_map(&mut phragmen_result, &stake_of); + + assert_eq!( + support_map.get(&2).unwrap(), + &_Support { own: 0.0, total: 25.0, others: vec![(10u64, 10.0), (30u64, 15.0)]} + ); + assert_eq!( + support_map.get(&3).unwrap(), + &_Support { own: 0.0, total: 35.0, others: vec![(20u64, 20.0), (30u64, 15.0)]} + ); + + equalize_float(phragmen_result.assignments, &mut support_map, 0.0, 2, stake_of); + + assert_eq!( + support_map.get(&2).unwrap(), + &_Support { own: 0.0, total: 30.0, others: vec![(10u64, 10.0), (30u64, 20.0)]} + ); + assert_eq!( + support_map.get(&3).unwrap(), + &_Support { own: 0.0, total: 30.0, others: vec![(20u64, 20.0), (30u64, 10.0)]} + ); +} + +#[test] +fn phragmen_poc_works() { + let candidates = vec![1, 2, 3]; + let voters = vec![ + (10, vec![1, 2]), + (20, vec![1, 3]), + (30, vec![2, 3]), + ]; + + let PhragmenResult { winners, assignments } = elect::<_, _, _, TestCurrencyToVote>( + 2, + 2, + candidates, + voters, + create_stake_of(&[(10, 10), (20, 20), (30, 30)]), + false, + ).unwrap(); + + assert_eq_uvec!(winners, vec![2, 3]); + assert_eq_uvec!( + assignments, + vec![ + (10, vec![(2, ACCURACY)]), + (20, vec![(3, ACCURACY)]), + (30, vec![(2, ACCURACY/2), (3, ACCURACY/2)]), + ] + ); +} + +#[test] +fn phragmen_poc_2_works() { + let candidates = vec![10, 20, 30]; + let voters = vec![ + (2, vec![10, 20, 30]), + (4, vec![10, 20, 40]), + ]; + let stake_of = create_stake_of(&[ + (10, 1000), + (20, 1000), + (30, 1000), + (40, 1000), + (2, 500), + (4, 500), + ]); + + run_and_compare(candidates, voters, stake_of, 2, 2, true); +} + +#[test] +fn phragmen_poc_3_works() { + let candidates = vec![10, 20, 30]; + let voters = vec![ + (2, vec![10, 20, 30]), + (4, vec![10, 20, 40]), + ]; + let stake_of = create_stake_of(&[ + (10, 1000), + (20, 1000), + (30, 1000), + (2, 50), + (4, 1000), + ]); + + run_and_compare(candidates, voters, stake_of, 2, 2, true); +} diff --git a/substrate/srml/staking/Cargo.toml b/substrate/srml/staking/Cargo.toml index 93de95caa2..092bcfda8e 100644 --- a/substrate/srml/staking/Cargo.toml +++ b/substrate/srml/staking/Cargo.toml @@ -23,11 +23,9 @@ authorship = { package = "srml-authorship", path = "../authorship", default-feat primitives = { package = "substrate-primitives", path = "../../core/primitives" } balances = { package = "srml-balances", path = "../balances" } timestamp = { package = "srml-timestamp", path = "../timestamp" } -rand = "0.6.5" [features] equalize = [] -bench = [] default = ["std", "equalize"] std = [ "serde", diff --git a/substrate/srml/staking/src/lib.rs b/substrate/srml/staking/src/lib.rs index 3a77e89223..dbd265b263 100644 --- a/substrate/srml/staking/src/lib.rs +++ b/substrate/srml/staking/src/lib.rs @@ -243,22 +243,14 @@ #![recursion_limit="128"] #![cfg_attr(not(feature = "std"), no_std)] -#![cfg_attr(all(feature = "bench", test), feature(test))] -#[cfg(all(feature = "bench", test))] -extern crate test; - -#[cfg(any(feature = "bench", test))] +#[cfg(test)] mod mock; - #[cfg(test)] mod tests; pub mod inflation; -#[cfg(all(feature = "bench", test))] -mod benches; - use rstd::{prelude::*, result}; use codec::{HasCompact, Encode, Decode}; use support::{ @@ -1295,7 +1287,7 @@ impl Module { if cfg!(feature = "equalize") { let tolerance = 0_u128; let iterations = 2_usize; - equalize::<_, _, T::CurrencyToVote, _>( + equalize::<_, _, _, T::CurrencyToVote>( assignments, &mut supports, tolerance, diff --git a/substrate/srml/staking/src/tests.rs b/substrate/srml/staking/src/tests.rs index 2f3f94bfbf..d754cd8500 100644 --- a/substrate/srml/staking/src/tests.rs +++ b/substrate/srml/staking/src/tests.rs @@ -1307,7 +1307,7 @@ fn phragmen_poc_works() { // 40 has load 0 and supported // 40 with stake 0 - // Sequential Phragmén with post processing gives + // Sequential Phragmén with post processing gives // 10 is elected with stake 1500.0 and score 0.0005 // 20 is elected with stake 1500.0 and score 0.00075 // @@ -1410,58 +1410,6 @@ fn phragmen_poc_works() { }); } -#[test] -fn phragmen_poc_2_works() { - // tests the encapsulated phragmen::elect function. - // Votes [ - // ('10', 1000, ['10']), - // ('20', 1000, ['20']), - // ('30', 1000, ['30']), - // ('2', 50, ['10', '20']), - // ('4', 1000, ['10', '30']) - // ] - // Sequential Phragmén gives - // 10 is elected with stake 1705.7377049180327 and score 0.0004878048780487805 - // 30 is elected with stake 1344.2622950819673 and score 0.0007439024390243903 - with_externalities(&mut ExtBuilder::default().nominate(false).build(), || { - // initial setup of 10 and 20, both validators - assert_eq_uvec!(validator_controllers(), vec![20, 10]); - - // Bond [30, 31] as the third validator - assert_ok!(Staking::bond_extra(Origin::signed(31), 999)); - assert_ok!(Staking::validate(Origin::signed(30), ValidatorPrefs::default())); - - // bond [2,1](A), [4,3](B), as 2 nominators - for i in &[1, 3] { let _ = Balances::deposit_creating(i, 2000); } - - assert_ok!(Staking::bond(Origin::signed(1), 2, 50, RewardDestination::default())); - assert_ok!(Staking::nominate(Origin::signed(2), vec![11, 21])); - - assert_ok!(Staking::bond(Origin::signed(3), 4, 1000, RewardDestination::default())); - assert_ok!(Staking::nominate(Origin::signed(4), vec![11, 31])); - - let results = phragmen::elect::<_, _, _, ::CurrencyToVote>( - 2, - Staking::minimum_validator_count() as usize, - >::enumerate().map(|(who, _)| who).collect::>(), - >::enumerate().collect(), - Staking::slashable_balance_of, - true, - ); - - let phragmen::PhragmenResult { winners, assignments } = results.unwrap(); - - // 10 and 30 must be the winners - assert_eq!(winners, vec![11, 31]); - assert_eq!(assignments, vec![ - (3, vec![(11, 2816371998), (31, 1478595298)]), - (1, vec![(11, 4294967296)]), - ]); - check_exposure_all(); - check_nominator_all(); - }) -} - #[test] fn switching_roles() { // Test that it should be possible to switch between roles (nominator, validator, idle) with minimal overhead.