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

Commit

Permalink
add a fuzzer which can validate Compact::encoded_size_for
Browse files Browse the repository at this point in the history
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
coriolinus committed Apr 14, 2021
1 parent 919c98d commit 9160387
Show file tree
Hide file tree
Showing 5 changed files with 328 additions and 0 deletions.
12 changes: 12 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ members = [
"primitives/maybe-compressed-blob",
"primitives/npos-elections",
"primitives/npos-elections/compact",
"primitives/npos-elections/compact/fuzzer",
"primitives/npos-elections/fuzzer",
"primitives/offchain",
"primitives/panic-handler",
Expand Down
35 changes: 35 additions & 0 deletions primitives/npos-elections/compact/fuzzer/Cargo.toml
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"
168 changes: 168 additions & 0 deletions primitives/npos-elections/compact/fuzzer/src/common.rs
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 primitives/npos-elections/compact/fuzzer/src/compact_32_16_16.rs
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());
}

0 comments on commit 9160387

Please sign in to comment.