Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Implement PJR checker #8160

Merged
38 commits merged into from
Mar 11, 2021
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
58f4b61
Apply.
kianenigma Aug 31, 2020
202981b
Merge remote-tracking branch 'origin/master' into prgn-npos-pjr
coriolinus Feb 18, 2021
12f4636
get rid of glob import
coriolinus Feb 18, 2021
834e675
use meaningful generic type name
coriolinus Feb 18, 2021
963f48c
pjr_check operates on `Supports` struct used elsewhere
coriolinus Feb 19, 2021
81a788a
improve algorithmic complexity of `prepare_pjr_input`
coriolinus Feb 19, 2021
dd83c66
fix rustdoc warnings
coriolinus Feb 19, 2021
0e15efb
improve module docs
coriolinus Feb 19, 2021
46a519d
typo
coriolinus Feb 19, 2021
d63ed93
simplify debug assertion
coriolinus Feb 19, 2021
158d01a
add test finding the phase-change threshold value for a constructed s…
coriolinus Feb 19, 2021
902c2df
add more threshold scenarios to disambiguate plausible interpretations
coriolinus Feb 19, 2021
4cff663
add link to npos paper reference
coriolinus Feb 19, 2021
c47eba6
docs: staked_assignment -> supports
coriolinus Feb 19, 2021
a129534
add utility method for generating npos inputs
coriolinus Feb 22, 2021
8049fcf
add a fuzzer which asserts that all unbalanced seq_phragmen are PJR
coriolinus Feb 22, 2021
2451a86
assert in all cases, not just debug
coriolinus Feb 22, 2021
1d4e2db
leverage a native solution to choose candidates
coriolinus Feb 22, 2021
fc64ff5
use existing helper methods
coriolinus Feb 22, 2021
f2f11bb
add pjr-check and incorporate into the fuzzer
coriolinus Feb 22, 2021
8d63aa4
fix compilation errors
coriolinus Feb 23, 2021
ba36529
Enable manually setting iteration parameters in single run.
coriolinus Feb 23, 2021
32da70d
update comment verbiage for accuracy
coriolinus Feb 25, 2021
6419d17
it is valid in PJR for an elected candidate to have 0 support
coriolinus Feb 25, 2021
e582b14
Fix phragmen_pjr fuzzer
coriolinus Mar 1, 2021
2f977df
restart ci; it appears to be stalled
coriolinus Mar 1, 2021
8cda3af
use necessary import for no-std
coriolinus Mar 1, 2021
9f928e3
use a more realistic distribution of voters and candidates
coriolinus Mar 2, 2021
9c7fb95
identify specifically which PJR check may fail
coriolinus Mar 2, 2021
06c0e8a
move candidate collection comment into correct place
coriolinus Mar 2, 2021
cabce8f
standard_threshold: use a calculation method which cannot overflow
coriolinus Mar 2, 2021
b761631
Apply suggestions from code review (update comments)
coriolinus Mar 2, 2021
eb94375
clarify the effectiveness bounds for t-pjr check
coriolinus Mar 2, 2021
30d83da
Merge remote-tracking branch 'origin/master' into prgn-npos-pjr
coriolinus Mar 3, 2021
cf6a96b
how to spell "committee"
coriolinus Mar 3, 2021
44c73ef
reorganize: high -> low abstraction
coriolinus Mar 3, 2021
f1721e0
ensure standard threshold calc cannot panic
coriolinus Mar 3, 2021
a98208f
Apply suggestions from code review
coriolinus Mar 11, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frame/staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2709,8 +2709,8 @@ impl<T: Config> Module<T> {
// write new results.
<QueuedElected<T>>::put(ElectionResult {
elected_stashes: winners,
compute,
exposures,
compute,
});
QueuedScore::put(submitted_score);

Expand Down
14 changes: 10 additions & 4 deletions primitives/npos-elections/fuzzer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ publish = false
targets = ["x86_64-unknown-linux-gnu"]

[dependencies]
sp-npos-elections = { version = "3.0.0", path = ".." }
sp-std = { version = "3.0.0", path = "../../std" }
sp-runtime = { version = "3.0.0", path = "../../runtime" }
codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] }
honggfuzz = "0.5"
rand = { version = "0.7.3", features = ["std", "small_rng"] }
codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] }
sp-arithmetic = { version = "3.0.0", path = "../../arithmetic" }
sp-npos-elections = { version = "3.0.0", path = ".." }
sp-runtime = { version = "3.0.0", path = "../../runtime" }
sp-std = { version = "3.0.0", path = "../../std" }
structopt = "0.3.21"

[[bin]]
name = "reduce"
Expand All @@ -36,3 +38,7 @@ path = "src/phragmms_balancing.rs"
[[bin]]
name = "compact"
path = "src/compact.rs"

[[bin]]
name = "phragmen_pjr"
path = "src/phragmen_pjr.rs"
125 changes: 98 additions & 27 deletions primitives/npos-elections/fuzzer/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
// 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 rand::{self, seq::SliceRandom, Rng, RngCore};
use sp_npos_elections::{phragmms, seq_phragmen, ElectionResult, VoteWeight};
use sp_runtime::Perbill;
use rand::{self, Rng, RngCore};
use std::collections::{BTreeMap, HashSet};

/// converts x into the range [a, b] in a pseudo-fair way.
pub fn to_range(x: usize, a: usize, b: usize) -> usize {
Expand All @@ -39,11 +39,81 @@ pub fn to_range(x: usize, a: usize, b: usize) -> usize {

pub enum ElectionType {
Phragmen(Option<(usize, u128)>),
Phragmms(Option<(usize, u128)>)
Phragmms(Option<(usize, u128)>),
}

pub type AccountId = u64;

/// Generate a set of inputs suitable for fuzzing an election algorithm
///
/// Given parameters governing how many candidates and voters should exist, generates a voting
/// scenario suitable for fuzz-testing an election algorithm.
///
/// The returned candidate list is sorted. This sorting property should not affect the result of the
/// calculation.
///
/// The returned voters list is sorted. This enables binary searching for a particular voter by
/// account id. This sorting property should not affect the results of the calculation.
///
/// Each voter's selection of candidates to vote for is sorted.
///
/// Note that this does not generate balancing parameters.
kianenigma marked this conversation as resolved.
Show resolved Hide resolved
pub fn generate_random_npos_inputs(
candidate_count: usize,
voter_count: usize,
mut rng: impl Rng,
) -> (
usize,
Vec<AccountId>,
Vec<(AccountId, VoteWeight, Vec<AccountId>)>,
) {
// cache for fast generation of unique candidate and voter ids
let mut used_ids = HashSet::with_capacity(candidate_count + voter_count);

// always generate a sensible desired number of candidates: elections are uninteresting if we
// desire 0 candidates, or a number of candidates >= the actual number of candidates present
let rounds = rng.gen_range(1, candidate_count);

// candidates are easy: just a completely random set of IDs
let mut candidates: Vec<AccountId> = Vec::with_capacity(candidate_count);
for _ in 0..candidate_count {
coriolinus marked this conversation as resolved.
Show resolved Hide resolved
let mut id = rng.gen();
// insert returns `false` when the value was already present
while !used_ids.insert(id) {
id = rng.gen();
}
candidates.push(id);
}
candidates.sort_unstable();
candidates.dedup();
assert_eq!(candidates.len(), candidate_count);

let mut voters = Vec::with_capacity(voter_count);
for _ in 0..voter_count {
let mut id = rng.gen();
// insert returns `false` when the value was already present
while !used_ids.insert(id) {
kianenigma marked this conversation as resolved.
Show resolved Hide resolved
id = rng.gen();
}

let vote_weight = rng.gen();

// it's not interesting if a voter chooses 0 or all candidates, so rule those cases out.
let n_candidates_chosen = rng.gen_range(1, candidates.len());

let mut chosen_candidates = Vec::with_capacity(n_candidates_chosen);
chosen_candidates.extend(candidates.choose_multiple(&mut rng, n_candidates_chosen));
chosen_candidates.sort();
voters.push((id, vote_weight, chosen_candidates));
}

voters.sort_unstable();
voters.dedup_by_key(|(id, _weight, _chosen_candidates)| *id);
assert_eq!(voters.len(), voter_count);

(rounds, candidates, voters)
}

pub fn generate_random_npos_result(
voter_count: u64,
target_count: u64,
Expand Down Expand Up @@ -71,40 +141,41 @@ pub fn generate_random_npos_result(
});

let mut voters = Vec::with_capacity(voter_count as usize);
(prefix ..= (prefix + voter_count)).for_each(|acc| {
(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 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(),
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,
Expand Down
118 changes: 118 additions & 0 deletions primitives/npos-elections/fuzzer/src/phragmen_pjr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// This file is part of Substrate.

// Copyright (C) 2020-2021 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 which ensures that running unbalanced sequential phragmen always produces a result
//! which satisfies our PJR checker.
//!
//! ## Running a single iteration
//!
//! Honggfuzz shuts down each individual loop iteration after a configurable time limit.
//! It can be helpful to run a single iteration on your hardware to help benchmark how long that time
//! limit should reasonably be. Simply run the program without the `fuzzing` configuration to run a
//! single iteration: `cargo run --bin phragmen_pjr`.
//!
//! ## Running
//!
//! Run with `HFUZZ_RUN_ARGS="-t 10" cargo hfuzz run phragmen_pjr`.
//!
//! Note the environment variable: by default, `cargo hfuzz` shuts down each iteration after 1 second
//! of runtime. We significantly increase that to ensure that the fuzzing gets a chance to complete.
//! Running a single iteration can help determine an appropriate value for this parameter.
//!
//! ## Debugging a panic
//!
//! Once a panic is found, it can be debugged with
//! `HFUZZ_RUN_ARGS="-t 10" cargo hfuzz run-debug phragmen_pjr hfuzz_workspace/phragmen_pjr/*.fuzz`.
//!

#[cfg(fuzzing)]
use honggfuzz::fuzz;

#[cfg(not(fuzzing))]
use structopt::StructOpt;

mod common;
use common::{generate_random_npos_inputs, to_range};
use rand::{self, SeedableRng};
use sp_npos_elections::{pjr_check_core, seq_phragmen_core, setup_inputs, standard_threshold};

type AccountId = u64;

const MIN_CANDIDATES: usize = 5;
const MAX_CANDIDATES: usize = 200;
const MIN_VOTERS: usize = 5;
const MAX_VOTERS: usize = 500;
coriolinus marked this conversation as resolved.
Show resolved Hide resolved

#[cfg(fuzzing)]
fn main() {
loop {
fuzz!(|data: (usize, usize, u64)| {
let (candidate_count, voter_count, seed) = data;
iteration(candidate_count, voter_count, seed);
});
}
}

#[cfg(not(fuzzing))]
#[derive(Debug, StructOpt)]
struct Opt {
/// How many candidates participate in this election
#[structopt(short, long)]
candidates: Option<usize>,

/// How many voters participate in this election
#[structopt(short, long)]
voters: Option<usize>,

/// Random seed to use in this election
#[structopt(long)]
seed: Option<u64>,
}

#[cfg(not(fuzzing))]
fn main() {
let opt = Opt::from_args();
// candidates and voters by default use the maxima, which turn out to be one less than
// the constant.
iteration(
opt.candidates.unwrap_or(MAX_CANDIDATES - 1),
opt.voters.unwrap_or(MAX_VOTERS - 1),
opt.seed.unwrap_or_default(),
);
}

fn iteration(mut candidate_count: usize, mut voter_count: usize, seed: u64) {
let rng = rand::rngs::SmallRng::seed_from_u64(seed);
candidate_count = to_range(candidate_count, MIN_CANDIDATES, MAX_CANDIDATES);
voter_count = to_range(voter_count, MIN_VOTERS, MAX_VOTERS);

let (rounds, candidates, voters) =
generate_random_npos_inputs(candidate_count, voter_count, rng);

let (candidates, voters) = setup_inputs(candidates, voters);

// Run seq-phragmen
let (candidates, voters) = seq_phragmen_core::<AccountId>(rounds, candidates, voters)
.expect("seq_phragmen must succeed");

let threshold = standard_threshold(rounds, voters.iter().map(|voter| voter.budget()));

assert!(
pjr_check_core(&candidates, &voters, threshold),
"unbalanced sequential phragmen must satisfy PJR",
);
}
Loading