feat: Rebrand Polkadot/Substrate references to PezkuwiChain

This commit systematically rebrands various references from Parity Technologies'
Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk.

Key changes include:
- Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks.
- Modified internal documentation and code comments to reflect PezkuwiChain naming and structure.
- Replaced direct references to  with  or specific paths within the  for XCM, Pezkuwi, and other modules.
- Cleaned up deprecated  issue and PR references in various  and  files, particularly in  and  modules.
- Adjusted image and logo URLs in documentation to point to PezkuwiChain assets.
- Removed or rephrased comments related to external Polkadot/Substrate PRs and issues.

This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
2025-12-14 00:04:10 +03:00
parent 286de54384
commit 1c0e57d984
9084 changed files with 997839 additions and 997557 deletions
@@ -0,0 +1,49 @@
[package]
name = "pezsp-npos-elections"
version = "26.0.0"
authors.workspace = true
edition.workspace = true
license = "Apache-2.0"
homepage.workspace = true
repository.workspace = true
description = "NPoS election algorithm primitives"
readme = "README.md"
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { features = ["derive"], workspace = true }
scale-info = { features = ["derive"], workspace = true }
serde = { features = ["alloc", "derive"], optional = true, workspace = true }
pezsp-arithmetic = { workspace = true }
pezsp-core = { workspace = true }
pezsp-runtime = { workspace = true }
[dev-dependencies]
bizinikiwi-test-utils = { workspace = true }
[features]
default = ["std"]
bench = []
std = [
"codec/std",
"scale-info/std",
"serde/std",
"pezsp-arithmetic/std",
"pezsp-core/std",
"pezsp-runtime/std",
]
# Serde support without relying on std features.
serde = [
"dep:serde",
"scale-info/serde",
"pezsp-arithmetic/serde",
"pezsp-core/serde",
"pezsp-runtime/serde",
]
runtime-benchmarks = ["pezsp-runtime/runtime-benchmarks"]
@@ -0,0 +1,56 @@
# pezsp-npos-elections
A set of election algorithms to be used with a Bizinikiwi runtime, typically within the staking sub-system. Notable
implementation include:
- [`seq_phragmen`]: Implements the Phragmén Sequential Method. An un-ranked, relatively fast election method that
ensures PJR, but does not provide a constant factor approximation of the maximin problem.
- [`phragmms`]: Implements a hybrid approach inspired by Phragmén which is executed faster but it can achieve a constant
factor approximation of the maximin problem, similar to that of the MMS algorithm.
- [`balance_solution`]: Implements the star balancing algorithm. This iterative process can push a solution toward being
more `balanced`, which in turn can increase its score.
## Terminology
This crate uses context-independent words, not to be confused with staking. This is because the election algorithms of
this crate, while designed for staking, can be used in other contexts as well.
`Voter`: The entity casting some votes to a number of `Targets`. This is the same as `Nominator` in the context of
staking. `Target`: The entities eligible to be voted upon. This is the same as `Validator` in the context of staking.
`Edge`: A mapping from a `Voter` to a `Target`.
The goal of an election algorithm is to provide an `ElectionResult`. A data composed of:
- `winners`: A flat list of identifiers belonging to those who have won the election, usually ordered in some meaningful
way. They are zipped with their total backing stake.
- `assignment`: A mapping from each voter to their winner-only targets, zipped with a ration denoting the amount of
support given to that particular target.
```rust
// the winners.
let winners = vec![(1, 100), (2, 50)];
let assignments = vec![
// A voter, giving equal backing to both 1 and 2.
Assignment {
who: 10,
distribution: vec![(1, Perbill::from_percent(50)), (2, Perbill::from_percent(50))],
},
// A voter, Only backing 1.
Assignment { who: 20, distribution: vec![(1, Perbill::from_percent(100))] },
];
// the combination of the two makes the election result.
let election_result = ElectionResult { winners, assignments };
```
The `Assignment` field of the election result is voter-major, i.e. it is from the perspective of the voter. The struct
that represents the opposite is called a `Support`. This struct is usually accessed in a map-like manner, i.e. keyed by
voters, therefore it is stored as a mapping called `SupportMap`.
Moreover, the support is built from absolute backing values, not ratios like the example above. A struct similar to
`Assignment` that has stake value instead of ratios is called an `StakedAssignment`.
More information can be found at: https://arxiv.org/abs/2004.12990
License: Apache-2.0
@@ -0,0 +1,2 @@
hfuzz_target
hfuzz_workspace
@@ -0,0 +1,49 @@
[package]
name = "pezsp-npos-elections-fuzzer"
version = "2.0.0-alpha.5"
authors.workspace = true
edition.workspace = true
license = "Apache-2.0"
homepage.workspace = true
repository.workspace = true
description = "Fuzzer for phragmén implementation."
documentation = "https://docs.rs/pezsp-npos-elections-fuzzer"
publish = false
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[[bin]]
name = "reduce"
path = "src/reduce.rs"
[[bin]]
name = "phragmen_balancing"
path = "src/phragmen_balancing.rs"
[[bin]]
name = "phragmms_balancing"
path = "src/phragmms_balancing.rs"
[[bin]]
name = "phragmen_pjr"
path = "src/phragmen_pjr.rs"
[dependencies]
clap = { features = ["derive"], workspace = true }
honggfuzz = { workspace = true }
rand = { features = [
"small_rng",
"std",
], workspace = true, default-features = true }
pezsp-npos-elections = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezsp-npos-elections/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
]
@@ -0,0 +1,170 @@
// This file is part of Bizinikiwi.
// Copyright (C) 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.
// Each function will be used based on which fuzzer binary is being used.
#![allow(dead_code)]
use rand::{self, seq::SliceRandom, Rng, RngCore};
use pezsp_npos_elections::{phragmms, seq_phragmen, BalancingConfig, ElectionResult, VoteWeight};
use pezsp_runtime::Perbill;
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 {
// does not work correctly if b < 2 * a
assert!(b >= 2 * a);
let collapsed = x % b;
if collapsed >= a {
collapsed
} else {
collapsed + a
}
}
pub enum ElectionType {
Phragmen(Option<BalancingConfig>),
Phragmms(Option<BalancingConfig>),
}
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.
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 {
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();
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) {
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();
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,
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(to_elect, candidates.clone(), voters.clone(), conf).unwrap(),
ElectionType::Phragmms(conf) =>
phragmms(to_elect, candidates.clone(), voters.clone(), conf).unwrap(),
},
candidates,
voters,
stake_of,
)
}
@@ -0,0 +1,96 @@
// This file is part of Bizinikiwi.
// Copyright (C) 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 rand::{self, SeedableRng};
use pezsp_npos_elections::{
assignment_ratio_to_staked_normalized, seq_phragmen, to_supports, BalancingConfig,
ElectionResult, EvaluateSupport, VoteWeight,
};
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, &stake_of)
.unwrap();
let score = to_supports(staked.as_ref()).evaluate();
if score.minimal_stake == 0 {
// such cases cannot be improved by balancing.
return;
}
score
};
if iterations > 0 {
let config = BalancingConfig { iterations, tolerance: 0 };
let balanced: ElectionResult<AccountId, pezsp_runtime::Perbill> =
seq_phragmen(to_elect, candidates, voters, Some(config)).unwrap();
let balanced_score = {
let staked =
assignment_ratio_to_staked_normalized(balanced.assignments, &stake_of)
.unwrap();
to_supports(staked.as_ref()).evaluate()
};
let enhance = balanced_score.strict_better(unbalanced_score);
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.minimal_stake >= unbalanced_score.minimal_stake &&
balanced_score.sum_stake == unbalanced_score.sum_stake &&
balanced_score.sum_stake_squared <= unbalanced_score.sum_stake_squared
);
}
});
}
}
@@ -0,0 +1,118 @@
// This file is part of Bizinikiwi.
// Copyright (C) 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 clap::Parser;
mod common;
use common::{generate_random_npos_inputs, to_range};
use rand::{self, SeedableRng};
use pezsp_npos_elections::{pjr_check_core, seq_phragmen_core, setup_inputs, standard_threshold};
type AccountId = u64;
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, Parser)]
#[command(author, version, about)]
struct Opt {
/// How many candidates participate in this election
#[arg(short, long)]
candidates: Option<usize>,
/// How many voters participate in this election
#[arg(short, long)]
voters: Option<usize>,
/// Random seed to use in this election
#[arg(long)]
seed: Option<u64>,
}
#[cfg(not(fuzzing))]
fn main() {
let opt = Opt::parse();
// 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).is_ok(),
"unbalanced sequential phragmen must satisfy PJR",
);
}
@@ -0,0 +1,94 @@
// This file is part of Bizinikiwi.
// Copyright (C) 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 rand::{self, SeedableRng};
use pezsp_npos_elections::{
assignment_ratio_to_staked_normalized, phragmms, to_supports, BalancingConfig, ElectionResult,
EvaluateSupport, VoteWeight,
};
use pezsp_runtime::Perbill;
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, &stake_of)
.unwrap();
let score = to_supports(&staked).evaluate();
if score.minimal_stake == 0 {
// such cases cannot be improved by balancing.
return;
}
score
};
let config = BalancingConfig { iterations, tolerance: 0 };
let balanced: ElectionResult<AccountId, Perbill> =
phragmms(to_elect, candidates, voters, Some(config)).unwrap();
let balanced_score = {
let staked =
assignment_ratio_to_staked_normalized(balanced.assignments, &stake_of).unwrap();
to_supports(staked.as_ref()).evaluate()
};
let enhance = balanced_score.strict_better(unbalanced_score);
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.minimal_stake >= unbalanced_score.minimal_stake &&
balanced_score.sum_stake == unbalanced_score.sum_stake &&
balanced_score.sum_stake_squared <= unbalanced_score.sum_stake_squared
);
});
}
}
@@ -0,0 +1,143 @@
// This file is part of Bizinikiwi.
// Copyright (C) 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 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`.
//!
//! # Debugging a panic
//!
//! Once a panic is found, it can be debugged with
//! `cargo hfuzz run-debug reduce hfuzz_workspace/reduce/*.fuzz`.
use honggfuzz::fuzz;
mod common;
use common::to_range;
use rand::{self, Rng, RngCore, SeedableRng};
use pezsp_npos_elections::{reduce, to_support_map, ExtendedBalance, StakedAssignment};
type Balance = u128;
type AccountId = u64;
/// Or any other token type.
const KSM: Balance = 1_000_000_000_000;
fn main() {
loop {
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);
});
}
}
fn generate_random_phragmen_assignment(
voter_count: usize,
target_count: usize,
avg_edge_per_voter: usize,
edge_per_voter_var: usize,
mut rng: impl RngCore,
) -> (Vec<StakedAssignment<AccountId>>, Vec<AccountId>) {
// prefix to distinguish the voter and target account ranges.
let target_prefix = 1_000_000;
assert!(voter_count < target_prefix);
let mut assignments = Vec::with_capacity(voter_count as usize);
let mut winners: Vec<AccountId> = Vec::new();
let all_targets = (target_prefix..(target_prefix + target_count))
.map(|a| a as AccountId)
.collect::<Vec<AccountId>>();
(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 {
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(rng.gen_range(0..targets_to_chose_from.len()));
if winners.iter().all(|w| *w != target) {
winners.push(target);
}
(target, rng.gen_range(1 * KSM..100 * KSM))
})
.collect::<Vec<(AccountId, ExtendedBalance)>>();
assignments.push(StakedAssignment { who: (acc as AccountId), distribution });
});
(assignments, winners)
}
fn assert_assignments_equal(
ass1: &Vec<StakedAssignment<AccountId>>,
ass2: &Vec<StakedAssignment<AccountId>>,
) {
let support_1 = to_support_map::<AccountId>(ass1);
let support_2 = to_support_map::<AccountId>(ass2);
for (who, support) in support_1.iter() {
assert_eq!(support.total, support_2.get(who).unwrap().total);
}
}
fn reduce_and_compare(assignment: &Vec<StakedAssignment<AccountId>>, winners: &Vec<AccountId>) {
let mut altered_assignment = assignment.clone();
let n = assignment.len() as u32;
let m = winners.len() as u32;
let edges_before = assignment_len(assignment);
let num_changed = reduce(&mut altered_assignment);
let edges_after = edges_before - num_changed;
assert!(
edges_after <= m + n,
"reduce bound not satisfied. n = {}, m = {}, edges after reduce = {} (removed {})",
n,
m,
edges_after,
num_changed,
);
assert_assignments_equal(&assignment, &altered_assignment);
}
fn assignment_len(assignments: &[StakedAssignment<AccountId>]) -> u32 {
let mut counter = 0;
assignments
.iter()
.for_each(|x| x.distribution.iter().for_each(|_| counter += 1));
counter
}
@@ -0,0 +1,167 @@
// This file is part of Bizinikiwi.
// Copyright (C) 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.
//! Structs and helpers for distributing a voter's stake among various winners.
use crate::{ExtendedBalance, IdentifierT, PerThing128};
use alloc::vec::Vec;
#[cfg(feature = "serde")]
use codec::{Decode, Encode};
use pezsp_arithmetic::{
traits::{Bounded, Zero},
Normalizable, PerThing,
};
use pezsp_core::RuntimeDebug;
/// A voter's stake assignment among a set of targets, represented as ratios.
#[derive(RuntimeDebug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(PartialEq, Eq, Encode, Decode))]
pub struct Assignment<AccountId, P: PerThing> {
/// Voter's identifier.
pub who: AccountId,
/// The distribution of the voter's stake.
pub distribution: Vec<(AccountId, P)>,
}
impl<AccountId: IdentifierT, P: PerThing128> Assignment<AccountId, P> {
/// Convert from a ratio assignment into one with absolute values aka. [`StakedAssignment`].
///
/// It needs `stake` which is the total budget of the voter.
///
/// Note that this might create _un-normalized_ assignments, due to accuracy loss of `P`. Call
/// site might compensate by calling `try_normalize()` on the returned `StakedAssignment` as a
/// post-precessing.
///
/// If an edge ratio is [`Bounded::min_value()`], it is dropped. This edge can never mean
/// anything useful.
pub fn into_staked(self, stake: ExtendedBalance) -> StakedAssignment<AccountId> {
let distribution = self
.distribution
.into_iter()
.filter_map(|(target, p)| {
// if this ratio is zero, then skip it.
if p.is_zero() {
None
} else {
// NOTE: this mul impl will always round to the nearest number, so we might both
// overflow and underflow.
let distribution_stake = p * stake;
Some((target, distribution_stake))
}
})
.collect::<Vec<(AccountId, ExtendedBalance)>>();
StakedAssignment { who: self.who, distribution }
}
/// Try and normalize this assignment.
///
/// If `Ok(())` is returned, then the assignment MUST have been successfully normalized to 100%.
///
/// ### Errors
///
/// This will return only if the internal `normalize` fails. This can happen if sum of
/// `self.distribution.map(|p| p.deconstruct())` fails to fit inside `UpperOf<P>`. A user of
/// this crate may statically assert that this can never happen and safely `expect` this to
/// return `Ok`.
pub fn try_normalize(&mut self) -> Result<(), &'static str> {
self.distribution
.iter()
.map(|(_, p)| *p)
.collect::<Vec<_>>()
.normalize(P::one())
.map(|normalized_ratios| {
self.distribution.iter_mut().zip(normalized_ratios).for_each(
|((_, old), corrected)| {
*old = corrected;
},
)
})
}
}
/// A voter's stake assignment among a set of targets, represented as absolute values in the scale
/// of [`ExtendedBalance`].
#[derive(RuntimeDebug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(PartialEq, Eq, Encode, Decode))]
pub struct StakedAssignment<AccountId> {
/// Voter's identifier
pub who: AccountId,
/// The distribution of the voter's stake.
pub distribution: Vec<(AccountId, ExtendedBalance)>,
}
impl<AccountId> StakedAssignment<AccountId> {
/// Converts self into the normal [`Assignment`] type.
///
/// NOTE: This will always round down, and thus the results might be less than a full 100% `P`.
/// Use a normalization post-processing to fix this. The data type returned here will
/// potentially get used to create a compact type; a compact type requires sum of ratios to be
/// less than 100% upon un-compacting.
///
/// If an edge stake is so small that it cannot be represented in `T`, it is ignored. This edge
/// can never be re-created and does not mean anything useful anymore.
pub fn into_assignment<P: PerThing>(self) -> Assignment<AccountId, P>
where
AccountId: IdentifierT,
{
let stake = self.total();
// most likely, the size of the staked assignment and normal assignments will be the same,
// so we pre-allocate it to prevent a sudden 2x allocation. `filter_map` starts with a size
// of 0 by default.
// https://www.reddit.com/r/rust/comments/3spfh1/does_collect_allocate_more_than_once_while/
let mut distribution = Vec::<(AccountId, P)>::with_capacity(self.distribution.len());
self.distribution.into_iter().for_each(|(target, w)| {
let per_thing = P::from_rational(w, stake);
if per_thing != Bounded::min_value() {
distribution.push((target, per_thing));
}
});
Assignment { who: self.who, distribution }
}
/// Try and normalize this assignment.
///
/// If `Ok(())` is returned, then the assignment MUST have been successfully normalized to
/// `stake`.
///
/// NOTE: current implementation of `.normalize` is almost safe to `expect()` upon. The only
/// error case is when the input cannot fit in `T`, or the sum of input cannot fit in `T`.
/// Sadly, both of these are dependent upon the implementation of `VoteLimit`, i.e. the limit of
/// edges per voter which is enforced from upstream. Hence, at this crate, we prefer returning a
/// result and a use the name prefix `try_`.
pub fn try_normalize(&mut self, stake: ExtendedBalance) -> Result<(), &'static str> {
self.distribution
.iter()
.map(|(_, ref weight)| *weight)
.collect::<Vec<_>>()
.normalize(stake)
.map(|normalized_weights| {
self.distribution.iter_mut().zip(normalized_weights.into_iter()).for_each(
|((_, weight), corrected)| {
*weight = corrected;
},
)
})
}
/// Get the total stake of this assignment (aka voter budget).
pub fn total(&self) -> ExtendedBalance {
self.distribution.iter().fold(Zero::zero(), |a, b| a.saturating_add(b.1))
}
}
@@ -0,0 +1,199 @@
// This file is part of Bizinikiwi.
// Copyright (C) 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.
//! Balancing algorithm implementation.
//!
//! Given a committee `A` and an edge weight vector `w`, a balanced solution is one that
//!
//! 1. it maximizes the sum of member supports, i.e `Argmax { sum(support(c)) }`. for all `c` in
//! `A`.
//! 2. it minimizes the sum of supports squared, i.e `Argmin { sum(support(c).pow(2)) }` for all `c`
//! in `A`.
//!
//! See [`balance`] for more information.
use crate::{BalancingConfig, Edge, ExtendedBalance, IdentifierT, Voter};
use alloc::vec::Vec;
use pezsp_arithmetic::traits::Zero;
/// Balance the weight distribution of a given `voters` at most `iterations` times, or up until the
/// point where the biggest difference created per iteration of all stakes is `tolerance`. If this
/// is called with `tolerance = 0`, then exactly `iterations` rounds will be executed, except if no
/// change has been made (`difference = 0`). `tolerance` and `iterations` are part of the
/// [`BalancingConfig`] struct.
///
/// In almost all cases, a balanced solution will have a better score than an unbalanced solution,
/// yet this is not 100% guaranteed because the first element of a [`crate::ElectionScore`] does not
/// directly relate to balancing.
///
/// Note that some reference implementation adopt an approach in which voters are balanced randomly
/// per round. To advocate determinism, we don't do this. In each round, all voters are exactly
/// balanced once, in the same order.
///
/// Also, note that due to re-distribution of weights, the outcome of this function might contain
/// edges with weight zero. The call site should filter such weight if desirable. Moreover, the
/// outcome might need balance re-normalization, see `Voter::try_normalize`.
///
/// ### References
///
/// - [A new approach to the maximum flow problem](https://dl.acm.org/doi/10.1145/48014.61051).
/// - [Validator election in nominated proof-of-stake](https://arxiv.org/abs/2004.12990) (Appendix
/// A.)
/// - [Computing a balanced solution](https://research.web3.foundation/en/latest/polkadot/NPoS/3.%20Balancing.html),
/// which contains the details of the algorithm implementation.
pub fn balance<AccountId: IdentifierT>(
voters: &mut Vec<Voter<AccountId>>,
config: &BalancingConfig,
) -> usize {
if config.iterations == 0 {
return 0;
}
let mut iter = 0;
loop {
let mut max_diff = 0;
for voter in voters.iter_mut() {
let diff = balance_voter(voter, config.tolerance);
if diff > max_diff {
max_diff = diff;
}
}
iter += 1;
if max_diff <= config.tolerance || iter >= config.iterations {
break iter;
}
}
}
/// Internal implementation of balancing for one voter.
pub(crate) fn balance_voter<AccountId: IdentifierT>(
voter: &mut Voter<AccountId>,
tolerance: ExtendedBalance,
) -> ExtendedBalance {
// create a shallow copy of the elected ones. The original one will not be used henceforth.
let mut elected_edges = voter
.edges
.iter_mut()
.filter(|e| e.candidate.borrow().elected)
.collect::<Vec<&mut Edge<AccountId>>>();
// Either empty, or a self vote. Not much to do in either case.
if elected_edges.len() <= 1 {
return Zero::zero();
}
// amount of stake from this voter that is used in edges.
let stake_used =
elected_edges.iter().fold(0, |a: ExtendedBalance, e| a.saturating_add(e.weight));
// backed stake of each of the elected edges.
let backed_stakes = elected_edges
.iter()
.map(|e| e.candidate.borrow().backed_stake)
.collect::<Vec<_>>();
// backed stake of all the edges for whom we've spent some stake.
let backing_backed_stake = elected_edges
.iter()
.filter_map(|e| if e.weight > 0 { Some(e.candidate.borrow().backed_stake) } else { None })
.collect::<Vec<_>>();
let difference = if backing_backed_stake.len() > 0 {
let max_stake = backing_backed_stake
.iter()
.max()
.expect("vector with positive length will have a max; qed");
let min_stake = backed_stakes
.iter()
.min()
.expect("iterator with positive length will have a min; qed");
let mut difference = max_stake.saturating_sub(*min_stake);
difference = difference.saturating_add(voter.budget.saturating_sub(stake_used));
if difference < tolerance {
return difference;
}
difference
} else {
voter.budget
};
// remove all backings.
for edge in elected_edges.iter_mut() {
let mut candidate = edge.candidate.borrow_mut();
candidate.backed_stake = candidate.backed_stake.saturating_sub(edge.weight);
edge.weight = 0;
}
elected_edges.sort_by_key(|e| e.candidate.borrow().backed_stake);
let mut cumulative_backed_stake = Zero::zero();
let mut last_index = elected_edges.len() - 1;
for (index, edge) in elected_edges.iter().enumerate() {
let index = index as ExtendedBalance;
let backed_stake = edge.candidate.borrow().backed_stake;
let temp = backed_stake.saturating_mul(index);
if temp.saturating_sub(cumulative_backed_stake) > voter.budget {
// defensive only. length of elected_edges is checked to be above 1.
last_index = index.saturating_sub(1) as usize;
break;
}
cumulative_backed_stake = cumulative_backed_stake.saturating_add(backed_stake);
}
let last_stake = elected_edges
.get(last_index)
.expect(
"length of elected_edges is greater than or equal 2; last_index index is at the \
minimum elected_edges.len() - 1; index is within range; qed",
)
.candidate
.borrow()
.backed_stake;
let ways_to_split = last_index + 1;
let excess = voter
.budget
.saturating_add(cumulative_backed_stake)
.saturating_sub(last_stake.saturating_mul(ways_to_split as ExtendedBalance));
// Do the final update.
for edge in elected_edges.into_iter().take(ways_to_split) {
// first, do one scoped borrow to get the previous candidate stake.
let candidate_backed_stake = {
let candidate = edge.candidate.borrow();
candidate.backed_stake
};
let new_edge_weight = (excess / ways_to_split as ExtendedBalance)
.saturating_add(last_stake)
.saturating_sub(candidate_backed_stake);
// write the new edge weight
edge.weight = new_edge_weight;
// write the new candidate stake
let mut candidate = edge.candidate.borrow_mut();
candidate.backed_stake = candidate.backed_stake.saturating_add(new_edge_weight);
}
// excess / ways_to_split can cause a small un-normalized voters to be created.
// We won't `expect` here because even a result which is not normalized is not corrupt;
let _ = voter.try_normalize_elected();
difference
}
@@ -0,0 +1,133 @@
// This file is part of Bizinikiwi.
// Copyright (C) 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.
//! Helper methods for npos-elections.
use crate::{
Assignment, Error, ExtendedBalance, IdentifierT, PerThing128, StakedAssignment, Supports,
VoteWeight,
};
use alloc::{collections::BTreeMap, vec::Vec};
use pezsp_arithmetic::PerThing;
/// Converts a vector of ratio assignments into ones with absolute budget value.
///
/// Note that this will NOT attempt at normalizing the result.
pub fn assignment_ratio_to_staked<A: IdentifierT, P: PerThing128, FS>(
ratios: Vec<Assignment<A, P>>,
stake_of: FS,
) -> Vec<StakedAssignment<A>>
where
for<'r> FS: Fn(&'r A) -> VoteWeight,
{
ratios
.into_iter()
.map(|a| {
let stake = stake_of(&a.who);
a.into_staked(stake.into())
})
.collect()
}
/// Same as [`assignment_ratio_to_staked`] and try and do normalization.
pub fn assignment_ratio_to_staked_normalized<A: IdentifierT, P: PerThing128, FS>(
ratio: Vec<Assignment<A, P>>,
stake_of: FS,
) -> Result<Vec<StakedAssignment<A>>, Error>
where
for<'r> FS: Fn(&'r A) -> VoteWeight,
{
let mut staked = assignment_ratio_to_staked(ratio, &stake_of);
staked.iter_mut().try_for_each(|a| {
a.try_normalize(stake_of(&a.who).into()).map_err(|_| Error::ArithmeticError)
})?;
Ok(staked)
}
/// Converts a vector of staked assignments into ones with ratio values.
///
/// Note that this will NOT attempt at normalizing the result.
pub fn assignment_staked_to_ratio<A: IdentifierT, P: PerThing>(
staked: Vec<StakedAssignment<A>>,
) -> Vec<Assignment<A, P>> {
staked.into_iter().map(|a| a.into_assignment()).collect()
}
/// Same as [`assignment_staked_to_ratio`] and try and do normalization.
pub fn assignment_staked_to_ratio_normalized<A: IdentifierT, P: PerThing128>(
staked: Vec<StakedAssignment<A>>,
) -> Result<Vec<Assignment<A, P>>, Error> {
let mut ratio = staked.into_iter().map(|a| a.into_assignment()).collect::<Vec<_>>();
for assignment in ratio.iter_mut() {
assignment.try_normalize().map_err(|_| Error::ArithmeticError)?;
}
Ok(ratio)
}
/// Convert some [`Supports`]s into vector of [`StakedAssignment`]
pub fn supports_to_staked_assignment<A: IdentifierT>(
supports: Supports<A>,
) -> Vec<StakedAssignment<A>> {
let mut staked: BTreeMap<A, Vec<(A, ExtendedBalance)>> = BTreeMap::new();
for (target, support) in supports {
for (voter, amount) in support.voters {
staked.entry(voter).or_default().push((target.clone(), amount))
}
}
staked
.into_iter()
.map(|(who, distribution)| StakedAssignment { who, distribution })
.collect::<Vec<_>>()
}
#[cfg(test)]
mod tests {
use super::*;
use pezsp_arithmetic::Perbill;
#[test]
fn into_staked_works() {
let assignments = vec![
Assignment {
who: 1u32,
distribution: vec![
(10u32, Perbill::from_float(0.5)),
(20, Perbill::from_float(0.5)),
],
},
Assignment {
who: 2u32,
distribution: vec![
(10, Perbill::from_float(0.33)),
(20, Perbill::from_float(0.67)),
],
},
];
let stake_of = |_: &u32| -> VoteWeight { 100 };
let staked = assignment_ratio_to_staked(assignments, stake_of);
assert_eq!(
staked,
vec![
StakedAssignment { who: 1u32, distribution: vec![(10u32, 50), (20, 50),] },
StakedAssignment { who: 2u32, distribution: vec![(10u32, 33), (20, 67),] }
]
);
}
}
@@ -0,0 +1,646 @@
// This file is part of Bizinikiwi.
// Copyright (C) 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.
//! A set of election algorithms to be used with a bizinikiwi runtime, typically within the staking
//! sub-system. Notable implementation include:
//!
//! - [`seq_phragmen`]: Implements the Phragmén Sequential Method. An un-ranked, relatively fast
//! election method that ensures PJR, but does not provide a constant factor approximation of the
//! maximin problem.
//! - [`ghragmms`](phragmms::phragmms()): Implements a hybrid approach inspired by Phragmén which is
//! executed faster but it can achieve a constant factor approximation of the maximin problem,
//! similar to that of the MMS algorithm.
//! - [`balance`]: Implements the star balancing algorithm. This iterative process can push a
//! solution toward being more "balanced", which in turn can increase its score.
//!
//! ### Terminology
//!
//! This crate uses context-independent words, not to be confused with staking. This is because the
//! election algorithms of this crate, while designed for staking, can be used in other contexts as
//! well.
//!
//! `Voter`: The entity casting some votes to a number of `Targets`. This is the same as `Nominator`
//! in the context of staking. `Target`: The entities eligible to be voted upon. This is the same as
//! `Validator` in the context of staking. `Edge`: A mapping from a `Voter` to a `Target`.
//!
//! The goal of an election algorithm is to provide an `ElectionResult`. A data composed of:
//! - `winners`: A flat list of identifiers belonging to those who have won the election, usually
//! ordered in some meaningful way. They are zipped with their total backing stake.
//! - `assignment`: A mapping from each voter to their winner-only targets, zipped with a ration
//! denoting the amount of support given to that particular target.
//!
//! ```rust
//! # use pezsp_npos_elections::*;
//! # use pezsp_runtime::Perbill;
//! // the winners.
//! let winners = vec![(1, 100), (2, 50)];
//! let assignments = vec![
//! // A voter, giving equal backing to both 1 and 2.
//! Assignment {
//! who: 10,
//! distribution: vec![(1, Perbill::from_percent(50)), (2, Perbill::from_percent(50))],
//! },
//! // A voter, Only backing 1.
//! Assignment { who: 20, distribution: vec![(1, Perbill::from_percent(100))] },
//! ];
//!
//! // the combination of the two makes the election result.
//! let election_result = ElectionResult { winners, assignments };
//! ```
//!
//! The `Assignment` field of the election result is voter-major, i.e. it is from the perspective of
//! the voter. The struct that represents the opposite is called a `Support`. This struct is usually
//! accessed in a map-like manner, i.e. keyed by voters, therefore it is stored as a mapping called
//! `SupportMap`.
//!
//! Moreover, the support is built from absolute backing values, not ratios like the example above.
//! A struct similar to `Assignment` that has stake value instead of ratios is called an
//! `StakedAssignment`.
//!
//!
//! More information can be found at: <https://arxiv.org/abs/2004.12990>
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
use alloc::{collections::btree_map::BTreeMap, rc::Rc, vec, vec::Vec};
use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
use core::{cell::RefCell, cmp::Ordering};
use scale_info::TypeInfo;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use pezsp_arithmetic::{traits::Zero, Normalizable, PerThing, Rational128, ThresholdOrd};
use pezsp_core::RuntimeDebug;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
mod assignments;
pub mod balancing;
pub mod helpers;
pub mod node;
pub mod phragmen;
pub mod phragmms;
pub mod pjr;
pub mod reduce;
pub mod traits;
pub use assignments::{Assignment, StakedAssignment};
pub use balancing::*;
pub use helpers::*;
pub use phragmen::*;
pub use phragmms::*;
pub use pjr::*;
pub use reduce::reduce;
pub use traits::{IdentifierT, PerThing128};
/// The errors that might occur in this crate and `pezframe-election-provider-solution-type`.
#[derive(
Eq,
PartialEq,
RuntimeDebug,
Clone,
codec::Encode,
codec::Decode,
codec::DecodeWithMemTracking,
scale_info::TypeInfo,
)]
pub enum Error {
/// While going from solution indices to ratio, the weight of all the edges has gone above the
/// total.
SolutionWeightOverflow,
/// The solution type has a voter who's number of targets is out of bound.
SolutionTargetOverflow,
/// One of the index functions returned none.
SolutionInvalidIndex,
/// One of the page indices was invalid.
SolutionInvalidPageIndex,
/// An error occurred in some arithmetic operation.
ArithmeticError,
/// The data provided to create support map was invalid.
InvalidSupportEdge,
/// The number of voters is bigger than the `MaxVoters` bound.
TooManyVoters,
/// Some bounds were exceeded when converting election types.
BoundsExceeded,
/// A duplicate voter was detected.
DuplicateVoter,
/// A duplicate target was detected.
DuplicateTarget,
}
/// A type which is used in the API of this crate as a numeric weight of a vote, most often the
/// stake of the voter. It is always converted to [`ExtendedBalance`] for computation.
pub type VoteWeight = u64;
/// A type in which performing operations on vote weights are safe.
pub type ExtendedBalance = u128;
/// The score of an election. This is the main measure of an election's quality.
///
/// By definition, the order of significance in [`ElectionScore`] is:
///
/// 1. `minimal_stake`.
/// 2. `sum_stake`.
/// 3. `sum_stake_squared`.
#[derive(
Clone,
Copy,
PartialEq,
Eq,
Encode,
Decode,
DecodeWithMemTracking,
MaxEncodedLen,
TypeInfo,
Debug,
Default,
)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ElectionScore {
/// The minimal winner, in terms of total backing stake.
///
/// This parameter should be maximized.
pub minimal_stake: ExtendedBalance,
/// The sum of the total backing of all winners.
///
/// This parameter should maximized
pub sum_stake: ExtendedBalance,
/// The sum squared of the total backing of all winners, aka. the variance.
///
/// This parameter should be minimized.
pub sum_stake_squared: ExtendedBalance,
}
#[cfg(feature = "std")]
impl ElectionScore {
/// format the election score in a pretty way with the given `token` symbol and `decimals`.
pub fn pretty(&self, token: &str, decimals: u32) -> String {
format!(
"ElectionScore (minimal_stake: {}, sum_stake: {}, sum_stake_squared: {})",
pretty_balance(self.minimal_stake, token, decimals),
pretty_balance(self.sum_stake, token, decimals),
pretty_balance(self.sum_stake_squared, token, decimals),
)
}
}
/// Format a single [`ExtendedBalance`] into a pretty string with the given `token` symbol and
/// `decimals`.
#[cfg(feature = "std")]
pub fn pretty_balance<B: Into<u128>>(b: B, token: &str, decimals: u32) -> String {
let b: u128 = b.into();
format!("{} {}", b / 10u128.pow(decimals), token)
}
impl ElectionScore {
/// Iterate over the inner items, first visiting the most significant one.
fn iter_by_significance(self) -> impl Iterator<Item = ExtendedBalance> {
[self.minimal_stake, self.sum_stake, self.sum_stake_squared].into_iter()
}
/// Compares two sets of election scores based on desirability, returning true if `self` is
/// strictly `threshold` better than `other`. In other words, each element of `self` must be
/// better than `other` relative to the given `threshold`.
///
/// Evaluation is done based on the order of significance of the fields of [`ElectionScore`].
pub fn strict_threshold_better(self, other: Self, threshold: impl PerThing) -> bool {
match self
.iter_by_significance()
.zip(other.iter_by_significance())
.map(|(this, that)| (this.ge(&that), this.tcmp(&that, threshold.mul_ceil(that))))
.collect::<Vec<(bool, Ordering)>>()
.as_slice()
{
// threshold better in the `score.minimal_stake`, accept.
[(x, Ordering::Greater), _, _] => {
debug_assert!(x);
true
},
// less than threshold better in `score.minimal_stake`, but more than threshold better
// in `score.sum_stake`.
[(true, Ordering::Equal), (_, Ordering::Greater), _] => true,
// less than threshold better in `score.minimal_stake` and `score.sum_stake`, but more
// than threshold better in `score.sum_stake_squared`.
[(true, Ordering::Equal), (true, Ordering::Equal), (_, Ordering::Less)] => true,
// anything else is not a good score.
_ => false,
}
}
/// Compares two sets of election scores based on desirability, returning true if `self` is
/// strictly better than `other`.
pub fn strict_better(self, other: Self) -> bool {
self.strict_threshold_better(other, pezsp_runtime::Perbill::zero())
}
}
impl core::cmp::Ord for ElectionScore {
fn cmp(&self, other: &Self) -> Ordering {
// we delegate this to the lexicographic cmp of slices`, and to incorporate that we want the
// third element to be minimized, we swap them.
[self.minimal_stake, self.sum_stake, other.sum_stake_squared].cmp(&[
other.minimal_stake,
other.sum_stake,
self.sum_stake_squared,
])
}
}
impl core::cmp::PartialOrd for ElectionScore {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
/// Utility struct to group parameters for the balancing algorithm.
#[derive(Clone, Copy)]
pub struct BalancingConfig {
pub iterations: usize,
pub tolerance: ExtendedBalance,
}
/// A pointer to a candidate struct with interior mutability.
pub type CandidatePtr<A> = Rc<RefCell<Candidate<A>>>;
/// A candidate entity for the election.
#[derive(RuntimeDebug, Clone, Default)]
pub struct Candidate<AccountId> {
/// Identifier.
who: AccountId,
/// Score of the candidate.
///
/// Used differently in seq-phragmen and max-score.
score: Rational128,
/// Approval stake of the candidate. Merely the sum of all the voter's stake who approve this
/// candidate.
approval_stake: ExtendedBalance,
/// The final stake of this candidate. Will be equal to a subset of approval stake.
backed_stake: ExtendedBalance,
/// True if this candidate is already elected in the current election.
elected: bool,
/// The round index at which this candidate was elected.
round: usize,
}
impl<AccountId> Candidate<AccountId> {
pub fn to_ptr(self) -> CandidatePtr<AccountId> {
Rc::new(RefCell::new(self))
}
}
/// A vote being casted by a [`Voter`] to a [`Candidate`] is an `Edge`.
#[derive(Clone)]
pub struct Edge<AccountId> {
/// Identifier of the target.
///
/// This is equivalent of `self.candidate.borrow().who`, yet it helps to avoid double borrow
/// errors of the candidate pointer.
who: AccountId,
/// Load of this edge.
load: Rational128,
/// Pointer to the candidate.
candidate: CandidatePtr<AccountId>,
/// The weight (i.e. stake given to `who`) of this edge.
weight: ExtendedBalance,
}
#[cfg(test)]
impl<AccountId: Clone> Edge<AccountId> {
fn new(candidate: Candidate<AccountId>, weight: ExtendedBalance) -> Self {
let who = candidate.who.clone();
let candidate = Rc::new(RefCell::new(candidate));
Self { weight, who, candidate, load: Default::default() }
}
}
#[cfg(feature = "std")]
impl<A: IdentifierT> core::fmt::Debug for Edge<A> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "Edge({:?}, weight = {:?})", self.who, self.weight)
}
}
/// A voter entity.
#[derive(Clone, Default)]
pub struct Voter<AccountId> {
/// Identifier.
who: AccountId,
/// List of candidates approved by this voter.
edges: Vec<Edge<AccountId>>,
/// The stake of this voter.
budget: ExtendedBalance,
/// Load of the voter.
load: Rational128,
}
#[cfg(feature = "std")]
impl<A: IdentifierT> std::fmt::Debug for Voter<A> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Voter({:?}, budget = {}, edges = {:?})", self.who, self.budget, self.edges)
}
}
impl<AccountId: IdentifierT> Voter<AccountId> {
/// Create a new `Voter`.
pub fn new(who: AccountId) -> Self {
Self {
who,
edges: Default::default(),
budget: Default::default(),
load: Default::default(),
}
}
/// Returns `true` if `self` votes for `target`.
///
/// Note that this does not take into account if `target` is elected (i.e. is *active*) or not.
pub fn votes_for(&self, target: &AccountId) -> bool {
self.edges.iter().any(|e| &e.who == target)
}
/// Returns none if this voter does not have any non-zero distributions.
///
/// Note that this might create _un-normalized_ assignments, due to accuracy loss of `P`. Call
/// site might compensate by calling `normalize()` on the returned `Assignment` as a
/// post-processing.
pub fn into_assignment<P: PerThing>(self) -> Option<Assignment<AccountId, P>> {
let who = self.who;
let budget = self.budget;
let distribution = self
.edges
.into_iter()
.filter_map(|e| {
let per_thing = P::from_rational(e.weight, budget);
// trim zero edges.
if per_thing.is_zero() {
None
} else {
Some((e.who, per_thing))
}
})
.collect::<Vec<_>>();
if distribution.len() > 0 {
Some(Assignment { who, distribution })
} else {
None
}
}
/// Try and normalize the votes of self.
///
/// If the normalization is successful then `Ok(())` is returned.
///
/// Note that this will not distinguish between elected and unelected edges. Thus, it should
/// only be called on a voter who has already been reduced to only elected edges.
///
/// ### Errors
///
/// This will return only if the internal `normalize` fails. This can happen if the sum of the
/// weights exceeds `ExtendedBalance::max_value()`.
pub fn try_normalize(&mut self) -> Result<(), &'static str> {
let edge_weights = self.edges.iter().map(|e| e.weight).collect::<Vec<_>>();
edge_weights.normalize(self.budget).map(|normalized| {
// here we count on the fact that normalize does not change the order.
for (edge, corrected) in self.edges.iter_mut().zip(normalized.into_iter()) {
let mut candidate = edge.candidate.borrow_mut();
// first, subtract the incorrect weight
candidate.backed_stake = candidate.backed_stake.saturating_sub(edge.weight);
edge.weight = corrected;
// Then add the correct one again.
candidate.backed_stake = candidate.backed_stake.saturating_add(edge.weight);
}
})
}
/// Same as [`Self::try_normalize`] but the normalization is only limited between elected edges.
pub fn try_normalize_elected(&mut self) -> Result<(), &'static str> {
let elected_edge_weights = self
.edges
.iter()
.filter_map(|e| if e.candidate.borrow().elected { Some(e.weight) } else { None })
.collect::<Vec<_>>();
elected_edge_weights.normalize(self.budget).map(|normalized| {
// here we count on the fact that normalize does not change the order, and that vector
// iteration is deterministic.
for (edge, corrected) in self
.edges
.iter_mut()
.filter(|e| e.candidate.borrow().elected)
.zip(normalized.into_iter())
{
let mut candidate = edge.candidate.borrow_mut();
// first, subtract the incorrect weight
candidate.backed_stake = candidate.backed_stake.saturating_sub(edge.weight);
edge.weight = corrected;
// Then add the correct one again.
candidate.backed_stake = candidate.backed_stake.saturating_add(edge.weight);
}
})
}
/// This voter's budget.
#[inline]
pub fn budget(&self) -> ExtendedBalance {
self.budget
}
}
/// Final result of the election.
#[derive(RuntimeDebug)]
pub struct ElectionResult<AccountId, P: PerThing> {
/// Just winners zipped with their approval stake. Note that the approval stake is merely the
/// sub of their received stake and could be used for very basic sorting and approval voting.
pub winners: Vec<(AccountId, ExtendedBalance)>,
/// Individual assignments. for each tuple, the first elements is a voter and the second is the
/// list of candidates that it supports.
pub assignments: Vec<Assignment<AccountId, P>>,
}
/// A structure to demonstrate the election result from the perspective of the candidate, i.e. how
/// much support each candidate is receiving.
///
/// This complements the [`ElectionResult`] and is needed to run the balancing post-processing.
///
/// This, at the current version, resembles the `Exposure` defined in the Staking pallet, yet they
/// do not necessarily have to be the same.
#[derive(RuntimeDebug, Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Support<AccountId> {
/// Total support.
pub total: ExtendedBalance,
/// Support from voters.
pub voters: Vec<(AccountId, ExtendedBalance)>,
}
impl<AccountId> Default for Support<AccountId> {
fn default() -> Self {
Self { total: Default::default(), voters: vec![] }
}
}
impl<AccountId> Backings for &Support<AccountId> {
fn total(&self) -> ExtendedBalance {
self.total
}
}
/// A target-major representation of the the election outcome.
///
/// Essentially a flat variant of [`SupportMap`].
///
/// The main advantage of this is that it is encodable.
pub type Supports<A> = Vec<(A, Support<A>)>;
/// Linkage from a winner to their [`Support`].
///
/// This is more helpful than a normal [`Supports`] as it allows faster error checking.
pub type SupportMap<A> = BTreeMap<A, Support<A>>;
/// Build the support map from the assignments.
pub fn to_support_map<AccountId: IdentifierT>(
assignments: &[StakedAssignment<AccountId>],
) -> SupportMap<AccountId> {
let mut supports = <BTreeMap<AccountId, Support<AccountId>>>::new();
// build support struct.
for StakedAssignment { who, distribution } in assignments.iter() {
for (c, weight_extended) in distribution.iter() {
let support = supports.entry(c.clone()).or_default();
support.total = support.total.saturating_add(*weight_extended);
support.voters.push((who.clone(), *weight_extended));
}
}
supports
}
/// Same as [`to_support_map`] except it returns a flat vector.
pub fn to_supports<AccountId: IdentifierT>(
assignments: &[StakedAssignment<AccountId>],
) -> Supports<AccountId> {
to_support_map(assignments).into_iter().collect()
}
/// Extension trait for evaluating a support map or vector.
pub trait EvaluateSupport {
/// Evaluate a support map. The returned tuple contains:
///
/// - Minimum support. This value must be **maximized**.
/// - Sum of all supports. This value must be **maximized**.
/// - Sum of all supports squared. This value must be **minimized**.
fn evaluate(&self) -> ElectionScore;
}
impl<AccountId: IdentifierT> EvaluateSupport for Supports<AccountId> {
fn evaluate(&self) -> ElectionScore {
evaluate_support(self.iter().map(|(_, s)| s))
}
}
/// Generic representation of a support.
pub trait Backings {
/// The total backing of an individual target.
fn total(&self) -> ExtendedBalance;
}
/// General evaluation of a list of backings that returns an election score.
pub fn evaluate_support(backings: impl Iterator<Item = impl Backings>) -> ElectionScore {
let mut minimal_stake = ExtendedBalance::max_value();
let mut sum_stake: ExtendedBalance = Zero::zero();
// NOTE: The third element might saturate but fine for now since this will run on-chain and
// need to be fast.
let mut sum_stake_squared: ExtendedBalance = Zero::zero();
for support in backings {
sum_stake = sum_stake.saturating_add(support.total());
let squared = support.total().saturating_mul(support.total());
sum_stake_squared = sum_stake_squared.saturating_add(squared);
if support.total() < minimal_stake {
minimal_stake = support.total();
}
}
ElectionScore { minimal_stake, sum_stake, sum_stake_squared }
}
/// Converts raw inputs to types used in this crate.
///
/// This will perform some cleanup that are most often important:
/// - It drops any votes that are pointing to non-candidates.
/// - It drops duplicate targets within a voter.
pub fn setup_inputs<AccountId: IdentifierT>(
initial_candidates: Vec<AccountId>,
initial_voters: Vec<(AccountId, VoteWeight, impl IntoIterator<Item = AccountId>)>,
) -> (Vec<CandidatePtr<AccountId>>, Vec<Voter<AccountId>>) {
// used to cache and access candidates index.
let mut c_idx_cache = BTreeMap::<AccountId, usize>::new();
let candidates = initial_candidates
.into_iter()
.enumerate()
.map(|(idx, who)| {
c_idx_cache.insert(who.clone(), idx);
Candidate {
who,
score: Default::default(),
approval_stake: Default::default(),
backed_stake: Default::default(),
elected: Default::default(),
round: Default::default(),
}
.to_ptr()
})
.collect::<Vec<CandidatePtr<AccountId>>>();
let voters = initial_voters
.into_iter()
.filter_map(|(who, voter_stake, votes)| {
let mut edges: Vec<Edge<AccountId>> = Vec::new();
for v in votes {
if edges.iter().any(|e| e.who == v) {
// duplicate edge.
continue;
}
if let Some(idx) = c_idx_cache.get(&v) {
// This candidate is valid + already cached.
let mut candidate = candidates[*idx].borrow_mut();
candidate.approval_stake =
candidate.approval_stake.saturating_add(voter_stake.into());
edges.push(Edge {
who: v.clone(),
candidate: Rc::clone(&candidates[*idx]),
load: Default::default(),
weight: Default::default(),
});
} // else {} would be wrong votes. We don't really care about it.
}
if edges.is_empty() {
None
} else {
Some(Voter { who, edges, budget: voter_stake.into(), load: Rational128::zero() })
}
})
.collect::<Vec<_>>();
(candidates, voters)
}
@@ -0,0 +1,386 @@
// This file is part of Bizinikiwi.
// Copyright (C) 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.
//! Mock file for npos-elections.
#![cfg(test)]
use alloc::collections::btree_map::BTreeMap;
use pezsp_arithmetic::{
traits::{One, SaturatedConversion, Zero},
PerThing,
};
use pezsp_runtime::assert_eq_error_rate;
use crate::{seq_phragmen, Assignment, ElectionResult, ExtendedBalance, PerThing128, VoteWeight};
pub type AccountId = u64;
#[derive(Default, Debug)]
pub(crate) struct _Candidate<A> {
who: A,
score: f64,
approval_stake: f64,
elected: bool,
}
#[derive(Default, Debug)]
pub(crate) struct _Voter<A> {
who: A,
edges: Vec<_Edge<A>>,
budget: f64,
load: f64,
}
#[derive(Default, Debug)]
pub(crate) struct _Edge<A> {
who: A,
load: f64,
candidate_index: usize,
}
#[derive(Default, Debug, PartialEq)]
pub(crate) struct _Support<A> {
pub own: f64,
pub total: f64,
pub others: Vec<_Assignment<A>>,
}
pub(crate) type _Assignment<A> = (A, f64);
pub(crate) type _SupportMap<A> = BTreeMap<A, _Support<A>>;
#[derive(Debug, Clone)]
pub(crate) struct _ElectionResult<A: Clone> {
pub winners: Vec<(A, ExtendedBalance)>,
pub assignments: Vec<(A, Vec<_Assignment<A>>)>,
}
pub(crate) fn auto_generate_self_voters<A: Clone>(candidates: &[A]) -> Vec<(A, Vec<A>)> {
candidates.iter().map(|c| (c.clone(), vec![c.clone()])).collect()
}
pub(crate) fn elect_float<A>(
candidate_count: usize,
initial_candidates: Vec<A>,
initial_voters: Vec<(A, Vec<A>)>,
stake_of: impl Fn(&A) -> VoteWeight,
) -> Option<_ElectionResult<A>>
where
A: Default + Ord + Copy,
{
let mut elected_candidates: Vec<(A, ExtendedBalance)>;
let mut assigned: Vec<(A, Vec<_Assignment<A>>)>;
let mut c_idx_cache = BTreeMap::<A, usize>::new();
let num_voters = initial_candidates.len() + initial_voters.len();
let mut voters: Vec<_Voter<A>> = Vec::with_capacity(num_voters);
let mut candidates = initial_candidates
.into_iter()
.enumerate()
.map(|(idx, who)| {
c_idx_cache.insert(who, idx);
_Candidate { who, ..Default::default() }
})
.collect::<Vec<_Candidate<A>>>();
voters.extend(initial_voters.into_iter().map(|(who, votes)| {
let voter_stake = stake_of(&who) as f64;
let mut edges: Vec<_Edge<A>> = Vec::with_capacity(votes.len());
for v in votes {
if let Some(idx) = c_idx_cache.get(&v) {
candidates[*idx].approval_stake = candidates[*idx].approval_stake + voter_stake;
edges.push(_Edge { who: v, candidate_index: *idx, ..Default::default() });
}
}
_Voter { who, edges, budget: voter_stake, load: 0f64 }
}));
let to_elect = candidate_count.min(candidates.len());
elected_candidates = Vec::with_capacity(candidate_count);
assigned = Vec::with_capacity(candidate_count);
for _round in 0..to_elect {
for c in &mut candidates {
if !c.elected {
c.score = 1.0 / c.approval_stake;
}
}
for n in &voters {
for e in &n.edges {
let c = &mut candidates[e.candidate_index];
if !c.elected && !(c.approval_stake == 0f64) {
c.score += n.budget * n.load / c.approval_stake;
}
}
}
if let Some(winner) = candidates
.iter_mut()
.filter(|c| !c.elected)
.min_by(|x, y| x.score.partial_cmp(&y.score).unwrap_or(core::cmp::Ordering::Equal))
{
winner.elected = true;
for n in &mut voters {
for e in &mut n.edges {
if e.who == winner.who {
e.load = winner.score - n.load;
n.load = winner.score;
}
}
}
elected_candidates.push((winner.who, winner.approval_stake as ExtendedBalance));
} else {
break;
}
}
for n in &mut voters {
let mut assignment = (n.who, vec![]);
for e in &mut n.edges {
if let Some(c) =
elected_candidates.iter().cloned().map(|(c, _)| c).find(|c| *c == e.who)
{
if c != n.who {
let ratio = e.load / n.load;
assignment.1.push((e.who, ratio));
}
}
}
if assignment.1.len() > 0 {
assigned.push(assignment);
}
}
Some(_ElectionResult { winners: elected_candidates, assignments: assigned })
}
pub(crate) fn equalize_float<A, FS>(
mut assignments: Vec<(A, Vec<_Assignment<A>>)>,
supports: &mut _SupportMap<A>,
tolerance: f64,
iterations: usize,
stake_of: FS,
) where
for<'r> FS: Fn(&'r A) -> VoteWeight,
A: Ord + Clone + std::fmt::Debug,
{
for _i in 0..iterations {
let mut max_diff = 0.0;
for (voter, assignment) in assignments.iter_mut() {
let voter_budget = stake_of(&voter);
let diff = do_equalize_float(voter, voter_budget, assignment, supports, tolerance);
if diff > max_diff {
max_diff = diff;
}
}
if max_diff < tolerance {
break;
}
}
}
pub(crate) fn do_equalize_float<A>(
voter: &A,
budget_balance: VoteWeight,
elected_edges: &mut Vec<_Assignment<A>>,
support_map: &mut _SupportMap<A>,
tolerance: f64,
) -> f64
where
A: Ord + Clone,
{
let budget = budget_balance as f64;
if elected_edges.is_empty() {
return 0.0;
}
let stake_used = elected_edges.iter().fold(0.0, |s, e| s + e.1);
let backed_stakes_iter =
elected_edges.iter().filter_map(|e| support_map.get(&e.0)).map(|e| e.total);
let backing_backed_stake = elected_edges
.iter()
.filter(|e| e.1 > 0.0)
.filter_map(|e| support_map.get(&e.0))
.map(|e| e.total)
.collect::<Vec<f64>>();
let mut difference;
if backing_backed_stake.len() > 0 {
let max_stake = backing_backed_stake
.iter()
.max_by(|x, y| x.partial_cmp(&y).unwrap_or(core::cmp::Ordering::Equal))
.expect("vector with positive length will have a max; qed");
let min_stake = backed_stakes_iter
.min_by(|x, y| x.partial_cmp(&y).unwrap_or(core::cmp::Ordering::Equal))
.expect("iterator with positive length will have a min; qed");
difference = max_stake - min_stake;
difference = difference + budget - stake_used;
if difference < tolerance {
return difference;
}
} else {
difference = budget;
}
// Undo updates to support
elected_edges.iter_mut().for_each(|e| {
if let Some(support) = support_map.get_mut(&e.0) {
support.total = support.total - e.1;
support.others.retain(|i_support| i_support.0 != *voter);
}
e.1 = 0.0;
});
elected_edges.sort_by(|x, y| {
support_map
.get(&x.0)
.and_then(|x| support_map.get(&y.0).and_then(|y| x.total.partial_cmp(&y.total)))
.unwrap_or(core::cmp::Ordering::Equal)
});
let mut cumulative_stake = 0.0;
let mut last_index = elected_edges.len() - 1;
elected_edges.iter_mut().enumerate().for_each(|(idx, e)| {
if let Some(support) = support_map.get_mut(&e.0) {
let stake = support.total;
let stake_mul = stake * (idx as f64);
let stake_sub = stake_mul - cumulative_stake;
if stake_sub > budget {
last_index = idx.checked_sub(1).unwrap_or(0);
return;
}
cumulative_stake = cumulative_stake + stake;
}
});
let last_stake = elected_edges[last_index].1;
let split_ways = last_index + 1;
let excess = budget + cumulative_stake - last_stake * (split_ways as f64);
elected_edges.iter_mut().take(split_ways).for_each(|e| {
if let Some(support) = support_map.get_mut(&e.0) {
e.1 = excess / (split_ways as f64) + last_stake - support.total;
support.total = support.total + e.1;
support.others.push((voter.clone(), e.1));
}
});
difference
}
pub(crate) fn create_stake_of(
stakes: &[(AccountId, VoteWeight)],
) -> impl Fn(&AccountId) -> VoteWeight {
let mut storage = BTreeMap::<AccountId, VoteWeight>::new();
stakes.iter().for_each(|s| {
storage.insert(s.0, s.1);
});
move |who: &AccountId| -> VoteWeight { storage.get(who).unwrap().to_owned() }
}
pub fn check_assignments_sum<T: PerThing>(assignments: &[Assignment<AccountId, T>]) {
for Assignment { distribution, .. } in assignments {
let mut sum: u128 = Zero::zero();
distribution
.iter()
.for_each(|(_, p)| sum += p.deconstruct().saturated_into::<u128>());
assert_eq!(sum, T::ACCURACY.saturated_into(), "Assignment ratio sum is not 100%");
}
}
pub(crate) fn run_and_compare<Output: PerThing128, FS>(
candidates: Vec<AccountId>,
voters: Vec<(AccountId, Vec<AccountId>)>,
stake_of: FS,
to_elect: usize,
) where
Output: PerThing128,
FS: Fn(&AccountId) -> VoteWeight,
{
// run fixed point code.
let ElectionResult::<_, Output> { winners, assignments } = seq_phragmen(
to_elect,
candidates.clone(),
voters
.iter()
.map(|(ref v, ref vs)| (*v, stake_of(v), vs.clone()))
.collect::<Vec<_>>(),
None,
)
.unwrap();
// run float poc code.
let truth_value = elect_float(to_elect, candidates, voters, &stake_of).unwrap();
assert_eq!(
winners.iter().map(|(x, _)| x).collect::<Vec<_>>(),
truth_value.winners.iter().map(|(x, _)| x).collect::<Vec<_>>()
);
for Assignment { who, distribution } in assignments.iter() {
if let Some(float_assignments) = truth_value.assignments.iter().find(|x| x.0 == *who) {
for (candidate, per_thingy) in distribution {
if let Some(float_assignment) =
float_assignments.1.iter().find(|x| x.0 == *candidate)
{
assert_eq_error_rate!(
Output::from_float(float_assignment.1).deconstruct(),
per_thingy.deconstruct(),
Output::Inner::one(),
);
} else {
panic!(
"candidate mismatch. This should never happen. could not find ({:?}, {:?})",
candidate, per_thingy,
)
}
}
} else {
panic!("nominator mismatch. This should never happen.")
}
}
check_assignments_sum(&assignments);
}
pub(crate) fn build_support_map_float(
result: &mut _ElectionResult<AccountId>,
stake_of: impl Fn(&AccountId) -> VoteWeight,
) -> _SupportMap<AccountId> {
let mut supports = <_SupportMap<AccountId>>::new();
result.winners.iter().map(|(e, _)| (e, stake_of(e) as f64)).for_each(|(e, s)| {
let item = _Support { own: s, total: s, ..Default::default() };
supports.insert(*e, item);
});
for (n, assignment) in result.assignments.iter_mut() {
for (c, r) in assignment.iter_mut() {
let nominator_stake = stake_of(n) as f64;
let other_stake = nominator_stake * *r;
if let Some(support) = supports.get_mut(c) {
support.total = support.total + other_stake;
support.others.push((*n, other_stake));
}
*r = other_stake;
}
}
supports
}
@@ -0,0 +1,261 @@
// This file is part of Bizinikiwi.
// Copyright (C) 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.
//! (very) Basic implementation of a graph node used in the reduce algorithm.
use alloc::{rc::Rc, vec::Vec};
use core::{cell::RefCell, fmt};
/// The role that a node can accept.
#[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Debug)]
pub(crate) enum NodeRole {
/// A voter. This is synonym to a nominator in a staking context.
Voter,
/// A target. This is synonym to a candidate/validator in a staking context.
Target,
}
pub(crate) type RefCellOf<T> = Rc<RefCell<T>>;
pub(crate) type NodeRef<A> = RefCellOf<Node<A>>;
/// Identifier of a node. This is particularly handy to have a proper `PartialEq` implementation.
/// Otherwise, self votes wouldn't have been indistinguishable.
#[derive(PartialOrd, Ord, Clone, PartialEq, Eq)]
pub(crate) struct NodeId<A> {
/// An account-like identifier representing the node.
pub who: A,
/// The role of the node.
pub role: NodeRole,
}
impl<A> NodeId<A> {
/// Create a new [`NodeId`].
pub fn from(who: A, role: NodeRole) -> Self {
Self { who, role }
}
}
#[cfg(feature = "std")]
impl<A: fmt::Debug> fmt::Debug for NodeId<A> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Node({:?}, {:?})",
self.who,
if self.role == NodeRole::Voter { "V" } else { "T" }
)
}
}
/// A one-way graph note. This can only store a pointer to its parent.
#[derive(Clone)]
pub(crate) struct Node<A> {
/// The identifier of the note.
pub(crate) id: NodeId<A>,
/// The parent pointer.
pub(crate) parent: Option<NodeRef<A>>,
}
impl<A: PartialEq> PartialEq for Node<A> {
fn eq(&self, other: &Node<A>) -> bool {
self.id == other.id
}
}
impl<A: PartialEq> Eq for Node<A> {}
#[cfg(feature = "std")]
impl<A: fmt::Debug + Clone> fmt::Debug for Node<A> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({:?} --> {:?})", self.id, self.parent.as_ref().map(|p| p.borrow().id.clone()))
}
}
impl<A: PartialEq + Eq + Clone + fmt::Debug> Node<A> {
/// Create a new [`Node`]
pub fn new(id: NodeId<A>) -> Node<A> {
Self { id, parent: None }
}
/// Returns true if `other` is the parent of `who`.
pub fn is_parent_of(who: &NodeRef<A>, other: &NodeRef<A>) -> bool {
if who.borrow().parent.is_none() {
return false;
}
who.borrow().parent.as_ref() == Some(other)
}
/// Removes the parent of `who`.
pub fn remove_parent(who: &NodeRef<A>) {
who.borrow_mut().parent = None;
}
/// Sets `who`'s parent to be `parent`.
pub fn set_parent_of(who: &NodeRef<A>, parent: &NodeRef<A>) {
who.borrow_mut().parent = Some(parent.clone());
}
/// Finds the root of `start`. It return a tuple of `(root, root_vec)` where `root_vec` is the
/// vector of Nodes leading to the root. Hence the first element is the start itself and the
/// last one is the root. As convenient, the root itself is also returned as the first element
/// of the tuple.
///
/// This function detects cycles and breaks as soon a duplicate node is visited, returning the
/// cycle up to but not including the duplicate node.
///
/// If you are certain that no cycles exist, you can use [`root_unchecked`].
pub fn root(start: &NodeRef<A>) -> (NodeRef<A>, Vec<NodeRef<A>>) {
let mut parent_path: Vec<NodeRef<A>> = Vec::new();
let mut visited: Vec<NodeRef<A>> = Vec::new();
parent_path.push(start.clone());
visited.push(start.clone());
let mut current = start.clone();
while let Some(ref next_parent) = current.clone().borrow().parent {
if visited.contains(next_parent) {
break;
}
parent_path.push(next_parent.clone());
current = next_parent.clone();
visited.push(current.clone());
}
(current, parent_path)
}
/// Consumes self and wraps it in a `Rc<RefCell<T>>`. This type can be used as the pointer type
/// to a parent node.
pub fn into_ref(self) -> NodeRef<A> {
Rc::from(RefCell::from(self))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn id(i: u32) -> NodeId<u32> {
NodeId::from(i, NodeRole::Target)
}
#[test]
fn basic_create_works() {
let node = Node::new(id(10));
assert_eq!(node, Node { id: NodeId { who: 10, role: NodeRole::Target }, parent: None });
}
#[test]
fn set_parent_works() {
let a = Node::new(id(10)).into_ref();
let b = Node::new(id(20)).into_ref();
assert_eq!(a.borrow().parent, None);
Node::set_parent_of(&a, &b);
assert_eq!(*a.borrow().parent.as_ref().unwrap(), b);
}
#[test]
fn get_root_singular() {
let a = Node::new(id(1)).into_ref();
assert_eq!(Node::root(&a), (a.clone(), vec![a.clone()]));
}
#[test]
fn get_root_works() {
// D <-- A <-- B <-- C
// \
// <-- E
let a = Node::new(id(1)).into_ref();
let b = Node::new(id(2)).into_ref();
let c = Node::new(id(3)).into_ref();
let d = Node::new(id(4)).into_ref();
let e = Node::new(id(5)).into_ref();
let f = Node::new(id(6)).into_ref();
Node::set_parent_of(&c, &b);
Node::set_parent_of(&b, &a);
Node::set_parent_of(&e, &a);
Node::set_parent_of(&a, &d);
assert_eq!(Node::root(&e), (d.clone(), vec![e.clone(), a.clone(), d.clone()]));
assert_eq!(Node::root(&a), (d.clone(), vec![a.clone(), d.clone()]));
assert_eq!(Node::root(&c), (d.clone(), vec![c.clone(), b.clone(), a.clone(), d.clone()]));
// D A <-- B <-- C
// F <-- / \
// <-- E
Node::set_parent_of(&a, &f);
assert_eq!(Node::root(&a), (f.clone(), vec![a.clone(), f.clone()]));
assert_eq!(Node::root(&c), (f.clone(), vec![c.clone(), b.clone(), a.clone(), f.clone()]));
}
#[test]
fn get_root_on_cycle() {
// A ---> B
// | |
// <---- C
let a = Node::new(id(1)).into_ref();
let b = Node::new(id(2)).into_ref();
let c = Node::new(id(3)).into_ref();
Node::set_parent_of(&a, &b);
Node::set_parent_of(&b, &c);
Node::set_parent_of(&c, &a);
let (root, path) = Node::root(&a);
assert_eq!(root, c);
assert_eq!(path.clone(), vec![a.clone(), b.clone(), c.clone()]);
}
#[test]
fn get_root_on_cycle_2() {
// A ---> B
// | | |
// - C
let a = Node::new(id(1)).into_ref();
let b = Node::new(id(2)).into_ref();
let c = Node::new(id(3)).into_ref();
Node::set_parent_of(&a, &b);
Node::set_parent_of(&b, &c);
Node::set_parent_of(&c, &b);
let (root, path) = Node::root(&a);
assert_eq!(root, c);
assert_eq!(path.clone(), vec![a.clone(), b.clone(), c.clone()]);
}
#[test]
fn node_cmp_stack_overflows_on_non_unique_elements() {
// To make sure we don't stack overflow on duplicate who. This needs manual impl of
// PartialEq.
let a = Node::new(id(1)).into_ref();
let b = Node::new(id(2)).into_ref();
let c = Node::new(id(3)).into_ref();
Node::set_parent_of(&a, &b);
Node::set_parent_of(&b, &c);
Node::set_parent_of(&c, &a);
Node::root(&a);
}
}
@@ -0,0 +1,212 @@
// This file is part of Bizinikiwi.
// Copyright (C) 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.
//! Implementation of the sequential-phragmen election method.
//!
//! This method is ensured to achieve PJR, yet, it does not achieve a constant factor approximation
//! to the Maximin problem.
use crate::{
balancing, setup_inputs, BalancingConfig, CandidatePtr, ElectionResult, ExtendedBalance,
IdentifierT, PerThing128, VoteWeight, Voter,
};
use alloc::vec::Vec;
use pezsp_arithmetic::{
helpers_128bit::multiply_by_rational_with_rounding,
traits::{Bounded, Zero},
Rational128, Rounding,
};
/// The denominator used for loads. Since votes are collected as u64, the smallest ratio that we
/// might collect is `1/approval_stake` where approval stake is the sum of votes. Hence, some number
/// bigger than u64::MAX is needed. For maximum accuracy we simply use u128;
const DEN: ExtendedBalance = ExtendedBalance::max_value();
/// Execute sequential phragmen with potentially some rounds of `balancing`. The return type is list
/// of winners and a weight distribution vector of all voters who contribute to the winners.
///
/// - This function is a best effort to elect `rounds` members. Nonetheless, if less candidates are
/// available, it will only return what is available. It is the responsibility of the call site to
/// ensure they have provided enough members.
/// - If `balance` parameter is `Some(i, t)`, `i` iterations of balancing is with tolerance `t` is
/// performed.
/// - Returning winners are sorted based on desirability. Voters are unsorted. Nonetheless,
/// seq-phragmen is in general an un-ranked election and the desirability should not be
/// interpreted with any significance.
/// - The returning winners are zipped with their final backing stake. Yet, to get the exact final
/// weight distribution from the winner's point of view, one needs to build a support map. See
/// [`crate::SupportMap`] for more info. Note that this backing stake is computed in
/// ExtendedBalance and may be slightly different that what will be computed from the support map,
/// due to accuracy loss.
/// - The accuracy of the returning edge weight ratios can be configured via the `P` generic
/// argument.
/// - The returning weight distribution is _normalized_, meaning that it is guaranteed that the sum
/// of the ratios in each voter's distribution sums up to exactly `P::one()`.
///
/// This can only fail of the normalization fails. This can happen if for any of the resulting
/// assignments, `assignment.distribution.map(|p| p.deconstruct()).sum()` fails to fit inside
/// `UpperOf<P>`. A user of this crate may statically assert that this can never happen and safely
/// `expect` this to return `Ok`.
///
/// This can only fail if the normalization fails.
///
/// Note that rounding errors can potentially cause the output of this function to fail a t-PJR
/// check where t is the standard threshold. The underlying algorithm is sound, but the conversions
/// between numeric types can be lossy.
pub fn seq_phragmen<AccountId: IdentifierT, P: PerThing128>(
to_elect: usize,
candidates: Vec<AccountId>,
voters: Vec<(AccountId, VoteWeight, impl IntoIterator<Item = AccountId>)>,
balancing: Option<BalancingConfig>,
) -> Result<ElectionResult<AccountId, P>, crate::Error> {
let (candidates, voters) = setup_inputs(candidates, voters);
let (candidates, mut voters) = seq_phragmen_core::<AccountId>(to_elect, candidates, voters)?;
if let Some(ref config) = balancing {
// NOTE: might create zero-edges, but we will strip them again when we convert voter into
// assignment.
let _iters = balancing::balance::<AccountId>(&mut voters, config);
}
let mut winners = candidates
.into_iter()
.filter(|c_ptr| c_ptr.borrow().elected)
// defensive only: seq-phragmen-core returns only up to rounds.
.take(to_elect)
.collect::<Vec<_>>();
// sort winners based on desirability.
winners.sort_by_key(|c_ptr| c_ptr.borrow().round);
let mut assignments =
voters.into_iter().filter_map(|v| v.into_assignment()).collect::<Vec<_>>();
assignments
.iter_mut()
.try_for_each(|a| a.try_normalize().map_err(|_| crate::Error::ArithmeticError))?;
let winners = winners
.into_iter()
.map(|w_ptr| (w_ptr.borrow().who.clone(), w_ptr.borrow().backed_stake))
.collect();
Ok(ElectionResult { winners, assignments })
}
/// Core implementation of seq-phragmen.
///
/// This is the internal implementation that works with the types defined in this crate. see
/// `seq_phragmen` for more information. This function is left public in case a crate needs to use
/// the implementation in a custom way.
///
/// This can only fail if the normalization fails.
// To create the inputs needed for this function, see [`crate::setup_inputs`].
pub fn seq_phragmen_core<AccountId: IdentifierT>(
to_elect: usize,
candidates: Vec<CandidatePtr<AccountId>>,
mut voters: Vec<Voter<AccountId>>,
) -> Result<(Vec<CandidatePtr<AccountId>>, Vec<Voter<AccountId>>), crate::Error> {
// we have already checked that we have more candidates than minimum_candidate_count.
let to_elect = to_elect.min(candidates.len());
// main election loop
for round in 0..to_elect {
// loop 1: initialize score
for c_ptr in &candidates {
let mut candidate = c_ptr.borrow_mut();
if !candidate.elected {
// 1 / approval_stake == (DEN / approval_stake) / DEN. If approval_stake is zero,
// then the ratio should be as large as possible, essentially `infinity`.
if candidate.approval_stake.is_zero() {
candidate.score = Bounded::max_value();
} else {
candidate.score = Rational128::from(DEN / candidate.approval_stake, DEN);
}
}
}
// loop 2: increment score
for voter in &voters {
for edge in &voter.edges {
let mut candidate = edge.candidate.borrow_mut();
if !candidate.elected && !candidate.approval_stake.is_zero() {
let temp_n = multiply_by_rational_with_rounding(
voter.load.n(),
voter.budget,
candidate.approval_stake,
Rounding::Down,
)
.unwrap_or(Bounded::max_value());
let temp_d = voter.load.d();
let temp = Rational128::from(temp_n, temp_d);
candidate.score = candidate.score.lazy_saturating_add(temp);
}
}
}
// loop 3: find the best
if let Some(winner_ptr) = candidates
.iter()
.filter(|c| !c.borrow().elected)
.min_by_key(|c| c.borrow().score)
{
let mut winner = winner_ptr.borrow_mut();
// loop 3: update voter and edge load
winner.elected = true;
winner.round = round;
for voter in &mut voters {
for edge in &mut voter.edges {
if edge.who == winner.who {
edge.load = winner.score.lazy_saturating_sub(voter.load);
voter.load = winner.score;
}
}
}
} else {
break;
}
}
// update backing stake of candidates and voters
for voter in &mut voters {
for edge in &mut voter.edges {
if edge.candidate.borrow().elected {
// update internal state.
edge.weight = multiply_by_rational_with_rounding(
voter.budget,
edge.load.n(),
voter.load.n(),
Rounding::Down,
)
// If result cannot fit in u128. Not much we can do about it.
.unwrap_or(Bounded::max_value());
} else {
edge.weight = 0
}
let mut candidate = edge.candidate.borrow_mut();
candidate.backed_stake = candidate.backed_stake.saturating_add(edge.weight);
}
// remove all zero edges. These can become phantom edges during normalization.
voter.edges.retain(|e| e.weight > 0);
// edge of all candidates that eventually have a non-zero weight must be elected.
debug_assert!(voter.edges.iter().all(|e| e.candidate.borrow().elected));
// inc budget to sum the budget.
voter.try_normalize_elected().map_err(|_| crate::Error::ArithmeticError)?;
}
Ok((candidates, voters))
}
@@ -0,0 +1,404 @@
// This file is part of Bizinikiwi.
// Copyright (C) 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.
//! Implementation of the PhragMMS method.
//!
//! The naming comes from the fact that this method is highly inspired by Phragmen's method, yet it
//! _also_ provides a constant factor approximation of the Maximin problem, similar to that of the
//! MMS algorithm.
use crate::{
balance, setup_inputs, BalancingConfig, CandidatePtr, ElectionResult, ExtendedBalance,
IdentifierT, PerThing128, VoteWeight, Voter,
};
use alloc::{rc::Rc, vec, vec::Vec};
use pezsp_arithmetic::{traits::Bounded, PerThing, Rational128};
/// Execute the phragmms method.
///
/// This can be used interchangeably with `seq-phragmen` and offers a similar API, namely:
///
/// - The resulting edge weight distribution is normalized (thus, safe to use for submission).
/// - The accuracy can be configured via the generic type `P`.
/// - The algorithm is a _best-effort_ to elect `to_elect`. If less candidates are provided, less
/// winners are returned, without an error.
///
/// This can only fail if the normalization fails. This can happen if for any of the resulting
/// assignments, `assignment.distribution.map(|p| p.deconstruct()).sum()` fails to fit inside
/// `UpperOf<P>`. A user of this crate may statically assert that this can never happen and safely
/// `expect` this to return `Ok`.
pub fn phragmms<AccountId: IdentifierT, P: PerThing128>(
to_elect: usize,
candidates: Vec<AccountId>,
voters: Vec<(AccountId, VoteWeight, impl IntoIterator<Item = AccountId>)>,
balancing: Option<BalancingConfig>,
) -> Result<ElectionResult<AccountId, P>, crate::Error> {
let (candidates, mut voters) = setup_inputs(candidates, voters);
let mut winners = vec![];
for round in 0..to_elect {
if let Some(round_winner) = calculate_max_score::<AccountId, P>(&candidates, &voters) {
apply_elected::<AccountId>(&mut voters, Rc::clone(&round_winner));
round_winner.borrow_mut().round = round;
round_winner.borrow_mut().elected = true;
winners.push(round_winner);
if let Some(ref config) = balancing {
balance(&mut voters, config);
}
} else {
break;
}
}
let mut assignments =
voters.into_iter().filter_map(|v| v.into_assignment()).collect::<Vec<_>>();
assignments
.iter_mut()
.try_for_each(|a| a.try_normalize())
.map_err(|_| crate::Error::ArithmeticError)?;
let winners = winners
.into_iter()
.map(|w_ptr| (w_ptr.borrow().who.clone(), w_ptr.borrow().backed_stake))
.collect();
Ok(ElectionResult { winners, assignments })
}
/// Find the candidate that can yield the maximum score for this round.
///
/// Returns a new `Some(CandidatePtr)` to the winner candidate. The score of the candidate is
/// updated and can be read from the returned pointer.
///
/// If no winner can be determined (i.e. everyone is already elected), then `None` is returned.
///
/// This is an internal part of the [`phragmms`].
pub(crate) fn calculate_max_score<AccountId: IdentifierT, P: PerThing>(
candidates: &[CandidatePtr<AccountId>],
voters: &[Voter<AccountId>],
) -> Option<CandidatePtr<AccountId>> {
for c_ptr in candidates.iter() {
let mut candidate = c_ptr.borrow_mut();
if !candidate.elected {
candidate.score = Rational128::from(1, P::ACCURACY.into());
}
}
for voter in voters.iter() {
let mut denominator_contribution: ExtendedBalance = 0;
// gather contribution from all elected edges.
for edge in voter.edges.iter() {
let edge_candidate = edge.candidate.borrow();
if edge_candidate.elected {
let edge_contribution: ExtendedBalance =
P::from_rational(edge.weight, edge_candidate.backed_stake).deconstruct().into();
denominator_contribution += edge_contribution;
}
}
// distribute to all _unelected_ edges.
for edge in voter.edges.iter() {
let mut edge_candidate = edge.candidate.borrow_mut();
if !edge_candidate.elected {
let prev_d = edge_candidate.score.d();
edge_candidate.score = Rational128::from(1, denominator_contribution + prev_d);
}
}
}
// finalise the score value, and find the best.
let mut best_score = Rational128::zero();
let mut best_candidate = None;
for c_ptr in candidates.iter() {
let mut candidate = c_ptr.borrow_mut();
if candidate.approval_stake > 0 {
// finalise the score value.
let score_d = candidate.score.d();
let one: ExtendedBalance = P::ACCURACY.into();
// Note: the accuracy here is questionable.
// First, let's consider what will happen if this saturates. In this case, two very
// whale-like validators will be effectively the same and their score will be equal.
// This is, more or less fine if the threshold of saturation is high and only a small
// subset or ever likely to become saturated. Once saturated, the score of these whales
// are effectively the same.
// Let's consider when this will happen. The approval stake of a target is the sum of
// stake of all the voter who have backed this target. Given the fact that the total
// issuance of a sane chain will fit in u128, it is safe to also assume that the
// approval stake will, since it is a subset of the total issuance at most.
// Finally, the only chance of overflow is multiplication by `one`. This highly depends
// on the `P` generic argument. With a PerBill and a 12 decimal token the maximum value
// that `candidate.approval_stake` can have is:
// (2 ** 128 - 1) / 10**9 / 10**12 = 340,282,366,920,938,463
// Assuming that each target will have 200,000 voters, then each voter's stake can be
// roughly:
// (2 ** 128 - 1) / 10**9 / 10**12 / 200000 = 1,701,411,834,604
//
// It is worth noting that these value would be _very_ different if one were to use
// `PerQuintill` as `P`. For now, we prefer the performance of using `Rational128` here.
// For the future, a properly benchmarked pull request can prove that using
// `RationalInfinite` as the score type does not introduce significant overhead. Then we
// can switch the score type to `RationalInfinite` and ensure compatibility with any
// crazy token scale.
let score_n =
candidate.approval_stake.checked_mul(one).unwrap_or_else(Bounded::max_value);
candidate.score = Rational128::from(score_n, score_d);
// check if we have a new winner.
if !candidate.elected && candidate.score > best_score {
best_score = candidate.score;
best_candidate = Some(Rc::clone(c_ptr));
}
} else {
candidate.score = Rational128::zero();
}
}
best_candidate
}
/// Update the weights of `voters` given that `elected_ptr` has been elected in the previous round.
///
/// Updates `voters` in place.
///
/// This is an internal part of the [`phragmms`] and should be called after
/// [`calculate_max_score`].
pub(crate) fn apply_elected<AccountId: IdentifierT>(
voters: &mut Vec<Voter<AccountId>>,
elected_ptr: CandidatePtr<AccountId>,
) {
let elected_who = elected_ptr.borrow().who.clone();
let cutoff = elected_ptr
.borrow()
.score
.to_den(1)
.expect("(n / d) < u128::MAX and (n' / 1) == (n / d), thus n' < u128::MAX'; qed.")
.n();
let mut elected_backed_stake = elected_ptr.borrow().backed_stake;
for voter in voters {
if let Some(new_edge_index) = voter.edges.iter().position(|e| e.who == elected_who) {
let used_budget: ExtendedBalance = voter.edges.iter().map(|e| e.weight).sum();
let mut new_edge_weight = voter.budget.saturating_sub(used_budget);
elected_backed_stake = elected_backed_stake.saturating_add(new_edge_weight);
// Iterate over all other edges.
for (_, edge) in
voter.edges.iter_mut().enumerate().filter(|(edge_index, edge_inner)| {
*edge_index != new_edge_index && edge_inner.weight > 0
}) {
let mut edge_candidate = edge.candidate.borrow_mut();
if edge_candidate.backed_stake > cutoff {
let stake_to_take =
edge.weight.saturating_mul(cutoff) / edge_candidate.backed_stake.max(1);
// subtract this amount from this edge.
edge.weight = edge.weight.saturating_sub(stake_to_take);
edge_candidate.backed_stake =
edge_candidate.backed_stake.saturating_sub(stake_to_take);
// inject it into the outer loop's edge.
elected_backed_stake = elected_backed_stake.saturating_add(stake_to_take);
new_edge_weight = new_edge_weight.saturating_add(stake_to_take);
}
}
voter.edges[new_edge_index].weight = new_edge_weight;
}
}
// final update.
elected_ptr.borrow_mut().backed_stake = elected_backed_stake;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Assignment, ElectionResult};
use alloc::rc::Rc;
use pezsp_runtime::{Perbill, Percent};
#[test]
fn basic_election_manual_works() {
//! Manually run the internal steps of phragmms. In each round we select a new winner by
//! `max_score`, then apply this change by `apply_elected`, and finally do a `balance`
//! round.
let candidates = vec![1, 2, 3];
let voters = vec![(10, 10, vec![1, 2]), (20, 20, vec![1, 3]), (30, 30, vec![2, 3])];
let (candidates, mut voters) = setup_inputs(candidates, voters);
// Round 1
let winner =
calculate_max_score::<u32, Percent>(candidates.as_ref(), voters.as_ref()).unwrap();
assert_eq!(winner.borrow().who, 3);
assert_eq!(winner.borrow().score, 50u32.into());
apply_elected(&mut voters, Rc::clone(&winner));
assert_eq!(
voters
.iter()
.find(|x| x.who == 30)
.map(|v| (v.who, v.edges.iter().map(|e| (e.who, e.weight)).collect::<Vec<_>>()))
.unwrap(),
(30, vec![(2, 0), (3, 30)]),
);
assert_eq!(
voters
.iter()
.find(|x| x.who == 20)
.map(|v| (v.who, v.edges.iter().map(|e| (e.who, e.weight)).collect::<Vec<_>>()))
.unwrap(),
(20, vec![(1, 0), (3, 20)]),
);
// finish the round.
winner.borrow_mut().elected = true;
winner.borrow_mut().round = 0;
drop(winner);
// balancing makes no difference here but anyhow.
let config = BalancingConfig { iterations: 10, tolerance: 0 };
balance(&mut voters, &config);
// round 2
let winner =
calculate_max_score::<u32, Percent>(candidates.as_ref(), voters.as_ref()).unwrap();
assert_eq!(winner.borrow().who, 2);
assert_eq!(winner.borrow().score, 25u32.into());
apply_elected(&mut voters, Rc::clone(&winner));
assert_eq!(
voters
.iter()
.find(|x| x.who == 30)
.map(|v| (v.who, v.edges.iter().map(|e| (e.who, e.weight)).collect::<Vec<_>>()))
.unwrap(),
(30, vec![(2, 15), (3, 15)]),
);
assert_eq!(
voters
.iter()
.find(|x| x.who == 20)
.map(|v| (v.who, v.edges.iter().map(|e| (e.who, e.weight)).collect::<Vec<_>>()))
.unwrap(),
(20, vec![(1, 0), (3, 20)]),
);
assert_eq!(
voters
.iter()
.find(|x| x.who == 10)
.map(|v| (v.who, v.edges.iter().map(|e| (e.who, e.weight)).collect::<Vec<_>>()))
.unwrap(),
(10, vec![(1, 0), (2, 10)]),
);
// finish the round.
winner.borrow_mut().elected = true;
winner.borrow_mut().round = 0;
drop(winner);
// balancing will improve stuff here.
balance(&mut voters, &config);
assert_eq!(
voters
.iter()
.find(|x| x.who == 30)
.map(|v| (v.who, v.edges.iter().map(|e| (e.who, e.weight)).collect::<Vec<_>>()))
.unwrap(),
(30, vec![(2, 20), (3, 10)]),
);
assert_eq!(
voters
.iter()
.find(|x| x.who == 20)
.map(|v| (v.who, v.edges.iter().map(|e| (e.who, e.weight)).collect::<Vec<_>>()))
.unwrap(),
(20, vec![(1, 0), (3, 20)]),
);
assert_eq!(
voters
.iter()
.find(|x| x.who == 10)
.map(|v| (v.who, v.edges.iter().map(|e| (e.who, e.weight)).collect::<Vec<_>>()))
.unwrap(),
(10, vec![(1, 0), (2, 10)]),
);
}
#[test]
fn basic_election_works() {
let candidates = vec![1, 2, 3];
let voters = vec![(10, 10, vec![1, 2]), (20, 20, vec![1, 3]), (30, 30, vec![2, 3])];
let config = BalancingConfig { iterations: 2, tolerance: 0 };
let ElectionResult::<_, Perbill> { winners, assignments } =
phragmms(2, candidates, voters, Some(config)).unwrap();
assert_eq!(winners, vec![(3, 30), (2, 30)]);
assert_eq!(
assignments,
vec![
Assignment { who: 10u64, distribution: vec![(2, Perbill::one())] },
Assignment { who: 20, distribution: vec![(3, Perbill::one())] },
Assignment {
who: 30,
distribution: vec![
(2, Perbill::from_parts(666666666)),
(3, Perbill::from_parts(333333334)),
],
},
]
)
}
#[test]
fn linear_voting_example_works() {
let candidates = vec![11, 21, 31, 41, 51, 61, 71];
let voters = vec![
(2, 2000, vec![11]),
(4, 1000, vec![11, 21]),
(6, 1000, vec![21, 31]),
(8, 1000, vec![31, 41]),
(110, 1000, vec![41, 51]),
(120, 1000, vec![51, 61]),
(130, 1000, vec![61, 71]),
];
let config = BalancingConfig { iterations: 2, tolerance: 0 };
let ElectionResult::<_, Perbill> { winners, assignments: _ } =
phragmms(4, candidates, voters, Some(config)).unwrap();
assert_eq!(winners, vec![(11, 3000), (31, 2000), (51, 1500), (61, 1500),]);
}
#[test]
fn large_balance_wont_overflow() {
let candidates = vec![1u32, 2, 3];
let mut voters = (0..1000).map(|i| (10 + i, u64::MAX, vec![1, 2, 3])).collect::<Vec<_>>();
// give a bit more to 1 and 3.
voters.push((2, u64::MAX, vec![1, 3]));
let config = BalancingConfig { iterations: 2, tolerance: 0 };
let ElectionResult::<_, Perbill> { winners, assignments: _ } =
phragmms(2, candidates, voters, Some(config)).unwrap();
assert_eq!(winners.into_iter().map(|(w, _)| w).collect::<Vec<_>>(), vec![1u32, 3]);
}
}
@@ -0,0 +1,626 @@
// This file is part of Bizinikiwi.
// Copyright (C) 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.
//! Implements functions and interfaces to check solutions for being t-PJR.
//!
//! PJR stands for proportional justified representation. PJR is an absolute measure to make
//! sure an NPoS solution adheres to a minimum standard.
//!
//! See [`pjr_check`] which is the main entry point of the module.
use crate::{
Candidate, CandidatePtr, Edge, ExtendedBalance, IdentifierT, Support, SupportMap, Supports,
VoteWeight, Voter,
};
use alloc::{collections::btree_map::BTreeMap, rc::Rc, vec::Vec};
use pezsp_arithmetic::{traits::Zero, Perbill};
/// The type used as the threshold.
///
/// Just some reading sugar; Must always be same as [`ExtendedBalance`];
type Threshold = ExtendedBalance;
/// Compute the threshold corresponding to the standard PJR property
///
/// `t-PJR` checks can check PJR according to an arbitrary threshold. The threshold can be any
/// value, but the property gets stronger as the threshold gets smaller. The strongest possible
/// `t-PJR` property corresponds to `t == 0`.
///
/// However, standard PJR is less stringent than that. This function returns the threshold whose
/// strength corresponds to the standard PJR property.
///
/// - `committee_size` is the number of winners of the election.
/// - `weights` is an iterator of voter stakes. If the sum of stakes is already known,
/// `std::iter::once(sum_of_stakes)` is appropriate here.
pub fn standard_threshold(
committee_size: usize,
weights: impl IntoIterator<Item = ExtendedBalance>,
) -> Threshold {
weights
.into_iter()
.fold(Threshold::zero(), |acc, elem| acc.saturating_add(elem)) /
committee_size.max(1) as Threshold
}
/// Check a solution to be PJR.
///
/// The PJR property is true if `t-PJR` is true when `t == sum(stake) / committee_size`.
pub fn pjr_check<AccountId: IdentifierT>(
supports: &Supports<AccountId>,
all_candidates: Vec<AccountId>,
all_voters: Vec<(AccountId, VoteWeight, Vec<AccountId>)>,
) -> Result<(), AccountId> {
let t = standard_threshold(
supports.len(),
all_voters.iter().map(|voter| voter.1 as ExtendedBalance),
);
t_pjr_check(supports, all_candidates, all_voters, t)
}
/// Check a solution to be t-PJR.
///
/// ### Semantics
///
/// The t-PJR property is defined in the paper ["Validator Election in Nominated
/// Proof-of-Stake"][NPoS], section 5, definition 1.
///
/// In plain language, the t-PJR condition is: if there is a group of `N` voters
/// who have `r` common candidates and can afford to support each of them with backing stake `t`
/// (i.e `sum(stake(v) for v in voters) == r * t`), then this committee needs to be represented by
/// at least `r` elected candidates.
///
/// Section 5 of the NPoS paper shows that this property can be tested by: for a feasible solution,
/// if `Max {score(c)} < t` where c is every unelected candidate, then this solution is t-PJR. There
/// may exist edge cases which satisfy the formal definition of t-PJR but do not pass this test, but
/// those should be rare enough that we can discount them.
///
/// ### Interface
///
/// In addition to data that can be computed from the [`Supports`] struct, a PJR check also
/// needs to inspect un-elected candidates and edges, thus `all_candidates` and `all_voters`.
///
/// [NPoS]: https://arxiv.org/pdf/2004.12990v1.pdf
// ### Implementation Notes
//
// The paper uses mathematical notation, which priorities single-symbol names. For programmer ease,
// we map these to more descriptive names as follows:
//
// C => all_candidates
// N => all_voters
// (A, w) => (candidates, voters)
//
// Note that while the names don't explicitly say so, `candidates` are the winning candidates, and
// `voters` is the set of weighted edges from nominators to winning validators.
pub fn t_pjr_check<AccountId: IdentifierT>(
supports: &Supports<AccountId>,
all_candidates: Vec<AccountId>,
all_voters: Vec<(AccountId, VoteWeight, Vec<AccountId>)>,
t: Threshold,
) -> Result<(), AccountId> {
// First order of business: derive `(candidates, voters)` from `supports`.
let (candidates, voters) = prepare_pjr_input(supports, all_candidates, all_voters);
// compute with threshold t.
pjr_check_core(candidates.as_ref(), voters.as_ref(), t)
}
/// The internal implementation of the PJR check after having the data converted.
///
/// [`pjr_check`] or [`t_pjr_check`] are typically easier to work with.
///
/// This function returns an `AccountId` in the `Err` case. This is the counter_example: the ID of
/// the unelected candidate with the highest prescore, such that `pre_score(counter_example) >= t`.
pub fn pjr_check_core<AccountId: IdentifierT>(
candidates: &[CandidatePtr<AccountId>],
voters: &[Voter<AccountId>],
t: Threshold,
) -> Result<(), AccountId> {
let unelected = candidates.iter().filter(|c| !c.borrow().elected);
let maybe_max_pre_score = unelected
.map(|c| (pre_score(Rc::clone(c), voters, t), c.borrow().who.clone()))
.max();
// if unelected is empty then the solution is indeed PJR.
match maybe_max_pre_score {
Some((max_pre_score, counter_example)) if max_pre_score >= t => Err(counter_example),
_ => Ok(()),
}
}
/// Validate a challenge to an election result.
///
/// A challenge to an election result is valid if there exists some counter_example for which
/// `pre_score(counter_example) >= threshold`. Validating an existing counter_example is
/// computationally cheaper than re-running the PJR check.
///
/// This function uses the standard threshold.
///
/// Returns `true` if the challenge is valid: the proposed solution does not satisfy PJR.
/// Returns `false` if the challenge is invalid: the proposed solution does in fact satisfy PJR.
pub fn validate_pjr_challenge<AccountId: IdentifierT>(
counter_example: AccountId,
supports: &Supports<AccountId>,
all_candidates: Vec<AccountId>,
all_voters: Vec<(AccountId, VoteWeight, Vec<AccountId>)>,
) -> bool {
let threshold = standard_threshold(
supports.len(),
all_voters.iter().map(|voter| voter.1 as ExtendedBalance),
);
validate_t_pjr_challenge(counter_example, supports, all_candidates, all_voters, threshold)
}
/// Validate a challenge to an election result.
///
/// A challenge to an election result is valid if there exists some counter_example for which
/// `pre_score(counter_example) >= threshold`. Validating an existing counter_example is
/// computationally cheaper than re-running the PJR check.
///
/// This function uses a supplied threshold.
///
/// Returns `true` if the challenge is valid: the proposed solution does not satisfy PJR.
/// Returns `false` if the challenge is invalid: the proposed solution does in fact satisfy PJR.
pub fn validate_t_pjr_challenge<AccountId: IdentifierT>(
counter_example: AccountId,
supports: &Supports<AccountId>,
all_candidates: Vec<AccountId>,
all_voters: Vec<(AccountId, VoteWeight, Vec<AccountId>)>,
threshold: Threshold,
) -> bool {
let (candidates, voters) = prepare_pjr_input(supports, all_candidates, all_voters);
validate_pjr_challenge_core(counter_example, &candidates, &voters, threshold)
}
/// Validate a challenge to an election result.
///
/// A challenge to an election result is valid if there exists some counter_example for which
/// `pre_score(counter_example) >= threshold`. Validating an existing counter_example is
/// computationally cheaper than re-running the PJR check.
///
/// Returns `true` if the challenge is valid: the proposed solution does not satisfy PJR.
/// Returns `false` if the challenge is invalid: the proposed solution does in fact satisfy PJR.
fn validate_pjr_challenge_core<AccountId: IdentifierT>(
counter_example: AccountId,
candidates: &[CandidatePtr<AccountId>],
voters: &[Voter<AccountId>],
threshold: Threshold,
) -> bool {
// Performing a linear search of the candidate list is not great, for obvious reasons. However,
// the alternatives are worse:
//
// - we could pre-sort the candidates list in `prepare_pjr_input` (n log n) which would let us
// binary search for the appropriate one here (log n). Overall runtime is `n log n` which is
// worse than the current runtime of `n`.
//
// - we could probably pre-sort the candidates list in `n` in `prepare_pjr_input` using some
// unsafe code leveraging the existing `candidates_index`: allocate an uninitialized vector of
// appropriate length, then copy in all the elements. We'd really prefer to avoid unsafe code
// in the runtime, though.
let candidate =
match candidates.iter().find(|candidate| candidate.borrow().who == counter_example) {
None => return false,
Some(candidate) => candidate.clone(),
};
pre_score(candidate, voters, threshold) >= threshold
}
/// Convert the data types that the user runtime has into ones that can be used by this module.
///
/// It is expected that this function's interface might change over time, or multiple variants of it
/// can be provided for different use cases.
///
/// The ultimate goal, in any case, is to convert the election data into [`Candidate`] and [`Voter`]
/// types defined by this crate, whilst setting correct value for some of their fields, namely:
/// 1. Candidate [`backing_stake`](Candidate::backing_stake) and [`elected`](Candidate::elected) if
/// they are a winner. 2. Voter edge [`weight`](Edge::weight) if they are backing a winner.
/// 3. Voter [`budget`](Voter::budget).
///
/// None of the `load` or `score` values are used and can be ignored. This is similar to
/// [`setup_inputs`] function of this crate.
///
/// ### Performance (Weight) Notes
///
/// Note that the current function is rather unfortunately inefficient. The most significant
/// slowdown is the fact that a typical solution that need to be checked for PJR only contains a
/// subset of the entire NPoS edge graph, encoded as `supports`. This only encodes the
/// edges that actually contribute to a winner's backing stake and ignores the rest to save space.
/// To check PJR, we need the entire voter set, including those edges that point to non-winners.
/// This could cause the caller runtime to have to read the entire list of voters, which is assumed
/// to be expensive.
///
/// A sensible user of this module should make sure that the PJR check is executed and checked as
/// little as possible, and take sufficient economical measures to ensure that this function cannot
/// be abused.
fn prepare_pjr_input<AccountId: IdentifierT>(
supports: &Supports<AccountId>,
all_candidates: Vec<AccountId>,
all_voters: Vec<(AccountId, VoteWeight, Vec<AccountId>)>,
) -> (Vec<CandidatePtr<AccountId>>, Vec<Voter<AccountId>>) {
let mut candidates_index: BTreeMap<AccountId, usize> = BTreeMap::new();
// dump the staked assignments in a voter-major map for faster access down the road.
let mut assignment_map: BTreeMap<AccountId, Vec<(AccountId, ExtendedBalance)>> =
BTreeMap::new();
for (winner_id, Support { voters, .. }) in supports.iter() {
for (voter_id, support) in voters.iter() {
assignment_map
.entry(voter_id.clone())
.or_default()
.push((winner_id.clone(), *support));
}
}
// Convert Supports into a SupportMap
//
// As a flat list, we're limited to linear search. That gives the production of `candidates`,
// below, a complexity of `O(s*c)`, where `s == supports.len()` and `c == all_candidates.len()`.
// For large lists, that's pretty bad.
//
// A `SupportMap`, as a `BTreeMap`, has access timing of `O(lg n)`. This means that constructing
// the map and then indexing from it gives us timing of `O((s + c) * lg(s))`. If in the future
// we get access to a deterministic `HashMap`, we can further improve that to `O(s+c)`.
//
// However, it does mean allocating sufficient space to store all the data again.
let supports: SupportMap<AccountId> = supports.iter().cloned().collect();
// collect all candidates and winners into a unified `Vec<CandidatePtr>`.
let candidates = all_candidates
.into_iter()
.enumerate()
.map(|(i, c)| {
candidates_index.insert(c.clone(), i);
// set the backing value and elected flag if the candidate is among the winners.
let who = c;
let maybe_support = supports.get(&who);
let elected = maybe_support.is_some();
let backed_stake = maybe_support.map(|support| support.total).unwrap_or_default();
Candidate {
who,
elected,
backed_stake,
score: Default::default(),
approval_stake: Default::default(),
round: Default::default(),
}
.to_ptr()
})
.collect::<Vec<_>>();
// collect all voters into a unified Vec<Voters>.
let voters = all_voters
.into_iter()
.map(|(v, w, ts)| {
let mut edges: Vec<Edge<AccountId>> = Vec::with_capacity(ts.len());
for t in ts {
if edges.iter().any(|e| e.who == t) {
// duplicate edge.
continue;
}
if let Some(idx) = candidates_index.get(&t) {
// if this edge is among the assignments, set the weight as well.
let weight = assignment_map
.get(&v)
.and_then(|d| {
d.iter().find_map(|(x, y)| if x == &t { Some(y) } else { None })
})
.cloned()
.unwrap_or_default();
edges.push(Edge {
who: t,
candidate: Rc::clone(&candidates[*idx]),
weight,
load: Default::default(),
});
}
}
let who = v;
let budget: ExtendedBalance = w.into();
Voter { who, budget, edges, load: Default::default() }
})
.collect::<Vec<_>>();
(candidates, voters)
}
/// The pre-score of an unelected candidate.
///
/// This is the amount of stake that *all voter* can spare to devote to this candidate without
/// allowing the backing stake of any other elected candidate to fall below `t`.
///
/// In essence, it is the sum(slack(n, t)) for all `n` who vote for `unelected`.
fn pre_score<AccountId: IdentifierT>(
unelected: CandidatePtr<AccountId>,
voters: &[Voter<AccountId>],
t: Threshold,
) -> ExtendedBalance {
debug_assert!(!unelected.borrow().elected);
voters
.iter()
.filter(|v| v.votes_for(&unelected.borrow().who))
.fold(Zero::zero(), |acc: ExtendedBalance, voter| acc.saturating_add(slack(voter, t)))
}
/// The slack of a voter at a given state.
///
/// The slack of each voter, with threshold `t` is the total amount of stake that this voter can
/// spare to a new potential member, whilst not dropping the backing stake of any of its currently
/// active members below `t`. In essence, for each of the current active candidates `c`, we assume
/// that we reduce the edge weight of `voter` to `c` from `w` to `w * min(1 / (t / support(c)))`.
///
/// More accurately:
///
/// 1. If `c` exactly has `t` backing or less, then we don't generate any slack.
/// 2. If `c` has more than `t`, then we reduce it to `t`.
fn slack<AccountId: IdentifierT>(voter: &Voter<AccountId>, t: Threshold) -> ExtendedBalance {
let budget = voter.budget;
let leftover = voter.edges.iter().fold(Zero::zero(), |acc: ExtendedBalance, edge| {
let candidate = edge.candidate.borrow();
if candidate.elected {
let extra =
Perbill::one().min(Perbill::from_rational(t, candidate.backed_stake)) * edge.weight;
acc.saturating_add(extra)
} else {
// No slack generated here.
acc
}
});
// NOTE: candidate for saturating_log_sub(). Defensive-only.
budget.saturating_sub(leftover)
}
#[cfg(test)]
mod tests {
use super::*;
fn setup_voter(who: u32, votes: Vec<(u32, u128, bool)>) -> Voter<u32> {
let mut voter = Voter::new(who);
let mut budget = 0u128;
let candidates = votes
.into_iter()
.map(|(t, w, e)| {
budget += w;
Candidate {
who: t,
elected: e,
backed_stake: w,
score: Default::default(),
approval_stake: Default::default(),
round: Default::default(),
}
})
.collect::<Vec<_>>();
let edges = candidates
.into_iter()
.map(|c| Edge {
who: c.who,
weight: c.backed_stake,
candidate: c.to_ptr(),
load: Default::default(),
})
.collect::<Vec<_>>();
voter.edges = edges;
voter.budget = budget;
voter
}
fn assert_core_failure<AccountId: IdentifierT>(
candidates: &[CandidatePtr<AccountId>],
voters: &[Voter<AccountId>],
t: Threshold,
) {
let counter_example = pjr_check_core(candidates, voters, t).unwrap_err();
assert!(validate_pjr_challenge_core(counter_example, candidates, voters, t));
}
#[test]
fn slack_works() {
let voter = setup_voter(10, vec![(1, 10, true), (2, 20, true)]);
assert_eq!(slack(&voter, 15), 5);
assert_eq!(slack(&voter, 17), 3);
assert_eq!(slack(&voter, 10), 10);
assert_eq!(slack(&voter, 5), 20);
}
#[test]
fn pre_score_works() {
// will give 5 slack
let v1 = setup_voter(10, vec![(1, 10, true), (2, 20, true), (3, 0, false)]);
// will give no slack
let v2 = setup_voter(20, vec![(1, 5, true), (2, 5, true)]);
// will give 10 slack.
let v3 = setup_voter(30, vec![(1, 20, true), (2, 20, true), (3, 0, false)]);
let unelected = Candidate {
who: 3u32,
elected: false,
score: Default::default(),
approval_stake: Default::default(),
backed_stake: Default::default(),
round: Default::default(),
}
.to_ptr();
let score = pre_score(unelected, &vec![v1, v2, v3], 15);
assert_eq!(score, 15);
}
#[test]
fn can_convert_data_from_external_api() {
let all_candidates = vec![10, 20, 30, 40];
let all_voters = vec![
(1, 10, vec![10, 20, 30, 40]),
(2, 20, vec![10, 20, 30, 40]),
(3, 30, vec![10, 30]),
];
// tuples in voters vector are (AccountId, Balance)
let supports: Supports<u32> = vec![
(20, Support { total: 15, voters: vec![(1, 5), (2, 10)] }),
(40, Support { total: 15, voters: vec![(1, 5), (2, 10)] }),
];
let (candidates, voters) = prepare_pjr_input(&supports, all_candidates, all_voters);
// elected flag and backing must be set correctly
assert_eq!(
candidates
.iter()
.map(|c| (c.borrow().who, c.borrow().elected, c.borrow().backed_stake))
.collect::<Vec<_>>(),
vec![(10, false, 0), (20, true, 15), (30, false, 0), (40, true, 15)],
);
// edge weight must be set correctly
assert_eq!(
voters
.iter()
.map(|v| (
v.who,
v.budget,
v.edges.iter().map(|e| (e.who, e.weight)).collect::<Vec<_>>(),
))
.collect::<Vec<_>>(),
vec![
(1, 10, vec![(10, 0), (20, 5), (30, 0), (40, 5)]),
(2, 20, vec![(10, 0), (20, 10), (30, 0), (40, 10)]),
(3, 30, vec![(10, 0), (30, 0)]),
],
);
// fyi. this is not PJR, obviously because the votes of 3 can bump the stake a lot but they
// are being ignored.
assert_core_failure(&candidates, &voters, 1);
assert_core_failure(&candidates, &voters, 10);
assert_core_failure(&candidates, &voters, 20);
}
// These next tests ensure that the threshold phase change property holds for us, but that's not
// their real purpose. They were written to help develop an intuition about what the threshold
// value actually means in layman's terms.
//
// The results tend to support the intuition that the threshold is the voting power at and below
// which a voter's preferences can simply be ignored.
#[test]
fn find_upper_bound_for_threshold_scenario_1() {
let all_candidates = vec![10, 20, 30, 40];
let all_voters = vec![
(1, 10, vec![10, 20, 30, 40]),
(2, 20, vec![10, 20, 30, 40]),
(3, 30, vec![10, 30]),
];
// tuples in voters vector are (AccountId, Balance)
let supports: Supports<u32> = vec![
(20, Support { total: 15, voters: vec![(1, 5), (2, 10)] }),
(40, Support { total: 15, voters: vec![(1, 5), (2, 10)] }),
];
let (candidates, voters) = prepare_pjr_input(&supports, all_candidates, all_voters);
find_threshold_phase_change_for_scenario(candidates, voters);
}
#[test]
fn find_upper_bound_for_threshold_scenario_2() {
let all_candidates = vec![10, 20, 30, 40];
let all_voters = vec![
(1, 10, vec![10, 20, 30, 40]),
(2, 20, vec![10, 20, 30, 40]),
(3, 25, vec![10, 30]),
];
// tuples in voters vector are (AccountId, Balance)
let supports: Supports<u32> = vec![
(20, Support { total: 15, voters: vec![(1, 5), (2, 10)] }),
(40, Support { total: 15, voters: vec![(1, 5), (2, 10)] }),
];
let (candidates, voters) = prepare_pjr_input(&supports, all_candidates, all_voters);
find_threshold_phase_change_for_scenario(candidates, voters);
}
#[test]
fn find_upper_bound_for_threshold_scenario_3() {
let all_candidates = vec![10, 20, 30, 40];
let all_voters = vec![
(1, 10, vec![10, 20, 30, 40]),
(2, 20, vec![10, 20, 30, 40]),
(3, 35, vec![10, 30]),
];
// tuples in voters vector are (AccountId, Balance)
let supports: Supports<u32> = vec![
(20, Support { total: 15, voters: vec![(1, 5), (2, 10)] }),
(40, Support { total: 15, voters: vec![(1, 5), (2, 10)] }),
];
let (candidates, voters) = prepare_pjr_input(&supports, all_candidates, all_voters);
find_threshold_phase_change_for_scenario(candidates, voters);
}
fn find_threshold_phase_change_for_scenario<AccountId: IdentifierT>(
candidates: Vec<CandidatePtr<AccountId>>,
voters: Vec<Voter<AccountId>>,
) -> Threshold {
let mut threshold = 1;
let mut prev_threshold = 0;
// find the binary range containing the threshold beyond which the PJR check succeeds
while pjr_check_core(&candidates, &voters, threshold).is_err() {
prev_threshold = threshold;
threshold = threshold
.checked_mul(2)
.expect("pjr check must fail before we run out of capacity in u128");
}
// now binary search within that range to find the phase threshold
let mut high_bound = threshold;
let mut low_bound = prev_threshold;
while high_bound - low_bound > 1 {
// maintain the invariant that low_bound fails and high_bound passes
let test = low_bound + ((high_bound - low_bound) / 2);
if pjr_check_core(&candidates, &voters, test).is_ok() {
high_bound = test;
} else {
low_bound = test;
}
}
println!("highest failing check: {}", low_bound);
println!("lowest succeeding check: {}", high_bound);
// for a value to be a threshold, it must be the boundary between two conditions
let mut unexpected_failures = Vec::new();
let mut unexpected_successes = Vec::new();
for t in 0..=low_bound {
if pjr_check_core(&candidates, &voters, t).is_ok() {
unexpected_successes.push(t);
}
}
for t in high_bound..(high_bound * 2) {
if pjr_check_core(&candidates, &voters, t).is_err() {
unexpected_failures.push(t);
}
}
dbg!(&unexpected_successes, &unexpected_failures);
assert!(unexpected_failures.is_empty() && unexpected_successes.is_empty());
high_bound
}
}
@@ -0,0 +1,924 @@
// This file is part of Bizinikiwi.
// Copyright (C) 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.
//! Rust implementation of the Phragmén reduce algorithm. This can be used by any off chain
//! application to reduce cycles from the edge assignment, which will result in smaller size.
//!
//! ### Notions
//! - `m`: size of the committee to elect.
//! - `k`: maximum allowed votes (16 as of this writing).
//! - `nv ∈ E` means that nominator `n ∈ N` supports the election of candidate `v ∈ V`.
//! - A valid solution consists of a tuple `(S, W)` , where `S ⊆ V` is a committee of m validators,
//! and `W : E → R ≥ 0` is an edge weight vector which describes how the budget of each nominator
//! n is fractionally assigned to n 's elected neighbors.
//! - `E_w := { e ∈ E : w_e > 0 }`.
//!
//! ### Algorithm overview
//!
//! > We consider the input edge weight vector `w` as a directed flow over `E_w` , where the flow in
//! > each edge is directed from the nominator to the validator. We build `w` from `w` by removing
//! > **circulations** to cancel out the flow over as many edges as possible, while preserving flow
//! > demands over all vertices and without reverting the flow direction over any edge. As long as
//! > there is a cycle, we can remove an additional circulation and eliminate at least one new edge
//! > from `E_w` . This shows that the algorithm always progresses and will eventually finish with
//! > an acyclic edge support. We keep a data structure that represents a forest of rooted trees,
//! > which is initialized as a collection of singletons one per vertex and to which edges in
//! > `E_w` are added one by one, causing the trees to merge. Whenever a new edge creates a cycle,
//! > we detect it and destroy it by removing a circulation. We also run a pre-computation which is
//! > designed to detect and remove cycles of length four exclusively. This pre-computation is
//! > optional, and if we skip it then the running time becomes `O (|E_w| ⋅ m), so the
//! > pre-computation makes sense only if `m >> k` and `|E_w| >> m^2`.
//!
//! ### Resources:
//!
//! 1. <https://hackmd.io/JOn9x98iS0e0DPWQ87zGWg?view>
use crate::{
node::{Node, NodeId, NodeRef, NodeRole},
ExtendedBalance, IdentifierT, StakedAssignment,
};
use alloc::{
collections::btree_map::{BTreeMap, Entry::*},
vec,
vec::Vec,
};
use pezsp_arithmetic::traits::{Bounded, Zero};
/// Map type used for reduce_4. Can be easily swapped with HashMap.
type Map<A> = BTreeMap<(A, A), A>;
/// Returns all combinations of size two in the collection `input` with no repetition.
fn combinations_2<T: Clone>(input: &[T]) -> Vec<(T, T)> {
let n = input.len();
if n < 2 {
return Default::default();
}
let mut comb = Vec::with_capacity(n * (n - 1) / 2);
for i in 0..n {
for j in i + 1..n {
comb.push((input[i].clone(), input[j].clone()))
}
}
comb
}
/// Returns the count of trailing common elements in two slices.
pub(crate) fn trailing_common<T: Eq>(t1: &[T], t2: &[T]) -> usize {
t1.iter().rev().zip(t2.iter().rev()).take_while(|e| e.0 == e.1).count()
}
/// Merges two parent roots as described by the reduce algorithm.
fn merge<A: IdentifierT>(voter_root_path: Vec<NodeRef<A>>, target_root_path: Vec<NodeRef<A>>) {
let (shorter_path, longer_path) = if voter_root_path.len() <= target_root_path.len() {
(voter_root_path, target_root_path)
} else {
(target_root_path, voter_root_path)
};
// iterate from last to beginning, skipping the first one. This asserts that
// indexing is always correct.
shorter_path
.iter()
.zip(shorter_path.iter().skip(1))
.for_each(|(voter, next)| Node::set_parent_of(next, voter));
Node::set_parent_of(&shorter_path[0], &longer_path[0]);
}
/// Reduce only redundant edges with cycle length of 4.
///
/// Returns the number of edges removed.
///
/// It is strictly assumed that the `who` attribute of all provided assignments are unique. The
/// result will most likely be corrupt otherwise.
///
/// O(|E_w| ⋅ k).
fn reduce_4<A: IdentifierT>(assignments: &mut Vec<StakedAssignment<A>>) -> u32 {
let mut combination_map: Map<A> = Map::new();
let mut num_changed: u32 = Zero::zero();
// we have to use the old fashioned loops here with manual indexing. Borrowing assignments will
// not work since then there is NO way to mutate it inside.
for assignment_index in 0..assignments.len() {
let who = assignments[assignment_index].who.clone();
// all combinations for this particular voter
let distribution_ids = &assignments[assignment_index]
.distribution
.iter()
.map(|(t, _p)| t.clone())
.collect::<Vec<A>>();
let candidate_combinations = combinations_2(distribution_ids);
for (v1, v2) in candidate_combinations {
match combination_map.entry((v1.clone(), v2.clone())) {
Vacant(entry) => {
entry.insert(who.clone());
},
Occupied(mut entry) => {
let other_who = entry.get_mut();
// double check if who is still voting for this pair. If not, it means that this
// pair is no longer valid and must have been removed in previous rounds. The
// reason for this is subtle; candidate_combinations is created once while the
// inner loop might remove some edges. Note that if count() > 2, the we have
// duplicates.
if assignments[assignment_index]
.distribution
.iter()
.filter(|(t, _)| *t == v1 || *t == v2)
.count() != 2
{
continue;
}
// check if other_who voted for the same pair v1, v2.
let maybe_other_assignments = assignments.iter().find(|a| a.who == *other_who);
if maybe_other_assignments.is_none() {
continue;
}
let other_assignment =
maybe_other_assignments.expect("value is checked to be 'Some'");
// Collect potential cycle votes
let mut other_cycle_votes =
other_assignment
.distribution
.iter()
.filter_map(|(t, w)| {
if *t == v1 || *t == v2 {
Some((t.clone(), *w))
} else {
None
}
})
.collect::<Vec<(A, ExtendedBalance)>>();
let other_votes_count = other_cycle_votes.len();
// If the length is more than 2, then we have identified duplicates. For now, we
// just skip. Later on we can early exit and stop processing this data since it
// is corrupt anyhow.
debug_assert!(other_votes_count <= 2);
if other_votes_count < 2 {
// This is not a cycle. Replace and continue.
*other_who = who.clone();
continue;
} else if other_votes_count == 2 {
// This is a cycle.
let mut who_cycle_votes: Vec<(A, ExtendedBalance)> = Vec::with_capacity(2);
assignments[assignment_index].distribution.iter().for_each(|(t, w)| {
if *t == v1 || *t == v2 {
who_cycle_votes.push((t.clone(), *w));
}
});
if who_cycle_votes.len() != 2 {
continue;
}
// Align the targets similarly. This helps with the circulation below.
if other_cycle_votes[0].0 != who_cycle_votes[0].0 {
other_cycle_votes.swap(0, 1);
}
// Find min
let mut min_value: ExtendedBalance = Bounded::max_value();
let mut min_index: usize = 0;
let cycle = who_cycle_votes
.iter()
.chain(other_cycle_votes.iter())
.enumerate()
.map(|(index, (t, w))| {
if *w <= min_value {
min_value = *w;
min_index = index;
}
(t.clone(), *w)
})
.collect::<Vec<(A, ExtendedBalance)>>();
// min was in the first part of the chained iters
let mut increase_indices: Vec<usize> = Vec::new();
let mut decrease_indices: Vec<usize> = Vec::new();
decrease_indices.push(min_index);
if min_index < 2 {
// min_index == 0 => sibling_index <- 1
// min_index == 1 => sibling_index <- 0
let sibling_index = 1 - min_index;
increase_indices.push(sibling_index);
// valid because the two chained sections of `cycle` are aligned;
// index [0, 2] are both voting for v1 or both v2. Same goes for [1, 3].
decrease_indices.push(sibling_index + 2);
increase_indices.push(min_index + 2);
} else {
// min_index == 2 => sibling_index <- 3
// min_index == 3 => sibling_index <- 2
let sibling_index = 3 - min_index % 2;
increase_indices.push(sibling_index);
// valid because the two chained sections of `cycle` are aligned;
// index [0, 2] are both voting for v1 or both v2. Same goes for [1, 3].
decrease_indices.push(sibling_index - 2);
increase_indices.push(min_index - 2);
}
// apply changes
let mut remove_indices: Vec<usize> = Vec::with_capacity(1);
increase_indices.into_iter().for_each(|i| {
let voter = if i < 2 { who.clone() } else { other_who.clone() };
// Note: so this is pretty ambiguous. We should only look for one
// assignment that meets this criteria and if we find multiple then that
// is a corrupt input. Same goes for the next block.
assignments.iter_mut().filter(|a| a.who == voter).for_each(|ass| {
ass.distribution
.iter_mut()
.position(|(t, _)| *t == cycle[i].0)
.map(|idx| {
let next_value =
ass.distribution[idx].1.saturating_add(min_value);
ass.distribution[idx].1 = next_value;
});
});
});
decrease_indices.into_iter().for_each(|i| {
let voter = if i < 2 { who.clone() } else { other_who.clone() };
assignments.iter_mut().filter(|a| a.who == voter).for_each(|ass| {
ass.distribution
.iter_mut()
.position(|(t, _)| *t == cycle[i].0)
.map(|idx| {
let next_value =
ass.distribution[idx].1.saturating_sub(min_value);
if next_value.is_zero() {
ass.distribution.remove(idx);
remove_indices.push(i);
num_changed += 1;
} else {
ass.distribution[idx].1 = next_value;
}
});
});
});
// remove either one of them.
let who_removed = remove_indices.iter().any(|i| *i < 2usize);
let other_removed = remove_indices.into_iter().any(|i| i >= 2usize);
match (who_removed, other_removed) {
(false, true) => {
*other_who = who.clone();
},
(true, false) => {
// nothing, other_who can stay there.
},
(true, true) => {
// remove and don't replace
entry.remove();
},
(false, false) => {
// Neither of the edges was removed? impossible.
panic!("Duplicate voter (or other corrupt input).");
},
}
}
},
}
}
}
num_changed
}
/// Reduce redundant edges from the edge weight graph, with all possible length.
///
/// To get the best performance, this should be called after `reduce_4()`.
///
/// Returns the number of edges removed.
///
/// It is strictly assumed that the `who` attribute of all provided assignments are unique. The
/// result will most likely be corrupt otherwise.
///
/// O(|Ew| ⋅ m)
fn reduce_all<A: IdentifierT>(assignments: &mut Vec<StakedAssignment<A>>) -> u32 {
let mut num_changed: u32 = Zero::zero();
let mut tree: BTreeMap<NodeId<A>, NodeRef<A>> = BTreeMap::new();
// NOTE: This code can heavily use an index cache. Looking up a pair of (voter, target) in the
// assignments happens numerous times and we can save time. For now it is written as such
// because abstracting some of this code into a function/closure is super hard due to borrow
// checks (and most likely needs unsafe code at the end). For now I will keep it as it and
// refactor later.
// a flat iterator of (voter, target) over all pairs of votes. Similar to reduce_4, we loop
// without borrowing.
for assignment_index in 0..assignments.len() {
let voter = assignments[assignment_index].who.clone();
let mut dist_index = 0;
loop {
// A distribution could have been removed. We don't know for sure. Hence, we check.
let maybe_dist = assignments[assignment_index].distribution.get(dist_index);
if maybe_dist.is_none() {
// The rest of this loop is moot.
break;
}
let (target, _) = maybe_dist.expect("Value checked to be some").clone();
// store if they existed already.
let voter_id = NodeId::from(voter.clone(), NodeRole::Voter);
let target_id = NodeId::from(target.clone(), NodeRole::Target);
let voter_exists = tree.contains_key(&voter_id);
let target_exists = tree.contains_key(&target_id);
// create both.
let voter_node = tree
.entry(voter_id.clone())
.or_insert_with(|| Node::new(voter_id).into_ref())
.clone();
let target_node = tree
.entry(target_id.clone())
.or_insert_with(|| Node::new(target_id).into_ref())
.clone();
// If one exists but the other one doesn't, or if both do not, then set the existing
// one as the parent of the non-existing one and move on. Else, continue with the rest
// of the code.
match (voter_exists, target_exists) {
(false, false) => {
Node::set_parent_of(&target_node, &voter_node);
dist_index += 1;
continue;
},
(false, true) => {
Node::set_parent_of(&voter_node, &target_node);
dist_index += 1;
continue;
},
(true, false) => {
Node::set_parent_of(&target_node, &voter_node);
dist_index += 1;
continue;
},
(true, true) => { /* don't continue and execute the rest */ },
};
let (voter_root, voter_root_path) = Node::root(&voter_node);
let (target_root, target_root_path) = Node::root(&target_node);
if voter_root != target_root {
// swap
merge(voter_root_path, target_root_path);
dist_index += 1;
} else {
// find common and cycle.
let common_count = trailing_common(&voter_root_path, &target_root_path);
// because roots are the same.
//debug_assert_eq!(target_root_path.last().unwrap(),
// voter_root_path.last().unwrap()); TODO: @kian
// the common path must be non-void..
debug_assert!(common_count > 0);
// and smaller than both
debug_assert!(common_count <= voter_root_path.len());
debug_assert!(common_count <= target_root_path.len());
// cycle part of each path will be `path[path.len() - common_count - 1 : 0]`
// NOTE: the order of chaining is important! it is always build from [target, ...,
// voter]
let cycle = target_root_path
.iter()
.take(target_root_path.len().saturating_sub(common_count).saturating_add(1))
.cloned()
.chain(
voter_root_path
.iter()
.take(voter_root_path.len().saturating_sub(common_count))
.rev()
.cloned(),
)
.collect::<Vec<NodeRef<A>>>();
// a cycle's length shall always be multiple of two.
debug_assert_eq!(cycle.len() % 2, 0);
// find minimum of cycle.
let mut min_value: ExtendedBalance = Bounded::max_value();
// The voter and the target pair that create the min edge. These MUST be set by the
// end of this code block, otherwise we skip.
let mut maybe_min_target: Option<A> = None;
let mut maybe_min_voter: Option<A> = None;
// The index of the min in opaque cycle list.
let mut maybe_min_index: Option<usize> = None;
// 1 -> next // 0 -> prev
let mut maybe_min_direction: Option<u32> = None;
// helpers
let next_index = |i| {
if i < (cycle.len() - 1) {
i + 1
} else {
0
}
};
let prev_index = |i| {
if i > 0 {
i - 1
} else {
cycle.len() - 1
}
};
for i in 0..cycle.len() {
if cycle[i].borrow().id.role == NodeRole::Voter {
// NOTE: sadly way too many clones since I don't want to make A: Copy
let current = cycle[i].borrow().id.who.clone();
let next = cycle[next_index(i)].borrow().id.who.clone();
let prev = cycle[prev_index(i)].borrow().id.who.clone();
assignments.iter().find(|a| a.who == current).map(|ass| {
ass.distribution.iter().find(|d| d.0 == next).map(|(_, w)| {
if *w < min_value {
min_value = *w;
maybe_min_target = Some(next.clone());
maybe_min_voter = Some(current.clone());
maybe_min_index = Some(i);
maybe_min_direction = Some(1);
}
})
});
assignments.iter().find(|a| a.who == current).map(|ass| {
ass.distribution.iter().find(|d| d.0 == prev).map(|(_, w)| {
if *w < min_value {
min_value = *w;
maybe_min_target = Some(prev.clone());
maybe_min_voter = Some(current.clone());
maybe_min_index = Some(i);
maybe_min_direction = Some(0);
}
})
});
}
}
// all of these values must be set by now, we assign them to un-mut values, no
// longer being optional either.
let (min_value, min_target, min_voter, min_index, min_direction) =
match (
min_value,
maybe_min_target,
maybe_min_voter,
maybe_min_index,
maybe_min_direction,
) {
(
min_value,
Some(min_target),
Some(min_voter),
Some(min_index),
Some(min_direction),
) => (min_value, min_target, min_voter, min_index, min_direction),
_ => {
pezsp_runtime::print("UNREACHABLE code reached in `reduce` algorithm. This must be a bug.");
break;
},
};
// if the min edge is in the voter's sub-chain.
// [target, ..., X, Y, ... voter]
let target_chunk = target_root_path.len() - common_count;
let min_chain_in_voter = (min_index + min_direction as usize) > target_chunk;
// walk over the cycle and update the weights
let mut should_inc_counter = true;
let start_operation_add = ((min_index % 2) + min_direction as usize) % 2 == 1;
let mut additional_removed = Vec::new();
for i in 0..cycle.len() {
let current = cycle[i].borrow();
if current.id.role == NodeRole::Voter {
let prev = cycle[prev_index(i)].borrow();
assignments
.iter_mut()
.enumerate()
.filter(|(_, a)| a.who == current.id.who)
.for_each(|(target_ass_index, ass)| {
ass.distribution
.iter_mut()
.position(|(t, _)| *t == prev.id.who)
.map(|idx| {
let next_value = if i % 2 == 0 {
if start_operation_add {
ass.distribution[idx].1.saturating_add(min_value)
} else {
ass.distribution[idx].1.saturating_sub(min_value)
}
} else if start_operation_add {
ass.distribution[idx].1.saturating_sub(min_value)
} else {
ass.distribution[idx].1.saturating_add(min_value)
};
if next_value.is_zero() {
// if the removed edge is from the current assignment,
// index should NOT be increased.
if target_ass_index == assignment_index {
should_inc_counter = false
}
ass.distribution.remove(idx);
num_changed += 1;
// only add if this is not the min itself.
if !(i == min_index && min_direction == 0) {
additional_removed.push((
cycle[i].clone(),
cycle[prev_index(i)].clone(),
));
}
} else {
ass.distribution[idx].1 = next_value;
}
});
});
let next = cycle[next_index(i)].borrow();
assignments
.iter_mut()
.enumerate()
.filter(|(_, a)| a.who == current.id.who)
.for_each(|(target_ass_index, ass)| {
ass.distribution
.iter_mut()
.position(|(t, _)| *t == next.id.who)
.map(|idx| {
let next_value = if i % 2 == 0 {
if start_operation_add {
ass.distribution[idx].1.saturating_sub(min_value)
} else {
ass.distribution[idx].1.saturating_add(min_value)
}
} else if start_operation_add {
ass.distribution[idx].1.saturating_add(min_value)
} else {
ass.distribution[idx].1.saturating_sub(min_value)
};
if next_value.is_zero() {
// if the removed edge is from the current assignment,
// index should NOT be increased.
if target_ass_index == assignment_index {
should_inc_counter = false
}
ass.distribution.remove(idx);
num_changed += 1;
if !(i == min_index && min_direction == 1) {
additional_removed.push((
cycle[i].clone(),
cycle[next_index(i)].clone(),
));
}
} else {
ass.distribution[idx].1 = next_value;
}
});
});
}
}
// don't do anything if the edge removed itself. This is always the first and last
// element
let should_reorg = !(min_index == (cycle.len() - 1) && min_direction == 1);
// re-org.
if should_reorg {
let min_edge = vec![min_voter, min_target];
if min_chain_in_voter {
// NOTE: safe; voter_root_path is always bigger than 1 element.
for i in 0..voter_root_path.len() - 1 {
let current = voter_root_path[i].clone().borrow().id.who.clone();
let next = voter_root_path[i + 1].clone().borrow().id.who.clone();
if min_edge.contains(&current) && min_edge.contains(&next) {
break;
}
Node::set_parent_of(&voter_root_path[i + 1], &voter_root_path[i]);
}
Node::set_parent_of(&voter_node, &target_node);
} else {
// NOTE: safe; target_root_path is always bigger than 1 element.
for i in 0..target_root_path.len() - 1 {
let current = target_root_path[i].clone().borrow().id.who.clone();
let next = target_root_path[i + 1].clone().borrow().id.who.clone();
if min_edge.contains(&current) && min_edge.contains(&next) {
break;
}
Node::set_parent_of(&target_root_path[i + 1], &target_root_path[i]);
}
Node::set_parent_of(&target_node, &voter_node);
}
}
// remove every other node which has collapsed to zero
for (r1, r2) in additional_removed {
if Node::is_parent_of(&r1, &r2) {
Node::remove_parent(&r1);
} else if Node::is_parent_of(&r2, &r1) {
Node::remove_parent(&r2);
}
}
// increment the counter if needed.
if should_inc_counter {
dist_index += 1;
}
}
}
}
num_changed
}
/// Reduce the given [`Vec<StakedAssignment<IdentifierT>>`]. This removes redundant edges without
/// changing the overall backing of any of the elected candidates.
///
/// Returns the number of edges removed.
///
/// IMPORTANT: It is strictly assumed that the `who` attribute of all provided assignments are
/// unique. The result will most likely be corrupt otherwise. Furthermore, if the _distribution
/// vector_ contains duplicate ids, only the first instance is ever updates.
///
/// O(min{ |Ew| ⋅ k + m3 , |Ew| ⋅ m })
pub fn reduce<A: IdentifierT>(assignments: &mut Vec<StakedAssignment<A>>) -> u32 where {
let mut num_changed = reduce_4(assignments);
num_changed += reduce_all(assignments);
num_changed
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn merging_works() {
// D <-- A <-- B <-- C
//
// F <-- E
let d = Node::new(NodeId::from(1, NodeRole::Target)).into_ref();
let a = Node::new(NodeId::from(2, NodeRole::Target)).into_ref();
let b = Node::new(NodeId::from(3, NodeRole::Target)).into_ref();
let c = Node::new(NodeId::from(4, NodeRole::Target)).into_ref();
let e = Node::new(NodeId::from(5, NodeRole::Target)).into_ref();
let f = Node::new(NodeId::from(6, NodeRole::Target)).into_ref();
Node::set_parent_of(&c, &b);
Node::set_parent_of(&b, &a);
Node::set_parent_of(&a, &d);
Node::set_parent_of(&e, &f);
let path1 = vec![c.clone(), b.clone(), a.clone(), d.clone()];
let path2 = vec![e.clone(), f.clone()];
merge(path1, path2);
// D <-- A <-- B <-- C
// |
// F --> E --> -->
assert_eq!(e.borrow().clone().parent.unwrap().borrow().id.who, 4u32); // c
}
#[test]
fn merge_with_len_one() {
// D <-- A <-- B <-- C
//
// F <-- E
let d = Node::new(NodeId::from(1, NodeRole::Target)).into_ref();
let a = Node::new(NodeId::from(2, NodeRole::Target)).into_ref();
let b = Node::new(NodeId::from(3, NodeRole::Target)).into_ref();
let c = Node::new(NodeId::from(4, NodeRole::Target)).into_ref();
let f = Node::new(NodeId::from(6, NodeRole::Target)).into_ref();
Node::set_parent_of(&c, &b);
Node::set_parent_of(&b, &a);
Node::set_parent_of(&a, &d);
let path1 = vec![c.clone(), b.clone(), a.clone(), d.clone()];
let path2 = vec![f.clone()];
merge(path1, path2);
// D <-- A <-- B <-- C
// |
// F --> -->
assert_eq!(f.borrow().clone().parent.unwrap().borrow().id.who, 4u32); // c
}
#[test]
fn basic_reduce_4_cycle_works() {
use super::*;
let assignments = vec![
StakedAssignment { who: 1, distribution: vec![(10, 25), (20, 75)] },
StakedAssignment { who: 2, distribution: vec![(10, 50), (20, 50)] },
];
let mut new_assignments = assignments.clone();
let num_reduced = reduce_4(&mut new_assignments);
assert_eq!(num_reduced, 1);
assert_eq!(
new_assignments,
vec![
StakedAssignment { who: 1, distribution: vec![(20, 100),] },
StakedAssignment { who: 2, distribution: vec![(10, 75), (20, 25),] },
],
);
}
#[test]
fn basic_reduce_all_cycles_works() {
let mut assignments = vec![
StakedAssignment { who: 1, distribution: vec![(10, 10)] },
StakedAssignment { who: 2, distribution: vec![(10, 15), (20, 5)] },
StakedAssignment { who: 3, distribution: vec![(20, 15), (40, 15)] },
StakedAssignment { who: 4, distribution: vec![(20, 10), (30, 10), (40, 20)] },
StakedAssignment { who: 5, distribution: vec![(20, 20), (30, 10), (40, 20)] },
];
assert_eq!(3, reduce_all(&mut assignments));
assert_eq!(
assignments,
vec![
StakedAssignment { who: 1, distribution: vec![(10, 10),] },
StakedAssignment { who: 2, distribution: vec![(10, 15), (20, 5),] },
StakedAssignment { who: 3, distribution: vec![(20, 30),] },
StakedAssignment { who: 4, distribution: vec![(40, 40),] },
StakedAssignment { who: 5, distribution: vec![(20, 15), (30, 20), (40, 15),] },
],
)
}
#[test]
fn basic_reduce_works() {
let mut assignments = vec![
StakedAssignment { who: 1, distribution: vec![(10, 10)] },
StakedAssignment { who: 2, distribution: vec![(10, 15), (20, 5)] },
StakedAssignment { who: 3, distribution: vec![(20, 15), (40, 15)] },
StakedAssignment { who: 4, distribution: vec![(20, 10), (30, 10), (40, 20)] },
StakedAssignment { who: 5, distribution: vec![(20, 20), (30, 10), (40, 20)] },
];
assert_eq!(3, reduce(&mut assignments));
assert_eq!(
assignments,
vec![
StakedAssignment { who: 1, distribution: vec![(10, 10),] },
StakedAssignment { who: 2, distribution: vec![(10, 15), (20, 5),] },
StakedAssignment { who: 3, distribution: vec![(20, 30),] },
StakedAssignment { who: 4, distribution: vec![(40, 40),] },
StakedAssignment { who: 5, distribution: vec![(20, 15), (30, 20), (40, 15),] },
],
)
}
#[test]
fn should_deal_with_self_vote() {
let mut assignments = vec![
StakedAssignment { who: 1, distribution: vec![(10, 10)] },
StakedAssignment { who: 2, distribution: vec![(10, 15), (20, 5)] },
StakedAssignment { who: 3, distribution: vec![(20, 15), (40, 15)] },
StakedAssignment { who: 4, distribution: vec![(20, 10), (30, 10), (40, 20)] },
StakedAssignment { who: 5, distribution: vec![(20, 20), (30, 10), (40, 20)] },
// self vote from 10 and 20 to itself.
StakedAssignment { who: 10, distribution: vec![(10, 100)] },
StakedAssignment { who: 20, distribution: vec![(20, 200)] },
];
assert_eq!(3, reduce(&mut assignments));
assert_eq!(
assignments,
vec![
StakedAssignment { who: 1, distribution: vec![(10, 10),] },
StakedAssignment { who: 2, distribution: vec![(10, 15), (20, 5),] },
StakedAssignment { who: 3, distribution: vec![(20, 30),] },
StakedAssignment { who: 4, distribution: vec![(40, 40),] },
StakedAssignment { who: 5, distribution: vec![(20, 15), (30, 20), (40, 15),] },
// should stay untouched.
StakedAssignment { who: 10, distribution: vec![(10, 100)] },
StakedAssignment { who: 20, distribution: vec![(20, 200)] },
],
)
}
#[test]
fn reduce_3_common_votes_same_weight() {
let mut assignments = vec![
StakedAssignment {
who: 4,
distribution: vec![(1000000, 100), (1000002, 100), (1000004, 100)],
},
StakedAssignment {
who: 5,
distribution: vec![(1000000, 100), (1000002, 100), (1000004, 100)],
},
];
reduce_4(&mut assignments);
assert_eq!(
assignments,
vec![
StakedAssignment { who: 4, distribution: vec![(1000000, 200,), (1000004, 100,),] },
StakedAssignment { who: 5, distribution: vec![(1000002, 200,), (1000004, 100,),] },
],
)
}
#[test]
#[should_panic]
fn reduce_panics_on_duplicate_voter() {
let mut assignments = vec![
StakedAssignment { who: 1, distribution: vec![(10, 10), (20, 10)] },
StakedAssignment { who: 1, distribution: vec![(10, 15), (20, 5)] },
StakedAssignment { who: 2, distribution: vec![(10, 15), (20, 15)] },
];
reduce(&mut assignments);
}
#[test]
fn should_deal_with_duplicates_target() {
let mut assignments = vec![
StakedAssignment { who: 1, distribution: vec![(10, 15), (20, 5)] },
StakedAssignment {
who: 2,
distribution: vec![
(10, 15),
(20, 15),
// duplicate
(10, 1),
// duplicate
(20, 1),
],
},
];
reduce(&mut assignments);
assert_eq!(
assignments,
vec![
StakedAssignment { who: 1, distribution: vec![(10, 20),] },
StakedAssignment {
who: 2,
distribution: vec![
(10, 10),
(20, 20),
// duplicate votes are silently ignored.
(10, 1),
(20, 1),
],
},
],
)
}
#[test]
fn bound_should_be_kept() {
let mut assignments = vec![
StakedAssignment {
who: 1,
distribution: vec![(103, 72), (101, 53), (100, 83), (102, 38)],
},
StakedAssignment {
who: 2,
distribution: vec![(103, 18), (101, 36), (102, 54), (100, 94)],
},
StakedAssignment {
who: 3,
distribution: vec![(100, 96), (101, 35), (102, 52), (103, 69)],
},
StakedAssignment {
who: 4,
distribution: vec![(102, 34), (100, 47), (103, 91), (101, 73)],
},
];
let winners = vec![103, 101, 100, 102];
let n = 4;
let m = winners.len() as u32;
let num_reduced = reduce_all(&mut assignments);
assert!(16 - num_reduced <= n + m);
}
}
@@ -0,0 +1,905 @@
// This file is part of Bizinikiwi.
// Copyright (C) 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.
//! Tests for npos-elections.
use crate::{
balancing, helpers::*, mock::*, seq_phragmen, seq_phragmen_core, setup_inputs, to_support_map,
Assignment, BalancingConfig, ElectionResult, ExtendedBalance, StakedAssignment, Support, Voter,
};
use pezsp_arithmetic::{PerU16, Perbill, Percent, Permill};
use bizinikiwi_test_utils::assert_eq_uvec;
#[test]
fn float_phragmen_poc_works() {
let candidates = vec![1, 2, 3];
let voters = vec![(10, vec![1, 2]), (20, vec![1, 3]), (30, vec![2, 3])];
let stake_of = create_stake_of(&[(10, 10), (20, 20), (30, 30), (1, 0), (2, 0), (3, 0)]);
let mut phragmen_result = elect_float(2, candidates, voters, &stake_of).unwrap();
let winners = phragmen_result.clone().winners;
let assignments = phragmen_result.clone().assignments;
assert_eq_uvec!(winners, vec![(2, 40), (3, 50)]);
assert_eq_uvec!(
assignments,
vec![(10, vec![(2, 1.0)]), (20, vec![(3, 1.0)]), (30, vec![(2, 0.5), (3, 0.5)]),]
);
let mut support_map = build_support_map_float(&mut phragmen_result, &stake_of);
assert_eq!(
support_map.get(&2).unwrap(),
&_Support { own: 0.0, total: 25.0, others: vec![(10u64, 10.0), (30u64, 15.0)] }
);
assert_eq!(
support_map.get(&3).unwrap(),
&_Support { own: 0.0, total: 35.0, others: vec![(20u64, 20.0), (30u64, 15.0)] }
);
equalize_float(phragmen_result.assignments, &mut support_map, 0.0, 2, stake_of);
assert_eq!(
support_map.get(&2).unwrap(),
&_Support { own: 0.0, total: 30.0, others: vec![(10u64, 10.0), (30u64, 20.0)] }
);
assert_eq!(
support_map.get(&3).unwrap(),
&_Support { own: 0.0, total: 30.0, others: vec![(20u64, 20.0), (30u64, 10.0)] }
);
}
#[test]
fn phragmen_core_test_without_edges() {
let candidates = vec![1, 2, 3];
let voters = vec![(10, 10, vec![]), (20, 20, vec![]), (30, 30, vec![])];
let (candidates, voters) = setup_inputs(candidates, voters);
assert_eq!(
voters
.iter()
.map(|v| (
v.who,
v.budget,
(v.edges.iter().map(|e| (e.who, e.weight)).collect::<Vec<_>>()),
))
.collect::<Vec<_>>(),
vec![]
);
assert_eq!(
candidates
.iter()
.map(|c_ptr| (
c_ptr.borrow().who,
c_ptr.borrow().elected,
c_ptr.borrow().round,
c_ptr.borrow().backed_stake,
))
.collect::<Vec<_>>(),
vec![(1, false, 0, 0), (2, false, 0, 0), (3, false, 0, 0),]
);
}
#[test]
fn phragmen_core_poc_works() {
let candidates = vec![1, 2, 3];
let voters = vec![(10, 10, vec![1, 2]), (20, 20, vec![1, 3]), (30, 30, vec![2, 3])];
let (candidates, voters) = setup_inputs(candidates, voters);
let (candidates, voters) = seq_phragmen_core(2, candidates, voters).unwrap();
assert_eq!(
voters
.iter()
.map(|v| (
v.who,
v.budget,
(v.edges.iter().map(|e| (e.who, e.weight)).collect::<Vec<_>>()),
))
.collect::<Vec<_>>(),
vec![(10, 10, vec![(2, 10)]), (20, 20, vec![(3, 20)]), (30, 30, vec![(2, 15), (3, 15)]),]
);
assert_eq!(
candidates
.iter()
.map(|c_ptr| (
c_ptr.borrow().who,
c_ptr.borrow().elected,
c_ptr.borrow().round,
c_ptr.borrow().backed_stake,
))
.collect::<Vec<_>>(),
vec![(1, false, 0, 0), (2, true, 1, 25), (3, true, 0, 35),]
);
}
#[test]
fn balancing_core_works() {
let candidates = vec![1, 2, 3, 4, 5];
let voters = vec![
(10, 10, vec![1, 2]),
(20, 20, vec![1, 3]),
(30, 30, vec![1, 2, 3, 4]),
(40, 40, vec![1, 3, 4, 5]),
(50, 50, vec![2, 4, 5]),
];
let (candidates, voters) = setup_inputs(candidates, voters);
let (candidates, mut voters) = seq_phragmen_core(4, candidates, voters).unwrap();
let config = BalancingConfig { iterations: 4, tolerance: 0 };
let iters = balancing::balance::<AccountId>(&mut voters, &config);
assert!(iters > 0);
assert_eq!(
voters
.iter()
.map(|v| (
v.who,
v.budget,
(v.edges.iter().map(|e| (e.who, e.weight)).collect::<Vec<_>>()),
))
.collect::<Vec<_>>(),
vec![
// note the 0 edge. This is know and not an issue per se. Also note that the stakes are
// normalized.
(10, 10, vec![(1, 9), (2, 1)]),
(20, 20, vec![(1, 9), (3, 11)]),
(30, 30, vec![(1, 8), (2, 7), (3, 8), (4, 7)]),
(40, 40, vec![(1, 11), (3, 18), (4, 11)]),
(50, 50, vec![(2, 30), (4, 20)]),
]
);
assert_eq!(
candidates
.iter()
.map(|c_ptr| (
c_ptr.borrow().who,
c_ptr.borrow().elected,
c_ptr.borrow().round,
c_ptr.borrow().backed_stake,
))
.collect::<Vec<_>>(),
vec![
(1, true, 1, 37),
(2, true, 2, 38),
(3, true, 3, 37),
(4, true, 0, 38),
(5, false, 0, 0),
]
);
}
#[test]
fn voter_normalize_ops_works() {
use crate::{Candidate, Edge};
// normalize
{
let c1 = Candidate { who: 10, elected: false, ..Default::default() };
let c2 = Candidate { who: 20, elected: false, ..Default::default() };
let c3 = Candidate { who: 30, elected: false, ..Default::default() };
let e1 = Edge::new(c1, 30);
let e2 = Edge::new(c2, 33);
let e3 = Edge::new(c3, 30);
let mut v = Voter { who: 1, budget: 100, edges: vec![e1, e2, e3], ..Default::default() };
v.try_normalize().unwrap();
assert_eq!(v.edges.iter().map(|e| e.weight).collect::<Vec<_>>(), vec![34, 33, 33]);
}
// // normalize_elected
{
let c1 = Candidate { who: 10, elected: false, ..Default::default() };
let c2 = Candidate { who: 20, elected: true, ..Default::default() };
let c3 = Candidate { who: 30, elected: true, ..Default::default() };
let e1 = Edge::new(c1, 30);
let e2 = Edge::new(c2, 33);
let e3 = Edge::new(c3, 30);
let mut v = Voter { who: 1, budget: 100, edges: vec![e1, e2, e3], ..Default::default() };
v.try_normalize_elected().unwrap();
assert_eq!(v.edges.iter().map(|e| e.weight).collect::<Vec<_>>(), vec![30, 34, 66]);
}
}
#[test]
fn phragmen_poc_works() {
let candidates = vec![1, 2, 3];
let voters = vec![(10, vec![1, 2]), (20, vec![1, 3]), (30, vec![2, 3])];
let stake_of = create_stake_of(&[(10, 10), (20, 20), (30, 30)]);
let ElectionResult::<_, Perbill> { winners, assignments } = seq_phragmen(
2,
candidates,
voters
.iter()
.map(|(ref v, ref vs)| (*v, stake_of(v), vs.clone()))
.collect::<Vec<_>>(),
None,
)
.unwrap();
assert_eq_uvec!(winners, vec![(2, 25), (3, 35)]);
assert_eq_uvec!(
assignments,
vec![
Assignment { who: 10u64, distribution: vec![(2, Perbill::from_percent(100))] },
Assignment { who: 20, distribution: vec![(3, Perbill::from_percent(100))] },
Assignment {
who: 30,
distribution: vec![
(2, Perbill::from_percent(100 / 2)),
(3, Perbill::from_percent(100 / 2)),
],
},
]
);
let staked = assignment_ratio_to_staked(assignments, &stake_of);
let support_map = to_support_map::<AccountId>(&staked);
assert_eq_uvec!(
staked,
vec![
StakedAssignment { who: 10u64, distribution: vec![(2, 10)] },
StakedAssignment { who: 20, distribution: vec![(3, 20)] },
StakedAssignment { who: 30, distribution: vec![(2, 15), (3, 15),] },
]
);
assert_eq!(
*support_map.get(&2).unwrap(),
Support::<AccountId> { total: 25, voters: vec![(10, 10), (30, 15)] },
);
assert_eq!(
*support_map.get(&3).unwrap(),
Support::<AccountId> { total: 35, voters: vec![(20, 20), (30, 15)] },
);
}
#[test]
fn phragmen_poc_works_with_balancing() {
let candidates = vec![1, 2, 3];
let voters = vec![(10, vec![1, 2]), (20, vec![1, 3]), (30, vec![2, 3])];
let stake_of = create_stake_of(&[(10, 10), (20, 20), (30, 30)]);
let config = BalancingConfig { iterations: 4, tolerance: 0 };
let ElectionResult::<_, Perbill> { winners, assignments } = seq_phragmen(
2,
candidates,
voters
.iter()
.map(|(ref v, ref vs)| (*v, stake_of(v), vs.clone()))
.collect::<Vec<_>>(),
Some(config),
)
.unwrap();
assert_eq_uvec!(winners, vec![(2, 30), (3, 30)]);
assert_eq_uvec!(
assignments,
vec![
Assignment { who: 10u64, distribution: vec![(2, Perbill::from_percent(100))] },
Assignment { who: 20, distribution: vec![(3, Perbill::from_percent(100))] },
Assignment {
who: 30,
distribution: vec![
(2, Perbill::from_parts(666666666)),
(3, Perbill::from_parts(333333334)),
],
},
]
);
let staked = assignment_ratio_to_staked(assignments, &stake_of);
let support_map = to_support_map::<AccountId>(&staked);
assert_eq_uvec!(
staked,
vec![
StakedAssignment { who: 10u64, distribution: vec![(2, 10)] },
StakedAssignment { who: 20, distribution: vec![(3, 20)] },
StakedAssignment { who: 30, distribution: vec![(2, 20), (3, 10),] },
]
);
assert_eq!(
*support_map.get(&2).unwrap(),
Support::<AccountId> { total: 30, voters: vec![(10, 10), (30, 20)] },
);
assert_eq!(
*support_map.get(&3).unwrap(),
Support::<AccountId> { total: 30, voters: vec![(20, 20), (30, 10)] },
);
}
#[test]
fn phragmen_poc_2_works() {
let candidates = vec![10, 20, 30];
let voters = vec![(2, vec![10, 20, 30]), (4, vec![10, 20, 40])];
let stake_of =
create_stake_of(&[(10, 1000), (20, 1000), (30, 1000), (40, 1000), (2, 500), (4, 500)]);
run_and_compare::<Perbill, _>(candidates.clone(), voters.clone(), &stake_of, 2);
run_and_compare::<Permill, _>(candidates.clone(), voters.clone(), &stake_of, 2);
run_and_compare::<Percent, _>(candidates.clone(), voters.clone(), &stake_of, 2);
run_and_compare::<PerU16, _>(candidates, voters, &stake_of, 2);
}
#[test]
fn phragmen_poc_3_works() {
let candidates = vec![10, 20, 30];
let voters = vec![(2, vec![10, 20, 30]), (4, vec![10, 20, 40])];
let stake_of = create_stake_of(&[(10, 1000), (20, 1000), (30, 1000), (2, 50), (4, 1000)]);
run_and_compare::<Perbill, _>(candidates.clone(), voters.clone(), &stake_of, 2);
run_and_compare::<Permill, _>(candidates.clone(), voters.clone(), &stake_of, 2);
run_and_compare::<Percent, _>(candidates.clone(), voters.clone(), &stake_of, 2);
run_and_compare::<PerU16, _>(candidates, voters, &stake_of, 2);
}
#[test]
fn phragmen_accuracy_on_large_scale_only_candidates() {
// because of this particular situation we had per_u128 and now rational128. In practice, a
// candidate can have the maximum amount of tokens, and also supported by the maximum.
let candidates = vec![1, 2, 3, 4, 5];
let stake_of = create_stake_of(&[
(1, (u64::MAX - 1).into()),
(2, (u64::MAX - 4).into()),
(3, (u64::MAX - 5).into()),
(4, (u64::MAX - 3).into()),
(5, (u64::MAX - 2).into()),
]);
let ElectionResult::<_, Perbill> { winners, assignments } = seq_phragmen(
2,
candidates.clone(),
auto_generate_self_voters(&candidates)
.iter()
.map(|(ref v, ref vs)| (*v, stake_of(v), vs.clone()))
.collect::<Vec<_>>(),
None,
)
.unwrap();
assert_eq_uvec!(winners, vec![(1, 18446744073709551614u128), (5, 18446744073709551613u128)]);
assert_eq!(assignments.len(), 2);
check_assignments_sum(&assignments);
}
#[test]
fn phragmen_accuracy_on_large_scale_voters_and_candidates() {
let candidates = vec![1, 2, 3, 4, 5];
let mut voters = vec![(13, vec![1, 3, 5]), (14, vec![2, 4])];
voters.extend(auto_generate_self_voters(&candidates));
let stake_of = create_stake_of(&[
(1, (u64::MAX - 1).into()),
(2, (u64::MAX - 4).into()),
(3, (u64::MAX - 5).into()),
(4, (u64::MAX - 3).into()),
(5, (u64::MAX - 2).into()),
(13, (u64::MAX - 10).into()),
(14, u64::MAX.into()),
]);
let ElectionResult::<_, Perbill> { winners, assignments } = seq_phragmen(
2,
candidates,
voters
.iter()
.map(|(ref v, ref vs)| (*v, stake_of(v), vs.clone()))
.collect::<Vec<_>>(),
None,
)
.unwrap();
assert_eq_uvec!(winners, vec![(2, 36893488147419103226u128), (1, 36893488147419103219u128)]);
assert_eq!(
assignments,
vec![
Assignment { who: 13u64, distribution: vec![(1, Perbill::one())] },
Assignment { who: 14, distribution: vec![(2, Perbill::one())] },
Assignment { who: 1, distribution: vec![(1, Perbill::one())] },
Assignment { who: 2, distribution: vec![(2, Perbill::one())] },
]
);
check_assignments_sum(&assignments);
}
#[test]
fn phragmen_accuracy_on_small_scale_self_vote() {
let candidates = vec![40, 10, 20, 30];
let voters = auto_generate_self_voters(&candidates);
let stake_of = create_stake_of(&[(40, 0), (10, 1), (20, 2), (30, 1)]);
let ElectionResult::<_, Perbill> { winners, assignments } = seq_phragmen(
3,
candidates,
voters
.iter()
.map(|(ref v, ref vs)| (*v, stake_of(v), vs.clone()))
.collect::<Vec<_>>(),
None,
)
.unwrap();
assert_eq_uvec!(winners, vec![(20, 2), (10, 1), (30, 1)]);
check_assignments_sum(&assignments);
}
#[test]
fn phragmen_accuracy_on_small_scale_no_self_vote() {
let candidates = vec![40, 10, 20, 30];
let voters = vec![(1, vec![10]), (2, vec![20]), (3, vec![30]), (4, vec![40])];
let stake_of = create_stake_of(&[
(40, 1000), // don't care
(10, 1000), // don't care
(20, 1000), // don't care
(30, 1000), // don't care
(4, 0),
(1, 1),
(2, 2),
(3, 1),
]);
let ElectionResult::<_, Perbill> { winners, assignments } = seq_phragmen(
3,
candidates,
voters
.iter()
.map(|(ref v, ref vs)| (*v, stake_of(v), vs.clone()))
.collect::<Vec<_>>(),
None,
)
.unwrap();
assert_eq_uvec!(winners, vec![(20, 2), (10, 1), (30, 1)]);
check_assignments_sum(&assignments);
}
#[test]
fn phragmen_large_scale_test() {
let candidates = vec![2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24];
let mut voters = vec![(50, vec![2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24])];
voters.extend(auto_generate_self_voters(&candidates));
let stake_of = create_stake_of(&[
(2, 1),
(4, 100),
(6, 1000000),
(8, 100000000001000),
(10, 100000000002000),
(12, 100000000003000),
(14, 400000000000000),
(16, 400000000001000),
(18, 18000000000000000),
(20, 20000000000000000),
(22, 500000000000100000),
(24, 500000000000200000),
(50, 990000000000000000),
]);
let ElectionResult::<_, Perbill> { winners, assignments } = seq_phragmen(
2,
candidates,
voters
.iter()
.map(|(ref v, ref vs)| (*v, stake_of(v), vs.clone()))
.collect::<Vec<_>>(),
None,
)
.unwrap();
assert_eq_uvec!(winners.iter().map(|(x, _)| *x).collect::<Vec<_>>(), vec![24, 22]);
check_assignments_sum(&assignments);
}
#[test]
fn phragmen_large_scale_test_2() {
let nom_budget: u64 = 1_000_000_000_000_000_000;
let c_budget: u64 = 4_000_000;
let candidates = vec![2, 4];
let mut voters = vec![(50, vec![2, 4])];
voters.extend(auto_generate_self_voters(&candidates));
let stake_of =
create_stake_of(&[(2, c_budget.into()), (4, c_budget.into()), (50, nom_budget.into())]);
let ElectionResult::<_, Perbill> { winners, assignments } = seq_phragmen(
2,
candidates,
voters
.iter()
.map(|(ref v, ref vs)| (*v, stake_of(v), vs.clone()))
.collect::<Vec<_>>(),
None,
)
.unwrap();
assert_eq_uvec!(winners, vec![(2, 500000000005000000u128), (4, 500000000003000000)]);
assert_eq_uvec!(
assignments,
vec![
Assignment {
who: 50u64,
distribution: vec![
(2, Perbill::from_parts(500000000)),
(4, Perbill::from_parts(500000000)),
],
},
Assignment { who: 2, distribution: vec![(2, Perbill::one())] },
Assignment { who: 4, distribution: vec![(4, Perbill::one())] },
],
);
check_assignments_sum(&assignments);
}
#[test]
fn phragmen_linear_equalize() {
let candidates = vec![11, 21, 31, 41, 51, 61, 71];
let voters = vec![
(2, vec![11]),
(4, vec![11, 21]),
(6, vec![21, 31]),
(8, vec![31, 41]),
(110, vec![41, 51]),
(120, vec![51, 61]),
(130, vec![61, 71]),
];
let stake_of = create_stake_of(&[
(11, 1000),
(21, 1000),
(31, 1000),
(41, 1000),
(51, 1000),
(61, 1000),
(71, 1000),
(2, 2000),
(4, 1000),
(6, 1000),
(8, 1000),
(110, 1000),
(120, 1000),
(130, 1000),
]);
run_and_compare::<Perbill, _>(candidates, voters, &stake_of, 2);
}
#[test]
fn elect_has_no_entry_barrier() {
let candidates = vec![10, 20, 30];
let voters = vec![(1, vec![10]), (2, vec![20])];
let stake_of = create_stake_of(&[(1, 10), (2, 10)]);
let ElectionResult::<_, Perbill> { winners, assignments: _ } = seq_phragmen(
3,
candidates,
voters
.iter()
.map(|(ref v, ref vs)| (*v, stake_of(v), vs.clone()))
.collect::<Vec<_>>(),
None,
)
.unwrap();
// 30 is elected with stake 0. The caller is responsible for stripping this.
assert_eq_uvec!(winners, vec![(10, 10), (20, 10), (30, 0),]);
}
#[test]
fn phragmen_self_votes_should_be_kept() {
let candidates = vec![5, 10, 20, 30];
let voters = vec![(5, vec![5]), (10, vec![10]), (20, vec![20]), (1, vec![10, 20])];
let stake_of = create_stake_of(&[(5, 5), (10, 10), (20, 20), (1, 8)]);
let result: ElectionResult<_, Perbill> = seq_phragmen(
2,
candidates,
voters
.iter()
.map(|(ref v, ref vs)| (*v, stake_of(v), vs.clone()))
.collect::<Vec<_>>(),
None,
)
.unwrap();
assert_eq!(result.winners, vec![(20, 24), (10, 14)]);
assert_eq_uvec!(
result.assignments,
vec![
Assignment {
who: 1,
distribution: vec![
(10, Perbill::from_percent(50)),
(20, Perbill::from_percent(50)),
]
},
Assignment { who: 10, distribution: vec![(10, Perbill::from_percent(100))] },
Assignment { who: 20, distribution: vec![(20, Perbill::from_percent(100))] },
]
);
let staked_assignments = assignment_ratio_to_staked(result.assignments, &stake_of);
let supports = to_support_map::<AccountId>(&staked_assignments);
assert_eq!(supports.get(&5u64), None);
assert_eq!(
supports.get(&10u64).unwrap(),
&Support { total: 14u128, voters: vec![(10u64, 10u128), (1u64, 4u128)] },
);
assert_eq!(
supports.get(&20u64).unwrap(),
&Support { total: 24u128, voters: vec![(20u64, 20u128), (1u64, 4u128)] },
);
}
#[test]
fn duplicate_target_is_ignored() {
let candidates = vec![1, 2, 3];
let voters = vec![(10, 100, vec![1, 1, 2, 3]), (20, 100, vec![2, 3]), (30, 50, vec![1, 1, 2])];
let ElectionResult::<_, Perbill> { winners, assignments } =
seq_phragmen(2, candidates, voters, None).unwrap();
assert_eq!(winners, vec![(2, 140), (3, 110)]);
assert_eq!(
assignments
.into_iter()
.map(|x| (x.who, x.distribution.into_iter().map(|(w, _)| w).collect::<Vec<_>>()))
.collect::<Vec<_>>(),
vec![(10, vec![2, 3]), (20, vec![2, 3]), (30, vec![2]),],
);
}
#[test]
fn duplicate_target_is_ignored_when_winner() {
let candidates = vec![1, 2, 3];
let voters = vec![(10, 100, vec![1, 1, 2, 3]), (20, 100, vec![1, 2])];
let ElectionResult::<_, Perbill> { winners, assignments } =
seq_phragmen(2, candidates, voters, None).unwrap();
assert_eq!(winners, vec![(1, 100), (2, 100)]);
assert_eq!(
assignments
.into_iter()
.map(|x| (x.who, x.distribution.into_iter().map(|(w, _)| w).collect::<Vec<_>>()))
.collect::<Vec<_>>(),
vec![(10, vec![1, 2]), (20, vec![1, 2]),],
);
}
mod assignment_convert_normalize {
use super::*;
#[test]
fn assignment_convert_works() {
let staked = StakedAssignment {
who: 1 as AccountId,
distribution: vec![(20, 100 as ExtendedBalance), (30, 25)],
};
let assignment = staked.clone().into_assignment();
assert_eq!(
assignment,
Assignment {
who: 1,
distribution: vec![
(20, Perbill::from_percent(80)),
(30, Perbill::from_percent(20)),
]
}
);
assert_eq!(assignment.into_staked(125), staked);
}
#[test]
fn assignment_convert_will_not_normalize() {
assert_eq!(
Assignment {
who: 1,
distribution: vec![(2, Perbill::from_percent(33)), (3, Perbill::from_percent(66)),]
}
.into_staked(100),
StakedAssignment {
who: 1,
distribution: vec![
(2, 33),
(3, 66),
// sum is not 100!
],
},
);
assert_eq!(
StakedAssignment {
who: 1,
distribution: vec![
(2, 333_333_333_333_333),
(3, 333_333_333_333_333),
(4, 666_666_666_666_333),
],
}
.into_assignment(),
Assignment {
who: 1,
distribution: vec![
(2, Perbill::from_parts(250000000)),
(3, Perbill::from_parts(250000000)),
(4, Perbill::from_parts(499999999)),
// sum is not 100%!
]
},
)
}
#[test]
fn assignment_can_normalize() {
let mut a = Assignment {
who: 1,
distribution: vec![
(2, Perbill::from_parts(330000000)),
(3, Perbill::from_parts(660000000)),
// sum is not 100%!
],
};
a.try_normalize().unwrap();
assert_eq!(
a,
Assignment {
who: 1,
distribution: vec![
(2, Perbill::from_parts(340000000)),
(3, Perbill::from_parts(660000000)),
]
},
);
}
#[test]
fn staked_assignment_can_normalize() {
let mut a = StakedAssignment { who: 1, distribution: vec![(2, 33), (3, 66)] };
a.try_normalize(100).unwrap();
assert_eq!(a, StakedAssignment { who: 1, distribution: vec![(2, 34), (3, 66),] });
}
}
mod score {
use super::*;
use crate::ElectionScore;
use pezsp_arithmetic::PerThing;
/// NOTE: in tests, we still use the legacy [u128; 3] since it is more compact. Each `u128`
/// corresponds to element at the respective field index of `ElectionScore`.
impl From<[ExtendedBalance; 3]> for ElectionScore {
fn from(t: [ExtendedBalance; 3]) -> Self {
Self { minimal_stake: t[0], sum_stake: t[1], sum_stake_squared: t[2] }
}
}
fn is_score_better(this: [u128; 3], that: [u128; 3], p: impl PerThing) -> bool {
ElectionScore::from(this).strict_threshold_better(ElectionScore::from(that), p)
}
#[test]
fn score_comparison_is_lexicographical_no_epsilon() {
let epsilon = Perbill::zero();
// only better in the fist parameter, worse in the other two ✅
assert_eq!(is_score_better([12, 10, 35], [10, 20, 30], epsilon), true);
// worse in the first, better in the other two ❌
assert_eq!(is_score_better([9, 30, 10], [10, 20, 30], epsilon), false);
// equal in the first, the second one dictates.
assert_eq!(is_score_better([10, 25, 40], [10, 20, 30], epsilon), true);
// equal in the first two, the last one dictates.
assert_eq!(is_score_better([10, 20, 40], [10, 20, 30], epsilon), false);
}
#[test]
fn score_comparison_with_epsilon() {
let epsilon = Perbill::from_percent(1);
{
// no more than 1 percent (10) better in the first param.
assert_eq!(is_score_better([1009, 5000, 100000], [1000, 5000, 100000], epsilon), false);
// now equal, still not better.
assert_eq!(is_score_better([1010, 5000, 100000], [1000, 5000, 100000], epsilon), false);
// now it is.
assert_eq!(is_score_better([1011, 5000, 100000], [1000, 5000, 100000], epsilon), true);
}
{
// First score score is epsilon better, but first score is no longer `ge`. Then this is
// still not a good solution.
assert_eq!(is_score_better([999, 6000, 100000], [1000, 5000, 100000], epsilon), false);
}
{
// first score is equal or better, but not epsilon. Then second one is the determinant.
assert_eq!(is_score_better([1005, 5000, 100000], [1000, 5000, 100000], epsilon), false);
assert_eq!(is_score_better([1005, 5050, 100000], [1000, 5000, 100000], epsilon), false);
assert_eq!(is_score_better([1005, 5051, 100000], [1000, 5000, 100000], epsilon), true);
}
{
// first score and second are equal or less than epsilon more, third is determinant.
assert_eq!(is_score_better([1005, 5025, 100000], [1000, 5000, 100000], epsilon), false);
assert_eq!(is_score_better([1005, 5025, 99_000], [1000, 5000, 100000], epsilon), false);
assert_eq!(is_score_better([1005, 5025, 98_999], [1000, 5000, 100000], epsilon), true);
}
}
#[test]
fn score_comparison_large_value() {
// some random value taken from eras in kusama.
let initial =
[12488167277027543u128, 5559266368032409496, 118749283262079244270992278287436446];
// this claim is 0.04090% better in the third component. It should be accepted as better if
// epsilon is smaller than 5/10_0000
let claim =
[12488167277027543u128, 5559266368032409496, 118700736389524721358337889258988054];
assert_eq!(is_score_better(claim, initial, Perbill::from_rational(1u32, 10_000),), true,);
assert_eq!(is_score_better(claim, initial, Perbill::from_rational(2u32, 10_000),), true,);
assert_eq!(is_score_better(claim, initial, Perbill::from_rational(3u32, 10_000),), true,);
assert_eq!(is_score_better(claim, initial, Perbill::from_rational(4u32, 10_000),), true,);
assert_eq!(is_score_better(claim, initial, Perbill::from_rational(5u32, 10_000),), false,);
}
#[test]
fn ord_works() {
// equal only when all elements are equal
assert!(ElectionScore::from([10, 5, 15]) == ElectionScore::from([10, 5, 15]));
assert!(ElectionScore::from([10, 5, 15]) != ElectionScore::from([9, 5, 15]));
assert!(ElectionScore::from([10, 5, 15]) != ElectionScore::from([10, 5, 14]));
// first element greater, rest don't matter
assert!(ElectionScore::from([10, 5, 15]) > ElectionScore::from([8, 5, 25]));
assert!(ElectionScore::from([10, 5, 15]) > ElectionScore::from([9, 20, 5]));
// second element greater, rest don't matter
assert!(ElectionScore::from([10, 5, 15]) > ElectionScore::from([10, 4, 25]));
assert!(ElectionScore::from([10, 5, 15]) > ElectionScore::from([10, 4, 5]));
// second element is less, rest don't matter. Note that this is swapped.
assert!(ElectionScore::from([10, 5, 15]) > ElectionScore::from([10, 5, 16]));
assert!(ElectionScore::from([10, 5, 15]) > ElectionScore::from([10, 5, 25]));
}
}
@@ -0,0 +1,31 @@
// This file is part of Bizinikiwi.
// Copyright (C) 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
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.
//! Traits for the npos-election operations.
use crate::ExtendedBalance;
use core::{fmt::Debug, ops::Mul};
use pezsp_arithmetic::PerThing;
/// an aggregator trait for a generic type of a voter/target identifier. This usually maps to
/// bizinikiwi's account id.
pub trait IdentifierT: Clone + Eq + Ord + Debug + codec::Codec {}
impl<T: Clone + Eq + Ord + Debug + codec::Codec> IdentifierT for T {}
/// Aggregator trait for a PerThing that can be multiplied by u128 (ExtendedBalance).
pub trait PerThing128: PerThing + Mul<ExtendedBalance, Output = ExtendedBalance> {}
impl<T: PerThing + Mul<ExtendedBalance, Output = ExtendedBalance>> PerThing128 for T {}