diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index fca33d20760e2..5705c413af48c 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -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, @@ -1037,7 +1037,7 @@ impl pallet_elections::Config for Runtime { type MaxVoters = MaxVoters; type MaxVotesPerVoter = MaxVotesPerVoter; type MaxCandidates = MaxCandidates; - type ElectionSolver = SequentialPhragmen; + type ElectionSolver = ApprovalVoting; type SolverWeightInfo = SubstrateWeight; type WeightInfo = pallet_elections::weights::SubstrateWeight; } diff --git a/frame/election-provider-support/benchmarking/src/lib.rs b/frame/election-provider-support/benchmarking/src/lib.rs index 5323513da98d5..7f27cf7521032 100644 --- a/frame/election-provider-support/benchmarking/src/lib.rs +++ b/frame/election-provider-support/benchmarking/src/lib.rs @@ -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(frame_system::Pallet); pub trait Config: frame_system::Config {} @@ -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::(v, t, d as usize); + }: { + assert!( + ApprovalVoting:: + ::solve(d as usize, targets, voters).is_ok() + ); + } } diff --git a/frame/election-provider-support/src/lib.rs b/frame/election-provider-support/src/lib.rs index 9e60eb3be1a6f..14018949e6da3 100644 --- a/frame/election-provider-support/src/lib.rs +++ b/frame/election-provider-support/src/lib.rs @@ -660,6 +660,29 @@ impl(sp_std::marker::PhantomData<(AccountId, Accuracy)>); + +impl NposSolver + for ApprovalVoting +{ + type AccountId = AccountId; + type Accuracy = Accuracy; + type Error = sp_npos_elections::Error; + fn solve( + winners: usize, + targets: Vec, + voters: Vec<(Self::AccountId, VoteWeight, impl IntoIterator)>, + ) -> Result, Self::Error> { + sp_npos_elections::approval_voting(winners, targets, voters) + } + + fn weight(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, VoteWeight, BoundedVec); diff --git a/frame/election-provider-support/src/onchain.rs b/frame/election-provider-support/src/onchain.rs index 483c402fe249c..6102a1c67f5f3 100644 --- a/frame/election-provider-support/src/onchain.rs +++ b/frame/election-provider-support/src/onchain.rs @@ -185,7 +185,7 @@ impl ElectionProvider for OnChainExecution { #[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; @@ -235,6 +235,7 @@ mod tests { struct PhragmenParams; struct PhragMMSParams; + struct ApprovalVotingParams; parameter_types! { pub static MaxWinners: u32 = 10; @@ -261,6 +262,16 @@ mod tests { type TargetsBound = ConstU32<400>; } + impl Config for ApprovalVotingParams { + type System = Runtime; + type Solver = ApprovalVoting; + 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}; @@ -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!( + 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)] }) + ] + ) + }) + } } diff --git a/frame/election-provider-support/src/weights.rs b/frame/election-provider-support/src/weights.rs index 44075ba871228..ad26debf382a1 100644 --- a/frame/election-provider-support/src/weights.rs +++ b/frame/election-provider-support/src/weights.rs @@ -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. @@ -70,6 +71,10 @@ impl WeightInfo for SubstrateWeight { // 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 @@ -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() + } } diff --git a/frame/elections/src/lib.rs b/frame/elections/src/lib.rs index 37caa95ce11d8..77e118835771b 100644 --- a/frame/elections/src/lib.rs +++ b/frame/elections/src/lib.rs @@ -1285,7 +1285,7 @@ impl ContainsLengthBound for Pallet { 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, @@ -1419,7 +1419,7 @@ mod tests { type WeightInfo = (); type MaxVoters = MaxVoters; type MaxCandidates = MaxCandidates; - type ElectionSolver = SequentialPhragmen; + type ElectionSolver = ApprovalVoting; type SolverWeightInfo = SubstrateWeight; type MaxVotesPerVoter = ConstU32<16>; } diff --git a/primitives/npos-elections/src/approval_voting.rs b/primitives/npos-elections/src/approval_voting.rs new file mode 100644 index 0000000000000..8c1d496d77f75 --- /dev/null +++ b/primitives/npos-elections/src/approval_voting.rs @@ -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

`. A user of this crate may statically assert that this can never happen and safely +/// `expect` this to return `Ok`. +pub fn approval_voting( + to_elect: usize, + candidates: Vec, + voters: Vec<(AccountId, VoteWeight, impl IntoIterator)>, +) -> Result, 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::>(); + + Ok(ElectionResult { winners, assignments }) +} diff --git a/primitives/npos-elections/src/lib.rs b/primitives/npos-elections/src/lib.rs index d0c9ed18caddc..5fbbd277cae06 100644 --- a/primitives/npos-elections/src/lib.rs +++ b/primitives/npos-elections/src/lib.rs @@ -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 //! @@ -89,6 +91,7 @@ mod mock; #[cfg(test)] mod tests; +pub mod approval_voting; mod assignments; pub mod balancing; pub mod helpers; @@ -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::*; diff --git a/primitives/npos-elections/src/phragmen.rs b/primitives/npos-elections/src/phragmen.rs index ca32780ed84b4..99c6522bdde77 100644 --- a/primitives/npos-elections/src/phragmen.rs +++ b/primitives/npos-elections/src/phragmen.rs @@ -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

`. A user of this crate may statically assert that this can never happen and safely /// `expect` this to return `Ok`. diff --git a/primitives/npos-elections/src/tests.rs b/primitives/npos-elections/src/tests.rs index 6f2e4fca77115..bd821f2571335 100644 --- a/primitives/npos-elections/src/tests.rs +++ b/primitives/npos-elections/src/tests.rs @@ -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::>(); + + 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];