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

Implements approval voting to use as a NposSolver #13367

Merged
merged 27 commits into from
Feb 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
401a2f0
Implements the approval voting methods in sp_npos_elections
gpestana Feb 9, 2023
5e5d0ff
Replaces phragmen elections in pallet-elections tests for approval vo…
gpestana Feb 12, 2023
5782216
nits for checks
gpestana Feb 12, 2023
a7643af
rfmt
gpestana Feb 12, 2023
e22b87c
Uses ApprovalVoting in node runtime
gpestana Feb 12, 2023
46697cc
Update primitives/npos-elections/src/approval_voting.rs
gpestana Feb 14, 2023
0b4a52d
reverts num max voters tests
gpestana Feb 14, 2023
e337c26
Rework generated API docs (#13178)
athei Feb 8, 2023
8511c9b
pallet-scheduler: Ensure we request a preimage (#13340)
bkchr Feb 9, 2023
1651553
[Fix] Try-state feature-gated for BagsList (#13296)
ruseinov Feb 9, 2023
eb4cff9
bump version of zombienet and update snaps links (#13359)
pepoviola Feb 10, 2023
e52858c
Fix longest chain finalization target lookup (#13289)
davxy Feb 11, 2023
87190e9
SetMembers configurable origin (#13159)
girazoki Feb 12, 2023
3b282df
grandpa: don't error if best block and finality target are inconsiste…
andresilva Feb 12, 2023
4d83204
Improve Weight Template and API (#13355)
shawntabrizi Feb 12, 2023
53e059a
[ci] Change label checker (#13360)
alvicsam Feb 13, 2023
c92e2b6
client/beefy: request justifs from peers further in consensus (#13343)
acatangiu Feb 13, 2023
483bbfb
[ci] Change GHA to add J2 labels instead Z0 (#13375)
alvicsam Feb 13, 2023
a056cd4
sc-client-db: Fix `PruningMode::ArchiveCanonical` (#13361)
bkchr Feb 13, 2023
aceb761
subkey: only decode hex if requested, CLI `0x` prefixed hex for all `…
ggwpez Feb 13, 2023
819a42a
[Feature] Introduce storage_alias for CountedStorageMap (#13366)
ruseinov Feb 13, 2023
b658723
simplifies approval edge weight calculation
gpestana Feb 14, 2023
198082a
Merge remote-tracking branch 'origin/gpestana8250_npossolver' into gp…
Feb 14, 2023
87503ee
Adds test
gpestana Feb 14, 2023
3e001b2
Merge branch 'master' into gpestana/approval_voting
gpestana Feb 15, 2023
186cf8d
Removes vote normalization for approval voting
gpestana Feb 15, 2023
f4a0f34
Merge remote-tracking branch 'origin/gpestana8250_npossolver' into gp…
Feb 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if

/// 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh interesting, I didn't recall that setup_inputs calculates approval stakes. Does it too much other stuff too? then we should perhaps extract the approval stake calculation to optimize.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We get a few things from fn setup_inputs:

  • Build a Vec of Rc<Ref<Candidates>> from a vec of candidate account IDs. Using pointers here helps to optimise the the subsequent computation;
  • Removes duplicate vote edges, if existent.
  • Calculate approval state per candidate;

I think it won't be easy to optimise this further for the approval voting case.


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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Curious if we need to borrow here to clone who.

.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!(
gpestana marked this conversation as resolved.
Show resolved Hide resolved
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