PhragMMS election. (#6685)

* Revamp npos-elections and implement phragmms

* Update primitives/npos-elections/src/phragmms.rs

* Fix build

* Some review grumbles

* Add some stuff for remote testing

* fix some of the grumbles.

* Add remote testing stuff.

* Cleanup

* fix docs

* Update primitives/arithmetic/src/rational.rs

Co-authored-by: Dan Forbes <dan@danforbes.dev>

* Small config change

* Better handling of approval_stake == 0

* Final touhces.

* Clean fuzzer a bit

* Clean fuzzer a bit

* Update primitives/npos-elections/src/balancing.rs

Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>

* Fix fuzzer.

* Better api for normalize

* Add noramlize_up

* A large number of small fixes.

* make it merge ready

* Fix warns

* bump

* Fix fuzzers a bit.

* Fix warns as well.

* Fix more tests.

Co-authored-by: Dan Forbes <dan@danforbes.dev>
Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>
This commit is contained in:
Kian Paimani
2020-09-23 10:16:10 +02:00
committed by GitHub
parent ecdc94420e
commit 313f86ec23
32 changed files with 2074 additions and 914 deletions
@@ -1,155 +0,0 @@
// This file is part of Substrate.
// Copyright (C) 2020 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Fuzzing fro the balance_solution algorithm
//!
//! It ensures that any solution which gets equalized will lead into a better or equally scored
//! one.
mod common;
use common::to_range;
use honggfuzz::fuzz;
use sp_npos_elections::{
balance_solution, assignment_ratio_to_staked, build_support_map, to_without_backing, seq_phragmen,
ElectionResult, VoteWeight, evaluate_support, is_score_better,
};
use sp_std::collections::btree_map::BTreeMap;
use sp_runtime::Perbill;
use rand::{self, Rng, SeedableRng, RngCore};
type AccountId = u64;
fn generate_random_phragmen_result(
voter_count: u64,
target_count: u64,
to_elect: usize,
edge_per_voter: u64,
mut rng: impl RngCore,
) -> (ElectionResult<AccountId, Perbill>, BTreeMap<AccountId, VoteWeight>) {
let prefix = 100_000;
// Note, it is important that stakes are always bigger than ed and
let base_stake: u64 = 1_000_000_000;
let ed: u64 = base_stake;
let mut candidates = Vec::with_capacity(target_count as usize);
let mut stake_of_tree: BTreeMap<AccountId, VoteWeight> = BTreeMap::new();
(1..=target_count).for_each(|acc| {
candidates.push(acc);
let stake_var = rng.gen_range(ed, 100 * ed);
stake_of_tree.insert(acc, base_stake + stake_var);
});
let mut voters = Vec::with_capacity(voter_count as usize);
(prefix ..= (prefix + voter_count)).for_each(|acc| {
// all possible targets
let mut all_targets = candidates.clone();
// we remove and pop into `targets` `edge_per_voter` times.
let targets = (0..edge_per_voter).map(|_| {
let upper = all_targets.len() - 1;
let idx = rng.gen_range(0, upper);
all_targets.remove(idx)
})
.collect::<Vec<AccountId>>();
let stake_var = rng.gen_range(ed, 100 * ed) ;
let stake = base_stake + stake_var;
stake_of_tree.insert(acc, stake);
voters.push((acc, stake, targets));
});
(
seq_phragmen::<AccountId, sp_runtime::Perbill>(
to_elect,
0,
candidates,
voters,
).unwrap(),
stake_of_tree,
)
}
fn main() {
loop {
fuzz!(|data: (usize, usize, usize, usize, usize, u64)| {
let (
mut target_count,
mut voter_count,
mut iterations,
mut edge_per_voter,
mut to_elect,
seed,
) = data;
let rng = rand::rngs::SmallRng::seed_from_u64(seed);
target_count = to_range(target_count, 50, 2000);
voter_count = to_range(voter_count, 50, 1000);
iterations = to_range(iterations, 1, 20);
to_elect = to_range(to_elect, 25, target_count);
edge_per_voter = to_range(edge_per_voter, 1, target_count);
println!("++ [{} / {} / {} / {}]", voter_count, target_count, to_elect, iterations);
let (ElectionResult { winners, assignments }, stake_of_tree) = generate_random_phragmen_result(
voter_count as u64,
target_count as u64,
to_elect,
edge_per_voter as u64,
rng,
);
let stake_of = |who: &AccountId| -> VoteWeight {
*stake_of_tree.get(who).unwrap()
};
let mut staked = assignment_ratio_to_staked(assignments, &stake_of);
let winners = to_without_backing(winners);
let mut support = build_support_map(winners.as_ref(), staked.as_ref()).0;
let initial_score = evaluate_support(&support);
if initial_score[0] == 0 {
// such cases cannot be improved by reduce.
return;
}
let i = balance_solution(
&mut staked,
&mut support,
10,
iterations,
);
let final_score = evaluate_support(&support);
if final_score[0] == initial_score[0] {
// such solutions can only be improved by such a tiny fiction that it is most often
// wrong due to rounding errors.
return;
}
let enhance = is_score_better(final_score, initial_score, Perbill::zero());
println!(
"iter = {} // {:?} -> {:?} [{}]",
i,
initial_score,
final_score,
enhance,
);
// if more than one iteration has been done, or they must be equal.
assert!(enhance || initial_score == final_score || i == 0)
});
}
}
@@ -17,6 +17,14 @@
//! Common fuzzing utils.
// Each function will be used based on which fuzzer binary is being used.
#![allow(dead_code)]
use sp_npos_elections::{ElectionResult, VoteWeight, phragmms, seq_phragmen};
use sp_std::collections::btree_map::BTreeMap;
use sp_runtime::Perbill;
use rand::{self, Rng, RngCore};
/// converts x into the range [a, b] in a pseudo-fair way.
pub fn to_range(x: usize, a: usize, b: usize) -> usize {
// does not work correctly if b < 2 * a
@@ -28,3 +36,78 @@ pub fn to_range(x: usize, a: usize, b: usize) -> usize {
collapsed + a
}
}
pub enum ElectionType {
Phragmen(Option<(usize, u128)>),
Phragmms(Option<(usize, u128)>)
}
pub type AccountId = u64;
pub fn generate_random_npos_result(
voter_count: u64,
target_count: u64,
to_elect: usize,
mut rng: impl RngCore,
election_type: ElectionType,
) -> (
ElectionResult<AccountId, Perbill>,
Vec<AccountId>,
Vec<(AccountId, VoteWeight, Vec<AccountId>)>,
BTreeMap<AccountId, VoteWeight>,
) {
let prefix = 100_000;
// Note, it is important that stakes are always bigger than ed.
let base_stake: u64 = 1_000_000_000_000;
let ed: u64 = base_stake;
let mut candidates = Vec::with_capacity(target_count as usize);
let mut stake_of: BTreeMap<AccountId, VoteWeight> = BTreeMap::new();
(1..=target_count).for_each(|acc| {
candidates.push(acc);
let stake_var = rng.gen_range(ed, 100 * ed);
stake_of.insert(acc, base_stake + stake_var);
});
let mut voters = Vec::with_capacity(voter_count as usize);
(prefix ..= (prefix + voter_count)).for_each(|acc| {
let edge_per_this_voter = rng.gen_range(1, candidates.len());
// all possible targets
let mut all_targets = candidates.clone();
// we remove and pop into `targets` `edge_per_this_voter` times.
let targets = (0..edge_per_this_voter).map(|_| {
let upper = all_targets.len() - 1;
let idx = rng.gen_range(0, upper);
all_targets.remove(idx)
})
.collect::<Vec<AccountId>>();
let stake_var = rng.gen_range(ed, 100 * ed) ;
let stake = base_stake + stake_var;
stake_of.insert(acc, stake);
voters.push((acc, stake, targets));
});
(
match election_type {
ElectionType::Phragmen(conf) =>
seq_phragmen::<AccountId, sp_runtime::Perbill>(
to_elect,
candidates.clone(),
voters.clone(),
conf,
).unwrap(),
ElectionType::Phragmms(conf) =>
phragmms::<AccountId, sp_runtime::Perbill>(
to_elect,
candidates.clone(),
voters.clone(),
conf,
).unwrap(),
},
candidates,
voters,
stake_of,
)
}
@@ -0,0 +1,117 @@
// This file is part of Substrate.
// Copyright (C) 2020 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Fuzzing for sequential phragmen with potential balancing.
mod common;
use common::*;
use honggfuzz::fuzz;
use sp_npos_elections::{
assignment_ratio_to_staked_normalized, build_support_map, to_without_backing, VoteWeight,
evaluate_support, is_score_better, seq_phragmen,
};
use sp_runtime::Perbill;
use rand::{self, SeedableRng};
fn main() {
loop {
fuzz!(|data: (usize, usize, usize, usize, u64)| {
let (
mut target_count,
mut voter_count,
mut iterations,
mut to_elect,
seed,
) = data;
let rng = rand::rngs::SmallRng::seed_from_u64(seed);
target_count = to_range(target_count, 100, 200);
voter_count = to_range(voter_count, 100, 200);
iterations = to_range(iterations, 0, 30);
to_elect = to_range(to_elect, 25, target_count);
println!(
"++ [voter_count: {} / target_count:{} / to_elect:{} / iterations:{}]",
voter_count, target_count, to_elect, iterations,
);
let (
unbalanced,
candidates,
voters,
stake_of_tree,
) = generate_random_npos_result(
voter_count as u64,
target_count as u64,
to_elect,
rng,
ElectionType::Phragmen(None),
);
let stake_of = |who: &AccountId| -> VoteWeight {
*stake_of_tree.get(who).unwrap()
};
let unbalanced_score = {
let staked = assignment_ratio_to_staked_normalized(unbalanced.assignments.clone(), &stake_of).unwrap();
let winners = to_without_backing(unbalanced.winners.clone());
let support = build_support_map(winners.as_ref(), staked.as_ref()).unwrap();
let score = evaluate_support(&support);
if score[0] == 0 {
// such cases cannot be improved by balancing.
return;
}
score
};
if iterations > 0 {
let balanced = seq_phragmen::<AccountId, sp_runtime::Perbill>(
to_elect,
candidates,
voters,
Some((iterations, 0)),
).unwrap();
let balanced_score = {
let staked = assignment_ratio_to_staked_normalized(balanced.assignments.clone(), &stake_of).unwrap();
let winners = to_without_backing(balanced.winners);
let support = build_support_map(winners.as_ref(), staked.as_ref()).unwrap();
evaluate_support(&support)
};
let enhance = is_score_better(balanced_score, unbalanced_score, Perbill::zero());
println!(
"iter = {} // {:?} -> {:?} [{}]",
iterations,
unbalanced_score,
balanced_score,
enhance,
);
// The only guarantee of balancing is such that the first and third element of the score
// cannot decrease.
assert!(
balanced_score[0] >= unbalanced_score[0] &&
balanced_score[1] == unbalanced_score[1] &&
balanced_score[2] <= unbalanced_score[2]
);
}
});
}
}
@@ -0,0 +1,115 @@
// This file is part of Substrate.
// Copyright (C) 2020 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Fuzzing for phragmms.
mod common;
use common::*;
use honggfuzz::fuzz;
use sp_npos_elections::{
assignment_ratio_to_staked_normalized, build_support_map, to_without_backing, VoteWeight,
evaluate_support, is_score_better, phragmms,
};
use sp_runtime::Perbill;
use rand::{self, SeedableRng};
fn main() {
loop {
fuzz!(|data: (usize, usize, usize, usize, u64)| {
let (
mut target_count,
mut voter_count,
mut iterations,
mut to_elect,
seed,
) = data;
let rng = rand::rngs::SmallRng::seed_from_u64(seed);
target_count = to_range(target_count, 100, 200);
voter_count = to_range(voter_count, 100, 200);
iterations = to_range(iterations, 5, 30);
to_elect = to_range(to_elect, 25, target_count);
println!(
"++ [voter_count: {} / target_count:{} / to_elect:{} / iterations:{}]",
voter_count, target_count, to_elect, iterations,
);
let (
unbalanced,
candidates,
voters,
stake_of_tree,
) = generate_random_npos_result(
voter_count as u64,
target_count as u64,
to_elect,
rng,
ElectionType::Phragmms(None),
);
let stake_of = |who: &AccountId| -> VoteWeight {
*stake_of_tree.get(who).unwrap()
};
let unbalanced_score = {
let staked = assignment_ratio_to_staked_normalized(unbalanced.assignments.clone(), &stake_of).unwrap();
let winners = to_without_backing(unbalanced.winners.clone());
let support = build_support_map(winners.as_ref(), staked.as_ref()).unwrap();
let score = evaluate_support(&support);
if score[0] == 0 {
// such cases cannot be improved by balancing.
return;
}
score
};
let balanced = phragmms::<AccountId, sp_runtime::Perbill>(
to_elect,
candidates,
voters,
Some((iterations, 0)),
).unwrap();
let balanced_score = {
let staked = assignment_ratio_to_staked_normalized(balanced.assignments.clone(), &stake_of).unwrap();
let winners = to_without_backing(balanced.winners);
let support = build_support_map(winners.as_ref(), staked.as_ref()).unwrap();
evaluate_support(&support)
};
let enhance = is_score_better(balanced_score, unbalanced_score, Perbill::zero());
println!(
"iter = {} // {:?} -> {:?} [{}]",
iterations,
unbalanced_score,
balanced_score,
enhance,
);
// The only guarantee of balancing is such that the first and third element of the score
// cannot decrease.
assert!(
balanced_score[0] >= unbalanced_score[0] &&
balanced_score[1] == unbalanced_score[1] &&
balanced_score[2] <= unbalanced_score[2]
);
});
}
}
@@ -110,8 +110,8 @@ fn assert_assignments_equal(
ass2: &Vec<StakedAssignment<AccountId>>,
) {
let (support_1, _) = build_support_map::<AccountId>(winners, ass1);
let (support_2, _) = build_support_map::<AccountId>(winners, ass2);
let support_1 = build_support_map::<AccountId>(winners, ass1).unwrap();
let support_2 = build_support_map::<AccountId>(winners, ass2).unwrap();
for (who, support) in support_1.iter() {
assert_eq!(support.total, support_2.get(who).unwrap().total);