This repository has been archived by the owner on Nov 15, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add a fuzzer which can validate
Compact::encoded_size_for
The `Compact` solution type is generated distinctly for each runtime, and has both three type parameters and a built-in limit to the number of candidates that each voter can vote for. Finally, they have an optional `#[compact]` attribute which changes the encoding behavior. The assignment truncation algorithm we're using depends on the ability to efficiently and accurately determine how much space a `Compact` solution will take once encoded. Together, these two facts imply that simple unit tests are not sufficient to validate the behavior of `Compact::encoded_size_for`. This commit adds such a fuzzer. It is designed such that it is possible to add a new fuzzer to the family by simply adjusting the `generate_solution_type` macro invocation as desired, and making a few minor documentation edits. Of course, the fuzzer still fails for now: the generated implementation for `encoded_size_for` is still `unimplemented!()`. However, once the macro is updated appropriately, this fuzzer family should allow us to gain confidence in the correctness of the generated code.
- Loading branch information
1 parent
919c98d
commit 9160387
Showing
5 changed files
with
328 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
[package] | ||
name = "sp-npos-elections-compact-fuzzer" | ||
version = "2.0.0-alpha.5" | ||
authors = ["Parity Technologies <admin@parity.io>"] | ||
edition = "2018" | ||
license = "Apache-2.0" | ||
homepage = "https://substrate.dev" | ||
repository = "https://github.com/paritytech/substrate/" | ||
description = "Fuzzer for sp-npos-elections-compact macro" | ||
publish = false | ||
|
||
[dependencies] | ||
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"] } | ||
structopt = "0.3.21" | ||
|
||
sp-npos-elections = { version = "3.0.0", path = "../.." } | ||
sp-runtime = { version = "3.0.0", path = "../../../runtime" } | ||
|
||
[[bin]] | ||
name = "compact_32_16_16" | ||
path = "src/compact_32_16_16.rs" | ||
|
||
# [[bin]] | ||
# name = "compact_24" | ||
# path = "src/compact24.rs" | ||
|
||
# [[bin]] | ||
# name = "non_compact_16" | ||
# path = "src/noncompact16.rs" | ||
|
||
# [[bin]] | ||
# name = "non_compact_24" | ||
# path = "src/noncompact24.rs" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
// 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. | ||
|
||
//! Common fuzzing utils. | ||
use rand::{self, seq::SliceRandom, Rng}; | ||
use std::{ | ||
collections::{HashSet, HashMap}, | ||
convert::TryInto, | ||
hash::Hash, | ||
}; | ||
|
||
pub type AccountId = u64; | ||
/// The candidate mask allows easy disambiguation between voters and candidates: accounts | ||
/// for which this bit is set are candidates, and without it, are voters. | ||
pub const CANDIDATE_MASK: AccountId = 1 << ((std::mem::size_of::<AccountId>() * 8) - 1); | ||
pub type CandidateId = AccountId; | ||
|
||
pub type Accuracy = sp_runtime::Perbill; | ||
|
||
pub type Assignment = sp_npos_elections::Assignment<AccountId, Accuracy>; | ||
pub type Voter = (AccountId, sp_npos_elections::VoteWeight, Vec<AccountId>); | ||
|
||
/// 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 | ||
} | ||
} | ||
|
||
/// Generate voter and assignment lists. Makes no attempt to be realistic about winner or assignment fairness. | ||
/// | ||
/// Maintains these invariants: | ||
/// | ||
/// - candidate ids have `CANDIDATE_MASK` bit set | ||
/// - voter ids do not have `CANDIDATE_MASK` bit set | ||
/// - assignments have the same ordering as voters | ||
/// - `assignments.distribution.iter().map(|(_, frac)| frac).sum() == One::one()` | ||
/// - a coherent set of winners is chosen. | ||
/// - the winner set is a subset of the candidate set. | ||
/// - `assignments.distribution.iter().all(|(who, _)| winners.contains(who))` | ||
pub fn generate_random_votes( | ||
candidate_count: usize, | ||
voter_count: usize, | ||
mut rng: impl Rng, | ||
) -> (Vec<Voter>, Vec<Assignment>, Vec<CandidateId>) { | ||
// cache for fast generation of unique candidate and voter ids | ||
let mut used_ids = HashSet::with_capacity(candidate_count + voter_count); | ||
|
||
// candidates are easy: just a completely random set of IDs | ||
let mut candidates: Vec<AccountId> = Vec::with_capacity(candidate_count); | ||
while candidates.len() < candidate_count { | ||
let mut new = || rng.gen::<AccountId>() | CANDIDATE_MASK; | ||
let mut id = new(); | ||
// insert returns `false` when the value was already present | ||
while !used_ids.insert(id) { | ||
id = new(); | ||
} | ||
candidates.push(id); | ||
} | ||
|
||
// voters are random ids, random weights, random selection from the candidates | ||
let mut voters = Vec::with_capacity(voter_count); | ||
while voters.len() < voter_count { | ||
let mut new = || rng.gen::<AccountId>() & !CANDIDATE_MASK; | ||
let mut id = new(); | ||
// insert returns `false` when the value was already present | ||
while !used_ids.insert(id) { | ||
id = new(); | ||
} | ||
|
||
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)); | ||
voters.push((id, vote_weight, chosen_candidates)); | ||
} | ||
|
||
// always generate a sensible number of winners: elections are uninteresting if nobody wins, | ||
// or everybody wins | ||
let num_winners = rng.gen_range(1, candidate_count); | ||
let mut winners: HashSet<AccountId> = HashSet::with_capacity(num_winners); | ||
winners.extend(candidates.choose_multiple(&mut rng, num_winners)); | ||
assert_eq!(winners.len(), num_winners); | ||
|
||
let mut assignments = Vec::with_capacity(voters.len()); | ||
for (voter_id, _, votes) in voters.iter() { | ||
let chosen_winners = votes.iter().filter(|vote| winners.contains(vote)).cloned(); | ||
let num_chosen_winners = chosen_winners.clone().count(); | ||
|
||
// distribute the available stake randomly | ||
let stake_distribution = if num_chosen_winners == 0 { | ||
Vec::new() | ||
} else { | ||
let mut available_stake = 1000; | ||
let mut stake_distribution = Vec::with_capacity(num_chosen_winners); | ||
for _ in 0..num_chosen_winners - 1 { | ||
let stake = rng.gen_range(0, available_stake); | ||
stake_distribution.push(Accuracy::from_perthousand(stake)); | ||
available_stake -= stake; | ||
} | ||
stake_distribution.push(Accuracy::from_perthousand(available_stake)); | ||
stake_distribution.shuffle(&mut rng); | ||
stake_distribution | ||
}; | ||
|
||
assignments.push(Assignment { | ||
who: *voter_id, | ||
distribution: chosen_winners.zip(stake_distribution).collect(), | ||
}); | ||
} | ||
|
||
(voters, assignments, candidates) | ||
} | ||
|
||
fn generate_cache<Voters, Item>(voters: Voters) -> HashMap<Item, usize> | ||
where | ||
Voters: Iterator<Item = Item>, | ||
Item: Hash + Eq + Copy, | ||
{ | ||
let mut cache = HashMap::new(); | ||
for (idx, voter_id) in voters.enumerate() { | ||
cache.insert(voter_id, idx); | ||
} | ||
cache | ||
} | ||
|
||
/// Create a function that returns the index of a voter in the voters list. | ||
pub fn make_voter_fn<VoterIndex>(voters: &[Voter]) -> impl Fn(&AccountId) -> Option<VoterIndex> | ||
where | ||
usize: TryInto<VoterIndex>, | ||
{ | ||
let cache = generate_cache(voters.iter().map(|(id, _, _)| *id)); | ||
move |who| cache.get(who).cloned().and_then(|i| i.try_into().ok()) | ||
} | ||
|
||
/// Create a function that returns the index of a candidate in the candidates list. | ||
pub fn make_target_fn<TargetIndex>( | ||
candidates: &[CandidateId], | ||
) -> impl Fn(&CandidateId) -> Option<TargetIndex> | ||
where | ||
usize: TryInto<TargetIndex>, | ||
{ | ||
let cache = generate_cache(candidates.iter().cloned()); | ||
move |who| cache.get(who).cloned().and_then(|i| i.try_into().ok()) | ||
} |
112 changes: 112 additions & 0 deletions
112
primitives/npos-elections/compact/fuzzer/src/compact_32_16_16.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
// 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 the generated implementation of `encoded_size_for` always accurately | ||
//! predicts the correct encoded size. | ||
//! | ||
//! ## Running a single iteration | ||
//! | ||
//! Simply run the program without the `fuzzing` configuration to run a single iteration: | ||
//! `cargo run --bin compact_16`. | ||
//! | ||
//! ## Running | ||
//! | ||
//! Run with `cargo hfuzz run compact_16`. | ||
//! | ||
//! ## Debugging a panic | ||
//! | ||
//! Once a panic is found, it can be debugged with | ||
//! `cargo hfuzz run-debug compact_16 hfuzz_workspace/compact_16/*.fuzz`. | ||
use _npos::CompactSolution; | ||
use codec::Encode; | ||
#[cfg(fuzzing)] | ||
use honggfuzz::fuzz; | ||
|
||
#[cfg(not(fuzzing))] | ||
use structopt::StructOpt; | ||
|
||
mod common; | ||
|
||
use common::{Accuracy, generate_random_votes, make_target_fn, make_voter_fn, to_range}; | ||
use rand::{self, SeedableRng}; | ||
|
||
sp_npos_elections::generate_solution_type!( | ||
#[compact] | ||
pub struct Compact::<VoterIndex = u32, TargetIndex = u16, Accuracy = Accuracy>(16) | ||
); | ||
|
||
const MIN_CANDIDATES: usize = 250; | ||
const MAX_CANDIDATES: usize = 1000; | ||
const MIN_VOTERS: usize = 500; | ||
const MAX_VOTERS: usize = 2500; | ||
|
||
#[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 (voters, assignments, candidates) = | ||
generate_random_votes(candidate_count, voter_count, rng); | ||
|
||
let predicted_size = Compact::encoded_size_for(&assignments); | ||
|
||
let compact = | ||
Compact::from_assignment(assignments, make_voter_fn(&voters), make_target_fn(&candidates)) | ||
.unwrap(); | ||
let encoding = compact.encode(); | ||
|
||
assert_eq!(predicted_size, encoding.len()); | ||
} |