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

Commit

Permalink
Implements approval voting to use as a NposSolver (#13367)
Browse files Browse the repository at this point in the history
* Implements the approval voting methods in sp_npos_elections
  • Loading branch information
gpestana authored Feb 17, 2023
1 parent 8a1ebac commit 5343a6c
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 9 deletions.
4 changes: 2 additions & 2 deletions bin/node/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
use codec::{Decode, Encode, MaxEncodedLen};
use frame_election_provider_support::{
onchain, weights::SubstrateWeight, BalancingConfig, ElectionDataProvider, SequentialPhragmen,
VoteWeight,
VoteWeight, ApprovalVoting,
};
use frame_support::{
construct_runtime,
Expand Down Expand Up @@ -1037,7 +1037,7 @@ impl pallet_elections::Config for Runtime {
type MaxVoters = MaxVoters;
type MaxVotesPerVoter = MaxVotesPerVoter;
type MaxCandidates = MaxCandidates;
type ElectionSolver = SequentialPhragmen<Self::AccountId, Perbill>;
type ElectionSolver = ApprovalVoting<Self::AccountId, Perbill>;
type SolverWeightInfo = SubstrateWeight<Runtime>;
type WeightInfo = pallet_elections::weights::SubstrateWeight<Runtime>;
}
Expand Down
15 changes: 14 additions & 1 deletion frame/election-provider-support/benchmarking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

use codec::Decode;
use frame_benchmarking::v1::{benchmarks, Vec};
use frame_election_provider_support::{NposSolver, PhragMMS, SequentialPhragmen};
use frame_election_provider_support::{ApprovalVoting, NposSolver, PhragMMS, SequentialPhragmen};

pub struct Pallet<T: Config>(frame_system::Pallet<T>);
pub trait Config: frame_system::Config {}
Expand Down Expand Up @@ -88,4 +88,17 @@ benchmarks! {
::solve(d as usize, targets, voters).is_ok()
);
}

approval_voting {
let v in (VOTERS[0]) .. VOTERS[1];
let t in (TARGETS[0]) .. TARGETS[1];
let d in (VOTES_PER_VOTER[0]) .. VOTES_PER_VOTER[1];

let (voters, targets) = set_up_voters_targets::<T::AccountId>(v, t, d as usize);
}: {
assert!(
ApprovalVoting::<T::AccountId, sp_runtime::Perbill>
::solve(d as usize, targets, voters).is_ok()
);
}
}
23 changes: 23 additions & 0 deletions frame/election-provider-support/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,29 @@ impl<AccountId: IdentifierT, Accuracy: PerThing128, Balancing: Get<Option<Balanc
}
}

/// A wrapper for [`sp_npos_elections::approval_voting()`] that implements [`NposSolver`]. See the
/// documentation of [`sp_npos_elections::approval_voting()`] for more info.
pub struct ApprovalVoting<AccountId, Accuracy>(sp_std::marker::PhantomData<(AccountId, Accuracy)>);

impl<AccountId: IdentifierT, Accuracy: PerThing128> NposSolver
for ApprovalVoting<AccountId, Accuracy>
{
type AccountId = AccountId;
type Accuracy = Accuracy;
type Error = sp_npos_elections::Error;
fn solve(
winners: usize,
targets: Vec<Self::AccountId>,
voters: Vec<(Self::AccountId, VoteWeight, impl IntoIterator<Item = Self::AccountId>)>,
) -> Result<ElectionResult<Self::AccountId, Self::Accuracy>, Self::Error> {
sp_npos_elections::approval_voting(winners, targets, voters)
}

fn weight<T: WeightInfo>(voters: u32, targets: u32, vote_degree: u32) -> Weight {
T::approval_voting(voters, targets, vote_degree)
}
}

/// A voter, at the level of abstraction of this crate.
pub type Voter<AccountId, Bound> = (AccountId, VoteWeight, BoundedVec<AccountId, Bound>);

Expand Down
30 changes: 29 additions & 1 deletion frame/election-provider-support/src/onchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ impl<T: Config> ElectionProvider for OnChainExecution<T> {
#[cfg(test)]
mod tests {
use super::*;
use crate::{ElectionProvider, PhragMMS, SequentialPhragmen};
use crate::{ApprovalVoting, ElectionProvider, PhragMMS, SequentialPhragmen};
use frame_support::{assert_noop, parameter_types, traits::ConstU32};
use sp_npos_elections::Support;
use sp_runtime::Perbill;
Expand Down Expand Up @@ -235,6 +235,7 @@ mod tests {

struct PhragmenParams;
struct PhragMMSParams;
struct ApprovalVotingParams;

parameter_types! {
pub static MaxWinners: u32 = 10;
Expand All @@ -261,6 +262,16 @@ mod tests {
type TargetsBound = ConstU32<400>;
}

impl Config for ApprovalVotingParams {
type System = Runtime;
type Solver = ApprovalVoting<AccountId, Perbill>;
type DataProvider = mock_data_provider::DataProvider;
type WeightInfo = ();
type MaxWinners = MaxWinners;
type VotersBound = ConstU32<600>;
type TargetsBound = ConstU32<400>;
}

mod mock_data_provider {
use frame_support::{bounded_vec, traits::ConstU32};

Expand Down Expand Up @@ -333,4 +344,21 @@ mod tests {
);
})
}

#[test]
fn onchain_approval_voting_works() {
sp_io::TestExternalities::new_empty().execute_with(|| {
DesiredTargets::set(3);

// note that the `OnChainExecution::elect` implementation normalizes the vote weights.
assert_eq!(
<OnChainExecution::<ApprovalVotingParams> as ElectionProvider>::elect().unwrap(),
vec![
(10, Support { total: 20, voters: vec![(1, 5), (3, 15)] }),
(20, Support { total: 15, voters: vec![(1, 5), (2, 10)] }),
(30, Support { total: 25, voters: vec![(2, 10), (3, 15)] })
]
)
})
}
}
8 changes: 8 additions & 0 deletions frame/election-provider-support/src/weights.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ use sp_std::marker::PhantomData;
pub trait WeightInfo {
fn phragmen(v: u32, t: u32, d: u32, ) -> Weight;
fn phragmms(v: u32, t: u32, d: u32, ) -> Weight;
fn approval_voting(v: u32, t: u32, d: u32, ) -> Weight;
}

/// Weights for pallet_election_provider_support_benchmarking using the Substrate node and recommended hardware.
Expand All @@ -70,6 +71,10 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
// Standard Error: 6_649_000
.saturating_add(Weight::from_ref_time(1_711_424_000 as u64).saturating_mul(d as u64))
}

fn approval_voting(_v: u32, _t: u32, _d: u32, ) -> Weight {
Weight::zero()
}
}

// For backwards compatibility and tests
Expand All @@ -92,4 +97,7 @@ impl WeightInfo for () {
// Standard Error: 6_649_000
.saturating_add(Weight::from_ref_time(1_711_424_000 as u64).saturating_mul(d as u64))
}
fn approval_voting(_v: u32, _t: u32, _d: u32, ) -> Weight {
Weight::zero()
}
}
4 changes: 2 additions & 2 deletions frame/elections/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1285,7 +1285,7 @@ impl<T: Config> ContainsLengthBound for Pallet<T> {
mod tests {
use super::*;
use crate as elections;
use frame_election_provider_support::{weights::SubstrateWeight, SequentialPhragmen};
use frame_election_provider_support::{weights::SubstrateWeight, ApprovalVoting};
use frame_support::{
assert_noop, assert_ok,
dispatch::DispatchResultWithPostInfo,
Expand Down Expand Up @@ -1419,7 +1419,7 @@ mod tests {
type WeightInfo = ();
type MaxVoters = MaxVoters;
type MaxCandidates = MaxCandidates;
type ElectionSolver = SequentialPhragmen<Self::AccountId, Perbill>;
type ElectionSolver = ApprovalVoting<Self::AccountId, Perbill>;
type SolverWeightInfo = SubstrateWeight<Test>;
type MaxVotesPerVoter = ConstU32<16>;
}
Expand Down
78 changes: 78 additions & 0 deletions primitives/npos-elections/src/approval_voting.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// This file is part of Substrate.

// Copyright (C) 2023 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Implementation of the approval voting election method.
//!
//! This method allows voters to select many candidates and backing each of them with the same
//! vote weight. The candidates with the most backing are the election winners.
use crate::{setup_inputs, ElectionResult, IdentifierT, PerThing128, VoteWeight};
use sp_arithmetic::traits::Zero;
use sp_std::{cmp::Reverse, vec::Vec};

/// Execute an approvals voting election scheme. The return type is a list of winners and a weight
/// distribution vector of all voters who contribute to the winners.
///
/// - The vote assignment distribution for each vote is always 100%, since a voter backs a candidate
/// with its full stake, regardless of how many candidates are backed by the same stake. However,
/// the caller may normalize votes on site if required.
/// - Returning winners are sorted based on desirability. Voters are unsorted.
/// - The returning winners are zipped with their final backing stake. Yet, to get the exact final
/// weight distribution from the winner's point of view, one needs to build a support map. See
/// [`crate::SupportMap`] for more info. Note that this backing stake is computed in
/// ExtendedBalance and may be slightly different that what will be computed from the support map,
/// due to accuracy loss.
///
/// This can only fail of the normalization fails. This can happen if for any of the resulting
/// assignments, `assignment.distribution.map(|p| p.deconstruct()).sum()` fails to fit inside
/// `UpperOf<P>`. A user of this crate may statically assert that this can never happen and safely
/// `expect` this to return `Ok`.
pub fn approval_voting<AccountId: IdentifierT, P: PerThing128>(
to_elect: usize,
candidates: Vec<AccountId>,
voters: Vec<(AccountId, VoteWeight, impl IntoIterator<Item = AccountId>)>,
) -> Result<ElectionResult<AccountId, P>, crate::Error> {
let to_elect = to_elect.min(candidates.len());

let (mut candidates, mut voters) = setup_inputs(candidates, voters);

candidates.sort_by_key(|c| Reverse(c.borrow().approval_stake));

let winners = candidates
.into_iter()
.take(to_elect)
.map(|w| {
w.borrow_mut().elected = true;
w
})
.map(|w_ptr| (w_ptr.borrow().who.clone(), w_ptr.borrow().approval_stake))
.collect();

for voter in &mut voters {
for edge in &mut voter.edges {
if edge.candidate.borrow().elected {
edge.weight = voter.budget
} else {
edge.weight = Zero::zero()
}
}
}

let assignments = voters.into_iter().filter_map(|v| v.into_assignment()).collect::<Vec<_>>();

Ok(ElectionResult { winners, assignments })
}
4 changes: 4 additions & 0 deletions primitives/npos-elections/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
//! - [`balance`](balancing::balance): Implements the star balancing algorithm. This iterative
//! process can push a solution toward being more "balanced", which in turn can increase its
//! score.
//! - [`approval_voting`](approval_voting::approval_voting): Implements an approval voting electoral
//! system where voters can back multiple candidates with the same stake.
//!
//! ### Terminology
//!
Expand Down Expand Up @@ -89,6 +91,7 @@ mod mock;
#[cfg(test)]
mod tests;

pub mod approval_voting;
mod assignments;
pub mod balancing;
pub mod helpers;
Expand All @@ -99,6 +102,7 @@ pub mod pjr;
pub mod reduce;
pub mod traits;

pub use approval_voting::*;
pub use assignments::{Assignment, StakedAssignment};
pub use balancing::*;
pub use helpers::*;
Expand Down
2 changes: 1 addition & 1 deletion primitives/npos-elections/src/phragmen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const DEN: ExtendedBalance = ExtendedBalance::max_value();
/// - The returning weight distribution is _normalized_, meaning that it is guaranteed that the sum
/// of the ratios in each voter's distribution sums up to exactly `P::one()`.
///
/// This can only fail of the normalization fails. This can happen if for any of the resulting
/// This can only fail if the normalization fails. This can happen if for any of the resulting
/// assignments, `assignment.distribution.map(|p| p.deconstruct()).sum()` fails to fit inside
/// `UpperOf<P>`. A user of this crate may statically assert that this can never happen and safely
/// `expect` this to return `Ok`.
Expand Down
49 changes: 47 additions & 2 deletions primitives/npos-elections/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,57 @@
//! Tests for npos-elections.
use crate::{
balancing, helpers::*, mock::*, seq_phragmen, seq_phragmen_core, setup_inputs, to_support_map,
Assignment, BalancingConfig, ElectionResult, ExtendedBalance, StakedAssignment, Support, Voter,
approval_voting::*, balancing, helpers::*, mock::*, seq_phragmen, seq_phragmen_core,
setup_inputs, to_support_map, Assignment, BalancingConfig, ElectionResult, ExtendedBalance,
StakedAssignment, Support, Voter,
};
use sp_arithmetic::{PerU16, Perbill, Percent, Permill};
use substrate_test_utils::assert_eq_uvec;

#[test]
fn approval_voting_works() {
let candidates = vec![1, 2, 3, 4];
let voters = vec![(10, vec![1, 2]), (20, vec![1, 2]), (30, vec![1, 2, 3]), (40, vec![4])];
let stake_of = create_stake_of(&[(10, 10), (20, 20), (30, 30), (40, 40)]);

let voters = voters
.iter()
.map(|(ref v, ref vs)| (*v, stake_of(v), vs.clone()))
.collect::<Vec<_>>();

let ElectionResult::<_, Perbill> { winners, assignments } =
approval_voting(3, candidates, voters).unwrap();

assert_eq_uvec!(winners, vec![(1, 60), (2, 60), (4, 40)]);
assert_eq_uvec!(
assignments,
vec![
Assignment {
who: 10u64,
distribution: vec![
(1, Perbill::from_percent(100)),
(2, Perbill::from_percent(100))
]
},
Assignment {
who: 20u64,
distribution: vec![
(1, Perbill::from_percent(100)),
(2, Perbill::from_percent(100))
]
},
Assignment {
who: 30u64,
distribution: vec![
(1, Perbill::from_percent(100)),
(2, Perbill::from_percent(100))
]
},
Assignment { who: 40u64, distribution: vec![(4, Perbill::from_percent(100))] },
]
);
}

#[test]
fn float_phragmen_poc_works() {
let candidates = vec![1, 2, 3];
Expand Down

0 comments on commit 5343a6c

Please sign in to comment.