Clean Phragmén Equlise API (#5452)

* Clean phragmen API and equalise()

* Stabilize new api

* Fix phragmen fuzzers

* More fixes

* Make fuzzers reproducible

* improvements

* Make equalize update assignments as well.

* total function for staked_assignment.

* Fix fuzzer build

* remvoe TODO

* Fix a bunch more.

* clean stray debug stuff

* Update primitives/phragmen/src/lib.rs

Co-Authored-By: thiolliere <gui.thiolliere@gmail.com>

* fix range function

* fix number generator

Co-authored-by: thiolliere <gui.thiolliere@gmail.com>
This commit is contained in:
Kian Paimani
2020-04-17 09:53:30 +02:00
committed by GitHub
parent 0fd5643e84
commit 762c741c55
17 changed files with 574 additions and 309 deletions
@@ -0,0 +1,29 @@
// Copyright 2020 Parity Technologies (UK) Ltd.
// This file is part of Substrate.
// Substrate is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Substrate is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
//! Common fuzzing utils.
/// 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
assert!(b > 2 * a);
let collapsed = x % b;
if collapsed >= a {
collapsed
} else {
collapsed + a
}
}
@@ -0,0 +1,146 @@
// Copyright 2020 Parity Technologies (UK) Ltd.
// This file is part of Substrate.
// Substrate is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Substrate is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
//! Fuzzing fro the equalize 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_phragmen::{
equalize, assignment_ratio_to_staked, build_support_map, to_without_backing, elect,
PhragmenResult, 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,
) -> (PhragmenResult<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));
});
(
elect::<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 (PhragmenResult { 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.clone(), &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 = equalize(
&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(initial_score, final_score);
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)
});
}
}
@@ -14,6 +14,12 @@
// You should have received a copy of the GNU General Public License
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
//! Fuzzing for the reduce algorithm.
//!
//! It that reduce always return a new set og edges in which the bound is kept (`edges_after <= m +
//! n,`) and the result must effectively be the same, meaning that the same support map should be
//! computable from both.
//!
//! # Running
//!
//! Run with `cargo hfuzz run reduce`. `honggfuzz`.
@@ -24,8 +30,11 @@
//! `cargo hfuzz run-debug reduce hfuzz_workspace/reduce/*.fuzz`.
use honggfuzz::fuzz;
mod common;
use common::to_range;
use sp_phragmen::{StakedAssignment, ExtendedBalance, build_support_map, reduce};
use rand::{self, Rng};
use rand::{self, Rng, SeedableRng, RngCore};
type Balance = u128;
type AccountId = u64;
@@ -35,15 +44,20 @@ const KSM: Balance = 1_000_000_000_000;
fn main() {
loop {
fuzz!(|_data: _| {
let (assignments, winners) = generate_random_phragmen_assignment(
rr(100, 1000),
rr(100, 2000),
8,
8,
);
fuzz!(|data: (usize, usize, u64)| {
let (mut voter_count, mut target_count, seed) = data;
let rng = rand::rngs::SmallRng::seed_from_u64(seed);
target_count = to_range(target_count, 100, 1000);
voter_count = to_range(voter_count, 100, 2000);
let (assignments, winners) = generate_random_phragmen_assignment(
voter_count,
target_count,
8,
8,
rng
);
reduce_and_compare(&assignments, &winners);
});
});
}
}
@@ -52,13 +66,10 @@ fn generate_random_phragmen_assignment(
target_count: usize,
avg_edge_per_voter: usize,
edge_per_voter_var: usize,
mut rng: impl RngCore,
) -> (Vec<StakedAssignment<AccountId>>, Vec<AccountId>) {
// random in range of (a, b)
let rr_128 = |a: u128, b: u128| -> u128 { rand::thread_rng().gen_range(a, b) };
// prefix to distinguish the voter and target account ranges.
let target_prefix = 1_000_000;
// let target_prefix = 1000;
assert!(voter_count < target_prefix);
let mut assignments = Vec::with_capacity(voter_count as usize);
@@ -70,17 +81,17 @@ fn generate_random_phragmen_assignment(
(1..=voter_count).for_each(|acc| {
let mut targets_to_chose_from = all_targets.clone();
let targets_to_chose = if edge_per_voter_var > 0 { rr(
let targets_to_chose = if edge_per_voter_var > 0 { rng.gen_range(
avg_edge_per_voter - edge_per_voter_var,
avg_edge_per_voter + edge_per_voter_var,
) } else { avg_edge_per_voter };
let distribution = (0..targets_to_chose).map(|_| {
let target = targets_to_chose_from.remove(rr(0, targets_to_chose_from.len()));
let target = targets_to_chose_from.remove(rng.gen_range(0, targets_to_chose_from.len()));
if winners.iter().find(|w| **w == target).is_none() {
winners.push(target.clone());
}
(target, rr_128(1 * KSM, 100 * KSM))
(target, rng.gen_range(1 * KSM, 100 * KSM))
}).collect::<Vec<(AccountId, ExtendedBalance)>>();
assignments.push(StakedAssignment {
@@ -139,7 +150,3 @@ fn assignment_len(assignments: &[StakedAssignment<AccountId>]) -> u32 {
assignments.iter().for_each(|x| x.distribution.iter().for_each(|_| counter += 1));
counter
}
fn rr(a: usize, b: usize) -> usize {
rand::thread_rng().gen_range(a, b)
}