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:
@@ -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(¤t) && 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(¤t) && 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 {}
|
||||
Reference in New Issue
Block a user