Decouple Stkaing and Election - Part1: Support traits (#7908)

* Base features and traits.

* Fix the build

* Remove unused boxing

* Self review cleanup

* Fix build
This commit is contained in:
Kian Paimani
2021-01-18 10:24:12 +00:00
committed by GitHub
parent c58a2d9a74
commit ced107b355
23 changed files with 925 additions and 341 deletions
+12
View File
@@ -8357,6 +8357,17 @@ dependencies = [
"syn",
]
[[package]]
name = "sp-election-providers"
version = "2.0.0"
dependencies = [
"parity-scale-codec",
"sp-arithmetic",
"sp-npos-elections",
"sp-runtime",
"sp-std",
]
[[package]]
name = "sp-externalities"
version = "0.8.1"
@@ -8453,6 +8464,7 @@ dependencies = [
"rand 0.7.3",
"serde",
"sp-arithmetic",
"sp-core",
"sp-npos-elections-compact",
"sp-runtime",
"sp-std",
+1
View File
@@ -139,6 +139,7 @@ members = [
"primitives/database",
"primitives/debug-derive",
"primitives/externalities",
"primitives/election-providers",
"primitives/finality-grandpa",
"primitives/inherents",
"primitives/io",
@@ -21,6 +21,7 @@ use super::*;
use crate::Module as Staking;
use testing_utils::*;
use sp_npos_elections::CompactSolution;
use sp_runtime::traits::One;
use frame_system::RawOrigin;
pub use frame_benchmarking::{benchmarks, account, whitelisted_caller, whitelist_account};
+17 -18
View File
@@ -232,10 +232,11 @@
//!
//! The controller account can free a portion (or all) of the funds using the
//! [`unbond`](enum.Call.html#variant.unbond) call. Note that the funds are not immediately
//! accessible. Instead, a duration denoted by [`BondingDuration`](./trait.Config.html#associatedtype.BondingDuration)
//! (in number of eras) must pass until the funds can actually be removed. Once the
//! `BondingDuration` is over, the [`withdraw_unbonded`](./enum.Call.html#variant.withdraw_unbonded)
//! call can be used to actually withdraw the funds.
//! accessible. Instead, a duration denoted by
//! [`BondingDuration`](./trait.Config.html#associatedtype.BondingDuration) (in number of eras) must
//! pass until the funds can actually be removed. Once the `BondingDuration` is over, the
//! [`withdraw_unbonded`](./enum.Call.html#variant.withdraw_unbonded) call can be used to actually
//! withdraw the funds.
//!
//! Note that there is a limitation to the number of fund-chunks that can be scheduled to be
//! unlocked in the future via [`unbond`](enum.Call.html#variant.unbond). In case this maximum
@@ -304,7 +305,7 @@ use frame_support::{
};
use pallet_session::historical;
use sp_runtime::{
Percent, Perbill, PerU16, PerThing, InnerOf, RuntimeDebug, DispatchError,
Percent, Perbill, PerU16, InnerOf, RuntimeDebug, DispatchError,
curve::PiecewiseLinear,
traits::{
Convert, Zero, StaticLookup, CheckedSub, Saturating, SaturatedConversion,
@@ -327,14 +328,14 @@ use frame_system::{
};
use sp_npos_elections::{
ExtendedBalance, Assignment, ElectionScore, ElectionResult as PrimitiveElectionResult,
build_support_map, evaluate_support, seq_phragmen, generate_solution_type,
is_score_better, VotingLimit, SupportMap, VoteWeight,
to_support_map, EvaluateSupport, seq_phragmen, generate_solution_type, is_score_better,
SupportMap, VoteWeight, CompactSolution, PerThing128,
};
pub use weights::WeightInfo;
const STAKING_ID: LockIdentifier = *b"staking ";
pub const MAX_UNLOCKING_CHUNKS: usize = 32;
pub const MAX_NOMINATIONS: usize = <CompactAssignments as VotingLimit>::LIMIT;
pub const MAX_NOMINATIONS: usize = <CompactAssignments as CompactSolution>::LIMIT;
pub(crate) const LOG_TARGET: &'static str = "staking";
@@ -2105,7 +2106,7 @@ decl_module! {
#[weight = T::WeightInfo::submit_solution_better(
size.validators.into(),
size.nominators.into(),
compact.len() as u32,
compact.voter_count() as u32,
winners.len() as u32,
)]
pub fn submit_election_solution(
@@ -2139,7 +2140,7 @@ decl_module! {
#[weight = T::WeightInfo::submit_solution_better(
size.validators.into(),
size.nominators.into(),
compact.len() as u32,
compact.voter_count() as u32,
winners.len() as u32,
)]
pub fn submit_election_solution_unsigned(
@@ -2601,13 +2602,11 @@ impl<T: Config> Module<T> {
);
// build the support map thereof in order to evaluate.
let supports = build_support_map::<T::AccountId>(
&winners,
&staked_assignments,
).map_err(|_| Error::<T>::OffchainElectionBogusEdge)?;
let supports = to_support_map::<T::AccountId>(&winners, &staked_assignments)
.map_err(|_| Error::<T>::OffchainElectionBogusEdge)?;
// Check if the score is the same as the claimed one.
let submitted_score = evaluate_support(&supports);
let submitted_score = (&supports).evaluate();
ensure!(submitted_score == claimed_score, Error::<T>::OffchainElectionBogusScore);
// At last, alles Ok. Exposures and store the result.
@@ -2863,7 +2862,7 @@ impl<T: Config> Module<T> {
Self::slashable_balance_of_fn(),
);
let supports = build_support_map::<T::AccountId>(
let supports = to_support_map::<T::AccountId>(
&elected_stashes,
&staked_assignments,
)
@@ -2902,7 +2901,7 @@ impl<T: Config> Module<T> {
/// Self votes are added and nominations before the most recent slashing span are ignored.
///
/// No storage item is updated.
pub fn do_phragmen<Accuracy: PerThing>(
pub fn do_phragmen<Accuracy: PerThing128>(
iterations: usize,
) -> Option<PrimitiveElectionResult<T::AccountId, Accuracy>>
where
@@ -2952,7 +2951,7 @@ impl<T: Config> Module<T> {
all_nominators,
Some((iterations, 0)), // exactly run `iterations` rounds.
)
.map_err(|err| log!(error, "Call to seq-phragmen failed due to {}", err))
.map_err(|err| log!(error, "Call to seq-phragmen failed due to {:?}", err))
.ok()
}
}
+5 -5
View File
@@ -27,7 +27,7 @@ use frame_support::{
use sp_core::H256;
use sp_io;
use sp_npos_elections::{
build_support_map, evaluate_support, reduce, ExtendedBalance, StakedAssignment, ElectionScore,
to_support_map, EvaluateSupport, reduce, ExtendedBalance, StakedAssignment, ElectionScore,
};
use sp_runtime::{
curve::PiecewiseLinear,
@@ -860,8 +860,8 @@ pub(crate) fn horrible_npos_solution(
let score = {
let (_, _, better_score) = prepare_submission_with(true, true, 0, |_| {});
let support = build_support_map::<AccountId>(&winners, &staked_assignment).unwrap();
let score = evaluate_support(&support);
let support = to_support_map::<AccountId>(&winners, &staked_assignment).unwrap();
let score = support.evaluate();
assert!(sp_npos_elections::is_score_better::<Perbill>(
better_score,
@@ -960,11 +960,11 @@ pub(crate) fn prepare_submission_with(
Staking::slashable_balance_of_fn(),
);
let support_map = build_support_map::<AccountId>(
let support_map = to_support_map::<AccountId>(
winners.as_slice(),
staked.as_slice(),
).unwrap();
evaluate_support::<AccountId>(&support_map)
support_map.evaluate()
} else {
Default::default()
};
@@ -25,8 +25,8 @@ use codec::Decode;
use frame_support::{traits::Get, weights::Weight, IterableStorageMap};
use frame_system::offchain::SubmitTransaction;
use sp_npos_elections::{
build_support_map, evaluate_support, reduce, Assignment, ElectionResult, ElectionScore,
ExtendedBalance,
to_support_map, EvaluateSupport, reduce, Assignment, ElectionResult, ElectionScore,
ExtendedBalance, CompactSolution,
};
use sp_runtime::{
offchain::storage::StorageValueRef, traits::TrailingZeroInput, PerThing, RuntimeDebug,
@@ -265,7 +265,7 @@ pub fn trim_to_weight<T: Config, FN>(
where
for<'r> FN: Fn(&'r T::AccountId) -> Option<NominatorIndex>,
{
match compact.len().checked_sub(maximum_allowed_voters as usize) {
match compact.voter_count().checked_sub(maximum_allowed_voters as usize) {
Some(to_remove) if to_remove > 0 => {
// grab all voters and sort them by least stake.
let balance_of = <Module<T>>::slashable_balance_of_fn();
@@ -300,7 +300,7 @@ where
warn,
"💸 {} nominators out of {} had to be removed from compact solution due to size limits.",
removed,
compact.len() + removed,
compact.voter_count() + removed,
);
Ok(compact)
}
@@ -324,12 +324,7 @@ pub fn prepare_submission<T: Config>(
do_reduce: bool,
maximum_weight: Weight,
) -> Result<
(
Vec<ValidatorIndex>,
CompactAssignments,
ElectionScore,
ElectionSize,
),
(Vec<ValidatorIndex>, CompactAssignments, ElectionScore, ElectionSize),
OffchainElectionError,
>
where
@@ -403,11 +398,11 @@ where
T::WeightInfo::submit_solution_better(
size.validators.into(),
size.nominators.into(),
compact.len() as u32,
compact.voter_count() as u32,
winners.len() as u32,
),
maximum_allowed_voters,
compact.len(),
compact.voter_count(),
);
let compact = trim_to_weight::<T, _>(maximum_allowed_voters, compact, &nominator_index)?;
@@ -423,9 +418,9 @@ where
<Module<T>>::slashable_balance_of_fn(),
);
let support_map = build_support_map::<T::AccountId>(&winners, &staked)
let support_map = to_support_map::<T::AccountId>(&winners, &staked)
.map_err(|_| OffchainElectionError::ElectionFailed)?;
evaluate_support::<T::AccountId>(&support_map)
support_map.evaluate()
};
// winners to index. Use a simple for loop for a more expressive early exit in case of error.
+3 -5
View File
@@ -244,11 +244,9 @@ pub fn get_weak_solution<T: Config>(
<Module<T>>::slashable_balance_of_fn(),
);
let support_map = build_support_map::<T::AccountId>(
winners.as_slice(),
staked.as_slice(),
).unwrap();
evaluate_support::<T::AccountId>(&support_map)
let support_map =
to_support_map::<T::AccountId>(winners.as_slice(), staked.as_slice()).unwrap();
support_map.evaluate()
};
// compact encode the assignment.
@@ -0,0 +1,33 @@
[package]
name = "sp-election-providers"
version = "2.0.0"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2018"
license = "Apache-2.0"
homepage = "https://substrate.dev"
repository = "https://github.com/paritytech/substrate/"
description = "Primitive election providers"
readme = "README.md"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { package = "parity-scale-codec", version = "1.3.4", default-features = false, features = ["derive"] }
sp-std = { version = "2.0.1", default-features = false, path = "../std" }
sp-arithmetic = { version = "2.0.1", default-features = false, path = "../arithmetic" }
sp-npos-elections = { version = "2.0.1", default-features = false, path = "../npos-elections" }
[dev-dependencies]
sp-npos-elections = { version = "2.0.1", path = "../npos-elections" }
sp-runtime = { version = "2.0.1", path = "../runtime" }
[features]
default = ["std"]
runtime-benchmarks = []
std = [
"codec/std",
"sp-std/std",
"sp-npos-elections/std",
"sp-arithmetic/std",
]
@@ -0,0 +1,241 @@
// This file is part of Substrate.
// Copyright (C) 2020 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.
//! Primitive traits for providing election functionality.
//!
//! This crate provides two traits that could interact to enable extensible election functionality
//! within FRAME pallets.
//!
//! Something that will provide the functionality of election will implement [`ElectionProvider`],
//! whilst needing an associated [`ElectionProvider::DataProvider`], which needs to be fulfilled by
//! an entity implementing [`ElectionDataProvider`]. Most often, *the data provider is* the receiver
//! of the election, resulting in a diagram as below:
//!
//! ```ignore
//! ElectionDataProvider
//! <------------------------------------------+
//! | |
//! v |
//! +-----+----+ +------+---+
//! | | | |
//! pallet-do-election | | | | pallet-needs-election
//! | | | |
//! | | | |
//! +-----+----+ +------+---+
//! | ^
//! | |
//! +------------------------------------------+
//! ElectionProvider
//! ```
//!
//! > It could also be possible that a third party pallet (C), provides the data of election to an
//! > election provider (B), which then passes the election result to another pallet (A).
//!
//! ## Election Types
//!
//! Typically, two types of elections exist:
//!
//! 1. **Stateless**: Election data is provided, and the election result is immediately ready.
//! 2. **Stateful**: Election data is is queried ahead of time, and the election result might be
//! ready some number of blocks in the future.
//!
//! To accommodate both type of elections in one trait, the traits lean toward **stateful
//! election**, as it is more general than the stateless. This is why [`ElectionProvider::elect`]
//! has no parameters. All value and type parameter must be provided by the [`ElectionDataProvider`]
//! trait, even if the election happens immediately.
//!
//! ## Election Data
//!
//! The data associated with an election, essentially what the [`ElectionDataProvider`] must convey
//! is as follows:
//!
//! 1. A list of voters, with their stake.
//! 2. A list of targets (i.e. _candidates_).
//! 3. A number of desired targets to be elected (i.e. _winners_)
//!
//! In addition to that, the [`ElectionDataProvider`] must also hint [`ElectionProvider`] at when
//! the next election might happen ([`ElectionDataProvider::next_election_prediction`]). A stateless
//! election provider would probably ignore this. A stateful election provider can use this to
//! prepare the election result in advance.
//!
//! Nonetheless, an [`ElectionProvider`] shan't rely on this and should preferably provide some
//! means of fallback election as well, in case the `elect` was called immaturely early.
//!
//! ## Example
//!
//! ```rust
//! # use sp_election_providers::*;
//! # use sp_npos_elections::{Support, Assignment};
//!
//! type AccountId = u64;
//! type Balance = u64;
//! type BlockNumber = u32;
//!
//! mod data_provider {
//! use super::*;
//!
//! pub trait Config: Sized {
//! type ElectionProvider: ElectionProvider<
//! AccountId,
//! BlockNumber,
//! DataProvider = Module<Self>,
//! >;
//! }
//!
//! pub struct Module<T: Config>(std::marker::PhantomData<T>);
//!
//! impl<T: Config> ElectionDataProvider<AccountId, BlockNumber> for Module<T> {
//! fn desired_targets() -> u32 {
//! 1
//! }
//! fn voters() -> Vec<(AccountId, VoteWeight, Vec<AccountId>)> {
//! Default::default()
//! }
//! fn targets() -> Vec<AccountId> {
//! vec![10, 20, 30]
//! }
//! fn next_election_prediction(now: BlockNumber) -> BlockNumber {
//! 0
//! }
//! }
//! }
//!
//!
//! mod generic_election_provider {
//! use super::*;
//!
//! pub struct GenericElectionProvider<T: Config>(std::marker::PhantomData<T>);
//!
//! pub trait Config {
//! type DataProvider: ElectionDataProvider<AccountId, BlockNumber>;
//! }
//!
//! impl<T: Config> ElectionProvider<AccountId, BlockNumber> for GenericElectionProvider<T> {
//! type Error = ();
//! type DataProvider = T::DataProvider;
//!
//! fn elect() -> Result<Supports<AccountId>, Self::Error> {
//! Self::DataProvider::targets()
//! .first()
//! .map(|winner| vec![(*winner, Support::default())])
//! .ok_or(())
//! }
//! }
//! }
//!
//! mod runtime {
//! use super::generic_election_provider;
//! use super::data_provider;
//! use super::AccountId;
//!
//! struct Runtime;
//! impl generic_election_provider::Config for Runtime {
//! type DataProvider = data_provider::Module<Runtime>;
//! }
//!
//! impl data_provider::Config for Runtime {
//! type ElectionProvider = generic_election_provider::GenericElectionProvider<Runtime>;
//! }
//!
//! }
//!
//! # fn main() {}
//! ```
#![cfg_attr(not(feature = "std"), no_std)]
pub mod onchain;
use sp_std::{prelude::*, fmt::Debug};
/// Re-export some type as they are used in the interface.
pub use sp_arithmetic::PerThing;
pub use sp_npos_elections::{Assignment, ExtendedBalance, PerThing128, Supports, VoteWeight};
/// Something that can provide the data to an [`ElectionProvider`].
pub trait ElectionDataProvider<AccountId, BlockNumber> {
/// All possible targets for the election, i.e. the candidates.
fn targets() -> Vec<AccountId>;
/// All possible voters for the election.
///
/// Note that if a notion of self-vote exists, it should be represented here.
fn voters() -> Vec<(AccountId, VoteWeight, Vec<AccountId>)>;
/// The number of targets to elect.
fn desired_targets() -> u32;
/// Provide a best effort prediction about when the next election is about to happen.
///
/// In essence, the implementor should predict with this function when it will trigger the
/// [`ElectionProvider::elect`].
///
/// This is only useful for stateful election providers.
fn next_election_prediction(now: BlockNumber) -> BlockNumber;
/// Utility function only to be used in benchmarking scenarios, to be implemented optionally,
/// else a noop.
#[cfg(any(feature = "runtime-benchmarks", test))]
fn put_snapshot(
_voters: Vec<(AccountId, VoteWeight, Vec<AccountId>)>,
_targets: Vec<AccountId>,
) {
}
}
#[cfg(feature = "std")]
impl<AccountId, BlockNumber> ElectionDataProvider<AccountId, BlockNumber> for () {
fn targets() -> Vec<AccountId> {
Default::default()
}
fn voters() -> Vec<(AccountId, VoteWeight, Vec<AccountId>)> {
Default::default()
}
fn desired_targets() -> u32 {
Default::default()
}
fn next_election_prediction(now: BlockNumber) -> BlockNumber {
now
}
}
/// Something that can compute the result of an election and pass it back to the caller.
///
/// This trait only provides an interface to _request_ an election, i.e.
/// [`ElectionProvider::elect`]. That data required for the election need to be passed to the
/// implemented of this trait through [`ElectionProvider::DataProvider`].
pub trait ElectionProvider<AccountId, BlockNumber> {
/// The error type that is returned by the provider.
type Error: Debug;
/// The data provider of the election.
type DataProvider: ElectionDataProvider<AccountId, BlockNumber>;
/// Elect a new set of winners.
///
/// The result is returned in a target major format, namely as vector of supports.
fn elect() -> Result<Supports<AccountId>, Self::Error>;
}
#[cfg(feature = "std")]
impl<AccountId, BlockNumber> ElectionProvider<AccountId, BlockNumber> for () {
type Error = &'static str;
type DataProvider = ();
fn elect() -> Result<Supports<AccountId>, Self::Error> {
Err("<() as ElectionProvider> cannot do anything.")
}
}
@@ -0,0 +1,163 @@
// This file is part of Substrate.
// Copyright (C) 2020 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.
//! An implementation of [`ElectionProvider`] that does an on-chain sequential phragmen.
use sp_arithmetic::InnerOf;
use crate::{ElectionDataProvider, ElectionProvider};
use sp_npos_elections::*;
use sp_std::{collections::btree_map::BTreeMap, marker::PhantomData, prelude::*};
/// Errors of the on-chain election.
#[derive(Eq, PartialEq, Debug)]
pub enum Error {
/// An internal error in the NPoS elections crate.
NposElections(sp_npos_elections::Error),
}
impl From<sp_npos_elections::Error> for Error {
fn from(e: sp_npos_elections::Error) -> Self {
Error::NposElections(e)
}
}
/// A simple on-chain implementation of the election provider trait.
///
/// This will accept voting data on the fly and produce the results immediately.
///
/// ### Warning
///
/// This can be very expensive to run frequently on-chain. Use with care.
pub struct OnChainSequentialPhragmen<T: Config>(PhantomData<T>);
/// Configuration trait of [`OnChainSequentialPhragmen`].
///
/// Note that this is similar to a pallet traits, but [`OnChainSequentialPhragmen`] is not a pallet.
pub trait Config {
/// The account identifier type.
type AccountId: IdentifierT;
/// The block number type.
type BlockNumber;
/// The accuracy used to compute the election:
type Accuracy: PerThing128;
/// Something that provides the data for election.
type DataProvider: ElectionDataProvider<Self::AccountId, Self::BlockNumber>;
}
impl<T: Config> ElectionProvider<T::AccountId, T::BlockNumber> for OnChainSequentialPhragmen<T>
where
ExtendedBalance: From<InnerOf<T::Accuracy>>,
{
type Error = Error;
type DataProvider = T::DataProvider;
fn elect() -> Result<Supports<T::AccountId>, Self::Error> {
let voters = Self::DataProvider::voters();
let targets = Self::DataProvider::targets();
let desired_targets = Self::DataProvider::desired_targets() as usize;
let mut stake_map: BTreeMap<T::AccountId, VoteWeight> = BTreeMap::new();
voters.iter().for_each(|(v, s, _)| {
stake_map.insert(v.clone(), *s);
});
let stake_of = |w: &T::AccountId| -> VoteWeight {
stake_map.get(w).cloned().unwrap_or_default()
};
let ElectionResult { winners, assignments } =
seq_phragmen::<_, T::Accuracy>(desired_targets, targets, voters, None)
.map_err(Error::from)?;
let staked = assignment_ratio_to_staked_normalized(assignments, &stake_of)?;
let winners = to_without_backing(winners);
to_supports(&winners, &staked).map_err(Error::from)
}
}
#[cfg(test)]
mod tests {
use super::*;
use sp_npos_elections::Support;
use sp_runtime::Perbill;
type AccountId = u64;
type BlockNumber = u32;
struct Runtime;
impl Config for Runtime {
type AccountId = AccountId;
type BlockNumber = BlockNumber;
type Accuracy = Perbill;
type DataProvider = mock_data_provider::DataProvider;
}
type OnChainPhragmen = OnChainSequentialPhragmen<Runtime>;
mod mock_data_provider {
use super::*;
pub struct DataProvider;
impl ElectionDataProvider<AccountId, BlockNumber> for DataProvider {
fn voters() -> Vec<(AccountId, VoteWeight, Vec<AccountId>)> {
vec![
(1, 10, vec![10, 20]),
(2, 20, vec![30, 20]),
(3, 30, vec![10, 30]),
]
}
fn targets() -> Vec<AccountId> {
vec![10, 20, 30]
}
fn desired_targets() -> u32 {
2
}
fn next_election_prediction(_: BlockNumber) -> BlockNumber {
0
}
}
}
#[test]
fn onchain_seq_phragmen_works() {
assert_eq!(
OnChainPhragmen::elect().unwrap(),
vec![
(
10,
Support {
total: 25,
voters: vec![(1, 10), (3, 15)]
}
),
(
30,
Support {
total: 35,
voters: vec![(2, 20), (3, 15)]
}
)
]
);
}
}
@@ -18,6 +18,7 @@ serde = { version = "1.0.101", optional = true, features = ["derive"] }
sp-std = { version = "2.0.0", default-features = false, path = "../std" }
sp-npos-elections-compact = { version = "2.0.0", path = "./compact" }
sp-arithmetic = { version = "2.0.0", default-features = false, path = "../arithmetic" }
sp-core = { version = "2.0.0", default-features = false, path = "../core" }
[dev-dependencies]
substrate-test-utils = { version = "2.0.0", path = "../../test-utils" }
@@ -32,4 +33,5 @@ std = [
"serde",
"sp-std/std",
"sp-arithmetic/std",
"sp-core/std",
]
@@ -30,7 +30,7 @@ use sp_npos_elections::{ElectionResult, VoteWeight};
use std::collections::BTreeMap;
use sp_runtime::{Perbill, PerThing, traits::Zero};
use sp_npos_elections::{
balance_solution, assignment_ratio_to_staked, build_support_map, to_without_backing, VoteWeight,
balance_solution, assignment_ratio_to_staked, to_support_map, to_without_backing, VoteWeight,
ExtendedBalance, Assignment, StakedAssignment, IdentifierT, assignment_ratio_to_staked,
seq_phragmen,
};
@@ -149,7 +149,7 @@ fn do_phragmen(
if eq_iters > 0 {
let staked = assignment_ratio_to_staked(assignments, &stake_of);
let winners = to_without_backing(winners);
let mut support = build_support_map(
let mut support = to_support_map(
winners.as_ref(),
staked.as_ref(),
).unwrap();
@@ -21,7 +21,7 @@ use crate::field_name_for;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
fn from_impl(count: usize) -> TokenStream2 {
pub(crate) fn from_impl(count: usize) -> TokenStream2 {
let from_impl_single = {
let name = field_name_for(1);
quote!(1 => compact.#name.push(
@@ -73,7 +73,7 @@ fn from_impl(count: usize) -> TokenStream2 {
)
}
fn into_impl(count: usize, per_thing: syn::Type) -> TokenStream2 {
pub(crate) fn into_impl(count: usize, per_thing: syn::Type) -> TokenStream2 {
let into_impl_single = {
let name = field_name_for(1);
quote!(
@@ -153,53 +153,3 @@ fn into_impl(count: usize, per_thing: syn::Type) -> TokenStream2 {
#into_impl_rest
)
}
pub(crate) fn assignment(
ident: syn::Ident,
voter_type: syn::Type,
target_type: syn::Type,
weight_type: syn::Type,
count: usize,
) -> TokenStream2 {
let from_impl = from_impl(count);
let into_impl = into_impl(count, weight_type.clone());
quote!(
use _npos::__OrInvalidIndex;
impl #ident {
pub fn from_assignment<FV, FT, A>(
assignments: Vec<_npos::Assignment<A, #weight_type>>,
index_of_voter: FV,
index_of_target: FT,
) -> Result<Self, _npos::Error>
where
A: _npos::IdentifierT,
for<'r> FV: Fn(&'r A) -> Option<#voter_type>,
for<'r> FT: Fn(&'r A) -> Option<#target_type>,
{
let mut compact: #ident = Default::default();
for _npos::Assignment { who, distribution } in assignments {
match distribution.len() {
0 => continue,
#from_impl
_ => {
return Err(_npos::Error::CompactTargetOverflow);
}
}
};
Ok(compact)
}
pub fn into_assignment<A: _npos::IdentifierT>(
self,
voter_at: impl Fn(#voter_type) -> Option<A>,
target_at: impl Fn(#target_type) -> Option<A>,
) -> Result<Vec<_npos::Assignment<A, #weight_type>>, _npos::Error> {
let mut assignments: Vec<_npos::Assignment<A, #weight_type>> = Default::default();
#into_impl
Ok(assignments)
}
}
)
}
@@ -95,19 +95,11 @@ pub fn generate_solution_type(item: TokenStream) -> TokenStream {
compact_encoding,
).unwrap_or_else(|e| e.to_compile_error());
let assignment_impls = assignment::assignment(
ident.clone(),
voter_type.clone(),
target_type.clone(),
weight_type.clone(),
count,
);
quote!(
#imports
#solution_struct
#assignment_impls
).into()
)
.into()
}
fn struct_def(
@@ -125,29 +117,32 @@ fn struct_def(
let singles = {
let name = field_name_for(1);
// NOTE: we use the visibility of the struct for the fields as well.. could be made better.
quote!(
#name: Vec<(#voter_type, #target_type)>,
#vis #name: Vec<(#voter_type, #target_type)>,
)
};
let doubles = {
let name = field_name_for(2);
quote!(
#name: Vec<(#voter_type, (#target_type, #weight_type), #target_type)>,
#vis #name: Vec<(#voter_type, (#target_type, #weight_type), #target_type)>,
)
};
let rest = (3..=count).map(|c| {
let field_name = field_name_for(c);
let array_len = c - 1;
quote!(
#field_name: Vec<(
#voter_type,
[(#target_type, #weight_type); #array_len],
#target_type
)>,
)
}).collect::<TokenStream2>();
let rest = (3..=count)
.map(|c| {
let field_name = field_name_for(c);
let array_len = c - 1;
quote!(
#vis #field_name: Vec<(
#voter_type,
[(#target_type, #weight_type); #array_len],
#target_type
)>,
)
})
.collect::<TokenStream2>();
let len_impl = len_impl(count);
let edge_count_impl = edge_count_impl(count);
@@ -172,40 +167,38 @@ fn struct_def(
quote!(#[derive(Default, PartialEq, Eq, Clone, Debug, _npos::codec::Encode, _npos::codec::Decode)])
};
let from_impl = assignment::from_impl(count);
let into_impl = assignment::into_impl(count, weight_type.clone());
Ok(quote! (
/// A struct to encode a election assignment in a compact way.
#derives_and_maybe_compact_encoding
#vis struct #ident { #singles #doubles #rest }
impl _npos::VotingLimit for #ident {
use _npos::__OrInvalidIndex;
impl _npos::CompactSolution for #ident {
const LIMIT: usize = #count;
}
type Voter = #voter_type;
type Target = #target_type;
type Accuracy = #weight_type;
impl #ident {
/// Get the length of all the assignments that this type is encoding. This is basically
/// the same as the number of assignments, or the number of voters in total.
pub fn len(&self) -> usize {
fn voter_count(&self) -> usize {
let mut all_len = 0usize;
#len_impl
all_len
}
/// Get the total count of edges.
pub fn edge_count(&self) -> usize {
fn edge_count(&self) -> usize {
let mut all_edges = 0usize;
#edge_count_impl
all_edges
}
/// Get the number of unique targets in the whole struct.
///
/// Once presented with a list of winners, this set and the set of winners must be
/// equal.
///
/// The resulting indices are sorted.
pub fn unique_targets(&self) -> Vec<#target_type> {
let mut all_targets: Vec<#target_type> = Vec::with_capacity(self.average_edge_count());
let mut maybe_insert_target = |t: #target_type| {
fn unique_targets(&self) -> Vec<Self::Target> {
// NOTE: this implementation returns the targets sorted, but we don't use it yet per
// se, nor is the API enforcing it.
let mut all_targets: Vec<Self::Target> = Vec::with_capacity(self.average_edge_count());
let mut maybe_insert_target = |t: Self::Target| {
match all_targets.binary_search(&t) {
Ok(_) => (),
Err(pos) => all_targets.insert(pos, t)
@@ -217,22 +210,44 @@ fn struct_def(
all_targets
}
/// Get the average edge count.
pub fn average_edge_count(&self) -> usize {
self.edge_count().checked_div(self.len()).unwrap_or(0)
}
/// Remove a certain voter.
///
/// This will only search until the first instance of `to_remove`, and return true. If
/// no instance is found (no-op), then it returns false.
///
/// In other words, if this return true, exactly one element must have been removed from
/// `self.len()`.
pub fn remove_voter(&mut self, to_remove: #voter_type) -> bool {
fn remove_voter(&mut self, to_remove: Self::Voter) -> bool {
#remove_voter_impl
return false
}
fn from_assignment<FV, FT, A>(
assignments: Vec<_npos::Assignment<A, #weight_type>>,
index_of_voter: FV,
index_of_target: FT,
) -> Result<Self, _npos::Error>
where
A: _npos::IdentifierT,
for<'r> FV: Fn(&'r A) -> Option<Self::Voter>,
for<'r> FT: Fn(&'r A) -> Option<Self::Target>,
{
let mut compact: #ident = Default::default();
for _npos::Assignment { who, distribution } in assignments {
match distribution.len() {
0 => continue,
#from_impl
_ => {
return Err(_npos::Error::CompactTargetOverflow);
}
}
};
Ok(compact)
}
fn into_assignment<A: _npos::IdentifierT>(
self,
voter_at: impl Fn(Self::Voter) -> Option<A>,
target_at: impl Fn(Self::Target) -> Option<A>,
) -> Result<Vec<_npos::Assignment<A, #weight_type>>, _npos::Error> {
let mut assignments: Vec<_npos::Assignment<A, #weight_type>> = Default::default();
#into_impl
Ok(assignments)
}
}
))
}
@@ -22,8 +22,8 @@ mod common;
use common::*;
use honggfuzz::fuzz;
use sp_npos_elections::{
assignment_ratio_to_staked_normalized, build_support_map, to_without_backing, VoteWeight,
evaluate_support, is_score_better, seq_phragmen,
assignment_ratio_to_staked_normalized, is_score_better, seq_phragmen, to_supports,
to_without_backing, EvaluateSupport, VoteWeight,
};
use sp_runtime::Perbill;
use rand::{self, SeedableRng};
@@ -66,11 +66,14 @@ fn main() {
};
let unbalanced_score = {
let staked = assignment_ratio_to_staked_normalized(unbalanced.assignments.clone(), &stake_of).unwrap();
let staked = assignment_ratio_to_staked_normalized(
unbalanced.assignments.clone(),
&stake_of,
)
.unwrap();
let winners = to_without_backing(unbalanced.winners.clone());
let support = build_support_map(winners.as_ref(), staked.as_ref()).unwrap();
let score = to_supports(winners.as_ref(), staked.as_ref()).unwrap().evaluate();
let score = evaluate_support(&support);
if score[0] == 0 {
// such cases cannot be improved by balancing.
return;
@@ -87,11 +90,13 @@ fn main() {
).unwrap();
let balanced_score = {
let staked = assignment_ratio_to_staked_normalized(balanced.assignments.clone(), &stake_of).unwrap();
let staked = assignment_ratio_to_staked_normalized(
balanced.assignments.clone(),
&stake_of,
).unwrap();
let winners = to_without_backing(balanced.winners);
let support = build_support_map(winners.as_ref(), staked.as_ref()).unwrap();
to_supports(winners.as_ref(), staked.as_ref()).unwrap().evaluate()
evaluate_support(&support)
};
let enhance = is_score_better(balanced_score, unbalanced_score, Perbill::zero());
@@ -22,8 +22,8 @@ mod common;
use common::*;
use honggfuzz::fuzz;
use sp_npos_elections::{
assignment_ratio_to_staked_normalized, build_support_map, to_without_backing, VoteWeight,
evaluate_support, is_score_better, phragmms,
assignment_ratio_to_staked_normalized, is_score_better, phragmms, to_supports,
to_without_backing, EvaluateSupport, VoteWeight,
};
use sp_runtime::Perbill;
use rand::{self, SeedableRng};
@@ -66,11 +66,14 @@ fn main() {
};
let unbalanced_score = {
let staked = assignment_ratio_to_staked_normalized(unbalanced.assignments.clone(), &stake_of).unwrap();
let staked = assignment_ratio_to_staked_normalized(
unbalanced.assignments.clone(),
&stake_of,
)
.unwrap();
let winners = to_without_backing(unbalanced.winners.clone());
let support = build_support_map(winners.as_ref(), staked.as_ref()).unwrap();
let score = to_supports(&winners, &staked).unwrap().evaluate();
let score = evaluate_support(&support);
if score[0] == 0 {
// such cases cannot be improved by balancing.
return;
@@ -86,11 +89,13 @@ fn main() {
).unwrap();
let balanced_score = {
let staked = assignment_ratio_to_staked_normalized(balanced.assignments.clone(), &stake_of).unwrap();
let staked =
assignment_ratio_to_staked_normalized(balanced.assignments.clone(), &stake_of)
.unwrap();
let winners = to_without_backing(balanced.winners);
let support = build_support_map(winners.as_ref(), staked.as_ref()).unwrap();
evaluate_support(&support)
to_supports(winners.as_ref(), staked.as_ref())
.unwrap()
.evaluate()
};
let enhance = is_score_better(balanced_score, unbalanced_score, Perbill::zero());
@@ -34,8 +34,8 @@ use honggfuzz::fuzz;
mod common;
use common::to_range;
use sp_npos_elections::{StakedAssignment, ExtendedBalance, build_support_map, reduce};
use rand::{self, Rng, SeedableRng, RngCore};
use sp_npos_elections::{reduce, to_support_map, ExtendedBalance, StakedAssignment};
use rand::{self, Rng, RngCore, SeedableRng};
type Balance = u128;
type AccountId = u64;
@@ -109,9 +109,8 @@ fn assert_assignments_equal(
ass1: &Vec<StakedAssignment<AccountId>>,
ass2: &Vec<StakedAssignment<AccountId>>,
) {
let support_1 = build_support_map::<AccountId>(winners, ass1).unwrap();
let support_2 = build_support_map::<AccountId>(winners, ass2).unwrap();
let support_1 = to_support_map::<AccountId>(winners, ass1).unwrap();
let support_2 = to_support_map::<AccountId>(winners, ass2).unwrap();
for (who, support) in support_1.iter() {
assert_eq!(support.total, support_2.get(who).unwrap().total);
@@ -18,21 +18,21 @@
//! Helper methods for npos-elections.
use crate::{
Assignment, ExtendedBalance, VoteWeight, IdentifierT, StakedAssignment, WithApprovalOf, Error,
Assignment, Error, ExtendedBalance, IdentifierT, PerThing128, StakedAssignment, VoteWeight,
WithApprovalOf,
};
use sp_arithmetic::{PerThing, InnerOf};
use sp_arithmetic::{InnerOf, PerThing};
use sp_std::prelude::*;
/// 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: PerThing, FS>(
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,
P: sp_std::ops::Mul<ExtendedBalance, Output = ExtendedBalance>,
ExtendedBalance: From<InnerOf<P>>,
{
ratios
@@ -45,19 +45,21 @@ where
}
/// Same as [`assignment_ratio_to_staked`] and try and do normalization.
pub fn assignment_ratio_to_staked_normalized<A: IdentifierT, P: PerThing, FS>(
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,
P: sp_std::ops::Mul<ExtendedBalance, Output = ExtendedBalance>,
ExtendedBalance: From<InnerOf<P>>,
{
let mut staked = assignment_ratio_to_staked(ratio, &stake_of);
staked.iter_mut().map(|a|
a.try_normalize(stake_of(&a.who).into()).map_err(|err| Error::ArithmeticError(err))
).collect::<Result<_, _>>()?;
staked
.iter_mut()
.map(|a| {
a.try_normalize(stake_of(&a.who).into()).map_err(|err| Error::ArithmeticError(err))
})
.collect::<Result<_, _>>()?;
Ok(staked)
}
@@ -74,7 +76,7 @@ where
}
/// Same as [`assignment_staked_to_ratio`] and try and do normalization.
pub fn assignment_staked_to_ratio_normalized<A: IdentifierT, P: PerThing>(
pub fn assignment_staked_to_ratio_normalized<A: IdentifierT, P: PerThing128>(
staked: Vec<StakedAssignment<A>>,
) -> Result<Vec<Assignment<A, P>>, Error>
where
+238 -112
View File
@@ -21,8 +21,8 @@
//! - [`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 `balances`, which in turn can increase its score.
//! - [`balance`]: Implements the star balancing algorithm. This iterative process can push a
//! solution toward being more `balances`, which in turn can increase its score.
//!
//! ### Terminology
//!
@@ -57,12 +57,11 @@
//!
//! // 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 vy voters, therefor it is stored as a mapping called
//! accessed in a map-like manner, i.e. keyed by voters, therefor it is stored as a mapping called
//! `SupportMap`.
//!
//! Moreover, the support is built from absolute backing values, not ratios like the example above.
@@ -74,18 +73,25 @@
#![cfg_attr(not(feature = "std"), no_std)]
use sp_std::{
prelude::*, collections::btree_map::BTreeMap, fmt::Debug, cmp::Ordering, rc::Rc, cell::RefCell,
};
use sp_arithmetic::{
PerThing, Rational128, ThresholdOrd, InnerOf, Normalizable,
traits::{Zero, Bounded},
traits::{Bounded, UniqueSaturatedInto, Zero},
InnerOf, Normalizable, PerThing, Rational128, ThresholdOrd,
};
use sp_std::{
cell::RefCell,
cmp::Ordering,
collections::btree_map::BTreeMap,
convert::{TryFrom, TryInto},
fmt::Debug,
ops::Mul,
prelude::*,
rc::Rc,
};
use sp_core::RuntimeDebug;
use codec::{Decode, Encode};
#[cfg(feature = "std")]
use serde::{Serialize, Deserialize};
#[cfg(feature = "std")]
use codec::{Encode, Decode};
use serde::{Deserialize, Serialize};
#[cfg(test)]
mod mock;
@@ -125,22 +131,107 @@ impl<T> __OrInvalidIndex<T> for Option<T> {
}
}
/// A common interface for all compact solutions.
///
/// See [`sp-npos-elections-compact`] for more info.
pub trait CompactSolution: Sized {
/// The maximum number of votes that are allowed.
const LIMIT: usize;
/// The voter type. Needs to be an index (convert to usize).
type Voter: UniqueSaturatedInto<usize> + TryInto<usize> + TryFrom<usize> + Debug + Copy + Clone;
/// The target type. Needs to be an index (convert to usize).
type Target: UniqueSaturatedInto<usize> + TryInto<usize> + TryFrom<usize> + Debug + Copy + Clone;
/// The weight/accuracy type of each vote.
type Accuracy: PerThing128;
/// Build self from a `assignments: Vec<Assignment<A, Self::Accuracy>>`.
fn from_assignment<FV, FT, A>(
assignments: Vec<Assignment<A, Self::Accuracy>>,
voter_index: FV,
target_index: FT,
) -> Result<Self, Error>
where
A: IdentifierT,
for<'r> FV: Fn(&'r A) -> Option<Self::Voter>,
for<'r> FT: Fn(&'r A) -> Option<Self::Target>;
/// Convert self into a `Vec<Assignment<A, Self::Accuracy>>`
fn into_assignment<A: IdentifierT>(
self,
voter_at: impl Fn(Self::Voter) -> Option<A>,
target_at: impl Fn(Self::Target) -> Option<A>,
) -> Result<Vec<Assignment<A, Self::Accuracy>>, Error>;
/// Get the length of all the voters that this type is encoding.
///
/// This is basically the same as the number of assignments, or number of active voters.
fn voter_count(&self) -> usize;
/// Get the total count of edges.
///
/// This is effectively in the range of {[`Self::voter_count`], [`Self::voter_count`] *
/// [`Self::LIMIT`]}.
fn edge_count(&self) -> usize;
/// Get the number of unique targets in the whole struct.
///
/// Once presented with a list of winners, this set and the set of winners must be
/// equal.
fn unique_targets(&self) -> Vec<Self::Target>;
/// Get the average edge count.
fn average_edge_count(&self) -> usize {
self.edge_count()
.checked_div(self.voter_count())
.unwrap_or(0)
}
/// Remove a certain voter.
///
/// This will only search until the first instance of `to_remove`, and return true. If
/// no instance is found (no-op), then it returns false.
///
/// In other words, if this return true, exactly **one** element must have been removed from
/// `self.len()`.
fn remove_voter(&mut self, to_remove: Self::Voter) -> bool;
/// Compute the score of this compact solution type.
fn score<A, FS>(
self,
winners: &[A],
stake_of: FS,
voter_at: impl Fn(Self::Voter) -> Option<A>,
target_at: impl Fn(Self::Target) -> Option<A>,
) -> Result<ElectionScore, Error>
where
for<'r> FS: Fn(&'r A) -> VoteWeight,
A: IdentifierT,
ExtendedBalance: From<InnerOf<Self::Accuracy>>,
{
let ratio = self.into_assignment(voter_at, target_at)?;
let staked = helpers::assignment_ratio_to_staked_normalized(ratio, stake_of)?;
let supports = to_supports(winners, &staked)?;
Ok(supports.evaluate())
}
}
// re-export the compact solution type.
pub use sp_npos_elections_compact::generate_solution_type;
/// A trait to limit the number of votes per voter. The generated compact type will implement this.
pub trait VotingLimit {
const LIMIT: usize;
}
/// an aggregator trait for a generic type of a voter/target identifier. This usually maps to
/// substrate's account id.
pub trait IdentifierT: Clone + Eq + Default + Ord + Debug + codec::Codec {}
impl<T: Clone + Eq + Default + 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 {}
/// The errors that might occur in the this crate and compact.
#[derive(Debug, Eq, PartialEq)]
#[derive(Eq, PartialEq, RuntimeDebug)]
pub enum Error {
/// While going from compact to staked, the stake of all the edges has gone above the total and
/// the last stake cannot be assigned.
@@ -151,6 +242,8 @@ pub enum Error {
CompactInvalidIndex,
/// An error occurred in some arithmetic operation.
ArithmeticError(&'static str),
/// The data provided to create support map was invalid.
InvalidSupportEdge,
}
/// A type which is used in the API of this crate as a numeric weight of a vote, most often the
@@ -160,7 +253,8 @@ pub type VoteWeight = u64;
/// A type in which performing operations on vote weights are safe.
pub type ExtendedBalance = u128;
/// The score of an assignment. This can be computed from the support map via [`evaluate_support`].
/// The score of an assignment. This can be computed from the support map via
/// [`EvaluateSupport::evaluate`].
pub type ElectionScore = [ExtendedBalance; 3];
/// A winner, with their respective approval stake.
@@ -170,7 +264,7 @@ pub type WithApprovalOf<A> = (A, ExtendedBalance);
pub type CandidatePtr<A> = Rc<RefCell<Candidate<A>>>;
/// A candidate entity for the election.
#[derive(Debug, Clone, Default)]
#[derive(RuntimeDebug, Clone, Default)]
pub struct Candidate<AccountId> {
/// Identifier.
who: AccountId,
@@ -311,7 +405,7 @@ impl<AccountId: IdentifierT> Voter<AccountId> {
}
/// Final result of the election.
#[derive(Debug)]
#[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.
@@ -322,7 +416,7 @@ pub struct ElectionResult<AccountId, P: PerThing> {
}
/// A voter's stake assignment among a set of targets, represented as ratios.
#[derive(Debug, Clone, Default)]
#[derive(RuntimeDebug, Clone, Default)]
#[cfg_attr(feature = "std", derive(PartialEq, Eq, Encode, Decode))]
pub struct Assignment<AccountId, P: PerThing> {
/// Voter's identifier.
@@ -331,24 +425,20 @@ pub struct Assignment<AccountId, P: PerThing> {
pub distribution: Vec<(AccountId, P)>,
}
impl<AccountId: IdentifierT, P: PerThing> Assignment<AccountId, P>
where
ExtendedBalance: From<InnerOf<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. If `fill` is set to true, it
/// _tries_ to ensure that all the potential rounding errors are compensated and the
/// distribution's sum is exactly equal to the total budget, by adding or subtracting the
/// remainder from the last distribution.
/// 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>
where
P: sp_std::ops::Mul<ExtendedBalance, Output = ExtendedBalance>,
{
let distribution = self.distribution
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.
@@ -396,7 +486,7 @@ where
/// A voter's stake assignment among a set of targets, represented as absolute values in the scale
/// of [`ExtendedBalance`].
#[derive(Debug, Clone, Default)]
#[derive(RuntimeDebug, Clone, Default)]
#[cfg_attr(feature = "std", derive(PartialEq, Eq, Encode, Decode))]
pub struct StakedAssignment<AccountId> {
/// Voter's identifier
@@ -408,11 +498,8 @@ pub struct StakedAssignment<AccountId> {
impl<AccountId> StakedAssignment<AccountId> {
/// Converts self into the normal [`Assignment`] type.
///
/// If `fill` is set to true, it _tries_ to ensure that all the potential rounding errors are
/// compensated and the distribution's sum is exactly equal to 100%, by adding or subtracting
/// the remainder from the last distribution.
///
/// NOTE: it is quite critical that this attempt always works. The data type returned here will
/// 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.
///
@@ -479,8 +566,8 @@ impl<AccountId> StakedAssignment<AccountId> {
///
/// This, at the current version, resembles the `Exposure` defined in the Staking pallet, yet they
/// do not necessarily have to be the same.
#[derive(Default, Debug)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Eq, PartialEq))]
#[derive(Default, RuntimeDebug, Encode, Decode, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub struct Support<AccountId> {
/// Total support.
pub total: ExtendedBalance,
@@ -488,51 +575,43 @@ pub struct Support<AccountId> {
pub voters: Vec<(AccountId, ExtendedBalance)>,
}
/// A linkage from a candidate and its [`Support`].
/// 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 given election result. It maps a flat structure like
/// Helper trait to convert from a support map to a flat support vector.
pub trait FlattenSupportMap<A> {
/// Flatten the support.
fn flatten(self) -> Supports<A>;
}
impl<A> FlattenSupportMap<A> for SupportMap<A> {
fn flatten(self) -> Supports<A> {
self.into_iter().collect::<Vec<_>>()
}
}
/// Build the support map from the winners and assignments.
///
/// ```nocompile
/// assignments: vec![
/// voter1, vec![(candidate1, w11), (candidate2, w12)],
/// voter2, vec![(candidate1, w21), (candidate2, w22)]
/// ]
/// ```
///
/// into a mapping of candidates and their respective support:
///
/// ```nocompile
/// SupportMap {
/// candidate1: Support {
/// own:0,
/// total: w11 + w21,
/// others: vec![(candidate1, w11), (candidate2, w21)]
/// },
/// candidate2: Support {
/// own:0,
/// total: w12 + w22,
/// others: vec![(candidate1, w12), (candidate2, w22)]
/// },
/// }
/// ```
///
/// The second returned flag indicates the number of edges who didn't corresponded to an actual
/// winner from the given winner set. A value in this place larger than 0 indicates a potentially
/// faulty assignment.
///
/// `O(E)` where `E` is the total number of edges.
pub fn build_support_map<AccountId>(
winners: &[AccountId],
assignments: &[StakedAssignment<AccountId>],
) -> Result<SupportMap<AccountId>, AccountId> where
AccountId: IdentifierT,
{
/// The list of winners is basically a redundancy for error checking only; It ensures that all the
/// targets pointed to by the [`Assignment`] are present in the `winners`.
pub fn to_support_map<A: IdentifierT>(
winners: &[A],
assignments: &[StakedAssignment<A>],
) -> Result<SupportMap<A>, Error> {
// Initialize the support of each candidate.
let mut supports = <SupportMap<AccountId>>::new();
winners
.iter()
.for_each(|e| { supports.insert(e.clone(), Default::default()); });
let mut supports = <SupportMap<A>>::new();
winners.iter().for_each(|e| {
supports.insert(e.clone(), Default::default());
});
// build support struct.
for StakedAssignment { who, distribution } in assignments.iter() {
@@ -541,37 +620,83 @@ pub fn build_support_map<AccountId>(
support.total = support.total.saturating_add(*weight_extended);
support.voters.push((who.clone(), *weight_extended));
} else {
return Err(c.clone())
return Err(Error::InvalidSupportEdge)
}
}
}
Ok(supports)
}
/// Evaluate a support map. The returned tuple contains:
/// Same as [`to_support_map`] except it calls `FlattenSupportMap` on top of the result to return a
/// flat vector.
///
/// - 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**.
/// Similar to [`to_support_map`], `winners` is used for error checking.
pub fn to_supports<A: IdentifierT>(
winners: &[A],
assignments: &[StakedAssignment<A>],
) -> Result<Supports<A>, Error> {
to_support_map(winners, assignments).map(FlattenSupportMap::flatten)
}
/// Extension trait for evaluating a support map or vector.
pub trait EvaluateSupport<K> {
/// 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;
}
/// A common wrapper trait for both (&A, &B) and &(A, B).
///
/// `O(E)` where `E` is the total number of edges.
pub fn evaluate_support<AccountId>(
support: &SupportMap<AccountId>,
) -> ElectionScore {
let mut min_support = ExtendedBalance::max_value();
let mut sum: 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_squared: ExtendedBalance = Zero::zero();
for (_, support) in support.iter() {
sum = sum.saturating_add(support.total);
let squared = support.total.saturating_mul(support.total);
sum_squared = sum_squared.saturating_add(squared);
if support.total < min_support {
min_support = support.total;
}
/// This allows us to implemented something for both `Vec<_>` and `BTreeMap<_>`, such as
/// [`EvaluateSupport`].
pub trait TupleRef<K, V> {
fn extract(&self) -> (&K, &V);
}
impl<K, V> TupleRef<K, V> for &(K, V) {
fn extract(&self) -> (&K, &V) {
(&self.0, &self.1)
}
}
impl<K, V> TupleRef<K, V> for (K, V) {
fn extract(&self) -> (&K, &V) {
(&self.0, &self.1)
}
}
impl<K, V> TupleRef<K, V> for (&K, &V) {
fn extract(&self) -> (&K, &V) {
(self.0, self.1)
}
}
impl<A, C, I> EvaluateSupport<A> for C
where
C: IntoIterator<Item = I>,
I: TupleRef<A, Support<A>>,
A: IdentifierT,
{
fn evaluate(self) -> ElectionScore {
let mut min_support = ExtendedBalance::max_value();
let mut sum: 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_squared: ExtendedBalance = Zero::zero();
for item in self {
let (_, support) = item.extract();
sum = sum.saturating_add(support.total);
let squared = support.total.saturating_mul(support.total);
sum_squared = sum_squared.saturating_add(squared);
if support.total < min_support {
min_support = support.total;
}
}
[min_support, sum, sum_squared]
}
[min_support, sum, sum_squared]
}
/// Compares two sets of election scores based on desirability and returns true if `this` is better
@@ -582,14 +707,15 @@ pub fn evaluate_support<AccountId>(
///
/// Note that the third component should be minimized.
pub fn is_score_better<P: PerThing>(this: ElectionScore, that: ElectionScore, epsilon: P) -> bool
where ExtendedBalance: From<sp_arithmetic::InnerOf<P>>
where
ExtendedBalance: From<InnerOf<P>>,
{
match this
.iter()
.enumerate()
.map(|(i, e)| (
e.ge(&that[i]),
e.tcmp(&that[i], epsilon.mul_ceil(that[i])),
.zip(that.iter())
.map(|(thi, tha)| (
thi.ge(&tha),
thi.tcmp(&tha, epsilon.mul_ceil(*tha)),
))
.collect::<Vec<(bool, Ordering)>>()
.as_slice()
@@ -19,10 +19,13 @@
#![cfg(test)]
use crate::{seq_phragmen, ElectionResult, Assignment, VoteWeight, ExtendedBalance};
use sp_arithmetic::{PerThing, InnerOf, traits::{SaturatedConversion, Zero, One}};
use sp_std::collections::btree_map::BTreeMap;
use crate::*;
use sp_arithmetic::{
traits::{One, SaturatedConversion, Zero},
InnerOf, PerThing,
};
use sp_runtime::assert_eq_error_rate;
use sp_std::collections::btree_map::BTreeMap;
#[derive(Default, Debug)]
pub(crate) struct _Candidate<A> {
@@ -313,14 +316,13 @@ pub fn check_assignments_sum<T: PerThing>(assignments: Vec<Assignment<AccountId,
}
}
pub(crate) fn run_and_compare<Output: PerThing>(
pub(crate) fn run_and_compare<Output: PerThing128>(
candidates: Vec<AccountId>,
voters: Vec<(AccountId, Vec<AccountId>)>,
stake_of: &Box<dyn Fn(&AccountId) -> VoteWeight>,
to_elect: usize,
) where
ExtendedBalance: From<InnerOf<Output>>,
Output: sp_std::ops::Mul<ExtendedBalance, Output = ExtendedBalance>,
{
// run fixed point code.
let ElectionResult { winners, assignments } = seq_phragmen::<_, Output>(
@@ -21,15 +21,15 @@
//! to the Maximin problem.
use crate::{
IdentifierT, VoteWeight, Voter, CandidatePtr, ExtendedBalance, setup_inputs, ElectionResult,
balancing, setup_inputs, CandidatePtr, ElectionResult, ExtendedBalance, IdentifierT,
PerThing128, VoteWeight, Voter,
};
use sp_arithmetic::{
helpers_128bit::multiply_by_rational,
traits::{Bounded, Zero},
InnerOf, Rational128,
};
use sp_std::prelude::*;
use sp_arithmetic::{
PerThing, InnerOf, Rational128,
helpers_128bit::multiply_by_rational,
traits::{Zero, Bounded},
};
use crate::balancing;
/// 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
@@ -63,12 +63,15 @@ const DEN: ExtendedBalance = ExtendedBalance::max_value();
/// `expect` this to return `Ok`.
///
/// This can only fail if the normalization fails.
pub fn seq_phragmen<AccountId: IdentifierT, P: PerThing>(
pub fn seq_phragmen<AccountId: IdentifierT, P: PerThing128>(
rounds: usize,
initial_candidates: Vec<AccountId>,
initial_voters: Vec<(AccountId, VoteWeight, Vec<AccountId>)>,
balance: Option<(usize, ExtendedBalance)>,
) -> Result<ElectionResult<AccountId, P>, &'static str> where ExtendedBalance: From<InnerOf<P>> {
) -> Result<ElectionResult<AccountId, P>, crate::Error>
where
ExtendedBalance: From<InnerOf<P>>,
{
let (candidates, voters) = setup_inputs(initial_candidates, initial_voters);
let (candidates, mut voters) = seq_phragmen_core::<AccountId>(
@@ -93,11 +96,16 @@ pub fn seq_phragmen<AccountId: IdentifierT, P: PerThing>(
// 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<_>>();
let _ = assignments.iter_mut().map(|a| a.try_normalize()).collect::<Result<(), _>>()?;
let winners = winners.into_iter().map(|w_ptr|
(w_ptr.borrow().who.clone(), w_ptr.borrow().backed_stake)
).collect();
let mut assignments =
voters.into_iter().filter_map(|v| v.into_assignment()).collect::<Vec<_>>();
let _ = assignments
.iter_mut()
.map(|a| a.try_normalize().map_err(|e| crate::Error::ArithmeticError(e)))
.collect::<Result<(), _>>()?;
let winners = winners
.into_iter()
.map(|w_ptr| (w_ptr.borrow().who.clone(), w_ptr.borrow().backed_stake))
.collect();
Ok(ElectionResult { winners, assignments })
}
@@ -114,7 +122,7 @@ pub fn seq_phragmen_core<AccountId: IdentifierT>(
rounds: usize,
candidates: Vec<CandidatePtr<AccountId>>,
mut voters: Vec<Voter<AccountId>>,
) -> Result<(Vec<CandidatePtr<AccountId>>, Vec<Voter<AccountId>>), &'static str> {
) -> 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 = rounds.min(candidates.len());
@@ -198,7 +206,7 @@ pub fn seq_phragmen_core<AccountId: IdentifierT>(
// 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()?;
voter.try_normalize_elected().map_err(|e| crate::Error::ArithmeticError(e))?;
}
Ok((candidates, voters))
@@ -23,7 +23,7 @@
use crate::{
IdentifierT, ElectionResult, ExtendedBalance, setup_inputs, VoteWeight, Voter, CandidatePtr,
balance,
balance, PerThing128,
};
use sp_arithmetic::{PerThing, InnerOf, Rational128, traits::Bounded};
use sp_std::{prelude::*, rc::Rc};
@@ -41,13 +41,14 @@ use sp_std::{prelude::*, rc::Rc};
/// 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: PerThing>(
pub fn phragmms<AccountId: IdentifierT, P: PerThing128>(
to_elect: usize,
initial_candidates: Vec<AccountId>,
initial_voters: Vec<(AccountId, VoteWeight, Vec<AccountId>)>,
balancing_config: Option<(usize, ExtendedBalance)>,
) -> Result<ElectionResult<AccountId, P>, &'static str>
where ExtendedBalance: From<InnerOf<P>>
where
ExtendedBalance: From<InnerOf<P>>,
{
let (candidates, mut voters) = setup_inputs(initial_candidates, initial_voters);
@@ -17,14 +17,13 @@
//! Tests for npos-elections.
use crate::mock::*;
use crate::{
seq_phragmen, balancing, build_support_map, is_score_better, helpers::*,
Support, StakedAssignment, Assignment, ElectionResult, ExtendedBalance, setup_inputs,
seq_phragmen_core, Voter,
balancing, helpers::*, is_score_better, mock::*, seq_phragmen, seq_phragmen_core, setup_inputs,
to_support_map, to_supports, Assignment, ElectionResult, ExtendedBalance, StakedAssignment,
Support, Voter, EvaluateSupport,
};
use sp_arithmetic::{PerU16, Perbill, Percent, Permill};
use substrate_test_utils::assert_eq_uvec;
use sp_arithmetic::{Perbill, Permill, Percent, PerU16};
#[test]
fn float_phragmen_poc_works() {
@@ -53,22 +52,22 @@ fn float_phragmen_poc_works() {
assert_eq!(
support_map.get(&2).unwrap(),
&_Support { own: 0.0, total: 25.0, others: vec![(10u64, 10.0), (30u64, 15.0)]}
&_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)]}
&_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)]}
&_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)]}
&_Support { own: 0.0, total: 30.0, others: vec![(20u64, 20.0), (30u64, 10.0)] }
);
}
@@ -300,7 +299,7 @@ fn phragmen_poc_works() {
let staked = assignment_ratio_to_staked(assignments, &stake_of);
let winners = to_without_backing(winners);
let support_map = build_support_map::<AccountId>(&winners, &staked).unwrap();
let support_map = to_support_map::<AccountId>(&winners, &staked).unwrap();
assert_eq_uvec!(
staked,
@@ -374,7 +373,7 @@ fn phragmen_poc_works_with_balancing() {
let staked = assignment_ratio_to_staked(assignments, &stake_of);
let winners = to_without_backing(winners);
let support_map = build_support_map::<AccountId>(&winners, &staked).unwrap();
let support_map = to_support_map::<AccountId>(&winners, &staked).unwrap();
assert_eq_uvec!(
staked,
@@ -766,7 +765,7 @@ fn phragmen_self_votes_should_be_kept() {
let staked_assignments = assignment_ratio_to_staked(result.assignments, &stake_of);
let winners = to_without_backing(result.winners);
let supports = build_support_map::<AccountId>(&winners, &staked_assignments).unwrap();
let supports = to_support_map::<AccountId>(&winners, &staked_assignments).unwrap();
assert_eq!(supports.get(&5u64), None);
assert_eq!(
@@ -839,6 +838,34 @@ fn duplicate_target_is_ignored_when_winner() {
);
}
#[test]
fn support_map_and_vec_can_be_evaluated() {
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 {
winners,
assignments,
} = seq_phragmen::<_, Perbill>(
2,
candidates,
voters
.iter()
.map(|(ref v, ref vs)| (v.clone(), stake_of(v), vs.clone()))
.collect::<Vec<_>>(),
None,
)
.unwrap();
let staked = assignment_ratio_to_staked(assignments, &stake_of);
let winners = to_without_backing(winners);
let support_map = to_support_map::<AccountId>(&winners, &staked).unwrap();
let support_vec = to_supports(&winners, &staked).unwrap();
assert_eq!(support_map.evaluate(), support_vec.evaluate());
}
mod assignment_convert_normalize {
use super::*;
#[test]
@@ -1112,15 +1139,12 @@ mod score {
}
mod solution_type {
use codec::{Decode, Encode};
use super::AccountId;
use codec::{Decode, Encode};
// these need to come from the same dev-dependency `sp-npos-elections`, not from the crate.
use crate::{
generate_solution_type, Assignment,
Error as PhragmenError,
};
use sp_std::{convert::TryInto, fmt::Debug};
use crate::{generate_solution_type, Assignment, CompactSolution, Error as PhragmenError};
use sp_arithmetic::Percent;
use sp_std::{convert::TryInto, fmt::Debug};
type TestAccuracy = Percent;
@@ -1136,7 +1160,6 @@ mod solution_type {
#[compact]
struct InnerTestSolutionCompact::<u32, u8, Percent>(12)
);
}
#[test]
@@ -1190,7 +1213,7 @@ mod solution_type {
compact,
Decode::decode(&mut &encoded[..]).unwrap(),
);
assert_eq!(compact.len(), 4);
assert_eq!(compact.voter_count(), 4);
assert_eq!(compact.edge_count(), 2 + 4);
assert_eq!(compact.unique_targets(), vec![10, 11, 20, 40, 50, 51]);
}
@@ -1326,7 +1349,7 @@ mod solution_type {
).unwrap();
// basically number of assignments that it is encoding.
assert_eq!(compacted.len(), assignments.len());
assert_eq!(compacted.voter_count(), assignments.len());
assert_eq!(
compacted.edge_count(),
assignments.iter().fold(0, |a, b| a + b.distribution.len()),
@@ -1410,9 +1433,12 @@ mod solution_type {
..Default::default()
};
assert_eq!(compact.unique_targets(), vec![1, 2, 3, 4, 7, 8, 11, 12, 13, 66, 67]);
assert_eq!(
compact.unique_targets(),
vec![1, 2, 3, 4, 7, 8, 11, 12, 13, 66, 67]
);
assert_eq!(compact.edge_count(), 2 + (2 * 2) + 3 + 16);
assert_eq!(compact.len(), 6);
assert_eq!(compact.voter_count(), 6);
// this one has some duplicates.
let compact = TestSolutionCompact {
@@ -1429,7 +1455,7 @@ mod solution_type {
assert_eq!(compact.unique_targets(), vec![1, 3, 4, 7, 8, 11, 13]);
assert_eq!(compact.edge_count(), 2 + (2 * 2) + 3);
assert_eq!(compact.len(), 5);
assert_eq!(compact.voter_count(), 5);
}
#[test]