mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-04-29 16:07:57 +00:00
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:
Generated
+12
@@ -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",
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user